1. 继承构造函数
一旦使用了继承构造函数,那么编译器就不会为派生类生成默认构造函数了。
struct A { A(int){} };
struct B: A { using A::A };
B b; // error , 没有默认无参构造函数
继承构造函数的出现是为了解决:基类有多个构造函数(不同参数)时,派生类仅仅要增加一个参数,却要“透传”拷贝多个基类构造函数,示例如下:
struct A {
A(int i) {}
A(double d, int i) {}
A(float f, int i, const char* c) {}
// ...
};
struct B : A {
B(int i): A(i) {}
B(double d, int i) : A(d, i) {}
B(float f, int i, const char* c) : A(f, i, c){}
// ...
virtual void ExtraInterface(){}
};
使用using 继承构造函数,则可以改造上诉代码
struct A {
A(int i) {}
A(double d, int i) {}
A(float f, int i, const char* c) {}
// ...
};
struct B : A {
using A::A; // 继承构造函数
// ...
virtual void ExtraInterface(){}
};
C++11 标准继承构造函数被设计为跟派生类中的默认函数(默认构造,析构,拷贝构造等)一样,是隐式声明的。这意味着如果一个继承构造函数不被相关代码使用,编译器不会为其产生真正的函数代码。
不过继承构造只会初始化基类的成员变量,对于派生类中的成员变量则无能为力。
struct A {
A(int i) {}
A(double d, int i) {}
A(float f, int i, const char* c) {}
// ...
};
struct B : A {
using A::A;
int d {0};
};
int main() {
B b(356); // b.d被初始化为0
}
有时,基类构造函数参数会有默认值,对于继承构造函数来讲,参数的默认值是不会被继承的。事实上,默认值会导致基类产生多个构造函数的版本,这些函数版本都会被派生类继承。
struct A {
A(int a = 3, double = 2.4) {}
};
struct B : A {
using A::A;
};
事实上,A会产生如下构造函数:
- A(int a = 3, double = 2.4)
- A(int a = 3)
- A(const A&) 默认的复制构造
- A()
相应的,B中的构造函数也会包括如下:
- B(int , double) 继承构造
- B(int) 继承构造
- B(const B&) 复制构造,不是继承来的
- B() 默认构造
2. 委派构造函数
委派构造函数:委派函数将构造的任务给了目标构造函数来完成类的构造的方式。
解决的问题:有多个参数表不同但是逻辑相近(或者有公共部分)的构造函数的时候,一个逻辑写好几遍造成代码重复
注意:委派构造函数不能有初始化列表,只能将成员函数放在结构体内赋值。不能既委派又初始化列表同时使用。
代码冗余的示例:
class Info{
public:
Info():type(1),name('a'){InitRest();}
Info(int i):type(i),name('a'){InitRest();}
Info(char e):type(1),name(e){InitRest();}
private:
void InitRest(){/*其他初始化*/}
int type;
char name;
//...
};
优化后如下:
class Info{
public:
Info():Info(1){}//委派构造函数
Info(int i):Info(i,'a'){}//既是目标构造函数,也是委派构造函数
Info(char e):Info(1,e){}
private:
Info(int i,char e):type(i),name(e){/*其他初始化*/}//目标构造函数
int type;
char name;
//...
};
需要注意,在链式委托构造函数中,不能出现委托环,如下是编译不过的
struct Rule2{
int i,c;
Rule2():Rule2(2){}
Rule2(int i):Rule2('c'){}
Rule2(char c):Rule2(2){}
};
3. 右值引用
3.1 指针成员与拷贝构造
浅拷贝示例:
class HasPtrMem{
public:
HasPtrMem() : d(new int(0)){}
~HasPtrMem(){
delete d;
d = nullptr;
}
int* d;
};
int main(){
HasPtrMem a;
HasPtrMem b(a);
cout << *a.d << endl; // 0
cout << *b.d << endl; // 0
} // 异常析构
就是a.d和b.d都指向了同一块堆内存。因此在main作用域结束的时候,对象b和对象a的析构函数会分别依次被调用。当其中之一完成析构之后(比如对象b先析构,b.d先被delete),那么a.d就成了一个“悬挂指针”(dangling pointer),因为其不再指向有效的内存了。
class HasPtrMem{
public:
HasPtrMem() : d(new int(0)){}
HasPtrMem(const HasPtrMem& h) : d(new int(*h.d)) { } // 拷贝构造函数,从堆中分配内存,并用*h.d初始化
~HasPtrMem(){
delete d;
d = nullptr;
}
int* d;
};
为类HasPtrMem添加了一个拷贝构造函数。拷贝构造函数从堆中分配新内存,将该分配来的内存的指针交还给d,又使用*(h.d)对*d进行了初始化。通过这样的方法,就避免了悬挂指针的困扰。
3.2 移动语义
先说点背景知识,调用复制构造函数的三种情况:
1.当用类一个对象去初始化另一个对象时。
2.如果函数形参是类对象。
3.如果函数返回值是类对象,函数执行完成返回调用时。
拷贝构造函数又称复制构造函数。
拷贝赋值:将一个类对象一模一样的复制给另一个类对象。注意此时发生在对象到对象之间,两个对象已经创建。
拷贝构造:发生在对象构造期间,即是在对象创建时发生的动作。
String s1(“hello”);
String s2;
s2 = s1;//调用拷贝赋值
#include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem():d(new int(0)){
cout<<"Construct: "<<++n_cstr<<endl;
}
HasPtrMem(const HasPtrMem &h):d(new int(*h.d)){
cout<<"Copy construct: "<<++n_cptr<<endl;
}
~HasPtrMem(){
cout<<"Destruct: "<<++n_dstr<<endl;
}
int*d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
};
int HasPtrMem::n_cstr=0;
int HasPtrMem::n_dstr=0;
int HasPtrMem::n_cptr=0;
HasPtrMem GetTemp(){
cout<<"Enter GetTemp"<<endl;
return HasPtrMem();
}
int main(){
HasPtrMem a = GetTemp();
}
如果默认开启了返回值优化(ROV - Return Value Optimization)则返回
Construct: 1
Destruct: 1
去掉返回值优化,需要编译的时候加上选项 -fno-elide-constructors
Enter GetTemp
Construct: 1 // HasPtrMem() 调用构造函数生成对象
Copy construct: 1 // 调用拷贝构造函数 将1步生成的对象拷贝生成临时对象
Destruct: 1 // 析构第一步生成的对象
Copy construct: 2 // 将拷贝生成的临时对象 拷贝到main函数中局部对象
Destruct: 2 // 析构第二步生成的对象
Destruct: 3 // 析构main函数中对象
如果构造一个移动构造函数,则上诉代码会调用移动构造函数替代 拷贝构造函数
HasPtrMem(HasPtrMem&&h):d(h.d){//移动构造函数
h.d=nullptr;//将临时值的指针成员置空
cout<<"Move construct:"<<++n_mvtr<<endl;
}
在C++11中,这样的“偷走”临时变量中资源的构造函数,就被称为“移动构造函数”。而这样的“偷”的行为,则称之为“移动语义”(move semantics)。当然,换成白话的中文,可以理解为“移为己用
3.3 左值、右值、左值引用和右值引用
左值是:locator value(可寻址的数据)
++x;
y*=33;
右值是:read value(不可寻址的数据或用来读的数据)
x++;
y+3;
123;
//y+3这个值会被计算,但是没有被承接到,
//后来即便我们再用y+3去获得这个临时的值,大小是一样的,
//但不是我上次计算的那个值,我又经过了以此计算的到的。
//x++;这个从底层去分析:
//x++会产生一个临时变量,用来保存x+1的值,
//等到语句结束,将x+1赋值给x.
//但是语句没结束时,这个临时变量时在寄存器中保存的,一个计算结果的临时变量,
//此时是不可寻址的!!即右值。
什么是引用?
引用表示为符号“&”;
引用就是用另外的名称来索引到该变量。
左值引用
左值引用得到的就是还是一个左值。
// 以下几个是对上面左值的左值引用
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;
右值引用
右值引用操作符为 “&&”;
右值引用得到的是一个左值。
右值引用通常将一个临时变量拿过来用。
右值引用最主要的功能是解决的是自定义类重复构造冗余的问题。
右值引用就是把右值变成左值,通常实在C++返回值上,对于自定子类的重复拷贝做了重要改善,大大提高了C++的效率。右值引用的概念是C++中的重要概念!!!!。
// 以下几个是对上面右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
对比总结
左值引用总结
- 左值引用智能引用左值,不能引用右值
- const左值引用既可以引用左值,又可以引用右值
// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;
//int& rt2 = 8; // 编译报错,因为10是右值,不能直接引用右值
// 2.但是const左值引用既可以引用左值
const int& rt3 = t;
const int& rt4 = 8; // 也可以引用右值
const double& r1 = x + y;
const double& r2 = fmin(x, y);
问:为什么const左值引用也可以引用右值?
答:在 C++11标准产生之前,是没有右值引用这个概念的,当时如果想要一个类型既能接收左值也能接收右值的话,需要用const左值引用,比如标准容器的 push_back 接口:void push_back (const T& val)。
也就是说,如果const左值引用不能引用右值的话,有些接口就不好支持了。
下面就是 C++98标准中相关接口const左值引用引用右值的例子:
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
右值引用总结
- 右值引用智能引用右值,不能直接引用左值。
- 右值引用可以被 move左值
move,本文指std::move(C++11),作用是将一个左值强制转化为右值,以实现移动语义。
左值被 move 后变为右值,于是右值引用可以引用。
// 1.右值引用只能引用右值
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = x + y;
int t = 10;
//int&& rrt = t; // 编译报错,不能直接引用左值
// 2.但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
int*&& rr4 = std::move(p);
int&& rr5 = std::move(*p);
const int&& rr6 = std::move(b);
左值引用的使用场景和意义
// 1.左值引用做参数
void func1(string s){...}
void func2(const string& s){...}
int main(){
string s1("Hello World!");
func1(s1); // 由于是传值传参且做的是深拷贝,代价较大
func2(s1); // 左值引用做参数减少了拷贝,提高了效率
return 0;
}
// 2.左值引用做返回值(仅限于对象出了函数作用域以后还存在的情况)
string s2("hello");
// string operator+=(char ch) 传值返回存在拷贝且是深拷贝
// string& operator+=(char ch) 左值引用做返回值没有拷贝,提高了效率
s2 += '!';
意义:
传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率。
短板:
左值引用虽然较完美地解决了大部分问题,但对于有些问题仍然不能很好地解决。
当对象出了函数作用域以后仍然存在时,可以使用左值引用返回,这是没问题的。
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
但当对象(对象是函数内的局部对象)出了函数作用域以后不存在时,就不可以使用左值引用返回了。
string operator+(const string& s, char ch)
{
string ret(s);
ret.push_back(ch);
return ret;
}
// 拿现在这个函数来举例:ret是函数内的局部对象,出了函数作用域后会被析构,即被销毁了
// 若此时再返回它的别名(左值引用),也就是再拿这个对象来用,就会出问题
右值引用的使用场景和意义
为了解决上述传值返回的拷贝问题,C++11标准就增加了右值引用和移动语义。移动语义:将一个对象中的资源移动到另一个对象(资源控制权的转移)。
- C++11标准的STL 容器的相关接口函数也增加了右值引用版本
3.4 std::move:强制转化为右值
功能:将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存搬迁或者内存拷贝。
特点:
- 可以非常简单的方式将一个左值强制转化为右值引用
- 避免不必要的拷贝操作,提高操作性能
引用移除remove_reference 具体实现:
//原始的,最通用的版本
template <typename T> struct remove_reference{
typedef T type; //定义 T 的类型别名为 type
};
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a; //使用原版本,
remove_refrence<decltype(i)>::type b; //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type b; //右值引用特例版本
move函数原型:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
std::move实现,首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后我们通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast之所以能使用类型转换,是通过remove_refrence::type模板移除T&&,T&的引用,获取具体类型T。关于引用折叠如下:
引用折叠
公式一)T& &、T&& &、T& &&都折叠成T&,用于处理左值
公式二)T&& &&折叠为T&&,用于处理右值
举例:
int var = 10;
转化过程:
- std::move(var) => std::move(int&& &) => 折叠后 std::move(int&)
- 此时:T 的类型为 int&,typename remove_reference::type 为 int,这里使用 remove_reference 的左值引用的特例化版本
- 通过 static_cast 将 int& 强制转换为 int&&
整个std::move被实例化如下
int&& move(int& t)
{
return static_cast<int&&>(t);
}
3.5 完美转发
完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。C++11标准为 C++ 引入了右值引用和移动语义,因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)
函数原型
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param) //重载接收左值
{
return static_cast<T&&>(param); // 强制类型转换
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param) //重载接收右值
{
return static_cast<T&&>(param); // 强制类型转换
}
forward有两个重载模板函数,一个接收左值,一个接收右值;
可以看到,std::forward模板函数对传入的参数进行强制类型转换,转换的目标类型符合引用折叠规则,因此左值参数最终转换后仍为左值,右值参数最终转成右值。由于forward只用了std::remove_reference,所以,它不仅可以保持左值或者右值不变,同时还可以保持const、Lreference、Rreference、validate等属性不变;
#include <iostream>
using namespace std;
template<typename T>
void myprint(T& t) {
cout <<"left"<< endl;
}
template<typename T>
void myprint(T&& t) {
std::cout << "right" << std::endl;
}
template<typename T>
void wrapper(T&& v) {
myprint(v);
myprint(std::forward<T>(v));
myprint(std::move(v));
}
int main(int argc, char * argv[]) {
wrapper(1); //int&& 右值经过参数传递,被分配了内存
std::cout << "--------------" << std::endl;
int x = 1;
wrapper(x); //int&
return 0;
}
打印结果如下
left // 传入值是右值,但是经过参数传递变成了左值(有内存分配)
right // 使用了forward 则保留右值属性
right // 使用了move,一定是右值
--------------
left // x变量是左值
left // 使用了forward 则保留左值属性
right // 使用了move,一定是右值