Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第二章 关于类型和变量,我还想说这些

本章概览

在第一章中,我们认识了变量——一块被命名的存储空间,用来存放数据,它的值可以随程序运行而改变。变量之所以叫“变量“,正是因为它可以“变化“。

但一个很自然的追问是:有没有不变量?——有没有一种数据,它也有名字、也占存储空间,但一旦被赋予了一个值之后,就再也不允许被改变?

答案是有的。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;
}

隐式转换遵循一套明确的规则(“普通算术转换”),但初学阶段只需记住最核心的一点:当不同类型的数值混合运算时,编译器会自动向“更大“或“更精确“的类型转换——intlongdouble 转换,floatdouble 转换。

隐式转换虽然方便,但也可能在不经意间造成精度丢失(如浮点数赋值给整数时的小数截断)或者意料之外的运算结果(如两个 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)aa 本身没有任何影响——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;
}