1. 函数
函数(function)是完成特定任务的独立程序代码单元。语法结构定义了函数的结构和使用方式。
函数的功能:
- 执行某些动作。比如
printf()把数据打印在屏幕上 - 返回值供其他程序使用。比如
strlen()返回字符长度
为什么使用函数:
- 避免编写重复代码
- 让程序更加模块化,提高代码可读性。
1.1 创建函数
#include <stdio.h>
void starbar(void);
int main() {
starbar();
printf("GIGATHINK, INC\n101 Megabuck Plaza\nMegapolis, CA 94904\n");
starbar();
return 0;
}
void starbar(void) {
int count;
for (count = 0; count < 40; count++) {
putchar('*');
}
putchar('\n');
}
void starbar(void);是函数原型(function prototype)告诉编译器函数的类型。starbar();是函数调用(function call)表明在此处执行函数。void starbar(void) {...}是函数定义(function definition)指定函数要做什么。- 函数原型指明函数的返回值类型和函数接收的参数类型,这些信息被称为该函数的签名(signature)。对于
void starbar(void);,其签名是该函数不返回值也不接收参数。 - 函数原型可以写在其他地方,但必须在函数调用或函数定义之前。
starbar()函数中的变量count是局部变量,该变量只属于starbar()函数。可以在其他地方使用count作为变量名。
1.2 函数参数
#include <stdio.h>
#include <string.h>
void show_n_char(int, char);
void show_n_char(int n, char ch) {
for (int i = 0; i < n; i++) {
putchar(ch);
}
}
int main() {
show_n_char(40, '*');
putchar('\n');
show_n_char((40 - strlen("GIGATHINK, INC")) / 2, ' ');
printf("GIGATHINK, INC\n");
show_n_char((40 - strlen("G101 Megabuck Plaza")) / 2, ' ');
printf("101 Megabuck Plaza\n");
show_n_char((40 - strlen("Megapolis, CA 94904")) / 2, ' ');
printf("Megapolis, CA 94904\n");
show_n_char(40, '*');
return 0;
}
1.2.1 定义带形式参数的函数
函数头:void show_n_char(int n, char ch),该行告诉编译器show_n_char()使用两个参数int类型的n和char类型的ch,这两个参数是形式参数,简称形参,形参属于局部变量,这里的n和ch归show_n_char()函数私有。
⚠️注意:每个形参都必须声明类型,因此不能像声明变量一样使用同一类型的变量列表。
void func(int x, y, z) // 无效的函数头
void func(int x, int y, int z) // 有效的函数头
1.2.2 声明带形式参数函数的原型
当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型。
void show_n_char(int, char);
⚠️注意:
- 函数原型中参数名为可选。
- 函数原型中并没有实际创建变量,其中的变量类型仅代表一个对应类型的变量而已。
void func();的写法是ANSI C之前的,void func(void);才是标准的,应尽可能地使用标准的写法。
1.2.3 调用带实际参数的函数
在函数调用中,实际参数(actual argument),简称实参,实际参数提供了参数的值。
形式参数是被调函数(called function)中的变量,实际参数是主调函数(call function)赋给被调函数的具体值。
实际参数可以常量、变量或表达式。
1.3 从函数中返回值:return
函数的返回值可以把信息从被调函数传回主调函数。
// 示例函数:返回两个值中的较小者
#include <stdio.h>
int imin(int, int);
int main() {
printf("%d\n", imin(3, 4));
printf("%d\n", imin(10, 4));
return 0;
}
int imin(int a, int b) {
return a < b ? a : b;
}
关键字return后面的表达式就是函数的返回值,返回值的类型应与函数原型中的类型相同,不相同时会进行自动类型转换,如果转换失败,函数就无法成功运行。
1.4 函数类型
声明函数时必须声明函数类型,带返回值的函数类型,应与返回值类型相同,没有返回值的函数应声明为void类型。
旧版本的C编译器会假定函数的类型是int,而C99标准不再支持int类型函数这种假定设置。
2. ANSI C函数原型
在ANSI C标准之前,声明函数只需要声明函数的类型,不用声明参数。
#include <stdio.h>
int imax();
int main() {
printf("The maxinum of %d and %d is %d.\n", 3, 5, imax(3));
printf("The maxinum of %d and %d is %d.\n", 3, 5, imax(3.0f, 5.0f));
return 0;
}
int imax()
int n, m;
{
return n > m ? n : m;
}
对于PC和VAX来说,主调函数会把它的参数存在被称为“栈(stack)”的临时存储区。
在第一次调用imax()函数时,只传递了一个参数,也就是栈中只放了一个数据,那么函数会把恰好放在第一个参数旁的值作为第二个参数。
在第二次调用时,传入了两个浮点值,也就是栈中存放了128位的数据,但是形参定义的是两个int类型总长度只有64位,因此程序实际上是将第一个实参拆开,第二个实参根本没有读取到。
2.1 ANSI的解决方案
ANSI C的标准要求是使用函数原型,也就是在函数声明时还要声明变量的类型。
使用函数原型后,当实参数量与形参数量不对时,编译器会给出警告,当类型不同时,会发生自动类型转换。
2.2 无参数和未指定参数
支持ANSI C编译器会假定用户没有用函数原型来声明函数,它将不会检查参数。为了表明函数确实没有参数,应在圆括号中使用void关键字。
当函数的参数没有固定数量和类型时,可以使用...表示。
int func(int a, char ch, ...) {
……
}
⚠️注意:没有固定类型但不固定数量的表示。
3. 递归
递归(recursion) 是指函数自己调用自己的过程。自己调用自己的函数叫作递归函数。
递归函数必须包含递归出口,也就是结束递归的条件。
3.1 尾递归
尾递归(tail recursion)是指把递归调用置于函数末尾,即return之前,这种递归最简单,相当于循环。
3.2 递归和倒序计算
递归在处理倒序时非常方便。以下是一个将十进制数转换为二进制并打印的函数。
void func(int num) {
if (num < 2) {
printf("%d", num);
}
else {
func(num / 2);
printf("%d", num % 2);
}
}
⚠️注意:
- 递归过程会占用大量内存空间
- 递归函数不方便阅读和维护
4. 多文件编译
以上文的转换函数举例。
//main.c
#include <stdio.h>
#include "fun.h"
int main() {
func(63);
return 0;
}
//fun.h
void func(int);
//fun.c
#include <stdio.h>
#include "fun.h"
void func(int num) {
if (num < 2) {
printf("%d", num);
}
else {
func(num / 2);
printf("%d", num % 2);
}
}
将用到的常量和函数声明写在头文件(.h)中,将函数实现写在源文件(.c)中。
⚠️注意:主程序文件和函数实现文件都需要包含头文件(例子中是fun.h)。
5. 查找地址:&运算符
指针(pointer)是C语言最重要的概念之一,用于储存变量的地址。一元运算符&给出变量的储存地址。
#include <stdio.h>
void func(int, int);
void func(int num1, int num2) {
printf("num1: %p num2: %p\n", &num1, &num2);
}
int main() {
int num1, num2;
printf("num1: %p num2: %p\n", &num1, &num2);
func(num1, num2);
return 0;
}
// 输出结果;
// num1: 000000315A4FF594 num2: 000000315A4FF5B4
// num1: 000000315A4FF570 num2: 000000315A4FF578
同名变量其地址不同,这代表在计算机看了,这是4个独立的变量。
5.1 更改主调函数中的变量
如果我们希望在不使用返回值的情况下,被调函数可以交换主调函数中两个变量的值,该如何做?
#include <stdio.h>
void func(int*, int*);
void func(int* num1, int* num2) {
int temp = *num1;
*num1 = *num2;
*num2 = temp;
return;
}
int main() {
int num1 = 10;
int num2 = 20;
printf("num1: %d num2: %d\n", num1, num2);
func(&num1, &num2);
printf("num1: %d num2: %d\n", num1, num2);
return 0;
}
// 输出结果:
// num1: 10 num2: 20
// num1: 20 num2: 10
*叫作间接运算符(indirection operator),也叫解引用运算符(dereferencing operator),它可以找出储存在变量地址中的值。
5.2 声明指针
声明指针类型的变量需要在相应类型后加*号。
int a; // 错误,这是int类型的变量
int * b; // 正确,这是一个指针变量,它指向的变量类型是int
⚠️注意:
- 声明指针变量时
*左右两边的空格可有可无。通常在声明时添加空格,解引用时省略空格。 - 指针的值是一个地址,由无符号整数表示,但不可当做整数来处理。一些整数操作不能处理指针,反之亦然。例如不能将两个指针相乘。