1. 继承的概念与定义
继承(inheritance)是面向对象程序设计(oop)中实现代码复用的核心机制。它允许我们在保持原有类特性的基础上进行扩展,增加新的成员变量和成员函数,从而产生新的类,称为派生类(derived class)。继承体现了面向对象程序设计的层次结构,反映了从简单到复杂的认知过程。
1.1 为什么需要继承?
在没有继承机制时,如果多个类有共同的属性和行为,我们只能在每个类中重复定义这些成员,导致代码冗余和维护困难。
例如,考虑学生(student)和教师(teacher)两个类:
// 没有继承的设计 - 存在大量冗余
class student {
public:
void identity() { /* 身份认证 */ }
void study() { /* 学习 */ }
protected:
string _name;
string _address;
string _tel;
int _age;
int _stuid; // 学号
};
class teacher {
public:
void identity() { /* 身份认证 */ }
void teaching() { /* 授课 */ }
protected:
string _name;
string _address;
string _tel;
int _age;
string _title; // 职称
};可以看到,两个类中都重复定义了姓名、地址、电话、年龄等成员变量,以及 identity() 成员函数。通过继承,我们可以将这些公共成员提取到基类中:
// 使用继承的设计
class person {
public:
void identity() {
cout << "身份认证:" << _name << endl;
}
protected:
string _name = "张三";
string _address;
string _tel;
int _age = 18;
};
class student : public person {
public:
void study() { /* 学习 */ }
protected:
int _stuid; // 学号
};
class teacher : public person {
public:
void teaching() { /* 授课 */ }
protected:
string _title; // 职称
};1.2 继承的定义格式
继承的基本语法如下:
class 派生类名 : 继承方式 基类名 {
// 派生类新增成员
};其中:
基类:也称为父类,是被继承的类
派生类:也称为子类,是继承基类的类
继承方式:public、protected、private,控制基类成员在派生类中的访问权限
1.3 继承方式与访问权限
继承方式会影响基类成员在派生类中的访问权限:
| 基类成员访问限定符 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可见 | 不可见 | 不可见 |
重要规则:
- 基类的 private 成员在派生类中无论以什么方式继承都是不可见的(但确实被继承到了派生类对象中)
- 基类的 protected 成员是为继承而设计的,可以在派生类中访问,但不能在类外访问
- 访问权限 = min(成员在基类的访问限定符,继承方式),其中 public > protected > private
- class 默认继承方式是 private,struct 默认继承方式是 public(建议显式指定)
- 实际开发中主要使用 public 继承,protected/private 继承较少使用
2. 基类与派生类的转换
在 public 继承中,存在以下转换规则:
2.1 派生类对象赋值给基类
class person {
protected:
string _name;
string _sex;
int _age;
};
class student : public person {
public:
int _no; // 学号
};
int main() {
student sobj;
// 1. 派生类对象可以赋值给基类的指针/引用(切片/切割)
person* pp = &sobj // 正确
person& rp = sobj; // 正确
// 2. 派生类对象可以赋值给基类对象(调用基类的拷贝构造)
person pobj = sobj; // 正确
// 3. 基类对象不能赋值给派生类对象
// sobj = pobj; // 错误:编译报错
// 4. 基类指针/引用可以通过强制类型转换赋值给派生类指针/引用
// 但必须是基类指针指向派生类对象时才安全
person* p = &sobj
student* ps = (student*)p; // 强制转换,需要谨慎
return 0;
}切片:将派生类对象赋值给基类对象时,只会复制基类部分的数据,派生类特有的部分会被"切掉"。
3. 继承中的作用域
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; // 访问派生类成员
}
protected:
int _num = 999; // 学号 - 隐藏了基类的 _num
};
int main() {
student s1;
s1.print(); // 输出:姓名:小李子
// 身份证号:111
// 学号:999
return 0;
}注意:
如果是成员函数的隐藏,只需要函数名相同就构成隐藏(参数不同也构成隐藏)
实际开发中应避免在继承体系中定义同名成员
可以使用基类::成员名显式访问被隐藏的基类成员
4. 派生类的默认成员函数
派生类的6个默认成员函数有其特殊性:
4.1 构造函数
派生类的构造函数必须调用基类的构造函数来初始化基类部分:
class person {
public:
person(const char* name = "peter")
: _name(name)
{
cout << "person()" << endl;
}
protected:
string _name;
};
class student : public person {
public:
student(const char* name, int num)
: person(name) // 必须调用基类构造函数
, _num(num) {
cout << "student()" << endl;
}
protected:
int _num;
};注意:_name不属于子类student成员,属于父类person的成员
初始化列表只能初始化当前类自己定义的成员变量,不能直接初始化父类的成员。
- _num :是 student 自己的成员,所以可以写 _num(num)
- _name :定义在 person 父类,student 无权在自己的初始化列表直接初始化它
2、正确处理父类成员的唯一方式
只能通过调用父类构造函数,由父类自己的初始化列表完成 _name 赋值:
4.2 拷贝构造函数
student(const student& s)
: person(s) // 调用基类的拷贝构造
, _num(s._num) {
cout << "student(const student& s)" << endl;
}4.3 赋值运算符重载
student& operator=(const student& s) {
if (this != &s) {
person::operator=(s); // 显式调用基类的operator=,这里需要使用父类的类域,因为名字相同,构成隐藏,访问时需要指明父类域
_num = s._num;
}
return *this;
}4.4 析构函数
派生类析构函数会在调用完成后自动调用基类的析构函数:
~student() {
cout << "~student()" << endl;
// 此处会自动调用 ~person()
}构造和析构顺序:
- 派生类对象构造:先调用基类构造,再调用派生类构造
- 派生类对象析构:先调用派生类析构,再调用基类析构
5. 继承与友元、静态成员
5.1 继承与友元
友元关系不能继承:基类的友元不能访问派生类的私有和保护成员。
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; // 正确:可以访问person的protected成员
// cout << s._stunum << endl; // 错误:不能访问student的protected成员
// 需要将display也声明为student的友元
}5.2 继承与静态成员
基类定义的静态成员在整个继承体系中只有一份:
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;
}6. 多继承与菱形继承
6.1 单继承与多继承
单继承:一个派生类只有一个直接基类
多继承:一个派生类有两个或以上直接基类
6.2 菱形继承问题
菱形继承是多继承的一种特殊情况,会导致数据冗余和二义性问题:
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() {
assistant a;
// a._name = "peter"; // 错误:对"_name"的访问不明确
a.student::_name = "xxx"; // 需要显式指定
a.teacher::_name = "yyy"; // 数据冗余:两份_name
return 0;
}6.3 虚继承解决方案
使用虚继承可以解决菱形继承的数据冗余和二义性问题:
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"; // 正确:只有一份_name
return 0;
}注意:虽然c++支持多继承和虚继承,但实际开发中应尽量避免设计出菱形继承,因为这会增加代码复杂性和维护难度。
7. 继承与组合
在面向对象设计中,除了继承还有另一种重要的代码复用方式——组合。
7.1 继承 vs 组合
| 特性 | 继承(is-a) | 组合(has-a) |
|---|---|---|
| 关系 | 派生类对象是一个基类对象 | b对象中有一个a对象 |
| 复用类型 | 白箱复用(内部细节可见) | 黑箱复用(内部细节不可见) |
| 耦合度 | 高(基类改变影响派生类) | 低(类之间依赖弱) |
| 封装性 | 破坏封装(基类细节对派生类可见) | 保持封装 |
7.2 设计原则
- 优先使用组合:组合的耦合度低,代码维护性好
- 适合继承时用继承:类之间确实是is-a关系时使用继承
- 实现多态必须用继承:要实现运行时多态必须使用继承
- 既适合继承又适合组合时用组合
// 组合示例:car has-a tire
class tire {
protected:
string _brand;
size_t _size;
};
class car {
protected:
string _colour;
string _num;
tire _t1, _t2, _t3, _t4; // 组合关系
};
// 继承示例:bmw is-a car
class bmw : public car {
public:
void drive() { cout << "好开-操控" << endl; }
};8. 继承的最佳实践
- 使用public继承:除非有特殊需求,否则使用public继承
- 避免菱形继承:多继承会增加复杂度,尽量避免设计菱形继承
- 合理使用protected:需要在派生类中访问但不在类外访问的成员设为protected
- 注意隐藏问题:避免在派生类中定义与基类同名的成员
- 正确调用基类构造函数:在派生类构造函数初始化列表中显式调用基类构造函数
- 考虑使用final:c++11的final关键字可以防止类被继承
// 使用final防止继承
class base final { // 这个类不能被继承
public:
void func() { cout << "base::func" << endl; }
};
// class derived : public base { // 错误:base是final的
// // ...
// };总结
继承是c++面向对象编程的核心特性之一,它提供了代码复用的强大机制。通过合理使用继承,我们可以:减少代码冗余,提高代码复用性、建立清晰的类层次结构、实现多态性(配合虚函数)。
到此这篇关于深入理解c++继承:从概念到实践的文章就介绍到这了,更多相关c++继承内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论