类型推导 Auto Type
过去的这种冗长的类型声明
1.std::map<int, std::string>::const_iterator itr = m.find(1);
现在可以写成这样了
1.auto itr = m.find(1);
编译器会自动推导出正确的类型。字面量也可以:
1.auto i = 1; // int
2.auto d = 1.1; // double
3.auto s = "hi"; // const char*
4.auto a = { 1, 2 }; // std:: initializer_list<int>
如果是用Visual Studio,把鼠标悬停在变量名上方,可以看到推导后的类型名称。类型推导对于泛型编程非常方便,比如:
1.template<typename T, typename K>
2.auto add(T a, K b) {
3. return a + b;
4.}
5.auto a = add(1, 2); // int add(int, int)
6.auto b = add(1, 2.2); // double add(int, double)
留意第二个调用,返回值被正确地推断为double类型。
遍历 foreach
以前遍历vector一般是这么写的
1.for (std::vector<int>::const_iterator itr = v.begin(); itr != v.end(); ++itr) {
2. std::cout << *itr << std::endl;
3.}
这样写有两个缺点:
1.迭代器声明很冗长 (用auto可以部分解决)
2.循环内部必须对迭代器解引用(主要是难看)
可以使用的新的遍历方式:
1.for (int i : v) {
2. std::cout << i << std::endl;
3.}
代码立马简洁了许多。但是要注意,这里每次循环,会对i进行一次拷贝。此处i是一个int值,拷贝不会造成问题,但是如果是一个class,我们就更希望用引用的方式进行遍历,一般写成:
1.std::vector<string> v = { "a", "b" };
2.for (auto& s : v) {
3. std::cout << s << std::endl;
4.}
用auto&即可以变成引用方式遍历,甚至还能在循环中改变它的值。也可以使用const auto&,只是一般没有必要。
using代替typedef
在C/C++中,我们经常通过typedef来定义类型的别名。
例如:
1.typedef unsigned char u1;
2.typedef unsigned short u2;
但是,这样定义有一点不好,新定义的别名是放在后面的。一般我们都是通过别名找原名,从后往前找还是不方便的。
那么,我们把别名定义在前面好不好?
1.using u4 = uint32_t;
2.using u8 = uint64_t;
在C++11中,using不再只是用于using namespace啦,从此在别名领域大显身手。
空指针 nullptr
以往我们使用NULL表示空指针。它实际上是个为0的int值。下面的代码会产生岐义:
1.void f(int i) {} // chose this one
2.void f(const char* s) {}
3.f(NULL);
为此C++ 11新增类型nullptrt,它只有一个值nullptr。上面的调用代码可以写成:
1.void f(int i) {}
2.void f(const char* s) {} // chose this one
3.f(nullptr);
强类型枚举 enum class
原来的enum有两个缺点:
1.容易命名冲突
2.类型不严格
如下代码:
1.enum Direction {
2. Left, Right
3.};
4.enum Answer {
5. Right, Wrong
6.};
此代码编译报错:Right重定义。这里使用了单个单词作为名称,很容易出现冲突。所以我们一般加个前缀,变成:
1.enum Direction {
2. Direction_Left, Direction_Right
3.};
4.enum Answer {
5. Answer_Right, Answer_Wrong
6.};
这样写很难看,而且如果这两个枚举是分别从两个第三方库引入的,那就无法自己改名字了。而且改成这样依然有个问题:
1.auto a = Direction_Left;
2.auto b = Answer_Right;
3.if (a == b)
4. std::cout << "a == b" << std::endl;
5.else
6. std::cout << "a != b" << std::endl;
这个代码将输出a == b,因为这两上值都为0。然而允许两个不同类型的值作比较,就是不合理的,容易隐藏一些bug。
C++ 11引入了enum class:
1.enum class Direction {
2. Left, Right
3.};
4.
5.enum class Answer {
6. Right, Wrong
7.};
8.
9.auto a = Direction::Left;
10.auto b = Answer::Right;
11.
12.if (a == b)
13. std::cout << "a == b" << std::endl;
14.else
15. std::cout << "a != b" << std::endl;
引用时必须加上枚举名称(Direction_Left变成Direction::Left),似乎写法上差不多,但是这样类型更加严格。下面的a == b编译将会报错,因为它们是不同的类型。
枚举值不再是全局的,而是限定在当前枚举类型的域内。所以使用单个单词作为值的名称也不会出现冲突。
构造函数的相互调用 delegating constructor
同一个class的多个构造函数的内部实现通常非常相似,比如:
1.class A {
2.public:
3. A(int x, int y, const std::string& name) : x(x), y(y), name(name) {
4. if (x < 0 || y < 0)
5. throw std::runtime_error("invalid coordination");
6. // other stuff
7. }
8.
9. A(int x, int y) : x(x), y(y), name("A") {
10. if (x < 0 || y < 0)
11. throw std::runtime_error("invalid coordination");
12. // other stuff
13. }
14.
15. A() : x(0), y(0), name("A") {
16. // other stuff
17. }
18.
19.private:
20. int x;
21. int y;
22. std::string name;
23.};
为了避免重复代码,通常会把共同的代码挪到一个init成员函数里:
1.class A {
2.public:
3. A(int x, int y, const std::string& name) {
4. init(x, y, name);
5. }
6.
7. A(int x, int y) {
8. init(x, y, "A");
9. }
10.
11. A() {
12. init(0, 0, "A");
13. }
14.
15.private:
16. void init(int x, int y, const std::string& name) {
17. if (x < 0 || y < 0)
18. throw std::runtime_error("invalid coordination");
19. this->x = x;
20. this->y = y;
21.
22. if (name.empty())
23. throw std::runtime_error("empty name");
24. this->name = name;
25.
26. // other stuff
27. }
28.
29.private:
30. int x;
31. int y;
32. std::string name;
33.};
这样写有三个问题:
1.二次赋值。执行到init函数时,数据成员实际已经初始化了。比如name成员,此时已经初始化为一个空字符串了。这里实际上是又调用了一次“=”操作符。对于初始化成本比较高的类型,这样做就有可能影响性能了。
2.只能调用成员的无参构造函数。只有构造函数的初始化列表才能调用成员的带参数构造函数。
3.无法保证init只被调用一次。有些初始化步骤必须保证只被执行一次,这一点只有构造函数可以保证。
C++ 11允许构造函数之间相互调用了:
1.class A {
2.public:
3. A(int x, int y, const std::string& name) : x(x), y(y), name(name) {
4. if (x < 0 || y < 0)
5. throw std::runtime_error("invalid coordination");
6. if (name.empty())
7. throw std::runtime_error("empty name");
8. // other stuff
9. }
10.
11. A(int x, int y) : A(x, y, "A")
12. {}
13.
14. A() : A(0, 0)
15. {}
16.
17.private:
18. int x;
19. int y;
20. std::string name;
21.};
除了优雅地解决了上述三个问题之外,代码也简洁了许多,连name成员的默认值"A"也只需要写一次。
禁止重写 final
禁止虚函数被重写
1.class A {
2.public:
3. virtual void f1() final {}
4.};
5.
6.class B : public A {
7. virtual void f1() {}
8.};
此代码编译报错,提示不能重写f1。虽然f1是虚函数,但是因为有final关键字,保证它不会被重写。你可能会说,那不声明virtual不就完了。但是如果A本身也有基类,f1是继承下来的,那virtual就是隐含的了。
禁止类被继承
1.class A final {
2.};
3.
4.class B : public A {
5.};
此代码编译报错,提示不能继承A。
显式声明重写 override
1.class A {
2.public:
3. virtual void f1() const {}
4.};
5.
6.class B : public A {
7. virtual void f1() {}
8.};
上面的代码在重写函数f1时不小心漏了const,但是编译器不会报错。因为它不知道你是要重写f1,而认为你是定义了一个新的函数。这样的情况也发生在基类的函数签名变化时,子类如果没有全部统一改过来,编译器也不能发现问题。
C++ 11引入了override声明,使重写更安全。
1.class B : public A {
2. virtual void f1() override {}
3.};
此时编译报错,提示找不到重写的函数。
Smart Pointers 智能指针
C++指针的内存管理相信是大部分C++入门程序员的梦魇,受到Boost的启发,C++11标准推出了智能指针,让我们从指针的内存管理中释放出来,几乎消灭所有new和delete。既然智能指针如此强大,今天我们来一窥智能指针的原理以及在多线程操作中需要注意的细节。
在远古时代,C++使用了指针这把双刃剑,既可以让程序员精确地控制堆上每一块内存,也让程序更容易发生crash,大大增加了使用指针的技术门槛。因此,从C++98开始便推出了auto_ptr,对裸指针进行封装,让程序员无需手动释放指针指向的内存区域,在auto_ptr生命周期结束时自动释放。
1.class A
2.{
3.public:
4. A() { cout << "A()" << endl; }
5. ~A() { cout << "~A()" << endl; }
6.};
7.
8.int main()
9.{
10. auto_ptr<A> pa(new A);
11. return 0;
12.}
然而,由于auto_ptr在转移指针所有权后会产生野指针,导致程序运行时crash,如下面示例代码所示:
1.auto_ptr<int> p1(new int(10));
2.//转移控制权
3.auto_ptr<int> p2 = p1;
4.
5.*p1 += 10; //crash,p1为空指针,可以用p1->get判空做保护
因此在C++11又推出了unique_ptr、shared_ptr、weak_ptr三种智能指针,慢慢取代auto_ptr。
unique_ptr的使用
unique_ptr是auto_ptr的继承者,对于同一块内存只能有一个持有者,而unique_ptr和auto_ptr唯一区别就是unique_ptr不允许赋值操作,也就是不能放在等号的右边(函数的参数和返回值例外),这一定程度避免了一些误操作导致指针所有权转移,然而,unique_str依然有提供所有权转移的方法move,调用move后,原unique_ptr就会失效,再用其访问裸指针也会发生和auto_ptr相似的crash,如下面示例代码,所以,即使使用了unique_ptr,也要慎重使用move方法,防止指针所有权被转移。
1.unique_ptr<int> up(new int(5));
2.auto up2 = up; // 编译错误
3.auto up2 = move(up);
4.cout << *up << endl; //crash,up已经失效,无法访问其裸指针
真正的智能指针:shared_ptr
auto_ptr和unique_ptr都有或多或少的缺陷,因此C++11还推出了shared_ptr,这也是目前工程内使用最多最广泛的智能指针,shared_ptr允许多个该智能指针共享第“拥有”同一堆分配对象的内存,这通过引用计数(reference counting)实现,会记录有多少个shared_ptr共同指向一个对象,一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。成员函数use_count()可以观测资源的引用计数。
1.class A
2.{
3.public:
4. int i;
5. A(int n) :i(n) { cout << "A(int)" << endl; };
6. ~A() { cout << i << " " << "~A()" << endl; }
7.};
8.int main()
9.{
10. shared_ptr<A> sp1(new A(2)); //A(2)由sp1托管,
11. shared_ptr<A> sp2(sp1); //A(2)同时交由sp2托管
12. shared_ptr<A> sp3;
13. sp3 = sp2; //A(2)同时交由sp3托管
14. cout << sp1->i << "," << sp2->i << "," << sp3->i << endl;
15. A * p = sp3.get(); // get返回托管的指针,p 指向 A(2)
16. cout << p->i << endl; //输出 2
17. sp1.reset(new A(3)); // reset导致托管新的指针, 此时sp1托管A(3)
18. sp2.reset(new A(4)); // sp2托管A(4)
19. cout << sp1->i << endl; //输出 3
20. sp3.reset(new A(5)); // sp3托管A(5),A(2)无人托管,被delete
21. cout << "end" << endl;
22. return 0;
23.}
刚柔并济:weak_ptr
weak_ptr这个指针天生一副“小弟”的模样,也是在C++11的时候引入的标准库,它的出现完全是为了弥补它老大shared_ptr天生有缺陷的问题,其实相比于上一代的智能指针auto_ptr来说,新进老大shared_ptr可以说近乎完美,但是通过引用计数实现的它,虽然解决了指针独占的问题,但也引来了引用成环的问题,这种问题靠它自己是没办法解决的,所以在C++11的时候将shared_ptr和weak_ptr一起引入了标准库,用来解决循环引用的问题。下面的例子就演示了循环引用的问题:
1.class B;
2.
3.class A
4.{
5.public:
6. A(){ cout << "A()" << endl; };
7. ~A() { cout << "~A()" << endl; }
8. shared_ptr<B> pb;
9.};
10.
11.class B
12.{
13.public:
14. B() { cout << "B()" << endl; };
15. ~B() { cout << "~B()" << endl; }
16. shared_ptr<A> pa;
17.};
18.
19.int main()
20.{
21. shared_ptr<A> PA(new A);
22. shared_ptr<B> PB(new B);
23.
24. PA->pb = PB;
25. PB->pa = PA;
26.
27. return 0;//A和B没有析构。
28.}
解决这种状况的办法就是将两个类中的一个成员变量改为weak_ptr对象,因为weak_ptr不会增加引用计数,使得引用形不成环,最后就可以正常的释放内部的对象,不会造成内存泄漏,比如将CB中的成员变量改为weak_ptr对象,代码如下:
29.class B;
30.
31.class A
32.{
33.public:
34. A(){ cout << "A()" << endl; };
35. ~A() { cout << "~A()" << endl; }
36. weak_ptr<B> pb;
37.};
38.
39.class B
40.{
41.public:
42. B() { cout << "B()" << endl; };
43. ~B() { cout << "~B()" << endl; }
44. shared_ptr<A> pa;
45.};
46.
47.int main()
48.{
49. shared_ptr<A> PA(new A);
50. shared_ptr<B> PB(new B);
51.
52. PA->pb = PB;
53. PB->pa = PA;
54.
55. return 0;//A和B都得以析构
56.}
Lambda函数
这是个非常强大的重量级功能。简单地讲,就是可以用它定义一个临时的函数对象,它像其它对象一样可以传递和保存。更为强大的是,它甚至可以访问当前函数的上下文。
特性
1.调用
1.auto add = [](int a, int b) { return a + b; };
2.std::cout << add(1, 2) << std::endl;
=后面的部分就是Lambda函数。先忽略前面的[]。()里面的是参数列表,{}里面的是实现。跟普通的函数基本一样。
这里没有声明返回值类型,编译器会根据return语句推导。如果有多个return语句,而且类型不一样,则会报错。
使用方式与普通函数一样。
2.传递
1.template<typename filter_func>
2.void print(const std::vector<int>& v, filter_func filter) {
3. for (auto i : v) {
4. if (filter(i))
5. std::cout << i << std::endl;
6. }
7.}
8.
9.bool isGreaterThanTen(int i) {
10. return i > 10;
11.}
12.
13.class GreaterThanTenFilter {
14.public:
15. bool operator()(int i) {
16. return i > 10;
17. }
18.};
19.
20.std::vector<int> v = { 5, 10, 15, 20 };
21.print(v, isGreaterThanTen); // 输出 15 20
22.print(v, GreaterThanTenFilter()); // 输出 15 20
以上代码分别使用了函数指针和函数对象来指定过滤条件。这两种方式存在以下缺点:
代码冗余。需要单独定义一个函数或class才能实现。
filter_func的类型不明确。此处filter_func是一个参数为一个int,返回值为bool型的函数。但是这一点无法从函数声明看出来。并且函数对象使用()操作符语义也不明确。
print函数必须使用模板。虽然print内部并没有使用泛型的必要,但是考虑到兼容函数指针和函数对象的用法,也只能使用模板实现。
不灵活。如果这个10是一个运行时才确定的数字n,就需要修改函数对象才能实现。(函数指针无法实现)
使用Lambda:
1.#include <functional>
2.
3.void print(const std::vector<int>& data, std::function<bool(int)> filter) {
4. for (auto i : data) {
5. if (filter(i))
6. std::cout << i << std::endl;
7. }
8. }
9.
10.std::vector<int> v = { 5, 10, 15, 20 };
11.print(v, [](int i) { return i > 10; }); // 输出 15 20
解决了上面提到的几个问题:
代码简洁。无需另外定义函数或class即可实现。整体代码缩小了不少。
类型明确。新增的std::function是一个通用的函数对象,可以使用Lambda初始化。最大的优点是参数和返回值都是明确的,可以从声明看出来。
无须使用模板。
更灵活。这一点接下来讲。
3.可以访问当前函数的上下文
上面的例子如果把硬编码的10改成变量n,只需要改调用的地方:
1.int n = 10;
2.print(v, [=](int i) { return i > n; });
可以看到前面的[]改成了[=],这表示Lambda使用值传递的方式捕获外部变量。
[]表示捕获列表,用来描述Lambda访问外部变量的方式。如下:
捕获列表 作用
[a] a为值传递
[a, &b] a为值传递,b为引用传递
[&] 所有变量都用引用传递。当前对象(即this指针)也用引用传递。
[=] 所有变量都用值传递。当前对象用引用传递。
注意事项
捕获时机
1.int i = 1;
2.auto f = = { std::cout << i << std::endl; };
3.i = 2;
4.f(); // 输出 1
可以看出,在定义Lambda的地方就已经捕获到i的值。后面修改i也不影响f的输出。
如果把[=]改成[&],则会输出2。因为Lambda实际上只捕获到i的引用。
局部变量的生命周期
1.std::function<void()> GetLambda() {
2. int i = 1;
3. return [&]() { std::cout << i << std::endl; };
4.}
5.
6.auto f = GetLambda();
7.f(); // 输出 -858993460 之类的乱码
使用引用的方式访问局部变量时,要注意Lambda的生命周期不能超过该局部变量的生命周期。