C++11新标基于C++编程社区的大量实践经验,并吸收了很多Boost库的特性,还对原有C++做了一些改进工作,是学习现代C++编程的基础。这里参考《C++ Primer Plus 第六版》,对一些常用的C++11新特性做一个总结:
##1、统一的初始化
C++11支持对于所有的内置类型和用户定义类型使用大括号方式的初始化列表,使用初始化列表时,可以添加等号,也可以没有。
int x = {5}; // 以前只有数组可以这样初始化
double y {2.75};
int* ar = new int[4] {2,4,6,8};
classType s1(构造参数1, 构造参数2); // 传统方式创建一个classType对象
classType s2{构造参数1, 构造参数2}; // C++11方式创建一个classType对象
classType s3 = {构造参数1, 构造参数2}; // C++11方式创建一个classType对象
注意:C++11还提供了一个用于构造函数的模板类initializer_list,STL容器的构造函数就使用了这个类
std::vector<int> a1(10); // 声明包含10个int元素的vector容器
std::vector<int> a2={10}; // 声明容器a2,并初始化一个值为10的元素
std::vector<int> a3={4,6,1}; // 声明容器a3,且初始化三个元素的值为4,6,1
##2、右值引用和移动语意
传统意义的左值,指的是出现在赋值语句左边的表达式。例如:
int a = 20; // 这里的a就是一个左值
传统的C++引用都是使一个标识符关联到一个左值,所以传统的C++引用也被称为左值引用,例如:
int a = 20;
int& ra = a; // 即ra是变量a的引用(或别名)
C++11新增了右值表达式,采用&&表示。和左值引用(可以关联到一个左值)类似,右值引用可以关联到一个右值。具体说:
int a = 10;
int b = 20;
int c = a + b; // 这里的a+b就是一个右值,以前是没有办法对a+b设置引用的
int&& r1 = a + b; // 根据C++11,r1可以关联到a+b产生的那个临时对象,a、b的值修改了,r1也不变
右值包括:字面常量(C风格的字符串除外)、类似于x+y的表达式、带有返回值的函数(返回值类型不是引用的那种函数)。基本上就是一些编译器产生的临时变量,在C++中尤其多。
右值引用的意义:就是为了和传统左值引用区分开(好像是废话啊)。要理解右值引用,需要先了解移动语意。
在传统C++,拷贝构造函数和赋值运算符的重载函数是两个非常重要的概念。
如果一个类的数据成员中有指针变量,且需要这个类负责这些指针所指向资源的申请和回收。当这个类对象作为函数的参数、返回值或者是一个对象用于给另外一个对象进行初始化时,必须由用户实现这个类的拷贝构造函数,否则采用编译器自动生成的拷贝构造函数(浅拷贝),将导致多个对象拥有同一个资源,当销毁这多个对象时,这些资源面临重复释放的问题。所以,在传统C++的世界里,我们似乎建立了一种观念即**“浅拷贝是不安全的,深拷贝是安全的”**。
但是,深拷贝带来的一个问题就是需要重新申请内存,例如,C++编译器常用临时对象初始化一个左值对象。当构造这些临时对象时,本身就执行了一遍内存申请的操作,再通过这些临时对象初始化一个左值对象时,左值对象又会因为深拷贝再重新申请内存。很明显这个过程,对于临时对象显得非常的多余,而C++恰好是一种特别喜欢在背后生成大量临时对象的语言,所以,当临时对象、深拷贝、STL这些东西遇到一起的时候,经常会导致软件效率奇低。
C++提出移动语意的原始目的就是要将临时对象申请的资源通过移动语意直接转移给左值对象(几乎就是浅拷贝),并且,接下来临时对象在析构的时候,也不会再释放这些已经转移的内存资源了。
理解了上述关于移动语意的目标了,所以接下来要做的就是,应该采用哪种方式让编译器知道什么时候需要转移,什么时候不需要转移呢?其实也很简单,那就是,如果使用传统的左值引用(T&)就表示不需要转移,如果使用右值引用(T&&)则表示需要转移。有了右值引用标识符T&&,程序员通过定义移动构造函数和移动赋值运算符函数就可以实现移动语意的目标了(因为编译器不提供默认移动构造函数)。具体如下:
class Useless
{
public:
Useless(const Useless& f); // 传统的拷贝构造函数
Useless(Useless&& f); // 移动构造函数
Useless& operator=(const Useless& f); // 传统的赋值运算符重载函数
Useless& operator=(seless&& f); // 移动赋值运算符函数
private:
int n;
char* pc;
}
// 移动构造函数
Useless::Useless(Useless&& f)
{
n = f.n;
pc = f.pc;
f.n = 0;
f.pc = nullptr;
}
// 移动赋值运算符函数实现
Useless& Useless::operator=(seless&& f)
{
if (thhis == &f)
return *this;
delete []pc;
n = f.n;
pc = f.pc;
f.n = 0;
f.pc = nullptr;
return *this;
}
强制移动:C++11新增std::move()函数可以无条件将其参数(左值)强制转换为右值,转化为右值后,以前由参数控制的资源就可以顺利的转移给另一个左值对象。需要注意的是,move函数执行完后,参数虽然还在,但是参数管理的资源已经转移了,所以这个参数不能再参与后续的计算了,否则一般都会出现异常。
总结:对于大多数程序员而言,右值引用带来的最大好处并非是自己编写使用右值引用的代码。而是很多STL利用右值引用实现了移动语意,程序员在使用这些STL时,代码性能会更好。
参考资料:关于右值引用、移动语意、完美转发
##3、智能指针
智能指针是支撑RAII(获取资源即初始化)编程理念的重要方法。对于C++的RAII,简单的讲,就是所有在堆上分配的资源都委托一个栈对象管理。通过栈对象的自动释放,实现堆上资源的自动释放,有效防止内存泄露。
C++11中的智能指针包括三种:std::unique_ptr、std::shared_ptr和std::weak_ptr。
这三种智能指针都是模板类,每个实例化的智能指针对象都是一个用于管理裸指针的栈对象。所以,智能指针对象一般都是直接定义在函数的栈上(不会使用new运算符创建),智能指针对象的内部实现一定是管理一个原始指针,且这个原始指针最终要指向堆上分配的资源。
因为智能指针重载指针运算符(-> 和 *)以返回封装的原始指针,所以,程序员可以像使用普通指针一样使用智能指针。
std::unique_ptr智能指针的创建和使用:
class Test
{
public:
Test(){}
Test(int x, int y)
: x_(x),y_(y)
{}
void output()
{
std::cout << "x=" << x_ << " y=" << y_ << std::endl;
}
private:
int x_;
int y_;
};
int main()
{
// C++11中一般只能用下面两种方式创建unique_ptr
std::unique_ptr<Test> pT4 = std::unique_ptr<Test>(new Test(4,4));
std::unique_ptr<Test> pT4(new Test(4,4));
std::unique_ptr<Test[]> pT3(new Test[4]); // 创建被智能指针管理的对象数组
// unique_ptr独占指针对象,并保证指针所指对象生命周期与其一致。
// unique_ptr对象禁止复制,无法通过值传递到函数(可以传递引用),
// 也无法用于需要副本的任何标准模板库(STL)算法。
std::unique_ptr<Test> pT5 = pT4; // 这一行会产生编译错误
std::unique_ptr<Test> pT6; // 允许创建一个空对象
pT4->output(); // 像使用普通指针一样,这一行可以正常运行
pT6->output(); // pT6还是一个空对象,所以这一行会产生异常
pT6= std::move(pT4); // pT4管理的指针指向的资源被强制转移到pT6
pT6->output(); // 此时pT6已不是空对象,所以这一行可以正常运行
pT4->output(); // 此时pT4变成了空对象,所以这一行会产生异常
if (pT4) // 可以使用if语句判断一个空对象,就像判断普通指针一样
pT4->output();
// 函数的返回值可以是unique_ptr,可用于初始化另一个unique_ptr对象
std::unique_ptr<Test> pT7 = clone(7);
pT7->output();
std::vector<std::unique_ptr<Test>> vec; // 创建一个保存智能指针对象的容器,
vec.push_back(std::move(pT7)); // 使用移动语义将pT7的资源转移到STL容器中
pT6.reset(); // 解除pT6关联的原始指针,则pT6变为空对象
}
std::unique_ptr<Test> clone(int p)
{
std::unique_ptr<Test> pTest(new Test(p, p));
return pTest; // 返回unique_ptr
}
std::unique_ptr的成员函数:
函数名 | 作用 |
---|---|
release | 返回一个指向被管理对象的指针,并释放所有权 |
reset | 替换所管理的对象 |
swap | 交换所管理的对象 |
get | 返回指向被管理对象的指针 |
get_deleter | 返回删除器,用于被管理对象的析构 |
operator bool | 检查是否有关联的被管理对象 |
operator* | 对指向被管理对象的指针进行解引用 |
operator-> | 对指向被管理对象的指针进行解引用 |
operator[] | 提供对所管理数组的按索引访问 |
总结std::unique_ptr的特点:
1、堆资源的管理者始终只有一个,一般不会导致堆资源的生存周期被意外的延长。
2、函数内部创建堆资源,并将堆资源转移到函数外部使用和管理,且不会产生堆资源所有权混乱的问题。
3、一般情况下使用std::unique_ptr就足够支持堆资源自动管理了,如果有多线程共同使用堆资源,或者是智能指针对象既要保存到STL容器中,同时还要在容器外部使用,此时就必须考虑使用std::shared_ptr(注意不要引起循环引用)。
std::shared_ptr智能指针的创建和使用:
此链接详细分析了shared_ptr和weak_ptr的实现原理
int main()
{
// C+11允许用下面三种方式创建shared_ptr
std::shared_ptr<Test>sp1(std::make_shared<Test>(1,1));
std::shared_ptr<Test>sp2 = std::make_shared<Test>(2,2);
std::shared_ptr<Test>sp3(new Test(3,3));
// shared_ptr允许复制,此时shared_ref_count=2
std::shared_ptr<Test>sp4 = sp1;
// weak_ptr总是通过shared_ptr复制,但是不会导致的shared_ref_count增加
std::weak_ptr<Test>wp5 = sp4;
std::shared_ptr<Test>sp6 = wp5.lock();// 使用weak_ptr之前必须先通过lock观察
if (sp6)
{
sp6->output();
}
}
把一个 shared_ptr对象传递给另一个函数的几种方式:
1、通过值传递shared_ptr,将会调用智能指针的复制构造函数生成一个属于被调用方的shared_ptr,此时智能指针的引用计数会增加,被调用方也成为了一个所有者。这次操作中有少量的开销,很大程度上取决于传递了多少 shared_ptr 对象。当调用方和被调用方之间的代码协定 (隐式或显式) 要求被调用方是所有者,使用此选项。
2、通过引用或常量引用来传递 shared_ptr。在这种情况下,引用计数不增加,并且只要调用方不超出范围(正常情况下,调用函数和被调用函数都在一个线程,肯定不会超出范围),被调用方就可以访问指针。或者,被调用方可以决定创建一个基于引用的 shared_ptr,从而成为一个共享所有者。当调用者并不知道被被调用方,或当您必须传递一个 shared_ptr,并希望避免由于性能原因的复制操作,请使用此选项。
3、获取被 shared_ptr管理指针,并向函数传递这个原始指针。这使得被调用方可以根据指针使用对象,但原来的 shared_ptr中的引用计数不会增加或对象的生存期也不会扩展生。如果被调用方用接收到的原始指针创建一个shared_ptr,则新的 shared_ptr 是独立于原来的(一般都不应该这么干)。
##4、Lambda函数
以前C++中,一些短小的函数,可以写成内联函数,到了C++11中,则可以使用Lambda函数来表示。
所以,lambda函数仍然是一个可调用的代码单元,可以将其理解为一个未命名的内联函数,C++11中Lambda表达式具体形式如下:
[capture](parameters)->return-type{body}
如果没有参数,空的圆括号()可以省略,如果函数没有返回值得话,“->return-type”也可以省略。
[](int x, int y) -> int { int z = x + y; return z; } // 一个完整的lambda函数定义
[](int x, int y) { return x + y; } // 隐式返回类型
[](int& x) { ++x; } // 如果没有return语句,则lambda函数的返回类型可省略
[]() { ++global_x; } // 没有参数,仅访问某个全局变量
[]{ ++global_x; } // 没有参数可省略了(),仅访问某个全局变量
Lambda函数可以引用在它之外声明的变量. 这些变量的集合叫做一个闭包. 闭包被定义在Lambda表达式声明中的方括号[]内. 这个机制允许这些变量被按值或按引用捕获。
[] //未定义变量.试图在Lambda内使用任何外部变量都是错误的.
[x, &y] //x 按值捕获, y 按引用捕获.
[&] //用到的任何外部变量都隐式按引用捕获
[=] //用到的任何外部变量都隐式按值捕获
[&, x] //x显式地按值捕获. 其它变量按引用捕获
[=, &z] //z按引用捕获. 其它变量按值捕获
从使用上看,Lambda函数的作用和函数指针或函数对象相似。具体的使用方式如下:
int main()
{
std::vector<int> number={1,2,3,4,5,6,7,8,9,10};
int count = 0;
std::for_each(number.begin(), number.end(),
[&count](int x){count = count+x;});
printf("count = %d \n", count);
// 也可以给Lambda函数定义一个名字,这样可以通过名字调用Lambda函数
auto func = [] () { printf("Hello world"); };
func();
}
如上所示,Lambda函数的好处就是函数的定义和函数的使用在一起,所以,一般要求Lambda函数必须很短。
##5、包装器
如果理解函数指针、函数对象、(具名)Lambda函数。我们会突然发现,C++中有多种类似函数的调用形式,这些调用形式的存在对于普通程序原来讲其实无所谓,够用就行。但是对于模板化编程来说,太多的调用形式会导致使用了这些调用形式的模板被实例化多次。所以,C++11提出了将多种函数调用形式统一的包装器std::function。
所以,简单的说,std::function是对C++中各种可调用实体(普通函数、Lambda表达式、函数指针、以及其它函数对象等)的一个统一封装,形成一个新的统一可调用对象(std::function对象)(主要是对模板化编程很有意义)。
#include <functional>
#include <iostream>
using namespace std;
std::function< int(int)> Functional;
// 普通函数
int TestFunc(int a)
{
return a;
}
// Lambda表达式
auto lambda = [](int a)->int{ return a; };
// 仿函数(functor)
class Functor
{
public:
int operator()(int a)
{
return a;
}
};
// 1.类成员函数
// 2.类静态函数
class TestClass
{
public:
int ClassMember(int a) { return a; }
static int StaticMember(int a) { return a; }
};
int main()
{
// 普通函数
Functional = TestFunc;
int result = Functional(10);
cout << "普通函数:"<< result << endl;
// Lambda表达式
Functional = lambda;
result = Functional(20);
cout << "Lambda表达式:"<< result << endl;
// 仿函数
Functor testFunctor;
Functional = testFunctor;
result = Functional(30);
cout << "仿函数:"<< result << endl;
// 类成员函数
TestClass testObj;
Functional = std::bind(&TestClass::ClassMember, testObj, std::placeholders::_1);
result = Functional(40);
cout << "类成员函数:"<< result << endl;
// 类静态函数
Functional = TestClass::StaticMember;
result = Functional(50);
cout << "类静态函数:"<< result << endl;
return 0;
}
std::bind机制可以预先把已有的变量和可调用实体的某些参数绑定,产生一个新的可调用实体,这种机制在回调函数的使用过程中也颇为有用。以前类的成员函数因为参数中隐含this指针,所以无法直接注册为回调函数,现在可以通过bind提前绑定this指针注册为回调函数bind的返回值是可调用实体,可以直接赋给std::function对象。
auto newConnectionCallback_ = std::bind(&EchoServer::newConnection, this, std::placeholders::_1);
bind能够在绑定时候就同时绑定一部分参数,未提供的参数则使用占位符表示,然后在运行时传入实际的参数值。如上所示,EchoServer::newConnection表示被包装的函数,第一个参数表示绑定函数所属对象的this指针。std::placeholders::_1表示是新函数的第1个参数(依次类推)。
bind的返回值是可调用实体,可以直接赋给std::function对象。