C程序中的选择语句
C程序中的选择语句
C语言的语句虽然数量不多,但每一种都有其独特的用途。程序的执行流程控制是编程的核心,而选择语句正是实现这种控制的关键工具。本文探讨C语言中的两种主要选择语句:if语句和switch语句,以及构建逻辑表达式所需的各种运算符。
逻辑表达式
在了解选择语句之前,必须先掌握逻辑表达式的构建方法。C语言独特之处在于它没有专门的布尔类型(至少在C99之前没有),而是将整数0视为假,非零值视为真。这种设计虽然简洁,但也容易引起初学者的困惑。
关系运算符
关系运算符是构建逻辑表达式的基础。C语言提供了四个关系运算符:<
、>
、<=
和>=
。它的一个重要特性是它们可以用于比较不同类型的数值。当整数和浮点数混合比较时,整数会自动转换为浮点数再进行比较。例如,表达式1 < 2.5
中,整数1首先被转换为1.0,然后与2.5进行比较,结果为真(值为1)。而关系运算符的优先级低于算术运算符,这意味着表达式i + j < k - 1
会被解析为(i + j) < (k - 1)
。这种优先级设置符合数学表达式的常规写法,使代码更加直观。
特别需要注意的是关系运算符的左结合性。表达式i < j < k
并不能测试j是否位于i和k之间。由于左结合性,这个表达式实际上等价于(i < j) < k
。首先计算i < j
得到0或1,然后将这个结果与k比较。正确测试j是否在i和k之间的表达式应该是i < j && j < k
。
判等运算符
判等运算符在C语言中有着独特的符号表示。由于单个等号=
已经被用作赋值运算符,因此相等性测试使用双等号==
,不等测试使用!=
。这种设计是C语言中最容易出错的地方之一。
考虑以下两个表达式的区别:
if (i == 0) /* 测试i是否等于0 */
if (i = 0) /* 将0赋值给i,然后测试结果(总是假) */
第二个表达式是一个常见的错误,它不是在测试i的值,而是在改变i的值。由于赋值表达式的值就是被赋的值(在这里是0),所以条件测试总是失败。判等运算符的优先级低于关系运算符,这使得表达式i < j == j < k
能够测试两个比较的结果是否相同。如果i < j
和j < k
同为真或同为假,整个表达式的值为真。
逻辑运算符
逻辑运算符提供了组合简单条件形成复杂逻辑表达式的能力。C语言有三个逻辑运算符:
!
(逻辑非)是一元运算符,它将真值变为假,假值变为真。任何非零值被视为真,因此!5
的结果是0,而!0
的结果是1。
&&
(逻辑与)要求两个操作数都为真时结果才为真。重要的是,&&
运算符执行短路计算:如果左操作数为假,右操作数不会被计算。这个特性非常有用,例如:
if (i != 0 && j / i > 0)/* 如果i为0,j/i不会被计算,避免了除零错误 */
||
(逻辑或)在至少一个操作数为真时结果为真。它同样执行短路计算:如果左操作数为真,右操作数不会被计算。
短路计算的副作用需要特别注意。在表达式i > 0 && ++j > 0
中,如果i > 0
为假,++j
不会执行,j的值不会改变。这种行为可能导致难以发现的bug,因此建议避免在逻辑表达式中使用有副作用的操作。
if语句
if语句是最基本也是最重要的选择语句。它的灵活性使其能够处理从简单的二路选择到复杂的多路决策的各种情况。
基本if语句和复合语句
最简单的if语句形式只包含一个条件和一条语句:
if (line_num == MAX_LINES)line_num = 0;
这段代码检查当前行号是否达到最大值,如果是,则将其重置为0。圆括号是if语句语法的一部分,不可省略。当需要在条件为真时执行多条语句时,必须使用复合语句(也称为语句块):
if (line_num == MAX_LINES) {line_num = 0; /* 重置行号 */page_num++; /* 增加页号 */print_page_header(); /* 打印新页的页眉 */
}
花括号创建了一个语句块,使多条语句作为一个整体被if语句控制。注意每条语句仍然以分号结尾,但复合语句本身不需要分号。
else子句和嵌套if
else子句为if语句提供了"否则"的执行路径:
if (i > j)max = i;
elsemax = j;
这段代码将i和j中的较大值赋给max。如果条件i > j
为真,执行第一条赋值语句;否则执行else后的语句。
if语句可以任意嵌套,形成复杂的决策树。寻找三个数中最大值的完整示例:
if (i > j) {if (i > k)max = i; /* i是最大的 */elsemax = k; /* k比i大,且i比j大,所以k最大 */
} else {if (j > k)max = j; /* j比i大,且j比k大,所以j最大 */elsemax = k; /* k比j大,且j比i大,所以k最大 */
}
级联式if语句
当需要测试一系列互斥条件时,级联式if语句提供了清晰的解决方案。虽然从技术上讲这只是嵌套的if-else语句,但特殊的缩进格式使其看起来像一种独立的结构:
if (score >= 90)grade = 'A';
else if (score >= 80)grade = 'B';
else if (score >= 70)grade = 'C';
else if (score >= 60)grade = 'D';
elsegrade = 'F';
这种格式避免了过度缩进,并清楚地表明这是一系列互斥的条件测试。注意条件的顺序很重要:由于使用了>=
,必须先测试较大的值。
完整示例:股票经纪人佣金计算器
下面是一个完整的程序,演示了级联式if语句在实际应用中的使用:
/* broker.c - 计算股票经纪人的佣金 */
#include <stdio.h>int main(void)
{float commission, value;printf("Enter value of trade: ");scanf("%f", &value);/* 根据交易额计算基本佣金 */if (value < 2500.00f)commission = 30.00f + .017f * value;else if (value < 6250.00f)commission = 56.00f + .0066f * value;else if (value < 20000.00f)commission = 76.00f + .0034f * value;else if (value < 50000.00f)commission = 100.00f + .0022f * value;else if (value < 500000.00f)commission = 155.00f + .0011f * value;elsecommission = 255.00f + .0009f * value;/* 确保佣金不低于最低收费 */if (commission < 39.00f)commission = 39.00f;printf("Commission: $%.2f\n", commission);return 0;
}
程序首先读取交易额,然后通过级联式if语句确定适用的佣金率。注意使用浮点常量(如2500.00f
)来避免不必要的类型转换。最后的独立if语句确保佣金不低于39美元的最低收费。
"悬空else"问题
在嵌套if语句中,else子句的归属可能产生歧义:
if (y != 0)if (x != 0)result = x / y;
elseprintf("Error: y is equal to 0\n");
尽管缩进暗示else属于外层if,但C语言的规则是:else总是与最近的未配对的if匹配。因此,这个else实际上属于内层if。要让else属于外层if,必须使用花括号:
if (y != 0) {if (x != 0)result = x / y;
} elseprintf("Error: y is equal to 0\n");
条件表达式
条件运算符?:
提供了在表达式内部进行条件选择的能力:
max = (i > j) ? i : j;
这行代码等价于前面的if-else语句,但更加简洁。条件表达式首先计算i > j
,如果为真,整个表达式的值为i;否则为j。
条件表达式在某些场合特别有用,例如在printf调用中:
printf("You have %d item%s.\n", n, n == 1 ? "" : "s");
这行代码根据物品数量自动选择单数或复数形式。
布尔值的处理
C89没有专门的布尔类型,程序员通常使用宏定义来提高代码可读性:
#define TRUE 1
#define FALSE 0
#define BOOL intBOOL is_valid = FALSE;if (check_input()) {is_valid = TRUE;
}if (is_valid) {process_data();
}
C99引入了_Bool
类型和<stdbool.h>
头文件,提供了更标准的布尔值支持:
#include <stdbool.h>bool is_valid = false;if (check_input()) {is_valid = true;
}
switch语句
switch语句为多路选择提供了一种更结构化的方法。当需要根据一个整型表达式的值执行不同的代码块时,switch语句通常比级联式if语句更清晰、更高效。
switch语句的结构
switch语句的基本结构包括一个控制表达式和多个case标号:
switch (expression) {case constant1:statements1break;case constant2:statements2break;...default:default_statementsbreak;
}
控制表达式必须是整型(包括char类型)。每个case标号后的常量必须是编译时常量,且值必须唯一。
break语句的重要性
break语句在switch中扮演着关键角色。没有break,执行会"穿透"到下一个case:
switch (grade) {case 4:printf("Excellent");/* 注意:这里故意没有break */case 3:printf("Good");break;case 2:printf("Average");break;
}
如果grade为4,输出将是"ExcellentGood",因为执行穿透到了case 3。虽然有时会故意利用这种穿透行为,但通常这是一个错误。
完整示例:日期格式转换器
这个程序将数字日期转换为法律文档格式:
/* date.c - 将日期转换为法律文档格式 */
#include <stdio.h>int main(void)
{int month, day, year;printf("Enter date (mm/dd/yy): ");scanf("%d /%d /%d", &month, &day, &year);printf("Dated this %d", day);/* 为日期添加适当的后缀 */switch (day) {case 1: case 21: case 31:printf("st");break;case 2: case 22:printf("nd");break;case 3: case 23:printf("rd");break;default:printf("th");break;}printf(" day of ");/* 将月份数字转换为月份名称 */switch (month) {case 1: printf("January"); break;case 2: printf("February"); break;case 3: printf("March"); break;case 4: printf("April"); break;case 5: printf("May"); break;case 6: printf("June"); break;case 7: printf("July"); break;case 8: printf("August"); break;case 9: printf("September"); break;case 10: printf("October"); break;case 11: printf("November"); break;case 12: printf("December"); break;default: printf("Invalid month"); break;}printf(", 20%.2d.\n", year);return 0;
}
这个程序展示了switch语句的两个重要用法。第一个switch使用多个case标号共享同一个动作(例如1、21和31都使用"st"后缀)。第二个switch展示了将数字映射到字符串的典型模式。%.2d
格式说明符确保年份始终显示两位数字。
附录:代码解析
A.1 逻辑表达式的高级应用
在实际编程中,逻辑表达式的巧妙运用可以大大简化代码。考虑以下利用短路计算特性的高级技巧:
/* 安全的除法操作 */
double safe_divide(double a, double b) {return (b != 0.0) ? a / b : 0.0;
}/* 使用逻辑表达式进行范围检查 */
int is_valid_month(int month) {return month >= 1 && month <= 12;
}/* 复杂条件的分解 */
int can_vote(int age, int citizenship, int registration) {return age >= 18 && citizenship == CITIZEN &®istration == REGISTERED;
}
A.2 宏定义在条件判断中的应用
宏定义可以使条件判断更具可读性和可维护性:
#define IN_RANGE(x, min, max) ((x) >= (min) && (x) <= (max))
#define IS_DIGIT(c) ((c) >= '0' && (c) <= '9')
#define IS_UPPER(c) ((c) >= 'A' && (c) <= 'Z')
#define IS_LOWER(c) ((c) >= 'a' && (c) <= 'z')/* 使用宏进行字符分类 */
char process_char(char c) {if (IS_DIGIT(c))return c - '0'; /* 转换为数值 */else if (IS_UPPER(c))return c - 'A' + 10; /* A=10, B=11, ... */else if (IS_LOWER(c))return c - 'a' + 10; /* a=10, b=11, ... */elsereturn -1; /* 无效字符 */
}
A.3 状态机的switch实现
switch语句特别适合实现状态机:
typedef enum {STATE_IDLE,STATE_READING,STATE_PROCESSING,STATE_ERROR
} State;void state_machine(void) {State current_state = STATE_IDLE;int input;while (1) {input = get_input();switch (current_state) {case STATE_IDLE:if (input == START_COMMAND) {current_state = STATE_READING;printf("Starting to read data...\n");}break;case STATE_READING:if (input == DATA_READY) {current_state = STATE_PROCESSING;printf("Processing data...\n");} else if (input == ERROR_SIGNAL) {current_state = STATE_ERROR;printf("Error occurred!\n");}break;case STATE_PROCESSING:if (input == PROCESSING_DONE) {current_state = STATE_IDLE;printf("Processing completed.\n");} else if (input == ERROR_SIGNAL) {current_state = STATE_ERROR;}break;case STATE_ERROR:if (input == RESET_COMMAND) {current_state = STATE_IDLE;printf("System reset.\n");}break;}}
}
A.4 性能优化考虑
在性能关键的代码中,选择语句的组织方式会影响执行效率:
/* 效率较低:每个条件都要测试 */
if (x == 1) func1();
else if (x == 2) func2();
else if (x == 3) func3();
else if (x == 4) func4();
/* ... 更多条件 ... *//* 效率较高:switch通常编译为跳转表 */
switch (x) {case 1: func1(); break;case 2: func2(); break;case 3: func3(); break;case 4: func4(); break;/* ... */
}/* 对于稀疏的case值,考虑使用函数指针数组 */
void (*func_table[])(void) = {func1, func2, func3, func4};
if (x >= 1 && x <= 4)func_table[x - 1]();
A.5 错误处理模式
良好的错误处理是健壮程序的标志:
int process_file(const char *filename) {FILE *fp;int status = SUCCESS;/* 使用级联式if进行错误检查 */if ((fp = fopen(filename, "r")) == NULL) {fprintf(stderr, "Cannot open file: %s\n", filename);return ERROR_FILE_OPEN;}if (check_file_header(fp) != VALID_HEADER) {fprintf(stderr, "Invalid file format\n");status = ERROR_INVALID_FORMAT;} else if (load_file_data(fp) != SUCCESS) {fprintf(stderr, "Failed to load data\n");status = ERROR_LOAD_FAILED;} else {/* 处理数据 */status = process_data();}fclose(fp);return status;
}
A.6 防御性编程技巧
防御性编程可以捕获潜在的逻辑错误:
/* 使用断言验证假设 */
#include <assert.h>void process_percentage(int percent) {assert(percent >= 0 && percent <= 100);if (percent < 0 || percent > 100) {/* 即使在发布版本中也要处理无效输入 */fprintf(stderr, "Invalid percentage: %d\n", percent);return;}/* 正常处理 */
}/* 完整的switch语句应该处理所有情况 */
const char* get_day_name(int day) {switch (day) {case 0: return "Sunday";case 1: return "Monday";case 2: return "Tuesday";case 3: return "Wednesday";case 4: return "Thursday";case 5: return "Friday";case 6: return "Saturday";default:assert(0); /* 不应该到达这里 */return "Invalid day";}
}