原始指针(慎用) miniWiki

new

分配内存 + 默认初始化

置于类型名之前的 new 运算符用于创建单个动态对象。 如果分配成功,则返回一个指向动态对象的指针,否则抛出异常

int* p = new int;

new 语句依次完成三个任务:

  1. 动态分配 (allocate) 所需内存;
  2. 默认初始化 (initialize) 对象;
  3. 返回指向该对象的原始指针 (raw pointer)

分配内存 + 值初始化

若要进行值初始化 (value initialization),需要在类型名后面紧跟 (){},例如

std::string* ps1 = new std::string;    // 默认初始化 为 空字符串
std::string* ps2 = new std::string();  // 值初始化 为 空字符串
int* pi1 = new int;    // 默认初始化 为 不确定值
int* pi2 = new int{-1};  // 值初始化 为 -1

常值对象

动态分配的常值对象必须由指向常量的指针接管,并且在创建时被初始化:

const int* pci = new const int(1024);
const std::string* pcs = new const std::string;

自 C++11 起,推荐用 auto 作为对象类型,编译器会推断出变量的类型:

auto pi = new int();
auto ps = new std::string();
auto pci = new const int(1024);
auto pcs = new const std::string("hello");

内存耗尽

动态内存资源在运行期有可能被耗尽,此时分配内存的任务无法完成。

  • 在默认情况下,分配失败会抛出 std::bad_alloc
  • 如果在 new类型名之间插入 (std::nothrow),则分配失败时不会抛出异常,而是以 nullptr 作为返回值。⚠️ 使用这种形式的 new 一定要记得检查返回值是否为 nullptr
  • std::bad_allocstd::nothrow 都定义在 <new> 中。
#include <new>
int* p1 = new int;                 // 如果分配失败, 将抛出 std::bad_alloc
int* p2 = new (std::nothrow) int;  // 如果分配失败, 将返回 nullptr

delete

析构对象 + 释放内存

传递给 delete 的指针必须是指向动态对象的指针nullptr

delete p;     // 析构并释放 (单个) 动态对象

⚠️ 编译器无法判断一个指针所指的对象是否是动态的,也无法判断一个指针所指的内存是否已经被释放

内存泄漏

Foo* factory(T arg) {
  return new Foo(arg);
}
void use_factory(T arg) {
  Foo *p = factory(arg)  // factory 返回一个指向动态内存的指针
  // 使用 p
  delete p;  // 调用者负责将其释放
}

如果 use_factory 在返回前没有释放 p 所指向的动态内存,则 use_factory 的调用者将不再有机会将其释放,可用的动态内存空间将会变小。这种现象被称为内存泄漏 (memory leak)

空悬指针

执行完 delete p 之后,p 将成为一个空悬指针 (dangling pointer),对其进行

  • 解引用,并进行
    • 读:返回无意义的值。
    • 写:有可能破坏数据。
  • 再次 delete p:会破坏内存空间。

为避免这些陷阱,应当

  • delete p 尽可能放在 p 的作用域末端,或者
  • delete p 后面紧跟 p = nullptr

即便如此,由于同一个动态对象有可能被多个指针所指向,还是会有危险:

auto q = p;   // p 和 q 指向同一块动态内存
delete p;     // 释放
p = nullptr;  // p 不再指向该地址
              // q 仍然指向该地址, 对其进行 解引用 或 再次释放 都有可能造成破坏

动态数组

new T[] + delete[]

大多数情况下应当优先选用标准库提供的容器而不是动态数组。 如果要显式创建动态数组,则需要在类型名称后面紧跟对象个数。 如果分配成功,则返回指向数组第一个对象的指针,否则抛出异常

auto pa = new int[42];
delete[] pa;  // 析构并释放 (整个) 动态数组

对象个数是数组类型的一部分。 可以先定义类型别名,然后就可以像普通类型一样创建动态对象:

typedef int Array[42];  // 等价于 using Array = int[42];
auto pa = new Array;

在给定位置构造对象

与单个对象类似,数组版本的 new 会依次完成分配内存 (allocate memory)构造对象 (construct object) 两个操作。对于动态数组,通常希望将这两个操作拆分开

  • 分配内存可以通过 new char[N]std::malloc() 完成。
  • 构造对象可以通过 placement new 完成。
int count = 5;
char* char_ptr = new char[count * sizeof(Foo)];  // 分配内存
Foo* foo_ptr = new (char_ptr) Foo(args);  // 构造 foo_ptr[0]
foo_ptr->~Foo();                          // 析构 foo_ptr[0]
new (foo_ptr + 3) Foo(args);  // 构造 foo_ptr[3]
(foo_ptr + 3)->~Foo();        // 析构 foo_ptr[3]
delete[] char_ptr;  // 释放整块内存

其中

  • char_ptrfoo_ptr相同,但类型不同,因此指针加减运算的步长也不同。
  • new (foo_ptr + 3) Foo(args) 不能foo_ptr[3] = Foo(args) 代替,因为赋值操作会析构左侧对象,而 foo_ptr[3] 还没有被构造过。

std::allocator

标准库定义的 std::allocator 类模板将以上操作(分配、构造、析构、释放)封装为其成员函数,使用时更加安全:

#include <memory>
std::allocator<T> a;      // 创建 allocator 对象
T* p = a.allocate(n);     // 分配 整块内存,不进行 构造,返回首元地址
a.deallocate(p, n);       // 释放 整块内存,p 指向首元
a.construct(p, args);     // 构造 单个对象,p 不必是首元地址,存储于 p 所指向的位置
a.destroy(p);             // 析构 单个对象,p 不必是首元地址

std::malloc

C 标准库的 <stdlib.h> 提供了一组动态内存管理函数,也可以用来管理动态数组:

// 分配 total_size 个字节的内存,不做初始化:
void* malloc(std::size_t total_size);
// 分配 num * each 个字节的内存,并将所有字节初始化为 0:
void* calloc(std::size_t num, std::size_t each);
// 释放由 malloc | calloc 分配的内存:
void free(void* p);

在 C++ 中,它们被声明在命名空间 std 中:

#include <cstdlib>
auto p = (int*) std::malloc(sizeof(int) * 100);
auto q = (int*) std::calloc(100, sizeof(int));
std::free(p);
std::free(q);

std::uninitialized_*

<memory> 提供了一组名为 std::uninitialized_* 的算法,用于在由 std::allocatorstd::malloc 获得的未初始化的内存 (uninitialized memory) 中填入对象:

#include <memory>
// 从另一个容器中 copy 或 move (C++17):
std::uninitialized_copy(b, e, p);    // 返回 p + n
std::uninitialized_move(b, e, p);    // 返回 p + n
std::uninitialized_copy_n(b, n, p);  // 返回 p + n
std::uninitialized_move_n(b, n, p);  // 返回 p + n
// 在一段范围内用 t 进行构造:
std::uninitialized_fill(b, e, t);    // 返回 void
std::uninitialized_fill_n(b, n, t);  // 返回 b + n