面向对象的三大特性之多态
面向对象的三大特性之多态
文章目录
- 面向对象的三大特性之多态
- 前言
- 一、静态多态
- 1.函数重载
- 2.函数模板
- 二、动态多态
- 1.函数重写
- 2.虚函数的继承特性
- 三.重定义
前言
多态的简单定义为同一种操作在作用于不同的对象时有不同的结果。在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_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl %edi, -4(%rbp)movl $5, %eaxpopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE971:.size _Z6test01i, .-_Z6test01i.globl _Z6test01il.type _Z6test01il, @function
_Z6test01il:
.LFB972:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl %edi, -4(%rbp)movq %rsi, -16(%rbp)popq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE972:.size _Z6test01il, .-_Z6test01il.globl _Z6test01ix.type _Z6test01ix, @function
_Z6test01ix:
.LFB973:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl %edi, -4(%rbp)movq %rsi, -16(%rbp)movl $5, %eaxpopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE973:.size _Z6test01ix, .-_Z6test01ix.globl _Z6test01icl.type _Z6test01icl, @function
_Z6test01icl:
.LFB974:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl %edi, -4(%rbp)movl %esi, %eaxmovq %rdx, -16(%rbp)movb %al, -8(%rbp)movl $5, %eaxpopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE974:.size _Z6test01icl, .-_Z6test01icl.globl main.type main, @function
main:
.LFB975:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl $0, %eaxpopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE975:.size main, .-main.type _Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB976:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspmovl %edi, -4(%rbp)movl %esi, -8(%rbp)cmpl $1, -4(%rbp)jne .L10cmpl $65535, -8(%rbp)jne .L10movl $_ZStL8__ioinit, %edicall _ZNSt8ios_base4InitC1Evmovl $__dso_handle, %edxmovl $_ZStL8__ioinit, %esimovl $_ZNSt8ios_base4InitD1Ev, %edicall __cxa_atexit
.L10:leave.cfi_def_cfa 7, 8ret.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_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl $65535, %esimovl $1, %edicall _Z41__static_initialization_and_destruction_0iipopq %rbp.cfi_def_cfa 7, 8ret.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_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $32, %rspmovl $1, -4(%rbp)movl $2, -8(%rbp)movl .LC0(%rip), %eaxmovl %eax, -12(%rbp)movl .LC1(%rip), %eaxmovl %eax, -16(%rbp)leaq -8(%rbp), %rdxleaq -4(%rbp), %raxmovq %rdx, %rsimovq %rax, %rdicall _Z4testIiET_RS0_S1_movl %eax, %esimovl $_ZSt4cout, %edicall _ZNSolsEimovl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esimovq %rax, %rdicall _ZNSolsEPFRSoS_Eleaq -16(%rbp), %rdxleaq -12(%rbp), %raxmovq %rdx, %rsimovq %rax, %rdicall _Z4testIfET_RS0_S1_movss %xmm0, -20(%rbp)movl -20(%rbp), %eaxmovl %eax, -20(%rbp)movss -20(%rbp), %xmm0movl $_ZSt4cout, %edicall _ZNSolsEfmovl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esimovq %rax, %rdicall _ZNSolsEPFRSoS_Emovl $0, %eaxleave.cfi_def_cfa 7, 8ret.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_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movq %rdi, -24(%rbp)movq %rsi, -32(%rbp)movq -24(%rbp), %raxmovl (%rax), %edxmovq -32(%rbp), %raxmovl (%rax), %eaxaddl %edx, %eaxmovl %eax, -4(%rbp)movl -4(%rbp), %eaxpopq %rbp.cfi_def_cfa 7, 8ret.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_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movq %rdi, -24(%rbp)movq %rsi, -32(%rbp)movq -24(%rbp), %raxmovss (%rax), %xmm1movq -32(%rbp), %raxmovss (%rax), %xmm0addss %xmm1, %xmm0movss %xmm0, -4(%rbp)movl -4(%rbp), %eaxmovl %eax, -36(%rbp)movss -36(%rbp), %xmm0popq %rbp.cfi_def_cfa 7, 8ret.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_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspmovl %edi, -4(%rbp)movl %esi, -8(%rbp)cmpl $1, -4(%rbp)jne .L7cmpl $65535, -8(%rbp)jne .L7movl $_ZStL8__ioinit, %edicall _ZNSt8ios_base4InitC1Evmovl $__dso_handle, %edxmovl $_ZStL8__ioinit, %esimovl $_ZNSt8ios_base4InitD1Ev, %edicall __cxa_atexit
.L7:leave.cfi_def_cfa 7, 8ret.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_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl $65535, %esimovl $1, %edicall _Z41__static_initialization_and_destruction_0iipopq %rbp.cfi_def_cfa 7, 8ret.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;
}
结果:
结论:只有当调用自己作用域的函数时,才能成功编译。
面向对象的三大特性之多态相关推荐
- Java学习笔记二十五:Java面向对象的三大特性之多态
Java面向对象的三大特性之多态 一:什么是多态: 多态是同一个行为具有多个不同表现形式或形态的能力. 多态就是同一个接口,使用不同的实例而执行不同操作. 多态性是对象多种表现形式的体现. 现实中,比 ...
- polymorphism java_Java基础-面向对象第三大特性之多态(polymorphism)
Java基础-面向对象第三大特性之多态(polymorphism) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.多态概述 多态是继封装,继承之后,面向对象的第三大特性,多态的 ...
- 面向对象的三大特性之多态,封装基础
面向对象的三大特性 : 继承 多态 封装 多态:一个类表现出多种形态:通过继承来实现的 在python中:由于函数的参数不需要指定数据类型,所以也不需要通过继承的形式来统一一组类的类型 ...
- C++ 面向对象程序三大特性之 多态
目录 多态的概念 多态的定义及使用 虚函数 虚函数的重写 虚函数重写的两个例外 C++11中 override 和 final 重载.重写.隐藏的区别与联系 抽象类 多态的原理 虚函数指针.虚函数.虚 ...
- Python面向对象编程三大特性之多态
多态 code:_9polymorphism.py # ***************************************** """ 多态:对于同一个方法, ...
- C++ 面向对象程序三大特性之 继承
目录 继承的概念 继承的定义及使用 继承方式和访问权限 基类和派生类对象的赋值转换 继承中的各成员的作用域 派生类的默认成员函数 构造函数 拷贝构造 赋值运算符重载函数 析构函数 继承与友元 继承与静 ...
- 到底什么是面向对象,面试中怎么回答。面向过程和面向对象的区别是什么。java跨平台特性以及java和C++的区别。面向对象的三大特性——封装、继承和多态。面向对象的高拓展性以及低耦合度怎么体现?
Java语言具有的特点:面向对象.跨平台.多线程以及网络编程 1. 和C++的区别 1.1 Java隐蔽了C++的指针,避免指针直接操作,程序更加安全. 1.2 Java类继承只能单继承,避免了C++ ...
- 面向对象之三大特性:继承,封装,多态
python面向对象的三大特性:继承,封装,多态. 1. 封装: 把很多数据封装到⼀个对象中. 把固定功能的代码封装到⼀个代码块, 函数, 对象, 打包成模块. 这都属于封装的思想. 具体的情况具体分 ...
- Python Day 21 面向对象 (面向对象的三大特性(二)继承,多态,封装,几个装饰器函数)...
Python Day 21 面向对象 (面向对象的三大特性(二)继承,多态,封装,几个装饰器函数) https://mubu.com/doc/1AqL_M0IbW 继承之钻石继承 多态 封装 几个装饰 ...
最新文章
- sql中进行计算并重命名
- OC语言Block 续
- 基于java 工单管理_实训任务工单1-2(编写规范Java代码) 实训任务工单1-2(编写规范Java代码).docx_学小易找答案...
- app登录界面背景 css_计算机毕业设计中Java web实现简登录页面(MyBatis+jsp+servlet+html+css+javascript)...
- 定义const变量是不可以赋值_JavaScript的声明方法和作用范围,常见的结构赋值类型和使用场景...
- 终于开源!基于Overlap的激光雷达全局定位算法!
- 视频软件会声会影支持哪些视频格式?
- 云服务器 信息安全,云服务器怎么保证信息安全
- html5透明图片格式,半透明图片制作
- (项目笔记)opencv人脸识别
- 阿里云产品分析(PPT)
- e语言截图软件怎么保存在c盘,电脑截图文件保存在哪_电脑截图保存在哪里
- mac电脑视频去水印
- 麒麟v10安装达梦数据库
- GUI(Graphic User Interface) 图形用户界面
- “宠物”项目、“西瓜”项目、怎样打这些“怪物项目”?
- 统计学基于matlab,MATPLOT:一款基于MATLAB的地球化学数据分析成图软件
- HTML的标签与选择器
- Red Hat Enterprise Linux Server release 7.1 (Maipo) 安装gcc 的几种方式
- 【备忘录】成本中心的分配与分摊