C: 常量与类型转换
常量就像数学里的圆周率——值一旦确定就不能改变。而类型转换就像货币兑换,不同币种之间有固定汇率,但换算时要注意精度损耗。
1. 字面量
字面量(Literal)是代码中直接写出来的固定值,不需要计算,一看就知道是多少。
(1) 整数字面量
int a = 42;
int b = 0xFF;
int c = 052;
int d = 0b101010;
| 进制 | 前缀 | 示例 | 十进制值 |
|---|---|---|---|
| 十进制 | 无 | 42 |
42 |
| 十六进制 | 0x 或 0X |
0xFF |
255 |
| 八进制 | 0 |
052 |
42 |
| 二进制(C99GNU扩展/C23标准) | 0b 或 0B |
0b101010 |
42 |
💡 八进制陷阱:不小心在数字前写0会被当成八进制。int x = 010; 的值是8而不是10,这是一个经典的初学者bug。
整数字面量默认类型是 int,如果超出int范围会自动变成 long 或 long long。你也可以手动指定:
| 后缀 | 含义 | 示例 |
|---|---|---|
u 或 U |
unsigned | 42U |
l 或 L |
long | 42L |
ll 或 LL |
long long | 42LL |
UL |
unsigned long | 42UL |
⚠️ 建议:用大写 L 而非小写 l,因为 l 容易和数字 1 混淆。42l 看起来像 421。
(2) 浮点字面量
double a = 3.14;
double b = 2.0e8;
double c = 1.5E-3;
float d = 3.14f;
浮点字面量默认类型是 double。加 f 或 F 后缀表示 float,加 l 或 L 表示 long double。
🔥 常见错误:
float f = 3.14;
3.14 是 double 字面量,赋值给 float 变量时会发生隐式截断,编译器可能发出警告。正确写法:
float f = 3.14f;
(3) 字符字面量
用单引号包裹的单个字符:
char ch = 'A';
char newline = '\n';
char zero = '\0';
字符字面量的类型是 int(不是 char),这在C中是一个容易忽略的细节:
printf("%zu\n", sizeof('A'));
4
在C中 sizeof('A') 是4(int的大小),而在C++中是1(char的大小)。这是C和C++的一个微妙差异。
(4) 字符串字面量
用双引号包裹的字符序列:
printf("Hello, World!\n");
字符串字面量的类型是 char[],编译器会自动在末尾添加 \0 作为结束标志。所以 "abc" 实际占4个字节(a、b、c、\0)。
⚠️ 重要:字符串字面量存储在只读内存区域,尝试修改会导致未定义行为(通常是程序崩溃):
char *s = "hello";
s[0] = 'H';
2. const修饰符
const 是C语言的关键字,用来修饰变量为"只读"——初始化后不能再修改:
const int MAX_SIZE = 100;
const float PI = 3.14159f;
const char NEWLINE = '\n';
试图修改const变量会导致编译错误:
const int MAX_SIZE = 100;
MAX_SIZE = 200;
error: assignment of read-only variable 'MAX_SIZE'
(1) const与数组
const变量可以用来指定数组大小吗?
const int SIZE = 10;
int arr[SIZE];
⚠️ 注意:在C89中,这不合法——数组大小必须是编译期常量,而 const int 在C中不是编译期常量(它只是运行期只读变量)。C99引入了变长数组(VLA),所以上面的代码在C99下可以编译,但VLA是可选特性,并非所有编译器都支持。
如果你需要一个真正的编译期常量来定义数组大小,用 #define 或 enum。
(2) const与指针
const与指针的组合是C语言的经典难点,有三种情况:
int value = 42;
const int *p1 = &value;
int * const p2 = &value;
const int * const p3 = &value;
| 声明 | 含义 |
|---|---|
const int *p |
指针指向的数据不可修改,指针本身可以指向别处 |
int * const p |
指针本身不可修改(固定指向一处),指向的数据可以修改 |
const int * const p |
指针和指向的数据都不可修改 |
💡 记忆技巧:看 const 在 * 的左边还是右边。左边修饰数据,右边修饰指针。
3. #define宏常量
#define 是预处理器指令,用来定义宏常量:
#define PI 3.14159
#define MAX_SIZE 100
#define NEWLINE '\n'
预处理器会在编译前将代码中所有 PI 替换为 3.14159。它不是变量,不占内存,没有类型检查。
(1) #define vs const
| 特性 | #define |
const |
|---|---|---|
| 处理时机 | 预处理阶段(文本替换) | 编译阶段 |
| 类型检查 | 无 | 有 |
| 作用域 | 从定义到文件末尾 | 遵循变量作用域规则 |
| 调试 | 调试器看不到宏名 | 调试器可以看到const变量 |
| 可取地址 | 不可以 | 可以 |
💡 建议:优先用 const,因为它有类型检查和作用域限制,更安全。#define 适合定义条件编译标志或需要编译期常量的场景。
(2) 宏定义的常见陷阱
#define DOUBLE(x) x + x
int result = DOUBLE(3) * 2;
你期望 3 + 3 * 2 = 12,实际展开为 3 + 3 * 2 = 9。宏是纯文本替换,不考虑运算优先级。
修正:给宏加括号:
#define DOUBLE(x) ((x) + (x))
4. 枚举常量
enum 是定义整数常量的另一种方式:
enum Color {
RED,
GREEN,
BLUE
};
默认从0开始递增:RED = 0,GREEN = 1,BLUE = 2。你可以手动指定值:
enum Weekday {
MON = 1,
TUE,
WED,
THU,
FRI,
SAT = 100,
SUN
};
TUE = 2,WED = 3,...,SUN = 101(在前一个值基础上加1)。
枚举值是真正的编译期常量,可以用于数组大小等场景。相比 #define,枚举有类型信息,调试时能看到名字,推荐优先使用。
5. 隐式类型转换
当不同类型的数据参与运算时,C语言会自动进行类型转换。理解隐式转换规则,是避免bug的关键。
(1) 整数提升
在表达式中,char 和 short 会自动提升为 int 再参与运算:
char a = 10;
char b = 20;
printf("%zu\n", sizeof(a + b));
4
a + b 的结果是 int 类型,占4字节,而不是1字节的char。
(2) 算术转换
当不同类型的值进行运算时,较低类型会自动转换为较高类型。转换等级从低到高:
int → unsigned int → long → unsigned long → long long → unsigned long long → float → double → long double
规则:运算前,较低等级的操作数自动转换为较高等级。
int a = 5;
double b = 2.0;
printf("%.1f\n", a / b);
2.5
a 被自动转换为 double,然后执行浮点除法,结果为2.5。
(3) 赋值转换
赋值时,右值自动转换为左值的类型:
int x = 3.14;
double y = 42;
3.14 赋值给 int 变量,截断为 3;42 赋值给 double,变成 42.0。
⚠️ 浮点→整数截断:浮点数赋值给整数类型时,小数部分直接丢弃(不是四舍五入):
int n = 2.9;
n 的值是2,不是3。
(4) 整数除法陷阱
这是初学者最容易踩的坑:
int a = 5;
int b = 2;
double result = a / b;
printf("%.1f\n", result);
2.0
你期望2.5,但得到2.0。原因:a / b 是整数除法,结果为2(整数),然后2才被转换为double的2.0。
修正方法:让至少一个操作数为浮点数:
double result = (double)a / b;
或者:
double result = a * 1.0 / b;
或者:
double result = 5.0 / 2;
6. 显式强制转换
当隐式转换不符合你的需要时,可以用强制类型转换(cast)明确告诉编译器你要转换类型:
(1) 语法
(类型名)表达式
▶ 示例
#include <stdio.h>
int main(void) {
int a = 7;
int b = 2;
printf("整数除法: %d\n", a / b);
printf("强制转换: %.2f\n", (double)a / b);
return 0;
}
整数除法: 3
强制转换: 3.50
(double)a 先把a转为7.0,然后与b运算时b也被隐式提升为double,执行浮点除法。
▶ 示例
#include <stdio.h>
#include <math.h>
int main(void) {
double x = 3.7;
printf("直接截断: %d\n", (int)x);
printf("四舍五入: %d\n", (int)(x + 0.5));
printf("round函数: %.0f\n", round(x));
return 0;
}
直接截断: 3
四舍五入: 4
round函数: 4
💡 负数的截断:C99规定浮点→整数的截断是"向零取整"。所以 (int)(-3.7) 的结果是 -3,不是 -4。这与"向下取整"不同,需要注意。
7. 类型转换的常见陷阱
(1) 有符号与无符号混合
int a = -5;
unsigned int b = 10;
if (a + b > 0) {
printf("大于0\n");
} else {
printf("小于等于0\n");
}
大于0
你可能以为 -5 + 10 = 5 > 0 输出"大于0"是对的,但这段代码的行为其实取决于隐式转换规则。当 int 和 unsigned int 混合运算时,int 会被转换为 unsigned int。-5 转为无符号后变成一个很大的正数,所以结果确实大于0——但这可能不是你期望的逻辑。
⚠️ 核心规则:永远不要将有符号和无符号整数混合运算。如果必须,显式转换。
(2) 精度丢失
int big = 123456789;
float f = big;
printf("%d\n", big);
printf("%.0f\n", f);
123456789
123456792
float 只有6-7位有效数字,无法精确表示9位整数,末尾3位被丢失。把大整数赋给float时要特别小心。
(3) 溢出转换
char ch = 200;
printf("%d\n", ch);
在 char 为有符号的平台上,200超出了char的范围(-128~127),结果是未定义行为,可能输出-56。如果需要存储0~255的值,用 unsigned char。
(4) 指针转换
不同类型的指针之间直接强制转换是危险的:
int value = 0x41424344;
char *p = (char *)&value;
printf("%c\n", *p);
输出取决于系统的字节序(大端序还是小端序),结果不可移植。这种转换只在底层编程中使用,初学阶段应该避免。
❓ 常见问题
📖 小节
- 字面量分整型、浮点、字符、字符串四种,注意后缀(f/L/UL)和进制前缀(0x/0/0b)
- const修饰变量为只读,优先用const替代#define;#define是预处理文本替换,无类型检查
- 隐式转换遵循整数提升和算术转换规则,低类型自动转高类型
- 整数除法是C语言最常见陷阱:5/2=2,要得2.5必须至少一个操作数为浮点
- 有符号与无符号混合运算会导致意外结果,应避免或显式转换
📝 作业
- 编写程序,分别用十进制、十六进制、八进制输出数字255,观察输出格式
- 编写程序,计算圆的面积(半径用int类型10,圆周率用const double定义),注意避免整数除法陷阱
- 编写程序,测试以下情况并记录结果:(int)3.9的值、(int)(-3.9)的值、char ch = 128后%d输出的值,解释每个结果的原因