all_note

发布时间 2024-01-11 23:21:37作者: 二氧化硅21

进程的虚拟地址空间内存划分和布局

编程语言->产生指令和数据

  • 程序生成exe可执行文件,加载到内存后(不是一步直接加载到物理内存中)如何存放。
  • x86 32位linux下,linux会给进程分配一块2的32次方大小的一块空间(4G),这块空间是一块虚拟内存空间,虚拟内存空间本质上是系列数据结构。
  • 这一块虚拟地址空间分为两块,3G的用户空间,1G的内核空间。
  • 地址起始的一段内容是不可访问的,不可读写。
  • .text为代码段,.rodata为read only只读数据段(常量区,只读不写)
  • .data数据段,.bss也是数据段:前者存储初始化且不为0的数据,后者存储未初始化或初始化为0的。如一个全局变量默认初始化为0,是内核启动阶段操作系统默认对bss中的未初始化变量赋值0。
  • .heap堆内存栈自上而下增长,即低地址->高地址。
  • 加载的共享库即动态库。
  • 函数运行,或产生线程时,都记录在栈stack上,栈自下而上增长,即高地址->低地址。
  • 最后一段是命令行参数和环境变量。
  • 内核空间。

img

图中gdata1-gdata6编译后产生数据于符号表,其中gdata1,gdata4存放于.data段;gdata2,gdata3,gdata5,gdata6存放于.bss段;a,b,c属于栈中的局部变量,链接器对此类变量不感兴趣,符号表不会记录,相反,这三行会产生三条指令。e存放于.data段;f,g存放于.bss段。打印c会显示栈上存放的无效值,打印g则输出0,因为g存储在bss段,自动初始化为0。

红色框存放于代码段.text,蓝色与棕色狂产生变量符号,存放于数据段。

a-c汇编后生成指令存放于代码段,代码执行期间,系统为main函数开辟栈,执行指令将abc的值存放到对应存储空间

每一个进程的用户空间是私有的,内核空间是共享的,进程间可以通过向内核空间中读写数据实现共享。

进程的通信方式:匿名管道通信

函数的调用堆栈详细过程

img
img

  • 第一句:初始化a,符号表中不产生a变量,仅在代码段中保存此句汇编代码,即mov语句,mov语句把10赋值给栈指针
  • 第二局:同第一句
  • 第三局:将实参的值传给形参,调用sum实现相加函数。
    • 首先,sum函数的参数a,b存为寄存器变量并压栈(push,注意压栈是将变量压到栈顶esp),其中形参压栈顺序是从右向左。然后call调用的sum函数,然后立即将call的下一条语句在.text区中的内存地址压栈,该地址标记sum返回后下一步运行的位置。
  • 进入sum后,执行代码前,要先压入ebp栈底地址(标记main),然后开新栈帧:mov ebp, esp,然后esp移动。
  • 执行sum内部的几条语句
  • 将返回结果保存至寄存器变量中
  • 栈帧回退:mov esp, ebp,即将栈顶指针指回栈底。这个过程中,栈被释放,但内容没有被清理,如果一个函数func1返回一个指针,在main中访问该指针指向的值,依然可以访问到,但不安全;如果后续有func2调用,从而建立新的栈帧,那么这块内存可能会被修改。
  • pop ebp弹栈:即将之前压入的ebp栈底地址赋值回给ebp,这意味着函数执行返回了main
  • ret把再次出栈的内容放入CPU的PC寄存器中,继续执行call sum的下一行语句,并继续把两个形参变量所占用的栈空间交还系统,栈顶地址-8

程序编译与链接原理

img

  • 预编译:处理#命令,但保留#pragma,删除注释。
  • 编译:词法分析、语法分析、语义分析和优化,生成汇编代码。
  • 汇编:将汇编代码翻译成机器码(AT&T,x86语法),打包为可重定位二进制目标文件,此文件不可执行。输出符号表。
  • 链接:合并.o文件段,合并符号表,解析并符号重定向。
//以下为main文件
extern int gdata;  //gdata *UND*代表需使用但未定义
int sum(int, int);  //sum *UND*

int data = 20;  //data .data
int main(){  //main .text
    int a = gdata;
    int b = data;
    
    int ret = sum(a, b);
    return 0;
}

//以下为sum文件
int gdata = 10;  //gdata .data
int sum(int a, int b){ //sum_int_int .text
    return a + b;
}

以上为.o文件中的部分符号注释
具体的.o文件的格式组成包括各种段

img
img
编译过程中不为符号分配地址,通过readelf可以观察到地址皆为0。虽然指令已经在编译阶段翻译好,但变量地址皆为0,需要在链接阶段补充,也因此.o文件无法执行。

链接

链接过程合并若干个.o文件的段合并:text段合并,data段合并...,以及符号表的合并。

  • 符号表合并:检查符号表,所有对符号的引用(*UND*),都要找到该符号定义的地方,当然符号定义只能有一个,不可重定义,不可没有。最终符号解析成功。

  • 符号解析成功后,在代码段中,为所有符号分配虚拟地址,并把这些地址写回指令中。这个过程叫做符号重定向。

可执行文件

链接产生可执行文件,可执行文件与二进制可重定向文件段格式几乎一致,但可执行文件中多了一个program headers段,这其中包含若干load项,比如.text,.data,这意味着这些load项要在代码执行时,加载如虚拟内存空间

参数为默认值的函数

  • 参数从右面开始给默认值。
  • 如果不缺省参数,需要在汇编层面mov实参到寄存器,并push压栈此寄存器值;而如果缺省一个参数,在汇编代码层面,相当于少一句mov指令,而可以直接push一个立即数。从而提高效率。
  • 定义和声明两阶段都可以给默认值,但二者只能给一次,即使定义和声明两阶段给了相同的默认值也不可以。不过有下述情况。
  • #include<iostream>
    using namespace std;
    
    int sum(int a, int b = 10);
    int sum(int a = 10, int b);
    //相当于int sum(int a = 10, int b = 10)
    
    int main(){
        int a = 20;
        cout << sum() << endl;
        cout << sum(10);
    }
    int sum(int a, int b){
        return a + b;
    }
    

内联函数

内联函数与普通函数的区别

inline函数:在编译过程中,就没有函数的调用开销了,在函数的调用点直接把函数的代码进行展开处理,符号表中也不产生内联函数符号。

  • 函数的调用开销是什么:参数压栈,栈帧开辟,栈帧回退。
  • inline只是建议,不是所有inline被编译器处理为内联函数,比如递归,编译器无法确定递归要执行多少次。大量出现的简单代码时候作为内联。
  • debug版本下,inline不起作用,因为会导致无法调试。inline只在release版本下起作用。

函数重载

什么是函数重载:一组函数,函数名相同,但参数类型或个数不同。

C++为什么支持函数重载,但C不支持

  • 本质是编译器产生符号的规则不一样:C++代码产生函数符号,由函数名和参数列表类型组成;而C代码产生函数符号,仅由函数名组成。当链接时,会发生符号的重定义。

重载的注意点

  • 重载的前提是几个函数在同一作用域下。

    //下述情况下可以正确调用对应的函数
    #include<cstring>
    #include<iostream>
    using namespace std;
    
    bool compare(int a, int b){
        cout << "int" << endl;
        return (a>b);
    }
    bool compare(double a, double b){
        cout << "double" << endl;
        return(a>b);
    }
    bool compare(const char * a, const char * b){
        cout << "double" << endl;
        return strcmp(a, b);
    }
    
    int main(){
        compare(1, 2);
        compare(1.0, 2.0);
        compare("aa", "vv");
        return 0;
    }
    
    //下述情况下无法正确调用对应的函数
    #include<cstring>
    #include<iostream>
    using namespace std;
    
    bool compare(int a, int b){
        cout << "int" << endl;
        return (a>b);
    }
    bool compare(double a, double b){
        cout << "double" << endl;
        return(a>b);
    }
    bool compare(const char * a, const char * b){
        cout << "double" << endl;
        return strcmp(a, b);
    }
    
    int main(){
        bool compare(int, int);//当在局部声明了一个compare函数
        compare(1, 2); //后续的所有compare函数在局部作用域即找到了可调用的函数,则不会继续向全局作用域寻找其他conpare函数
        compare(1.0, 2.0); //会double强制类型转化int
        compare("aa", "vv"); //报错,无法强制类型转换
        return 0;
    }
    
  • const和volitale修饰形参,怎么影响形参类型的? 待补坑

  • 一组函数的函数名和参数相同,返回类型不同,则不算重载。

  • 什么是多态:编译期的静态多态:包含重载;运行期的动态多态。编译时期,指令就确定要重载哪个函数。

C++和C之间如何互相调用

  • 下述为C++主程序调用C函数

    //C规则下,只依据函数名生成符号表
    int sum(int a, int b){ //sum .text
        return a + b;
    }
    
    //C++规则下,依据函数名与形参生成符号表
    int sum(int a, int b); //sum_int_int "UND", 在链接时,无法匹配C中的符号sum
    int main(){
        int ret = sum(10, 20);
        cout << "ret" << ret << endl;
        return 0;
    }
    
    extern "C" //这里把C函数的声明加入extern C中
    {
        int sum(int a, int b); //按照C编译生成符号表 sum “UND”
    }
    int main(){
        int ret = sum(10, 20);
        cout << "ret" << ret << endl;
        return 0;
    }
    
  • 同样,如果C主程序调用C++

    extern "C" //cpp文件按照C编译
    {
        int sum(int a, int b){ //sum .text
            return a + b;
        }
    }
    
    int sum(int a, int b);  //sum “UND”
    
    int main(){
        int ret = sum(10, 20);
        cout << "ret" << ret << endl;
        return 0;
    }
    
  • 经常写作

    #ifdef __cplusplus //C++编译器内置的宏
    extern "C"{
    #endif
    //C++代码
    #ifdef __cplusplus
    }
    #endif
    

const

怎么理解const?

  • const修饰的变量不能再作为左值。即初始化后不能被修改。

C和C++中的const有什么区别?

  • C中const量可以只定义,但不被初始化(之后无法再赋值),称作常变量。事实上,通过指针仍可修改变量的值。

  • C++中的const必须初始化,称作常量。通过指针不可修改变量的值。

  • C和C++中对const变量的编译方式不同,C中const当作一个变量编译生成;C++中出现const常量名字的地方,都被常量的初始值替换,包括*(&a)这样的形式,也是直接当作a,替换。事实上,a所处的内存上的值已经被替换。

  • 如果C++中const初始化为另一个变量,也是常变量。

    int main()
    {
        const int a = 2;
        int arr[a];  //a是常变量,报错
        int* p = (int *) & a;
        *p = 20;
        cout << a << endl << *p << endl << *(&a); //三者都是20。
    }
    
    int main()
    {
        const int a = 2;
        int arr[a];  //a是常量,没问题
        int* p = (int *) & a;
        *p = 20;
        cout << a << endl << *p << endl << *(&a); //a依然是2,*p是20,*(&a)依然是2。
    }
    
    int main()
    {
        int c = 5;
        const int d = c; //即使是在C++中,这种初始化const变量的方式也只生成常变量。
        cout << d << endl;
        int* p = (int*)&d;
        *p = 4;
        cout << d << endl; //可以被修改。
    }
    

const修饰的量常出现错误:

  • 常量不能直接作为左值(不可直接修改)
  • 不能把常量的地址泄露给一个普通指针或普通引用(不可间接修改)
    int main()
    {
        const int a = 5;
        int* p = &a;  //不能把const int*转换为int*,但可以int*转换为const int*
    }
    

const结合一级指针

C++语言规范:const修饰的是离它最近的类型(*不能单独作为类型)

//一般使用前两种保护常量。
const int *p; //const修饰int,即指针所指向的值,p指向内容不变,但p的指向可变。
int const *p; //const修饰int,同上。
int *const p; //const修饰int *,指针p为常量,指向不可变,但可通过指针解引用修改指向内容。
const int *const p; //p指向const int且p的指向不变。

总结:

int * <- const int *  //错误
const int * <- int *  //正确
int main()
{
    //const右面没有指针*的话,const不参与类型。二者类型皆为int *,二者也可相互赋值
    int* p1 = nullptr;
    int* const p2 = nullptr;
    p1 = p2;
    cout << typeid(p1).name() << endl;
    cout << typeid(p2).name() << endl;
}

const结合二级(多级)指针

int main()
{
    int a = 0;
    int* p = &a;
    //以下情况均错误
    //const int** q = &p; //const修饰int,二次指向的值不能变,但q,*q可被赋值。同时也把const int暴露给了*p。
    //int* const* q = &p; //const修饰前方int *,*q不能改变,但q,**q可被赋值。
    //int** const q = &p; //const修饰前方int **,q不能改变,但*q,**q可被赋值。
    const int *const* q = &p; //const修饰int和*q,即**q和*q都不能改变,q指向可改变。
    
}

总结:

int ** <- const int **  //错误
const int ** <- int **  //错误
int ** <- int* const*  //错误
int* const* <- int **  //正确

题目

引用

引用和指针的区别

  • 引用是一种更安全的指针。

  • 引用必须初始化,指针可以不初始化。引用初始化为另一个同类变量,特殊情况下可初始化为常量。

  • 引用只有一级,没有多级引用。

  • 从汇编指令层面看。对引用的处理,是采用指针的方法。因为引用是一种const指针。通过引用变量修改所引用内存的值,与通过指针解引用修改指针指向内存的值,二者的底层指令也是一样的。

  • 数组如何被引用?

    int a;
    int *b = &a;
    int &b = a;
    
    int array[5];
    int (*p)[5] = &array
    int (&p)[5] = array
    
  • 引用虽然本质是指针,但使用时,总是要先进行解引用,因此进行sizeof运算,依然为所引用变量的字节数。

左值引用和右值引用

  • 左值:有内存,有名字,值是可以修改的。
  • 右值:反之。
  • C++11提供了右值引用:右值引用也必须立即进行初始化操作,且只能使用右值进行初始化。右值引用可以修改右值。
    • 右值引用专门引用右值,不能引用左值。
    • 右值引用变量本身是一个左值,只能用左值引用引用它。
    • 指令上,自动产生临时量,然后直接引用临时量。
      //一般情况下,C++只允许左值引用,而不允许右值引用
      int num = 10;
      const int &b = num; //正确
      const int &c = 10;  //错误
      
      //右值引用
      int num = 10;
      //int && a = num;  //右值引用不能初始化为左值
      int && a = 10;  //指令层面,临时量xx=20,然后a引用xx
      a = 11; //右值引用可以修改右值
      

左值引用和右值引用总结

const,一级指针,引用的结合使用

为了观察赋值是否可行,可以先将引用换成指针的写法:引用号&修改为*,等式右侧加上取地址符&,然后比较等式两边的类型是否符合赋值规范。video

int main()
{
    //int& p = 10 //错误
    const int& p = 10; //正确,产生临时量,p是临时量的引用
    int* const& q = (int*)0x0018ff44; //q是const修饰的int指针,不能写成const int* &q
}
int main()
{
    int a = 1;
    int* p = &a;
    const int *& q = p; 
    //相当于const int ** q = &p; const int **和int **不能相互赋值。
    return 0;
}

new和delete

new和malloc区别,delete和free区别

  • malloc和free,是C的库函数;new和delete,是运算符。
  • new不仅内存开辟,还能做内存的初始化,返回指针,如果开辟失败,抛出异常bad_alloc,而不是返回空指针;malloc只能进行内存开辟,返回指针,如果开辟失败,返回nullptr。malloc需要类型强制转换,new不需要。
  • new有多少种?
int main()
{
    int* p1 = new int(10);
    int* p2 = new(nothrow)int;
    const int* p3 = new const int(40);
    //定位new,固定位置new一个对象
    int data = 0;
    int* p4 = new(&data) int(20);
    cout << data;
}

类和对象,this