04HelloTriangle

你好,三角#

在 OpenGL 中,所有物体都处于三维空间,但屏幕或窗口是一个二维像素数组,因此 OpenGL 的大部分工作是将所有三维坐标转换为适合屏幕的二维像素。将三维坐标转换为二维像素的过程由 OpenGL 的*图形管线 graphics pipeline *管理。图形管线可以分为两大部分:==第一部分将三维坐标转换为二维坐标,第二部分将二维坐标转换为实际的彩色像素 第二部分第一步:把由坐标定义的几何形状,切割成数以万计的待处理像素(片段)。第二步才是填充像素 ==。本章将简要讨论图形管线以及如何利用它来创建精美的像素。

图形管线以一组三维坐标作为输入,并将其转换为屏幕上的彩色二维像素。图形管线可以分为多个步骤,每个步骤都需要前一步的输出作为输入 串行,阶段性依赖 。所有这些步骤都高度专业化(每个步骤都具有特定的功能),并且可以轻松地并行执行 同一个步骤并行 。由于其并行特性,如今的显卡拥有数千个小型处理核心,以便在图形管线中快速处理数据。这些处理核心在 GPU 上运行小型程序,用于执行管线的每个步骤。这些小型程序被称为着色器

并行还有另一个意思:当第一批顶点处理完进入“光栅化”车间时,顶点车间并不需要闲着。它可以立刻开始处理下一批模型或者下一帧的顶点

开发者可以配置部分着色器,从而编写自定义着色器来替换现有的默认着色器。这使我们能够对渲染管线的特定部分进行更精细的控制,并且由于它们运行在 GPU 上,因此还可以节省宝贵的 CPU 时间。着色器使用 OpenGL 着色语言 (GLSL) ~~OpenGL Shading Language ~~ 编写,我们将在下一章中深入探讨。

下方是图形管线所有阶段的抽象表示。请注意,蓝色部分表示我们可以注入自定义着色器的部分。

顶点着色器 把 3D 空间的坐标转换成屏幕上的 2D 坐标(术语叫标准化设备坐标) ->几何着色器 增减图形:可以把一个点变成一个三角形,或者把一个三角形删掉。这里,几何着色器根据这 3 个点,计算出了第 4 个点的位置,并把它们重新组合。输出:它“发射”出了足够的顶点,组成了两个独立的三角形图元。 ->图元组装 把处理好的点“连线” ->光栅化 计算这个三角形到底盖住了屏幕上哪些像素格子 ->片段着色器 根据光照、纹理、材质,给这个像素算出一个最终的 RGBA 值 ->测试与混合 深度测试 (Depth Test):检查这个像素是不是被别的物体挡住了。模板测试:一些特殊遮罩效果。混合 (Blending):如果是半透明的,就把它和背景颜色融合

如您所见,图形管线包含大量模块 应该说的是这六个 ,每个模块负责将顶点数据转换为完整渲染像素的特定 某个步骤 步骤。我们将以简化的方式简要解释管线的每个部分,以便您对管线的工作原理有一个大致的了解。

作为图形管线的输入,我们传入一个名为 Vertex Data 的数组,其中包含三个 3D 坐标,它们应该构成一个三角形;这个顶点数据是一个顶点集合。每个顶点都是一个包含 3D 坐标数据的集合。顶点数据使用顶点属性来表示,这些属性可以包含我们想要的任何数据,但为了简单起见,我们假设每个顶点仅包含一个 3D 位置和一个颜色值

105弱指针

共享指针与唯一指针例子#

#ifdef LY_EP105
#include <iostream>
#include <memory>

class Entity {
public:
	Entity() { std::cout << "Entity Created" << std::endl; }
	~Entity() { std::cout << "Entity Destroyed" << std::endl; }
};

int main() {
	// --- unique_ptr 示例 ---
	{
		std::unique_ptr<Entity> e1 = std::make_unique<Entity>();
		// std::unique_ptr<Entity> e2 = e1; // 错误!禁止复制
		std::unique_ptr<Entity> e2 = std::move(e1); // 允许移动所有权,e1 现在为空
	} // e2 离开作用域,Entity 在这里被销毁

	std::cout << "-----------------" << std::endl;

	{
		// --- shared_ptr 示例 ---
		std::shared_ptr<Entity> sharedOuter;
		{
			//std::shared_ptr对象实例内部有一个引用计数,简单记录有多少个共享指针指向内部Entity对象的实例
			std::shared_ptr<Entity> sharedInner = std::make_shared<Entity>();

			sharedOuter = sharedInner; // 允许复制(拷贝赋值函数),引用计数变为 2
			std::cout << "Inner scope ending1..." << std::endl;
			std::shared_ptr<Entity> sharedInner1 = std::move(sharedOuter);//这里把sharedInner的所有权移走了,计数减一,但是本身又导致计数加一,所以目前是2 

			std::cout << "Inner scope ending2..." << std::endl;
		} // sharedInner1 离开作用域,sharedOuter所有权被移走,所以没有任何共享指针指向Entity了,  Entity 被销毁 

		std::cout << "Outer scope still holds Entity?" << std::endl;
	} //作用域结束,sharedOuter 离开作用域,Entity 最终在这里被销毁

	std::cin.get();
}
/*
Entity Created
Entity Destroyed
-----------------
Entity Created
Inner scope ending1...
Inner scope ending2...
Entity Destroyed
Outer scope still holds Entity?
*/
#endif

共享指针、强引用,能防止对象销毁,也被称作具名引用。即他们对一个对象各自都拥有所有权

100cpp映射容器

简介#

什么是 Map?#

  • Map 是一种容器,用于存储键值对。它允许你通过一个“键”(Key)来快速查找对应的“值”(Value)。
  • 类比:就像一本字典,单词是键,定义是值。

std::map vs std::unordered_map#

  • std::map (有序映射):
    • 底层结构:红黑树(一种自平衡二叉搜索树)。
    • 特点:元素按键的顺序自动排序。
    • 复杂度:查找、插入、删除均为 O(logn)
  • std::unordered_map (无序映射):
    • 底层结构:哈希表(Hash Table)。
    • 特点:元素没有特定顺序
    • 复杂度:平均情况下查找速度为 O(1),通常比 std::map 快,除非哈希冲突严重。

代码演示:基础用法#

  • 展示如何定义 std::map<std::string, CityRecord>
  • 使用 operator[] 进行赋值:map["Berlin"] = CityRecord { ... };
  • 注意:如果访问一个不存在的键,operator[] 会自动创建一个默认构造的对象并插入。

迭代与访问#

  • 演示如何使用 for (auto& [key, value] : map)(C++17 结构化绑定)遍历 Map。
  • 解释了老式迭代器用法:it->first 是键,it->second 是值。

性能取舍与选择建议#

  • 优先选择 std::unordered_map:如果你只需要快速查找,不需要元素有序,那么无序映射通常性能更优。
  • 选择 std::map 的场景:当你需要遍历时保持特定顺序,或者需要使用类似 lower_bound 的区间查找功能时。

复杂键的处理#

  • 讲解了如果使用自定义类作为键,std::map 需要该类重载 < 运算符,而 std::unordered_map 则需要提供哈希函数。
#ifdef LY_EP100

#include <iostream>  
#include <map>
#include <unordered_map>
#include <string>

struct CityRecord
{
	std::string Name;
	uint64_t Population;
	double Latitude, Longitude;

};

 std::ostream& operator<<(std::ostream& stream,
	const CityRecord& cityRecord)
{ 
	std::cout << "Name: " << cityRecord.Name << ","
		<< "Population: " << cityRecord.Population << ","
		<< "Latitude: " << cityRecord.Latitude << ","
		<< "Longitude: " << cityRecord.Longitude << std::endl;
	return stream;
}

namespace std {

	template<>
	struct hash<CityRecord>
	{
		size_t operator()(const CityRecord& key)
		{
			//hash<std::string>()这是调用构造函数,
			//然后构造了std::hash<CityRecord> 类型的对象,
			//之后调用了该对象的()重载方法
			return hash<std::string>()(key.Name);
		}
	};
}
int main()
{

	//计算City
	CityRecord cityRecord = { "name1",10000,2.3,4.5 };
	auto hashcode=std::hash<CityRecord>()(cityRecord);
	if (hashcode)
	{
		std::cout << "hashcode: " << hashcode << std::endl;
		std::cout << cityRecord << std::endl;
	}
	std::cin.get();
	return 0;
}

#endif

模板特化#

1. 什么是普通模板?(工厂的模具)#

模板就像一个通用的模具。比如你想写一个 print 函数,不管是 intfloat 还是 string 都能用:

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

(例子)定时器#

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