当前位置: 首页 > news >正文

C++面向对象编程之继承:深入理解与应用实践

目录

一、继承的概念及定义

1.1 继承的基本概念

1.2 继承的定义与访问方式

1.2.1 定义格式

1.2.2 继承方式与访问权限

1.3 继承类模板

二、基类与派生类的转换

2.1 向上转型(Upcasting)

2.2 向下转型(Downcasting)

三、 继承中的作用域

3.1 隐藏规则

3.2 函数隐藏

四、派生类的默认成员函数

4.1 六大默认成员函数在继承中的行为

4.2 代码示例

4.3 实现不能被继承的类

五、继承与友元

六、 继承与静态成员

七、多继承与菱形继承

7.1 继承模型

7.2 虚继承

7.3 IO库中的菱形虚拟继承

八、继承与组合

8.1 关系类型比较

8.2 选择原则

九、总结


一、继承的概念及定义

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成员

不可见

不可见

不可见

重要规则

  1. 基类private成员在派生类中不可见(但仍被继承)

  2. 访问权限计算:Min(成员在基类的访问限定符,继承方式)

  3. class默认private继承,struct默认public继承

  4. 实践中主要使用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 隐藏规则

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用基类::基类成员 显示访问)
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 函数隐藏

  1.  需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  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个默认成员函数,默认的意思就是指我们不写,编译器会帮我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢???

  1. 构造函数:必须调用基类构造函数初始化基类部分

  2. 拷贝构造函数:必须调用基类拷贝构造完成基类部分的拷贝

  3. 赋值运算符:必须调用基类赋值运算符完成基类部分的赋值

  4. 析构函数:完成后自动调用基类析构函数

  5. 取地址运算符:通常使用编译器生成版本

  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 选择原则

  1. 优先使用组合而不是继承   

  2. 只有真正的"is-a"关系才使用继承

  3. 需要实现多态时必须使用继承

  4. 组合耦合度低,更易于维护

// 组合示例
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++面向对象编程的核心机制之一,正确理解和使用继承对于编写高质量、可维护的代码至关重要。本文详细介绍了继承的各个方面,包括基本概念、访问控制、作用域规则、成员函数特性、多继承问题以及继承与组合的选择原则。

在实际开发中,应遵循以下最佳实践:

  1. 优先使用public继承

  2. 尽量避免多继承和菱形继承

  3. 优先选择组合而不是继承

  4. 注意继承中的隐藏规则和作用域问题

  5. 正确处理派生类中的默认成员函数

通过深入理解这些概念和原则,开发者可以更好地利用C++的继承机制,编写出更加健壮和可维护的面向对象程序。

参考资料

  1. Stanley B. Lippman, "C++ Primer"

  2. Scott Meyers, "Effective C++"

  3. Bjarne Stroustrup, "The C++ Programming Language"

http://www.dtcms.com/a/394087.html

相关文章:

  • [Windows] OFD转PDF 1.2.0
  • TDengine 聚合函数 VAR_POP 用户手册
  • 跨域及其解决方法
  • LeetCode:37.二叉树的最大深度
  • 【C++深学日志】C++“类”的完全指南--从基础到实践(一)
  • BUS-消息总线
  • 23种设计模式之【单例模式模式】-核心原理与 Java实践
  • 精度至上,杜绝失真,机器视觉检测中为何常用BMP格式?
  • 关于wireshark流量分析软件brim(Zui)安装方法
  • springboot3.4.1集成pulsar
  • 信息量、熵、KL散度和交叉熵
  • 使用Python一站式提取Word、Excel、PDF 和PPT文档内容v1.0
  • 线性代数 | REF / RREF
  • TLCP的一些内容
  • dock容器网络存储相关练习
  • 鸿蒙Next ArkTS卡片提供方开发指南:从入门到实战
  • Netty LengthFieldBasedFrameDecoder
  • 后端_HTTP 接口签名防篡改实战指南
  • 区块链论文速读 CCF A--WWW 2025(5)
  • 机器学习周报十四
  • 如何解决stun服务无法打洞建立p2p连接的问题
  • 解决项目实践中 java.lang.NoSuchMethodError:的问题
  • JavaSE-多线程(5.2)- ReentrantLock (源码解析,公平模式)
  • 2025华为杯A题B题C题D题E题F题选题建议思路数学建模研研究生数学建模思路代码文章成品
  • 【记录】Docker|Docker中git克隆私有库的安全方法
  • Web之防XSS(跨站脚本攻击)
  • 使用 AI 对 QT应用程序进行翻译
  • Windows下游戏闪退?软件崩溃?游戏环境缺失?软件运行缺少依赖?这个免费工具一键帮您自动修复(DLL文件/DirectX/运行库等问题一键搞定)
  • 【从入门到精通Spring Cloud】统一服务入口Spring Cloud Gateway
  • setfacl 命令