进一步认识函数
接受可变参数的函数
C语言中支持函数调用的参数为变参形式。例如,printf() ,它的函数原型是int printf(const char* format, ... ),它除了有一个参数format 固定以外,后面跟的参数的个数和类型都是可变的,可以有以下多种不同的调用方法
printf("%d", i); printf("%s", s); printf("the number is %d, string is: %s", i, s);
也可以根据实际需求编写变参函数,例如实现多参数求和:
#include <stdarg.h>
int add(int num, ...)
{
va_list arg_ptr;
int i;
int sum = 0, t = 0;
va_start(arg_ptr, num);
for (i = 0; i < num; i ++)
{
sum += va_arg(arg_ptr, int);
}
va_end(arg_ptr);
return sum;
}
其中,va_start ,va_arg ,va_end 是在stdarg.h 中被定义成宏的,由于硬件平台和编译器的不同,定义的宏也有所不同。
在ANSI C中,这些宏的定义位于stdarg.h 中,典型的实现如下:
-
typedef char *va_list; -
va_start 宏,获取可变参数列表的第一个参数的地址(list是类型为va_list 的指针,param1 是可变参数最左边的参数):
#define va_start(list, param1) ( list = (va_list)¶m1+ sizeof(param1) )
va_arg 宏,获取可变参数的当前参数,返回指定类型并将指针指向下一参数(mode参数描述了当前参数的类型):
#define va_arg(list,mode) ( (mode *) ( list += sizeof(mode) ) )[-1]
va_end 宏,清空va_list 可变参数列表:
#define va_end(list) ( list = (va_list)0 )
函数指针与指针函数
- 指针函数是返回值为指针类型的函数,本质上是一个函数,函数返回类型是某一类型的指针。其形式如下:
类型标识符 *函数名(参数列表)
- 函数指针是指向函数的指针变量,即本质是一个指针变量,它指向的是一个函数。其形式如下:
类型说明符 (*函数名)(参数)
例如,int (*pf)(int x) ,它的意思是声明一个函数指针,而pf=func 则是将func函数的首地址赋值给指针变量pf
举例:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int minus(int a, int b)
{
return a - b;
}
int(*fn[])(int, int)
{
add,
minus
};
int calculate(int a, int b, int op)
{
return fn[op](a, b);
}
函数模板与模板函数
函数模板是对一批模样相同的函数的说明描述,它不是某一个具体的函数;而模板函数则是将函数模板内的“数据类型参数”具体化后得到的重载函数(就是由模板而来的函数)。简单地说,函数模板是抽象的,模板函数是具体的
C++函数传递参数的方式
值传递
进行值传递时,就是将实参的值复制到形参中,而形参和实参不是同一个存储单元,所以在函数调用结束后,实参的值不会发生改变
指针传递
进行指针传递时,形参是指针变量,实参是一个变量的地址,调用函数时,形参(指针变量)指向实参变量单元。这种方式其实还是“值传递”,只不过实参的值是变量的地址而已
引用传递
被调函数的形参虽然也作为局部变量在被调函数的栈中开辟了内存空间,但是这时存放的是由主调函数传递进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,也就是说通过栈中存放的地址来访问主调函数中的实参变量。所以被调函数对形参做的任何操作对实参变量都可见。与指针不同的是,使用指针传递的时候,在函数体内可以直接修改这个地址的值,而使用引用传递是无法做到这一点的
全局变量传递
这里的“全局”变量并不一定是真正的全局(所有代码都可以直接访问),只要这个变量的作用域足够这两个函数访问就可以了,例如一个类中的两个成员函数可以使用一个成员变量实现参数传递,或者使用static关键字定义,或者使用namespace进行限制等。当然,可以使用一个类外的真正的全局变量来实现参数传递,但有时并没有必要。从工程上讲,作用域越小越好
全局变量传递的优点是效率高,但它对多线程的支持不好,如果两个进程同时调用同一个函数,而通过全局变量进行传递参数,该函数就不能总是得到想要的结果
重载与覆盖的区别
重载是函数名字相同,函数参数不同(参数的类型、个数、顺序不同),在同一可访问区域内声明的几个具有不同参数列表的同名函数。程序会根据不同的参数列来确定具体调用哪个函数。对于重载函数的调用,在编译期间就已经确定了,是静态的,它们的地址在编译期间就确定了,与多态无关
覆盖是指派生类中存在重新定义基类的函数,其函数名、参数列表、返回值类型必须同父类中的向对应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体不同。当派生类对象调用子类中该同名函数时,会自动调用子类中的覆盖版本,而不是父类中的被覆盖的函数版本,它和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的该函数,这样的函数在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的
重载与覆盖的区别如下:
-
覆盖是子类和父类之间的关系,是垂直关系;重载是同一个类中函数之间的关系,是水平关系 -
覆盖只能由一个函数,或只能由一对函数产生关系;函数的重载是多个函数之间的关系 -
覆盖要求参数列表相同;重载要求参数列表不同 -
覆盖关系中,调用哪个函数是根据对象的类型(对象对于存储空间类型)决定的,重载关系是根据调用时的实参表与形参表来选择方法体的 -
覆盖是运行时确定的,而重载是编译时确定的
隐藏与覆盖
隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
-
如果派生类的函数与基类的函数同名,但是参数不同,则不论有无virtual 关键字,基类的函数在子类中都将被隐藏 -
如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字,此时基类的函数在子类中也被隐藏
当参数相同的时候,就要看是否有virtual关键字,如果有,就是覆盖关系;如果没有,就是隐藏关系
无参构造函数能否调用单参构造函数
无参构造函数可以调用单参构造函数,但是,这种调用方法只是被当作一个普通函数的调用,同直接使用单参构造函数来实例化一个对象是有区别的。
例如:
#include <iostream>
using namespace std;
class A
{
public:
A()
{
A(0);
Print();
}
A(int j) :i(j)
{
cout << "Call A(int j)" << endl;
}
void Print()
{
cout << "Call Print()" << endl;
}
int i;
};
int main()
{
A a;
cout << a.i << endl;
system("pause");
return 0;
}
程序的输出结果为
Call A(int j)
Call Print()
-858993460
以上代码希望无参构造函数调用带参构造函数,但未能实现。因为在无参构造函数内部调用带参的构造函数属用户行为,而非编译器行为,它只执行函数调用,而不会执行其后的初始化表达式。只有在通过构造函数实例化对象时,初始化表达式才会随相应的构造函数一起调用
函数调用的方式
编译器一般使用堆栈实现函数调用。堆栈是存储器的一个区域,嵌入式环境有时需要程序员自己定义一个数组作为堆栈。Windows为每个线程自动维护一个堆栈,堆栈的大小可以设置。编译器使用堆栈来存放每个函数的参数、局部变量等信息
由于函数调用经常会被嵌套,同一时刻,堆栈中会存储多个函数的信息,每个函数又占用一个连续的区域,一个函数占用的区域常被称为帧(frame),编译器是从高地址开始使用堆栈的,在多线程(任务)环境,CPU的堆栈指针指向的存储器区域就是当前使用的堆栈。切换线程的一个重要工作,就是将堆栈指针设为当前线程的堆栈栈顶地址。不同CPU,不同编译器的堆栈布局、函数调用方法都可能不同,但是堆栈的基本概念是一样的
当一个函数被调用时,进程内核对象为其在进程的地址空间的堆栈部分分配一定的栈内存给该函数使用,函数堆栈功能如下:
-
进入函数前,保存“返回地址”和环境变量。返回地址是指该函数结束后,从进入该函数之前的那个地址继续执行下去 -
进入函数后,保存实参或实参复制、局部变量
函数原型:[连接规范] 函数类型 [调用约定] 函数名 参数列表 {......}
调用约定:调用约定是决定函数实参或实参复制进入和退出函数堆栈的方式以及函数堆栈释放的方式,简单讲,就是实参或实参复制入栈、出栈、函数堆栈释放的方式。在Win32下有4中调用方式
-
_cdecl 。它是C/C++的默认调用方式。实参是以参数列表从右依次向左入栈,出栈相反,函数堆栈由调用方来释放,主要在那些带有可变参数的函数上,对于传送参数的内存栈是由调动者来维护的。另外,在函数名修饰约定方面也有所不同。由于每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall 函数的大 -
_stdcall 。它是WIN API的调用约定。其实,COM接口等只要是声明定义接口都要显式指定其调用约定为_stdcall 。实参以参数列表从右依次向左入栈,出栈相反。函数堆栈是由被调用方字节释放的。但是,若函数含有可变参数,那么即使显式指定了_stdcall ,编译器也会自动把其变成_cdecl -
_thiscall 。它是类的非静态成员函数默认的调约定,其不能用在含有可变参数的函数上,否则编译会出错。实参以参数列表从右依次向左入栈,出栈相反。函数堆栈是由被调用方自己释放的。但是,类的非静态成员函数内部都隐含一个this指针,该指针不是存放在函数堆栈上的,而是直接存放在CPU的寄存器上 -
_fastcall 。快速调用。它们的实参并不是存放在函数堆栈上,而是直接放在CPU寄存器上,所以不存在入栈、出栈、函数堆栈释放
需要注意的是,全局函数或类静态成员函数,若没指定调用,则约定默认是_cdcel 或是IDE设置的
可重入函数
可重入函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,由操作系统调度去执行另外一段代码,而返回控制时不会出现什么错误而且每次运行都能得到正确的结果。可重入函数主要用于多任务环境中;而不可重入函数由于使用了一些系统资源,比如全局变量区、中断向量表等,所以它如果被中断,可能会出现问题(被中断后,其他进行可能会修改这些全局变量,从而导致函数在每次运行都可能会得到不同的返回值),这类函数是不运行在多任务环境下的
满足下列条件的函数多数是不可重入的:
-
函数体内使用了静态的数据结构或者全局变量 -
函数体内调用了malloc() 或者free() 函数 -
函数体内调用了标准I/O函数
只有在函数体内不访问那些全局变量,不使用静态局部变量,坚持只使用缺省态(auto)局部变量,写出的函数就是可重入的。如果必须访问全局变量,必须使用互斥信号量来保护全局变量。和硬件交互时关中断,当完成交互后,再开中断
需要注意的是,当调用了不可重入的函数时,会使该函数也变为不可重入的函数。一般驱动程序都是不可重入的函数,因此在编写驱动程序时一定要注意重入的问题
|