102如何正确搭建cpp项目

创建项目并克隆#

https://github.com/TheCherno/ProjectTemplate ,到这个地址 UseThisTemplate -> CreateNewRepository

在本地windows克隆下来

git clone git@github.com:lwmfjc/ChernoTemplateProject.git

之后我把项目删掉.git文件夹后放到了另一个自己的备份gitRepository中

使用#

双击Setup-Windows.bat 运行

在visual studio中,File-Open-Solution,选择 NewProject.sln

打开之后直接F5运行

==这集先跳过,貌似有点不太常用 注:看到00:07:34 ==

95如何真正学好cpp

核心问题:学完基础后该做什么?#

  • 现状:许多人看完了视频、读完了教科书、甚至通过了大学考试,但依然觉得不会自由地编写代码,无法“流利”地使用 C++。
  • 核心建议:沉浸在开源项目(Open Source Projects)中。

真实世界的代码 vs. 教科书#

  • 差异:教科书和作业里的代码(如实现一个 Vector 或 List)是教学性质的,而现实世界的代码规模更大、逻辑更复杂。
  • 面试视角:雇主想看到的是你理解软件是如何构建的,以及你是否见过并能处理真实代码库中的复杂性。

“语言学习”的类比#

  • 学习环境:学习编程就像学外语(德语或日语)。只看语法书和做练习是不够的,你必须开始看电视、电影,甚至搬到那个国家居住。
  • 编程沉浸:对于 C++,这意味着你需要阅读、运行并尝试修改别人的代码,让自己置身于那个语言环境中。

如何开始?选择你感兴趣的项目#

  • 兴趣导向:
    • 喜欢游戏?去 GitHub 找简单的开源游戏(如 Mario 克隆版或 80 年代街机游戏)。
    • 喜欢引擎?研究 Unreal Engine、Godot 或 Cherno 自己的 Hazel 引擎。
  • Bug 赏金计划 :参与寻找 C++ 代码中的 bug,这不仅能锻炼能力,有时还能获得报酬。

什么时候可以开始“沉浸”?#

结论:随时。 即使你觉得还太早,也不必担心。就像没学多少日语就去日本一样,虽然痛苦,但那是进步最快的方式。

实战演示:分析 OpenCV 源码#

  • 流程:从 GitHub 克隆项目 -> 使用 CMake 生成 VS 工程 -> 编译并运行。
  • 工具辅助(PVS-Studio) :Cherno 演示了使用静态分析工具查看 OpenCV 源码中的潜在错误。
  • 学习点 :例如通过分析宏定义中的 if 语句冗余问题,你可以学到更深层的编码经验,这些是只看语法书学不到的。

总结:超越代码本身的收获#

通过阅读开源项目,你不仅学会了写 C++,还学会了:

94编写迭代器

  • 迭代器是用来遍历某种数据结构的容器的
  • 迭代器统一了接口,所以通过同样的接口不同的实现来迭代不同的容器
  • 迭代器:记录一个指针,然后遍历就像移动指针一样,直到末尾

Vector类#

为Vector添加部分代码

class Vector
{
public:

	//Vector的值类型
	using ValueType = T;
	//public 类型别名 (using/typedef):属于类(Class/Type)。它是一个“类型标签”,就像 Vector<int> 内部定义的一个子类型。
	using Iterator = VectorIterator<Vector<T>>;

	Iterator begin()
	{
		return Iterator(m_Data);
	}


	Iterator  end()
	{
		return Iterator(m_Data + m_Size);
	}
}

Vector类完整代码

#pragma once
#ifdef LY_EP94
#include <memory>
#include <iostream>

//为某种类型(Vector)写迭代器
template<typename Vector>
class VectorIterator
{
public:
	//Vector的值的类型
	using ValueType = typename Vector::ValueType;
	using PointerType = ValueType*;
	using ReferenceType = ValueType&;


	VectorIterator(PointerType ptr)
		:m_Ptr(ptr)
	{

	}

	//前缀运算符
	VectorIterator& operator++()
	{
		m_Ptr++;
		return *this;
	}

	//前缀运算符
	VectorIterator& operator--()
	{
		m_Ptr--;
		return *this;
	}

	//后缀运算符
	//编译器用来区分前缀和后缀的唯一手段,int
	//是语法占位符
	VectorIterator& operator++(int)
	{
		//这里是复制,由于成员只有一个指针,代价可以接受
		VectorIterator iterator = *this;
		//前缀运算符
		++(*this);
		return iterator;
	}

	//后缀运算符
	VectorIterator& operator--(int)
	{
		//这里是复制,由于成员只有一个指针,代价可以接受
		VectorIterator iterator = *this;
		//前缀运算符
		--(*this);
		return iterator;
	}

	ReferenceType operator[](int index)
	{
		return *(m_Ptr + index);
	}

	//C++ 对 -> 有特殊处理。当你使用 it->Method() 时,它会调用 it.operator->() 得到 m_Ptr,然后再次对结果应用 ->。所以最终执行的是 m_Ptr->Method()。
	//所以这里需要返回一个指针
	PointerType operator->()
	{
		return m_Ptr;
	}


	ReferenceType operator*()
	{
		return *m_Ptr;
	}

	bool operator==(const VectorIterator& other) const
	{
		return m_Ptr == other.m_Ptr;
	}
	bool operator!=(const VectorIterator& other) const
	{
		return !(*this == other);
	}

private:
	PointerType m_Ptr;
};

template<typename T>
class Vector
{
public:

	//Vector的值类型
	using ValueType = T;
	//public 类型别名 (using/typedef):属于类(Class/Type)。它是一个“类型标签”,就像 Vector<int> 内部定义的一个子类型。
	using Iterator = VectorIterator<Vector<T>>;

	Iterator begin()
	{
		return Iterator(m_Data);
	}


	Iterator  end()
	{
		return Iterator(m_Data + m_Size);
	}

public:

	Vector()
	{
		//allocate 2 elements
		ReAlloc(2);
		std::cout << "====构造器中初始化容量2======" << std::endl;
	}

	void PushBack(const T& value)
	{
		//如果当前数量量已经大于等于容量
		if (m_Size >= m_Capacity)
		{
			//如果增长,容量扩大为1.5倍
			ReAlloc(m_Capacity + m_Capacity / 2);
		}

		//m_Data[m_Size] 是一个已经存在的对象,所以
		//这里调用的是拷贝赋值函数
		//m_Data[m_Size] = value;

		// new (&地址) 类型(参数)
		new(&m_Data[m_Size]) T(value);

		m_Size++;
		std::cout << "添加元素:" << value << std::endl;
		std::cout << "==================" << std::endl;
	}

	//1. PushBack的push右值引用的版本
	//2. value 的类型始终是 Vector3&&(右值引用),
	//   但它在函数体内的表现(属性)是一个左值
	void PushBack(T&& value)
	{
		//如果当前数量量已经大于等于容量
		if (m_Size >= m_Capacity)
		{
			//如果增长,容量扩大为1.5倍
			ReAlloc(m_Capacity + m_Capacity / 2);
		}

		//1. m_Data[m_Size] 是一个已经存在的对象,所以
		//这里调用的是移动赋值函数
		//2. value 传入时是右值引用,但在函数内部它有了名
		// 字,就变成了左值。为了触发移动赋值,
		// 必须用 std::move 把它重新转回右值。
		//3. 如果没有移动赋值函数,会去调用拷贝赋值函数
		//m_Data[m_Size] = std::move(value);

		// 使用移动构造函数在原始内存上创建对象
		new(&m_Data[m_Size]) T(std::move(value));

		m_Size++;
		std::cout << "添加元素:" << value << std::endl;
		std::cout << "==================" << std::endl;
	}

	//变长参数模板,三个点 ... 表示它可以接收
	//   任意数量、任意类型的参数
	template<typename... Args>
	//&& 配合模板参数 Args 使用时,不再仅仅是
	//   右值引用,它被称为万能引用,既能接收左值
	//   ,也能接收右值
	T& EmplaceBack(Args&&... args)
	{

		//如果当前数量量已经大于等于容量
		if (m_Size >= m_Capacity)
		{
			//如果增长,容量扩大为1.5倍
			ReAlloc(m_Capacity + m_Capacity / 2);
		}
		//1. 参数一旦有了名字 args,它在函数内部就会变成左值。
		//2. 解决:std::forward 的作用是:如果你传进来时是右值,
		//   它就把它恢复成右值;如果你传进来时是左值,它就保持左
		//   值。
		//3. T(std::forward<Args>(args)...)这个是右值,调用移动
		//   赋值函数后,马上destroy了

		//m_Data[m_Size] = T(std::forward<Args>(args)...); 

		//使用placement new运算符,原地构造,就不会有对临时对象move以及destroy的操作了
		new(&m_Data[m_Size]) T(std::forward<Args>(args)...);

		std::cout << "添加元素:" << m_Data[m_Size] << std::endl;
		std::cout << "==================" << std::endl;

		//1. 取值(Evaluate):首先取出 m_Size 当前的值(比如现在是 0)。
		//2. 定位(Access):利用这个旧值 0 找到 m_Data[0] 的引用,作为函数的返回值准备好。
		//3. 自增(Increment):在这一行语句的逻辑完成,“返回”动作即将发生前(但还在函数体内),将 m_Size 的值加 1 变成 1。
		return m_Data[m_Size++];
	}

	void PopBack()
	{
		if (m_Size > 0)
		{
			m_Size--;
			//手动调用析构函数
			m_Data[m_Size].~T();
		}
	}

	void Clear()
	{
		for (size_t i = 0; i < m_Size; i++)
			m_Data[i].~T();
		m_Size = 0;
	}

	T& operator[](size_t index)
	{
		if (index >= m_Size)
		{
			//assert
		}
		return m_Data[index];
	}

	const T& operator[](size_t index) const
	{
		if (index >= m_Size)
		{
			//assert被设计为一种调试期工具,在发布版本会被去掉
			//assert
		}
		return m_Data[index];
	}

	size_t Size() const { return m_Size; }

	~Vector()
	{
		//会依次调用m_Data指向的所有对象的析构函数(
		// 但是PopBack及Clear()已经调用过并已经释放了
		// Vector3的m_MemoryBlock

		Clear();
		//		delete[] m_Data;

		::operator delete(m_Data, m_Capacity * sizeof(T));

	}
private:
	void ReAlloc(size_t newCapacity) {
		// 1. allocate a new block of memory
		// 2. copy/move old elements into new block
		// 3. delete

		//1. 尽可能低层次的访问内存,而不是智能指针
		//2. 在堆上申请了一块连续(物理连续)的内存空间
		//3. 在堆上创建了 newCapacity 个对象,并调用了它们
		//的默认构造函数。也就是说,此时内存里已经存在了一
		//堆“空”的 Vector3 对象
		//T* newBlock = new T[newCapacity];

		//分配原始字节内存,不调用任何构造函数
		T* newBlock = (T*)::operator new(newCapacity * sizeof(T));

		//如果新容量小于当前大小,即缩小容量
		if (newCapacity < m_Size)
		{
			// 当前大小设置为新容量大小
			m_Size = newCapacity;
			std::cout << "缩小容量--" << std::endl;
		}
		else
		{

			std::cout << "增大容量--" << m_Capacity << "->" << newCapacity << std::endl;
		}

		for (size_t i = 0; i < m_Size; i++)
		{
			//不会触发复制构造函数
			//memcpy(newBlock, m_Data, i * sizeof(T);

			//复制,会触发复制构造函数
			//newBlock[i] = std::move(m_Data[i]);

			// 在 newBlock 的位置上,“构造”一个新对象,其内容移动自旧对象
			new(&newBlock[i]) T(std::move(m_Data[i]));
		}

		//手动释放原来的对象
		for (size_t i = 0; i < m_Size; i++)
		{
			m_Data[i].~T();
		}

		//1. 逐一析构:编译器会根据该内存块记录的大小信息(通常存储在数组头部的隐藏偏移量中),从后往前(或从前往后,取决于实现)调用每一个 Vector3 对象的析构函数。
		//2. 释放内存:在所有对象的析构函数执行完毕后,才会一次性将整块堆内存归还给操作系统。
		//delete[] m_Data;//释放原来指向的内存块

		::operator delete(m_Data, m_Capacity * sizeof(T));

		//delete m_Data;只会触发第一个元素的析构函数

		m_Data = newBlock;//指向新的那个内存块
		m_Capacity = newCapacity;

	}



	T* m_Data = nullptr;
	//实际数组个数
	size_t m_Size = 0;
	//可分配存储的元素个数
	size_t m_Capacity = 0;
};
#endif

Main使用#

#ifdef LY_EP94

#include <iostream>   
#include <vector>
#include "94_01_vector.h"

int main()
{  
	Vector<int> values;
	values.EmplaceBack(1);
	values.EmplaceBack(2);
	values.EmplaceBack(3);
	values.EmplaceBack(4);
	values.EmplaceBack(5);

	std::cout << "Not using iterators:\n";
	for (int i = 0; i < values.Size(); i++)
	{
		std::cout << values[i] << std::endl;
	}

	std::cout << "Range-based for loop" << std::endl;
	for (int value : values)
	{
		std::cout << value << std::endl;

	}

	std::cout << "Iterator:\n";
	for (Vector<int>::Iterator it = values.begin();
		it != values.end(); it++)
	{
		std::cout << *it << std::endl;
	}

	std::cin.get();
	return 0;
}
#endif;

92动态数组实现

涉及到了手动内存管理堆分配以及模板类的深层逻辑。

简要概述#

  • 核心目标:实现动态扩容
    • 作者明确了本集的目标:创建一个可以根据需要自动增长内存空间的容器,模拟 std::vector 的核心行为。
  • 成员变量的改变:从栈到堆
    • 不再使用 T m_Data[S],而是改为 T* m_Data 指针。
    • 引入两个关键计数器:m_Size(当前元素个数)和 m_Capacity(当前分配的总空间)。
  • 构造函数与析构函数
    • 在构造函数中预分配一小块初始内存
    • 强调了析构函数的重要性:必须使用 delete[] m_Data 释放堆内存,否则会导致内存泄漏。
  • 实现 PushBack 方法(核心逻辑)
    • 这是本集最精彩的部分:当 m_Size >= m_Capacity 时,触发“重新分配(Reallocation)”。
    • 扩容策略:作者演示了将容量翻倍(m_Capacity * 2)的常用工程策略。
  • 手动执行内存迁移
    • 详细步骤:申请一块更大的新内存 -> 将旧数据拷贝到新内存 -> 释放旧内存 -> 指针重定向。
  • 模板化的挑战
    • 讨论了为什么 Vector 需要模板化,以及在处理非简单类型(如 std::string)时,简单的拷贝可能会失效(引出对后续移动语义的思考)。
  • 性能测试与优化预告
    • 对比了自定义 Vector 与标准库 std::vector 的性能。
    • 提到目前的实现虽然可用,但频繁的拷贝开销巨大,引出后续关于 emplace_back 的话题。

代码vector.h#

//91_01_vector.h
#pragma once
#ifdef LY_EP92
#include <memory>

template<typename T>
class Vector
{
public:
	Vector()
	{
		//allocate 2 elements
		ReAlloc(2);
		std::cout << "====构造器中初始化容量2======" << std::endl;
	}

	void PushBack(const T& value)
	{
		//如果当前数量量已经大于等于容量
		if (m_Size >= m_Capacity)
		{
			//如果增长,容量扩大为1.5倍
			ReAlloc(m_Capacity + m_Capacity / 2);
		}

		//m_Data[m_Size] 是一个已经存在的对象,所以
		//这里调用的是拷贝赋值函数
		m_Data[m_Size] = value;
		m_Size++;
		std::cout << "添加元素:" << value << std::endl;
		std::cout << "==================" << std::endl;
	}

	T& operator[](size_t index)
	{
		if (index >= m_Size)
		{
			//assert
		}
		return m_Data[index];
	}

	const T& operator[](size_t index) const
	{
		if (index >= m_Size)
		{
			//assert被设计为一种调试期工具,在发布版本会被去掉
			//assert
		}
		return m_Data[index];
	}

	size_t Size() const { return m_Size; }

	~Vector()
	{
		delete[] m_Data;  
	}
private:
	void ReAlloc(size_t newCapacity) {
		// 1. allocate a new block of memory
		// 2. copy/move old elements into new block
		// 3. delete

		//1. 尽可能低层次的访问内存,而不是智能指针
		//2. 在堆上申请了一块连续(物理连续)的内存空间
		//3. 在堆上创建了 newCapacity 个对象,并调用了它们
		//的默认构造函数。也就是说,此时内存里已经存在了一
		//堆“空”的 Vector3 对象
		T* newBlock = new T[newCapacity];

		//如果新容量小于当前大小,即缩小容量
		if (newCapacity < m_Size)
		{
			// 当前大小设置为新容量大小
			m_Size = newCapacity;
			std::cout << "缩小容量--" << std::endl;
		}
		else
		{

			std::cout << "增大容量--" << m_Capacity << "->" << newCapacity << std::endl;
		}

		for (size_t i = 0; i < m_Size; i++)
		{
			//不会触发复制构造函数
			//memcpy(newBlock, m_Data, i * sizeof(T);

			//复制,会触发复制构造函数
			newBlock[i] = m_Data[i];
		}

		//1. 逐一析构:编译器会根据该内存块记录的大小信息(通常存储在数组头部的隐藏偏移量中),从后往前(或从前往后,取决于实现)调用每一个 Vector3 对象的析构函数。
		//2. 释放内存:在所有对象的析构函数执行完毕后,才会一次性将整块堆内存归还给操作系统。
		delete[] m_Data;//释放原来指向的内存块

		//delete m_Data;只会触发第一个元素的析构函数

		m_Data = newBlock;//指向新的那个内存块
		m_Capacity = newCapacity;

	}



	T* m_Data = nullptr;
	//实际数组个数
	size_t m_Size = 0;
	//可分配存储的元素个数
	size_t m_Capacity = 0;
};
#endif

应用:添加std::string元素#

#ifdef LY_EP92
//在堆上分配内存,创建动态数组
//需要一个指向堆分配内存(数据缓冲区)的指针,随着不断
//添加,当元素越来越多会达到临界点,没有足够空间存储新元素时,
//分配一个新的内存块使它有足够空间容纳这个新元素,将内存块中所有元素
//复制到新内存块,然后删除旧内存块

#include <iostream>   
#include <string>
#include "92_01_vector.h"

template<typename T>
void PrintVector(const Vector<T>& vector)
{
	for (size_t i = 0; i < vector.Size(); i++)
	{
		std::cout << vector[i] << std::endl;
	}

	std::cout << "==================" << std::endl;
}

int main()
{
	Vector<std::string> vector;
	vector.PushBack("Cherno");
	vector.PushBack("C++");
	vector.PushBack("Vector01");
	vector.PushBack("Vector02");
	vector.PushBack("Vector03");
	vector.PushBack("Vector04");
	vector.PushBack("Vector05");
	vector.PushBack("Vector06");
	vector.PushBack("Vector07");

	PrintVector(vector);
	std::cin.get();
	return 0;
}
/*
增大容量--0->2
====构造器中初始化容量2======
添加元素:Cherno
==================
添加元素:C++
==================
增大容量--2->3
添加元素:Vector01
==================
增大容量--3->4
添加元素:Vector02
==================
增大容量--4->6
添加元素:Vector03
==================
添加元素:Vector04
==================
增大容量--6->9
添加元素:Vector05
==================
添加元素:Vector06
==================
添加元素:Vector07
==================
Cherno
C++
Vector01
Vector02
Vector03
Vector04
Vector05
Vector06
Vector07
==================

*/

#endif

应用添加自定义类元素#

#ifdef LY_EP92

#include <iostream>   
#include <string>
#include "92_01_vector.h"


struct Vector3
{
	float x = 0.0f, y = 0.0f, z = 0.0f;

	Vector3() {}
	Vector3(float scalar)
		: x(scalar), y(scalar), z(scalar) {
	}
	Vector3(float x, float y, float z)
		: x(x), y(y), z(z) {
	}

	// 拷贝构造函数 (Copy Constructor)
	Vector3(const Vector3& other)
		: x(other.x), y(other.y), z(other.z)
	{
		std::cout << "Copy\n";
	}


	//移动构造函数 (move Constructor)
	Vector3(Vector3&& other)
		: x(other.x), y(other.y), z(other.z)
	{
		std::cout << "Move\n";
	}

	//拷贝赋值
	Vector3& operator=(const Vector3& other)
	{
		std::cout << "copy=\n";
		x = other.x;
		y = other.y;
		z = other.z;
		return *this;
	}

	//移动赋值
	Vector3& operator=(Vector3&& other)
	{
		std::cout << "move=\n";
		x = other.x;
		y = other.y;
		z = other.z;
		return *this;
	}

	~Vector3()
	{
		std::cout << "Destroy\n";
	}
};

//全局重载函数,C++ 规定:对于二元运算符(有两个操作数的运算符),如果写成全局函数,第一个参数对应运算符左边的对象,第二个参数对应右边的对象
std::ostream& operator<<(std::ostream& stream, const Vector3& v)
{
	stream << v.x << ", " << v.y << ", " << v.z;
	return stream;
}

void PrintVector(const Vector<Vector3>& vector)
{
	for (size_t i = 0; i < vector.Size(); i++)
	{
		std::cout << vector[i] << std::endl;
	}

	std::cout << "==================" << std::endl;
}

int main()
{
	Vector<Vector3> vector;

	//Vector3(1.0f)->创建一个匿名临时对象
	//临时对象会在包含它的那个“完整表达式(Full-expression)”结束时被销毁。即这行代码结束后该临时对象被析构
	vector.PushBack(Vector3(1.0f));

	vector.PushBack(Vector3{ 2,3,4 });
	vector.PushBack(Vector3{});


	PrintVector(vector);

	std::cin.get();
	return 0;
}
/*
增大容量--0->2
====构造器中初始化容量2======
copy=
添加元素:1, 1, 1
==================
Destroy
copy=
添加元素:2, 3, 4
==================
Destroy
增大容量--2->3
copy=
copy=
Destroy
Destroy
copy=
添加元素:0, 0, 0
==================
Destroy
1, 1, 1
2, 3, 4
0, 0, 0
==================
*/
#endif

如上,每次添加元素,以及扩容时的搬运原元素,都触发了复制赋值函数

91静态数组实现

  • 为什么要手写数据结构?
    • 作者解释说,虽然 std::array 很好用,但作为底层程序员,你应该了解它在堆栈(Stack)上是如何分配内存的。这有助于你理解内存布局和模板编程
  • 模板类(Template Class)的声明
    • 开始编写 Array<T, S>
    • 重点:这里的 T 是元素类型,S编译期常量(数组大小)。作者解释了为什么在 C++ 中通过模板传递数组大小比通过构造函数传递更高效(因为它在编译时就确定了内存空间)
  • 内存分配的核心:原始数组
    • 在类内部定义 T m_Data[S]
    • 关键点:这证明了 Array 是分配在上的,其生命周期由作用域决定。
  • 实现 Size() 方法
    • 定义一个简单的 constexpr size_t Size() const { return S; }
    • 强调了 constexpr 的作用,让这个大小在编译阶段就能被其他代码使用。
  • 运算符重载:实现 operator[]
    • 为了像使用普通数组一样使用它(例如 data[0]),作者演示了如何编写两个版本的下标运算符:
      1. 普通版本T& operator[](size_t index)
      2. Const 版本const T& operator[](size_t index) const(为了处理 const Array 对象)。
  • 内存布局验证
    • 作者通过调试器(Debugger)展示了 Array 在内存中的连续排布情况。
  • 总结与后续预告
    • 这集完成了一个静态的由栈分配的数组 连续存储一定数量数据元素 。作者提到,下一集(92 集)将在此基础上引入堆(Heap)内存分配,实现动态扩容的 Vector

用C++编写一个固定大小,基于栈分配的数组数据结构

91静态数组实现#

栈分配和堆分配#

#ifdef LY_EP91

#include <iostream>   
#include <array>

int main()
{
	int k = 3;
	const int const_k = 5; //常量表达式,编译时可确定

	//栈分配
	//int myarray[k];//编译出错
	//const_k必须是编译时已知的常量
	int myarray[const_k];
	for(int i=0;i<const_k;i++)
	{ 
		//未初始化,值为栈上的垃圾值
		std::cout << myarray[i] << std::endl;
	}
	//myarray在栈帧弹出时自动释放


	//堆分配,且用指针寻址
	//动态分配,大小无需在编译时确定
	int* heapArray = new int[k];

	delete[] heapArray; //释放堆内存

	//c++11提供的标准数组,大小必须在编译时确定
	//这是个模板类,通过模板指定类型和大小
	// _EXPORT_STD template <class _Ty, size_t _Size>
	std::array<int, 5> collection;

	collection.size();//成员函数,返回数组大小

	for (int i = 0; i < collection.size(); i++)
	{
		std::cout << collection[i] << std::endl;
	}

	//要让 for (int i : collection) 运行,C++ 编译器会寻找两个特定的函数:begin() 和 end()。
	for (int i : collection)
	{

	}

	std::cin.get();
	return 0;
} 
#endif

使用alloca函数在栈上分配内存#

#ifdef LY_EP91

#include <iostream>   

class Array
{
public:
	Array(int size)
	{
		//alloca函数在栈上分配内存,分配的内存在当前函数返回时自动释放
		m_Data = (int*)alloca(size * sizeof(int));
	}
private:
	int* m_Data;
};

int main()
{
	int size = 5;
	Array arr(size);


	std::cin.get();
	return 0;
}

#endif

代码示例#

#ifdef LY_EP91

#include <iostream>   

//size_t 的使命是“能够表示内存中最大的对象”,
//在几位的机器上运行就是几位大小
template<typename T, size_t S>
class Array
{
public:

	//编译期常量
	constexpr size_t Size() const
	{
		return S;
	}

	//这样会导致取出数组元素后会被复制,且也无法修改
	//T operator[] (int index)

	//返回引用,是能修改的左值
	//1. 普通版本:允许读写
	T& operator[] (size_t index)
	{
		return m_Data[index];
	}


	//2:只允许读,而且返回const引用
	//通过这个返回的普通引用,别人依然可以修改 m_Data 里的内容,这违背了函数末尾 const 的初衷
	 //T& operator[] (int index) const

	//2:只允许读,而且返回const引用
	//取决于你调用该函数的对象本身是否是 const,是const
	//对象则调用该重载版本
	const T& operator[] (size_t index) const
	{
		return m_Data[index];
	}

	T* Data()
	{
		return m_Data;
	}

	//常量版本
	const T* Data() const
	{
		return m_Data;
	}

private:
	T m_Data[S];
};

int main()
{
	//每次创建新类,编译器都会根据模板参数生成一个新的类
	Array<int, 5> data;


	//每个字节都是 1
	memset(&data, 1, data.Size() * sizeof(int));

	//每个字节都是 2
	memset(data.Data(), 2, data.Size() * sizeof(int));

	//把数组中每个字节设为0
	memset(&data[0], 0, data.Size() * sizeof(int));

	//编译期的断言检查。让编译器在把代码编译成程序之前,先检查一下你的逻辑对不对,如果条件不满足,编译器会直接报错并拒绝生成程序 
	static_assert(data.Size() < 10, "Size is too large!");

	//因为data.Size()是编译期常量
	Array<std::string, data.Size()> a;

	//
	const auto& arrayReference = data;

	//arrayReference[2] = 3; //

	//arrayReference是const修饰的,编译器只能调用带有 const 后缀的版本
	std::cout << arrayReference[2] << std::endl;

	for (size_t i = 0; i < data.Size(); i++)
	{
		//data[i] = i;
		std::cout << data[i] << std::endl;
	}

	Array<std::string, 3> data1;
	data1[0] = "hello";
	data1[1] = "world";
	for (size_t i = 0; i < data1.Size(); i++)
	{
		//data[i] = i;
		std::cout << data1[i] << std::endl;
	}

	std::cin.get();
	return 0;
}
/*
0
0
0
0
0
0
hello
world

*/
#endif

93迭代器详解

  • 迭代器的基本定义迭代器是一个对象,其行为类似于指针,用于遍历容器中的元素。它提供了一种统一的方法来访问不同类型容器(如数组、链表、树)中的数据,而无需了解容器的底层实现。
  • ==为什么需要迭代器?==不同的容器有不同的存储结构(内存连续或不连续)。迭代器将“遍历数据”这一行为抽象化,使得开发者可以使用相同的语法(如 ++*)来处理各种复杂的集合。
  • 核心概念:begin() 与 end()
    • begin():返回指向容器中第一个元素的迭代器。
    • end():返回指向容器中最后一个元素之后位置的迭代器(即“越界”位置)。这用于标记遍历的结束。
  • 迭代器的基本操作演示了如何使用迭代器进行手动遍历:
    • 使用 *it(解引用)获取当前元素的值。
    • 使用 ++it 将迭代器移动到下一个元素。
    • 使用 it != map.end() 作为循环终止条件。
  • ==现代 C++ 中的简化写法 (Range-based for loop)==解释了基于范围的 for 循环(如 for (auto& v : values))在底层其实就是通过调用容器的 begin()end() 迭代器来实现的。
  • ==在不同容器中的应用(以 std::map 为例)==演示了由于 std::map 不是连续内存空间,不能使用下标 [i] 遍历,此时迭代器是访问元素的唯一标准方式。对于 map,迭代器指向的是 std::pair 类型。

简单例子#

std::vector有几种迭代器:

#ifdef LY_EP86

#include <iostream>     
#include <vector>

//无序映射(哈希表)
#include <unordered_map>

int main()
{
	std::vector<int> values = { 1,2,3,4,5 };
	for (int i = 0; i < values.size(); i++)
	{
		//没有索引系统的集合类型,无法使用下标访问元素
		std::cout << values[i] << std::endl;
	}
	std::cout << "====0===" << std::endl;

	//基于范围的for循环
	for (int value : values)
	{
		std::cout << value << std::endl;
	}
	std::cout << "====1===" << std::endl;

	//values.end()返回一个超出可接受范围的迭代器对象,是个无效迭代器,是最后一个元素的下一个元素,所以不能访问它指向的元素
	//当你修改容器(添加、删除或重新分配内存)时,指向该容器元素的迭代器可能会变得像“野指针”一样,指向错误的内存地址。
	for (std::vector<int>::iterator it = values.begin(); it != values.end(); it++)
	{
		//像对待指针那样,进行解引用
		//因为实际的迭代器类里实现了类似星号的解引用操作符重载 
		std::cout << *it << std::endl;
	}
	std::cout << "====2===" << std::endl;
	using ScoreMap = std::unordered_map<std::string, int>;
	ScoreMap map;
	using ScoreMapConstIter = ScoreMap::const_iterator;
	map["Cherno"] = 5;
	map["c++"] = 2;

	//不修改容器中的元素时,使用const_iterator
	for (ScoreMapConstIter it = map.begin(); it != map.end(); it++)
	{
		//*it是一个pair对象,first成员是key,second成员是value
		//auto&,引用,不会实际复制值
		//迭代器指向的是 std::pair 类型
		auto& key = it->first;
		auto& value = it->second;
		std::cout << key << " " << value << std::endl;
	}
	std::cout << "====3===" << std::endl;
	//kv的类型: std::pair<const std:;string,int> &kv
	for (auto& kv : map)
	{
		auto& key = kv.first;
		auto& value = kv.second;
		std::cout << key << " " << value << std::endl;

	}
	std::cout << "====4===" << std::endl;

	//c++17才支持
	//C++17 的结构化绑定 (Structured Bindings)
	for (const auto& [key,value] : map)
	{ 
		std::cout << key << " " << value << std::endl;

	}

	std::cin.get();
	return 0;;
}
/*
1
2
3
4
5
====0===
1
2
3
4
5
====1===
1
2
3
4
5
====2===
Cherno 5
c++ 2
====3===
Cherno 5
c++ 2
====4===
Cherno 5
c++ 2
*/
#endif

86类型转换运算符

核心概念#

这一集主要讲解了如何定义用户自定义类型转换运算符(User-Defined Conversion Operators),允许你将一个类或结构体的对象隐式或显式地转换为另一种类型(如 intfloat 或其他自定义类)。

什么是转换运算符#

  • 背景:通常我们使用构造函数来进行类型转换(例如 Entity(int x) 可以将 int 转为 Entity)。而转换运算符则是反过来的:它定义了如何将你的对象转换为其他类型。
  • 语法:operator type() const { ... }。注意它没有返回类型,因为返回类型已经包含在函数名中了。

代码示例#

  • 假设有一个 Entity 类,包含 std::string Nameint Age
  • 如果你希望将 Entity 对象直接赋值给一个 int(代表 Age),可以写:
operator int() const { return Age; }
  • 这样在调用 int a = entity; 时,编译器会自动调用该运算符。

简单代码#

#ifdef LY_EP86
 
#include <iostream>    
struct Orange
{
	operator float() const
	{
		return 3.14f;
	}
};

void PrintFloat(float value)
{
	std::cout << "PrintFloat(): " << value << std::endl;
}
 
int main()
{ 
	Orange orange;
	float f = orange; //隐式转换,调用 operator float()

	std::cout << f << std::endl; // 输出 3.14
	std::cout << (float)orange << std::endl; // 输出 3.14
	PrintFloat(f);//orange先隐式转换成float

	std::cin.get();
	return 0;
}

#endif

(例子)作用域指针-bool转换符#

#ifdef LY_EP86

#include <iostream>     

template<typename T>
class ScopedPtr
{
public:
	ScopedPtr() = default;
	ScopedPtr(T* ptr) : m_Ptr(ptr) {}
	~ScopedPtr() { delete m_Ptr; }

	//指针是否有效
	bool IsValid() const { return m_Ptr != nullptr; }

	//把对象实例转为bool类型,判断指针是否有效
	/*explicit*/ operator bool() const { return IsValid(); }

	T* Get() { return m_Ptr; }
	const T* Get() const { return m_Ptr; }

private:
	T* m_Ptr = nullptr;
};

struct Entity
{
	float X = 0.0f, Y = 0.0f;
};

void ProcessEntity(const ScopedPtr<Entity>& entity)
{
	if (entity) //隐式调用 operator bool()
	{
		std::cout << "Entity position: (" << (*(entity.Get())).X << ", " << (*(entity.Get())).Y << ")" << std::endl;
	}
	else
	{
		std::cout << "Invalid entity pointer." << std::endl;
	}
}

int main()
{
	//隐式调用ScopedPtr(T* ptr)构造函数创建ScopedPtr对象
	ScopedPtr<Entity> e = new Entity();
	ProcessEntity(e); //输出 Entity position: (0, 0)

	/* std::unique_ptr源码 
	    explicit operator bool() const noexcept {
        return get() != nullptr;
    }
	*/
	std::unique_ptr<Entity> e(new Entity());
	//因为独占指针也可以转换为bool类型 
	ProcessEntity(e);

	std::cin.get();
	return 0;
}
#endif

(例子)定时器#

直接转成双精度(总定时时间)

90stdmove与移动运算符

std::move 到底是什么?#

  • 核心真相std::move 并不移动任何东西。它只是一个简单的类型转换(Casting)。
  • 底层逻辑:它把一个左值(Lvalue)强制转换为右值引用(Rvalue Reference)。它的作用仅仅是告诉编译器:“嘿,我知道这个变量原本是个持久的对象,但现在我不再需要它了,请把它当成一个临时对象,允许别人‘偷’走它的资源。”
  • 代码本质std::move(x) 基本上等同于 static_cast<T&&>(x)

移动赋值运算符 (Move Assignment Operator)#

  • 场景引入:第 89 集讲的是构造新对象,而这一集讲的是给已有对象重新赋值
String dest = "Hello";

//如果你只写了移动构造,没写移动赋值:编译器会“退而求其
//次”,去调用拷贝赋值运算符 operator=(const String& other)。
dest = String("World"); // 这里会涉及移动赋值
  • 手写实现步骤

    1. 清理旧资源:在接管别人的资源前,必须先 delete[] m_Data 释放自己当前的内存(否则会内存泄漏)。
    2. 接管资源:像移动构造函数一样,把对方的指针和大小拷过来。
    3. 置空对方:将源对象的指针设为 nullptr
    4. 返回自身return *this; 以支持链式赋值。

为什么 std::move 是必需的?#

  • 实战演示:Cherno 展示了当你想把一个已命名的变量移动到另一个地方时,必须显式使用 std::move
  • 原因:即使一个变量的类型是 String&&,只要它有名字,它就是一个左值
  • 安全性:这种设计是为了防止你无意中移动了以后还要用到的数据。使用 std::move 是一种显式的“主权放弃”。

常见的错误用法与陷阱#

  • 不要移动 const 对象:如果你尝试 std::move 一个 const 对象,它会默默地退化回拷贝。因为移动语义需要修改原对象(将其置空),而 const 禁止修改。
  • 自赋值检查:在移动赋值运算符中,通常建议检查 if (this != &other),防止自己移动给自己导致资源被意外提前释放。

(例子1)移动构造函数与移动赋值函数#

#ifdef LY_EP90

//c++11才引入了右值引用
#include <iostream>   
#include <utility> // std::move 在这个头文件里

class String
{
public:

	String() = default;

	//构造函数
	String(const char* string)
	{
		printf("Created!\n");
		//不包括\0
		m_Size = strlen(string);
		m_Data = new char[m_Size];
		memcpy(m_Data, string, m_Size);
		std::cout << "String(const char* string)" << std::endl;
	}

	//复制构造函数
	String(const String& other)
	{
		printf("Copied!\n");
		//不包括\0
		m_Size = other.m_Size;
		m_Data = new char[m_Size];

		//在 C++ 中,访问控制(public/private)是基于“类(Class)”层面的,而不是基于“对象(Object)”层面的。
		//简单来说:只要是在 String 类的成员函数内部,你就可以访问任何 String 对象的私有成员。
		memcpy(m_Data, other.m_Data, m_Size);

		std::cout << "String(const String& other)" << std::endl;
	}


	//移动构造函数
	//接收一个右值引用参数,表示可以从一个将要被销毁的临时对象中“窃取”资源,而不是复制资源。
	//如果手动定义了“移动构造函数”,编译器就不再为你自动生成“默认赋值运算符”了。
	String(String&& other) noexcept
	{
		printf("Moved!\n");
		//不包括\0
		m_Size = other.m_Size;
		m_Data = other.m_Data;

		other.m_Size = 0;//将原对象的大小置为0,表示它不再拥有资源

		//把被接管控制权的资源指针置空,防止原对象的析构函数删除已经被移动的资源
		other.m_Data = nullptr;


		std::cout << "String(String&& other)" << std::endl;
	}

	~String()
	{
		delete[] m_Data;
		printf("Destroyed!\n");
		std::cout << "~String()" << std::endl;
	}

	// 移动赋值运算符
	String& operator=(String&& other) noexcept
	{
		printf("Move Assigned!\n");

		// 1. 自赋值检查 (防止自己移动给自己,如 a = std::move(a))
		if (this != &other)
		{
			// 2. 释放旧资源 (dest 已经有内存了,必须先删掉,否则内存泄漏)
			delete[] m_Data;

			// 3. 窃取资源
			m_Size = other.m_Size;
			m_Data = other.m_Data;

			// 4. 将原对象置空 (让它变成空壳)
			other.m_Data = nullptr;
			other.m_Size = 0;
		}

		return *this;
	}

	//拷贝赋值运算符
	String& operator=(const String& other)
	{
		printf("Copy Assigned!\n");
		if (this != &other)
		{
			delete[] m_Data;
			m_Size = other.m_Size;
			m_Data = new char[m_Size]; // 必须申请新内存
			memcpy(m_Data, other.m_Data, m_Size);
		}
		return *this;
	}

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

	//这是一个通过 typedef 或 using 定义的类型,表示
	//无符号整型32位数。
	// int 在某些古老的 16 位系统上可能是 2 字节,在现代系统上通常是 4 字节,uint32_t 强制规定在任何符合标准的编译器上,它永远是 32 位(4 字节)
	uint32_t m_Size;
};

class Entity
{
public:


	Entity(const String& name)
		:m_Name(name)
	{
		std::cout << "Entity(const String& name)" << std::endl;
	}



	//Entity(String&& name)
	//	//一旦右值引用有了名字,它在后续表达式中就变成了左值。
	//	//编译器会想:“这个 name 虽然是引用进来的,但它现在是有名有姓的变量,为了安全起见,我必须调用 复制构造函数 来保护它。
	//	:m_Name(name)
	//{
	//	std::cout << "Entity( String&& name)" << std::endl;
	//}



	//接收一个临时对象作为参数,使用移动语义来构造 Entity 对象,避免不必要的复制,提高性能。
	Entity(String&& name)
		//强转为右值引用,告诉编译器:我知道 name 是一个左值,但我想把它当作一个将要被销毁的临时对象来处理,所以请调用 移动构造函数 来构造 m_Name。
		//:m_Name((String&&)name) //这种写法也行
		:m_Name(std::move(name))  // 将左值 name 强行转回右值
	{
		std::cout << "Entity( String&& name)" << std::endl;
	}

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

private:
	String m_Name;
};

int main()
{
	//这里将"Cherno"隐式调用构造函数构造了一个String
	//之后move给了Entity(Entity内部的m_Name接管了这个临时String指针指向的堆内存)
	Entity entity("Cherno");
	entity.PrintName();
	std::cout << "===0==" << std::endl;
	String string = "Hello";
	std::cout << "===1==" << std::endl;
	String dest = string;//这是复制
	std::cout << "===2==" << std::endl;
	String dest1 = std::move(string);//调用移动构造函数
	dest1.Print();
	std::cout << "===3==" << std::endl;
	//如果没写移动赋值,编译器会“退而求其次”,去调用拷贝赋值运算符 operator=(const String & other)
	dest = "abc";
	std::cout << "===4==" << std::endl;
	 
	dest = std::move(string);
	dest.Print();//string已经被移动了,变成了空壳,所以dest打印出来是空的

	std::cin.get();
	return 0;
}
/*
Created!
String(const char* string)
Moved!
String(String&& other)
Entity( String&& name)
Destroyed!
~String()
Cherno
===0==
Created!
String(const char* string)
===1==
Copied!
String(const String& other)
===2==
Moved!
String(String&& other)
Hello
===3==
Created!
String(const char* string)
Move Assigned!
Destroyed!
~String()
===4==
Move Assigned!

*/
#endif

(例子2)详细解释#

#ifdef LY_EP90

//c++11才引入了右值引用
#include <iostream>   
#include <utility> // std::move 在这个头文件里

class String
{
public:

	String() = default;

	//构造函数
	String(const char* string)
	{
		printf("Created!\n");
		//不包括\0
		m_Size = strlen(string);
		m_Data = new char[m_Size];
		memcpy(m_Data, string, m_Size);
		std::cout << "String(const char* string)" << std::endl;
	}

	//复制构造函数
	String(const String& other)
	{
		printf("Copied!\n");
		//不包括\0
		m_Size = other.m_Size;
		m_Data = new char[m_Size];

		//在 C++ 中,访问控制(public/private)是基于“类(Class)”层面的,而不是基于“对象(Object)”层面的。
		//简单来说:只要是在 String 类的成员函数内部,你就可以访问任何 String 对象的私有成员。
		memcpy(m_Data, other.m_Data, m_Size);

		std::cout << "String(const String& other)" << std::endl;
	}


	//移动构造函数
	//接收一个右值引用参数,表示可以从一个将要被销毁的临时对象中“窃取”资源,而不是复制资源。
	//如果手动定义了“移动构造函数”,编译器就不再为你自动生成“默认赋值运算符”了。
	String(String&& other) noexcept
	{
		printf("Moved!\n");
		//不包括\0
		m_Size = other.m_Size;
		m_Data = other.m_Data;

		other.m_Size = 0;//将原对象的大小置为0,表示它不再拥有资源

		//把被接管控制权的资源指针置空,防止原对象的析构函数删除已经被移动的资源
		other.m_Data = nullptr;


		std::cout << "String(String&& other)" << std::endl;
	}

	~String()
	{
		delete[] m_Data;
		printf("Destroyed!\n");
		std::cout << "~String()" << std::endl;
	}

	// 移动赋值运算符:将另一个对象,移入当前这个对象自身
	//语义契约(Semantic Contract)。	C++ 的设计哲学是:让自定义类型的行为表现得像内置类型(如 int)一样。标准做法始终是返回非 const 的 *this 引用
	String& operator=(String&& other) noexcept
	{
		printf("Move Assigned!\n");

		// 1. 自赋值检查 (防止自己移动给自己,如 a = std::move(a),
		// 因为如下是会释放旧资源的,所以移动给自己就什么都没有了)
		if (this != &other)
		{
			// 2. 释放旧资源 (dest[当前对象] 已经有内存了,必须先删掉,否则内存泄漏)
			delete[] m_Data;

			// 3. 窃取资源
			m_Size = other.m_Size;
			m_Data = other.m_Data;

			// 4. 将原对象置空 (让它变成空壳)
			other.m_Data = nullptr;
			other.m_Size = 0;
		}

		//找到 this 指向的对象,返回它的引用(别名),
		// 而不是创建一个新的副本
		return *this;
	}

	//拷贝赋值运算符
	String& operator=(const String& other)
	{
		printf("Copy Assigned!\n");
		if (this != &other)
		{
			delete[] m_Data;
			m_Size = other.m_Size;
			m_Data = new char[m_Size]; // 必须申请新内存
			memcpy(m_Data, other.m_Data, m_Size);
		}
		return *this;
	}

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

	//这是一个通过 typedef 或 using 定义的类型,表示
	//无符号整型32位数。
	// int 在某些古老的 16 位系统上可能是 2 字节,在现代系统上通常是 4 字节,uint32_t 强制规定在任何符合标准的编译器上,它永远是 32 位(4 字节)
	uint32_t m_Size;
};

class Entity
{
public:


	Entity(const String& name)
		:m_Name(name)
	{
		std::cout << "Entity(const String& name)" << std::endl;
	}



	//Entity(String&& name)
	//	//一旦右值引用有了名字,它在后续表达式中就变成了左值。
	//	//编译器会想:“这个 name 虽然是引用进来的,但它现在是有名有姓的变量,为了安全起见,我必须调用 复制构造函数 来保护它。
	//	:m_Name(name)
	//{
	//	std::cout << "Entity( String&& name)" << std::endl;
	//}



	//接收一个临时对象作为参数,使用移动语义来构造 Entity 对象,避免不必要的复制,提高性能。
	Entity(String&& name)
		//强转为右值引用,告诉编译器:我知道 name 是一个左值,但我想把它当作一个将要被销毁的临时对象来处理,所以请调用 移动构造函数 来构造 m_Name。
		//:m_Name((String&&)name) //这种写法也行
		:m_Name(std::move(name))  // 将左值 name 强行转回右值
	{
		std::cout << "Entity( String&& name)" << std::endl;
	}

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

private:
	String m_Name;
};

int main()
{
	//临时对象经历了:Created! -> Moved! -> Destroyed!
	Entity entity("Cherno");
	//打印字符串
	entity.PrintName();
	std::cout << "=====0=====" << std::endl;

	//隐式调用构造函数String(const char* string),Created! 
	String string = "Hello";
	std::cout << "=====1=====" << std::endl;

	//隐式调用(复制)构造函数, 本质上就是“通过复制一个已有的对象来创建一个新对象”,这就是所谓的“复制构造”。
	String dest = string;
	std::cout << "=====2.1=====" << std::endl;
	//使用接受字符串右值引用的构造函数,来构造一个String
	String dest1 = (String&&)string;
	std::cout << "=====2.2=====" << std::endl;
	String dest2((String&&)string);//这个string内部的指针已经被dest1接管了,所以dest2构造的时候,string已经是空壳了,所以dest2也是空的
	std::cout << "=====2.3=====" << std::endl;

	//这样写无需知道string是什么类型的
	//remove_reference_t<_Ty>&& move(_Ty&& _Arg)  ,查看源码可知返回的是右值引用
	//这里是移动构造,而非移动赋值,因为dest3是第一次定义并使用
	String dest3 = std::move(string);
	std::cout << "=====3=====" << std::endl;

	//当将一个变量(数值、表达式)赋给一个已有变量时,使用的是赋值
	//就像这里是移动赋值
	dest = std::move(string);


	std::cin.get();
	return 0;
}
/*
Created!
String(const char* string)
Moved!
String(String&& other)
Entity( String&& name)
Destroyed!
~String()
Cherno
=====0=====
Created!
String(const char* string)
=====1=====
Copied!
String(const String& other)
=====2.1=====
Moved!
String(String&& other)
=====2.2=====
Moved!
String(String&& other)
=====2.3=====
Moved!
String(String&& other)
=====3=====
Move Assigned!
*/
#endif

(例子3)视频举的#

#ifdef LY_EP90

//c++11才引入了右值引用
#include <iostream>   
#include <utility> // std::move 在这个头文件里

class String
{
public:

	String() = default;

	//构造函数
	String(const char* string)
	{
		printf("Created!\n");
		//不包括\0
		m_Size = strlen(string);
		m_Data = new char[m_Size];
		memcpy(m_Data, string, m_Size);
		std::cout << "String(const char* string)" << std::endl;
	}

	//复制构造函数
	String(const String& other)
	{
		printf("Copied!\n");
		//不包括\0
		m_Size = other.m_Size;
		m_Data = new char[m_Size];

		//在 C++ 中,访问控制(public/private)是基于“类(Class)”层面的,而不是基于“对象(Object)”层面的。
		//简单来说:只要是在 String 类的成员函数内部,你就可以访问任何 String 对象的私有成员。
		memcpy(m_Data, other.m_Data, m_Size);

		std::cout << "String(const String& other)" << std::endl;
	}


	//移动构造函数
	//接收一个右值引用参数,表示可以从一个将要被销毁的临时对象中“窃取”资源,而不是复制资源。
	//如果手动定义了“移动构造函数”,编译器就不再为你自动生成“默认赋值运算符”了。
	String(String&& other) noexcept
	{
		printf("Moved!\n");
		//不包括\0
		m_Size = other.m_Size;
		m_Data = other.m_Data;

		other.m_Size = 0;//将原对象的大小置为0,表示它不再拥有资源

		//把被接管控制权的资源指针置空,防止原对象的析构函数删除已经被移动的资源
		other.m_Data = nullptr;


		std::cout << "String(String&& other)" << std::endl;
	}

	~String()
	{
		delete[] m_Data;
		printf("Destroyed!\n");
		std::cout << "~String()" << std::endl;
	}

	// 移动赋值运算符:将另一个对象,移入当前这个对象自身
	//语义契约(Semantic Contract)。	C++ 的设计哲学是:让自定义类型的行为表现得像内置类型(如 int)一样。标准做法始终是返回非 const 的 *this 引用
	String& operator=(String&& other) noexcept
	{
		printf("Move Assigned!\n");

		// 1. 自赋值检查 (防止自己移动给自己,如 a = std::move(a),
		// 因为如下是会释放旧资源的,所以移动给自己就什么都没有了)
		if (this != &other)
		{
			// 2. 释放旧资源 (dest[当前对象] 已经有内存了,必须先删掉,否则内存泄漏)
			delete[] m_Data;

			// 3. 窃取资源
			m_Size = other.m_Size;
			m_Data = other.m_Data;

			// 4. 将原对象置空 (让它变成空壳)
			other.m_Data = nullptr;
			other.m_Size = 0;
		}

		//找到 this 指向的对象,返回它的引用(别名),
		// 而不是创建一个新的副本
		return *this;
	}

	//拷贝赋值运算符
	String& operator=(const String& other)
	{
		printf("Copy Assigned!\n");
		if (this != &other)
		{
			delete[] m_Data;
			m_Size = other.m_Size;
			m_Data = new char[m_Size]; // 必须申请新内存
			memcpy(m_Data, other.m_Data, m_Size);
		}
		return *this;
	}

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

	//这是一个通过 typedef 或 using 定义的类型,表示
	//无符号整型32位数。
	// int 在某些古老的 16 位系统上可能是 2 字节,在现代系统上通常是 4 字节,uint32_t 强制规定在任何符合标准的编译器上,它永远是 32 位(4 字节)
	uint32_t m_Size;
};

class Entity
{
public:


	Entity(const String& name)
		:m_Name(name)
	{
		std::cout << "Entity(const String& name)" << std::endl;
	}



	//Entity(String&& name)
	//	//一旦右值引用有了名字,它在后续表达式中就变成了左值。
	//	//编译器会想:“这个 name 虽然是引用进来的,但它现在是有名有姓的变量,为了安全起见,我必须调用 复制构造函数 来保护它。
	//	:m_Name(name)
	//{
	//	std::cout << "Entity( String&& name)" << std::endl;
	//}



	//接收一个临时对象作为参数,使用移动语义来构造 Entity 对象,避免不必要的复制,提高性能。
	Entity(String&& name)
		//强转为右值引用,告诉编译器:我知道 name 是一个左值,但我想把它当作一个将要被销毁的临时对象来处理,所以请调用 移动构造函数 来构造 m_Name。
		//:m_Name((String&&)name) //这种写法也行
		:m_Name(std::move(name))  // 将左值 name 强行转回右值
	{
		std::cout << "Entity( String&& name)" << std::endl;
	}

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

private:
	String m_Name;
};

int main()
{ 
	{
		std::cout << "=====0=====" << std::endl;


		//使用String(const char* string)构造String对象,Created!
		String apple = "Apple";
		//使用默认构造函数构造String对象
		String dest;

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

		//使用移动赋值运算符将apple的资源移入dest,Move Assigned!
		//相当于dest.operator=(std::move(apple));
		dest = std::move(apple);

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

		//综上,没有任何复制就移动了整个字符数组的所有权,交换了两个
		//变量
	}
	

	std::cin.get();
	return 0;
}
/* 
=====0=====
Created!
String(const char* string)
=====1=====
Apple: Apple
Dest:
=====1=====
Move Assigned!
=====2=====
Apple:
Dest: Apple
=====2=====
Destroyed!
~String()
Destroyed!
~String()
*/
#endif

结尾 | 总结:拷贝 vs 移动#

  • 拷贝 (Copy):两份独立的资源,安全但慢。
  • 移动 (Move):一份资源的所有权转移,极快。
  • 金句:如果你希望你的 C++ 程序拥有极致性能,学会何时及如何使用 std::move 是必修课。
  • 当包含移动构造函数时,应该也包含移动赋值函数

89移动语义

为什么要引入移动语义?#

  • 核心痛点:在 C++11 之前,将对象从一处转移到另一处通常只能通过“拷贝”。

    • 向一个函数传递一个对象
    • 从函数返回一个对象
      • 如果有返回值优化RVO (Return Value Optimization)编译器直接在调用者(Caller)的栈帧中构造该对象,而不是在函数内部构造再拷贝出来。结果:既没有拷贝构造,也没有移动构造,性能损耗为零。
      • 当编译器因为逻辑复杂(例如根据条件返回不同的局部变量)而无法进行 RVO 时,移动语义就成了“二号救兵”。
        • 旧时代 (C++11 之前):如果 RVO 失效,必须调用拷贝构造函数,这涉及昂贵的深拷贝。
        • 现代 C++ (C++11 之后):编译器会尝试调用移动构造函数。1. 因为返回的对象是一个即将销毁的右值,它可以被“掠夺”。2. 你不需要手动写 return std::move(obj);,编译器会自动处理局部变量的返回
  • 性能代价:如果对象持有堆内存(如字符串或数组),拷贝意味着:申请新内存 -> 复制所有数据 -> 销毁旧对象。这在处理临时对象(扔掉的对象)时是非常巨大的浪费。

  • 解决方案:如果我们可以直接“移动”对象——即把旧对象的内存指针直接交给新对象,就能省去内存分配和数据复制的开销。

基础:构建实验用的 String#

  • 模拟场景:Cherno 编写了一个简易的 String 类,包含一个 char* 指针和 size
  • 可视化调试:在构造函数中加入 printf("Created"),在拷贝构造函数中加入 printf("Copied"),以便观察内存分配发生的时机。

拷贝的代价演示#

  • 实验代码:创建一个 Entity 类,它拥有一个 String 成员。当通过构造函数传入一个字符串字面量时,会发现控制台打印了 “Created” 然后是 “Copied”
  • 问题分析:即便传入的是一个临时字符串(右值),Entity 仍然会调用拷贝构造函数进行深拷贝。明明临时对象马上就要销毁了,我们却还要复制它,这很不科学。
#ifdef LY_EP89

//c++11才引入了右值引用
#include <iostream>   

class String
{
public:
	String() = default;

	//构造函数
	String(const char* string)
	{
		printf("Created!\n");
		//不包括\0
		m_Size = strlen(string);
		m_Data = new char[m_Size];
		memcpy(m_Data, string, m_Size);
		std::cout << "String(const char* string)" << std::endl;
	}
	
	//复制构造函数
	String(const String& other)
	{
		printf("Copied!\n");
		//不包括\0
		m_Size = other.m_Size;
		m_Data = new char[m_Size];

		//在 C++ 中,访问控制(public/private)是基于“类(Class)”层面的,而不是基于“对象(Object)”层面的。
		//简单来说:只要是在 String 类的成员函数内部,你就可以访问任何 String 对象的私有成员。
		memcpy(m_Data, other.m_Data, m_Size);

		std::cout << "String(const String& other)" << std::endl;
	}
	 

	~String()
	{
		delete[] m_Data;
		printf("Destroyed!\n");
		std::cout << "~String()" << std::endl;
	}

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

	//这是一个通过 typedef 或 using 定义的类型,表示
	//无符号整型32位数。
	// int 在某些古老的 16 位系统上可能是 2 字节,在现代系统上通常是 4 字节,uint32_t 强制规定在任何符合标准的编译器上,它永远是 32 位(4 字节)
	uint32_t m_Size;
};

class Entity
{
public:
	Entity(const String& name)
		:m_Name(name)
	{
		std::cout << "Entity(const String& name)" << std::endl;
	}  

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

private:
	String m_Name;
};

int main()
{
	//隐式构造函数构造String对象,调用String(const char* string)构造函数
	//Entity entity("Cherno");

	//临时对象String("Cherno")的生命周期直到支持它的那个“完整表达式”计算完成为止,即该行代码分号结束时
	//!!现在main函数创建这个(临时)对象后,传(复制)给Entity构造函数的当参数使用,又马上销毁了这个临时对象(只留下复制的)
	Entity entity(String("Cherno"));

	entity.PrintName();

	std::cin.get();
	return 0;
}
/*
Created!
String(const char* string)
Copied!
String(const String& other)
Entity(const String& name)
Destroyed!
~String()
Cherno
*/
#endif

编写移动构造函数 (Move Constructor)#

  • 核心语法:使用右值引用 String(String&& other)。注意要加上 noexcept 关键字。
  • 实现逻辑(偷走资源)
    1. 直接将 this->m_Data 指向 other.m_Data(浅拷贝指针)。
    2. this->m_Size 设为 other.m_Size
  • 关键的一步(置空原对象):必须将 other.m_Data 设为 nullptr,并将 other.m_Size 设为 0
  • 原理:这把旧对象变成了一个“空壳”。当旧对象析构时,delete nullptr 不会做任何事,从而保证了内存所有权的完美转移。
#ifdef LY_EP89

//c++11才引入了右值引用
#include <iostream>   
#include <utility> // std::move 在这个头文件里

class String
{
public:
	String() = default;

	//构造函数
	String(const char* string)
	{
		printf("Created!\n");
		//不包括\0
		m_Size = strlen(string);
		m_Data = new char[m_Size];
		memcpy(m_Data, string, m_Size);
		std::cout << "String(const char* string)" << std::endl;
	}

	//复制构造函数
	String(const String& other)
	{
		printf("Copied!\n");
		//不包括\0
		m_Size = other.m_Size;
		m_Data = new char[m_Size];

		//在 C++ 中,访问控制(public/private)是基于“类(Class)”层面的,而不是基于“对象(Object)”层面的。
		//简单来说:只要是在 String 类的成员函数内部,你就可以访问任何 String 对象的私有成员。
		memcpy(m_Data, other.m_Data, m_Size);

		std::cout << "String(const String& other)" << std::endl;
	}


	//移动构造函数
	//接收一个右值引用参数,表示可以从一个将要被销毁的临时对象中“窃取”资源,而不是复制资源。
	String(String&& other) noexcept
	{
		printf("Moved!\n");
		//不包括\0
		m_Size = other.m_Size;
		m_Data = other.m_Data;

		other.m_Size = 0;//将原对象的大小置为0,表示它不再拥有资源

		//把被接管控制权的资源指针置空,防止原对象的析构函数删除已经被移动的资源
		other.m_Data = nullptr;


		std::cout << "String(String&& other)" << std::endl;
	}

	~String()
	{
		delete[] m_Data;
		printf("Destroyed!\n");
		std::cout << "~String()" << std::endl;
	}

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

	//这是一个通过 typedef 或 using 定义的类型,表示
	//无符号整型32位数。
	// int 在某些古老的 16 位系统上可能是 2 字节,在现代系统上通常是 4 字节,uint32_t 强制规定在任何符合标准的编译器上,它永远是 32 位(4 字节)
	uint32_t m_Size;
};

class Entity
{
public:


	Entity(const String& name) 
		:m_Name(name)
	{
		std::cout << "Entity(const String& name)" << std::endl;
	}



	//Entity(String&& name)
	//	//一旦右值引用有了名字,它在后续表达式中就变成了左值。
	//	//编译器会想:“这个 name 虽然是引用进来的,但它现在是有名有姓的变量,为了安全起见,我必须调用 复制构造函数 来保护它。
	//	:m_Name(name)
	//{
	//	std::cout << "Entity( String&& name)" << std::endl;
	//}
	


	//接收一个临时对象作为参数,使用移动语义来构造 Entity 对象,避免不必要的复制,提高性能。
	Entity(String&& name)
		//强转为右值引用,告诉编译器:我知道 name 是一个左值,但我想把它当作一个将要被销毁的临时对象来处理,所以请调用 移动构造函数 来构造 m_Name。
		//:m_Name((String&&)name) //这种写法也行
		:m_Name(std::move(name))  // 将左值 name 强行转回右值
	{
		std::cout << "Entity( String&& name)" << std::endl;
	}

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

private:
	String m_Name;
};

int main()
{
	//隐式构造函数构造String对象,调用String(const char* string)构造函数
	//Entity entity("Cherno");

	//临时对象String("Cherno")的生命周期直到支持它的那个“完整表达式”计算完成为止,即该行代码分号结束时
	//!!现在main函数创建这个(临时)对象后,传(复制)给Entity构造函数的当参数使用,又马上销毁了这个临时对象(只留下复制的)
	Entity entity(String("Cherno"));

	entity.PrintName();

	std::cin.get();
	return 0;
}
/*
Created!
String(const char* string)
Moved!
String(String&& other)
Entity( String&& name)
Destroyed!
~String()
Cherno
*/
#endif

激活移动语义:std::move#

  • 常见误区:即便写了移动构造函数,编译器在==某些情况下(如成员变量赋值)==可能仍会选择拷贝。
  • 解决方法:在 Entity 的构造函数中使用 m_Name(std::move(name))
  • 效果验证:再次运行程序,你会发现 “Copied” 变成了 “Moved”。这意味着没有发生新的内存分配,程序运行得更快了。
#ifdef LY_EP89_

//#include <utility> //不加的话,std::move编译报错

int main()
{
	//int a=std::move(3);
	return 0;
}

#endif

结尾 | 总结与展望#

  • 核心价值:移动语义让你在不损失代码可读性的前提下,极大地提升了处理大型对象和临时对象的性能。

85左值与右值

什么是左值和右值?#

  • 核心定义:Cherno 用最简单的方式解释了两者。
    • 左值 (lvalue) 定位值 :有存储地址、有名字的变量。你可以给它赋值,它通常存在于表达式的左侧。
    • 右值 (rvalue):临时值,没有名字,通常在表达式结束后就销毁了(如字面量 10 或临时函数返回值)。
  • 直观判定法:如果你能取这个东西的地址(使用 & 运算符),它通常就是左值。

左值引用 (lvalue references)#

  • 回顾:这就是我们常用的 int& a = b;
  • 规则限制:左值引用不能绑定到右值上。
    • int& a = 10; 会报错,因为 10 是右值。
    • 例外情况const int& a = 10; 是合法的。编译器会产生一个临时变量来存放 10,然后让引用绑定它。这在函数参数传递中非常常见。

例子#

#ifdef LY_EP85

#include <iostream>   



void SetValueConst(const int& value)
{

}

void SetValue1(int& value)
{

}
void SetValue(int value)
{

}

int GetValue()
{
	return 10;//10是一个右值
}

int& GetRefValue()
{
	static int i=10;
	std::cout << "GetRefValue():" <<  i << std::endl; 
	return i;
}

int main()
{
	int i = 10;//i为左值,10为右值
	//10 = 3;//表达式必须为左值,10是右值,不能作为表达式的左边
	int a = i;//讲一个左值(a)设置为另一个左值(i)
	int i1 = GetValue();//GetValue()是一个右值,i是一个左值
	//GetValue() = 5;//GetValue()是一个右值,不能作为表达式的左边,而表达式必须是一个可修改的左值

	for (int i = 0; i < 3; i++)
	{
		GetRefValue()++;//GetRefValue()是一个左值,可以作为表达式的左边 

	}
	/*
GetRefValue():10
GetRefValue():11
GetRefValue():12
	*/

	SetValue(i);//i是一个左值,可以作为表达式的右边,用左值创建一个临时对象,并将其作为实参传递给函数
	SetValue(10);//10是一个右值,可以作为表达式的右边,即用右值创建一个临时对象,并将其作为实参传递给函数

	SetValue1(i);//从左值创建一个左值引用,所以可以将左值i作为实参传递给函数SetValue1(int& value),编译器会将i作为实参传递给函数

	//SetValue1(10); //编译出错:不能从右值创建一个左值引用,所以不能将右值10作为实参传递给函数SetValue1(int& value),编译器会报错 //非const的引用,必须通过左值来初始化

	SetValueConst(i);//从左值创建一个const左值引用 

	SetValueConst(10);//从右值创建一个const左值引用 
	//int& a1 = 10;//非const的引用,必须通过左值来初始化

	//编译器可能创建了一个带实际存储的临时变量(比如temp=10),然后
	//const int& b1=temp;
	const int& b1 = 11;//const的引用,还可以通过右值来初始化

	std::cin.get();
	return 0;
}
 
#endif

右值引用 (rvalue references) —— &&#

  • 新语法:C++11 引入了 int&&
  • 作用:它专门用来绑定右值。
    • int&& a = 10; 是合法的。
    • 它允许我们“拦截”那些即将销毁的临时对象。
  • 意义何在?:这是为了性能优化。既然右值是临时的,我们与其拷贝它的数据,不如直接“偷”走它的资源(这就是移动语义的基础)。

实际应用:函数重载#

  • 场景演示:Cherno 展示了如何通过重载同一函数名,分别处理左值和右值:
void PrintName(const std::string& name); // 接收左值和 const 右值
void PrintName(std::string&& name);      // 专门接收右值(临时对象)
  • 解释