函数 重载,重写,重入

8 篇文章 0 订阅

函数重载,重写,重入

前言

最近被问到函数的重载,重写和重入有什么区别,突然一问这些概念,有点懵了,这里梳理总结一下这三个概念。

其实重载,重写和重入完全是不同维度的概念,但是名字比较像,所以经常被拿来比较。
尤其是重载和重写是最容易被比较的。这两个概念也是针对C++的,对于C语言的语法,重载和重写都是不支持的。下面也会详细说一下为什么C不支持。

重载

首先是定义:重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。
重载是为了解决功能类似的方法命名的问题,如下做一个加法运算,如果每种类型取一个名字,在记忆上也比较困难,只是想名字脑袋都大。

C++的函数都写成全局函数,当然成员函数实现重载更没有问题。 没有写成员函数是为了下面和C做对比,说明为什么C++能实现重载。
同样的函数名add(),在调用的时候就可以根据传入的参数的类型个数不同找对应的函数。然执行正确的代码段。这样就不需要记录繁杂的名字。对人非常友好。

#include <iostream>
int add(int a, int b)
{
        return a + b;
}
double add(double a, double b)
{
        return a + b;
}
int add(int a, int b, int c)
{
        return a + b + c;
}
int main()
{
        int a = 1, b = 2, c = 3;
        int d = add(a , b);
        return 0;
}

接下来进一步说明为什么可以实现函数重载,为什么程序能够找到正确的代码段执行。
这个涉及到C++的编译原理。在对一个C++的函数进行编译的时候,会生成程序的符号表,程序执行的时候就是通过符号表里的符号寻找对应的函数,变量。在C++中,函数编译后对应的符号是 函数名+参数构成的。

在linux上直接使用nm 命令就可以查看符号表,如下,可以看到_Z3adddd, _Z3addii, _Z3addiii, 这三个就是上面代码的add函数编译后对应的符号表。因为函数符号包含了函数名和参数类型和参数个数信息。所以在调用的时候,也就能够通过参数
类型或者参数个数找到对应的函数代码段。同时,这样也就很号理解为什么返回值不同不是函数重载,因为在符号表中是没有返回值信息的,通过返回值无法找到正确的函数代码段。

yang@legion:~/Desktop/sample/rewrite$ nm test2 
............
...........
0000000000000890 T __libc_csu_fini
0000000000000820 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000000784 T main
00000000000006a0 t register_tm_clones
0000000000000630 T _start
0000000000201010 D __TMC_END__
000000000000074e T _Z3adddd
000000000000073a T _Z3addii
0000000000000768 T _Z3addiii
00000000000007ba t _Z41__static_initialization_and_destruction_0ii
                 U _ZNSt8ios_base4InitC1Ev@@GLIBCXX_3.4
                 U _ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4
00000000000008a4 r _ZStL19piecewise_construct
0000000000201011 b _ZStL8__ioinit

上面这是C++的程序,因为C++和C有很多相似的地方,此时,我们就会想知道为什么C不支持函数重载。这同样和符号表有关系。
如下面的一段C的代码:

#include <stdio.h>
int add(int a, int b)
{
        return a + b;
}
int add(double a, double b)
{
        return a+b;
}
int main()
{
        int a = 0, b = 1;
        int c ;
        c = add(a,b);
        return 0;
}
~    

这段代码如果编译会直接报错,函数重定义。

yang@legion:~/Desktop/sample/rewrite$ gcc test.c -o test
test.c:8:5: error: conflicting types for ‘add’
 int add(double a, double b)
     ^~~
test.c:3:5: note: previous definition of ‘add’ was here
 int add(int a, int b)

正因为C语言会认为两个函数相同,所以才会编译出错,也因此,C是不支持函数重载的。
如果将定义的第二个add()函数删除掉,编译后,查看一下符号表,原因就一目了然了。如下第一行,
在函数表里只有一个add的符号。即C语言在编译的时候,生成的符号表,只有函数名而没有后面的参数等信息。
所以如果名字相同就会认为是重定义。

yang@legion:~/Desktop/sample/rewrite$ nm test
00000000000005fa T add
0000000000201010 B __bss_start
0000000000201010 b completed.7698
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000201000 D __data_start
0000000000201000 W data_start
.............
............
............
                 U __libc_start_main@@GLIBC_2.2.5
000000000000060e T main
0000000000000560 t register_tm_clones
00000000000004f0 T _start
0000000000201010 D __TMC_END__

其实基于以上C和C++编译的符号表的不同,也能够解释我们在写C++代码的时候为什么要加上extern "C"将C的代码段包括起来,才能够正确的调用C的代码。

对于重载来说有以下几个规则:
①必须具有不同的参数列表。
②可以有不同的访问修饰符。
③可以抛出不同的异常

重写

        因为C++也支持全局函数定义,对于重载的话,有时候还会混淆为什么C不可以。而对于重写,则完全是一个面向对象的概念。
         先上重写的定义:覆盖(也叫重写)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样。
         重写是为了实现面向对象的多态行为。不同的子类将父类的函数进行重写,执行不同的逻辑,表现出不同的行为,即实现面向对象的多态。这里重点说一下函数为什么能实现重写,关于多态可以在其他资料上看一下。
本文重点说一下函数重写有哪些规则,为什么会有这些规则,为什么重写可以实现,对父类方法的覆盖。
先说一些重写的规则:
①参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载。
②返回的类型必须一直与被重写的方法的返回类型相同,否则不能称其为重写而是重载。
③访问修饰符的限制一定要大于被重写方法的访问修饰符。
④重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。

        先说第①条,这个比较好理解参数列表必须相同,通过对对上面重载函数的分析中,能知道,C++的函数被编译后符号是带参数的的。虽然这个理由勉强能解释通,但是后面解释不通为什么必须完全相同,也就是参数数量为什么要一样?还有就是②为什么又要求返回值?函数符号表中是没有返回值的啊。
         这里就体现出来重载和重写的实现原理上的区别。重载是是因为C++的函数符号表是带参数类型的。所以可以重载。
而重写则不是通过符号表,而是通过虚函数表实现的。在声明类的时候,成员函数如果加了virtual关键字,如下图,在生成对象的时候,就会在对象的头部生成一个虚函数表。虚函数表里面存在就是虚函数的指针。
         一个函数指针的大小是4字节,这个都比较清楚。所以在计算一个对象大小的时候,除了考虑成员变量占用的内存大小,还要考虑有多少个虚函数。
在这里插入图片描述
因为虚函数是通过虚函数表实现的多态的行为。也就是说重写,其本质是对应的函数指针。如果考虑函数指针的话,就可以理解为什么要求返回值,参数列表必须完全一致了。
下面的代码举了一个重写的例子。

#include <iostream>
#include <memory>

using namespace std;

class Anamal
{
public:
        Anamal() = default;
        virtual ~Anamal() = default;

        virtual int voice(int a, int b) { cout<<"anamal voice ..."<<endl; };

};

class cat: public Anamal
{
public:
        cat() = default;
        virtual ~cat() = default;

        int voice(int b, int a) { cout <<"cat voice ..."<<endl;};
};

int main()
{
        unique_ptr<Anamal> p;
        p.reset(new cat());
        p->voice(1,2);

        return 0;
}

从这个例子中有一个地方需要注意一下,在重写voice()函数的时候,我故意将形参,int b, int a颠倒了一下,但是编译也没有问题,可实现了多态的行为。这个是因为函数指针也是只检查参数的类型,而对于形参的名字叫什么,是没有限制的。

重入

        最后说一下函数重入,或者叫可重入函数。这个其实是一个动态的概念。
可重入函数是指函数可以由多个任务并发使用,而不必担心数据错误。意思就是可以被中断的函数,该函数可以在任何时刻中断它,并执行另一块代码, 当执行完毕后,回到原本的代码还可以正常继续运行。
        只要是多任务的系统,就存在一个函数执行中被打断,转入OS调度,去执行另一段代码,执行完后再回来执行这个函数。如果函数使用了系统资源,比如全局变量等,如果被打断的话,就可能会出问题,这种就是不可重入函数。这类函数是不能运行在多任务环境的。
满足下列条件的多数函数,都是不可重入函数:

  • 函数体内使用了静态数据数据结构
  • 调用了动态内存分配和释放的函数,如使用了new, malloc等
  • 函数体内调用了I/O操作。

举一个例子如下代码,使用了全局变量,则这个函数就是一个不可重入的函数。

int  c;
void  swap(int * a, int *b)
{
	 c    = *a;
	*a  = *b;
	*b  = c;
}
为什么满足上面条件的函数,大多是不可重入的函数?

在多任务的环境中,中断前后不是都要保存和恢复上下文吗?怎么会出现函数经过中断后变的不可重入了?
原因是,中断确认保存了一些上下文,但仅限于函数返回值地址,CPU寄存器等等少量的栈信息,而函数内部使用的全局变量,buffer等数据区不会被保护。所以放中断发生后,再返回执行,这些数据的值是不确定的,不安全的。
因此可重入函数对应的就是多任务环境下安全的函数。
基于此,编写可重入函数,遵循下面规则:
1、不使用(返回)静态的数据、全局变量(除非用信号量互斥)。
2、不调用动态内存分配、释放的函数。
3、不调用任何不可重入的函数(如标准 I/O 函数)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值