今天咱们把常量从头到尾捋一遍,不光是"常量就是不变的值"这么一句废话,咱们把每种常量在内存里怎么存的、怎么用的、跟变量有啥区别,全部拆开揉碎了讲。
一、啥是常量?就是"写死了的值"
说白了,常量就是程序运行过程中不能被改变的值。你写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的区别那段,自己动手改一改代码看编译器报什么错。出几次错,就长记性了。