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

探索C/C++的奥秘之C++中的继承

1.继承的概念及定义

继承是面向对象的三大特性之一,继承是第二大特性。

1.1继承的概念

继承的好处是可以复用,通俗点来讲就是父辈打下的江山或者是基业我们可以继续使用。继承是类设计层次的复用。

1.2 继承定义

1.2.1定义格式

被继承的类Person是父类,也称作基类。Student是子类,也称作派生类。

继承代码演示:

class Person
{
public:
    void Print()
    {
        cout << "name:" << _name << endl;
        cout << "age:" << _age << endl;
    }
protected:
    string _name = "peter"; // 姓名
    int _age = 18;          //年龄
};

class Student : public Person
{
protected:
    int _stuid; // 学号
}; 

class Teacher : public Person
{
protected:
    int _jobid; // 工号
};

int main()
{
    Student s;
    Teacher t;
    s.Print();//可以使用基类中的成员函数和成员变量
    t.Print();
    return 0;
}

但是如果基类中的保护变成私有,无论什么方式继承,基类中的成员子类都用不了,并且还会报错。 代码演示:

class Person
{
public:
    void Print()
    {
        cout << "name:" << _name << endl;
        cout << "age:" << _age << endl;
    }
private:
    string _name = "peter"; // 姓名
    int _age = 18;          //年龄
};

class Student : public Person
{
public:
    void func()
    {
        cout << _name << endl;//
        cout << _age << endl;   //这两行代码会出现错误,也就是红色波浪线
    }
protected:
    int _stuid; // 学号
}; 

class Teacher : public Person
{
protected:
    int _jobid; // 工号
};

int main()
{
    Student s;
    Teacher t;
    s.Print();
    t.Print();
    return 0;
}

Student类中看起来没有Print()函数,也没有名字,但实际上有,因为继承了Person的类。调试观察一下:

1.2.2继承关系和访问限定符 

1.2.3继承基类成员访问方式的变化

如果是class默认是私有继承,类外面不能访问。 

如果是struct默认是公有继承,类外面、类里面都可以访问。

基类的private成员在派生类都是不可见的,也就是不能用。(1)不可见:内存上存在,语法上不能访问(类里面和类外面都不能用),跟private也不一样,private是类外面不能使用,类里面可以使用。

总结:

1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。 4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。

5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

实际上最常用的也就是下面这两个:

 派生类成员变量由两个部分数据构成,一个是从父类继承下来的成员变量,一个是自己的成员变量。当然成员函数也会继承,但是成员函数在对象里面不存,因为每个对象的成员变量都是独一份的,成员函数是放在一个公共的代码区中的,当然对象中也可以存,但因为每个对象中存一份太浪费了,所以两个不同的对象调用函数还是同一个函数。

2.基类和派生类对象赋值转换

Person p;
Student s;
p = s;
s = p;//这种写法是不行的,即使强制类型转换也不行

s = (Person)p;//错误写法

1.派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。p = s,s = p这两种转换分别向上转换和向下转换,叫做子给父叫做向上转换。向上转换是可以的,不同的类型赋值,不管走的是隐式类型还是强制类型,中间都会产生临时变量。 p = s在语法中进行了一个特殊处理,发生的是一个赋值兼容转换,有些地方也叫做切割或者切片,中间不会产生临时变量。这种方法也就是把子类当中属于父类的一部分切出来拷贝给父类。

但是如何证明没有产生临时变量呢?

d要发生隐式类型转换,产生临时变量,临时变量具有常性,用引用相当于权限的放大。

 这里的p相当于子类中这部分的别名。

引用: 

指针: 

2.基类对象不能赋值给派生类对象。

3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后 面再讲解,这里先了解一下)

扩展一个小知识:类型转换分为强制类型转换和隐式类型转换。

从对象的角度来说,父对象是不能给子对象的。

3.继承中的作用域

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;
    }
protected:
    int _num = 999; // 学号
};

int main()
{
    Student s1;
    s1.Print();
    return 0;
}

体现在成员函数:

class Person
{
public:
    void func()
    {
        cout << "Person::func" << endl;
    }
protected:
    string _name = "小李子"; // 姓名
    int _num = 111;          // 身份证号
};
class Student : public Person
{
public:
    void Print()
    {
        cout << " 姓名:" << _name << endl;
        cout << " 身份证号:" << Person::_num << endl;
        cout << " 学号:" << _num << endl;
    }
    void func()
    {
        cout << "Student::func" << endl;
    }
protected:
    int _num = 999; // 学号
};

int main()
{
    Student s1;
    s1.Print();
    s1.func();

    s1.Person::func();//指定去父类中去找
    return 0;
}

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

4. 注意在实际中在继承体系里面最好不要定义同名的成员。

扩展知识点:在类里面,查找一个变量的原则是先在该成员函数局部域中找,再去类域中找,再去父类的类域中去找,再去全局域去找,全局域也没有的话就会报错。

扩展知识点:父子类域中,成员函数名相同就构成隐藏。

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

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类 中,这几个成员函数是如何生成的呢?

1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

编译器规定,子类不能显示的在初始化列表初始化父类成员变量,在函数体内可以,并且这个时候父类中必须要有构造函数。

没有定义父类对象为什么还会调用父类构造函数呢?

因为C++还规定必须调用父类的构造函数初始化父类的成员,并且是在子类的初始化列表自动调用父类的默认构造,如果父类不提供默认构造会报错。

派生类初始化只初始化自己的,父类的那部分交给父类的构造函数,但默认情况下会调用父类的默认构造,如果父类没有默认构造就调不动,这个时候要在子类的初始化列表去初始化。初始化列表是按照声明的顺序去初始化,被继承的成员变量一般都在继承的成员变量前面。

如果父类中没有默认构造函数,在子类中要这样定义:

当然有默认构造的话也可以这样在初始化列表初始化,只是说如果父类中没有默认构造,用Student 定义的对象程序运行的时候会报错。

在子类的初始化列表中显示定义_name的方式:

2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

子类中的拷贝构造要求调用父类的拷贝构造,不可以下面这样写,必须要调用父类的拷贝构造。

要是下面这样写:

正确的写法: 

这里直接写Person(s)也可以, 因为基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。

如果不写:Person(s):

为什么不是张三呢?不写的话默认不会调用拷贝构造,它会调用父类默认构造,拷贝构造也是构造,拷贝构造在子类初始化列表不写,会调用父类默认构造,拷贝构造里面我们说了,对于Person类你不写,父类对象他会当成一个整体,它会调用父类默认构造,如果没有提供默认构造就会报错,所以还要显示的调用拷贝构造。

3. 派生类的operator=必须要调用基类的operator=完成基类的复制。

这里的赋值运算符重载会出现错误:

在vs2022上抛异常,在vs2019上面会出现上图的东西,vs2019上调试会抛异常栈溢出,这里的问题就是出现隐藏,子类和父类函数名相同,想调父类调不到,优先调用自己。 指定一下就可以了。

扩展知识:在我们的设计当中,不要去设计一些父类的私有成员,也不要用保护继承和私有继承,如果用保护继承和私有继承这些会出现很多的坑,包括赋值兼容规则,如果用的保护继承,这个规则就不支持了。刚刚指的赋值兼容是在公有继承里面才用。

s1= s3还可以用,Person p = s1就不能用了。 

如果用了保护继承以后,Person的成员权限可能会存在变化。平时就用公有继承就可以了。

如果想调用析构的话需要显示的调用,是因为后面多态的原因(具体后面会讲),析构函数的函数名被特殊处理了。因为要形成多态的原因,析构函数的函数名都被统一处理成destructor。~Student()和~Person()它们两个看起来名字不一样,因为后面被处理的原因,它们两个构成隐藏,这个时候就要指定一下。

指定一下这里就不对了,总共有三个对象,却调用了六次析构函数。

但是要把代码屏蔽掉的话就正确了。因此不需要我们调, 析构函数是一个特殊,它会自动调。为什么会自动调用,因为它要保证析构的顺序。

把一个子对象分成父的部分和子的部分,父类对象先构造和初始化,如果有两个子类对象,析构的时候后定义的先析构,也就是子的部分先析构。

子类析构函数完成后,自动调用父类析构,这样就保证了先子父,显示调用父类析构是没办法保证先子后父的:

为什么要先子后父?

这里有两个原因,第一个是符合在栈里面定义的顺序,第二个是子类当中有可能会用到父类成员的, 父类不可能用到子类。

如果先父后子肯定坑, 因为子类中可能还会访问父类的成员,假设先析构在去访问肯定是不行的。

父类析构:

子类析构: 

4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

5. 派生类对象初始化先调用基类构造再调派生类构造。

6. 派生类对象析构清理先调用派生类析构再调基类的析构。

7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

六个默认成员函数前四个是最重要的。

5.继承与友元

友元关系不能继承,也就是说基类的友元函数不能访问子类私有和保护成员。

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

void main()
{
    Person p;
    Student s;
    Display(p, s);
}

void main()
{
    Person p;
    Student s;
    Display(p, s);
}

Display()是父类的友元,但是不是子类的友元,因此运行的时候,这个代码会报错,也就是友元关系不能继承。

如果想让上述代码通过:

class Student;

class Person
{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name; // 姓名
};

class Student : public Person
{
    friend void Display(const Person& p, const Student& s);
protected:
    int _stuNum; // 学号
};

void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}

void main()
{
    Person p;
    Student s;
    Display(p, s);
}

6. 继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。

父类的静态成员能不能继承?这里要分为两个角度。


http://www.kler.cn/a/414585.html

相关文章:

  • 【C++】深入解析 using namespace std 语句
  • dns 服务器简单介绍
  • css:项目
  • Windows修复SSL/TLS协议信息泄露漏洞(CVE-2016-2183) --亲测
  • 道可云人工智能元宇宙每日资讯|第三届京西地区发展论坛成功召开
  • 华为昇腾 acl_pytorch
  • 【C++】 list接口以及模拟实现
  • 【AI技术赋能有限元分析应用实践】pycharm终端与界面设置导入Abaqus2024自带python开发环境
  • 美畅物联丨如何通过ffmpeg排查视频问题
  • 直播实时美颜平台开发详解:基于视频美颜SDK的技术路径
  • go 和java 编写方式的理解
  • 数据安全与隐私保护:大数据时代的挑战与机遇
  • 华为海思2025届校招笔试面试经验分享
  • 关于Spring基础了解
  • SOLID原则学习【目录篇】
  • Ubuntu20.04下安装VSCode(配置C/C++开发环境)和设备树插件用于嵌入式开发
  • ESP32学习笔记_Peripherals(1)——UART
  • 企业建站高性能的内容管理系统
  • Swagger记录一次生成失败
  • 关于IDE的相关知识之一【使用技巧】
  • python(四)os模块、sys模块
  • git如何给历史提交打标签
  • 【Vue2.x】vue-treeselect修改宽度、回显
  • 电脑无互联网连接怎么解决?分享5种解决方案
  • (0基础保姆教程)-JavaEE开课啦!--11课程(初识Spring MVC + Vue2.0 + Mybatis)-实验9
  • SpringBoot源码-Spring Boot启动时控制台为何会打印logo以及自定义banner.txt文件控制台打印