【C++】继承

在这里插入图片描述

欢迎来到Cefler的博客
博客主页:那个传说中的man的主页
个人专栏:题目解析
推荐文章:题目大解析(3)


目录

  • 继承的概念
    • 概念
    • 继承的方式
  • 基类和派生类对象赋值转换
  • 继承中的作用域
  • 派生类的默认成员函数
  • ✍关于继承的一些选择题

继承的概念

概念

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

继承的方式

【C++】继承_第1张图片
如上图所示不同继承下的派生类的访问权限各有不同。
但是我们可以注意到,私有继承和保护继承似乎在单继承上的访问权限上没有体现什么太大区别,这个我们不用急,在多次继承中会慢慢体现差别,或者我们想让成员函数在类外不能被访问,在派生类中能被访问,就可以用到保护继承。

对于表格,我们大致能做出以下总结

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

  2. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
    最好显示的写出继承方式

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


。C++ 中的继承分为单继承多继承两种形式,其格式语法如下:

  1. 单继承格式

单继承是指一个派生类只能从一个基类派生出来。其继承格式如下:

class 派生类名 : 访问说明符 基类名{
    // 派生类的成员声明
};

其中,访问说明符有三种:public、protected 和 private。这些访问说明符用于控制基类成员在派生类中的访问权限。如果不指定访问说明符,默认情况下是 private 继承。

  1. 多继承格式

多继承是指一个派生类可以同时从多个基类派生出来。其继承格式如下:

class 派生类名 : 访问说明符 基类名1, 访问说明符 基类名2, ...{
    // 派生类的成员声明
};

其中,每个基类名都需要指定一个访问说明符。多继承的访问说明符也有三种:public、protected 和 private。这些访问说明符用于控制不同基类的成员在派生类中的访问权限。

需要注意的是,当一个派生类同时从多个基类中继承了同名成员时,会出现二义性问题。为了解决这个问题,C++ 提供了虚继承机制。在虚继承中,如果多个基类都包含了同名成员,派生类只继承其中的一个,从而避免了二义性的问题。虚继承的格式如下:

class 派生类名 : 虚继承 访问说明符 基类名{
    // 派生类的成员声明
};

(虚继承我们后续再详细说明)

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

我们知道,在c++赋值操作时,在很多场景下都存在这隐式类型转换,如下代码:

int main() {
    string& str = "xxxx";
    return 0;
}

【C++】继承_第2张图片
这里报了错误,原因就是隐式类型转换产生临时变量,而临时变量具有常性,肯定不能直接赋值给引用str,会导致权限的放大,解决方法是用const 修饰string& str以达到对str权限的缩小。
在这里插入图片描述

但是在继承这里,我们传统概念上的认知就有点产生崩塌了,看如下代码:

class Person
{
protected:
    string _name; // 姓名
    string _sex;  // 性别
    int _age; // 年龄
};
class Student : public Person
{
public:
    int _No; // 学号
};
int main() {
    Student stu;
    Person per = stu;
    return 0;
}

【C++】继承_第3张图片
竟然没有报错?明明Student类赋值给Person类中间肯定会发生隐式类型转换产生临时变量。

但答案是:这中间不产生临时对象!!

我们需要引入新的概念
public继承中,父类和子类是is a 的关系,子类对象赋值给父类对象/父类的指针/父类的引用,都不产生临时对象,这个叫做父子类赋值兼容规则

这里补充一下,父类对象不能赋值给子类对象,会报错
【C++】继承_第4张图片
总结一下:

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
    或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
    的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再讲解,这里先了解一下!

继承中的作用域

若在子类中有和父类同名的成员变量/函数,在子类中访问这个成员变量,到底是访问哪个呢?
答案是我们遵循就近原则,访问子类本身的成员变量/函数。而如果我们想访问父类的同名成员,可以用访问限定符::,
父类名::成员变量/函数去指明访问。

这里总结一下

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

派生类的默认成员函数

在派生类中,如果没有显示定义诸如构造函数、拷贝构造函数、析构函数等的默认成员函数。
派生类会默认调用父类的默认成员函数
1.有参构造函数初始化基类成员变量
【C++】继承_第5张图片

上述代码中,在我们初始化基类的成员_name的时候出现了报错,我们稍微改正一下:
【C++】继承_第6张图片
原因是,在c++规定中,在派生类如果想初始化基类的成员变量,必须显示调用父类的构造函数
也就是说,子类你自己定义的成员变量,你在你的构造函数中想怎么初始化我不管,但如果要初始化我父类的成员变量,
不好意思抱歉,必须用我自己的构造函数。

2.operator = 运算符重载赋值

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=构成重定义(也就是隐藏)
 //而我们想要的是调用父类的operator =运算符重载,所以用访问限定符
 _num = s ._num;
 }
 return *this ;
 } 
 protected :
 int _num ; //学号

3.析构函数析构基类成员变量

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=构成重定义(也就是隐藏)
 //而我们想要的是调用父类的operator =运算符重载,所以用访问限定符
 _num = s ._num;
 }
 return *this ;
 } 
 ~Student()
 {
 //Person::~Person();
 cout<<"~Student()" <<endl;
 }
 protected :
 int _num ; //学号

这里有两个问题

  • 为什么要用Person::~Person(),不直接 ~Person()?
    原因是因为在c++规定中,父子类的析构函数,最终都会被解释为destructor.
    所以父子类的析构函数构成重定义(也就是隐藏),必须要用访问限定符才能确定访问哪个类的析构函数。
  • 这里为什么又不调用Person::~Person()了?
    原因是因为在c++规定中,对于构造函数,是先父类后子类,这个好理解,先有父后有子。
    但是对于析构函数,是先子类后父类,这是为什么呢?举个反面例子就能明白:如果是父类先析构,父类的成员变量全部销毁,此时如果在子类的析构函数中有调用父类的成员变量,这边不就会出错吗?
    于是乎,为了能够达成先子类后父 类的析构顺序,c++规定了,在子类的析构函数中不用再调用父类的析构函数来析构父类的成员变量了,编译器会自动在子类的析构函数调用完之后,再调用父类的析构函数。

————————————————————————————————————————————
对于这个小点,我们做个总结

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
    的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
    保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造
  6. 派生类对象析构清理先调用派生类析构再调基类的析构
  7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
    解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
    virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

✍关于继承的一些选择题

1.关于模板的编译说法错误的是( A )
A.模板在.h文件中声明,在.cpp里面实现
B.模板程序一般直接在一个文件里面进行定义与实现
C.不久的将来,编译器有望支持export关键字,实现模板分离编译
D.模板不能分离编译,是因为模板程序在编译过程中需要经过两次编译

解析:
A.模板不支持分离编译,所以不能在.h声明,在.cpp实现

B.由于不支持分离编译,模板程序一般只能放在一个文件里实现

C.不支持分离编译并不是语法错误,而是暂时的编译器不支持,不久将来,或许会被支持

D.模板程序被编译两次,这是不能分离编译的原因所在

2.下面代码输出结果:( D)

class A

{

public:

  void f(){ cout<<"A::f()"<<endl; }

  int a;   

};



class B : public A

{

public:

  void f(int a){cout<<"B::f()"<<endl;}

  int a;

};



int main()

{

  B b;

  b.f();

  return 0;

}

A.打印A::f()
B.打印B::f()
C.不能通过编译,因为基类和派生类中a的类型以及名称完全相同
D.以上说法都不对

解析:
A.错误

B.错误

C.不能通过编译是正确的,不过原因不是因为成员变量a的问题,而是子类同名隐藏了父类方法的原因

D.很显然以上说法都不对

下面说法正确的是(D )
A.派生类构造函数初始化列表的位置必须显式调用基类的构造函数,已完成基类部分成员的初始化
B.派生类构造函数先初始化子类成员,再初始化基类成员
C.派生类析构函数不会自动析构基类部分成员
D.子类构造函数的定义有时需要参考基类构造函数

解析:
A.如果父类有默认构造函数,此时就不需要

B.顺序相反,先初始化父类,再是子类

C.会调用,并且按照构造的相反顺序进行调用

D.是的,需要看父类构造函数是否需要参数子类的,从而你决定子类构造函数的定义

4.关于基类哪些成员被子类继承说法不正确的是( C)

A.静态成员函数
B.所有成员变量
C.基类的友元函数
D.静态成员变量在整个继承体系中只有一份

解析:
A.静态成员函数也可以被继承

B.成员变量所有的都会被继承,无论公有私有

C.友元函数不能被继承,相当于你爹的朋友不一定是你的朋友

D.静态成员属于整个类,不属于任何对象,所以在整体体系中只有一份

你可能感兴趣的:(C++,c++,开发语言,继承)