第二章 关于类型和变量,我还想说这些
本章概览
在第一章中,我们认识了变量——一块被命名的存储空间,用来存放数据,它的值可以随程序运行而改变。变量之所以叫“变量“,正是因为它可以“变化“。
但一个很自然的追问是:有没有不变量?——有没有一种数据,它也有名字、也占存储空间,但一旦被赋予了一个值之后,就再也不允许被改变?
答案是有的。C 语言给“变量“这个概念附加了丰富的内涵。一个变量除了可以存储数据,还可以被施加额外的约束:它可以被标记为 const(只读),不允许在初始化之后再修改。
此外,数据的类型也不是铁板一块——在不同类型之间,变量可以按照规则进行类型转换。
本章围绕这两个主题展开:
- const——让一个变量在初始化之后不可修改,回答“有没有不变量“的问题。
- 类型转换——在不同类型之间架设桥梁,让数据在需要时以另一种类型的面貌参与运算。
它们不创造新的数据类型,也不引入新的程序结构。它们只是对“变量“这个你已经熟悉的概念,进行一次更深入的审视和补充。
一、const —— 只读的契约
1.1 不变的变量
在第一章中,我们使用的变量都是可读可写的——你可以随时修改它的值。但实际编程中,有些数据一旦确定下来,就不应该再被改变:
- 数学常量:比如圆周率 π
- 物理常量:一天的小时数(24)
- 运行时确定的配置:程序开头计算出来的、后续不应改变的参数
如果你或他人在代码中意外修改了这些值,程序的行为就会变得难以预测。
C 语言提供了 const 关键字来解决这个问题。在变量声明时加上 const,意味着“这个变量在初始化之后不能被修改“:
const double PI = 3.1415926;
const int HOURS_PER_DAY = 24;
// PI = 3.14; // 编译错误!不能修改 const 变量
// HOURS_PER_DAY = 12; // 编译错误!
实际编程中,const 对象应当在声明时完成初始化,否则之后不能再用赋值语句补上初值。需要精确区分的是:下面第一行声明本身在块作用域中是合法的,但 MAX 具有不确定值,读取它会产生未定义行为;第二行赋值才是编译器必须诊断的问题:
const int MAX; // 语法合法,但值不确定,不能读取
MAX = 100; // 错误:不能在声明之后给 const 对象赋值
从语义上说,const 变量更像一个不可修改的承诺。你告诉编译器(也告诉阅读代码的人):“这个值在这里确定下来之后,整个程序中再也不会变。“编译器会帮助你强制执行这个承诺——任何试图修改 const 变量的代码都会触发编译错误,而不是在运行时才暴露问题。
二、强制类型转换
2.1 隐式转换:编译器帮你做的“自动换算“
在第一章中,我们学过整数除法的规则:两个整数相除,结果是整数,小数部分被截断。如果想得到小数结果,需要用 7.0 / 3 这样的写法。这背后的机制就是隐式类型转换——编译器在运算双方类型不匹配时,会自动将其中一方“提升“或“转换“为另一方的类型。
常见的隐式转换场景:
#include <stdio.h>
int main(void)
{
// 整数 → 浮点数(自动提升)
int a = 5;
double b = 2.0;
double result = a / b; // a 被自动转换为 5.0,再执行浮点除法
printf("5 / 2.0 = %.2f\n", result); // 2.50
// 赋值时的隐式转换
double pi = 3.14159;
int truncated = (int)pi; // 显式表达有意丢弃小数部分,truncated = 3
printf("pi 截断为 int:%d\n", truncated);
// 混合运算中的提升
int x = 10;
long y = 200;
// x + y 的结果是 long 类型(int 被提升为 long)
printf("sizeof(x + y) = %d\n", (int)sizeof(x + y));
return 0;
}
隐式转换遵循一套明确的规则(“普通算术转换”),但初学阶段只需记住最核心的一点:当不同类型的数值混合运算时,编译器会自动向“更大“或“更精确“的类型转换——int 向 long 和 double 转换,float 向 double 转换。
隐式转换虽然方便,但也可能在不经意间造成精度丢失(如浮点数赋值给整数时的小数截断)或者意料之外的运算结果(如两个 int 相除本期望得到小数,却忘了至少转一个为浮点)。因此,很多时候显式地标明类型转换,比依赖隐式行为更安全。
2.2 显式类型转换:(type) 语法
当隐式转换不够精确,或者想明确表达转换意图时,可以使用强制类型转换(cast)。它的语法很简单:
(目标类型) 表达式
基本用法示例:
#include <stdio.h>
int main(void)
{
// 确保浮点除法
int a = 5, b = 2;
double result = (double)a / b; // 把 a 显式转为 double,b 自动跟着提升
printf("5 / 2 = %.2f\n", result); // 2.50
// 截断小数
double pi = 3.14159;
int int_pi = (int)pi; // 显式截断,int_pi = 3
printf("(int)3.14159 = %d\n", int_pi);
// 在整数运算中保留精度
int total = 100, count = 3;
double avg = (double)total / count;
printf("平均 = %.2f\n", avg); // 33.33 而不是 33.00
return 0;
}
(double)a 对 a 本身没有任何影响——a 仍然是 int 类型,值为 5。转换只产生一个临时值参与后续运算。这就像你查词典把一个英文词翻译成中文——原文不变,只是你用译文去接续下一步工作。
2.3 常见应用场景
强制类型转换在实际编程中频繁出现,以下是几个最常见的场景:
场景一:整数除法中取得小数结果。 这已经在前面的例子中演示过了。要点是:只要在除法表达式中的任何一个操作数上使用 (double) 或 (float) 转换,整个除法就会按浮点运算执行。
场景二:在 printf 中使用正确的占位符。 比如 sizeof 的返回值类型是 size_t,要使输出在不同平台上一致,有时需要显式转换:
printf("数组大小:%d 字节\n", (int)sizeof(arr)); // 显式转为 int
场景三:与 malloc 返回值配合(将在堆内存章节详述)。 malloc 返回的是 void * 类型,可以直接赋值给任意指针类型。但在一些编码规范中,习惯显式写出转换以增强可读性。
场景四:浮点数取整。 (int) 转换直接丢弃小数部分(向零截断),不同于四舍五入。如果需要对负数取整,需注意 (int)(-3.7) 的结果是 -3 而非 -4。
注意:强制类型转换并非万能。C 只允许标准规定的一组转换,例如算术类型之间的转换、满足约束的指针转换等;结构体不能直接转换成
int,数组也不能整体转换成double。“兼容类型”在 C 标准中另有严格含义,因此不要把强制转换笼统理解成只发生在“兼容类型”之间。转换也不会自动保证数值不丢失或指针可安全解引用。
三、动手练习
#include <stdio.h>
/* ============================================
* 练习1:const 变量的使用
* ============================================ */
int main(void)
{
// 声明 const 变量并尝试修改(观察编译错误)
const int MAX_STUDENTS = 50;
printf("最大学生人数:%d\n", MAX_STUDENTS);
// MAX_STUDENTS = 60; // 取消注释会触发编译错误
printf("\n");
/* ============================================
* 练习2:类型转换实战——精确除法与取整
* ============================================ */
int numerator = 22, denominator = 7;
// 整数除法(丢失精度)
printf("整数除法:%d / %d = %d\n", numerator, denominator,
numerator / denominator);
// 显式转换,保留精度
printf("浮点除法:%d / %d = %.4f\n", numerator, denominator,
(double)numerator / denominator);
// 浮点数转整数
double value = 9.87;
printf("(int)%.2f = %d\n", value, (int)value);
// 四舍五入的简单实现(正数)
double pi = 3.14159;
int rounded = (int)(pi * 100 + 0.5); // 先放大100倍,加0.5再截断
printf("pi 保留两位小数:%d.%02d\n", rounded / 100, rounded % 100);
return 0;
}