C++编译底层
C/C++编译底层
C++内存管理
- 栈: 存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长;
- 堆:
malloc
/free
开辟内存的空间,从低地址向高地址增长; - 自由存储区:
new
/delete
开辟内存的空间; - 数据区:数据区包含全局、静态存储区和常量存储区,存储已初始化的全局变量和静态变量、未初始化的全局变量和静态变量及字符串常量;
- 代码区 存储程序的机器代码和程序指令;
LINUX进程区分段及存储数据
Linux的每个进程都有各自独立的4G逻辑地址,其中0-3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。
逻辑地址分段如下,自下而上:
- 代码段(
text
和rodata
段)。分为只读存储区和代码区,存放字符串常量和程序机器代码和指令 - 数据段(
data
段)。存储已初始化的全局变量和静态变量。 bss
段。存储未初始化的全局变量和静态变量,及初始化为 0 的全局变量和静态变量- 堆。 当进程未调用
malloc
时是没有堆段的,malloc/free
开辟的内存空间,向上生长 - 映射区。存储动态链接库以及调用
mmap
函数进行的文件映射 - 栈。存储函数的返回地址、参数、局部变量、返回值,向下生长。
GCC编译流程
- 预处理阶段:
hello.c
– “gcc -E
预处理,头文件展开,宏替换,删除注释、空白” –hello.i
- 编译阶段:
hello.i
– “gcc -s
检查语法规范、生成汇编文件” –hello.s
- 汇编阶段:
hello.s
– “gcc -c
生成二进制文件” –hello.o
- 链接阶段:
hello.o
– “数据段合并、地址回填,调用ld
进行链接” –a.out
动态库静态库区别及GCC加载库
静态库特点:
- 编译时期链接;
- 浪费空间和资源,如果多个程序链接了同一个库,则每一个生成的可执行文件就都会有一个库的副本,必然会浪费系统空间;
- 若静态库需修改,需重新编译所有链接该库的程序;
动态库特点:
- 运行时链接;
- 运行时被链接,故程序的运行速度稍慢;
- 动态库是在程序运行时被链接的,所以磁盘上只须保留一份副本,因此节约了磁盘空间。如果发现了 bug 或要升级也很简单,只要用新的库把原来的替换掉即可;
GCC 编译加载静态库:
- 将所有的
.c
文件编译成.o
目标文件gcc -c add.c
生成add.o
gcc -c max.c
生成max.o
- 对生成的.o目标文件打包生成静态库
ar crv libfoo.a add.o max.o //libfoo.a是库的名字,其中lib一定要有
- 命令
ar
:做库的命令 - 参数
c
:创建库 - 参数
r
:将方法添加到库里 - 参数
v
:显示过程,可以不要
- 使用静态库
gcc -o main main.c -static -L. -lfoo //这里写的foo是去掉前后缀后库的名字
-L
:指定要链接的库的搜索路径,.
代表当前路径-l
:指定要链接的库名
GCC 编译和加载动态库:
- 对生成的
.o
文件处理生成共享库(动态库),共享库的名字为libfoo.so
gcc -shared -fPIC -o libfoo.so add.o max.o
-shared
表示输出结果是共享库类型的-fPIC
表示使用地址无关代码(Position Independent Code)技术来生产输出文件
- 动态库的使用,也即如何使程序在运行时能动态连接到动态库(此时必须将动态库加载到内存中),有以下方案:
cp libfoo.so /usr/lib // 将库拷贝到系统库路径下
(不推荐)- 使用
export
更改LD_LIBRARY_PATH
当前终端的环境变量(例如,如果你的终端用的是 zsh,则可以在.zshrc
文件中使用export
修改LD_LIBRARY_PATH
环境变量) - 修改
/etc/ld.so.conf
文件,加入库文件所在目录的路径,然后运行ldconfig
目录名字,该命令会重建/etc/ld.so.cache
文件即可 - 上面三种选一个即可
gcc -o main main.c -lfoo
extern-C的结果和CPP编译的区别
- 一个C语言文件p.c
1
2
3
4
5
void print(int a,int b)
{
printf("这里调用的是C语言的函数:%d,%d\n",a,b);
} - 一个头文件p.h
1
2
3
4
5
6
void print(int a,int b); - C++文件调用C函数
1
2
3
4
5
6
7
8
9
using namespace std;
int main()
{
cout<<"现在调用C语言函数\n";
print(3,4);
return 0;
} - 编译后链接出错:
main.cpp
对print(int, int
)未定义的引用。 - 原因分析
p.c
我们使用的是 C 语言的编译器 gcc 进行编译的,其中的函数print
编译之后,在符号表中的名字为_print
- 我们链接的时候采用的是 g++进行链接,也就是 C++链接方式,程序在运行到调用
print
函数的代码时,会在符号表中寻找_print_int_int
(是按照 C++的链接方法来寻找的,所以是找_print_int_int
而不是找_print
)的名字,发现找不到,所以会提示“未定义的引用” - 此时如果我们在对
print
的声明中加入extern “C”
,这个时候,g++编译器就会按照 C 语言的链接方式进行寻找,也就是在符号表中寻找_print
,这个时候是可以找到的,是不会报错的。
- 总结
- 编译后底层解析的符号不同,C 语言是
_print
,C++是_print_int_int
- 编译后底层解析的符号不同,C 语言是
重载的底层原理
根据上面的编译分析,可以知道C语言没有重载,只有C++才有函数重载,因为函数重载通过参数列表的不同来实现。
- C语言没有重载在 C 语言中被解析为
1
2
3"int __cdecl Add(int,int)" (?Add@@YAHHH@Z)
"double __cdecl Add(double,double)" (?Add@@YANNN@Z)
"long __cdecl Add(long,long)" (?Add@@YAJJJ@Z)_Add
,三个一样,所以不能进行区分,因此 C 语言不支持函数重载 - C++重载:底层的重命名机制将
Add
函数根据参数的个数,参数的类型,返回值的类型都做了重新命名。那么借助函数重载,一个函数就有多种命名机制。_Add_int_int
,_Add_long_long
,_Add_double_double
- C++中可以通过在函数声明前加
extern "C"
将一个函数按照 C 语言的风格来进行编译。
编译性语言和解释性语言的本质区别和优缺点
- 根本区别
- 计算机不能直接的理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言的编写的程序。翻译的方式有两种,一个是编译,一个是解释。两种方式只是翻译的时间不同。
- 解释性语言不用编译,在运行时翻译
- 编译性语言是编译的时候直接编译成机器可以执行的语言,编译和运行是分开的,但是不能跨平台。比如 exe 文件,以后要运行的话就不用重新编译了,直接使用编译的结果就行了(exe 文件),因为翻译只做了一次,运行的时不要翻译,所以编译型语言的程序执行效率高
- 编译性语言的优缺点
- 优点
- 运行速度快,代码效率高,编译后程序不可以修改,保密性好
- 缺点
- 代码需要经过编译方可运行,可移植性差,只能在兼容的操作系统上运行。
- 优点
- 解释性语言的优缺点
- 优点
- 可移植性好,只要有解释环境,可以在不同的操作系统上运行。
- 缺点
- 运行需要解释环境,运行起来比编译的要慢,占用的资源也要多一些,代码效率低,代码修改后就可以运行,不需要编译过程
- 优点