C++11(六)nullptr 与 Lambda

原图

nullptr

nullptr、NULL 和 0

般情况下,NULL 是一个宏定义。在传统的 C 头文件( stddef.h)里我们可以找到如下代码

1
2
3
4
5
6
#undef NULL
#if defined(__cplusplus)
#define NULL O
#else
#define NULL ((void *)0)
#endif

可以看到,NULL 可能被定义为字面常量 0,或者是定义为无类型指针(void*)常量。不过无论采用什么样的定义,我们在使用空值的指针时,都不可避免地会遇到一些麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void func(int i)
{
std::cout << "int" << std::endl;
}

void func(void* p)
{
std::cout << "ptr" << std::endl;
}

int main()
{
func(0); // int
func(NULL); // int,本意是调用指针函数,实际却调用了 int 函数
func((void*)0); // ptr

getchar();
return 0;
}

以上的代码中 func(NULL); 本意是调用指针函数,实际却调用了 int 函数,因为 NULL 被定义成了 0。虽然在前面加上 (void*) 强制转换能解决这个问题,但是某些编译器缺会在产生这种二义性时报错,多以对于代码移植来说会有一定限制。于是 C++11 中新增了 nullptr 来代表指针空值,以上问题就得到了解决:

1
2
3
4
5
6
7
8
9
int main()
{
func(0); // int
func(nullptr); // ptr
func((void*)0); // ptr

getchar();
return 0;
}

nullptr 和 nullptr_t

同时,C++11 还定义了指针空值类型 nullptr_t,就是说指针空值并非只有 nullptr 一个实例,也可以通过 nullptr_t 来声明一个空指针类型的变量,虽然好像没什么用。另外,nullptr_t 也规定了一些规则:

  • 所有定义为 nullptr_t 类型的数据都是等价的,行为也完全一致。
  • nullptr_t 类型数据可以隐式转换成任意一个指针类型。
  • nullptr_t 类型数据不能转换成非指针类型。
  • nullptr_t 类型数据不适用于算术运算表达式。
  • nullptr_t 类型数据可以用于关系运算表达式,但是只能和 nullptr_t 类型或者指针类型进行比较。

nullptr_t 类型还是比较有意思的,它的定义是从 nullptr 反推而来,从这点又能看出 C++11 用法的灵活性:

1
typedef decltype(nullptr) nullptr_t;

默认函数的控制

在 C++中声明自定义的类之后,编译器会默认生成一些成员函数,这些函数被称为默认函数。包括构造函数、拷贝构造函数、拷贝赋值构造函数、移动构造函数、移动拷贝函数、析构函数。另外,编译器还会默认生成一些操作符函数,包括 operator ,、operator &、operator &&、operator 、operator ->、operator ->、operator new、operator delete。

显式缺省函数 (= default)

但是如果实现了这些函数的自定义版本后,编译器就不会去生成默认的版本。有时候我们需要声明带参数的构造函数,此时就不会生成默认的构造函数,这样会导致类不再是 POD 类型,从而影响类的优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Example {
public:
Example(int i) : data(i) {}

private:
int data;
};

int main()
{
std::cout << std::is_pod <Example>::value << std::endl; // 0

return 0;
}

C++11 中提供了新的机制来控制默认函数生成来避免这个问题,我们在声明时在函数末尾加上”= default”来显式地指示编译器去生成该函数的默认版本,这样就保证了类还是 POD 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Example {
public:
Example() = default;
Example(int i) : data(i) {}

private:
int data;
};

int main()
{
std::cout << std::is_pod <Example>::value << std::endl; // 1

return 0;
}

显式删除函数 (= delete)

另一方面,有时候可能需要限制一些默认函数的生成,例如需要禁止拷贝构造函数的使用。原来,通过把拷贝构造函数声明为 private 成员,这样一旦使用编译器就会报错。而在 C++11 中,在函数的定义或者声明后面加上”= delete”就能实现这个效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Example {
public:
Example() = default;
Example(const Example& ex) = delete;

private:
int data;
};

int main()
{
Example ex;
Example ex1(ex); // 编译失败

return 0;
}

其他用法

= default 和= delete 能够更加精准的控制类的默认函数版本。其中,= default 同样也能在类外的定义中修饰成员函数:

1
2
3
4
5
6
7
8
9
10
class Example {
public:
Example() = default;
Example(const Example&);

private:
int data;
};

Example::Example(const Example& ex) = default;

这样的好处是,能够为一个类实现多个版本,只要我们在头文件里声明同样的函数,而在不同的 cpp 文件中用不同的方法实现,当选择不同的 cpp 编译时产生的版本就不同。

关于= delete,它不仅局限于限制生成默认函数,还能避免编译器做一些不必要的隐式数据转换:

1
2
3
4
5
6
7
8
9
10
11
12
class Example {
public:
Example(int i) {}
};

int main()
{
Example ex(1);
Example ex1('a'); // 编译成功,char 会隐式装换成 int 型

return 0;
}

有时候我们不想要这种隐式转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Example {
public:
Example(int i) {}
Example(char c) = delete;
};

int main()
{
Example ex(1);
Example ex1('a'); // 编译失败

return 0;
}

这个方法也能用于普通函数:

1
2
3
4
5
6
7
8
9
10
void func(int i) {}
void func(char c) = delete;

int main()
{
func(1);
func('a'); // 编译失败

return 0;
}

另外,还有很多用法,例如显示删除 new 操作符来避免在堆上分配对象、显示删除析构函数用于构建单例模式等等。

Lambda

什么是 Lambda 表达式?

Lambda 表达式是 C++11 提出的新特性,主要用来实现代码中函数的内嵌,简化了编程,提高了效率。

它的基本格式如下:

1
auto fun = [捕获参数](函数参数){函数体};

例如这个 Hello Lambda:

1
auto fun = [](){ std::cout << "Hello Lambda" << std::endl; };

Lambda`本质上是一个内联函数,只是定义和使用的方式与普通的函数有些不同,下面来具体介绍下它的基本语法。

基本语法

最简单的 Lambda

后面不加 () 的 Lambda 表达式相当于函数指针,加上 ()`就相当于调用这个 Lambda 函数。

1
2
3
4
5
6
7
int main(void) {

[](){ cout << "Hello World!" << endl; };

[](){ cout << "Hello World!" << endl; }();
return 0;
}

Lambda 可以赋值

可以将 Lambda 表达式赋值给一个变量,之后这个变量可以当作函数指针来调用,需要加上 ()。

1
2
3
4
5
int main(void) {
auto fun = [](){ cout << "Hello World!" << endl; };
fun();
return 0;
}

为 Lambda 传递参数

可以为 Lambda 指定函数参数,该参数也具有副本机制。

1
2
3
4
5
6
7
8
9
int main(void) {
int num = 100;

auto fun = [](int num){ num = 5; cout << num << endl; };
fun(num);

cout << num << endl;
return 0;
}

获取 Lambda 的返回值

Lambda 的返回值类型可以进行类型转换或者使用 decltype 自动推导。

1
2
3
4
5
6
7
8
9
10
11
12
int main(void) {

int num1 = [](int a, int b){ return a + b; }(1, 2);

int num2 = [](double a, double b)->int{ return a + b; }(1.2, 2.1);

double num3 = [](double a, double b)->decltype(a + b){ return a + b; }(1.2, 2.1);
cout << num1 << endl;
cout << num2 << endl;
cout << num3 << endl;
return 0;
}

[ ] 的使用方法

Lambda 表达式的 [] 用来确定捕获参数:

  1. [=]:捕获的局部变量只可读不可写,捕获范围是当前 Lambda 表达式之前的作用域。
  2. [&]:捕获的局部变量可读可写。
1
2
3
4
5
6
7
8
9
10
11
12
int main(void) {
int num = 100;

auto fun1 = [=](){ cout << num << endl; };
fun1();

auto fun2 = [&num](){ num = 200; cout << num << endl; };
fun2();

cout << num << endl;
return 0;
}

Lambda 是 const 函数

Lambda 默认是 const 函数,不能修改引用的变量,使用 mutable 可以取消该属性,但是 mutable 修改的只是副本。

1
2
3
4
5
6
7
8
9
int main(void) {
int num = 100;

auto fun = [=]() mutable { num = 200; cout << num << endl; };
fun();

cout << num << endl;
return 0;
}

Lambda 没有地址

Lambda 表达式是内联展开的,没有实际的地址,这是与普通函数的一个很大的区别。

1
2
3
4
5
6
7
8
9
int main(void)
{
string str;
auto fun = [](string str){cout << str << endl; };
cin >> str;
fun(str);

return 0;
}

Lambda 在 class 中的使用

Lambda 在 C++ class 中的使用需要知道如何捕获 this。

1
2
3
4
5
6
void MyClass::function() {

auto fun1 = [this](int v){cout << v + this->num << endl; };

auto fun2 = [&](int v){cout << v + this->num << endl; };
}

Lambda 与仿函数

Lambda 与仿函数有些类似,仿函数是编译器实现 Lambda 的一种方式,编译器会将 Lambda 转换为一个仿函数对象,Lambda 可以视为仿函数的等价形式,编译器在发现 Lambda 函数出现语法错误的时候,会报出一些与构造函数相关的信息。

下面是一个仿函数例子:

1
2
3
4
5
6
7
8
9
10
class TestLambda {
public:
TestLambda(float f) : x(f) {}

float operator()(float price) {
return price * (1 - x / 100);
}
private:
float x;
};

可以看到调用仿函数和 Lambda 是很类似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
float f = 5.5f;
TestLambda t(f);
auto fun = [f](float price)->float{ return price * (1 - f / 100);};

float p1 = t(3000);

float p2 = fun(3000);

cout << p1 << endl;

cout << p2 << endl;
return 0;
}

Lambda 是 C++11 提出的新特性,简化了我们的代码,提高了编程的效率,应该了解并学会使用它。

参考文献

https://blog.csdn.net/WizardtoH/article/details/81164740
https://blog.csdn.net/WizardtoH/article/details/81169570
https://www.jianshu.com/p/a200a2dab960
《深入理解 C++11:C++11 新特性解析与应用》