在完成前面基础部分后,函数、数组、指针,一连串大的要来了。
全书共分17章,这是关于本书第9章内容的博客。本章介绍了关于函数的内容。虽然前面一些地方已经有了一些关于函数的内容,但是这一章将详细介绍函数。博客的目录和书上目录是相似的。此系列博客的代码都在Visual Studio 2022环境下编译运行。
我目前大一刚刚结束,水平有限,博客中若有错误或者总结不到位的地方也请见谅。
目录
9.1 复习函数
函数是完成特定任务的独立程序代码单元。使用函数可以省去编写重复代码的时间,使用函数可以让程序更加模块化,提高程序可读性,方便后续修改和完善。
编写函数前要考虑函数完成的任务以及和程序整体的关系。
9.1.1 创建并使用简单函数
下面是一个示例,函数starbar()输出40个*。这类不需要传参也不需要返回值的函数是最简单的函数。
#include<stdio.h>
#define NAME "GIGATHINK, INC"
#define ADDRESS "101 Megabuck Plaza"
#define PLACE "Megapolis, CA 94904"
#define WIDTH 40
void starbar(void);
int main(void)
{
starbar();
printf("%s\n", NAME);
printf("%s\n", ADDRESS);
printf("%s\n", PLACE);
starbar();
return 0;
}
void starbar(void)
{
int count;
for (count = 1; count <= WIDTH; count++)
putchar('*');
putchar('\n');
}
一般程序中自定义函数的标识符会出现三次:函数原型,函数调用,函数定义。函数原型告诉编译器函数的类型。函数调用表明此处开始执行函数。函数定义指定了函数要做什么。
函数也有多种类型,任何程序在使用函数之前都要声明函数的类型。
函数原型的格式是:
返回值类型 函数名称(形参声明列表);
圆括号表明这是一个函数名。void类型表明函数没有返回值。圆括号中的void表示函数不带参数。分号表示这是在声明函数,不是定义函数。函数原型告诉编译器函数定义在别处。
函数原型指明了函数的返回值类型和函数接受的参数类型,这些信息称为函数的签名。
函数原型可以放在主函数的前面,也可以放在函数内函数调用的前面。
函数调用的格式是:
函数名称(实参)
不带参数的函数调用时花括号内为空。
程序执行到函数调用语句时会找到函数的定义并执行内容。执行完函数定义的内容后会返回主调函数并继续执行下一行。
函数定义与main()函数规则相同。函数头包括函数类型、函数名和圆括号。圆括号内是形参。接着是花括号,花括号内是具体语句。函数头末尾没有分号,告诉编译器这是定义函数。
不同函数可以放在同一个源文件中,也可以放在不同源文件中。放在同一个源文件方便编译,使用多个源文件方便在不同程序中使用函数。如果把函数放在单独的源文件中,要注意有必要的#include指令和#define指令。
函数中的变量是局部变量,只属于函数本身。不同函数中的同名变量不会冲突。
9.1.2 定义带形式参数的函数
形式参数也是局部变量,属于该函数私有。在其他函数中的同名变量不会引起冲突。每次调用函数都会给形式参数进行赋值。
要在每个形式参数名前都声明类型。
9.1.3 声明带形式参数函数的原型
在声明带形式参数函数时,用逗号分隔的变量表指明参数的数量和类型,可以省略变量名。
函数原型只是声明了形式参数,没有实际创建变量。
9.1.4 调用带实际参数的函数
形式参数是被调函数中的变量,实际参数是主调函数赋给被调函数的具体值。实际参数可以是常量,变量,表达式。实际参数一定会被求值,然后被赋值给被调函数相应的形式参数。无论被调函数对形参做什么操作,都不会影响主调函数中的原始数据。
实际参数是出现在函数调用圆括号中的表达式,形式参数是函数定义的函数头中声明的变量。调用函数时创建了声明为形式参数的变量并初始化为实际参数的求值结果。
9.1.5 使用return从函数中返回值
被设计用于测试函数的程序有时被称为驱动程序。
关键字return后面的表达式的值就是函数的返回值。
如果函数返回值的类型和声明的类型不匹配,实际得到的返回值相当于把函数中指定的返回值赋给与函数类型相同的变量的值。
return语句终止函数并将控制返回主调函数的下一条语句。函数中可以使用多个return语句。
return;会终止函数,把控制返回主调函数。这条语句没有返回值,适用于void类型语句。
9.1.6 函数类型
声明函数时必须声明函数的类型,带返回值的函数类型就是返回值类型,没有返回值的函数是void类型。类型声明是函数定义的一部分。
要正确地使用函数,程序在第一次使用函数之前就必须知道函数的类型。通常的做法是提前声明函数,把函数的信息告知编译器。
在ANSI标准库中,函数被分成多个系列,每系列都有自己的头文件。
9.2 ANSI C函数原型
ANSI C之前的函数声明不需要声明参数,会造成一些问题。ANSI C标准要求函数声明时声明变量类型,即使用函数原型声明函数的返回类型、参数的数量和每个参数的类型。
函数声明可以不带形参的名字,变量名也不必与函数定义的形参名一致。
通过函数原型,编译器可以检查函数调用是否与函数原型匹配。如果实际参数和形式参数类型不匹配,编译器会把实际参数的类型转换成形式参数的类型。
9.2.1 无参数和未指定参数
为了表明函数没有参数,应在圆括号里加上void。
9.2.2 函数原型的优点
函数原型便于编译器捕获使用函数时的许多错误。使用函数原型是为了让编译器提前知道怎么使用函数。
对于很小的函数,可以直接把函数定义放在前面。
9.3 递归
C语言允许函数调用自己,这种调用称为递归。递归有时难以理解,有时又很方便。递归的函数需要有结束递归的条件部分,不然会无限递归。
可以递归的地方通常可以用循环。
9.3.1 递归的基本原理
递归中每级函数调用都有自己的变量。尽管不同级中变量名字相同,但是是不同的变量。
每次递归都会返回一次。程序必须按顺序逐级返回,不能直接跳级返回到main()函数。
递归函数中位于递归调用之前的语句按被调函数的顺序执行。
递归函数中位于递归调用之后的语句按与被调函数相反的顺序执行。
虽然每级递归都有自己的变量,但是没有拷贝函数的代码。程序按顺序执行函数的代码,递归调用相当于又从头开始执行函数的代码。
递归函数必须包含能让递归调用停止的语句。
9.3.2 尾递归
将递归调用置于函数末尾的递归称为尾递归,尾递归是最简单的递归形式,类似于循环。
下面程序用循环和递归计算阶乘。
#include<stdio.h>
long fact(int n);
long rfact(int n);
int main(void)
{
int num;
printf("This program calculates factorials.\n");
while (scanf("%d", &num) == 1)
{
if (num < 0)
printf("No negative numbers,please.\n");
else if (num > 12)
printf("Please input under 13.\n");
else
{
printf("loop: %d factorial = %ld\n", num, fact(num));
printf("recusion:%d factorial = %ld\n", num, rfact(num));
}
printf("Enter a value in the range 0-12 (q to quit):\n");
}
printf("Bye.\n");
return 0;
}
long fact(int n)
{
long ans;
for (ans = 1; n > 1; n--)
ans *= n;
return ans;
}
long rfact(int n)
{
long ans;
if (n > 0)
ans = n * rfact(n - 1);
else
ans = 1;
return ans;
}
一般情况下递归和循环使用循环更好。每次递归都会创建变量,因此递归使用内存较多,并且调用函数需要时间,这导致递归的效率低。
但是某些情况下不能用简单的循环代替递归。
9.3.3 递归和倒序计算
递归在处理倒序时非常方便,这种情况下一般比循环简单。
下面程序展示了用递归的方法用二进制形式输出整数。
#include<stdio.h>
void to_binary(unsigned long n);
int main(void)
{
unsigned long number;
printf("Enter an integar (q to quit):\n");
while (scanf("%lu", &number) == 1)
{
printf("Binary equivalent: ");
to_binary(number);
putchar('\n');
printf("Enter an integar (q to quit):\n");
}
printf("Done.\n");
return 0;
}
void to_binary(unsigned long n)
{
int r;
r = n % 2;
if (n >= 2)
to_binary(n / 2);
putchar(r == 0 ? '0' : '1');
return;
}
9.3.4 递归的优缺点
递归既有优点又有缺点。递归为某些问题提供了最简单的解决方案。但是有些递归会快速消耗内存,并且递归的效率低,递归不方便阅读和维护。
程序中的每个函数都可以调用其他函数,也可以被其他函数调用。main()函数一般是最开始执行的函数,main()函数也可以被自己或其他函数递归调用,不过很少这样做。
9.4 编译多源代码文件的程序
9.4.1 Windows的IDE编译器
Windows的编译器Microsoft Visual Studio是面向项目的。项目描述特定程序使用的资源。VS要创建项目来运行程序,多个源文件需要放到一个项目中才能一起编译运行。
9.4.2 使用头文件
在多源代码文件程序中,可以把函数原型放在头文件中(C标准库就是这样做的,这样不必使用时在源代码中写函数原型),也可以把#define定义的明示常量放在头文件中。把函数原型和定义的明示常量放在头文件中是一个良好的编程习惯。
在VS中,使用#include指令可以包含自创的头文件,在include后加双引号,双引号里面是头文件名称。
9.5 查找地址:&运算符
指针存储变量的地址。
一元运算符&给出变量的存储地址,&后接一个变量名得到这个变量的地址。地址是变量在内存中的位置。
用%p表示地址。C语言不同函数的同名变量的地址不相同,每个函数都有自己的变量。这样可以防止原始变量被意外修改。
9.6 更改主调函数中的变量
有时候需要在一个函数中更改其他函数的变量,这种情况下需要用指针。
return语句只能返回一个值。
9.7 指针简介
指针是一个值为内存地址的变量,指针变量的值是地址。
可以用&对一个变量取地址后赋给一个指针变量。此时称指针变量指向前者。指针也可以修改值以指向其他变量。
9.7.1 间接运算符:*
间接运算符*用于取出地址中的值,该运算符也称为解引用运算符。该运算符和乘法运算符*的功能不同。
*后跟一个指针变量或地址可以给出这个地址的值。
9.7.2 声明指针
声明指针变量时必须指定指针指向变量的类型,因为不同类型占用不同的存储空间,一些指针操作需要知道操作对象的大小。另外程序要知道存储在指定地址上的数据类型。
声明指针的格式是:
所指对象类型 * 变量名称。
*表明声明的变量是指针。*和指针名的空格可有可无。
地址是一个无符号整数。但是整数的操作和指针的操作不完全一样。指针实际是一个新类型。
9.7.3 使用指针在函数间通信
下面程序用指针交换两个变量的值。
#include<stdio.h>
void interchange(int* u, int* v);
int main(void)
{
int x = 5, y = 10;
printf("Originally x = %d and %y = %d.\n", x, y);
interchange(&x, &y);
printf("Now x = %d and y = %d.\n", x, y);
return 0;
}
void interchange(int* u, int* v)
{
int temp;
temp = *u;
*u = *v;
*v = temp;
}
函数传参不仅可以传递变量的值,还可以传递变量的地址(此时对应形参需要是指针)。使用指针可以改变主调函数中变量的值。
scanf()函数后面的参数都是指针。
变量有两个属性:地址和值。普通变量把值作为基本量,把地址作为派生量。(指针变量本身也有地址)
通过指针可以改变指向变量的值。