本文列出C++11的部分特性以及注意点与用途。
1. nullptr
- 为了解决的问题是:
void func(int);
void func(int *);
func(NULL); //这个函数调用会调用参数是int的版本;
- (1)nullptr的特点是可以隐式的转化为任意指针类型(比如nullptr当做函数参数),但是不可以转化为其他任何类型,包括bool类型(if(nullptr)不允许),reinterpret_cast强制转化也不被允许。
(2)nullptr本质是一个编译期的常量,换句话说,其是一个右值,大小与指针一样大(sizeof(nullptr) == sizeof((void*)0)),是先有的nullptr,然后定义的nullptr_t
#define decltype(nullptr) nullptr_t
2. =default与=delete
1 编译器为类中默认生成的成员函数有六个:
默认构造函数
析构函数
拷贝构造函数
拷贝赋值函数
移动构造函数
移动赋值函数
2 使用=default的好处
对定义了非默认构造函数的类的默认构造函数使用=default,可以使得类还有可能是POD类型。
POD类型的解释,维基百科这样说:
可见,POD类类型就是指class、struct、union,且不具有用户定义的构造函数、析构函数、拷贝算子、赋值算子;不具有继承关系,因此没有基类;不具有虚函数,所以就没有虚表;非静态数据成员没有私有或保护属性的、没有引用类型的、没有非POD类类型的(即嵌套类都必须是POD)、没有指针到成员类型的(因为这个类型内含了this指针)。
POD类型的好处是:
- 与C语言的struct的内存布局相同,所以可以和C语言结构体交互使用;
- 是拷贝不变的(trivially copyable)的class,可以使用memcpy, memmove不改变它的语义。拷贝的时候调用这些函数是更快的。
使用上:
- 《STL源码分析》中介绍了SGI STL用到的__type_traits,大量的构造函数和析构函数在调用前会先根据__type_traits判断一下是否存在trival的构造函数和析构函数,如果是的话,就不进行循环,否则就需要进行。但使用这个的前提是用户在自定义类中需要定义__type_traits中用到的类型的typedef。
- C++11中支持了std::is_pod,即不需要用户在类中定义出来很多typedef,就可以判断出是否是POD类型。
另:
std::is_pod的返回值是constexpr类型,意思是这个函数的调用结果在编译期就可以得到,不会等到运行期才执行。
3 使用=delete的好处
- 可以很方便地对编译器自动生成拷贝操作符或者拷贝构造等行为提供禁止,不需要使用私有化这些函数或者将这些函数放到父类中设置为私有并不提供定义的方式来处理了。
- 可以禁止隐式的类型转化,包括类的成员函数,也包括全局的函数。
void func(int);
void func(char) = delete; //此时就是禁止char隐式转化为int;
- 可以实现禁止在堆中或者栈中分配内存的操作
void* operator new(std::size_t) = delete; //禁止在堆中分配内存;
~NoStackAlloc() = delete; //禁止在栈中分配内存。(栈中的会自动调用析构,new出的需要人为delete才调用)
3. lambda函数
1 lambda函数可以在函数逻辑不是很复杂的情况下代替函数指针与仿函数的使用。其行为类似于一个函数内定义函数。
2 lambda相对于函数指针的好处
- 函数指针默认不是inline的,lambda默认是inline的;
- 仿函数针对函数指针的好处就是存在初始状态,lambda也可以通过捕获参数的方式来得到初始状态。
lambda除了比仿函数要简单,但缺点是:
lambda所能捕获的参数只有父函数内部的局部变量,更外层的变量是无法捕获到的。
3 使用lambda需要注意的的问题
- 以值捕获的情况下,定义该lambda的时候会将捕获到的变量保存到lambda函数内部,后期在lambda外部改变了被捕获变量的值,不影响lambda函数内部:
int a = 10;
auto lambda = [=]() {return a + 10;}; //定义了之后,a就保存到lambda里面了
a = 30; //后期再怎么改
cout << lambda() << endl; //输出也是20;
a = 20;
cout << lambda() << endl; //输出也是20;
原因就是,lambda的实现原理是,编译器会在编译期将其转化为仿函数,所以值以构造函数的方式传递了进去就无法再改变了。
- 以引用方式捕获的时候,lambda改变了被捕获的数据,外部可以感知到。
这里涉及到lambda是默认const属性的,含义是会默认将捕获进来的变量定义为const类型,而引用类型还可以在里面改变的原因是a所引用的对象没有变,变化的只是其值。成员变量是引用类型,在const函数内是可以改变其值的,如下:
class D {
public:
D(int a):d(a) {}
void change() const {
d = 10; //d是引用类型,在const函数中可以被改变
}
void print() const {
cout << d << endl;
}
private:
int& d;
};
int main()
{
int a = 20;
D d(a);
d.change();
d.print(); //结果输出的是10;
}
- 引用方式捕获的时候没有以值捕获时候(第1点)的那种问题。
4. 强枚举类型
- enum有几个特别的性质:
- enum内部定义的常量的作用域会被拓展到enum所在作用域,容易引起冲突;
当然,将enum的定义放到特定的命名空间中,或者放到类中定义,则不会污染全局空间。- 存在于整数的隐式转换,表现为在比较的时候,可以与整数做比较,可以与另一个enum对象内的常量进行比较而不会报错。
- enum定义的常量所占空间不是一定的,与编译器有关。
- 针对以上三个问题,提出强枚举类型,在enum于类名之间加上class,也就对应有如下好处:
- 合适的常量作用域,内部常量必须通过enum类名来访问,作用域不再是全局。(enum class想要正常使用的话,必须有一个名字)
- 不能进行隐式转换,但是可以进行显示转换;(也是每个常量对应一个整数,但是当做整数用时必须显示转化)
- 保存常量的数据结构可以指定,在enum class name : type的方式指定,这样就解决了常量所占空间不一致的问题。
5. 智能指针与内存管理
- 几个智能指针
- auto_ptr,C++11之前就有的,允许拷贝,但是拷贝之后指针所有权易主;
- unique_ptr,也是只有一个unique_ptr可以拥有这个指针(内存资源),实现的方法是,=delete掉了拷贝构造和拷贝运算符重载,但是定义了移动构造函数和移动运算符重载;
- shared_ptr则是可以有多个shared_ptr拥有这个内存资源,采用的是引用计数的方式,但是会有循环引用的问题;
- weak_ptr则是和shared_ptr搭配使用,可以认为其仅仅就是查询shared_ptr管理的内存的状态,比如引用计数是多少,释放没有,里面的值是什么,不占有这个资源(引用计数不加1),但是也可以查看状态信息。
- shared_ptr存在循环引用的问题无法解决,可以的解决方式是:
采用基于跟踪处理的垃圾回收器:
跟踪是:从当前正在使用的对象(可能很多)开始,找到活的对象,标记一下;
清除:将未被标记的对象释放;(存在内存碎片)
整理:是将被标记的对象左对齐移动;(存在移动的代价)
拷贝:将活的对象拷贝到另一个空间:TO空间,然后释放FROM空间,下次FROM与TO空间的用途反过来。(利用率低且需要移动)
- C++11中最小垃圾回收支持
由于指针的灵活性,导致垃圾回收器实现起来相当困难,比如指针的移动等,解决办法可以是,由用户来将移动后的指针指向的空间标记为不要清除。
6. sizeof的拓展
sizeof可以用于类的成员变量了:
struct AAA {
int a; //public的才可以。
};
//使用上,需要使用类作用域标识符
cout << sizeof(AAA::a) << endl;
7. friend的拓展
friend声明的类名可以不用加class了。
template<class T> struct AAA {
friend T; //如果T是基本类型,该句会被忽略掉
int a;
};
friend的一个用途是用于测试,测试类声明为被测试类的friend,这样就可以测试私有成员函数了。friend被拓展后,就可以当做这个用途。
8. 新引入的final关键字和override关键字
- 说明一个现象:
class AAA {
public:
virtual void func() {
cout << "AAA" << endl;
}
};
class BBB : public AAA{
public:
void func() {
cout << "BBB" << endl;
}
};
class CCC :public BBB{
public:
void func() {
cout << "CCC" << endl;
}
};
//结论是输出:CCC,即不论中间有没有缺少virtual的,只要父类指针有(AAA有),就可以找到正确的对象(CCC)。(不会经由BBB)
final的作用是禁止子类重写函数(override);
override的作用是告诉编译器我在重写,帮我检查;
使用方法上,final和override是一样的,都是在函数参数后边。
9. 名字查找规则引起的问题
名字查找规则,当调用一个类成员函数时,其会当前类寻找,如果找到了,就不再向下寻找了,这个找到,是找到的名字,不包括参数;所以当存在子类和父类的函数重载时,通过子类对象是无法调用父类那个版本的函数的。
class AAA {
public:
void func() {
cout << "AAA" << endl;
}
};
class BBB : public AAA{
public:
void func(int a) {
cout << "BBB" << endl;
}
};
//通过如下方式调用func会报错
BBB b;
b.func();
非指针或者引用的方式调用,在找到BBB中的func就不再寻找AAA中的了,导致参数数量不匹配报错。
解决办法是使用using关键字,可以继承父类中的同名方法:
class AAA {
public:
void func() {
cout << "AAA" << endl;
}
};
class BBB : public AAA{
public:
using AAA::func; //强行拉取,解决名字查找规则的问题
void func(int a) {
cout << "BBB" << endl;
}
};
using还有一个用途是子类想要继承父类的构造函数,也可以使用using AAA::AAA,此时AAA在BBB中就变成了BBB,免去了BBB为构造AAA而写的一系列转发构造函数。
以上还有两个可能的问题:
(1)多个父类的构造函数冲突如何处理:在子类中显式定义出冲突的那个构造函数;
(2)继承了父类的,子类中还存在成员变量如何赋值:使用成员变量就地初始化;
10. 委托构造函数(有点水)
含义是,构造函数可以在初始化列表中调用另一个构造函数,但调用后不能再有初始化列表。这样可以避免很多重复的定义。
一个用途是,被调用的构造函数被声明成template时,可以实现模板构造函数。