更倾向于通用而不是特殊化
继承构造函数
子类自动获取父类的成员变量和接口
接口是指: 虚函数和纯虚函数
言外之意是说,父类的非虚函数不能被子类继承
构造函数也遵循这个规则 如果父类的构造函数不是虚函数,则不会被子类继承
子类在初始化过程中,要先初始化父类,
如果父类的构造函数不是虚函数,
那么子类就不能继承父类构造函数,
从而不能为父类初始化,
一般子类在自己的构造函数中显示的声明使用父类的构造函数, 这样就解决了父类构造函数不是虚函数也能初始化父类这个问题
struct A {
A(int i){} //构造函数没有virtual 不是虚函数 不能被子类继承
};
struct B : public A {
B(int i) : A(i) {} //显示的声明使用父类的构造方法 解决不能继承父类的构造方法
};
问题: 如果基类有很多构造方法 而且这些方法都不是虚函数 那么子类为了使用父类的构造方法,就必须为每一个父类的构造显示的写出一个子类对应的构造函数,这样程序员就累死了
struct A{
A(int i){}
A(double d, int i){}
A(float f, int i, const char * c) {}
//... 后面还有 假设有10万个
};
struct B : public 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) {}
//... 后面还有 假设有10万个 写B类的程序员累死了
virtual void ExtraInterface{}
};
B的目的是 就为了写一个扩展的接口 或功能 ExtraInterface(){} 但B要初始化父类 那必须显示的写完10万多个父类的构造函数 对应的版本 此时写B类的程序员立马就放弃了c++
为了解决这个问题?c++使用using来声明继承基类的构造函数
struct A{
A(int i){}
A(double d, int i){}
A(float f, int i, const char * c) {}
//... 后面还有 假设有10万个
};
struct B : public A {
using A::A; //继承构造函数
//... 后面还有 假设有10万个 写B类的程序员很轻松
virtual void ExtraInterface{}
};
问题: 继承构造函数只会初始化父类的成员 子类自己的成员不能被初始化,怎么办?
那么结合使用就地初始化给子类成员一个默认值
struct A{
A(int i){}
A(double d, int i){}
A(float f, int i, const char * c) {}
};
struct B : public A {
using A::A; //继承构造函数
virtual void ExtraInterface{}
int b{10}; //就地初始化一个默认值 这样子类成员也能初始化
};
问题: 如果子类需要外部传参数来初始化子类成员变量 那么程序员只能自己来实现构造方法 结合使用继承构造函数 和 初始化列表 当然就地初始化也可以存在 但是列表初始化的优先级高 所以拿的是初始化列表的参数
struct A{
A(int i){}
A(double d, int i){}
A(float f, int i, const char * c) {}
};
struct B : public A {
B(int i, int j)):A(i),b(j){}
B(double b, inti, int j):A(d, i),b(j){}
B(float f, int i, const char * c, int j):A(f,i,c),b(j){}
virtual void ExtraInterface{}
int b{10};
};
问题: 有时候父类构造函数的参数又默认值。对于继承构造函数来将,参数的默认值是不会被继承的。默认值会导致父类产生多个构造函数的版本,这些多个构造函数版本都会被派生类继承
struct A {
A (int a = 3, double = 2.4){}
};
struct B : public A{
using A:A;
};
//对于A会产生多个构造函数
struct A {
A(); //不使用参数的我情况
A(int a, double = 2.4); //使用一个参数的情况
A(int a, double b); //使用两个参数的情况
A(const A &); //默认拷贝构造函数
};
//相应的B继承了A 那么里面会有多个继承构造函数
struct B : public A{
B():A(); //不使用参数的我情况
B(int a, double = 2.4):B(a, 2.4); //不使用参数的我情况
B(int a, double b):A(a,b); //使用两个参数的情况
B(const B &rhs):A(rhs); //默认拷贝构造函数
};
参数默认值会导致多个构造函数版本,因此程序员要特别的小心
问题: 多继承时 子类拥有多个父类 那么多个父类中的构造函数会导致函数名、参数都相同,会产生冲突 二义性
struct A {
A(int a){}
};
struct B {
B(int b){}
};
struct C:A,B {
using A:A;
using B:B;
};
//解决办法显示的声明一下C的构造函数
struct C:A,B {
using A:A;
using B:B;
C(int){}
};
问题:
如果父类的构造函数被声明为private成员函数 或者 子类是从父类中虚继承, 那么就不能够在子类中声明继承构造函数
如果子类使用了继承构造函数, 编译器不会再为子类生成默认构造函数,那么程序员需要手动写一个无参数的构造函数
#include <iostream>
struct A {
A(int){}
};
struct B:public A{
using A::A;
};
int main(int argc, char* argv[], char **env) {
B b; //调用隐式删除的“B”默认构造函数
return EXIT_SUCCESS;
}
委托构造函数
目的是减少程序员书写构造函数的时间
通过委托其他构造函数, 多个构造函数的类 编写起来减少很多代码
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;
};
//发现每个构造函数都使用初始化列表来初始化type和name,并且都调用了相同的函数InitRest()
//除了初始化列表有的不同 其他的部分都相同, 3个构造函数基本上是相似的 代码存在重复的地方
//改造1 使用就地初始化 确实简单了不少 但是每个都还是调用了InitRest()
class Info{
public:
Info() {InitRest();}
Info(int i):type(i) {InitRest();}
Info(char e):name(e) {InitRest();}
private:
void InitRest(){}
int type{1};
char name{'a'};
};
//再次改造 编译器不允许this->Info()不允许在构造函数中调用构造函数
class Info{
public:
Info() {InitRest();}
Info(int i) {this->Info(); type=i;} //编译器报错
Info(char e){this->Info(); name = 2} //编译器报错
private:
void InitRest(){}
int type{1};
char name{'a'};
};
//再次改造 黑客技术 虽然绕开了编译器的检查 看起来不错 但是这是在已经初始化一部分的对象上再次调用构造函数, 这就不叫初始化了
class Info{
public:
Info() {InitRest();}
Info(int i) {new (this) Info(); type = i;}
Info(char e){new (this) Info(); name = 2;}
private:
void InitRest(){}
int type{1};
char name{'a'};
};
//再次改造 使用委托构造函数
//委托构造函数只能在函数体内为type,name等成员赋初始值,因为委托构造函数不能有初始化列表(不能同时“委派”和初始化列表)
//委托和初始化列表不能同时使用(因为初始化列表优先级高)
class Info{
public:
Info() {InitRest();} //目标构造函数
Info(int i):Info(){type=i;} //委托构造函数
Info(char e):Info(){name = 2;} //委托构造函数
private:
void InitRest(){type+=1;}
int type{1};
char name{'a'};
};
//f(3) 先是4后来又被赋值为3了 目标构造函数比委托构造函数先执行
//再次改造 使用委托构造函数
class Info{
public:
Info():Info(1,'a'){} //委托构造函数
Info(int i):Info(i,'a'){} //委托构造函数
Info(char e):Info(1,e){} //委托构造函数
private:
Info(int i, char e):type(i),name(e){type+=1;} //定义一个私有的目标构造函数 替代 InitRest()
int type;
char name;
};
//f(3) 就是4
链状的委托构造 不要形成委托环
//正常的链状的委托构造
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){} //定义一个私有的目标构造函数 替代 InitRest()
int type;
char name;
};
//Info()委托Info(int)委托Info(int,char)
//委托环
struct Rule2 {
int i, c;
Rule2():Rule2(2){}
Rule2(int i):Rule2('c'){}
Rule2(char c):Rule2(2){}
};
//Rule2()委托Rule2(int)委托Rule2(char)委托Rule2(int)
//此时编译器就没办法了 一直在这死循环
委托构造函数应用在构造模版函数产生目标构造函数
class TDConstructed{
template<class T> TDConstructed(T first, T last) : l(first, last){} //目标构造函数
std::list<int> l;
public:
TDConstructed(std::vector<short> & v):TDConstructed(v.begin(), v.end()){} //委托构造函数
TDConstructed(std::deque<int> & d):TDConstructed(d.begin(), d.end()){} //委托构造函数
};
//T会分别推导为vector<short>::iterator 和 deque<int>::iterator两种类型
//这样的好处就是 很容易接收多种容器 对其进行初始化
//总比你罗列不同的类型的构造函数方便的多
//委托构造使得构造函数泛型编程也称为可能
委托构造函数在异常中的使用
#include <iostream>
#include <list>
class DCExcept {
public:
DCExcept(double d)
try : DCExcept(1,d){ //委托构造函数 捕获到了异常 不会执行 try 这块代码
std::cout << "Run the body." << std::endl;
}catch (...){
std::cout << "caught exception." << std::endl; //会执行
}
private:
DCExcept(int i, double d){ //目标构造函数 抛出异常
std::cout << "going to throw!" << std::endl;
throw 0;
}
int type;
double data;
};
int main(int argc, char* argv[], char **env) {
DCExcept a(1.2); //由于构造函数抛出了异常 而在main 函数中 没有捕获 那么直接调用 std::terminate() 结束程序
return EXIT_SUCCESS;
}
/**
going to throw!
caught exception.
libc++abi: terminating with uncaught exception of type int
Process finished with exit code 134 (interrupted by signal 6: SIGABRT)
*/
委托构造函数try代码满足了不应该执行 而应该执行catch里面的代码
右值引用: 移动语以和完美转发
指针成员和拷贝构造函数
类成员有指针成员 就必须特别小心拷贝构造函数的编写 否则一不小心会出现内存泄露
浅拷贝:
#include <iostream>
class HasPtrMem{
public:
HasPtrMem():d(new int(0)){}
~HasPtrMem(){delete d;}
int * d;
};
int main(int argc, char* argv[], char **env) {
HasPtrMem a;
HasPtrMem b(a);
std::cout << *a.d << std::endl; //0
std::cout << *b.d << std::endl; //0
return EXIT_SUCCESS; //正常析构
}
/*
primer(29288,0x1116cee00) malloc: *** error for object 0x7fdbe9405b50: pointer being freed was not allocated
primer(29288,0x1116cee00) malloc: *** set a breakpoint in malloc_error_break to debug
0
0
*/
//堆上的内存会被析构两次 第一次正常 第二次就出现错误
深拷贝:
#include <iostream>
class HasPtrMem{
public:
HasPtrMem():d(new int(0)){}
HasPtrMem(const HasPtrMem &rhs):d(new int(*rhs.d)){}//拷贝构造函数 从堆中分配内存 并用 *h.d初始化
~HasPtrMem(){delete d;}
int * d;
};
int main(int argc, char* argv[], char **env) {
HasPtrMem a;
HasPtrMem b(a);
std::cout << *a.d << std::endl; //0
std::cout << *b.d << std::endl; //0
return EXIT_SUCCESS; //正常析构
}
移动语义
拷贝构造函数中为指针成员分配新的内存再进行拷贝的做法在c++编程中几乎被视为不可违背的
不过有些时候真的不需要拷贝 而且拷贝大块内存非常的消耗资源 性能不高
#include <iostream>
class HasPtrMem{
public:
HasPtrMem():d(new int(0)){
std::cout << "构造函数: " << ++n_cstr << std::endl;
}
HasPtrMem(const HasPtrMem &rhs):d(new int(*rhs.d)){
std::cout << "拷贝构造函数: " << ++n_cptr << std::endl;
}//拷贝构造函数 从堆中分配内存 并用 *h.d初始化
~HasPtrMem(){
std::cout << "析构函数: " << ++n_dstr << std::endl;
delete d;
}
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(){
return HasPtrMem();
}
int main(int argc, char* argv[], char **env) {
HasPtrMem a = GetTemp();
return EXIT_SUCCESS;
}
/* g++ main.cpp -fno-elide-constructors
构造函数: 1
拷贝构造函数: 1
析构函数: 1
拷贝构造函数: 2
析构函数: 2
析构函数: 3
*/
看图
为了得到一个HasPtrMem需要2次拷贝 2次析构 中间的临时对象等等这些操作,那么性能很差
对比拷贝和移动的模型:
增加移动构造函数
#include <iostream>
class HasPtrMem{
public:
HasPtrMem():d(new int(0)){
std::cout << "构造函数: " << ++n_cstr << std::endl;
}
HasPtrMem(const HasPtrMem &rhs):d(new int(*rhs.d)){
std::cout << "拷贝构造函数: " << ++n_cptr << std::endl;
}//拷贝构造函数 从堆中分配内存 并用 *h.d初始化
HasPtrMem(HasPtrMem && rhs):d(rhs.d){ //移动构造函数 先将传入的对象的指针初始化当前对象的成语
rhs.d = nullptr; //然后将传入的对象的指针设为nullptr 不进行拷贝 而是直接将指针以赋值的形式 拿过来
std::cout << "移动构造函数: " << ++n_mvptr << std::endl;
}
~HasPtrMem(){
std::cout << "析构函数: " << ++n_dstr << std::endl;
delete d;
}
int * d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
static int n_mvptr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvptr = 0;
HasPtrMem GetTemp(){
return HasPtrMem();
}
int main(int argc, char* argv[], char **env) {
HasPtrMem a = GetTemp();
return EXIT_SUCCESS;
}
/*
构造函数: 1
移动构造函数: 1
析构函数: 1
移动构造函数: 2
析构函数: 2
析构函数: 3
*/
前后对比 拷贝2次和移动2次 效率完全不一样
问题: 左值引用或者使用指针当函数的传出参数 也能达到同样的效果 但是为什么不用呢?
从性能上将这样做没毛病, 但是从使用的方便性上来说差点
比如:
Caculate(GetTemp(), SomeOther(Maybe(), Useful(Values,2)));
但是用指针和引用的方法而不返回值的话, 通常需要多些很多语句
比如:
string *a; vector b; //事先声明一些变量
...
Useful(Values,2,a); //最后一个参数是指针,用于返回结果
SomeOther(Maybe(), a, b); //最后一个参数是引用,用于返回结果
Caculate(GetTemp(), b);
最起码你要先定义一个指针,然后传给函数
但是移动就不需要这样,直截了当 在函数return 里面返回临时量就可以了
总之: 程序员就舒服了 使用最简单的语句 完成大量的工作 代码也好看
左值、右值与右值引用
问题: 判断左值和右值
c语言中 一个经典的方法就是
在赋值表达式中,出现在等号左边的就是左值 不能在左边的就是右值
比如 a = b+c; a是左值 b+c = a; 编译器报错 那么b+c就是右值
这种方法在c++中有时候好用 有时候不好用
c++一种方法是:
可以取地址的、有名字的就是左值
不能取地址的、没有名字的就是右值
比如: a = b+c;
&a编译器不报错 那么a是左值
&(b+c)编译器报错 那么b+c是右值
细致的看 c++11 右值由两个概念构成的 将亡值 和 纯右值
将亡值:
返回右值引用T&&的 函数返回值
std::move的返回值
转换为T&&的类型转换函数的返回值
纯右值:
非引用返回的函数返回的临时变量值就是一个纯右值
一些运算表达式 比如:1+3产生的临时变量 也是一个纯右值
不跟对象关联的字面量值 比如 2, 'c', true也是纯右值
类型转换函数的返回值 也是纯右值
lambda表达式 也是纯右值
c++中的左值和右值很难归纳
始终记住无论是左值引用还是右值引用 只要是引用都是对象的简称或者别名 引用是不占内存的 所以引用本身不是对象 引用是说你已经有了一个实实在在的对象,我只是绑定到这个实实在在的对象上 所以引用一开始就需要初始化(也可以这么理解: 没有对象,就没办法绑定,也就没办法给对象起别名,所以引用一开始就需要初始化) 无非就是左值引用绑定到一个有名字的对象 而右值绑定到一个没有名字的对象(匿名对象) 还要记住引用一开始初始化绑定到对象后 以后就不能解除绑定和不能绑定到另外一个对象 除非你另起一个名字 也就是再声明一个引用
移动构造函数如何触发?
如何判断产生了临时对象?
如何将临时对象用于移动构造函数?
是否只有临时变量才可以用于移动构造?