74性能基准测试指南

  • 利用 C++ 对象的生命周期(析构函数)来自动化测量任务。
  • 处理一个对性能要求很高的部分时,或者测试刚学到的新技术,此时想比较性能
  • 本章节讲解如何实际测量C++代码的性能

测试循环所需时间#

#ifdef LY_EP74

#include <iostream>   
#include <chrono>

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

	~Timer()
	{
		auto endTimepoint = std::chrono::high_resolution_clock::now();
		//算出起始时间的微秒数 
		auto start = std::chrono::time_point_cast<std::chrono::microseconds>(m_StartTimepoint).time_since_epoch().count();
		//算出结束时间的微秒数
		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)" << std::endl;
	}

private:
	std::chrono::time_point<std::chrono::high_resolution_clock> m_StartTimepoint;
};

int main()
{
	int value = 0;
	{
		Timer timer; // 创建一个 Timer 对象,开始计时

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

	std::cout << value << std::endl;

	//__debugbreak() 是 MSVC (Windows) 特有的编译器内置函数,会在这里中断程序,进入调试器,方便我们查看变量的值
	//正式运行程序时也会中断甚至发生异常
	__debugbreak();

}
#endif

先设置断点,然后进入反汇编查看

73动态类型转换(dynamic_cast)

引入:什么是 Casting?#

  • 背景:在 C++ 中,我们经常需要在父类和子类指针之间转换。
  • 痛点:传统的 C 风格转换 (Player*)entitystatic_cast 是“强行”转换。编译器完全信任你,即使你把一个“敌人”对象强行转成“玩家”对象,它也不会报错,但这会导致程序在运行时崩溃。

Dynamic Cast 的特殊性#

  • 一针见血的定义:dynamic_cast 是专门用于继承体系中的安全转换
  • 运行时检查 (RTTI 运行时类型信息 ):它不像其他转换在编译时完成,而是在程序运行时去检查这个对象“到底是不是”你要转的那个类型。
  • 返回值逻辑
    • 如果转换成功:返回有效的指针。
    • 如果转换失败(类型不匹配):返回 NULL (或 nullptr)。

核心前置条件:虚函数表#

  • 关键约束:要使用 dynamic_cast,你的类必须是多态的 (Polymorphic)。
  • 原理:这意味着基类必须至少有一个 虚函数 (Virtual Function)。
  • 底层逻辑:dynamic_cast 依赖于 RTTI (Run-Time Type Information),而 RTTI 信息存储在虚函数表 (vtable) 中。如果类没有虚函数,就没有 RTTI,编译器会直接报错。

代码实战演练#

Cherno 演示了一个经典的场景:

  • 基类:Entity
  • 子类:PlayerEnemy
  • 场景:你有一个 Entity* 指针,你想知道它指向的到底是Player 还是 Enemy
  • 写法:
#ifdef LY_EP73

#include <iostream> 

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

class Player : public Entity
{
};

class Enemy : public Entity
{
};


int main()
{ 
	Player* player = new Player();
	Entity* e = player;//隐式转换,Player* 转换为 Entity*

	Entity* actuallyEnemy = new Enemy();
	Entity* actuallyPlayer = new Player();

	//Player* p = e;//错误,Entity* 转换为 Player*,需要显示转换

	Player* p = (Player*)actuallyEnemy;//不安全的转换,Enemy* 转换为 Player*,编译器允许,但运行时会出问题;如果访问Player和Entity共有的成员,可能不会出问题,但如果访问Player特有的成员,就会出问题,因为actuallyEnemy实际上是一个Enemy对象,而不是Player对象。

	Player* p1 = static_cast<Player*>(actuallyEnemy);//和上面的显示转换一样,属于不安全的转换 

	//actuallyEnemy指向的类必须是多态的,在 C++ 的设计哲学里,非多态类确实没有存储运行时的类型信息。
	Player* p2 = dynamic_cast<Player*>(actuallyEnemy);//安全的转换,运行时会检查actuallyEnemy是否实际上是一个Player对象,如果是,则返回指向Player对象的指针;如果不是,则返回nullptr。由于actuallyEnemy实际上是一个Enemy对象,所以p2将被赋值为nullptr。


	Player* p3 = dynamic_cast<Player*>(actuallyPlayer);

	if (p2)
	{
		std::cout << "actuallyEnemy is a Player." << std::endl;
	}
	else
	{
		std::cout << "actuallyEnemy is NOT a Player." << std::endl;
	}

	if(p3)
	{
		std::cout << "actuallyPlayer is a Player." << std::endl;
	}
	else
	{
		std::cout << "actuallyPlayer is NOT a Player." << std::endl;
	}

	std::cin.get();
}

/*
actuallyEnemy is NOT a Player.
actuallyPlayer is a Player.
*/
#endif

性能代价 (The Cost)#

Cherno 给出了一针见血的职业警告:

72预编译头文件详解

预编译头文件的作用是将大量稳定的头文件提前编译成二进制缓存。这样编译器在处理每个源文件时,直接读取该缓存,而无需重新解析成千上万行重复的代码,从而极大地缩短编译时间。

比如常用的标准模版库:比如向量、字符串、标准输出之类,假设我们每次(在每个.cpp文件)都要包含vector,需要读取vector.h头文件并编译,而且vector本身也包含其他一系列头文件,所有其他头文件被复制到vector.h中,再把vector.h复制到.cpp中(大约十万行代码),还要进行解析和标记化,并以某种形式进行编译

  • 如果项目中有多个cpp文件,很多地方都包括vector.h,他们会被逐个包含在每个文件中 每个翻译单元(.cpp)都会被单独编译,然后链接器将它们链接起来
  • 且每次对那个cpp文件 包含了vector.h的 重新更改时,整个 这里是说要把vector.h复制进来然后再编译 都得重新编译

建议

  • 改用预编译头文件,它会获取一堆头文件(要包含的那些),然后它编译一次,并将起转换为二进制格式。这对编译器来说处理起来比纯文件快得多 因为它以处于解析状态,而且是二进制文件,随时可以,不用再重新编译
    • 举个例子,可能本来要1分钟,使用预编译头文件后,编译可能只需8秒钟
  • 预编译头文件其实就是一个头文件,且包含一系列其他头文件
    • 不可以把所有头文件都放进去,因为如果一个头文件经常变化,那么每次改动都需要重新编译整个预编译头文件
    • 不需要在项目的每个cpp都包含日志文件,只需要包含含有日志的预编译头文件
  • C库、标准模板库、WindowsAPI调用库,建议把这些都放进预编译头文件 每次编译可能他的代码量比实际代码还多 。这样就能从每个C文件中获取所有内容,如果要用只能指针,就不用实际直接包含memory头文件,因为他在预编译头文件中,它会包含在实际编写的每个C++文件中
  • 因为把所有文件都放进了pch.h 也可以叫其他名字 ,所以不能一眼看出实际include了什么,且pch.h中包含了所有的,所以也不清楚实际它只依赖哪些头文件
  • 如果编写游戏引擎,可能只有一个cpp文件需要包含GLFW 编译一起就搞定 ,那么可以把GLFW放入pch.h中,因为其他内容都被抽象放在自己的api中;而其他的标准库、vector、标准输入输出之类的,很多都会用的,也可以让它编译一次就搞定

例子#

//Main.cpp
#include "pch.h"

int main()
{
	std::cout << "Hello World!" << std::endl;
}
//pch.h
#pragma once

#include <iostream>
#include <algorithm>
#include <functional>
#include <memory>
#include <thread>
#include <utility>

// Data structures
#include <string>
#include <stack>
#include <deque>
#include <array>
#include <vector>
#include <set>
#include <map>
#include <unordered_set>
#include <unordered_map>

//Windows API
#include <windows.h>

做个测试

然后ctrl+f7编译Main.cpp,查看生成的Main.i文件

71现代CPP的安全性和教学理念

  • 重点围绕 原始指针(Raw Pointers)与智能指针(Smart Pointers) 的长期争论,以及为什么坚持从底层原理开始教学

视频看过一遍了,全程没有讲到代码例子,所以这里用了ai总结这个视频

The Cherno 在本集中讨论了“安全编程”在 C++ 中的定义,重点围绕 原始指针(Raw Pointers)与智能指针(Smart Pointers) 的长期争论,以及他为什么坚持从底层原理开始教学。

时间线总结

话题引入#

  • 讨论 C++ 社区中关于“现代 C++”与“安全性”的激烈争论。
  • 两种极端观点:
    • 现代派: 只应使用现代 C++,永远不碰原始指针。
    • 传统派: 智能指针有性能开销,真男人就该手动管理内存。

什么是 C++ 中的“安全性”?#

  • 安全编程的核心目标是:减少崩溃、内存泄漏和非法访问(Access Violations)
  • 从 C++11 开始,社区转向智能指针,主要是为了解决堆内存分配中的人为错误

内存管理的两大痛点#

  • 忘记释放内存: 导致内存泄漏,可能导致程序最终因内存不足而崩溃。
  • 所有权问题(Ownership): 当一个指针在多个函数或类之间传递时,不清楚由谁负责销毁它。

智能指针的本质#

  • 智能指针本质上只是对一两行代码的自动化:自动调用 delete 或释放内存
  • 它们通过自动化来减少人为疏忽,提高代码的健壮性。

他的立场:你应该使用智能指针#

  • 在实际的生产代码或大型框架中,100% 应该使用智能指针
  • 不使用它们会极大地增加代码维护的难度和出错的风险。

为什么他还在视频里用原始指针?#

  • 简洁性: 在简单的 Sandbox(沙盒)测试或只有 100 行左右的小样板代码中,原始指针写起来更快、读起来更直观。
  • 环境区别: 学习阶段的小程序不需要像生产环境那样严谨地考虑所有权转移。

教学责任与影响#

  • 反思作为拥有大量订阅者的博主,使用原始指针是否会给初学者带来不良示范(类似于红灯穿马路)
  • 但他认为,如果不教原始指针,学生就无法真正理解 C++ 的工作原理。

核心教学哲学:理解底层(非常重要)#

  • 智能指针只是原始指针的封装(Wrapper)。
  • 如果你不理解原始指针和内存是如何运作的,你就无法成为一名优秀的 C++ 程序员,尤其是处理高性能或实时系统的开发。
  • ==学习“旧东西”==是为了更好地掌握“新工具”。

核心结论#

  • 在项目开发中: 推荐使用智能指针,为了安全和效率。
  • 学习过程中: 必须掌握原始指针,为了理解底层和内存真相。

Cherno 的观点: 避开底层原理而只学“安全”的做法,无法培养出真正顶尖的开发者。

70条件断点与操作断点设置

  • 本节介绍visual studio 快速小技巧,也是一个适用于开发和调试的通用技巧,围绕断点展开
  • 围绕条件展开,以及一些可应用于断点的操作
    • 条件断点 断点在特定条件下触发
    • 动作断点 断点触发时进行一些操作在控制台打印内容并继续/或暂停执行程序

断点在代码里也可以实现

  • 写if语句并(手动)设置断点 也可以直接debug break;之类的编译器内部函数

调试中发现问题的情况下,如果使用ide的调试断点功能,就能省去重编译、编写代码再调试运行

添加Action和Condition#

#ifdef LY_EP70
#include <iostream>

int main()
{ 
	int a = 2, b = 3;
	for (int i = 0; i < 5; i++) {
		std::cout << "i:" << i << std::endl;
	}
	std::cout << a << std::endl;
	std::cout << b << std::endl;
	std::cin.get();
	return 0;
}
#endif
  • 先添加断点,然后再点击Action,最后添加打印内容i is {i} ,values of a,b is : {(float)a}, {(float)b}
  • 然后设置Condition,设置为i == 3,即只有在i == 3时才触发断点

这个contine code execution如果勾上了,则调试时不会停止,会直接跳过该断点

69类型转换

  • 类型转换(Type Casting)是在 C++ 强类型系统中进行类型间转换的过程。
  • cpp作为一种强类型语言,意味着有一个类型系统,且类型是强制规定的
    • 如果一个东西是整型,不能突然把它拿来当双精度数,除非有简单的==隐式转换 意味着cpp知道如何在这两种类型之间转换且不丢失数据 ==
    • 可以使用显示转换
  • 类型转换包括C风格转换C++风格转换

基本的显示/隐式转换#

#ifdef LY_EP69
#include <iostream>

int main()
{
	int a = 5;
	double value = a;//隐式转换

	double value1 = 5.25;
	int a1 = value1;//隐式转换

	int a2 = (int)value1;//显示转换

	std::cout << "a:" << a << std::endl;//5
	std::cout << "value:" << value << std::endl;//5
	std::cout << "value1:" << value1 << std::endl;//5.25
	std::cout << "a1:" << a1 << std::endl;//5
	std::cout << "a2:" << a2 << std::endl;//5


	std::cin.get();
	//成功的情况只有一种(0),而失败的原因
	//可以有成千上万种(1, 2, 3...)。
	return 0;
}
#endif

类型转换例子#

#ifdef LY_EP69
#include <iostream>

class Base
{
public:
	Base() {}

	//这里应该把析构函数设为vitual,我只是为了演示
	//只要有虚函数就能用动态转换转换成功,而这个虚函数
	//不一定非得是析构函数
	~Base() {} 

	virtual void test() {}
};

//public Base中 <public> 决定:基类(Base)中的成员在
//派生类(Derived)中保持什么样的访问权限。(public则成员
//的最高权限是public)
class Derived : public Base
{
public:
	Derived() {}
	~Derived() {}
};

class AnotherClass : public Base
{
public:
	AnotherClass() {}
	~AnotherClass() {}
};

int main()
{
	//C风格转换
	double value = 5.25;
	double a1 = value + 5.3;//10.55
	double a2 = (int)value + 5.3;//强制截断,10.3 
	double a3 = (int)(value + 5.3);//强制截断,10 
	std::cout << a1 << std::endl;
	std::cout << a2 << std::endl;
	std::cout << a3 << std::endl;

	// C++风格转换(以下C风格转换都能做到,但是C++风格还会做点
	// 额外的事情)
	double s = static_cast<int>(value) + 5.3;//1 静态转换,会有编译时检查

	//double s1 = static_cast<AnotherClass>(value) + 5.3;//编译失败,没有哪个构造函数支持,即没有 AnotherClass(int)这样的构造函数先隐式转换成AnotherClass

	//AnotherClass s11 = static_cast<AnotherClass*>(value) ;//编译失败,无效的类型转换
	//AnotherClass s12 = static_cast<AnotherClass*>(&value) ;;//编译失败,无效的类型转换


	//2 重新解释转换 //类似类型双关
	AnotherClass* a = reinterpret_cast<AnotherClass*>(&value);//“重新解释转换”,编译通过,即指向该值的指针,解释成:指向另一个类实例的指针


	//3 动态类型转换 //多态时用到
	Derived* derived = new Derived();
	Base* base = derived;
	//检查基类指针是否是(某个)派生类实例 
	//静态转换,但很明显这会出问题(base是指向Derived实例而非AnotherClass)
	AnotherClass* ac1 = static_cast<AnotherClass*>(base);
	AnotherClass* ac1_1 = (AnotherClass*)(base);

	Derived* ac3 = dynamic_cast<Derived*>(base);//编译通过

	AnotherClass* ac2 = dynamic_cast<AnotherClass*>(base);//编译通过,但是返回null
	if (ac3)
	{
		std::cout << "ac3转换成功" << std::endl;//ac3转换成功
	}
	if (ac2)
	{
		std::cout << "ac2转换成功" << std::endl;//没成功
	}

	//4 常量解释转换:移除或添加常量
	//视频没说到

	std::cin.get();
	//成功的情况只有一种(0),而失败的原因
	//可以有成千上万种(1, 2, 3...)。
	return 0;
}
#endif
  • dynamic_cast 是在==运行时(Runtime)==通过查询虚函数表(vtable)来确认对象的实际类型的。
    • 规则:只有当类中至少包含一个==虚函数(virtual function)==时,编译器才会为该类生成类型信息(RTTI)。
    • 所以如果被转换的实例类中完全没有虚函数,那么使用dynamic_cast时在编译阶段就报错了
  • RTTI (运行时类型识别):当类包含虚函数时,编译器会在对象内存布局中增加一个指向“虚函数表”的指针。表中包含了该类的 type_info
  • 安全性检查:执行 dynamic_cast 时,程序会检查内存中对象的实际 type_info。如果 Base* base 实际上指向的是 Derived,转换成功;如果指向的是其他不相关的类,则返回 NULL

常量转换#

#ifdef LY_EP69
#include <iostream>

void IncreaseValue(const int* val_ptr) {
	// 编译失败:不能修改 const 指针指向的内容
	//*val_ptr = 10; 

	// 使用 const_cast 去掉 const 属性
	int* modifiable_ptr = const_cast<int*>(val_ptr);
	*modifiable_ptr += 5;
}

int main() {

	//number 是一个普通的非 const 变量 
	int number = 10;

	std::cout << "修改前: " << number << std::endl;

	// 传入 number 的地址
	IncreaseValue(&number);

	std::cout << "修改后: " << number << std::endl; // 输出 15


	//=====演示有问题情况=====
	const int constant_var = 100; // 原始变量就是常量
	const int* ptr = &constant_var;

	int* naughty_ptr = const_cast<int*>(ptr);
	*naughty_ptr = 200; // 危险!这是“未定义行为”
	std::cout << "constant_var修改后: " << constant_var << std::endl;

	// 此时打印 constant_var,结果可能还是 100,也可能是 200,取决于编译器优化

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

为什么number修改成功,而constant_var修改失败

68虚析构函数

  • 虚析构函数对处理多态性而言非常重要
  • 如果类B从类A派生而来,A a=new B(),现在如果决定detele a ,此时希望运行的不仅是A的析构函数,还有B的析构函数。这本质上就是虚析构函数的作用

虚函数的核心作用与机制#

在 C++ 中,虚函数(Virtual Function) 是实现多态性(Polymorphism)的核心机制。它允许程序在运行期间(Runtime)根据对象的实际类型来调用相应的函数,而不是在编译期间(Compile time)就固定死。

1. 实现动态绑定 (Dynamic Binding)#

这是虚函数最直接的作用。在基类指针或引用指向派生类对象时,调用虚函数会执行“子类版本”而非“基类版本”。

  • 非虚函数: 编译器根据指针的类型决定调用哪个函数(静态绑定)。
  • 虚函数: 编译器根据指针指向的实际对象决定调用哪个函数(动态绑定)。

2. 支持接口复用与扩展 (Extensibility)#

通过虚函数,你可以编写通用的代码来处理不同类型的对象,而无需知道它们的具体类别。

特性说明
一致性基类定义接口规范,所有子类必须遵循。
可扩展性增加新的子类时,不需要修改原有的基类或调用方的逻辑。
重写 (Override)子类可以根据自身需求,重新定义基类的行为。

3. 虚析构函数的重要性 (Virtual Destructor)#

在多态场景下,基类的析构函数必须声明为 virtual

  • 原因: 如果基类析构函数不是虚函数,当删除一个指向派生类对象的基类指针时,只会调用基类的析构函数,导致派生类特有的成员变量无法被正确释放,从而产生内存泄漏 (Memory Leak)

4. 底层实现:虚函数表 (Vtable)#

虚函数是通过 虚函数表 (Virtual Table, vtbl)虚函数表指针 (vptr) 实现的:

  1. vptr: 每个含有虚函数的对象内部都有一个隐藏指针。
  2. vtbl: 存储了该类所有虚函数的地址。
  3. 调用过程: 程序运行阶段,通过对象的 vptr 找到 vtbl,再从中找到对应的函数地址。这虽然比普通函数调用稍微多了一点点开销,但换取了极大的灵活性。

简短代码示例#

class Animal {
public:
    virtual void speak() { cout << "Animal speaks"; } // 虚函数
    virtual ~Animal() {} // 虚析构函数
};

class Dog : public Animal {
public:
    void speak() override { cout << "Woof!"; } // 重写
};

void makeItSpeak(Animal* a) {
    a->speak(); // 运行阶段根据 a 指向的具体对象决定调用哪个 speak()
}

类继承时构造与析构的顺序#

#ifdef LY_EP68
#include <iostream>

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

class Derived : public Base
{
public:
	Derived() { std::cout << "Derived constructor\n"; }
	~Derived() { std::cout << "Derived destructor\n"; }
};

int main()
{
	Base* base = new Base();
	delete base;
	std::cout << "------------------\n";
	Derived* derived = new Derived();
	delete derived;
	std::cout << "------------------\n";
	Base* poly = new Derived();
	delete poly;

	std::cin.get();
}
#endif
/*输出
Base constructor
Base destructor
------------------
Base constructor
Base destructor
------------------
Base constructor
Derived constructor
Derived destructor
Base destructor
------------------
Base constructor
Derived constructor
====================> 输出没有这行:Derived destructor,没有调用子类析构函数,即内存泄露,没有释放m_array
Base destructor
*/

第一部分:new Base() 这是最基础的情况:

67联合体

  • 联合体有点像结构体,不过一次只能占用一个成员的内存,即每个成员都共享同一块内存
    • 如果我们有一个结构体,我们在里面声明四个浮点数,那就意味着我们有4个4字节,即那个结构体共有16个字节
    • 联合体只能有一个成员,如果声明4个浮点数(float),如A、B、C、D,联合体的大小仍为4个字节。当尝试访问A或B或C或D时,他们实际上是同一块内存,即把A改成5,马上访问D时值也是5
  • Struct 会为每个成员分配独立空间,而 Union 里的所有成员起始地址相同。
  • 使用联合体的方式和使用结构体或类完全一样,也能添加静态函数、普通函数和方法 不能有虚方法;静态成员不占 Union 实例空间
  • 类型双关(Type Punning):用不同的名称或类型查看同一块原始内存。
    • 当要给同一个变量取两个不同名字时,可以用联合体
  • 通常联合体匿名使用、也不添加方法

联合体不能有虚方法的原因#

虚函数的工作原理依赖于虚函数表指针(vptr)。

  • Class/Struct 的做法:编译器会在对象内存的开头插入一个隐藏的指针(vptr),指向该类的虚函数表(vtable)。
  • Union 的冲突:Union 的所有成员都必须从偏移量 0 开始共享内存。
    • 如果 Union 有虚函数,那么它必须有一个 vptr。
    • 这个 vptr 会占用内存。由于 Union 成员共享空间,你的数据成员(比如 int 或 float)会和这个 vptr 重叠。
    • 一旦你给成员赋值,就会覆盖掉 vptr,导致程序在调用虚函数时崩溃;反之,虚函数机制也会破坏你的数据。

例子:类型双关#

#ifdef LY_EP67_
#include <iostream>
 

int main()
{
	struct Union
	{
		union
		{
			float a;
			int b;
		};
	};

	Union u;

	//在 C++ 中,如果你在一个 struct 或 class 内部定义了一个
	//_没有名字_的 union,编译器会把这个联合体的成员直接注
	//入(Injected)到外层结构体的作用域中。
	u.a = 2.0f;//0x00F6FE20  00 00 00 40
	//40 00 00 00 为 0100 0000 0000 0000 0000 0000 0000 0000
	//u.b:0表示正数,所以这个值为2^30次方,即 10,7374,1824 
	//也就是我们拿到了构成那个浮点数的内存,然后把它当做int来解释(
	//类型双关)
	std::cout << u.a << "," << u.b << std::endl;

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

相对拙劣的做法#

#ifdef LY_EP67
#include <iostream>

struct Vector2
{
	float x, y;
};

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

	Vector2& GetA()
	{
		//把&x当成一个Vector2的地址来解释
		//*表示取值的时候连续取两个float的值来构成一个Vector2
		return *(Vector2*)&x;
	}

	//尝试把Vector4看成是两个Vector2
	//Vector2 GetA()
	//{
	//	//..
	//	//添加一些成员并返回,这里省略了
	//	return Vector2();
	//}
};

void PrintVector2(const Vector2& v)
{
	std::cout << "Vector2: (" << v.x << ", " << v.y << ")\n";
}

int main()
{
	std::cin.get();
}
#endif

利用union原理并巧妙使用它#

#ifdef LY_EP67
#include <iostream>

struct Vector2
{
	float x, y;
};

//最外层 struct Vector4:定义了这块内
// 存的总大小(4 x 4 = 16 字节)。
struct Vector4
{

	//这是一个“平行空间”。它(union)告诉编译器:“我内部定义的所有东西,都从同一个起始地址开始存放”。
	union
	{

		//union的大小,是它内部最大的成员的大小。在这个例子中,union的大小是16字节(4个float,每个4字节)。

		//它把这 16 字节切分成了四个 float
		//,并分别取名为 x, y, z, w。(这个struct
		//是联合体的第1个成员)
		struct
		{
			float x, y, z, w;
		};

		//(这个struct是联合体的第2个成员)
		struct
		{
			Vector2 a, b;
		};

	};
};

void PrintVector2(const Vector2& v)
{
	std::cout << "Vector2: (" << v.x << ", " << v.y << ")\n";
}

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

	//可以用vector.x,vector.y,vector.z,vector.w来访问这四个float
	//或者用vector.a和vector.b来访问这两个Vector2
	//且vector.a.x和vector.a.y分别对应vector.x和vector.y,vector.b.x和vector.b.y分别对应vector.z和vector.w(
	//因为它们共享同一块内存,所以修改其中一个成员会影响到其他成员的值)
	vector.x = 2.0f;
	std::cout << "Vector4: (" << vector.x << ", " << vector.y << ", " << vector.z << ", " << vector.w << ")\n";// Vector4: (2, 2, 3, 4)

	//验证上述结论
	//这里修改了z却间接修改了vector.b
	vector.z = 500.0f;
	std::cout << "-----" << std::endl;

	PrintVector2(vector.a); // Vector2: (2, 2)
	PrintVector2(vector.b); // Vector2: (500, 4)
	std::cin.get();
}
#endif

66类型双关

  • 类型双关指的是,C绕过类型系统的一种方式,C是强类型语言,不会把所有东西都设置为auto(可以,但不推荐)
  • C++中类型在一定程度上由编译器强制限定,但你可以直接访问内存
  • 本章节实例会访问某个int数值的内存,并把它当做双精度数来处理(轻易绕过类型系统)
    • 假设有一个类(基本类型结构体),我们想把它写成字节流,它没有其他指针指向它,那么我们可以直接解释整个结构体、类、或者其他东西,这里会视为字节数组:只要我们知道任何大小,就可以直接访问数据

转换#

#ifdef LY_EP66
#include <iostream>

int main()
{ 
	//小端法,低字节的数据存储在内存的低地址处,
	// 高字节的数据存储在内存的高地址处
	//Ox00000032
	int a = 50;// 32 00 00 00 
	 
	//隐式转换,a隐式转换成了double类型
	//相当于 double value =(double)a
	//Ox404900000000000000,double数值50
	//的16进制表示为0x4049000000000000
	double value = a;// 00 00 00 00 00 00 49 40 
	std::cout << value << std::endl;

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

转换后内存中的数值变了

类型双关-不同方式读取同一内存#

#ifdef LY_EP66
#include <iostream>

int main()
{
	int a = 50;
	//把a(内存处)的内存以double类型的方式进行访问
	//将整型进行类型双关变成双精度型
	//* 操作符会尝试从该地址开始,一口气往后读 8 个字节,这里是
	//32 00 00 00 cc cc cc cc (小端法)
	double  value = *(double*)&a;
	std::cout << a << std::endl;
	std::cout << value << std::endl;

	//不想创建全新变量,只是想把这个整数当成双精度数来访问,
	//那么可以在这个双精度数后面加&,这样就能引用他
	//强行让 value1 成为 &a 处那块 8 字节内存的别名。通过
	// value1 修改数据会直接破坏 a 及其相邻内存。
	double&  value1 = *(double*)&a;
	//这里会把8个字节(a和a后面的4个字节)都写入0
	value1 = 0;
	std::cin.get();
}
#endif

65排序

  • 数据结构决定了数据的存储方式
  • 本节主要讲解如何对现有数据进行排序
  • 当使用C及其内置集合类型,如std::vector时,优先用C内置的函数排序:能根据你提供的任何迭代器进行实际排序

cppreference中的解释#

//std::sort
//Defined in header <algorithm>
template< class RandomIt >
void sort( RandomIt first, RandomIt last ); (1)(constexpr since C++20)

template< class ExecutionPolicy, class RandomIt >
void sort( ExecutionPolicy&& policy,
           RandomIt first, RandomIt last ); (2)(since C++17)
           
template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp ); (3)(constexpr since C++20)

template< class ExecutionPolicy, class RandomIt, class Compare >
void sort( ExecutionPolicy&& policy, RandomIt first, RandomIt last, Compare comp );  (4)(since C++17)

可以传入迭代器、谓词(非必须)

迭代器:存储了原数据的地址。可以利用迭代器迭代原数据,存储了地址所以可以方便的交换数据

没有返回值,时间复杂度为O(N·log(N))

Given N as last - first:

1,2) O(N·log(N)) comparisons using operator<(until C++20)std::less{}(since C++20).
3,4) O(N·log(N)) applications of the comparator comp.

例子#

algorithm:英[ˈælɡərɪðəm]