C++学习笔记(下)

多维数组

如 二维数组即包含数组的集合,数组的数组。

C++用指针的方式处理数组。

假如有一个一维数组,int* array = new int[50] ,意为申请了一个整型指针,该指针指向一个50整数数组的开端。在32位编译模式下,一个指针占四个字节的内存空间。但是50个整数申请了200字节的空间。

假如有一个二维数组,int** a2dArray = new int* [50] ,分配了一个指向50个int*(整型指针)开端的指针,即申请了50个整型指针的内存块,在32位编译模式下,50个指针占四个字节的内存空间,其中的每个指针又会指向一个整数数组的开端。即 一个包含五十个数组内存开端的数组。

不过因为此二维数组指向的内存还未分配,我们将它分配为真正的数组,代码如下:

	int** a2dArray = new int* [50];
	for (int i = 0; i < 50; i++)
	{
		a2dArray[i] = new int[50];
	}

三维数组则继续类似的嵌套即可,如下:

	int*** a3dArray = new int** [50];

	for (int i = 0; i < 50; i++)
	{
		a3dArray[i] = new int* [50];

		for (int j = 0; j < 50; j++)
		{
			a3dArray[i][j] = new int[50];
		}
	}

以上的 [ ] 为对指针的解引用。

既然使用堆分配,就要delete的释放内存。但是以二维数组为例,如果delete[][] a2dArray; (实际并没有[][]这样的运算符),会释放此二维数组分配的50个指针即200字节的内存,但每个指针指向的数组内存并未释放掉,会造成内存泄露。因此,要遍历删除每个实际的数组,如下:

	for (int i = 0; i < 50; i++)
	{
		delete[] a2dArray[i];
	}
	delete[] a2dArray;

整个过程即:自顶向下初始化分配内存,从下至顶释放空间

关于优化。分配如5行5列的二维数组时,并不是简单的分配25个连续的地址空间,而是五个不同的缓冲区,在循环时,往往会跳出循环,找到下一个缓冲区,继续循环... 造成了更少cache hit(缓存命中),比起一维数组的效率低不少。

如何优化,我们可以用一维想象成二维的方式,人为的设置循环的行和列,如下。

	int* array = new int[5 * 5];

	for (int x = 0; x < 5; x++)
	{
		for (int y = 0; y < 5; y++)
		{
			array[y + x * 5] = y + x * 5;
		}
	}

C++排序

主要用到了std::sort函数,它可以有两个或者三个参数,前两个必须包括的参数为 迭代器的开始,迭代器的结束,第三个参数是 谓语,即排序的方式,下列例子分别使用了默认、大于的函数和自建lambda函数。

#include <iostream>
#include <vector>
#include <algorithm>	//包含std::sort
#include <functional>	//包含std::greater

int main()
{
	std::vector<int> vector = { 3,5,1,4,2 };

	//----------------------------------------------------
	std::sort(vector.begin(), vector.end());

	for (int value : vector)
		std::cout << value << std::endl;

	//----------------------------------------------------
	std::cout << std::endl;

	std::sort(vector.begin(), vector.end(), std::greater<int>()); //按大于的形式排序

	for (int value : vector)
		std::cout << value << std::endl;

	//----------------------------------------------------
	std::cout << std::endl;

	std::sort(vector.begin(), vector.end(), [](int a, int b)
		{
			if (a == 1)
				return false;

			return a < b;
		});	//lambda函数作谓语,按小于的形式排,即前者小于后者,前者为真,排前面,但前者为1时,前者总为假,即值为1的数排最后

	for (int value : vector)
		std::cout << value << std::endl;
}

运行结果如下:

类型双关

类型双关 是计算机科学的术语,指任何编程技术能颠覆或者绕过一门 程序设计语言 的 类型系统 ,以达成在形式语言内部难以甚至不可能实现的效果。

如果你不想处理某种类型的复制或转换,它会非常有用。

如果我们用不同的方式解析同一段内存,会得到不同的结果。不同的类型如整型,浮点,字符,类,结构体....只是C++约定的解析内存的方式。 这就是C++的类型双关。

下列类型双关代码及其运行结果,解析内存方式的变化,自行理解。

//类型双关

#include <iostream>

struct Entity
{
	int x, y;

	int* GetPosition()
	{
		return &x;
	}
};

int main()
{
	Entity e = { 3,8 };

	int* array = (int*)&e;	//Entity e的地址被转化为整数数组的地址
	std::cout << array[0] << "," << array[1] << std::endl;

	int x = *((char*)&e);
	int y = *((char*)&e + 4);	//Entity e的地址转换为char(单字节),此时指向的是e中的第一个整数的地址,再加4个字节,指向e中第二个数字的地址,再解引用即可得到该数。
	std::cout << "x:" << x << " y:" << y << std::endl;

	int* position = e.GetPosition();
	std::cout << position[0] << " " << position[1] << std::endl;
}

联合体Union

关键点在于对于同一段内存中数据的处理方式不同。

#include <iostream>

struct Vector2
{
	float x, y;
};

struct Vector4
{
	union
	{
		struct
		{
			float x, y, z, w;
		};

		struct
		{
			Vector2 a, b;
		};
	};
};

void PrintVector2(const Vector2& vector2)
{
	std::cout << vector2.x << "," << vector2.y << std::endl;
}

int main()
{
	Vector4 vector = { 1.0f, 2.0f, 3.0f, 4.0f };

	PrintVector2(vector.a);
	PrintVector2(vector.b);
	std::cout << "---------------------------" << std::endl;

	vector.z = 9.0f;
	PrintVector2(vector.a);
	PrintVector2(vector.b);
}

union里的成员会共享内存,分配的大小是按最大成员的sizeof, 上例里有两个成员,也就是那两个结构体,改变其中一个另外一个里面对应的也会改变。上例的运行结果如下:

虚折构函数

#include <iostream>

class Base
{
public:
	Base() { std::cout << "Base Constructor \n"; }
	~Base() { std::cout << "Base Destructor \n"; }
};

class Derived : public Base
{
private:
	int* m_Array;
public:
	Derived() { m_Array = new int[5]; std::cout << "Derived Constructor \n"; }
	~Derived() { delete[] m_Array; std::cout << "Derived Destructor \n"; }
};

int main()
{
	Base* base = new Base();
	delete base;

	std::cout << "----------------------------\n";

	Derived* derived = new Derived();
	delete derived;
}

上述代码有一个基类和它的一个派生类,运行结果如下:

可以看到派生类实例被创建时,先调用了基类的构造函数,再调用了派生类的构造函数,再调用了派生类的折构函数,再调用了基类的构造函数。

但是如果用基类指针引用派生类对象时,那么基类的析构函数必须是 virtual 的,否则 C++ 只会调用基类的析构函数,不会调用派生类的析构函数。这样会造成内存泄漏。

    Base* base = new Base();
	delete base;

	std::cout << "----------------------------\n";

	Base* poly = new Derived();
	delete poly;

下面是没有virtual声明的上述语句的运行结果 

其中,poly代表polymorphism多态。用基类指针引用派生类对象,且没有声明的基类折构函数为virtual,可以看到派生函数的折构函数没有被调用,则构造函数中创建的数组的内存,没有被释放,造成了内存泄漏。

在Base的折构函数前加virtual声明,告诉编译器该类可能被扩展,该折构函数可能被重写。此时子类的折构函数就会被成功调用。

基类中只要定义了虚析构(且只能在基类中定义虚析构,子类析构才是虚析构,如果在二级子类中定义虚析构,编译器不认,且virtual失效),在编译器角度来讲,那么由此基类派生出的所有子类地析构均为对基类的虚析构的重写,当多态发生时,用父类引用,引用子类实例时,此时的虚指针保存的子类虚表的地址,该函数指针数组中的第一元素永远留给虚析构函数指针。所以当delete 父类引用时,即第一个调用子类虚表中的子类重写的虚析构函数此为第一阶段。然后进入第二阶段:(二阶段纯为内存释放而触发的逐级析构与虚析构就没有半毛钱关系了)而当子类发生析构时,子类内存开始释放,因内存包涵关系,触发父类析构执行,层层向上递进,至到子类所包涵的所有内存释放完成。

预编译头文件

precompiled header(pch)

在大型项目中可以大大节省编译时间。假如在一个项目中有很多cpp文件,它们都需要包含vector头文件,编译器会分开编译,再链接它们,那么每个cpp文件都要编译一次vector头文件。

不应该使用预编译头文件的地方:那些会频繁更改的文件不应该,因为每次更改哪个文件,它都会重新编译到预编译头文件中,那么整个预编译头文件,么此都会重新构建一遍。

但像stl这样的外部的,不经常更改甚至不会更改的外部库,就更适合使用。它们有太多代码量,甚至可能比程序中的都多,所以它们没有理由不在预编译头文件中,不过,不怎么常用的外部库,比如一个项目中只有一个cpp文件包含,那么它也不应该包括在预编译头文件中

下面举一个具体例子演示使用 pch 的过程:

如图所示,假设pch.h文件中的头文件是一个大型项目中每个cpp都需要包含的头文件,那么每个cpp文件都要检查和编译至少几万行的代码量。

为了避免这样的情况,我们使用预编译头文件。

在vs中,有一个头文件,就要有一个包含该头文件的cpp文件,比如我们有个pch.cpp文件,对该文件右键-属性-C/C++-precompiled Headers,然后选择precompiled header为create,如下图

再到整个项目下的属性中选择使用

预编译头文件中添加pch.h

现在整个项目都使用该预编译头文件了,我们打开Main.cpp的属性检查,发现也应用上了,如图

这样就应用上了,以上就是所有的步骤,接下来检查以下提升了多少的编译时间


在工具-选项中打开下面的页面,打开生成计时 

项目属性中不使用预编译头文件

对项目clean,然后build,时长如下图

启用预编译头文件,再对项目clean,然后build,时长如下

提升了半秒效率,值得注意的是,这只是个很小的项目文件。 

动态强制类型转换

casting 强制类型转换

dynamic_cast作为c++风格的强制类型转换的一种安全机制,在程序开始执行时才转换,失败会返回NULL。专门用于继承层次结构的指针转换,且必须是多态类类型(基类有虚函数表)

它一般用于检查类型,比如现在有一个基类,以及该基类延申出的两个派生类。此时,有一个实体,我们不知道它的类型,假设它是派生类Player,然后用dynamic_cast 将它转换成派生类 Enemy,在程序执行时,转换会失败,并返回NULL,也就是0. 

比如误将Player转换为Enemy,那么调用Player中特有的一些变量和方法时就会造成程序崩溃,这种安全机制防范了这些问题。

如上图所示 ,运行 dynamic_cast的操作数必须包含多态类类型

#include <iostream>
#include <string>

class Entity
{
public:
	virtual void PrintName(){}
};

class Player : public Entity 
{
};

class Enemy : public Entity
{
};

int main()
{
	Player* player = new Player();
	Entity* entity = new Player();
	Enemy* e1 = new Enemy();
	//Player* p = e1;	//此行会报错,因为编译器不知道e1对象是不是enemy
	//所以我们要向编译器保证e1是player对象,如下
	//Player* p = (Player*)e1; //但是像这行这样做是危险的,因为e1实际上是Enemy,可能造成程序崩溃

	//因此,可以使用动态强制类型转换,如下
	if (Player* p = dynamic_cast<Player*>(e1))
		std::cout << "convertion success";
	else
		std::cout << "convertion failed";
	//Player* p = dynamic_cast<Player*>(e1)语句发生报错,因为 dynamic_cast只用于多态, 我们需要一个虚函数表,告诉我们实际上它是一个多态类类型,在基类中添加虚函数即可,即基类中有了需要重写的东西,意味着它是多态类型。
}

设置断点进行调试,可以看到p的值变为NULL

动态转换能实现这一点是因为它储存了运行时类型信息 runtime type information,它储存我们所有类型的运行时类型信息,这会增加额外的性能开销。

而 运行时类型信息 实际上是可以关闭的,如下:

不过关闭了就不能使用dynamic_cast了,要保持开启状态。

基准测试

有时要测试某程序的性能,会用到基准测试。

可以创建一个基于作用域自动销毁的Timer类,用花括号将要计时的内容包裹起来,在开始的地方放一个Timer类对象,具体代码如下:

#include <iostream>
#include <memory>

#include <chrono>

class Timer
{
private:
	std::chrono::time_point<std::chrono::high_resolution_clock> m_StartTimePoint;

public:
	Timer()
	{
		m_StartTimePoint = std::chrono::high_resolution_clock::now();
	}

	~Timer()
	{
		Stop();
	}

	void Stop()
	{
		auto EndTimePoint = std::chrono::high_resolution_clock::now();

		auto start = std::chrono::time_point_cast<std::chrono::microseconds>(m_StartTimePoint).time_since_epoch().count();
		/*有时时间单位不够小,如果程序是微秒级,用毫秒精度就没有意义,可以使用时间精度转换 time_point_cast
		time_since_epoch 是自时间起始点到现在的时长。然后接count()计数,返回longlong类型数据*/
		auto end = std::chrono::time_point_cast<std::chrono::microseconds>(EndTimePoint).time_since_epoch().count();

		auto duration = end - start;

		//如果我们想计算毫秒而非微秒
		double ms = duration * 0.001;

		std::cout << duration << "us(" << ms << "ms)\n";
	}
};

int main()
{
	int value = 0;

	{
		Timer timer_circulate;
		for (int i = 0; i < 1000000; i++)
			value += 2;
	}

	std::cout << "value:" << value;

	__debugbreak();	//这里是双下划线,vs为windouws设置的设置断点中断编译的代码
}

我们再用Timer测试一下不同智能指针初始化的耗时,理论上make_unique快于make_shared快于new出shared_ptr,测试代码如下:

#include <array>

int main()
{
	std::cout << std::endl << "//计算初始化各个智能指针的耗时" << std::endl;

	class Vector2
	{
	public:
		float x, y;
	};

	std::cout << "Make Shared: ";
	{
		std::array<std::shared_ptr<Vector2>, 1000> sharedPtrs1;

		Timer timer_sharedPtrs1;

		for (int i = 0; i < sharedPtrs1.size(); i++)
			sharedPtrs1[i] = std::make_shared<Vector2>();
	}

	std::cout << "New Shared: ";
	{
		std::array<std::shared_ptr<Vector2>, 1000> sharedPtrs2;

		Timer timer_sharedPtrs2;

		for (int i = 0; i < sharedPtrs2.size(); i++)
			sharedPtrs2[i] = std::shared_ptr<Vector2>(new Vector2());
	}

	std::cout << "Make Unique: ";
	{
		std::array<std::unique_ptr<Vector2>, 1000> uniquePtrs;

		Timer timer_uniquePtrs;

		for (int i = 0; i < uniquePtrs.size(); i++)
			uniquePtrs[i] = std::make_unique<Vector2>();
	}
}

测试结果如下:

基本符合理论情况,不过debug下编译器做了很多工作确保安全,换成release模式运行结果如下:

一定要在发布(release)模式下测试性能!才有意义。 

结构化绑定

C++17新特性,确保它生效,需在项目属性中选择C++17标准,如下

对于处理多返回值,C++17之前和之后有不同处理方法,代码如下:

//C++17 特性:结构化绑定

#include <iostream>
#include <string>
#include <tuple>

struct Person
{
	std::string Name;
	int Age;
};

std::tuple<std::string, int> CreatPerson()
{
	return { "张锐", 20 };
}

int main()
{
	//---------之前的处理方法---------------
	auto person1 = CreatPerson();
	std::string& name1 = std::get<0>(person1);
	int age1 = std::get<1>(person1);

	std::string name2;
	int age2;
	std::tie(name2, age2) = CreatPerson();

	Person person2;
	person2.Name = "张锐";
	person2.Age = 20;

	//---------之后的处理方法---------------
	auto [name, age] = CreatPerson();

}

 结构化绑定显然更简洁。

optional,variant,any

optional:

C++17新特性,使用场景:目标值可能存在也可能不存在,比如读取文件并返回内容,可能读取成功有数据,读取成功无数据,读取不成功。实例代码如下

//C++17 新特性:optional

#include <iostream>
#include <fstream>
#include <optional>

std::optional<std::string> ReadFileAsString(const std::string& filepath/*, bool outSuccess*/)
{
	std::ifstream stream(filepath);

	if (stream)
	{
		std::string result;
		//read 读取过程略
		stream.close();
		//outSuccess = true;
		return result;
	}

	//outSuccess = false;
	return {}; //如果读取失败,返回一个optional:{}
}

int main()
{
	//bool fileOpenedSuccessfully;
	std::optional<std::string> data = ReadFileAsString("data.txt"/*, fileOpenedSuccessfully*/);
	/*if (data != "")
	{

	}*/	//一般的处理方法,但是不知道读取是否成功,文件本身可能就是空的,可以在ReadFileAsString函数中加入布尔参数
	
	/*if (fileOpenedSuccessfully)
	{

	}*/	//比空字符串好,但仍然不够好,用optional更好

	std::string value = data.value_or("Not presents");	//可以检测文本中是否有特定值,若没有则使用默认值
	std::cout << value << std::endl;

	if (data.has_value())	//或 if(data) 更简洁
	{
		std::cout << "file read successfully!\n" << data.value() << "\n";
	}
	else
	{
		std::cout << "file could not be opened!\n";
	}
}

1: result.has_value()判断数据是否存在, 通过result.value()获取数据
2: result.value_or(xxx)其中xxx作为默认值,如果存在数据返回数据,不存在返回xxx
3:通过if (result)判断数据是否存在

variant:

C++17新特性,单一变量存储多类型的数据

#include <variant>
std::variant<type1, type2> data;
data = type1(xxx)
类似于union,type1与type2表示存储的数据类型。

读取:
1: std::get<type>(data)
2: auto *value = std::get_if(type)(&data)
注:类型安全

any:

C++17新特性,存储任意类型的数据

#include <any>

class Entity
{
	float a, b;
};

int main()
{
	std::any data;
	
	data = 3;
	data = '2a';
	data = std::string("ok");
	data = new Entity();	//可以是any数据,所以叫any

	//std::string string = std::any_cast<std::string>(data);	//不知道为什么,编译时未抛出具体错误,而是运行时的未知错误
	data = std::string("Celine");
	std::string& string = std::any_cast<std::string&>(data);	//引用传递要确保模板参数也是引用形式
}

在variant和any中最好使用variant,它相当于any 的类型安全形式,而且不会动态分配内存也确保了它的性能效率好于any。

跟踪内存分配

重写new和delete,在其中设置断点,调试,打开调用堆栈窗口,如下图所示,即可在调用堆栈窗口内双击各语句追踪内存分配,如某字符串是不是使用了new分配。

还可以使用自建内存指标结构体监测目前的内存使用状况,总代码如下:

#include <iostream>
#include <memory>    //new/delete


struct AllocationMetrics	//中译:分配指标
{
	uint32_t TotalAllocated = 0;
	uint32_t TotalFreed = 0;

	uint32_t CurrentUsage() { return TotalAllocated - TotalFreed; }
};

static AllocationMetrics s_AllocationMetrics;


void* operator new(size_t size)
{
	s_AllocationMetrics.TotalAllocated += size;
	std::cout << "Allocating " << size << " bytes\n";

	return malloc(size);	//返回的是分配内存的初始地址
}

void operator delete(void* memory, size_t size)	//参数是[指向需要删除的内存地址的]指针
{
	s_AllocationMetrics.TotalFreed += size;
	std::cout << "freeing " << size << " bytes\n";

	free(memory);
}

struct Object
{
	 
};

static void PrintMemoryUsage()
{
	std::cout << "Current Usage: " << s_AllocationMetrics.CurrentUsage() << " bytes" <<std::endl;
}

int main()
{
	PrintMemoryUsage();
	{
		std::unique_ptr<Object> obj_uniPtr = std::make_unique<Object>();
		PrintMemoryUsage();
	}
	
	std::string string = "Bonsour";
	PrintMemoryUsage();

	Object* obj_ptr = new Object;
	delete obj_ptr;
	PrintMemoryUsage();
}

运行结果如下:

左值&右值

  • 左值是可寻址的变量,有持久性;
  • 右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。

左值和右值主要的区别之一是左值一般可以被修改(const修饰的不能),而右值不能。

  • 左值引用:引用一个对象;
  • 右值引用:就是必须绑定到右值的引用,C++11中右值引用可以实现“移动语义”,通过 && 获得右值引用。
int x = 6; // x是左值,6是右值
int &y = x; // 左值引用,y引用x

int &z1 = x * 6; // 错误,x*6是一个右值
const int &z2 =  x * 6; // 正确,可以将一个const引用绑定到一个右值

int &&z3 = x * 6; // 正确,右值引用
int &&z4 = x; // 错误,x是一个左值

右值引用和相关的移动语义是C++11标准中引入的最强大的特性之一,通过std::move()可以避免无谓的复制,提高程序性能。

//左值,右值
#include <iostream>
#include <string>

int GetValue()
{
	return 10;
}

void SetValue111(int& i)
{

}

void SetValue222(const int& j) 
{

}

void PrintName1(std::string& name)
{
	std::cout << "[lvalue]" << name << "\n";
}
void PrintName2(const std::string& name)	//常量引用参数以兼容右值和左值传递的参数,因为右值引用时也会自动创建一个变量
{
	std::cout << "[lvalue]" << name << "\n";
}
void PrintName3(std::string&& name)	//常量引用参数以兼容右值和左值传递的参数,因为右值引用时也会自动创建一个变量
{
	std::cout << "[rvalue]" << name << "\n";
}

void PrintInt(int&& Int)
{
	std::cout << Int << std::endl;
}

int main()
{
	int a;
	a = 10;
	//10 = a;
	
	int b;
	b = GetValue();
	//GetValue() = b;	//左值不能赋右值

	SetValue111(b);
	//SetValue111(10);	//引用不能传递右值参数

	SetValue222(b);
	SetValue222(10);

	std::string s1 = "Tom";
	std::string s2 = "Cruson";
	std::string name = s1 + s2;	//s3是左值。右侧两个左值相加,但是组成了一个无地址临时字符串,即它们的和是右值

	PrintName1(name);
	//PrintName(s1 + s2);	//会报错,因为括号内是一个右值,所以很多c++程序使用PrintName2的常量引用参数以兼容右值和左值传递的参数
	//那么有没有一个函数参数,只接受临时对象,而不接受左值呢,那就是双引用符号&&

	PrintName2(name);
	PrintName2(s1 + s2);

	//那么有没有一个函数参数,只接受临时对象,而不接受左值呢,那就是双引用符号&&
	PrintName3(s1 + s2);
	//PrintName3(name);	//该函数不能传递左值参数
	PrintInt(4);
}

代码的运行结果如下:

移动语义

移动 而非复制,这对提升性能很有帮助。

举一个例子:

#include <iostream>

class String
{
private:
	uint32_t m_Size;
	char* m_Data;

public:
	String() = default;

	String(const char* string)
	{
		printf("Created!\n");
		m_Size = strlen(string);
		m_Data = new char[m_Size];
		memcpy(m_Data, string, m_Size);
	}

	String(const String& other)
	{
		printf("Copied!\n");
		m_Size = other.m_Size;
		m_Data = new char[m_Size];
		memcpy(m_Data, other.m_Data, m_Size);
	}

	~String()
	{
		delete[] m_Data;
	}

	void Print()
	{
		for (uint32_t i = 0; i < m_Size; i++)
		{
			printf("%c", m_Data[i]);
		}
		printf("\n");
	}
};

class Entity
{
private:
	String m_Name;

public:
	Entity(const String& name)
		: m_Name(name){}    //在这一行造成了String类实例的复制

	void PrintName()
	{
		m_Name.Print();
	}
};


int main()
{
	Entity entity(String("Celine"));
	entity.PrintName();
}

如上代码段有如下的运行结果:

可以看到数据被复制了,对于一个int的数据可能无所谓,但在大型项目中,大型类或其他在堆上分配内存类型的多次复制会造成很多的性能损耗,本例中复制构造函数被调用,即该类被整个复制了。 这时,我们需要一个移动(move)构造函数,让实例仅仅移动到另一个空间以被使用而不是被复制以被使用。如下

	String(String&& other)
	{
		printf("Moved!\n");
		m_Size = other.m_Size;
		m_Data = other.m_Data;	//不再是分配新空间然后复制整个数据块,而是将原字符串地址指针赋值给新字符串

		//但是因为新旧字符数组指向的是同一数据块,如~String折构函数,这个数据块在旧字符串也就是传递的临时值消失后会被清除
		//所以在新字符数组指向旧字符数组数据块之后,需要设置旧字符数组的size为0,并指向空指针,这样删除它的数据就没有任何影响了
		other.m_Size = 0;
		other.m_Data = nullptr;	//相当于指向了一个空对象
	}

运行结果如下:

String类还是被复制了,即问题并没有解决。这是因为

    Entity(String&& name)
        : m_Name(name){}    //在这一行造成了String类实例的复制 

造成了复制,必须显示地将name转换成一个临时对象,即

    Entity(String&& name)
        : m_Name((String&&)name){}  

修改代码后的运行结果如下:  

不过,在实际写代码中,往往用的不是这样的转换为 右值的方法,而是使用std::move,它本质上做的是同样的事情 ,如下

        Entity(String&& name)
                : m_Name(std::move(name)) {}

每个表达式都有两种特征:一是类型二是值类别。很多人迷惑的右值引用为啥是个左值,那是因为右值引用是它的类型,左值是它的值类别。
想理解右值首先要先知道类型和值类别的区别;其次是各个值类别的定义是满足了某种形式它就是那个类别,经常说的能取地址就是左值,否则就是右值,这是定义之上的不严谨经验总结,换句话说,是左值还是右值是强行规定好的,你只需要对照标准看这个表达式满足什么形式就知道它是什么值类别了。
为什么要有这个分类,是为了语义,当一个表达式出现的形式表示它是一个右值,就是告诉编译器,我以后不会再用到这个资源,放心大胆的转移销毁,这就可以做优化,比如节省拷贝之类的。
move的作用是无条件的把表达式转成右值,也就是rvalue_cast,虽然编译器可以推断出左右值,但人有时比编译器“聪明”,人知道这个表达式的值以后我不会用到,所以可以在正常情况下会推成左值的地方强行告诉编译器,我这是个右值,请你按右值的语义来做事。

同时,移动赋值操作符也会是一个你需要包含在类中的东西,它可以将一个类实例移动到一个已有类实例中去,本例String类的移动赋值操作符重载代码如下:

	String& operator=(String&& other)
	{
		printf("Moved!\n");

		if(this != &other)	//若相等,删除自己后将自己移动给自己,一定会有内存问题
		{
			delete[] m_Data;	//先释放原来指向空间的数据,以免造成内存泄漏

			m_Size = other.m_Size;
			m_Data = other.m_Data;	//不再是分配新空间然后复制整个数据块,而是将原字符串地址指针赋值给新字符串

			//但是因为新旧字符数组指向的是同一数据块,如~String折构函数,这个数据块在旧字符串也就是传递的临时值消失后会被清除
			//所以在新字符数组指向旧字符数组数据块之后,需要设置旧字符数组的size为0,并指向空指针,这样删除它的数据就没有任何影响了
			other.m_Size = 0;
			other.m_Data = nullptr;	//相当于指向了一个空对象
		}

下面为演示代码即运行结果:

	std::cout << "-------------------------------" << std::endl;
	String string = "Hello";
	String destination;
	std::cout << "string:";			string.Print();
	std::cout << "destination:";	destination.Print();
	std::cout << "-------------------------------" << std::endl;

	destination = std::move(string);
	std::cout << "string:";			string.Print();
	std::cout << "destination:";	destination.Print();

关于移动构造函数和移动赋值运算符,要加以区分,因为以下两个语句是很像的:

	String string2 = "Hello2";
	String destination2 = std::move(string2);
	//前面加了String,因为是在构造一个String类实例,这就是调用了移动构造函数,而不是移动赋值运算符,要加以区分

    String string2 = "Hello2";
    String destination2;
    destination2 = std::move(string2);
    //也可以用下面这种写法,而移动构造函数不能
    destination2.operator=(std::move(string2));

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值