1.为什么需要继承
一个类继承另一个类,这样类中可以少定义一些成员,直接定义类,代码重复比较严重。
c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。
一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类),类B成为派生类(子类)。派生类中的成员,包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员。
从基类继承过过来的表现其共性,而新增的成员体现了其个性。
2.派生类定义格式
class 派生类名:继承方式基类名
派生类新增的数据成员和成员函数
class DerivedClass : [访问修饰符] BaseClass {
// 派生类的成员声明
public:
// 派生类的公有成员
protected:
// 派生类的保护成员
private:
// 派生类的私有成员
};
DerivedClass
是派生类的名称。[访问修饰符]
可以是public
、protected
或private
,用于指定派生类继承基类的成员的访问级别。这个部分可以省略,默认为private
继承。BaseClass
是基类的名称,表示派生类从该基类继承成员。
下面是一个例子,演示了派生类的定义:
class BaseClass {
public:
int basePublic;
protected:
int baseProtected;
private:
int basePrivate;
};
class DerivedClass : public BaseClass {
public:
int derivedPublic;
};
int main() {
DerivedClass obj;
obj.basePublic; // 可以访问公有成员
obj.baseProtected; // 可以访问保护成员,因为是公有继承
// obj.basePrivate; // 无法访问私有成员
obj.derivedPublic; // 可以访问派生类的公有成员
return 0;
}
DerivedClass
公有继承了 BaseClass
,因此在 DerivedClass
中可以访问 BaseClass
的公有和保护成员。
3.三种继承方式:
- public :公有继承
- private :私有继承
- protected :保护继承
从继承源上分:
- 单继承:指每个派生类只直接继承了一个基类的特征
- 多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征。
派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。
派生类的访问权限规则如下:
4.继承中的对象模型:
在C++编译器的内部可以理解为结构休,子类是由父类成员叠加子类新成员而成:
#include <iostream>
class AcClass {
public:
int mA;
int mB;
};
class BcClass : public AcClass {
public:
int mC;
};
class CClass : public BcClass {
public:
int mD;
};
void test() {
std::cout << "A size: " << sizeof(AcClass) << std::endl;
std::cout << "B size: " << sizeof(BcClass) << std::endl;
std::cout << "C size: " << sizeof(CClass) << std::endl;
}
int main() {
test();
return 0;
}
这段代码涉及了三个类:`AcClass`、`BcClass` 和 `CClass`,它们之间通过继承建立了一种层次关系。
- AcClass` 类:具有两个公有成员变量 `mA` 和 `mB`。
- `BcClass` 类:公有继承了 `AcClass` 类,因此继承了 `AcClass` 的成员变量 `mA` 和 `mB`。增加了自己的成员变量 `mC`。
- `CClass` 类:公有继承了 `BcClass` 类,因此继承了 `AcClass` 的成员变量 `mA` 和 `mB`,以及 `BcClass` 的成员变量 `mC`。增加了自己的成员变量 `mD`。
这种继承关系形成了一个类的层次结构,其中子类(派生类)`BcClass` 和 `CClass` 继承了父类(基类)`AcClass` 的成员,同时可以添加自己的成员。这有助于代码的组织和重用,使得相关的功能和数据可以被逻辑地组织在一起。
5.对象构造和析构的调用原则
继承中的构造和析构
- 子类对象在创建时会首先调用父类的构造函数
- 父类构造函数执行完毕后,才会调用子类的构造函数
- 当父类构造函数有参数时,需要在了类初始化列表(参数列表)中显示调用父类构造函数析构函数调用顺序和构造函数相反。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
using namespace std;
class Base {
public:
Base(int age, string name) {
this->age = age;
this->name = name;
cout << "Base的构造函数" << endl;
}
~Base() {
cout << "Base的析构函数" << endl;
}
string name;
int age;
};
class Son : public Base {
public:
Son(int id, int age, string name) : Base(age, name) {
this->id = id;
cout << "Son的构造函数" << endl;
}
~Son() {
cout << "Son的析构函数" << endl;
}
int id;
};
void test01() {
Son p(19, 18, "Lucy");
}
int main() {
test01();
return 0;
}
6.继承中同名成员的处理方法
- 当子类成员和父类成员同名时,子类依然从父类继承同名成员
- 如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域就近原则)
- 在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)
如果子类和父类有同名的成员变量,父类的成员变量会被隐藏,访问的是子类的成员变量如果子类和父类有同名的成员函数,父类的成员函数会被隐藏,访问的是子类的成员函数
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <string>
using namespace std;
class Base {
public:
Base(int a) {
this->a = a;
}
void foo() {
cout << "父类的foo函数" << endl;
}
int a;
};
class Son : public Base {
public:
Son(int a1, int a2) : Base(a1) {
this->a = a2;
}
void foo() {
cout << "子类的foo函数" << endl;
}
int a;
};
void test01() {
Son p(10, 20);
// 如果子类和父类有同名的成员变量,父类的成员变量会被隐藏,访问的是子类的成员变量
// 如果子类和父类有同名的成员函数,父类的成员函数会被隐藏,访问的是子类的成员函数
cout << p.a << endl;
p.foo();
}
int main() {
test01();
return 0;
}
7.非自动继承的函数
- 不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。
- 另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。
在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。
8.继承中的静态成员特性
静态成员函数和非静态成员函数的共同点:
- 他们都可以被继承到派生类中。
- 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。
- 如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。
#include <iostream>
using namespace std;
class Base {
public:
static int getNum() {
return sNum;
}
static int getNum(int param) {
return sNum + param;
}
static int sNum;
};
int Base::sNum = 10;
class Derived : public Base {
public:
static int sNum;
// 基类静态成员属性将被隐藏
// 如果定义了相同的函数名,则基类中的所有同名函数都会被隐藏
// static int getNum(int param1, int param2) {
// return sNum + param1 + param2;
// }
// 如果改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类的同名函数
static void getNum(int param1, int param2) {
cout << sNum + param1 + param2 << endl;
}
};
int Derived::sNum = 20;
void test01() {
Derived p1;
// 如果子类和父类有同名的静态成员变量, 父类中的静态成员变量会被隐藏
cout << p1.sNum << endl;
}
int main() {
test01();
return 0;
}
![](https://i-blog.csdnimg.cn/blog_migrate/dbb8612768940be1a1e0b38df6dc1593.jpeg)
9. 多继承概念
我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <string>
using namespace std;
class A {
public:
int a;
};
class B {
public:
int a;
};
class C : public A, public B {
public:
int c;
};
int main() {
C obj;
obj.A::a = 1; // Accessing 'a' from class A
obj.B::a = 2; // Accessing 'a' from class B
obj.c = 3;
cout << "A::a: " << obj.A::a << endl;
cout << "B::a: " << obj.B::a << endl;
cout << "C::c: " << obj.c << endl;
return 0;
}
10.菱形继承和虚继承
两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。
基本结构:
GrandParent
/ \
Father Mother
\ /
GrandSon
11.二义性问题:
当GrandSon继承了Father和Mother,而Father和Mother都继承了GrandParent时,GrandSon中可能存在两份GrandParent的数据成员,引发二义性问题。
- 资源浪费: 如果不采用虚继承,GrandSon内可能包含两份相同的GrandParent部分,导致资源浪费。
虚继承的应用:
- 解决二义性问题: 通过使用virtual关键字进行虚继承,可以确保在最终的派生类中只包含一份共同基类的实例,解决了二义性问题。
class GrandParent { /* ... */ };
class Father : virtual public GrandParent { /* ... */ };
class Mother : virtual public GrandParent { /* ... */ };
class GrandSon : public Father, public Mother { /* ... */ };
- 资源节省: 虚继承避免了派生类中包含多份相同基类的实例,节省了内存空间,避免了资源浪费。
- 多重继承的合理应用: 在某些情况下,多重继承是必要的,虚继承使得多重继承更加灵活和可控。
虚继承是通过在基类前加上virtual关键字实现的,这告诉编译器在继承链中只保留一份虚基类的实例。通过合理使用虚继承,可以解决菱形继承带来的问题,确保继承体系的正确性和高效性。
#include <iostream>
#include <string>
using namespace std;
class animal
{
public:
int age;
};
class sheep : virtual public animal
{
public:
int id;
};
class camel : virtual public animal
{
public:
int camel_num;
};
class shenshou : public sheep, public camel
{
public:
int a;
};
void teste1()
{
shenshou p;
// p.age = 100; // Sheep's age is ambiguous due to multiple inheritance
p.sheep::age = 100; // Set sheep's age
p.age = 108; // Set camel's age
}
int main()
{
teste1();
return 0;
}
12.虚继承的实现原理
虚继承的实现原理涉及到虚基类表(Virtual Base Class Table)和虚基类指针(Virtual Base Class Pointer)的概念。:
-
虚基类表(Virtual Base Class Table):
- 对于每个包含虚基类的类,编译器会在对象的内存布局中插入一个指向虚基类表的指针,该表记录了该类所继承的虚基类的偏移量。
- 虚基类表的内容包括虚基类的偏移量和其他信息,用于在运行时进行动态调整。
-
虚基类指针(Virtual Base Class Pointer):
- 对于每个包含虚基类的对象,编译器会在对象的内存布局中插入一个虚基类指针,指向虚基类表。
- 这个指针用于在运行时查找虚基类的实际位置。
-
虚继承的对象内存布局:
- 在虚继承中,对于共同的虚基类,只会在派生类的对象中存在一份实例,而不是每个派生类都包含一份。这避免了菱形继承带来的二义性和资源浪费问题。
- 派生类中的虚基类的成员在内存中的位置通过虚基类表和虚基类指针进行调整,确保访问到正确的虚基类成员。
-
构造函数和析构函数的调用:
- 在虚继承中,派生类的构造函数和析构函数中会负责调用虚基类的构造函数和析构函数,并根据虚基类指针和虚基类表的信息进行调整。
class Base {
public:
int commonData;
};
class Derived1 : public virtual Base {
public:
int derivedData1;
};
class Derived2 : public virtual Base {
public:
int derivedData2;
};
class FinalDerived : public Derived1, public Derived2 {
public:
int finalData;
};
int main() {
FinalDerived obj;
obj.commonData = 10; // Accessing common data through the final derived class
return 0;
}
在这个例子中,FinalDerived
通过虚继承继承了Base
类,确保在内存中只有一份Base
类的实例。虚继承通过虚基类表和虚基类指针的机制,解决了菱形继承可能引起的问题。