c++ double 转 int_认识CC++中的函数

本文介绍了C++中函数原型的重要性和作用,强调了它在强类型编程中的角色。通过实例解释了函数原型声明、调用以及类型转换的过程。此外,还讨论了函数重载的概念,展示了如何通过形参类型的不同来实现同一函数的多种功能。最后,探讨了函数内联的必要性,分析了内联函数如何提高程序执行效率,并给出了内联函数的定义和使用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、函数原型

  1. 函数原型
    函数原型又称为函数签名,顾名思义,通过函数原型就能够找到你要的函数。函数原型是一种区分函数身份的声明,声明指定了函数的名称、形参类型及函数的返回类型。形参名称在函数原型声明中并不是必须的,即形参名称事实上是可以忽略的。如果两个函数的原型声明仅返回类型不同其它都相同的原型声明是不允许的。

通过提供函数原型声明,然后再进行函数定义或调用,C++成为强类型的程序设计语言。强类型的程序设计语言要求:调用的函数的实参类型必须和函数原型的形参类型一致。在匹配调用函数的实参和函数原型的形参时,编译程序检查函数名和形参类型,至于形参名称则无关紧要。由于扩展名为.h的头文件包含函数原型声明,因此,在调用其中的任何函数之前必须include,如果不使用include就必须直接进行原型声明。如下例子展示了函数的原型声明和调用方法。

例.函数原型的声明和函数的调用。

double sin(double x);//直接进行有参数名的原型声明double cos(double);//直接进行无参数名的原型声明int main(void){double x=3;double y=4;return sin(3.0)+sin(x)+sin(y)+cos(3.0)+ cos(x)+cos(x+y);}

对于例1,你可能有几个疑问:

(1)双斜线“//”的作用是什么?它表示程序注解,从双斜线开始到当前行结束都是注解,编译程序会自动将注解忽略。此外,C++还支持C语言原有的形式为“/注解内容/”的注解,这种注解通常可以跨多行存在。

(2)主函数main的返回类型是int,可是return的表达式的值却是double类型的?这没有关系,因为编译程序会自动将double类型的值转化为int类型。

(3)变量x、y都是double类型的,可是却用int类型的值初始化?这也没有关系,因为编译程序会自动将int类型的值转化为double类型。

(4)变量x和sin函数原型中的x相同,这不会引起混淆吗?这不用担心,在调用sin(x)时,实参是主函数main定义的变量x,实参的值传给sin(x)的形参,之后实参和形参就没有任何关系。调用sin(x)时,编译程序会检查实参和形参的类型是否一致。

(5)在调用sin(3.0)中,实参3.0没有名称能行吗?能行,因为编译程序检查的只是实参和形参类型,两者的类型相同就可以了,然后将实参的值3.0传给sin(double x)的形参x。

(6)在调用cos(x+y)中,使用x+y不会出错吗?当然不会,只要x+y的值的类型和形参类型一致即可,x+y的值将传给cos(double)的形参,尽管在原型声明double cos(double) 中形参没有名字,但在定义该函数的函数体时一定会给出形参名称。

从例1来看,函数原型的声明出现在函数调用之前,这显然符合C++强类型程序设计语言的要求。为了简单起见,这些数学运算函数的函数原型声明被一起打包,存放在一个名字为math.h的头文件中。在调用这些函数之前,使用#include 进行原型声明。如果使用#include "math.h",则表示math.h可能是你自己建立的一个头文件,这个文件应该和你的扩展名为“.cpp”的程序文件放在同一目录。

例.声明和函数的调用。

include  //math.h中的所有原型声明将出现于此、替换本行int main(void){double x=3;double y=4;return sin(3.0)+sin(x)+sin(y)+cos(3.0)+ cos(x)+cos(x+y);}

宏命令#include 在编译时起的作用是宏替换,它将math.h中的所有函数原型声明替换#include 所在的行。即使有些数学函数你不使用,比如对数函数log(double),它也会作为原型声明出现在主函数main的前面。不过这并不会增加你编译后的可执行程序的长度,因为一个函数如果没有被使用,就不会被连接到可执行程序中。

对于sin等被C++预定义的函数来说,只需要进行函数原型声明就行了,不必进行函数定义,即程序员不必定义其函数体,编译程序会自动连接扩展名为.lib的库文件;如果编译程序没有连接相应.lib的库文件,也可要求操作系统动态连接.dll库文件。但是,对于程序员自己定义的函数原型,就必须在某个程序文件内定义相应的函数体。

例.函数的原型声明和调用。int square(int);//函数原型说明int main(void){//必须先进行函数原型声明或定义函数(体),才能调用函数int y= square(5);//函数原型已经在前面声明,故可调用return 0;}int square(int x)//程序员自己定义的函数,必须自定义函数体{ return x*x; }//自定义的函数体

2.函数重载函数重载即是对同一名称函数或者运算符加载不同的功能。假如要定义一组减法函数sub,用来完成:

(1)整数减整数;(2)浮点数减浮点数;(3)整数减浮点数;(4)浮点数减整数;(5)单目减,求整数的负整数;(6)单目减,求浮点数的负浮点数。

如果重载时形参再考虑其它类型,那么定义出来的重载函数就会更加多样了。

例. 减法函数的原型声明和调用。

int sub(int x, int y){ return x – y; }//整数减整数int sub(int x) { return -x; }//单目减:求整数的负整数double sub(double x, double y){ return x – y; }//浮点数减浮点数double sub(int x, double y){ return x – y; }//整数减浮点数double sub(double x, int y){ return x – y; }//浮点数减整数double sub(double x) { return -x; }//单目减:求浮点数的负浮点数int main(void){double x, y=4, z=5;x=sub(7, 1);//调用int sub(int x, int y)y=sub(x, 1);//调用int sub(double x, int y)y=sub(3.2, 1);//调用int sub(double x, int y)return sub(-3);//调用int sub(int x)返回整数3}

当同一个函数sub被重载用于完成不同的功能时,编译程序在调用这些函数时是如何避免其相互混淆的?编译程序是根据实参类型与形参类型是否匹配来进行区分的。例如,在例3中函数调用sub(3.2, 1)的两个实参分别为double和int类型,因此,编译程序将调用的函数原型为int sub(double x, int y)。要把各种形参类型都定义一遍,要定义的重载函数实在太多了。

有时并没有定义各种形参类型的重载函数,在调用函数时编译程序会作何选择呢?例如,在例3中就没有重载sub(char, char)函数,如果要用sub('B ', 'A') 调用sub函数,那么会调用哪个函数呢?根据C和C++的类型转换规则,char类型首先转换为int类型,因此,sub(‘B’, ‘A’)会调用int sub(int x, int y)函数。调用时需要注意实参类型可能会被转换,以便能够成功地匹配被重载的原型函数。

3.重载的实现函数重载就是同一个函数名称,通过其形参个数或者类型的不同,区分定义其完成的不同功能。为了弄清楚编译程序如何实现函数重载,有必要检查一下例4编译后的汇编程序。假定例4程序的文件名为sub.cpp,采用C++ builder 6.0的32位命令行编译器bcc32.exe,编译产生汇编代码的命令为bcc32 –S sub.cpp。为了节省篇幅及更清楚的查看汇编代码,在省略了不太重要的部分汇编代码后,例4编译后的汇编语言程序如例5简要所示。

例5. 例4程序编译后的部分汇编代码。

; int sub(int x, int y);//编译后的函数名为@@sub$qii

@@sub$qii proc near;//@@sub$qii的两个i代表两个参数int

push ebp;//保存ebp的值mov ebp,esp;//使用ebp代替栈指针mov eax,dword ptr [ebp+8];//取x的值送入寄存器eaxsub eax,dword ptr [ebp+12];//减去变量y的值,结果在eaxpop ebp;//恢复ebp的值ret;//函数返回值在eax寄存器

; int sub(int x) { return -x; };//编译后的函数名为@@sub$qi

@@sub$qi proc near;//@@sub$qii的一个i代表参数int

push ebpmov ebp,espmov eax,dword ptr [ebp+8]neg eax;//求eax存储的整数的负整数pop ebpret;//函数返回值在eax寄存器

; double sub(double x, int y);//编译后的函数名为@@sub$qdi

@@sub$qdi proc near;//@@sub$qdi表示有参数double, int

push ebpmov ebp,espfild dword ptr [ebp+16]fsubr qword ptr [ebp+8];//真正有效的减法指令pop ebpret

; int main(void);//编译后的函数名为@_main

@_main proc near

push ebpmov ebp,espadd esp,-28;//为变量x,y,z等分配栈空间

; x=sub(7, 1);;//调用int sub(int x, int y)

push 1;//从右至左传实参:第2个参数,4字节入栈push 7;//从右至左传实参:第1个参数,4字节入栈call @@sub$qii;//调用int sub(int x, int y)add esp,8;//使调用前后栈指针平衡:8字节出栈mov dword ptr [ebp-28],eax;//将减法结果赋给变量x,x的地址为[ebp-28]

...

; y=sub(3.2, 1);;//调用int sub(double x, int y)

push 1;//从右至左传实参:第2个参数,4字节入栈push 1074370969;//从右至左传实参:第1个参数分两次8字节入栈push -1717986918call @@sub$qdiadd esp,12;//使调用前后栈指针相同:12字节出栈fstp st(0)

; return sub(-3);;//调用int sub(int x)返回整数3

push -3;//从右至左传实参:整数-3用4字节入栈call @@sub$qi;//调用int sub(int x),返回值在eaxpop ecx;//4字节出栈mov esp,ebppop ebpret;//主函数返回值在eax

在上述汇编语言程序中,使用分号表示注解。仔细分析该汇编代码,可以初步了解C++的编译方法。首先要注意的是编译后的函数名:int sub(int x, int y)编译后函数名为@@sub$qii,而double sub(double x, int y) 编译后函数名为@@sub$qdi。汇编语言的@和$就像C++的字母一样,可以作为函数名和变量名等标识符的一部分,编译自动产生的@@和$q主要起分隔和引导作用,分别表示后面出现的是函数名和形参类型,因此,汇编程序中的函数名@@sub$qii既包括了C++的原始函数名sub,也包括了该C++函数的两个参数类型int。从C++的角度来看,所有重载函数的函数名是相同的;而从汇编语言的角度来看,所有重载函数的函数名是不同的。

当了解了编译程序如何实现和编译重载函数以后,就会理解为什么调用时编译程序不会产生混淆。例如,假如sub函数调用使用两个整型实参7和1,编译程序就会根据参数类型自动产生call @@sub$qii调用指令,而其对应的C++函数原型就是int sub(int, int)。需要注意的是调用后,函数的整型返回值总是存放在eax寄存器中,eax是编译器使用的X86系列CPU的32位通用寄存器。另外,在main调用sub函数的前后,栈STACK的指针总是平衡或相同的,这就说明了为什么使用高级语言编程很少出现栈溢出。值得注意的是:在传递实参时,实参的传递顺序是自右至左的,这只是C++ builder 6.0的实现方法,当然也是大多数编译器的实现方法。但是,C++国际标准并未强行规定传递实参一定要从右向左。

二、函数内联1.为什么需要内联函数如果继续观察例4的汇编代码,就知道为什么需要函数内联了。首先来看为了计算7-1,调用sub(7, 1)的汇编程序到底做了多少有用和近乎无用的工作。参看例4汇编后的主程序:

(1)首先要传递实参,先后通过两条压栈指令push 1和push 7完成,向栈顶压入共计8个字节;

(2)然后发出函数调用指令call @@sub$qii;

(3)最后使调用前后的栈指针保持平衡,即使用add esp,8使栈顶回调8个字节。

主函数一共使用了4条指令完成sub(7, 1)的调用。

然后再来看看函数@@sub$qii做了哪些工作。在函数@@sub$qii中,一共使用了6条指令。其中,只有两条指令真正用来完成减法操作:mov eax,dword ptr [ebp+8]用于取被减数的值,sub eax,dword ptr [ebp+12]用于减去减数的值,结果遗留在eax寄存器中,并被当作函数的返回值。在函数 @@sub$qii中只有两条指令实际完成减法操作。

由此可见,主函数main和减法函数sub一共使用10条指令完成7-1运算,但其中仅仅两条指令真正用来进行减法操作,可见指令的实际利用效率非常之低。如果设法用 @@sub$qii函数体的两条减法指令,来替换主函数所有的调用减法函数的四条指令,然后去掉@@sub$qii函数,节约出来的指令总数将极其可观,同时程序执行的速度也会大大加快。

假如主函数一共有20个int sub(int x, int y)调用,按上述方法用sub函数体的有效减法指令替换后,可执行程序的总长度将减少46条指令(20次调用4指令/每次+sub函数6指令-替换后20次调用2指令/每次),而替换前需要执行2010共计200条指令,替换后需要执行202共计40条指令,很显然程序的执行速度也提高了4倍。上述替换方法就是函数内联(inline,内嵌)。在被内联函数的函数体很小时,内联将使程序的总长度变短;而如果被调内联函数的函数体很长时,内联反而会使程序更加冗长。

2.内联函数的定义及使用当被调函数的函数体很小时,函数内联能减少程序代码,大大降低程序运行开销。函数内联即用被内联的有效函数体替代调用指令从而降低程序运行开销。内联函数用保留字inline声明或定义。一个函数若被定义为内联函数,编译程序就会用其函数体替换每个调用,而不是把调用编译成压栈、调用和退栈指令。编译为了提高效率大都只扫描程序一次,因此,内联函数的函数体定义一定要出现在调用之前,否则,编译程序会因无法找到内联函数体而无法替代调用指令,从而造成函数内联失败而仍然按非内联的方式编译调用。

例. 编程计算圆的面积。

说明:程序在调用函数之前定义了内联函数area的函数体。编译会将主程序中的调用area(m)直接替换为3.1415926*m*m,而不是编译成push、call、pop等若干机器指令。

程序:

includeinline double area(double r){return 3.1415926*r*r;}void main(void){double m;cout<>m;cout<

如前所述,若内联函数的函数体定义出现在调用之后,可能会造成编译时函数内联失败。在内联函数内部,不能使用分支、循环、开关和函数调用等引起转移的语句,这些函数被视为函数体复杂的函数,因编译倾向于内联函数体简单的函数而造成内联失败。此外,若其他函数访问了被内联函数的入口地址,或者被内联的函数是类的虚函数或纯虚函数,都会导致内联函数在编译时内联失败。

例7. 编程计算圆的面积。

includeinline double area(double r);//内联函数原型说明inline double girth(double r);//内联函数原型说明inline double girth(double r)//内联函数定义:inline可省略{return 2*3.1415926*r;}void main(void){double m;cout<>m;cout<

内联失败并不表示程序出现了错误,只是仍然按函数调用指令进行编译。内联函数无论内联是否成功,其作用域都局部于当前程序文件,也就是说相当于在定义函数时使用了static保留字。因此,其他程序文件是无法访问该内联函数的。如果其它文件希望调用该内联函数,可以重新进行相同的内联定义或使用include。因为static函数或变量的作用域局限于其定义文件,因此,在不同程序文件内重复定义相同的static函数和变量不会冲突。

是不是所有的函数都可以定义为inline的呢?当然不是,比如主函数main。现有的C++编译器只允许出现一个main,这个main被作为操作系统的唯一调用入口,该函数的作用域是全局的而不是static的,即main在任何程序文件都能访问到。现有C++编译器不允许调用main或者取main的地址。事实上,如果能够取main的地址,就可以通过函数指针调用main了,所以归根结底是现有编译不允许调用main。

以前的编译允许定义多个static的main函数,但是,新版编译器似乎都不允许这样的定义。如此一来,用inline说明main也不被允许了:如前所述,如果main内联成功,则相当于在main之前加上static,main的作用域就会局限于当前程序文件;并且如前所述如果内联成功,被内联的main函数代码将被抛弃,如此main函数也就不复存在,操作系统就无法调用和进入main了。所以,新编译器大都不允许内联main函数。

三、函数参数1.具有缺省值的函数参数在声明函数原型和定义函数体时,必须说明函数的形参类型。形参名称在进行原型声明时并不是必须的。但在定义函数的函数体时,如果定义的形参没有名称,函数体就无法访问该形参,因此,如果希望访问该形参就应说明其名称。具有缺省值的函数形参是这样一种形参,给定的值将作为调用时不传实参时的缺省实参值。当然,如果无名形参有缺省值,该值也无法被函数体使用。因此,在定义形参的缺省值时,通常都应该给出参数名称,但不给也不意味程序出错。

例8. 编程输出对春夏秋冬的评价。

include void evaluate (char *season=" Spring")//具有缺省值的形参说明{ cout<

在上述主函数main中,第1个调用没有给出参数,故使用定时时的缺省参数值,相当于调用evaluate("Spring");第2个调用没有使用缺省参数值。在定义具有缺省值的形参时,可以定义多个有缺省值的参数,但是它们必须出现在参数表的右部,且中间不得参杂不缺省值的形参。在声明函数原型和定义函数体时,不能重复定义函数形参的缺省值。此外,缺省值的表达式也不能用同一参数表的形参。

例. 具有缺省值的形参定义方法。

int u, v; //全局变量的u=v=0int m(int x, int y=5) {return x+y; }//正确int a(int u, int x=5,int y=6+m(u,v))//正确{ return u+x+y; }int b(int x=1,char,int z=1);//错误,夹杂非缺省值参数int b(int x=u);//正确,定义缺省值x=u,此时u=0int w=++u;//正确:w=u=1;int b(int x=u) {return x*x;}//错误:不能重复定义缺省值x=u,此时u=1int c(int x, int y=x++);//错误:表达式中出现同一参数表的参数xvoid main( ){int q=0;c(q);//使用缺省值,等价于c(q, q++);}

为什么缺省值的表达式不能使用同一参数表的形参呢?在调用函数时,C++的国际标准没有规定实参是从左向右计算还是从右向左计算,对于等价于c(q, q++)的函数调用c(q),两个方向计算实参的调用结果是不同的:(1)从左向右计算实参得到调用c(0, 0)及q=1;(2)从右向左计算实参得到调用c(1, 0)及q=1。这种不同造成程序不可移植,即不能使用不同的编译程序:不同的编译计算实参的次序可能不同,从而可能产生不可移植(不同)的运行结果。

2.函数的省略参数当不能确定函数到底有多少形参时,可以将函数参数定义为省略参数。省略参数是这样一种参数,它出现在函数参数表的最右部,表示参数个数和参数类型不定。省略参数使用…定义,而…的左边可以定义若干参数。实际上在头文件stdio.h中,就已经说明了若干省略参数的函数,其中最常用的几个函数说明如下:

int scanf(const char , …);//参数const char 控制输入:确定要输入几个变量

int printf(const char , …);//参数const char 控制输出:确定要输出几个值

int sprintf(char , const char , …);//参数char 存放输出结果,参数const char 控制输出

注意在上述函数中,scanf的返回值表示成功输入的变量个数,printf和sprintf的返回值表示成功输出的字符个数。对于printf和sprintf来说,输出总是会成功的。但是,对于scanf来说,希望输入的变量个数可能少于成功输入的变量个数:因为键盘设备可以看作一个输入文件,而任何文件都可能遇到文件结束而输入尚未完成。对于不同的操作系统,键盘文件结束的表示方式不同,例如UNIX操作系统的文件结束为CTRL-D字符,而微软的MS-DOS和Windows的文件结束为CTRL-Z。字符CTRL-Z表示按住CTRL键不放,再按下字符Z键然后同时释放两个键;字符CTRL-D的键入方法以此类推。

例. 编程输入若干正整数,并计算每个整数有几位有效数字。

includevoid main( ){int x;while(scanf("%d", &x)==1)//输入直到遇到键盘文件结束为止printf(" has %d digits", printf("%d", x));}

在上述程序中,printf("%d", x)用于打印整数,并将成功输出的字符个数作为整型返回值,这个值即正整数的有效数字位数。在MS-DOS或Windows操作系统上运行程序,可采用如下使用CTRL-Z结束键盘文件的输入:

35 ↵

567 ↵

CTRL-Z ↵

如果不想同时将整数x输出到屏幕上,可采用sprintf替代printf("%d", x)函数调用。从int printf(const char *, …)使用省略参数的方式来看,通常要用第1个参数如格式串确定后面省略了几个参数。在定义自己的省略参数函数时,也应该遵循这种定义模式。当然,参数表仅有省略参数也是允许的,只是定义函数体时处理起来相当麻烦。

例11. 编写函数计算n个整数的和。

int sum(int n, …)//第1个参数n表示后面省略了n个整数{int s=0, *p=&n+1;//p指向第1个省略参数,即n后面的参数for(int h=0; h

由此可以了解,printf和sprintf等函数是如何实现的。需要注意的是:int printf(const char *, …)省略的可能是int、double等不同类型的参数,通过第1个参数即格式串可以知道省略的每个参数的类型,由于通过实参传递的不同类型的值依次存放在栈上(见例5),因此通过栈指针可依次为某参数取若干字节、然后通过强制类型转换将其转换成相应类型的值,就可以得到每个要输出的省略参数的值。

3.函数调用的二义性当调用一个函数时,若有二个以上的函数原型能与之匹配,则该函数调用就产生了二义性。要从根本上解决二义性问题,可以从函数原型声明上着手,在说明函数原型或定义函数体时,应尽量降低产生二义性的可能性。当然,如果原型声明实在无法避免,则只能在调用时避免二义性,即通过更多或更明确的实参明确区分。

例. 函数调用的二义性问题。

int f(int x) {return x; }int f(int x, int y=3) {return x+y; }char f(int x, char c="ABCDEFGH") {return x+c; }int f(int x, …) {return x; }void main( ){f(2);//二义性调用:四个函数原型均可匹配}

在上述f(2)调用中,四个重载函数f都能和调用匹配:

(1)由于2是int类型,故能同第1个函数int f(int x)匹配;

(2)由于调用f(2)可被认为使用缺省y=3,故能和函数int f(int x, int y=3)匹配,相当于函数调用f(2,3);

(3)同理,可以和函数char f(int x, char c="ABCDEFGH")匹配,相当于函数调用f(2, "ABCDEFGH");

(4)由于调用f(2)可被认为省略0个参数,因此,调用f(2)能和函数原型int f(int x, …)匹配。

排除二义性的方法无非有两种:

(1)在函数声明或定义时避免二义性,如极端的情况是不定义重载函数;

(2)在函数调用时给出更多实参,以便提供更充分的信息匹配函数原型。

例如,调用f时给出两个实参能区分前三个重载函数,这样一来调用函数就不能使用参数的缺省值了。在例中,只有给出三个以上的实参调用f才能匹配第4个函数。这个例子说明在定义重载函数时,出现调用二义性问题的可能性极大,往往导致参数缺省值无法使用,失去了缺省值的定义意义。

a22998aecf4a4b62e00e23c1fb47c5c8.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值