第15章 类的继承

继承的概念

继承是面向对象编程的三大核心特性之一,它允许一个类(派生类)继承另一个类(基类)的属性和方法。通过继承,派生类可以重用基类的代码,同时添加自己特有的功能。

继承的目的

  1. 代码重用:避免重复编写相同的代码
  2. 建立层次关系:通过继承建立类之间的层次结构
  3. 多态性:为实现多态性提供基础

继承的基本语法

定义派生类

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
void baseMethod() {
std::cout << "Base method" << std::endl;
}
};

class Derived : public Base {
public:
void derivedMethod() {
std::cout << "Derived method" << std::endl;
}
};

访问控制

C++支持三种继承方式,它们决定了基类成员在派生类中的访问权限:

  1. public继承:基类的public成员在派生类中仍然是public,protected成员在派生类中仍然是protected,private成员在派生类中不可访问
  2. protected继承:基类的public和protected成员在派生类中都是protected,private成员在派生类中不可访问
  3. private继承:基类的public和protected成员在派生类中都是private,private成员在派生类中不可访问
1
2
3
4
5
6
7
8
// public继承
class DerivedPublic : public Base { /* ... */ };

// protected继承
class DerivedProtected : protected Base { /* ... */ };

// private继承
class DerivedPrivate : private Base { /* ... */ };

构造函数与析构函数

构造函数的调用顺序

当创建派生类对象时,构造函数的调用顺序是:

  1. 基类的构造函数
  2. 派生类的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {
public:
Base() {
std::cout << "Base constructor" << std::endl;
}
};

class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor" << std::endl;
}
};

// 输出:
// Base constructor
// Derived constructor
Derived d;

带参数的构造函数

如果基类有带参数的构造函数,派生类必须在其构造函数中显式调用基类的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base {
public:
Base(int value) : data(value) {
std::cout << "Base constructor with value: " << value << std::endl;
}
private:
int data;
};

class Derived : public Base {
public:
Derived(int baseValue, int derivedValue)
: Base(baseValue), derivedData(derivedValue) {
std::cout << "Derived constructor with value: " << derivedValue << std::endl;
}
private:
int derivedData;
};

// 输出:
// Base constructor with value: 10
// Derived constructor with value: 20
Derived d(10, 20);

析构函数的调用顺序

当销毁派生类对象时,析构函数的调用顺序是:

  1. 派生类的析构函数
  2. 基类的析构函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
~Base() {
std::cout << "Base destructor" << std::endl;
}
};

class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};

// 输出:
// Derived destructor
// Base destructor
{
Derived d;
}

成员函数的重写

重写基类的成员函数

派生类可以重写基类的成员函数,以提供自己的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
void show() {
std::cout << "Base::show()" << std::endl;
}
};

class Derived : public Base {
public:
void show() {
std::cout << "Derived::show()" << std::endl;
}
};

调用基类的成员函数

派生类可以通过作用域解析运算符(::)调用基类的成员函数。

1
2
3
4
5
6
7
class Derived : public Base {
public:
void show() {
Base::show(); // 调用基类的show()方法
std::cout << "Derived::show()" << std::endl;
}
};

继承中的特殊成员函数

拷贝构造函数和赋值运算符

如果基类有拷贝构造函数或赋值运算符,派生类需要显式调用它们。

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
class Base {
public:
Base(const Base& other) : data(other.data) {
std::cout << "Base copy constructor" << std::endl;
}

Base& operator=(const Base& other) {
if (this != &other) {
data = other.data;
std::cout << "Base assignment operator" << std::endl;
}
return *this;
}
private:
int data;
};

class Derived : public Base {
public:
Derived(const Derived& other) : Base(other), derivedData(other.derivedData) {
std::cout << "Derived copy constructor" << std::endl;
}

Derived& operator=(const Derived& other) {
if (this != &other) {
Base::operator=(other); // 调用基类的赋值运算符
derivedData = other.derivedData;
std::cout << "Derived assignment operator" << std::endl;
}
return *this;
}
private:
int derivedData;
};

移动构造函数和移动赋值运算符

C++11引入了移动语义,派生类也需要正确处理移动构造函数和移动赋值运算符。

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
class Base {
public:
Base(Base&& other) noexcept : data(std::move(other.data)) {
std::cout << "Base move constructor" << std::endl;
}

Base& operator=(Base&& other) noexcept {
if (this != &other) {
data = std::move(other.data);
std::cout << "Base move assignment operator" << std::endl;
}
return *this;
}
private:
std::string data;
};

class Derived : public Base {
public:
Derived(Derived&& other) noexcept : Base(std::move(other)), derivedData(std::move(other.derivedData)) {
std::cout << "Derived move constructor" << std::endl;
}

Derived& operator=(Derived&& other) noexcept {
if (this != &other) {
Base::operator=(std::move(other)); // 调用基类的移动赋值运算符
derivedData = std::move(other.derivedData);
std::cout << "Derived move assignment operator" << std::endl;
}
return *this;
}
private:
std::string derivedData;
};

继承的层次结构

单继承

单继承是指一个派生类只有一个直接基类。

1
2
3
class A { /* ... */ };
class B : public A { /* ... */ };
class C : public B { /* ... */ };

多继承

多继承是指一个派生类有多个直接基类。

1
2
3
class A { /* ... */ };
class B { /* ... */ };
class C : public A, public B { /* ... */ };

菱形继承

菱形继承是多继承的一种特殊情况,当一个派生类从两个不同的基类继承,而这两个基类又从同一个基类继承时,就会形成菱形继承。

1
2
3
4
class A { /* ... */ };
class B : public A { /* ... */ };
class C : public A { /* ... */ };
class D : public B, public C { /* ... */ };

菱形继承会导致二义性问题,因为派生类会继承基类的两个副本。可以使用虚继承来解决这个问题。

虚继承

虚继承是一种特殊的继承方式,它确保派生类只继承基类的一个副本。

1
2
3
4
class A { /* ... */ };
class B : virtual public A { /* ... */ };
class C : virtual public A { /* ... */ };
class D : public B, public C { /* ... */ };

继承与访问控制

protected成员

protected成员可以被基类的成员函数、派生类的成员函数和友元访问,但不能被外部代码访问。

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
class Base {
protected:
int protectedData;
public:
void baseMethod() {
protectedData = 10; // 可以访问
}
};

class Derived : public Base {
public:
void derivedMethod() {
protectedData = 20; // 可以访问
}
};

int main() {
Base b;
// b.protectedData = 30; // 错误:不能访问protected成员

Derived d;
// d.protectedData = 40; // 错误:不能访问protected成员

return 0;
}

private成员

private成员只能被基类的成员函数和友元访问,不能被派生类的成员函数访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
private:
int privateData;
public:
void baseMethod() {
privateData = 10; // 可以访问
}
};

class Derived : public Base {
public:
void derivedMethod() {
// privateData = 20; // 错误:不能访问private成员
}
};

继承的最佳实践

1. 合理使用继承

  • 继承是一种”is-a”关系:派生类应该是基类的一种特殊类型
  • 避免过深的继承层次:继承层次应该控制在合理的范围内(一般不超过3-4层)
  • 优先使用组合而非继承:当派生类和基类之间不是”is-a”关系时,应该使用组合

2. 设计良好的基类

  • 提供虚析构函数:如果基类可能被继承,应该提供虚析构函数
  • 使用protected成员:对于需要被派生类访问的成员,使用protected而非private
  • 提供合适的构造函数:为基类提供合适的构造函数,包括默认构造函数

3. 正确使用访问修饰符

  • public继承:最常用的继承方式,适用于”is-a”关系
  • protected继承:适用于需要将基类的接口作为派生类内部实现的一部分
  • private继承:适用于需要使用基类的实现但不希望暴露基类接口

示例:继承的应用

图形类层次结构

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <memory>

class Shape {
public:
virtual void draw() const = 0;
virtual double area() const = 0;
virtual ~Shape() = default;
};

class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}

void draw() const override {
std::cout << "Drawing a circle with radius " << radius << std::endl;
}

double area() const override {
return 3.14159 * radius * radius;
}
};

class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}

void draw() const override {
std::cout << "Drawing a rectangle with width " << width << " and height " << height << std::endl;
}

double area() const override {
return width * height;
}
};

int main() {
std::unique_ptr<Shape> shape1 = std::make_unique<Circle>(5.0);
std::unique_ptr<Shape> shape2 = std::make_unique<Rectangle>(4.0, 6.0);

shape1->draw();
std::cout << "Area: " << shape1->area() << std::endl;

shape2->draw();
std::cout << "Area: " << shape2->area() << std::endl;

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <string>

class Animal {
protected:
std::string name;
public:
Animal(const std::string& n) : name(n) {}

virtual void makeSound() const = 0;
virtual void move() const = 0;
virtual ~Animal() = default;
};

class Mammal : public Animal {
public:
Mammal(const std::string& n) : Animal(n) {}

void move() const override {
std::cout << name << " walks" << std::endl;
}
};

class Bird : public Animal {
public:
Bird(const std::string& n) : Animal(n) {}

void move() const override {
std::cout << name << " flies" << std::endl;
}
};

class Dog : public Mammal {
public:
Dog(const std::string& n) : Mammal(n) {}

void makeSound() const override {
std::cout << name << " barks" << std::endl;
}
};

class Cat : public Mammal {
public:
Cat(const std::string& n) : Mammal(n) {}

void makeSound() const override {
std::cout << name << " meows" << std::endl;
}
};

class Eagle : public Bird {
public:
Eagle(const std::string& n) : Bird(n) {}

void makeSound() const override {
std::cout << name << " screeches" << std::endl;
}
};

int main() {
Dog dog("Rex");
Cat cat("Whiskers");
Eagle eagle("Baldy");

dog.makeSound();
dog.move();

cat.makeSound();
cat.move();

eagle.makeSound();
eagle.move();

return 0;
}

总结

继承是C++面向对象编程的重要特性,它允许派生类继承基类的属性和方法,实现代码重用和建立类的层次结构。合理使用继承可以提高代码的可维护性和可扩展性,但也需要注意避免过度使用继承,特别是在复杂的系统中。在设计类层次结构时,应该遵循”is-a”关系原则,优先使用组合而非继承,并注意处理好构造函数、析构函数和访问控制等问题。