一、char类型有无符号取决于编译器的实现
在linux gcc下测试发现gcc编译器将char类型实现为了有符号的,范围为-128~127。arm-linux-gcc将char实现为了无符号的,范围0~255。mipsel-linux-gcc将char实现为有符号的,范围-128~127。测试方法:定义一个char类型变量,并且用一个最高位为1的字面值常量将其初始化,然后比较该变量和该字面值常量,若相等,则该编译器将其char类型实现为unsigned,否则实现为signed。
char c = 0x80;
if (0x80 == c) {
printf("gcc char is unsigned\n");
} else {
printf("gcc char is signed\n");
}
这个问题主要是出于移植性方面的考虑,原书作者建议将char变量值限制在signed和unsigned的交集内使用,也就是0~127(即用低7位二进制数表示的基础ASCII码),并且只有在char类型显示声明为signed或者unsigned时才对其进行算术运算,这是个不错的建议。
二、变量的三个属性——作用域、链接属性、存储类型,这三个属性决定了一个变量的“可视性”(即它在什么地方可以引用)和生命周期(即它的值将保存多久)。
在读到这一部分内容之前,自己已经把extern、static等关键字用的相当熟练,并且想起自己当初找工作时简历上赫然写过“精通C语言”等字样,做笔试时看到类似题目甚至还嗤之以鼻,但是读完这一部分内容,并且是花了一个多小时我认认真真读了两遍之后,不禁满脸羞愧,原来自己的某些理解是错的,并且错的离谱。究其原因,没搞懂作用域和链接属性,甚至之前都不知道变量有个链接属性。
1、作用域。编译器可以确认4种不同类型的作用域,分别是文件作用域、函数作用域、代码块作用域、原型作用域。标识符声明的位置决定它的作用域。注:(前面这两句话直接摘抄于原文)。特别注意第二句话,作用域仅仅是由标识符的声明位置决定的。1)代码块作用域。一个代码块就是用一对花括号包起来的那部分语句,代码块作用域从代码块的开始位置开始到配对的花括号处结束。代码块允许嵌套,当内外层分别出现同名标识符时,内层标识符隐藏外层标识符,即只有内层标识符起作用(当然,这种情况应该避免)。2)文件作用域。声明在所有代码块之外的标识符都具有文件作用域,包括函数名。它们的作用域从在本文件的声明处开始到本文件结束,但有一个例外,就是用#include头文件包含的全局标识符,它们的作用域延伸到包含它们的文件(从被包含的地方起一直到文件结束)。这一点也解释了为什么在函数调用时,被调用的函数必须事先声明或定义。3)原型作用域,只适用于函数原型声明的形参名。函数声明中的形参名非必需,也不必与函数定义的形参名形同,更不必与被调用时传递的实参名相同。原型作用域防止函数声明时的形参名与程序其他地方的变量名冲突,其实,只要在同一个函数声明中不使用两个或以上相同的形参名就可以了。4)函数作用域,只适用于goto的语句标签,在一个函数内,goto的语句标签必须唯一。
2、链接属性,共三种:extern、internal、none(即没有链接属性)。标识符的链接属性是为了处理不同文件中出现的同名标识符。标识符的作用域与链接属性有关,但这两个属性并不相同。(注:以上三句话直接摘抄于原文)。我的误区就在这里。作者有三句话比较经典:1)没有链接属性的标识符总是被当做独立不同的实体; 2)internal链接属性的标识符在同一文件内代表同一个实体,不同文件内的internal链接属性标识符代表不同的实体 ;3)extern链接属性标识符不论声明多少次,在不同文件内都表示同一个实体。再说说哪些标识符的链接属性是extern、哪些的是internal、哪些标识符没有链接属性,作者原文中是以示例说明,我这里概括了一下:1)凡是具有文件作用域的标识符(注意包括函数名),其默认链接属性是extern,其余的标识符都没有链接属性;2)当对默认链接属性为extern的标识符使用static修饰符限定时,标识符的链接属性被改变为internal。特别注意,static修改链接属性时仅仅对默认链接属性为extern的标识符有效,这一点可以区别以下情况:当给一个具有代码块作用域的标识符(即一个局部变量)加以static修饰时,它不会改变该标识符的链接属性,因为根据上面两条中第一条,一个只具有代码块作用域的标识符是没有链接属性的,不是extern,因此第二条规则无效。这种情况下,static仅仅改变了代码块标识符的存储类型而已。最后,当extern关键字用于源文件中一个标识符的第一次声明时,该标识符具有extern链接属性,但是如果extern用于一个标识符的第二次或以后的声明时,它不会修改由第一次声明所指定的链接属性,作者还举了个例子,
static int i; //在该源文件中第一次声明了一个变量i,它的链接属性为internal
int func()
{
int j;
extern int k;//在此之前,k没有被声明过,第一次被extern声明,它的链接属性就是extern
extern int i;//在此之前,i已经被static声明为了internal链接属性,此时extern不会改变其链接属性
}
3、存储类型,指定存储变量值的内存类型(动态内存(即堆栈)还是静态内存)或位置,它决定变量何时创建、何时销毁,即决定变量的生命周期。共有三个地方可以存储变量:普通内存、运行时堆栈、硬件寄存器。1)具有文件作用域的变量存储在静态内存中,它们在程序运行之前就被创建,直到程序运行结束;2)具有代码块作用域的变量默认存储类型为自动的(auto),存储于运行时堆栈,但是若用static修饰会改变其存储类型(注意,作用域仍然不变),该变量将会存储于静态内存中,在整个程序运行过程中它一直存在,并且只会初始化一次。在函数return时可以返回此类变量。还要注意,函数形参不可以声明为static,因为实参总是通过运行时堆栈传递,用于支持递归。3)可以用register关键字声明具有代码块作用域的自动变量,用于将该变量存储于硬件寄存器而不是内存中来提高变量的访问效率。另外要注意,register的使用是否生效和编译器有关,因为编译器有可能会忽略该关键字,出现于以下两种情况:1)如果声明太多的寄存器变量,编译器可能只取前几个放进寄存器,后面的仍然当做自动变量处理;2)如果编译器自己有一套寄存器优化方案,它有可能忽略register关键字。最后,寄存器变量的创建和销毁时间个自动变量相同,但在函数运行之前和返回时它们需要堆栈来进行寄存器内容的保护(入栈)和恢复(出栈)。