函数是C++语言的编程模块。一个程序是由若干个函数组成的,比如程序的入口函数——main函数。但实际上函数的功能和他涉及的语法并不总是这么简单。本篇笔记将总结函数的基本知识。并将在后面的笔记中一步步深入总结函数的相关功能。
1.使用函数的基本工作
要使用函数需要完成三项基本工作,即:提供函数的定义,提供函数原型,调用函数。当然这三者从语法上讲并不要求同时存在。比如,可以只定义函数,而不调用。而且如果在调用之前定义函数,甚至可以不用再提供函数原型,编译器会把该函数定义理解为函数原型。
函数定义的一般形式是:
typeName FunctionaName(paralist)
{
Statement;
return value;
}
函数分为有返回值和没有返回值两类。无返回值用void修饰,即上述typename为void,由返回值则用具体的返回值类型修饰,该类型则可以是除数组外的任意类型。如果是无返回值的函数,则可以省略return value的返回语句,或者只写return;而没有返回值。
其中的paralist是函数参数。函数参数通常按值传递参数(按值传递通常指表达式的左值),具体的函数参数内容将在后文中详细介绍。当然,函数也可以不带参数,仅为一个空括号或者也用void表示。
比如每个程序都不可缺少的main函数。通常的定义形式是
int main()
{
//dosth
return 0;
}
main函数是一个可执行程序必不可少的,如果你编写过MFC的程序,会发现没有main函数而是一个tmain函数,这种情况的main函数是被隐藏的,而这个tmain函数是被隐藏的卖弄调用的函数。main函数通常是一个以int类型为返回值的,没有函数参数的函数。return 0返回的0是告诉操作系统的,一般认为0是执行成功,而非0值尤其是负值表示执行失败。
函数原型为何存在呢?函数原型实际提供了函数到编译器的接口,也就是说它将函数返回值的类型以及参数的类型和数量告诉编译器,因此编译器知道应检索多少个字节以及如何解释调用的函数。如果想避免使用函数原型,则唯一的方法是在函数被调用之前定义它,但C++通常不这么做,c++的函数原型通常被写在头文件中,并通过预编译指令首先编译它。
函数原型的最简单写法是将函数头加上分号,形成函数声明语句。寒素原型不要求必须提供变量名,因为编译器做这项工作时不需要知道名字。通常看到的原型中是写有变量名的,主要是因为看上去更好理解。这里的变量名仅仅是占位符而已。
函数原型可以帮助编译器,或者说帮助程序员检查是否正确处理返回值,使用的函数参数是否正确,参数的类型是否正确等等。这种检查被称为静态类型检查。
2.函数参数
函数参数可以用来按值传递变量,成为连接函数的接口。用以接收传递值的变量被称为形参,实际传递给函数的值被称为实参。所以定义函数时的函数参数是形参,所谓形参,即形式上的参数,它的地位等同于函数内部建立的临时变量(局部变量)。只有在函数调用时采为这些变量分配内存,在函数使用结束后会自动释放内存。这意味着形参只是作用与函数体内的实参的一个临时副本而已。
举个栗子:
int add(int a, int b);
int main()
{
int d1 = 3;
int d2 = 4;
int sum = add(3,4);
return 0;
}
int add(int a, int b)
{
return a+b;
}
以上第一句为函数原型。这使得在main中使用add函数时编译器已经知道它是什么了。add函数是一个返回值为int类型的函数。函数参数为两个int型数据,a和b即为形参,在调用时的d1和d2是实参。调用函数时,将实参的值3和4传递给形参a和b,在函数内部,a的值对应为3,b的值对应为4。返回值a+b的表达式左值7.所有main中的sum即为返回值7.当函数执行到main的return 0;时,a和b的值被释放(无法查看值)。
3.函数和复合类型
函数参数可以任何类型。基本类型的处理很简单,下面总结下传递复合类型,如数组,指针以及结构体的基本原理。
首先要谨记一个原理,那就是C++处理数组的根本方式是通过指针。因此,在函数参数为数组时,实际上传递的是一个指针。如下面的函数:
int sum_arr(int arr[], int n);
这里arr并不是一个数组,而是指针,它等同于数组名。所以上述的表达方式不多见,但确实是合法的。
那么函数参数为指针是如何处理的?函数参数为指针实际上也是一种按值传递的体现,只不过传递的是一个地址值。相当于将实参的地址传递给函数形参,函数的内部则可以通过指针指向该地址来获取值,甚至通过地址偏移或数组下标的方式获取不同地址下的值(不同的元素)。
我们要时刻清楚以下三个公式则可以举一反三:
arr[i] == *(ar+i);
&arr[i] == ar+I;
arr[r][c] == *(*(ar+r) + c);//二维数组
既然,传递给函数的是地址,如果实参是数组,则意味着函数并不知道数组的大小,这就需要在形参中加入一个指示数组大小,甚至不等于大小而是函数要处理的长度的变量。如上述函数中的n。如果要处理的是字符串,那么则不必将字符串的长度传递给函数,因为字符串有自己的结束标志(’\0’),可以通过一些方式来获取长度,比如strlen。
对于函数参数是结构体或类,可以当做基本类型那样来处理,也可以用指针处理,那么在实参的地方需要用&来传递地址。但实际上对于较大的结构体,创建一个副本非常消耗资源,用指针或者引用的方式更好。
4.引用
引用因为是C++新增的符合类型,这里单独总结一下。引用是已定义变量的别名,通常用在函数参数的类型中。通过引用变量类型,函数将使用原始数据而不是其副本。这意味着可以节省大量资源,尤其是对于较大的结构体或类。
引用的类型用&符号,这里的&不再是取地址,而是类型标识。就像int *pr一样,int &表示指向int的引用。不同于指针,必须在声明引用的时候将其初始化,比如int b; int &a; a=b;这种写法编译器将报错。但实际上几乎不存在这种用法,因为引用通常仅仅用于形参。
当引用用于函数形参,函数参数将不再按值传递,而是按引用传递。这使得函数内可以使用实参本身。这么做的直接目的是避免复制副本,以节约资源。另一种使用引用的目的是修改实参,但要注意,如果实参和形参的类型不一致,但可以自动转化,如int 转为double,则函数会自动创建临时变量,则达不到修改实参的目的。
5.如何让函数输出变量
函数的输出一种是用返回值。用返回值返回基本类型,不用赘述。如果要返回指针(函数不能返回数组,但可以通过返回指针的形式间接返回数组),尤其是常见的返回字符串地址。这种情况下要在函数内为要返回的字符串动态分配内存,这些内存将被分配在自由存储空间上,在用户delete前会一直保持。但也导致用户一定要记得手动delete这些被分配的内存,不然就造成内存泄露了。
栗子:
char *GetChar(int n)
{
char *pch = new[n+1];
pch = ‘\0’;
while(n-- > 0)
pch[n] = ‘h’;
return pch;
}
int main()
{
char *pr = GetChar(4);
delete[] pr;
return 0;
}
另一种输出方式就是通过函数参数输出,这要求参数为指针或者引用。如果参数为函数指针,传递的值是地址,则可以在函数内对该地址内的数据进行修改输出。另外,引用直接用的是函数的实参变量,没有创建副本,则可以在函数内直接修改该值,这似乎更简单粗暴。
void add1(int a, int b int*sum)
{
*sum = a+b;
}
void add2(int a, int b, int &sum)
{
sum = a+b;
}
int main()
{
int a=1;
int b=2;
int c=0;
add1(a,b,&c);
add2(a,b,c);//the same as add1
}
6.函数参数到底应该用什么类型
在总结函数参数在什么情况下用什么类型之前,首先总结一下一个关键字const。
const约束使得编译器知道它修饰的值是不变的。用const修饰一个基本类型变量,如const int a编译器就会提示你const类型必须赋初值。其作用与#define a 1类似。
当const 用以修饰指针,如const int *pa;则pa指向的值为一个常量,表示该地址内的值不可修改。因此C++禁止将const的地址赋给非const的指针。再如int * const pa;则pa的值是常量,也即地址是不可修改的,但其存放的具体值是可以修改的。当然,如果const int * const pa;则都不可修改。
如果用以修饰函数形参,但来的效果将非常明显。这可以避免无意的操作而修改你本不想修改的值,同时const使得函数能够处理const和非const的类型,负责将只能接收非const的数据类型,而且const使得函数能够正确生成并使用临时变量。所以应尽可能的使用const。
那么现在来总结下如何定义函数的参数类型:
a)对应使用传递值而不作修改的情况:
如果数据量较小,如内置基本数据类型或小型结构,则按值传递;
如果数据对象是数组,则使用指针,因为这是唯一的选择。并将指针声明为指向const的指针。
如果数据量比较大的结构,则使用const指针或const引用,以提高程序效率。这样可以节省复制结构的时间和空间。
如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用。这是C++增加这项特性的原因。因此传递类对象参数的标准方式是按引用传递。
b)对于需要修改调用函数中数据的函数(通常讲的输出)
如果对象是内置数据类型,则使用指针。
如果对象是数组,则只能只用指针。
如果对象是结构。则使用引用或指针。
如果对象是类对象,则使用引用。
且上述变量都不能加const修饰。