C/C++之函数

Tips:
 1. 本人当初学习C/C++的记录。
 2. 资源很多都是来自网上的,如有版权请及时告知!
 3. 可能会有些错误。如果看到,希望能指出,以此共勉!

  说到函数,第一个要说的就是main函数,他是C/C++标准规定的!main的形式如下:

/* 形式一*/
int main(int argc, char *argv[])
{
    statements;
    return 0;
}
/* 形式二*/
int main(void)
{
    statements;
    return 0;
}
/* 形式三*/
void main()
{
    statements;
    return;
}
/* 形式四*/
main()
{
    statements;
    return;
}

  上面这些形式,是比较常见的几种main函数的形式,下面来详细解释一下。关于main函数,最详细的说明当然是C/C++的标准文档,以下内容是对标准文档的记录。ANSI C89和C90、C99和C11以及C++标准各个版本之间没有实质变化,只有一些细节上的东西有些区别。

C 标准

  首先是最后一种:这在C89标准中允许出现,Brian W. Kernighan 和Dennis M. Ritchie 的经典巨著 The C programming Language(《C 程序设计语言》)用的就是main( )这种形式。这主要是因为C语言诞生初期,只有一种类型,那就是int,没有char、long、float等。既然只有一种类型,那么就可以不写,后来的C标准为了兼容以前的代码于是规定:不明确标明返回值的,默认返回值为int,也就是说 main()等同于int main(),而不是等同于void main()。但是,该条规则,从C99开始,就不在支持了!
  在C99中,第5.1.2.2.1 Program startup节,指出了main函数的定义:
  The function called at program startup is named main. The implementation declares no prototype for this function. It shall be defined with a return type of int and with no parameters:
int main(void) { /* ... */ }
or with two parameters (referred to here as argc and argv, though any names may be used, as they are local to the function in which they are declared):
int main(int argc, char *argv[]) { /* ... */ }
or equivalent;9) or in some other implementation-defined manner.
9) Thus, int can be replaced by a typedef name defined as int, or the type of argv can be written as char ** argv, and so on

如果main函数的末尾没写return语句,C99 规定编译器要自动在生成的目标文件中加入 return 0

  第三种:关于第三种,返回值为void的,C++之父 Bjarne Stroustrup 在他的主页上的 FAQ 中明确地写着 The definition void main( ) { /* …… */ } is not and never has been C++, nor has it even been C。至于出现的原因,可能是由于在 C 和 C++ 中,不接收任何参数也不返回任何信息的函数原型为“void foo(void);”。但是,这条规则并不适用于main函数。事实上,void main()在C仍然可以是符合标准(conforming)的扩展,为什么呢?因为or in some other implementation-defined manner这句的存在。(ISO C/C++中,根据对环境的要求,分为两类:一类是独立实现(freestanding implementation),另一类是宿主实现(hosted implementation),不同的实现环境是有区别的,更详细的就只能看标准了。)

C++标准

  在C++98中,第3.6.1 Main function节,指出了main函数的定义:
  An implementation shall not predefine the main function. This function shall not be overloaded. It shall have a return type of type int, but otherwise its type is implementationdefined.
All implementations shall allow both of the following definitions of main:
int main() { /* ... */ }
and
int main(int argc, char* argv[]) { /* ... */ }

  • main 函数的返回值类型也必须是int。如果main函数的末尾没写return语句,C++98 规定编译器要自动在生成的目标文件中加入 return 0
  • 全局main禁止被使用。因此不像C,C++中main无法递归调用。&::main也是错误的
    再来说说函数返回值:
      C语言的(void)或函数定义中的()表示不接受任何参数,相当于C++的(),也和C++的(void)等价。C语言的()在函数定义外表示接受任何参数,相当于C++的(…)。所以在C语言中,声明时最好不要省略(void)中的void,要是省略就不是预期想要的函数原型了。在定义中可以使用(),如int main(){},同int main(void){}。但若要保证声明和定义通用,只用(void)表示函数没有参数。
      而C++中,不接受任何参数的参数列表写成(void)是不必要的。C++中函数参数空着和写void等效。

  • C语言中,main函数省略返回值相当于返回int,但是在C++中,不再主张这样用!

  • 关于注释,/**/是C风格的注释;// 是C++风格注释。现在,C及C++同时支持这两种注释风格
  • 关于头文件,xx.h是C和C++旧风格;C++新风格没有扩展名(iostream),原来旧的C头文件以c加原文件名组成(cmath)
  • 在旧的C标准中,变量的定义必须在函数开始的位置(C99支持随时定义),而C++中,习惯做法是在使用的地方定义!
  • C++中,可以使用连续赋值(a = b = c = 10 这个语句从右向左执行),C中不允许这样做
  • C++和C一样,不允许嵌套定义函数(可以嵌套声明),每个函数都必须是独立的!

函数定义

  函数分为有返回值和没有返回值两大类。

void functionName(parameterlist)          // 无返回值
{
statements;
return;     // 可以省略
}

typeName functionName(parameterlist)            // 有返回值
{
statements;
return value;   // value的类型为typeName类型
}

  C和C++对于返回值类型有一定的限制:不能是数组。但可以是其他任何类型:整型、浮点型、指针、结构、对象。(但是,数组可以作为结构或对象的组成部分被返回)。通常,函数将返回值放到寄存器或者内存中进行返回。

函数原型(声明)

  1. 函数原型声明是一条语句,因此必须以分号结尾。
  2. 通常将函数的原型声明放到头文件中。
  3. 函数原型不要求提供形参名,只需要有类型就可以了。原型中的形参名相当于占位符,因此不必与函数定义中的形参名相同,

C++函数原型与ANSI C函数原型
 ANSI C借鉴了C++中函数原型,但是两者是有区别的。其中,最大的区别就是:
  1. 为了与C兼容,ANSI C中的函数原型是可选的,但是,在C++中是必不可少的。
  2. 在C++中形参列表为空与写void是等效的——意味着函数没有参数。在ANSI C中,形参为空意味着不指明参数——意味着将在函数定义中给出形参列表。
  3. 在C++中,不指明形参列表时,需要使用省略号 … 。例如:void func(…); 通常,只有在需要与可变参数的C函数(如:printf)交互时,才需要这样做。
4.

为什么需要函数原型(声明)

  原型提供了函数到编译器的接口。原型告诉编译器函数有什么类型的返回值、参数类型和数量。方便编译器捕捉错误。关于编译器为什么不直接取cpp文件找函数定义,原因之一就是这样效率太低;再一个,函数的定义可能不在一个文件中或者定义在一个动态库中。

函数的形参和返回值

  参数的形参和返回值可以是基本类型、结构体变量、类变量、指针变量、引用等,变量的传递方式可以分为值传递和指针传递。需要重点说明的是引用型形参以及引用型返回值。

函数的返回值

返回值的隐式转换

  C和C++中同样:如果函数的实际返回值和声明的类型不一致,那么实际返回值将被自动转换为函数声明的返回类型。(超出范围,将可能出错)
返回值
VC编译器会给出如上图所示的警告!

返回值为引用的函数(仅C++)

类型标识符 &函数名(形参列表及类型说明)
{
    //函数体
}

  好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!

注意:
1. 不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了”无所指”的引用,程序会进入未知状态。
2. 不能返回函数内部new分配的内存的引用。 这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
3. 可以返回类成员的引用,但最好是const。 这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
4. 流操作符重载返回值声明为“引用”的作用:
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout <<”hello” << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。 因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性。 赋值操作符 = 和流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
5. 在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一 个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。

形参

形参的隐式转换

  C和C++中同样:将一个表达式作为实参传递给函数调用,此时形参和实参类型不一致:目标转换类型为形参的类型
转换
同4.1.1的返回值情况,编译器会给出警告!

引用型形参和临时变量(仅C++)

  如果实参和引用形参类型不匹配,C++将产生临时变量,当且仅当引用形参为const时。如果形参为const 引用,当出现以下两种情况时,C++将产生临时变量:

  1. 实参类型正确,但是不是左值(最新C++标准中,变量为可修改左值,常量为不可修改左值)
  2. 实参类型不正确,但是可以转化为正确的类型
    类型

  3. 如果变量为基本类型,尽量不要使用引用形参,而是使用按值传递的方式;当数据比较大(类、结构体)时,使用引用传递较好。

  4. 如果函数要修改字符串,则函数声明为:typeNmae functionName(typeNmae name[],int num);如果函数不需要修改字符串,则函数声明为:typeNmae functionName(const typeNmae name[],int num);
  5. 使用数组名作为形参,和使用指针作为形参效果是一样的:typeNmae name[] = typeNmae * name
  6. 指定数组区间。可以通过使用两个指针分别指向数组的开头和结尾的方式来指出数组的区间。例如:标准模板库(STL)使用“超尾”概念,即:指向数组最后一个元素的下一个位置。typeNmae functionName(const typeNmae *begin,const typeNmae *end);

形参的sizeof问题(C和C++同样)

函数形参在使用sizeof()取大小时,有许多需要注意的地方。
sizeof

参数及返回值的传递问题

  C语言函数参数和返回值的传递方式有:值传递指针传递两种,C++函数又增加了一种:引用传递。
  值传递:形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。
  指针传递:形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作
  (C++)引用传递:形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
  实际上,这里的指针传递可以归为值传递,下面给出解释。在Pascal语言中,形参有两种:值形参和变量形参,前者是值传递方式,后者是指针传递方式。在C语言中,只有“值形参”而无“变量形参”,全部采用值传递方式。C++把引用型变量作为函数形参,就弥补了这个不足。
传递方式
从编译的角度来阐述它们之间的区别:
  程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

参数传递内存问题

  函数调用过程中,必然存在着内存的开辟与释放。处理不好往往就会造成内存泄露,甚至程序崩溃。
函数传递内存
上图中,用到了指针的引用。注意指针的引用的写法。

  • 千万别返回指向“栈内存”的指针、引用、包括数组,因为该内存在函数结束时自动消亡了,返回的指针是个野指针了。
    引用
  • 可以返回指向堆的指针和引用,但是不推荐这样做。上图中,返回malloc分配的内存,因为这是在堆上分配的,如果程序员不去释放,将一直持续的程序结束,由操作系统回收,第二个函数,返回栈上的变量,是错误的!可以用函数返回值来传递动态内存,只能返回在堆上分配的内存、全局变量内存、静态变量

指针函数与函数指针

  辨别指针函数与函数指针最简单的方式就是看函数名前面的指针*号有没有被括号包含,如果被包含就是函数指针,反之则是指针函数。
指针函数就是返回类型是某一类型的指针的函数,形式如下:
类型标识符 *函数名(参数表);
函数指针就是指向函数的指针变量,即本质是一个指针变量。形式如下:
类型说明符 (*函数名)(参数); 当然 也可以定义函数指针数组: 类型说明符 (*函数名[ ])(参数);
例如:我们定义一个函数指针数组:

void *Func[](int*,int) = {BubbleSort, SelectionSort}; // 这就说明 函数BubbleSort 和SelectionSort 真正样子为 void BubbleSort()(int*,int)和void SelectionSort ()(int*,int)

定义和调用方式,如下图:
函数指针和指针函数

地址跳转(多出现在嵌入式C中)

再此之前,先看下面的例子:
跳转
  按照&运算符本来的意义,它要求其操作数是一个对象,但函数名不是对象(函数是一个对象),本来&func是非法的,但很久以前有些编译器已经允许这样做,c/c++标准的制定者出于对象的概念已经有所发展的缘故,也承认了&func的合法性。
  因此,对于func和&func可以这样理解:func是函数的首地址,它的类型是void(),&func表示一个指向函数void func(void)这个对象的地址,它的类型是void(*)(),因此func和&func所代表的地址值是一样的,但类型不一样。func是一个函数,&func表达式的值是一个指针!
  这时,我们来看一道嵌入式笔试题:想让程序跳转到绝对地址0x100000处执行,该如何做?
  答案有两种:(*(void(*)(void))0x100000)(); // 和上面的(*funcPtr)();是不是很像
或者((void(*)(void))0x100000)(); // 和上面的(funcPtr)(); 是不是很像
  首先,来看(void()(void),这就好比一个类型,就像int是指向int的指针,那么(void(*)(void) 就是指向 void func(void)这样函数的函数指针。这样,就回到了我们的函数指针上。
  将地址0x100000转换时,其当然是没有参数和返回值的,因此:(void()(void))0x100000 // 这就和(int)a类似了。
  与此同时,(void()(void)是指向函数的指针,转换后自然带有函数特性,因此:(void()(void))0x100000 就是个函数指针
  最后,我们调用这个函数指针:就是出现了上面两种答案了。
  可能有人会问,上面函数指针调用中还有个funcPtr(),这是不是就意味着,(void(*)(void))0x100000() 这也是正确的呢?实际上,这是不对的。
  在很多嵌入式代码中,我们常看到如下代码:
void(*reset)(void) = (void(*)(void))0;
  其中,void(reset)(void)就是函数指针定义,(void()(void))0是强制类型转换操作,将数值“0”强制转换为函数指针地址“0”。通过调用reset()函数,程序就会跳转到程序执行的“0”地址处重新执行。在一些其他高级单片机Bootloader中,如NBoot、UBoot、EBoot,经常通过这些Bootloader进行下载程序,然后通过函数指针跳转到要执行程序的地址处。

void (*theUboot)(void);  // 定义函数指针
...
theUboot = (void (*)(void))(0x30700000);
theUboot();   // 这样程序就从0x30700000这个地址开始执行了
...
(*(void (*)(void))(0x30700000))();  // 强制类型转换,将一个绝对地址转换为一个函数指针,并调用这个函数以跳转到前面提到的绝对地址
上面的语句翻译成汇编就是:
mov r0,0x30700000;
mov pc,r0

  对于(*(void(*)(void))(0x30700000))();可以这样理解:首先(void()(void))是一个强制类型转换符,他将后面的0x30700000这个无符号整数强制转化为一个函数指针,该函数指针所指向的函数入口参数为void,返回值也是void。如果到这步你看懂了,那么设(void()(void))(0x30700000)为fp;那么上面的表达式就可以简化为(*fp)();

函数递归调用

  C++函数和C一样,允许函数调用自己。(与C不同的是,C++不允许main调用自己)。递归函数必须要有结束条件,否者将一直调用下去。

  1. 递归分为直接递归和间接递归:直接递归:函数直接调用自己;间接递归:函数调用其他函数,其他函数又调用本函数,实现间接调用。
  2. 递归须有完成函数任务的条件语句,且先有条件语句,后有递归调用。也就是说,递归调用是有条件的,满足了条件后,才可以递归
  3. 大多数递归函数都能用非递归函数来代替。
  4. 递归的目的是简化程序设计,使程序易读。但递归增加了系统开销。时间上,执行调用与返回的额外工作要占用CPU时间。空间上,随着每递归一次,栈内存就多占用一截。
    单一递归

内联函数(C99添加)

  内联函数是C++为了提高程序执行速度而做的改进(节省时间但是消耗空间),常规函数与内联函数的区别主要在于C++编译器如何将他们组合到程序中。使用内联函数特性必须符合以下条件之一:

  • 在函数声明中加上关键字inline
  • 在函数定义中加上关键字inline

  C++内联函数用来取代C语言中的宏函数。内联函数的参数传递方式为:值传递;宏函数则是文字替换。

  1. 一个函数可以自已调用自已,称为递归调用,含有递归调用的函数不能设置为inline;
  2. 使用了复杂流程控制语句:循环语句和switch语句,无法设置为inline;
  3. inline仅做为一种“请求”,特定的情况下(如上),编译器将不理会inline关键字,而强制让函数成为普通函数。出现这种情况,编译器会给出警告消息。
  4. 由于inline增加体积的特性,所以建议inline函数内的代码应短小。最好不超过5行。
  5. 在你调用一个内联函数之前,这个函数一定要在之前有声明或已定义为inline,如果在前面声明为普通函数,而在调用代码后面才定义为一个inline函数,程序可以通过编译,但该函数没有实现inline。比如下面代码片段:
// 函数一开始没有被声明为inline: 
void foo();
// 然后就有代码调用它:
foo();
// 在调用后才有定义函数为inline:
inline void foo()
{
    ...... 
}
  1. 为了调试方便,在程序处于调试阶段时,所有内联函数都不被实现。
  2. 在一个文件中定义的内联函数不能在另一个文件中使用。它们通常放在头文件中共享。

C++函数重载

什么是函数重载

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

为什么需要函数重载

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

如何实现函数重载

  函数重载的关键是函数的参数列表,如果参数的个数不同或者类型不同就可实现重载。注意引用形参和普通形参不能重载。注意:编译器将类型引用和类型视作相同类型,不能重载。

C++函数重写、重载、隐藏的区别

重写(override)
 覆盖也称重写,英文override,重写(覆盖)的规则:

  1. 不同的范围(分别位于派生类与基类);
  2. 重写方法的函数名和参数列表必须完全与被重写方法的相同,否则不能称其为重写而是重载.
  3. 重写方法的访问修饰符一定要大于被重写方法的访问修饰符(public>protected>default>private)。
  4. 重写的方法的返回值必须与被重写的方法的返回一致;
  5. 重写的方法所抛出的异常必须和被重写方法的所抛出的异常一致,或者是其子类;
  6. 被重写的方法不能为private,否则在其子类中只是新定义了一个方法。
  7. 静态方法不能被重写为非静态的方法(会编译出错)。
  8. 基类函数必须有virtual关键字。

作用:
  基类指针和引用在调用对应方法时,根据所指对象类型实现动态绑定。
重载(overload)
  overload是重载,一般是用于在一个类内实现若干重载的方法,这些方法的名称相同而参数形式不同。重载的规则:

  1. 相同的范围(在同一个类中);
  2. 函数名字相同;
  3. 参数不同:类型不同、数量不同;
  4. virtual关键字可有可无。

作用:
  同一方法,根据传递消息的不同(类型或个数),产生不同的动作(相同方法名,实现不同)。
隐藏

  1. 不同作用域:派生类的函数屏蔽了与其同名的基类函数
  2. 基类和派生类函数名相同,但是参数列表不同,不同有无virtual,基类函数在派生类中被隐藏,派生类只能调用新的方法,不能调用已被隐藏的基类方法(不同于重载,作用域不同)
  3. 基类与派生类同名,同参,但基类函数无virtual,同样派生类中同样隐藏基类的同名同参函数(不同于覆盖,无virtual)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZC·Shou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值