面向对象的三大特性之多态
前言
多态的简单定义为同一种操作在作用于不同的对象时有不同的结果。在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;
}
结果:
结论:只有当调用自己作用域的函数时,才能成功编译。