C语言系列6——存储类型、作用域和位运算


  C提供了多种不同的模型或存储类别(storage class)在内存中存储数据。在此之前,复习一些概念和术语。
  从硬件方面来看,被储存的每个值都占用一定的物理内存,C语言把这样的一块内存称为 对象(object)。从软件方面来看,程序需要一种方法访问对象,这可以通过声明变量来完成,例如 int n = 3;该声明创建一个标识符n,它可以用来指定特定对象的内容。变量名不是指定特定对象的唯一途径,例如 int * pt = &n;表达式*pt不是一个标识符,但是它确实与n指定的对象相同。通常,将用于标识特定数据对象的名称或表达式称为 左值,如果可以使用左值改变对象中的值,该左值就是一个可修改的左值。 右值指的是能赋给可修改左值的量,且本身不是左值。上例中n是左值,3是右值。

1. 作用域和链接

  作用域描述程序中可访问标识符的区域。一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。

  • 定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含该定义的块的末尾。另外,函数的形参虽然声明在左花括号之前,但是它也具有块作用域,属于函数体这个块。
  • 函数作用域仅用于goto语句的标签,这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。
  • 函数原型作用域用于函数原型的形参名,它的范围是从形参定义处到原型声明结束。这意味着,编译器在处理函数原型形参时只关心它的类型,而形参名无关紧要(可以省略),而且形参名不必与函数定义中的形参名相匹配(前面介绍过这点)。
  • 变量的定义在函数外面具有文件作用域。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。例如
#include <stdio.h>
int n=0;
void name(int);

int main(void)
{
	...
}
void name(int)
{
	...
}

这里,变量n就具有文件作用域,main()和name()函数都可以使用它(更准确的说,n具有外部链接文件作用域)。由于这样的变量可用于多个函数,所以文件作用域变量也称为全局变量(global variable)。
  C变量有3种链接属性:外部链接、内部链接或无链接。具有块作用域、函数作用域、函数原型作用域的变量都是无链接变量,意味着这些变量属于定义它们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元使用(即一个源代码文件和它所包含的头文件)。一些人把“内部链接的文件作用域”简称为文件作用域,把“外部链接的文件作用域”简称为全局作用域或程序作用域。

2. 存储类别

  作用域和链接描述了标识符的可见性,存储期描述了通过这些标识符访问的对象的生存期。C对象有4种存储期:静态存储期、动态分配存储期、线程存储期、自动存储期。如果对象具有静态存储期,那么它在程序的执行期间一直存在;线程存储期用于并发程序设计,这里不介绍;块作用域的变量通常具有自动存储期,当程序进入这些定义变量的块时,为这些变量分配内存,退出时,释放内存;动态分配的内存在调用malloc()或相关函数时存在,在调用free()函数后释放,因此,动态分配内存的存储期从调用malloc()函数分配内存到调用free()函数释放内存为止。下表列出5种存储类别:

存储类别存储期作用域链接声明方式
自动自动块内
寄存器自动块内,使用关键字register
静态外部链接静态文件外部所有函数外
静态内部链接静态文件内部所有函数外,使用关键字static
静态无链接静态块内,使用关键字static

补充:

  1. 属于自动存储类别的变量具有自动存储期、块作用域且无链接,默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。有时为了更清楚的表达你的意图,可显示使用关键字auto,但是在编写C/C++兼容程序时,最好不要使用auto,因为在C++中,auto的使用完全不同。
  2. 寄存器变量存储在CPU的寄存器中,不在内存中,无法获取它的地址。相较于一般的变量而言,访问和处理这些变量的速度更快,使用关键字register声明寄存器变量。有时,寄存器变量不会声明成功,比如寄存器内存不够,编译器可能会忽略该请求。
  3. 具有文件作用域的变量自动具有(也必须是)静态存储期。可以是外部链接的静态变量,也可以是内部链接的静态变量。外部链接的静态变量有时称为外部存储类别,属于该类别的变量称为外部变量,把变量的定义式声明放在所有函数之外便创建了外部变量。为了指出该函数使用了外部变量,可以在函数中使用关键字extern进行引用式声明。如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须使用extern进行引用式声明。
  4. 块作用域的静态变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数以后,这些变量不会消失(也无法访问),计算机在多次函数调用之间会记录它们的值。

  存储类别关键字有六个:auto、register、static、extern、typedef、_Thread_local。typedef关键字与任何内存存储无关,把它归于此类是一些语法上的原因。绝大多数情况下,不能在声明中使用多个存储类别说明符,唯一例外的是_Thread_local可以和static或extern一起使用。register只能用于声明块作用域的变量。如果包含extern的声明具有文件作用域,则引用的变量必须具有外部链接,如果包含extern的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,取决于该变量的定义式声明。
  函数也有存储类别,可以是外部函数(默认)或静态函数。外部函数可以被其它文件的函数访问,静态函数只能用于其定义所在的文件。例如有以下函数原型:

double gamma(double); //默认为外部函数
static double beta(int, int);
extern double delta(double, int);

在同一个程序中,其它文件可以调用gamma()和delta()函数,但是不能调用beta(),因为以static存储类别说明符创建的函数属于特定模块私有。
  C99新增了内联函数,关键字为inline,它是为了节省调用函数的时间所设计的,所以一般比较简短。内联函数的定义必须与调用该函数的代码在同一个文件中。鉴于此,一般情况下内联函数具有内部链接。内联函数无法在调试器中显示,由于并未给内联函数预留单独的代码块,所以也无法获得内联函数的地址。

3. 分配内存

  C提供库函数让程序员们可以更灵活的分配和管理内存,malloc()函数可以在程序运行时分配内存,通常和free()函数配套使用(都定义在stdlib.h头文件中)。malloc()函数接受一个参数:所需要的内存字节数。malloc()函数可以找到合适的空闲内存块,但是不会为其赋名。但是会返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。在ANSI之前,该函数返回char类型指针,在ANSI之后,该函数返回void类型指针。该类型相当于一个“通用指针”,malloc()函数可用于返回指向数组的指针、指向结构的指针等,所以通常该函数的指针会被强制转换为匹配的类型。在ANSI C中,应该坚持使用强制类型转换,提高代码的可读性。如果malloc()函数分配内存失败,会返回一个空指针(NULL)。下面看一个具体的例子:

double * ptd;
ptd = (double *) malloc(30 * sizeof(double)); //强制类型转换

上述代码为30个double类型的值请求内存空间,并设置ptd指向该位置。前面说过,数组名表示该数组首元素的地址,因此,如果让ptd指向这个块的首元素,则可以像使用数组名一样使用它,可以用ptd[0]访问该块的首元素,ptd[1]访问第2个元素,以此类推。
  free()函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配的内存。因为动态分配的内存数量只会增加,所以一定不要忘记及时释放动态分配的内存,否则会造成内存泄漏。
  calloc()函数也可以分配内存,它接受两个无符号整数作为参数。第1个参数是所需存储单元数量,第2个参数是所需的存储单元大小(以字节为单位)。例子:

double * newman;
newman = (double *) calloc(100, sizeof(double));

4. 复合字面量

  C99的复合字面量很好用,用于创建指定类型的无名对象,语法为( type ) { initializer-list }type为 指定任何完整对象类型或未知大小的数组的类型名,但不能是 VLA,initializer-list是初始化列表。例:

struct book      //结构模板
{
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
};
struct book readfirst;
/*创建一个复合字面量*/
readfirst = (struct book){"Crime and Punishment","Fyodor Dostoyevsky",11.25};

5. 位运算

  在C语言中,可以单独操作变量中的位。这里推荐看一点微机原理和数电,里面详细介绍了进制、计算机硬件方面的组成、逻辑运算、位运算等。当然不看也不妨碍。

5.1. C的位运算符

  下表列出来C的位运算符:

运算符作用运算结果
~取反取反操作:0变成1,1变成0
&与运算和0与得0,和1与保持不变
|或运算和1或得1,和0或保持不变
^异或和1异或取反,和0异或保持不变
<<左移乘以2的幂次
>>右移如果值非负,除以2的幂次

左移运算符将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端的值丢失,用0填充。例如:

int a = -4;
a <<= 1;

得到-8。右移运算符将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出左末端的值丢失,对于无符号类型,用0填充;对于有符号类型,结果取决于机器(可以用0填充,也可以用符号位填充)。例如:

int a = -4;
a >>= 1;

得到-2,用0填充。

5.2. 位字段

  操控位的另一种方法是位字段(bit field),位字段是一个signed int或unsigned int类型变量的一组相邻的位(C99和C11新增了_Bool类型的位字段)。位字段通过一个结构来声明,该结构声明为每个位字段提供标签,并确定该字段的宽度。例如:

struct {
	unsigned int autfd : 1;
	unsigned int bldfc : 1;
	unsigned int undln : 1;
	unsigned int itals : 1;
} prnt;

根据该结构声明,prnt包含4个1位的字段。现在,可以通过普通的结构运算符单独给这些字段赋值:

prnt.itals = 0;
prnt.undln = 1;

字段不限制1位大小,可以使用如下的代码:

struct {
	unsigned int code1 : 2;
	unsigned int code2 : 2;
	unsigned int code3 : 8;
} prcode;

以上代码创建了2个2位的字段,1个8位的字段。可以这样赋值:

precode.code1 = 1;
precode.code2 = 3;
precode.code3 = 102

这里需要注意的是所赋的值不能超过字段的容纳范围,比如8位无符号只能表示0-255。如果声明的总位数超过了unsigned int类型的大小,会用到下一个unsigned int类型的位置。一个字段不允许跨越两个unsigned int之间的边界,编译器会自动移动跨界的字段,保持unsigned int的边界对齐。一旦发生这种情况,第1个unsigned int中会留下一个未命名的“洞”,示意图如下,这里假设一个unsigned int为8位(实际远不止),声明一个5位的字段和一个6位的字段,一共11位,超过unsigned int的8位:
在这里插入图片描述
可以用未命名的字段“填充”未命名的“洞”,使用一个宽度为0的未命名字段迫使下一个字段与下一个整数对齐:

struct {
	unsigned int field1 : 5;
	unsigned int        : 2;
	unsigned int field2 : 1;
	unsigned int        : 0;
	unsigned int field3 : 6;
} stuff;

在stuff.field1和stuff.field2之间有2位的空隙,stuff.field3存储在下一个unsigned int中。
  最后说一下C的对齐特性。C11的对齐特性比用位填充字节更自然,新增了关键字_Alignof。_Alignof运算符给出一个类型的对齐要求,在关键字后面的圆括号写上类型名即可:size_t d_align = _Alignof(float);假设d_align=4,意思就是4是储存float类型值相邻地址的字节数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值