这是一个C语言系列文章,如果是初学者的话,建议先行阅读之前的文章。笔者也会按照章节顺序发布。
上一篇我们介绍了表达式和语句,这一篇我们介绍C语言中的函数。
首先,我们先来看个例子:
int add(int num1, int num2)
{
return num1 + num2;
}
上面这段内容就是一个完整的C语言函数。
函数与我们在数学中了解到的函数概念比较相似。粗略地讲,在数学中,函数是用来完成某种功能的,例如,sin、cos等。在C语言中,与之类似,都是用于封装某种功能的。函数不但将功能封装成小的个体函数,还可以在需要的地方直接调用,避免通篇复制功能细节代码。
上面的例子就是计算两数相加的结果。当我给出具体的数值,例如1和2,那么这个函数就会返回3,其中1和2我们称作输入参数,3称作函数的输出(或者 返回值)。
读过码哥前面几篇文章的读者可能记得,在表达式一节中,函数调用表达式的值就是函数返回值,我们将在下面进一步介绍。
在C语言中,函数的命名规范与变量的命名规范是一样的,都是以下划线(_)或字母开头,后续字符可以是字母、数字、下划线。下面给出一些例子:
_abc 符合命名规范
123abc 不符合规范
abc 符合规范
~abc 不符合规范
由于在C语言中有多种基础数据类型,因此,函数的参数类型、参数数量、返回值类型都有所不同。除去这些不同,其他部分则是有统一形式的。
返回值类型 函数名 (参数1, ...)
{
各种语句
return 返回值;
}
其中:
根据功能需要可有参数也可以没有参数。例如:
int get10(void)
{
return 10;
}
//或者
int get10()
{
return 10;
}
对于不需要参数的函数,可以在参数列表处写void,也可以省略不写。
如果函数不需要返回值,那么return语句可以省略,但是函数 返回值类型 处要写void。
如果不写返回值类型,那么编译器默认为int型。
void print_help(void)
{
printf("Help information\n");
}
或
void print_help(void)
{
printf("Help information\n");
return;
}
其中,printf是C标准IO库中定义的终端输出函数,初学者暂时不必纠结于此,只记住这个函数可以将里面的字符串部分输出到屏幕上。
可能有的读者会发现,笔者从来没说过基础数据类型中有字符串类型。是的,C语言中确实没有这个基础类型,字符串在C中都是以字符数组出现的,关于数组相关内容,笔者将会在后面文章中讲解。
函数的声明是用来告知编译器,函数在当前代码文件或者其他代码文件中有定义。
为何有这种需求呢?我们来看下面这个例子:
int main(void)
{
foo();
}
void foo(void)
{
}
这个例子的功能是,在main函数中调用foo函数。
将代码直接写入文件然后编译,编译器会报错,大致是说main中的foo和外面的foo类型冲突。一般导致这样的报错有两种原因:
这里,是因为第二个原因导致的。为何是第二个呢?
**由于因为C语言编译器在处理源文件(即代码文件)时,是由上至下,由左至右的读取和处理文件内容的,因此就会对函数定义的位置、顺序较为敏感。**在上面的这个例子中,当处理到main时,编译器并不知道这个文件中定义了foo函数,因此当看到main函数中调用了foo函数时就会误将调用当函数声明。而后当读取到foo函数定义时,发现和之前的声明不一致(返回值类型)。
解决方法很容,见下例:
void foo(void);
int main(void)
{
foo();
}
void foo(void)
{
}
再次编译即可通过。
我们在main函数定义前加入的内容就是foo函数的声明。
函数声明也有其一般形式:
返回值类型 函数名(参数列表);
其中,参数列表部分,若无参数可以省略不写或者写void,与定义一样。但如果有参数,则可省略参数名,亦可不省略。例如:
int add(int num1, int num2);
或
int add(int, int);
都是可行的,因为编译器在读取声明时只关心函数参数的类型,而不关心具体名字。
上面我们其实已经见过函数的调用了。下面先说其一般形式:
函数名(输入参数列表);
例如,
add(1, 2); //调用最开始定义的add函数,函数有返回值,但是并未使用到
int result = add(1, 2); //调用add函数,并将返回值赋给变量result,result的值为3
在C语言中,我们将函数定义中的参数称做形式参数(形参),函数调用中的参数称作实际参数(实参)。
上面的例子中,1和2就是实参,而定义中的num1和num2就是形参。
在一些关于C语言的文章中,有人说参数的传递包含两种方式,这点笔者不敢苟同。在C语言中只有一种传参方式——值传递。
其中,参数传递是指:调用函数时,将实际参数传递给被调用函数的形式参数,也可以看作是一种映射过程。
值传递(此处内容建议初学者在阅读后续数组和指针文章后再来阅读):
值传递最直接的理解就是传递数值。
在C语言中,基础数据类型的参数传递比较显而易见,例如上面add调用的例子,传递的是两个整数值。但是数组的传递,对于一些写惯脚本语言的开发者来说会不太习惯。看一个例子:
int new_add(int *nums)
{
return nums[0] + nums[1];
}
int main(void)
{
int nums = {1, 2};
int result = new_add(nums);
return 0;
}
这里,在main中定义了一个整型数组,其中含有两个元素分别是1和2。我要将数组传给new_add函数,然后让函数返回两个元素相加的结果。此时,我们传递给new_add函数的参数并非完整数组的拷贝,而是数组的首地址,即指针。而指针也是一个值(可被看作无符号长整型),而非一种结构。
main函数也被叫做主函数,每一个生成可执行程序的C语言工程都一定有一个main函数,缺失主函数,编译器会报错。所有C程序都会从main函数开始执行。
在UNIX系统下(Linux、OSX等),主函数的形式如下:
int main(void)
{
...//具体内容
return 0;//具体返回值可自己定但一定是整数
}
或
int main(int argc, char *argv[])
{
...//具体内容
return 0;//具体返回值可自己定但一定是整数
}
其中,argc是命令行参数个数,而argv则是每一个命令行参数内容组成的字符指针数组,例如这个源文件生成的可执行程序叫a,那么在命令行下执行:
$ ./a argument1 2 3
此时,argc为4,而argv的内容为: “./a”, “argument1”, “2”, "3"这4个字符数组,即可执行程序名和其命令行参数。关于数组和指针的话题我们后面文章中会介绍。
下面我们介绍一种特殊的函数调用方式。先来看个例子:
void recursive_func(int i)
{
if (i > 3)
return;
recursive_func(++i);
}
int main(void)
{
recursive_func(0);
return 0;
}
在这个例子中,main函数中调用了一个名为recursive_func的函数,而recursive_func函数就是一个递归函数,即在函数中调用其自身。在main中传给recursive_func的参数i的值为0,然后在recursive_func内部每次调用自身时都将函数参数自增1,如此一层层调用下去。当i增加到3时,函数停止自身调用,然后逐层返回。