文章目录

  • 继承
    • 单继承
    • 多重继承
  • 派生
    • 派生类的声明方式
    • 派生类的构成
    • 派生类成员的访问属性
      • 公用继承
      • 私有继承
      • 保护继承
    • 普通派生类的构造函数
      • 创建普通对象的构造函数
    • 有子对象的派生类的构造函数
      • 创建子对象派生类构造函数
    • 普通派生类的析构函数
    • 有子对象的派生类的析构函数
  • 虚函数
    • 虚函数的作用
    • 虚函数的使用方法
      • 虚函数表
    • 在什么情况下应当声明为虚函数
    • 构造函数和析构函数能不能设计成虚函数?
  • 非虚函数
  • 静态关联
  • 动态关联
  • 多态性
  • 纯虚函数
  • 抽象类

重载:函数名相同,参数列表不同、处在同一个作用域。
隐藏:基类和派生类当中 函数名相同 派生类会将基类的同名方法隐藏。如果想通过派生类对象访问基类的成员方法需要加作用域。
覆盖:基类和派生类中 函数的返回值 函数名 参数列表,而且基类的函数是virtual虚函数

继承

继承的作用:面向对象技术强调软件的可重用性。C++使用类的继承机制,解决了软件重用问题。

所谓继承就是在一个已存在的类的基础上建立一个新的类。已存在的类称为基类或父类。新建立的类称为派生类或子类。
一个新类从已有的类哪里获得其已有特性,这种现象称为类的继承

单继承

一个派生类只从一个基类派生,称为单继承,这种继承关系 所形成的曾次是一个树形结构。

多重继承

一个派生类有两个或多个基类的称为多重继承。

派生

从已存在类(父类)产生一个新的子类,称为类的派生

派生类继承了基类的所有数据成员和成员函数.

一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类,因此基类和派生类是相对而言的。类的每一次派生,都继承类其基类的基本特征,同时又根据需要调整和扩充原有特征。

基类和派生类的关系:派生类是基类的具体化,而基类是派生类的抽象。
基类综合了派生类的公共特征,派生类则在基类的基础上增加了某些特性,把抽象编成具体的、实用的类型。

派生类的声明方式

class A
{public:int ma;
protected:int mb;
private:int mc;
};
class B : private A // 类B私有继承了类A
{public:void test() { cout << mb << endl; }int md;
protected:int me;
private:int mf;
};


在class后面的B是新建的类名,冒号后面的A是已经存在的基类。在A前有一个关键字private,用来表示基类A中的成员在派生类B中的继承方式。
继承方式包括:public(公用的),private(私有的)和protected(受保护的),如果不写,默认是private(私有的),而用struct 定义的类,如果不写,默认是public(公用的)。

派生类的构成

派生类中的成员

  • 从基类继承过来的成员
  • 自己增加的成员

从基类 继承的成员体现了派生类从基类继承而获得的共性,而新增加的成员体现了派生类的个性。正是这些新增加的成员体现了派生类与基类的不同,体现了不同派生类之间的区别。

构造一个派生类

(1)从基类接收成员

派生类把基类的全部的成员(不包括构造函数和析构函数)接收过来,不能选择接收其中一部分成员,而舍弃另一部分成员。

这就有可能出现一种情况:有些基类的成员在派生类是用不到的,但是也必须继承过来。这样就或造成数据的冗余,尤其是在多次派生之后,会在许多派生类对象中存在大量无用的数据,不仅浪费了大量的空间,而且在对象的建立、复制、赋值和参数的传递中,花费更多无谓的时间,降低了效率。所以,我们要根据根据派生类的需要合理的选择基类,使冗余量尽可能的小。

(2)调整从基类接收的成员

可以改变基类成员在派生类中的访问属性,这是通过指定继承方式实现的。

如可以通过继承把基类的公有成员指定位在派生类的访问属性为私有(派生类外不能访问)。
可以在派生类中声明一个与基类成员同名的成员,则派生类中的新成员户覆盖基类的同名成员。

(3)增加自己的成员

在声明派生类时,一般还应当自己定义派生类的构造函数和析构函数,因为构造函数和析构是不能从基类继承的。

派生类成员的访问属性

确定基类的成员在派生类的访问属性,不仅要考虑对基类成员所声明的访问属性,还要考虑派生类所声明的对基类的继承方式,这两个因素共同决定基类成员在派生类的访问属性。

公用继承

可以看出公有继承唯一改变了不能访问基类的私有成员。

因为私有成员体现了数据的封装性,隐藏私有成员有利于测试、调试和修改系统。如果把基类所有成员的访问权限都原封不动地继承到派生类,使基类的私有成员在派生类中仍保持其私有性质,派生类成员能访问基类的私有成员,那么基类和派生类就没有界限了,这就破坏了基类的封装性。
要记住:保护私有成员是一条重要的原则!!!

私有继承

可以看出:既然声明为私有继承,就表示将原来能被外界引用的成员隐藏起来,不让外界引用,因此基类的公有成员和保护成员理所当然地成为派生类的私有成员。

对于不需要再往下继承的类的功能可以用私有继承方式把它隐藏起来,这样下一层的派生类就无法访问它的任何成员。

保护继承

由"protected"声明的成员称为“受保护的成员”。受保护的成员不能被类外访问。

保护成员和私有成员类似,但有一点和私有成员不同,保护成员可以被派生类的成员函数引用。

普通派生类的构造函数

前面已经说过,基类的构造函数是不能继承的,再声明派生类时,派生类并没有把基类的构造函数继承过来,因此对继承过来的基类成员的初始化工作也要由派生类的构造函数承担。所以在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员的初始化,还应当考虑基类的数据成员初始化。也就是说,在执行派生类的构造函数时,使派生类的数据成员和基类的数据成员同时都被初始化。

解决办法:在执行派生类的构造函数时,调用基类的构造函数。

class Base
{public:Base(int a) :ma(a) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }void show() { cout << "Base::show()" << endl; }void show(int i) { cout << "Base::show(int)" << endl; }
protected:int ma;
};
class Derive : public Base
{public://“Base”: 没有合适的默认构造函数可用//Derive(int data) :ma(data), mb(data) { cout << "Derive()" << endl; }//errorDerive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }
/*
可以看到:派生类构造函数(Derive)后面括号内中的参数表中包括参数的类型和参数名,而基类构造函数后面括号内的参数列表只有参数名而不包括参数类型,因为这里不是定义基类构造函数,而是调用基类的构造函数,因此这些参数是实参而不是形参。
它们可以是常量、全局变量和派生类构造函数总参数列表中的参数。也可以将派生类的构造函数在类外面定义,只在类体中写该函数的声明:
Derive(int data);
请注意:在类中对派生类构造函数作声明时,不包括一般形式中的“基类构造函数名(参数表)”部分,即Base(data).只在定义函数时才将它列出
在类的外面定义派生类构造函数:
Derive::Derive(int data):Base(data),mb(data){}*/~Derive() { cout << "~Derive()" << endl; }void show() { cout << "Derive::show()" << endl;}
private:int ma;int mb;
};

创建普通对象的构造函数

  • 派生类构造函数先调用基类构造函数
  • 再执行派生类构造函数本身

有子对象的派生类的构造函数

对子对象的初始化是在建立派生类的构造函数来实现的。
此时派生类构造函数的任务应该包括3部分:

  • 对基类数据成员仅从初始化
  • 对子对象数据成员初始化
  • 对派生类对象成员初始化

创建子对象派生类构造函数

  • 调用基类构造函数,对基类数据成员初始化
  • 调用子对象构造函数数,对派生类数据成员初始化
  • 执行派生类构造函数本身,对派生类数据成员进行初始化。
#include<iostream>
#include<string>
using namespace std;class Student  //基类
{public:Student(int n, string nam) //基类的构造函数{num = n;name = nam;}void display() //基类的成员函数,用来输出基类数据成员{cout << "num:" << num << endl << "name:" << name << endl;}
protected:int num;string name;
};class Student1 :public Student  //声明公用派生类 Student1
{public:Student1(int n, string nam, int n1, string nam1, int a, string ad) :Student(n,nam), monitor(n1, nam1){age = a;addr = ad;}void Show(){cout << "This Student is:" << endl;display();          //输出num和namecout << "age:" << age << endl;//输出agecout << "address:" << addr << endl;//输出addr}void show_monitor()  //用来输出子对象的成员函数{cout << endl << "Class monitor is :" << endl;monitor.display();  //调用基类的成员函数}private://派生类的私有数据Student monitor; //定义子对象(班长)int age;string addr;
};int main()
{Student1 stud1(161001,"张三", 10001, "李四", 20, "weiyang Rode,xi'an");stud1.Show();stud1.show_monitor();return 0;
}


对上例来说,先初始化基类的中的数据成员num,name,然后再初始化子对象的数据成员num,name,最后初始化派生类中的数据成员age,addr.

派生类构造函数的总参数表中的参数,应当包括基类构造函数和子对象的参数表中的参数。基类构造函数和子对象的次序可以是任意的,如上面的派生类构造函数首部可以写成

Student1(int n,string nam,int n1,string nam1,int a,string ad):monitor(n1,nam1),Student(n,nam)

编译系统是根据相同的参数名(而不是根据参数的顺序)来确立它们之间的传递关系的。如总参数表中的n传给基类构造函数Student的参数n,总参数表中的n1传给子对象的参数n1.但是习惯上一般先写基类构造函数,以与调用的顺序一致,看起来清晰一些。

普通派生类的析构函数

执行析构函数的顺序是:

  • 先执行派生类的析构函数
  • 再执行其基类的析构函数

有子对象的派生类的析构函数

执行析构函数的顺序是:

  • 先执行派生类自己的析构函数,对派生类新增加成员进行清理
  • 然后调用子对象的析构函数,调用子对象的析构函数,对子对象进行清理
  • 最后调用基类的析构函数

虚函数

虚函数的作用

允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

#include<iostream>
#include<string>
using namespace std;class Base
{public:Base(int a) :ma(a) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }void show() { cout << "Base::show()" << endl; }void show(int i) { cout << "Base::show(int)" << endl; }
protected:int ma;
};
class Derive : public Base
{public:Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }~Derive() { cout << "~Derive()" << endl; }void show() { cout << "Derive::show()" << endl; }
private:int mb;
};
int main()
{Derive d(20);Base *pb = &d;pb->show(); //静态绑定 Base::show   call  Base::show (0E912DAh)cout << sizeof(Base) << endl;  // 4  cout << sizeof(Derive) << endl; // 8  // 识别的都是静态(编译时期)类型cout << typeid(pb).name() << endl;  // Base*  cout << typeid(*pb).name() << endl; // Base  return 0;
}

虚函数的使用方法

  • 在基类中用virtual声明成员函数为虚函数。在类外定义虚函数时,不必再加virtual.
  • 在派生类中重新定义此函数,函数名,函数的参数列表和返回值必须与基类的虚函数相同,根据派生类的需要重新定义函数体。
  • 当一个成员函数被声明为虚函数时,其派生类中的同名函数都自动成为虚函数。因此,在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明函数时都加virtual,使程序更清晰。
    如果在派生类中没有对基类的虚函数重新定义,则派生类简单的继承其直接基类的虚函数
  • 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
  • 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
#include<iostream>
#include<string>
using namespace std;class Base
{public:Base(int a) :ma(a) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }virtual void show() { cout << "Base::show()" << endl; }//虚函数virtual void show(int i) { cout << "Base::show(int)" << endl; }
protected:int ma;
};
class Derive : public Base
{public:Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }~Derive() { cout << "~Derive()" << endl; }virtual void show() { cout << "Derive::show()" << endl; }//同名虚函数
private:int mb;
};
int main()
{Derive d(20);Base *pb = &d;pb->show(); //动态绑定cout << sizeof(Base) << endl;  //  8cout << sizeof(Derive) << endl; //  12cout << typeid(pb).name() << endl;  //  Base*cout << typeid(*pb).name() << endl; //  Derivereturn 0;
}

虚函数表

每个含有虚函数的类有一张虚函数表(vtbl),表中每一项是一个虚函数的地址。也就是说,虚函数表的每一项是一个虚函数的指针。(在编译阶段产生)
我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。


(1)虚函数按照其声明顺序放于表中。

(2)父类的虚函数在子类的虚函数前面。

在什么情况下应当声明为虚函数

  • 用基类指针指向堆上创建的派生类对象,当delete基类指针的时候,由于指针的类型是基类类型,而且基类的析构函数是一个普通的函数,直接静态绑定调用基类的析构函数,导致派生类对象的析构函数无法调用,造成资源泄漏。
  • 用基类指针指向堆上创建的派生类对象,当delete基类指针的时候,由于指针的类型是基类类型,而且基类的析构函数是虚函数,此时析构函数的调用就要动态绑定,因为指针指向的是一个派生类对象,所以访问的就是派生类对象的虚函数表,最终取得得就是派生类的析构函数的地址,进而调用派生类的析构函数,那么当派生类的析构函数调用完,会自动调用基类的析构函数。

使用虚函数要注意的:

  • 只能用virtual声明类的成员函数,把它作为虚函数,而不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中队基类的虚函数重新定义。显然,它只能用于类的继承层次中。
  • 一个成员函数被声明为虚函数之后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同参数和返回值类型的同名函数。

在哪些情况下可以考虑将一个成员函数声明为虚函数

  • 看成员函数所在的类是否会作为基类,然后看成员函数在类的继承后有无可能被更改功能,如果希望更改功能,一般应该将它声明为虚函数。
  • 看对成员函数的调用时通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数

构造函数和析构函数能不能设计成虚函数?

构造函数不能实现成虚函数(同static函数)

  1. 调用构造函数之前,对象还不存在,无法进行动态绑定
  2. 如果构造函数实现成虚函数,那么一个派生类对象的构造,它首先会去调用基类的构造,试图初始化从基类继承过来的成员时,因为基类和派生类的构造函数达成覆盖关系,所以基类的构造根本无法调用(因为只要调用基类的构造函数,根据动态绑定,就直接调用到派生类的同名覆盖方法了),造成派生类对象根本无法构造起来!!

析构函数可以实现成析构函数

非虚函数

有时在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数,如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为。

静态关联

函数重载、通过对象名调用的虚函数,在编译时期就可以确定其调用的虚函数属于哪一个类,其过程称为静态关联。

动态关联

在调用虚函数使没有指定对象名,那么系统是如何确定关联的呢?是通过基类指针和虚函数的结合来实现多态性的,先定义一个指向基类的指针变量,并使他指向相应的类对象,然后通过该基类指针去调用虚函数,显然对于这样的调用方式,编译系统在编译该行时是无法确定调用哪一个类对象的虚函数的。因为编译只做静态的语法检查,光从语句形式是无法确定调用对象的。
在这种情况下,编译系统把它放到运行阶段处理,在运行阶段确定关联关系。在运行阶段,基类指针变量先指向了某一个类对象,然后通过该指针变量调用该对象中的函数。此时调用哪一个对象的函数就是确定的了。由于是在运行阶段把虚函数和类对象绑定在一起的,此过程称为动态关联。这种多态性时运行时的多态性。

多态性

通过虚函数与指向基类对象的指针变量的配合使用,就能实现动态的多态性。

如果想调用同一类族中不同类的同名函数,只要先用基类指针指向该类的对象即可。如果指针先后指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数

纯虚函数

定义:纯虚函数是在声明虚函数时被“初始化”为0的函数。
声明纯虚函数的一般形式是

virtual  函数类型  函数名  (参数列表) =0;

注意:

  • 纯虚函数没有函数体
  • 最后面的“=0”并不是说函数的返回值为0,它只起到形式上的作用,告诉编译系统“这是纯虚函数”
  • 这是个声明语句,最后要记得加分号
    作用:在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。如果在基类中没有保留函数名字,则无法实现多态性。

抽象类

定义:不用来定义对象而只作为一种基本类型用作继承的类,称为抽象类
凡是包含纯虚函数的类都是抽象类
注意:

  • 抽象类不能定义对象,但是可以定义指向抽象类的指针变量。当派生类成为具体类之后,就可以用这种指针指向派生类对象,然后 通过该指针调用虚函数,实现多态性。
  • 如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该函数在派生类中仍然为纯虚函数

继承与派生、虚函数、多态相关推荐

  1. C++中类,对象,封装,继承(派生),多态

    文章目录 1.类 2.对象 3.封装 4.继承(派生) 5.多态 1.类 类就是同一类事物的总称,比如我(一个对象)可以讲话,那么基本上所有人都具备这个属性,就将我这一类的对象称为类,类的思想就是这样 ...

  2. c++ 虚函数多态、纯虚函数、虚函数表指针、虚基类表指针详解

    文章目录 静态多态.动态多态 虚函数 哪些函数类型不可以被定义成虚函数? 虚函数的访问方式 析构函数中的虚函数 虚函数表指针 vptr 多继承下的虚函数表 虚基类表指针 bptr 纯虚函数 抽象类 虚 ...

  3. 透过汇编另眼看世界之多继承下的虚函数函数调用

    在我的前一篇文章"透过汇编另眼看世界之函数调用"中,我们通过汇编了解了虚函数调用的全部过程.在本文中我将分析多继承的情况下虚函数调用的情况. 首先还是写一些简单的代码作为本文分析的 ...

  4. C++基本概念复习之二:多重继承、虚继承、纯虚函数(抽象类)

    一.多重继承: #include <iostream> using namespace std; class Horse { public: Horse(){cout<<&qu ...

  5. 面试准备每日五题:C++(七)——左值右值、面向对象、四种cast转换、拷贝构造函数赋值、虚函数多态

    文章目录 一. 什么是右值引用,跟左值又有什么区别? 二. 面向对象的三大特征 三. c++中四种cast转换 四.拷贝构造函数和赋值运算符的认识 五. 对虚函数和多态的理解 一. 什么是右值引用,跟 ...

  6. C++ 论公有继承时纯虚函数、虚函数、普通函数的行为表现及虚函数的重写(深度好文)

    文章目录 1 公有继承时三种类型的函数行为 1.1 纯虚函数 (pure virtual) 1.2 普通虚函数 **1.2.1 方法一** **1.2.2 方法二** 1.3 非虚函数 2 重写 (o ...

  7. 揭秘继承技术之虚函数

    虚函数 调用虚函数时函数行为将根据对象所属类的不同而变化. 父类指针或引用指向子类对象时,可访问子类重写方法( virtual函数)但无法访问在父类中没有定义的子类方法和数据成员. #include ...

  8. Unity2D学习笔记Day12:敌人统一死亡动画+Class的继承(含虚函数virtual,重写override)

    学习资源:B站 M_Studio<Unity教程2D入门>17 Unity Assets:Sunnyland Day12 调用frog死亡动画效果 这里我没有完全按照教程的方法,是自己写的 ...

  9. C++ 面向对象(二)多态 : 虚函数、多态原理、抽象类、虚函数表、继承与虚函数表

    目录 多态 多态的概念 多态的构成条件 虚函数 虚函数的重写 协变(返回值不同) 析构函数的重写(函数名不同) final和override final override 重载, 重写, 重定义对比 ...

  10. C++编程思想:继承与虚函数以及多态

    文章目录 简介 实现虚函数多态的技术原理 对象切边 析构函数和构造函数中的虚函数 使用继承的类的析构函数应该使用虚函数修饰符 简介 继承与虚函数与多态是浑然一体的三个概念,父类中虚函数可以表现出多态特 ...

最新文章

  1. Perl 模块安装遇到的问题解决办法
  2. 【论文解读】MV3D-Net、AVOD-Ne用于自动驾驶的多视图3D目标检测网络
  3. SpringMVC - 1.DispatcherServlet
  4. 如何在网页中添加flash文件
  5. java队列queue的我觉得很好的使用方式
  6. python优雅写法
  7. 通俗易懂讲解自适应提升算法AdaBoost
  8. STL标准库vector笔记
  9. Linux学习第二步(Java环境安装)
  10. 阿里大数据分析与应用(part7)--机器学习平台PAI
  11. python中三种分支结构的_python 运算符与分支结构
  12. .NET Core 下的爬虫利器
  13. 【生信进阶练习1000days】day1-Bioconductor的一些补充小用法与Working with Genomic Ranges
  14. 安川涂装机器人离线编程_安川机器人离线编程软件
  15. BIOS知识枝桠—— Protocol
  16. 浅析2D横版过关游戏关卡制作要点
  17. Flink大数据计算框架
  18. iOS内购项目的接入与审核问题
  19. netcat使用方法
  20. JQuery 图片滚动或者div滚屏,适合多图轮播

热门文章

  1. unbuntu安装pycharm
  2. 北京智和信通:IT资产全生命周期运维监控管理方案
  3. 华为HCIP RS题库221 221-230题
  4. 快手视频素材在哪找?各大平台视频素材批量下载
  5. 基于xilinx fpga的ofdm通信系统基带设计_仪器设备研制 | 基于AD9361的雷达干扰信号模拟器设计...
  6. 软考报名照片验证软件报错invalid floating point operation
  7. 苹果短信迁移到安卓手机
  8. Android | 教你如何开发一个证件照DIY小程序
  9. ASO优化之如何回复Google Play评论
  10. 小学生体测测试环境怎么填_国家学生体质健康标准测试上报常见问题解答