2C程序设计语言(第2版)

类型、运算符与表达式

数据的表示与操作

数据类型运算符类型转换表达式

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 整型家族

c
char        // 至少8位,通常恰好8位
short       // 至少16位
int         // 至少16位,通常32位
long        // 至少32位
long long   // 至少64位(C99引入)

每种整型都可以加上 signed(有符号)或 unsigned(无符号)修饰符:

c
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。

c
#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 浮点型

c
float       // 通常32位,IEEE 754单精度,约7位有效数字
double      // 通常64位,IEEE 754双精度,约15位有效数字
long double // 通常80位或128位,精度取决于平台

浮点数在内存中的表示遵循IEEE 754标准,由符号位、指数位和尾数位组成。这种表示方法可以在很大的范围内表示数值,但会牺牲精度。

c
#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 是一个编译时运算符(不是函数),它返回类型或变量在内存中占用的字节数。

c
#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 整型常量

c
42          // 十进制
052         // 八进制(以0开头)
0x2A        // 十六进制(以0x开头)
42U         // unsigned int
42L         // long
42UL        // unsigned long
42LL        // long long(C99)

2.2.2 浮点型常量

c
3.14        // double
3.14f       // float(后缀f或F)
3.14L       // long double(后缀l或L)
1.0e-5      // 科学计数法:1.0 × 10^(-5)

2.2.3 字符常量

c
'A'         // 字符常量,值为65(ASCII码)
'\n'        // 转义字符:换行
'\x41'      // 十六进制表示:ASCII 0x41 = 'A'
'\0'        // 空字符

字符常量 vs 字符串常量:'A' 是字符常量,类型为 int(C语言)或 char(C++),值为65。"A" 是字符串常量,是一个包含 'A' 和 '\0' 的字符数组,类型为 char[2]。

c
sizeof('A')     // C语言中为 sizeof(int),通常4
sizeof("A")     // 2(包含 '\0')

2.2.4 枚举常量

c
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 算术运算符

c
+       // 加法
-       // 减法
*       // 乘法
/       // 除法(整数除法向零取整)
%       // 取模(仅适用于整数)

整数除法的陷阱:

c
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规定余数符号与被除数相同)

取模运算符的应用:

c
// 判断奇偶
if (n % 2 == 0) { /* n是偶数 */ }

// 提取个位数
int last_digit = n % 10;

// 循环索引(环形缓冲区)
index = (index + 1) % size;

2.3.2 关系运算符和逻辑运算符

c
==      // 等于
!=      // 不等于
>       // 大于
<       // 小于
>=      // 大于等于
<=      // 小于等于

&&      // 逻辑与
||      // 逻辑或
!       // 逻辑非

短路求值:&& 和 || 运算符具有短路特性。对于 a && b,如果 a 为假,b 不会被求值。对于 a || b,如果 a 为真,b 不会被求值。

c
// 安全的空指针检查
if (ptr != NULL && *ptr > 0) {
    // 如果 ptr 为 NULL,*ptr 不会被求值,避免段错误
}

// 字符串比较中的短路
if (s != NULL && strcmp(s, "hello") == 0) {
    // 安全
}

常见错误:混淆 = 和 ==。

c
int x = 5;
if (x = 3) {      // 错误!这是赋值,不是比较
    // x 被赋值为3,条件为真(3 != 0)
}
if (x == 3) {     // 正确
    // 比较 x 是否等于3
}

2.3.3 位运算符

位运算符直接操作二进制位,是C语言接近硬件的体现:

c
&       // 按位与(AND)
|       // 按位或(OR)
^       // 按位异或(XOR)
~       // 按位取反(NOT,一元运算符)
<<      // 左移
>>      // 右移

位运算的实际应用:

c
// 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) { /* 有执行权限 */ }

左移和右移:

c
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 赋值运算符

c
=       // 简单赋值
+=      // 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 是复杂表达式(如数组元素)时很重要。

c
a[expr] += 1;   // expr 只求值一次
a[expr] = a[expr] + 1;  // expr 求值两次(如果 expr 有副作用,结果可能不同)

2.3.5 自增自减运算符

c
++i     // 前缀形式:先加1,再返回新值
i++     // 后缀形式:先返回旧值,再加1
--i     // 前缀形式:先减1,再返回新值
i--     // 后缀形式:先返回旧值,再减1
c
int i = 5;
int a = ++i;    // i变为6,a=6
int b = i++;    // b=6,i变为7

K&R的建议:在表达式中避免使用 ++,因为它可能导致令人困惑的行为。但在循环的步进部分(如 for (i = 0; i < n; i++))和数组遍历中,++ 是惯用的写法。

2.4 类型转换

2.4.1 隐式类型转换

C语言会在以下情况自动进行类型转换:

整型提升(Integer Promotion):char、short 在参与运算时会自动提升为 int。

c
char a = 100;
char b = 50;
int c = a + b;    // a和b先提升为int,再相加

算术转换(Arithmetic Conversion):当不同类型参与运算时,较小类型会自动转换为较大类型。

c
int a = 10;
float b = 3.14;
double c = a + b;  // a先转换为float,再转换为double

转换规则(从低到高):

char/short → int → long → long long → float → double → long double

危险的隐式转换:

c
unsigned int a = 10;
int b = -5;

if (a < b) {
    printf("a < b\n");   // 不会执行
} else {
    printf("a >= b\n");  // 会执行!
}
// 原因:b被隐式转换为unsigned,-5变成了一个很大的正数
c
int i = 2147483647;    // INT_MAX
i = i + 1;             // 有符号整数溢出:未定义行为!

2.4.2 强制类型转换

c
(type) expression
c
int 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语言中,大多数运算符的操作数求值顺序是未指定的。编译器可以选择先求值左操作数或右操作数。

c
int i = 0;
int a = f() + g();   // f()和g()的调用顺序未指定

更严重的是,某些表达式的行为是未定义的(undefined behavior):

c
a[i] = i++;           // 未定义行为:i在同一个表达式中被修改和使用
printf("%d %d", i, ++i);  // 未定义行为
x = a + (a = 2);      // 未定义行为

规则:如果一个表达式中某个变量被修改(通过 ++、--、赋值等),那么这个变量在该表达式中最多只能被引用一次(用于确定要存储的值)。违反此规则的代码具有未定义行为。

c
// 安全的写法
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 案例一:位操作实现集合

c
#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 7

3.2 案例二:进制转换器

c
#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: 0x2A

3.3 案例三:安全的类型转换工具

c
#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):支持设置位、清除位、测试位操作,用于管理一组布尔标志。

2/8类型、运
0%