文章目录
- 1.智能指针
- 1.1.unique_ptr(优先使用,开销低)
- 1.2.shared_ptr
- 1.3.weak_ptr
- 2.P45 复制与拷贝构造函数
- 2.1.浅拷贝和深拷贝
- 2.1.1.浅拷贝示例
- 2.1.2.深拷贝示例
- 2.2.总是(always)用常量引用传递对象
- 3.P46 箭头操作符
- 3.1.箭头操作符的重载
- 3.2.使用箭头操作符查看类成员变量的偏移地址
1.智能指针
1.1.unique_ptr(优先使用,开销低)
unique_ptr
是作用域指针,超出作用域时它会被销毁,然后自动调用delete
。为什么叫做unique独一无二的?因为我们不能复制一个unique_ptr
,因为如果复制一个unique_ptr
会有两个指针,两个unique_ptr
指向同一个内存块。如果其中一个死了,它会释放那段内存,而另一个unique_ptr
指针就会指向被释放的内存,这样这个指针就是无效的。因此当你想要使用一个作用域指针的时候unique_ptr
是最优选择。
要访问智能指针,首先要包括memory
头文件,即#include <memory>
。如果想要在特定的作用域下(两个大括号)创建一个unique_ptr
来分配Entity
,可以调用构造函数然后输入new Entity()
。但是注意下面的写法有错误,因为unique_ptr
的构造函数声明了explicit
,即禁止了隐式类型转换,需要显示调用构造函数。
#include <iostream>
#include <memory>
class Entity
{
public:
Entity()
{
std::cout << "Construct Entity!" << std::endl;
}
~Entity()
{
std::cout << "Destroyed Entity!" << std::endl;
}
void Print()
{
// Print
}
};
int main()
{
{
// 1.错误写法:隐式类型转换
std::unique_ptr<Entity> entity=new Entity();
// 2.正确写法:显式调用构造函数
std::unique_ptr<Entity> entity(new Entity());
// 创建智能指针后就可以像普通指针一样使用它
entity->Print();
}
std::cin.get();
}
一个更好的方法是把entity
赋值给std::make_unique
,主要是因为异常安全。如果构造函数碰巧抛出异常,使用make_unique(C++14)
会保证最终得到的不是没有引用的悬空指针,从而造成内存泄漏。
std::unique_ptr<Entity> entity = std::make_unique<Entity>();
entity->Print();
之所以unique_ptr
不能进行复制,是因为unique_ptr
的拷贝构造函数和拷贝构造操作符实际上被删除了。
1.2.shared_ptr
shared_ptr
是共享指针,它的工作方式是通过引用计数,可以跟踪我们的指针有多少个引用,一旦引用计数达到0,这个指针指向的内存就会被释放。
在unique_ptr
中不直接调用new
而使用make_unique
的原因是因为异常安全,但是在shared_ptr
中有所不同。shared_ptr
需要分配另一块内存,叫做控制块,用来存储引用计数。如果使用 std::shared_ptr<Entity> entity(new Entity);
的方式,则过程是先创建一个new Entity
分配一次内存,然后将其传递给shared_ptr
构造函数再对控制块分配一次内存,这样它必须做两次内存分配。若使用make_unique就可以把它们组合起来做一次内存分配,提高性能。
如下面的代码,解释shared_ptr
如果工作。这里有两个作用域,在外面的作用域中有e0
,里面的作用域中有sharedEntity
,然后把e0
赋值为sharedEntity
。在27行设置断点按F5运行,再按F10发现Entity
被创建了。当里面的作用域死亡时,sharedEntity
就会死亡,但是e0
还存活并且持有对该Entity
的引用,所以这里没有调用析构函数。
若再按F10Entity
就会被销毁。当所有的引用都消失,当所有的栈分配对象,追踪shared_ptr
的从内存释放后,底层的Entity
才会被删除(调用析构函数)。
1.3.weak_ptr
weak_ptr
被称为弱指针,可以和shared_ptr
一起使用。它只是像声明其他东西一样声明,可以给它赋值为sharedEntity
。这里和之前复制sharedEntity
所做的一样,但是这里不会增加引用计数。当我们将一个shared_ptr
赋值给另外一个shared_ptr
时它会增加引用计数。但是当把一个shared_ptr
赋值给一个weak_ptr
时不会增加引用计数。
如果不想要Entity
的所有权,例如在排序一个Entity
列表时不关心它们是否有效,只需要存储一个它们的引用就可以了。我们可能会问weak_ptr
底层对象是否还存活,但它不会保持底层对象存活,因为它不会增加引用计数。
若把之前的shared_ptr
换成一个weak_ptr
。在27行设置断点按F5运行,再按F10发现Entity
在这里被创建,然后它会被分配到sharedEntity
。
但当退出里面的作用域时就是它被摧毁的时候,所以这个weak_ptr现在指向一个无效的Entity。
2.P45 复制与拷贝构造函数
2.1.浅拷贝和深拷贝
笔记里基本都在写浅拷贝和深拷贝的内容,实际上没有什么新的东西。
就是在类中存在指针成员变量的时候,并且这个指针是指向堆上的内存。如果使用浅拷贝,那么复制的时候就是仅仅复制指针,而不会复制指针指向的内存的数据。这样当前一个对象被释放的时候,其指针成员变量一并被释放,导致这块内存无效。而后一个对象的指针成员变量还是指向堆上的这块内存,当后一个对象被释放的时候它再次尝试释放堆上的这块内存,从而报错。
2.1.1.浅拷贝示例
但如果按回车键,代码执行完cin.get()之后,代码就会崩溃。
2.1.2.深拷贝示例
2.2.总是(always)用常量引用传递对象
Cherno在视频中说,在基础使用中尽量用常量引用来传递对象,尽管某些情况下可能复制对象更快。常量引用除了减少拷贝之外,还有其他的好处。
如下代码,只会输出一句"Copied String"
,因为只在second=string
的时候调用了拷贝构造函数,在PrintString
函数传参的时候并没有进行拷贝。
3.P46 箭头操作符
3.1.箭头操作符的重载
代码如下。
问题:ScopedPtr
对象重载的->
运算符得到的是Entity
对象的指针,那么entity->Print();
这句话调用Print
函数的时候不就相当于少了一个Entity
对象的指针调用Print
函数的->
操作符吗?
#include<iostream>
#include<string>
class Entity
{
public:
int x;
public:
void Print() const { std::cout << "Hello!" << std::endl; }
};
class ScopedPtr
{
private:
Entity* m_Obj;
public:
ScopedPtr(Entity* entity)
:m_Obj(entity) {}
~ScopedPtr()
{
delete m_Obj;
}
Entity* operator->() //重载
{
return m_Obj;
}
};
int main()
{
ScopedPtr entity = new Entity();
entity->Print();
std::cin.get();
}
3.2.使用箭头操作符查看类成员变量的偏移地址
可以使用箭头操作符来获取内存中某个成员变量的偏移量。假设有一个Vector3结构体有三个浮点数分量xyz,若想要找出这个变量在内存中的偏移量,例如x偏移量是0,y是4,z是8。代码如下:
#include<iostream>
#include<string>
struct Vector3
{
float x, y, z;
};
int main()
{
int offse_x = (int)&((Vector3*)0)->x;
//int offse_x = (int)&((Vector3*)nullptr)->x;
int offse_y = (int)&((Vector3*)0)->y;
//int offse_y = (int)&((Vector3*)nullptr)->y;
int offse_z = (int)&((Vector3*)0)->z;
//int offse_z = (int)&((Vector3*)nullptr)->z;
std::cout << offse_x << std::endl;
std::cout << offse_y << std::endl;
std::cout << offse_z << std::endl;
std::cin.get();
}
代码解释:
因为"指针->属性"访问属性的方法实际上是通过把指针的值和属性的偏移量相加,得到属性的内存地址进而实现访问。而把指针设为nullptr(0)
,然后->
属性就等于0+属性偏移量
。编译器能知道你指定属性的偏移量是因为你把nullptr
转换为类指针,而这个类的结构你已经写出来了(float x,y,z)
,float
4字节,所以它在编译的时候就知道偏移量(0,4,8)
,所以无关对象是否创建。