【C++从0到王者】第二十一站:继承

简介: 【C++从0到王者】第二十一站:继承



前言

继承是面向对象的三大特性之一。我们常常会遇到这样的情况。很多角色的信息是十分类似的,他们有公共的信息,还有独有的信息。比如学生、老师、保安大叔、食堂阿姨等。他们都有一份公有的信息。如果将这些接口给重复写很多次,是非常麻烦的。

class student
{
  string name;
  int age;
  string address;
  int tel;
  //其他独有信息
  //宿舍号、学号、专业...
};
class teacher
{
  string name;
  int age;
  string address;
  int tel;
  //其他独有信息
  //工号、学院、职称...
};

基于以上的原因我们引出了继承。即将公有的信息全部单独做好,然后让其他身份可以直接使用这个类,即继承了这个类

class Person
{
  string name;
  int age;
  string address;
  int tel;
};

一、继承的概念及定义

1. 继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保

持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象

程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继

承是类设计层次的复用。

通俗的讲:继承的本质就是复用,不过这里是类层面的复用,包括成员变量和成员函数

2.继承的格式

如下图所示,继承的格式即在定义新的类的时候,在后面加上冒号,继承方式和基类。

Person是父类,也被称之为基类。Student是子类,也被称之为派生类

如下是一个简单的继承,其中,Stundet和Teacher继承了Person。

class Person
{
public:
  void Print()
  {
    cout << "name :" << _name << endl;
    cout << "age :" << _age << endl;
  }
protected:
  string _name = "zhangsan";
  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;
}

我们在监视窗口看到的样子是这样的

可以观测到,继承即直接在s或t中有了Person这样一个类。拥有它的成员变量和成员函数。注意这里的拥有的成员函数指的是可以去调用它的成员函数,因为在类里面本身成员函数就是放在一个公共区域的。所以这里调用的成员函数其实还是公共区域的成员函数

代码运行结果如下所示

3.继承关系与访问限定符

我们知道访问限定符有三种,public,protected,private三种。同样的继承方式也是一样的。

那么这些又有何关系呢?

如下表所示,是继承基类成员访问的变化

类成员/继承方式 public 继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

对于这个表,我们需要知道的是

  1. 基类的私有成员,在派生类中是不可见的。这里的不可见指的是在派生类中有,但是没法用(类里面和类外面都不能用)。即语法上限制了访问,但是在内存中是存在的。与private是不一样的, private是类内可以使用,类外不可使用。

下面是一个样例,即父类私有成员,子类无论如何都用不了

  1. 对于公有和保护的,他们的关系其实就是取小的那一个,关系是public > protected >private。即public继承后,原来是什么成员,派生类还是什么成员。protected继承,原来是public还是protected成员现在都变成了protected成员。将原来的公有都变成了只在类里面可以使用的成员,但是这些成员还是可以被再次继承的并使用的。而private继承的话,原来无论是什么成员现在一律变为private成员,只能在派生类中适合用,而且如果别人在继承这个子类的话,那么新的派生类是无法访问这个成员的。
    所以我们就知道了,protected和private这两个访问限定符的区别。在之前他们还是一样的,但是现在,在继承中,他们有了区别,如果基类成员不想被类外的访问,但是在派生类中可以被访问的话,那么就使用protected。可以看出保护成员限定符是因为继承而出现的
  2. 还有一点需要注意的是,默认继承。class是private继承,struct是public继承,但是我们最好写出他们的继承方式。

  1. 事实上,我们常用的就是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
    我们一般使用的就是如下两种场景

二、基类和派生类的赋值转换

在我们正常的两个不同类型的对象进行赋值的时候一般是不允许的操作。如果真的允许了,那也是通过类型转换实现的

class Person
{
public:
  void Print()
  {
    cout << "name:" << _name << endl;
    cout << "age:" << _age << endl;
  }
protected:
  string _name = "zhangsan";
  int _age = 18;
};
class Student : public Person
{
protected:
  int _stuid;
};
class Teacher : public Person
{
protected:
  int _jobid;
};
int main()
{
  int i = 0;
  double d = i; //发生了类型转换
  Person p;
  Student s;
  p = s;
  //s = p; 不允许的操作
  return 0;
}

在继承中,也是存在类似于类型转换的。

在赋值的过程中,子可以给父,但是父不可以给子

至于原因也是很简单的。因为派生类中有些成员基类就没有,而基类的所有成员派生类都有,所以有了子可以给父的赋值

这里的父不可以给子是很严格的,即便使用了强制类型转换,依然报错。语法上直接给禁掉了。

一般我们也将子赋值给父称之向上转换,这样做是可以的。而向下转换,即父对象赋值给子对象是不允许的

这里的赋值转换和普通的赋值还是有一些不一样的。在我们之前的不同类型的赋值中,都要走一个隐式类型转换、强制类型转换等。这些都会产生临时变量。而这里是不会产生临时变量的。这里发生了一个特殊处理,即赋值兼容转换(或切割、切片)

这个赋值兼容(切割、切片)是天然的,不会产生临时变量。它不像以前一样不同类型转换会产生临时变量。

这里的切割切片就是认为每一个子类对象都是一个特殊的父类对象,它会将属于父类的一部分切出来进行赋值,然后将它拷贝给父类,所以称为切片。

那么如何证明没有临时变量呢?

如下代码所示就可以进行证明。如果中间产生了临时变量,那么我们使用引用的时候必须加上const进行修饰,因为临时变量具有常性。而我们父类引用子类的时候却没有加上const也不报错,故中间一定没有产生临时变量。而且我们还得出了,引用也可以向上转换

在上面代码中,经过引用以后p1就变成了s中父类部分的别名。我们先将 Person中的成员变量改为公有,然后使用p1这个别名进行修改,可以看到s也被修改了。从而印证了子类的别名也是可以给父类的。父类可以去引用子类。

除了引用之外,还有指针也是可以通过向上转换的。

现在我们就知道了对于向上转换而言,子类对象给父类对象,父类引用子类,父类指针指向子类都是可以的。

而对于向下转换,首先父类对象给子类对象是绝对不可以的,那么子类引用父类,子类指针指向父类呢?其实是可以的。不过这里稍微有点复杂,我们在后面文章在详细探讨。

三、继承中的作用域

我们知道,定义了一个类,这个类就有它自己的类域。对于派生类和基类都有它们自己的类域。

对于父类和子类,是允许有同名成员的。语法上是没有任何问题的。

但是当父类和子类出现同名成员的时候,优先使用子类的成员,如果子类没有,才去父类找。

如下代码所示:

class Person
{
protected:
  string _name = "zhangsan";
  int _age = 18;
  int _num = 666;
};
class Student : public Person
{
public:
  void Print()
  {
    cout << "name:" << _name << endl;
    cout << "age:" << _age << endl;
    cout << "num:" << _num << endl;
  }
protected:
  int _stuid;
  int _num = 111;
};
int main()
{
  Student s;
  s.Print();
  return 0;
}

可以看到最终结果是111.

但是如果就想访问父类的也是可以的,我们使用域作用限定符即可。

而编译器这样的操作,我们也称之为:隐藏/重定义,即子类和父类有同名成员,默认子类的成员隐藏了父类的成员

同样的,对于成员函数,我们也是同样的道理,默认访问子类的成员函数,但是如果使用域作用限定符,也是可以访问到父类的函数的。

不仅仅对于成员变量存在隐藏,对于成员函数也是存在隐藏的。规则与前面是一样的

class Person
{
public:
  void func()
  {
    cout << "Person::func()" << endl;
  }
protected:
  string _name = "zhangsan";
  int _age = 18;
  int _num = 666;
};
class Student : public Person
{
public:
  void func()
  {
    cout << "Student::func()" << endl;
  }
  void Print()
  {
    cout << "name:" << _name << endl;
    cout << "age:" << _age << endl;
    cout << "num:" << Person::_num << endl;
  }
protected:
  int _stuid;
  int _num = 111;
};
int main()
{
  Student s;
  s.func();
  s.Person::func();
  return 0;
}

我们如果对上面的代码稍作修改

即,在下面这种情况下,两个func构成什么关系?

a.隐藏/重定义 b.重载 c.重写/覆盖 d.编译报错

class Person
{
public:
  void func()
  {
    cout << "Person::func()" << endl;
  }
protected:
  string _name = "zhangsan";
  int _age = 18;
  int _num = 666;
};
class Student : public Person
{
public:
  void func(int i)
  {
    cout << "Student::func(i)" << endl;
  }
  void Print()
  {
    cout << "name:" << _name << endl;
    cout << "age:" << _age << endl;
    cout << "num:" << Person::_num << endl;
  }
protected:
  int _stuid;
  int _num = 111;
};

这道题答案是选a的,我们很容易误选为b,事实上重载的前提条件是在同一个作用域,这两个并不在同一个作用域,所以肯定不是重载。

如下面的测试,只要函数名相同就会构成隐藏,不会考虑到参数这些问题(因为函数名修饰规则在链接阶段)。中间的会在编译阶段就已经报错了。编译阶段带参的隐藏了无参的。所以最终中间的代码会报错

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

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

我们知道类有六个默认成员函数。“默认”的意思就是指我们不写,编译器会变我们自动生成一个。那么在派生类中,它们的生成又是如何进行变化的呢?

我们将下面这个类作为父类

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; // 姓名
};

然后当我们对派生类写它的构造函数的时候,我们传统的理解为_name可以直接使用,于是我们直接对_name放在了初始化列表中进行初始化。但是很遗憾,报错了。

初此之外,当我们决定先不管这个变量的时候,我们会发现编译器自动调用了父类的构造和析构

这是为什么呢?其实是因为C++规定了派生类必须调用父类的构造函数进行初始化。

而且这里的调用是在初始化列表调用的且调用的是默认构造,如果不提供默认构造也会报错。

这里其实就有点类似于将父类当成一个自定义类型的成员进行处理了。

相当于这里其实就分的很清楚,父类的交给父类的构造函数去搞。子类的自己去搞

而这里如果我们要自己去调用构造函数的话,我们就要像定义一个匿名对象一样在初始化列表中

而在初始化列表中,永远也是父类的第一个进行执行。相当于它永远是第一个成员变量。

以上是针对于构造函数的分析。

下面是针对拷贝构造的分析

当我们想要写一个拷贝构造的时候,拷贝构造本质也是一个构造函数,所以也要写初始化列表

如上所示,我们这里对于Person要显式调用它的拷贝构造函数,这里虽然我们没有父类对象,但是由于前面说了,可以向上转换,所以直接将s传过去就可以了。所以下面会被初始化为zhangsan

这里如果我们不显式写拷贝构造的化也是没问题的。不写,它就初始化列表自动调用默认构造函数(因为拷贝构造也是一个构造函数要遵循构造函数的规则),所以下面会被初始化为peter。

还有一个默认成员函数是赋值运算符重载,我们不难写出这样的代码,注意这里必须指定父类中的赋值运算符重载,才能将父类的成员函数给赋值过去。然后再来一个普通的赋值即可。如果不指定父类的话,就是默认找子类的,就会发生无穷递归,栈溢出了。

下面是析构函数

如下所示,是我们想象中析构函数应该有的样子。注意,这里也必须加上父类的访问限定符,虽然看上去好像可以直接调用,但是必须加上,因为不加会报错,报错是因为由于多态的原因,析构函数的函数名被特殊处理了,统一处理为destructor

但是上面仅仅是我们所想的,实际上上面是错误的。因为如下图所示,我们会发现Person被析构的次数多了一倍。

而一旦我们显示调用的析构给屏蔽掉,就正确了

所以说,析构函数不需要我们自己去调用。因为它必须要保证析构顺序,默认是最后才析构的(构造顺序是,先父后子,析构顺序是先子后父),为了保证这个顺序,于是编译器始终默认最后才自动调用析构函数。而如果让我们显式调用的话,是没法保证先子后父的。而必须先析构子在析构父的一个原因就是子可以用父,父不能用子。也就是说,如果先析构了父的话,但是如果后面子突然调用了父的一部分成员,就会出错了。

五、继承与友元

一个核心:友元关系不可以被继承

如下代码所示:我们先声明了Student类,然后我们用Student继承Person类,Display函数是Person的友元。所以在Display函数中可以去访问Person类成员变量,但是这个友元关系不可以被继承,所以Display中直接访问Student成员变量直接报错

如果要让这个函数可以访问子类,那么可以对子类也使用友元

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

六、继承与静态成员

静态成员能否被继承呢?

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

换言之:静态成员可以认为是继承了,也可以认为没有被继承

在前面的继承中,继承就是指在子类里面存了一份父类的成员。在子类里面可以去访问父类的成员。子类里面存的父类成员和父类成员是没有关系的。都是单独的个体。

在静态成员中,由于一个静态成员只存储一份。所以子类里面并没有这个部分,但是子类确实可以去访问父类里面的这个静态成员。介于一个中间状态,所以我们可以认为它继承了,也可以认为它没有被继承

class Person
{
public:
  Person() { ++_count; }
//protected:
  string _name; // 姓名
public:
  static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
  int _stuNum; // 学号
};
int main()
{
  Person p;
  Student s;
  cout << Person::_count << endl;
  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;
}


好了本期内容就到这里了

如果对你有帮助的话,不要忘记点赞加收藏哦!!!

相关文章
|
4天前
|
Java C++
C++的学习之路:21、继承(2)
C++的学习之路:21、继承(2)
14 0
|
29天前
|
C++
8. C++继承
8. C++继承
22 0
|
29天前
|
安全 Java 编译器
C++:继承
C++:继承
32 0
|
1月前
|
安全 Java 编译器
C++:继承与派生
C++:继承与派生
|
1天前
|
设计模式 编译器 数据安全/隐私保护
C++ 多级继承与多重继承:代码组织与灵活性的平衡
C++的多级和多重继承允许类从多个基类继承,促进代码重用和组织。优点包括代码效率和灵活性,但复杂性、菱形继承问题(导致命名冲突和歧义)以及对基类修改的脆弱性是潜在缺点。建议使用接口继承或组合来避免菱形继承。访问控制规则遵循公有、私有和受保护继承的原则。在使用这些继承形式时,需谨慎权衡优缺点。
12 1
|
1月前
|
安全 编译器 程序员
C++中的继承
C++中的继承
22 2
|
1月前
|
算法 编译器 程序员
【C++ 泛型编程 入门篇】全面掌握C++元模板中的模板继承:模板继承深入指南和教程
【C++ 泛型编程 入门篇】全面掌握C++元模板中的模板继承:模板继承深入指南和教程
37 0
|
1月前
|
Java 编译器 C#
【C++ 继承的多样性】C++ 多继承与虚拟继承:探究核心原理与应用实践
【C++ 继承的多样性】C++ 多继承与虚拟继承:探究核心原理与应用实践
37 1
|
1月前
|
算法 编译器 程序员
【C++入门到精通】C++入门 —— 继承(基类、派生类和多态性)
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
81 0
|
1月前
|
设计模式 算法 数据库
【C++ 继承】C++继承解密:一步步引领您从基础到实战
【C++ 继承】C++继承解密:一步步引领您从基础到实战
64 0