CPP 智能指针

发布时间 2023-11-13 17:33:48作者: RVIER

健壮编程面临的问题之一是知道何时删除对象。可能会发生几种故障。

第一个问题是根本不删除对象(无法释放存储空间)。这种情况被称为内存泄漏,即对象累积并占据空间,但却没有被使用。

另一个问题是,一段代码删除了存储空间,而另一段代码仍然在指向该存储空间的指针,导致指向该存储空间的指针不再使用或为其他目的重新分配。这些称为悬空指针

还有一个问题是,当一段代码释放存储空间时,另一段代码试图释放相同的存储空间。这就是所谓的双重删除

所有这些问题都倾向于导致某种形式的程序失败。有些故障很容易检测到,可能会使应用程序崩溃;另一些则导致程序产生错误的结果。这些错误大多难以发现和修复。c++使用智能指针解决了这些问题:unique_ptr、shared_ptr和weak_ptr,它们都定义在中。第7章会讨论这些智能指针

内存管理

C++11 引入智能指针,建议使用智能指针,而不是原始指针,因为智能指针可以自动释放不再需要的资源

对于每一个指针,在声明时,要么置为nullptr要么初始化

new and delete

new +变量类型,申请得到一块内存后,返回值时指向这片内存的指针,当指针丢失时,这一块内存没有被释放掉但是找不到方法访问到这块内存时,这一块内存就是孤儿内存。也是内存泄漏

使用原生指针时,每一个new都要对应一个delete和指针置空操作

不要使用malloc 和free

malloc和new会申请大小一致的内存空间。区别在于:

new会执行构造函数,而malloc时C中的用法,C没有类,对于类对象,new会执行类的构造函数,而malloc不会。同样的,delete释放类资源时,会执行类的析构函数。所以C++永远永远不要使用malloc和free

new失败了怎么办

当在C++中使用new运算符时,如果内存分配失败,它将抛出std::bad_alloc异常。这可能是由于许多原因引起的,例如内存不足、堆栈溢出、操作系统资源不足等等。

为了处理这种情况,您可以使用try-catch块来捕获异常并采取适当的措施。例如,您可以尝试释放其他不必要的内存或减少内存使用量。以下是一个示例代码块,演示如何使用try-catch块来处理new运算符失败的情况:

try {
  int* myArray = new int[1000000000]; 
   // Attempt to allocate a large amount of memory*
}
catch (std::bad_alloc& e) {
  std::cerr << "Memory allocation failed: " << e.what() << '\n';
// Take appropriate action, such as freeing other 
//memory or reducing memory usage*
}

请注意,如果您使用的是C++11或更高版本,则可以使用noexcept关键字来指示new运算符不会抛出异常。例如:

int* myArray = new (std::nothrow) int[1000000000]; // Attempt to allocate a large amount of memory*
if (myArray == nullptr) {
  std::cerr << "Memory allocation failed\n";
  // Take appropriate action, such as freeing other memory or reducing memory usage*
}

在这种情况下,如果内存分配失败,new运算符将返回nullptr,而不是抛出异常。

区别动态数组和动态分配数组

动态数组: 数组的size是可变的

动态分配数组,数组的size在申请时是参数,是可变的,但是一旦申请之后,对象的size是不可变的。

简而言之:使用STL

对象数组

new[] 会自动调用默认构造函数

基本变量,如int,是未初始化的值

class Simple
{
public:
Simple() { cout << "Simple constructor called!" << endl; }
~Simple() { cout << "Simple destructor called!" << endl; }
};
Simple* mySimpleArray { new Simple[4] };
// Use mySimpleArray...
delete [] mySimpleArray;
mySimpleArray = nullptr;
/*
Simple constructor called!
Simple constructor called!
Simple constructor called!
Simple constructor called!
Simple destructor called!
Simple destructor called!
Simple destructor called!
Simple destructor called!
*/

永远对应关系

new -- delete

new [] ---delete []

因为如果对于new[]申请的4个对象内存空间,使用delete mySimpleArray,编译器会出现很奇怪的现象,比如:只删除了指针指向的第一个对象,而剩下的三个对象就变成了孤儿内存,这会造成内存泄漏

再次强调:在现代CPP中避免使用C风格的指针,而应该使用STL和智能指针

多维数组

当程序使用多维数组的名字时,会自动将其转换为指向数组首元素的指针

int arr[3][4]={123,12};
for(auto row: arr)
   for(auto col:row)
       /*...*/
       //

注意上述代码是非法的。因为编译器在初始化时,会将row初始化为int*的指针,那么第二层的for循环就将不再成立。

对于多维数组,使用for循环时,除了最内层的循环外,都要使用引用

int arr[3][4]={123,12};
for(auto & row: arr)
   for(auto col:row)
       //这个才是正确的

new 申请多维数组

// 申请一个二维数组,行数为3,列数为4
int** arr = new int*[3];
for(int i = 0; i < 3; i++){
    arr[i] = new int[4];
}
//非法
//int** arr = new int[3][4];

image-20230430095306510

C-风格new 多维数组的方式如上图所示,最后释放内存的时候也需要层层delete,不然很容易造成内存泄漏(外层内存删除,子数组忘掉释放)

// 释放二维数组内存
for(int i = 0; i < 3; i++){
    delete[] arr[i];
}
delete[] arr;

数组即指针

编译器将数组名处理为指针。所以将数组名作为参数传递进去的时候,函数不是对数组进行拷贝,而是对指针进行操作,是会直接改变数组内部的元素的。

下面三个函数是一样的,因为编译器都会将theArray作为指针,而直接忽视了后面的数字

void doubleInts(int* theArray, size_t size);
void doubleInts(int theArray[], size_t size);
void doubleInts(int theArray[2], size_t size);

并不是所有的指针都是数组

int* ptr {new int};

指针操作

C++中对于指针进行操作时,比如int * 的指针ptr,因为指针的类型都是声明的,所以ptr+1是在内存里前面进了init类型的size

相同两个类型的指针相减的值是它们之间的元素个数,而不是绝对字节数。

资源回收

当资源的不再有引用使用时,会被自动回收。C++没有垃圾回收(java,c#有垃圾回收)

智能指针

当某个资源的最后一个shared_ptr实例被销毁时,该资源也会在那个时间点被销毁。

mark and sweep

使用这种方法,垃圾收集器定期检查程序中的每个指针,并注释标记被认为没有使用的指针并释放掉

使用垃圾回收会有很多问题:

  1. 垃圾回收运行时,程序可能会变成无响应
  2. 没有析构函数,那么一些资源(比如关掉文件流,释放锁)会在一个非确定的时间执行,这对程序也会造成影响。

对象池

对象池相当于回收。你买了合理数量的盘子,用完一个盘子后,你清洗它,以便以后可以重复使用。对象池非常适合需要在一段时间内使用许多相同类型的对象,并且创建每个对象都会产生开销的情况。

--## 数据缓冲区分配不足和内存越界访问

已经说过了使用原生指针使用动态分配的内存会造成一些潜在的问题:

重复删除

内存泄漏

浮空指针

现代C++引入智能指针来解决这个问题。

当智能指针超出作用域或被重置时,它可以自动释放它持有的资源。它们还可以用于通过函数参数传递动态分配资源的所有权

智能指针有几种类型。

  1. unique_ptr
    作为资源的唯一所有者,是一种独占所有权的智能指针,它不能被复制或共享。当unique_ptr超出范围时,它所拥有的对象将被自动销毁

  2. shared_ptr
    一种共享所有权的智能指针,它可以被复制和共享。当最后一个shared_ptr超出范围时,它所拥有的对象将被自动销毁

  3. weak_ptr

    是一种弱引用的智能指针,它可以指向一个由shared_ptr管理的对象,但不会增加该对象的引用计数。当最后一个shared_ptr超出范围时,weak_ptr将自动失效。以下是一个使用weak_ptr的示例:

#include <memory>

int main() {

    std::unique_ptr<int> ptr(new int(42));
    std::cout << *ptr << std::endl;
    /*-----------------------------------------------*/
    std::shared_ptr<int> ptr1(new int(42));
    std::shared_ptr<int> ptr2 = ptr1;
    std::cout << *ptr1 << " " << *ptr2 << std::endl;
    /*-----------------------------------------------*/
    std::shared_ptr<int> ptr1(new int(42));
    std::weak_ptr<int> ptr2 = ptr1;
    std::cout << *ptr1 << " " << *ptr2.lock() << std::endl;
    return 0;
}

使用智能指针的必要之处。

当你使用下面的代码,良好的时候,资源管理没有任何问题,但是一旦go函数里面跑出一个异常,那么delete函数就永远没有被执行,就出现了内存泄漏。

void couldBeLeaky()
{
    Simple* mySimplePtr { new Simple{} };
    mySimplePtr->go();
    delete mySimplePtr;
}

unique_ptr

void notLeaky()
{
    auto mySimpleSmartPtr { make_unique<Simple>() };
    mySimpleSmartPtr->go();
}

可以使用make_unique.()make_unique会自动执行类的构造函数

foo(unique_ptr<Simple> { new Simple{} };
unique_ptr<Bar> { new Bar { data() } });

同样的使用unique_ptr的用法,第一个make_unique更好,因为如果制在构造函数出错的话(抛出异常)。第二个会出现内存泄漏的问题。

智能指针的解引用*->都和原生指针一样

unique_ptr.get()可以得到指针访问资源的位置。

void processData(Simple* simple) { 
    /* Use the simple pointer... */ 
}
//Then you can call it as follows:
processData(mySimpleSmartPtr.get());

mySimpleSmartPtr.reset(); 
// Free resource and set to nullptr
mySimpleSmartPtr.reset(new Simple{}); 
// Free resource and set to a new
// Simple instance

接触unique_ptr的拥有权

使用realease

Simple* simple { mySimpleSmartPtr.release() }; 
// Release ownership

// Use the simple pointer...
delete simple;
simple = nullptr;

release 会接触智能指针的所有权,将其设为nullptr,但是资源还没有被删除,release会返回一个一般的指针,指向这个资源。

unique_ptr不能被复制,但是可以使用move将一个unique_ptr转移到另一个unique_ptr

class Foo
{
    public:
    	Foo(unique_ptr<int> data) : m_data { move(data) } { }
    private:
    	unique_ptr<int> m_data;
};
auto myIntSmartPtr { make_unique<int>(42) };
Foo f { move(myIntSmartPtr) };

shared_ptr

支持多个指针指向同一内存,可以被复制。

使用引用计数。维持一个内存的引用指针数目,当引用计数为0时,说明内存不再有指针指向,释放资源。

类似的,选哟使用make_shared<>声明资源

auto mySimpleSmartPtr { make_shared<Simple>() };

get reset()方法同样存在。区别在于当shared_ptr调用reset时,并不会删掉内存资源,除非他是最后一个指针。

shared_ptr并不支持release方法,不过可以使用use_count()方法得到共享指针的引用数目。

引用计数

当多一个对象或者指针,指向这一内存时,内存的引用计数+1,当一个脱离作用域或者被重置时,引用计数-1.当引用计数将为0时,释放资源。

当原生指针和智能指针混用,可能会出现问题。

强制转换

  1. const_pointer_cast()
  2. dynamic_pointer_cast()
  3. static_pointer_cast()
  4. reinterpret_pointer_cast()

功能类似于

  1. const_cast()
  2. dynamic_cast()
  3. static_cast()
  4. reinterpret_cast()

aliasing

aliasing将对象的所有权从一个指针(拥有指针)共享给另一个指针(存储指针),另一个指针指向的是不同类型的对象。

一个shared_ptr指向一个对象的一部分,同时还拥有对象的所有权。

下面给一个例子

class Foo
{
public:
    Foo(int value) : m_data { value } { }
    int m_data;
};
auto foo { make_shared<Foo>(42) };
auto aliasing { shared_ptr<int> { foo, &foo->m_data } };

拥有的指针用于引用计数,而存储的指针在对指针解引用或对其调用get()时返回。

weak_ptr

weak_ptr指向shared_ptr的资源,但是并不拥有资源。

具体地。weak_ptr是一种弱引用的智能指针,它可以指向一个由shared_ptr管理的对象,但不会增加该对象的引用计数。当最后一个shared_ptr超出范围时,weak_ptr将自动失效。

weak_ptr销毁时,并不会销毁资源。而且 weak_ptr不会阻止shared_ptr释放资源。

weak_ptr的构造函数需要shared_ptr或另一个weak_ptr作为实参。要访问存储在weak_ptr中的指针,需要将其转换为shared_ptr。有两种方法:

  1. 使用lock方法,会返回一个shared_ptr。如果资源已经被释放,会返回一个nullptr
  2. 再构造一个shared_ptr。如果资源被释放掉,会返回一个bad_weak_ptr异常
void useResource(weak_ptr<Simple>& weakSimple)
{
    auto resource { weakSimple.lock() };
    if (resource) {
    	cout << "Resource still alive." << endl;
    } else {
    	cout << "Resource has been freed!" << endl;
    }
}
int main()
{
    auto sharedSimple { make_shared<Simple>() };
    weak_ptr<Simple> weakSimple { sharedSimple };
    // Try to use the weak_ptr.
    useResource(weakSimple);
    // Reset the shared_ptr.
    // Since there is only 1 shared_ptr to the Simple resource, this will
    // free the resource, even though there is still a weak_ptr alive.
    sharedSimple.reset();
    // Try to use the weak_ptr a second time.
    useResource(weakSimple);
}
/* 输出结果
Simple constructor called!
Resource still alive.
Simple destructor called!
Resource has been freed!
*/

函数传参--智能指针

接受指针作为其参数之一的函数只有在涉及所有权转移或所有权共享的情况下才应该接受智能指针。要共享shared_ptr的所有权,只需按值接受shared_ptr作为参数。类似地,要转移unique_ptr的所有权,只需按值接受unique_ptr作为参数。后者需要使用move

如果既不涉及所有权转移也不涉及所有权共享,那么函数应该只有一个指向 非const或指向const的引用形参。

如果nullptr是形参的有效值,则应该有一个原始指针。使用const shared_ptr&或const unique_ptr&这样的形参类型没有多大意义。

函数返回值---智能指针

shared_ptr, unique_ptr, and weak_ptr, can easily and efficiently be returned from functions by value。直接返回就可

this

std::enable_shared_from_this派生类允许在对象上调用的方法安全地将shared_ptrweak_ptr返回给自身。

如果没有这个基类,返回有效的shared_ptr或weak_ptr的一种方法是将weak_ptr作为成员添加到类中,并返回它的副本或返回由它构造的shared_ptrsenable_shared_from_this类将以下两个方法添加到它的派生类中:

  1. shared_from_this() Returns a shared_ptr that shares ownership of the object
  2. weak_from_this() Returns a weak_ptr that tracks ownership of the object
class Foo : public enable_shared_from_this<Foo>//派生
{
public:
    shared_ptr<Foo> getPointer() {
    	return shared_from_this();	//
        //return weak_from_this();
	}
};
int main()
{
    auto ptr1 { make_shared<Foo>() };
    auto ptr2 { ptr1->getPointer() };
}

如果不使用enable_shared_from_this <Foo>就像下面这样的话,就会得到

class Foo{
public:
    shared_ptr<Foo> getPointer() {
    	return shared_ptr<Foo>(this);
	}
};

就会得到两个不相干的shared_ptrs指向同一个资源,还是会出现重复删除的问题。

永远不要使用 auto_ptr

#include <iostream>
#include <utility/unique_ptr.h>

struct MyObject {
  std::unique_ptr<std::shared_ptr<MyObject>> owner;
};

struct MyObject::DummyClass1 {
  int x;
  void(*f)(x);
};

int main() {
  std::unique_ptr<std::shared_ptr<MyObject>> p(std::make_unique<std::shared_ptr<MyObject>>("A"));
  p->DummyClass1 = std::make_unique<DummyClass1>("B");
  p->f(p->x);
  return 0;
}

This code will create a unique_ptr object that owns a pointer to a shared_ptr object. We can then assign the underlying memory to the unique_ptr object by calling the constructor of a