前文
之前我们只学习过静态内存和栈内存。现在还有一个堆内存,他们保存的变量类型分别为
内存名字 | 该内存中保存的类型 |
---|---|
静态内存 | static局部变量、类的static数据成员、定义在函数体之外的变量 |
栈内存 | 函数体中的所有非static变量 |
堆内存 | 动态分配内存空间的变量 |
12.1动态内存和智能指针
我们可以使用new关键字动态的分配内存空间,并返回指向该对象的指针,我们可以使用delete关键字,传入一个动态分配内存的指针,销毁该对象,回收内存。
但是直接使用new和delete是非常容易犯错的,所以C++标准提供了两个智能指针。
shared_ptr,unique_ptr。
shared_ptr允许多个指针指向同一个对象。
unique_ptr,只允许一个指针指向一个对象。
这两个指针定义在memory头文件中
12.1.1 shared_ptr类
shared_ptr是一个模板,我们在创建一个智能指针变量时,需要指定这个指针的类型。
std::shared_ptr<string> p;
下面是unique_ptr和shared_ptr都支持的操作。
可以看到智能指针的用法和普通指针没有什么区别,我们可以使用get来获取,智能指针中保存的普通指针。
shared_ptr独有的操作
shared_ptr使用的是引用计数的方法来管理内存。我们不必在乎引用计数是如何实现了,只需要知道什么时候引用计数什么时候会+1,什么时候会-1,什么时候会回收内存空间。
引用计数情况 | 发生的情况 |
---|---|
引用计数+1 | 创建一个智能指针+1;进行拷贝时,被拷贝者+1;进行赋值时,赋值语句右侧的智能指针+1;作为实参传入函数时+1;当做返回值返回时+1 |
引用计数-1 | 赋值语句左侧的智能指针-1;生命周期结束-1 |
引用计数=0 | 销毁指针所指向的对象,回收内存 |
例子
//创建p1,p1的引用计数+1,此时为1
std::shared_ptr<string> p1 = std::make_shared<string>("123");
//p2的引用计数+1,此时为1
std::shared_ptr<string> p2 = std::make_shared<string>("233");
//将p2的值赋值给p1,p2引用计数+1,p1引用计数-1,p1的引用计数为0,销毁p1指向的对象回收内存
//p1和p2现在指向同一个对象,引用计数为2.
p1 = p2;
最安全的分配和使用动态内存的方法是调用make_shared的标准库函数,他是一个模板函数,所以需要传入类型,和容器的emplace()一样,我们可以在()中,传入类型的构造函数所需的参数。
std::shared_ptr<string> p = std::make_shared<string>("123");
当引用计数为0的时候,智能指针会销毁指向的对象并回收内存,其本质是调用对象的析构函数。
什么时候使用动态内存
1.程序不知道自己需要使用多少对象。(比如可以动态添加元素的容器类,就是使用动态内存)
2.程序不知道所需对象的准确类型(这个没有体会到)
3.程序需要在多个对象间共享数据。(比如问中提到了StrBlob)
练习
12.1
因为b1=b2,所以b1和b2的数据成员data指向的是同一个对象,所以他们的元素都为4个。
12.2
注意在编写fornt和back的const版本时,front和back的返回值也需要为const。不然我们依旧可以通过返回值来修改data所指向的对象的值。
class StrBlob {
public:
using size_type = vector<string>::size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const {
return data->size(); };
bool empty() const {
return data->empty(); };
void push_back(const string& str) {
data->push_back(str); };
void pop_back();
std::string& front();
const std::string& front()const ;
std::string& back();
const std::string& back() const;
private:
std::shared_ptr<vector<string>> data;
void check(size_type i,const string& msg) const;
};
void StrBlob::check(size_type i, const string& msg) const{
if (i>=data->size()) {
throw std::out_of_range(msg);
}
}
string& StrBlob::front() {
check(0, "front out of range");
return data->front();
}
const string& StrBlob::front() const{
check(0, "front out of range");
return data->front();
}
string& StrBlob::back(){
check(0, "back out of range");
return data->back();
}
const string& StrBlob::back() const{
check(0, "back out of range");
return data->back();
}
void StrBlob::pop_back() {
check(0, "pop_back() out of range");
data->pop_back();
}
StrBlob::StrBlob():data(std::make_shared<vector<string>>()) {
}
StrBlob::StrBlob(std::initializer_list<string> il) : data(std::make_shared<vector<string>>(il)) {
}
12.3
不需要,因为如果一个对象为常量,我们认为他是不能够改变数据成员的。push_back()和pop_back()都会data所指向的对象的数据成员,所以当常量调用push_back,pop_back()时,应该编译报错
12.4
因为i是stirng::size_type类型,而data->size()也是type()类型,size_type类型是size_t,而size_t无符号整型,它永远都不会为负数。
另一方面,我们写的check,只需要判断data所指向的对象是否有元素,所以可以直接传入0,如果0>=data->size()则表示容器为空,而不需要考虑是否大于0.其实我觉得用==也可以。
12.5
如果我们加入了explict。
优点是:
我们可以避免隐式转换,以免发生意料之外的事情
缺点:
如果加explicit,意味着我们需要显式传入一个类的对象,这增加的编码的负担,降低了灵活性。
12.1.2 直接管理内存
之前说到直接使用new和delete关键字是非常麻烦的,那么为什么这么棘手呢
使用new动态分配和初始化对象
我们可以使用new直接动态分类对象,因为new出来的对象是没有名字的,但是它会返回一个指向对象的指针,所以我们可以写
int