C/C++编译底层
- C++内存管理
- LINUX进程区分段及存储数据
- GCC编译流程
- 动态库静态库区别及LINUX加载库
- extern C的结果和CPP编译的区别
- 重载的底层原理
- 编译性语言和解释性语言的本质区别和优缺点
C++内存管理
- 栈 存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长
- 堆 malloc/free开辟内存的空间,从低地址向高地址增长
- 自由存储区 new/delete开辟内存空间
- 数据区
数据区包含全局/静态存储区和常量存储区,存储已初始化的全局变量和静态变量、未初始化的全局变量和静态变量及字符串常量- 代码区 存储程序的机器代码和程序指令
LINUX进程区分段及存储数据
Linux的每个进程都有各自独立的4G逻辑地址,其中0-3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。
逻辑地址分段如下,自下而上:
- 代码段。分为只读存储区和代码区,存放字符串常量和程序机器代码和指令
- 数据段。存储已初始化的全局变量和静态变量。
- 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.ogcc -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 当前终端的环境变量
-
修改/etc/ld.so.conf文件,加入库文件所在目录的路径,然后
运行ldconfig 目录名字,该命令会重建/etc/ld.so.cache文件即可 -
上面三种选一个即可
gcc -o main main.c -lfoo
-
extern-C的结果和CPP编译的区别
- 一个C语言文件p.c
#include <stdio.h>
void print(int a,int b)
{
printf("这里调用的是C语言的函数:%d,%d\n",a,b);
}
- 一个头文件p.h
#ifndef _P_H
#define _P_H
void print(int a,int b);
#endif
- C++文件调用C函数
#include <iostream>
using namespace std;
#include "p.h"
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)的名字,发现找不到,所以会t提示“未定义的引用”
- 此时如果我们在对print的声明中加入 extern “C” ,这个时候,g++编译器就会按照C语言的链接方式进行寻找,也就是在符号表中寻找_print,这个时候是可以找到的,是不会报错的。
-
总结
- 编译后底层解析的符号不同,C语言是_print,C++是_print_int_int
重载的底层原理
根据上面的编译分析,可以知道C语言没有重载,只有C++才有函数重载,因为函数重载通过参数列表的不同来实现。
- C语言没有重载
"int __cdecl Add(int,int)" (?Add@@YAHHH@Z)
"double __cdecl Add(double,double)" (?Add@@YANNN@Z)
"long __cdecl Add(long,long)" (?Add@@YAJJJ@Z)
在C语言中被解析为_Add,三个一样,所以不能进行区分,因此C语言不支持函数重载
-
C++重载
底层的重命名机制将Add函数根据参数的个数,参数的类型,返回值的类型都做了重新命名。那么借助函数重载,一个函数就有多种命名机制。 _Add_int_int,_Add_long_long,_Add_double_double -
C++中可以通过在函数声明前加 extern “C” 将一个函数按照 C 语言的风格来进行编译。
编译性语言和解释性语言的本质区别和优缺点
-
根本区别
- 计算机不能直接的理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言的编写的程序。翻译的方式有两种,一个是编译,一个是解释。两种方式只是翻译的时间不同。
- 解释性语言不用编译,在运行时翻译
- 编译性语言是编译的时候直接编译成机器可以执行的语言,编译和运行是分开的,但是不能跨平台。比如exe文件,以后要运行的话就不用重新编译了,直接使用编译的结果就行了(exe文件),因为翻译只做了一次,运行的时不要翻译,所以编译型语言的程序执行效率高
-
编译性语言的优缺点
- 优点
- 运行速度快,代码效率高,编译后程序不可以修改,保密性好
- 缺点
- 代码需要经过编译方可运行,可移植性差,只能在兼容的操作系统上运行。
- 优点
-
解释性语言的优缺点
- 优点
- 可移植性好,只要有解释环境,可以在不同的操作系统上运行。
- 缺点
- 运行需要解释环境,运行起来比编译的要慢,占用的资源也要多一些,代码效率低,代码修改后就可以运行,不需要编译过程
- 优点