一、继承的本质和原理
组合(Composition): 组合是一种"有一个"的关系,表示一个类包含另一个类的对象作为其成员。这意味着一个类的对象包含另一个类的对象作为其一部分。组合关系通常表示强关联,被包含的对象的生命周期通常受到包含它的对象的生命周期的限制。
示例:
class Engine {
// Engine class definition
};
class Car {
Engine engine; // Car "has a" Engine
// Other members and methods
};
继承(Inheritance): 继承是一种"是一个"的关系,表示一个类可以继承另一个类的属性和行为。子类(派生类)可以继承父类(基类)的成员和方法,并且可以添加新的成员和方法。继承关系建立了一个类层次结构,允许代码重用和实现多态。
示例:
class Animal {
// Animal class definition
};
class Dog : public Animal { // Dog "is a" Animal
// Dog-specific members and methods
};
在C++中,
struct
和class
都是用于定义自定义类型(类)的关键字,它们有一些相同点和区别。相同点:
成员变量和成员函数:
struct
和class
都可以包含成员变量和成员函数。访问控制:
struct
和class
都支持访问控制修饰符public
、protected
和private
,用于控制成员的访问权限。封装性:
struct
和class
都支持封装性,即将数据和操作封装在一个单元中,隐藏内部实现细节,对外提供接口。继承:
struct
和class
都支持继承,可以通过派生类继承基类的成员和方法。区别:
默认访问控制:在
struct
中,默认的成员访问级别是public
,而在class
中,默认的成员访问级别是private
。这意味着,struct
中定义的成员默认是公共的,而class
中定义的成员默认是私有的。继承访问控制:当使用
class
来定义一个继承关系时,默认的继承访问级别是private
,而使用struct
时,默认的继承访问级别是public
。这意味着,如果你从class
继承,派生类的成员默认为private
;如果你从struct
继承,派生类的成员默认为public
。用法习惯:一般来说,
struct
更适合用来表示简单的数据结构,成员默认是公共的,不涉及复杂的封装;而class
更适合用来表示有复杂行为的对象,需要进行更严格的封装和访问控制。
继承的本质可以总结如下:
代码重用:子类可以继承父类的成员变量和成员函数,无需重新实现相同的功能,从而实现代码的重用。
扩展性:子类可以添加新的成员变量和成员函数,从而扩展父类的功能,使其具有更多的行为。
多态性:继承是实现多态的基础。通过基类的指针或引用指向子类的对象,可以实现运行时多态性,即在运行时根据对象的实际类型调用相应的方法。
继承链:继承关系可以形成继承链,允许多层次的继承,子类可以继承父类的属性和行为,而子类的子类又可以继承子类的属性和行为,以此类推。
二、 派生类的构造过程
三、重载、隐藏、覆盖
1. 函数的重载:
函数重载(Function Overloading)是指在同一个作用域内,可以定义多个函数,它们具有相同的名称但是参数列表不同(参数的类型、个数或顺序不同),编译器根据调用时提供的参数来确定具体调用哪个函数。
函数重载的主要特点包括:
- 相同的函数名:重载的函数具有相同的函数名。
- 不同的参数列表:重载的函数具有不同的参数列表,可以是参数的类型、个数或顺序不同。
- 在同一个作用域内:重载的函数必须在同一个作用域内定义。
函数重载使得函数命名更加灵活,可以根据函数的功能和参数的类型选择合适的函数来调用,从而提高了代码的可读性和灵活性。
#include <iostream>
// 函数重载示例
int add(int x, int y) {
return x + y;
}
double add(double x, double y) {
return x + y;
}
int add(int x, int y, int z) {
return x + y + z;
}
int main() {
std::cout << "Sum of 3 and 5 is: " << add(3, 5) << std::endl; // 调用第一个 add 函数
std::cout << "Sum of 3.5 and 2.5 is: " << add(3.5, 2.5) << std::endl; // 调用第二个 add 函数
std::cout << "Sum of 2, 4, and 6 is: " << add(2, 4, 6) << std::endl; // 调用第三个 add 函数
return 0;
}
2 基类和派生类
基类和派生类的show()不能说是重载,因为作用域不同。其是隐藏关系(隐藏的是作用域)。 如果派生类中没有show()函数,则可以调用基类的show()方法,但是如果派生类中存在show()同名的,不管参数列表,会将基类的show隐藏,包括基类中show函数的全部重载。
3.继承的相互转换
指针的类型限制了指针的解引用能力
四、虚函数、静态绑定和动态绑定
1.typeid
在C++中,typeid
是一个操作符,用于获取一个表达式的类型信息。它返回一个std::type_info
对象,该对象包含有关表达式类型的信息,如类名、基类等。std::type_info
是一个标准库类型,定义在 <typeinfo>
头文件中。通常,typeid
主要用于运行时类型识别(RTTI)和多态代码中。
例如,你可以使用 typeid
来比较两个对象的类型是否相同,或者在运行时查找对象的实际类型以执行相应的操作。以下是一个示例:
#include <iostream>
#include <typeinfo>
class Base {
virtual void foo() {}
};
class Derived : public Base {};
int main() {
Base* basePtr = new Derived();
if (typeid(*basePtr) == typeid(Derived)) {
std::cout << "basePtr 指向的对象是 Derived 类型" << std::endl;
} else {
std::cout << "basePtr 指向的对象不是 Derived 类型" << std::endl;
}
delete basePtr;
return 0;
}
2. 静态绑定,没有虚函数,编译阶段就确定调用
3.虚函数和虚函数表
这里的show()加不加virtual,都是虚函数
虚函数表(vtable)和虚函数表指针(vptr)是用于实现C++中虚函数机制的重要组成部分。它们的创建时机如下:
-
虚函数表(vtable)的创建时机:
- 虚函数表是在编译阶段由编译器生成的。当一个类中包含至少一个虚函数时,编译器会在编译时生成该类的虚函数表。虚函数表是一个存储了该类中所有虚函数地址的数组,每个虚函数在表中占据一个位置,编译器会根据虚函数声明的顺序依次将其地址存入虚函数表中。
-
虚函数表指针(vptr)的创建时机:
- 虚函数表指针是在运行时由编译器插入到对象的内存布局中的。当一个类包含虚函数时,每个对象都会包含一个虚函数表指针,用来指向该类的虚函数表。这个虚函数表指针(vptr)是在对象创建时初始化的,通常是在构造函数中完成初始化。当对象被销毁时,虚函数表指针也会被销毁。
4. 动态绑定
首先看指针的类型,然后看调用函数在父类中是正常的函数还是虚函数,如果是正常函数在编译的时候就知道了,如果是虚函数,先要查找子类的虚函数表(因为虚函数会被重写或者覆盖),然后调用函数,
【C++】RTTI有什么用?怎么用? - 知乎 (zhihu.com)
五、虚析构函数(new出来的派生类对象)
在C++中,构造函数不能声明为虚函数。这是因为在构造对象时,需要确定构造函数的调用路径。如果构造函数是虚的,那么在构造对象时,需要在虚函数表中查找适当的构造函数。但是,在对象构造过程中,虚函数表尚未被设置,因此无法进行动态绑定。
先构造函数,才有对象。
静态成员方法不依赖于对象,就不会去对象中查询虚函数指针,不会去查虚函数表
基类的析构函数会在派生类的析构函数执行完毕后被隐式调用。这是因为在派生类的析构函数中,会自动调用其直接基类的析构函数,然后依次向上调用每个基类的析构函数,直到调用完毕为止。
六、多态
七、抽象类
八、类型转换
九、多继承
1 虚继承和虚继承
1 是因为基类没有虚函数,才建的自己的虚函数指针和虚函数表
2. 是继承A的虚函数表和虚函数指针
2. 菱形继承
虚基类的数据搬到派生类中最后面,在原来的地方补vbptr