小白入门C++ 继承 多态 函数重载

来源:互联网 发布:js internal 编辑:程序博客网 时间:2024/06/02 00:41

概念:继承和多态

继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写;
有了继承,才能有多态。在调用类实例方法的时候,尽量把变量视作父类类型,这样,所有子类类型都可以正常被接收;
虚函数是面向对象编程实现多态的基本手段。它是面向对象程序设计中的一个重要的概念。只能适用于指针和参考的计算机工程运算。当从父类中继承的时候,虚函数和被继承的函数具有相同的签名。但是在运行过程中,运行系统将根据对象的类型,自动地选择适当的具体实现运行。

例如,一个基类 Animal 有一个虚函数 eat。子类 Fish 要实做一个函数 eat(),这个子类 Fish 与子类 Wolf 是完全不同的,但是你可以引用类别 Animal 底下的函数 eat() 定义,而使用子类 Fish 底下函数 eat() 的进程。
using namespace std;
class Animal
{
public:
virtual void eat() const { cout << “I eat like a generic Animal.” << endl; }
virtual ~Animal() {}
};

class Wolf : public Animal
{
public:
void eat() const { cout << “I eat like a wolf!” << endl; }
};

class Fish : public Animal
{
public:
void eat() const { cout << “I eat like a fish!” << endl; }
};

函数重载

函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。

1、声明/定义重载函数时,是如何解决命名冲突的?(抛开函数重载不谈,using就是一种解决命名冲突的方法,解决命名冲突还有很多其它的方法,这里就不论述了)
2、当我们调用一个重载的函数时,又是如何去解析的?(即怎么知道调用的是哪个函数呢).

这两个问题是任何支持函数重载的语言都必须要解决的问题!带着这两个问题,我们开始本文的探讨。本文的主要内容如下:

1、例子引入(现象)
什么是函数重载(what)?
为什么需要函数重载(why)?
2、编译器如何解决命名冲突的?
函数重载为什么不考虑返回值类型
3、重载函数的调用匹配
模凌两可的情况
4、编译器是如何解析重载函数调用的?
根据函数名确定候选函数集
确定可用函数
确定最佳匹配函数
5、总结

1、例子引入(现象)

1.1、什么是函数重载(what)?

函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。

看下面的一个例子,来体会一下:实现一个打印函数,既可以打印int型、也可以打印字符串型。在C++中,我们可以这样做:

#include<iostream>using namespace std;void print(int i){        cout<<"print a integer :"<<i<<endl;}void print(string str){        cout<<"print a string :"<<str<<endl;}int main(){        print(12);        print("hello world!");        return 0;

通过上面代码的实现,可以根据具体的print()的参数去调用print(int)还是print(string)。上面print(12)会去调用print(int),print(“hello world”)会去调用print(string),如下面的结果:(先用g++ test.c编译,然后执行)
1.2、为什么需要函数重载(why)?

试想如果没有函数重载机制,如在C中,你必须要这样去做:为这个print函数取不同的名字,如print_int、print_string。这里还只是两个的情况,如果是很多个的话,就需要为实现同一个功能的函数取很多个名字,如加入打印long型、char*、各种类型的数组等等。这样做很不友好!
类的构造函数跟类名相同,也就是说:构造函数都同名。如果没有函数重载机制,要想实例化不同的对象,那是相当的麻烦!
操作符重载,本质上就是函数重载,它大大丰富了已有操作符的含义,方便使用,如+可用于连接字符串等!
通过上面的介绍我们对函数重载,应该唤醒了我们对函数重载的大概记忆。下面我们就来分析,C++是如何实现函数重载机制的。

2、编译器如何解决命名冲突的?

为了了解编译器是如何处理这些重载函数的,我们反编译下上面我们生成的执行文件,看下汇编代码(全文都是在Linux下面做的实验,Windows类似,你也可以参考《一道简单的题目引发的思考》一文,那里既用到Linux下面的反汇编和Windows下面的反汇编,并注明了Linux和Windows汇编语言的区别)。我们执行命令objdump -d a.out >log.txt反汇编并将结果重定向到log.txt文件中,然后分析log.txt文件。

发现函数void print(int i) 编译之后为:(注意它的函数签名变为——_Z5printi)

我们可以发现编译之后,重载函数的名字变了不再都是print!这样不存在命名冲突的问题了,但又有新的问题了——变名机制是怎样的,即如何将一个重载函数的签名映射到一个新的标识?我的第一反应是:函数名+参数列表,因为函数重载取决于参数的类型、个数,而跟返回类型无关。但看下面的映射关系:

void print(int i) –> _Z5printi
void print(string str) –> _Z5printSs

进一步猜想,前面的Z5表示返回值类型,print函数名,i表示整型int,Ss表示字符串string,即映射为返回类型+函数名+参数列表。最后在main函数中就是通过_Z5printi、_Z5printSs来调用对应的函数的:

80489bc: e8 73 ff ff ff call 8048934 <_Z5printi>
……………
80489f0: e8 7a ff ff ff call 804896f <_Z5printSs>

我们再写几个重载函数来验证一下猜想,如:

void print(long l) –> _Z5printl
void print(char str) –> _Z5printc
可以发现大概是int->i,long->l,char->c,string->Ss….基本上都是用首字母代表,现在我们来现在一个函数的返回值类型是否真的对函数变名有影响,如:

#include<iostream>using namespace std;int max(int a,int b){        return a>=b?a:b;}double max(double a,double b){        return a>=b?a:b;}int main(){        cout<<"max int is: "<<max(1,3)<<endl;        cout<<"max double is: "<<max(1.2,1.3)<<endl;        return 0;}

int max(int a,int b) 映射为_Z3maxii、double max(double a,double b) 映射为_Z3maxdd,这证实了我的猜想,Z后面的数字代码各种返回类型。更加详细的对应关系,如那个数字对应那个返回类型,哪个字符代表哪重参数类型,就不去具体研究了,因为这个东西跟编译器有关,上面的研究都是基于g++编译器,如果用的是vs编译器的话,对应关系跟这个肯定不一样。但是规则是一样的:“返回类型+函数名+参数列表”。

既然返回类型也考虑到映射机制中,这样不同的返回类型映射之后的函数名肯定不一样了,但为什么不将函数返回类型考虑到函数重载中呢?——这是为了保持解析操作符或函数调用时,独立于上下文(不依赖于上下文),看下面的例子

float sqrt(float);
double sqrt(double);

void f(double da, float fla)
{
float fl=sqrt(da);//调用sqrt(double)
double d=sqrt(da);//调用sqrt(double)

  fl=sqrt(fla);//调用sqrt(float)

d=sqrt(fla);//调用sqrt(float)
}
如果返回类型考虑到函数重载中,这样将不可能再独立于上下文决定调用哪个函数。

至此似乎已经完全分析清楚了,但我们还漏了函数重载的重要限定——作用域。上面我们介绍的函数重载都是全局函数,下面我们来看一下一个类中的函数重载,用类的对象调用print函数,并根据实参调用不同的函数。
void print(int i) –> _ZN4test5printEi

void print(char c) –> _ZN4test5printEc

注意前面的N4test,我们可以很容易猜到应该表示作用域,N4可能为命名空间、test类名等等。这说明最准确的映射机制为:作用域+返回类型+函数名+参数列表

原创粉丝点击