C编程
变量
像大多数编程语言一样,C语言使用并处理变量。在C语言中,变量是程序运行时用于计算机内存地址的可读名称。变量通过为存储程序数据的内存地址关联易记的标签,使得存储、读取和更改计算机内存中的数据更加方便。变量所关联的内存地址直到程序编译并在计算机上运行后才会确定。
最初,最容易将变量想象为数学中的占位符。你可以将变量视为其分配的值的等价物。因此,如果有一个变量i被初始化(设置为)4,那么i + 1就等于5。然而,熟练的C程序员更关注在表面下进行的隐形抽象层:变量是数据所在内存地址的代表,而不是数据本身。你在学习指针时会更加清楚这一点。
由于C语言是一种相对底层的编程语言,在C程序利用内存存储变量之前,它必须声明所需的内存空间。这是通过声明变量来完成的。声明变量是C程序告知需要多少个变量、它们的名称以及它们将需要多少内存的方式。
在C语言中,处理和使用变量时,了解变量的类型及其大小是非常重要的。类型的大小是存储此类型一个值所需的计算机内存量。由于C是一种相对底层的编程语言,类型的大小可能会依赖于硬件和编译器——即语言在某种类型的机器上如何工作,可能与在另一种机器上工作时有所不同。
C语言中的所有变量都有类型。也就是说,每个声明的变量都必须指定为某种类型的变量。
声明、初始化和赋值变量
下面是声明一个整数的例子,我们称之为some_number。(注意行末的分号;它是编译器区分程序语句的方式。)
int some_number;
这条语句告诉编译器创建一个名为some_number的变量,并将其与计算机中的一个内存位置关联起来。我们还告诉编译器将在该地址存储的数据类型,在这个例子中是一个整数。请注意,在C语言中,我们必须指定变量将存储的数据类型。这让编译器知道为数据分配多少内存(在大多数现代机器中,int类型通常占用4个字节)。我们将在下一节讨论其他数据类型。
多个变量可以通过一条语句同时声明,例如:
int anumber, anothernumber, yetanothernumber;
在早期版本的C语言中,变量必须在代码块的开头声明。而在C99标准中,声明和语句可以任意混合——但是这样做并不常见,因为通常没有必要,某些编译器仍不支持C99(可移植性),而且这种做法不常见,可能会让同事程序员感到困扰(可维护性)。
声明变量后,可以通过如下语句将一个值赋给变量:
some_number = 3;
给变量赋值被称为初始化。上述语句指示编译器将整数3插入到与some_number关联的内存地址中。我们也可以通过同时声明并赋值来简化代码:
int some_new_number = 4;
你还可以将一个变量赋值为另一个变量的值,如下所示:
some_number = some_new_number;
或者通过一条语句为多个变量赋相同的值:
anumber = anothernumber = yetanothernumber = 8;
这是因为赋值操作 x = y
会返回赋值的结果,即y。例如,some_number = 4
会返回4。换句话说,x = y = z
实际上是 x = (y = z)
的简写。
命名变量
C语言中的变量名由字母(大小写字母)和数字组成,且允许使用下划线字符("_")。变量名不能以数字开头。与某些语言(如Perl和某些BASIC方言)不同,C语言中的变量名不使用任何特殊的前缀字符。
一些有效(但不太具描述性的)C语言变量名示例:
foo
Bar
BAZ
foo_bar
_foo42
_
QuUx
一些无效的C语言变量名示例:
2foo // 不能以数字开头
my foo // 变量名中不能有空格
$foo // 不允许使用$字符,只允许字母和下划线
while // 语言关键字不能用作变量名
正如最后一个示例所示,某些单词在语言中被保留作为关键字,不能用作变量名。
在同一作用域内,不允许使用相同的名称来表示多个变量。因此,与其他开发者协作时,你应该避免为全局变量或函数命名使用相同的名称。一些大型项目遵循命名规范[1],以避免重复命名并保持一致性。
此外,还有一些名称集合,虽然不是语言关键字,但由于某些原因被保留。例如,C编译器可能会在后台使用某些名称,这可能会导致程序在使用这些名称时出现问题。此外,一些名称可能会为C标准库的未来版本保留。关于确定哪些名称被保留(以及它们在哪些上下文中被保留)的规则过于复杂,在这里不做详细说明[需要引用],而作为初学者,你不必过多担心这些问题。现在,只要避免使用以下划线开头的名称即可。
C语言的变量命名规则同样适用于其他语言构造的命名,如函数名、结构体标签和宏,所有这些内容将在后续讨论。
字面量
在程序中,当你显式指定一个值,而不是引用变量或其他形式的数据时,这个值被称为字面量。在上面的初始化例子中,3就是一个字面量。字面量可以采用由其类型定义的形式(稍后会讲到),或者可以使用十六进制(hex)表示法直接将数据插入到变量中,而不考虑其类型。[citation needed] 十六进制数字总是以0x开头。不过现在,你可能不需要太关心十六进制。
四种基本数据类型
在标准C语言中,有四种基本数据类型,它们分别是:int
、char
、float
和 double
。
int 类型
int
类型用于存储整数,表示“整数值”。一个整数通常与计算机中的一个机器字的大小相同,在大多数现代个人电脑上是32位(4个字节)。字面量的例子是像 1、2、3、10、100 等整数。当 int
为32位时,它可以存储的整数范围是从 -2147483648 到 2147483647。32位字(数字)可以表示的数字有4294967296种可能性(2的32次方)。
如果你想声明一个新的 int
变量,可以使用 int
关键字。例如:
int numberOfStudents, i, j = 5;
在这个声明中,我们声明了3个变量,numberOfStudents
、i
和 j
,其中 j
被赋值为字面量5。
char 类型
char
类型能够存储执行字符集中的任意成员。它与 int
类型存储相同种类的数据(即整数),但通常占用一个字节的大小。一个字节的大小由宏 CHAR_BIT
指定,该宏定义了 char
(字节)中包含的位数。在标准C中,char
的大小永远不小于8位。char
类型的变量通常用来存储字符数据,因此得名。大多数实现使用 ASCII 字符集作为执行字符集,但除非实际值非常重要,否则不必特别关心这一点。
字符字面量的例子有:'a'
、'b'
、'1'
等,也有一些特殊字符,如 '\0'
(空字符)和 '\n'
(换行,参考 "Hello, World" 示例)。请注意,字符值必须用单引号括起来。
当我们初始化字符变量时,有两种方式。第一种是推荐的,另一种则是编程中的不良习惯。
第一种方式是:
char letter1 = 'a';
这是良好的编程实践,因为它使阅读代码的人能理解 letter1
被初始化为字母 'a'
。
第二种方式是,尽量避免使用:
char letter2 = 97; /* 在 ASCII 中,97 = 'a' */
一些人认为这种方式极其不推荐使用,因为如果我们用它来存储字符(而不是小数字符),当别人阅读你的代码时,大多数人必须查阅编码方案来了解数字97对应的字符是什么。最终,letter1
和 letter2
存储的内容是一样的——字母 'a'
,但是第一种方法更清晰、调试更容易,也更直接。
需要强调的一点是,数字字符的表示与其对应的数字是不同的,即 '1'
不等于 1。简而言之,任何被单引号包围的单个项都代表字符。
还有一种与 char
相关的字面量需要解释:字符串字面量。字符串是字符的序列,通常用于显示。它们被双引号括起来("
"
,而不是 ' '
)。例如,在 "Hello, World" 示例中的字符串字面量是 "Hello,
World!\n"
。
字符串字面量通常赋值给字符数组,数组将在后面讲解。例如:
const char MY_CONSTANT_PEDANTIC_ITCH[] = "learn the usage context.\n";
printf("Square brackets after a variable name means it is a pointer to a string of memory blocks the size of the type of the array element.\n");
float 类型
float
是浮点数的缩写。它存储实数的近似表示,包括整数和非整数值。它可以用于存储比最大整数值更大的数字。float
字面量必须以 F
或 f
作为后缀。例如:3.1415926f
、4.0f
、6.022e+23f
。
需要注意的是,浮点数是近似的。某些数字,如 0.1f
,无法精确表示为 float
,但会存在小的误差。非常大或非常小的数字将具有较低的精度,且算术运算有时不满足结合律或分配律,因为缺乏精度。尽管如此,浮点数通常用于近似实数,并且在现代微处理器上进行运算时效率较高。[2] 浮点运算在维基百科上有更详细的解释。
float
变量可以使用 float
关键字声明。一个 float
类型的变量通常只占用一个机器字的大小,因此在需要较少精度而不是 double
提供的高精度时使用 float
。
double 类型
double
和 float
类型非常相似。float
类型允许你存储单精度浮点数,而 double
关键字允许你存储双精度浮点数——即实数。它的大小通常是两个机器字,或者说在大多数机器上是8个字节。double
字面量的例子有:3.1415926535897932
、4.0
、6.022e+23
(科学计数法)。如果使用 4
而不是 4.0
,则 4
将被解释为 int
类型。
float
和 double
的区分是因为这两种类型的大小不同。当C语言最初使用时,空间非常有限,因此合理使用 float
来代替 double
节省了一些内存。现在,内存更为充足,你很少需要这样节省内存——因此,一致使用 double
可能会更好。实际上,某些C实现会使用 double
替代 float
,即使你声明了 float
变量。
如果你想使用 double
变量,请使用 double
关键字。
sizeof
如果你对任何变量(以及我们稍后会讨论的类型)实际占用的内存大小有任何疑问,可以使用 sizeof
运算符来确定它。需要补充说明的是,sizeof
是一元运算符,而不是函数。它的语法如下:
sizeof object
sizeof(type)
以上两种表达式将返回指定对象和类型的大小,单位为字节。返回类型是 size_t
(在头文件 <stddef.h>
中定义),它是一个无符号值。示例:
size_t size;
int i;
size = sizeof(i);
假设 CHAR_BIT
被定义为8,且整数是32位宽的,那么 size
将被设置为4。sizeof
的结果值是字节数。
请注意,当 sizeof
应用于 char
时,结果为1,即:
sizeof(char)
总是返回1。
数据类型修饰符
通过在数据类型前加上某些修饰符,可以改变数据类型的存储方式。
long
和 short
是修饰符,使得数据类型可以使用更多或更少的内存。int
关键字通常不跟在 short
和 long
后面。short
用于存储范围比 int
小的值,通常是从 -32768 到 32767。long
用于存储更大范围的值。并不保证 short
占用的内存小于 int
,也不保证 long
占用的内存比 int
多。只保证 sizeof(short) <= sizeof(int) <=
sizeof(long)
。通常,short
占用2字节,int
占用4字节,long
占用4字节或8字节。现代C编译器还提供了 long long
,通常是8字节的整数。
在上面描述的所有类型中,1个位用于表示值的符号(正或负)。如果你决定变量永远不存储负值,可以使用 unsigned
修饰符将这一位用于存储其他数据,实际上是将值的范围扩大一倍,同时强制这些值为正。unsigned
修饰符也可以不加 int
,此时默认大小与 int
相同。还有 signed
修饰符,表示相反,但除非某些特定情况下的 `
const限定符
当使用const
限定符时,声明的变量必须在声明时初始化,并且之后不能再修改。
虽然“一个永远不变的变量”这一概念可能看起来没有什么用处,但实际上使用const
有许多优点。首先,许多编译器可以对那些知道永远不会改变的数据进行一些小的优化。例如,如果你需要在计算中使用π的值,你可以声明一个const
类型的变量pi
,这样程序或其他人编写的函数就无法改变pi
的值。
需要注意的是,符合标准的编译器在尝试修改const
变量时,必须发出警告,但即便如此,编译器仍然可以忽略const
限定符。
魔法数字
在编写C程序时,你可能会想用一些特定的数字。举个例子,你可能在为一个杂货店编写程序,程序非常复杂,代码有成千上万行。程序员决定把一罐玉米的价格(99美分)作为常量写在代码中。如果价格变更为89美分,程序员就得手动修改所有99美分的地方。虽然这不是很大问题,许多文本编辑器提供了“全局查找替换”功能,但考虑到另一种情况:一罐青豆的价格也最初是99美分。为了可靠地更改价格,你必须查看所有99的出现位置。
C语言提供了一些方法来避免这种情况。虽然这些方法大体相同,但有时候某些方法在特定情况下会更有用。
使用const
关键字
const
关键字有助于消除魔法数字。通过在代码块开始时声明一个const
corn
变量,程序员只需修改这个const
变量,而不必担心在其他地方修改它的值。
使用#define
另一种避免魔法数字的方法是使用宏定义(#define
)。当编译器读取代码时,它会将所有出现的某个单词替换为指定的表达式。
例如,如果你写:
#define PRICE_OF_CORN 0.99
当你想要打印玉米的价格时,你只需使用PRICE_OF_CORN
,而不是数字0.99
。预处理器会将所有PRICE_OF_CORN
替换为0.99
,然后编译器会将其解释为字面值double
0.99
。预处理器执行替换,即PRICE_OF_CORN
被替换为0.99
,所以不需要分号。
需要注意的是,#define
的功能基本上与许多文本编辑器或文字处理软件中的“查找并替换”功能相同。
对于某些用途,#define
可能被滥用,通常最好使用const
,如果不需要使用#define
。例如,假设你定义了一个宏DOG
为数字3,但如果你试图打印该宏,以为它是一个字符串可显示在屏幕上,程序会报错。#define
也不关心类型,它忽略了程序的结构,替换文本时没有考虑作用域,这可能在某些情况下有优势,但也可能导致难以追踪的错误。
通常约定,#define
的宏名称写成全大写,以便程序员知道这不是你声明的变量,而是一个宏定义。预处理指令如#define
不需要以分号结束;事实上,如果你加上分号,某些编译器可能会警告你代码中有不必要的标记。
作用域
在“基本概念”部分中,介绍了作用域的概念。理解局部类型和全局类型的区别以及如何声明这两种类型的变量非常重要。声明局部变量时,将声明放在块的开始(即在任何非声明语句之前)。声明全局变量时,将变量声明放在任何代码块之外。如果一个变量是全局的,它可以在程序中的任何地方读取和写入。
全局变量通常不被视为良好的编程实践,应尽量避免。它们会影响代码的可读性,可能引发命名冲突,浪费内存,还可能引发难以追踪的错误。过度使用全局变量通常是懒惰或设计不当的标志。然而,如果局部变量可能导致更复杂且不易阅读的代码,使用全局变量并不丢人。
其他修饰符
为完整起见,以下列出了一些C语言标准提供的修饰符。对于初学者,static
和extern
可能有用;volatile
则更多地面向高级程序员。register
和auto
已基本废弃,通常对初学者或高级程序员都不太感兴趣。
static
static
是一个有用的关键字。很多人误以为它唯一的作用是使变量保留在内存中。
当你将函数或全局变量声明为static
时,其他文件就无法通过extern
关键字访问这些函数或变量,这叫做静态链接。
当你将局部变量声明为static
时,它与其他变量的创建方式一样。然而,当该变量超出作用域时(即所在的块执行完毕),它仍然保留在内存中,并保持其值,直到程序结束。虽然这种行为类似于全局变量,但静态变量仍然遵循作用域规则,因此无法在作用域外访问。这叫做静态存储持续时间。
extern
extern
用于在文件需要访问另一个文件中的变量时,提供足够的信息让编译器访问该变量。extern
不会为变量分配内存,只是告诉编译器如何访问另一个文件中声明的变量。
volatile
volatile
是一种特殊类型的修饰符,用于通知编译器变量的值可能会被程序之外的外部实体改变。这对于某些编译了优化的程序至关重要,因为如果没有使用volatile
,编译器可能会假设某些操作是安全的,从而优化掉不应被优化的操作。
auto
auto
是指定“自动”变量的修饰符,这些变量在作用域内自动创建,超出作用域时自动销毁。实际上,在声明变量时,所有局部变量都默认为“自动”,因此auto
关键字并不常用,很多程序员甚至不知道它的存在。
register
register
是给编译器提供的提示,告知它将变量存储在CPU的寄存器中,以加速访问速度。大多数优化编译器会自动做这件事,因此通常不需要使用这个关键字。