一、内存划分

    在C++中内存可被划分为:栈区、堆区、数据段、代码段等。其中数据段可分为全局/静态区和常量区。

  1. 栈区:又叫堆栈,存储非静态局部变量、函数参数、返回值等。栈是向下增长的。
  2. 堆区:用于程序运行时动态内存分配。堆是向上增长的。
  3. 数据段:存储全局变量、静态变量、字符串常量和const修饰的全局变量。
  4. 代码段:存储可执行代码和只读常量。

注意:很多编译器会把常量区和代码段合并(因为两者都是只读的),像字符串常量和const修饰的全局变量就可能被放到代码段。

二、new和delete

2.1 动态内存申请释放

     在C语言中用于申请释放空间的是malloc和free,而在C++中依然可以使用这两个函数,不过C++引入了新的操作符new和delete。

int main()
{
    int* p = new int;
    *p = 1;
    //int* p = new int(1);
    std::cout << *p;
    delete p;

    return 0;
}

    如果想申请多个空间则可以这样

int main()
{
    int* p = new int[10];
    delete[] p;

    return 0;
}

2.2 底层原理

    new和delete用于动态内存申请释放。new通过operator new全局函数来申请空间,delete通过operator delete全局函数来释放空间。在operator new中是调用malloc来申请空间,operator delete是调用free来释放空间。

  1. new:先调用operator new来开辟空间,然后在申请的空间上执行构造函数。
  2. delete:先在将要释放的空间上执行析构函数,然后调用operator delete来释放空间。
  3. new[]:在operator new[]会先调用operator new函数来完成N个对象空间的申请,然后在申请的空间上执行N次构造函数。
  4. delete[]:会先在将要释放的空间上进行N次执行析构函数,然后在operator delete[]中实际调用operator delete来释放整块空间。

    注意:new[]会在数组头部多存一个size_t类型的数,记录数组元素个数,方便后续析构多少次。new和delete一定要匹配使用!详见关于new和delete的匹配问题-CSDN博客

三、智能指针

3.1 介绍

    RAII(Resource Acquisition Is Initialization)即资源获取即初始化。

  1. 资源的获取(比如申请内存、打开文件)和对象的初始化绑定。
  2. 资源的释放(比如释放内存、关闭文件)和对象的析构绑定。
  3. 当对象的生命周期结束,析构函数会自动调用,资源也就被自动释放,无需手动管理。

    智能指针就是按照RAII的思想设计的。

3.2 C++标准库中智能指针的使用

    下面的指针都在<memory>头文件里。

3.2.1 auto_ptr

    这是C++98设计出来的智能指针,功能是拷贝时把被拷贝的对象的资源管理权移交给拷贝对象,这时被拷贝的对象会悬空,这是一个很糟糕的问题。

     像上图并不会出现悬空,因为p是以值传递的方式传给ap的构造函数,所以外部p就不受影响。但是如果ap的生命周期提前结束的话,p就会变为悬空指针。如果是auto_ptr就会发生变为悬空。

3.2.2 unique_ptr

    这是C++11设计出来的智能指针,特点是不支持拷贝,支持移动,在不需要拷贝的场景就可以优先考虑使用它。如果拷贝就会报错:

    在移动时被移动的对象会悬空。

3.2.3 shared_ptr

    这是C++11设计出来的智能指针,特点是支持拷贝,支持移动,在需要拷贝的场景就可以优先考虑使用它。它的底层是靠引用计数的方式实现。

3.2.4 weak_ptr

    这是C++11设计出来的智能指针,它不支持RAII,也意味着不能管理资源,它只要是解决shared_ptr的循环引用的内存泄漏问题。

3.2.5 删除器

    智能指针析构默认是delete释放资源,也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。所以智能指针在构造时支持给一个删除器(本质上就是可调用对象),当你给定后,析构时就会调用删除器。像new[]经常使用所以unique_ptr和shared_ptr都特化了一个版本,使用时 unique_ptr<T[]>、shared_ptr<T[]> 在析构时就会delete[]。

unique_ptr

//仿函数
template <class T>
class Delete
{
public:
    void operator()(T* p)//这里的p是unique_ptr内部管理的A*类型指针
    {
        cout << "删除器" << endl;
        delete p;
    }
};

//函数指针
template <class T>
void delete_func(T* p)
{
    cout << "删除器" << endl;
    delete p;
}

int main()
{
    //仿函数
    unique_ptr<A, Delete<A>> ap1(new A(1));

    //函数指针
    unique_ptr<A, void (*)(A*)> ap2(new A(1), delete_func<A>);
    
    //lambda表达式
    auto del = [](A* p) 
        { 
            cout << "删除器" << endl;
            delete p; 
        };
    unique_ptr<A, decltype(del)> ap3(new A(1), del);

    return 0;
}

shared_ptr

int main()
{
    //仿函数
    shared_ptr<A> ap1(new A(1), Delete<A>());

    //函数指针
    shared_ptr<A> ap2(new A(1), delete_func<A>);
    
    //lambda表达式
    auto del = [](A* p) 
        { 
            cout << "删除器" << endl;
            delete p; 
        };
    shared_ptr<A> ap3(new A(1), del);

    return 0;
}

    shared_ptr和unique_ptr的构造函数都使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。

    关于shared_ptr对象本身来说它会在栈/堆/全局区,而它里面的控制块(引用计数分为强引用和弱引用)、实际资源则在堆上。

3.3 shared_ptr的循环引用问题

    假设现在有下面的代码:

struct Node {
    Node(int val)
        :_val(val)
        ,_next(nullptr)
    {}

    int _val;
    shared_ptr<Node> _next;
};

int main()
{
    shared_ptr<Node> n1 = make_shared<Node>(1);
    shared_ptr<Node> n2 = make_shared<Node>(2);

    n1->_next = n2;
    n2->_next = n1;

    return 0;
}

    这就是典型的循环引用。当创建完智能指针之后,n1和n2的强引用计数为1。然后执行下面的两行代码这时n1和n2的强引用计数为2,当程序结束后栈上的n1和n2的shared_ptr被销毁,这时n1和n2的强引用计数变为1。

    此时,n1的强引用计数为 1,这个计数由n2->_next持有。n2对象的强引用计数为 1,这个计数由n1->_next持有。由于两个对象的强引用计数都不为 0,它们的内存和控制块都不会被释放,造成了内存泄漏

    这时候就要使用weak_ptr,它只是观测资源,不拥有它,因此它的存在与否不影响资源的释放。使用它之后弱引用加一,只要强引用变为0就会立即销毁shared_ptr对象。

struct Node {
    Node(int val)
        :_val(val)
    {}

    int _val;
    weak_ptr<Node> _next;
};

int main()
{
    shared_ptr<Node> n1 = make_shared<Node>(1);
    shared_ptr<Node> n2 = make_shared<Node>(2);

    n1->_next = n2;
    n2->_next = n1;

    return 0;
}

    weak_ptr和shraed_ptr是可以相互转换的。

3.4 shared_ptr的线程安全问题

    如果shared_ptr的对象在多个线程中,对shared_ptr进行拷贝析构时会修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或原子操作保证线程安全。



3.5 补充

    当想定义智能指针时不要这样写

shared_ptr<Node> n();

    编译器会把上面的代码解析为函数声明。可以用下面的几种写法

shared_ptr<Node> n;
shared_ptr<Node> n{};
shared_ptr<Node> n = make_shared<Node>();


 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐