• C/C++-内存


    工具

    valgrind

    CPU与内存

    程序加载到内存后,操作系统会给不同的内存指定不同的权限,拥有读取和执行权限的内存块是代码,拥有读取和写入权限的内存块是数据
    CPU一般通过地址来取得内存中的代码和数据
    CPU访问内存时需要的是地址,而不是变量名和函数名,当源文件被编译和链接成可执行程序后,都会被替换成地址
    (变量名是数据,函数、字符串名、数组名是代码块或数据的首地址)

    程序在硬盘中,要载入内存才能运行
    CPU只能从内存中读取数据和指令
    对于CPU,内存时存放指令和数据,并不能完成计算功能
    计算机:硬盘->内存->CPU:缓存->>
    CPU=运算器+寄存器+缓存

    寄存器:一个寄存器能够存储32/64位的数据,CPU一般由上百个寄存器(多少位的计算机,表示的是寄存器是多少位的)。寄存器用来进行数学运算或流程控制
    缓存:内存读取速度小于CPU,因此将频繁使用的数据暂时读取到缓存区。CPU只能从缓存中读取到部分数据,对于使用不是很频繁的数据会直接从内存中读取

    指令集

    编译、汇编之后会变成一条条的CPU指令,

    虚拟内存/地址

    虚拟地址通过CPU的转换才能对应到物理地址:虚拟地址–> 内存映射机制 --> 物理地址
    每次程序运行的时候,操作系统都会重新安排虚拟地址和物理地址的对应关系,哪一段物理内存空闲就使用哪一段
    虚拟地址和物理地址的映射关系由操作系统决定,虚拟地址空间的大小由操作系统决定

    虚拟地址可以使不同程序的地址空间相互隔离,防止被互相篡改
    程序A 和程序B虽然都可以访问同一个地址,但是他们对应的物理地址是不同的,因此不会互相修改对方的内存,因此程序不应该直接使用物理内存地址

    虚拟地址 作为一个中间层,用来屏蔽复杂的底层细节,只给用户提供简单的接口

    内存映射 ???

    只要能够控制这个虚拟地址到物理地址的映射过程,就可以保证程序每次运行时都可以使用相同的地址???

    内存分页 ???

    内存空间/进程

    一个进程用于一个独立的地址空间
    一个程序可能会有多个进程,而一个进程对应一个独立的地址空间,所以一个程序可能有多个地址空间

    • 内核空间

    存放操作系统内核代码和数据,被所有程序共享
    在程序中修改内核空间的数据不仅会影响操作系统本身,还会影响其他程序(一般操作系统进制用户访问内核空间)
    内核空间的访问需要操作系统的API函数,执行内核提供的代码
    系统调用 访问内核空间,执行内核代码(内核也是程序)叫做 内核模式 (发生系统调用时会暂停用户程序,转而执行内核代码)

    内核用于管理硬件,提供接口供上层使用

    • 用户空间

    用户空间保存的是引用程序的代码和数据,程序私有
    执行程序叫做用户模式
    下面的内存分区实际上说的是用户空间中的内存分区

    • 内核空间与用户空间的切换

    当运行在用户模式的应用程序需要输入输出、申请内存等底层操作时,就需要调用系统API函数进入内核模式,执行结束后,又回到用户模式

    内存分区 – 用户地址空间

    数据以二进制的形式保存在内存中,字节是最小的可操作单位
    在内存管理中,为每个字节分配了一个编号,使用该字节时,只要知道编号就可以,这个编号,就是地址

    注意:内存分区和变量的作用域不是完全等价的,甚至是没有关系的

    获取地址

    &data,(data可以是数值、字符)

    • 栈区

    编译器自动分配
    局部变量、形参、返回值,const 定义的局部变量也存放在栈区

    操作系统自动管理
    栈区上的内容只在函数范围内存在,函数结束自动销毁
    栈区内存地址:由高到低,即后定义的变量地址低于先定义的变量地址
    先进后出
    属于动态内存分配

    函数中定义的局部变量按照先后定义的顺序一次压入栈中,即,相邻变量的地址之间不会存在其他变量(debug/release不同)
    函数调用栈的例子

    实际上,程序启动时会为栈区分配一块大小适当的内存(包括局部函数调用栈的时候),这对于一般的函数调用就已经足够了,函数进出栈只是ebp、esp等寄存器指向的变换,或者是向已有的内存中写入数据,其实不涉及内存的分配和释放
    只要当函数中有较大的局部数组时,编译器才会在函数代码中插入针对栈的动态内存分配函数,这样函数被调用时才分配内存,不调用就不分配
    因此栈内存的分配效率要高于堆,也就是大部分情况下并没有真的分配栈内存,而是对已有内存的操作

    • 堆区

    程序员分配内存和释放,若开发人员不释放,程序结束时有系统回收
    malloc() free()
    new delete

    堆区内存地址:由低到高
    大小由系统内存/虚拟内存上限决定
    属于动态内存分配

    注意:后申请的内存空间并不一定在先申请的内存空间的后面,因为先申请的内存空间一旦释放,后申请的内存空间则会利用先前被释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系
    堆中存储的数据若未释放,则生命周期等同于程序的生命周期
    由于系统是通过链表来存储空闲的内存地址,因此堆数据时不连续的内存空间(这个堆区域数据结构的堆不一样,是通过链表实现的)

    对于堆分配,操作系统有一个记录空闲内存地址的链表,当申请内存时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的内存空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址出记录本次分配的大小,这样使用delete才能正确释放内存空间。由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空间链表

    free(p)并不能改变指针p的值,p依然指向以前的内存,为了防止再次使用该内存,建议将p的值手动置为NULL
    free(p) 只是释放掉动态分配的内存,p的值并不是NULL,仍然指向之前这个被释放掉的内存,所以if(p)仍然会执行,但是输出p指向的内存会报错

    // c 用malloc
    
    // 注意指针的类型转换
    char* p1=(char*)malloc(10);
    free(p1);
    //free(p)并不能改变指针p的值,p依然指向以前的内存,为了防止再次使用该内存,建议将p的值手动置为NULL
    
    // C++ 用new
    char* p2=new char[10];
    delete[] p2
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    calloc/realloc

    • calloc(size_t n,size_t size)

    void * calloc(size_t n,size_t size)
    在堆区分配n*size 字节的连续空间
    成功分配返回内存地址,失败则返回NULL

    n: 单元个数(相当于有多少个单位),size: 单位字节数(每个单元有多少个字节)

    • realloc

    void * realloc(void *ptr,size_t size)
    对ptr指向的内存重新分配size大小的空间,size可大可小
    成功分配返回内存地址,失败则返回NULL
    realloc 之后ip不变

    free(p ) 并不改变指针p的值,p依然指向以前的内存,为了防止再次使用该内存,将p=NULL

    #include 
    using namespace std;
    
    #define N 5
    #define N1 7
    #define N2 3
    
    int main()
    {
        int *ip;
        int *large_ip;
        int *small_ip;
    
        if ((ip=(int *)malloc(N*sizeof(int)))==NULL)
        {
            cout<<"malloc ERROR";
            exit(1);
        }
    
        for (int i=0;i<N;i++)
        {
            ip[i]=i;
            cout<<ip[i]<<endl;
        }
    
        // cout<
        // (int *)realloc(ip,N1*sizeof(int));
        // cout<
        
        if ((large_ip=(int *)realloc(ip,N1*sizeof(int)))==NULL)
        {
            cout<<"realloc large ERROR";
            exit(1);
        }
    
        // 两个指向同一块内存
        cout<<ip<<endl; // 0x661f50
        cout<<large_ip<<endl; // 0x661f50
    
        for (int i=N;i<N1;i++)
        {
            large_ip[i]=i;
        }
        for (int i=0;i<N1;i++)
        {
            cout<<large_ip[i]<<" "<<ip[i]<<endl;
        }
    
        if ((small_ip=(int *)realloc(large_ip,N2*sizeof(int)))==NULL)
        {
            cout<<"realloc small ERROR";
            exit(1);
        }
    
        // 三个指向同一块内存
        cout<<ip<<endl; // 0x661f50
        cout<<large_ip<<endl; // 0x661f50
        cout<<small_ip<<endl; // 0x661f50
    
        // 缩小只会在有效范围内数值正常,越界会产生随机数
        for (int i=0;i<N2;i++) // N 或者N1都是错的
        {
            cout<<small_ip[i]<<" "<<ip[i]<<endl;
        }
    
        free(small_ip); // 只用释放small_ip 其他两个被系统回收
        small_ip=NULL;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    malloc 动态内存分配 内存池 – 还没看

    • 全局(静态)区

    未初始化全局(静态)区 .bass
    已初始化全局(静态)区 .data

    在编译期间就能确定 存储大小的变量 的存储区,且在运行期间可以进行修改
    包括全局变量,静态变量(包括静态全局变量、静态局部变量
    属于静态内存分配
    这块内存具有读写权限
    静态数据区的变量只能初始化一次,也就是可修改但是不能初始化(即使二次初始化了,也会被视为无效)

    .bass

    读写
    未初始化的全局变量或未初始化的静态变量
    初始化为0的全局变量或初始化为0的静态变量
    .bass段不占用可执行文件空间,由操作系统初始化

    .data

    读写
    已初始化的全局变量 (但初始化不为0的)
    已初始化的静态变量 (但初始化不为0的)
    .data段占用可执行文件空间,由程序初始化

    • 常量区

    .rodata

    字符、字面值、字符串等常量
    const 修饰的全局变量(const修饰的局部变量存放在栈区)
    这部分内存只有读取权限,没写入权限,即运行期间,常量区的内容不可被修改
    属于静态内存分配

    • 代码区

    存放程序的代码 .txt 二进制
    只读,不可修改
    属于静态内存分配

    • 动态链接库

    在程序运行期间加载和卸载动态连接库

    初始化注意

    https://www.cnblogs.com/iBoundary/p/15014843.html
    https://blog.csdn.net/weixin_43609874/article/details/123903650
    https://blog.csdn.net/u014552102/article/details/126494557

    c语言中,全局变量、静态变量(全局静态变量、局部静态变量)不能用变量赋值,只能用常量赋值
    c++可以

    例子1

    int a=10;
    int b=a; // initializer element is not constant 表达式必须含有常量值
    
    int main()
    {
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    例子2

    int a=10;
    static int b=a; // initializer element is not constant 表达式必须含有常量值
    
    int main()
    {
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    例子3

    #include 
    #include 
    
    int main()
    {
        int a=10;
        static int b=a; // ERROR
    
        printf("%d\n",b);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    例子4

    下面这个是对的,&a 是常量,a是变量

    #include 
    #include 
    
    int a=10;
    int *b=&a; // initializer element is not constant 表达式必须含有常量值
    
    int main()
    {
        printf("%d\n",*b);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    静态/动态内存

    • 静态内存分配

    代码区常量区全局数据区的内存在程序启动时就已经分配好了,大小固定、不能由程序分配或释放,只能等到程序运行结束由操作系统回收

    • 动态内存分配

    栈区堆区的内存在程序运行期间可以根据实际需求来分配和释放,不用在程序刚启动时就备足所有内存

    char *str1="hello"; // hello字符串在常量区,str1在全局数据区
    
    int n; // 全局数据区
    
    char *func()
    {
        char *str="world"; // world字符串在常量区,str在栈区
        return str;
    }
    
    int main()
    {
        int a; // 栈区
        char arr[10]; // 栈区
        char *pstr=func(); // 栈区
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    内存泄漏

    一块内存没有被指针指向它,(程序和内存失去了联系,再无法对他进行任何操作),这块内存直到程序结束被系统回收

    char *p=(char *)malloc(100*sizeof(char)); // 这段内存存在内存泄漏,无法被释放,只有等程序运行结束由操作系统回收
    p=(char *)malloc(50*sizeof(char));
    free(p);
    p=NULL;
    
    • 1
    • 2
    • 3
    • 4

    栈溢出

    第一种栈溢出

    (整个)用于栈的内存空间是有限的,超出最大值(跟编译器有关)就会栈溢出
    一个程序可以包含多个线程,每个线程都有自己的栈,栈的最大是是针对线程的

    第二种栈溢出

    栈中局部变量的内存空间有限,占用了栈中其他内存空间的地址,导致栈发生错误

    调用栈 -函数相关

    函数调用与栈有关

    栈帧

    又叫做活动记录
    函数调用过程中,存储全部的信息的栈叫做栈帧

    一个不典型的例子:由高到底地址分别为:实参、返回地址、xxx、一块内存(包括局部变量、返回值等)、xxx

    函数调用惯例

    调用方和被调用方之间遵守的约定

    • 函数参数的传递方式,是通过栈传递还是通过寄存器传递
    • 参数参数传递的方式,是从左到右入栈还是从右到左入栈
    • 参数弹出方式,函数调用结束后需要将压入栈中的参数全部弹出,使得栈在函数调用前后保持一致,(这个弹出的工作可以由调用方完成,也可以由被调用方完成)
      函数调用惯例可以进行修改 – 没看,见文档

    补充:函数调用是有时间和空间开销的,程序在执行一个函数之前需要做一些准备工作,要将实参、函数变量、返回值地址以及若干寄存器都压入栈中,然后再才能执行函数体中的代码,函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码
    如果函数体代码比较多,需要较长的执行事件,那么函数调用机制占用的时间可以忽略
    如果函数只有一两句语句,那么大部分的时间都会花费在函数调用机制上,这种事件开销就不容忽略

    函数调用栈实例 – 见文档 很重要!!!

    局部变量分配内存,是否初始化也与调用栈有关

    内存对齐

    https://blog.csdn.net/weixin_46251230/article/details/123755070

    CPU 通过地址总线访问内存,一次能处理几个字节的数据,就命令地址总线读取几个字节的数据(32位CPU一次可以处理4个字节数据,64位CPU一次可以处理8个字节)

    寻址步长4个字节或8个字节
    将一个数据尽量放在一个步长内,避免跨步长存储,这称为内存对齐
    32位默认4字节对齐,64位默认8字节对齐

    具体对齐方式,与编译器有关
    一般来讲全局变量会自动内存对齐,而局部变量不会进行内存对齐

    动态内存分配 – 内存池 --还没看

    野指针/空指针/void指针

    空指针

    对未初始化的指针赋值为NULL
    空指针是不指向任何数据的指针,是无效指针

    // NULL 其实是一个宏定义,指向了内存的0地址,
    #define NULL ((Void*)0)
    
    • 1
    • 2
    char* s=NULL;
    
    if (p==NULL){
    	// ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    void指针

    void* 表示一个有效指针,指向实实在在的数据,只是数据的类型尚未确定,在后序使用过程中需要进行强制类型转换

    char* s=(char*)malloc(sizeof(char)*30);
    
    • 1

    野指针

    如果一个指针指向的内存没有访问权限,或者指向一块已经释放掉的内存,那么就无法对该指针进行操作,这样的指针就是野指针
    内存泄漏,是因为存在野指针

    free

    free(p ) 并不能改变指针p的值,p依然指向以前的内存,为了防止再次使用该内存,建议将p的值手动置为NULL
    free(p ) 只是释放掉动态分配的内存,p的值并不是NULL,仍然指向之前这个被释放掉的内存,所以if(p )仍然会执行,但是输出p指向的内存会报错

    避免野指针

    初始化为NULL
    free之后,赋值为NULL

    缓存与缓冲区

    内存中用于临时保存输入输出数据
    缓冲区 :内存空间的一部分
    作用: 减少磁盘的读写次数 ;
    分类

    输入缓冲区/输出缓冲区
    根据数据刷新时机

    全缓冲:缓冲区被填满时(一般是对硬盘的读写)
    行缓冲:遇到换行符时 (标准I/O),prinf(“\n”),scanf回车
    无缓冲: getche(),getch() 这两个函数没有缓冲
    (程序结束的时候 也会刷新缓冲区)
    (输出之后有输入的操作,也会刷新缓冲区)

    注意:不同的操作系统对缓冲区的定义时不同的,printf在windows下无需缓冲,在linux下有缓冲

    不刷新缓冲区的例子

    // 这个例子,进入死循环
    // 缓冲区没有被填满、没有遇到换行符、程序没有结束,所以标准输出始终没有显示字符串
    int main()
    {
        printf("hello world"); // 只要加上printf("hello world\n") 立即刷新
        while (1);
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    刷新

    行缓冲,全缓冲:缓冲区满时自动刷新
    行缓冲:遇到’\n’时,自动刷新
    关闭文件时自动刷新
    程序关闭时自动刷新
    使用刷新函数

    清空输出缓冲区

    fflush(stdout) // 一般用于linux系统下,清空标准输出缓冲区,清空屏幕缓冲区
    
    • 1

    清空输入缓冲区

    int c
    while (c=getchar()!='\n' && c!=EOF)
    
    • 1
    • 2

    FILE 与缓冲区

    FILE *fp;
    
    FILE 结构体
    int cn // 剩余字符,缓冲区中还有多少个字符未被读取
    char *ptr //下一个要被读取的字符的地址
    char *base // 缓冲区的基地址
    int flag // 读写状态标志位
    int fd // 文件描述符
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    FILE是文件缓冲区的结构体,fp是指向文件缓冲区的指针
    缓冲区的刷新表示将 缓冲区的指针变为缓冲区的基地址

    c++输出缓冲区

    每个输出流管理一个缓冲区

    缓冲刷新

    • 程序结束
    • 缓冲区满
    • \n 或 endl 显示刷新
    • 在每个输出操作之后,用操作符unitbuf设置流的内部状态,清空缓冲区
      • 一个输出流被关联到另一个流???

    flush/ends/endl

    cout<<endl; // 输出一个换行符,然后刷新缓冲区
    cout<<flush; // 直接刷新缓冲区
    cout<<ends; // 输出一个空字符,然后刷新缓冲区 
    
    • 1
    • 2
    • 3

    unitbuf

    每次写操作之后都进行一次flush

    cout<<unitbuf; // 所有输出操作后都会理解刷新缓冲区
    /*coding*/
    cout<<nounitbuf; // 回到正常的缓冲方式
    
    • 1
    • 2
    • 3

    (警告:如果程序崩溃,输出缓冲区不会被刷新 如果程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。 当调试一个已经崩溃的程序时,需要确认那些你认为已经输出的数据确实已经刷新了。否则,可能将大量时间浪费在追踪代码为什么没有执行上,而实际上代码已经执行了,只是程序崩溃后缓冲区没有被刷新,输出数据被挂起没有打印而已。)

    关联输入和输出流 – ???

  • 相关阅读:
    知识图谱推理研究综述9.3
    DS200DCFBG1BLC IS220PAICH1A 构建人工智能能力背后的紧迫性
    Azure AD统一认证及用户数据同步开发指导
    【API封装接口的应用】大数据值得深思的十二个典型应用案例,和未来机遇畅想
    FPGA 20个例程篇:14.千兆网口实现ICMP、UDP通信协议(下)
    广和通正式发布工业级低功耗单频双模GNSS模组G030&G031
    Java基础之IO流操作
    Spring MVC 中的数据验证技术
    【C++初阶】模板
    详解 InnoDB Cluster 主机名问题
  • 原文地址:https://blog.csdn.net/L_fengzifei/article/details/128053101