24、C语言编程技巧与模块设计解析

C语言模块化编程与技巧解析

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 语言编程有所帮助。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值