文章目录

  • 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的拷贝构造函数和拷贝构造操作符实际上被删除了。

codesys指针偏移_codesys指针偏移

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的引用,所以这里没有调用析构函数。

codesys指针偏移_c++_02

若再按F10Entity就会被销毁。当所有的引用都消失,当所有的栈分配对象,追踪shared_ptr的从内存释放后,底层的Entity才会被删除(调用析构函数)。

codesys指针偏移_操作符_03

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

codesys指针偏移_作用域_04

但当退出里面的作用域时就是它被摧毁的时候,所以这个weak_ptr现在指向一个无效的Entity。

codesys指针偏移_操作符_05

2.P45 复制与拷贝构造函数

参考:视频 笔记

2.1.浅拷贝和深拷贝

笔记里基本都在写浅拷贝和深拷贝的内容,实际上没有什么新的东西。

就是在类中存在指针成员变量的时候,并且这个指针是指向堆上的内存。如果使用浅拷贝,那么复制的时候就是仅仅复制指针,而不会复制指针指向的内存的数据。这样当前一个对象被释放的时候,其指针成员变量一并被释放,导致这块内存无效。而后一个对象的指针成员变量还是指向堆上的这块内存,当后一个对象被释放的时候它再次尝试释放堆上的这块内存,从而报错。

2.1.1.浅拷贝示例

codesys指针偏移_作用域_06

但如果按回车键,代码执行完cin.get()之后,代码就会崩溃。

codesys指针偏移_操作符_07

2.1.2.深拷贝示例

codesys指针偏移_codesys指针偏移_08

2.2.总是(always)用常量引用传递对象

Cherno在视频中说,在基础使用中尽量用常量引用来传递对象,尽管某些情况下可能复制对象更快。常量引用除了减少拷贝之外,还有其他的好处。

如下代码,只会输出一句"Copied String",因为只在second=string的时候调用了拷贝构造函数,在PrintString函数传参的时候并没有进行拷贝。

codesys指针偏移_操作符_09

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)float4字节,所以它在编译的时候就知道偏移量(0,4,8),所以无关对象是否创建。