Hey guys,my name is blue,金牌 bule!!!(模仿Cherno~~~)
每天进步一点点!!!
最近在重新系统学习c++,大学虽然学过,但那时道行浅,很多东西学的更多的只是能用起来就行。
但随着工作年限的增长,越来越发现,涉及性能相关的问题,越是知道底层原理,或者对规则越熟悉,才能做的更好。
今天学的这个移动构造函数,就是c++中一种提升性能的方法。
内容来源号称最好的c++教程,来自国外的大神cherno的视频。
讲的确实很好,想更深入了解c++底层实现原理或者是想提高c++在性能上使用的伙伴,他的视频是值得一看的。(B站搜c++视频,神经元猫up主翻译的就是)
在了解本篇之前,需要具备c++中左值、右值的知识,大佬Cherno的详细讲解链接如下:
Cherno 左值右值讲解
Cherno 关于移动语义和移动构造的原版讲解在这里
移动语义构造函数核心:
通过偷取临时变量(右值)的资源,给到自己的类对象中。减少对象的赋值操作,以此来提升性能。
回归正题:
先来说一下构造函数:
构造函数
在声明的类对象中,给对象赋值的操作可以简称为构造函数。
最简单的:
类对象中有int 变量或者其他非指针变量的,在构造函数中,我们做的一般是给这些变量直接赋值的方式完成初始化。
在定义类对象过程中,一般有如下几种方式初始化类对象:
①可以通过定义 对象名(赋值1、赋值2),方式定义初始化对象;
②也可以使用对象2 = 对象2(已初始化过的)方式初始化。
上面这些过程都是会使用到对象的拷贝构造函数完成类对象的初始化。
那么问题来了:
如果我的类里定义了指针变量,比如String 的name或者char *类型的变量时。
按照c语言的知识或者动态内存申请释放的原理来讲。
如果仅通过赋值,其实表示的是:“两个指针指向了同一个内存空间”,那在释放这个内存空间的时候(一般在有指针的类的析构函数中实现delete 操作),会产生double free的问题。
所以传统的简单赋值构造函数是满足不了有指针的这种场景。
再有指针的类中,我们要做的是在赋值的同时,给对象申请对应的新的内存空间。然后将已初始化的对象中的内容拷贝到新的内存空间中,这才是正确的做法–这也是拷贝构造函数的由来
拷贝构造函数
拷贝构造,其实就是不管类中的变量是何种形式,都是要进行完整的拷贝。
实现的形式与构造函数区别仅在于形参的不同,具体如下:
比如我有一个String类,里面是保存字符串内容和大小。
// A code block
class String {
private:
char* m_Data;
uint32_t m_Size;
public:
String() = default; //不使用默认的无参构造函数
String(const char* string) { //正常的有参拷贝构造函数
printf("String Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
~String() {
printf("String destruct!\r\n");
delete[] m_Data;
}
void Print() {
for (uint32_t i = 0; i < m_Size; ++i)
printf("%c", m_Data[i]);
printf("\n");
}
};
//
我要实现String类的拷贝构造:
String(const String & other)//核心在这里,传入的参数是同类的引用参数
{
申请内存;
给新申请的内存,复制引用中的参数;
}
代码实现如下
// An highlighted block
String(const String& other) { //拷贝构造实现(所谓的深拷贝)
printf("String Copied!\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
实际应用:
1、我有一个Entity 的类,类中包含了String 的名字;
2、我要给这个类的名字设置为“cherno”
实现如下:
那么此种方式会涉及几次构造呢?
// An highlighted block
#include <iostream>
#include <cstring>
#include <stdio.h>
typedef unsigned int uint32_t;
class String {
private:
char* m_Data;
uint32_t m_Size;
public:
String() = default; //屏蔽默认的无参构造函数
String(const char* string) { //初始化String对象
printf("String Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other) { //拷贝构造实现(所谓的深拷贝)
printf("String Copied!\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
~String() {
printf("String destruct!\r\n");
delete[] m_Data;
}
void Print() {
for (uint32_t i = 0; i < m_Size; ++i)
printf("%c", m_Data[i]);
printf("\n");
}
};
class Entity {
public:
Entity(const String& name) //成员列表初始化的方式初始化entity中的变量m_Name
: m_Name(name) {}
void PrintName() {
m_Name.Print();
}
private:
String m_Name;
};
int main(int argc, const char* argv[]) {
Entity entity(String("Cherno"));
entity.PrintName();
return 0;
}
答案是:2次拷贝构造函数,2次析构;
第一次构造:“Cherno”,传参进去,会创建String这个对象,会调用Create;
第二次构造,调用拷贝构造:将创建的String这个对象,拷贝给entity中的m_Name这个变量。再将“Cherno” 这个临时的String对象复制给entity对象后,这个临时的对象生命周期结束,会自动触发析构函数。
而entity 对象,它的作用域在main函数内部,所以entity中的String对象的析构会走最后退出main后,才会被触发调用。
运行结果如下:
问题来了:
我能不能把中间对“Cherno”临时对象赋值给entity对象的拷贝去掉?我想减少拷贝的次数。
答案当时是OK的,移动构造函数就是解决这个问题的。
移动构造函数
移动构造函数实现核心:
① 参数传递临时值(通过传递右值<右值其实就是临时变量在c++中的含义>)
通过String &&或者std::move
② 函数内部:
a.将当前对象指针指向右值对象的空间,其他内容拷贝给当前对象;
b.将右值对象指针指向nullptr,其他值赋值为0(清掉右值对象(临时变量)中有效信息,这样临时变量在离开作用域的时候,释放相关空间就不会释放到之前它指向的关键内容了<关键内容已经被我们关注的对象偷走了>)
上述具体实现:
核心:
第一步:实现底层的偷取动作
第二步:实现类对象中的移动拷贝构造函数
// string 类对象中:完成核心的偷取临时变量的动作
String(String&& other) //c++中右值通过&&来表示
{
printf("String move!\n");
m_Size = other.m_Size; //第一步先偷关键内容
m_Data = other.m_Data;
other.m_Data = nullptr; //第二步:把右值参数给清理下,避免乱指
other.m_Size = 0;
}
//entity类中,实现移动拷贝构造
//不优雅的实现方式1:
Entity(String&& name) //传参为右值
: m_Name((String &&)name) {} //这里传参也得需要右值,否则上面传进来到这里就失效了
//优雅、官方的实现方式2:
Entity(String&& name) //传参为右值
: m_Name(std::move(name)) {} //这里传参也得需要右值,否则上面传进来到这里就实现了
完整代码:
// An highlighted block
#include <iostream>
#include <cstring>
#include <stdio.h>
typedef unsigned int uint32_t;
class String {
private:
char* m_Data;
uint32_t m_Size;
public:
String() = default;
String(const char* string) {
printf("String Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other) {
printf("String Copied!\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
String(String&& other)
{
printf("String move!\n");
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Data = nullptr;
other.m_Size = 0;
}
~String() {
printf("String destruct!\r\n");
delete[] m_Data;
}
void Print() {
for (uint32_t i = 0; i < m_Size; ++i)
printf("%c", m_Data[i]);
printf("\n");
}
};
class Entity {
public:
Entity(const String& name)
: m_Name(name) {}
//不优雅实现方式1:
// Entity(String&& name)
// : m_Name((String &&)name) {}
//优雅实现方式2:
Entity(String&& name)
: m_Name(std::move(name)) {}
void PrintName() {
m_Name.Print();
}
private:
String m_Name;
};
int main(int argc, const char* argv[]) {
Entity entity(String("Cherno"));
entity.PrintName();
return 0;
}
运行结果如下:
是不是很神奇呢?
这下好了,又学会了一个新的概念!
距离精进c++又近了一步!
c++精进参考链接:
c++ 三/五法则