本节书摘来自异步社区《你必须知道的495个C语言问题》一书中的第1章,第1.1节,作者 傅道坤,更多章节内容可以访问云栖社区“异步社区”公众号查看
第1章 声明和初始化
你必须知道的495个C语言问题
C语言的声明语法本身实际上就是一种小的编程语言。一个声明包含如下几个部分(但是并非都必不可少):存储类型、基本类型、类型限定词和最终的声明符(也可能包含初始化列表)。每个声明符不仅声明一个新的标识符,同时也表明标识符是数组、指针、函数还是其他任意的复杂组合。基本的思想是让声明符模仿标识符的最终用法。(问题1.21将会更加详细地讨论这种“声明模仿使用”的关系!)
基本类型
让一些程序员惊奇的是,尽管C语言是一种相当低级的语言,但它的类型体系仍然略显抽象。语言本身并没有精确定义基本类型的大小和表示法。
1.1
问:我该如何决定使用哪种整数类型?
答:如果可能用到很大的数值(大于32 767或小于−32 767),就使用long型。否则,如果空间很重要(例如有很大的数组或很多的结构),就使用short型。除此之外,就用int型。如果定义明确的溢出特征很重要而负值无关紧要,或者希望在操作二进制位和字节时避免符号扩展的问题,请使用对应的unsigned类型。(但是,在表达式中混用有符号和无符号值的时候,要特别注意。参见问题3.21。)
尽管字符类型(尤其是unsigned char型)可以当成“小”整数使用,但这样做有时候很麻烦,不值得。编译器需要生成额外的代码来进行char型和int型之间的转换(导致目标代码量增大),而且不可预知的符号扩展也会带来一堆麻烦。(使用unsigned char会有所帮助。类似的问题参见问题12.1。)
在决定使用float型还是double型时也有类似的空间/时间权衡。(很多编译器在表达式求值的时候仍然把所有的float型转换为double型进行运算)。但如果一个变量的地址确定且必须为特定的类型时,以上规则就不再适用。
很多时候,人们错误地认为C语言类型的大小都有精确的定义。事实上,能够确保的只有如下几点:
char 类型可以存放小于等于127的值;[1]
short int和int可以存放小于等于32 767的值;
long int可以保存小于等于2 147 483 647的值;
char至少有8位,short int和int至少有16位,而long int则至少有32位。在C99中,long long至少有64位。(各类型的有符号和无符号版本的大小可以确保一致。)
根据ANSI C的规定,可以在头文件中找到特定机器下上述类型的最大和最小值,具体如下表所示。
表中的值是标准能够确保的最小值。很多系统允许更大的值,但可移植的程序不能依赖这些值。
如果因为某种原因需要声明一个有精确大小的变量,确保像C99的那样用某种适当的typedef封装这种选择。通常,需要精确大小的唯一的合理原因是试图符合某种外部强加的存储布局。也可参见问题1.3和20.5。
参考资料:[18, Sec. 2.2 p. 34]
[19, Sec. 2.2 p. 36, Sec. A4.2 pp. 195-196, Sec. B11 p. 257]
[8, Sec. 5.2.4.2.1, Sec. 6.1.2.5]
[11, Secs. 5.1, 5.2 pp. 110-114]
1.2
问:为什么不精确定义标准类型的大小?
答:尽管跟其他的高级语言比起来,C语言是相对低级的,但它还是认为对象的具体大小应该由具体的实现来决定。(在C语言中,唯一能够让你以二进制位的方式指定大小的地方就是结构中的位域。参见问题2.26和2.27。)多数程序不需要精确控制这些大小,那些试图达到这一目的的程序如果不这样做也许会更好。
类型int代表机器的自然字长。这是多数整型变量的当然之选。关于整型的选择,参见问题1.1。另请参见问题12.45和20.5。
1.3
问:因为C语言没有精确定义类型的大小,所以我一般都用typedef定义int16和int32。然后根据实际的机器环境把它们定义为int、short、long等类型。这样看来,所有的问题都解决了,是吗?
答:如果你真的需要精确控制类型大小,这的确是正确的方法。但还是有几点需要注意。
在某些机器上可能没有严格的对应关系。(例如,有36位的机器。)
如果定义int16和int32只是为了表明“至少”这么长,则没有什么实际意义。因为int和long类型已经分别被定义为“至少16位”和“至少32位”。
typedef定义对于字节顺序问题不能提供任何帮助。(例如,当你需要交换数据或者满足外部强加的存储布局时。)
你再也不必自己定义这些类型了,因为标准头文件已经定义了标准类型名称int16_t和uint32_t等。
参见问题10.16和20.5。
1.4
问:新的64位机上的64位类型是什么样的?
答:C99标准定义了long long类型,其长度可以保证至少64位,这种类型在某些编译器上实现已经颇有时日了。其他的编译器则实现了类似 longlong的扩展。另一方面,也可以实现16位的short、32位的int和64位的long int。有些编译器正是这样做的。
参见问题18.19。
参考资料:[9, Sec. 5.2.4.2.1, Sec. 6.1.2.5]
指针声明
多数有关指针的问题出现在第4章至第7章,但这里的两个问题和声明的关系特别紧密。
1.5
问:这样的声明有什么问题?
char *p1, p2;
我在使用p2的时候报错了。
答:这样的声明没有任何问题——但它可能不是你想要的。指针声明中的*号并不是基本类型的一部分,它只是包含被声明标识符的声明符(declarator)的一部分(参见问题1.21)。也就是说,在C语言中,声明的语法和解释并非
类型 标识符;
而是
基本类型 生成基本类型的东西;
其中“生成基本类型的东西”——声明符——或者是一个简单标识符,或者是如同*p、a[10]或f()这样的符号,表明被声明的变量是指向基本类型的指针、基本类型的数组或者返回基本类型的函数。(当然,更加复杂的声明符也可以这样组成)。
在问题里的声明中,无论空白的位置暗示了什么,基本类型都是char,而第一个声明符是“p1”。因为声明符中带有号,所以这表明p1是一个指向char类的指针。而p2的声明符中却只有p2,因此p2被声明成了普通的char型变量。这可能并非你所希望。在一行代码中声明两个指针可使用如下方式:
char *p1, *p2;
因为号是声明符的一部分,所以最好像上面这样使用空白;写成char往往导致错误和困惑。
参见问题1.13。
也可参考Bjarne Stroustrup的意见(http://www.hymnsandcarolsofchristmas.com/santa/virginia’s question.htm )。
1.6
问:我想声明一个指针,并为它分配一些空间,但却不行。这样的代码有什么问题?
char *p;
*p = malloc(10);
答:这里声明的指针是p而不是*p。参见问题4.2。
声明风格
在使用函数和变量之前声明它们并不只是为了消除编译器的警告,它也为编程项目注入了有用的秩序。当项目中的声明安排得井然有序的时候,(类型)不匹配和其他的困难就可以更容易地避免,同时编译器也更容易找到出现的错误。
1.7
问:怎样声明和定义全局变量和函数最好?
答:首先,尽管一个全局变量或函数可以(在多个编译单元中)有多处“声明(declaration)”,但是“定义(definition)”却最多只能允许出现一次。对于全局变量,定义是真正分配空间并赋初值(如果有)的声明。对于函数,定义是提供函数体的“声明”。
例如,这些是声明:
extern int i;
extern int f();
而这些是定义:
int i = 0;
int f()
{
return 1;
}
(事实上,在函数的声明中,关键字extern是可选的。参见问题1.11。)
当希望在多个源文件中共享变量或函数时,需要确保定义和声明的一致性。最好的安排是在某个相关的.c文件中定义,然后在头.h(文件)中进行外部声明,在需要使用的时候,只要包含对应的头文件即可。定义变量的.c文件也应该包含该头文件,以便编译器检查定义和声明的一致性。
这条规则提供了高度的可移植性:它和ANSI/ISO C标准一致,同时也兼容大多数ANSI
前的编译器和连接器。(UNIX编译器和连接器常常使用允许多重定义的“通用模式”,只要保证最多对一处进行初始化就可以了。这种方式被ANSI C标准称为一种“通用扩展”,没有语带双关的意思。有几个很老的系统可能曾经要求使用显式的初始化来区别定义和外部声明。)
可以使用预处理技巧来使类似
DEFINE(int, i);
的语句在一个头文件中只出现一次,然后根据某个宏的设定在需要的时候转化成定义或声明。但不清楚这样带来的麻烦是否值得,因为尽量减少全局变量的数量往往是个更好的主意。
把全局声明放到头文件绝对是个好主意:如果希望让编译器检查声明的一致性,一定要把全局声明放到头文件中。特别是,永远不要把外部函数的原型放到.c文件中。如果函数的定义发生改变,很容易忘记修改原型,而错误的原型贻害无穷。
参见问题1.24、10.6、17.2和18.7。
参考资料:[18, Sec. 4.5 pp. 76-77]
[19, Sec. 4.4 pp. 80-81]
[8, Sec. 6.1.2.2, Sec. 6.7, Sec. 6.7.2, Sec. G.5.11]
[14, Sec. 3.1.2.2]
[11, Sec. 4.8 pp. 101-104, Sec. 9.2.3 p. 267]
[22, Sec. 4.2 pp. 54-56]
1.8
问:如何在C中实现不透明(抽象)数据类型?
答:参见问题2.4。
1.9
问:如何生成“半全局变量”,就是那种只能被部分源文件中的部分函数访问的变量?
答:这在C语言中办不到。如果不能或不方便在一个源文件中放下所有的函数,那么有两种常用的解决方案:
(1) 为一个库或相关函数的包中的所有函数和全局变量增加一个唯一的前缀,并警告包的用户不能定义和使用除文档中列出的公用符号以外的任何带有相同前缀的其他符号。(换言之,文档中没有提及的带有相同前缀的全局变量被约定为“私有”。)
(2) 使用以下划线开头的名称,因为这样的名称普通代码不能使用。(关于更多的信息及对用户命名空间和实现命名空间之间的“无人地带”的描述,参见问题1.30。)
也可以使用一些特殊的连接器参数来调整名称的可见性,但这已经超出了C语言的范围了。
存储类型
我们已经讨论了声明的两个部分:基本类型和声明符。下面的几个问题将讨论存储类型,它决定了所声明对象或函数的可见性和生命周期(又称“作用域”和“持续性”)。
1.10
问:同一个静态(static)函数或变量的所有声明都必须包含static存储类型吗?
答:语言标准并没有严格规定这一点(最重要的是第一个声明必须包含static),但是规则却比较复杂,而且对函数和数据对象的规定不太一致。(这个领域有很多历史变化。)因此,最安全的做法是让static一致地出现在所有的定义和声明中。
外部链接:Jutta Degener 的一篇文章(http://c-faq.com/decl/static.jd.html)解释了静态变量和静态函数的规则中的微妙区别。
参考资料:[8, Sec. 6.1.2.2]
[14, Sec. 3.1.2.2]
[11, Sec. 4.3 p. 75]
1.11
问:extern在函数声明中是什么意思?
答:存储类型extern只对数据声明有意义。对于函数声明,它可以用作一种格式上的提示,表明函数的定义可能在另一个源文件中,但在
extern int f();
和
int f();
之间并没有实质的区别。
参考资料:[8, Sec. 6.1.2.2, Sec. 6.5.1]
[14, Sec. 3.1.2.2]
[11, Secs. 4.3, 4.3.1 pp. 75-76]
1.12
问:关键字auto到底有什么用途?
答:毫无用途,它已经过时了。(它是从C语言的无类型前身B语言中继承下来的。在B语言中,没有像int这样的关键字,声明必须包含存储类型。)参见问题20.43。
参考资料:[18, Sec. A8.1 p. 193]
[8, Sec. 6.1.2.4, Sec. 6.5.1]
[11, Sec. 4.3 p. 75, Sec. 4.3.1 p. 76]
类型定义(typedef)
typedef关键字尽管在语法上是一种存储类型,但正如其名称所示,它用来定义新的类型名称,而不是定义新的变量或函数。
1.13
问:对于用户定义类型,typedef和#define有什么区别?
答:一般来说,最好使用typedef,部分原因是它能正确处理指针类型。例如,考虑这些声明:
typedef char *String_t;
#define String_d char *
String_t s1, s2;
String_d s3, s4;
s1、s2和s3都被定义成了char`` *,但s4却被定义成了char型。这可能并非原来所希望的。(参见问题1.5。)
define也有它的优点,因为可以在其中使用#ifdef(参见问题10.15)。另一方面,typedef具有遵守作用域规则的优点(也就是说,它可以在一个函数或块内声明)。
参见问题1.17、2.23、11.12和15.11。
参考资料:[18, Sec. 6.9 p. 141]
[19, Sec. 6.7 pp. 146-147]
[22, Sec. 6.4 pp. 83-84]
1.14
问:我似乎不能成功定义一个链表。我试过
typedef struct {
char *item;
NODEPTR next;
} *NODEPTR;
但是编译器报了错误信息。难道在C语言中结构不能包含指向自己的指针吗?
答:C语言中的结构当然可以包含指向自己的指针。[19]的6.5节的讨论和例子表明了这点。
这里的问题在于typedef。typedef定义了一个新的类型名称。在更简单的情况下[2],可以同时定义一个新的结构类型和typedef类型。但在这里不行。不能在定义typedef类型之前使用它。在上边的代码片段中,在next域声明的地方还没有定义NODEPTR。
要解决这个问题,首先赋予这个结构一个标签(“struct node”)。然后,声明“next”域为“struct node *”,或者分开typedef声明和结构定义,或者两者都采纳。以下是一个修正后的版本:
typedef struct node {
char *item;
struct node *next;
} *NODEPTR;
也可以在声明结构之前先用typedef,然后就可以在声明next域的时候使用类型定义NODEPTR了:
struct node;
typedef struct node *NODEPTR;
struct node {
char *item;
NODEPTR next;
};
这种情况下,你在struct node还没有完全定义的情况下就使用它来声明一个新的typedf,这是允许的。
最后,这是一个两种建议都采纳的修改方法:
struct node {
char *item;
struct node *next;
};
typedef struct node *NODEPTR;
使用哪种方式不过是个风格问题。参见第17章。
参见问题1.15和2.1。
参考资料:[18, Sec. 6.5 p. 101]
[19, Sec. 6.5 p. 139]
[8, Sec. 6.5.2, Sec. 6.5.2.3]
[11, Sec. 5.6.1 pp. 132-133]
1.15
问:如何定义一对相互引用的结构?我试过
typedef struct {
int afield;
BPTR bpointer;
} *APTR;
typedef struct {
int bfield;
APTR apointer;
} *BPTR;
但是编译器在遇到第一次使用BPTR的时候,它还没有定义。
答:与问题1.14类似,这里的问题不在于结构或指针,而在于类型定义。首先,我们定义两个结构标签,然后(不用typedef``)定义链接指针:
struct a {
int afield;
struct b *bpointer;
};
struct b {
int bfield;
struct a *apointer;
};
对于结构a中的域定义struct b *bpointer,尽管编译器此时尚未完成结构b(它在此处还处于“未完成”阶段)的定义,但它仍然可以接受。有时候需要在这对定义之前加上这样一行:
struct b;
这个空声明将这对结构声明(如果处于某个内部作用域)同外部作用域的struct b区分开来。
声明了两个带结构标签的结构之后,可以再分别定义两个类型。
typedef struct a *APTR;
typedef struct b *BPTR;
另外也可以先定义两个类型,然后再使用这些类型来定义链接指针域:
struct a;
struct b;
typedef struct a *APTR;
typedef struct b *BPTR;
struct a {
int afield;
BPTR bpointer;
};
struct b {
int bfield;
APTR apointer;
};
参见问题1.14。
参考资料:[19, Sec. 6.5 p. 140]
[35, Sec. 3.5.2.3]
[8, Sec. 6.5.2.3]
[11, Sec. 5.6.1 p. 132]
1.16
问:struct {...} x1;和typedef struct{...} x2;这两个声明有什么区别?
答:参见问题2.1。
1.17
问:“typedef int (*funcptr)();”是什么意思?
答:它定义了一个类型funcptr,表示指向返回值为int型(参数未指明)的函数的指针。它可以用来声明一个或多个函数指针。
funcptr fp1, fp2;
这个声明等价于以下这种更冗长而且可能更难理解的写法:
int (*pf1)(), (*pf2)();
参见问题1.21、4.12和15.11。
const限定词
C语言的声明还包括类型限定词。这是ANSI C中新提出来的。关于限定词的问题收集在第11章。
1.18
问:我有这样一组声明:
typedef char *charp;
const charp p;
为什么是p而不是它指向的字符为const?
答:参见问题11.12。
1.19
问:为什么不能像下面这样在初始式和数组维度值中使用const值?
const int n = 5;
int a[n];
答:参见问题11.9。
1.20
问:const char p、char const p和char *const p有什么区别?
答:参见问题11.10和1.21。
复杂的声明
C语言的声明可以任意复杂。一旦你熟悉了解读它们的方法,即使最复杂的声明也可以看得明白。不过,首先来说,那些令人眼花缭乱的复杂声明很少是真正必要的。如果你不希望用((a[N])())()这样的神秘声明把你的程序变得混乱不堪,你总是可以像问题1.21的选择(2)那样,用几个类型定义清楚明了地完成。