C++11的新特性学习体会
lambda表达式
略
auto 关键字
类型推导
好处之一是可以在某些地方省去繁复地写一些东西
比如遍历一个容器
以前(C++98) 是这样
std::vector<int> v;
//
...
//
std::vector<int>::const_iterator it = v.begin();
for ( it; it != v.end(): it++) {
}
在C++11中用auto可以简单地写成
for(auto it = v.begin(); it != it.end(); ++it) {
}
编译器根据auto可以做类型推导,判断出 it的类型就是 v 类型内置的迭代器 const_iterator
auto 也可以这么用
auto i = 1; // 推导 i为 int
因此auto有点动态类型的特点,但是auto的出现并没有让C++11编程动态类型语言,为什么?因为尽管auto可以类型推导,但是这个动作是出现在编译阶段而非运行阶段,但是Python的变量能够在运行时进行推导。
评述
以上的一点auto的优势的意义,因为
std::vector<int>::const_iterator it = v.begin();
的表达在很多时候实际上没有什么太大的意义,我们知道 it 是容器内的一个迭代器,但是不太确定它到底是写成什么样,写出来非常冗长,同时好像只是表明我们知道它,当然直接告诉编译器,这是一个这样的类型,编译器可以免去推导的工作,但是既然编译器擅长此事,我们可以把这种繁琐的事情交给它,本质上只是增加了编译的负担,但是运行效果理论上是一样的。
另一个关键是,同行之间代码的阅读沟通,auto的存在让阅读代码者不知道这个变量的类型,好像缺了什么,带了含糊,实际上也不然——既然代码作者可以让编译器去推导,阅读者自然也无须明确知道知道这个变量确切的表达式。
在一个项目中所有变量声明都用auto,会发生什么?
第一,代码可读性变差,有的地方变量的定义相当遥远,无法快速知道变量的定义
第二,代码的维护性必然变差,因为auto出现在全局文件中,一旦使用这个变量声明,这里的修改将会殃及所有引用该变量的代码块,这里用“殃及”这个词,是因为,auto的右半部分的变化,会随时改变auto的推导结果,而其它使用该变量的上下文可能并不知道这个变化的用意所在
以上一些讨论表明auto的使用需要克制而为。那么实践应当如何,auto的引入是为了解决某些代码繁冗的问题,而不是为了引入更多歧义,所以它的用法更多是关于什么时候应该使用
- 我们不想在显而易见的繁琐类型声明中书写太多的字符
- 我们明确知道某处的类型可以依照上下文推导时
第一条准则表明我们最好不要在全局区域声明一些auto变量;几乎所有的auto变量只是给自己使用,离开局部作用域之后auto变量最好也会被销毁
类型推导decltype
委托构造函数
nullptr
右值引用和移动构造函数
区分左值和右值
- 引入移动构造函数的背景
有些特性的引进是为了减少代码容量,如委托构造函数
有些特性的引进是为了增强语言能力
有些特性的引进是为了特定的编程场景下把某些编程工作自动化,后台化,减少程序员的心智负担。
移动构造函数的出现就是属于这种场景。
在没有移动构造函数之前,程序员写函数参数的传递的代码,需要充分考虑按值传递,和按引用传递的区别,以及明确一个函数如果返回临时对象会发生什么?
例如
以下的代码
struct ComplexNumber {
double real;
double image;
}
double real(const ComplexNumber number) {
return number.real;
}
double image(const ComplexNumber& number) {
return number.image;
}
ComplexNumber add(const ComplexNumber& number1, const ComplexNumber& number2) {
ComplextNumber ans;
ans.real = number1.real + number2.real;
ans.image = number2.real + number2.image;
return ans;
}
real方法的参数是一个传值的模型,这意味着传递参数时会调用一次复制构造函数——这里是默认生成的,所以可以编译通过;
image方法是按引用传递,直接返回对象的虚部,而不会调用复制构造函数;
add 方法虽然两个参数都按引用传递,但是因为局部构造了一个临时对象,并且在return 时返回了ans,这时,add 调用一次ComplexNumber的构造函数,额外有一次复制构造函数,因为return时复制了一个临时对象。
除此以外,临时对象的申请和析构,增加了析构函数的调用次数,如果,成员变量涉及内存分配,就会因为传值的使用增加了内存分配和申请的开销。
以上 real 方法多余的复制构造函数调用可以用按引用传递参数的方式解决,而add方法呢?似乎束手无策,我们再C98里,如果 ComplexNumber 是一个复制开销极大的对象,只能把函数声明成这样
void add(const ComplexNumber& number1, const ComplexNumber& number2, ComplexNumber& result);
将结果带到 result变量中,这样写很不直接,client代码写得很不自然
移动构造函数的目的
- 减少内存分配和回收的次数
- 让客户代码写得更自然而无须考虑传值带来的性能开销
移动语义
大概意思是,发生移动构造时,本来需要调用复制构造函数的时候,将原临时对象指向的内存搬到了自己的指针指向之下,而原来的临时对象(右值)的指针标记为 nullptr这样就完成了移动。这个操作减少了一个内存分配和析构的开销过程,因而效率是有保证的。
例子:
class ExampleObject {
public:
ExampleObject() : data(new int(10)){
cout << " Call construct: " << ++n_construct << endl;
}
ExampleObject(const ExampleObject & other) : data(new int(*other.data)){
cout << " Call copy construct: " << ++n_copy<< endl;
}
ExampleObject(ExampleObject && other): data(other.data) {
other.data = nullptr;
cout << " Call move construct: " << ++n_move << endl;
}
int * data;
static int n_move;
static int n_copy;
static int n _construct;
static int n_descons;
} ;
int ExampleObject::n_move = 0;
int ExampleObject::n_copy = 0;
int ExampleObject::n_construct = 0;
int ExampleObject::n_descons = 0;
ExampleObject GetTmpO() {
ExampleObject o;
cout << "res from " << __func__ << ":" << hex << o.data << endl;
return o;
}
int main() {
ExampleObject o = GetTmpO();
cout << "res from " << __func__ ":" << hex << o.data << endl;
}
CompexNumber a(1,2);
CompexNumber b(3, 4);
CompexNumber result;
add(a, b result) ;
// operate result)
我们希望可以这样写
CompexNumber result = add(a, b);
同时不损失性能
这就给新标准提出了一个需求。
C++11的移动构造函数就是为了解决这个问题的。
智能指针的进化
略
多线程支持
小特性集合
移位符号和模板尖括号的兼容
在上一个版本 C98中,编译器无法通过如下的表达
template <class T> class TypeA;
template <int i > class T;
TypeA<T<1> > t1; // 编译成功
TypeA<T<2>> 22; // 编译失败
又如类型转换表达
static_cast<std::vector<int>>(var);//无法编译
C++11通过标准化约定以上的表达都可以行得通,而不用增加额外的括号,
评述
实际上 对于模板嵌套和右移符号冲突的问题,大多数并不是很尖锐,完全可以让编译器做出智能判断,在极少数的模板尖括号内使用位移符号的情况,只是一种很刁钻的场景。如
template <int i> class T;
T< 1>> 5> t; //C11无法通过,C98可以。
要使得C11也顺利通过只需加一个括号
T< (1 >> 5)> t;
在编程用户端减少一些无聊的分歧是必要的,因此C++加入了这样的需求
另一个例子并不是出现在C++11 标准中