文章目录
基本数据类型
C语言中有4种基本数据类型——整型、浮点型、指针和聚合类型(如数组和结构等)。
整型
整型家族包括字符、短整型、整型、长整型,它们都分为有符号(signed)和无符号(unsigned)两种版本。
规定整型值相互之间大小的规则很简单:
长整型至少应该和整型一样长,而整型至少应该和短整型一样长。
ANSI标准加入了一个规范,说明了各种整型值的最小允许范围,如下表所示。
类型 | 最小范围 |
---|---|
char | 0到127 |
signed char | -127到127 |
unsigned char | 0至255 |
short int | -32767到32767 |
unsigned short int | 0到65535 |
int | -32767到32767 |
unsigned int | 0到65535 |
long int | -2147483647到2147483647 |
unsigned long int | 0到4294967295 |
- 整型字面值
字面值(literal) 这个术语是字面值常量的缩写——这是一种实体,指定了自身的值,并且不允许发生改变。
例如,
十进制整型值(可能是int
、long
或unsigned long
)表示整型字面值:123 65535 -2752
八进制整型值(以0开头)表示整型字面值:0173 0177777 000060
十六进制整型值(以0x开头)表示整型字面值:0x7b 0xFFFF 0xabcdef00
在整型字面值后面添加字符L或l(小写),可以使这个整数被解释为long
整型值,字符U或u则用于把数值指定为unsigned
整型值。如果在一个字面值后面添加这两组字符中的各一个,那么它就被解释为unsigned long
整型值。
- 枚举类型
枚举(emumerated) 类型就是指它的值为符号常量而不是字面值的类型,它们以下面这种形式定义:
enum Jar_Type { CUP, PINT, QUART, HALF_GALLON, GALLON };
这条语句定义了一个类型,称为Jar_Type。
这种类型的变量按下列方式声明:
enum Jar_Type milk_jug, gas_can, medicine_bottle;
也可以将定义和声明两条语句组合成下面的样子:
enum { CUP, PINT, QUART, HALF_GALLON, GALLON }
milk_jug, gas_can, medicine_bottle;
这种类型的变量实际上以整型值的方式存储,这些符号名的实际值都是整型值。
枚举类型中枚举值默认从0开始,往后逐个加1
适当的时候,你可以为这些符号指定特定的整型值,如下所示:
enum Jar_Type { CUP = 8, PINT = 16, QUART = 32, HALF_GALLON = 64, GALLON = 128 };
只对部分符号名用这种方式进行赋值也是合法的。如果某个符号名未显式指定一个值,那么它的值就比前面一个符号名的值大1。(更简单的作法是为第一个枚举名赋值,之后依次递增)
枚举与宏定义#define
作用类似,可避免宏名过多影响代码可读性。
浮点类型
浮点数家族包括float
、double
和long double
类型。ANSI标准仅仅规定long double
至少和double
一样长,而double
至少和float
一样长。标准同时规定了一个最小范围:所有浮点类型至少能够容纳从10-37到1037之间的任何值。
浮点数字面值在缺省情况下都是double
类型的,除非它的后面跟一个L或l表示它是一个long double
类型的值,或者跟一个F或f表示它是一个float
类型的值。
指针
指针可以有效地实现诸如tree
和list
这类高级数据结构。
指针变量就是一个其值为另外一个(一些)内存地址的变量。C语言拥有一些操作符,你可以获得一个变量的地址,也可以通过一个指针变量取得它所指向的值或数据结构。
- 指针常量(pointer constant)
编译器负责把变量赋值给计算机内存中的位置,程序员事先无法知道某个特定的变量将存储到内存中的哪个位置,所以把指针常量表达为数值字面值的形式几乎没有用处,所以C语言内部并没有特地定义这个概念1。
- 字符串常量(string literal)
C语言存在字符串的概念:它就是一串以NUL字节结尾的零个或多个字符。
字符串通常存储在字符数组中,这也是C语言没有显式的字符串类型的原因。由于NUL字节是用于终结字符串的,所以在字符串内不能有NUL字节。之所以选择NUL作为字符串的终止符,是因为它不是一个可打印的字符。
字符串常量的书写方式是用一对双引号包围一串字符,如下所示:
"Hello" "\aWarning!\a" "Line 1\nLine2" ""
ANSI C声明如果对一个字符串常量进行修改,其效果是未定义的。在实践中,请尽量避免这样做!
如果需要修改字符串,请把它存储于数组中!
你可以把字符串常量赋值给一个“指向字符的指针”,后者指向这些字符所存储的地址。但是,你不能把字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。
如果你觉得不能赋值或复制字符串显得不方便,你应该知道标准C函数库包含了一组函数,它们就用于操纵字符串,包括对字符串进行复制、连接、比较以及计算字符串长度和在字符串中查找特定字符的函数。
基本声明
声明变量的基本形式是:
说明符(一个或多个) 声明表达式列表
说明符(specifier) 包含了一些关键字,用于描述被声明的标识符的基本类型。说明符也可以用于改变标识符的缺省存储类型和作用域。
初始化
在一个声明中,你可以给一个标量变量指定一个初始值,方法是在变量名后面跟一个等号(赋值号),后面是你想要赋给变量的值。
声明简单数组
为了声明一个一维数组,在数组名后面要跟一对方括号,方括号里面是一个整数,指定数组中元素的个数。举例如下:
int values[20];
数组的下标总是从0开始,最后一个元素的下标是元素的数目减1。
C数组另一个值得关注的地方是,==编译器并不检查程序对数组下标的引用是否在数组的合法范围之内2。==这种不加检查的行为有好处也有坏处。好处是不需要浪费时间对有些已知是正确的数组下标进行检查。坏处是这样做将使无效的下标引用无法诶检测出来。
一个良好的经验法则是:
如果下标是从那些已知是正确的值计算得来,那么就无需检查它的值。如果一个用作下标的值是根据某种方法从用户输入的数据产生而来的,那么在使用它之前必须进行检测,确保它们位于有效的范围之内。
声明指针
声明表达式也可用于声明指针。在C语言的声明中,先给出一个基本类型,紧随其后的是一个标识符列表,这些标识符组成表达式,用于产生基本类型的变量。例如:
int *a;
这条语句标识表达式*a
产生的结果类型是int
。知道了*
操作符执行的是间接访问操作以后,我们可以推断a
肯定是一个指向int
的指针3。
⚠️警告:
指针的*
操作符需写在靠近变量名的一侧!
##隐式声明
int a[10];
int c;
b[10];
d;
f( x )
{
return x + 1;
}
这个程序第3行和第4行在ANSI C中是非法的。第3行缺少类型名,但对于K&R编译器而言,它已经拥有足够的信息判断出这条语句是一个声明。但令人惊奇的是,有些K&R编译器还能正确地把第4行也按照声明进行处理。函数f缺少返回类型,于是编译器就默认它返回整型。参数x也没有类型名,同样被默认为整型。
typedef
C语言支持一种叫作typedef
的机制,它允许你为各种数据类型定义新名字。typedef
声明的写法和普通的声明基本相同,只是把typedef
这个关键字出现在声明的前面。例如:
typedef char *ptr_to_char;
这个声明把标识符ptr_to_char
作为指向字符的指针类型的新名字。你可以向使用任何预定义名字一样在下面的声明中使用这个新名字。例如:
ptr_to_char a;
声明a
是一个指向字符的指针。
使用typedef
声明类型可以减少使声明变得又臭又长的危险,尤其是那些复杂的声明4。而且,如果你以后觉得应该修改程序所使用的一些数据的类型时,修改一个typedef
声明比修改程序中与这种类型有关的所有变量(和函数)的所有声明要容易的得多。
Tips:
你应该使用typedef
而不是#define
来创建新的类型名,因为后者无法正确地处理指针类型,例如:
#define d_ptr_to_char char *
d_ptr_to_char a,b;
正确的声明了
a
,但是b
却被声明为一个字符。在定义更为复杂的类型名字时,如函数指针或指向数组的指针,使用typedef
更为合适。
常量
ANSI C允许你声明常量,常量的样子和变量完全一样,只是它们的值不能修改。你可以使用const
关键字来声明常量,如下面例子所示:
int const a;
const int a;
这两条语句都把a声明为一个整数,它的值不能被修改。
由于a的值无法被修改,所以你无法把任何东西赋值给它。如此一来,你怎样才能让它在一开始拥有一个值呢?
有两种方法:
- 首先,你可以在声明时对它进行初始化,如下所示:
int const a=15;
- 其次,在函数中声明为
const
的形参在函数被调用时会得到实参的值。
?当涉及指针变量时,情况就变得更加有趣,因为有两样东西都有可能成为常量——指针变量和它所指向的实体。下面是几个声明的例子:
int *pi;
pi是一个普通指向整型的指针。而变量
int const *pci;
则是一个指向整型常量的指针。你可以修改指针的值,但你不能修改它所指向的值。相比之下:
int * const cpi;
则声明cpi为一个指向整型的常量指针。此时指针是常量,它的值无法修改,但你可以修改它所指向的整型的值。
int const * const cpci;
最后,在cpci这个例子里,无论是指针本身还是它所指向的值都是常量,不允许修改。
#define
指令是另一种创建名字常量的机制。例如下面这两个声明都为50这个值创建了名字常量。
#define MAX_ELEMENTS 50
int const max_elements = 50;
在这种情况下,使用#define
比使用const
变量更好。因为只要允许使用字面值常量的地方都可以使用前者,比如声明数组的长度。const
变量只能用于允许使用变量的地方。
#作用域
当变量在程序的某个部分被声明时,它只有在程序的一定区域才能被访问。这个区域由标识符的作用域(scope)决定。标识符的作用域就是程序中该标识符可以被使用的区域。
编译器可以确认4种不同类型的作用域——文件作用域、函数作用域、代码块作用域和原型作用域。标识符声明的位置决定它的作用域。
代码块作用域
位于一对花括号之间的所有语句称为一个代码块。任何在代码块的其他的开始位置声明的标识符都具有代码块作用域(block scope),表示它们可以被这个代码块中的所有语句访问。
函数定义的形式参数在函数体内部也具有代码块作用域。
当代码块处于嵌套状态时,声明于内层代码块的标识符的作用域到达该代码块的尾部便告终止。然而,如果内层代码块有一个标识符的名字与外层代码块的一个标识符同名,内层的那个标识符就将隐藏外层的标识符——外层的那个标识符无法在内层代码块中通过名字访问。
文件作用域
任何在所有代码块之外声明的标识符都具有文件作用域(file scope),它表示这些标识符从它们声明之处直到它所在的源文件结尾处都是可以访问的。
在头文件中编写并通过#include
指令包含到其他文件中的声明就好像它们是直接写在那些文件中一样。它们的作用域并不局限于头文件的文件尾。
原型作用域
原型作用域(prototype scope) 只适用于在函数原型中声明的参数名。
函数作用域
函数作用域(function scope) 只适用于语句标签,语句标签用于goto
语句。基本上,函数作用域可以简化为一条规则——一个函数中的所有语句标签必须唯一。
链接属性
当组成一个程序的各个源文件分别被编译之后,所有的目标文件以及那些从一个或多个函数库中引用的函数链接在一起,形成可执行程序。
标识符的**链接属性(linkage)**决定如何处理在不同文件中出现的标识符。标识符的作用域与它的链接属性有关,但这两个属性并不相同。
链接属性一共有3种——external(外部)、internal(内部) 和 none(无)。
没有链接属性的标识符(none)总是被当做单独的个体,也就是说该标识符的多个声明被当作独立不同的实体。属于internal链接属性的标识符在同一个源文件内的所有声明中都指同一个实体,但位于不同源文件的多个声明则分属不同的实体。最后,属于external链接属性的标识符不论声明多少次、位于几个源文件都表示同一个实体。
存储类型
变量的存储类型(storage class)是指存储变量值的内存类型。变量的存储类型决定变量何时创建、何时销毁以及它的值将保持多久。有三个地方可以用于存储变量:普通内存、运行时堆栈、硬件寄存器。
凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内,这类变量称为静态(static)变量。对于这类变量,你无法为它们指定其他存储类型。静态变量在程序运行之前创建,在程序的整个执行期间始终存在。它始终保持原先的值,除非给它赋一个不同的值或者程序结束。
在代码块内部声明的变量的缺省存储类型是自动的(automatic),也就是说它存储于堆栈中,称为自动(auto)变量。在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开该代码块时,这些自动变量便自行销毁。在代码块再次执行时,这些自动变量在堆栈中所占据的内存位置有可能和原先位置相同,也可能不同。
对于在代码块内部声明的变量,如果给它加上关键字static
,可以使它的存储类型从自动变为静态。具有静态存储类型的变量在整个程序执行过程中一直存在,而不仅仅在声明它的代码块的执行时存在。
关键字register
可以用于自动变量的声明,提示它们应该存储于机器的硬件寄存器而不是内存中,这类变量称为寄存器变量。通常,寄存器变量比存储于内存的变量访问起来效率更高。
static
关键字
当它用于函数定义时,或用于代码块之外的变量声明时,static
关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用于不受影响。用这种方式声明的函数或变量只能在声明它们的源文件中访问。
当它用于代码块内部的变量声明时,static
关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用于不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。
作用域、存储类型示例
下面包含了一个例子程序,阐明了作用域和存储类型。
int a = 5;
extern int b;
static int c;
int d( int e )
{
int f = 15;
register int b;
static int g = 20;
extern int a;
...
{
int e;
int a;
extern int h;
...
}
...
{
int x;
int e;
...
}
...
}
static int i()
{
...
}
...
属于文件作用域的声明在缺省情况下为external链接属性,所以第1行的a的链接属性为external。如果b的定义在其他地方,第2行的extern
关键字在技术上并非必需,但在风格上却是加上这个关键字为好。第3行的static
关键字修改了c的缺省链接属性,把它改为internal。声明了变量a和b(具有external链接属性)的其他源文件在使用这两个变量时实际所访问的是声明于此处的这两个变量。但是,变量c只能由这个源文件访问,因为它具有internal链接属性。
变量a、b、c的存储类型为静态,表示它们并不是存储于堆栈中。这些变量在程序执行之前,并一直保持它们的值,直到程序结束。
当程序开始执行时,变量a将初始化为5。
第8行和第14行声明的局部变量a和b在那部分程序中将隐藏同名的静态变量。因此,这3个变量的作用域为:
a 第1至13行,第18至32行
b 第2至7行,第26至32行
c 第3至32行
第5行声明了2个标识符。d的作用域从第5行直到文件结束。函数d的定义对于这个源文件中任何想要调用它的函数而言起到了函数原型的作用。作为函数名,d在缺省情况下具有external链接属性,所以其他源文件只要在文件上存在d的原型5,就可以调用d。如果我们将函数声明为static
,就可以把它的链接属性从external改为internal,但这样做将使其他源文件不能访问这个函数。对于函数而言,存储类型并不是问题,因为代码总是存储于静态内存中。
参数e不具有链接属性,所以我们只能从函数内部通过名字访问它。它具有自动存储类型,所以它在函数被调用时被创建,当函数返回时消失。由于与局部变量冲突,它的作用域限于第7至12行,第18至20行以及第24至25行。
第7至9行声明局部变量,所以它们的作用域到函数结束为止。它们不具有链接属性,所以它们不能在函数的外部通过名字访问(这是它们称为局部变量的原因)。f的存储类型是自动,当函数每次被调用时,它通过隐式赋值被初始化为15。b的存储类型是寄存器类型,所以它的初始值是辣鸡。g的存储类型是静态,所以它在程序的整个执行过程中一直存在。当程序开始执行时,它被初始化为20。当程序每次被调用时,它并不会被重新初始化。
第9行的声明并不需要。这个代码块位于第1行声明的作用域之内。
第13和14行为代码块声明局部变量。它们都具有自动存储类型,不具有链接属性,它们的作用域延伸至第17行。这些变量和先前声明的a和e不同,而且由于名字冲突,在这个代码块中,以前声明的同名变量时不能被访问的。
第15行使全局变量h在这个代码块内可以被访问。它具有external链接属性,存储于静态内存中。这是唯一一个必须使用extern
关键字的声明,如果没有它,h将变成另一个局部变量。
第20和21行用于创建局部变量(自动、无链接属性、作用域限于本代码块)。这个e和参数e是不同的变量,它和第13行声明的e也不相同。
最后,第27行声明了函数i,它具有静态链接属性。静态链接属性可以防止它被这个源文件之外的任何函数调用。事实上,其他的源文件也可能声明它自己的函数i,它与这个源文件的i是不同的函数。i的作用域从它声明的位置直到这个源文件结束。函数d不可以调用函数i,因为在d之前不存在i的原型。
总结
具有external链接属性的实体在其他语言的术语里称为全局实体(global),所有源文件中的所有函数均可以访问它。只要变量并非声明于代码块或函数定义内部,它在缺省情况下的链接属性即为external。如果一个变量声明于代码块内部,在它前面添加extern
关键字将使它所引用的是全局变量而非局部变量。
具有external链接属性的实体总是具有静态存储类型。
局部变量由函数内部使用,不能被其他函数通过名字引用。它在缺省情况下的存储类型为自动,这是基于两个原因:其一,当这些变量需要时才为它们分配存储,这样可以减少内存的总需求量。其二,在堆栈上为它们分配存储可以有效地实现递归。
作用域、链接属性和存储类型总结:
变量类型 | 声明的位置 | 是否存于堆栈 | 作用域 | 如果声明为static |
---|---|---|---|---|
全局 | 所有代码块之外 | 否6 | 从声明处到文件尾 | 不允许从其他源文件访问 |
局部 | 代码块起始处 | 是7 | 整个代码块8 | 变量不存储于堆栈中,它的值在程序整个执行期一直保持 |
形式参数 | 函数头部 | 是7 | 整个函数8 | 不允许 |
有一个例外:
NULL
指针,它可以用零值来表示。 ↩︎从技术上说,让编译器准确地检查下标值是否有效是做得到的,但这样做将带来极大的额外负担。有些后期的编译器,如Borland C++ 5.0,把下标检查作为一种调试工具,你可以选择是否启用它。 ↩︎
间接访问操作只对指针变量才是合法的。指针指向结果值。对指针进行间接访问操作可以获得这个结果值。 ↩︎
typedef
在结构中特别有用。 ↩︎实际上,只有当d的返回值不是整型时才需要原型。推荐为你调用的所有函数添加原型,因为它减少了发生难以检测的错误的机会。 ↩︎
存储于堆栈的变量只有当该代码块处于活动期间,它们才能保持自己的值。当程序的执行流离开该代码块时,这些变量的值将丢失。 ↩︎
并非出处与堆栈的变量在程序开始执行时创建,并在整个程序执行期间一直保持它们的值,不管它们是全局变量还是局部变量。 ↩︎ ↩︎