目录括号内为适合人群,所有库作者的内容暂不做学习,可自行查阅《深入理解C++11:C++11新特性解析与应用》。网盘链接: https://pan.baidu.com/s/1Jf29R7-foOoXJ5UW3mTKVA 密码: 7vgq
目录
1.指针空值——nullptr(所有人)
①指针空值:从0到NULL,再到nullptr
②nullptr和nullptr_t
2.默认函数的控制(类作者)
①类与默认函数
②“=default”与“=deleted”
3.lambda函数
①lambda的一些历史
②C++11中的lambda函数
③lambda与仿函数
④lambda的基础使用
⑤关于lambda的一些问题及有趣的实验
⑥lambda与STL
⑦更多的一些关于lambda的讨论
1.指针空值——nullptr ^
①指针空值:从0到NULL,再到nullptr ^
在良好的C++编程习惯中,声明一个指针的时候,通常会初始化为0或者NULL,以免造成悬挂指针。NULL是一个宏定义,从定义中可以看成,NULL可能被定义为字面常量0,或者是定义为无类型指针(void*)常量。这也导致了一些问题,如下的函数重载:
void func(char *c)
{
cout << "指针类型" << endl;
}
void func(int i)
{
cout << "整型" << endl;
}
int main()
{
func(0); //输出:整型
func(NULL); //输出:整型
func(nullptr); //输出:指针类型
}
上述例子中,我们的本意是通过NULL调用指针版本,但由于字面常量0的二义性导致意料之外的结果,二义性即既可以是整型,又可以是一个无类型指针(void*)。在C++11标准中,并没有消除字面常量0的二义性,而是引入了nullptr关键字,nullptr是一个所谓“指针空值类型”的常量。指针空值类型被命名为nullptr_t。
②nullptr和nullptr_t ^
C++11标准不仅定义了指针空值常量nullptr,也定义了其指针空值类型nullptr_t,也就表示了指针空值类型并非仅有nullptr一个实例,通常情况下,我们也可以通过nullptr_t来声明一个指针空值类型的变量。C++11标准严格规定了nullptr_t类型与其他数据间的关系,如下:
- 所有定义为nullptr_t类型的数据都是等价的,行为也是完全一致
- nullptr_t类型数据可以隐式转换成任意一个指针类型
- nullptr_t类型数据不能转换为非指针类型,即使使用reinterpret_cast强制转换也不行(VS2017可以转换成功)
- nullptr_t类型数据不适用于算术运算表达式
- nullptr_t类型数据可以用于关系运算表达式,但仅能与nullptr_t类型数据或者指针类型数据进行比较,当且仅当关系运算符为==,<=,>=等时返回true
-
int main() { //nullptr可以隐式转换为任意一个指针类型 char* cp = nullptr; //不能转换为整型 //int a1 = nullptr; //可以通过强制转换为整型,值为0,原书籍中说不能通过reinterpret_cast强制转换 int a2 = reinterpret_cast<int>(nullptr); nullptr_t nptr = 0; //输出:相等 if (nptr == nullptr) cout << "相等" << endl; else cout << "不相等" << endl; //输出:为假 if (nptr < nullptr) cout << "为真" << endl; else cout << "为假" << endl; //原书籍中说不能转换为整型或bool类型,因为0是特殊的,具有二义性,可转换为空指针类型(void*),换其他整型就不行了,至于bool类型,个人猜想可能nullptr转换为0了,也可能是编译器原因(vs2017) //输出:相等 if (0 == nullptr) cout << "相等" << endl; //输出:为假 if (nullptr) cout << "为真" << endl; else cout << "为假" << endl; //不可以进行算术运算 //nullptr += 5; //nullptr *= 5; //以下操作均可以正常运行 sizeof(nullptr); //sizeof用于返回所占空间大小 typeid(nullptr); //typeid用于返回指针或引用所指对象的实际类型 throw(nullptr); }
虽然nullptr_t看起来像是个指针类型,用起来更是,但在把nullptr_t应用于模板中时候,模板却只能把它作为一个普通的类型来进行推导,如下:
template <class T> void f(T* t) { cout << "指针版本" << endl; } template <class T> void h(T t) { cout << "整型版本"<<endl; } int main() { //f(nullptr); //编译失败,nullptr的类型是nullptr_t,而不是指针 f((int*)nullptr); //对nullptr进行显式转换,推导出T=int h(nullptr); //推导出T=nullptr_t h((int*)nullptr); //推导出T=int* }
2.默认函数的控制 ^
①类与默认函数 ^
在C++中声明自定义的类,编译器会默认的帮我们生成一些未定义的成员函数,这些函数版本被称为“默认函数”,如下:
- 构造函数
- 拷贝构造函数
- 拷贝赋值函数(operator=)
- 移动构造函数
- 移动拷贝函数
- 祈构函数
-
另外,C++编译器还会为以下这些自定义类型提供全局默认操作符函数:
- operator,
- operator&
- operator&&
- operator*
- operator->
- operator->*
- operator new
- operator delete
-
在C++语言规则中,一旦我们实现了这些函数的自定义版本,则编译器不会再为该类自动生成默认版本,这将导致一个问题,即一旦声明了自定义版本的构造函数,则导致我们定义的类型不再是POD的,为此C++11标准中,提供default关键字来控制默认版本函数的生成,只需要在默认函数定义或者声明时加上“=default”,就可以显式地指示编译器生成该函数的默认版本。如下:
class A { public: A() = default; A(int i) :a(i) {} private: int a; }; int main() { cout << is_pod<A>::value << endl; //输出:1 }
在一些情况下,我们希望能限制一些默认函数的生成,C++11标准中,一个方法,在函数的定义或者声明加上“=delete”。“=delete”会指示编译器不生成函数的默认版本。如下:
class A { public: A() = default; A(int i) :a(i) {} A(const A& a)=delete; private: int a; }; int main() { A a; A b(a); //编译失败,提示A(const A& a)是已删除的函数 }
注:一旦默认版本删除了,重载该函数也是非法的
②“=default”与“=deleted” ^
“=default”只能用于有默认版本的,但“=delete“并非局限于有默认版本的成员函数,使用显式删除可以避免编译器做一些不必要的隐式数据类型转换,如下:
class A { public: A() = default; A(int i) :a(i) {} A(char a) = delete; A(const A& a)=delete; void f(A a); private: int a; }; int main() { A a(1); //A a('a'); //编译不通过 a.f(1); a.f('a'); //编译不通过,避免了隐式转换 }
在使用“=delete”时,应避免函数与explicit关键字使用,不然会发生意料之外的结果,如下:
class A { public: A() = default; A(int i) :a(i) {} explicit A(char a) = delete; //删除char版本 void f(A a) {}; private: int a; }; int main() { A a; //A b('a'); //编译不通过 a.f('a'); //编译通过了 }
3.lambda函数 ^
①lambda的一些历史 ^
lambda(λ)源于希腊字母,在数理逻辑或计算机科学领域中,lambda则是被用来表示一种匿名函数,这种匿名函数代表了一种所谓的λ演算。
②C++11中的lambda函数 ^
lambda函数的语法格式如下:
[捕捉列表] (参数列表) mutable修饰符 ->返回类型 { 函数体 };
- 捕捉列表:首先编译器会根据[]判别接下来的代码是否是lambda函数。捕捉列表能够捕捉上下文中的变量以供lambda函数使用
- 参数列表:与普通函数的参数列表一致,如果不需要参数传递,则可以连同括号()一起省略
- mutable修饰符:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)
- 返回类型:用追踪返回类型形式声明函数的返回类型。不需要返回值的时候可以连同->一起省略,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导
- 函数体:内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
-
在lambda函数的定义中,参数列表和返回类型都是可选的部分,而捕获列表和函数体都可能为空,所以C++11中最为简略的lambda函数只需要声明为:
[] {}; //该lambda函数不能做任何事情
语法上,捕捉列表由多个捕捉项组成,并以逗号分割。捕捉列表有如下几种形式:
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕捉所有父作用域的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针。
-
注:捕捉列表中可以随意组合,如:[=,&a,&b]表示以引用传递方式捕捉变量a和b,以值传递方式捕捉其他所有变量,但不允许变量重复传递,如:[=,a]=已经以值传递方式捕捉了所有变量,捕捉a重复。
int main() { int a = 5; int b = 10; //这里的返回类型由编译器推断为int auto f1 = [a] {return a; }; auto f2 = [=] {return a + b; }; auto f3 = [&] {return a + b; }; auto f4 = [&a] {return a; }; cout << f1() << " " << f2() << " " << f3() << " " << f4() << endl; }
③lambda与仿函数 ^
我们在使用STL算法时,通常会使用到一种特别的对象,一般来说,我们称之为函数对象,或者仿函数。仿函数就是重定义了成员函数operator()的一种自定义类型对象。特点就是其使用在代码层面感觉跟函数的使用并无二样,但究其本质却并非函数。如下:
class A { public: int operator() (int a, int b) { return a + b; } }; int main() { A a; cout << a(20, 30) << endl; //输出:50,这里的a只是一个对象 }
事实上,仿函数是编译器实现lambda的一种方式。在C++11中,lambda可以视为仿函数的一种等价形式。
④lambda的基础使用 ^
lambda函数在C++11标准中默认是内联的。具体例子可以查阅原书籍P241
⑤关于lambda的一些问题及有趣的实验 ^
使用lambda函数的时候,捕捉列表不同会导致不同的结果,具体地说就是按值方式传递捕捉列表和按引用方式传递捕捉列表效果是不一样的。对于按值方式传递的捕捉列表,其传递的值在lambda函数定义的时候就可以决定了。而按引用传递的捕捉列表变量,其传递的值则等于lambda函数调用时的值。如下:
int main() { int i = 10; auto func1 = [=] {return i + 1; }; //按值方式传递 auto func2 = [&] {return i + 1; }; //按引用方式传递 cout << func1() << " " << func2() << endl; //输出:11 11 ++i; cout << func1() << " " << func2() << endl; //输出:11 12 }
总之,如果需要捕捉的值成为lambda函数的常量,我们通常会使用按值传递的方式捕捉;反之,需要捕捉的值成为lambda函数运行时的变量(类似于参数的效果),则应该采用按引用方式进行捕捉。
关于lambda函数的类型以及该类型跟函数指针之间的关系。在C++11标准中,lambda的类型被定义为”闭包“(特有的,匿名且非联合体)的类,而每个lambda表达式会产生一个闭包类型的临时对象(右值)。所以严格地说,lambda函数并非函数指针。不过C++11标准允许lambda表达式向函数指针的转换,前提条件是lambda函数没有捕捉任何变量,且函数指针所示的函数原型,必须跟lambda函数有着相同的调用方式,如下:
int main() { auto func = [](int a, int b) {return a + b; }; //lambda函数,返回类型为int,有两个int参数 typedef int(*p1) (int x, int y); //函数指针,返回类型为int,有两个int参数 typedef int(*p2) (int x); //函数指针,返回类型为int,有一个int参 p1 pp1; p2 pp2; pp1 = func; //pp2 = func; //编译不通过,参数必须一致 }
lambda函数的常量性及mutable修饰符的问题,如下:
int main() { int a = 5; //auto func = [=] {a = 6; }; //默认情况下,lambda函数总是一个const函数 //可以修改常量数据,不过因为是按值方式传递,所以不修改原值 auto func2 = [=]() mutable {a = 6; }; //对于按引用方式传递的lambda表达式,mutable修饰可有可无 auto func3 = [&] {a = 6; }; auto func4 = [&]()mutable {a = 6; }; }
⑥lambda与STL ^
lambda对C++11最大的贡献,是在STL库中的算法。如下:
#include <algorithm> //标准库中大部分算法所在的头文件 int main() { vector<int> ivec{ 2,4,6,8,10,12,14,16,18,20 }; //这些算法第一,二个参数为容器首尾迭代器,第三个参数为函数指针 //find_if算法是查找第一个满足条件的元素,并返回这个元素的迭代器,从左往右遍历 //count_if算法是返回满足条件的元素个数 auto p1 = find_if(ivec.begin(), ivec.end(), [](int i) {return i < 10; }); auto p2 = count_if(ivec.begin(), ivec.end(), [](int i) {return i > 10; }); cout << *p1 << endl; //输出:2 即容器内第一个元素 cout << p2 << endl; //输出:5 即有5个元素满足条件 }
更多例子可查阅原书籍P248
⑦更多的一些关于lambda的讨论 ^
在现有C++11中,lambda并不是仿函数的完全替代者,这一点很大程度上是由lambda的捕捉列表的作用域限制造成的。lambda函数被设计的目的,就是要就地书写,就地使用。
使用lambda代替仿函数应该满足以下一些条件:
- 是局限于一个局部作用域中使用的代码逻辑
- 这些代码逻辑需要被作为参数传递