当前位置: 首页 > news >正文

C++派生类核心机制:类型转换与对象复制控制深度剖析

目录

基类与派生类之间的转换

派生类对象间的复制控制(重点)


基类与派生类之间的转换

一般情况下,基类对象占据的空间小于派生类对象。

(空继承时,有可能相等)

1:可否把一个基类对象赋值给一个派生类对象?可否把一个派生类对象赋值给一个基类对象?

2:可否将一个基类指针指向一个派生类对象?可否将一个派生类指针指向一个基类对象?

3:可否将一个基类引用绑定一个派生类对象?可否将一个派生类引用绑定一个基类对象?

Base base;
Derived d1;base = d1; //ok
d1 = base; //errorBase * pbase = &d1; //ok
Derived * pderived = &base //errorBase & rbase = d1; //ok
Derived & rderived = base; //error

以上三个ok的操作,叫做向上转型(往基类方向就是向上),向上转型是可行的

反之,基类向派生类转型称为向下转型,直接进行向下转型都会报错。

  • 用基类对象接受派生类对象的赋值(ok)

  • 用基类引用绑定派生类对象(ok)

  • 用基类指针指向派生类对象(ok)

—— 体现派生类向基类的转型。

向下转型有风险(如下)—— 以指针为例

Base类的指针指向Derived类的对象,d1对象中存在一个Base类的基类子对象,这个Base类指针所能操纵只有继承自Base类的部分;

Derived类的指针指向Base对象,除了操纵Base对象的空间,还需要操纵一片空间,只能是非法空间,所以会报错。其实可以理解为一个指针类型不同,所要管理的空间也不同。 还有可能是因为派生类都会包含有一个基类子对象,基类子对象放在内存的上面,指针可以指向类型相同的对象,所以基类可以指向派生类。派生类不可以指向基类Derived对象的内存结构为[Base::a][Derived::b]。基类指针指向派生类对象时,实际指向的是该对象中基类部分的起始地址,这种设计保证了基类指针能安全访问继承自基类的成员

补充:基类对象和派生类对象之间的转换没有太大的意义,基类指针指向派生类对象(基类引用绑定派生类对象)重点掌握,只能访问到基类的部分。

  • 有些场景下,向下转型是合理的,可以使用dynamic_cast来进行转换,如果属于合理情况,可以转换成功。

即基类向派生类的转型,我们看看这样的例子

Base base;
Derived d1;Base * pbase = &d1;
Derived * pderived = pbase;//这种转型是合理的,但是不能直接转型,会报错

注意:在使用dynamic_cast时还需要有多态的内容,我们需要加上一个虚函数。

class Base {
public:Base(long base): _base(base){ cout << "Base()" << endl; }virtual void display(){cout << "Base::display()" << endl;}~Base()
{ cout << "~Base()" << endl; 
}long _base = 10;
};class Derived
: public Base 
{
public:Derived(long base,long derived): Base(base), _derived(derived){ cout << "Derived(long)" << endl; 
}~Derived(){ cout << "~Derived()" << endl; 
}long _derived;
};
void test0(){	
Base base;
Derived d1;
Base * pbase = &d1;//此处的pbase本身就是指向派生类赌侠你给
//向下转型是合理的
//但是不能直接向下转型,直接向下转型会发生切片(就是派生类的部分被丢掉了)
//Derived *pderived = pbase;//如果不合理的向下转型,会返回一个空指针
//如果合理的向下转型,会返回一个合法的Derived *//向下转型
Derived * pd = dynamic_cast<Derived*>(pbase);
if(pd){
cout << "转换成功" << endl;
pd->display();
}else{
cout << "转换失败" << endl;
}
}

这里可以转换成功,因为pbase本身就是指向一个Derived对象

如下,属于不合理的转换,因为pbase本身是指向一个Base对象的,此时dynamic_cast会返回一个空指针。

void test1(){
Base base(1);
Derived d1(2,3);
Base * pbase = &base; Derived * pd = dynamic_cast<Derived*>(pbase);
if(pd){
cout << "转换成功" << endl;
pd->display();
}else{
cout << "转换失败" << endl;
}
}

—— 如上图,可以转换成功

结论:

可以用派生类对象赋值给基类对象(用基类对象接受派生类对象的赋值),可以用基类指针指向派生类对象,可以用基类引用绑定派生类对象。

反之则均不可。

派生类对象间的复制控制(重点)

复制控制函数就是 拷贝构造函数、赋值运算符函数

原则:基类部分与派生类部分要单独处理

(1)当派生类中没有显式定义复制控制函数时,就会自动完成基类部分的复制控制操作;

(2)当派生类中有显式定义复制控制函数时,不会再自动完成基类部分的复制控制操作,需要显式地调用;

对于拷贝构造,如果显式定义了派生类的拷贝构造,在其中不去显式调用基类的拷贝构造,那么无法通过复制初始化基类的部分,只能尝试用Base无参构造初始化基类的部分。如果Base没有无参构造,编译器就会报错。

对于赋值运算符函数,如果显式定义了派生类的赋值运算符函数,在其中不去显式调用基类的赋值运算符函数,那么基类的部分没有完成赋值操作。

如下,Derived对象没有指针成员申请堆空间,不需要显式定义拷贝构造函数和赋值运算符函数。编译器会自动完成基类部分的复制工作。

但是如果在Derived类中显式写出了复制控制的函数,就需要显式地调用基类的复制控制函数。

class Base{
public:Base(long base): _base(base){}protected:long _base = 10;
};class Derived
: public Base
{
public:Derived(long base, long derived): Base(base), _derived(derived){}Derived(const Derived & rhs): Base(rhs)//调用Base的拷贝构造, _derived(rhs._derived){cout << "Derived(const Derived & rhs)" << endl;}Derived &operator=(const Derived & rhs){//调用Base的赋值运算符函数Base::operator=(rhs);_derived = rhs._derived;cout << "Derived& operator=(const Derived &)" << endl;return *this;}private:long _derived = 12;
};

下面为测试代码可自行测试

#include <iostream>
using namespace std;class Base {
public:Base(long base): _base(base){//cout << "Base()" << endl;}Base(const Base& rhs):_base(rhs._base){cout << "Base()" << endl;}Base& operator=(const Base& rhs){cout << "Base的赋值运算符函数" << endl;_base = rhs._base;return *this;}void print()const {cout << "_base:" << _base << endl;}
protected:long _base;
};class Derived: public Base
{
public:Derived(long base, long derived): Base(base), _derived(derived){//cout << "Derived()" << endl;}void print()const {Base:: print();cout << "_derived:" << _derived << endl;}//显示的提供拷贝构造,要显示的调用基类的构造函数Derived(const Derived& rhs):Base(rhs)//显示调用基类的拷贝构造函数, _derived(rhs._derived){cout << "Derived()" << endl;}Derived& operator=(const Derived& rhs) {//要显示调用Base的赋值运算符函数Base::operator=(rhs);_derived = rhs._derived;cout << "Derived& operator=(const Derived &)" << endl;return *this;}private:long _derived;
};
void test()
{//Base base(10);//Derived d1(4, 6);//底层是base对象调用Base类的赋值运算符函数//base.operator=(d1);//形参 const Base & rhs  实参 d1//base = d1;//cout << base._base << endl;
}
void test1()
{Derived d1(6, 9);d1.print();Derived d2(5, 7);d1 = d2;d1.print();Derived d3 = d2;d3.print();
}
int main()
{test1();return 0;
}#include <iostream>
using namespace std;class Base {
public:Base(long base): _base(base){//cout << "Base()" << endl;}Base(const Base& rhs):_base(rhs._base){cout << "Base()" << endl;}Base& operator=(const Base& rhs){cout << "Base的赋值运算符函数" << endl;_base = rhs._base;return *this;}void print()const {cout << "_base:" << _base << endl;}
protected:long _base;
};class Derived: public Base
{
public:Derived(long base, long derived): Base(base), _derived(derived){//cout << "Derived()" << endl;}void print()const {Base:: print();cout << "_derived:" << _derived << endl;}//显示的提供拷贝构造,要显示的调用基类的构造函数Derived(const Derived& rhs):Base(rhs)//显示调用基类的拷贝构造函数, _derived(rhs._derived){cout << "Derived()" << endl;}Derived& operator=(const Derived& rhs) {//要显示调用Base的赋值运算符函数Base::operator=(rhs);_derived = rhs._derived;cout << "Derived& operator=(const Derived &)" << endl;return *this;}private:long _derived;
};
void test()
{//Base base(10);//Derived d1(4, 6);//底层是base对象调用Base类的赋值运算符函数//base.operator=(d1);//形参 const Base & rhs  实参 d1//base = d1;//cout << base._base << endl;
}
void test1()
{Derived d1(6, 9);d1.print();Derived d2(5, 7);d1 = d2;d1.print();Derived d3 = d2;d3.print();
}
int main()
{test1();return 0;
}

如果Derived类的数据成员申请了堆空间,那么必须手动写出Derived类的复制控制函数,此时就要考虑到基类的复制控制函数的显式调用。

(如果只是Base类的数据成员申请了堆空间,那么Base类的复制控制函数必须显式定义,Derived类自身的数据成员如果没有申请堆空间,不用显式定义复制控制函数)

练习:将Base类的数据成员换成char *类型,体验一下派生类的复制。

如果派生类中没有指针数据成员,不需要显式写出复制控制函数。编译器会自动进行基类部分的复制控制。

  • 对于派生类的拷贝构造函数

如果给Derived类中添加一个char * 成员,依然不显式定义Derived的复制控制函数。

那么进行派生类对象的复制时,基类的部分会完成正确的复制,派生类的部分只能完成浅拷贝(最终对象销毁时导致double free问题)

Derived d1("hello","world");
Derived d2 = d1;

如果接下来给Derived类显式定义了拷贝构造,但是没有在这个拷贝构造中显式调用基类的拷贝构造(没有写任何的基类子对象的创建语句),会直接报错。

(—— 在派生类的构造函数的初始化列表中没有显式调用基类的任何的构造函数,编译器会自动调用基类的无参构造,此时基类没有无参构造,所以报错)

因为没有初始化d2的基类子对象,需要在derived的拷贝构造函数中显式调用Base的拷贝构造。

  • 对于赋值运算符函数

如果接下来给Derived显式定义赋值运算符函数,但是没有在其中显式调用基类的赋值运算符函数

Derived d1("hello","world");
Derived d2 = d1;
Derived d3("beijing","shanghai");d2 = d3;  //派生类对象的部分完成了复制,但是基类部分没有完成复制

基类的部分不会自动完成复制,需要在Derived的赋值运算符函数中显式调用Base的赋值运算符函数,才能完成正确的复制

总结:

给Derived类手动定义复制控制函数,注意在其中显式调用相应的基类的复制控制函数

(注意:派生类对象进行复制时一定会马上调用派生类的复制控制函数,在进行复制时会首先复制基类的部分,此时调用基类的复制控制函数)

Derived(const Derived & rhs): Base(rhs)//显式调用基类的拷贝构造, _pderived(new char[strlen(rhs._pderived) + 1]()){strcpy(_pderived, rhs._pderived);cout << "Derived(const Derived &)" << endl;}Derived & operator=(const Derived & rhs){cout << "Derived & operator=(const Derived &)" << endl;if(this != &rhs){//显式调用基类的赋值运算符函数Base::operator=(rhs);//关键delete [] _pderived;_pderived = new char[strlen(rhs._pderived) + 1]();strcpy(_pderived,rhs._pderived);_derived = rhs._derived;}return *this;
}

下面为测试代码可自行测试

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;class Base {
public:Base(long base, const char* pbase): _base(base),_pbase(new char [strlen(pbase)+1]()){strcpy(_pbase, pbase);cout << "Base()" << endl;}Base(const Base& rhs):_base(rhs._base),_pbase(new char [strlen(rhs._pbase)+1]()){strcpy(_pbase, rhs._pbase);cout << "Base()" << endl;}Base& operator=(const Base& rhs){if (this != &rhs){delete[] _pbase;_pbase = new char[strlen(rhs._pbase) + 1]();strcpy(_pbase, rhs._pbase);_base = rhs._base;}cout << "Base的赋值运算符函数" << endl;return *this;}void print()const {cout << "_pbase:" << _pbase << endl;cout << "_base:" << _base << endl;}
protected:long _base;char* _pbase;
};class Derived: public Base
{
public:Derived(long base, const char* pbase,long derived, const char * pderived): Base(base, pbase), _derived(derived), _pderived(new char [strlen(pderived) + 1] ()){strcpy(_pderived, pderived);cout << "Derived()" << endl;}void print()const {Base::print();cout << "_derived:" << _derived << endl;cout << "_pderived:" << _pderived << endl;}//显示的提供拷贝构造,要显示的调用基类的构造函数Derived(const Derived& rhs):Base(rhs)//显示调用基类的拷贝构造函数, _derived(rhs._derived), _pderived(new char [strlen(rhs._pderived)+1]()){strcpy(_pderived, rhs._pderived);cout << "Derived()" << endl;}Derived& operator=(const Derived& rhs) {if (this != &rhs){Base::operator=(rhs);delete[] _pderived;//要显示调用Base的赋值运算符函数_derived = rhs._derived;_pderived = new char[strlen(rhs._pderived) + 1]();strcpy(_pderived, rhs._pderived);}cout << "Derived& operator=(const Derived &)" << endl;return *this;}~Derived(){if (_pderived){delete[] _pderived;_pderived = nullptr;}}
private:long _derived;char* _pderived;
};
void test()
{//Base base(10);//Derived d1(4, 6);//底层是base对象调用Base类的赋值运算符函数//base.operator=(d1);//形参 const Base & rhs  实参 d1//base = d1;//cout << base._base << endl;
}
void test1()
{Derived d1(6,"hello", 9,"beijing");d1.print();Derived d2(5, "world", 7, "hhhh");d1 = d2;d1.print();Derived d3 = d2;d3.print();
}
int main()
{test1();return 0;
}

相关文章:

  • 通信协议记录仪-产品规格书
  • 如何让通义千问大模型支持结构化输出?
  • 使用xlwings将两张顺序错乱的表格进行数据核对
  • NVIDIA Omniverse在数字孪生中的算力消耗模型构建方法
  • C++ std::initializer_list 详解
  • 为美好的XCPC献上典题 ABC359 G - Sum of Tree Distance(根号分治)
  • 【AI面试准备】传统测试工程师Prompt Engineering转型指南
  • 在 Windows 中安装 Pynini 的记录
  • ECMAScript 2(ES2):标准化的微调与巩固
  • 每天一道算法题——推多米诺
  • leetcode 838. 推多米诺 中等
  • A2A Python 教程 - 综合指南
  • 深度理解linux系统—— 进程切换和调度
  • 数据结构-线性结构(链表、栈、队列)实现
  • Python 中 DAO 层使用泛型的探索
  • 接口测试实战指南:从入门到精通的质量保障之道
  • Linux系统:详解文件描述符与重定向原理以及相关接口(open,read,write,dup2)
  • 分布式理论:常见分布式协议的概览与解析
  • 51c大模型~合集123
  • C++ 复习
  • 广西科学调度保障春灌面积1373.53万亩
  • 奥斯卡新规:评委必须看完影片再投票;网友:以前不是啊?
  • 龚惠民已任江西省司法厅党组书记
  • 国新办发布《关于新冠疫情防控与病毒溯源的中方行动和立场》白皮书
  • 魔都眼|静安光影派对五一启幕:苏河湾看徐悲鸿艺术画作
  • 解放日报社论:只争朝夕、不负重托,加快建成具有全球影响力的科技创新高地