C++学习笔记(6)

二十八、智能指针

智能指针是实现分配内存、释放内存这一过程自动化的一种方式。若使用智能指针,当我们调用new时不需要调用delete,甚至不需要调用new。智能指针本质上是一个原始指针的包装,当创建一个智能指针,它会调用new并为其分配内存,基于这个智能指针的内存会在某一时刻自动释放。

1、unique_ptr

unique_ptr是作用域指针,意味着超出作用域时,它会被销毁然后调用delete。

#include <iostream>
#include <memory>//此头文件用于调用智能指针

struct Entity
{
	Entity()
	{
		std::cout << "Create!" << std::endl;
	}

	~Entity()
	{
		std::cout << "Destroy!" << std::endl;
	}
};

int main()
{		//1
	{		//2,两层大括号
		std::unique_ptr<Entity> test1(new Entity());//
		std::unique_ptr<Entity> test2 = std::make_unique<Entity>();
	}

	std::cin.get();
}

我们不能复制一个unique_ptr,因为如果复制一个unique_ptr会有两个指针,两个unique_ptr指向同一个内存块。如果其中一个死了,它会释放那段内存,而另一个unique_ptr指针就会指向被释放的内存。

如果想要在特定的作用域下(两个大括号)创建一个unique_ptr来分配Entity,可以调用构造函数然后输入new Entity()。

std::unique_ptr<Entity> entity=new Entity();

但是会出现错误,因为unique_ptr的构造函数的是explicit的,需要显式调用构造函数,不能包含隐式转换。如果想要调用一个函数只需要通过箭头操作符来访问。

std::unique_ptr<Entity> entity(new Entity());
entity->Print();//问题点

一个更好的方法是把entity赋值给std::make_unique,主要是因为异常安全。如果构造函数抛出异常,使用make_unique(C++14)会保证最终得到的不是没有引用的悬空指针,从而造成内存泄漏。

std::unique_ptr<Entity> entity = std::make_unique<Entity>();
entity->Print();

2、shared_ptr

shared_ptr实现的方式实际上取决于编译器和你在编译器中使用的标准库。

shared_ptr的工作方式是通过引用计数,引用计数基本上是一种方法,可以跟踪我们的指针有多少个引用,一旦引用计数达到0,它就会被删除。

#include <iostream>
#include <memory>

struct Entity
{
	Entity()
	{
		std::cout << "Create!" << std::endl;
	}

	~Entity()
	{
		std::cout << "Destroy!" << std::endl;
	}
};

int main()
{
	{
		std::shared_ptr<Entity> e0;
		{
			std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
			e0 = sharedEntity;
		}
	}

	std::cin.get();
}

3、weak_ptr

weak_ptr被称为弱指针,可以和shared_ptr一起使用。它只是像声明其他东西一样声明,可以给它赋值为sharedEntity。这里和之前复制sharedEntity所做的一样,但是这里不会增加引用计数。当我们将一个shared_ptr赋值给另外一个shared_ptr时它会增加引用计数。但是当把一个shared_ptr赋值给一个weak_ptr时不会增加引用计数。

如果不想要Entity的所有权,例如在排序一个Entity列表时不关心它们是否有效,只需要存储一个它们的引用就可以了。我们可能会问weak_ptr底层对象是否还存活,但它不会保持底层对象存活,因为它不会增加引用计数。

{
	std::shared_ptr<Entity> e0;
	{
		std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
		std::weak_ptr<Entity> weakEntity = sharedEntity;
		e0 = sharedEntity;
	}
}

二十九、复制与拷贝构造函数

看如下代码,a和b是两个独立的变量,它们有不同的内存地址,若将b=3则a仍然是2。在Vector2类中是同样的原理,c.x仍然会是2。若要在堆中使用new关键字来进行分配则复制了指针,e和f两个指针本质上有相同的值(内存地址),但是如果访问这个内存地址并设为某个值,则会同时影响e和f。

#include<iostream>
#include<string>
#include<memory>

struct Vector2
{
	float x, y;
};

int main()
{
	int a = 5;
	int b = a;

	Vector2 c = { 2,3 };
	Vector2 d = c;
	d.x = 5;

	Vector2* e = new Vector2();//可以认为是指针的特性
	Vector2* f = e;
	f->x = 2;//同时改变e.x和f.x

	std::cin.get();
}

若使用C++原始特性写一个字符串类String,使用标准的std::cout来打印字符串,所以重载左移字符串。将运算符的重载函数作为这个类的友元,就可以从函数中访问m_Buffer。

#include<iostream>

class String
{
private:
	char* m_Buffer;
	unsigned int m_Size;
public:
	String(const char* string)
	{
		m_Size = strlen(string);
		m_Buffer = new char[m_Size+1];//+1为终止字符
	    memcpy(m_Buffer, string, m_Size+1);
	}

	~String()
	{
		delete[] m_Buffer;
	}

	friend std::ostream& operator<<(std::ostream& stream, const String& string);//友元~~~,不理解在此程序中的作用
};

std::ostream& operator<<(std::ostream& stream, const String& string)//重载
{
	stream << string.m_Buffer;
	return stream;
}

int main()
{
	String string = "Cherno";
	std::cout << string << std::endl;

	std::cin.get();
}

1、浅拷贝

试着复制这个字符串叫它second,然后把它们打印出来。按F5可以看到打印了两次Cherno。
在这里插入图片描述但如果按回车键,代码执行完cin.get()之后,代码就会崩溃。

当我们复制这个String时,C++自动为我们做的是将所有类成员变量复制到一个新的内存地址里面,这个新的内存地址包含了second字符串。现在内存中有两个String,因为它们直接进行复制,这种复制被称为浅拷贝,它所做的是复制这个指针内存中的两个String对象,它们有相同的char*的值,也就是相同的地址。这个m_Buffer的内存地址,对于这两个String对象来说是相同的,程序会崩溃。当我们到达作用域的尽头时,两个String都被销毁了,析构函数会被调用,执行两次delete[ ] m_Buffer,程序试图两次释放同一个内存块所以会崩溃。

2、深拷贝

我们需要做的是分配一个新的char数组来存储复制的字符串,而现在做的只是复制指针,两个字符串对象指向完全相同的内存缓冲区。若希望第二个字符串拥有自己的指针以拥有唯一的内存块,当修改或删除第二个字符串时不会触及第一个字符串。

这里需要执行一种叫做深度复制(深拷贝)的东西,使用拷贝构造函数,拷贝构造函数是一个构造函数,当复制第二个字符串时它会被调用。当把一个字符串赋值给一个对象时,这个对象也是一个字符串。当试图创建一个新的变量并给它分配另一个变量时,它和正在创建的变量有相同的类型。复制这个变量也就是所谓的拷贝构造函数。

c++默认提供一个拷贝构造函数,拷贝构造函数的函数签名对同样的类对象的常引用const &,它的作用是内存复制,将other对象的内存浅层拷贝进这些成员变量。

String(const String& other)//拷贝构造函数
		:m_Buffer(other.m_Buffer), m_Size(other.m_Size) {}
//这样不行,因为不仅仅想复制指针还想复制指针所指向的内存
String(const String& other)
		:m_Size(other.m_Size)
{
		m_Buffer = new char[m_Size + 1];
		memcpy(m_Buffer, other.m_Buffer, m_Size+1);
}//right

三十、C++箭头操作

箭头可以理解为:让它完成指向函数的操作。

#include <iostream>

struct Entity
{
	Entity()
		{std::cout << "Create!" << std::endl;}
	~Entity()
		{std::cout << "Destroy!" << std::endl;}

	void Print()
		{std::cout << "Hello!" << std::endl;}
};

class ScopedPtr
{
private:
	Entity* m_Ptr;
public:
	ScopedPtr(Entity* ptr)
		: m_Ptr(ptr){}

	~ScopedPtr()
		{delete m_Ptr;}

	Entity* operator->()
		{return m_Ptr;}
};

int main()
{
	{
		ScopedPtr test = new Entity();
		test->Print();
	}
	std::cin.get();
}

CONST版本

#include <iostream>

class Entity
{
private:

public:
	Entity()
		{std::cout << "Create!" << std::endl;}

	~Entity()
		{std::cout << "Destroy!" << std::endl;}

	void Print() const //3
		{std::cout << "hello!" << std::endl;}
};

class ScopedPtr
{
private:
	Entity* m_Ptr;
public:
	ScopedPtr(Entity* ptr)
		: m_Ptr(ptr){}

	~ScopedPtr()
		{delete m_Ptr;}

	Entity* operator->()
		{return m_Ptr;}

	const Entity* operator->() const//1
		{return m_Ptr;}
};

int main()
{
	{
		const ScopedPtr test = new Entity();//2
		test->Print();
	}
	std::cin.get();
}

十三一、动态数组(std::vector)

标准模板库本质上是一个库,里面装满了容器,容器类型,这些容器包含特定的数据。之所以被称为标准模板库,因为它可以模板化任何东西。这意味着容器的底层数据类型(容器包含的数据类型)由我们自己决定,所有东西由模板组成,模板可以处理我们提供的底层数据类型,意味着不需要编写自己的数据结构或类似的东西。

C++提供给我们一个叫做Vector的类,这个Vector在std命名空间中,它应该被称为ArrayList,本质上是一个动态数组(不是向量)。在创建动态数组时(Vector),它没有固定大小(可以给一个特定大小来初始化)。创建Vector后每次往里面添加一个元素,Vector的数组大小会增长。当添加的元素超过Vector数组的大小时,它会在内存中创建一个比第一个大的新数组,把所有东西都复制到这里,然后删除旧的那个

#include<iostream>
#include<string>
#include<vector>//库

struct Vertex
{
	float x, y, z;
};

//输出运算符的重载
std::ostream& operator<<(std::ostream& stream, const Vertex& vertex)
{
	stream << vertex.x << "," << vertex.y << "," << vertex.z;

	return stream;
}

int main()
{
	std::vector<Vertex> vertices;
	std::cin.get();
}

若我们想要有一个静态数组有两个选择,不考虑std::array类的话可以创建一个静态数组,其中可能有5个元素。但这种方式需要绑定大小即在堆上创建,我们可以访问索引0到4。

	Vertex  vertices[5];
	Vertex* vertices = new Vertex[5];//堆上创建
	vertices[4];

若要不断地添加点,想访问大一点的索引,可以用vector类来代替:首先要包含vector头文件然后创建一个数组,先输入std::vector然后指定数组中元素的类型Vertex并取名数组叫vertices

std::vector<Vertex> vertices;
//注意这里并没有存储一堆vertex指针,实际上只是把vertex存储在一条直线(在一段内存)上。

如果是vertex对象则它的内存分配将是一条线上的,而动态数组是内存连续的数组,这意味着它在内存中不是碎片,内容都在一条高速缓存线上。

向vector中添加元素,只需输入vertices.push_back(),因为vector是一个完整的类,所以知道它的大小(vertices.size())。

int main()
{
	std::vector<Vertex> vertices;
	vertices.push_back({ 1,2,3 });//会自动换行
	vertices.push_back({ 4,5,6 });
	
	for (int i = 0; i < vertices.size(); i++)
	{
		std::cout << vertices[i] << std::endl;
	}

	std::cin.get();
}

在这里插入图片描述
或者把for循环嵌套:

for (Vertex v: vertices)
	{
		std::cout << v << std::endl;
	}
//这样写vertex是将每个vertex复制到这个for范围循环中,但我们希望尽可能避免复制,在这里用&符号就行。
for (Vertex& v: vertices)
	{
		std::cout << v << std::endl;
	}

若想清除vertex列表,只需要输入vertices.clear();它会将数组大小设回0;也可以用vertices.erase();单独移除某个vertex,若想要移除第二个元素可以通过vertices.begin()然后再加1。
在这里插入图片描述
当我们将这些vector传递给函数或类或其他东西时间,要确保是通过引用传递它们的。若不会修改它们则使用const引用,这样可以确保没有把整个数组复制到这个函数中

void Function(const std::vector<Vertex> & vertices)
	{}

三十二、std::vector使用优化

std::vector是这样工作的:创建一个vector然后开始push_back元素,也就是向数组中添加元素。如果vector的容量不够大,不能容纳我们想要的新元素,vector需要分配新的内存,当前vector的内容从内存中的旧位置复制到内存中的新位置,然后删除旧位置的内存。

这就是将代码拖慢的原因,因为我们需要不断地重新分配并复制所有现有的元素。所以我们的优化思路就是如何避免复制对象,如果我们处理的是基于vector的对而没有存储vector指针。

#include <iostream>
#include <vector>

struct Vertex
{
	float x, y, z;

	Vertex(float x, float y, float z)
		: x(x), y(y), z(z)
	{
	}

	Vertex(const Vertex& vertex)
		: x(vertex.x), y(vertex.y), z(vertex.z)
	{
		std::cout << "Copied!" << std::endl;
	}
};

int main()
{
	std::vector<Vertex> vertices;
	vertices.push_back({ 1, 2, 3 });
	vertices.push_back({ 4, 5, 6 });
	vertices.push_back({ 7, 8, 9 });

	std::cin.get();
}

可以看到运行结果:
在这里插入图片描述
有6次拷贝,这里首先是initializer_list去初始化一个Vertex类,但是实际上只是一个临时对象。

  • 第一次push_back,capacity扩容到1,临时对象拷贝到真正的vertices所占内存中,第一次Copied;
  • 第二次push_back,发生扩容,capacity扩容到2,vertices发生内存搬移发生的拷贝为第二次Copied;
  • 然后再是临时对象的搬移,为第三次Copied;
  • 接着第三次push_back,capacity扩容到3(2*1.5 = 3,3之后是4,4之后是6…), vertices发生内存搬移发生的拷贝为第四和第五个Copied;
  • 然后再是临时对象的搬移为第六个Copied。

优化1:若是首先vertices.reserve(3);,改变的为capacity的大小,变为3;所以没有发生vertices扩容带来的Copied,因此只有三次临时对象的搬移;
优化2:如果改为emplace_back,则是直接在vertices的内存中调用Vertex的构造函数,那自然没有临时对象的搬移,所以没有Copied。
因为vertex实际上是在main函数中构造的,然后复制到实际的Vector中。
只运行一次只打印一次copied。

那么emplace_back这要怎么做到呢?可以用placement new;不会有一次拷贝发生;placement new允许我们将对象构建在已经分配的内存中,比如我写的一个示例:

char* BufferPtr = new char[10];
Vertex* PlacementVertexPtr = new(BufferPtr) Vertex(1, 2, 3);
std::cout << PlacementVertexPtr->y << std::endl;

如果我们写的是vertices.emplace_back(Vertex(1, 2, 3));,则仍然会发生临时对象的搬移,仍然会带来拷贝。

三十三、C++中使用库

对于其他语言,比如Java、C#或python等,添加库是一项非常简单的任务。你可能用的是包管理器,也可能不是,但无论如何都是很简单的。

1、静态链接

对于C++库,cherno倾向于在实际解决方案中的实际项目文件夹中,保留使用的库的版本。所以cherno实际上是使用那些物理二进制文件或代码的副本,这取决于在解决方案的实际工作目录中使用的方法。

对于大多数严肃的项目,cherno绝对推荐,实际构建源代码。如果是用VS,则可以添加另一个项目,该项目包含你的依赖库的源代码,然后将其编译为静态或动态库。

然而,如果拿不到源代码,或者这只是一个快速项目,不想花太多时间去设置,因为这是一种一次性的东西,或者只是一个不那么重要的项目,那么cherno可能倾向于链接二进制文件,因为它会更快更容易。

这一节将以二进制文件形式进行链接,而不是获取实际依赖库的源代码并自己进行编译。而在一个更加专业的大项目中,在有时间的地方,cherno肯定会自己编译它,因为它有助于调试,并且如果想修改库可以稍微改变一下。
在这里插入图片描述
对于Windows的这些二进制文件,是拿32位的还是64位的呢?这与你实际的操作系统没有任何关系,而是和你的目标应用程序相关。 如果你正在编译的应用程序是win32程序(x86),那么就要32位的二进制文件,当然你的操作系统却很可能是win10 64位。

库通常包含两部分:include和library(包含目录和库目录)。包含目录是一堆头文件。基本上include目录是一堆我们需要使用的头文件,这样我们就可以实际使用预构建的二进制文件中的函数,然后lib目录有那些预先构建的二进制文件。这里通常有两部分:dynamic library和static library。可以选择静态链接或动态链接(不是所有的库都提供了这两种方式)。

  • 静态链接意味着这个库会被放到你的可执行文件中,它在你的exe文件中,或者其他操作系统下的可执行文件。
  • 动态链接库是在运行时被链接的,所以你仍然有一些链接,你可以选择在程序运行时装载动态链接库。
  • 有一个叫做loadLibrary的函数,你可以在WindowsAPI中使用它作为例子,它会载入你的动态库,可以从中拉出函数,然后开始调用函数;也可以在应用程序启动时加载你的dll文件,这就是动态链接库。
  • 动静态链接最主要的区别就是库文件是否被编译到exe文件中或链接到exe文件中,还是只是一个单独的文件在运行时需要把它放在你的exe文件旁边或某个地方,然后你的exe文件可以加载它。

静态链接在技术上更快,因为编译器或链接器实际上可以执行链接时优化之类的。静态链接在技术上可以产生更快的应用程序。
VS配置具体方法

2、动态链接

叫动态链接是因为链接发生在运行时,而静态链接是在编译时发生的。
在这里插入图片描述
要确保可访问的地方有dll文件,可以在整个应用程序中设置库搜索位置,可执行文件的目录下是一种自动搜索路径,如果把它们放在同一个文件夹里,一定不会有问题。
VS配置具体方法

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值