1.auto关键字
C++11中引入auto第一种作用是为了自动类型推导。auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作。auto实际上是在编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响。另外,似乎auto并不会影响编译速度,因为编译时本来也要右侧推导然后判断与左侧是否匹配。
auto a; // 错误,auto是通过初始化表达式进行类型推导,如果没有初始化表达式,就无法确定a的类型 auto i = 1;//i is an int auto d = 1.0;//d is a double auto str = "Hello World"; auto ch = 'A'; auto func = less<int>(); vector<int> iv; auto iter = iv.begin(); auto p = new foo(); // p is a foo*
2.decltype
decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型,有实例如下:
auto i = 1; auto d = 1.0; auto str = "Hello World"; auto ch = 'A'; cout << sizeof(decltype(i)) << endl;//4 cout << sizeof(decltype(d)) << endl;//8 cout << sizeof(decltype(str)) << endl;//4 cout << sizeof(decltype(ch)) << endl; //1 3.nullptrnullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0
#include <assert.h> #include <iostream> using namespace std; void F(int a) { cout << a << endl; } void F(int *p) { assert(p != NULL); cout << p << endl; } int main() { int *p = nullptr; int *q = NULL; bool equal = (p == q); // equal的值为true,说明p和q都是空指针 int a = nullptr; // C++11下编译失败,nullptr不能转型为int F(0); // 在C++98中编译失败,有二义性;在C++11中调用F(int) F(nullptr); return 0; }4.序列for循环
在C++11中for循环可以使用类似java的简化的for循环,可以用于遍历数组,容器,string以及由begin和end函数定义的序列(即有Iterator),示例代码如下:
#include <iostream> #include <map> using namespace std; int main() { map<string, int> m{{"123",1},{"456",2},{"789",3}}; for (const auto& p : m) { cout<<p.first.c_str()<<":"<<p.second<<endl; } }5.更加优雅的初始化方法
在引入C++11之前,只有数组能使用初始化列表,其他容器想要使用初始化列表,只能用以下方法:
int arr[3] = {1, 2, 3} vector<int> v(arr, arr + 3);在C++11中,我们可以使用以下语法来进行替换:
int arr[3] = {1,2,3}; vector<int> v{1,2,3}; string s{"hello world"}; cout << s.c_str() << endl; for (auto p : v) { cout << p << endl; } map<int, string> m{ {1, "111"}, {2, "222"}, {3, "333"} }; for (auto p : m) { cout << p.first << ":" <<p.second.c_str() << endl; }6.override和final
我总觉得 C++中虚函数的设计很差劲,因为时至今日仍然没有一个强制的机制来标识虚函数会在派生类里被改写。vitual关键字是可选的,这使得阅读代码变得很费劲。因为可能需要追溯到继承体系的源头才能确定某个方法是否是虚函数。为了增加可读性,我总是在派生类里也写上virtual关键字,并且也鼓励大家都这么做。即使这样,仍然会产生一些微妙的错误。看下面这个例子:
#include <iostream> using namespace std; class B { public: virtual void f(short) { cout << "B::f" << endl; } }; class D : public B { public: virtual void f(int) { cout << "D::f" << endl; } }; int main() { B* b = new D(); b->f(2);//调用的是B::f而不是D::f return 0; }D::f 按理应当重写 B::f。然而二者的声明是不同的,一个参数是short,另一个是int。因此D::f(原文为B::f,可能是作者笔误——译者注)只是拥有同样名字的另一个函数(重载)而不是重写。当你通过B类型的指针调用f()可能会期望打印出D::f,但实际上则会打出 B::f 。 另一个很微妙的错误情况:参数相同,但是基类的函数是const的,派生类的函数却不是。 #include <iostream> using namespace std; class B { public: virtual void f(int) const { cout << "B::f" << endl; } }; class D : public B { public: virtual void f(int) { cout << "D::f" << endl; } }; int main() { B* b = new D(); b->f(2);//调用的是B::f而不是D::f return 0; }同样,这两个函数是重载而不是重写,所以你通过B类型指针调用f()将打印B::f,而不是D::f。 幸运的是,现在有一种方式能描述你的意图。新标准加入了两个新的标识符(不是关键字): override:声明成override的函数,其子类必须重载,否则编译通不过。 final:表示派生类不应当重写这个虚函数。 将第一个例子里D::f函数声明如下: virtual void f(int) override { cout << "D::f" << endl; } 现在这将触发一个编译错误(后面那个例子,如果也写上override标识,会得到相同的错误提示): 'D::f' : method with override specifier 'override' did not override any base class methods 另一方面,如果你希望函数不要再被派生类进一步重写,你可以把它标识为final。可以在基类或任何派生类中使用final。在派生类中,可以同时使用override和final标识。被标记成final的函数将不能再被子类重写。
7.强类型枚举
传统的C++枚举类型存在一些缺陷:它们会将枚举常量暴露在外层作用域中(这可能导致名字冲突,如果同一个作用域中存在两个不同的枚举类型,但是具有相同的枚举常量就会冲突),而且它们会被隐式转换为整形,无法拥有特定的用户定义类型。例如:
enum Option{NONE, ONE, ALL}; enum Type {NONE = 9, ONE, ALL};//命名冲突 int i = ONE;//可以将枚举转换成整型 在C++11中通过引入了一个称为强类型枚举的新类型,修正了这种情况。强类型枚举由关键字enum class标识。它不会将枚举常量暴露到外层作用域中,也不会隐式转换为整形,并且拥有用户指定的特定类型(传统枚举也增加了这个性质)。 enum class Option {NONE, ONE, ALL}; Option o = Option::ALL;//Optioin o = ALL是错误的 int i = Option::ONE;//error8.模板函数的默认模板参数
在C++11中模板和函数一样,可以有默认的参数。这就带来了一定的复杂性。我可以通过如下所示的这个简单的模板函数的例子来回顾一下函数模板的定义。
#include <iostream> using namespace std; // 定义一个函数模板 template <typename T> void func(T a) { cout << a << endl; } int main() { func(1); // 1, (实例化为func<const int>(1)) func("1"); // 1, (实例化为func<const char *>("1")) } 在如上代码中,当编译器解析到函数调用func(1)的时候,发现func是一个函数模板。这时候编译器就会根据实参1的类型const int推导实例化出模板函数void func<const int>(int),再进行调用。相应的,对于func("1")来说也是类似的,不过编译器实例化出的模板函数的参数的类型将是const char *。 函数模板在C++98中与类模板一起被引入,不过在模板类声明的时候,标准允许其有默认模板参数。默认的模板参数的作用好比函数的默认形参。然而由于种种原因,C++98标准却不支持函数模板的默认模板参数。不过在C++11中,这一限制已经被解除了,我们可以看看下面这个例子: template<typename T1, typename T2 = int> void DefFunc1(T1 a, T2 b); 可以看到,DefFunc1函数模板拥有一个默认参数。使用仅支持C++98的编译器编译,DefFunc1的编译会失败,而支持C++11的编译器则毫无问题。不过在语法上,与类模板有些不同的是,在为多个默认模板参数声明指定默认值的时候,程序员必须遵照“从右往左”的规则进行指定。而这个条件对函数模板来说并不是必须的: //template<typename T1 = int, typename T2> class //DefClass2;//通不过编译,多个默认模板参数指定默认值时,必须遵守从右向左的原则 template<typename T1 = int, typename T2> void DefFunc2(T1 a, T2 b);//允许9.Defaulted函数
C++ 的类有四类特殊成员函数,它们分别是:默认构造函数、析构函数、拷贝构造函数以及拷贝赋值运算符。这些类的特殊成员函数负责创建、初始化、销毁,或者拷贝类的对象。如果程序员没有显式地为一个类定义某个特殊成员函数,而又需要用到该特殊成员函数时,则编译器会隐式的为这个类生成一个默认的特殊成员函数。例如:
清单 1
class X{ private: int a; }; X x; 在清单 1 中,程序员并没有定义类 X 的默认构造函数,但是在创建类 X 的对象 x 的时候,又需要用到类 X 的默认构造函数,此时,编译器会隐式的为类 X 生成一个默认构造函数。该自动生成的默认构造函数没有参数,包含一个空的函数体,即 X::X(){ }。虽然自动生成的默认构造函数仅有一个空函数体,但是它仍可用来成功创建类 X 的对象 x,清单 1 也可以编译通过。 但是,如果程序员为类 X 显式的自定义了非默认构造函数,却没有定义默认构造函数的时候,清单 2 将会出现编译错误:清单 2
class X{ public: X(int i){ a = i; } private: int a; }; X x; // 错误 , 默认构造函数 X::X() 不存在 清单 2 编译出错的原因在于类 X 已经有了用户自定义的构造函数,所以编译器将不再会为它隐式的生成默认构造函数。如果需要用到默认构造函数来创建类的对象时,程序员必须自己显式的定义默认构造函数。例如:清单 3
class X{ public: X(){}; // 手动定义默认构造函数 X(int i){ a = i; } private: int a; }; X x; // 正确,默认构造函数 X::X() 存在 从清单 3 可以看出,原本期望编译器自动生成的默认构造函数需要程序员手动编写了,即程序员的工作量加大了。此外,手动编写的默认构造函数的代码执行效率比编译器自动生成的默认构造函数低。类的其它几类特殊成员函数也和默认构造函数一样,当存在用户自定义的特殊成员函数时,编译器将不会隐式的自动生成默认特殊成员函数,而需要程序员手动编写,加大了程序员的工作量。类似的,手动编写的特殊成员函数的代码执行效率比编译器自动生成的特殊成员函数低。 Defaulted 函数的提出 为了解决如清单 3 所示的两个问题:1. 减轻程序员的编程工作量;2. 获得编译器自动生成的默认特殊成员函数的高的代码执行效率,C++11 标准引入了一个新特性:defaulted 函数。程序员只需在函数声明后加上“=default;”,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体。例如:清单 4
class X{ public: X()= default; X(int i){ a = i; } private: int a; }; X x; 在清单 4 中,编译器会自动生成默认构造函数 X::X(){},该函数可以比用户自己定义的默认构造函数获得更高的代码效率。 Defaulted 函数的用法及示例 Defaulted 函数特性仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数。例如:清单 5
class X { public: int f() = default; // 错误 , 函数 f() 非类 X 的特殊成员函数 X(int) = default; // 错误 , 构造函数 X(int) 非 X 的特殊成员函数 X(int = 1) = default; // 错误 , 默认构造函数 X(int=1) 含有默认参数 }; Defaulted 函数既可以在类体里(inline)定义,也可以在类体外(out-of-line)定义。例如:清单 6
class X{ public: X() = default; //Inline defaulted 默认构造函数 X(const X&); X& operator = (const X&); ~X() = default; //Inline defaulted 析构函数 }; X::X(const X&) = default; //Out-of-line defaulted 拷贝构造函数 X& X::operator = (const X&) = default; //Out-of-line defaulted // 拷贝赋值操作符 在 C++ 代码编译过程中,如果程序员没有为类 X 定义析构函数,但是在销毁类 X 对象的时候又需要调用类 X 的析构函数时,编译器会自动隐式的为该类生成一个析构函数。该自动生成的析构函数没有参数,包含一个空的函数体,即 X::~X(){ }。例如:清单 7
class X { private: int x; }; class Y: public X { private: int y; }; void main(){ X* x = new Y; delete x; } 在清单 7 中,程序员没有为基类 X 和派生类 Y 定义析构函数,当在主函数内 delete 基类指针 x 的时候,需要调用基类的析构函数。于是,编译器会隐式自动的为类 X 生成一个析构函数,从而可以成功的销毁 x 指向的派生类对象中的基类子对象(即 int 型成员变量 x)。 但是,这段代码存在内存泄露的问题,当利用 delete 语句删除指向派生类对象的指针 x 时,系统调用的是基类的析构函数,而非派生类 Y 类的析构函数,因此,编译器无法析构派生类的 int 型成员变量 y。 因此,一般情况下我们需要将基类的析构函数定义为虚函数,当利用 delete 语句删除指向派生类对象的基类指针时,系统会调用相应的派生类的析构函数(实现多态性),从而避免内存泄露。但是编译器隐式自动生成的析构函数都是非虚函数,这就需要由程序员手动的为基类 X 定义虚析构函数,例如:清单 8
class X { public: virtual ~X(){}; // 手动定义虚析构函数 private: int x; }; class Y: public X { private: int y; }; void main(){ X* x = new Y; delete x; } 在清单 8 中,由于程序员手动为基类 X 定义了虚析构函数,当利用 delete 语句删除指向派生类对象的基类指针 x 时,系统会调用相应的派生类 Y 的析构函数(由编译器隐式自动生成)以及基类 X 的析构函数,从而将派生类对象完整的销毁,可以避免内存泄露。 但是,在清单 8 中,程序员需要手动的编写基类的虚构函数的定义(哪怕函数体是空的),增加了程序员的编程工作量。更值得一提的是,手动定义的析构函数的代码执行效率要低于编译器自动生成的析构函数。 为了解决上述问题,我们可以将基类的虚析构函数声明为 defaulted 函数,这样就可以显式的指定编译器为该函数自动生成函数体。例如:清单 9
class X { public: virtual ~X()= defaulted; // 编译器自动生成 defaulted 函数定义体 private: int x; }; class Y: public X { private: int y; }; void main(){ X* x = new Y; delete x; } 在清单 9 中,编译器会自动生成虚析构函数 virtual X::X(){},该函数比用户自己定义的虚析构函数具有更高的代码执行效率。
10.Deleted函数
背景问题 对于 C++ 的类,如果程序员没有为其定义特殊成员函数,那么在需要用到某个特殊成员函数的时候,编译器会隐式的自动生成一个默认的特殊成员函数,比如拷贝构造函数,或者拷贝赋值操作符。例如: 清单 10
class X{ public: X(); }; void main(){ X x1; X x2=x1; // 正确,调用编译器隐式生成的默认拷贝构造函数 X x3; x3=x1; // 正确,调用编译器隐式生成的默认拷贝赋值操作符 } 在清单 10 中,程序员不需要自己手动编写拷贝构造函数以及拷贝赋值操作符,依靠编译器自动生成的默认拷贝构造函数以及拷贝赋值操作符就可以实现类对象的拷贝和赋值。这在某些情况下是非常方便省事的,但是在某些情况下,假设我们不允许发生类对象之间的拷贝和赋值,可是又无法阻止编译器隐式自动生成默认的拷贝构造函数以及拷贝赋值操作符,那这就成为一个问题了。 Deleted 函数的提出 为了能够让程序员显式的禁用某个函数,C++11 标准引入了一个新特性:deleted 函数。程序员只需在函数声明后加上“=delete;”,就可将该函数禁用。例如,我们可以将类 X 的拷贝构造函数以及拷贝赋值操作符声明为 deleted 函数,就可以禁止类 X 对象之间的拷贝和赋值。清单 11
class X{ public: X(); X(const X&) = delete; // 声明拷贝构造函数为 deleted 函数 X& operator = (const X &) = delete; // 声明拷贝赋值操作符为 deleted 函数 }; void main(){ X x1; X x2=x1; // 错误,拷贝构造函数被禁用 X x3; x3=x1; // 错误,拷贝赋值操作符被禁用 } 在清单 11 中,虽然只显式的禁用了一个拷贝构造函数和一个拷贝赋值操作符,但是由于编译器检测到类 X 存在用户自定义的拷贝构造函数和拷贝赋值操作符的声明,所以不会再隐式的生成其它参数类型的拷贝构造函数或拷贝赋值操作符,也就相当于类 X 没有任何拷贝构造函数和拷贝赋值操作符,所以对象间的拷贝和赋值被完全禁止了。 Deleted 函数的用法及示例 Deleted 函数特性还可用于禁用类的某些转换构造函数,从而避免不期望的类型转换。在清单 12 中,假设类 X 只支持参数为双精度浮点数 double 类型的转换构造函数,而不支持参数为整数 int 类型的转换构造函数,则可以将参数为 int 类型的转换构造函数声明为 deleted 函数。清单 12
class X{ public: X(double); X(int) = delete; }; void main(){ X x1(1.2); X x2(2); // 错误,参数为整数 int 类型的转换构造函数被禁用 } Deleted 函数特性还可以用来禁用某些用户自定义的类的 new 操作符,从而避免在自由存储区创建类的对象。例如:清单 13
#include <cstddef> using namespace std; class X{ public: void *operator new(size_t) = delete; void *operator new[](size_t) = delete; }; void main(){ X *pa = new X; // 错误,new 操作符被禁用 X *pb = new X[10]; // 错误,new[] 操作符被禁用 } 必须在函数第一次声明的时候将其声明为 deleted 函数,否则编译器会报错。即对于类的成员函数而言,deleted 函数必须在类体里(inline)定义,而不能在类体外(out-of-line)定义。例如:清单 14
class X { public: X(const X&); }; X::X(const X&) = delete; // 错误,deleted 函数必须在函数第一次声明处声明 虽然 defaulted 函数特性规定了只有类的特殊成员函数才能被声明为 defaulted 函数,但是 deleted 函数特性并没有此限制。非类的成员函数,即普通函数也可以被声明为 deleted 函数。例如:清单 15
int add (int,int)=delete; int main(){ int a, b; add(a,b); // 错误,函数 add(int, int) 被禁用 } 值得一提的是,在清单 15 中,虽然 add(int, int)函数被禁用了,但是禁用的仅是函数的定义,即该函数不能被调用。但是函数标示符 add 仍是有效的,在名字查找和函数重载解析时仍会查找到该函数标示符。如果编译器在解析重载函数时,解析结果为 deleted 函数,则会出现编译错误。例如:清单 16
#include <iostream> using namespace std; int add(int,int) = delete; double add(double a,double b){ return a+b; } int main(){ cout << add(1,3) << endl; // 错误,调用了 deleted 函数 add(int, int) cout << add(1.2,1.3) << endl; return 0; } 待补充。。。