C 编程/变量
与大多数编程语言一样,C 使用并处理变量。在 C 中,变量是运行程序使用的计算机内存地址的易于阅读的名称。变量通过允许您将易于记忆的标签与存储程序数据的内存地址相关联,从而更轻松地存储、读取和更改计算机内存中的数据。与变量关联的内存地址直到程序编译并在计算机上运行后才会确定。
最初,最容易将变量想象成值的占位符,就像数学中的那样。您可以将变量视为等效于其分配的值。因此,如果您有一个变量i,它被初始化(设置为等于)4,那么i + 1将等于5。然而,熟练的 C 程序员更多地关注幕后发生的看不见的抽象层:变量是可以在其中找到数据的内存地址的替代,而不是数据本身。当您学习指针时,您将对此点有更清晰的认识。
由于 C 是一种相对低级的编程语言,在 C 程序可以使用内存来存储变量之前,它必须声明用于存储变量值的所需内存。这是通过声明变量来完成的。声明变量是 C 程序显示它需要的变量数量、它们将被命名的名称以及它们需要多少内存的方式。
在 C 编程语言中,在管理和使用变量时,了解变量的类型和这些类型的大小非常重要。类型的 size 是存储一个此类型的值所需的计算机内存量。由于 C 是一种相当低级的编程语言,因此类型的 size 可能特定于所使用的硬件和编译器——也就是说,使语言在一种机器上工作的机制可能与使语言在另一种机器上工作的机制不同。
C 中的所有变量都是类型化的。也就是说,每个声明的变量都必须被分配为某种类型的变量。
以下是如何声明一个整数的示例,我们将其命名为some_number。(注意行末的分号;编译器使用它来区分程序中的一个语句与另一个。)
int some_number;
此语句告诉编译器创建一个名为 some_number
的变量,并将其与计算机上的内存位置相关联。我们还告诉编译器将存储在该地址上的数据类型,在本例中是integer。请注意,在 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 在变量名上不使用任何特殊前缀字符。
foo
Bar
BAZ
foo_bar
_foo42
_
QuUx
一些有效的(但不太描述性的)C 变量名称示例
2foo (must not begin with a digit)
my foo (spaces not allowed in names)
$foo ($ not allowed -- only letters, and _)
while (language keywords cannot be used as names)
一些无效的 C 变量名称示例
正如最后一个例子所示,某些词在语言中被保留为关键字,因此不能用作变量名。
不允许在同一 作用域中使用相同的名称来命名多个变量。因此,在与其他开发人员合作时,您应该采取措施避免对全局变量或函数名使用相同的名称。一些大型项目遵循命名指南[1]来避免重复的名称并确保一致性。
此外,还有一些名称集,虽然不是语言关键字,但出于某种原因被保留。例如,C 编译器可能会“幕后”使用某些名称,这可能会给尝试使用它们的程序带来问题。此外,一些名称为 C 标准库中将来可能的使用而保留。确定哪些名称被保留(以及它们在哪些上下文中被保留)的规则过于复杂,无法在此处描述[需要引用],作为初学者,您不必过分担心它们。现在,只需避免使用以下划线字符开头的名称即可。
C 变量的命名规则也适用于命名其他语言结构,例如函数名、结构标记和宏,所有这些都将在后面介绍。
字面量在程序中任何您显式指定值而不是引用变量或其他形式数据的地方,该值被称为字面量。在上面的初始化示例中,3 是一个字面量。字面量可以采用由其类型定义的形式(稍后将详细介绍),也可以使用十六进制(十六进制)表示法直接将数据插入变量,而无论其类型如何[需要引用]。十六进制数始终以0x开头。不过,现在,您可能不必太担心十六进制。
四种基本数据类型该int类型以“整数”的形式存储整数。一个整数通常是一个机器字的大小,在大多数现代家用 PC 上是 32 位(4 个字节)。字面量的例子是诸如 1、2、3、10、100... 的整数(整数)。当int为 32 位(4 个字节)时,它可以存储 -2147483648 到 2147483647 之间的任何整数。一个 32 位字(数字)有 4294967296 种可能性(2 的 32 次方)来表示任何一个数字。
如果你想声明一个新的 int 变量,使用int关键字。例如
int numberOfStudents, i, j = 5;
在这个声明中,我们声明了 3 个变量,numberOfStudents、i 和 j,j 在这里被分配了字面量 5。
char
类型能够保存执行 字符集 的任何成员。它存储与 int
相同类型的数据(即整数),但通常大小为 1 个字节。字节的大小由宏 CHAR_BIT
指定,它指定一个 char(字节)中的位数。在标准 C 中,它永远不能小于 8 位。类型为 char
的变量最常用于存储字符数据,因此得名。大多数实现使用 ASCII 字符集作为执行字符集,但最好不要知道或关心它,除非实际值很重要。
字符字面量的例子是 'a'、'b'、'1' 等,以及一些特殊字符,如 '\0
'(空字符)和 '\n
'(换行符,回忆“Hello, World”。注意,char
值必须用单引号括起来。
当我们初始化一个字符变量时,我们可以通过两种方式来实现。一种是首选的,另一种方式是 * **糟糕** 的编程实践。
第一种方式是写
char letter1 = 'a';
这是 * **好** 的编程实践,因为它允许阅读代码的人理解 letter1 从一开始就被初始化为字母 'a'。
第二种方式,当你编码字母字符时 * **不应** 使用,是写
char letter2 = 97; /* in ASCII, 97 = 'a' */
有些人认为这是一种非常 * **糟糕** 的做法,如果我们使用它来存储一个字符,而不是一个小的数字,如果有人阅读你的代码,大多数读者被迫查找编码方案中与数字 97 对应的字符是什么。最后,letter1
和 letter2
都存储相同的东西——字母 'a',但第一种方法更清晰,更容易调试,而且更直接。
有一件重要的事情需要提到,数字的字符与相应的数字不同,即 '1' 不等于 1。简而言之,任何用 '单引号' 括起来的单个条目。
还有一种类型的字面量需要与 chars 结合解释: **字符串字面量**。字符串是一系列字符,通常用于显示。它们用双引号括起来(" ",而不是 ' ')。字符串字面量的例子是 "Hello, World!\n" 中的 "Hello, World" 示例。
字符串字面量被分配给一个字符 **数组**,数组将在后面介绍。示例
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
是 **浮点数** 的缩写。它存储实数(整数和非整数)的不精确表示。它可以用于比最大可能的 int
大得多的数字。float
字面量必须以 F 或 f 结尾。例如:3.1415926f、4.0f、6.022e+23f。
需要注意的是,浮点数是不精确的。像 0.1f 这样的某些数字不能精确地表示为 float
,但会存在很小的误差。非常大和非常小的数字将具有较低的精度,并且算术运算有时由于缺乏精度而不可结合或不可分配。尽管如此,浮点数最常用于逼近实数,并且对它们的运算在现代微处理器上非常有效。[2] 浮点运算 在维基百科上有更详细的解释。
float
变量可以使用float关键字声明。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。事实上,当声明 float 变量时,某些 C 实现使用 double 而不是 float。
如果你想使用 double 变量,使用double关键字。
如果你对任何变量实际使用的内存量有任何疑问(这适用于我们将在后面讨论的类型,也是),你可以使用sizeof运算符来确定。为了完整起见,必须提到sizeof是 一元运算符,而不是函数。)它的语法是
sizeof object
sizeof(type)
上面的两个表达式以字节为单位返回指定对象和类型的 size。返回类型是size_t(在头文件中定义<stddef.h>)它是一个无符号值。这是一个示例用法
size_t size;
int i;
size = sizeof(i);
size将设置为 4,假设CHAR_BIT定义为 8,并且整数为 32 位宽。的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 字节整数。
在上面描述的所有类型中,一个位用于指示值的符号(正或负)。如果你确定一个变量永远不会保存负值,你可以使用unsigned修饰符来使用该位来存储其他数据,有效地将值的范围加倍,同时强制这些值必须为正数。该unsigned说明符也可以在没有尾随int的情况下使用,在这种情况下,大小默认为int的大小。还有一个signed修饰符,它是相反的,但它不是必需的,除非用于某些char用法,而且很少使用,因为所有类型(除了char)默认情况下是有符号的。
该long修饰符也可以与double一起使用以创建一个long double类型。这种浮点类型可以(但不一定)具有比double类型更高的精度。
要使用修饰符,只需用数据类型和相关修饰符声明一个变量
unsigned short int usi; /* fully qualified -- unsigned short int */
short si; /* short int */
unsigned long uli; /* unsigned long int */
当使用const限定符时,必须在声明时初始化声明的变量。然后不允许更改它。
虽然一个永远不会改变的变量的概念可能看起来没有用,但使用const有充分的理由。首先,许多编译器可以在知道数据永远不会改变时对数据执行一些小的优化。例如,如果你的计算需要 π 的值,你可以声明一个pi的 const 变量,这样其他人编写的程序或其他函数就无法改变pi.
的值。注意,符合标准的编译器在尝试更改const变量时必须发出警告——但在这样做之后,编译器可以随意忽略const限定符。
魔数
[edit | edit source]在编写 C 程序时,你可能会倾向于编写依赖于特定数字的代码。例如,你可能正在为一家杂货店编写程序。这个复杂的程序有成千上万行代码。程序员决定在整个代码中用一个字面量来表示一罐玉米的成本,目前为 99 美分。现在,假设一罐玉米的成本变为 89 美分。程序员现在必须手动将每个 99 美分的条目更改为 89 美分。虽然这不是什么大问题,考虑到许多文本编辑器的“全局查找替换”功能,但请考虑另一个问题:一罐青豆的成本最初也是 99 美分。为了可靠地更改价格,你必须查看数字 99 的每次出现。
C 拥有某些功能来避免这种情况。这种功能大致相同,尽管一种方法可能在一种情况下比另一种方法更有用。
使用const关键字
[edit | edit source]该const关键字有助于消除幻数。通过声明一个变量const corn在块的开头,程序员可以简单地更改该常量,而不必担心在其他地方设置值。
还有一种方法可以避免幻数。它比const更灵活,在许多方面也更有问题。它也涉及预处理器,而不是编译器。看吧……
#define
[edit | edit source]在编写程序时,你可以创建所谓的宏,这样当计算机读取你的代码时,它会将所有单词的实例替换为指定的表达式。
以下是一个例子。如果你写
#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
会造成危害,如果#define
是不必要的,通常最好使用const
。例如,你可以#define
一个宏DOG
为数字 3,但是如果你尝试打印该宏,认为DOG
表示一个可以在屏幕上显示的字符串,程序将出错。#define
也不考虑类型。它不考虑程序的结构,到处替换文本(实际上,忽略作用域),这在某些情况下可能有利,但可能是造成问题性错误的根源。
你将在本文后面看到#define
指令的更多示例。约定俗成地,将#define
的单词全部大写,这样程序员就会知道这不是你声明的变量,而是#define
的宏。不需要在#define
等预处理器指令末尾添加分号;实际上,如果你这样做,某些编译器可能会警告你代码中存在不必要的标记。
作用域
[edit | edit source]在基本概念部分,介绍了作用域的概念。重要的是要重新审视局部类型和全局类型之间的区别,以及如何声明每种类型的变量。要声明局部变量,你将声明放在变量被认为是局部的块的开头(即在任何非声明语句之前)。要声明全局变量,在任何块之外声明变量。如果一个变量是全局的,那么你的程序中的任何地方都可以读取和写入它。
全局变量不被认为是良好的编程习惯,应该尽可能避免。它们会影响代码可读性,造成命名冲突,浪费内存,并可能导致难以追踪的错误。过度使用全局变量通常是懒惰或设计糟糕的标志。但是,如果存在局部变量可能导致代码更加晦涩难懂和难以阅读的情况,那么使用全局变量也不可耻。
其他修饰符
[edit | edit source]为了完整起见,这里列出了标准 C 提供的更多修饰符。对于初学者来说,static 和 extern 可能有用。volatile 更受高级程序员关注。register 和 auto 基本上已被弃用,通常对初学者和高级程序员都没有兴趣。
static
[edit | edit source]static有时是一个有用的关键字。一个常见的误解是它唯一的用途是让变量留在内存中。
当将函数或全局变量声明为static 时,你无法通过其他文件中的extern(见下文)关键字访问该函数或变量。这被称为静态链接。
当将局部变量声明为static 时,它会像任何其他变量一样创建。但是,当变量超出作用域(即它所属的块已结束)时,变量会保留在内存中,保留其值。变量会保留在内存中,直到程序结束。虽然这种行为类似于全局变量,但静态变量仍然遵循作用域规则,因此无法在其作用域之外访问。这被称为静态存储期限。
默认情况下,声明为静态的变量被初始化为零(或对于指针,为 NULL[3][4])。可以在声明时明确地将它们初始化为任何常量值。初始化只进行一次,在编译时进行。
你可以用(至少)两种不同的方式使用静态。考虑这段代码,并想象它在一个名为 jfile.c 的文件中
#include <stdio.h>
static int j = 0;
void up(void)
{
/* k is set to 0 when the program starts. The line is then "ignored"
* for the rest of the program (i.e. k is not set to 0 every time up()
* is called)
*/
static int k = 0;
j++;
k++;
printf("up() called. k= %2d, j= %2d\n", k , j);
}
void down(void)
{
static int k = 0;
j--;
k--;
printf("down() called. k= %2d, j= %2d\n", k , j);
}
int main(void)
{
int i;
/* call the up function 3 times, then the down function 2 times */
for (i = 0; i < 3; i++)
up();
for (i = 0; i < 2; i++)
down();
return 0;
}
j
变量可以被 up 和 down 访问,并且保留其值。k
变量也保留其值,但它们是两个不同的变量,每个变量都在其作用域内。静态变量是实现封装的一种好方法,封装是面向对象思维中的一个术语,它实际上意味着不允许对变量进行更改,除非通过函数调用。
运行上面的程序将产生以下输出
up() called. k= 1, j= 1
up() called. k= 2, j= 2
up() called. k= 3, j= 3
down() called. k= -1, j= 2
down() called. k= -2, j= 1
static
变量的特性
1. Keyword used - static 2. Storage - Memory 3. Default value - Zero 4. Scope - Local to the block in which it is declared 5. Lifetime - Value persists between different function calls 6. Keyword optionality - Mandatory to use the keyword
extern
[edit | edit source]extern用于当一个文件需要访问另一个文件中它可能没有的变量时#include直接。因此,extern 不会为新变量分配内存,它只是向编译器提供足够的信息来访问另一个文件中声明的变量。
extern
变量的特性
1. Keyword used - extern 2. Storage - Memory 3. Default value - Zero 4. Scope - Global (all over the program) 5. Lifetime - Value persists till the program's execution comes to an end 6. Keyword optionality - Optional if declared outside all the functions
volatile
[edit | edit source]volatile 是一种特殊的修饰符类型,它通知编译器变量的值可能会被除程序本身以外的外部实体更改。对于某些使用优化编译的程序,这是必要的 - 如果一个变量没有定义volatile那么编译器可能会假设某些涉及该变量的操作是安全的,可以优化掉,但实际上并非如此。volatile 在处理嵌入式系统(程序可能无法完全控制变量)和多线程应用程序时尤其相关。
auto
[edit | edit source]auto是一个修饰符,它指定一个“自动”变量,该变量在作用域内时自动创建,在作用域外时自动销毁。如果你认为这听起来很像你一直都在做的事情,当你声明一个变量时,你是对的:块中声明的所有项目都是隐式“自动”的。因此,auto 关键字更像是琐事问题的答案,而不是有用的修饰符,而且有很多非常有能力的程序员不知道它的存在。
automatic
变量的特性
1. Keyword used - auto 2. Storage - Memory 3. Default value - Garbage value (random value) 4. Scope - Local to the block in which it is defined 5. Lifetime - Value persists while the control remains within the block 6. Keyword optionality - Optional
register 是对编译器的提示,尝试通过在程序运行时将给定变量存储在计算机 CPU 的寄存器中来优化其存储。大多数优化编译器都会这样做,因此使用此关键字通常是多余的。事实上,ANSI C 规定编译器可以随意忽略此关键字——而且许多编译器确实如此。Microsoft Visual C++ 就是一个完全忽略 register 关键字的实现示例。
register
变量的特性
1. Keyword used - register 2. Storage - CPU registers (values can be retrieved faster than from memory) 3. Default value - Garbage value 4. Scope - Local to the block in which it is defined 5. Lifetime - Value persists while the control remains within the block 6. Keyword optionality - Mandatory to use the keyword
- ↑ 命名指南的示例包括 GNOME 项目 或使用 C 编写的 Python 解释器 的部分。
- ↑ 除了浮点数以外,还存在其他实数表示方法,但它们不是 C 中的基本数据类型。一些 C 编译器支持 定点算术 数据类型,但这些类型不属于标准 C。 GNU 多精度算术库 等库提供了更多用于实数和极大数的数据类型。
- ↑ [1] - 什么是 NULL 以及如何定义它?
- ↑ [2] - 应该使用 NULL 还是 0?