【C语言-常量】C语言中的常量——那些"程序运行中雷打不动"的值

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 295
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

今天咱们把常量从头到尾捋一遍,不光是"常量就是不变的值"这么一句废话,咱们把每种常量在内存里怎么存的、怎么用的、跟变量有啥区别,全部拆开揉碎了讲。


一、啥是常量?就是"写死了的值"

说白了,常量就是程序运行过程中不能被改变的值。你写int a = 10;这里的10就是常量。你写printf("Hello");这里的"Hello"也是常量。它们的特点是:编译的时候就已经确定了,程序跑起来之后不会变。

跟变量做个对比你就清楚了:

  • 变量:有名字,有内存空间,里面的值可以改来改去。

  • 常量:也有值,但没有自己独立的内存空间(除非你用const),值写死了不能改。

C语言里的常量大概分这么几类:整型常量、浮点型常量、字符常量、字符串常量、符号常量(用#define定义的)、const修饰的常变量、枚举常量。咱们一个个说。


二、字面常量——直接写在代码里的值

这些是你最常打交道的,就是代码里那些"赤裸裸"的数字和字符。

1. 整型常量

就是整数。比如10、-5、0、255这些都是。

整型常量默认是int类型。但你可以加后缀来指定它是什么类型:

  • 10 默认是int

  • 10L 或 10l 是long

  • 10LL 或 10ll 是long long

  • 10U 或 10u 是unsigned int

  • 10UL 是unsigned long

还可以用不同进制表示:

  • 十进制:10

  • 八进制:012(以0开头,表示八进制的12,也就是十进制的10)

  • 十六进制:0xA(以0x开头,表示十六进制的A,也就是十进制的10)

#include <stdio.h>

int main() {
    int a = 10;        // 十进制
    int b = 012;       // 八进制,值是10
    int c = 0xA;       // 十六进制,值是10

    printf("%d %d %d\n", a, b, c);   // 输出:10 10 10

    // 各种后缀
    long d = 100L;
    unsigned int e = 200U;
    long long f = 300LL;

    printf("%ld %u %lld\n", d, e, f);   // 100 200 300

    return 0;
}

2. 浮点型常量

就是带小数点的数,比如3.14、-0.5、2.0。默认是double类型。

加后缀可以指定类型:

  • 3.14 默认是double

  • 3.14f 或 3.14F 是float

  • 3.14L 是long double

科学计数法也可以:

  • 1.2e3 表示1.2×10³,也就是1200

  • 1.2e-3 表示1.2×10⁻³,也就是0.0012

#include <stdio.h>

int main() {
    double pi = 3.14159;           // double常量
    float f = 3.14f;               // float常量
    long double ld = 3.14159L;     // long double常量

    double big = 1.5e8;            // 1.5亿
    double small = 1.5e-8;         // 0.000000015

    printf("%lf %f %Lf\n", pi, f, ld);
    printf("%lf %lf\n", big, small);

    return 0;
}

3. 字符常量

用单引号括起来的单个字符,比如'A'、'5'、'#'。字符常量实际上存的是它的ASCII码值(整数),所以你可以把它当整数用。

#include <stdio.h>

int main() {
    char ch = 'A';
    printf("%c %d\n", ch, ch);   // 输出 A 65

    // 字符常量本质上是整数
    int num = 'A';
    printf("%d\n", num);         // 输出 65

    // 转义字符——特殊字符常量
    char newline = '\n';
    char tab = '\t';
    char single = '\'';          // 单引号本身
    char backslash = '\\';       // 反斜杠本身

    printf("%c%c%c\n", single, tab, backslash);

    return 0;
}

常见的转义字符你得记住几个:

  • \n 换行

  • \t 制表符(Tab)

  • \\ 反斜杠

  • \' 单引号

  • \" 双引号

  • \0 空字符(字符串结束标志)

  • \r 回车

4. 字符串常量

用双引号括起来的一串字符,比如"Hello"、"C语言"。字符串常量在内存里是一串字符加一个\0结尾。字符串常量本质上是一个地址,指向存储字符串的那块内存。

#include <stdio.h>

int main() {
    // 字符串常量
    char *p = "Hello";   // p指向常量字符串
    char str[] = "World"; // 字符串被拷贝到数组里

    printf("%s\n", "Hello");   // 字符串常量直接当参数传

    // 注意:字符串常量不能修改
    // p[0] = 'h';   // 这行会崩溃,因为常量字符串在只读内存区

    str[0] = 'w';      // 数组里的可以改
    printf("%s\n", str);   // 输出 "world"

    return 0;
}

这里有个很重要的点:字符串常量是只读的,不能修改。 你写char *p = "Hello"; p[0] = 'h';程序会崩,因为"Hello"这个字符串在编译后放在只读数据段里。但如果你用数组char str[] = "World";,它会把字符串拷贝一份到栈上,这就可以改。

三、符号常量——#define定义的名字

#define是预处理指令,在编译之前先把代码里的名字替换成对应的值。它的本质是"文本替换"。

#include <stdio.h>

#define PI 3.14159
#define MAX_STUDENTS 100
#define HELLO_MSG "你好,世界"

int main() {
    double radius = 5.0;
    double area = PI * radius * radius;

    printf("面积:%lf\n", area);
    printf("最多学生人数:%d\n", MAX_STUDENTS);
    printf("%s\n", HELLO_MSG);

    // PI = 3.14;   // 这行会报错,PI不是变量

    return 0;
}

#define的特点:

  • 它只是一个"文本替换",不分配内存空间,没有类型信息。

  • 预处理器在编译之前就把所有的PI替换成了3.14159,所以编译器真正看到的是3.14159 * radius * radius。

  • 因为没有类型,所以不会做类型检查,可能会有隐患。

  • 好处是改一个地方,整个程序里所有的PI都变了,方便维护。

一个常见用法: 定义数组大小

#define SIZE 100

int main() {
    int arr[SIZE];   // 数组长度是100
    // ... 使用arr
    return 0;
}

如果你想修改数组大小,只改#define SIZE那一行就行了。

四、const修饰的常变量

const关键字可以把一个变量声明成"只读",也就是它的值初始化之后就不能再改了。

#include <stdio.h>

int main() {
    const int MAX = 100;   // const常变量
    // MAX = 200;   // 这行报错,不能修改

    const double PI = 3.14159;

    // 注意:const变量必须在声明时初始化
    // const int a;   // 错误,没初始化,a的值是不确定的

    printf("%d %lf\n", MAX, PI);

    // const变量也可以用在数组长度里(C99之后可以)
    const int n = 10;
    int arr[n];   // 有些编译器支持,C标准里这叫变长数组

    return 0;
}

const和#define的区别,这可能是最常被问到的:

 
  #define const
什么时候处理 预处理阶段 编译阶段
分配内存吗 不分配 分配
有类型吗 没有 有
做类型检查吗 不做 做
能不能取地址 不能 能
作用范围 全局,从定义处到文件尾 遵循C语言的作用域规则

光看表可能有点抽象,咱们用代码演示一下:

#include <stdio.h>

#define A 10
const int B = 20;

int main() {
    // #define没有类型,是纯文本替换
    // 所以编译器看到A就是10,没有任何类型信息

    // const有类型,是int,会做类型检查
    const int c = 30;

    // const变量可以取地址
    const int *p = &B;   // 合法
    printf("%p\n", p);

    // #define不能取地址
    // int *q = &A;   // 报错,A不是变量

    // const变量在内存里有一个实际的位置
    // #define不存在于内存里

    return 0;
}

那什么时候用#define,什么时候用const呢?我个人的建议是:

  • 定义数组长度、宏函数这种,用#define。

  • 定义真正的常量(有类型、需要类型检查、可能取地址的),用const。

  • 现代C编程推荐多用const,少用#define,因为const有类型检查,更安全。

五、枚举常量

枚举(enum)是C语言里一种"给整数起名字"的方式。它定义了一组具名的整数常量。

#include <stdio.h>

enum Weekday {
    MON,     // 默认值是0
    TUE,     // 1
    WED,     // 2
    THU,     // 3
    FRI,     // 4
    SAT,     // 5
    SUN      // 6
};

enum Color {
    RED = 10,   // 手动指定值
    GREEN = 20,
    BLUE = 30
};

enum Month {
    JAN = 1,    // 从1开始
    FEB,
    MAR,
    APR
};

int main() {
    enum Weekday today = WED;
    printf("%d\n", today);   // 输出 2

    enum Color c = RED;
    printf("%d\n", c);       // 输出 10

    enum Month m = MAR;
    printf("%d\n", m);       // 输出 3

    // 可以用枚举值做比较
    if (today == WED) {
        printf("今天是周三\n");
    }

    return 0;
}

枚举的本质就是一串整数常量,默认从0开始递增。你指定了某个值,后面的跟着递增。枚举的好处是让代码可读性更好——写today = WED比写today = 2更容易让人明白是什么意思。

另外,枚举常量是有作用域的,在函数外面定义的话整个文件都能用,在函数里面定义的话只有那个函数能用。

六、常量指针和指针常量——这俩容易搞混

const用在与指针相关的地方时,有两种不同的含义。很多同学学到指针的时候被const和指针的组合搞得一头雾水,咱们把它们拆开说清楚。

#include <stdio.h>

int main() {
    int a = 10;
    int b = 20;

    // 1. 指向常量的指针:指针指向的内容不能改,但指针本身可以指向别处
    const int *p1 = &a;
    // *p1 = 30;   // 错误,不能通过p1修改a的值
    p1 = &b;        // 合法,p1可以指向别处

    // 2. 指针常量:指针本身不能改,但指向的内容可以改
    int *const p2 = &a;
    *p2 = 30;        // 合法,可以修改a的值
    // p2 = &b;      // 错误,p2本身不能修改

    // 3. 指向常量的指针常量:内容和指针都不能改
    const int *const p3 = &a;
    // *p3 = 30;     // 错误
    // p3 = &b;      // 错误

    return 0;
}

记口诀:看const在星号(*)的左边还是右边。在左边表示"指向的内容不能改",在右边表示"指针本身不能改"。

  • const int *p; = 指向常量的指针(内容只读)

  • int *const p; = 指针常量(地址只读)

  • const int *const p; = 内容只读,地址也只读

七个易踩的坑:

坑1:试图修改字符串常量

char *p = "Hello";
p[0] = 'h';   // 崩溃!字符串常量在只读区

正确做法:如果你需要修改,用数组存。

char str[] = "Hello";
str[0] = 'h';   // 安全,str是数组,在栈上

坑2:#define后面多加了个分号

#define PI 3.14159;   // 注意这个分号!

int main() {
    double area = PI * 5 * 5;   // 展开后是 3.14159; * 5 * 5,语法错误
    return 0;
}

#define的结尾不需要分号,加了分号就变成了值的一部分,会导致编译错误。

坑3:const变量没初始化

const int a;   // 错误,const变量必须在声明时初始化
a = 10;        // 不行,后面赋值也不允许

 坑4:#define没有作用域限制

#define从定义的位置开始,到文件末尾都有效(或者被#undef取消),不受花括号限制。这可能导致命名冲突。

void func() {
    #define TEMP 100
}
// TEMP在这里依然有效,会污染全局命名空间

而const是有作用域的,在函数里的const只在函数里有效。

 坑5:整型常量溢出

int a = 2147483647 + 1;   // 溢出,结果变成-2147483648

注意整数范围,别溢出了。

坑6:把'0'和'\0'搞混了

  • '0' 是字符0,ASCII码是48

  • '\0' 是空字符,ASCII码是0,字符串结束标志

char ch1 = '0';   // 48
char ch2 = '\0';  // 0

        坑7:const和#define混用风格不一致

        同一个项目里,如果一半人用#define一半人用const,代码风格就会混乱。建议团队统一:数组大小、宏用#define;真正的常量用const。

最后说两句:

常量这东西,看着不起眼,但几乎所有程序里都有它的影子。你用#define定义数组长度,用const保护函数参数不被修改,用枚举让代码更可读——这些都是常量在实际开发中的应用。

练到什么程度算过关? 你搞清楚了几种常量的区别:字面常量、#define符号常量、const常变量、枚举常量。知道什么时候该用哪一种。能解释const int *p和int *const p的区别。写代码的时候能自觉把那些"不会变的值"定义为常量,而不是写成"魔法数字"散落在代码各个角落。

就这样,常量的知识你就算是拿下了。下一篇咱们聊啥?变量吧。你先把这些消化消化,练练手。

打开编译器,把上面每种常量都写个例子跑一跑,尤其是const和#define的区别那段,自己动手改一改代码看编译器报什么错。出几次错,就长记性了。