C++

重学C++系列(五)内存

Posted by YaPi on January 19, 2022

内存分配

栈内存分配,地址由高到低;由编译器去完成分配回收等操作 堆内存分配,地址由低到高;在指定时机可由程序指定

#include <iostream>
using namespace std;
int a =0;               // GVAR 全局初始化区
int *p1;                // bss 全局未初始化区
int main() { // text 代码区
    int b =1;           // stack 栈区变量
    char s[] = "abc";   // stack 栈区变量
    int *p2 = NULL;     // stack 栈区变量
    char *p3 = "123456";// 123456\0在常量区,p3在stack栈区
    static int c = 0;   // GVAR 全局静态初始化区
    p1 = new int(10);   // heap 堆区变量
    p2 = new int(20);   // heap 堆区变量
    char *p4 = new char[7]; // heap 堆区变量
    strcpy(p4,"123456"); // text 代码区
    
    if (p1 != NULL){
        delete p1;
        p1 = NULL;
    }

    if (p2 != NULL){
        delete p2;
        p2 = NULL;
    }
    // 对数组的delete需要delete数组
    if (p4 != NULL){
        delete []p4;
        p4 = NULL;
    }
    
    return 0; // text 代码区
}

avatar

作用域 函数体内,语句快{}作用域 整个程序范围内,由new、malloc开始,delete,free结束
编译间大小确定 变量大小范围确定 不确定,运行时确定
大小范围 windows默认1M,linux默认8M或10M(使用ulimit -s)查看 所有系统堆空间接近虚拟内存上限,一部分被os占用
内存分配 地址由高到低 地址由低到高
内容是否可变 可变 可变
全局静态存储区 常量存储区
存储内容 全局变量 常量
编译器见大小是否确定 确定 确定
内容是否可变 可变 | 不可变  

堆 heap

当在堆上分配内存的时候,很多语言会使用new这样的关键字,有些语言则是 隐式分配。在c++中new对应的是delete。以便完全接管内存的分配和释放

资源管理方案 RALL

RAII (Resource Acquisition is Initialization)

  • RAII是C++所持有的资源管理方式,有少量其他语言,如:D,Ada,Rust也采用了此方式,但 主流的编程语言中,c++是唯一一个依赖RALL做资源管理的
  • RaII 依托栈和析构函数来对所有的资源包括堆内存在内进行管理。
  • RAII有些比较成熟的只能指针代表:如std:auto_ptr和boost:shared_ptr

内存泄露(Memory Leak) 指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放。造成系统内存的浪费, 导致程序运行速度减慢甚至系统崩溃等后果

比指针安全的操作

两种方案

  • 使用智能指针
  • 使用引用
智能指针
  • unique_ptr avatar
#include <iostream>
using namespace std;
int a =0;               // GVAR 全局初始化区
int *p1;                // bss 全局未初始化区
int main() {
    // unique_ptr
    auto w = std::make_unique<int>(10);
    cout << *(w.get()) << endl;

    // auto w2 = w;// 编译错误,如果想要把w复制给w2,是不可以的。
    // 因为复制从语义上来说,两个对象将共享同一内存

    // unique_ptr 只支持移动语义

    // w2获得内存所有权,w此时等于nullptr
    auto w2 = std::move(w);

    cout << ((w.get() != nullptr) ? *w.get():-1) << endl;

    cout << ((w2.get() != nullptr) ? *w2.get():-1) << endl;

    return 0; // text 代码区
}
  • shared_ptr

shared_ptr 能够解决所有权的问题,但是引用计数会带来循环引用的问题,同样会造成内存泄露 avatar

#include <iostream>
using namespace std;

int main() {
    // shared_ptr
    {
        // shared_ptr代表的是共享所有权,即多个shared_ptr可以共享
        // 同一块内存
        auto wA = shared_ptr<int>(new int(20));
        {
            auto wA2 = wA;
            cout << ((wA2.get() != nullptr) ? (*wA2.get()) : -1) << endl;
            cout << ((wA.get() != nullptr) ? (*wA2.get()) : -1) << endl;
            cout << wA2.use_count() << endl;
            cout << wA.use_count() <<endl;
        }
        // cout << wA2.use_cout() << endl;
        cout << wA.use_count() << endl;
        // shared_ptr 在复制时,引用计数会加1,当一个shared_ptr离开作用域时
        // 会减1,当引用计数为0时,delete内存
    }
    return 0; // text 代码区
}
  • weak_ptr

avatar

引用

引用是一种特殊的指针,不允许修改的指针。

使用引用

  1. 不存在空引用
  2. 必须初始化
  3. 一个引用永远指向它初始化的那个对象
#include <iostream>
#include "assert.h"
using namespace std;

void swap (int &a, int &b){
    int t = a;
    a = b;
    b = t;
}

void swap2 (int *a, int *b){
    int t = *a;
    *a = *b;
    *b = t;
}

int main() {
    int a = 3;
    int b = 4;
    swap(a,b);
    cout << a << endl;
    cout << b << endl;
    assert(a == 4 && b == 3);

    swap2(&a,&b);
    cout << a << endl;
    cout << b << endl;
    assert(a == 3 && b == 4);
    return 0; // text 代码区
}

有了指针为什么还需要引用? Bjarne Stroustrup的解释: 为了支持函数运算符重载;

有了引用为什么还需要指针? 为了兼容c语言

对于内置基础类型(如:int,double)而言,在函数中传递 value更高效 对OO面向对象中自定义类型而言,在函数传递reference 更高效