C编程
在C语言中,所有可执行的代码都必须位于一个函数内。需要注意的是,其他编程语言可能会区分“函数”、“子程序”、“子程序块”、“过程”或“方法”——但在C语言中,这些都统称为函数。函数是任何高级编程语言的基本特性,它们使得我们能够通过将复杂的任务拆解成更小、更易管理的代码块来处理大型复杂的任务。
从低层次来看,函数不过是计算机内存中的一个内存地址,函数的指令存储在这个地址上。在源代码中,这个内存地址通常会赋予一个描述性的名称,程序员可以使用这个名称来调用函数并执行从该函数起始地址开始的指令。与函数相关的指令通常被称为代码块。当函数的指令执行完毕后,函数可以返回一个值,然后程序会从最初调用该函数后的下一条指令继续执行。如果这对你来说还不太理解,别担心。理解计算机在底层发生的事情一开始可能会让人困惑,但随着你C语言编程技能的提高,最终会变得非常直观。
目前,了解函数及其相关代码块在程序的单次执行过程中,通常会被多次执行(被调用)就足够了。
作为一个基本例子,假设你正在编写一个计算给定(x,y)点到x轴和y轴距离的程序,你需要计算x和y这两个整数的绝对值。我们可以像下面这样编写代码(假设我们没有任何库中定义的绝对值函数):
#include <stdio.h>
/* 这个函数计算一个整数的绝对值 */
int abs(int x)
{
if (x >= 0) return x;
else return -x;
}
/* 该程序调用上面定义的abs()函数两次 */
int main()
{
int x, y;
printf("请输入二维平面上某点的坐标,例如P = (x,y)。首先输入x=");
scanf("%d", &x);
printf("然后输入y=");
scanf("%d", &y);
printf("点P到x轴的距离是 %d。\n 它到y轴的距离是 %d。\n", abs(y), abs(x));
return 0;
}
下一个例子展示了如何将函数用作过程调用。这是一个简单的程序,它询问学生三门不同课程的成绩,并告诉他们是否通过了某门课程。在这个例子中,我们创建了一个函数,叫做 check(),可以根据需要调用多次。这个函数使我们不需要为每个学生选修的课程写相同的指令。
#include<stdio.h>
/* 'check' 函数在这里定义 */
void check(int x)
{
if (x < 60)
printf("抱歉!你需要重新学习这门课程。\n");
else
printf("祝你假期愉快!你已经通过了。\n");
}
/* 程序从这里开始,在main()函数中调用check()函数三次 */
int main()
{
int a, b, c;
printf("请输入数学成绩(整数)。\n");
scanf("%d", &a);
check(a);
printf("请输入科学成绩(整数)。\n");
scanf("%d", &b);
check(b);
printf("请输入编程成绩(整数)。\n");
scanf("%d", &c);
check(c);
/* 这个程序应该替换成更有意义的内容。 */
return 0;
}
请注意,在上面的程序中,check 函数没有返回值。它只是执行了一个过程。
这正是函数的作用。
关于函数的更多内容
将函数视为工厂中的一台机器是很有用的概念。在机器的输入端,你将“原料”或输入数据传递给机器处理。然后,机器开始工作,并将完成的产品,即“返回值”,传递到输出端,你可以收集并用于其他目的。
在C语言中,你必须明确告诉机器它需要处理什么原料,并且告诉它你希望机器返回什么样的最终产品。如果你提供的原料与机器所期望的不同,或者你尝试返回与机器所要求的不同产品,C编译器将会报错。
需要注意的是,函数并不一定需要接受任何输入,它也不一定需要返回任何东西。如果我们修改上述示例,让程序在 check 函数内部要求用户输入成绩,就不需要将成绩值传递给函数。而且注意 check 函数没有返回值,它只是向屏幕输出了一条信息。
你应该熟悉一些与函数相关的基本术语:
- 一个函数(我们称之为f)如果使用另一个函数(称为g),我们说f调用了g。例如,f调用g来打印十个数字的平方。f被称为调用函数(caller function),而g被称为被调用函数(callee)。
- 我们传递给函数的输入称为该函数的参数(arguments)。当我们声明函数时,我们会描述哪些类型的参数可以传递给函数。我们会在函数名后面的圆括号内向编译器描述这些参数。
- 一个函数g,如果给了某种答案返回给f,我们说g返回了该答案或值。例如,g返回它的参数之和。
在C语言中编写函数
通过例子学习总是很有帮助的。让我们写一个函数,返回一个数字的平方。
int square(int x)
{
int square_of_x;
square_of_x = x * x;
return square_of_x;
}
要理解如何编写这样的函数,可能需要看看这个函数整体做了什么。它接受一个整数x,将其平方,并将结果存储在变量 square_of_x 中。然后,返回该值。
函数声明开始时的第一个 int 是函数返回的数据类型。在这种情况下,当我们对一个整数求平方时,得到的仍然是一个整数,因此我们将返回该整数,所以返回类型为 int。
接下来是函数的名称。为你编写的函数使用有意义且描述性的名称是一个好习惯。给函数命名时,可以根据它的功能来命名。在这个例子中,我们把函数命名为“square”,因为它的作用就是计算平方。
然后是函数的第一个也是唯一的参数 int x,它在函数中作为输入。
大括号中的部分是函数的实际内容。它声明了一个名为 square_of_x 的整数变量,用于存储x的平方。注意,变量 square_of_x 只能在这个函数内部使用,外部无法访问。我们稍后会学习到这一点,并看到这个特性非常有用。
接着我们将 x * x(即x的平方)赋值给变量 square_of_x,这正是函数的主要作用。接下来是返回语句。我们希望返回 square_of_x 的值,因此我们必须写一个 return 语句,返回 square_of_x 的内容。
最后,闭合大括号,函数声明完成。
以更简洁的方式编写,这段代码与上述功能完全相同:
int square(int x)
{
return x * x;
}
请注意,这看起来应该很熟悉——你已经在编写函数了,实际上 main 就是一个始终编写的函数。
一般规则
一般来说,如果我们想声明一个函数,我们写:
type name(type1 arg1, type2 arg2, ...)
{
/* 代码 */
}
我们之前提到过,函数可以不接受任何参数,或者不返回任何值,或者两者都不返回。如果我们想让函数不返回任何值,我们使用C语言的 void 关键字。void 基本上意味着“什么也没有”——所以如果我们想编写一个不返回值的函数,例如,可以这样写:
void sayhello(int number_of_times)
{
int i;
for(i = 1; i <= number_of_times; i++) {
printf("Hello!\n");
}
}
请注意,以上函数没有 return 语句。由于没有返回值,我们将返回类型声明为 void。实际上,在过程中可以使用 return 关键字在过程结束之前提前返回,但不能像函数那样返回一个值。
如果我们希望编写一个不接受任何参数的函数,应该怎么做呢?如果我们要这样做,可以像下面这样写:
float calculate_number(void)
{
float to_return = 1;
int i;
for(i = 0; i < 100; i++) {
to_return += 1;
to_return = 1 / to_return;
}
return to_return;
}
请注意,这个函数没有接受任何输入,而只是返回一个由该函数计算出的数字。
当然,你也可以将 void 作为返回类型和参数类型一起使用,从而得到一个有效的函数。
递归
下面是一个简单的函数,它会执行一个无限循环。它打印一行并调用自己,然后再次打印一行并再次调用自己,这样一直继续下去,直到栈溢出并且程序崩溃。一个函数调用自身被称为递归,通常你会设置一个条件来停止递归,通常是在经过有限的步骤后停止。
// 切勿运行此代码!
void infinite_recursion()
{
printf("Infinite loop!\n");
infinite_recursion();
}
可以像这样进行简单的检查。注意 ++depth 被用在传递给函数之前,因此递增会在值被传递之前进行。或者,你也可以在递归调用前的单独一行中递增。假设你写 print_me(3,0);,函数将打印出 “Recursion” 三次。
void print_me(int j, int depth)
{
if(depth < j) {
printf("Recursion! depth = %d j = %d\n", depth, j); // j 保持其值
print_me(j, ++depth);
}
}
递归通常用于以下工作,例如:目录树扫描、寻找链表的末尾、解析数据库中的树结构、因数分解(以及寻找素数)等。
静态函数
如果一个函数只在声明它的文件内部被调用,那么最好将其声明为静态函数。当一个函数被声明为静态时,编译器会知道以一种方式编译目标文件,防止该函数在其他文件中的代码被调用。示例:
static int compare(int a, int b)
{
return (a + 4 < b) ? a : b;
}
使用C函数
我们现在可以编写函数了,但如何使用它们呢?当我们编写 main 函数时,我们将其他函数写在包含 main 的大括号外。
当我们想要使用某个函数,例如使用我们上面提到的 calculate_number 函数时,我们可以写:
float f;
f = calculate_number();
如果一个函数接受参数,我们可以写:
int square_of_10;
square_of_10 = square(10);
如果一个函数没有返回值,我们可以直接这样写:
say_hello();
因为我们不需要一个变量来接收它的返回值。
C标准库中的函数
虽然C语言本身不包含函数,但通常会与C标准库一起链接。要使用这个库,你需要在C文件的顶部添加一个 #include 指令,它可以是C89/C90中的以下之一:
<assert.h><ctype.h><errno.h><float.h><limits.h><locale.h><math.h><setjmp.h><signal.h><stdarg.h><stddef.h><stdio.h><stdlib.h><string.h><time.h>
可用的函数有:
<assert.h> <limits.h> <signal.h> <stdlib.h>
assert(int)(仅常量)int raise(int sig)void* signal(int sig, void (*func)(int))atof(char*),atoi(char*),atol(char*)strtod(char *str, char **endptr),strtol(char *str, char **endptr),strtoul(char *str, char **endptr)rand(),srand()malloc(size_t),calloc(size_t elements, size_t elementSize),realloc(void*, int)free(void*)exit(int),abort()atexit(void (*func)())getenvsystemqsort(void *, size_t number, size_t size, int (*sortfunc)(void*, void*))abs,labsdiv,ldiv
<ctype.h> <locale.h> <stdarg.h> <string.h>
isalnum,isalpha,isblankiscntrl,isdigit,isgraphislower,isprint,ispunctisspace,isupper,isxdigittolower,toupperstruct lconv* localeconv(void)char* setlocale(int, const char*)va_start(va_list, ap)va_arg(ap, (type))va_end(ap)va_copy(va_list, va_list)memcpy,memmovememchr,memcmp,memsetstrcat,strncat,strchr,strrchrstrcmp,strncmp,strccollstrcpy,strncpystrerrorstrlenstrspn,strcspnstrpbrkstrstrstrtokstrxfrm
errno.h math.h stddef.h time.h
(errno)sin,cos,tanasin,acos,atan,atan2sinh,cosh,tanhceilexpfabsfloorfmodfrexpldexplog,log10modfpowsqrtoffsetof宏asctime(struct tm* tmptr)clock_t clock()char* ctime(const time_t* timer)double difftime(time_t timer2, time_t timer1)struct tm* gmtime(const time_t* timer)struct tm* gmtime_r(const time_t* timer, struct tm* result)struct tm* localtime(const time_t* timer)time_t mktime(struct tm* ptm)time_t time(time_t* timer)char* strptime(const char* buf, const char* format, struct tm* tptr)time_t timegm(struct tm *brokentime)
<float.h> <setjmp.h> <stdio.h>
- (常量)
int setjmp(jmp_buf env)void longjmp(jmp_buf env, int value)fclosefopen,freopenremoverenamerewindtmpfileclearerrfeof,ferrorfflushfgetpos,fsetposfgetc,fputcfgets,fputsftell,fseekfread,fwritegetc,putcgetchar,putchar,fputchargets,putsprintf,vprintffprintf,vfprintfsprintf,snprintf,vsprintf,vsnprintfperrorscanf,vscanffscanf,vfscanfsscanf,vsscanfsetbuf,setvbuftmpnamungetc
可变长度参数列表
具有可变长度参数列表的函数是能够接受数量不固定参数的函数。C标准库中的一个例子是 printf 函数,它可以根据程序员的需求接受任意数量的参数。
C程序员很少需要编写具有可变长度参数的新函数。如果他们想传递多个参数,通常会定义一个结构来保存这些参数——可能是链表或数组——然后将这些数据作为参数传递给函数。
但是,偶尔你可能需要编写一个支持可变长度参数列表的新函数。为了创建一个可以接受可变长度参数列表的函数,首先必须包含标准库头文件 stdarg.h。接着,像通常一样声明函数。然后,在最后一个参数后添加省略号("..."),这告诉编译器接下来的参数是一个可变参数列表。例如,以下函数声明是返回一组数字平均值的函数:
float average(int n_args, ...);
注意,由于可变长度参数的工作方式,我们必须以某种方式在参数中指定可变参数部分的元素个数。在这里,average 函数通过名为 n_args 的参数来实现。在 printf 函数中,这个数字是通过你在第一个字符串中的格式代码来指定的。
现在,函数已经声明为使用可变长度参数,我们需要编写实际处理函数内部工作的代码。为了访问存储在可变长度参数列表中的数字,我们首先需要声明一个变量来表示该列表本身:
va_list myList;
va_list 是在 stdarg.h 头文件中声明的类型,基本上允许你跟踪参数列表。然而,要开始使用 myList,我们首先必须为其赋值。毕竟,单独声明它并不会做任何事情。为此,我们必须调用 va_start,这是在 stdarg.h 中定义的一个宏。在 va_start 的参数中,必须提供你计划使用的 va_list 变量以及函数声明中省略号前的最后一个变量名:
#include <stdarg.h>
float average(int n_args, ...)
{
va_list myList;
va_start(myList, n_args);
va_end(myList);
}
现在 myList 已经为使用做好了准备,我们可以开始访问存储在其中的变量。要做到这一点,使用 va_arg 宏,它将弹出列表中的下一个参数。在 va_arg 的参数中,提供你正在使用的 va_list 变量,以及你要访问的变量应该是的基本数据类型(例如 int、char):
#include <stdarg.h>
float average(int n_args, ...)
{
va_list myList;
va_start(myList, n_args);
int myNumber = va_arg(myList, int);
va_end(myList);
}
通过从可变长度参数列表中弹出 n_args 个整数,我们可以计算这些数字的平均值:
#include <stdarg.h>
float average(int n_args, ...)
{
va_list myList;
va_start(myList, n_args);
int numbersAdded = 0;
int sum = 0;
while (numbersAdded < n_args) {
int number = va_arg(myList, int); // 从列表中获取下一个数字
sum += number;
numbersAdded += 1;
}
va_end(myList);
float avg = (float)(sum) / (float)(numbersAdded); // 计算平均值
return avg;
}
通过调用 average(2, 10, 20),我们得到 10 和 20 的平均值,即 15。