【C++】继承的介绍

news/2024/12/12 16:51:51/

继承

  • 1.继承的概念及定义
    • 1.1继承的概念:
    • 1.2 继承定义
    • 1.3继承类模板
  • 2.继承中的函数隐藏
  • 3.派生类的默认成员函数
  • 4.继承中的切割
  • 5.多继承及其菱形继承问题
    • 5.1继承模型
    • 5.2解决菱形继承问题的方法(虚继承)
  • 6.继承和组合

1.继承的概念及定义

1.1继承的概念:

在C++中,继承是面向对象编程的一个重要特性。
从概念上讲,继承允许你创建一个新类(派生类/子类),这个新类从一个已存在的类(基类/父类)那里获取成员变量和成员函数。继承的主要目的是代码重用和建立类之间的层次结构

class Student
{
public:
// 进校园/图书馆/实验室刷二维码进行身份认证void identity(){// ...}// 学习void study(){// ...}
protected:string _name = "peter"; // 姓名string _address; // 地址string _tel; // 电话int _age = 18; // 年龄int _stuid; // 学号
};
class Teacher
{
public:void identity(){// ...}// 授课void teaching(){//...}
protected:string _name = "张三"; // 姓名int _age = 30; // 年龄string _address; // 地址string _tel; // 电话string _title; // 职称
};

上面我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/
电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类里面就是冗余的。当然他们
也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣
的独有成员函数是学习,⽼师的独有成员函数是授课。
下⾯我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复⽤这些成员,就不需要重复定义了,省去了很多麻烦。

class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" <<_name<< endl;
}
protected:string _name = "张三"; // 姓名string _address; // 地址string _tel; // 电话int _age = 18; // 年龄
};

用Student和teacher都继承Person

class Student : public Person
{
public:
// 学习void study(){// ...}
protected:int _stuid; // 学号
};
class Teacher : public Person
{
public:// 授课void teaching(){//...}
protected:string title; // 职称
};int main()
{Student s;Teacher t;s.identity();t.identity();
return 0;
}

1.2 继承定义

定义格式
下面我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以
既叫基类/派⽣类,也叫⽗类/子类)
在这里插入图片描述
继承基类成员访问方式的变化:
在这里插入图片描述
1.基类private成员在派⽣类中⽆论以什么⽅式继承都是不可见的。这⾥的不可见是指基类的私有成员
还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问
它。
2.基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类
中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3.基类的私有成员在派⽣类都是不可见,public > protected >private。
4.使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显
⽰的写出继承方式。在实际运⽤中⼀般使⽤都是public继承,几乎很少使⽤protetced/private继承。

#include <iostream>
using namespace std;
// 基类
class Base {
public:int public_var;void public_func() {cout << "这是基类的公有函数" << endl;}
protected:int protected_var;void protected_func() {cout << "这是基类的保护函数" << endl;}
private:int private_var;void private_func() {cout << "这是基类的私有函数" << endl;}
};// 公有继承的派生类
class PublicDerived : public Base {
public:void accessMembers() {public_var = 10;  // 可以访问基类的公有成员protected_var = 20;  // 可以访问基类的保护成员public_func();  // 可以调用基类的公有函数protected_func();  // 可以调用基类的保护函数}
};// 私有继承的派生类
class PrivateDerived : private Base {
public:void accessMembers() {public_var = 30;  // 可以访问,但在派生类中变成私有成员了protected_var = 40;  // 可以访问,但在派生类中变成私有成员了public_func();  // 可以调用,但在派生类中变成私有函数了protected_func();  // 可以调用,但在派生类中变成私有函数了}
};// 保护继承的派生类
class ProtectedDerived : protected Base {
public:void accessMembers() {public_var = 50;  // 可以访问,在派生类中变成保护成员protected_var = 60;  // 可以访问,在派生类中变成保护成员public_func();  // 可以调用,在派生类中变成保护函数protected_func();  // 可以调用,在派生类中变成保护函数}
};int main() {PublicDerived pub_derived;pub_derived.public_var = 5;pub_derived.public_func();PrivateDerived pri_derived;// pri_derived.public_var = 6;  // 错误,在外部不能访问私有继承后变成私有的成员了// pri_derived.public_func();  // 错误,在外部不能访问私有继承后变成私有的函数了ProtectedDerived pro_derived;// pro_derived.public_var = 7;  // 错误,在外部不能访问保护继承后变成保护的成员了// pro_derived.public_func();  // 错误,在外部不能访问保护继承后变成保护的函数了return 0;
}

1.3继承类模板

namespace stack
{template<class T>
class stack : public std::vector<T>
{
public:void push(const T& x)// 基类是类模板时,需要指定⼀下类域,// 否则编译报错:error C3861: “push_back”: 找不到标识符// 因为stack<int>实例化时,也实例化vector<int>了// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到vector<T>::push_back(x);//push_back(x);void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}

2.继承中的函数隐藏

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

//Base是基类,Derived继承Base
//Base和Derived都有printf这个函数
Derived d;
d.printf()//调用派生类printf函数
d.Base::printf();//使用作用域解析运算符访问基类被隐藏的print函数

3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4.注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
重载和隐藏的区别:
重载(Overloading):
重载发生在同一个类中,是指函数名相同但参数列表不同的多个函数。例如,在一个类中可以有 int add(int a, int b) 和 double add(double a, double b) ,它们通过参数的类型、个数或者顺序的不同来区分,编译器会根据调用时的实际参数来决定调用哪个重载函数。
隐藏(Hiding):
隐藏发生在基类和派生类之间,是因为函数名相同而产生的现象。 并且隐藏不像重载那样要求参数列表不同,只要函数名相同就会隐藏基类中的同名函数。而且,派生类对象调用同名函数时,默认调用的是派生类中定义的函数,而不是基类中的函数。

3.派生类的默认成员函数

默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动生成⼀个。
在这里插入图片描述
1.派生类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造
函数,则必须在派⽣类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调⽤基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的
operator=隐藏了基类的operator=,所以显示调用基类的
operator=
,需要指定基类作用域。
4.派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派
生类对象先清理派⽣类成员再清理基类成员的顺序。
4. 派生类对象初始化先调⽤基类构造再调派生类构造。
5. 派生类对象析构清理先调⽤派⽣类析构再调基类的析构。
6. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。
在这里插入图片描述
以下代码来说明上面的默认成员函数调用的规则。

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);_num = s ._num;}return *this ;}~Student(){cout<<"~Student()" <<endl;}
protected :int _num ; //学号};
int main()
{Student s1 ("jack", 18);Student s2 (s1);Student s3 ("rose", 17);s1 = s3 ;return 0;
}

实现⼀个不能被继承的类
⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不见就不能调用了,那么派⽣类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。

// C++11的用方法
class Base final
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:
// C++98的方法
/*Base()
{}*/
};
class Derive :public Base
{void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};
int main()
{Base b;Derive d;return 0;
}

继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员。

4.继承中的切割

在继承关系中,“切割”(也称为对象切割)是指当把派生类对象赋值给基类对象或者将派生类对象作为参数传递给以基类对象为参数的函数时,派生类对象会被“切割”,只保留基类部分的信息。这是因为基类对象没有足够的空间来存储派生类对象中的所有成员。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分,会导致数据丢失

class Derived : public Base {
public:int derivedData;Derived(int base, int derived): Base(base), derivedData(derived){}
};void func(Base b) {cout << "Base类对象中的数据: " << b.baseData << endl;
}
int main() {Derived d(10, 20);func(d);  // 派生类对象d传递给Base类型的参数,发生对象切割return 0;
}

在这个例子中, Derived 类继承自 Base 类。 func 函数的参数是 Base 类型。当把 Derived 类的对象 d 传递给 func 函数时,对象 d 会被切割,只传递和处理 Base 类部分的数据(也就是 baseData ), Derived 类特有的 derivedData 成员在这个过程中被忽略了。
解决方法:
1.可以使用基类指针或基类引用作为函数参数,这样就不会发生对象切割,并且能够利用多态性来正确地处理派生类对象。
例如

void func(Base& b) {cout << "Base类对象中的数据: " << b.baseData << endl;
}int main() {Derived d(10, 20);func(d);  // 此时不会发生切割,因为是引用传递return 0;
}

或者使用指针:

void func(Base* b) {cout << "Base类对象中的数据: " << b->baseData << endl;
}
int main() {Derived d(10, 20);func(&d);  // 此时不会发生切割,因为是指针传递return 0;
}

这样,通过引用或指针传递,能够在函数内部正确地访问派生类对象(如果需要访问派生类特有的成员,可能需要进行类型转换),同时避免了对象切割的问题。

5.多继承及其菱形继承问题

5.1继承模型

单继承:⼀个派生类只有⼀个直接基类时称这个继承关系为单继承。
在这里插入图片描述

多继承:⼀个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型
是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后面。

在这里插入图片描述
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以
看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就
⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议
设计出菱形继承这样的模型的。
在这里插入图片描述

5.2解决菱形继承问题的方法(虚继承)

虚继承:在菱形继承结构中,当两个或多个派生类共同继承一个基类,并且又有一个类继承这些派生类时,为了确保基类在最终派生类中只有一个实例,使用虚继承。这样,在内存布局和对象访问上会更加合理

class Animal {
public:int animalData;
};class Mammal : virtual public Animal {
};
//在继承方式public前面加virtual这个关键字,就变成了虚继承
class Bird : virtual public Animal {
};class Bat : public Mammal, public Bird {
};

通过虚继承, Bat 类对象中 Animal 类的成员只有一份,解决了数据冗余和二义性问题。在类图表示中,虚继承可以用虚箭头(比如虚线箭头)来表示从 Mammal 和 Bird 类到 Animal 类的继承关系,以突出与普通继承的区别。

6.继承和组合

•public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是一个基类对象。
• 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
•在继承方式中,基类的内部细节对派⽣类可见 。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很大的影响。派⽣类和基类间的依赖关系很强,耦合度高。
•优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太
那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的
关系既适合⽤继承(is-a)也适合组合(has-a),就用组合

// Tire(轮胎)和Car(⻋)更符合has-a的关系
class Tire {
protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺⼨
};
class Car {
protected:string _colour = "⽩⾊"; // 颜⾊string _num = "陕ABIT00"; // ⻋牌号Tire _t1; // 轮胎Tire _t2; // 轮胎Tire _t3; // 轮胎Tire _t4; // 轮胎
};
class BMW : public Car {
public:void Drive() { cout << "好开-操控" << endl; }
};
// Car和BMW/Benz更符合is-a的关系
class Benz : public Car {
public:void Drive() { cout << "好坐-舒适" << endl; }
};
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public vector<T>
{};
template<class T>
class stack
{
public:vector<T> _v;
};
int main()
{return 0;
}

http://www.ppmy.cn/news/1554544.html

相关文章

多标签分类SOTA | ADDS论文解读

论文标题&#xff1a;Open Vocabulary Multi-Label Classification with Dual-Modal Decoder on Aligned Visual-Textual Features 论文传送门&#xff1a;https://arxiv.org/pdf/2208.09562 paperwithcode多标签分类排名&#xff1a; 这篇文章主要用来解决多标签分类问题&am…

Django Fixtures 使用指南:JSON 格式详解

在Django开发中&#xff0c;fixtures是一种非常有用的工具&#xff0c;它们可以帮助我们序列化数据库内容&#xff0c;并在不同的环境或测试中重用这些数据。本文将详细介绍Django fixtures的概念、如何生成和使用JSON格式的fixtures。 什么是Fixtures&#xff1f; Fixtures是…

如何使用 Python 发送 HTTP 请求?

在Python中发送HTTP请求最常用的库是requests&#xff0c;它提供了简单易用的API来发送各种类型的HTTP请求。 除此之外&#xff0c;还有标准库中的http.client&#xff08;以前叫做httplib&#xff09;和urllib&#xff0c;但它们相对更底层&#xff0c;代码量较大&#xff0c…

vue地址解析+虚拟手机号解析

&#xff08;1&#xff09;安装 address-parse模块 npm install address-parse --save &#xff08;2&#xff09;地址修改-弹窗页面 <template><div><el-dialog title"修改收货地址" :visible.sync"dialogVisible" width"45%"…

群控系统服务端开发模式-应用开发-登录退出发送邮件

一、登录成功发送邮件 在根目录下app文件夹下controller文件夹下common文件夹下&#xff0c;修改Login.php&#xff0c;代码如下 <?php /*** 登录退出操作* User: 龙哥三年风水* Date: 2024/10/29* Time: 15:53*/ namespace app\controller\common; use app\controller\Em…

单元测试SpringBoot

添加测试专用属性 加载测试专用bean Web环境模拟测试 数据层测试回滚 测试用例数据设定

Rust快速入门(二)

三个指令&#xff1a; cargo run 执行 --release&#xff1a; 由于使用run命令rust默认为debug模式&#xff0c;代码中很多debug数据就会打印&#xff0c;于是我们使用relsase参数就可以不输出debug的代码。 cargo check 校验是否能够通过编译 cargo build 打包为可执行文件 …

【jvm】GC Roots有哪些

目录 1. 说明2. 虚拟机栈&#xff08;栈帧中的局部变量表&#xff09;中的引用3. 方法区中的类静态属性引用4. 本地方法栈&#xff08;Native方法栈&#xff09;中JNI&#xff08;Java Native Interface&#xff09;的引用5. 活跃线程&#xff08;Active Threads&#xff09;6.…