12.1 编译优化
C语言定义了3种编译修饰符,volatile、const、restrict,用于指导编译器优化代码。
volatile易变的,const不变的。restrict只能修饰malloc返回的指针,表示该指针变量是访问某内存块的唯一方式。
12.2 存储类型
存储类型有4种,寄存器、栈、堆和全局数据区。
C语言定义了5种存储类型修饰符,分别是register、auto、static、extern、thread_local。
register建议存储在寄存器中,只能用于代码块作用域的局部变量。
auto存储在栈中。static存储在全局数据区。
extern表示变量定义在其它文件中,此处只是声明变量,不是定义变量。
thread_local是C++11引入的线程局部变量,每个线程都有一个副本。
12.3 链接方式
链接方式是关于不同文件之间互相访问函数和全局变量的概念。
链接方式有3种,外部链接、内部链接、空链接。
其它文件可以使用的普通函数和全局变量具有外部链接,其它文件想要访问需要声明,变量需要加extern,函数不需要。
只能在文件内部使用的函数和全局变量具有内部链接,static修饰函数和全局变量,可以使其具有内部链接,其它文件不能使用。
static修饰的函数,函数声明也需要加static修饰。
只能在代码块内部访问的变量具有空链接,局部变量具有空链接。
12.4 作用域
作用域是关于单个文件内部函数和变量的概念。
作用域有两种,文件作用域和代码块作用域。
定义在函数外部的变量,叫全局变量,也叫外部变量,具有文件作用域,其作用域是从其定义的位置开始到文件结束。
全局变量定义时不初始化,会被默认初始化为0。对全局变量的修改只能在变量定义时和函数中,除此之外对全局变量的修改无法被执行。
定义在函数内部的变量叫局部变量,具有代码块作用域。
代码块内部的局部变量会覆盖同名的全局变量。同名全局变量和局部变量不会冲突,因为他们的存储位置不同,一个在全局数据区,一个在栈中。
所有的函数和全局变量都具有文件作用域,其作用域都是从其定义的位置开始到文件结束。
文件中函数和全局变量定义位置之前的代码想要使用它们,需要声明,与在其它文件中使用一样,变量需要加extern,函数不需要。
使用static修饰的函数和全局变量,其作用域不变,但其链接方式已变成内部链接,其它文件不能访问。
static修饰局部变量,称为静态局部变量,表示其存储位置固定不变,静态局部变量存储在全局数据区中,而不是栈中。
static修饰局部变量会改变其存储类型,static修饰函数和全局变量会改变其链接方式。
static不论修饰函数还是变量(包含全局变量和局部变量),都不改变其作用域。
对于具有文件作用域的函数和全局变量,使用static修饰会改变其链接方式。
对于具有代码块作用域的局部变量,使用static修饰会改变其存储类型。
12.5 程序的编译、链接、加载和执行
12.5.1 编译
编译器为每个外部链接(没有static修饰的)函数和全局变量分配一个符号,导出符号表给链接器使用。
编译器不会为内部链接(有static修饰的)函数和全局变量以及空链接变量(局部变量)分配符号。
编译器在编译程序时是逐个源文件单独编译的,编译器只处理单个源文件内部函数和变量名字的冲突,不处理不同源文件的符号冲突。
编译器会把存储在全局数据区的变量分为两种类型:
(1)定义时已初始化的全局变量和静态局部变量。
(2)定义时未初始化的全局变量和静态局部变量。
编译器会把源文件中的上述两种变量分别存储在两种数据区,已初始化数据区和未初始化数据区。
定义时已初始化的全局变量和局部变量全部存储在已初始化数据区。
定义时未初始化的全局变量和局部变量全部存储在未初始化数据区。
编译器会为已初始化数据区分配存储空间,并把变量的初始化值存入其中。
编译器不会为未初始化数据区分配存储空间,只会计算其需要占用总存储空间的大小。
上述两个数据区的起始地址是未决参数,在编译时是不确定的,在链接时由连接器指定。
编译器会为每个变量分配一个偏移地址,这个偏移地址是从对应数据区起始地址开始的偏移,这样就可以通过对应数据区起始地址和偏移地址找到每个变量。
只有编译器可以看到所有函数和变量的名字,链接器只能看到编译器导出的符号表、两种数据区地址和每个变量的偏移地址,看不到变量的名字。
12.5.2 链接
链接器把多个源文件链接成一个可执行镜像文件。
链接器会把所有源文件的已初始化数据区打包在一起,保存在可执行文件中,并计算每个源文件已初始化数据区起始地址相对于程序全局数据区的偏移量。
执行程序时,加载器从可执行文件中把所有已初始化数据区整体读出,加载到内存的全局数据区。
链接器会计算所有源文件未初始化数据区需要的总存储空间大小,保存在可执行文件中,并计算每个源文件未初始化数据区起始地址相对于程序全局数据区的偏移量。
执行程序时,加载器从可执行文件中读出大小信息,在内存的全局数据区分配存储空间。
链接程序时,分配给所有全局变量和静态局部变量的寻址地址,并不是变量在内存中的真实地址,而是变量在内存中的真实地址相对于全局数据区的偏移量。
对于在Flash上直接执行程序的单片机,全局数据区的起始地址在链接时就确定了。
对于需要把可执行文件加载进内存执行的程序,全局数据区的起始地址由加载器确定。
12.5.3 连接器的链接规则
链接器在链接程序时会检查所有源文件导出的符号表,C语言链接决议的规则是:强符号重复会报错,弱符号重复会合并。
(1)没有static修饰的函数和已初始化的全局变量是强符号(strong symbol)。
未初始化的全局变量是弱符号(weak symbol)。
(2)多重强符号是链接错误。
(3)一强多弱取强符号。
(4)多个弱符号取占用空间最大的一个。
C语言中,不同源文件定义同名全局变量是否冲突,取决于变量的初始化状态和链接属性。
(1)同名全局变量全部是弱符号,链接器不会报错,而是会合并符号表,选择其中占用空间最大的一个作为最终定义。
所有同名变量将共享同一块内存地址,可能导致数据被意外覆盖。
(2)同名全局变量只有一个是强符号,其它是弱符号,链接器不会报错,而是会合并符号表,选择已初始化的一个作为最终定义。
所有同名变量将共享同一块内存地址,可能导致数据被意外覆盖。
(3)同名全局变量有一个以上强符号,链接阶段会报“multiple definition”错误。
(4)使用static修饰的全局变量,其链接属性是内链接,其作用域被限制在当前文件内部,其他文件无法访问,不会发生冲突。
不同文件的static全局变量存储在不同位置,各自独立分配内存。
定义全局变量时应全部使用static修饰,可以提供接口函数给其它文件使用。
12.5.4 如何判断同名函数和变量是否冲突?
定义在同一个源文件的同名函数和变量,不管有没有static修饰,编译器可以发现错误,报错提示函数或变量重复定义。
定义在不同源文件的同名函数和变量,没有static修饰,编译器无法发现错误,需要链接器处理,在链接器看来,是符号相同的同一个变量。
定义在不同源文件的同名函数和变量,有static修饰,在链接器看来,是偏移量不同的不同变量。
12.5.5 局部变量
普通局部变量存储在栈中,编译器给普通局部变量分配的寻址地址并不是变量在内存中的真实地址,而是变量在内存中的真实地址相对于栈顶的偏移量。
12.6 内存管理
free释放内存时如何知道需要释放的内存有多大?
内存分配函数库会维护2个链表,一个链表记录已分配的内存块,另一个链表记录空闲的内存块,链表的每个节点都会标记内存块的大小。
new和delete与malloc和free的区别,new和delete是C++定义的内存分配方式,在分配和释放内存时会调用构造函数和析构函数。
calloc在分配内存的同时会把内存块中所有内容初始化为0。