C++面向对象编程之继承
目录
一、继承的概念及定义
1.1 继承的基本概念
继承(Inheritance) 是面向对象程序设计(OOP)中最核心的机制之一,它允许我们基于已有类创建新类,实现代码复用和功能扩展。通过继承,派生类(子类)可以自动获得基类(父类)的属性和方法,同时可以添加自己特有的成员。
没有继承时的代码冗余问题:
class Student {
public:
void identity() { /* ... */ }
void study() { /* ... */ }
protected:
string _name = "peter";
string _address;
string _tel;
int _age = 18;
int _stud; // 学号
};
class Teacher {
public:
void identity() { /* ... */ }
void teaching() { /* ... */ }
protected:
string _name = "张三";
int _age = 18;
string _address;
string _tel;
string _title; // 职称
};
上述代码中,Student和Teacher类有许多相同的成员变量和函数,造成了代码冗余。
使用继承优化后的代码:
class Person {
public:
void identity() {
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三";
string _address;
string _tel;
int _age = 18;
};
class Student : public Person {
public:
void study() { /* ... */ }
protected:
int _stud; // 学号
};
class Teacher : public Person {
public:
void teaching() { /* ... */ }
protected:
string _title; // 职称
};
1.2 继承的定义与访问方式
1.2.1 定义格式
下面我们看到Person是基类,也称作父类。Student是派生类,也称作子类。


1.2.2 继承方式与访问权限
继承方式有三种:public、protected和private。不同继承方式下,基类成员在派生类中的访问权限变化如下:
| 基类成员/继承方式 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| public成员 | public | protected | private |
| protected成员 | protected | protected | private |
| private成员 | 不可见 | 不可见 | 不可见 |
重要规则:
-
基类private成员在派生类中不可见(但仍被继承)
-
访问权限计算:Min(成员在基类的访问限定符,继承方式)
-
class默认private继承,struct默认public继承
-
实践中主要使用public继承
// 实例演⽰三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public :
void Print ()
{
cout<<_name <<endl;
}
protected :
string _name ; // 姓名
private :
int _age ; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected :
int _stunum ; // 学号
};
1.3 继承类模板
namespace bit {
template<class T>
class stack : public std::vector<T> {
public:
void push(const T& x) {
std::vector<T>::push_back(x);
}
void pop() {
std::vector<T>::pop_back();
}
const T& top() {
return std::vector<T>::back();
}
bool empty() {
return std::vector<T>::empty();
}
};
}
二、基类与派生类的转换
2.1 向上转型(Upcasting)
public继承的派生类对象,可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
class Person {
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person {
public:
int _No; // 学号
};
int main() {
Student sobj;
// 派生类对象赋值给基类指针/引用
Person* p = &sobj;
Person& rp = sobj;
// 派生类对象赋值给基类对象
Person pobj = sobj;
// 错误:基类对象不能赋值给派生类对象
// sobj = pobj;
return 0;
}
2.2 向下转型(Downcasting)
基类指针或引用可以通过强制类型转换赋值给派生类的指针或引用,但需要注意安全性。

三、 继承中的作用域
3.1 隐藏规则
- 在继承体系中基类和派生类都有独立的作用域。
- 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用基类::基类成员 显示访问)
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; // 访问派生类的_num
}
protected:
int _num = 999; // 学号,隐藏了基类的_num
};
3.2 函数隐藏
- 需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 在实际中,在继承体系里面最好不要定义同名的成员。
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;
}
};
int main() {
B b;
b.fun(10); // 正确
// b.fun(); // 错误:被隐藏
b.A::fun(); // 正确:显式调用
return 0;
}
四、派生类的默认成员函数
4.1 六大默认成员函数在继承中的行为
6个默认成员函数,默认的意思就是指我们不写,编译器会帮我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢???
-
构造函数:必须调用基类构造函数初始化基类部分
-
拷贝构造函数:必须调用基类拷贝构造完成基类部分的拷贝
-
赋值运算符:必须调用基类赋值运算符完成基类部分的赋值
-
析构函数:完成后自动调用基类析构函数
-
取地址运算符:通常使用编译器生成版本
-
const取地址运算符:通常使用编译器生成版本
因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同((这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。


4.2 代码示例
class Person {
public:
Person(const char* name = "peter") : _name(name) {
cout << "Person()" << endl;
}
Person(const Person& p) : _name(p._name) {
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p) {
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person() {
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person {
public:
Student(const char* name, int num)
: Person(name), _num(num) {
cout << "Student()" << endl;
}
Student(const Student& s)
: Person(s), _num(s._num) {
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s) {
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s) {
Person::operator=(s); // 显式调用基类operator=
_num = s._num;
}
return *this;
}
~Student() {
cout << "~Student()" << endl;
}
protected:
int _num;
};
4.3 实现不能被继承的类
方法一:C++98方式(构造函数私有化)
基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构造函数私有化以后,派生类看不见就不能调用了。那么派生类就无法实例化出对象。
class NonInheritable {
private:
NonInheritable() {} // 私有构造函数
};
class Derived : public NonInheritable { // 错误:无法访问私有构造函数
};
方法二:C++11方式(使用final关键字)
class Base final { // 使用final关键字
public:
void func() { cout << "Base::func" << endl; }
};
class Derive : public Base { // 错误:不能继承final类
};
五、继承与友元
友元关系不能继承,基类的友元不能访问派生类的私有和保护成员。
class Student;
class Person {
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person {
protected:
int _stuNum;
};
void Display(const Person& p, const Student& s) {
cout << p._name << endl; // 正确:友元访问
// cout << s._stuNum << endl; // 错误:不是Student的友元
}
// 解决方案:让Display也成为Student的友元
class Student : public Person {
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum;
};
六、 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个static成员实例。
class Person {
public:
string _name;
static int _count; // 静态成员
};
int Person::_count = 0;
class Student : public Person {
protected:
int _stuNum;
};
int main() {
Person p;
Student s;
cout << &p._name << endl; // 不同地址
cout << &s._name << endl; // 不同地址
cout << &p._count << endl; // 相同地址
cout << &s._count << endl; // 相同地址
cout << Person::_count << endl; // 相同值
cout << Student::_count << endl; // 相同值
return 0;
}
七、多继承与菱形继承
7.1 继承模型
单继承:一个派生类只有一个直接基类时称这个继承关系为单继承。
多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承。多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员放到最后面。
菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。



class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职⼯编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
// 编译报错:error C2385: 对“_name”的访问不明确
Assistant a;
a._name = "peter";
// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
7.2 虚继承
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚继承,底层实现就很复杂,性能也会有一些损失。所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后来的一些编程语言大多没有多继承,如Java。
class Person {
public:
string _name;
};
class Student : virtual public Person { // 虚继承
protected:
int _num;
};
class Teacher : virtual public Person { // 虚继承
protected:
int _id;
};
class Assistant : public Student, public Teacher {
protected:
string _majorCourse;
};
int main() {
Assistant a;
a._name = "peter"; // 正确:无二义性且无数据冗余
return 0;
}
我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后。⽆论是使⽤还是底层逻辑,都会复杂很多。当然,有多继承语法支持,就⼀定存在会设计出菱形继承的情况,像Java是不支持多继承的,就避开了菱形继承。
7.3 IO库中的菱形虚拟继承

template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};
八、继承与组合
8.1 关系类型比较
-
继承(is-a关系):派生类是基类的一种特殊类型
-
组合(has-a关系):类中包含另一个类的对象作为成员
8.2 选择原则
-
优先使用组合而不是继承
-
只有真正的"is-a"关系才使用继承
-
需要实现多态时必须使用继承
-
组合耦合度低,更易于维护
// 组合示例
class Tire {
protected:
string _brand = "Michelin";
size_t _size = 17;
};
class Car {
protected:
string _colour = "白色";
string _num = "陕ABIT00";
Tire _t1, _t2, _t3, _t4; // 组合关系
};
// 继承示例
class BMW : public Car { // is-a关系
public:
void Drive() { cout << "好开-操控" << endl; }
};
class Benz : public Car { // is-a关系
public:
void Drive() { cout << "好坐-舒适" << endl; }
};
九、总结
继承是C++面向对象编程的核心机制之一,正确理解和使用继承对于编写高质量、可维护的代码至关重要。本文详细介绍了继承的各个方面,包括基本概念、访问控制、作用域规则、成员函数特性、多继承问题以及继承与组合的选择原则。
在实际开发中,应遵循以下最佳实践:
-
优先使用public继承
-
尽量避免多继承和菱形继承
-
优先选择组合而不是继承
-
注意继承中的隐藏规则和作用域问题
-
正确处理派生类中的默认成员函数
通过深入理解这些概念和原则,开发者可以更好地利用C++的继承机制,编写出更加健壮和可维护的面向对象程序。
参考资料:
-
Stanley B. Lippman, "C++ Primer"
-
Scott Meyers, "Effective C++"
-
Bjarne Stroustrup, "The C++ Programming Language"
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)