目录
1.C++的const变化
#include <stdio.h>
int main(){
const int n = 10;
int *p = (int*)&n; //必须强制类型转换
*p = 99; //修改const变量的值
printf("%d\n", n);
return 0;
}
注意,&n
得到的指针的类型是const int *
,必须强制转换为int *
后才能赋给 p,否则类型是不兼容的。
将代码放到.c
文件中,以C语言的方式编译,运行结果为99
。再将代码放到.cpp
文件中,以C++的方式编译,运行结果就变成了10
。这种差异正是由于C和C++对 const 的处理方式不同造成的。
在C语言中,使用 printf 输出 n 时会到内存中获取 n 的值,这个时候 n 所在内存中的数据已经被修改成了 99,所以输出结果也是 99。而在C++中,printf("%d\n", n);
语句在编译时就将 n 的值替换成了 10,效果和printf("%d\n", 10);
一样,不管 n 所在的内存如何变化,都不会影响输出结果。
当然,这种修改常量的变态代码在实际开发中基本不会出现,本例只是为了说明C和C++对 const 的处理方式的差异:C语言对 const 的处理和普通变量一样,会到内存中读取数据;C++ 对 const 的处理更像是编译时期的#define
,是一个值替换的过程。
const全局变量有说法,它可以在头文件中被定义,可以被重复包含,因为作用域只有当前文件。所以调用需要include不需要extern。
2.头文件处理
(1)根据这份规范,头文件可以包含如下的内容:
- 可以声明函数,但不可以定义函数。
- 可以声明变量,但不可以定义变量。
- 可以定义宏,包括带参的宏和不带参的宏。
- 结构体的定义、自定义数据类型一般也放在头文件中。
(2)强符号与弱符号处理
在C语言中,编译器默认函数和初始化了的全局变量为强符号(Strong Symbol),未初始化的全局变量为弱符号(Weak Symbol)。强符号之所以强,是因为它们拥有确切的数据,变量有值,函数有函数体;弱符号之所以弱,是因为它们还未被初始化,没有确切的数据。
链接器会按照如下的规则处理被多次定义的强符号和弱符号:
1) 不允许强符号被多次定义,也即不同的目标文件中不能有同名的强符号;如果有多个强符号,那么链接器会报符号重复定义错误。
2) 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,那么选择强符号。
3) 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个。
比如目标文件 a.o 定义全局变量 global 为 int 类型,占用4个字节,目标文件 b.o 定义 global 为 double 类型,占用8个字节,那么被链接后,符号 global 占用8个字节。请尽量不要使用多个不同类型的弱符号,否则有时候很难发现程序错误。
main.c 源码:
#include <stdio.h>
//弱符号
__attribute__((weak)) int a = 20;
__attribute__((weak)) void func(){
printf("C Language\n");
}
int main(){
printf("a = %d\n", a);
func();
return 0;
}
module.c 源码:
#include <stdio.h>
//强符号
int a = 9999;
void func(){
printf("c.biancheng.net\n");
}
在 GCC 中,使用下面的命令来运行程序:
$gcc main.c module.c
$./a.out
a = 9999
c.biancheng.net
在 GCC 中,可以通过__attribute__((weak))
来强制定义任何一个符号为弱符号。
需要注意的是,__attribute__((weak))
只对链接器有效,对编译器不起作用,编译器不区分强符号和弱符号,只要在一个源文件中定义两个相同的符号,不管它们是强是弱,都会报“重复定义”错误。下面代码报错。
#include <stdio.h>
__attribute__((weak)) int a = 20;
int a = 9999;
int main(){
printf("a = %d\n", a);
return 0;
}
(3)强引用和弱引用
弱引用和强引用非常利于程序的模块化开发,我们可以将程序的扩展模块定义为弱引用,当我们将扩展模块和程序链接在一起时,程序就可以正常使用;如果我们去掉了某些模块,那么程序也可以正常链接,只是缺少了某些功能,这使得程序的功能更加容易裁剪和组合。
(4)extern变量用法
当用extern声明一个全局变量的时候,首先应明确一点:extern的作用范围是整个工程,也就是说当我们在.h文件中写了extern int a;链接的时候链接器会去其他的.c文件中找有没有int a的定义,如果没有,链接报错;当extern int a;写在.c文件中时,链接器会在这个.c文件该声明语句之后找有没有int a的定义,然后去其他的.c文件中找,如果都找不到,链接报错。
我们知道,普通全局变量的作用域是当前文件,但是在其他文件中也是可见的,使用extern
声明后就可以使用,这在《C语言头文件的编写》一章中进行了深入讲解。下面是多文件编程的演示代码,VS2013可用,需用VS2013提供的控制台程序模板编写。
#include <stdio.h>
extern void func();
extern int m;
int n = 200;
int main(){
func();
printf("m = %d, n = %d\n", m, n);
return 0;
}
#include <stdio.h>
int m = 100;
void func(){
printf("Multiple file programming!\n");
}
3.符号问题解释
(1)静态链接与动态链接
这种在程序运行之前确定符号地址的过程叫做静态链接(Static Linking);如果需要等到程序运行期间再确定符号地址,就叫做动态链接(Dynamic Linking)。
Windows 下的 .dll 或者 Linux 下的 .so 必须要嵌入到可执行程序、作为可执行程序的一部分运行,它们所包含的符号的地址就是在程序运行期间确定的,所以称为动态链接库(Dynamic Linking Library)。
变量和函数一样,都是符号,都需要确定它的地址。例如在 a.c 中有一个 int 类型的全局变量 var,现在需要在 b.c 中对它赋值 42,对应的C语言代码是:var = 100;
对应的汇编代码为:mov 0x2a, var
mov 用来将一份数据移动到一个存储位置,这里表示将 0x2a 移动到 var 符号所代表的位置,也就是对 var 变量赋值。
(2)符号的概念
函数和变量在本质上是一样的,都是地址的助记符,在链接过程中,它们被称为符号(Symbol)。链接器的一个重要任务就是找到符号的地址,并对每个重定位入口进行修正。
我们可以将符号看做是链接中的粘合剂,整个链接过程正是基于符号才能正确完成。
在《目标文件里面有什么,它是如何组织的》一节中讲到,目标文件被分成了多个部分,其中有一个叫做符号表(Symbol Value),它的段名是.symtab
。符号表记录了当前目标文件用到的所有符号,包括:
-
全局符号,也就是函数和全局变量,它们可以被其他目标文件引用。
-
外部符号(External Symbol),也就是在当前文件中使用到、却没有在当前文件中定义的全局符号。
-
局部符号,也就是局部变量。它们只在函数内部可见,对链接过程没有作用,所以链接器往往也忽略它们。
-
段名,这种符号往往由编译器产生,它的值就是该段的起始地址,比如
.text
、.data
等。
对链接来说,最值得关注的是全局符号,也就是上面的第一类和第二类,其它符号都是次要的。
所有的符号都保存在符号表.symtab
中,它一个结构体数组,每个数组元素都包含了一个符号的信息,包括符号名、符号在段中的偏移、符号大小(符号所占用的字节数)、符号类型等。
确切地说,真正的符号名字是保存在字符串表.strtab
中的,符号表仅仅保存了当前符号在字符串表中的偏移。
(3)符号决议即重定位
当要进行链接时,链接器首先扫描所有的目标文件,获得各个段的长度、属性、位置等信息,并将目标文件中的所有(符号表中的)符号收集起来,统一放到一个全局符号表。
在这一步中,链接器会将目标文件中的各个段合并到可执行文件,并计算出合并后的各个段的长度、位置、虚拟地址等。
在目标文件的符号表中,保存了各个符号在段内的偏移,生成可执行文件后,原来各个段(Section)起始位置的虚拟地址就确定了下来,这样,使用起始地址加上偏移量就能够得到符号的地址(在进程中的虚拟地址)。
这种计算符号地址的过程被称为符号决议(Symbol Resolution)。
重定位表.rel.text
和.rel.data
中保存了需要重定位的全局符号以及重定位入口,完成了符号决议,链接器会根据重定位表调整代码中的地址,使它指向正确的内存位置。
至此,可执行文件就生成了,链接器完成了它的使命。
(4)全局变量和局部变量
在《C语言内存精讲》中的《Linux下C语言程序的内存布局(内存模型)》一节讲到,当程序被加载到内存后,全局变量要在数据区(全局数据区)分配内存,局部变量要在栈上分配内存。
数据区在程序运行期间一直存在,全局变量的位置不会改变,地址也是固定的,所以在链接时就能够计算出全局变量的地址。而栈区内存会随着函数的调用不断被分配和释放,局部变量的地址不能预先计算,必须等到发生函数调用时才能确定,所以链接过程会忽略局部变量。
关于局部变量的定位,在《一个函数在栈上到底是怎样的》中已经进行了讲解,就是 ebp 加上偏移量,这在编译阶段就能给出计算公式(一条简单的语句),程序运行后,只要执行这条语句,就能够得到局部变量的地址。
总结起来,链接的一项重要任务就是确定函数和全局变量的地址,并对每一个重定位入口进行修正。
(5)强符号和弱符号
4.new和delete
在C语言中,动态分配内存用 malloc() 函数,释放内存用 free() 函数。如下所示:
-
int *p = (int*) malloc( sizeof(int) * 10 ); //分配10个int型的内存空间
-
free(p); //释放内存
在C++中,这两个函数仍然可以使用,但是C++又新增了两个关键字,new 和 delete:new 用来动态分配内存,delete 用来释放内存。
用 new 和 delete 分配内存更加简单:
int *p = new int; //分配1个int型的内存空间
delete p; //释放内存
new 操作符会根据后面的数据类型来推断所需空间的大小。
如果希望分配一组连续的数据,可以使用 new[]:
int *p = new int[10]; //分配10个int型的内存空间
delete[] p;
用 new[] 分配的内存需要用 delete[] 释放,它们是一一对应的。
和 malloc() 一样,new 也是在堆区分配内存,必须手动释放,否则只能等到程序运行结束由操作系统回收。为了避免内存泄露,通常 new 和 delete、new[] 和 delete[] 操作符应该成对出现,并且不要和C语言中 malloc()、free() 一起混用。
在C++中,建议使用 new 和 delete 来管理内存,它们可以使用C++的一些新特性,最明显的是可以自动调用构造函数和析构函数,后续我们将会讲解。