第2章 类型、运算符与表达式
数据的表示与操作
一、导读
1.1 本章学习目标
第2章是C语言的基础设施章节。如果说第1章让你学会了如何搭建一个C程序的骨架,那么第2章将教你如何用"砖块"来填充它——数据类型是砖块的规格,运算符是砌筑的方式,表达式则是最终砌成的墙体。
本章的核心学习目标包括:
- 掌握C语言的基本数据类型:
char、int、float、double及其修饰符(short、long、signed、unsigned)的区别和适用场景。 - 理解运算符的优先级和结合性:为什么
a + b * c先算乘法?多个相同优先级的运算符如何分组? - 掌握类型转换规则:隐式转换何时发生?可能导致什么问题?强制类型转换如何使用?
- 理解自增自减运算符:
++i和i++的区别是什么?为什么C语言要提供这两种形式? - 掌握位运算符:
&、|、^、~、<<、>>如何操作二进制位?在什么场景下使用? - 理解表达式的求值顺序:为什么
a[i] = i++是错误的?什么是"未定义行为"?
1.2 为什么本章如此重要
类型系统是C语言的基石。C语言是一种静态类型语言——每个变量在使用前必须声明类型,编译器在编译时就能检查类型错误。理解类型系统,是写出正确、高效C程序的前提。
K&R 在第二章花了大量篇幅讨论类型,这绝非偶然。C语言的类型系统看似简单,实则暗藏陷阱。sizeof(int) 在不同平台上可能是2、4甚至8字节;char 在某些平台上是有符号的,在另一些平台上是无符号的;整数溢出是未定义行为,编译器可以做出任何假设。不理解这些细节,你的程序可能在一个平台上运行正常,在另一个平台上却崩溃。
运算符和表达式是C语言的"肌肉"。C语言之所以以高效著称,很大程度上是因为它提供了丰富的运算符,让程序员能够用接近机器语言的方式操作数据。位运算符让你能直接操作硬件寄存器,自增自减运算符让你能写出紧凑的循环,赋值运算符的简化形式让你能减少冗余代码。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
为什么C语言需要这么多整型变体(short、int、long、long long)?它们各自的适用场景是什么?
char 到底是 signed 还是 unsigned?为什么这很重要?
为什么 i = i++ + 1 是未定义行为?编译器会如何处理它?
位运算在实际编程中有哪些应用?(提示:权限掩码、特征标志位)
隐式类型转换在什么时候会导致精度丢失或逻辑错误?
sizeof 是函数还是运算符?它在编译时还是运行时求值?
二、核心概念详解
2.1 基本数据类型
C语言的基本数据类型可以分为两大类:整型和浮点型。此外还有 void 类型表示"无类型"。
2.1.1 整型家族
char // 至少8位,通常恰好8位
short // 至少16位
int // 至少16位,通常32位
long // 至少32位
long long // 至少64位(C99引入)每种整型都可以加上 signed(有符号)或 unsigned(无符号)修饰符:
signed char // -128 ~ 127
unsigned char // 0 ~ 255
signed short // -32768 ~ 32767
unsigned short // 0 ~ 65535
signed int // -2^31 ~ 2^31-1(32位平台上)
unsigned int // 0 ~ 2^32-1关键规则:unsigned 类型的所有位都用于表示正值,因此它的正值范围是 signed 类型的两倍。unsigned char 可以表示 0~255,而 signed char 只能表示 -128~127。
#include <stdio.h>
#include <limits.h>
int main() {
printf("char: %d ~ %d (%lu bytes)\n", CHAR_MIN, CHAR_MAX, sizeof(char));
printf("short: %d ~ %d (%lu bytes)\n", SHRT_MIN, SHRT_MAX, sizeof(short));
printf("int: %d ~ %d (%lu bytes)\n", INT_MIN, INT_MAX, sizeof(int));
printf("long: %ld ~ %ld (%lu bytes)\n", LONG_MIN, LONG_MAX, sizeof(long));
printf("ullong: 0 ~ %llu (%lu bytes)\n", ULLONG_MAX, sizeof(unsigned long long));
return 0;
}2.1.2 浮点型
float // 通常32位,IEEE 754单精度,约7位有效数字
double // 通常64位,IEEE 754双精度,约15位有效数字
long double // 通常80位或128位,精度取决于平台浮点数在内存中的表示遵循IEEE 754标准,由符号位、指数位和尾数位组成。这种表示方法可以在很大的范围内表示数值,但会牺牲精度。
#include <stdio.h>
#include <float.h>
int main() {
printf("float: %e ~ %e, precision: %d digits\n",
FLT_MIN, FLT_MAX, FLT_DIG);
printf("double: %e ~ %e, precision: %d digits\n",
DBL_MIN, DBL_MAX, DBL_DIG);
return 0;
}2.1.3 sizeof 运算符
sizeof 是一个编译时运算符(不是函数),它返回类型或变量在内存中占用的字节数。
#include <stdio.h>
int main() {
printf("sizeof(char): %lu\n", sizeof(char));
printf("sizeof(int): %lu\n", sizeof(int));
printf("sizeof(float): %lu\n", sizeof(float));
printf("sizeof(double): %lu\n", sizeof(double));
printf("sizeof(void*): %lu\n", sizeof(void*));
int arr[10];
printf("sizeof(arr): %lu\n", sizeof(arr));
printf("array length: %lu\n", sizeof(arr) / sizeof(arr[0]));
return 0;
}注意:sizeof 的返回值类型是 size_t(通常是 unsigned long),使用 %lu 格式说明符输出。对于数组,sizeof 返回整个数组的字节数,而不是指针的大小。但当数组作为函数参数传递时,它会退化为指针,sizeof 返回的是指针的大小。
2.2 常量与字面量
2.2.1 整型常量
42 // 十进制
052 // 八进制(以0开头)
0x2A // 十六进制(以0x开头)
42U // unsigned int
42L // long
42UL // unsigned long
42LL // long long(C99)2.2.2 浮点型常量
3.14 // double
3.14f // float(后缀f或F)
3.14L // long double(后缀l或L)
1.0e-5 // 科学计数法:1.0 × 10^(-5)2.2.3 字符常量
'A' // 字符常量,值为65(ASCII码)
'\n' // 转义字符:换行
'\x41' // 十六进制表示:ASCII 0x41 = 'A'
'\0' // 空字符字符常量 vs 字符串常量:'A' 是字符常量,类型为 int(C语言)或 char(C++),值为65。"A" 是字符串常量,是一个包含 'A' 和 '\0' 的字符数组,类型为 char[2]。
sizeof('A') // C语言中为 sizeof(int),通常4
sizeof("A") // 2(包含 '\0')2.2.4 枚举常量
enum boolean { FALSE, TRUE };
enum color { RED = 1, GREEN, BLUE }; // GREEN=2, BLUE=3
enum boolean b = TRUE; // b = 1
enum color c = GREEN; // c = 2枚举常量本质上是 int 类型的命名常量。枚举的第一个值默认为0,后续值依次递增,除非显式指定。
2.3 运算符详解
2.3.1 算术运算符
+ // 加法
- // 减法
* // 乘法
/ // 除法(整数除法向零取整)
% // 取模(仅适用于整数)整数除法的陷阱:
int a = 7 / 2; // a = 3(不是3.5)
int b = -7 / 2; // b = -3(C99规定向零取整,C89允许向下取整)
int c = 7 % 2; // c = 1
int d = -7 % 2; // d = -1(C99规定余数符号与被除数相同)取模运算符的应用:
// 判断奇偶
if (n % 2 == 0) { /* n是偶数 */ }
// 提取个位数
int last_digit = n % 10;
// 循环索引(环形缓冲区)
index = (index + 1) % size;2.3.2 关系运算符和逻辑运算符
== // 等于
!= // 不等于
> // 大于
< // 小于
>= // 大于等于
<= // 小于等于
&& // 逻辑与
|| // 逻辑或
! // 逻辑非短路求值:&& 和 || 运算符具有短路特性。对于 a && b,如果 a 为假,b 不会被求值。对于 a || b,如果 a 为真,b 不会被求值。
// 安全的空指针检查
if (ptr != NULL && *ptr > 0) {
// 如果 ptr 为 NULL,*ptr 不会被求值,避免段错误
}
// 字符串比较中的短路
if (s != NULL && strcmp(s, "hello") == 0) {
// 安全
}常见错误:混淆 = 和 ==。
int x = 5;
if (x = 3) { // 错误!这是赋值,不是比较
// x 被赋值为3,条件为真(3 != 0)
}
if (x == 3) { // 正确
// 比较 x 是否等于3
}2.3.3 位运算符
位运算符直接操作二进制位,是C语言接近硬件的体现:
& // 按位与(AND)
| // 按位或(OR)
^ // 按位异或(XOR)
~ // 按位取反(NOT,一元运算符)
<< // 左移
>> // 右移位运算的实际应用:
// 1. 设置特定位(置1)
unsigned int flags = 0;
flags |= (1 << 3); // 设置第3位:flags = 0b00001000
// 2. 清除特定位(置0)
flags &= ~(1 << 3); // 清除第3位:flags = 0b00000000
// 3. 翻转特定位
flags ^= (1 << 3); // 翻转第3位
// 4. 测试特定位
if (flags & (1 << 3)) {
// 第3位为1
}
// 5. 权限掩码(UNIX文件权限的经典应用)
#define READ (1 << 0) // 0x01
#define WRITE (1 << 1) // 0x02
#define EXECUTE (1 << 2) // 0x04
unsigned int perm = READ | WRITE; // 0x03
if (perm & EXECUTE) { /* 有执行权限 */ }左移和右移:
unsigned int x = 5; // 0b00000101
x << 1; // 0b00001010 = 10(左移1位等于乘以2)
x >> 1; // 0b00000010 = 2(右移1位等于除以2)
// 左移n位等价于乘以2^n(在不溢出的情况下)
// 右移n位等价于除以2^n(对无符号数)注意:对有符号数的右移,C标准未定义行为——可以是算术右移(高位补符号位)或逻辑右移(高位补0)。实际中大多数编译器执行算术右移。
2.3.4 赋值运算符
= // 简单赋值
+= // a += b 等价于 a = a + b
-= // a -= b 等价于 a = a - b
*= // a *= b 等价于 a = a * b
/= // a /= b 等价于 a = a / b
%= // a %= b 等价于 a = a % b
&= // a &= b 等价于 a = a & b
|= // a |= b 等价于 a = a | b
^= // a ^= b 等价于 a = a ^ b
<<= // a <<= b 等价于 a = a << b
>>= // a >>= b 等价于 a = a >> b赋值运算符的右侧先求值:a += expr 中,a 只求值一次。这在 a 是复杂表达式(如数组元素)时很重要。
a[expr] += 1; // expr 只求值一次
a[expr] = a[expr] + 1; // expr 求值两次(如果 expr 有副作用,结果可能不同)2.3.5 自增自减运算符
++i // 前缀形式:先加1,再返回新值
i++ // 后缀形式:先返回旧值,再加1
--i // 前缀形式:先减1,再返回新值
i-- // 后缀形式:先返回旧值,再减1int i = 5;
int a = ++i; // i变为6,a=6
int b = i++; // b=6,i变为7K&R的建议:在表达式中避免使用 ++,因为它可能导致令人困惑的行为。但在循环的步进部分(如 for (i = 0; i < n; i++))和数组遍历中,++ 是惯用的写法。
2.4 类型转换
2.4.1 隐式类型转换
C语言会在以下情况自动进行类型转换:
整型提升(Integer Promotion):char、short 在参与运算时会自动提升为 int。
char a = 100;
char b = 50;
int c = a + b; // a和b先提升为int,再相加算术转换(Arithmetic Conversion):当不同类型参与运算时,较小类型会自动转换为较大类型。
int a = 10;
float b = 3.14;
double c = a + b; // a先转换为float,再转换为double转换规则(从低到高):
char/short → int → long → long long → float → double → long double危险的隐式转换:
unsigned int a = 10;
int b = -5;
if (a < b) {
printf("a < b\n"); // 不会执行
} else {
printf("a >= b\n"); // 会执行!
}
// 原因:b被隐式转换为unsigned,-5变成了一个很大的正数int i = 2147483647; // INT_MAX
i = i + 1; // 有符号整数溢出:未定义行为!2.4.2 强制类型转换
(type) expressionint i = 10;
float f = (float) i / 3; // i先转换为float,再做浮点除法
float x = 3.7;
int n = (int) x; // n = 3(截断小数部分)
void *ptr = malloc(100);
int *ip = (int *) ptr; // void* 转换为 int*注意:强制类型转换不会改变原始变量的值或类型,它只是创建一个临时的转换后的值。
2.5 表达式的求值顺序
C语言中,大多数运算符的操作数求值顺序是未指定的。编译器可以选择先求值左操作数或右操作数。
int i = 0;
int a = f() + g(); // f()和g()的调用顺序未指定更严重的是,某些表达式的行为是未定义的(undefined behavior):
a[i] = i++; // 未定义行为:i在同一个表达式中被修改和使用
printf("%d %d", i, ++i); // 未定义行为
x = a + (a = 2); // 未定义行为规则:如果一个表达式中某个变量被修改(通过 ++、--、赋值等),那么这个变量在该表达式中最多只能被引用一次(用于确定要存储的值)。违反此规则的代码具有未定义行为。
// 安全的写法
a[i] = i;
i++;
// 或者
i++;
a[i-1] = i - 1;2.6 运算符优先级总表
以下是C语言运算符的优先级(从高到低):
优先级 运算符 结合性
─────────────────────────────────────────
1 () [] -> . 左到右
2 ! ~ ++ -- + - * & sizeof 右到右(一元)
3 * / % 左到右
4 + - 左到右
5 << >> 左到右
6 < <= > >= 左到右
7 == != 左到右
8 & 左到右
9 ^ 左到右
10 | 左到右
11 && 左到右
12 || 左到右
13 ?: 右到左
14 = += -= *= /= ... 右到左
15 , 左到右实用建议:当运算符优先级不确定时,使用括号明确分组。好的代码应该在清晰性和简洁性之间取得平衡。
三、实战案例
3.1 案例一:位操作实现集合
#include <stdio.h>
#define SET_BIT(word, n) ((word) |= (1U << (n)))
#define CLEAR_BIT(word, n) ((word) &= ~(1U << (n)))
#define TEST_BIT(word, n) ((word) & (1U << (n)))
#define TOGGLE_BIT(word, n) ((word) ^= (1U << (n)))
int main() {
unsigned int set = 0;
SET_BIT(set, 0);
SET_BIT(set, 3);
SET_BIT(set, 7);
printf("Set: ");
for (int i = 0; i < 8; i++) {
if (TEST_BIT(set, i)) {
printf("%d ", i);
}
}
printf("\n");
TOGGLE_BIT(set, 3);
printf("After toggle bit 3: ");
for (int i = 0; i < 8; i++) {
if (TEST_BIT(set, i)) {
printf("%d ", i);
}
}
printf("\n");
return 0;
}运行结果:
Set: 0 3 7
After toggle bit 3: 0 73.2 案例二:进制转换器
#include <stdio.h>
void print_binary(unsigned int n) {
if (n == 0) {
printf("0");
return;
}
char binary[33];
int i = 0;
while (n > 0) {
binary[i++] = (n & 1) ? '1' : '0';
n >>= 1;
}
for (int j = i - 1; j >= 0; j--) {
printf("%c", binary[j]);
if (j > 0 && j % 4 == 0) printf(" ");
}
}
int main() {
unsigned int num;
printf("Enter a positive integer: ");
scanf("%u", &num);
printf("Decimal: %u\n", num);
printf("Binary: ");
print_binary(num);
printf("\n");
printf("Octal: %o\n", num);
printf("Hexadecimal: 0x%X\n", num);
return 0;
}运行结果:
Enter a positive integer: 42
Decimal: 42
Binary: 0010 1010
Octal: 52
Hexadecimal: 0x2A3.3 案例三:安全的类型转换工具
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
int safe_add(int a, int b, int *result) {
if (b > 0 && a > INT_MAX - b) return -1;
if (b < 0 && a < INT_MIN - b) return -1;
*result = a + b;
return 0;
}
int safe_multiply(int a, int b, int *result) {
if (a > 0 && b > 0 && a > INT_MAX / b) return -1;
if (a > 0 && b < 0 && b < INT_MIN / a) return -1;
if (a < 0 && b > 0 && a < INT_MIN / b) return -1;
if (a < 0 && b < 0 && a < INT_MAX / b) return -1;
*result = a * b;
return 0;
}
int main() {
int result;
if (safe_add(INT_MAX, 1, &result) == 0) {
printf("INT_MAX + 1 = %d\n", result);
} else {
printf("INT_MAX + 1: overflow detected!\n");
}
if (safe_multiply(1000000, 1000000, &result) == 0) {
printf("1000000 * 1000000 = %d\n", result);
} else {
printf("1000000 * 1000000: overflow detected!\n");
}
if (safe_add(100, 200, &result) == 0) {
printf("100 + 200 = %d\n", result);
}
return 0;
}运行结果:
INT_MAX + 1: overflow detected!
1000000 * 1000000: overflow detected!
100 + 200 = 300四、本章小结
第2章系统地介绍了C语言的类型系统、运算符和表达式:
数据类型:C语言提供了 char、int、float、double 四种基本类型及其多种变体。sizeof 运算符可以查询类型的大小。不同平台上的类型大小可能不同,编写可移植代码时应使用 <stdint.h> 中的固定宽度类型。
运算符:C语言提供了丰富的运算符——算术、关系、逻辑、位运算、赋值、自增自减等。理解运算符的优先级和结合性是正确编写表达式的基础。
类型转换:隐式转换在整型提升和算术运算中自动发生,但可能导致意外的精度丢失或逻辑错误。强制类型转换可以显式控制转换过程。
表达式求值:C语言中某些表达式的求值顺序是未指定的,某些表达式甚至具有未定义行为。避免在同一个表达式中多次修改同一个变量。
位运算:&、|、^、~、<<、>> 直接操作二进制位,在权限掩码、特征标志位、底层硬件操作等场景中非常有用。
类型系统是C语言的基石。理解类型的大小、范围、转换规则,是避免bug、编写健壮程序的关键。下一章将学习如何使用控制流语句来组织程序的逻辑结构。
五、思考与练习
思考题
为什么 sizeof(char) 总是1?sizeof(int) 在不同平台上可能不同,这对程序的可移植性有什么影响?
解释 unsigned int x = -1; 的含义。x 的值是多少?为什么?
为什么 i = i++ + 1 是未定义行为?编译器可以做出哪些"合理"的处理?
位运算 x ^ x 的结果是什么?x & ~x 呢?x | ~x 呢?
隐式类型转换在什么情况下会导致数据丢失?如何避免?
编程练习
编写一个程序:打印出所有基本数据类型的大小和取值范围(使用 <limits.h> 和 <float.h> 中的常量)。
实现一个函数:统计一个整数的二进制表示中有多少个1(提示:使用 n & (n-1) 可以消除最低位的1)。
编写一个程序:不使用临时变量,交换两个整数的值(提示:使用异或运算)。
实现一个简单的位图(bitmap):支持设置位、清除位、测试位操作,用于管理一组布尔标志。