当前位置: 首页 > news >正文

C程序的核心基石:深入理解与精通函数

C程序的核心基石:深入理解与精通函数

在C语言中,函数(Function)无疑是其最核心、最基本的构件块。它们是为完成特定任务而设计的独立程序代码单元。掌握如何创建和运用自定义函数,是C语言能力从入门到精通的分水岭。本章将系统地引领我们探索函数的完整生命周期:从定义、声明到调用,从参数传递、返回值机制到函数原型的必要性,并进一步拓展至递归与指针这两个C语言中至关重要的高级应用。

函数的基本构成与工作流程

引入函数的核心动机在于代码复用程序模块化。首先,函数能让我们避免重复编写相同的代码,极大地提升了开发效率和代码的可维护性。其次,通过将庞大的程序分解为一个个目标明确的函数,程序的逻辑结构会变得异常清晰,如同精心设计的蓝图,便于阅读、调试和未来的功能扩展。

想象一个数据处理程序,其核心任务可以分解为四个步骤:读取数据、排序、计算平均值、绘制图表。其主函数 main() 的结构可以设计得如同一位运筹帷幄的指挥官,它不亲自执行具体任务,而是调度各个职能函数来协同工作:

#include <stdio.h>
#define SIZE 50// 函数原型声明:预先告知编译器这些函数的存在和接口
void readlist(float list[], int size);
void sort(float list[], int size);
void average(float list[], int size);
void bargraph(float list[], int size);int main(void)
{float list[SIZE];// 依次调用各个函数,完成程序流程readlist(list, SIZE);    sort(list, SIZE);        average(list, SIZE);     bargraph(list, SIZE);    return 0;
}

这样的结构使得 main() 函数的核心逻辑一目了然。每个函数名本身就构成了自解释的文档,清晰地表达了其功能。

要完整地使用一个C函数,必须理解其三个关键环节:函数原型(function prototype)函数调用(function call)函数定义(function definition)

让我们通过一个在屏幕上打印一行星号的简单实例,来具体剖析这三个概念。

程序清单 9.1 lethead1.c

/* lethead1.c */
#include <stdio.h>
#define NAME "GIGATHINK, INC."
#define ADDRESS "101 Megabuck Plaza"
#define PLACE "Megapolis, CA 94904"
#define WIDTH 40void starbar(void); /* 1. 函数原型 */int main(void)
{starbar(); /* 2. 函数调用 */printf("%s\n", NAME);printf("%s\n", ADDRESS);printf("%s\n", PLACE);starbar(); /* 2. 再次调用函数 */return 0;
}void starbar(void) /* 3. 函数定义 */
{int count; // 局部变量// 循环打印 40 个星号for (count = 1; count <= WIDTH; count++)putchar('*');putchar('\n'); // 打印换行符
}

1. 函数原型 void starbar(void);
这行代码是向编译器的“预告”。它精确地描述了 starbar 函数的接口签名(signature):第一个 void 关键字表明该函数执行完毕后不返回任何值;括号内的 void 表明该函数不接受任何输入参数。分号结尾表明这仅仅是一个声明。编译器读取原型后,便知道了 starbar 的正确使用方式。当后续代码中出现对 starbar 的调用时,编译器就能检查该调用是否符合原型规定(例如,是否错误地传递了参数)。

2. 函数调用 starbar();
这是执行函数的指令。当程序运行到这一行时,CPU的控制权会从当前所在的 main() 函数,跳转到 starbar() 函数的定义处,开始执行其内部的代码。当 starbar() 函数的所有代码执行完毕后,控制权会返回到 main() 函数中调用点的下一条语句,继续往下执行。

3. 函数定义 void starbar(void) { ... }
这部分是函数的实体,详细规定了函数需要执行的具体操作。它由两部分组成:

  • 函数头 (void starbar(void)) :与原型非常相似,但没有末尾的分号。它标志着一个函数定义的开始。
  • 函数体 ({...}) :由一对花括号包围的代码块。函数的所有逻辑都在这里实现。

starbar 函数体内定义的变量 count 是一个局部变量(local variable)。它的生命周期和作用域被严格限制在 starbar 函数内部。这意味着,即使在 main() 或其他函数中也定义了一个名为 count 的变量,它们之间也是完全独立、互不干扰的。

函数间的双向通信:参数与返回值

1. 传递信息给函数:深入理解参数

为了让函数更具通用性和灵活性,我们需要一种机制向它传递信息。这就是**参数(parameter/argument)**的职责。**形式参数(formal parameter)是在函数定义时声明的变量,如同函数的“输入端口”;而实际参数(actual argument)**是在函数调用时提供的具体值或表达式,是输送到这些端口的“数据”。

现在,我们将 starbar 函数升级为一个更通用的 show_n_char() 函数,它可以打印指定数量的任意字符。

程序清单 9.2 lethead2.c (部分)

#include <stdio.h>
#include <string.h> 
#define NAME "GIGATHINK, INC."
#define WIDTH 40
#define SPACE ' '// 原型现在指明了参数的类型和数量
void show_n_char(char ch, int num); int main(void)
{int spaces;// 第一次调用:实参是符号常量 '*' 和 WIDTHshow_n_char('*', WIDTH); putchar('\n');// 计算居中所需的空格数spaces = (WIDTH - strlen(NAME)) / 2; // 第二次调用:实参是字符常量 SPACE 和整型变量 spacesshow_n_char(SPACE, spaces);printf("%s\n", NAME);// 第三次调用:实参是一个复杂的表达式show_n_char(SPACE, (WIDTH - strlen(ADDRESS)) / 2);printf("%s\n", ADDRESS);// ...return 0;
}// 函数定义
void show_n_char(char ch, int num) // ch 和 num 是形式参数
{int count;for (count = 1; count <= num; count++)putchar(ch);
}

代码解读:
show_n_char 的函数头 void show_n_char(char ch, int num) 中,chnum 是形式参数,它们是 show_n_char 函数私有的局部变量。

main 函数执行 show_n_char(SPACE, spaces); 调用时,C语言执行的是**“按值传递”(pass-by-value)**。这意味着:

  1. 计算实际参数的值:SPACE 的值是字符 ' 'spaces 的值是之前计算出的一个整数。
  2. 创建形式参数的存储空间:为 chnum 分配内存。
  3. 拷贝实际参数的值到形式参数:将 ' ' 复制到 ch,将 spaces 的值复制到 num

关键在于“拷贝”。show_n_char 函数内部操作的是这些值的副本。因此,即使在函数内部修改了 chnum 的值,也绝不会影响到 main 函数中的原始数据。实际参数可以是常量、变量,甚至是像 (WIDTH - strlen(ADDRESS)) / 2 这样的复杂表达式,函数只关心最终计算出的结果值。

2. 从函数获取信息:驾驭返回值

函数不仅可以接收数据,还能将处理结果反馈给调用方,这需要通过 return 关键字和函数的返回类型来实现。

以下程序定义了一个 imin() 函数,它接收两个整数并返回其中较小的一个。

程序清单 9.3 lesser.c

/* lesser.c -- 找出两个整数中较小的一个 */
#include <stdio.h>// 原型声明了函数名、参数类型,以及最重要的——返回类型为 int
int imin(int, int); int main(void)
{int evil1, evil2;printf("Enter a pair of integers (q to quit):\n");// scanf 返回成功读取的项目数,这里用于循环控制while (scanf("%d %d", &evil1, &evil2) == 2){// 函数调用 imin(evil1, evil2) 本身就是一个表达式,// 其值就是函数的返回值,可以直接用在 printf 中printf("The lesser of %d and %d is %d.\n",evil1, evil2, imin(evil1, evil2));printf("Enter a pair of integers (q to quit):\n");}printf("Bye.\n");return 0;
}// 函数定义,返回类型必须与原型一致
int imin(int n, int m)
{int min; // 局部变量,用于存储较小值if (n < m)min = n;elsemin = m;// return 语句将 min 变量的值作为结果返回给主调函数return min; 
}

代码解读:
imin 函数的返回类型被声明为 intreturn min; 语句执行两个核心操作:首先,它立即终止 imin 函数的执行;其次,它将变量 min 当前的值作为函数的最终结果“弹出”,返回给 main 函数中调用它的地方。这个返回值可以被赋给一个变量(如 int lesser = imin(evil1, evil2);),或者像例子中那样,直接作为另一个函数(printf)的参数使用。

imin 函数的实现可以更加精炼,直接返回一个表达式的值:

/* 返回最小值的函数, 第2个版本 */
int imin(int n, int m)
{// 条件运算符(三元运算符)直接计算出 n 和 m 中的较小者并返回return (n < m) ? n : m; 
}

这个版本在功能上完全等价,但代码更紧凑,也可能更高效。

ANSI C 函数原型的绝对重要性

在ANSI C标准统一之前,旧式的函数声明允许省略参数信息,如 int imax();。这带来了巨大的安全隐患,因为编译器无法检查函数调用时参数的数量和类型是否正确,极易导致难以追踪的运行时错误。

程序清单 9.4 misuse.c (旧式声明的危害)

#include <stdio.h>
int imax(); /* 旧式函数声明,信息不完整 */int main(void)
{// 错误1: 参数数量不足,只传了1个,但函数需要2个printf("The maximum of %d and %d is %d.\n", 3, 5, imax(3));// 错误2: 参数类型不匹配,传了浮点数,但函数需要整数printf("The maximum of %d and %d is %d.\n", 3, 5, imax(3.0, 5.0));return 0;
}// 旧式函数定义
int imax(n, m)int n, m;
{return (n > m ? n : m);
}

运行此程序会产生无法预测的、错误的垃圾结果。其深层原因是,主调函数和被调函数在内存栈(stack)上传递和解析数据的方式不一致,导致数据错位和误读。

ANSI C 函数原型则彻底解决了这个问题。它要求在声明时提供完整的函数签名。

程序清单 9.5 proto.c (使用原型)

#include <stdio.h>
int imax(int, int); /* 完整的 ANSI C 函数原型 */int main(void)
{// 编译器在编译此行时,会根据原型发现 imax(3) 参数数量不足,直接报错printf("The maximum of %d and %d is %d.\n",3, 5, imax(3)); // 编译器发现 3.0 和 5.0 是 double 类型,与原型的 int 不符// 它会自动将浮点数转换为整数(截断小数部分),再传递给函数printf("The maximum of %d and %d is %d.\n",3, 5, imax(3.0, 5.0)); return 0;
}
// ... imax 定义不变 ...

代码解读:
有了 int imax(int, int); 这个精确的原型,编译器就获得了审查函数调用的“尚方宝剑”:

  • 对于 imax(3),编译器发现需要2个 int 参数却只给了1个,会立即在编译阶段报错,阻止有问题的代码生成。
  • 对于 imax(3.0, 5.0),编译器识别到类型不匹配,但由于原型明确了目标类型是 int,它会执行安全的隐式类型转换,将 3.0 转为 35.0 转为 5,从而保证了函数调用的正确性(尽管可能会产生数据丢失的警告)。

函数原型是C语言从“草莽时代”走向“规范化”的重要标志,它将大量潜在的运行时逻辑错误提前暴露在编译阶段,极大地增强了代码的健壮性。

递归:函数对自身的调用

C语言支持函数调用自身的行为,即递归(recursion)。递归能为某些问题提供极为简洁和符合直觉的解决方案,尤其是那些可以被分解为与自身结构相同的子问题。但使用递归必须小心设计一个终止条件(base case),否则将陷入无限循环,最终导致栈溢出而程序崩溃。

程序清单 9.6 recur.c

/* recur.c -- 递归演示 */
#include <stdio.h>
void up_and_down(int);int main(void)
{up_and_down(1);return 0;
}void up_and_down(int n)
{// 递归“递进”阶段执行的语句printf("Level %d: n location %p\n", n, &n); if (n < 4) // 递归条件{up_and_down(n + 1); // 递归调用,进入下一层}// 递归“回归”阶段执行的语句printf("LEVEL %d: n location %p\n", n, &n); 
}

代码解读:
这个程序的执行流程完美地诠释了递归的“递进”与“回归”两个阶段:

  1. 递进 (Diving In): main 调用 up_and_down(1),打印 “Level 1”。由于 1 < 4,它调用 up_and_down(2)up_and_down(2) 打印 “Level 2”,再调用 up_and_down(3)… 这个过程持续到 up_and_down(4)被调用。在 up_and_down(4) 中,n 为4,n < 4 条件为假,递归调用链在此中断。
  2. 回归 (Backing Out): 既然 up_and_down(4) 不再进行新的调用,它会执行完自己余下的代码,打印 “LEVEL 4”,然后函数结束,控制权返回给它的调用者——up_and_down(3)。此时,up_and_down(3) 从它暂停的地方(即递归调用之后)继续执行,打印 “LEVEL 3”,然后返回给 up_and_down(2)。这个回归过程像解开层层嵌套的盒子,依次打印 “LEVEL 2” 和 “LEVEL 1”,最终返回到 main 函数。

递归的核心原理:

  • 独立的变量栈: 每次函数调用(无论是普通调用还是递归调用),系统都会在内存的栈区为该次调用的所有局部变量(包括形式参数)分配一块独立的内存空间。因此,up_and_down(1)nup_and_down(2)n 是两个完全不同的变量,地址也不同(如程序输出所示)。
  • LIFO执行顺序: 递归调用之后的语句,其执行顺序与函数被调用的顺序正好相反,遵循“后进先出”(Last-In, First-Out)的原则。这使得递归特别适合处理需要颠倒顺序的问题。

一个绝佳的例子就是将十进制数转换为二进制。转换算法是反复对原数除以2取余,得到的余数序列是二进制表示的逆序。递归的“回归”阶段恰好能自然地实现这个逆序输出。

程序清单 9.8 binary.c

/* binary.c -- 以二进制形式打印整数 */
#include <stdio.h>
void to_binary(unsigned long n);int main(void) { /* 主函数,用于接收输入和调用转换函数 */ }void to_binary(unsigned long n) /* 递归函数 */
{int r;r = n % 2; // 计算当前最低位if (n >= 2){// 只要数还大于等于2,就先递归处理更高位的部分to_binary(n / 2); }// 当递归回归时,才打印本层计算出的位// 这样,最先计算出的最低位,最后被打印putchar(r == 0 ? '0' : '1'); return;
}

当调用 to_binary(9) 时,函数计算出余数 1,然后调用 to_binary(4)。这个递进过程直到 to_binary(1) 为止。在回归时,to_binary(1) 先打印 ‘1’,然后 to_binary(2) 打印 ‘0’,to_binary(4) 打印 ‘0’,最后最初的 to_binary(9) 打印 ‘1’,最终屏幕上正确地显示 “1001”。

终极武器:指针、地址与函数

一个常见的需求是在一个函数内部修改另一个函数(通常是主调函数)中的变量。由于C语言默认的“按值传递”机制,这无法直接实现。

程序清单 9.13 swap1.c (失败的尝试)

#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); // 传递的是 x 和 y 的值的副本printf("Now x = %d and y = %d.\n", x, y); // x 和 y 的值并未改变return 0;
}void interchange(int u, int v)
{int temp;temp = u;u = v;v = temp;// 这里交换的仅仅是 interchange 函数自己的局部变量 u 和 v
}

代码解读:
interchange 函数确实成功交换了 uv 的值。但 uv 只是 xy 值的拷贝,它们是独立的变量。对副本的操作,无法影响到正本。

要突破这层限制,我们必须绕过值的传递,直接操作变量的内存地址。这正是指针大显身手的舞台。

  • 地址运算符 &:用于获取一个变量的内存地址。例如,&x 得到的就是变量 x 在内存中的具体位置。
  • 间接/解引用运算符 *:用于访问存储在某个地址上的值。如果一个指针变量 p 存储了 x 的地址(即 p = &x;),那么表达式 *p 就等价于变量 x 本身。

声明指针时,必须指明它所指向的数据类型,如 int *p; 表示 p 是一个指向 int 类型数据的指针。*p 的类型是 int,而 p 的类型是 int * (指向int的指针)。

现在,我们可以编写出能够真正交换值的函数。

程序清单 9.15 swap3.c

/* swap3.c -- 使用指针解决交换函数的问题 */
#include <stdio.h>
// 函数原型,参数类型是指向 int 的指针 (int *)
void interchange(int * u, int * v); int main(void)
{int x = 5, y = 10;printf("Originally x = %d and y = %d.\n", x, y);// 调用时,传递的不再是 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;// *u 表示“u所指向地址上的值”,即 x 的值temp = *u;   // *u = ... 表示“向 u 所指向的地址写入新值”// 这条语句将 v 指向的值(y 的值)写入 u 指向的位置(x 的位置)*u = *v;   // 将之前保存的 x 的值写入 v 指向的位置(y 的位置)*v = temp; 
}

代码解读:
这次,main 函数通过 interchange(&x, &y) 传递了 xy 的真实内存地址。interchange 函数的参数 uv 是指针变量,它们分别接收并存储了这两个地址。在函数体内:

  • temp = *u;u 存的是 x 的地址,*u 就是对这个地址进行解引用,取出了 x 的值(5),存入 temp
  • *u = *v;v 存的是 y 的地址,*vy 的值(10)。*u 作为左值,表示 x 的存储位置。这条语句把 y 的值(10)写入了 x 的内存空间。此时 main 函数中的 x 已经变成了10。
  • *v = temp;:把 temp 中保存的原始 x 的值(5)写入 y 的内存空间。此时 main 函数中的 y 变成了5。

通过传递地址和使用指针,interchange 函数获得了直接读写 main 函数作用域内变量的能力,成功地完成了交换任务。这是C语言强大功能和底层操控能力的核心体现。

附录:深度代码解读

1. hotel.c 中健壮的输入验证循环

hotel.cmenu() 函数中,有一个设计得非常巧妙的 while 循环,用于确保用户输入一个有效选项:

// from hotel.c, function menu()
while ((status = scanf("%d", &code)) != 1 || (code < 1 || code > 5))
{if (status != 1)scanf("%*s"); // 处理并丢弃非整数输入printf("Enter an integer from 1 to 5, please.\n");
}

逐层剖析:

  1. while 条件的复合逻辑: (status = scanf(...)) != 1 || (code < 1 || code > 5)

    • 短路求值: C语言的逻辑或 || 运算符遵循“短路”原则。如果左侧表达式为真,右侧表达式将不会被求值。这在这里至关重要。
    • 第一部分 (status = scanf("%d", &code)) != 1:
      • scanf("%d", &code) 尝试读取一个整数。如果成功,它返回 1;如果用户输入了非数字(如字母’a’),它会读取失败,返回 0status 变量会接收这个返回值。
      • 所以 status != 1 在输入非整数时为真,在输入整数时为假。
    • 第二部分 (code < 1 || code > 5):
      • 这部分检查读取到的整数 code 是否在有效范围 [1, 5] 之外。
      • 只有当第一部分为假(即 scanf 成功读取了一个整数)时,这部分才会被求值。如果 code 的值无效(例如用户输入了7),则此表达式为真。
  2. 循环体内的错误处理:

    • if (status != 1): 这个判断专门用来处理 scanf 读取失败的情况(即用户输入了非数字)。
    • scanf("%*s");: 这是一个精妙的技巧。%*s 格式说明符告诉 scanf 读取一个字符串(s),但是星号 * 表示读取后立即丢弃,不存储到任何变量中。这恰好能清理掉输入缓冲区中导致错误的那个非数字字符串(如"abc"),为下一次循环的 scanf("%d", ...) 扫清障碍。
    • printf(...): 无论错误是类型不对还是范围超限,都会给用户一个清晰的提示,引导他们进行正确的输入。

这个循环结构优雅地处理了两种主要的输入错误:类型不匹配范围越界,确保了函数最终返回的 code 值一定是 1, 2, 3, 4, 5 中的一个,极大地增强了程序的鲁棒性。

http://www.dtcms.com/a/504459.html

相关文章:

  • 网站建设教程赚找湖南岚鸿认 可网站必备功能
  • 阿里巴巴外贸网站首页手机网站布局教程
  • CMOS图像传感器驱动程序原理
  • 移动电商网站设计wordpress 获取文章别名
  • 惠州建设集团网站淘宝上面建设网站安全么
  • 深圳市做网站建设中国响应式网站
  • 双Token机制
  • 网站后台管理模板免费下载WordPress导购模板
  • 简述对网站进行评析的几个方面.网站建设开发设计营销公司厦门
  • php5mysql网站开发实例精讲又拍云 cdn WordPress
  • 宜章泰鑫建设有限公司网站给村里做网站
  • 【学习系列】SAP RAP 10:行为定义-Determinations和Validations
  • 织梦可以做导航网站网络营销的推广方式
  • 建设网站方法wordpress文章显示时间
  • 公司微信网站建设方案模板下载黑白色调网站
  • 中企视窗做网站怎么样今天哈尔滨最新通知
  • 网站域名的所有权网站建设资质备案
  • 网站建设目的主要包括哪些做网站要不要35类商标
  • MySQL 核心数据类型详解与实战案例
  • 郑州做网站公司电话php+mysql网站开发全程实例 pdf
  • 老薛主机做两个网站营销业务应用系统
  • SpringMVC初始
  • 哈尔滨房产信息网官网论坛seo网站
  • 离石网站建设16岁0元开网店赚钱软件
  • 建阳建设局网站建设网站企业运营
  • 基于 Zynq 的目标检测系统软硬协同架构设计(一)
  • 软件工程模块复盘:高频考点清单 + 5 道真题汇总
  • 网站js代码不显示wordpress 模板结构
  • 宁波网站推广厂家云浮网站设计
  • 做网站都需要什么东西网站开始怎么做