3.1 继承构造函数
派生类可以继承基类的成员变量和接口,但是构造函数(包括拷贝构造)、析构函数、赋值运算符重载函数不能被继承。
这样派生类对象构造的的时候需要显示调用基类的构造函数,才能完成基类成员的初始化;有时候这种做法会产生很多的冗余代码,比如当基类中有多中构造函数时,派生类中需要添加对应的构造函数完成基类成员变量的初始化,这种做法很不方便;
在c++11中,派生类中可以通过使用using来继承基类的构造函数;这种做法可以节省代码占用空间,而且编译器会将没有使用到的构造函数优化掉;
但是要注意,一旦使用了继承的构造函数,编译器就不会再为派生类生成默认的无参构造函数了
3.2 委托构造函数
目的是为了让构造函数实现更简单。
上述的代码不够简洁,一种方法是可以在变量声明的时候进行初始化:
能不能进一步进行简化?c++11中可以使用委派构造函数进行初始化:
其中Info()为目标构造函数;info(int i)和Info(char e)为委派构造函数;委派构造是指委派构造函数将构造任务委派给了目标构造函数;
但要注意:
1)委派和成员初始化列表不能同时使用;
但是可以改造一下目标构造函数函数,使委派构造函数依然可以在初始化列表中完成成员的初始化;
2)目标构造函数总是先于委派构造函数执行
假设“其他初始化”的代码是type += 1;
定义:Info f(3);
那么3-11和3-12产生的初始化结果是不一样的;
显然3-12更符合我们的预期。
3)委派构造函数可以形成链式的委托构造关系,但是不能出现环式的委托;
环式委派关系是不允许的:
3.3 右值引用
3.3.1 移动构造函数
拷贝构造的过程中会产生一些临时变量,会给程序的运行带来一些额外的开销,使用移动构造函数减少临时变量创建销毁带来的开销。
使用拷贝构造的例子:
程序的输出为:
拷贝构造函数被调用了两次,一次是GetTemp()返回的临时变量;另一次是使用该临时变量对a进行拷贝构造;
而采用移动构造函数可以避免临时对象的再次拷贝,临时对象的内存被转移给a,从而避免再次拷贝带来的开销;
代码如下;
输出为:
可以看出,调用了两次移动构造函数,堆内存被复用;
3.3.2 左值、右值与右值引用
左值:可以被取地址的,有名字的就是左值;
右值:不能被取地址的,不具名的一般为右值;比如非引用返回的函数的返回的临时变量值,一些运算表达式、类型转换函数的返回值、lambda表达式;
右值又分纯右值和将亡值,将亡值是与右值引用相关的表达式,通常是指将要被移动的对象,比如返回右值引用T&&的返回值、std::move的返回值、转换为T&&的类型转换函数的返回值;
在C++11中,所有的值必属于左值、将亡值、纯右值之一;
由于右值是不具名的,通过右值引用可以给右值赋予一个别名;
T&& a = ReturnValue();
上面的表达式中,函数返回的右值在表达式语句结束之后,其生命也就中介了,但是通过右值引用,该右值又被延长了生命;
相对于T b = ReturnValue(),右值引用变量声明,就会少一次对象的构造与析构,因为a直接绑定了ReturnValue()返回的临时变量,而b知识又临时变量值构造而成的。
通常右值引用只能绑定右值,是不能绑定左值的;如下的表达式将会编译出错:
Int c;
Int&& d = c;
而非常量的左值引用也不能绑定右值,只有常量的左值引用才能绑定右值;
T& e = ReturnValue(); //编译出错
Const T& e = ReturnValue() // ok
事实上常量左值引用可以接受非常量左值、常量左值、右值对其进行初始化,是一个万能的引用类型;
不过在使用右值对其初始化的时候,相对于右值引用,常量左值引用也可以延长右值的生命周期,但是在生命周期内只能是只读的。
程序的输出:
上面的例子可以看出,常量左值引用同样可以直接使用右值进行初始化,从而减少一次拷贝构造的调用;而值传递则不能直接使用临时对象;
通常情况下,移动构造函数和拷贝构造函数会同时被声明,以保证在移动构造函数构造不成时,可以使用拷贝构造函数。
3.3.3 std::move
Move可以将左值转换为右值,从而使用右值引用,但是被强转的左值不会被销毁,因此在使用move时要特别注意,下面是一个错误使用的例子:
左值a被强制转换成右值,从而调用移动构造函数,但是a.i被赋值为空,但是a的生命周期并没有结束,所以出现悬挂指针。
因此,move常用于强转生命周期即将结束的对象。在下面的例子中,m.h被强制转换为右值,从而调用移动构造函数进行初始化,同时m也是即将被销毁的对象;
值得注意的是,可以接受右值的右值引用的本身确实一个左值,比如下面例子中的m.h.
使用移动语义需要注意以下问题;
1)常量右值引用即const T&&会导致右值无法被修改,从而无法实现移动语义,移动语义一定是要可以修改临时变量的值。
2)默认情况下,编译器会隐式生成一个移动构造函数,但是如果程序中声明了自定义的拷贝构造函数、拷贝赋值函数、析构函数中的一个或多个,编译器都不会再隐式生成默认版本。而且默认生成的移动构造函数与默认的拷贝构造函数一样,只是浅拷贝,不会实现移动语义,因此,如果要实现移动语义,需要自定义移动构造函数。
针对资源类型的对象建议使用移动语义,因为资源通常是唯一的,比如智能指针、文件流等;
在c++11中,拷贝或移动构造函数有以下3个版本:
3)对于移动构造函数如果抛出异常会导致悬挂指针,因为此时移动语义可能还没有完成;因此,可以在移动构造函数声明中加noexcept关键字;可以使用move_if_noexcept的模板函数代替move函数,该函数在类的移动构造函数没有noexcept关键字修饰是返回一个左值引用从而调用拷贝构造语义,而在移动构造函数有noexcept关键字时,返回一个右值引用,从而使变量可以使用移动语义;
4)现在编译器通常会对函数返回值进行优化(RVO),一旦开启这个优化,函数返回的临时变量的构造与析构、以及从临时变量拷贝或者移动构造统统被优化掉了。
比如上面的代码。一旦开启优化,任何的拷贝和移动都没有,b直接使用a变量的资源进行初始化;但是RVO并不是对任何情况都有效,有些情况下,这些构造函数是无法省略的。
3.3.4 完美转发
完美转发是指在函数模板中,完全按照模板参数的类型,将参数传递给函数模板中调用的另外一个函数。即转发函数传入什么类型,目标函数就能获得什么类型;
C++11中,通过引用折叠的规则,并结合模板类型推导规则实现完美转发;
上面的代码在c++11中,将会出现引用折叠。引用折叠的规则如下:
规则的规律是:一旦出现了总之引用,引用折叠总是会优先将其折叠为左值引用。
下面看一个例子:
根据上面的规则,可以将转发函数写为如下形式:
- 当传入一个X的左值引用时:
- 所以上面的写法,可以在传入左值时,转发左值,传入右值时转发右值,从而实现完美转发。在c++11中,有一个forward函数可以代替上面的static_cast功能;
- 下面看一个完美转发的例子:
完美转发一个作用就是做包装函数,示例如下;
3.4 显式转换操作符
仅含一个参数的构造函数和除了第一个参数外其余参数都有默认值的多参构造函数不仅仅是一个构造函数,而且还是一个默认且隐含的类型转换操作符;看一个例子:
class Base
{
public:
Base (int i = 10)
{
a = i;
}
private:
int a;
};
int main()
{
Base b;
b = 1; //此处编译器做了隐式转换,即Base tmp(1);b = tmp;
return 0;
}
有时候隐式的类型转换会带来一些问题:
上面的代码中通过重载类型转换函数将自定义类型转换为bool类型,以方便进行对象的if判断。但是也会使p+pd两个指针的加法在语法上得到允许;
Explicit可以防止隐式转换的发生,其作用是指定仅有一个参数或除第一个参数外其它参数均有默认值的类构造函数不能作为类型转化操作符被隐含的使用,防止该类的对象直接被对应内置类型隐式转化后赋值,从而规定这个构造函数必须被明确调用。
C++11中将explicit的使用扩展到自定义的类型转换操作符上,以支持所谓的显式类型转换。Explicit作用于类型转换操作符上,意味着只有在直接构造目标类型或显式类型转换的时候才可以使用该类型。
Ct变量直接初始化构造于变量c,可以直接编译通过;
Ct2和FUNc需要从c中拷贝构造,因而不能通过编译;
显式类型转换并没有完全禁止从源类型到目标类型的转换,不过此时的拷贝构造和非显式类型转换不被允许,也就不能通过赋值表达式或者函数参数的方式产生一个对象。这样做的好处是可以提前暴露隐式转换所带来的问题。