目录括号内为适合人群,所有库作者的内容暂不做学习,可自行查阅《深入理解C++11:C++11新特性解析与应用》。网盘链接: https://pan.baidu.com/s/1Jf29R7-foOoXJ5UW3mTKVA 密码: 7vgq
目录
1.继承构造函数(类作者)
2.委派构造函数(类作者)
3.右值引用:移动语义和完美转发(类作者)
①指针成员与拷贝构造
②移动语义
③左值,右值与右值引用
④std::move:强制转化为右值
⑤移动语义的一些其他问题
⑥完美转发
4.显式转换操作符(库作者)
5.列表初始化(所有人)
①初始化列表
②防止类型收窄
6.POS类型(部分人)
7.非受限联合体(部分人)
8.用户自定义字面量(部分人)
9.内联名字空间(部分人)
10.模板的别名(部分人)
11.一般化的SFINEA规则(库作者)
1.继承构造函数 ^
在C++98标准中,如果一个类有多个构造函数,则其派生类需要一一构造相应的构造函数,如:
class A
{
public:
A() {}
A(int i, int j, int o) { a = i; b = j; c = o; }
A(A& _a) { a = _a.a; b = _a.b; c = _a.c; }
void outPut() { cout << a << " " << b << " " << c << endl; }
private:
int a{ 1 };
int b{ 2 };
int c{ 3 };
};
class B:public A
{
public:
B() :A() {}
B(int i, int j, int o, int p) :A(i, j, o), d(p) {}
B(B& _b) :A(_b), d(_b.d) {}
private:
int d { 4 };
};
上述的写法无疑是十分不方便的,所以在C++11标准中,有一个好用的规则,即可以通过using声明来完成,如:
class A
{
public:
A() {}
A(int i, int j, int o) { a = i; b = j; c = o; }
A(A& _a) { a = _a.a; b = _a.b; c = _a.c; }
void outPut() { cout << a << " " << b << " " << c << endl; }
private:
int a{ 1 };
int b{ 2 };
int c{ 3 };
};
class B:public A
{
public:
using A::A;//继承构造函数
private:
int d { 4 };//类成员的初始化表达式
};
不过继承构造函数只会初始化基类中的成员变量,对于派生类中的成员变量,则需要类成员的初始化表达式。
有的时候,基类构造函数的参数会有默认值,对于继承构造函数来讲,参数的默认值是不会被继承的(??VS2017中还是继承了默认值),事实上,默认值会导致基类产生多个构造函数的版本,这些函数版本都会被派生类继承。如:
class A
{
public:
A(int i = 5, double = 2.3) {}
};
class B:public A
{
public:
using A::A;
};
//B中的构造函数可能有如下:
//B(int ,double );继承构造函数
//B(int);减少一个参数后的继承构造函数
//B(const B&);复制构造函数,这不是继承来的
//B();默认构造函数
可以看出,参数默认值会导致多个构造函数版本的产生,因此我们在使用有参数默认值的构造函数的基类的时候,必须小心。
当派生类有多个基类时,会出现继承构造函数“冲突”的情况。多个基类中的部分构造函数可能导致派生类中的继承构造函数的函数名,参数都相同,那么继承类中的冲突的继承构造函数将导致不合法的派生类代码,如:
class A
{
A(){}
A(int){}
}
class B
{
B(){}
B(int){}
}
class C:public A,public B
{
using A::A;
using B::B; //编译器将报错
}
上述代码中,A和B的构造函数会导致C中重复定义相同类型的继承构造函数。这种情况下,我们可以通过显式定义继承类的冲突的构造函数,阻止隐式生成相应的继承构造函数来解决冲突。如:
class C:public A,public B
{
using A::A;
using B::B;
C(int){} //即冲突的继承构造函数自己重新声明定义一个,不使用基类的这个构造函数,然后继承其他不冲突的构造函数
}
注:如果基类的构造函数被声明为私有成员函数,或者派生类是从基类中虚继承的,那么就不能够在派生类中声明继承构造函数。另外,如果一旦使用了继承构造函数,编译器就不会为派生类生成默认构造函数。
2.委派构造函数 ^
与继承构造函数类似的,委派构造函数也是C++11中对C++的构造函数的一项改进,其目的是为了减少我们书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写将更加容易。首先来看看代码冗余的例子:
class A
{
public:
A(){ doSomething(); }
A(int i):num(i) { doSomething(); }
A(char c):ch(c) { doSomething(); }
private:
void doSomething(){/*其他初始化*/}
int num { 1 };
char ch { 'c' };
};
上述代码中,每个构造函数都需要调用doSomething函数进行初始化,而现实编程中,构造函数中的代码还会更长。为此,在C++11中,我们可以使用委派构造函数来达到期望的效果。所谓委派构造,就是指委派函数将构造的任务委派给了目标构造函数来完成这样一种类构造的方式。通过将一个构造函数设定为”基准版本”,调用这个”基准版本”的构造函数为委派构造函数,而被调用的”基准版本”则为目标构造函数。如:
class A
{
public:
//下面三个构造函数都是委派构造函数
A():A(10,'c') {} //通过目标构造函数进行初始化
A(int i) :A(i, 'a') {} //通过目标构造函数进行初始化
A(char j) :A(10, j) {} //通过目标构造函数进行初始化
private:
A(int i, char j) :n(i),ch(j) { cout << "目标构造函数" << endl; }//目标构造函数
int n;
char ch;
};
需要注意的是,构造函数不能同时“委派”和使用初始化列表,一般通过目标构造函数进行初始化。因为目标构造函数的执行总是先于委派构造函数,所以如果将数据成员放在函数体内初始化,在部分情况下会出现意料之外的结果。
在构造函数比较多的时候,我们可能会拥有不止一个委派构造函数,而一些目标构造函数也可能是委派构造函数,因此,我们可能在委派构造函数中形成链状的委派构造关系。如:
class A
{
public:
A():A(10) {} //委派构造函数
A(int i) :A(i, 'a') {} //既是目标构造函数,又是委派构造函数
A(char j) :A(10, j) {}
void outPut() { cout << n << " " << ch << endl; }
private:
A(int i, char j) :n(i),ch(j) { cout << "目标构造函数" << endl; }//目标构造函数
int n{ 10 };
char ch{ 'c' };
};
注:在委派构造的链状关系中,不能形成委托环,即构造函数①委托②,②又委托③,③委托①,形成死循环
委派构造的一个很实际的应用就是使用构造模板函数产生目标构造函数,如下所示:
#include<iostream>
#include<list>
#include<vector>
#include<deque>
using namespace std;
class A
{
public:
A(vector<int> &v) :A(v.begin(), v.end()) {}
A(deque<int> &d) :A(d.begin(), d.end()) {}
void outPut() { for (int& lis : l)cout << lis << " "; }//遍历容器输出所有元素
private:
template <class T> A(T first, T last) :l(first, last) {}
list<int> l;
};
int main()
{
vector<int> ivec;
for (int i = 0; i != 10; ++i)
ivec.push_back(i);
A a(ivec);
a.outPut();//输出:0 1 2 3 4 5 6 7 8 9
}
在异常处理方面,如果在委派构造函数中使用try的话,那么从目标构造函数中产生的异常,都可以在委派构造函数中被捕捉到。具体例子可查看原书籍p67
3.右值引用:移动语义和完美转发 ^
①指针成员与拷贝构造 ^
当类中包含了一个指针成员时,要特别小心拷贝构造函数的编写,因为一不小心,就会出现内存泄露,例:
class A
{
public:
A() {}
A(const int& a) :p(new int(a)){}
~A(){ delete p;}
private:
int *p{ nullptr };
};
int main()
{
int b = 5;
A aa(b);
A ab(aa);//浅拷贝,由编译器隐式生成
}
上述例子中,由于没有拷贝构造函数,所以将由编译器隐式生成一个拷贝构造函数,这种拷贝构造函数将进行“浅拷贝”,即两个类中的指针都指向了同一块堆内存地址,所以在main作用域结束的时候,当其中之一完成祈构后,另一个类中的指针就变成了一个“悬挂指针”,也称为野指针,指向非法的内存地址,所在对这个指针释放内存就会造成严重的错误。
解决该方法的最佳方案就是我们自定义拷贝构造函数来实现”深拷贝”,如:
class A
{
public:
A() {}
A(const int& a) :p(new int(a)){}
A(const A& a) :p(new int(*a.p)) {}//深拷贝
~A(){ delete p;}
private:
int *p{ nullptr };
};
所谓“深拷贝“,便是自定义拷贝构造函数,从堆中分配新内存,从而避免悬挂指针的困扰。
②移动语义 ^
在某种特定的情况下,需要返回一个临时变量且赋值给类对象,或者调用拷贝构造函数构造一个对象的开销过大。这时,临时变量的创建与销毁会造成不必要的开销,。如:
class A
{
public:
A() :p(new int(3)) {}
A(const A& a) :p(new int(*a.p)) {}//拷贝构造函数
~A() {delete p;}
private:
int *p;
};
A getA() { A a = A(); return a; }//返回临时变量
int main()
{
A b= getA();//调用b的拷贝构造函数
}
在C++11中,我们可以用移动构造函数进行处理,将临时变量中的资源“偷走”,这样的”偷“行为,称之为“移动语义”。如下所示:
class A
{
public:
A() :p(new int(3)) {}
A( A&& a) :p(a.p) { a.p=nullptr;}//移动构造函数
~A() {delete p;}
private:
int *p;
};
A getA() { A a = A(); return a; }//返回临时变量
int main()
{
A b= getA();//调用b的移动构造函数
}
所谓的移动构造函数,即让类成员指针指向临时变量所指向的内存地址,然后让临时变量的指针赋为指针空值nullptr,这样做是为了避免祈构函数释放了这块内存。移动构造函数接受一个“右值引用”的参数。
③左值,右值与右值引用 ^
左右值最为典型的判别方法就是,在赋值表达式中,出现在等号左边的就是左值,而在等号右边的即为右值。
另一种被广泛认同的判别方法,就是可以取地址的,有名字的就是左值,反之,不能取地址的,没有名字的就是右值。
在C++11标准中,右值是由两个概念构成的,一个是将亡值,另一个则是纯右值。其中,纯右值是C++98标准中右值的概念,讲的是用于辨识临时变量和一些不跟对象关联的值。如下:
- 非引用返回的函数返回的临时变量值就是一个纯右值。
- 一些运算表达式,如1+3产生的临时变量值也是纯右值。
- 而不跟对象关联的字面量值,如:2,‘c’,true,都是纯右值。
- 类型转换函数的返回值,lambda表达式等,也都是右值。
-
而将亡值则是C++11新增的跟右值引用相关的表达式,这样的表达式通常是将要被移动的对象(移为他用),如下:
- 返回右值引用T&&的函数返回值
- sta::move的返回值
- 转换为T&&的类型转换函数的返回值
-
剩余的,可以标识函数,对象的值都属于左值。在C++11中,所有的值必属于左值,将亡值,纯右值三者之一。
在C++11中,右值引用就是对一个右值进行引用的类型。不过由于右值通常不具有名字,我们只能通过引用的方式找到它的存在,通常情况下,我们只能从右值表达式获得其引用。如:
T && a =returnValue();
为了区分C++98中的引用类型,称C++98中的引用为“左值引用”。右值引用和左值引用都是属于引用类型,无论声明哪一个都必须立即进行初始化。总之一句话,使用引用类型,某些情况下可以减少临时对象的开销。
具体的解释查阅原书籍P76,讲解的比较详细
④std::move:强制转化为右值 ^
在C++11中,标准库在utility头文件中提供了一个有用的函数std::move,它唯一的功能就是将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。
被转化的左值,其生命周期并没有随着左右值的转化而改变。如:
class A { public: A():p(new int(3)) {} A(const A& a):p(new int(*a.p)) {} A(A && a) :p(a.p) { a.p = nullptr; } ~A(){delete p;} int *p; }; int main() { A a; A b(move(a)); //调用移动构造函数 cout << *a.p<< endl;//运行时错误,因为是空指针 }
使用了move后,我们应该要知道,a的值已经不能再使用了。为了保证移动语义的传递,我们在编写移动构造函数的时候,应该总是记得使用std::move转换拥有形如堆内存,文件句柄等资源的成员为右值。这样一来,如果成员支持移动构造的话,就可以实现其移动语义。而即使成员没有移动构造函数,那么接受常量左值的构造函数版本也会轻松地实现拷贝构造,因此也不会引起大的问题。
⑤移动语义的一些其他问题 ^
因为移动语义一定是要修改临时变量的值,所以移动构造函数的参数为const修饰的时候,将导致错误,无法实现移动语义。所以在实现移动语义的时候要避免使用不必要的const。
默认情况下,编译器会隐式地生成一个移动构造函数,不过当我们声明了自定义拷贝构造函数,移动构造函数或者祈构函数中的一个或多个的时候,编译器都不会生成默认版本。但默认移动构造函数不足以实现移动语义,所以通常情况下,我们都需要自定义移动构造函数。
移动语义一个比较典型的应用就是可以实现高性能的置换(swap)函数。如下:
template <class T> void swap(T& a,T&b) { T tmp(move(a)); a=move(b); b=move(tmp); }
移动构造函数中的异常处理,是件危险的事情,因为可能移动语义还没完成,一个异常被抛出,就会导致一些指针成为悬挂指针。为此,我们应该尽量编写不抛出异常的移动构造函数。通过添加noexcept关键字,可以保证移动构造函数中抛出来的异常会直接调用terminate终止程序运行。
⑥完美转发 ^
所谓完美转发,是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板调用的另外一个函数。在C++11中,引入了一条所谓的“引用折叠”的新语言规则,并结合新的模板推导规则来完成完美转发。一个完美转发的例子如下:
void fun(int &x) { cout << "lvalue ref" << endl; } void fun(int &&x) { cout << "rvalue ref" << endl; } void fun(const int &x) { cout << "const lvalue ref" << endl; } void fun(const int &&x) { cout << "const rvalue ref" << endl; } template<typename T> void PerfectForward(T &&t) { fun(forward<T>(t)); }//完美转发 int main() { int a; const int b = 1; PerfectForward(a); // lvalue ref PerfectForward(std::move(a)); // rvalue ref PerfectForward(b); // const lvalue ref PerfectForward(std::move(b)); // const rvalue ref }
完美转发的一个作用就是包装函数。
4.显式转换操作符 ^
暂不作学习,可查阅《深入理解C++11:C++11新特性解析与应用》。
5.列表初始化 ^
①初始化列表 ^
在C++11标准中,可以通过使用花括号进行初始化,如:
int a{5+6}; vector<int> ivec{ 1,2,3,4,5,6,7,8,9 };
目前,我们可以使用以下几种形式完成初始化的工作:
- 等号“=”加上赋值表达式,如int a=3+4;
- 等号“=”加上花括号的初始化列表,比如int a={3 + 4};
- 圆括号式的表达式列表,比如int a(3+4);
- 花括号式的初始化列表,比如 int a{3+4};
-
对于在自定义的类中使用列表初始化,我们只需要声明一个以initialize_list<T>模板类(在命名空间std里)为参数的构造函数,同样可以使得自定义的类使用列表初始化。
我们可以在[]符号中使用列表,也可以将初始化列表用于函数返回的情况。
②防止类型收窄 ^
类型收窄一般是指一些可以使得数据变化或精度丢失的隐式类型转换,使用列表初始化的一个最大优势就是可以防止类型收窄。可能导致类型收窄的典型情况如下:
- 从浮点数隐式地转化为整型数。比如int a=1.2;这里a实际保存的值为整数1,可以视为类型收窄
- 从高精度的浮点数转为低精度的浮点数
- 从整型转化为浮点型,如果整型数大到浮点数无法精确地表示,则也可以视为类型收窄。
- 从整型转化为较低长度的整型。
-
在C++11中,使用初始化列表进行初始化的数据编译器是会检查其是否发生类型收窄的。列表初始化也是唯一一种可以防止类型收窄的初始化方式。
6.POD类型 ^
POD是英文中Plain Old Data的缩写。POD在C++中时非常重要的一个概念,通常用于说明一个类型的属性,尤其是用户自定义类型的属性。POD属性在C++11中往往又是构造其他C++概念的基础。C++11将POD划分为两个基本概念的合集,即:平凡的(trivial)和标准布局的(standard layout)
一个平凡的类或结构体应该符合以下定义:
- 拥有平凡的默认构造函数和祈构函数
- 拥有平凡的拷贝构造函数和移动构造函数
- 拥有平凡的拷贝赋值运算符和移动赋值运算符
- 不能包含虚函数以及虚基类
- 注:这里的平凡,指的是编译器隐式生成默认构造函数,祈构函数,拷贝构造函数等。而自定义的这些函数则可以使用=default关键字来显式地声明为平凡类型。
标准布局的类或结构体应该符合以下定义:
- 所有非静态成员有相同的访问权限
- 在类或结构体继承时,满足以下两种情况之一:
-
①派生类中有非静态成员,且只有一个仅包含静态成员的基类
②基类有非静态成员,而派生类没有非静态成员
- 类中第一个非静态成员的类型与其基类不同
- 没有虚函数和虚基类
- 所有非静态数据成员均符合标准布局类型,其基类也符合标准布局,这是一个递归的定义。
-
在C++11中,我们可以通过一些辅助的类模板来帮我们进行以上属性的判断:
is_pod<T>::value可以用来判断T的类型是否是POD类型 is_trivial<T>::value可以用来判断T的类型是否是一个平凡的类型 is_standard_layout<T>::value可以用来判断T的类型是否是一个标准布局的类型
使用POD的好处:
- 字节赋值,我们可以使用memset和memcpy对POD类型进行初始化和拷贝操作
- 提供对C内存布局兼容
- 保证了静态初始化的安全有效
-
7.非受限联合体(union)^
在C++98标准中,并不是所有的数据类型都能够成为联合体的数据成员。而在C++11标准中,取消了联合体对于数据成员类型的限制。标准规定,任何非引用类型都可以成为联合体的数据成员,这样的联合体即所谓的非受限联合体
C++98标准中,标准规定了联合体会自动对未在初始化成员列表中出现的成员赋默认初值,这种初始化会带来疑问,因为在任何时刻只有一个成员可以是有效的。而在C++11标准中,为了减少这样的疑问,标准会默认删除一些非受限联合体的默认构造函数。
8.用户自定义字面量 ^
在C++11中,标准要求声明字面量操作符有一定的规则,该规则跟字面量的“类型”密切相关,具体如下:
- 如果字面量为整型数,那么字面量操作符函数只可接受unsigned long long 或 const char* 为其参数。当unsigned long long 无法容纳该字面量的时候,编译器会自动将该字面量转化为以’\0’为结束符的字符串,并调用以const char* 为参数的版本进行处理。
- 如果字面量为浮点整数,则字面量操作符函数只可接受long double或者const char* 为参数。const char*调用规则同整型的一样
- 如果字面量为字符串,则字面量操作符函数只可接受const char* ,size_t为参数(已知长度的字符串)
- 如果字面量为字符,则字面量操作符函数只可接受一个char为参数
-
注:在字面量操作符函数的声明中,operator “”与用户自定义后缀之间必须有空格
注:后缀建议以下划线开始。以非下划线开始的后缀,会被编译器警告。如下为一个字面量操作符的一个例子:
#include <iostream> using namespace std; struct RGBA { uint8_t r; uint8_t g; uint8_t b; uint8_t a; RGBA(uint8_t R, uint8_t G, uint8_t B, uint8_t A = 0) :r(R), g(G), b(B), a(A) {} }; RGBA operator""_C(const char* col ,size_t n) { const char *p = col; const char *end = col + n; const char *r, *g, *b, *a; r = g = b = a = nullptr; for (; p != end; ++p) { if (*p == 'r') r = p; else if (*p == 'g') g = p; else if (*p == 'b') b = p; else if (*p == 'a') a = p; } //这里的atoi函数是用来把字符串转换成整型的。跳过前面的空白字符,直到遇到数字或正负符号才开始做转换,遇到非数字或字符串结束符'\0'才结束转换 if ((r == nullptr) || (g == nullptr) || (b == nullptr)) throw; else if (a == nullptr) return RGBA(atoi(r + 1), atoi(g + 1), atoi(b + 1)); else return RGBA(atoi(r + 1), atoi(g + 1), atoi(b + 1),atoi(a+1)); } ostream& operator<<(ostream& out, const RGBA &col) { return out << "r:" << (int)col.r << " g:" << (int)col.g << " b:" << (int)col.b << " a:" << (int)col.a << endl; } int main() { cout << "r111 g201 b255 a3"_C;//输出:r:111 g:201 b:255 a:3 }
9.内联名字空间 ^
在C++11标准中,引入了“内联的名字空间”,通过关键字“inline namespace ”就可以声明一个内联的名字空间。内联的名字空间允许我们在父名字空间定义或特化子名字空间的模板。如:
namespace A { namespace B { class BB { public: BB() { cout << "这是命名空间B" << endl; } }; } //内联命名空间 inline namespace C { class CC { public: CC() { cout << "这是命名空间C" << endl; } }; } } namespace A { BB b;//因为声明了内联命名空间。所以编译通过 CC c;//并没有声明内联命名空间,编译不通过,需要C::CC c; }
10.模板的别名 ^
在C++中,我们通常使用typedef为类型定义别名,如:typedef int myint;定义了int 的别名myint。当遇到一些比较长的名字,尤其是在使用模板和域的时候,使用别名的优势会更加明显。
在C++11中,我们可以使用using定义类型的别名。如:
using myint=int; is_same<int,myint>::value;//如果类型一致则返回1; template<class T> using myMap = map<T,int>;//模板编程中使用using定义别名,这是typedef无法做到的。 myMap<char> mmap;//实例化
我们可以通过使用C++11标准库中的is_same模板类来帮助我们判断两个类型是否一致
11.一般化的SFINEA规则 ^
暂不作学习,可查阅《深入理解C++11:C++11新特性解析与应用》。