基类和派生类关系
派生类对象模型简介
子类对象,通常包含多个组成部分(也就是多个子对象)
1)一个是含有派生类自己定义的成员变量、成员函数的子对象
2)一个是该派生类所继承的基类子对象,这个子对象包含的是基类中定义的成员变量和成员函数
基类指针可以new派生类对象,就是因为派生类对象含有基类部分,我们可以把派生类对象当作基类对象使用
编译器帮助我们做了这种隐式类型转换
这种转换的好处就是有些需要基类引用/指针的地方可以用这个派生类对象引用/指针来代替
派生类构造函数
派生类实际是使用基类的构造函数来初始化它的基类部分,使用派生类的构造函数来初始化派生类的部分
传递参数给基类构造函数:通过派生类的构造函数初始化列表
class A{
public:
A(int i):data(i){};
virtual ~A(){};
private:
int data;
}
class B: public A
{
public :
B(int i,int j,int k):A(i),m_value(k){};
~B() {};
private:
int m_value;
}
B b(1,2,3);
既当父类又当子类
class A{};
class B : public A{}; //A是B的直接基类
class C : public B{}; //A是C的间接基类
继承关系一直传递,构成了一种继承链,最终结果就是派生类会包含它的直接基类的成员以及所有间接基类的成员
不想当基类的类
c++11中,使用final
关键字,在类的后面添加final
表示该类不能做基类
class A final{ // A不能做基类
public:
A(int i):data(i){};
virtual ~A(){};
private:
int data;
}
class B;
class C final: public B{}; // B不能做基类
静态类型与动态类型
human *phuman = new man; //基类指针指向派生类对象
human &q = *phuman; //基类引用绑定到派生类对象上
静态类型:变量声明时的类型,静态类型编译时是已知的
动态类型:指的是指针或者引用所表达的内存中的对象的类型,运行是才可知。这里是man类型
只有基类指针/引用才有静态类型和动态类型不一致的情况,如果不是基类指针/引用,那么静态类型和动态类型永远都应该是一致的
派生类向基类的隐式类型转换
可以将派生类隐式转换为基类,但是基类不能转换为派生类
因为派生类中含有基类的部分,但是基类中没有派生类的部分
man man;
huamn *phuman = &man; //可以
man *pman = phuman; //不可以,编译器通过静态类型推断转换合法性,发现基类不能转成派生类
//如果基类中有虚函数
man *pman2 = dynamic_cast<man *>(phuman); //可以
父类子类之间的拷贝与赋值
man man; //派生类对象
human human(man); //用派生类对象定义初始化基类对象,这个会导致基类拷贝构造函数的执行
用派生类对象为一个基类对象初始化或者赋值时,只有该派生类对象的基类部分会被拷贝或者赋值,派生类部分不会被赋值
左值与右值
左值
能用在赋值语句中等号左边的位置,它能够代表一个地址。左值有的时候可以被当作右值使用
用到左值的运算符:
a)赋值运算符=
b)取址运算符&
c)string,vector等容器的下标[],迭代器iterator
d)通过看一个运算符在字面值上能否操作,若可以则为左值,否则为右值
右值
不能作为左值的值就是右值,右值不能出现在赋值语句中等号左边的位置
c++的一条表达式,要么是左值,要么是右值,不可能两者都不是
i = i + 1
中,i是个左值,不是右值,虽然他也出现在了等号右边。
i出现在等号右边的时候,我们说i有一种右值属性;i出现在等号左边,用的是i代表的内存中的地址,我们说i有一种左值属性
一个左值他可能同时拥有左值属性和右值属性
引用分类
1)左值引用:绑定到左值
2)const引用:也是左值引用,我们不希望改变值的对象
3)右值引用:绑定到右值 int &&rvalue = 3;
绑定到一个常量
左值引用
绑定到左值上,没有空引用,所以左值引用初始化的时候就绑定左值
int a=1;
int &b{a}; //可以
int &c; //不可以,必须初始化
int &d = 1; //不可以,左值引用不能绑定到右值,必须绑定到左值
const int &e = 1; //可以,const引用特殊
//int tmp = 1;
// const int &e = tmp; // 这两句相当于const int &e = 1;
右值引用
必须绑定到右值上,希望使用右值引用绑定一些即将销毁或者临时的对象上
c++11引入,代表一个新的数据类型,为了提高程序运行效率,把拷贝对象变为移动对象
能绑定到左值引用上的一般不能绑定到右值引用,能绑定到右值引用上的一般不能绑定到左值引用
string strtest{"i love china"};
string &r1(strtest); //可以,左值引用绑定左值
string &r2{"i love china"}; //不可以,左值引用不能绑定右值
const string &r3{"i love china"}; //可以,创建临时变量,绑定到左值r3
string &&r4(strtest); //不可以,右值引用不能绑定左值
string &&r5{"i love china"}; //可以,绑定到一个临时变量,临时变量的内容为"i love china"
返回左值引用的函数,连同赋值、下标、解引用和前置递增递减运算符(–i),都是返回左值表达式的
返回非引用类型的函数,连同算数、关系、为以及后置递增递减运算符(i–),都是返回右值表达式的
强调:
1)所有变量,看成左值,因为他们都是有地址的
2)任何函数里的形参都是左值,void test(int i,int &&w)
,w是右值引用,但w本身是左值
3)临时对象都是右值
std::move()函数
c++11标准库函数,把一个左值强制转换为右值
int i = 10;
int &&ri = i; //不能绑定
int &&ri2 = std::move(i) //可以绑定
int &&ri3 = 100;
int &&ri4 = ri3; // 不能绑定
int &&ri5 = std::move(ri3); //可以绑定
系统建议我们在调用完std::move()
函数之后,尽量不要再使用move()中的参数,而是使用右值引用
临时对象
临时对象存放在栈上,临时会影响程序的性能,很多是我们书写问题产生的
产生临时对象的情况和解决
1)以传值的方式给函数传递参数,会产生临时对象,建议使用引用
2)类型转换生成的临时对象,隐式类型转换以保证函数调用成功
human hm;
hm = 100; //这里产生了一个真正的临时变量
human hm = 100; //没有生成临时变量
c++只会为const string &
产生临时变量,而不会为非const
的参数产生临时变量
3)函数返回对象的时候,因为返回临时对象的时候导致占用了一个拷贝构造函数和一个析构函数
对象移动
c++11提出,保存临时变量中的有用的数据,转移有用数据的所有权。
移动构造函数和移动赋值运算符
也是c++11引入,提高程序效率,类似于拷贝构造函数和拷贝赋值运算符
1)将A中的数据移动到B,那么A对象就不能继续使用该数据了;拷贝的话可以继续使用
2)虽然叫作移动,不是把数据从一个地址移动到另一个地址,而是将所有权改变,地址没有改变
// 拷贝构造函数
Time::Time(const Time &tmptime){}; //const左值引用
//移动构造函数
Time::Time(Time &&tmptime){}; // 右值引用&&
移动构造函数和移动赋值运算符应该完成的任务
1)完成必要的内存移动,斩断源对象与内存的关系
2)确保源对象处于一种即使被销毁也不会出现问题的状态,确保不再使用源对象
class B{
public:
int m_bm;
//默认构造函数
B():m_bm(100){
cout<<"B()"<<endl;
};
//拷贝构造函数
B(const B &tmp):m_bm(tmp.m_bm){
cout<<"B(const B &tmp)"<<endl;
};
virtual ~B(){
cout<<"~B()"<<endl;
}
}
B *pb = new B; //默认构造函数
pb->m_bm = 19;
B *pb2 = new B(*pb); //拷贝构造函数
delete pb;
delete pb2;
/********************************************/
class A{
public:
//默认构造函数
A():m_pb(new B()){
cout<<"A()"<<endl;
}
//拷贝构造函数
A(const A &tmp):m_pb(new B(*(tmp.m_pb))){
cout<<"A(const A &tmp)"<<endl;
}
//移动构造函数
A(A &&tmp) noexcept :m_pb(tmp.m_pb){ // tmp对象指向内存m_pb,直接让临时对象也指向这个内存
tmp.m_pb = nullptr; //打断tmp对象对于m_pb的指向
cout<<"A(A &&tmp)"<<endl;
}
//拷贝赋值运算符
A& operator=(const A &src){
if(this == &src)
return *this;
delete m_pb; //把自己的m_pb干掉
m_pb = new A(*(src.m_pb)); //重新分配一块内存
return *this;
}
//移动赋值运算符
A& operator=(const A &&src) noexcept {
if(this == &src)
return *this;
delete m_pb; //把自己的m_pb干掉
m_pb = src,m_pb; //将对方的直接拿过来
src.m_pb = nullptr; // 打断src对象对于m_pb的指向
return *this;
}
virtual ~A(){
delete m_pb;
cout<<"~A()"<<endl;
}
private:
int *m_pb;
}
static A getA()
{
A a;
return a; //不加移动构造函数,创建临时对象,调用拷贝构造函数
} //加上移动构造函数,调用移动构造函数
A a = getA();
//不加移动构造函数,调用了1次构造函数,1次拷贝构造函数,2次析构函数
//加入移动构造函数,调用了1次构造函数,1次移动构造函数,2次析构函数
A a1(a); //调用拷贝构造函数
A a2(std::move(a)); //调用移动构造函数,传入了右值
A &&a3(std::move(a)); //没有创建新对象,不会调用移动构造函数,对象a有了新别名a3,后续建议使用a3
A a4;
a4 = std::move(a); //调用移动赋值运算符
noexcept
:通知标准库不抛出任何异常,提高效率
移动构造函数中通常要加noexcept
关键字,这是一种约定俗成的习惯,声明和实现时都要加
合成的移动操作
某些条件下,编译器能合成移动构造函数,移动赋值运算符
1)有自己的拷贝构造函数,自己的拷贝赋值运算符,或者自己的析构,那么编译器就不会为他合成移动构造函数和移动赋值运算符;所以有一些类是没有移动构造函数和移动赋值运算符的。
2)如果没有移动构造函数和移动赋值运算符,系统会自动调用拷贝构造函数和拷贝赋值运算符
3)只有一个类没有定义任何自己版本的拷贝构造成员,且类的每个非静态成员都可以移动时,编译器才会为该类合成移动构造函数或者移动赋值运算符。内置类型可以移动,类类型的成员,则这个类要有对应的移动操作相关的函数,就可以移动
class TC{
int i; // 内置类型可以移动
std::string s; //string 类型定义了自己的移动操作
}
TC a;
a.i = 100;
a.s = "i love china";
const char *p = a.s.c_str();
TC b = std::move(a); //导致TC类的移动构造函数(系统帮我们生成)的执行
const char *q = b.s.c_str(); // p和q的地址不同,string类特性决定,假移动
评论区