跳转至

C/C++编译底层

C++内存管理

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

LINUX进程区分段及存储数据

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

  • 代码段。分为只读存储区和代码区,存放字符串是常量和程序机器代码和指令
  • 数据段。存储已初始化的全局变量和静态变量。
  • 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是库的名字
    • 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文件),因为翻译只做了一次,运行的时不要翻译,所以编译型语言的程序执行效率高
  • 编译性语言的优缺点

    • 优点
      • 运行速度快,代码效率高,编译后程序不可以修改,保密性好
    • 缺点
      • 代码需要经过编译方可运行,可移植性差,只能在兼容的操作系统上运行。
  • 解释性语言的优缺点
    • 优点
      • 可移植性好,只要有解释环境,可以在不同的操作系统上运行。
    • 缺点
      • 运行需要解释环境,运行起来比编译的要慢,占用的资源也要多一些,代码效率低,代码修改后就可以运行,不需要编译过程