《C专家编程》读书笔记(2)

 ***数组和指针是如何访问的***

标准规定赋值符必须用可修改的左值(为了与数组名区分,数组名也用于确定对象在内存中的位置,也是左值,但它不能作为赋值的对象)作为它左侧的操作数,而其右侧的操作数是地址的内容(右值)。

数组和指针的差别:
(1)数组只需将起始地址加上偏移地址,再解除引用既可取得数组中的各个元素;而指针需先将自身解除引用以获取所指向对象的地址,再解除引用才可取得所指向对象的内容;

(a)对数组下标的引用:

char  a[ 9 =   " abcdefgh " ;
char  c;
=  a[i];
// 编译器符号表具有一个地址9980
// 运行时步骤1:取i的值,将它与9980相加
// 运行时步骤2:取地址(9980+i)的内容
(b)对指针的引用:
char *  p;
char  c;
=   * p;
// 编译器符号表有一个符号p,它的地址是4624
// 运行时步骤1:取地址4624的内容,得到5081
// 运行时步骤2:取地址5081的内容

(c)定义为指针,但以数组方式引用:

char *  p  =   " abcdefgh " ;
char  c;
=  p[i];
// 编译器符号表具有一个p,地址为4624
// 运行时步骤1:取地址4624的内容,得到5081
// 运行时步骤2:取i的值,将它与5081相加
// 运行时步骤3:取地址(5081+i)的内容

其实质是上述a、b访问方式的组合,只有在p原先定义为指针时这个方法才是正确的。
如果p被声明为extern char* p,而它原先的定义却是char p[10]的情形,既然把p声明为指针,那么不管p原先是定义为指针还是数组,都会按照c方式进行操作。当用p[i]这种方式提取一个声明的内容时,实际上得到的是一个字符。但按照上述的方法,编译器却把它当成是一个指针,把字符解释为地址显然是非法的。

(2)指针通常用于动态数据结构,而数组通常用于存储固定数目且数据类型相同的元素;
(3)指针通常指向匿名数据,而数组自身即为数据名。

 

***编译器的构成***

绝大多数编译器并不是一个单一的庞大程序,而是由预处理器(preprocessor)、语法和语义检查器(syntax and semantic checker)、代码生成器(code generator)、汇编程序(assembler)、优化器(optimizer)、链接器(linker)、驱动器程序(driver program)组成。

 

目标文件并不能直接执行,它首先需要载入到链接器中。链接器确认main函数为初始进入点,把符号引用(symbolic reference)绑定到内存地址,把所有的目标文件集中在一起,再加上库文件,从而产生可执行文件。

 

如果函数库的一份拷贝是可执行文件的物理组成部分,那么我们称之为静态链接;如果可执行文件只是包含了文件名,让载入器在运行时能够找到程序所需要的函数库,那么我们称之为动态链接。

 

***动态链接的相关知识点***

动态链接的优点:
(1)可执行文件小(运行速度稍慢);
(2)链接-编译阶段的时间短(部分工作被推迟到载入时);
(3)ABI(Application Binary Interface),即应用程序二进制接口,把程序与它们使用的特定的函数库版本分离开来,程序可以调用接口所承诺的服务,而不必担心这些功能是怎么提供的或者它们的底层实现是否改变。
(4)允许用户在运行时选择需要执行的函数库。

 

与位置无关的代码表示用这种方法产生的代码保证对于任何全局数据的访问都是通过额外的间接方法完成的。这使它很容易对数据进行重新定位,只要简单地修改全局偏移量表中的一个值即可。根据经验,对于函数库应始终使用与位置无关代码,尤其是对于共享库,因为每个使用共享库的进程一般会把它映射到不同的虚拟地址(尽管共享同一份物理拷贝)。

 

纯代码,也被称作纯可执行文件,是只包含代码(无静态或初始化过的数据)的文件。它之所以称为“纯”是因为它不必进行修改就能被其它特定进程执行,它从堆栈或其它段引用数据。如果生成与位置无关代码(意味着共享),你通常也希望它是纯代码。

 

***警惕Interpositioning***

Interpositioning就是通过编写与库函数同名的函数来取代该库函数的行为。但这种方式有一个十分隐蔽的bug:不仅你自己所进行的所有对该库函数的调用将被自己版本的函数调用所取代,而且所有调用该库函数的系统调用也将用你的函数取而代之。因此,应尽量避免使用Interpositioning。

 

***运行时数据结构***

UNIX中所有的输出文件都缺省地使用同一个名字a.out,在a.out中的第一个字是一个无条件跳转指令,以进入程序第一个真正的可执行指令。紧跟在后面的第二至第四个字节为“ELF”。现在绝大多数SVr4实现都采用这种称作ELF(Executable and Linking Format)的可执行文件。

在UNIX中,段(segment)表示一个二进制文件相关的内容块,section是ELF文件中的最小组织单位。一个段一般包含几个section。

 

a.out文件依次包含以下几部分内容:
(1)a.out神奇数字(用于跳转到第一个真正的可执行指令);
(2)a.out的其它内容;
(3)BSS段所需的大小(BSS段只保存没有值的变量,所以事实上它不需要保存这些变量的映像。运行时所需要的BSS段的大小记录在目标文件中);
(4)数据段(放置初始化后的全局和静态变量);
(5)文本段(放置可执行文件的指令)
备注:程序中的局部变量不进入a.out,它们在运行时创建。

 

***操作系统如何载入a.out文件中的内容***

段可以方便地映射到链接器在运行时可以直接载入的对象中。从本质上说,段在正在执行的程序中是一块内存区域。
进程的地址空间通常包括以下几部分(从低地址到高地址):
(1)未映射区域:
在典型情况下,它是从地址零开始的几K字节,用于捕捉使用空指针和小整型值的指针引用内存的情况;
(1)文本段:
文本段包含程序的指令。链接器把指令直接从文件拷贝到内存中,以后便再也不用管它;
(2)数据段:
数据段包含经过初始化的全局和静态变量以及它们的值。
(3)BSS段:
链接器从a.out中读出BSS段所需的大小,然后得到这个大小的内存块,紧跟在数据段之后。包括数据段和BSS段的整个区段通常统称为数据区。
(4)堆栈段:
用于保存局部变量、临时数据、传递到函数中的参数等。
备注:还需要堆(heap)空间,用于动态分配的内存。

 

***C语言怎样组织运行时数据结构***

运行时数据结构有以下几种:
(1)堆栈段:
运行时系统维护一个指针(常位于寄存器中),通常称为sp,用于提示堆栈当前的顶部位置。
堆栈段主要有三个用途:
(a)堆栈为函数内部声明的局部变量(即自动变量)提供存储空间;
(b)进行函数调用时,堆栈维护与此相关的一些维护性信息(即过程活动记录),包括函数调用地址,任何不适合装入寄存器的参数等;
(c)堆栈也可以被用作暂时存储区,如一些复杂算术表达式的结果可被压到堆栈中。
备注:除了递归调用,堆栈并非必需。
(2)过程活动记录:
过程活动记录的规范描述包括:局部变量、参数、静态链接(用于上层引用,C语言中不使用)、指向先前结构的指针、返回地址等。

 

绝大多数语言允许函数的嵌套定义,而静态链接就是一个指向它的外层函数的活动记录的指针。
静态链接与动态链接的区别:
静态链接(指向从词法上讲属于外层过程的活动记录,由编译时决定)
动态链接(在运行时指向最靠近自己的前一个过程调用的活动记录)

 

***auto和static关键字***
如果从函数中返回一个指向该函数局部自动变量的指针,将会出现“悬垂指针(dangling pointer)”(并不引用有用的东西,而是悬在地址空间中)。原因在于自动变量在堆栈中进行分配,而当函数结束后,变量不复存在,它所占用的堆栈空间被回收,可能在任何时候被覆盖。

 

***内存管理***

堆位于BSS段的上端(更高内存地址),其中的所有东西都是匿名的,只能通过指针间接访问。

总线错误几乎都是由于未对齐的读或写引起的。它之所以称为总线错误,是因为出现未对齐的内存访问请求时,被阻塞的组件就是地址总线。在现代的计算机架构中,数据对齐可极大地简化如Cache等硬件,迫使每个内存访问局限在一个Cache行或一个单独地页面内。例如,访问一个8字节的double数据时,地址只允许是8的整数倍。
一个会引起总线错误的小程序:

union
{
    
char a[10];
    
int i;
}
u;
int *  p  =  ( int * ) & (u.a[ 1 ]);
* =   17 ;     // p中未对齐的地址会引起一个总线错误

 

段错误是由于内存管理单元(负责支持虚拟内存的硬件)的异常所致,而该异常通常是由于解除引用一个未初始化或非法值的指针引起的。如果指针引用一个并不位于尼的地址空间中的地址,操作系统便会对此进行干涉。
一个会引起段错误的小程序:

int   * =   0 ;
* =   17 ;

一个极为常见的与释放内存有关的错误就是在for(p = start;p;p=p->next)这样的循环中迭代使用free(p)释放内存。正确的做法是:

struct  node  * p,  * start,  * tmp;
for (p  =  start;p;p  =  tmp)
{
tmp 
= p->next;
free(p);
}

 

ANSI C中流行的一种不良方法:调用函数和通过指针调用函数(或任意层次的指针间接引用)可以使用同一种语法。具体如下例所示:
int  ( * state[MAX_STATES])();     // 声明函数指针数组
extern   int  a(), b(), c(), d();
int  ( * state[])()  =   { a, b, c, d } ;     // 初始化
( * state[i])();     // 通常的调用函数的方式
state[i]();        // 通过指针调用函数
( **** state[i])();  // 任意层次的指针间接引用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值