理解基类与派生类之间的类型转换是理解C++语言面向对象编程的关键所在
继承
通过继承联系在一起的类构成一种层次关系,层次关系的根部有一个基类,其他直接或间接从基类继承而来,称为派生类。
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
类派生列表的形式是:
基类与派生类之间的类型转换
一个派生类对象包含多个组成部分:一个派生类自己定义的非静态的子对象,一个该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。
C++没有明确规定派生类对象在内存中如何分布,但是我们可以分析下:
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
在一个对象中,继承自基类部分和派生类自定义的部分不一定是连续存储的,上图只是工作机理概念模型,不是物理模型
派生类对象含有其基类对应的组成部分,所以我们能把派生类对象当作基类对象使用,我们能将基类的指针或引用绑定到派生类对象中的基类部分。
Person a; //基类对象a
Student b; //派生类对象b
Person* c = &a; //c指向Person对象
c = &b; //c指向b的基类Person部分
Person& d = b; //d绑定到b的基类Person部分
这种转换通常称为派生类到基类的类型转换,编译器会隐式的执行派生类到基类的转换
通常情况下,我们想把引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的const类型转换规则(后续会写关于C++自己的类型转换的规则)。但是,继承关系的类是一个重要例外,我们可以将基类的指针或引用绑定到派生类对象上。
这里面有一层重要含义:当使用基类的引用或指针是,实际上我们并不清楚该引用或指针所绑定的真实类型,该对象可能是基类对象也可能是派生类对象。
智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针中
静态类型与动态类型
我们在使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开。
表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型
动态类型则是变量或表达式表示的内存中的对象的类型。动态类型只有在运行时才知道
Person a; //基类对象a
Student b; //派生类对象b
Person* c = &b; //c指向Student对象b的基类部分
如上述,比如我们知道c的静态类型是 Person* ,它的动态类型依赖&b的类型,动态类型只有在运行时调用该语句才知道。
如果表达式既不是指针也不是引用,那么它的静态类型与动态类型永远一致。比如Person类型的变量,永远是一个Person对象,我们无论如何都不能改变该变量所对应的类型。
基类的指针或引用的静态类型与动态类型可能不一致
不存在基类向派生类的隐式类型转换
之所以会有派生类向基类的类型转换,是因为每个派生类对象都包含一个基类部分,而使用基类的指针或引用可以绑定到该基类部分。一个基类对象既可以以独立的形式存在,也可以作为派生类的一部分存在。
比如下图:
Person a; //基类对象a
Student* b = &a; //错误:不能将基类转成派生类
Student& c = a; //错误:不能将基类转成派生类
如果允许这样赋值,那么我们很有可能使用b或c去访问原本a中不存在的成员
Person a; //基类对象a
Student b; //派生类对象b
Person* c = &b; //c指向b的Person基类部分、动态类型是Person
Student* d = c; //错误
上面这种情况也不允许,一个基类的指针或引用已经绑定了一个派生类对象,又继续基类向派生类的转换。
编译器在编译时无法确定某个特定的转换在运行时是否安全,这是因为编译器只能通过检查指针或引用的静态类型来推断该转换是否合法。如果在基类中含有一个或多个虚函数,我们可以使用dynamic_cast请求一个类型转换,该转换的安全检查将在运行时执行。同样,如果我们已知某个基类向派生类的转换是安全的,则我们可以使用static_cast来强制覆盖掉编译器的检查工作。
赋值转换
对象之间不存在类型转换
派生类向基类的自动类型转换只针对指针或引用有效,在派生类类型与基类类型之间不存在这样的转换。
当我们在初始化或者赋值一个类类型的时候,我们通常都是调用某个函数,初始化就调用它的构造函数,赋值就调用赋值运算符(重载),这些成员通常都包含一个参数,该参数类型是类类型的const版本引用。
这些成员接收引用作为参数,所以派生类向基类的转换允许我们给基类的拷贝或移动操作传递一个派生类对象,这些操作不是虚函数。当我们给基类的构造函数传递一个派生类对象的时候,实际运行的构造函数是基类定义的那个,显然这个构造函数只能初始化基类本身的成员,赋值也是相同的道理。
比如:
Student b; //派生类对象b
Person a(b); //调用Person::Person(const Person&)构造函数
a=b; //调用Person::operator=(const Person&)赋值重载
构造a的时候,运行Person的拷贝构造函数,只能构造基类部分成员,忽略派生类部分。b赋值给a,也是一样,只赋值b的基类部分给a,派生类多的部分忽略。
在上述过程我们忽略派生类部分的行为,可以说是派生类比基类多的部分切掉了
当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类的基类部分会被拷贝、移动或赋值,它的派生类会被忽略
总结:
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去
- 基类对象不能赋值给派生类对象
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换
Student stu_obj;
// 1.子类对象可以赋值给父类对象/指针/引用
Person per_obj = stu_obj;
Person* pp = &stu_obj;
Person& rp = stu_obj;
//2.基类对象不能赋值给派生类对象
//stu_obj = per_obj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &stu_obj;
Student * ps1 = (Student*)pp; // 这种情况转换时可以的。
pp = &per_obj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
存在继承关系的类型之间转换规则:
- 从派生类向基类的类型转换只对指针和引用有效
- 基类向派生类不存在隐式类型转换
- 和任何其他成员一样,派生类向基类的类型转换也可能由于访问权限的受限而变得不行,具体访问性可看这篇:
尽管自动类型转换只对指针或引用类型有效,但是继承体系中的大多数类仍然(显式或隐式地)定义了拷贝控制成员。因此,我们通常能够将一个派生类对象拷贝、移动或赋值给一个基类对象。不过需要注意的是,这种操作只处理派生类对象的基类部分。
如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀