C++虚函数多态原理-最直白的讲解

C++的主要特点是抽象,继承,封装和多态。我们先理解抽象是什么?在这之前,首先我问你C++是什么,是用来干什么的?

一切的语言都是用来描述现实世界的 ,C++也是.C++的任何特性都是为了去描述这个世界,并为其解决提供方法。但是C++还是并不能完全去描述这个世界,因为现实世界是无法完全认知的,只能不断去认知的,扯远了,感到到了哲学这段。抽象?例如现实世界的人类,书,树,桌子,椅子等名词都是一种对某些具有共同特征实体的抽象描述。感觉有点可以用集合来理解。难道每次去描述人时都是直立行走,两只眼睛,一直嘴巴?所以需要抽象来解决这个问题。因此抽象就对应了C++中的类。

类的基本定义形式:

class human{
    public:
        friend void ShowN(Internet &obj);
    protected:

    private:
};
void ShowN(human &obj)        
{ 
cout<<obj.name<<endl;          //可访问human类中的成员
} 
如果不标注属性,默认为public。
那protected右包括什么呢?类的本质属性,无法被类外访问的特性,只可以被子类与本类访问。
public:类外,类内均可访问,真正面对使用者的接口。
private:只可被类内使用,子类不可用。
也有特殊的情况,比如友元函数和友元类。会破坏类的封装性。
友元函数 : 可以访问类成员的普通函数。
       友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:
       friend 类型 函数名(形式参数);
       友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。
       一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
       友元函数的调用与一般函数的调用方式和原理一致。

友元类 : 
       友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。       
       当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
       friend class 类名;
       其中:friend和class是关键字,类名必须是程序中的一个已定义过的类。

再比如描述人时,你还是不知道它是亚洲人,欧洲人,还是非洲人,或许是外星人?所以又出现了继承这一概念,对应人集合中的子集。

1、公开继承:在公开继承下,父类型中的数据是公开的到子类型中权限是公开的;父类型中保护权限的数据到子类中是保护的;父类中私有的数据到子类中会隐藏掉(就是说看不见权限,但是实际上式在子类中的);

2、私有继承:在私有继承下,父类中的公开数据到子类中变成私有的,父类中的保护数据到子类中称为私有的,父类中的私有数据到子类中隐藏;

3、保护继承:保护继承下,父类中的公开数据和保护数据到了子类中都成为保护权限,父类中私有的数据到了子类中就变成了隐藏的;

继承的特殊点:

构造函数和析构函数是不能被继承的,但是可以被调用。并且子类一定会调用父类的构造函数。
子类默认调用父类的无参构造,也可以制定调用构造函数;当程序中没有显式的给出构造函数时候,系统会提供默认的构造函数,并不会中任何事情;

析构函数的调用和构造函数的调用顺序相反;

拷贝构造函数和赋值运算符函数也不能被继承:在子类不提供拷贝构造和赋值运算符时,子类默认调用父类的赋值运算符和拷贝构造函数。但子类一旦提供拷贝构造和赋值运算符函数则不再调用父类拷贝构造和赋值运算符函数。

封装就是对类中的信息进行分级,主要是针对类的实例来说,有些东西你可以调用,有些东西你不能调用。也就是类的设计者并不想让类的实例为所欲为。你只需要知道类的方法成员接口,具体内部的实现细节给隐藏起来了,随便搜一个开源项目比如opencv,你就只能知道类的具体接口以及功能,却并不知道具体是如何实现的?这就是封装。

突然想到人到底有没有灵魂一说呢?假如上帝是人类的设计者,每个人就是一个实例,将灵魂属性进行隐藏,你又如何知道它的存在呢?或许只有对实例代码进行研究透,搞明白任何原理才知道吧。

先来讨论一下子类和父类的关系?先讨论函数。

子类会继承父类的成员变量和成员函数,同时子类也可以有自己的成员函数和成员变量。如果子类的成员变量的名称和父类的成员变量名称一样,调用子类实例时,是调用父类变量还是子类变量呢?如果不加作用域的情况下,调用子类变量。这是隐藏。子类将父类的变量隐藏了。同样:如果子类的成员函数的名称和父类的成员函数名称一样时,尽管返回值或者参数类型,参数个数也不一样,调用子类实例时,父类的同名成员函数均会被隐藏,无法调用。这就是函数隐藏。隐藏不代表就没有了,可以通过类名作用域::访问到被隐藏的成员。b.A::show();b为子类对象,A为父类。

至于如果子类有多个成员函数的名称一样,其他不同,则他们彼此之间的关系为函数重载。根据参数类型,返回值类型而调用不同的成员函数。同一作用域下。

函数重写:在父类中出现一个虚函数,如果在子类中提供和父类同名的函数(注意区分名字隐藏),这就加函数重写。

函数重写要求必须有相同函数名,相同的参数列表,相同的返回值类型。

虚函数在函数前面加上virtual关键字修饰过的就是虚函数.虚函数的主要表现为会占用四个字节的空间,只要成员中出现虚函数,不管有多少个虚函数,都只用四个字节来维护这个虚关系。虚函数会影响对象的大小。维护虚关系使用一个指针来维护的,所以是四个字节。

定义下多态的含义:

通过父类类型的指针或者引用指向子类对象。通过该指针或者引用就可以实现,若父类和子类中均有虚函数,只能调用子类的虚函数以及继承父类的函数,而无法对子类其它的函数进行调用的功能。

多态的实现主要依赖于下面的三个东西:
虚函数:成员函数加了virtual修饰,virtual关键字只能修饰成员函数或者析构函数,其他的函数都不行。

虚函数表指针:一个类型有虚函数,则对这个类型提供一个指针,这个指针放在生成对象的前四个字节。同类型的对象共享一张虚函数表。并且不同类型的虚函数表地址不同。

虚函数表:虚函数表中的每个元素都是虚函数的地址。

一个类型一旦出现虚函数,则会生成一张虚函数表,虚函数表中存放的就是虚函数的函数地址,通过这个函数地址可以到代码区中去执行对应的函数。虚函数表中只存放类型中的虚函数,不是虚函数的一概不管。在每个生成的类型对象的前四个字节中存放的是虚函数表指针,通过这个指针可以访问到虚函数表,从而访问其中的函数。同种类型共享虚函数表,不同类型有自己独立的虚函数表,继承关系中的子类和父类属于不同类型,所以有自己独立的函数表。

假如现在有一个函数:输入为人类型比如亚洲或者欧洲或者非洲啊,输出为该人类型的肤色,那如何传参数呢?形参如何写呢?第一种是对该函数进行重载,这样会很浪费代码。

第二种就是用多态了,利用子类和父类之间的多态性来解决,来对应形参和实参之间的关系。形参为父类实例引用,实参为子类实例,这样调用的虚函数就对应了相应实参类型的虚函数。

多态的产生条件:

继承是构成多态的基础;

虚函数是构成多态的关键;

函数覆盖是构成多态的必备条件;

多态的应用:函数参数,函数返回值。

代码如下:

class CBse
{
public:
    virtual void f1(){}
};
class CDerive1 : public CBse
{
public:
	 void f1()
	{
		cout << "Derive1" << endl;
	}
};
class CDerive2 : public CBse
{
public:
	 void f1()
	{
		cout << "Derive2" << endl;
	}
};
void test(CBase &cbase)
{
    cbase.f1();
}
CBase * test1(int x){
    if(1==x) return new CDerive1();
    else {
        return new CDerive2();
    }
}
int main()
{
	CDerive1 test1;
    CDerive2 test2;
    test(test1);
    test(test2);
	
}

关于虚析构函数:

当我们用new创建一个指向子类对象的父类指针时,例如Animal * animal = new Dog()时,其中Animal时父类,Dog是子类,并且delete animal时,其子类对象的析构函数不会被调用,只会调用父类之上的析构函数。所以就会遇到一个问题,如果子类对象有自己独立的堆内存时,这部分内存就无法释放。这时,我们只需要在父类的析构函数上用virtual修饰即可,子类析构函数就会被调用,同时子类的析构函数需要显示定义来释放动态内存。至于原因为什么?可查看该链接C++内存泄露与类继承.

对于delete object;这种形式,如果delete操作的对象的静态类型不同于动态类型,那么该静态类型必须是动态类型的基类,而且该静态类型必须有一个虚析构函数,否则行为未定义。而对于delete[]这种形式,如果静态类型和动态类型不一致,那么行为未定义(即使你静态类型包含虚析构函数,因为对数组用多态就是一个错误)

若不是用new创键的,则均会调用构造函数。

这样定义的话就会涉及到类类型转换问题,通过父类类型的指针或者引用指向子类对象,即这样,指针或者引用到底指向什么呢?指针或者引用指向子类实例。

理解基类和子类之间的类型转换:

通常情况下:引用或指针绑定到一个对象上,引用或指针的类型与对象的类型一样。或者对象的类型含有一个可接受的const类型转换规则。

将基类的指针或引用绑定到子类对象上含义:并不清楚所绑定对象的类型。

静态类型和动态类型:编译时变量的类型,运行时对象的类型。

动态绑定指调用虚函数时才发生。

当用一个子类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动、赋值,子类部分将被忽略。

子类经常对基类的所有虚函数进行声明与定义。继承基类时会重写(覆盖)虚函数放在表中,继承公有,保护成员,隐藏私有成员。如果子类没有覆盖基类的虚函数,则该虚函数也被子类以虚函数继承下来放在子类虚函数表中。

关于虚函数表继承详细情况 。每一个类仅有一个虚函数表。关于子类继承基类时对象组成部分:基类对象,子类对象,更新虚函数表。

将基类的指针或引用绑定到子类对象上含义:可调用基类部分和子类重写基类的虚函数,子类虚函数表。派生类的作用域嵌套在基类的作用域内。

问题是只能调用基类对象部分,但是为何调用基类虚函数(被子类重写)则调用子类的重写虚函数,是如何找到其函数地址的?1.首先调用一个函数,若其不为为虚函数则可直接找到输出,该函数是从基类对象中。若为虚函数,又分两种情况,其是否被重写。是该从子类的虚表中寻找,还是从基类的虚表中寻找呢?若未被重写,则该虚函数会被移到子类虚函数表中,该指针始终指向的是子类对象,所以是从子虚函数表中找,参数名,返回值,函数名均相同。即可。若被重写,还是从子对象的虚函数表中寻找,即可找到对应函数。

也就是说是根据基类的成员,在子类的虚函数表中寻找。从而实现多态。建立虚函数表时就已经确定了。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值