在 C++11 中添加了一个新的关键字 constexpr,这个关键字是用来修饰常量表达式的。所谓常量表达式,指的就是由多个(≥1)常量(值不会改变)组成并且在编译过程中就得到计算结果的表达式。
2、委托构造和继承构造函数 委托构造函数托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化:
#includeusing namespace std; class Test { public: Test() {}; Test(int max) { this->m_max = max > 0 ? max : 100; } Test(int max, int min) { this->m_max = max > 0 ? max : 100; // 冗余代码 this->m_min = min > 0 && min < max ? min : 1; } Test(int max, int min, int mid) { this->m_max = max > 0 ? max : 100; // 冗余代码 this->m_min = min > 0 && min < max ? min : 1; // 冗余代码 this->m_middle = mid < max && mid > min ? mid : 50; } int m_min; int m_max; int m_middle; }; int main() { Test t(90, 30, 60); cout << "min: " << t.m_min << ", middle: " << t.m_middle << ", max: " << t.m_max << endl; return 0; }
三个构造函数中都有重复的代码,在 C++11 之前构造函数是不能调用构造函数的,加入了委托构造之后,我们就可以轻松地完成代码的优化了:
#includeusing namespace std; class Test { public: Test() {}; Test(int max) { this->m_max = max > 0 ? max : 100; } Test(int max, int min):Test(max) { this->m_min = min > 0 && min < max ? min : 1; } Test(int max, int min, int mid):Test(max, min) { this->m_middle = mid < max && mid > min ? mid : 50; } int m_min; int m_max; int m_middle; }; int main() { Test t(90, 30, 60); cout << "min: " << t.m_min << ", middle: " << t.m_middle << ", max: " << t.m_max << endl; return 0; }
在修改之后的代码中可以看到,重复的代码全部没有了,并且在一个构造函数中调用了其他的构造函数用于相关数据的初始化,相当于是一个链式调用。在使用委托构造函数的时候还需要注意一些几个问题:
- 这种链式的构造函数调用不能形成一个闭环(死循环),否则会在运行期抛异常。
- 如果要进行多层构造函数的链式调用,建议将构造函数的调用的写在初始列表中而不是函数体内部,否则编译器会提示形参的重复定义。
Test(int max)
{
this->m_max = max > 0 ? max : 100;
}
Test(int max, int min)
{
Test(max); // error, 此处编译器会报错, 提示形参max被重复定义
this->m_min = min > 0 && min < max ? min : 1;
}
在初始化列表中调用了代理构造函数初始化某个类成员变量之后,就不能在初始化列表中再次初始化这个变量了。
// 错误, 使用了委托构造函数就不能再次m_max初始化了
Test(int max, int min) : Test(max), m_max(max)
{
this->m_min = min > 0 && min < max ? min : 1;
}
继承构造函数
C++11 中提供的继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。先来看没有继承构造函数之前的处理方式:
#include#include using namespace std; class Base { public: Base(int i) :m_i(i) {} Base(int i, double j) :m_i(i), m_j(j) {} Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {} int m_i; double m_j; string m_k; }; class Child : public Base { public: Child(int i) :Base(i) {} Child(int i, double j) :Base(i, j) {} Child(int i, double j, string k) :Base(i, j, k) {} }; int main() { Child c(520, 13.14, "i love you"); cout << "int: " << c.m_i << ", double: " << c.m_j << ", string: " << c.m_k << endl; return 0; }
在子类中初始化从基类继承的类成员,需要在子类中重新定义和基类一致的构造函数,这是非常繁琐的,C++11 中通过添加继承构造函数这个新特性完美的解决了这个问题,使得代码更加精简。
继承构造函数的使用方法是这样的:通过使用 using 类名::构造函数名(其实类名和构造函数名是一样的)来声明使用基类的构造函数,这样子类中就可以不定义相同的构造函数了,直接使用基类的构造函数来构造派生类对象。
#include#include using namespace std; class Base { public: Base(int i) :m_i(i) {} Base(int i, double j) :m_i(i), m_j(j) {} Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {} int m_i; double m_j; string m_k; }; class Child : public Base { public: //这样就可以在子类中直接继承父类的所有的构造函数,通过他们去构造子类对象了。 using Base::Base; //但是如果Child类有了自己的构造函数,就要手工调用父类构造函数了 }; int main() { Child c1(520, 13.14); cout << "int: " << c1.m_i << ", double: " << c1.m_j << endl; Child c2(520, 13.14, "i love you"); cout << "int: " << c2.m_i << ", double: " << c2.m_j << ", string: " << c2.m_k << endl; return 0; }
另外如果在子类中隐藏了父类中的同名函数,也可以通过 using 的方式在子类中使用基类中的这些父类函数:
#include#include using namespace std; class Base { public: Base(int i) :m_i(i) {} Base(int i, double j) :m_i(i), m_j(j) {} Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {} void func(int i) { cout << "base class: i = " << i << endl; } void func(int i, string str) { cout << "base class: i = " << i << ", str = " << str << endl; } int m_i; double m_j; string m_k; }; class Child : public Base { public: using Base::Base; using Base::func; void func() { cout << "child class: i'am luffy!!!" << endl; } }; int main() { Child c(250); c.func(); c.func(19); c.func(19, "luffy"); return 0; }
子类中的 func() 函数隐藏了基类中的两个 func() 因此默认情况下通过子类对象只能调用无参的 func(),在上面的子类代码中添加了 using Base::func; 之后,就可以通过子类对象直接调用父类中被隐藏的带参 func() 函数了。
3、右值引用 右值C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&。
- 左值是指存储在内存中、有明确存储地址(可取地址)的数据;
- 右值是指可以提供数据值的数据(不可取地址);
C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):
- 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
- 将亡值:与右值引用相关的表达式,比如,T&& 类型函数的返回值、 std::move 的返回值等。
右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”:其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
#include#include #include #include using namespace std; int&& value = 520; class Test { public: Test() { cout << "Test()" << endl; } Test(const Test& a) { cout << "Test(const Test& a)" << endl; } ~Test() { cout << "~Test()" << endl; } }; Test getObj() { return Test(); } void test1() { int a1; //int&& a2 = a1; // error,无法将右值引用绑定到左值 //Test& t = getObj(); // error,非常量引用的初始值必须为左值 Test&& t = getObj(); // getObj() 返回的临时对象被称之为将亡值,t 是这个将亡值的右值引用。 const Test& t2 = getObj(); //ok,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。 getchar(); } int main(void) { test1(); system("pause"); return 0; }
运行结果:
代码停留在test1最后一行。我们看到即使getObj函数运行完后,返回的临时变量也没有被析构, 通过右值引用的声明,该临时对象又“重获新生”,生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
性能优化在 C++ 中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。
#include#include #include #include using namespace std; class Test { public: Test() : m_num(new int(100)) { cout << "Test()" << endl; } Test(const Test& a) : m_num(new int(*a.m_num)) { cout << "Test(const Test& a)" << endl; } ~Test() { cout << "~Test()" << endl; delete m_num; } int* m_num; }; Test getObj() { Test tmp; return tmp; } void test1() { Test t = getObj(); cout << "t.m_num: " << *t.m_num << endl; getchar(); } int main(void) { test1(); system("pause"); return 0; }
通过输出的结果可以看到调用 Test t = getObj(); 的时候调用拷贝构造函数对返回的匿名对象进行了深拷贝,得到了对象 t。在 getObj() 函数中创建的对象tmp,虽然进行了内存的申请操作,但是没有使用就释放掉了。
如果能够使用临时对象tmp,已经申请的资源,既能节省资源,还能节省资源申请和释放的时间,如果要执行这样的操作就需要使用右值引用了
右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象,这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高 C++ 应用程序的性能。
#include#include #include #include using namespace std; class Test { public: Test() : m_num(new int(100)) { cout << "Test()" << endl; } Test& operator=(const Test& obj) { cout << "Test& operator=" << endl; if (!m_num) delete m_num; m_num = new int(*obj.m_num); return *this; } Test(const Test& a) : m_num(new int(*a.m_num)) { cout << "Test(const Test& a)" << endl; } // 添加移动构造函数 Test(Test&& a) : m_num(a.m_num) //首先先接管传进来的右值代表的那片堆空间 { a.m_num = nullptr; //让 右值a 的那片堆空间置空 cout << "Test(Test&& a)" << endl; } ~Test() { cout << "~Test()" << endl; delete m_num; } int* m_num; }; Test getObj() { Test tmp; //调用无参Test无参构造函数 return tmp; //利用tmp构造返回的匿名对象(没有移动构造函数时) //函数返回后,tmp析构 } void test1() { Test t = getObj(); //返回的匿名对象进行赋值操作,然后匿名对象析构 cout << "t.m_num: " << *t.m_num << endl; getchar(); } int main(void) { test1(); system("pause"); return 0; }
通过修改,在上面的代码给 Test 类添加了移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj(); 操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对返回的匿名对象进行深拷贝,提高了性能。
&&的特性在 C++ 中,并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中。
如果是模板参数需要指定为 T&&,如果是自动类型推导需要指定为 auto &&,在这两种场景下 && 被称作未定的引用类型。另外还有一点需要额外注意 const T&& 表示一个右值引用,不是未定引用类型。
templatevoid f(T&& param); void f1(const T&& param); f(10); int x = 10; f(x); f1(x);
- 第 4 行中,对于 f(10) 来说传入的实参 10 是右值,因此 T&& 表示右值引用
- 第 6 行中,对于 f(x) 来说传入的实参是 x 是左值,因此 T&& 表示左值引用
- 第 7 行中,f1(x) 的参数是 const T&& 不是未定引用类型,不需要推导,本身就表示一个右值引用
int main()
{
int x = 520, y = 1314;
// 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&&
// 或者 auto&& 得到的是一个左值引用类型
auto&& v1 = x; // auto&& 表示一个整形的左值引用,用左值x推导auto&&
//通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型
auto&& v2 = 250; //auto&& 表示一个整形的右值引用,右值推导auto&&
// decltype(x)&& 等价于 int&& 是一个右值引用不是未定引用类型,
// y 是一个左值,不能使用左值初始化一个右值引用类型。
decltype(x)&& v3 = y; // error
cout << "v1: " << v1 << ", v2: " << v2 << endl;
return 0;
};
由于上述代码中存在 T&& 或者 auto&& 这种未定引用类型,当它作为参数时,有可能被一个右值引用初始化,也有可能被一个左值引用初始化,在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠。在 C++11 中引用折叠的规则如下:
- 通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型
- 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 或者 auto&& 得到的是一个左值引用类型
int&& a1 = 5; //a1是右值引用 auto&& bb = a1; //使用右值引用(非右值)推导auto&&,bb 为左值引用类型 auto&& bb1 = 5; // 5 为右值,推导出的 bb1 为右值引用类型 int a2 = 5; int &a3 = a2; auto&& cc = a3; //a3 为左值引用,推导出的 cc 为左值引用类型 auto&& cc1 = a2; //a2 为左值,推导出的 cc1 为左值引用类型 const int& s1 = 100; const int&& s2 = 100; auto&& dd = s1; //s1 为常量左值引用,推导出的 dd 为常量左值引用类型 auto&& ee = s2; //s2 为常量右值引用,推导出的 ee 为常量左值引用类型 const auto&& x = 5; //x 为右值引用,不需要推导
#includeusing namespace std; void printValue(int &i) { cout << "l-value: " << i << endl; } void printValue(int &&i) { cout << "r-value: " << i << endl; } void forward(int &&k) { //编译器会根据传入的参数的类型(左值还是右值)调用对应的重置函数(printValue) printValue(k); } int main() { int i = 520; printValue(i); //void printValue(int &i) printValue(1314); //void printValue(int &&i) forward(250); //orward () 接收的是一个右值,但是在这个函数中调用函数 printValue () 时, // 参数 k 变成了一个命名对象,编译器会将其当做左值来处理。 return 0; };
编译器会将已命名的右值引用(上面的k)视为左值,将未命名的右值引用视为右值。
4、转移和完美转发 move在 C++11 添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化一个右值引用需要借助 std::move () 函数。
使用std::move方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。
class Test
{
public:
Test(){}
......
}
int main()
{
Test t;
Test && v1 = t; // error
Test && v2 = move(t); // ok
list ls;
ls.push_back("hello");
ls.push_back("world");
......
list ls1 = ls; // 需要拷贝, 效率低
list ls2 = move(ls); //
return 0;
}
如果不使用 std::move,拷贝的代价很大,性能较低。使用 move 几乎没有任何代价,只是转换了资源的所有权。如果一个对象内部有较大的堆内存或者动态数组时,使用 move () 就可以非常方便的进行数据所有权的转移。另外,我们也可以给类编写相应的移动构造函数(T::T(T&& another))和和具有移动语义的赋值函数(T&& T::operator=(T&& rhs)),在构造对象和赋值的时候尽可能的进行资源的重复利用,因为它们都是接收一个右值引用参数。
forward右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值(编译器会将已命名的右值引用视为左值),并不是原来的类型了。
如果需要按照参数原来的类型转发到另一个函数,可以使用 C++11 提供的 std::forward () 函数,该函数实现的功能称之为完美转发。
// 函数原型 templateT&& forward (typename remove_reference ::type& t) noexcept; template T&& forward (typename remove_reference ::type&& t) noexcept; // 精简之后的样子 std::forward (t);
- 当T为左值引用类型时,t将被转换为T类型的左值
- 当T不是左值引用类型时,t将被转换为T类型的右值
#includeusing namespace std; template void printValue(T& t) { cout << "l-value: " << t << endl; } template void printValue(T&& t) { cout << "r-value: " << t << endl; } template void testForward(T && v) { printValue(v); printValue(move(v)); printValue(forward (v)); cout << endl; } int main() { testForward(520); int num = 1314; testForward(num); testForward(forward (num)); testForward(forward (num)); testForward(forward (num)); return 0; }
打印结果如下:
l-value: 520
r-value: 520
r-value: 520l-value: 1314
r-value: 1314
l-value: 1314l-value: 1314
r-value: 1314
r-value: 1314l-value: 1314
r-value: 1314
l-value: 1314l-value: 1314
r-value: 1314
r-value: 1314
- testForward(520); 函数的形参为未定引用类型 T&&,实参为右值(520),初始化后被推导为一个右值引用。
- printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值
-
printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值
-
printValue(forward
(v));forward 的模板参数为右值引用,最终得到一个右值,实参为 ``右值`
testForward(num); 函数的形参为未定引用类型 T&&,实参为左值,初始化后被推导为一个左值引用
- printValue(v); 实参为左值
- printValue(move(v)); 通过 move 将左值转换为右值,实参为右值
- printValue(forward
(v));forward 的模板参数为左值引用,最终得到一个左值引用,实参为左值
testForward(forward
- printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值
- printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值
- printValue(forward
(v)); forward 的模板参数为右值引用,最终得到一个右值,实参为右值
testForward(forward
- printValue(v); 实参为左值
- printValue(move(v)); 通过 move 将左值转换为右值,实参为右值
- printValue(forward
(v)); forward 的模板参数为左值引用,最终得到一个左值,实参为左值
testForward(forward
- printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值
- printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值
- printValue(forward
(v));forward 的模板参数为右值引用,最终得到一个右值,实参为右值
在 C++ 的 STL 容器中,可以进行任意长度的数据的初始化。
使用初始化列表也只能进行固定参数的初始化,如果想要做到和 STL 一样有任意长度初始化的能力,可以使用 std::initializer_list 这个轻量级的类模板来实现。
- 它是一个轻量级的容器类型,内部定义了迭代器 iterator 等容器必须的概念,遍历时得到的迭代器是只读的
- 对于 std::initializer_list
而言,它可以接收任意长度的初始化列表,但是要求元素必须是同种类型 T - 在 std::initializer_list 内部有三个成员接口:size(), begin(), end()。
- std::initializer_list 对象只能被整体初始化或者赋值。
#include#include using namespace std; void traversal(std::initializer_list a) { for (auto it = a.begin(); it != a.end(); ++it) { cout << *it << " "; } cout << endl; } int main(void) { initializer_list list; cout << "current list size: " << list.size() << endl; traversal(list); list = { 1,2,3,4,5,6,7,8,9,0 }; cout << "current list size: " << list.size() << endl; traversal(list); cout << endl; list = { 1,3,5,7,9 }; cout << "current list size: " << list.size() << endl; traversal(list); cout << endl; // 直接通过初始化列表传递数据 // traversal({ 2, 4, 6, 8, 0 }); cout << endl; traversal({ 11,12,13,14,15,16 }); cout << endl; return 0; }
结果:
current list size: 0 current list size: 10 1 2 3 4 5 6 7 8 9 0 current list size: 5 1 3 5 7 9 2 4 6 8 0 11 12 13 14 15 16
- std::initializer_list拥有一个无参构造函数,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list
- 因为在遍历这种类型的容器的时候得到的是一个只读的迭代器,因此我们不能修改里边的数据,只能通过值覆盖的方式进行容器内部数据的修改。虽然如此,在效率方面也无需担心,
- std::initializer_list的效率是非常高的,它的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了初始化列表中元素的引用。
#include6、using的使用 定义别名#include #include using namespace std; class Test { public: Test(std::initializer_list list) { for (auto it = list.begin(); it != list.end(); ++it) { cout << *it << " "; m_names.push_back(*it); } cout << endl; } private: vector m_names; }; int main(void) { Test t({ "jack", "lucy", "tom" }); Test t1({ "hello", "world", "nihao", "shijie" }); return 0; }
typedef 旧的类型名 新的类型名; // 使用举例 typedef unsigned int uint_t; using 新的类型 = 旧的类型; // 使用举例 using uint_t = int; // 使用typedef定义函数指针 typedef int(*func_ptr)(int, double); // 使用using定义函数指针 using func_ptr1 = int(*)(int, double);模板的别名
使用 typedef 重定义类似很方便,但是它有一点限制,比如无法重定义一个模板
templatetypedef map type; // error, 语法错误
使用 typename 不支持给模板定义别名,这个简单的需求仅通过 typedef 很难办到,需要添加一个外敷类:
#include#include #include
在 C++11 中,新增了一个特性就是可以通过使用 using 来为一个模板定义别名,对于上面的需求可以写成这样:
templateusing mymap = map ;
#include#include #include
最后在强调一点:using 语法和 typedef 一样,并不会创建出新的类型,它们只是给某些类型定义了新的别名。using 相较于 typedef 的优势在于定义函数指针别名时看起来更加直观,并且可以给模板定义别名。



