面向对象的三大特性之多态

面向对象的三大特性之多态


前言

多态的简单定义为同一种操作在作用于不同的对象时有不同的结果。在C++中分为静态多态和动态多态。

一、静态多态

在编译时确定。代表:函数重载,函数模板。

1.函数重载

定义:在同一作用域内,具有相同函数名、不同的形参个数或者类型。C语言没有函数重载。
函数重载原理:

无论是.c文件还是.cpp文件,形成可执行文件前在操作系统中都要经过下面的四个阶段:预处理、编译、汇编、链接。

预编译阶段:头文件替换,把头文件中的声明挎贝到源文件。
编译阶段:把c/c++代码转成汇编代码。
汇编阶段:给刚刚的函数名加一个地址(函数定义处的地址),也就是之后只要有函数名就能链到函数定义的位置,直接执行函数内的操作。
链接阶段:之前所有的操作都是各个文件自己走自己的,这一步就要合起来了,把各个文件的符号表合在一起,声明处的符号与定义处的符号合并。

那么在C++中,编译器如何识别带不参数的同名函数呢?
在linux系统下,用G++编译器对下面的代码进行汇编(g++ -s 文件名):

int test01(int a){return 5;}
void test01(int a,long b){};
int test01(int a,long long b){return 5;}
int test01(int a,char b,long c){return 5;}

int main()
{
   
    return 0;
}

汇编文件部分内容如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

整个汇编文件如下:

	.file	"duotai.cpp"
	.local	_ZStL8__ioinit
	.comm	_ZStL8__ioinit,1,1
	.text
	.globl	_Z6test01i
	.type	_Z6test01i, @function
_Z6test01i:
.LFB971:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -4(%rbp)
	movl	$5, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE971:
	.size	_Z6test01i, .-_Z6test01i
	.globl	_Z6test01il
	.type	_Z6test01il, @function
_Z6test01il:
.LFB972:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -4(%rbp)
	movq	%rsi, -16(%rbp)
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE972:
	.size	_Z6test01il, .-_Z6test01il
	.globl	_Z6test01ix
	.type	_Z6test01ix, @function
_Z6test01ix:
.LFB973:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -4(%rbp)
	movq	%rsi, -16(%rbp)
	movl	$5, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE973:
	.size	_Z6test01ix, .-_Z6test01ix
	.globl	_Z6test01icl
	.type	_Z6test01icl, @function
_Z6test01icl:
.LFB974:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -4(%rbp)
	movl	%esi, %eax
	movq	%rdx, -16(%rbp)
	movb	%al, -8(%rbp)
	movl	$5, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE974:
	.size	_Z6test01icl, .-_Z6test01icl
	.globl	main
	.type	main, @function
main:
.LFB975:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE975:
	.size	main, .-main
	.type	_Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB976:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	cmpl	$1, -4(%rbp)
	jne	.L10
	cmpl	$65535, -8(%rbp)
	jne	.L10
	movl	$_ZStL8__ioinit, %edi
	call	_ZNSt8ios_base4InitC1Ev
	movl	$__dso_handle, %edx
	movl	$_ZStL8__ioinit, %esi
	movl	$_ZNSt8ios_base4InitD1Ev, %edi
	call	__cxa_atexit
.L10:
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE976:
	.size	_Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii
	.type	_GLOBAL__sub_I__Z6test01i, @function
_GLOBAL__sub_I__Z6test01i:
.LFB977:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$65535, %esi
	movl	$1, %edi
	call	_Z41__static_initialization_and_destruction_0ii
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE977:
	.size	_GLOBAL__sub_I__Z6test01i, .-_GLOBAL__sub_I__Z6test01i
	.section	.init_array,"aw"
	.align 8
	.quad	_GLOBAL__sub_I__Z6test01i
	.hidden	__dso_handle
	.ident	"GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
	.section	.note.GNU-stack,"",@progbits

汇编下面的代码:

int test(int a){return 5;}
int main()
{
    return 0;
}

在这里插入图片描述

可知,在C++下,汇编之后的函数名变成了:_Z+函数名长度+函数名+类型首字母。

那么,对上面的代码,如果是C语言的编译规则会是怎样的呢?
在这里插入图片描述

会报错!!!!!!

汇编下列语句:

int test01(int a){return 5;}
int main()
{
    return 0;
}

在这里插入图片描述

可知,在c规则下汇编后的函数名称还是原来的函数名。

于是我们可以得出,C++的函数重载是通过在汇编后的汇编文件中给不同的函数重命名来实现的。

2.函数模板

运行下面的代码:

template<class T1>
T1 test(T1 &a,T1 &b)
{
    T1 c;
    c=a+b;
    return c;
    
}
int main()
{
    int a=1,b=2;
    float c=1.0,d=2.0;
    cout<<test<int>(a,b)<<endl;
    cout<<test<float>(c,d)<<endl;
    return 0;
}

这里是引用

上述代码在Linux下的G++的完整汇编代码如下:

	.file	"duotai.cpp"
	.local	_ZStL8__ioinit
	.comm	_ZStL8__ioinit,1,1
	.text
	.globl	main
	.type	main, @function
main:
.LFB972:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$32, %rsp
	movl	$1, -4(%rbp)
	movl	$2, -8(%rbp)
	movl	.LC0(%rip), %eax
	movl	%eax, -12(%rbp)
	movl	.LC1(%rip), %eax
	movl	%eax, -16(%rbp)
	leaq	-8(%rbp), %rdx
	leaq	-4(%rbp), %rax
	movq	%rdx, %rsi
	movq	%rax, %rdi
	call	_Z4testIiET_RS0_S1_
	movl	%eax, %esi
	movl	$_ZSt4cout, %edi
	call	_ZNSolsEi
	movl	$_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi
	movq	%rax, %rdi
	call	_ZNSolsEPFRSoS_E
	leaq	-16(%rbp), %rdx
	leaq	-12(%rbp), %rax
	movq	%rdx, %rsi
	movq	%rax, %rdi
	call	_Z4testIfET_RS0_S1_
	movss	%xmm0, -20(%rbp)
	movl	-20(%rbp), %eax
	movl	%eax, -20(%rbp)
	movss	-20(%rbp), %xmm0
	movl	$_ZSt4cout, %edi
	call	_ZNSolsEf
	movl	$_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi
	movq	%rax, %rdi
	call	_ZNSolsEPFRSoS_E
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE972:
	.size	main, .-main
	.section	.text._Z4testIiET_RS0_S1_,"axG",@progbits,_Z4testIiET_RS0_S1_,comdat
	.weak	_Z4testIiET_RS0_S1_
	.type	_Z4testIiET_RS0_S1_, @function
_Z4testIiET_RS0_S1_:
.LFB973:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movq	%rdi, -24(%rbp)
	movq	%rsi, -32(%rbp)
	movq	-24(%rbp), %rax
	movl	(%rax), %edx
	movq	-32(%rbp), %rax
	movl	(%rax), %eax
	addl	%edx, %eax
	movl	%eax, -4(%rbp)
	movl	-4(%rbp), %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE973:
	.size	_Z4testIiET_RS0_S1_, .-_Z4testIiET_RS0_S1_
	.section	.text._Z4testIfET_RS0_S1_,"axG",@progbits,_Z4testIfET_RS0_S1_,comdat
	.weak	_Z4testIfET_RS0_S1_
	.type	_Z4testIfET_RS0_S1_, @function
_Z4testIfET_RS0_S1_:
.LFB976:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movq	%rdi, -24(%rbp)
	movq	%rsi, -32(%rbp)
	movq	-24(%rbp), %rax
	movss	(%rax), %xmm1
	movq	-32(%rbp), %rax
	movss	(%rax), %xmm0
	addss	%xmm1, %xmm0
	movss	%xmm0, -4(%rbp)
	movl	-4(%rbp), %eax
	movl	%eax, -36(%rbp)
	movss	-36(%rbp), %xmm0
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE976:
	.size	_Z4testIfET_RS0_S1_, .-_Z4testIfET_RS0_S1_
	.text
	.type	_Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB981:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	cmpl	$1, -4(%rbp)
	jne	.L7
	cmpl	$65535, -8(%rbp)
	jne	.L7
	movl	$_ZStL8__ioinit, %edi
	call	_ZNSt8ios_base4InitC1Ev
	movl	$__dso_handle, %edx
	movl	$_ZStL8__ioinit, %esi
	movl	$_ZNSt8ios_base4InitD1Ev, %edi
	call	__cxa_atexit
.L7:
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE981:
	.size	_Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii
	.type	_GLOBAL__sub_I_main, @function
_GLOBAL__sub_I_main:
.LFB982:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$65535, %esi
	movl	$1, %edi
	call	_Z41__static_initialization_and_destruction_0ii
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE982:
	.size	_GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main
	.section	.init_array,"aw"
	.align 8
	.quad	_GLOBAL__sub_I_main
	.section	.rodata
	.align 4
.LC0:
	.long	1065353216
	.align 4
.LC1:
	.long	1073741824
	.hidden	__dso_handle
	.ident	"GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
	.section	.note.GNU-stack,"",@progbits

模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。
下边来深入理解下函数模板:
- 对于函数模板中使用的类型不同,编译器会产生不同的函数
- 编译器会对函数模板进行两次编译
- 第一次是对函数模板本身进行编译,包括语法检查等
- 第二次是对参数替换后的代码进行编译,这就相当于编译普通函数一样,进行类型规则检查等。
需要注意的是

函数模板是不允许隐式类型转换的,调用时类型必须严格匹配。 函数模板跟普通函数一样,也可以被重载
C++编译器优先考虑普通函数
如果函数模板可以产生一个更好的匹配,那么就选择函数模板
也可以通过空模板实参列表<>限定编译器只匹配函数模板

实验:

template<class T1>
T1 test(T1 &a,T1 &b)
{
    T1 c;
    c=a+b;
    return c;
    
}

int test(int a,int b)
{
    int c;
    c=a-b;
    return c;
}

int main()
{
    int a=1,b=2;
    cout<<test(a,b)<<endl;
    cout<<test<>(a,b)<<endl;
    return 0;
}

实验结果:在这里插入图片描述

二、动态多态

在运行时确定。代表:函数重写.

1.函数重写

函数重写:在不同作用域内(一个在父类一个在子类),具有相同函数名,相同的参数个数和类型,返回值也必须相同。有一种情况返回值可以不同,就是返回值必须是父子类的指针或者引用,这种情况称为协变。要实现动态多态,在父类中必须定义为虚函数。C++实现重写的方式也跟编译器有关,编译器在实例化一个具有虚函数的类时会生成一个vpointer指针,vpointer指针在类的内存空间中占最低地址的四字节。vpointer指向的空间称为虚函数表,vpointer指针指向其表头,在虚函数表里面按声明顺序存放了虚函数的函数指针。如果在子类中重写了,在子类的内存空间中也会产生一个vpointer指针,同时会把父类的虚函数表复制一份当成自己的。然后如果在子类中重写了虚函数,就会将复制过来的虚函数表中的对应的父亲虚函数替换。在调用虚函数时,不管调用他的是父类的指针、引用还是子类的指针、引用,他都不管,只看他所指向或者引用的对象的类型(这也称为动态联编),如果是父类的对象,那就调用父类里面的vpointer指针然后找到相应的虚函数,如果是子类的对象,那就调用子类里面的vpointer指针然后找到相应的虚函数。当然这样子的过程相比静态多态而言,时间和空间上的开销都多了(这也是为什么内联函数为什么不能声明为虚函数,因为这和内联函数加快执行速度的初衷相矛盾)。

1.返回类型不同不能实现函数重写。在这里插入图片描述 在这里插入图片描述

2.协变
在这里插入图片描述

运行以下代码:

#include<iostream>
using namespace std;
class Father{
    public:
    Father(){};
    ~Father(){};
    int test01(int a){return 5;}
};
class Son:Father{
    public:
    Son(){};
    ~Son(){};
    int test01(int a){return 10;}
};
int main()
{
    Son s;
    Father f;
    cout<<s.test01(1)<<endl;
    cout<<f.test01(1)<<endl;
    return 0;
}

结果:
在这里插入图片描述
结论:并不能得出什么结论。

运行以下代码:


#include<iostream>
using namespace std;
class Father{
    public:
    Father(){};
    ~Father(){};
    int test01(int a){return 5;}
};
class Son:public Father{
    public:
    Son(){};
    ~Son(){};
    int test01(int a){return 10;}
};
int main()
{
    Son *s=new Son;
    Father *f=s;
    cout<<s->test01(1)<<endl;
    cout<<f->test01(1)<<endl;
    return 0;
}

结果:
在这里插入图片描述
结论:没有声明虚函数时,父类指向子类的指针无法访问子类的同名函数,只能访问自己内部的那个与子类函数同名的函数。

运行以下代码:

#include<iostream>
using namespace std;
class Father{
    public:
    Father(){};
    ~Father(){};
    virtual int test01(int a){return 5;}
};
class Son:public Father{
    public:
    Son(){};
    ~Son(){};
    int test01(int a){return 10;}
};
int main()
{
    Son *s=new Son;
    Father *f=s;
    cout<<s->test01(1)<<endl;
    cout<<f->test01(1)<<endl;
    return 0;
}

结果:
在这里插入图片描述
结论:将父类的test01()函数定义为虚函数后,父类指向子类的指针就可以访问子类中的同名函数了。

2.虚函数的继承特性

2.1验证虚函数从哪一代开始

//验证虚函数从哪一代开始
#include<iostream>
using namespace std;
class A
{
public:
	 void func2(){ cout << "func2 in class A" << endl; }
};
class B : public A
{
public:
	virtual void func2() { cout << "func2 in class B" << endl; }
};
class C :public B
{
public:
	void func2() { cout << "func2 in class C" << endl; }
};
int main()
{
	A *a=new C;
	B *b=new C;
	a->func2();
	b->func2();
	
	return 0;
}

结果:
在这里插入图片描述
结论:在某一代定义了虚函数,之前都不是虚函数,则其后代都是虚函数

2.2 验证虚函数能否隔代继承:

#include<iostream>
using namespace std;

class A
{
public:
	virtual void func2(){ cout << "func2 in class A" << endl; }
};

class B : public A
{
public:
	 void func2() { cout << "func2 in class B" << endl; }
};

class C :public B
{
public:
	void func2() { cout << "func2 in class C" << endl; }
};

int main()
{
	A *a=new C;
	B *b=new C;
	a->func2();
	b->func2();
	return 0;
}

结果:
在这里插入图片描述
结论:虚函数可以隔代继承

总结:
在这里插入图片描述

验证实验:

#include<iostream>
using namespace std;

class A
{
public:

	 void func2(){ cout << "func2 in class A" << endl; }
};

class B : public A
{
public:
	
	virtual void func2() { cout << "func2 in class B" << endl; }
};

class C :public B
{
public:
	
	void func2() { cout << "func2 in class C" << endl; }
};

class D :public C
{
public:
	
	void func2() { cout << "func2 in class D" << endl; }
};

int main()
{
    A* a=new D;
	B* b=new D;
    a->func2();
	b->func2();
	
	
	return 0;
}

结果:
在这里插入图片描述

三.重定义

子类和父类的成员变量相同或者函数名相同,子类隐藏父类的对应成员。
重定义实际上是同名隐藏:在派生类中定义基类中存在的函数,派生类对象就只能访问自己的函数,而不能访问基类的同名函数(除非进行作用域扩展声明)重定义同样可以增强程序的可读性,减少函数名的数量,更重要的是它可以让相同的方法在不同派生类中有不同的实现避免了在基类中过多的存在重载。
重定义要求:
1.在不同的作用域内(原函数在基类中,重定义函数在派生类中)
2.函数名字相同(只有这一个要求)

#include<iostream>
using namespace std;
class Father{
    public:
    Father(){};
    ~Father(){};
    int test01(int a){return 5;}
};
class Son:public Father{
    public:
    Son(){};
    ~Son(){};
    int test01(){return 10;}
};
int main()
{
    Son *s=new Son;
    Father *f=s;
    cout<<s->test01()<<endl;
    cout<<f->test01()<<endl;
    return 0;
}

编译结果:
在这里插入图片描述
结论:父类只能调用父类内部的重定义函数,即使父类指针指向子类对象时,无法调用子类同名函数。

类似:

#include<iostream>
using namespace std;
class Father{
    public:
    Father(){};
    ~Father(){};
    int test01(int a){return 5;}
};
class Son:public Father{
    public:
    Son(){};
    ~Son(){};
    int test01(){return 10;}
};
int main()
{
    Son *s=new Son;
    Father *f=s;
    cout<<s->test01(1)<<endl;
    cout<<f->test01(1)<<endl;
    return 0;
}

编译结果:
在这里插入图片描述
结论:子类对象无法调用父类同名函数。

代码改正:

#include<iostream>
using namespace std;
class Father{
    public:
    Father(){};
    ~Father(){};
    int test01(int a){return 5;}
};
class Son:public Father{
    public:
    Son(){};
    ~Son(){};
    int test01(){return 10;}
};
int main()
{
    Son *s=new Son;
    Father *f=s;
    cout<<s->test01()<<endl;
    cout<<f->test01(1)<<endl;
    return 0;
}

结果:
在这里插入图片描述

#include<iostream>
using namespace std;
class Father{
    public:
    Father(){};
    ~Father(){};
    int test01(int a){return 5;}
};
class Son:public Father{
    public:
    Son(){};
    ~Son(){};
    int test01(int a){return 10;}
};
int main()
{
    Son *s=new Son;
    Father *f=s;
    cout<<s->test01(1)<<endl;
    cout<<f->test01(1)<<endl;
    return 0;
}

结果:
在这里插入图片描述
结论:只有当调用自己作用域的函数时,才能成功编译。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值