C++实战笔记(二)智能指针与内存管理

 
Category: C_C++

写在前面

总结一下罗剑锋老师的C++实战课程中C++智能指针与内存管理的一些内容. API及示例部分参考了1.

智能指针概述

智能指针实际上是一个像vector一样的类模板, 所以不能使用Delete方法释放所指对象的内存, 因为智能指针可以自主管理初始化时候的指针, 在所指对象离开作用域时自动析构释放内存.

常见的智能指针有

  • shared_ptr
  • unique_ptr
  • weak_ptr

都定义在memory头文件中.

其他需要注意的点:

  1. 智能指针对象没有定义加减操作;
  2. 智能指针对象没有定义移动地址操作;
  3. 智能指针对象不可以先声明后解引用, 否则相当于解引用空指针, 造成内存问题;

shared_ptr

指针可以共享, 即可以由多个指针指向同一对象. (区别于unique_ptr)

基本使用

主要API, 参考C++primer5ed.

代码 操作与解释
shared_ptr<T> sp 声明空智能指针,指向T类型的对象
p 用于条件判断, 若p指向一个对象, 则为true, 否则false
*p 解引用p, 得到其指向的对象
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针,
若智能指针自动释放了其指向的对象, 则返回的指针为空指针
swap(p, q)p.swap(q) 交换两智能指针变量
make_shared<T>(args) 返回一个shared_ptr, 指向一个动态分配类型王巍T的对象,
args可以是T类型的构造函数参数,
shared_ptr<T>p(q) 拷贝构造, 此操作递增q指针的计数器, p引用计数不变
q中指针的类型必须能转为T*类型(q可以为智能指针或一般指针变量)
p=q p,q都为智能指针, 并且所保存的指针能够相互转换,
此操作递减p的引用计数, 递增q的引用计数,
若p的引用计数变为0,则自动释放p管理的内存
p.use_count() 返回与p共享对象的智能指针的数量(速度较慢), 并非引用计数
p.unique() 返回p.use_count()==1, bool值

基本使用实例:(已忽略头文件)

void t1() {
    shared_ptr<string> p1;
    shared_ptr<string> p11 = make_shared<string>("");
    shared_ptr<string> p12 = make_shared<string>("Hello!");
    shared_ptr<list<int>> p2;
    if (!p1) cout << "p1 is nullptr" << endl;

    if (p11 && p11->empty()) {
        cout << "p11 is not nullptr" << endl;
        *p11 = "hi";
    }
    cout << "p11=" << p11 << endl;
    cout << "*p11=" << *p11 << endl;
    /*
    p1 is nullptr
    p11 is not nullptr
    p11=0x600003b64010
    *p11=hi
    */
    cout << "p12=" << p12 << endl;
    cout << "*p12=" << *p12 << endl;
    /*p12=0x600003b68010
     *p12=Hello!*/
}
void t2() {
    auto p1 = make_shared<int>(100);
    // auto p2 = p1;
    auto p2(p1);
    cout << p1 << " " << *p1 << " " << p1.use_count() << endl;
    cout << p2 << " " << *p2 << " " << p2.use_count() << endl;
    /*0x600003ce5138 200 2
    0x600003ce5138 200 2*/
}

与new/delete联合使用

代码 操作与解释
shared_ptr<T>p(q) p管理普通指针q(内置指针)指向的对象,
q指向的必须是通过new动态分配的内存,且能转为T*类型
shared_ptr<T>p(u) p从unique_ptr对象u那里接管对象所有权, 并将u置为空
shared_ptr<T>p(q, d) p接管内置指针q所指对象的所有权,q要能转为T*类型,
可使用可调用对象(仿函数)代替Delete操作
p.reset() 若p是唯一指向其所指对象的shared_ptr, reset会释放此对象(此操作更新引用计数)
p.reset(q) 若参数为内置指针q, 则reset会令p指向q, 否则p置空
p.reset(q,d) 调用仿函数d而非Delete释放q

void process(shared_ptr<int> ptr) { // use ptr
} // ptr离开作用域, 被销毁

void t1() {
    int *x(new int(1024));
    // process(x);
    /* error: could not convert 'x' from 'int*' to 'std::shared_ptr<int>'*/
    process(shared_ptr<int>(
        x)); // ok, but memery be released,临时变量被销毁,引用计数已经为0
    int j = *x;
    cout << j << endl; // random value.
}
/*
当将一个智能指针类型绑定到一个普通指针上时候,
我们就将内存的管理责任交给了智能指针.
此时不应该再用内置指针访问智能指针所指向的内存了*/

void t2() {
    // do not use `.get()` init another shared_ptr
    shared_ptr<int> p(new int(42)); // refcnt=1
    // p.get()用于返回p中保存的指针, 小心使用, 若智能指针释放了其对象,
    // 返回的指针所指向的对象也消失了
    int *q = p.get();
    { // 两个独立的shared_ptr指向相同的内存
        shared_ptr<int> r(q); // 这块书上错了, 少了r
    } // 作用域结束,q和q指向的内存都被销毁,导致p指向的内存已经被释放了,p成为悬空指针
    // 并且p被销毁时, 同一块内存会被二次delete
    int foo = *p;
    cout << foo << " " << p << endl; //error, 二次delete
}

void t3() {
    // use reset()
    shared_ptr<int> p;
    cout << typeid(p).name() << endl; // St10shared_ptrIiE
    // p=new int(102); // 不能将指针赋予shared_ptr
    p.reset(new int(102));            //智能指针p指向新的对象
    cout << typeid(p).name() << endl; // St10shared_ptrIiE

    // reset还会更新引用计数.
    if (!p.unique()) { //不是唯一的用户, 就分配一份新的拷贝
        p.reset(new int(*p));
    }
    *p += 10;
    cout << p << " " << *p << endl; // 112
}

这里关于代码示例部分没什么要说的, 书上写的很详细, 不过要注意的是t2()函数中关于内部作用域的代码, 英文版和中文版都有一个小问题, 如果只是shared_ptr<int> (q); 并不会有报错, 即指针不会销毁, 除非使用shared_ptr的拷贝构造写法.

非常建议大家跟着书敲一遍代码, 有很多收获. API只有先会用了才能逐渐去了解其背后的运行原理.

总结

shared_ptr使用引用计数技术来支持多指针指向(管理)同一个对象, 当引用计数为0自动销毁对象, 实际开发中若大量使用shared_ptr会导致性能下降(引用计数占用资源).

除此之外, 由于shared_ptr的共享性, 就可能出现循环引用的问题, 导致引用计数始终不会清零, 从而内存泄漏.

unique_ptr

每次只能指向一个对象, 且只能被一个对象拥有.

代码 操作与解释
unique_ptr<T> u1 声明空智能指针,指向T类型的对象
unique_ptr<T, D> u2 声明的同时指定可调用对象来释放对象内存
unique_ptr<T, D> u2(d) 声明的同时指定类型为D的可调用对象d来释放对象内存
p 用于条件判断, 若p指向一个对象, 则为true, 否则false
*p 解引用p, 得到其指向的对象
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针,
若智能指针自动释放了其指向的对象, 则返回的指针为空指针
swap(p, q)p.swap(q) 交换两智能指针变量
u=nullptr 释放u所指对象的内存, u置空
u.release() u放弃对指针的控制权, 返回指针并将u置空
u.reset()u.reset(nullptr) 释放u指对象, u置空
u.reset(q) 令u指向内置指针q的同时释放u当前所指对象

基本操作

void t1() {
    // init unique_ptr
    unique_ptr<int> p1;
    unique_ptr<double> p2(new double(1.2));
    // unique_ptr拥有其所指向的对象, 所以不支持拷贝操作
    unique_ptr<string> p3(new string("12"));
    // unique_ptr<string> p4(p3);//error
    unique_ptr<string> p5;
    // p5=p3;//error
}
void t11() {
    unique_ptr<string> p1(new string("1"));
    p1.reset();
    cout << p1 << endl;
    unique_ptr<string> p2(new string("2"));
    p2.reset(nullptr);
    cout << p2 << endl;
    /*0x0
    0x0*/
}

void t12() {
    auto u1 = make_unique<int>(1);
    cout << typeid(u1).name() << endl;
    // NSt3__110unique_ptrIiNS_14default_deleteIiEEEE
}

void t2() { // use release() and reset()
    // u.release() 放弃对指针的控制权, 返回指针u, 并将指针u置为空
    // release()返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值
    // =============================================================
    // u.reset() 释放指针u指向的对象, 若函数带有参数(内置指针,记为q),
    // 则令u释放之后指向指针q指向的对象, 将所有权从p1转移给p2
    unique_ptr<string> p1(new string("hello"));
    unique_ptr<string> p2(p1.release()); // 同时, release将p1置空
    cout << "p1=" << p1 << endl;         // p1=0x0
    cout << "nullptr==p1: " << (nullptr == p1) << endl;
    // cout << "*p1=" << *p1 << endl;//segfault
    cout << "*p2=" << *p2 << endl;
    /*
    nullptr==p1: 1
    *p2=hello
    */
    unique_ptr<string> p3(new string("world"));
    p2.reset(p3.release()); // 将所有权从p3转移给p2
    cout << "*p2=" << *p2 << endl;
    cout << "p3==nullptr: " << (p3 == nullptr) << endl;
    /*
    *p2=world
    p3==nullptr: 1
    */
    // p2.release();//内存泄漏, p2不会释放内存(但是系统会释放),
    // 同时指针指向的内存丢失 最好写成下面这样:
    auto p = p2.release(); // 此时指针不是智能指针,所以需要在程序最后`delete p;`
    cout << p << endl; // 0x6000023b5120
    delete p;
}

unique_ptr<int> clone(int p) { return unique_ptr<int>(new int(p)); }

unique_ptr<int> clone1(int p) {
    unique_ptr<int> ret(new int(p));
    return ret;
}

void t3() { /*pass value and return unique_ptr*/
    // cout << clone(12) << endl;
    cout << *clone(12) << endl;
    // cout << clone1(121) << endl;
    cout << *clone1(121) << endl;
    /*//为什么地址一样呢?,clang++ OK, g++ error
    0x60000352c040
    12
    0x60000352c040
    121
    */
}

总结

unique_ptr所指向的对象的内存必须是new动态分配出来的, 可以通过内置指针向其赋值, 不能调用其拷贝构造函数, 但是可以拷贝一个即将被销毁的unique_ptr(如t3).

weak_ptr

weak_ptr: is a weak reference to an object managed by a shared_ptr

weak_ptr为shared_ptr提供了一种解决循环引用问题的解决方案, 通过弱引用实现.

有点像const成员函数中引入mutable来使内部变量可变(一种修补措施)

基本用法

代码 操作与解释
weak_ptr<T> w 声明空智能指针,指向T类型的对象
weak_ptr<T> w(sp) 与shared_ptr sp指向相同对象的weak_ptr, T要能转换为sp所指对象
w=p p可以是shared_ptr或weak_ptr,赋值后w,p共享对象
w.reset() w置空
w.use_count() 与w共享对象的shared_ptr数量
w.expired() 返回w.use_count()==0
w.lock() w.expired()为真, 返回空shared_ptr,
否则返回指向w对象的shared_ptr(提升为shared_ptr)
/*weak_ptr
是一种不控制所指向对象生存期的智能指针, 其指向一个由shared_ptr管理的对象,
将一个weak_ptr绑定到一个shared_ptr上不会改变shared_ptr的引用计数.
主要还是看shared_ptr的引用计数,为0则释放, 
而不会因为weak_ptr(指向该对象后)影响引用计数从而影响释放,
如其名'weak_ptr'
*/
void t1() {
    auto p = make_shared<int>(42);
    weak_ptr<int> wp(p); // wp弱共享p,p的引用计数不变
    // 或者通过赋值:
    weak_ptr<int> wp1;
    wp1 = p;
    wp1.reset();                                           // 置为nullptr
    cout << "wp1.use_count()=" << wp1.use_count() << endl; // 0
    cout << "wp1.expired()=" << wp1.expired() << endl;     // 1
    cout << "wp1.lock()==nullptr: " << (wp1.lock() == nullptr) << endl; // 1
    cout << p << endl;
    cout << *p << endl;
    // wp指向的对象可能不存在, 所以不能直接取值,
    // 需要采用lock()[lock()返回弱指针指向对象的shared_ptr]
    // cout<<*wp<<endl;//弱指针不能直接解引用
    // cout<<wp<<endl;//弱指针不能直接输出值(地址)
    cout<<wp.lock()<<endl;
    cout<<*(wp.lock())<<endl;
    if (shared_ptr<int> np = wp.lock())
    {
        cout<<np<<" "<<*np<<endl;
    }//0x600000ff9110 42
}

小结

为了解决强引用(shared_ptr)的循环引用问题, 观察其值不会导致其对应的shared_ptr引用计数增加.

ref

  1. C++ primer 5ed;