面向对象编程基础¶
本课程入选教育部产学合作协同育人项目 课程主页:http://cpp.njuer.org 课程老师:陈明 http://cv.mchen.org
ppt和代码下载地址
git clone https://gitee.com/cpp-njuer-org/book
第12章¶
动态内存¶
动态内存¶
到目前为止,我们编写的程序中所使用的对象都有着严格定义的生存期。
- 全局对象在程序启动时分配,在程序结束时销毁。
- 对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。
- 局部static对象在第一次使用前分配,在程序结束时销毁。
C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的
- 只有当显式地被释放时,这些对象才会销毁。
动态内存¶
动态对象的正确释放被证明是编程中极其容易出错的地方。
为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。
当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。
静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
栈内存用来保存定义在函数内的非static对象。
分配在静态或栈内存中的对象由编译器自动创建和销毁。
对于栈对象,仅在其定义的程序块运行时才存在;
static对象在使用之前分配,在程序结束时销毁。
动态内存¶
除了静态内存和栈内存,每个程序还拥有一个内存池。
这部分内存被称作自由空间(free store)或堆(heap)。
程序用堆来存储动态分配(dynamicallyallocate)的对象
- 即在程序运行时分配的对象。动态对象的生存期由程序来控制,
- 当动态对象不再使用时,我们的代码必须显式地销毁它们。
虽然使用动态内存有时是必要的,但众所周知,正确地管理动态内存是非常棘手的。
动态内存与智能指针¶
动态内存的管理是通过一对运算符来完成的:
- new,在动态内存中为对象分配空间并返回一个指向该对象的指针,
我们可以选择对对象进行初始化;
- delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
动态内存的使用很容易出问题,
- 因为确保在正确的时间释放内存是极其困难的。
- 有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;
- 有时在尚有指针引用内存的情况下我们就释放了它,
在这种情况下就会产生引用非法内存的指针。
动态内存与智能指针¶
新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。
智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。
新标准库提供的这两种智能指针的区别在于管理底层指针的方式:
- shared_ptr允许多个指针指向同一个对象;
- unique_ptr则“独占”所指向的对象。
- 标准库还定义了一个名为weak_ptr的伴随类,
它是一种弱引用,指向shared_ptr所管理的对象。
这三种类型都定义在memory头文件中。
shared_ptr类¶
类似vector,智能指针也是模板
创建一个智能指针时,必须提供额外的信息——指针可以指向的类型
shared_ptr<string> p1;
shared_ptr<list<int>> p2;
//默认初始化的智能指针中保存着一个空指针
智能指针的使用方式与普通指针类似
//解引用一个智能指针返回它指向的对象。
//如果在一个条件判断中使用智能指针,效果就是检测它是否为空:
//如果p1不为空 检查是否指向空string
if(p1 && p1->empty()){
*p1 = "hi";
}
shared_ptr和unique_ptr都支持的操作¶
shared_ptr<T> sp unique_ptr<T> up 空智能指针,可以指向类型是T的对象
p 将p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象。
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针,要小心使用,
若智能指针释放了对象,返回的指针所指向的对象也就消失了。
swap(p, q) p.swap(q) 交换p和q中的指针
shared_ptr独有的操作¶
make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。
使用args初始化此对象。
shared_ptr<T>p(q) p是shared_ptr q的拷贝;此操作会递增q中的计数器。
q中的指针必须能转换为T*
p = q p和q都是shared_ptr,所保存的指针必须能互相转换。
此操作会递减p的引用计数,递增q的引用计数;
若p的引用计数变为0,则将其管理的原内存释放。
p.unique() 若p.use_count()是1,返回true;否则返回false
p.use_count() 返回与p共享对象的智能指针数量;可能很慢,主要用于调试
make_shared函数¶
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。
此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
与智能指针一样,make_shared也定义在头文件memory中。
当要用make_shared时,必须指定想要创建的对象的类型。
定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10,'9');
shared_ptr<int> p5 = make_shared<int>();
//make_shared用其参数来构造给定类型的对象。
//例如,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配,
//调用make_shared<int>时传递的参数必须能用来初始化一个int,依此类推。
//如果我们不传递任何参数,对象就会进行值初始化
通常用auto定义一个对象来保存make_shared的结果,这种方式较为简单:
auto p6 = make_shared<vector<string>>();
shared_ptr的拷贝和赋值¶
当进行拷贝或赋值操作时,每个shared_ptr都会记录
有多少个其他shared_ptr指向相同的对象:
auto p = make_shared<int>(42);
auto q(p);
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。
无论何时我们拷贝一个shared_ptr,计数器都会递增。
- 例如,当用一个shared_ptr初始化另一个shared_ptr,
- 或将它作为参数传递给一个函数以及作为函数的返回值
当我们给shared_ptr赋予一个新值或是shared_ptr被销毁,计数器就会递减
- 例如一个局部的shared_ptr离开其作用域
¶
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
auto r = make_shared<int>(42);
r = q;
//我们分配了一个int,将其指针保存在r中。
//接下来,我们将一个新值赋予r。
//r是唯一指向此int的shared_ptr,在把q赋给r的过程中,此int被自动释放。
到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,
完全由标准库的具体实现来决定。
关键是智能指针类能记录有多少个shared_ptr指向相同的对象,
并能在恰当的时候自动释放对象。
shared_ptr自动销毁所管理的对象¶
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。
它是通过另一个特殊的成员函数——析构函数(destructor)完成销毁工作的。
类似于构造函数,每个类都有一个析构函数。
就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。
析构函数一般用来释放对象所分配的资源。
- 例如,string的构造函数(以及其他string成员)会分配内存来保存构成string的字符。
string的析构函数就负责释放这些内存。
- 类似的,vector的若干操作都会分配内存来保存其元素。
vector的析构函数就负责销毁这些元素,并释放它们所占用的内存。
shared_ptr的析构函数会递减它所指向的对象的引用计数。
- 如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
shared_ptr还会自动释放相关联的内存¶
当动态对象不再被使用时,shared_ptr类会自动释放动态对象,
这一特性使得动态内存的使用变得非常容易。
- 例如,我们可能有一个函数,它返回一个shared_ptr,指向一个Foo类型的动态分配
的对象,对象是通过一个类型为T的参数进行初始化的:
shared_ptr<foo> factory(T arg){
//恰当处理arg shared_ptr负责释放内存
return shared_ptr<Foo>(arg);
}
//由于factory返回一个shared_ptr,
//所以我们可以确保它分配的对象会在恰当的时刻被释放。
- 例如,下面的函数将factory返回的shared_ptr保存在局部变量中
void use_factory(T arg){
shared_ptr<Foo> p = factory(arg);
//使用p
}// p离开作用域 指向的内存会被自动释放掉
//由于p是use_factory的局部变量,在use_factory结束时它将被销毁
//当p被销毁时,将递减其引用计数并检查它是否为0。
//在此例中,p是唯一引用factory返回的内存的对象。
//由于p将要销毁,p指向的这个对象也会被销毁,所占用的内存会被释放。
¶
但如果有其他shared_ptr也指向这块内存,它就不会被释放掉:
shared_ptr<Foo> use_factory(T arg){
shared_ptr<Foo> p = factory(arg);
//使用p
return p;//当我们返回p 引用计数进行了递增操作
}// p离开作用域
//在此版本中,use_factory中的return语句向此函数的调用者返回一个p的拷贝。
//拷贝一个shared_ptr会增加所管理对象的引用计数值。
//现在当p被销毁时,它所指向的内存还有其他使用者。对于一块内存,
//shared_ptr类保证只要有任何shared_ptr对象引用它,它就不会被释放掉。
//由于在最后一个shared_ptr销毁前内存都不会释放,
//保证shared_ptr在无用之后不再保留就非常重要了。
如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。
share_ptr在无用之后仍然保留的一种可能情况是
- 你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。
在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。
//如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,
//而只使用其中一部分,要记得用erase删除不再需要的那些元素。
使用了动态生存期的资源的类¶
程序使用动态内存出于以下三种原因之一:
1.程序不知道自己需要使用多少对象
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据
- 容器类是出于第一种原因而使用动态内存的典型例子
- 将在后续看到出于第二种原因而使用动态内存的例子
- 我们将定义一个类,它使用动态内存是为了让多个对象能共享相同的底层数据。
到目前为止,我们使用过的类中,¶
分配的资源都与对应对象生存期一致。¶
例如,每个vector“拥有”其自己的元素。
当我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的:
vector<string> v1; // empty vector
{ // new scope
vector<string> v2 = {"a", "an", "the"};
v1 = v2; // copies the elements from v2 into v1
} // v2 is destroyed, which destroys the elements in v2
// v1 has three elements, which are copies of the ones originally in v2
由一个vector分配的元素只有当这个vector存在时才存在。
当一个vector被销毁时,这个vector中的元素也都被销毁。
使用动态内存 允许多个对象共享相同的状态¶
但某些类分配的资源具有与原对象相独立的生存期。
例如,假定我们希望定义一个名为Blob的类,保存一组元素。
与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。
即,当我们拷贝一个Blob时,原Blob对象及其拷贝应该引用相同的底层元素
如果两个对象共享底层的数据,当某个对象被销毁时,我们不能单方面地销毁底层数据:
Blob<string> b1; // empty Blob
{ // new scope
Blob<string> b2 = {"a", "an", "the"};
b1 = b2; // b1 and b2 share the same elements
} // b2 is destroyed, but the elements in b2 must not be destroyed
// b1 points to the elements originally created in b2
在此例中,b1和b2共享相同的元素。
当b2离开作用域时,这些元素必须保留,因为b1仍然在使用它们。
定义StrBlob类¶
现在我们先定义一个管理string的类,此版本命名为StrBlob。
实现一个新的集合类型的最简单方法是使用某个标准库容器来管理元素。
- 在本例中,我们将使用vector来保存元素。
但是,我们不能在一个Blob对象内直接保存vector,
- 因为一个对象的成员在对象销毁时也会被销毁。
- 例如,假定b1和b2是两个Blob对象,共享相同的vector。
- 如果此vector保存在其中一个Blob中——例如b2中,那么当b2离开作用域时,
此vector也将被销毁,也就是说其中的元素都将不复存在。
- 为了保证vector中的元素继续存在,我们将vector保存在动态内存中。
- 为了实现我们所希望的数据共享
我们为每个StrBlob设置一个shared_ptr来管理动态分配的vector
- 此shared_ptr的成员将记录有多少个StrBlob共享相同的vector,
并在vector的最后一个使用者被销毁时释放vector。
- 我们还需要确定这个类应该提供什么操作。
- 当前,我们将实现一个vector操作的小的子集。
我们会修改访问元素的操作(如front和back):
- 如果用户试图访问不存在的元素,这些操作会抛出一个异常。
定义StrBlob类¶
class StrBlob {
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// add and remove elements
void push_back(const std::string &t) {data->push_back(t);}
void pop_back();
// element access
std::string& front();
std::string& back();
private:
std::shared_ptr<std::vector<std::string>> data;
// throws msg if data[i] isn't valid
void check(size_type i, const std::string &msg) const;
};
//我们的类有一个默认构造函数和一个构造函数,
//接受单一的initializer_list<string>类型参数
//此构造函数可以接受一个初始化器的花括号列表。
StrBlob构造函数¶
//两个构造函数都使用初始化列表来初始化其data成员,
//令它指向一个动态分配的vector。
//默认构造函数分配一个空vector:
StrBlob::StrBlob(): data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list<string> il):
data(make_shared<vector<string>>(il)) { }
//接受一个initializer_list的构造函数将其参数传递给对应的vector构造函数
//此构造函数通过拷贝列表中的值来初始化vector的元素。
元素访问成员函数¶
//名为check的private工具函数,它检查一个给定索引是否在合法范围内
void StrBlob::check(size_type i, const string &msg) const
{
if (i >= data->size())
throw out_of_range(msg);
}
元素访问成员函数¶
//pop_back和元素访问成员函数首先调用check。
//如果check成功,这些成员函数继续利用底层vector的操作来完成自己的工作:
string& StrBlob::front()
{
// if the vector is empty, check will throw
check(0, "front on empty StrBlob");
return data->front();
}
string& StrBlob::back()
{
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
//front和back应该对const进行重载,这些版本的定义留作练习。
StrBlob的拷贝、赋值和销毁¶
StrBlob使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作
默认情况下,这些操作拷贝、赋值和销毁类的数据成员。
当我们拷贝、赋值或销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝、赋值或销毁。
如前所见,拷贝一个shared_ptr会递增其引用计数;
- 将一个shared_ptr赋予另一个shared_ptr会递增赋值号右侧shared_ptr的引用计数,
- 而递减左侧shared_ptr的引用计数。
如果一个shared_ptr的引用计数变为0,它所指向的对象会被自动销毁。
- 对于由StrBlob构造函数分配的vector,当最后一个指向它的StrBlob对象被销毁时,
它会随之被自动销毁。
练习¶
在此代码的结尾,b1 和 b2 各包含多少个元素?
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
}
它们实际操作的是同一个vector,都包含4个元素。
在代码的结尾,b2 被析构了,不影响 b1 的元素。
练习¶
编写你自己的StrBlob 类,包含const 版本的 front 和 back。
//StrBlb.h 文件:
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <exception>
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)) { }
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
练习¶
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 {
check(0, "front on empty StrBlob");
return data->front();
}
const std::string& back() const {
check(0, "back on empty StrBlob");
return data->back();
}
private:
void check(size_type i, const string &msg) const {
if (i >= data->size()) throw std::out_of_range(msg);
}
private:
std::shared_ptr<vector<string>> data;
};
练习¶
主函数:
#include "StrBlb.h"
#include <iostream>
int main()
{
const StrBlob csb{ "hello", "world", "pezy" };
StrBlob sb{ "hello", "world", "Mooophy" };
std::cout << csb.front() << " " << csb.back() << std::endl;
sb.back() = "pezy";
std::cout << sb.front() << " " << sb.back() << std::endl;
}
练习¶
StrBlob 需要const 版本的push_back 和 pop_back吗?
如需要,添加进去。否则,解释为什么不需要。
不需要。push_back 和 pop_back 会改变对象的内容。
而 const 对象是只读的,因此不需要。
练习¶
在我们的 check 函数中,没有检查 i 是否大于0。为什么可以忽略这个检查?
因为 size_type 是一个无符号整型,
当传递给 check 的参数小于 0 的时候,参数值会转换成一个正整数。
练习¶
我们未编写接受一个 initializer_list explicit 参数的构造函数。
讨论这个设计策略的优点和缺点。
构造函数不是 explicit 的,意味着可以从 initializer_list 隐式转换为 StrBlob。
在 StrBlob 对象中,只有一个数据成员 data,
而 StrBlob 对象本身的含义,也是一个管理字符串的序列。
因此,从 initializer_list 到 StrBlob 的转换,在逻辑上是可行的。
而这个设计策略的缺点,可能在某些地方我们确实需要 initializer_list,
而编译器仍会将之转换为 StrBlob。
直接管理内存¶
C++语言定义了两个运算符来分配和释放动态内存。
- 运算符new分配内存,
- delete释放new分配的内存。
- 使用这两个运算符管理内存非常容易出错
- 直接管理内存的类与使用智能指针的类不同,
它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义
使用智能指针的程序更容易编写和调试
使用new动态分配和初始化对象¶
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,
而是返回一个指向该对象的指针:
int *pi = new int; // pi points to a dynamically allocated,
// unnamed, uninitialized int
//此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针。
默认情况下,动态分配的对象是默认初始化的
这意味着内置类型或组合类型的对象的值将是未定义的,
而类类型对象将用默认构造函数进行初始化:
string *ps = new string; // initialized to empty string
int *pi = new int; // pi points to an uninitialized int
使用new动态分配和初始化对象¶
我们可以使用直接初始化方式来初始化一个动态分配的对象。
我们可以使用传统的构造方式(使用圆括号),
在新标准下,也可以使用列表初始化(使用花括号):
int *pi = new int(1024); // object to which pi points has value 1024
string *ps = new string(10, '9'); // *ps is "9999999999"
// vector with ten elements with values from 0 to 9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:
string *ps1 = new string; //default initialized to the empty string
string *ps = new string(); //value initialized to the empty string
int *pi1 = new int; //default initialized; *pi1 is undefined
int *pi2 = new int(); //value initialized to 0; *pi2 is 0
使用new动态分配和初始化对象¶
对于定义了自己的构造函数的类类型(例如string)来说,要求值初始化是没有意义的;
- 不管采用什么形式,对象都会通过默认构造函数来初始化。
但对于内置类型,两种形式的差别就很大了;
- 值初始化的内置类型对象有着良好定义的值,
- 而默认初始化的对象的值则是未定义的。类似的,
对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,
- 如果它们未在类内被初始化,那么它们的值也是未定义的。
出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。
使用new动态分配和初始化对象¶
如果我们提供了一个括号包围的初始化器,就可以使用auto,
从此初始化器来推断我们想要分配的对象的类型。
但是,由于编译器要用初始化器的类型来推断要分配的类型,
只有当括号中仅有单一初始化器时才可以使用auto:
auto p1 = new auto(obj); // p points to an object of the type of obj
// that object is initialized from obj
auto p2 = new auto{a,b,c}; // error: 括号中只能有单个初始化器
//p1的类型是一个指针,指向从obj自动推断出的类型。
//若obj是一个int,那么p1就是int*;
//若obj是一个string,那么p1是一个string*;
//依此类推。新分配的对象用obj的值进行初始化。
动态分配的const对象¶
用new分配const对象是合法的:
// allocate and initialize a const int
const int *pci = new const int(1024);
// allocate a default-initialized const empty string
const string *pcs = new const string;
类似其他任何const对象,一个动态分配的const对象必须进行初始化。
对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,
而其他类型的对象就必须显式初始化。
由于分配的对象是const的,new返回的指针是一个指向const的指针
内存耗尽¶
虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。
一旦一个程序用光了它所有可用的内存,new表达式就会失败。
- 默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。
- 我们可以改变使用new的方式来阻止它抛出异常:
// if allocation fails, new returns a null pointer
int *p1 = new int; // if allocation fails, new throws std::bad_alloc
int *p2 = new (nothrow) int; // if allocation fails, new returns a null pointer
我们称这种形式的new为定位new(placement new)
定位new表达式允许我们向new传递额外的参数。
//在此例中,我们传递给它一个由标准库定义的名为nothrow的对象
//如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。
//如果这种形式的new不能分配所需内存,它会返回一个空指针。
bad_alloc和nothrow都定义在头文件new中。
释放动态内存¶
为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。
我们通过delete表达式(delete expression)来将动态内存归还给系统。
delete表达式接受一个指针,指向我们想要释放的对象:
delete p; // p must point to a dynamically allocated object or be null
与new类型类似,delete表达式也执行两个动作:
- 销毁给定的指针指向的对象;
- 释放对应的内存。
指针值和delete¶
我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。
释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的:
int i,*pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; // error: i is not a pointer
delete pi1; // undefined: pi1 refers to a local
delete pd; // ok
delete pd2; // undefined: the memory pointed to by pd2 was already freed
delete pi2; // ok: it is always ok to delete a null pointer
//对于delete i的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。
//执行delete pi1和pd2所产生的错误则更具潜在危害:
//通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。
//类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。
//对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的
指针值和delete¶
虽然一个const对象的值不能被改变,但它本身是可以被销毁的。
如同任何其他动态对象一样,想要释放一个const动态对象,
只要delete指向它的指针即可:
const int *pci = new const int(1024);
delete pci; // ok: deletes a const object
动态对象的生存期直到被释放时为止¶
由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放。
但对于通过内置指针类型来管理的内存,就不是这样了。
对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。
返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担
— 调用者必须记得释放内存:
// factory returns a pointer to a dynamically allocated object
Foo* factory(T arg)
{
// process arg as appropriate
return new Foo(arg); // caller is responsible for deleting this memory
}
//这个版本的factory分配一个对象,但并不delete它。
//factory的调用者负责在不需要此对象时释放它。
//不幸的是,调用者经常忘记释放对象:
动态对象的生存期直到被释放时为止¶
void use_factory(T arg)
{
Foo *p = factory(arg);
// use p but do not delete it
} // p goes out of scope, but the memory to which p points is not freed!
//此处,use_factory函数调用factory,后者分配一个类型为Foo的新对象。
//当use_factory返回时,局部变量p被销毁。
//此变量是一个内置指针,而不是一个智能指针。
与类类型不同,内置类型的对象被销毁时什么也不会发生。
特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。
如果这个指针指向的是动态内存,那么内存将不会被自动释放。
由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。
¶
p是指向factory分配的内存的唯一指针。
一旦use_factory返回,程序就没有办法释放这块内存了。
根据整个程序的逻辑,修正这个错误的正确方法是在use_factory中记得释放内存:
void use_factory(T arg)
{
Foo *p = factory(arg);
// use p
delete p; // remember to free the memory now that we no longer need it
}
还有一种可能,我们的系统中的其他代码要使用use_factory所分配的对象,
我们就应该修改此函数,让它返回一个指针,指向它分配的内存:
Foo* use_factory(T arg)
{
Foo *p = factory(arg);
// use p
return p; // caller must delete the memory
}
动态内存的管理非常容易出错¶
使用new和delete管理动态内存存在三个常见问题
1.忘记delete内存。
忘记释放动态内存会导致人们常说的“内存泄漏”问题,
因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,
因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。
2.使用已经释放掉的对象。
通过在释放内存后将指针置为空,有时可以检测出这种错误。
3.同一块内存释放两次。
当有两个指针指向相同的动态分配对象时,可能发生这种错误。
如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。
如果我们随后又delete第二个指针,自由空间就可能被破坏。
相对于查找和修正这些错误来说,制造出这些错误要简单得多。
坚持只使用智能指针,就可以避免所有这些问题。
对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。
delete之后重置指针值¶
当我们delete一个指针后,指针值就变为无效了。
虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。
在delete之后,指针就变成了人们所说的空悬指针(danglingpointer),
- 即,指向一块曾经保存数据对象但现在已经无效的内存的指针。
未初始化指针的所有缺点空悬指针也都有。
有一种方法可以避免空悬指针的问题:
- 在指针即将要离开其作用域之前释放掉它所关联的内存。
这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。
如果我们需要保留指针,可以在delete之后将nullptr赋予指针,
这样就清楚地指出指针不指向任何对象。
这只是提供了有限的保护¶
动态内存的一个基本问题是可能有多个指针指向相同的内存。
在delete内存之后重置指针的方法只对这个指针有效,
对其他任何仍指向(已释放的)内存的指针是没有作用的。例如:
int *p(new int(42)); // p points to dynamic memory
auto q = p; // p and q point to the same memory
delete p; // invalidates both p and q
p = nullptr; // indicates that p is no longer bound to an object
//本例中p和q指向相同的动态分配的对象。
//我们delete此内存,然后将p置为nullptr,指出它不再指向任何对象。
//但是,重置p对q没有任何作用,
//在我们释放p所指向的(同时也是q所指向的!)内存时,q也变为无效了。
//在实际系统中,查找指向相同内存的所有指针是异常困难的。
练习¶
编写函数,返回一个动态分配的 int 的vector。将此vector 传递给另一个函数,
这个函数读取标准输入,将读入的值保存在 vector 元素中。
再将vector传递给另一个函数,打印读入的值。记得在恰当的时刻delete vector。
#include <iostream>
#include <vector>
using std::vector;
vector<int>* alloc_vector()
{
return new vector<int>();
}
练习¶
void assign_vector(vector<int>* p)
{
int i;
while (std::cin >> i)
{
p->push_back(i);
}
}
void print_vector(vector<int>* p)
{
for (auto i : *p)
{
std::cout << i << std::endl;
}
}
练习¶
int main()
{
auto p = alloc_vector();
assign_vector(p);
print_vector(p);
delete p;
return 0;
}
练习¶
重做上一题,这次使用 shared_ptr 而不是内置指针。
#include <iostream>
#include <vector>
#include <memory>
using std::vector;
std::shared_ptr<vector<int>> alloc_vector()
{
return std::make_shared<vector<int>>();
}
void assign_vector(std::shared_ptr<vector<int>> p)
{
int i;
while (std::cin >> i)
{
p->push_back(i);
}
}
练习¶
void print_vector(std::shared_ptr<vector<int>> p)
{
for (auto i : *p)
{
std::cout << i << std::endl;
}
}
int main()
{
auto p = alloc_vector();
assign_vector(p);
print_vector(p);
return 0;
}
练习¶
下面的函数是否有错误?如果有,解释错误原因。
bool b() {
int* p = new int;
// ...
return p;
}
有错误。p会被强制转换成bool,继而没有释放指针 p 指向的对象。
练习¶
解释下面代码执行的结果。
int *q = new int(42), *r = new int(100);
r = q;
auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);
r2 = q2;
r 和 q 指向 42,
而之前 r 指向的 100 的内存空间并没有被释放,因此会发生内存泄漏。
r2 和 q2 都是智能指针,当对象空间不被引用的时候会自动释放。
定义和改变shared_ptr的其他方法¶
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*类型。p将使用可调用对象d来代替delete。
shared_ptr<T> p(p2, d) p是shared_ptr p2的拷贝,
唯一的区别是p将可调用对象d来代替delete。
p.reset() 若p是唯一指向其对象的shared_ptr,reset会释放此对象。
若传递了可选的参数内置指针q,会令p指向q,否则会将p置空。
若还传递了参数d,则会调用d而不是delete来释放q。
p.reset(q) 同上
p.reset(q, d) 同上
shared_ptr和new结合使用¶
如果我们不初始化一个智能指针,它就会被初始化为一个空指针。
我们还可以用new返回的指针来初始化智能指针:
shared_ptr<double> p1; // shared_ptr that can point at a double
shared_ptr<int> p2(new int(42)); // p2 points to an int with value 42
接受指针参数的智能指针构造函数是explicit的。
因此,我们不能将一个内置指针隐式转换为一个智能指针,
必须使用直接初始化形式来初始化一个智能指针:
shared_ptr<int> p1 = new int(1024); // error: must use direct initialization
shared_ptr<int> p2(new int(1024)); // ok: uses direct initialization
//p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr。
//由于我们不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。
shared_ptr和new结合使用¶
出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:
shared_ptr<int> clone(int p) {
return new int(p); // error: implicit conversion to shared_ptr<int>
}
我们必须将shared_ptr显式绑定到一个想要返回的指针上:
shared_ptr<int> clone(int p) {
// ok: explicitly create a shared_ptr<int> from int*
return shared_ptr<int>(new int(p));
}
shared_ptr和new结合使用¶
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,
因为智能指针默认使用delete释放它所关联的对象。
我们可以将智能指针绑定到一个指向其他类型的资源的指针上,
但是为了这样做,必须提供自己的操作来替代delete。
我们将后续介绍如何定义自己的释放操作。
不要混合使用普通指针和智能指针¶
shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。
这也是为什么我们推荐使用make_shared而不是new的原因。
这样,我们就能在分配对象的同时就将shared_ptr与之绑定,
从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。
不要混合使用普通指针和智能指针¶
下面对shared_ptr进行操作的函数:
// ptr is created and initialized when process is called
void process(shared_ptr<int> ptr)
{
// use ptr
} // ptr goes out of scope and is destroyed
//process的参数是传值方式传递的,因此实参会被拷贝到ptr中。
//拷贝一个shared_ptr会递增其引用计数,
//因此,在process运行过程中,引用计数值至少为2。
//当process结束时,ptr的引用计数会递减,但不会变为0。
//因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放。
使用此函数的正确方法是传递给它一个shared_ptr:
shared_ptr<int> p(new int(42)); // reference count is 1
process(p); // copying p increments its count;
//in process the reference count is 2
int i = *p; // ok: reference count is 1
不要混合使用普通指针和智能指针¶
这样做很可能会导致错误:
int *x(new int(1024)); // dangerous: x is a plain pointer, not a smart pointer
process(x); // error: cannot convert int* to shared_ptr<int>
process(shared_ptr<int>(x)); // legal, but the memory will be deleted!
int j = *x; // undefined: x is a dangling pointer!
//将一个临时shared_ptr传递给process。
//当这个调用所在的表达式结束时,这个临时对象就被销毁了。
//销毁这个临时变量会递减引用计数,此时引用计数就变为0了。
//因此,当临时对象被销毁时,它所指向的内存会被释放。
//但x继续指向(已经释放的)内存,从而变成一个空悬指针。
//如果试图使用x的值,其行为是未定义的。
当将一个shared_ptr绑定到一个普通指针时,
我们就将内存的管理责任交给了这个shared_ptr。
一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,
因为我们无法知道对象何时会被销毁。
不要使用get初始化另一个智能指针或为智能指针赋值¶
智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。
此函数是为了这样一种情况而设计的:
- 我们需要向不能使用智能指针的代码传递一个内置指针。
- 使用get返回的指针的代码不能delete此指针。
虽然编译器不会给出错误信息,但将另一个智能指针也绑定到get返回的指针上是错误的:
shared_ptr<int> p(new int(42)); // reference count is 1
int *q = p.get(); // ok: but don't use q in any way that might delete its pointer
{ // new block
// undefined: two independent shared_ptrs point to the same memory
shared_ptr<int>(q);
} // block ends, q is destroyed, and the memory to which q points is freed
int foo = *p; // undefined; the memory to which p points was freed
//p和q指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。
//当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放。
//从而p变成一个空悬指针,意味着当我们试图使用p时,将发生未定义的行为。
//而且,当p被销毁时,这块内存会被第二次delete。
get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,
才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。
其他shared_ptr操作¶
可以用reset来将一个新的指针赋予一个shared_ptr
p = new int(1024); // error: cannot assign a pointer to a shared_ptr
p.reset(new int(1024)); // ok: p points to a new object
与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。
reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。
在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。
如果不是,在改变之前要制作一份新的拷贝:
if (!p.unique())
p.reset(new string(*p)); // we aren't alone; allocate a new copy
*p += newVal; // now that we know we're the only pointer, okay to change this object
练习¶
下面的代码调用了process 函数,解释此调用是否正确。如果不正确,应如何修改?
shared_ptr<int> p(new int(42));
process(shared_ptr<int>(p));
正确。shared_ptr<int>(p) 会创建一个临时的智能指针,
这个智能指针与 p 引用同一个对象,此时引用计数为 2。
当表达式结束时,临时的智能指针被销毁,此时引用计数为 1。
练习¶
如果我们像下面这样调用 process,会发生什么?
process(shared_ptr<int>(p.get()));
这样会创建一个新的智能指针,它的引用计数为 1,
这个智能指针所指向的空间与 p 相同。
在表达式结束后,这个临时智能指针会被销毁,引用计数为 0,
所指向的内存空间也会被释放。
而导致 p 所指向的空间被释放,使得 p 成为一个空悬指针。
练习¶
p 和 sp 的定义如下,对于接下来的对 process 的每个调用,
如果合法,解释它做了什么,如果不合法,解释错误原因:
auto p = new int();
auto sp = make_shared<int>();
(a) process(sp);
(b) process(new int());
(c) process(p);
(d) process(shared_ptr<int>(p));
(a) 合法。将sp 拷贝给 process函数的形参,
在函数里面引用计数为 2,函数结束后引用计数为 1。
(b) 不合法。不能从内置指针隐式转换为智能指针。
(c) 不合法。不能从内置指针隐式转换为智能指针。
(d) 合法。但是智能指针和内置指针一起使用可能会出现问题,
在表达式结束后智能指针会被销毁,它所指向的对象也被释放。
而此时内置指针 p 依旧指向该内存空间。
之后对内置指针 p 的操作可能会引发错误。
练习¶
如果执行下面的代码,会发生什么?
auto sp = make_shared<int>();
auto p = sp.get();
delete p;
智能指针 sp 所指向空间已经被释放,再对 sp 进行操作会出现错误。
智能指针和异常¶
程序需要确保在异常发生后资源能被正确地释放。
一个简单的确保资源被释放的方法是使用智能指针。
如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:
void f()
{
shared_ptr<int> sp(new int(42)); // allocate a new object
// code that throws an exception that is not caught inside f
} // shared_ptr freed automatically when the function ends
//函数的退出有两种可能,正常处理结束或者发生了异常,
//无论哪种情况,局部对象都会被销毁。
//在上面的程序中,sp是一个shared_ptr,因此sp销毁时会检查引用计数。
//在此例中,sp是指向这块内存的唯一指针,因此内存会被释放掉。
智能指针和异常¶
当发生异常时,我们直接管理的内存是不会自动释放的。
如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放:
void f()
{
int *ip = new int(42); // dynamically allocate a new object
// code that throws an exception that is not caught inside f
delete ip; // free the memory before exiting
}
//如果在new和delete之间发生异常,且异常未在f中被捕获,则内存就永远不会被释放了。
//在函数f之外没有指针指向这块内存,因此就无法释放它了。
智能指针和哑类¶
包括所有标准库类在内的很多C++类都定义了析构函数
- 负责清理对象使用的资源。
但是,不是所有的类都是这样良好定义的。
特别是那些为C和C++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。
那些分配了资源,而又没有定义析构函数来释放这些资源的类,
可能会遇到与使用动态内存相同的错误——程序员非常容易忘记释放资源。
类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。
智能指针和哑类¶
与管理动态内存类似,可以使用类似的技术来管理不具有良好定义的析构函数的类。
struct destination; // represents what we are connecting to
struct connection; // information needed to use the connection
connection connect(destination*); // open the connection
void disconnect(connection); // close the given connection
void f(destination &d /* other parameters */)
{
// get a connection; must remember to close it when done
connection c = connect(&d);
// use the connection
// if we forget to call disconnect before exiting f, there will be no way to close c
}
如果connection有一个析构函数,就可以在f结束时由析构函数自动关闭连接。
但是,connection没有析构函数。
这个问题与我们上一个程序中使用shared_ptr避免内存泄漏几乎是等价的。
使用shared_ptr来保证connection被正确关闭,已被证明是一种有效的方法。
使用我们自己的释放操作¶
当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。
为了用shared_ptr来管理一个connection,我们必须首先定义一个函数来代替delete。
这个删除器(deleter)函数必须能够完成对shared_ptr中保存的指针进行释放的操作。
void end_connection(connection *p) { disconnect(*p); }
当我们创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数
void f(destination &d /* other parameters */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// use the connection
// when f exits, even if by an exception, the connection will be properly closed
}
//当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection。
//接下来,end_connection会调用disconnect,从而确保连接被关闭。
//如果f正常退出,那么p的销毁会作为结束处理的一部分。
//如果发生了异常,p同样会被销毁,从而连接被关闭。
注意:智能指针陷阱¶
智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。
为了正确使用智能指针,我们必须坚持一些基本规范:
- 不使用相同的内置指针值初始化(或reset)多个智能指针。·
- 不delete get()返回的指针。·
- 不使用get()初始化或reset另一个智能指针。·
- 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,
你的指针就变为无效了。·
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
练习¶
编写你自己版本的用 shared_ptr 管理 connection 的函数。
#include <iostream>
#include <memory>
#include <string>
struct connection
{
std::string ip;
int port;
connection(std::string i, int p) : ip(i), port(p) {}
};
struct destination
{
std::string ip;
int port;
destination(std::string i, int p) : ip(i), port(p) {}
};
练习¶
connection connect(destination* pDest)
{
std::shared_ptr<connection>
pConn(new connection(pDest->ip, pDest->port));
std::cout << "creating connection("
<< pConn.use_count() << ")" << std::endl;
return *pConn;
}
void disconnect(connection pConn)
{
std::cout << "connection close(" << pConn.ip << ":"
<< pConn.port << ")" << std::endl;
}
void end_connection(connection* pConn)
{
disconnect(*pConn);
}
练习¶
void f(destination &d)
{
connection conn = connect(&d);
std::shared_ptr<connection> p(&conn, end_connection);
std::cout << "connecting now(" << p.use_count() << ")" << std::endl;
}
int main()
{
destination dest("220.181.111.111", 10086);
f(dest);
return 0;
}
练习¶
重写上一题的程序,用 lambda 代替end_connection 函数。
#include <iostream>
#include <memory>
#include <string>
struct connection
{
std::string ip;
int port;
connection(std::string i, int p) : ip(i), port(p) {}
};
struct destination
{
std::string ip;
int port;
destination(std::string i, int p) : ip(i), port(p) {}
};
练习¶
connection connect(destination* pDest)
{
std::shared_ptr<connection>
pConn(new connection(pDest->ip, pDest->port));
std::cout << "creating connection("
<< pConn.use_count() << ")" << std::endl;
return *pConn;
}
void disconnect(connection pConn)
{
std::cout << "connection close("
<< pConn.ip << ":" << pConn.port << ")" << std::endl;
}
练习¶
void f(destination &d)
{
connection conn = connect(&d);
std::shared_ptr<connection>
p(&conn, [] (connection* p){ disconnect(*p); });
std::cout << "connecting now(" << p.use_count() << ")" << std::endl;
}
int main()
{
destination dest("220.181.111.111", 10086);
f(dest);
return 0;
}
unique_ptr¶
一个unique_ptr“拥有”它所指向的对象。
与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。
当unique_ptr被销毁时,它所指向的对象也被销毁。
当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。
unique_ptr<double> p1; // unique_ptr that can point at a double
unique_ptr<int> p2(new int(42)); // p2 points to int with value 42
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1); // error: no copy for unique_ptr
unique_ptr<string> p3;
p3 = p2; // error: no assign for unique_ptr
unique_ptr操作¶
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置为空。
u.release() u放弃对指针的控制权,返回指针,并将u置空。
u.reset() 释放u指向的对象
u.reset(q) 令u指向q指向的对象
u.reset(nullptr) 将u置空
unique_ptr¶
虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将
指针的所有权从一个(非const)unique_ptr转移给另一个unique:
// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Trex"));
// transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed
//release成员返回unique_ptr当前保存的指针并将其置为空。
//因此,p2被初始化为p1原来保存的指针,而p1被置为空。
reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。
如果unique_ptr不为空,它原来指向的对象被释放。
因此,对p2调用reset释放了用"Stegosaurus"初始化的string所使用的内存,
将p3对指针的所有权转移给p2,并将p3置为空。
unique_ptr¶
调用release会切断unique_ptr和它原来管理的对象间的联系。
release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。
在本例中,管理内存的责任简单地从一个智能指针转移给另一个。
如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
p2.release(); // WRONG: p2 won't free the memory and we've lost the pointer
auto p = p2.release(); // ok, but we must remember to delete(p)
传递unique_ptr参数和返回unique_ptr¶
不能拷贝unique_ptr的规则有一个例外:
我们可以拷贝或赋值一个将要被销毁的unique_ptr。
最常见的例子是从函数返回一个unique_ptr:
unique_ptr<int> clone(int p) {
// ok: explicitly create a unique_ptr<int> from int*
return unique_ptr<int>(new int(p));
}
还可以返回一个局部对象的拷贝:
unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int (p));
// . . .
return ret;
}
//对于两段代码,编译器都知道要返回的对象将要被销毁。
//在此情况下,编译器执行一种特殊的“拷贝”
向后兼容:auto_ptr¶
标准库的较早版本包含了一个名为auto_ptr的类,
它具有unique_ptr的部分特性,但不是全部。
特别是,我们不能在容器中保存auto_ptr,也不能从函数中返回auto_ptr。
虽然auto_ptr仍是标准库的一部分,但编写程序时应该使用unique_ptr。
向unique_ptr传递删除器¶
unique_ptr默认情况下用delete释放它指向的对象。
与shared_ptr一样,我们可以重载一个unique_ptr中默认的删除器
但是,unique_ptr管理删除器的方式与shared_ptr不同
重载一个unique_ptr中的删除器会影响到unique_ptr类型以及
如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,
我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。
在创建或reset一个这种unique_ptr类型的对象时,
必须提供一个指定类型的可调用对象(删除器):
// p points to an object of type objT
//and uses an object of type delT to free that object
// it will call an object named fcn of type delT
unique_ptr<objT, delT> p (new objT, fcn);
重写连接程序,用unique_ptr来代替shared_ptr¶
void f(destination &d /* other needed parameters */)
{
connection c = connect(&d); // open the connection
// when p is destroyed, the connection will be closed
unique_ptr<connection, decltype(end_connection)*>
p(&c, end_connection);
// use the connection
// when f exits, even if by an exception,
//the connection will be properly closed
}
//在本例中我们使用了decltype来指明函数指针类型。
//由于decltype(end_connection)返回一个函数类型,
//所以我们必须添加一个*来指出我们正在使用该类型的一个指针
练习¶
如果你试图拷贝或赋值 unique_ptr,编译器并不总是能给出易于理解的错误信息。
编写包含这种错误的程序,观察编译器如何诊断这种错误。
#include <iostream>
#include <string>
#include <memory>
using std::string; using std::unique_ptr;
int main()
{
unique_ptr<string> p1(new string("pezy"));
// unique_ptr<string> p2(p1); // copy
// ^
// Error: Call to implicitly-deleted copy constructor of 'unique_ptr<string>'
//
// unique_ptr<string> p3 = p1; // assign
// ^
// Error: Call to implicitly-deleted copy constructor of 'unique_ptr<string>'
std::cout << *p1 << std::endl;
p1.reset(nullptr);
}
练习¶
下面的 unique_ptr 声明中,哪些是合法的,哪些可能导致后续的程序错误?
解释每个错误的问题在哪里。
int ix = 1024, *pi = &ix, *pi2 = new int(2048);
typedef unique_ptr<int> IntP;
(a) IntP p0(ix);
(b) IntP p1(pi);
(c) IntP p2(pi2);
(d) IntP p3(&ix);
(e) IntP p4(new int(2048));
(f) IntP p5(p2.get());
(a) 不合法。在定义一个 unique_ptr 时,需要将其绑定到一个new 返回的指针上。
(b) 不合法。理由同上。
(c) 合法。但是也可能会使得 pi2 成为空悬指针。
(d) 不合法。当 p3 被销毁时,它试图释放一个栈空间的对象。
(e) 合法。
(f) 不合法。p5 和 p2 指向同一个对象,当 p5 和 p2 被销毁时,会使得同一个指针被释放两次。
练习¶
shared_ptr 为什么没有 release 成员?
release 成员的作用是放弃控制权并返回指针,
因为在某一时刻只能有一个 unique_ptr 指向某个对象,unique_ptr 不能被赋值,
所以要使用 release 成员将一个 unique_ptr 的指针的所有权传递给另一个 unique_ptr。
而 shared_ptr 允许有多个 shared_ptr 指向同一个对象,因此不需要 release 成员。
weak_ptr¶
weak_ptr是一种不控制所指向对象生存期的智能指针,
它指向由一个shared_ptr管理的对象。
将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。
即使有weak_ptr指向对象,对象也还是会被释放,
因此,weak_ptr的名字抓住了这种智能指针“弱”共享对象的特点
当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp weakly shares with p; use count in p is unchanged
//本例中wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数;
//wp指向的对象可能被释放掉。
weak_ptr操作¶
weak_ptr<T> w 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp) 与shared_ptr指向相同对象的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,返回true,否则返回false
w.lock() 如果expired为true,则返回一个空shared_ptr;
否则返回一个指向w的对象的shared_ptr。
¶
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。
此函数检查weak_ptr指向的对象是否仍存在。
如果存在,lock返回一个指向共享对象的shared_ptr。
与任何其他shared_ptr类似,只要此shared_ptr存在,
它所指向的底层对象也就会一直存在。例如:
if (shared_ptr<int> np = wp.lock()) { // true if np is not null
// inside the if, np shares its object with p
}
//在这段代码中,只有当lock调用返回true时我们才会进入if语句体。
//在if中,使用np访问共享对象是安全的。
核查指针类¶
weak_ptr用途的一个展示
// StrBlobPtr throws an exception on attempts to access a nonexistent element
class StrBlobPtr {
public:
StrBlobPtr(): curr(0) { }
StrBlobPtr(StrBlob &a, size_t sz = 0):
wptr(a.data), curr(sz) { }
std::string& deref() const;
StrBlobPtr& incr(); // prefix version
private:
// check returns a shared_ptr to the vector if the check succeeds
std::shared_ptr<std::vector<std::string>>
check(std::size_t, const std::string&) const;
// store a weak_ptr, which means the underlying vector might be destroyed
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr; // current position within the array
};
核查指针类¶
std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i, const std::string &msg) const
{
auto ret = wptr.lock(); // is the vector still around?
if (!ret)
throw std::runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw std::out_of_range(msg);
return ret; // otherwise, return a shared_ptr to the vector
}
指针操作¶
定义名为deref和incr的函数,分别用来解引用和递增StrBlobPtr。
std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) is the vector to which this object points
}
// prefix: return a reference to the incremented object
StrBlobPtr& StrBlobPtr::incr()
{
// if curr already points past the end of the container, can't increment it
check(curr, "increment past end of StrBlobPtr");
++curr; // advance the current state
return *this;
}
¶
为StrBlob类定义begin和end操作,返回一个指向它自身的StrBlobPtr
// forward declaration needed for friend declaration in StrBlob
class StrBlobPtr;
class StrBlob {
friend class StrBlobPtr;
// other members ...
// return StrBlobPtr to the first and one past the last elements
StrBlobPtr begin() { return StrBlobPtr(*this); }
StrBlobPtr end()
{ auto ret = StrBlobPtr(*this, data->size());
return ret; }
};
练习¶
定义你自己版本的 StrBlobPtr,更新 StrBlob 类,加入恰当的 friend 声明以及 begin 和 end 成员。
#include <string>
#include <vector>
#include <initializer_list>
#include <memory>
#include <stdexcept>
using std::vector; using std::string;
class StrBlobPtr;
class StrBlob
{
public:
using size_type = vector<string>::size_type;
friend class StrBlobPtr;
StrBlobPtr begin();
StrBlobPtr end();
练习¶
StrBlob() : data(std::make_shared<vector<string>>()) {}
StrBlob(std::initializer_list<string> il) : data(std::make_shared<vector<string>>(il)) {}
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const string& s) { data->push_back(s); }
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
{
check(0, "front on empty StrBlob");
return data->front();
}
const std::string& back() const
{
check(0, "back on empty StrBlob");
return data->back();
}
练习¶
private:
void check(size_type i, const string& msg) const
{
if (i >= data->size())
throw std::out_of_range(msg);
}
private:
std::shared_ptr<vector<string>> data;
};
练习¶
class StrBlobPtr
{
public:
StrBlobPtr() :curr(0) {}
StrBlobPtr(StrBlob &a, size_t sz = 0) :wptr(a.data), curr(sz) {}
bool operator!=(const StrBlobPtr& p) { return p.curr != curr; }
string& deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
StrBlobPtr& incr()
{
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
练习¶
private:
std::shared_ptr<vector<string>> check(size_t i, const string &msg) const
{
auto ret = wptr.lock();
if (!ret) throw std::runtime_error("unbound StrBlobPtr");
if (i >= ret->size()) throw std::out_of_range(msg);
return ret;
}
std::weak_ptr<vector<string>> wptr;
size_t curr;
};
StrBlobPtr StrBlob::begin()
{
return StrBlobPtr(*this);
}
StrBlobPtr StrBlob::end()
{
return StrBlobPtr(*this, data->size());
}
练习¶
编写程序,逐行读入一个输入文件,将内容存入一个 StrBlob 中,
用一个 StrBlobPtr 打印出 StrBlob 中的每个元素。
#include <iostream>
#include <fstream>
#include "StrBlob.h"
using namespace std;
int main(){
ifstream ifs("books.txt");
StrBlob sb;
string s;
while (getline(ifs, s)){
sb.push_back(s);
}
for (StrBlobPtr sbp = sb.begin(); sbp != sb.end(); sbp.incr()){
cout << sbp.deref() << endl;
}
return 0;
}
练习¶
也可以这样编写 StrBlobPtr 的 deref 成员:
std::string& deref() const {
return (*check(curr, "dereference past end"))[curr];
}
你认为哪个版本更好?为什么?
原来的版本更好,可读性更高。
练习¶
为了能让 StrBlobPtr 使用 const StrBlob,你觉得应该如何修改?
定义一个名为ConstStrBlobPtr 的类,使其能够指向 const StrBlob。
构造函数改为接受 const Strblob &
然后给 Strblob 类添加两个 const 成员函数 cbegin 和 cend,返回 ConstStrBlobPtr
动态数组¶
C++语言和标准库提供了两种一次分配一个对象数组的方法。
另一种new表达式语法,可以分配并初始化一个对象数组。
标准库中包含一个名为allocator的类,允许我们将分配和初始化分离。
使用allocator通常会提供更好的性能和更灵活的内存管理能力
当一个应用需要可变数量的对象时,
- 使用vector(或其他标准库容器)更简单、更快速并且更安全
大多数应用应该使用标准库容器而不是动态分配的数组。
使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。
使用容器的类可以使用默认版本的拷贝、赋值和析构操作。
分配动态数组的类则必须定义自己版本的操作,在拷贝复制以及销毁对象时管理所关联的内存。
new和数组¶
为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,
在其中指明要分配的对象的数目。(假定分配成功后)返回指向第一个对象的指针:
// call get_size to determine how many ints to allocate
int *pia = new int[get_size()]; // pia points to the first of these ints
方括号中的大小必须是整型,但不必是常量。
可以用一个表示数组类型的类型别名来分配一个数组,new表达式中就不需要方括号了:
typedef int arrT[42]; // arrT names the type array of 42 ints
int *p = new arrT; // allocates an array of 42 ints; p points to the first one
//int *p = new int[42];
分配一个数组会得到一个元素类型的指针¶
当用new分配一个数组时到一个数组元素类型的指针。
由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。
这些函数使用数组维度来返回指向首元素和尾后元素的指针。
出于相同的原因,也不能用范围for语句来处理(所谓的)动态数组中的元素。
初始化动态分配对象的数组¶
默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。
可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。
int *pia = new int[10]; //block of ten uninitialized ints
int *pia2 = new int[10](); //block of ten ints value initialized to 0
string *psa = new string[10]; //block of ten empty strings
string *psa2 = new string[10](); //block of ten empty strings
在新标准中,我们还可以提供一个元素初始化器的花括号列表:
// block of ten ints each initialized from the corresponding initializer
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
// block of ten strings; the first four are initialized from the given initializers
// remaining elements are value initialized
string *psa3 = new string[10]{"a","an",string(3,'x')};
虽然我们用空括号对数组中元素进行值初始化,
但不能在括号中给出初始化器,这意味着不能用auto分配数组
动态分配一个空数组是合法的¶
可以用任意表达式来确定要分配的对象的数目:
size_t n = get_size(); // get_size returns the number of elements needed
int* p = new int[n]; // allocate an array to hold the elements
for (int* q = p; q != p + n; ++q)
/* process the array */ ;
char arr[0]; // error: cannot define a zero-length array
char *cp = new char[0]; // ok: but cp can't be dereferenced
释放动态数组¶
为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对:
delete p; // p must point to a dynamically allocated object or be null
delete [] pa; // pa must point to a dynamically allocated array or be null
//第二条语句销毁pa指向的数组中的元素,并释放对应的内存。
//数组中的元素按逆序销毁
当我们释放一个指向数组的指针时,空方括号对是必需的:
它指示编译器此指针指向一个对象数组的第一个元素。
如果我们在delete一个指向数组的指针时忽略了方括号
(或者在delete一个指向单一对象的指针时使用了方括号),其行为是未定义的。
typedef int arrT[42]; // arrT names the type array of 42 ints
int *p = new arrT; // allocates an array of 42 ints; p points to the first one
delete [] p; // brackets are necessary because we allocated an array
如果我们在delete一个数组指针时忘记了方括号,或者在delete一个单一对象的指针
时使用了方括号,编译器很可能不会给出警告。我们的程序可能在执行过程中在没有
任何警告的情况下行为异常。
智能指针和动态数组¶
标准库提供了一个可以管理new分配的数组的unique_ptr版本。
为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:
// up points to an array of ten uninitialized ints
unique_ptr<int[]> up(new int[10]);
up.release(); // automatically uses delete[] to destroy its pointer
//由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete[]。
当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符。
- 毕竟unique_ptr指向的是一个数组而不是单个对象,因此这些运算符是无意义的。
当一个unique_ptr指向一个数组时,我们可以使用下标运算符来访问数组中的元素:
for (size_t i = 0; i != 10; ++i)
up[i] = i; // assign a new value to each of the elements
指向数组的unique_ptr¶
- 指向数组的unique_ptr不支持成员访问运算符(点和箭头)。
unique_ptr<T[]> u u可以指向一个动态分配的数组,整数元素类型为T
unique_ptr<T[]> u(p) u指向内置指针p所指向的动态分配的数组。
p必须能转换为类型T*。
u[i] 返回u拥有的数组中位置i处的对象。u必须指向一个数组。
智能指针和动态数组¶
与unique_ptr不同,shared_ptr不直接支持管理动态数组。
如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // uses the lambda we supplied that uses delete[] to free the array
//如果未提供删除器,这段代码将是未定义的。
//默认情况下,shared_ptr使用delete销毁它指向的对象。
//如果此对象是一个动态数组,对其使用delete所产生的问题
//与释放一个动态数组指针时忘记[]产生的问题一样
shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素:
// shared_ptrs don't have subscript operator and don't support pointer arithmetic
for (size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i; // use get to get a built-in pointer
//shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。
//为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。
练习¶
编写一个程序,连接两个字符串字面常量,将结果保存在一个动态分配的char数组中。
重写这个程序,连接两个标准库string对象。
#include <iostream>
#include <string>
#include <cstring>
#include <memory>
using namespace std;
int main() {
const char *c1 = "Hello ";
const char *c2 = "World";
unsigned len = strlen(c1) + strlen(c2) + 1;
char *r = new char[len]();
strcat(r, c1);
strcat(r, c2);
std::cout << r << std::endl;
std::string s1 = "Hello ";
std::string s2 = "World";
strcpy(r, (s1 + s2).c_str());
std::cout << r << std::endl;
delete[] r;
return 0;
}
练习¶
编写一个程序,从标准输入读取一个字符串,存入一个动态分配的字符数组中。
描述你的程序如何处理变长输入。测试你的程序,输入一个超出你分配的数组长度的字符串。
#include <iostream>
int main()
{
std::cout << "How long do you want the string? ";
int size{ 0 };
std::cin >> size;
char *input = new char[size + 1]();
std::cin.ignore();
std::cout << "input the string: ";
std::cin.get(input, size + 1);
std::cout << input;
delete[] input;
return 0;
}
练习¶
给定下面的new表达式,你应该如何释放pa?
int *pa = new int[10];
delete [] pa;
allocator类¶
new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。
当分配一大块内存时,我们通常计划在这块内存上按需构造对象。
在此情况下,我们希望将内存分配和对象构造分离。
这意味着我们可以分配大块内存,
但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。
//将内存分配和对象构造组合在一起可能会导致不必要的浪费。
string *const p = new string[n]; // construct n empty strings
string s;
string *q = p; // q points to the first string
while (cin >> s && q != p + n)
*q++ = s; // assign a new value to *q
const size_t size = q - p; // remember how many strings we read
// use the array
delete[] p; // p points to an array; must remember to use delete[]
//new表达式分配并初始化了n个string。但是,我们可能不需要n个string,
//少量string可能就足够了。这样,我们就可能创建了一些永远也用不到的对象。
//而且,对于那些确实要使用的对象,我们也在初始化之后立即赋予了它们新值。
//每个使用到的元素都被赋值了两次:第一次是在默认初始化时,随后是在赋值时。
//更重要的是,那些没有默认构造函数的类就不能动态分配数组了。
allocator类¶
标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。
它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
类似vector,allocator是一个模板。
为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。
当一个allocator对象分配内存时,它会根据给定的对象类型来
确定恰当的内存大小和对齐位置:
allocator<string> alloc; // object that can allocate strings
auto const p = alloc.allocate(n); // allocate n unconstructed strings
//这个allocate调用为n个string分配了内存。
标准库allocator类及其算法¶
allocator<T> a 定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存
a.allocate(n) 分配一段原始的、未构造的内存,保存n个类型为T的对象。
a.deallocate(p, n) 释放从T*指针p中地址开始的内存,
这块内存保存了n个类型为T的对象;
p必须是一个先前由allocate返回的指针。
且n必须是p创建时所要求的大小。
在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy。
a.construct(p, args) p必须是一个类型是T*的指针,指向一块原始内存;
args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象。
a.destroy(p) p为T*类型的指针,此算法对p指向的对象执行析构函数。
allocator分配未构造的内存¶
allocator分配的内存是未构造的(unconstructed)。我们按需要在此内存中构造对象。
在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,
在给定位置构造一个元素。额外参数用来初始化构造的对象。
类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器:
auto q = p; // q will point to one past the last constructed element
alloc.construct(q++); // *q is the empty string
alloc.construct(q++, 10, 'c'); // *q is cccccccccc
alloc.construct(q++, "hi"); // *q is hi!
在早期版本的标准库中,construct只接受两个参数:
指向创建对象位置的指针和一个元素类型的值。
因此,我们只能将一个元素拷贝到未构造空间中,
而不能用元素类型的任何其他构造函数来构造一个元素。
allocator分配未构造的内存¶
还未构造对象的情况下就使用原始内存是错误的:
cout << *p << endl; // ok: uses the string output operator
cout << *q << endl; // disaster: q points to unconstructed memory!
为了使用allocate返回的内存,我们必须用construct构造对象。
使用未构造的内存,其行为是未定义的。
当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。
函数destroy接受一个指针,对指向的对象执行析构函数
while (q != p)
alloc.destroy(--q); // free allocated
allocator分配未构造的内存¶
一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,
也可以将其归还给系统。释放内存通过调用deallocate来完成:
alloc.deallocate(p, n);
allocator算法¶
标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象。
uninitialized_copy(b, e, b2) 从迭代器b和e给定的输入范围中拷贝元素到迭代器
b2指定的未构造的原始内存中。b2指向的内存必须足够大,
能够容纳输入序列中元素的拷贝。
uninitialized_copy_n(b, n, b2) 从迭代器b指向的元素开始,
拷贝n个元素到b2开始的内存中。
uninitialized_fill(b, e, t) 在迭代器b和e执行的原始内存范围中创建对象,
对象的值均为t的拷贝。
uninitialized_fill_n(b, n, t) 从迭代器b指向的内存地址开始创建n个对象。
b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。
¶
// allocate twice as many elements as vi holds
auto p = alloc.allocate(vi.size() * 2);
// construct elements starting at p as copies of elements in vi
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
// initialize the remaining elements to 42
uninitialized_fill_n(q, vi.size(), 42);
//一次uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。
¶
用 allocator 重写的程序。
#include <iostream>
#include <string>
#include <memory>
using namespace std;
int main(){
int n = 5;
allocator<string> alloc;
auto p = alloc.allocate(n);
string s;
auto q = p;
while (cin >> s && q != p + n){
alloc.construct(q++, s);
}
while (q != p){
std::cout << *--q << " ";
alloc.destroy(q);
}
alloc.deallocate(p, n);
return 0;
}
使用标准库:文本查询程序¶
实现一个简单的文本查询程序,作为标准库相关内容学习的总结。
我们的程序允许用户在一个给定文件中查询单词。
查询结果是单词在文件中出现的次数及其所在行的列表。
如果一个单词在一行中出现多次,此行只列出一次。行会按照升序输出
文本查询程序设计¶
开始一个程序的设计的一种好方法是列出程序的操作。
了解需要哪些操作会帮助我们分析出需要什么样的数据结构。
从需求入手,我们的文本查询程序需要完成如下任务:
当程序读取输入文件时
- 它必须记住单词出现的每一行。
- 因此,程序需要逐行读取输入文件,并将每一行分解为独立的单词
当程序生成输出时
- 它必须能提取每个单词所关联的行号
- 行号必须按升序出现且无重复
- 它必须能打印给定行号中的文本。
利用多种标准库设施¶
vector<string>来保存整个输入文件的一份拷贝。
- 输入文件中的每行保存为vector中的一个元素。
- 当需要打印一行时,可以用行号作为下标来提取行文本。
istringstream将每行分解为单词。
set来保存每个单词在输入文本中出现的行号。
- 这保证了每行只出现一次且行号按升序保存
map来将每个单词与它出现的行号set关联起来。
- 这样我们就可以方便地提取任意单词的set。
方案还使用了shared_ptr
数据结构¶
虽然我们可以用vector、set和map来直接编写文本查询程序,
但如果定义一个更为抽象的解决方案,会更为有效。
保存输入文件的类TextQuery,它包含一个vector和一个map。
- vector用来保存输入文件的文本,map用来关联每个单词和它出现的行号的set。
- 这个类将会有一个用来读取给定输入文件的构造函数和一个执行查询的操作。
- 查询操作要完成的任务非常简单:查找map成员,检查给定单词是否出现。
设计这个函数的难点是确定应该返回什么内容。一旦找到了一个单词,我们需要
知道它出现了多少次、它出现的行号以及每行的文本。
- 返回所有这些内容的最简单的方法是定义另一个类,可以命名为QueryResult,
来保存查询结果。这个类会有一个print函数,完成结果打印工作。
在类之间共享数据¶
QueryResult类要表达查询的结果。这些结果包括与给定单词关联的行号的set和
这些行对应的文本。这些数据都保存在TextQuery类型的对象中。
QueryResult所需要的数据都保存在一个TextQuery对象中,我们就必须确定如何访问它们
- 不希望拷贝
- 通过返回指向TextQuery对象内部的迭代器(或指针),我们可以避免拷贝操作
- 对于QueryResult对象和对应的TextQuery对象的生存期应该同步这一观察结果,
其实已经暗示了问题的解决方案。
- 考虑到这两个类概念上“共享”了数据,
可以使用shared_ptr来反映数据结构中的这种共享关系。
使用TextQuery类¶
当我们设计一个类时,在真正实现成员之前先编写程序使用这个类,是一种非常有用的方法。
- 通过这种方法,可以看到类是否具有我们所需要的操作。
下面的程序使用了TextQuery和QueryResult类。这个函数接受一个指向要处理的
文件的ifstream,并与用户交互,打印给定单词的查询结果
void runQueries(ifstream &infile)
{
// infile is an ifstream that is the file we want to query
TextQuery tq(infile); // store the file and build the query map
// iterate with the user: prompt for a word to find and print results
while (true) {
cout << "enter word to look for, or q to quit: ";
string s;
// stop if we hit end-of-file on the input or if a 'q' is entered
if (!(cin >> s) || s == "q") break;
// run the query and print the results
print(cout, tq.query(s)) << endl;
}
}
练习¶
TextQuery 和 QueryResult 类只使用了我们已经介绍过的语言和标准库特性。
不要提前看后续章节内容,只用已经学到的知识对这两个类编写你自己的版本。
头文件:
#ifndef EX12_27_H
#define EX12_27_H
#include <fstream>
#include <memory>
#include <vector>
#include <string>
#include <map>
#include <set>
class QueryResult;
class TextQuery
{
public:
using line_no = std::vector<std::string>::size_type;
TextQuery(std::ifstream&);
QueryResult query(const std::string& s) const;
练习¶
private:
std::shared_ptr<std::vector<std::string>> file;
std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
};
class QueryResult
{
public:
friend std::ostream& print(std::ostream&, const QueryResult&);
QueryResult(std::string s,
std::shared_ptr<std::set<TextQuery::line_no>> p,
std::shared_ptr<std::vector<std::string>> f) :
sought(s), lines(p), file(f)
{}
private:
std::string sought;
std::shared_ptr<std::set<TextQuery::line_no>> lines;
std::shared_ptr<std::vector<std::string>> file;
};
std::ostream& print(std::ostream&, const QueryResult&);
#endif
练习¶
实现:
#include "ex_12_27.h"
#include <sstream>
#include <fstream>
#include <vector>
#include <string>
using namespace std;
TextQuery::TextQuery(ifstream& ifs) : file(new vector<string>)
{
string text;
while (getline(ifs, text))
{
file->push_back(text);
int n = file->size() - 1;
istringstream line(text);
string word;
练习¶
while (line >> word)
{
auto &lines = wm[word];
if (!lines)
lines.reset(new set<line_no>);
lines->insert(n);
}
}
}
QueryResult TextQuery::query(const string& s) const
{
static shared_ptr<set<line_no>> nodata(new set<line_no>);
auto loc = wm.find(s);
if (loc == wm.end())
return QueryResult(s, nodata, file);
else
return QueryResult(s, loc->second, file);
}
练习¶
std::ostream& print(std::ostream& os, const QueryResult& qr)
{
os << qr.sought << " occurs " << qr.lines->size() << " "
<< "time" << (qr.lines->size() > 1 ? "s" : "") << endl;
for (auto num : *qr.lines)
os << "\t(line " << num + 1 << ") " << *(qr.file->begin() + num) << endl;
return os;
}
练习¶
主函数:
#include <iostream>
#include <string>
#include <fstream>
#include "ex_12_27.h"
using namespace std;
void runQueries(ifstream& infile)
{
TextQuery tq(infile);
while (true)
{
cout << "enter word to look for, or q to quit: ";
string s;
if (!(cin >> s) || s == "q") break;
print(cout, tq.query(s)) << endl;
}
}
练习¶
int main()
{
ifstream ifs("storyDataFile.txt");
runQueries(ifs);
return 0;
}
练习¶
编写程序实现文本查询,不要定义类来管理数据。
你的程序应该接受一个文件,并与用户交互来查询单词。
使用vector、map 和 set 容器来保存来自文件的数据并生成查询结果。
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <memory>
using std::shared_ptr;
#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <set>
#include <algorithm>
练习¶
int main()
{
std::ifstream file("storyDataFile.txt");
vector<string> input;
std::map<string, std::set<decltype(input.size())>> dictionary;
decltype(input.size()) lineNo{ 0 };
for (string line; std::getline(file, line); ++lineNo)
{
input.push_back(line);
std::istringstream line_stream(line);
for (string text, word; line_stream >> text; word.clear())
{
std::remove_copy_if(text.begin(), text.end(),
std::back_inserter(word), ispunct);
dictionary[word].insert(lineNo);
}
}
练习¶
while (true)
{
std::cout << "enter word to look for, or q to quit: ";
string s;
if (!(std::cin >> s) || s == "q") break;
auto found = dictionary.find(s);
if (found != dictionary.end())
{
std::cout << s << " occurs " << found->second.size() <<
(found->second.size() > 1 ? " times" : " time") << std::endl;
for (auto i : found->second)
std::cout << "\t(line " << i + 1 << ") "
<< input.at(i) << std::endl;
}
else std::cout << s << " occurs 0 time" << std::endl;
}
}
练习¶
我们曾经用do while 循环来编写管理用户交互的循环。
用do while 重写本节程序,解释你倾向于哪个版本,为什么?
do {
std::cout << "enter word to look for, or q to quit: ";
string s;
if (!(std::cin >> s) || s == "q") break;
print(std::cout, tq.query(s)) << std::endl;
} while ( true );
更喜欢 while,这可能是习惯。
文本查询程序类的定义¶
我们以TextQuery类的定义开始。
用户创建此类的对象时会提供一个istream,用来读取输入文件。
还提供一个query操作,接受一个string,返回一个QueryResult表示string出现的那些行。
class QueryResult; // declaration needed for return type in the query function
class TextQuery {
public:
using line_no = std::vector<std::string>::size_type;
TextQuery(std::ifstream&);
QueryResult query(const std::string&) const;
private:
std::shared_ptr<std::vector<std::string>> file; // input file
// map of each word to the set of the lines in which that word appears
std::map<std::string,
std::shared_ptr<std::set<line_no>>> wm;
};
TextQuery构造函数¶
TextQuery的构造函数接受一个ifstream,逐行读取输入文件:
// read the input file and build the map of lines to line numbers
TextQuery::TextQuery(ifstream &is): file(new vector<string>)
{
string text;
while (getline(is, text)) { // for each line in the file
file->push_back(text); // remember this line of text
int n = file->size() - 1; // the current line number
istringstream line(text); // separate the line into words
string word;
while (line >> word) { // for each word in that line
// if word isn't already in wm, subscripting adds a new entry
auto &lines = wm[word]; // lines is a shared_ptr
if (!lines) // that pointer is null the first time we see word
lines.reset(new set<line_no>); // allocate a new set
lines->insert(n); // insert this line number
}
}
}
QueryResult类¶
QueryResult类有三个数据成员:
一个string,保存查询单词;
一个shared_ptr,指向保存输入文件的vector;
一个shared_ptr,指向保存单词出现行号的set。
它唯一的一个成员函数是一个构造函数,初始化这三个数据成员:
class QueryResult {
friend std::ostream& print(std::ostream&,const QueryResult&);
public:
QueryResult(std::string s,
std::shared_ptr<std::set<line_no>> p,
std::shared_ptr<std::vector<std::string>> f):
sought(s), lines(p), file(f) { }
private:
std::string sought; // word this query represents
std::shared_ptr<std::set<line_no>> lines; // lines it's on
std::shared_ptr<std::vector<std::string>> file; // input file
};
query函数¶
query函数接受一个string参数,即查询单词,query用它来在map中定位对应的行号set
- 如果找到了这个string,query函数构造一个QueryResult,
保存给定string、TextQuery的file成员以及从wm中提取的set。
- 给定string未找到,定义一个局部static对象,指向空的行号set的shared_ptr。
返回此对象的一个拷贝:
QueryResult
TextQuery::query(const string &sought) const
{
// we'll return a pointer to this set if we don't find sought
static shared_ptr<set<line_no>> nodata(new set<line_no>);
// use find and not a subscript to avoid adding words to wm!
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought, nodata, file); // not found
else
return QueryResult(sought, loc->second, file);
}
打印结果¶
print函数在给定的流上打印出给定的QueryResult对象:
ostream &print(ostream & os, const QueryResult &qr)
{
// if the word was found, print the count and all occurrences
os << qr.sought << " occurs " << qr.lines->size() << " "
<< make_plural(qr.lines->size(), "time", "s") <<endl;
// print each line in which the word appeared
for (auto num : *qr.lines) // for every element in the set
// don't confound the user with text lines starting at 0
os << "\t(line " << num + 1 << ") "
<< *(qr.file->begin() + num) << endl;
return os;
}
练习¶
定义你自己版本的 TextQuery 和 QueryResult 类,并执行runQueries 函数。
同之前练习
练习¶
如果用vector 代替 set 保存行号,会有什么差别?哪个方法更好?为什么?
如果用 vector 则会有单词重复的情况出现。
而这里保存的是行号,不需要重复元素,所以 set 更好。
练习¶
重写 TextQuery 和 QueryResult类,用StrBlob 代替 vector<string> 保存输入文件。
TextQuery 和 QueryResult 类中的 file 成员,改为 指向 StrBlob 的智能指针。
在访问 StrBlob 时,要使用 StrBlobPtr。
练习¶
我们将扩展查询系统,在 QueryResult 类中将会需要一些额外的成员。
添加名为 begin 和 end 的成员,返回一个迭代器,
指向一个给定查询返回的行号的 set 中的位置。
再添加一个名为 get_file 的成员,返回一个 shared_ptr,
指向 QueryResult 对象中的文件。
class QueryResult{
public:
using Iter = std::set<line_no>::iterator;
// ...
Iter begin() const { return lines->begin(); }
Iter end() const { return lines->end(); }
shared_ptr<std::vector<std::string>> get_file() const
{
return std::make_shared<std::vector<std::string>>(file);
}
private:
// ...
};
小结¶
在C++中,内存是通过new表达式分配,通过delete表达式释放的。
标准库还定义了一个allocator类来分配动态内存块。
分配动态内存的程序应负责释放它所分配的内存。
内存的正确释放是非常容易出错的地方:
- 要么内存永远不会被释放,要么在仍有指针引用它时就被释放了。
新的标准库定义了智能指针类型——shared_ptr、unique_ptr和weak_ptr,
可令动态内存管理更为安全。
对于一块内存,当没有任何用户使用它时,智能指针会自动释放它。
现代C++程序应尽可能使用智能指针。
实践课¶
- 从课程主页 cpp.njuer.org 打开 《面向对象编程基础》实验课 cloud shell界面 https://developer.aliyun.com/adc/scenario/476f2ff6afad4221bac08e33cf6984fc
- 使用g++编译代码
- 编辑一个 readme.md 文档,键入本次实验心得.
- 使用git进行版本控制 可使用之前的gitee代码仓库
- 云服务器(elastic compute service,简称ecs) - aliyun linux 2是阿里云推出的 linux 发行版 - vim是从vi发展出来的一个文本编辑器. - g++ 是c++编译器
习题1
使用 shared_ptr 而不是内置指针,编写函数,返回一个动态分配的 int 的vector。
将此vector 传递给另一个函数,这个函数读取标准输入,
将读入的值保存在 vector 元素中。再将vector传递给另一个函数,打印读入的值。
习题2
编写用 shared_ptr 管理 connection 的函数。
习题3
TextQuery 和 QueryResult 类只使用了我们已经介绍过的语言和标准库特性。
对这两个类编写你自己的版本。
编辑c++代码和markdown文档,使用git进行版本控制
yum install -y git gcc-c++
使用git工具进行版本控制
git clone你之前的网络git仓库test(或其它名字)
cd test 进入文件夹test
(clone的仓库,可移动旧文件到目录weekN: mkdir -p weekN ; mv 文件名 weekN;)
vim test1.cpp
g++ ./test1.cpp 编译
./a.out 执行程序
vim test2.cpp
g++ ./test2.cpp 编译
./a.out 执行程序
vim test3.cpp
g++ ./test3.cpp 编译
./a.out 执行程序
git add . 加入当前文件夹下所有文件到暂存区
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
vim readme.md 键入新内容(实验感想),按ESC 再按:wq退出
git add .
git commit –m "weekN" 表示提交到本地,备注weekN
git push 到你的git仓库
git log --oneline --graph 可看git记录
键入命令并截图或复制文字,并提交到群作业.
cat test* readme.md
提交¶
- 截图或复制文字,提交到群作业.
- 填写阿里云平台(本实验)的网页实验报告栏,发布保存.本次报告不需要分享提交
- 填写问卷调查 https://rnk6jc.aliwork.com/o/cppinfo
关于使用tmux¶
sudo yum install -y tmux
cd ~ && wget https://cpp.njuer.org/tmux && mv tmux .tmux.conf
tmux 进入会话 .
前缀按键prefix= ctrl+a,
prefix+c创建新面板,
prefix+"分屏,
prefix+k选上面,prefix+j选下面,
prefix+1选择第一,prefix+n选择第n,
prefix+d脱离会话
tmux attach-session -t 0 回到会话0
vim 共分为三种模式¶
- 命令模式
- 刚启动 vim,便进入了命令模式.其它模式下按ESC,可切换回命令模式
- i 切换到输入模式,以输入字符.
- x 删除当前光标所在处的字符.
- : 切换到底线命令模式,可输入命令.
- 输入模式
- 命令模式下按下i就进入了输入模式.
- ESC,退出输入模式,切换到命令模式
- 底线命令模式
- 命令模式下按下:(英文冒号)就进入了底线命令模式.
- wq 保存退出
vim 常用按键说明¶
除了 i, Esc, :wq 之外,其实 vim 还有非常多的按键可以使用.命令模式下:
- 光标移动
- j下 k上 h左 l右
- w前进一个词 b后退一个词
- Ctrl+d 向下半屏 ctrl+u 向上半屏
- G 移动到最后一行 gg 第一行 ngg 第n行
- 复制粘贴
- dd 删一行 ndd 删n行
- yy 复制一行 nyy复制n行
- p将复制的数据粘贴在下一行 P粘贴到上一行
- u恢复到前一个动作 ctrl+r重做上一个动作
- 搜索替换
- /word 向下找word ?word 向上找
- n重复搜索 N反向搜索
- :1,$s/word1/word2/g从第一行到最后一行寻找 word1 字符串,并将该字符串
取代为 word2
vim 常用按键说明¶
底线命令模式下:
- :set nu 显示行号
- :set nonu 取消行号
- :set paste 粘贴代码不乱序
【注:把caps lock按键映射为ctrl,能提高编辑效率.】
Markdown 文档语法¶
# 一级标题
## 二级标题
*斜体* **粗体**
- 列表项
- 子列表项
> 引用
[超链接](http://asdf.com)
![图片名](http://asdf.com/a.jpg)
|表格标题1|表格标题2|
|-|-|
|内容1|内容2|