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

(C++)继承全解析及运用

(C++)继承全解析及运用

  • 继承的定义
  • 格式
  • 访问限定符+继承方式 确定继承规则
  • 继承类模板
  • 基类和派生类间的转换
  • 继承中的作用域
    • 隐藏
    • 重点题
  • 派生类的默认成员函数
    • 构造函数
    • 拷贝构造
    • 赋值重载
    • 析构函数
  • 实现一个不能被继承的类
  • 继承与友元
  • 继承与静态成员
  • 单继承、多继承及其菱形继承问题
    • 单继承
    • 多继承
    • 菱形继承
    • 菱形继承的解决方法
      • 虚继承
    • io库中的菱形继承
    • 多继承的指针偏移问题
  • 继承和组合
    • 理解
    • 复用方法的选择

继承的定义

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的只是函数层次的复用,继承是类设计层次的复用。

格式

下⾯我们看到Person是基类/父类,Student是派生类/子类。
在这里插入图片描述
代码示例:

//基类/父类
#include <iostream>
using namespace std;
class Person
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){cout << "void identity()" << _name << endl;}
protected:string _name = "张三"; // 姓名string _address; // 地址string _tel; // 电话int _age = 18; // 年龄
};
//派生类/子类
//继承方式是public继承
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和3理解记忆表格规则。

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的,即基类的私有成员在派生类都不可见。(这⾥的”不可见“是指基类的私有成员虽然被继承到了派⽣类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。)
  • 注:父类的私有成员变量虽然在子类中直接”不可见“,但是可以通过父类的公有函数来对其进行间接操作(类似于get函数的思路)。
  1. 基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,派生类的继承方式),public > protected >private。(对照表格梳理)
  2. 如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。(单从一个类出发,protected和private的效果是一样的,只有在继承中才体现出区别)
  3. 如果不显示写继承方式:使用关键字class时默认的继承方式是private,使⽤struct时默认的继承⽅式是public,不过最好显示的写出继承方式。
  4. 在实际运用中⼀般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用
    protected/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用(权限小于或等于protected),实际中扩展维护性不强。

继承类模板

类模板同样可以完成继承。
可以是父类(模板类)+子类(模板类)
父类(模板类)+子类(普通类,需要对父类进行实例化)
父类(普通类)+子类(模板类)

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);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}
};

基类是类模板时,使用基类中的成员需要指定⼀下类域:
原理:

  1. 模板是“按需实例化”
    只有代码里真正用到的成员函数、嵌套类型、静态变量,编译器才会为当前模板实参生成实体。
    没用到的部分就像不存在一样。
  2. 模板里出现的名字分“依赖名 / 非依赖名”
    • 依赖名 指名字里含有模板参数(如 T、vector<T> 的成员),可以简单理解为与模板参数有关的标识符(可以是变量、函数、类型、模板)。
    • 非依赖名 指与模板参数无关的名字(如 int、printf)。
    | 表达式/名字 | 是否依赖名 | 原因 |
    | --------------------------- | -------------------------------------------------------------------- | ------------------------------------------- |
    | T x; | 是 | T 是模板参数 |
    | vector<T> v; | 是 | 作用域 vector<T>T |
    | vector<T>::iterator it; | 是 | vector<T>T,且 iterator 所在的类型也依赖 T |
    | vector<int>::iterator it; | 否 | 与模板参数 T 毫无关系 |
    | push_back(x) | 如果 push_backvector<T> 的成员,则它所在的类模板依赖 T,因此 push_back 也是依赖名 | |

编译器在第一次解析模板时(还没给 T 代入具体类型)就会检查非依赖名;
对于依赖名,它等到模板被实例化(stack<int><double>)再去检查。
依赖名的查找分“两阶段”
• 阶段一:解析模板本身(还没给 T 赋具体类型),查找非依赖名。
• 阶段二:模板被实例化(T 变成 int、double …)后,再对依赖名做最终查找。
在阶段一,编译器无法知道 vector<T> 里到底有没有 push_back,因为它还没实例化 vector<int> 等具体类。
如果你把名字写成 非依赖形式(直接写 push_back(x)),阶段一就必须找到它;而此时 vector<T> 的成员函数尚未实例化,于是报错。
写成 依赖形式(vector<T>::push_back 或 this->push_back)后,编译器把查找推迟到阶段二;阶段二发生时,vector<int> 已经被实例化,push_back 随之生成,名字就能成功解析。

基类和派生类间的转换

• public继承的派生类对象 可以(在任意位置)赋值给 基类的指针 / 基类的引用。这⾥有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派⽣类中切出来的基类那部分
• 基类对象不能赋值给派生类对象。
“切片”特殊在哪?
在我们之前的认知中,不同类型的变量赋值会进行隐式类型转换,产生临时对象。

int i=1;
double c;
const double& d = i;//d接收的是i转化为double类型的临时对象,需要用常引用接收

但是 public继承的派生类对象赋值给 基类的指针 / 基类的引用是“赋值兼容转换”,过程不会产生临时对象,就是直接的切片!

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;// 派生类对象可以赋值给基类的对象,是通过调用后面会讲解的基类的拷贝构造完成的Person pobj = sobj;//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错sobj = pobj;return 0;
}

继承中的作用域

隐藏

  1. 在继承体系中基类和派生类都有独立的作用域。(我们知道,同一个域内不可以有同名变量,不同域可以,即基类和派生类可以有同名变量)
  2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。隐藏分为成员变量的隐藏和成员函数的隐藏。
    (在派生类成员函数中,可以使用基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(注意:不要错认为重载)
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

重点题

class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){cout << "func(int i)" <<i<<endl;}
};
int main()
{B b;b.fun(10);b.fun();return 0;
};
  1. A和B类中的两个func构成什么关系(B)
    A. 重载 B. 隐藏 C.没关系
  • 复习:
    函数重载条件:
    1. 同一作用域下
    2. 函数名相同
    3. 参数不同
  1. 以上程序的编译运行结果是什么(A)
    A. 编译报错 B. 运行报错 C. 正常运行

对于b.fun() ,父类的函数被隐藏了,默认为没有,这里就认为是调用了fun(int i)但缺少参数。

派生类的默认成员函数

默认生成的行为?
什么情况下不用自己写?
自己写的情况下要怎么写?

构造函数

派生类的构造函数需要对基类的成员变量、派生类自己的成员变量初始化,派生类自己的成员变量初始化和我们之前学的规则相同,需要注意的是,

  • 对于一个派生类对象中基类成员变量的初始化,构造函数会把这些变量看成一个“整体”。我们称这个“整体”,也就是派生类对象内部那块真正属于基类的数据区域为“基类子对象”。
  • 派生类对象初始化先调⽤/用基类构造再调派生类构造。也就是说,在 C++ 中,基类子对象在进入派生类构造函数体之前就已经构造完成。

因此得出派生类构造函数的规则:

  • 不能使用初始化列表或在构造函数内单独初始化基类的成员变量(因为他们看作一个整体)。
  • 派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类有默认构造,编译器会隐式地在初始化列表里调用基类的默认构造函数,如果基类没有默认构造,则必须在派生类构造函数的初始化列表阶段显示调用构造函数
  • 初始化列表中初始化变量的顺序是:基类变量–>派生类中先声明的变量
#include <iostream>
using namespace std;
class Person
{
public:Person(const char* name = "张三"): _name(name){cout << "Person()" << endl;}
protected:string _name; // 姓名
};// 派生类
// 基类成员--整体
// 派生类成员-内置类型、自定义类型
class Student : public Person
{
public:Student(const char* name, const char* address, int num):Person(name)//显示调用基类构造函数, _address(address), _num(num){}
protected:string _address = "李四";int _num = 1; //学号
};

拷贝构造

  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  • 一般情况默认生成的函数够用,出现深拷贝(资源管理)等特殊情况时才显示写拷贝构造
    如何拷贝基类对象?我们知道切片逻辑中,当派生类对象赋值给基类的引用时,会切出来对象中基类的部分,所以直接如下操作👇
	Student(const Student& s):Person(s)//如何拷贝基类对象?切片逻辑,_address(s._address),_num(s._num){//深拷贝代码}

将派生类对象赋值给基类对象,底层使用的是基类的拷贝构造

Student a;
Person b=a;//使用Person(const Person& s)

赋值重载

  • 派生类的operator=必须要调用基类的operator=完成基类的复制。
  • 一般情况默认生成的函数够用,出现深拷贝(资源管理)等特殊情况时才显示写拷贝构造。这里需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域👇。
	Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_address = s._address;_num = s._num;}return *this;}

析构函数

  • 一般情况默认生成的函数够用,出现深拷贝(资源管理)等特殊情况时才显示写析构函数。
  • 先子后父:因为子类有可能会访问到父类的成员,所以要保证先释放子类成员,再释放父类成员。即子类对象析构清理先调用子类析构再调父类的析构。
  • 子类的析构函数会在被调用完成后自动调用父类的析构函数清理基类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。也就是说,我们完全不需要自己手动显示调用父类的析构函数。
  • 底层的一些原理:由于一些场景下析构函数构成多态,需要基类和派生类析构函数名称相同,所以在编译后,底层会统一将析构函数名称处理成统一的destructor,所以父类和子类的析构函数其实是会构成隐藏关系的,所以如果在子类里要显示调用父类析构函数(设Person是父类,Student是子类),写成Person::~Person()

实现一个不能被继承的类

  • 方法1(C++98):基类的构造函数私有。
    派生类对象的实例化必须调用基类的构造函数,但是基类的构造函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
    缺点:不够直观

  • 方法2(C++11):用final关键字修饰基类,派生类就不能继承了。

class Base final
{……
};

继承与友元

友元关系不能继承,也就是说基类的友元不是派生类的友元,基类友元不能访问派生类私有和保护成员。
–>爸爸的朋友不一定是我的朋友

class Student;//前置声明
class Person
{
public:friend void Display(const Person& p, const Student& s);//在Student类定义前使用Student对象,需要进行前置声明
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;//
}
int main()
{Person p;Student s;// 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员(基类的友元不是派生类的友元)// 解决⽅案:Display也变成Student 的友元即可Display(p, s);return 0;
}

继承与静态成员

基类定义了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;// 这里的运行结果可以看到非静态成员_name的地址是不一样的// 说明派生类继承下来的非静态成员变量与基类的是不同的两份cout << &p._name << endl;cout << &s._name << endl;// 这里的运行结果可以看到静态成员_count的地址是一样的// 说明派生类和基类共用同一份静态成员cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,基类和派生类指定类域都可以访问静态成员(对象当然也可以,但不常用)cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}

单继承、多继承及其菱形继承问题

单继承

⼀个派生类只有⼀个直接基类时称这个继承关系为单继承。
在这里插入图片描述
在 C++ 的单继承链 A → B → C 中:
语言规则:C 继承了 B,而 B 又继承了 A,因此 C 间接地也继承了 A。
实际结果

  • C 的对象里包含 一个 A 子对象(在 B 子对象内部)。
  • C 拥有 A 的所有 public/protected 成员(受 B 和 C 的继承方式限制可见性)。
  • 指针/引用可以一路向上转换:C* → B* → A* 都是天然合法的隐式转换。

多继承

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

在这里插入图片描述

菱形继承

菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题(在Assistant的对象中Person成员会有两份,这两份数据一会导致冗余,二是会互相打架,编译器不知道用哪个)。支持多继承就⼀定会产生可能的菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
在这里插入图片描述
以下这种也是菱形继承:
判断依据:发现某个子类中存在共同基类,
比如E中会有两份A,分别来自B和D
virtual加在B和D这。
在这里插入图片描述

菱形继承的解决方法

虚继承

对菱形的腰上这两个类(Student和Teacher)用virtual关键字修饰Person类。
这样在Assistant对象定义的时候,可以理解为会单独生成一个唯一的Person类对象,而此时Student和Teacher类对象进行初始化时,调用的都会是这个唯一的Person类对象。
在子类都没有默认构造函数的情况下,Assistant的初始化列表中要单独显示调用Person的构造函数,生成前面说的唯一的Person类对象,Student和Teacher类对象的Person部分都用是这个单独生成的Person对象,包括最终生成的Assistant对象用的也是这个Person对象的值!(见下代码例子)
虚继承对于编译器的底层是非常复杂的处理。

class Person
{
public:Person(const char* name):_name(name){}string _name; // 姓名
};//class Student : public Person
class Student : virtual public Person
{
public:Student(const char* name, int num):Person(name), _num(num){}
protected:int _num; //学号
};class Teacher : virtual public Person
{
public:Teacher(const char* name, int id):Person(name), _id(id){}protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher
{
public:Assistant(const char* name1, const char* name2, const char* name3): Student(name1, 1), Teacher(name2, 2), Person(name3){}protected:string _majorCourse; // 主修课程
};
int main()
{// 思考⼀下这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个?Assistant a("张三", "李四", "王五");return 0;
}

最终a对象中的_name一定是“王五”,因为初始化列表中Person(name3)。
在这里插入图片描述

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>
{};

多继承的指针偏移问题

下⾯说法正确的是 ( C )
A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 
{ public: int _d; };
int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

知识点:

  • 多继承中写在前面的类先继承,构造函数优先调用
    (对于class Derive : public Base1, public Base2 ,Derive对象构造函数调用顺序Base1->Base2->Derive)
  • 临时对象在栈区开辟空间,栈区地址向下生长(从高地址到低地址开辟)
  • 把子类对象赋值给父类指针,相当于切片给父类。
    所以切片取得地址如下图:
    在这里插入图片描述

继承和组合

理解

  • 继承和组合都是对代码的复用
  • public继承是⼀种is-a的关系。也就是说每个派生类对象都是⼀个基类对象。
  • 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
  • 继承允许根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类可见 。继承⼀定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  • 对象组合是类继承之外的另⼀种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复用风格被称为黑箱复⽤(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于保持每个类被封装,耦合度低。
  • 从软件工程角度出发,耦合度越低越好,因为模块耦合度低->模块间关联度低->软件易维护,从这个角度出发,组合会比继承更优一些。

在这里插入图片描述

复用方法的选择

优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(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; }
};
template<class T>
class vector
{
};
// 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.dtcms.com/a/328526.html

相关文章:

  • Labelme从安装到标注:零基础完整指南
  • MySQL基础面试
  • Springboot整合Thrift
  • 移动端网页调试实战,键盘弹出与视口错位问题的定位与优化
  • 汉高携手SAP推出AI驱动的退换货与争议管理解决方案
  • 赛灵思ZYNQ官方文档UG585自学翻译笔记:UART Controller,通用异步收发传输器控制器
  • Vue接口平台十三——测试记录
  • Ubuntu 全盘备份
  • 九尾狐未来机械晶核技术
  • k3s部署
  • 电脑硬件详解
  • ZYNQ AXI-GPIO学习——ZYNQ学习笔记8
  • 学习游制作记录(背包UI以及各种物品的存储)8.12
  • kafka 消费者组的概念是什么?它是如何实现消息的点对点和发布/订阅模式?
  • Supabase快速入门与实战指南
  • LangChain 入门学习
  • Spring AI Alibaba - 聊天机器人快速上手
  • SpringAI 使用通义千问进行聊天对话开发
  • 考研复习-计算机组成原理-第五章-CPU
  • [NoC]Outstanding和Credit的概念详解
  • Fluent Bit 日志合并正则表达式(上)
  • Nginx 高级配置
  • Python训练Day41
  • 基于PAI-ChatLearn的GSPO强化学习实践
  • LLM - 搭建 Grounded SAM 2 模型的视觉检测与分割服务 API
  • CMake笔记:PUBLIC/PRIVATE/INTERFACE的使用
  • FreeRTOS---基础知识6---事件组
  • Effective C++ 条款37:绝不重新定义继承而来的缺省参数值
  • Linux系统编程Day13 -- 程序地址空间
  • Vue3 整合高德地图完成搜索、定位、选址功能,已封装为组件开箱即用(最新)