C++Cherno 学习笔记day21 [86]-[90] 持续集成、静态分析、参数计算顺序、移动语义、stdmove与移动赋值操作符

b站Cherno的课[86]-[90]

一、C++持续集成

Jenkins 商业软件

二、C++静态分析

静态分析器会检查你的代码,并尝试检测各种错误,这些错误

可能是你无意中编写的,有点像代码复查,但不是由人来做

PVS-Studio 商业软件

三、C++的参数计算顺序

(参数求值顺序)

argument evaluation order

未定义的行为

也就是说它会根据编译器的不同而变化,完全依赖于C++编译器将代码转换成机器码的实际实现

cpp 复制代码
#include <iostream>

void PrintSum(int a, int b)
{
	std::cout << a << "+" << b << "=" << (a + b) << std::endl;
}

int main()
{
	int value = 0;
	PrintSum(value++, value++);
	std::cin.get();
}

release模式下的常数折叠

C++标准添加了一个从C++17开始的新规则

后缀表达式必须在别的表达式之前被计算

wandbox.org 在线编译网站

C++的参数计算顺序:没有定义

因为c++实际上并没有提供c++规范,并没有提供一个定义,来说明在这种情况下应该发生什么,参数(形参)或实参应该按照什么顺序求值

但如果你提到c++17说了这两件事不能同时做(同时计算)

那就加分了

也就是说,

它们必须一个接一个地完成

但是再说一次,,这个顺序并没有在规范中定义

这意味着你在技术上无法知道计算顺序是什么

四、C++移动语义

左值与右值

移动语义本质上允许我们移动对象

cpp 复制代码
#include <iostream>

class String
{
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");
	}

private:
	char* m_Data;
	uint32_t m_Size;
};

class Entity
{
public:
	Entity(const String& name)
		:m_Name(name)
	{

	}
	void PrintName()
	{
		m_Name.Print();
	}
private:
	String m_Name;
};

int main()
{
	Entity entity(String("wm"));
	entity.PrintName();
	std::cin.get();
}
cpp 复制代码
#include <iostream>

class String
{
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(String&& other) noexcept
	{
		printf("Moved!\n");
		m_Size = other.m_Size;
		m_Data = other.m_Data;

		other.m_Size = 0;
		other.m_Data = nullptr;
	}

	~String()
	{
		printf("Destroyed!\n");
		delete m_Data;
	}

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

private:
	char* m_Data;
	uint32_t m_Size;
};

class Entity
{
public:
	Entity(const String& name)
		:m_Name(name)
	{

	}

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

	}

	void PrintName()
	{
		m_Name.Print();
	}
private:
	String m_Name;
};

int main()
{
	Entity entity(String("wm"));
	entity.PrintName();
	std::cin.get();
}
cpp 复制代码
#include <iostream>

class String
{
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(String&& other) noexcept
	{
		printf("Moved!\n");
		m_Size = other.m_Size;
		m_Data = other.m_Data;

		other.m_Size = 0;
		other.m_Data = nullptr;
	}

	~String()
	{
		printf("Destroyed!\n");
		delete m_Data;
	}

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

private:
	char* m_Data;
	uint32_t m_Size;
};

class Entity
{
public:
	Entity(const String& name)
		:m_Name(name)
	{

	}

	Entity(String&& name)
	   // 只修改了此处,输出Moved
		//:m_Name((String&&)name)
		:m_Name((std::move)name)
	{

	}

	void PrintName()
	{
		m_Name.Print();
	}
private:
	String m_Name;
};

int main()
{
	Entity entity(String("wm"));
	entity.PrintName();
	std::cin.get();
}

Entity(String&& name)

:m_Name(name)

{

}

这里的问题是,此构造函数的参数name应该是一个右值(String&&),所以m_Name(name)应该是调用的String的移动构造函数,为啥要m_Name(std::move(name)) 才行

因为右值引用在进入函数体内的之后,参数类型会变为左值

也就是在函数体内你可以对 String&& val中的 val取&

所以要触发移动语义必须让他变为右值引用,std::move就是干这个转换的

你看std::move的代码 就是干了一个找到源参数类型的static_cast转换

把参数换成了&&类型

所以就能触发后面的移动构造函数

每个表达式都有两种特征:一是类型二是值类别。

很多人迷惑的右值引用为啥是个左值,那是因为右值引用是它的类型,左值是它的值类别。

想理解右值首先要先知道类型和值类别的区别;其次是各个值类别的定义是满足了某种形式它就是那个类别,经常说的能取地址就是左值,否则就是右值,这是定义之上的不严谨经验总结,换句话说,是左值还是右值是强行规定好的,你只需要对照标准看这个表达式满足什么形式就知道它是什么值类别了。

为什么要有这个分类,是为了语义,当一个表达式出现的形式表示它是一个右值,就是告诉编译器,我以后不会再用到这个资源,放心大胆的转移销毁,这就可以做优化,比如节省拷贝之类的。

move的作用是无条件的把表达式转成右值,也就是rvalue_cast,虽然编译器可以推断出左右值,但人有时比编译器"聪明",人知道这个表达式的值以后我不会用到,所以可以在正常情况下会推成左值的地方强行告诉编译器,我这是个右值,请你按右值的语义来做事。

五、stdmove与移动赋值操作符

移动语义能够将一个对象移动到另一个对象上

move assignment operator(移动赋值运算符)

cpp 复制代码
#include <iostream>

class String
{
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(String&& other) noexcept
	{
		printf("Moved!\n");
		m_Size = other.m_Size;
		m_Data = other.m_Data;

		other.m_Size = 0;
		other.m_Data = nullptr;
	}

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

		if (this != &other)
		{
			delete[] m_Data;

			m_Size = other.m_Size;
			m_Data = other.m_Data;

			other.m_Size = 0;
			other.m_Data = nullptr;
		}
		return *this;
	}

	~String()
	{
		printf("Destroyed!\n");
		delete m_Data;
	}

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

private:
	char* m_Data;
	uint32_t m_Size;
};

class Entity
{
public:
	Entity(const String& name)
		:m_Name(name)
	{

	}

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

	}

	void PrintName()
	{
		m_Name.Print();
	}
private:
	String m_Name;
};

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

	String apple = "Apple";
	String dest;

	//String string = "Hello";
	//String dest((String&&)string);
	//String dest = (String&&)string;
 
	//String dest(std::move(string));
	//String dest = std::move(string);

	//dest.assign(std::move(string));
	//dest = std::move(dest);

	apple.Print();
	dest.Print();

	dest = std::move(apple);

	apple.Print();
	dest.Print();
	
	std::cin.get();
}
cpp 复制代码
int main()
{
	String apple = "Apple";
	String dest;

	std::cout << "Apple: ";
	apple.Print();
	std::cout << "Dest: ";
	dest.Print();

	dest = std::move(apple);

	std::cout << "Apple: ";
	apple.Print();
	std::cout << "Dest: ";
	dest.Print();

	std::cin.get();
}

总而言之

移动赋值操作符是你想要包含在类中的东西,当你包含一个移动构造函数时,因为它当然是想要将一个对象移动到一个现有的变量中

它基本上是五法则的一部分,还有三法则,五法则包含了所有的新移动语义

注:

C++三法则:如果需要析构函数,则一定需要拷贝构造函数和拷贝赋值操作符C++五法则:为了支持移动语义,又增加了移动构造函数和移动赋值运算符

赋值操作符与使用构造函数的区别:

通过使用这个运算符,它就像我们写.operator=,并像调用函数一样调用它

std:move是你想要将一个对象转换为临时对象时要做的

换句话说,如果你需要把一个已经存在的变量变成临时变量

你可以标记它,表示你可以从这个特定的变量中窃取资源

这使我们能够在现有的变量上执行移动操作

如果你在创建一个新变量,如果是在函数参数中或在移动构造函数中(创建),那么它已经是一个临时变量,这是可以的

但如果你有一个已经存在的变量,比如这个apple的例子

这是一个已经存在的变量,你需要确保使用std:move来将它转换成一个临时变量

这样你就可以使用移动构造函数或移动赋值操作符,从那个变量中获取资源,并进行移动

相关推荐
kchmmd10 分钟前
CMakeLists生成调用静态动态库可执行demo
c++
华清远见成都中心15 分钟前
嵌入式开发入门指南:从零开始学习嵌入式
学习·嵌入式
武昌库里写JAVA18 分钟前
MacOS Python3安装
java·开发语言·spring boot·学习·课程设计
cdut_suye26 分钟前
【Linux系统】从 C 语言文件操作到系统调用的核心原理
java·linux·数据结构·c++·人工智能·机器学习·云计算
wuqingshun31415926 分钟前
蓝桥杯 10. 全球变暖
c++·算法·职场和发展·蓝桥杯
Tummer836339 分钟前
C++四种类型转换:static_cast、 dynamic_cast const cast、 reinterpret_cast
c++
ghost1431 小时前
C#学习第22天:网络编程
开发语言·学习·c#
范纹杉想快点毕业1 小时前
以项目的方式学QT开发(三)——超详细讲解(120000多字详细讲解,涵盖qt大量知识)逐步更新!
c语言·开发语言·c++·qt·mysql·算法·命令模式
ALINX技术博客2 小时前
【ALINX 实战笔记】FPGA 大神 Adam Taylor 使用 ChipScope 调试 AMD Versal 设计
笔记·fpga开发
敲代码的瓦龙2 小时前
STL?list!!!
c语言·开发语言·数据结构·c++·windows·list