深入理解C语言程序结构
立即解锁
发布时间: 2025-08-21 00:53:42 阅读量: 1 订阅数: 3 


C语言编程入门与实践
### 深入理解C语言程序结构
#### 1. 技术要求
在学习过程中,需要准备以下工具:
- 选择一个纯文本编辑器。
- 准备一个控制台、终端或命令行窗口(根据你的操作系统而定)。
- 选择一个编译器,如GNU编译器集合(GCC)或Clang(clang),以适配你的操作系统。
为了保持一致性,建议在所有练习中使用相同的计算机和编程工具,这样能更专注于C语言在你计算机上的细节。本章的源代码可在https://github.com/PacktPublishing/Learn-C-Programming找到。要自己完整输入源代码,并确保程序能正确运行。
#### 2. 语句和代码块介绍
以经典的“Hello, world!”程序为例:
```c
#include<stdio.h>
int main() {
printf( "Hello, world!\n" );
return 0;
}
```
程序中有很多成对的标点符号,如`<`和`>`、`(`和`)`(出现两次)、`{`和`}`、`"`和`"`,还有一些其他有特殊意义的标点,如`#`、`.`、`;`、`\`、`<space>`和`<newline>`。
在字符串`"Hello, world!\n"`中,逗号和感叹号可忽略,因为它们会作为问候语的一部分输出。但`\`在字符串中有特殊意义,它常与其他字符组成双字符序列(双合字符),如`\n`代表换行符。不同操作系统对换行的表示不同,Linux、Unix和macOS用`\n`,部分Unix版本和其他操作系统用`\r`,Windows用`\r\n`。
下面是合法的C语言双合字符(转义序列):
| 符号 | 含义 |
| ---- | ---- |
| `\a` | 警报 |
| `\b` | 退格 |
| `\f` | 换页 |
| `\n` | 换行 |
| `\r` | 回车 |
| `\t` | 水平制表符 |
| `\v` | 垂直制表符 |
| `\'` | 单引号 |
| `\"` | 双引号 |
| `\?` | 问号 |
| `\\` | 反斜杠本身 |
示例代码如下:
```c
#include <stdio.h>
int main( void ) {
printf( "Hello, world without a new line" );
printf( "Hello, world with a new line\n" );
printf( "A string with \"quoted text\" inside of it\n\n" );
printf( "Tabbed\tColumn\tHeadings\n" );
printf( "The\tquick\tbrown\n" );
printf( "fox\tjumps\tover\n" );
printf( "the\tlazy\tdog.\n\n" );
printf( "A line of text that\nspans three lines\nand completes the line\n\n" );
return 0;
}
```
操作步骤:
1. 创建文件`printingExcapeSequences.c`,输入上述代码。
2. 保存文件。
3. 在控制台窗口使用以下命令编译和运行:
```
cc printingEscapeSequences.c <return>
a.out<return>
```
#### 3. 理解分隔符
分隔符用于分隔程序中的较小部分,这些较小部分称为标记(token)。标记是C语言中最小的完整元素,可分为预定义的关键字(如`int`、`return`等)和自定义的字符序列。
在“Hello, world!”程序中,有三种类型的分隔符:
- 单分隔符:`;`和`<space>`。
- 成对的对称分隔符:`<>`、`()`、`{}`和`""`。
- 不对称分隔符:以一个字符开始,以另一个字符结束,如`#`和`<newline>`、`//`和`<newline>`。
在程序中,`<space>`作为分隔符用于分隔关键字或标记,如`int`和`main()`之间、`return`和返回值之间。成对的对称分隔符各有特定用途,如`<>`用于特定类型的文件名,`()`表示与函数名关联,`{}`表示代码块,`""`表示字符串。`#`和`<newline>`组成的不对称分隔符用于预处理指令,如`#include <stdio.h>`会让编译器搜索并插入`stdio.h`文件。
可以将程序精简为只包含关键字、标记和分隔符的形式,如`hello_nowhitespace.c`:
```c
#include<stdio.h>
int main(){printf("Hello, world!\n");return 0;}
```
操作步骤:
1. 创建文件`hello_nowhitespace.c`,输入上述代码。
2. 保存文件。
3. 编译并运行,验证输出是否与原程序相同。
但这种做法不是好的编程习惯,因为程序被阅读的次数远多于创建或修改的次数,使用空格和注释能让程序更易理解。
#### 4. 理解空白字符
当`<space>`或`<newline>`字符不需要用于分隔C代码的一部分时,它们被视为空白字符。空白字符还包括`<tab>`、`<carriage return>`等,但不建议在C源文件中使用制表符,因为在不同计算机或打印时可能会影响代码的清晰度。
不同程序员对如何有效使用空白字符有不同看法,重要的是保持代码格式的一致性,不一致的空白字符格式会使代码难以阅读,甚至引入错误。
例如,下面是一个使用了过多无意义空白字符的“Hello, world!”程序:
```c
# include <stdio.h>
int
main
(
)
{
printf
(
"Hello, world!\n"
)
;
return
0
;
}
```
虽然这仍是一个有效的C程序,但不建议这样编写。
以下是已遇到的分隔符及其用途:
| 符号 | 符号名称 | 符号用途 |
| ---- | ---- | ---- |
| `<space>` | 空格 | 基本标记分隔符或空白字符 |
| `<newline>` | 换行 | 预处理指令和C++风格注释的终止分隔符或空白字符 |
| `;` | 分号 | 语句终止符 |
| `//` | 双正斜杠 | C++风格注释的开始 |
| `#` | 井号 | 预处理指令的开始 |
| `< >` | 尖括号 | 预处理指令中使用的文件名分隔符(成对使用) |
| `{ }` | 花括号 | 代码块分隔符 |
| `( )` | 圆括号 | 函数参数分隔符,也用于表达式分组 |
| `" "` | 双引号 | 字符串分隔符或预处理指令中多字符文件名分隔符 |
| `' '` | 单引号 | 单字符分隔符 |
| `[]` | 方括号 | 数组表示法 |
#### 5. 语句介绍
C语言中的语句是程序的基本构建块,每种语句构成一个完整的计算逻辑单元。语句有多种类型:
- 简单语句:以`;`结尾,如`return 0;`。
- 代码块语句:以`{`开始,以`}`结束,用于包含和分组其他语句,如`{ … }`。
- 复杂语句:由关键字和一个或多个代码块语句组成,如`main(){…}`。
- 复合语句:由简单语句和/或复杂语句组成,如程序主体是一个复合代码块语句,包含对`printf()`的调用和`return 0;`语句。
在“Hello, world!”程序中,已遇到以下几种语句:
- 预处理指令:以`#`开始,以`<newline>`结束,它不是真正执行计算的C语句,而是命令编译器以特定方式处理C文件。
- 函数语句:`main()`函数是程序执行的起点,是一种预定义的函数语句,每个可执行的C程序必须有且只有一个`main()`函数。
- 函数调用语句:如调用预定义的`printf()`函数,调用时当前函数的执行会暂停,跳转到被调用函数继续执行。
- 返回语句:如`return 0;`,会使当前函数的执行结束,返回调用者。
- 代码块语句:由一个或多个语句用`{ }`括起来,用于函数语句和控制语句。
后续还会遇到控制语句(如`if {} else {}`、`goto`、`break`、`continue`)、循环语句(如`while()`、`do()… while`、`for()`)和表达式语句等。
#### 6. 函数介绍
函数是可调用的程序代码段,用于执行一个或多个相关的计算工作。函数将语句组合成一组连贯的指令,用于执行特定的复杂任务。解决C语言中的问题,很大程度上是将问题分解为更小的功能部分,并编写函数来解决每个小问题。
#### 7. 函数定义理解
每个函数由以下部分组成:
- 函数标识符:函数的名称,应与函数实际执行的操作紧密匹配。
- 函数结果类型或返回值类型:函数可以向调用者返回一个值,若指定了返回值类型,函数必须返回该类型的值。
- 函数代码块:与函数名和参数列表直接关联的代码块,用于添加执行函数工作的额外语句。
- 返回语句:用于将指定类型的值从被调用函数返回给调用者。
- 函数参数列表:可选的输入值列表,函数在计算时可以使用这些值。
函数类型、函数标识符和函数参数列表构成函数签名。在C程序中,每个函数标识符必须唯一,函数签名不用于唯一标识函数,两个具有相同标识符但参数列表或结果类型不同的函数会导致编译失败。
#### 8. 函数标识符探索
`main()`函数是一个特殊的函数,其标识符是保留的,不能在程序中命名其他函数为`main`,且程序不能自己调用`main`,只能由系统调用。
函数标识符应具有描述性,例如`printGreeting()`应打印问候语,`printWord()`应打印单个单词。函数标识符区分大小写,应避免使用全大写的函数名,因为难以阅读。
当两个函数目的相似但略有不同时,不要依赖大小写来区分它们,最好在长度或名称修饰符上有所不同。常见的使函数名具有描述性且易读的方法有驼峰命名法(camel-case)和下划线分隔法(snake-case),如:
- 全小写:`makelightgreen()`、`makemediumgreen()`、`makedarkgreen()`。
- 驼峰命名法:`makeLightGreen()`、`makeMediumGreen()`、`makeDarkGreen()`。
- 下划线分隔法:`make_light_green()`、`make_medium_green()`、`make_dark_green()`。
建议选择一种标识符命名约定并在整个程序中保持一致。
#### 9. 函数代码块探索
函数代码块是函数执行工作的地方,包含一个或多个语句。虽然函数代码块的大小没有理想标准,但通常不超过终端的行数(25行)或打印页面的行数(60行)的函数更受青睐,最好在25到50行之间。保持函数小有助于快速理解和解决子问题。
#### 10. 函数返回值探索
函数可以向调用者返回一个值,调用者可以选择忽略该返回值。当函数指定了返回类型时,必须返回该类型的值。如`main.c`中,`int`指定`main()`函数必须返回一个整数值,`return 0;`返回整数`0`,在大多数操作系统系统调用中,返回值`0`通常表示没有遇到错误。
如果函数的返回类型是`void`,则没有返回值,返回语句是可选的。例如:
```c
void printComma() {
...
return;
}
int main() {
...
return 0;
}
```
在`hello2.c`程序中:
```c
#include <stdio.h>
void printComma() {
printf( ", " );
return;
}
int main() {
printf( "Hello" );
printComma();
printf( "world!\n" );
return 0;
}
```
现代C语言中,`return 0;`是可选的,如果没有`return;`或`return 0;`语句,函数默认返回值为`0`。对于返回结果代码的函数,捕获并处理错误是好的编程实践。
#### 11. 函数参数传递值
函数可以接收输入值,并在函数体内使用它们。定义函数时,需指定可以传入或接收的参数的类型和数量。调用函数时,需提供参数的值,函数调用参数必须与指定的参数类型和数量匹配。
例如`printf( "Hello, world!\n" );`函数调用,参数是一个字符串。函数参数在函数定义中用`( … )`分隔符指定,参数列表可以有零个或多个参数,用逗号分隔。每个参数由数据类型和标识符组成,数据类型指定使用的值的类型,标识符用于在函数体内访问该值。
以下是不同参数数量的函数示例:
```c
void printComma( void ) {
...
}
void printAGreeting( char* aGreeting ) {
...
}
void printSalutation( char* aGreeting , char* who ) {
...
}
```
在下面的程序中:
```c
#include <stdio.h>
void printComma( void ) {
printf( ", " );
}
void printWord( char* word ) {
printf( "%s" , word );
}
int main() {
printWord( "Hello" );
printComma();
printWord( "world" );
printf( "!\n" );
}
```
`printWord()`函数的参数`word`是一个字符串,使用`%s`格式说明符在`printf()`函数中打印该字符串。
可以使用这些函数构建一个更通用的问候函数,如`hello4.c`:
```c
#include <stdio.h>
void printComma() {
printf( ", " );
}
void printWord( char* word ) {
printf( "%s" , word );
}
void printGreeting( char* greeting , char* addressee ) {
printWord( greeting );
printComma();
printWord( addressee );
printf( "!\n" );
}
int main() {
printGreeting( "Hello" , "world" );
printGreeting( "Good day" , "Your Royal Highness" );
printGreeting( "Howdy" , "John Q. and Jane P. Doe" );
printGreeting( "Hey" , "Moe, Larry, and Joe" );
return 0;
}
```
也可以将`printComma()`和`printWord()`合并为一个`printf()`语句,如`hello5.c`:
```c
#include <stdio.h>
void printGreeting( char* greeting , char* who ) {
printf( "%s, %s!\n" , greeting , who );
}
int main() {
printGreeting( "Hello" , "world" );
printGreeting( "Greetings" , "Your Royal Highness" );
printGreeting( "Howdy" , "John Q. and Jane R. Doe" );
printGreeting( "Hey" , "Moe, Larry, and Curly" );
return 0;
}
```
还可以将`printGreeting()`拆分为更小的函数,如`hello6.c`:
```c
#include <stdio.h>
void printAGreeting( char* greeting ) {
printf( "%s" , greeting );
}
void printAComma( void ) {
printf( ", " );
}
void printAnAddressee( char* aName ) {
printf( "%s" );
}
void printANewLine() {
printf( "\n" );
}
void printGreeting( char* aGreeting , char* aName ) {
printAGreeting( aGreeting );
printAComma();
printAnAddressee( aName );
printANewLine();
}
int main() {
printGreeting( "Hi" , "Bub" );
return 0;
}
```
通过这些示例可以看出,使用函数可以以多种方式组织程序,根据问题的需求选择合适的方式。
mermaid流程图展示函数调用顺序:
```mermaid
graph LR;
A[main()] --> B[printGreeting()];
B --> C[printAGreeting()];
C --> B;
B --> D[printAComma()];
D --> B;
B --> E[printAnAddressee()];
E --> B;
B --> F[printANewLine()];
F --> B;
B --> A;
A --> return 0;
```
#### 12. 函数执行顺序
当程序执行时,首先找到`main()`函数并开始执行其中的语句。遇到函数调用语句时,会发生以下操作:
1. 如果有函数参数,将函数调用语句中的实际值分配给函数参数名。
2. 程序执行跳转到该函数,开始执行函数代码块中的语句。
3. 执行继续,直到遇到返回语句或代码块结束(`}`)。
4. 执行跳回调用函数,从该点继续执行。
如果在步骤2中又遇到函数调用语句,重复上述步骤。
#### 13. 函数声明理解
为了让编译器在看到函数调用时能识别该函数,必须先处理函数语句的定义。但这有一定局限性,因为可能希望在程序的任何位置调用函数。
C语言提供了函数声明(也称为函数原型)的方式,让编译器在处理函数定义之前就知道足够的信息来处理函数调用。函数声明只声明函数名、返回类型和参数列表,函数定义必须存在,且函数签名在声明、定义和调用时必须匹配,否则会导致编译错误。
例如`hello7.c`程序:
```c
#include <stdio.h>
// 函数原型
void printGreeting( char* aGreeting , char* aName );
void printAGreeting( char* greeting );
void printAnAddressee( char* aName );
void printAComma( void );
void printANewLine();
int main() {
printGreeting( "Hi" , "Bub" );
return 0;
}
void printGreeting( char* aGreeting , char* aName ) {
printAGreeting( aGreeting );
printAComma();
printAnAddressee( aName );
printANewLine();
}
void printAGreeting( char* greeting ) {
printf( "%s" , greeting );
}
void printAnAddressee( char* aName ) {
printf( "%s" );
}
void printAComma( void ) {
printf( ", " );
}
void printANewLine() {
printf( "\n" );
}
```
在这个程序中,函数定义按调用顺序排列,使用了函数原型来支持这种自上而下的实现方式。函数原型必须出现在函数调用之前,但它们的出现顺序不重要。
操作步骤:
1. 可以将`hello6.c`复制到`hello7.c`并重新排列函数,或者重新输入程序。
2. 尝试编译程序,可能会遇到与移除`#include <stdio.h>`行时类似的错误。
3. 添加函数原型后,再次编译、运行并验证`hello7.c`的输出是否与`hello6.c`相同。
将所有函数原型放在文件开头是好的做法,但不是必需的。函数原型不一定与函数定义的顺序相同,但按顺序排列有助于查找函数定义。
通过本章的学习,我们从简单的C程序开始,探索了C语言的语句,通过使用函数扩展和改变了程序,学会了定义函数、调用函数和声明函数原型,还了解了使用自上而下或自下而上的方法来构建程序。将问题分解为小部分并通过函数解决每个部分是解决复杂问题的重要技能。后续将开始了解数据类型,它决定了如何解释值以及可以对该值进行何种操作。
### 深入理解C语言程序结构(续)
#### 14. 函数调用与返回的详细分析
在前面我们了解了函数执行顺序的基本流程,现在进一步详细分析函数调用与返回过程中的一些关键细节。
当一个函数被调用时,系统会为该函数创建一个新的栈帧(stack frame)。栈帧是内存中的一块区域,用于存储函数的局部变量、参数以及返回地址等信息。例如,当`main()`函数调用`printGreeting()`函数时,系统会为`printGreeting()`创建一个栈帧,将传递的参数存储在栈帧中,同时记录返回地址(即`main()`函数中调用`printGreeting()`之后的下一条语句的地址)。
在函数执行过程中,所有的局部变量都存储在这个栈帧中。当函数执行结束,无论是遇到返回语句还是到达函数块的末尾,系统会销毁该栈帧,释放其中存储的局部变量所占用的内存。这就是为什么函数内部的局部变量在函数执行结束后就无法再访问的原因。
例如,在`printGreeting()`函数中:
```c
void printGreeting( char* aGreeting , char* aName ) {
char temp[100];
// 一些操作
printAGreeting( aGreeting );
printAComma();
printAnAddressee( aName );
printANewLine();
}
```
`temp`数组是`printGreeting()`函数的局部变量,它存储在`printGreeting()`的栈帧中。当`printGreeting()`执行结束,`temp`数组所占用的内存就会被释放。
#### 15. 函数参数传递方式
在C语言中,函数参数传递有两种主要方式:值传递和地址传递。
- **值传递**:在值传递中,函数接收的是参数的副本,而不是参数本身。这意味着在函数内部对参数的修改不会影响到调用函数时传递的原始值。例如:
```c
#include <stdio.h>
void modifyValue(int num) {
num = num + 10;
printf("Inside modifyValue: %d\n", num);
}
int main() {
int value = 20;
modifyValue(value);
printf("In main: %d\n", value);
return 0;
}
```
在这个例子中,`modifyValue()`函数接收的是`value`的副本,在函数内部对`num`的修改不会影响到`main()`函数中的`value`。运行结果如下:
```
Inside modifyValue: 30
In main: 20
```
- **地址传递**:地址传递是将参数的地址传递给函数,函数可以通过该地址访问和修改原始数据。例如:
```c
#include <stdio.h>
void modifyValue(int *num) {
*num = *num + 10;
printf("Inside modifyValue: %d\n", *num);
}
int main() {
int value = 20;
modifyValue(&value);
printf("In main: %d\n", value);
return 0;
}
```
在这个例子中,`modifyValue()`函数接收的是`value`的地址,通过解引用操作`*num`可以直接修改`main()`函数中的`value`。运行结果如下:
```
Inside modifyValue: 30
In main: 30
```
#### 16. 函数递归
函数递归是指函数直接或间接地调用自身。递归函数通常包含两个部分:基本情况(base case)和递归情况(recursive case)。基本情况是递归终止的条件,避免无限递归;递归情况是函数调用自身的部分。
例如,计算阶乘的递归函数:
```c
#include <stdio.h>
int factorial(int n) {
if (n == 0 || n == 1) {
return 1; // 基本情况
} else {
return n * factorial(n - 1); // 递归情况
}
}
int main() {
int num = 5;
int result = factorial(num);
printf("Factorial of %d is %d\n", num, result);
return 0;
}
```
在这个例子中,`factorial()`函数在`n`等于0或1时返回1,这是基本情况;否则,函数调用自身计算`n - 1`的阶乘,这是递归情况。
递归函数的优点是代码简洁,逻辑清晰,但也存在一些缺点,如可能会导致栈溢出(stack overflow)错误,因为每次递归调用都会创建一个新的栈帧,当递归深度过大时,栈空间会被耗尽。
#### 17. 函数指针
函数指针是指向函数的指针变量。通过函数指针,可以将函数作为参数传递给其他函数,实现回调机制等功能。
函数指针的声明格式如下:
```c
return_type (*pointer_name)(parameter_list);
```
例如,定义一个指向返回值为`int`,参数为两个`int`类型的函数的指针:
```c
int add(int a, int b) {
return a + b;
}
int main() {
int (*func_ptr)(int, int);
func_ptr = add;
int result = func_ptr(3, 5);
printf("Result: %d\n", result);
return 0;
}
```
在这个例子中,`func_ptr`是一个函数指针,它指向`add()`函数。通过函数指针调用函数的方式与直接调用函数类似。
#### 18. 函数的作用域和链接属性
函数也有作用域和链接属性。
- **作用域**:函数的作用域通常是文件作用域(file scope),即函数在整个源文件中可见。如果要在其他源文件中使用该函数,需要进行声明。
- **链接属性**:函数有两种链接属性,外部链接(external linkage)和内部链接(internal linkage)。默认情况下,函数具有外部链接属性,这意味着该函数可以被其他源文件中的函数调用。如果在函数声明前加上`static`关键字,函数将具有内部链接属性,只能在当前源文件中使用。
例如:
```c
// file1.c
#include <stdio.h>
static void internalFunction() {
printf("This is an internal function.\n");
}
void externalFunction() {
internalFunction();
printf("This is an external function.\n");
}
// file2.c
extern void externalFunction();
int main() {
externalFunction();
// internalFunction(); // 错误,无法访问内部链接的函数
return 0;
}
```
在这个例子中,`internalFunction()`具有内部链接属性,只能在`file1.c`中使用;`externalFunction()`具有外部链接属性,可以在`file2.c`中调用。
#### 19. 总结与展望
通过前面的学习,我们全面深入地了解了C语言中函数的各个方面,包括函数的定义、调用、返回、参数传递、递归、指针以及作用域和链接属性等。函数是C语言中非常重要的组成部分,合理地使用函数可以使程序结构清晰、易于维护和扩展。
将问题分解为小的功能部分并通过函数来解决,是解决复杂问题的有效方法。在实际编程中,我们应该根据问题的特点选择合适的函数设计和实现方式。
未来,我们可以进一步探索C语言的其他特性,如数据类型、数组、结构体、指针的高级应用等,这些知识将帮助我们编写更加复杂和强大的程序。同时,我们也可以将所学的知识应用到实际项目中,提高自己的编程能力和解决问题的能力。
以下是一个总结表格,回顾函数的各个关键知识点:
| 知识点 | 描述 |
| ---- | ---- |
| 函数定义 | 由函数标识符、返回值类型、参数列表、函数块和返回语句组成 |
| 函数调用 | 程序执行跳转到被调用函数,执行结束后返回调用函数 |
| 函数参数传递 | 包括值传递和地址传递 |
| 函数递归 | 函数直接或间接地调用自身,需有基本情况和递归情况 |
| 函数指针 | 指向函数的指针变量,可将函数作为参数传递 |
| 函数作用域和链接属性 | 作用域通常为文件作用域,链接属性有外部链接和内部链接 |
mermaid流程图展示函数指针调用过程:
```mermaid
graph LR;
A[main()] --> B[定义函数指针];
B --> C[函数指针指向函数];
C --> D[通过函数指针调用函数];
D --> E[函数执行];
E --> F[返回结果到main()];
```
通过不断学习和实践,我们将能够更加熟练地运用C语言的函数特性,编写出高质量的程序。
0
0
复制全文
相关推荐









