二十八、智能指针
智能指针是实现分配内存、释放内存这一过程自动化的一种方式。若使用智能指针,当我们调用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配置具体方法