4. 【C语言】数据的模样:常量和变量
前几篇我们一直在和Hello, World!较劲程序“说”了什么却从没真正“记”住什么。如果程序只能输出固定的文字那它不过是个复读机。要让程序有记忆力、能处理不同的数据你就得和“变量”与“常量”交朋友了。这篇文章里你会第一次接触 C 语言的数据世界了解如何给数据起名字、如何在内存里存放值、以及如何定义不会改变的量。这是往后所有复杂程序的地基——地基打得牢才盖得起高楼。一、变量是什么一个容易理解的比喻变量就是一块带标签的内存抽屉。想象你面前有一整面墙的小抽屉每个抽屉能放下一个数字或一个字符。当你要存储数据时你指着一个空抽屉说“这个抽屉叫age里面放 25。”以后只要说age就能从那个抽屉里取出 25也可以把它换成别的数字。这就是变量的本质你的程序向操作系统申请一小块内存。你给它取个名字比如age。你往里面放数据。之后通过名字来读、来改。名字本身不占内存它只是“标签”但抽屉内存空间会实际占用若干个字节。至于占多少字节取决于你打算在抽屉里放什么类型的数据。二、标识符命名规则——给抽屉起名字的规矩在 C 语言里变量名、函数名等统称为标识符。起名字不能随心所欲必须遵守以下规则只能由字母A-Z、a-z、数字0-9和下划线_组成。第一个字符不能是数字。2ndValue不合法value2合法。区分大小写。age、Age、AGE是三个完全不同的变量。不能使用 C 语言的关键字。比如int、return、if、while等已经被系统占了你不能再拿来用。长度没有硬性上限但只有前 31 个字符保证有意义现代编译器通常支持更长的有效长度但过长的名字没必要。此外还有一些强烈建议遵守的“好品味”用有意义的英文单词age、student_count、max_speed不要用a、x1、dsf除非是临时循环变量。变量名通常全小写单词间用下划线连接snake_case或者用驼峰命名法camelCase如studentCount。在一个项目里保持一致即可C 社区里 snake_case 更为常见。三、声明与初始化——要抽屉、放东西要使用变量必须先声明后使用。声明的作用是告诉编译器“我需要一个抽屉请给我准备出足够的空间。”声明的基本格式是类型 变量名;比如intage;// 声明一个整数变量 agefloatscore;// 声明一个单精度浮点数变量 scorechargrade;// 声明一个字符变量 grade这时候抽屉是准备好了但里面放的是不确定的垃圾值之前留在那儿的随机数据。直接读取未初始化的变量是危险且无意义的。所以最好在声明的同时给它一个初始值这叫做初始化intage25;floatscore98.5;chargradeA;也可以一次声明多个同类型变量intx,y,z;// 三个未初始化的 intinta1,b2,c3;// 三个初始化了的 int初始化形式还有一些灵活的地方比如可以用常量表达式、甚至之前声明过的变量inta10;intba5;// b 初始化为 15intcb*2;// c 初始化为 30注意C 语言中全局变量在所有函数外面定义的和静态局部变量如果不手动初始化会被自动初始化为 0而普通的局部变量在函数内部定义的则不会自动初始化内容是垃圾值。初学阶段我们主要和局部变量打交道所以请务必养成声明即初始化的习惯。四、常量的两种面孔const与#define有时候程序里需要用到一些固定不变的值比如圆周率、一天的小时数、最大用户数量。这些值在程序运行过程中不应该被修改。我们就可以用常量来表示它们。C 语言给了你两种主流的定义方式。方式一const关键字const是“constant”的缩写加在类型声明前面意思是“这个抽屉里的东西只读不能改”。constintHOURS_PER_DAY24;constfloatPI3.14159;一旦用const声明并初始化后试图修改它就会导致编译错误constintMAX100;MAX200;// 编译报错assignment of read-only variable MAX用const定义常量的好处是它有明确的类型编译器能做类型检查用起来和普通变量一样安全只是不能赋值。常量名通常全大写字母加下划线这是一种约定看到全大写就知道它是常量。方式二#define预处理宏我们之前学过#include#define也是预处理指令。它的用法是#definePI3.14159#defineHOURS_PER_DAY24这行代码做了一件文本替换的事在预处理阶段编译器会把代码里所有出现PI的地方原样替换成3.14159。它不是变量不占运行时内存没有类型。例如#includestdio.h#defineGREETING你好世界intmain(void){printf(%s\n,GREETING);return0;}预处理后printf那行实际上变成了printf(%s\n,你好世界);你可以用gcc -E查看替换后的结果会看到GREETING消失了只剩字符串。#define宏的优点它不占用内存执行时不需取址。缺点也很明显没有类型检查出现错误时编译器给出的信息可能很隐晦。比如#definePI3.14159// ...intxPI*2;// PI 是 double这里会有隐式类型转换编译器可能不会警告如果#define用来定义复杂表达式不加括号还可能导致诡异的错误后面讲带参宏时会深入。对于简单的数值常量或字符串常量两种方式都可以但初学阶段建议优先用const它更安全更像“真正的”变量。五、代码示例常量与变量同台登场我们写一个小程序计算半径为 5 的圆的周长和面积把所有学到的都用上#includestdio.h#definePI3.14159// 宏定义常量intmain(void){constintradius5;// const 常量半径floatcircumference;// 未初始化变量存放周长floatarea;// 存放面积circumference2*PI*radius;// 计算周长areaPI*radius*radius;// 计算面积printf(半径: %d\n,radius);printf(周长: %.2f\n,circumference);printf(面积: %.2f\n,area);return0;}%.2f表示输出的浮点数保留两位小数后面学格式化输入输出会详细解释。运行输出半径: 5 周长: 31.42 面积: 78.54试试把const int radius改成int radius再在下面加一行radius 10;看看周长和面积的变化。你开始体会到变量的威力在于可变常量的价值在于安全。六、常见错误与陷阱1. 未声明就使用变量intmain(void){a10;// 错误a 从未声明return0;}编译报错a undeclared。必须先声明后使用。2. 声明了但未初始化就读取intage;printf(%d\n,age);// 未定义行为打印垃圾值始终给变量一个初始值。3. 试图修改const常量constintMAX100;MAX200;// 编译错误这很直观错就错在去修改。4. 宏定义里加等号或分号#definePI3.14159;// 错误宏替换后PI会被替换成 3.14159;代码立刻就乱套了。#define不需要也不需要;。七、小结今天我们了解了变量和常量的基础。变量让程序有了“记忆”常量让固定值有了名字且更安全。你还学会了命名标识符的规矩以及声明、初始化的正确姿势。可能你会觉得这些内容有点“干”但它们是构建一切程序的地基。就像学外语要先背单词学音乐要练音阶枯燥的基本功决定了你以后能走多远。下一篇文章我们要进入数据类型的家族int、float、double、char都是什么样的抽屉各占多少空间彼此怎么转换。一个个认识它们你会逐渐看清 C 语言内存世界的轮廓。课后小练习声明一个float变量存储你的身高米一个int变量存储你的年龄分别初始化然后用printf打印出来。提示%f打印 float%d打印 int用const定义一年中月份的数量12再用#define定义一周的天数7写程序计算一年大约有多少周输出结果。故意写一个未初始化变量并打印它的值看看编译器是否会给出警告。如果有警告读一读它说了什么。思考题下面这段代码哪里错了#includestdio.hintmain(void){constintmax_students;max_students45;printf(%d\n,max_students);return0;}试着编译看结果然后解释为什么。获取本系列示例代码请访问 GitCode 仓库。