C++编译底层
2024-01-05 16:19:02 # 技术 # CS基础

C/C++编译底层

C++内存管理

  • : 存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长;
  • malloc / free 开辟内存的空间,从低地址向高地址增长;
  • 自由存储区new / delete 开辟内存的空间;
  • 数据区:数据区包含全局、静态存储区和常量存储区,存储已初始化的全局变量和静态变量、未初始化的全局变量和静态变量及字符串常量;
  • 代码区 存储程序的机器代码和程序指令;

LINUX进程区分段及存储数据

Linux的每个进程都有各自独立的4G逻辑地址,其中0-3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。
逻辑地址分段如下,自下而上:

  • 代码段(textrodata 段)。分为只读存储区和代码区,存放字符串常量和程序机器代码和指令
  • 数据段(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
    #include <stdio.h>
    void print(int a,int b)
    {
    printf("这里调用的是C语言的函数:%d,%d\n",a,b);
    }
  • 一个头文件p.h
    1
    2
    3
    4
    5
    6
    #ifndef _P_H
    #define _P_H

    void print(int a,int b);

    #endif
  • C++文件调用C函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <iostream>
    using namespace std;
    #include "p.h"
    int main()
    {
    cout<<"现在调用C语言函数\n";
    print(3,4);
    return 0;
    }
  • 编译后链接出错:main.cppprint(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语言没有重载
    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)
    在 C 语言中被解析为_Add,三个一样,所以不能进行区分,因此 C 语言不支持函数重载
  • C++重载:底层的重命名机制将 Add 函数根据参数的个数,参数的类型,返回值的类型都做了重新命名。那么借助函数重载,一个函数就有多种命名机制。 _Add_int_int,_ Add_long_long_Add_double_double
  • C++中可以通过在函数声明前加 extern "C" 将一个函数按照 C 语言的风格来进行编译。

编译性语言和解释性语言的本质区别和优缺点

  • 根本区别
    • 计算机不能直接的理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言的编写的程序。翻译的方式有两种,一个是编译,一个是解释两种方式只是翻译的时间不同
    • 解释性语言不用编译,在运行时翻译
    • 编译性语言是编译的时候直接编译成机器可以执行的语言,编译和运行是分开的,但是不能跨平台。比如 exe 文件,以后要运行的话就不用重新编译了,直接使用编译的结果就行了(exe 文件),因为翻译只做了一次,运行的时不要翻译,所以编译型语言的程序执行效率高
  • 编译性语言的优缺点
    • 优点
      • 运行速度快,代码效率高,编译后程序不可以修改,保密性好
    • 缺点
      • 代码需要经过编译方可运行,可移植性差,只能在兼容的操作系统上运行。
  • 解释性语言的优缺点
    • 优点
      • 可移植性好,只要有解释环境,可以在不同的操作系统上运行。
    • 缺点
      • 运行需要解释环境,运行起来比编译的要慢,占用的资源也要多一些,代码效率低,代码修改后就可以运行,不需要编译过程