目录
一、继承的概念
1.继承的基本概念
2.继承的定义和语法
3.继承基类成员访问方式的变化
编辑 4.总结
二、基类和派生类对象赋值转换
三、继承中的作用域
四、派生类的默认成员函数
1.派生类中的默认构造函数
2.派生类中的拷贝构造函数
3.派生类中的移动构造函数
4.派生类的拷贝赋值运算符
5.派生类的移动赋值运算符
6.派生类的析构函数
为什么基类析构函数需要virtual关键字修饰?
理由:多态性和正确的析构顺序
问题:非虚析构函数导致的资源泄漏
总结
五、继承和友元
六、继承与静态成员
七、复杂的菱形继承和菱形虚拟继承
1.单继承
2.多继承
3.菱形继承
菱形继承的问题
4.菱形虚拟继承
5.虚拟继承解决数据冗余和二义性的原理
虚基表的工作机制
一、继承的概念
在C++中,继承是一种面向对象编程的重要特性,它允许一个类(称为派生类或子类)从另一个类(称为基类或父类)继承属性和行为(成员变量和成员函数)。通过继承,派生类不仅可以拥有基类的所有成员,还可以扩展或修改这些成员以提供更具体或特殊的功能。
1.继承的基本概念
- 基类(Base Class):提供基础属性和行为的类。
- 派生类(Derived Class):从基类继承并扩展或修改其功能的类。
- 访问控制(Access Control):
- Public 继承:基类的public和protected成员在派生类中保持其访问级别不变,public成员依然是public,protected成员依然是protected。
- Protected 继承:基类的public和protected成员在派生类中都变为protected成员。
- Private 继承:基类的public和protected成员在派生类中都变为private成员。
- 构造函数和析构函数:派生类的构造函数在执行前会先调用基类的构造函数,析构函数的调用顺序则相反,先调用派生类的析构函数,再调用基类的析构函数。
- 多重继承(Multiple Inheritance):C++允许一个派生类从多个基类继承。
2.继承的定义和语法
class Base {
public:
int baseValue;
void baseFunction() {
// 基类成员函数
}
};
class Derived : public Base {
public:
int derivedValue;
void derivedFunction() {
// 派生类成员函数
}
};
3.继承基类成员访问方式的变化
4.总结
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管是在类内还是在类外都不能去访问它。
- 基类private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是继承才出现的。
- 实际上面的表格我们进行一下总结就能发现,基类的私有成员在子类中都是不可见的。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般都是使用public继承,几乎很少使用protected/private继承,也不提倡使用protected/private继承,因为 protected/private继承下来的成员都只能在派生类的类里使用,实际中扩展维护性不强。
二、基类和派生类对象赋值转换
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
class Base {
public:
int baseValue;
virtual void display() {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base {
public:
int derivedValue;
void display() override {
std::cout << "Derived class" << std::endl;
}
};
Base baseObj;
Derived derivedObj;
baseObj = derivedObj; // 对象切割发生
baseObj.display(); // 输出 "Base class"
在上面的例子中,尽管derivedObj赋值给了baseObj,但baseObj只保留了Base类的部分,派生类的derivedValue被切割掉了,调用display函数时也只会调用基类的版本。
此外,C++允许使用基类的指针或引用来指向派生类对象,这可以实现多态性。多态性允许你通过基类接口调用派生类的重载函数。
Base* basePtr = &derivedObj;
basePtr->display(); // 输出 "Derived class"(多态性)
Base& baseRef = derivedObj;
baseRef.display(); // 输出 "Derived class"(多态性)
在上面代码中,basePtr和baseRef都指向Derived对象,并且调用display方法时,会调用派生类Derived中的版本,这是因为display函数被声明为virtual。(virtual关键字我们下面会讲)
另外还有类型转换:static_cast和dynamic_cast,感兴趣的可以去了解下。
三、继承中的作用域
1. 在继承体系中 基类 和 派生类 都有 独立的作用域 。2. 子类和父类中有同名成员, 子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。 (在子类成员函数中,可以 使用 基类 :: 基类成员 显示访问 )3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。4. 注意在实际中在 继承体系里 面最好 不要定义同名的成员 。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份证号:"<<Person::_num<< endl;
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" <<i<<endl;
}
};
void Test()
{
B b;
b.fun(10);
};
四、派生类的默认成员函数
在之前的学习中, 我们知道类可以自动生成一些默认的成员函数,这些成员函数包括默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数。而对于派生类,这些默认成员函数的生成和行为有一些特殊的规则和注意事项,下面我讲详细介绍。
1.派生类中的默认构造函数
默认构造函数在没有用户定义的构造函数时自动生成。对于派生类的默认构造函数,它会调用基类的默认构造函数(如果存在),然后初始化派生类的成员。而如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
class Base {
public:
Base() {
std::cout << "Base default constructor" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived default constructor" << std::endl;
}
};
int main() {
Derived d; // 输出:Base default constructor
// Derived default constructor
return 0;
}
2.派生类中的拷贝构造函数
拷贝构造函数在没有用户定义的情况下自动生成,用于创建类的对象副本。派生类的拷贝构造函数会首先调用基类的拷贝构造函数,然后复制派生类的成员。
class Base {
public:
Base(const Base&) {
std::cout << "Base copy constructor" << std::endl;
}
};
class Derived : public Base {
public:
Derived(const Derived& other) : Base(other) {
std::cout << "Derived copy constructor" << std::endl;
}
};
int main() {
Derived d1;
Derived d2 = d1; // 输出:Base copy constructor
// Derived copy constructor
return 0;
}
3.派生类中的移动构造函数
移动构造函数在没有用户定义的情况下自动生成,用于移动资源所有权。派生类的移动构造函数会首先调用基类的移动构造函数,然后移动派生类的成员。
class Base {
public:
Base(Base&&) noexcept {
std::cout << "Base move constructor" << std::endl;
}
};
class Derived : public Base {
public:
Derived(Derived&& other) noexcept : Base(std::move(other)) {
std::cout << "Derived move constructor" << std::endl;
}
};
int main() {
Derived d1;
Derived d2 = std::move(d1); // 输出:Base move constructor
// Derived move constructor
return 0;
}
4.派生类的拷贝赋值运算符
拷贝赋值运算符在没有用户定义的情况下自动生成,用于将一个对象的内容赋值给另一个对象。派生类的拷贝赋值运算符会首先调用基类的拷贝赋值运算符,然后赋值派生类的成员。
class Base {
public:
Base& operator=(const Base&) {
std::cout << "Base copy assignment operator" << std::endl;
return *this;
}
};
class Derived : public Base {
public:
Derived& operator=(const Derived& other) {
Base::operator=(other);
std::cout << "Derived copy assignment operator" << std::endl;
return *this;
}
};
int main() {
Derived d1, d2;
d1 = d2; // 输出:Base copy assignment operator
// Derived copy assignment operator
return 0;
}
5.派生类的移动赋值运算符
移动赋值运算符在没有用户定义的情况下自动生成,用于将一个对象的内容移动到另一个对象。派生类的移动赋值运算符会首先调用基类的移动赋值运算符,然后移动派生类的成员。
class Base {
public:
Base& operator=(Base&&) noexcept {
std::cout << "Base move assignment operator" << std::endl;
return *this;
}
};
class Derived : public Base {
public:
Derived& operator=(Derived&& other) noexcept {
Base::operator=(std::move(other));
std::cout << "Derived move assignment operator" << std::endl;
return *this;
}
};
int main() {
Derived d1, d2;
d1 = std::move(d2); // 输出:Base move assignment operator
// Derived move assignment operator
return 0;
}
6.派生类的析构函数
析构函数在没有用户定义的情况下自动生成,用于清理对象。派生类的析构函数会首先调用派生类的析构函数,然后调用基类的析构函数。
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* b = new Derived();
delete b; // 输出:Derived destructor
// Base destructor
return 0;
}
为什么基类析构函数需要virtual关键字修饰?
基类的析构函数需要加virtual关键字是为了确保在删除派生类对象时能够正确调用析构函数。这是一个非常重要的概念,尤其是在使用多态性和通过基类指针或引用操作派生类对象时。
理由:多态性和正确的析构顺序
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
当基类的析构函数是虚函数时,通过基类指针删除派生类对象时,C++会首先调用派生类的析构函数,然后再调用基类的析构函数。这确保了派生类中分配的资源可以先被正确释放,再释放基类中分配的资源。
int main() {
Base* b = new Derived();
delete b; // 输出顺序:Derived destructor
// Base destructor
return 0;
}
问题:非虚析构函数导致的资源泄漏
如果基类的析构函数不是虚函数,则通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致派生类中的资源没有被正确释放,造成资源泄漏。
class Base {
public:
~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* b = new Derived();
delete b; // 只输出:Base destructor
return 0;
}
在上述代码中,由于 Base
类的析构函数不是虚函数,删除 b
时只调用了 Base
的析构函数,Derived
类的析构函数没有被调用,这会导致 Derived
类中的资源没有被正确释放。
总结
为了确保在使用多态性时派生类对象可以被正确地销毁,避免资源泄漏,基类的析构函数应该声明为虚函数。这一做法可以保证删除派生类对象时,派生类和基类的析构函数都能被正确调用。以下是总结的要点:
- 多态性支持:使用基类指针或引用操作派生类对象时,确保正确调用派生类的析构函数。
- 正确的析构顺序:先调用派生类的析构函数,再调用基类的析构函数,确保资源正确释放。
- 避免资源泄漏:防止派生类中的资源没有被释放,导致内存泄漏或其他资源泄漏。
五、继承和友元
友元关系是单向的和局部的,友元关系不能继承!也就是说基类友元不能访问子类私有和保护成员。
#include <iostream>
// 基类
class Base {
private:
int basePrivateVar;
protected:
int baseProtectedVar;
public:
int basePublicVar;
Base() : basePrivateVar(1), baseProtectedVar(2), basePublicVar(3) {}
friend void baseFriendFunction(Base &obj);
};
// 基类的友元函数
void baseFriendFunction(Base &obj) {
std::cout << "Base Private Var: " << obj.basePrivateVar << std::endl;
std::cout << "Base Protected Var: " << obj.baseProtectedVar << std::endl;
}
// 派生类
class Derived : public Base {
private:
int derivedPrivateVar;
protected:
int derivedProtectedVar;
public:
int derivedPublicVar;
Derived() : derivedPrivateVar(4), derivedProtectedVar(5), derivedPublicVar(6) {}
friend void derivedFriendFunction(Derived &obj);
};
// 派生类的友元函数
void derivedFriendFunction(Derived &obj) {
// 基类的友元不能访问派生类的私有或保护成员
// std::cout << "Derived Private Var: " << obj.derivedPrivateVar << std::endl; // 错误
// std::cout << "Derived Protected Var: " << obj.derivedProtectedVar << std::endl; // 错误
std::cout << "Derived Public Var: " << obj.derivedPublicVar << std::endl;
}
int main() {
Base baseObj;
Derived derivedObj;
baseFriendFunction(baseObj); // 可以访问Base类的私有和保护成员
derivedFriendFunction(derivedObj); // 可以访问Derived类的公共成员
// 基类的友元函数不能访问派生类的私有和保护成员
// baseFriendFunction(derivedObj); // 错误
return 0;
}
六、继承与静态成员
基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
静态成员变量需要在类外进行定义和初始化。静态成员函数则不需要在类外定义。
#include <iostream>
// 基类
class Base {
public:
static int staticVar; // 声明静态成员变量
static void staticFunction() { // 声明并定义静态成员函数
std::cout << "Static Function in Base" << std::endl;
}
};
// 定义静态成员变量
int Base::staticVar = 10;
// 派生类
class Derived : public Base {
public:
void display() {
std::cout << "Base staticVar: " << staticVar << std::endl; // 访问基类的静态成员变量
staticFunction(); // 调用基类的静态成员函数
}
};
int main() {
Derived obj;
obj.display();
// 静态成员可以通过类名直接访问
Base::staticVar = 20;
Derived::staticVar = 30;
std::cout << "Base staticVar after modification: " << Base::staticVar << std::endl;
std::cout << "Derived staticVar after modification: " << Derived::staticVar << std::endl;
return 0;
}
静态成员的特点
- 类共享性:所有类的对象共享同一个静态成员变量。
- 类作用域:静态成员变量和静态成员函数在类作用域内,但可以通过类名直接访问。
- 内存管理:静态成员变量在程序启动时分配内存,程序结束时释放内存。
注意:
- 静态成员函数:静态成员函数不能访问非静态成员变量和非静态成员函数,因为它们属于类本身,而不是类的某个对象。但是静态成员函数可以访问静态成员变量和其他静态成员函数。
七、复杂的菱形继承和菱形虚拟继承
1.单继承
一个子类只有一个直接父类时称这个继承关系为单继承。
2.多继承
一个子类有两个或以上直接父类时称这个继承关系为多继承
3.菱形继承
菱形继承(也称钻石继承)是指一种特殊的多继承情况,其中一个类从两个基类继承,而这两个基类又继承自同一个祖先类。这种继承关系形成了一个菱形结构。
菱形继承的问题
从上面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。
#include <iostream>
// 祖先类
class A {
public:
int value;
A() : value(0) {}
};
// 两个派生类继承自 A
class B : public A {};
class C : public A {};
// 派生类 D 同时继承自 B 和 C
class D : public B, public C {};
int main() {
D obj;
// obj.value; // 错误:二义性问题,不知道是从 B 继承的 A 还是从 C 继承的 A
// 解决方法之一是明确指定路径
obj.B::value = 1;
obj.C::value = 2;
std::cout << "obj.B::value: " << obj.B::value << std::endl;
std::cout << "obj.C::value: " << obj.C::value << std::endl;
return 0;
}
4.菱形虚拟继承
为了解决上面菱形继承所带来的问题,我们可以使用虚拟继承。虚拟继承确保在菱形继承结构中只存在一个基类的实例。
如在上面的代码中,我们可以在B和C继承A的时候使用虚拟继承,即
class B : virtual public A {};
class C : virtual public A {};
需要注意的是,虚拟继承不要在其他地方去使用。
5.虚拟继承解决数据冗余和二义性的原理
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
下面是菱形继承的内存对象成员模型:这里可以看到数据冗余
下面是菱形虚拟继承的内存对象成员模型:
这里可以分析出D对象中将A放到了D对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存储的是偏移量,通过偏移量就能找到A 。
总结:
虚拟继承通过确保每个虚拟基类在派生类中只有一个共享实例,从而避免了重复实例化和二义性问题。为了实现这一点,编译器会使用虚基表来跟踪和管理虚拟基类的实例。
虚基表的工作机制
虚基表的引入: 每个使用虚拟继承的类会包含一个虚基表指针。这个指针指向一个虚基表,该表包含虚拟基类的指针。
共享基类实例: 在派生类(如
D
)的对象中,虚基表指针确保所有虚拟基类实例都指向同一个实际基类实例。这意味着D
中只有一个A
类的实例。成员访问的重定向: 在访问基类成员时,编译器使用虚基表来正确地定位基类成员,确保访问的是唯一的基类实例。
所以,当一个类虚拟继承另一个类时,编译器在对象布局中插入一个虚基表指针(vbptr)。这个指针指向一个虚基表(vbtbl),而虚基表中包含指向虚拟基类的偏移量或地址。通过这种方式,每个派生类能够正确地定位并访问唯一的虚拟基类实例。
上面就是我们对C++继承的全部理解了~