C语言编程技巧与模块设计解析
1. C语言中的特殊语句与运算符
在C语言编程中,有一些特殊的语句和运算符值得我们深入探讨。
1.1 goto语句
在实际编程里,goto语句使用频率较低。其正确语法为:
gotolabel;
,其中label是语句标签,语句标签和变量名遵循相同的命名规则,给语句添加标签的方式如下:
label:statement
。
下面是一个使用goto语句的示例:
for (x = 0; x < X_LIMIT; x++) {
for (y = 0; y < Y_LIMIT; y++) {
if (data[x][y] == 0)
goto found;
}
}
printf("Not found\n");
exit(8);
found:
printf("Found at (%d,%d)\n", x, y);
在这个例子中,如果在二维数组
data
中找到值为0的元素,程序会跳转到
found
标签处继续执行。
此外,在示例
def/def.c
中,当输入错误命令时没有打印错误信息,原因是“default”被误拼为“defualt”,而“defualt:”被编译器当作了有效的goto标签,所以编译器没有将其视为错误。示例代码如下:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char line[10];
while (1) {
printf("Enter add(a), delete(d), quit(q): ");
fgets(line, sizeof(line), stdin);
switch (line[0]) {
case 'a':
printf("Add\n");
break;
case 'd':
printf("Delete\n");
break;
case 'q':
printf("Quit\n");
exit(0);
defualt:
printf("Error:Bad command %c\n", line[0]);
break;
}
}
}
1.2 ? : 构造
? :
运算符的作用类似于
if/then/else
,但它可以用在表达式内部。其一般形式为:
(expression) ? value1 : value2
。
例如:
amount_owed = (balance < 0) ? 0 : balance;
该语句会根据
balance
的值,将
amount_owed
赋值为
balance
或0。
还可以使用宏来返回两个参数中的最小值:
#define min(x,y) ((x) < (y) ? (x) : (y))
1.3 , 运算符
逗号运算符可以用来组合语句。例如:
if (total < 0) {
printf("You owe nothing\n");
total = 0;
}
可以改写为:
if (total < 0)
printf("You owe nothing\n"),total = 0;
不过,大多数情况下建议使用花括号
{}
,逗号运算符主要在
for
语句中比较有用。例如:
for (two = 0, three = 0;
two < 10;
two += 2, three += 3)
printf("%d %d\n", two, three);
这个
for
循环会同时对
two
和
three
进行递增操作。
1.4 volatile限定符
volatile
关键字用于表明变量的值可能随时改变,常用于内存映射I/O设备或实时控制应用中,在这些场景下变量可能会被中断程序修改。但这属于高级编程内容,一般编程时较少使用。
2. 完整程序的设计与实现
下面我们来探讨一个完整程序的设计与实现过程,该程序用于读取C源文件并生成关于括号嵌套和注释与代码行比例的简单统计信息。
2.1 需求分析
在开始编程之前,明确需求非常重要。这个程序需要满足以下要求:
- 长度适中,既能展示模块化编程,又能在一个章节内完整呈现。
- 复杂度合适,既能体现C语言的多种特性,又要让初学者能够理解。
- 对C语言程序员有实际用途。
2.2 规格说明
程序
stat
用于收集C源文件的统计信息并打印出来,命令行格式为:
stat <files..>
,其中
<files..>
是源文件列表。以下是程序在一个简短测试文件上的输出示例:
[File: stat/stat.out]
1 (0 {0 /*-*/
2 (0 {0
/*****************************************************
***
3 (0 {0 * Name: Calculator (Version 2).
*
4 (0 {0 *
*
5 (0 {0 * Purpose:
*
6 (0 {0 * Act like a simple four-function
calculator. *
7 (0 {0 *
*
8 (0 {0 * Usage:
*
9 (0 {0 * Run the program.
*
10 (0 {0 * Type in an operator (+ - * /) and
a number. *
11 (0 {0 * The operation will be performed on
the current *
12 (0 {0 * result, and a new result will be
displayed. *
13 (0 {0 *
*
14 (0 {0 * Type 'Q' to quit.
*
15 (0 {0 *
*
16 (0 {0 * Notes: Like version 1 but written with
a switch *
17 (0 {0 * statement.
*
18 (0 {0
******************************************************
**/
19 (0 {0 /*+*/
20 (0 {0 #include <stdio.h>
21 (0 {0 char line[100]; /* line of text from
input */
22 (0 {0
23 (0 {0 int result; /* the result of the
calculations */
24 (0 {0 char operator; /* operator the user
specified */
25 (0 {0 int value; /* value specified after
the operator */
26 (0 {0 int main()
27 (0 {1 {
28 (0 {1 result = 0; /* initialize the result
*/
29 (0 {1
30 (0 {1 /* loop forever (or until break
reached) */
31 (0 {2 while (1) {
32 (0 {2 printf("Result: %d\n", result);
33 (0 {2 printf("Enter operator and number:
");
34 (0 {2
35 (0 {2 fgets(line, sizeof(line), stdin);
36 (0 {2 sscanf(line, "%c %d", &operator,
&value);
37 (0 {2
38 (0 {2 if ((operator == 'q') || (operator
== 'Q'))
39 (0 {2 break;
40 (0 {3 switch (operator) {
41 (0 {3 case '+':
42 (0 {3 result += value;
43 (0 {3 break;
44 (0 {3 case '-':
45 (0 {3 result -= value;
46 (0 {3 break;
47 (0 {3 case '*':
48 (0 {3 result *= value;
49 (0 {3 break;
50 (0 {3 case '/':
51 (0 {4 if (value == 0) {
52 (0 {4 printf("Error:Divide by
zero\n");
53 (0 {4 printf(" operation
ignored\n");
54 (0 {3 } else
55 (0 {3 result /= value;
56 (0 {3 break;
57 (0 {3 default:
58 (0 {3 printf("Unknown operator
%c\n", operator);
59 (0 {3 break;
60 (0 {2 }
61 (0 {1 }
62 (0 {1 return (0);
63 (0 {0 }
Total number of lines: 63
Maximum nesting of () : 2
Maximum nesting of {} : 4
Number of blank lines .................4
Number of comment only lines ..........20
Number of code only lines .............34
Number of lines with code and comments 5
Comment to code ratio 64.1%
2.3 代码设计
代码设计有多种方法,如结构化编程,将代码划分为模块、子模块等。本程序主要分为以下几个逻辑模块:
-
令牌扫描器
:读取原始C代码并将其转换为令牌,可进一步细分为三个小模块:
- 读取输入文件。
- 确定字符类型。
- 将信息组合成令牌。
-
主模块
:消耗令牌并输出统计信息,可细分为:
-
do_file
过程来管理每个文件。
- 每个统计信息对应一个子模块。
下面是程序模块的流程图:
graph LR
A[程序] --> B[令牌扫描器]
B --> B1[读取输入文件]
B --> B2[确定字符类型]
B --> B3[组合成令牌]
A --> C[主模块]
C --> C1[do_file过程]
C --> C2[统计子模块1]
C --> C3[统计子模块2]
C --> C4[统计子模块3]
C --> C5[统计子模块4]
3. 各模块详细解析
3.1 令牌模块
该模块扫描C源代码并使用令牌生成统计信息。例如,代码行
answer = (123 + 456) / 89; /* Compute some sort of result */
包含以下令牌:
| 令牌类型 | 具体内容 |
| ---- | ---- |
| T_ID | “answer” |
| T_OPERATOR | “=” |
| T_L_PAREN | 左括号 |
| T_NUMBER | 123 |
| T_OPERATOR | “+” |
| T_NUMBER | 456 |
| T_R_PAREN | 右括号 |
| T_OPERATOR | 除号 |
| T_NUMBER | 89 |
| T_OPERATOR | 分号 |
| T_COMMENT | 注释 |
| T_NEW_LINE | 换行符 |
大多数情况下,通过查看第一个字符就能识别令牌,但有些字符具有歧义,如
/
可能是除号,也可能是注释的开始,这就需要向前查看一个字符。令牌模块构建令牌的伪代码如下:
if the current character is a letter, then
scan until we get a character that's not
a letter or digit.
3.2 输入模块
输入模块需要完成两件事:
- 向令牌模块提供当前和下一个字符。
- 缓存整行内容以便后续显示。
最初的输入模块设计存在一些问题,它包含一个公共结构:
struct input_file {
FILE *file; /* File we are reading */
char line[LINE_MAX];/* Current line */
char *char_ptr; /* Current character on the line */
int cur_char; /* Current character (can be EOF) */
int next_char; /* Next character (can be EOF) */
};
以及操作该结构的函数:
extern void in_open(struct input_file *in_file, const char name[]);
extern void in_read_char(struct input_file *in_file);
extern void in_flush(struct input_file *in_file);
这种设计要求用户了解模块的内部结构,使用起来较为复杂。
改进后的输入模块提供了以下函数:
extern int in_open(const char name[]);
extern void in_close(void);
extern void in_read_char(void);
extern int in_cur_char(void);
extern int in_next_char(void);
extern void in_flush(void);
这些函数隐藏了处理输入文件所需的所有记录信息,简化了文件的打开操作,用户无需了解输入文件的结构,虽然该模块函数较多且只能一次打开一个文件,但在当前需求下,简化接口带来的好处大于灵活性的降低。
3.3 字符类型模块
该模块的目的是读取字符并解码其类型,部分类型存在重叠,如
C_ALPHA_NUMERIC
包含
C_NUMERIC
字符集。模块将大部分类型信息存储在数组中,处理特殊类型(如
C_ALPHA_NUMERIC
)只需少量逻辑。模块中的函数有:
extern int is_char_type(int ch, enum CHAR_TYPE kind);
extern enum CHAR_TYPE get_char_type(int ch);
关于字符数组的初始化,有两种方法:
- 要求用户在调用函数前初始化,如:
main() {
/* .... */
init_char_type();
/* ...... */
type_info = ch_to_type(ch);
}
- 在每个函数开头添加检查,按需初始化数组:
int is_char_type(int ch, enum CHAR_TYPE kind)
if (!ch_setup) {
init_char_type();
ch_setup = 0;
}
第二种方法虽然代码稍多,但对用户更友好,减少了出错的可能性,还隐藏了内部记录信息。
3.4 统计子模块
程序收集了四种统计信息:行数、括号
()
嵌套、花括号
{}
嵌套以及注释行与非注释行的数量。每个统计子模块都有相似的函数名:
-
xx_init
:初始化统计信息,在每个文件开始时调用。
-
xx_take_token
:接收令牌并更新统计信息。
-
xx_line_start
:在每行开始时输出统计信息。
-
xx_eof
:在文件结束时输出统计信息。
其中
xx
代表子模块标识符,分别为:
-
lc
:行计数器子模块。
-
pc
:括号计数器子模块。
-
bc
:花括号计数器子模块。
-
cc
:注释/非注释行计数器子模块。
4. 各模块功能描述
4.1 ch_type模块
该模块计算字符的类型,大部分计算通过名为
type_info
的表完成,对于像
C_ALPHA_NUMERIC
这种包含多种字符类型的情况,需要额外的代码处理。
4.2 in_file模块
该模块一次从输入文件中读取一个字符,缓存一行内容,并可按需将其写入输出。
4.3 token模块
该模块的主要功能是将字符转换为令牌,其令牌化过程相对简单,因为不需要处理完整C令牌化器的大部分细节。不过,它会将多行注释拆分为一系列
T_COMMENT
和
T_NEW_LINE
令牌。
4.4 行计数器子模块(lc)
这是最简单的统计子模块,用于统计已处理的行数。它只关注
T_NEW_LINE
令牌,在每行开始时输出行号,文件结束时不输出任何信息。虽然定义了
lc_eof
函数,但该函数不执行任何操作。
4.5 花括号计数器子模块(bc)
该子模块跟踪花括号
{}
的嵌套级别,通过
bc_take_token
函数接收令牌流,只处理左右花括号,忽略其他令牌:
void bc_take_token(enum TOKEN_TYPE token) {
switch (token) {
case T_L_CURLY:
++bc_cur_level;
if (bc_cur_level > bc_max_level)
bc_max_level = bc_cur_level;
break;
case T_R_CURLY:
--bc_cur_level;
break;
default:
/* Ignore */
break;
}
}
统计结果在每行开始和文件结束时输出,分别通过以下两个函数实现:
static void bc_line_start(void) {
printf("{%-2d ", bc_cur_level);
}
static void bc_eof(void) {
printf("Maximum nesting of {} : %d\n", bc_max_level);
}
4.6 括号计数器子模块(pc)
虽然文档未详细给出该子模块的代码,但可以推测其功能与花括号计数器子模块类似,用于跟踪括号
()
的嵌套级别。
综上所述,通过对这些模块的设计和实现,我们可以构建一个完整的程序来收集C源文件的统计信息。在实际编程中,合理运用这些模块和技巧,能够提高代码的可读性和可维护性。
5. 编码过程与问题处理
在编码过程中,整体相对顺利,但遇到了一个关键问题,即处理行尾的问题。不过,文中未详细说明该问题的具体表现和解决办法,推测可能与文件读取时对换行符的处理、不同操作系统换行符的差异等有关。在实际编码时,可参考以下步骤来处理行尾问题:
1.
了解不同操作系统的换行符
:Windows 使用
\r\n
,Unix/Linux 使用
\n
,Mac OS 旧版本使用
\r
。
2.
统一处理换行符
:在读取文件时,将不同的换行符统一转换为一种格式,方便后续处理。
3.
检查行尾字符
:在处理每一行时,检查行尾是否有多余的换行符或其他特殊字符,并进行相应处理。
6. 总结与最佳实践建议
通过上述对 C 语言编程技巧和模块设计的详细分析,我们可以总结出以下一些最佳实践建议:
6.1 特殊语句和运算符的使用
- goto 语句 :尽量少用,仅在极少数必要的情况下使用,以避免代码逻辑混乱。
- ? : 运算符 :在简单的条件赋值场景中使用,可使代码更简洁。
-
逗号运算符
:主要在
for语句中使用,其他情况优先使用花括号。 - volatile 限定符 :仅在处理内存映射 I/O 设备或实时控制应用时使用。
6.2 模块设计原则
- 信息隐藏 :像改进后的输入模块一样,隐藏模块的内部实现细节,减少用户对模块内部结构的依赖。
- 简化接口 :提供简单易用的接口,降低用户使用模块的难度。
- 功能模块化 :将程序划分为多个逻辑模块,每个模块负责单一功能,提高代码的可维护性和可扩展性。
6.3 统计子模块设计
- 统一命名规范 :如统计子模块使用统一的函数名,方便代码的管理和维护。
- 功能分离 :每个统计子模块专注于一项统计任务,避免功能混杂。
以下是一个总结各模块功能和使用建议的表格:
| 模块名称 | 功能描述 | 使用建议 |
| ---- | ---- | ---- |
| 令牌扫描器 | 读取 C 代码并转换为令牌 | 合理处理字符歧义,确保令牌识别准确 |
| 输入模块 | 提供字符和缓存行内容 | 使用简化接口的设计,减少用户负担 |
| 字符类型模块 | 解码字符类型 | 采用按需初始化的方式,提高用户体验 |
| 统计子模块 | 收集各种统计信息 | 遵循统一命名规范,功能分离 |
下面是一个整体程序执行流程的 mermaid 流程图:
graph LR
A[开始] --> B[打开输入文件]
B --> C[令牌扫描器处理]
C --> D[字符类型模块识别]
D --> E[生成令牌]
E --> F[主模块处理]
F --> F1[do_file 过程]
F --> F2[统计子模块更新统计信息]
F2 --> G{是否文件结束}
G -- 否 --> C
G -- 是 --> H[输出统计结果]
H --> I[关闭文件]
I --> J[结束]
通过遵循这些最佳实践建议,我们可以编写出更高效、更易维护的 C 语言程序。在实际应用中,还可以根据具体需求对这些模块进行扩展和优化,以满足不同的业务场景。希望这些内容能对大家的 C 语言编程有所帮助。
C语言模块化编程与技巧解析
超级会员免费看

被折叠的 条评论
为什么被折叠?



