变量是一块有名字的内存,保存着程序可以读取和修改的值。在 C 语言中,你总是要事先说明它保存的是哪种值,而且这个选择在变量的整个生命周期内都不会改变。
声明变量
格式是类型、名字、分号:
int age;
double price;
char grade;
每一行都为对应的数据类型预留足够的内存,并给它起一个名字。此时还没有存入任何东西:空间已经存在,但里面的内容是之前运行的程序留下的任何东西。
你也可以在声明的同时赋一个值,这叫作初始化:
注意格式说明符:int 用 %d,double 用 %f,char 用 %c。printf 不会检查传给它的值,它完全信任你的格式字符串,所以一旦不匹配,打印出来的是乱七八糟的结果,而不是报错。
声明之后再赋值
变量存在以后,用 = 来修改它:
C 语言中的 = 是赋值,不是相等。score = score + 10 的意思是“取出 score 中的值,加上 10,再把结果放回 score”。比较运算符是 ==,把两者搞混是非常常见的 bug,布尔值页面专门为它写了一节。
简写 score += 10 用更少的字符做同样的事;复合赋值运算符涵盖了所有的算术运算。
未初始化变量的陷阱
这是 C 语言新手最常见的 bug,而且它不会报错:
运行一下。你可能看到 15,这是正确答案。你也可能看到一个巨大的随机数。第二次运行或换一台机器,你还可能看到别的结果。这些都是“正确”的行为,因为读取未初始化的局部变量属于未定义行为:C 标准对会发生什么不作任何要求。
和许多语言不同,C 不会把局部变量清零。局部变量是栈上的一小块空间,而栈上满是之前函数调用留下的残余数据。有时这些残余数据恰好是零,这正是这个 bug 如此危险的原因:它在测试时常常能正常工作,到了生产环境却出问题。
修复只需要一处小改动:
养成习惯:在声明每个变量时就初始化它。如果你确实还没有合理的值,用 0 或 NULL 也比垃圾值强。另外,用 -Wall 编译,它能捕获大多数这类情况:
warning: 'total' is used uninitialized in this function
全局变量和 static 变量是例外,它们会被自动初始化为零。依赖这一点没有问题,但还是写上 = 0,可以表明你的意图。
一次声明多个变量
相同类型的变量可以写在同一个声明里,带不带初始值都可以:
对于 x, y, z 这类紧密相关的值,这种写法合法又常见。但一个经典的误读也藏在这里:
int a = 0, b = 0, c = 0; /* all three are 0 */
int d, e, f = 0; /* only f is 0 - d and e are garbage */
初始值只属于一个名字,而不是整行。当几个值的含义不同时,每个变量单独写一行,读起来更清楚,diff 也更清楚。
在一行里声明多个指针也有同样的陷阱,而且更隐蔽:
int *p, q; /* p is a pointer to int; q is a plain int */
* 绑定的是名字,不是类型。指针要一行声明一个。
命名规则和惯例
编译器的规则:
- 只能使用字母、数字和下划线。
- 不能以数字开头:
total2可以,2total不行。 - 不能是 C 语言关键字:不能把变量命名为
int、return、for、double。 - 区分大小写:
count、Count和COUNT是三个不同的变量。 - 以下划线开头或包含双下划线的名字保留给编译器和标准库使用。不要使用这样的名字。
int itemCount; /* legal */
int item_count; /* legal - the usual C style */
int _count; /* legal but reserved - avoid */
int item-count; /* error: '-' is the minus operator */
int 3rd_place; /* error: starts with a digit */
int float; /* error: keyword */
大多数 C 代码遵循的惯例是:变量和函数用 snake_case,宏常量用 UPPER_CASE,名字要说明值的含义,而不是它的类型。elapsed_ms 好过 t;customer_count 好过 n2。
在作用域很小的地方,短名字依然没问题:i 作为循环计数器人人都懂,谁也不想写 for (int loop_iteration_index = 0; ...)。
变量在哪里有效
在哪里声明变量,决定了谁能看到它、它能存在多久。这就是作用域,简单来说:
变量从声明处开始可见,直到包含它的代码块的右花括号为止。全局变量在整个程序运行期间都存在,并且处处可见,这听起来很方便,而这正是它们惹麻烦的原因:任何函数都能修改它,要查出是谁改的,就得把整个文件翻一遍。优先使用局部变量,并通过参数把值传给函数。
随处声明(C99 及以后)
在最初的 C89 中,代码块里的所有声明都必须出现在任何语句之前:
int main(void) {
int i, sum = 0; /* all declarations first */
printf("Starting\n");
for (i = 0; i < 10; i++) { sum += i; }
return 0;
}
从 C99 开始,你可以在第一次需要这个值的地方声明它,包括在 for 的头部:
这是更好的风格:声明紧挨着第一次使用的地方,而且 i 不会泄漏到函数的其余部分。用 -std=c17(或任何 C99 及以上的设置)编译,在哪里都能正常工作。
常见问题
C 语言怎么声明变量?
先写类型,再写名字,最后加分号:int age;。你也可以同时给它一个值,写成 int age = 30;,这叫作初始化,而且几乎总是你想要的写法。
声明变量和初始化变量有什么区别?
声明是预留空间并给它起名字(int count;)。初始化是在创建的那一刻就放入一个值(int count = 0;)。只声明而未初始化的局部变量,里面是那个地址上原本就有的字节,读取它属于未定义行为。
C 语言变量名有哪些规则?
只能包含字母、数字和下划线;不能以数字开头;不能是 int、for 这样的关键字;并且区分大小写,所以 total 和 Total 是两个不同的变量。以下划线开头的名字是保留的,应避免使用。
C 语言可以在函数的任何位置声明变量吗?
从 C99 开始可以,每个变量在第一次需要时再声明即可。在最初的 C89 标准中,所有声明都必须出现在代码块的开头,所以老代码在真正干活之前总有一大堆声明。