继承和派生
继承和派生是密切相关的两个概念,从不同的视角来看待父类与子类的关系。继承是从父类(基类)角度描述的,它强调的是子类可以复用父类的成员。换句话说,父类为子类提供了功能,并且这种功能可以被扩展或修改;派生是从子类角度描述的,它强调的是子类是从父类生成的,并在父类的基础上扩展或修改功能。
1. 基本语法
1 | class 基类名 { |
访问控制符可以是public、protected、private,分别表示公有继承、保护继承、私有继承。
2. 继承方式
继承方式决定了父类成员在子类中的访问权限,包括公有继承、保护继承、私有继承。
- public继承:基类的公有成员和保护成员成为派生类的公有成员和保护成员,基类的私有成员派生类不可访问。
- protected继承:基类的公有成员和保护成员成为派生类的保护成员,基类的私有成员派生类不可访问。
- private继承:基类的公有成员和保护成员成为派生类的私有成员,基类的私有成员派生类不可访问。
3. 继承与派生中的构造函数
构造函数的调用顺序
当创建一个派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。
这是因为派生类继承了基类的成员,因此派生类对象的创建需要先初始化基类部分。基类构造函数的调用方式
- 如果派生类没有定义构造函数,则编译器会自动生成一个默认的构造函数,该构造函数会自动调用基类的默认构造函数(前提是基类有默认的构造函数)。
- 如果派生类定义了构造函数:
- 如果基类有默认构造函数,派生类可以不显式调用基类的构造函数。编译器会自动调用基类的默认构造函数。
- 如果基类只有带参数的构造函数,派生类必须显式调用基类的构造函数,否则编译器会报错,因为它不知道该如何初始化基类部分。
1 |
|
4. 继承与派生中的析构函数
析构函数的调用顺序
当派生类对象销毁时,先调用派生类的析构函数,再调用基类的析构函数。
这是为了确保派生类对象的资源(如动态内存)能够先释放,再释放基类资源。虚析构函数
如果你使用了继承,并且派生类需要对基类的析构进行重载,建议将基类的析构函数声明为虚函数。这样可以确保当通过基类指针删除派生类对象时,派生类的析构函数也能正确调用,
从而避免资源泄露。
1 |
|
5. 继承和派生中的复用
复用 是继承和派生的重要特点之一,它允许派生类直接使用基类已有的代码,而无需重新编写。
5.1. 复用
派生类会继承基类的属性(成员变量)和方法(成员函数),根据访问权限,派生类可以直接复用基类的成员,或者通过间接访问实现复用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using namespace std;
class Animal {
public:
void eat() { cout << "I can eat!" << endl; }
void sleep() { cout << "I can sleep!" << endl; }
};
class Dog : public Animal {
public:
void bark() { cout << "I can bark! Woof woof!" << endl; }
};
int main() {
Dog dog;
dog.eat(); // 复用基类方法
dog.sleep(); // 复用基类方法
dog.bark(); // 扩展新功能
return 0;
}
5.2. 组合继承与复用
虽然继承支持代码复用,但有时会引发一些问题,如脆弱基类问题(基类的改动可能导致派生类问题)。此时可以通过组合代替部分继承,避免不必要的耦合。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Engine {
public:
void start() { cout << "Engine started" << endl; }
};
class Car {
private:
Engine engine; // 组合:Car 包含 Engine
public:
void drive() {
engine.start(); // 复用 Engine 的功能
cout << "Car is driving" << endl;
}
};
int main() {
Car car;
car.drive();
return 0;
}
/* 输出
Engine started
Car is driving
*/
6. 继承和派生中的重载
6.1. 函数重载
函数重载是指在同一个作用域内定义多个同名函数,但这些函数的参数数量或类型不同。它是静态多态的一种形式,因为调用哪个函数是在编译时决定的。
继承与派生关系中,如果基类和派生类中有同名但不同参数的函数,这依然是函数重载,而不是多态。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using namespace std;
class Base {
public:
void display() {
cout << "Base: display()" << endl;
}
void display(int x) {
cout << "Base: display(int x) with x = " << x << endl;
}
};
class Derived : public Base {
public:
void display(double y) {
cout << "Derived: display(double y) with y = " << y << endl;
}
};
int main() {
Derived obj;
obj.display(); // 错误!派生类隐藏了基类的同名函数
obj.display(10); // 错误!派生类隐藏了基类的同名函数
obj.display(3.14); // 正确!调用派生类的重载函数
return 0;
}
如果没有特别处理,基类的同名函数会被派生类隐藏,即使参数不同。
为了解决这个问题,可以显式地引入基类的函数:1
2
3
4
5
6
7class Derived : public Base {
public:
using Base::display; // 显式引入基类的 display() 方法
void display(double y) {
cout << "Derived: display(double y) with y = " << y << endl;
}
};
结果1
2
3
4Derived obj;
obj.display(); // 调用 Base 的 display()
obj.display(10); // 调用 Base 的 display(int)
obj.display(3.14); // 调用 Derived 的 display(double)
6.2. 函数重写 (动态多态)
函数重写 指派生类重新定义基类中的虚函数。
- 重写要求函数名、参数列表和返回类型完全一致。
- 基类中的函数必须声明为
virtual
,才能被派生类重写。 - 函数重写是多态的基础,派生类的实现可以通过基类指针或引用调用。
1 |
|
如果基类中的虚函数没有具体实现,可以将其声明为纯虚函数,派生类必须重写该函数。
6.3. 运算符重载 (静态多态的一种形式)
运算符重载是函数重载的一种特殊形式,派生类可以重载基类的运算符。
7. 多继承
多继承 是指一个派生类同时继承多个基类,从而能够复用多个基类的功能。C++ 支持多继承,这是与一些其他编程语言(如 Java)的重要区别之一。
7.1. 多继承的语法
1 | class Base1 { |
7.2. 多继承中的冲突
7.2.1. 名字冲突
如果基类之间有相同名字的成员函数或变量,派生类中访问这些成员可能会产生二义性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Base1 {
public:
void show() { cout << "Base1 show" << endl; }
};
class Base2 {
public:
void show() { cout << "Base2 show" << endl; }
};
class Derived : public Base1, public Base2 {
};
int main() {
Derived obj;
// obj.show(); // 编译错误:二义性
obj.Base1::show(); // 通过基类名解决二义性
obj.Base2::show(); // 指定调用 Base2 的 show
return 0;
}
7.2.2. 菱形继承问题
菱形继承指的是派生类从两个基类继承,而这两个基类又继承自同一个基类。这会导致基类的成员在派生类中存在多份副本,可能引发冲突。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class A {
public:
void show() { cout << "A show" << endl; }
};
class B : public A {
};
class C : public A {
};
class D : public B, public C {
};
int main() {
D obj;
// obj.show(); // 编译错误:二义性,A 的成员有两份
obj.B::show(); // 指定调用 B 中的 A
obj.C::show(); // 指定调用 C 中的 A
return 0;
}
为了解决这个问题,C++ 提供了虚继承(virtual inheritance)机制,使得派生类只继承基类的一份成员。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class A {
public:
void show() { cout << "A show" << endl; }
};
class B : virtual public A {
};
class C : virtual public A {
};
class D : public B, public C {
};
int main() {
D obj;
obj.show(); // 正确,调用 A 的 show
return 0;
}
7.2.3. 虚基类
虚基类 是 C++ 中用于解决菱形继承问题和确保唯一实例的机制。它通过使一个基类在派生类中只保留一份实例,避免了重复继承引发的冲突和资源浪费。
虚基类通过在继承声明中使用 virtual
关键字来指定。例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class A {
public:
void show() { cout << "A show" << endl; }
};
class B : virtual public A {
};
class C : virtual public A {
};
class D : public B, public C {
};
int main() {
D obj;
obj.show(); // 正确,调用 A 的 show
return 0;
}
构造函数与虚基类
- 虚基类的构造函数由最派生类负责调用。
- 即使多个中间基类继承虚基类,也只会构造一份虚基类实例。
- 调用顺序:虚基类的构造函数在派生类构造函数之前被调用。
虚基类成员初始化
派生类需要通过自己的构造函数初始化虚基类的成员。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34class A {
public:
int value;
A(int v) : value(v) { cout << "A constructor with value = " << value << endl; }
};
class B : virtual public A {
public:
B() : A(0) { cout << "B constructor" << endl; }
};
class C : virtual public A {
public:
C() : A(0) { cout << "C constructor" << endl; }
};
class D : public B, public C {
public:
D() : A(42), B(), C() { cout << "D constructor" << endl; } // 初始化基类成员
};
int main() {
D obj;
cout << "D's A::value = " << obj.value << endl;
return 0;
}
/* 输出
A constructor with value = 42
B constructor
C constructor
D constructor
D's A::value = 42
*/
7.3. 多继承中的构造和析构
在多继承中,构造函数的调用顺序按照基类在继承列表中的声明顺序执行。对于虚基类,无论声明顺序如何,总是在最派生类(最终派生类)中初始化。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using namespace std;
class A {
public:
A() { cout << "A constructor" << endl; }
};
class B : public A {
public:
B() { cout << "B constructor" << endl; }
};
class C : public A {
public:
C() { cout << "C constructor" << endl; }
};
class D : public B, public C {
public:
D() { cout << "D constructor" << endl; }
};
int main() {
D obj;
return 0;
}
/* 输出
A constructor
B constructor
A constructor
C constructor
D constructor
*/
可以看到,每个基类都调用 A 的构造函数,导致重复初始化。
解决方法:虚基类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class A {
public:
A() { cout << "A constructor" << endl; }
};
class B : virtual public A {
public:
B() { cout << "B constructor" << endl; }
};
class C : virtual public A {
public:
C() { cout << "C constructor" << endl; }
};
class D : public B, public C {
public:
D() { cout << "D constructor" << endl; }
};
int main() {
D obj;
return 0;
}
/* 输出
A constructor
B constructor
C constructor
D constructor
*/
虚基类的构造函数由最派生类负责调用,避免重复初始化。