C++秋招记录(二)——c++多态

析构函数、构造函数、虚函数相关问题

一、虚函数

1、继承下的虚函数表

虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。

  • 单继承(无重写):虚函数按照声明的顺序放置于表中;父类的虚函数在子类前面
  • 单继承(有重写):有重写的虚函数,父类的虚函数被子类覆盖;其它同上
  • 多继承:每一个父类都有自己的虚函数表;子类的虚函数放于第一个父类(按照声明顺序)的虚函数表中;重写同上
  • 虚继承:虚继承的子类有单独一个虚函数表,也单独存放父类的虚函数表。

2、virtual继承和virtual函数

  • 虚继承与普通继承是两种继承方式。
  • 由于c++支持多重继承,所以对于一个派生类而言,可能有多个直接父类,其直接父类可能会分别继承同一个基类,那么会出现最终派生类中含有多个同一基类的情况,就会产生二义性(不知道调用哪个类的成员和函数)
  • 使用虚继承,只对此基类生成一块内存区间,最终派生类只含有一个基类。

3、virtual函数的优缺点

优点:多态
缺点:

  • 在子类很少覆盖基类函数实现的时候内存开销太大,每个类需要产生一张虚表,每生成一个对象的时候,需要在对象里存放一个vptr。
  • 基类指针指向派生类对象的情况下,无法内联优化
  • 在执行效率上多了一些开销,需要寻找函数地址,多了几个汇编指令
  • MFC中的消息机制没有采用C++中的虚函数机制,原因是消息太多,虚函数内存开销太大。在Qt中也没有采用C++中的虚函数机制,STL里的容器都没有虚析构函数。

4、构造函数为什么不能是virtual?

  • (对象没有创建,何来vptr)构造函数执行前,对象的内存信息都没有,vptr也没有初始化,如何找到虚函数表,实现虚调用。
  • 虚函数是为了实现多态,让子类去覆盖父类的操作。而构造函数是从父类开始构造成子类,不能跳过父类的构造函数,子类的很多成员可能需要在父类的基础上去初始化。

5、如何定义clone来实现类似virtual constructor的功能?

只是想去通过子类的构造函数做一些特别的初始化,所以排除C++构造函数底层的执行逻辑,调用虚构造函数也是有意义的。参考Delphi语言,因为他有一种类类型,所有的类都继承自这个类类型Tclass,那么当自定义一个类的时候,你的父类其实是通过调用子类的虚拟tclass->creat函数来创建一个子类对象,那这个操作就相当于C++里面的虚构造函数了。

6、析构函数为什么要是virtual?

基类指向派生类(析构函数非virtual的话),只能调用基类析构函数(基类对象),内存部分释放,可能会造成内存泄露,而有virtual函数的话,通过虚函数表查找到派生类的虚析构函数,先释放派生类在释放基类。

7、virtual函数可以是static吗?

不可以

  • virtual是动态绑定,晚绑定;static是静态绑定,早绑定
  • static无this指针,virtual 函数调用需要引用或指针

8、构造、析构函数包含函数可以有virtual函数?

  • 构造函数和析构函数调用虚函数并不能达到多态的效果:因为在析构和构造过程中,该对象变为一个基类对象,调用的方法都是基类的方法。
  • 构造函数:在基类的构造过程中,虚函数调用从不会被传递到派生类中。代之的是,派生类对象表现出来的行为好象其本身就是基类型。不规范地说,在基类的构造过程中,虚函数并没有被"构造"。在基类对象构造期间,调用的虚函数的版本是基类的而不是子类的。
  • 析构函数:一个派生类的析构器运行起来,该对象的派生类数据成员就被假设为是未定义的值,这样以来,C++就把它们当做是不存在一样。一旦进入到基类的析构器中,该对象即变为一个基类对象,C++中各个部分(虚函数,dynamic_cast运算符等等)都这样处理。
	#include <iostream>
	class fa {
	 public:
	    fa()
	    {
	        con();
	    }
	    virtual void con() {
	        std::cout << "fa construct virtual" << std::endl;
	    }
	    virtual void decon() {
	        std::cout << "fa deconstruct virtual" << std::endl;
	    }
	    virtual ~fa(){
	        decon();
	    }
	};
	
	class son:public fa {
	public:
	    son(){
	        con();
	    }
	    virtual void con() {
	        std::cout << "son construct virtual" << std::endl;
	    }
	    virtual void decon() {
	        std::cout << "son deconstruct virtual" << std::endl;
	    }
	    ~son(){
	        decon();
	    }
	};
	int main()
	{
	    fa* fath = new son;
	    delete fath;
	    //std::cout << sizeof(xx);
	}

结果为:

fa construct virtual
son construct virtual
son deconstruct virtual
fa deconstruct virtual

二、多态性

1、C++的多态性

  • 多态的作用主要是两个:
    • 隐藏实现细节,使得代码模块化;扩展代码模块,实现代码重用
    • 接口重用,为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用。
  • 多态类型:
    • 静态多态性:函数多态性——函数重载
      模板多态性——C++模板(类模板、函数模板)
    • 动态多态性:虚函数(只有用地址才能实现动态多态性)
  • 运行期多态的优缺点:
    • 优点:OO设计中重要的特性,对客观世界直觉认识;能够处理同一个继承体系下的异质类集合。
    • 缺点;运行期间进行虚函数绑定,提高了程序运行开销;庞大的类继承层次,对接口的修改易影响类继承层次。由于虚函数在运行期在确定,所以编译器无法对虚函数进行优化;虚表指针增大了对象体积,类也多了一张虚函数表。
  • 编译期多态的优缺点:
    • 优点:它带来了泛型编程的概念,使得C++拥有泛型编程与STL这样的强大武器。在编译器完成多态,提高运行期效率。具有很强的适配性与松耦合性,对于特殊类型可由模板偏特化、全特化来处理。
    • 缺点:程序可读性降低,代码调试带来困难。无法实现模板的分离编译,当工程很大时,编译时间不可小觑。无法处理异质对象集合。这类似于重载函数在编译器进行推导,以确定哪一个函数被调用。

2、静态绑定和动态绑定

  • 静态绑定:编译时绑定,通过对象调用
    又名前期绑定(eraly binding),绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期

    • template参数而言,隐式接口,它并不基于函数签名式,而是由有效表达式(valid expressions) 组成
  • 动态绑定:运行时绑定,通过地址实现
    又名后期绑定(late binding),绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

    • 显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。所谓的显式接口是指类继承层次中定义的接口或是某个具体类提供的接口,总而言之,我们能够在源代码中找到这个接口.
  • 动态多态:1)类内有虚函数,2)采用“指针->函数()”或“引用变量.函数()”的方式调用C++类中的虚函数,3)父类指向子类,才会执行动态绑定。对于C++中的非虚函数,因为其不具备动态绑定的特征,所以不管采用什么样的方式调用,都不会执行动态绑定。
    在这里插入图片描述

3、模板类?

模板使程序员能够快速建立具有类型安全的类库集合和函数集合,它的实现,方便了大规模的软件开发。

  • 类模板,可以定义相同的操作,拥有不同数据类型的成员属性。通常使用template来声明。告诉编译器,碰到T不要报错,表示一种泛型。模板类:类模板实例化。
    (1)在模板类的继承中,需要注意以下几点:
    • 如果父类自定义了构造函数,记得子类要使用构造函数列表来初始化
    • 继承的时候,如果子类不是模板类,则必须指明当前的父类的类型,要分配内存空间
    • 继承的时候,如果子类是模板类,要么指定父类的类型,要么用子类的泛型来指定父类
      在这里插入图片描述
      (2)内部声明友元模板函数+外部定义友元模板函数
      如果普通的模板函数声明在内的内部,定义在类的外部,不管是否处于同一个文件,就跟普通的函数一样,不会出现任何错误提示。但是如果是友元函数就会出现报错,是因为有二次编译这个机制存在。

(3)模板类和模板函数的机制

  • 在编译器进行编译的时候,编译器会产生类的模板函数的声明,当时实际确认类型后调用的时候,会根据调用的类型进行再次帮我们生成对应类型的函数声明和定义。我们称之为二次编译

  • 因为这个机制,会经常报错找不到类的函数的实现。在模板类的友元函数外部定义时,也会出现这个错误。解决方法是 “ 类的前置声明和函数的前置声明

    template<typename T>
    class CBar;
    

(4)模板偏特化和全特化
在这里插入图片描述
(5)模板函数和模板成员函数
在这里插入图片描述

4、模板类中可以使用虚函数吗?

1)模板类中也可以使用虚函数
2)模板函数不能是虚函数

编译器期望在处理类定义的时候就能确定虚函数表的大小,如果允许有类的虚成员模板函数,那么就必须要求编译器提前知道程序中国有对该类的该虚成员模板函数的调用,而这时不可行的。

实例化模板类时,需要创建virtual table,而在模板类被实例化完成之前不能确定函数模板会被实例化多少,而虚函数的个数必须知道,否则这个类无法被实例化。

	#include<iostream>
	using namespace std;
	
	template<typename T>
	class A {
	public:
	    virtual void beep() { 
	    cout << "tempalte class father" << endl; }
	    //成员函数模板不能为虚函数 
	    template<typename T2>  void print(T2 type) { 
	    cout << type << ":" << "tempalte function " << endl; }
	};
	
	template<typename T>
	class B: public A<T>{
	public:
	    virtual void beep() { 
	    cout << "tempalte class son" << endl; }
	    //成员函数模板不能为虚函数 
	    template<typename T2>  void print(T2 type) { 
	    cout << type<<":"<<"tempalte function" << endl; }
	};
	
	int main()
	{
	    A<int>* a ; //声明,对象类型,静态绑定,类模板
	    a = new B<int>; //定义,地址,动态绑定,virtual多态
	    a->beep();
	    a->print("fa"); //调用时根据参数选择template function类型T2
	    system("pause");
	    return 0;
	}

三、

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值