
🎉作者简介:👓 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢 c + + , g o , p y t h o n , 目前熟悉 c + + , g o 语言,数据库,网络编程,了解分布式等相关内容 \textcolor{orange}{博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢c++,go,python,目前熟悉c++,go语言,数据库,网络编程,了解分布式等相关内容} 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢c++,go,python,目前熟悉c++,go语言,数据库,网络编程,了解分布式等相关内容
📃 个人主页: \textcolor{gray}{个人主页:} 个人主页: 小呆鸟_coding
🔎 支持 : \textcolor{gray}{支持:} 支持: 如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦 \textcolor{green}{如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦} 如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦👍 就是给予我最大的支持! \textcolor{green}{就是给予我最大的支持!} 就是给予我最大的支持!🎁
💛本文摘要💛
本专栏主要是对c++ primer这本圣经的总结,以及每章的相关笔记。目前正在复习这本书。同时希望能够帮助大家一起,学完这本书。 本文主要讲解第12章 动态内存
c++ primer 第五版 系列文章:可面试可复习第2章 变量和基本类型
第3章 字符串、向量和数组
第4章 表达式
第5章 语句
第6章 函数
第8章 IO库
第9章 顺序容器
第10章 泛型算法
第11章 关联容器
第12章 动态内存
第13章 拷贝控制
第 14章 重载运算符
第15章 面向对象程序设计
第 16章 模板与泛型编程
对象的生命周期:
static对象在第一次使用前分配,在程序结束时销毁。对象的内存位置:
static对象、类static对象、定义在任何函数之外的变量。static对象。堆内存和栈内存的比较:
控制权:
栈由编译器自动分配和释放;堆由程序员分配和释放空间大小:
分配效率:
速度较快。速度较慢,但使用方便。系统响应:
存储内容:
存储函数的各个参数、局部变量、函数返回地址等。第一个进栈的就是函数返回地址内容由程序员决定。c++ 使用 new 和 delete 管理动态内存
动态内存使用出现的问题:
忘记释放内存会引起内存泄漏释放了后继续引用指针会引用非法内存。智能指针
memory 中智能指针作用
智能指针实际上是一个类模板。但是它的操作与指针十分相似,在创建模板时,必须提供指针指向的类型。shared_ptr<string> p1; // 可以指向 string
shared_ptr<vector<int>> p2; // 可以指向 int 的 vector
默认初始化的智能指针中保存着空指针。
定义shared_ptr方式
shared_ptr<int> p; // 默认初始化为空指针
shared_ptr<int> p(q); // q 也是一个 shared_ptr,p 是 q 的拷贝,此操作会递增 q 中的计数器。
shared_ptr<int> p(qnew); // qnew 是一个指向动态内存的内置指针(qnew = new int;))
shared_ptr<int> p(u); // u 是一个 unique_ptr。p 从 u 接管了对象的所有权,u 被置为空
shared_ptr<int> p(q, deleter); // q 是一个内置指针。p 将使用可调用对象 deleter 来代替 delete
shared_ptr<int> p(p2, deleter); // p2 是一个 shared_ptr,p 是 p2 的拷贝,唯一的区别是 p 将可调用对象 d 来代替 delete。
auto p = make_shared<int>(10); //返回一个 shared_ptr,指向一个初始化为 10 的动态分配的 int 对象。注意不同于 make_pair
shared_ptr 操作
sp // 智能指针作为 if 的判断条件时检查其是否为空,若 sp 指向一个对象,则为 true
sp->mem; // 等价于 (*p).mem。用于当 sp 指向一个类时
sp.get(); // 返回 sp 中保存的指针。要小心使用!
swap(p, q); // 交换 p 和 q 中的指针
p.swap(q); // 同上
p = q; // 此操作会递增 q 中的计数器,递减 p 原来的计数器,若其变为 0,则释放。
p.unique(); // 若 p.use_count() 为 1,返回 true,否则返回 false
p.use_count(); // 返回与 p 共享对象的智能指针数量。可能运行很慢,主要用于调试
p.reset(); // 将 p 置为空,如果 p 计数值为 1,释放对象。
p.reset(q); // q 是一个内置指针,令 p 指向 q。
p.reset(q, d); // 调用可调用对象 d 而不是 delete 来释放 q
make_shared 函数
shared_ptr<int> p1 = make_shared<int>(10);
shared_ptr<int> p2 = make_shared<int>(); //值初始化
auto p3 = make_shared<string>(10,'s');
shared_ptr的拷贝和赋值
每个 shared_ptr 都有一个关联的计数器,如果拷贝一个 shared_ptr,计数器就会递增。如果 shared_ptr 的计数器变为 0,就会自动释放管理的对象。auto r = make_shared<int>(42); // r 指向的 int 只有一个引用者
r = q; // 给 r 赋值,令它指向另一个地址。
//这会递增 q 指向的对象的引用计数,
//递减 r 原来指向的对象的引用计数。
//因为 r 原来指向的对象没有已经没有引用者,所以会自动释放。
shared_ptr 自动销毁所管理的对象
shared_ptr 的析构函数会递减对象的引用计数,如果计数变为 0,则销毁对象并释放内存。shared_ptr 自动释放相关联的内存

每个类都有析构函数。析构函数控制对象销毁时执行什么操作。 析构函数一般用来释放对象分配的资源。如 vector 的析构函数销毁它的元素并释放内存。使用动态内存的三种情况
使用动态内存在多个对象间共享内存
即可实现在多个类对象间共享同一个 vector。注意一个类只会与它的拷贝共享一个 vector,单独定义的两个类是不共享的。案例:定义StrBlob类
#include
#include
#include
#include
#include
using std::vector; using std::string;
class StrBlob {
public:
using size_type = vector<string>::size_type; // 灵活使用类型别名
StrBlob():data(std::make_shared<vector<string>>()) { }
StrBlob(std::initializer_list<string> il):data(std::make_shared<vector<string>>(il)) { } //定义了一个接受初始化列表的转换构造函数(注意不是 explicit 的)
size_type size() const { return data->size(); } // size() 函数不改变数据成员,所以声明为 const 的
bool empty() const { return data->empty(); } // 声明为 const 的
void push_back(const string &t) { data->push_back(t); }
void pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
std::string& front() {
check(0, "front on empty StrBlob");
return data->front();
}
std::string& back() {
check(0, "back on empty StrBlob");
return data->back();
}
const std::string& front() const { //在普通的 front() 函数外又重载了一个 const 的版本
check(0, "front on empty StrBlob");
return data->front();
}
const std::string& back() const { //在普通的 back() 函数外又重载了一个 const 的版本
check(0, "back on empty StrBlob");
return data->back();
}
private:
void check(size_type i, const string &msg) const { //定义了一个 check 函数来检查索引是否超出边界
if (i >= data->size()) throw std::out_of_range(msg); //不检查 i 是否小于 0 是因为 i 的类型是 size_type,是无符号类型,如果 i<0 会被自动转换为大于 0 的数
}
private:
std::shared_ptr<vector<string>> data;
};
注意点
对于不改变类的成员的函数,要声明为 const 的。既要定义返回普通引用的版本,也要定义返回常量引用的版本。返回常量引用的版本要声明为 const 的,这样才能成功地进行重载,不然只有返回值类型不同,编译器无法区分。new 和 delete 来直接管理内存。相比于智能指针,它们非常容易出错。注意
用new
动态分配和初始化对象。
new无法为分配的对象命名(因为自由空间分配的内存是无名的),因此是返回一个指向该对象的指针。int *pi = new int(123);bad_alloc的异常。用delete
将动态内存归还给系统。
delete后的指针称为空悬指针(dangling pointer)。使用new和delete
管理动态内存存在三个常见问题:
delete内存。默认情况下,动态分配的对象会被默认初始化,也可以直接初始化,列表初始化,值初始化来初始化动态分配的对象
int* p = new int; //默认初始化
string* sp = new string(10,'g');//直接初始化
vector<int>* vp = new vector<int>{0,1,2,3};//列表初始化
'对于类来说,值初始化与默认初始化没有什么区别,对于内置类型来说,值初始化对象会有一个良好的值,默认初始化对象值未定义。'
int* p1 = new int; // 默认初始化,p1 所指对象的值是未定义的
int* p2 = new int(); // 值初始化,p2 所指对象的值初始化为 0



释放动态内存
delete执行两个动作:
delete p; // p 必须指向一个动态分配的对象或是一个空指针
1.指针不在内存还在
就无法再释放了。这就是忘记 delete 产生的内存泄漏的问题。2. 指针还在内存不在
delete一个指针后,指针值已经无效,但是指针还是保存着地址,此时就变成了空悬指针。

导致内存泄漏
bool b() {
int* p = new int; // p 是一个 int 型指针
return p; // 函数返回值是 bool 类型,将 int 型指针转换为 bool 类型会使内存无法释放,造成内存泄漏
}
可以使用new来初始化智能指针explicit 的,因此不能将内置指针隐式地转换为智能指针。必须使用直接初始化,不能进行拷贝初始化(=,也就是赋值)。shared_ptr<double> p2 = new int(1024); // 错误:转换构造函数是 explicit 的,不能隐式转换
shared_ptr<double> p1(new int(1024)); // 正确:调用了转换构造函数
默认情况下一个用来初始化智能指针的普通指针只能指向动态内存,因为智能指针默认使用 delete 释放对象。不要混用智能指针和普通指针
使用普通指针(即 new 返回的指针)来创建一个 shared_ptr 有两个易错之处:
使用一个内置指针来访问一个智能指针所负责的对象是危险的,因为我们无法知道对象何时会被销毁
不要使用 get 初始化另一个智能指针或为智能指针赋值
get返回一个内置指针,指向智能指针所管理的对象,主要用于向不能使用智能指针的代码传递一个内置指针shared_ptr<int> p(new int(42));
int* q = p.get(); // 这是正确的,但是使用get返回的指针代码不能delete指针
俩种错误情况
shared_ptr 的关联计数只应用于自己的拷贝,如果使用某智能指针的 get 函数初始化另一个智能指针,两个指针的计数是不关联的,销毁一个就会直接释放内存使另一个成为空悬指针。
错误的例子
auto p = make_shared<int>();
auto q = p.get();
delete q; //错误,这会造成 double free。
其他shared_ptr操作

异常处理的程序能在异常发生后令程序流程继续,它需要确保在异常发生后资源能被正确地释放,一种简单的方法是使用智能指针。'shared_ptrp'
void f()
{
shared_ptr<int> sp(new int(42));
//这段代码抛出异常,且在f中未被捕获
} //在函数结束时shared_ptr自动释放内存
'new delete'
void f()
{
int *ip = new int(42);
//这段代码抛出异常,且在f中未被捕获
delete ip;
} //如果在New和delete之间发生异常,且未被捕获,则内存永远不会释放
智能指针和哑类
所有标准库类都定义了析构函数,负责清理对象使用的资源。但是那些为 C 和C++ 两种语言设计的类,通常都没有良好定义的析构函数,必须显式释放资源。
如果在资源分配和释放之间发生了异常,或程序员忘记释放资源,程序也会发生资源泄漏。
例如网络连接中的在释放连接前发生了异常,那么连接就不会被释放了。
struct destination
struct connection
connection connect(destination*);
void disconnect(connection);
void f(destination &d)
{
connection c = connect(&d);
//如果我们在f退出前忘记调用disconnect,就无法关闭c了
}
使用我们自己的释放操作
为了使用 shared_ptr 管理其他对象,如网络连接,这时就需要定义一个函数来代替delete,这个函数就称为删除器share_ptr<T> p(&t, deleter); //deleter 必须是一个接受一个 T* 类型参数的函数
使用 shared_ptr 管理网络连接
shared_ptr<connection> p(&c, end_connection);// end_connection 是 p 的删除器,它接受一个 connection* 参数
struct destination
struct connection
connection connect(destination*);
void disconnect(connection);
void end_connection(connection *p){disconnection(*p)};
void f(destination &d)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
//如果我们在f退出(即使由于异常退出),connection会被正常关闭
}
智能指针陷阱
1. 不使用相同的内置指针值初始化或 reset 多个智能指针
2. 不 delete get() 返回的指针
3. 不使用 get() 初始化或 reset 另一个智能指针
4. 如果使用 get() 返回的指针,当最后一个对应的智能指针被销毁后,指针就变为无效的了
5. 如果智能指针管理的不是 new 分配的内存,记住传递给它一个删除器
某一个时刻只能有一个unique_ptr指向一个给定的对象。(与shared_ptr不同)不支持拷贝或者赋值操作当 unique_ptr 被销毁时,指向对象也被销毁。类似 shared_ptr,初始化 unique_ptr 必须采用直接初始化。(这里指使用 new 初始化)定义 unique_ptr 时,需要绑定到一个 new 返回的指针上unique_ptr定义和初始化
unique_ptr<int> u1; // 定义一个空 unique_ptr
unique_ptr<int> u1(new int()); // 正确
unique_ptr<int,deleter> u; // 定义一个空 unqiue,用可调用对象 deleter 代替 delete
unique_ptr<int,deleter> u(d); // 空 unique,用类型为 deleter 的对象 d 代替delete
unique_ptr<int,decltype(d)*> u(new int(42),d);
unique_ptr<int> u2(u1); // 错误:不支持拷贝
unique_ptr<int> u3;
u3 = u2; // 错误,不支持赋值
注意:
我们不能通过拷贝或者赋值unique_ptr,但是可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个uniqueunique_ptr 管理删除器的方式与 shared_ptr 不一样。unique_ptr 将删除器放在尖括号中unique_ptr操作
u.get();
u1.swap(u2);swap(u1,u2);
unique_ptr<T> u1 //空unique_ptr,可以指向类型是T的对象。u1会使用delete来是释放它的指针。
unique_ptr<T,D> u2 //u2会使用一个类型为D的可调用对象来释放它的指针。
unique_ptr<T,D> u(d) //空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr; // 释放 u 指向的对象并将 u 置为空
auto u2 = u.release(); // u 放弃对指针的控制权,返回 u 中保存的内置指针,并将 u 置为空,注意 u2 的类型是内置指针,而不是 unique_ptr
'u.reset'
u.reset(); // 释放 u 指向的对象
u.reset(nullptr); // 释放 u 指向的对象,并将 u 置为空,等价于 u = nullptr;
u.reset(q); // 令 u 指向内置指针 q 指向的对象
unique_ptr<int> u2(u1.release()); // 所有权转移给 u2,u1 置为空
u3.reset(u1.release()); // 释放 u3 原来指向的内存,u3 接管 u1 指向的对象。
release使用
p2.release(); //错误,p2不会释放内存,而且我们失去了指针
auto p = p2.release(); //正确,但是要必须记得delete(p)
shared_ptr p = p2.release(); //不需要delete(p)
传递unique_ptr参数和返回unique_ptr
可以拷贝或赋值一个将要被销毁的 unique_ptr。如从函数返回一个 unique_ptr,最常见的例子是从函数返回一个unique_ptr;unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int(p));
return ret; //返回一个局部对象的拷贝
}
return unique_ptr<int> (new int(p));
//返回一个临时对象,与局部对象是一样的
auto_ptr
向 unique_ptr 传递删除器
但 unique_ptr 管理删除器的方式与 shared_ptr 不一样。unique_ptr 将删除器放在尖括号中//p指向一个类型为objT对象,并使用一个类型为delT对象释放objT对象
//它会调用fun的delT类型对象
unique_ptr<objT, delT> p(new objT, func);
'具体例子'
void end_connection(connection *p){disconnection(*p)};
void f(destination &d)
{
connection c = connect(&d);
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
//如果我们在f退出(即使由于异常退出),connection会被正常关闭
}
错误案例
int ix = 1024, *pi = &ix, *pi2 = new int(2048);
unique_ptr<int> p0(ix); // 错误:从 int 到 unique_ptr 的无效的转换
unique_ptr<int> p1(pi); // 运行时错误:当 p1 被销毁时会对 pi 调用 delete,这是一个对非动态分配返回的指针调用 delete 的错误。
unique_ptr<int> p2(pi2); // 不会报错,但当 p2 被销毁后会使 pi2 成为一个悬空指针
unique_ptr<int> p3(new int(2048)); // 正确,推荐的用法
不改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。weak_ptr初始化
创建 weak_ptr 时,要用 shared_ptr 来初始化它。weak_ptr<T> w; // 默认初始化,定义一个空 weak_ptr w,w 可以指向类型为 T 的对象
w = p; // p 可以是一个 shared_ptr 或 weak_ptr,赋值后 w 与 p 共享对象
weak_ptr<T> w(sp); // 定义一个与 shared_ptr sp 指向相同对象的 weak_ptr。T 必须能转换成 sp 指向的类型(不必相同)
weak_ptr 操作
由于 weak_ptr 的对象可能被释放的,因此不能直接访问对象,必须调用 lock()。lock()w = p; // p 可以是一个 shared_ptr 或 weak_ptr。赋值后 w 与 p 共享对象。
w.reset(); // 将 w 置为空
w.use_count(); // 返回与 w 共享对象的 shared_ptr 的数量
w.expired(); // 若 w.use_count() 为 0,返回 true,否则返回 false。expired 是 “过期的” 意思
w.lock(); // 如果 w.expired 为 true,返回一个空 shared_ptr;否则返回一个指向 w 的对象的 shared_ptr
if(shared_ptr<int> np = wp.lock()) // 如果 np 不为空则条件成立
一个实例:StrBlobPtr类
class StrBlobPtr
{
public:
StrBlobPtr() : curr(0) {}
StrBlobPtr(StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
string &deref() const;
StrBlobPtr &incr();
bool operator!=(const StrBlobPtr &rhs) const { return this->curr != rhs.curr; }
private:
shared_ptr<vector<string>> check(std::size_t, const string &msg) const; //不能在 const 成员函数内调用本类的非 const 成员函数,调用的必须也是 const 成员函数
private:
weak_ptr<vector<string>> wptr;
size_t curr;
};
shared_ptr<vector<string>> StrBlobPtr::check(std::size_t sz, const string &msg) const{
auto ret = wptr.lock();
if (!ret) throw std::runtime_error("unbound StrBlobPtr"); //检查 wptr 是否绑定了一个 StrBlob
if (sz >= ret->size()) throw std::out_of_range("msg");
return ret;
}
string &StrBlobPtr::deref() const { //const 成员函数在定义时也要加上 const
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
StrBlobPtr &StrBlobPtr::incr(){
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
解释
check返回的是一个shared_ptrreturn (*p) [curr] 解引用获得vector,然后通过下标访问vector当中的元素c++中提供俩种一次分配一个对象数组的方法:
1. 使用如 new int[10] 来分配一个对象数组
2. 使用 allocator 类。allocator 类可以实现内存分配与对象构造的分离,更灵活地管理内存。
new一个动态数组:
int *p = new int[size];delete一个动态数组:
delete [] p;unique_ptr和数组:
unique_ptr不支持成员访问运算符(点和箭头)。
分配一个数组得到一个元素类型的指针
new T[] 分配的内存叫做动态数组,但是实际上它并不是一个数组,而只是返回第一个元素的指针。理解:数组类型是包含其维度的,而 new 分配动态数组时提供的大小不必是常量,这正是因为它并非分配了一个“数组类型”。不能对它调用 begin() 或 end() 函数(这两个函数根据数组维度返回指向首元素和尾后元素的指针),也不能使用范围 for 语句来处理动态数组。初始化动态分配对象的数组

动态分配一个空数组是合法的

释放动态数组
使用 delete [] 会将动态数组中的元素按逆序销毁并释放内存。
智能指针和动态数组-unique_ptr


智能指针和动态数组-shared_ptr

new 有一个局限性是它将内存分配和对象构造结合在了一起,对应的 delete 将对象析构和内存释放结合在了一起。memory 中,可以实现内存分配与对象构造的分离。allocator 是一个类模板。定义时需指出这个 allocator 可以分配的对象类型,它会根据对象类型来分配恰当的内存。
allocator的定义与操作
allocator<string> alloc; // 定义一个可以分配 string 的 allocator 对象
auto const p = alloc.allocate(n); // 分配 n 个未初始化的 string,返回一个 string* 指针
alloc.construct(p, args); // p 是一个 string* 指针,指向原始内存。arg 被传递给 string 的构造函数,用来在 p 指向的内存中构造对象。
alloc.destory(p); // p 是一个 string* 指针,此算法对 p 指向的对象执行析构函数
alloc.deallocate(p, n); // 释放从 p 开始的长度为 n 的内存。p 是一个 allocate() 返回的指针,n 是 p 创建时要求的大小。
// 在 deallocate 之前必须先 destory 掉这块内存中创建的每个对象。
解释:内存分配、对象构造、对象销毁、内存释放四种操作,且这四种操作是分开的,分别对应一个函数。
allocator分配未构造的内存
allocator 分配的内存是未构造的,需要使用 construct 成员函数按需在内存中构造对象。
使用完对象后,必须对每个构造的元素都调用 destory 来摧毁它们。destory 接受一个指针,对指向的对象执行析构函数。注意只能对构造了的元素执行 destory 操作。元素被销毁后可以重新在这块内存构造对象也可以释放掉内存。construct 和 destory 一次都只能构造或销毁一个对象,要想完成对所有元素的操作,需要通过指针来遍历对每个元素进行操作。deallocate 释放内存
拷贝和填充未初始化内存的算法

uninitialized_copy 函数返回指向构造的最后一个元素之后位置的指针。将一个 vector 拷贝到动态内存中,并对后一半空间用给定值填充。
