C语言解惑:指针、数组、函数和多文件编程
上QQ阅读APP看书,第一时间看更新

1.1 变量的三要素

一个变量具有3个要素:数据类型、名字和存放变量的内存地址。本节将简要回顾变量的3个要素,以便为引入指针打下基础。

1.基本数据类型

数据类型是C语言中非常重要的一个概念,它将C语言所处理的对象按其性质不同分为不同的子集,以便对不同类型的数据规定不同的运算。void是无类型标识符,只能声明函数的返回类型,不能声明变量,但可以声明指针。

本节只涉及基本数据类型,C语言的基本数据类型有如下4种。

❑char字符型

❑int整数型

❑float浮点数型(又称为单精度数)

❑double双精度浮点数型

另外还有用于整型的限定词short、long、signed和unsigned。short和long表示不同长度的整型量;unsigned表示无符号整型数(它的存放值总是正的);可以省略signed限定词。例如,可以将如下声明

中的说明符int省略。即它们与如下声明

是等效的。上述数据类型的长度及存储的值域也随编译器不同而变化,ANSI C标准只限定int和short至少要有16位,而long至少32位,short不得长于int, int不得长于long。表1-1是数据类型的长度及存储的值域表,表1-1中VC是Visual C++ 6.0的缩写。表1-2是加了限定词的数据类型及它们的长度和取值范围。

表1-1 数据类型的长度及存储的值域

表1-2 加限定词的数据类型及其长度和取值范围

C语言提供一个关键字sizeof,用来求出对于一个指定数据类型,编译系统将为它在内存中分配的字节长度。例如,语句“printf("%d", sizeof(double)); ”的输出结果为8。

注意在表1-1中的标注,在VC中int使用4字节,这是本章计算的依据。

C语言定义的存储类型有4种:auto、extern、static和register,分别称为自动型、外部型、静态型和寄存器型。自动型变量可以省略关键字auto。存储类型在类型之前,即

例如auto int和static f loat等。可以省略auto,其他类型均不可以省略。

2.变量的名字和变量声明

C语言中大小写字母是具有不同含义的,例如,name和NAME就代表不同的标识符。原来的C语言中虽然规定标识符的长度不限,但只有前8个字符有效,所以对定义为

的两个变量是无法区别的。

现在流行的为32位操作系统配备的C编译器已经能识别长文件名,不再受8位的限制。另外,在选取时不仅要保证正确性,还要考虑容易区分,不易混淆。例如,数字1和字母i在一起,就不易辨认。在取名时,还应该使名字有很清楚的含义,例如使用area作为求面积函数的名字,area的英文含义就是“面积”,这就很容易从名字猜出函数的功能。对一个可读性好的程序,必须选择恰当的标识符,取名应统一规范,以便使读者能一目了然。

在现在的编译系统中,内部名字中至少前31个字符是有效的,所以应该采用直观的名字。一般可以遵循如下简单规律。

1)使用能代表数据类型的前缀。

2)名称尽量接近变量的作用。

3)如果名称由多个英文单词组成,每个单词的第一个字母大写。

4)由于库函数通常使用下划线开头的名字,因此不要将这类名字用作变量名。

5)局部变量使用比较短的名字,尤其是循环控制变量(又称循环位标)的名字。

6)外部变量使用比较长且贴近所代表变量的含义。

7)函数名字使用动词,如Get_char(void)。变量使用名词,如iMen_Number。

变量命名可以参考Windows API编程推荐的匈牙利命名法。它是通过在数据和函数名中加入额外的信息,既增进程序员对程序的理解,又方便查错。

所有的变量在使用之前必须声明,所谓声明即指出该变量的数据类型及长度等信息。声明由类型和具有该类型的变量列表组成。如:

变量可按任何方式分布在若干个声明中,上述声明同样可以写成:

后一种形式会使源程序冗长,但便于给每个声明加注释,也便于修改。

变量的存储类型在变量声明中指定。变量声明的一般形式为:

应该养成在声明时就为变量赋初值的习惯,但在某些特殊场合则只能声明,如头文件中对外部变量的声明,下面是一些典型的例子。

3.变量的地址

内存地址由系统分配,不同机器为变量分配的地址大小虽然可以不一样,但都必须给它分配地址。

在C语言中,声明和定义两个概念是有区别的。声明是对一个变量的性质(如构成它的数据类型)加以说明,并不为其分配存储空间;而定义则是既说明一个变量的性质,又为其分配存储空间。定义一个函数,也是为它提供代码。

1.2 变量的操作

从三要素可知,既可以通过名字对变量进行操作,也可以通过地址对存放在该地址的变量进行操作。

1.左值和右值的概念

变量是一个指名的存储区域,左值是指向某个变量的表达式。“左值”来源于赋值表达式“A=B”,其中左运算分量“A”必须能被计算和修改。左值表达式在赋值语句中既可以作为左操作数,也可以作为右操作数,例如“x=56”和“y=x”, x既可以作为左值(x=56),又可以作为右值(y=x)。但右值“56”只能作为右操作数,而不能作为左操作数。由此可见,常量只能作为右值,而普通变量既可以作为左值,也可以作为右值。如下语句

定义的a,显然不能作为左值,只能作为右值。

由此可见,值可以作为右值,如整数、浮点数、字符串、数组的一个元素等。在C语言中,右值以单一值的形式出现。假设有字符数组a和b,则这两个字符数组的每个元素均可以作为右值,即“a[0]=b[0]”是正确的,“b[0]=a[0]”也是正确的。需要注意的是,它们在“=”号左右两边的含义是不同的。以a[0]为例,在“b[0]=a[0]”中,它是作为值出现的,即a[0]是数组第1个元素的值;而在“a[0]=b[0]”中,它是作为变量出现的,即a[0]是数组的第1个元素的变量名,所以a[0]可以作为左值。即可以使用数组的具体元素作为左值和右值。

a和b都不是字符串的单个元素,所以都不能作为右值。而因为a和b可以作为数组首地址的值赋给指针变量,所以在这种情况下它们又都可以作为右值。

由此可见,在C语言中,左值是一个具体的变量,右值一定是一个具体类型的值,所以有些可以既可以作为左值,也可以作为右值,但有些只能作为右值。

2.对变量的基本操作

C语言使用地址运算符“&”来取变量存储在内存中的首地址。假设变量a=55,但不同机器和系统为它分配的地址是不一样的,这里也假设分配的十六进制地址是0x0012FF7C。如何从这个地址取出“55”呢?

C语言提供了“*”运算符,用来取出地址里的值。“&a”代表地址,显然“*&a”可以取出55。使用下面语句

可以得到输出结果为“55,55”,即证明a和*&a是等价的。

1.3 指针变量

1.2节介绍了“*”和“&”运算符,本节将通过具体的例子说明它们的用途,从而引入指针变量。

1.对有效地址进行操作

【例1.1】取地址里的值和取地址里存放的地址值的例子。

语句“int a=65; ”定义了整型变量a的值为65, VC使用4字节存储65。假设存放它的内存首地址为十六进制的“0x0012ff7c”,则可以使用输出格式“%p”来输出这个地址。“0x”是标注它为十六进制地址,也可以简单地使用“%#p”输出地址。

一个变量具有地址和值,&是取地址值运算符。系统为整型变量a和adder分别分配地址“0x0012ff7c”和“0x0012ff78”。给整型变量addr赋十六进制数,这个数可以代表地址,但不一定是有效的地址(将计算机可以存取的地址称为有效地址)。已经验证0x0012ff7c是分配给变量a的地址,所以addr是被赋给一个有效地址。

将“0x0012ff7c”赋给变量addr, &addr是系统分给它的地址“0x0012ff78”,这个地址与a的地址相差4字节,证明它们是连续存放的。现在这个地址里存放的是地址0x0012ff7c,也就是变量a的地址。因为*&addr应该输出a的地址而不是a的值,所以要使用%p格式。程序输出结果也验证了如上分析。即输出为

既然addr存放的是有效地址,*addr也应该能输出这个地址里的值,也就是变量a的地址。不过,&a虽然和addr的值是一个值,但它们对运算的反应并不一样。a是变量,其值为65, &a是存储地址,所以*&a是取地址里的值。类似的,*addr应该输出它的存储内容,即地址“0x0012ff7c”,而**addr应该输出地址“0x0012ff7c”里的内容65。其实这是不行的,因为编译系统并不知道*addr存储的“0x0012ff7c”是地址,所以将它作为整数,因此编译系统会报错,当然使用**addr也要出错。

但addr里面确实装的是地址,所以可以将这个整数强制转为地址。*addr加上强制转换,应该是“(int *)addr”,它的内容是变量a的地址“0x0012ff7c”。

再对它使用*运算符,即*(int *)addr,输出结果应该是存在这个地址里的变量a的值65。下面的例子验证了如上分析。

【例1.2】取地址里的整数值和取地址里存放的地址值。

输出结果如下:

2.引入指针的概念

在【例1.2】中,要使用a的地址直接给addr赋值,必须事先知道这个地址。为了避免这个麻烦,可以直接将地址表达式“&a”赋给变量。即

因为&a是地址值,addr是整型变量,所以会给出警告信息。不过,可以使用强制转换让警告信息“闭嘴”。即

这样一来,使用起来就方便多了。

【例1.3】直接将变量地址赋给另一个变量的例子。

程序运行结果如下:

运行结果完全吻合。如果想像对待变量一样对待addr,即使用“*”和“&”运算符的结果与变量a一样,就必须定义新的变量类型。分析下述表达式:

由此可见,如果定义一种变量,使它存储的数据类型是地址,问题就可以迎刃而解了。

要使用(int*)addr的addr存储地址,那么就要用“int *”声明“addr”,即

这时“addr=&a”就无需转换,赋值顺理成章了。

“addr”输出地址,则“*addr”输出地址里的值。这就与普通变量的使用方法完全一样了。

暂且将使用“int *”定义的变量称为指针变量,下面编程验证一下这个设想。

【例1.4】使用新的数据类型(指针)的例子。

输出结果如下:

输出结果与【例1.3】的完全一样。

这种类型称为指针类型,指针类型存储的是地址值。因为这里使用的地址值是另外一个变量的地址,所以是有效地址。要明确的是,地址值不一定是有效地址,所以说从指针的引入开始,也就暗示着它存在着无法预防的错误。

3.引入字符指针再次验证

下面再使用字符来验证一下,看是否与整数的结论相同。

【例1.5】使用字符的例子。

程序输出结果如下:

程序验证了“(char*)addr”和“*(char*)addr”的作用,从而推知,可以定义字符类型的指针。

【例1.6】使用字符指针的例子。

程序输出结果如下:

4.声明指针类型的变量

由此可见,声明指针变量是用普通数据类型加“*”号。例如:

至于“*”号的位置,对于整型类型指针p,以下三个位置均可。

至于哪种写法好,也要根据实际情况,以不造成误会为准。下面是正确的使用实例。

在上面的声明中,只有a是指针变量,p和d都是整型变量。使用下面的声明就可以提高可读性。

系统不管是何种数据类型的指针,一律分配4字节,即各种类型的指针所占内存的大小是一样的。

【例1.7】演示典型指针长度的例子。

NULL代表空指针。程序输出结果为:

下面再以整型指针p为例,说明指针的含义。

【例1.8】演示整型指针的例子。

程序输出结果如下:

可以画出变量p和a之间的关系如图1-1所示。

图1-1 变量p和a之间的关系示意图

系统为指针变量分配地址0x0012FF78,一般不需要管它。它存储的地址是变量a的首地址,正是因为这种数据类型声明的变量代表指向另一个数据类型变量的存储首地址,所以得名为“指针”类型。注意这句话:指向另一个数据类型变量的存储首地址。

读者有时会对使用如下方法在声明指针的同时初始化指针的方式感到困惑,即

实际上,选择“int* p; ”,认为“int*”是一种指向整型的指针类型,用它声明指针变量p, p应该赋予a的地址,所以应是“p=&a”。声明指向整型的指针变量p并同时初始化,也就顺理成章为“int* p=&a”。显然,称p为指针变量(存放的是变量的首地址),而不是称*p为指针变量(*p代表指针指向的地址单元所存放的值)。

由此可知,p的值是地址,虽然这个地址就是变量a在内存中的存储首地址,但并不直接说p的值是a的地址,而说成p指向a的存储首地址,简称p指向a的地址。

1.4 指针类型

假设已经知道变量的地址(NULL也算已知),现在将上一节的构造语法总结如下:

或者采取直接初始化的方法:

默认的存储类型为自动存储类型(auto),目前也仅以自动存储类型为例,以后将通过例子进一步介绍存储类型。现在假设它们具有如图1-2所示的形式。由关联关系可知,*p和a同步变化,即改变任何一个的值,它们的值保持一致。如果改变p的内容,如使用语句“p=&b; ”,这使得*p=66,它与b同步,不再与a有任何关系。

图1-2 指针操作关系图

【例1.9】说明对p和*p进行赋值操作含义的程序。

程序输出如下:

在【例1.9】中,指针本身的地址不会变化,它反映了系统需要为指针p分配地址这一概念。正如使用a不要再考虑&a一样,以后也不再考虑&p。

【例1.10】下面是一个使用数组b的首地址作为右值的例子,该程序将数组a的内容复制到数组b中,然后输出两个数组的内容以便验证。

程序运行结果如下: