53(补充)字符串_数组_作为参数

普通数组退化 vs. 字符串字面量退化#

1. 本质上的统一:都是“数组退化” (Array Decay)#

在 C++ 编译器眼中,无论是你定义的数组还是手写的字符串,它们的原始身份都是固定长度的数组

  • int arr[5] 的原始类型是:int[5]
  • "abc" 的原始类型是:const char[4](包含隐藏的 \0

退化规则:当它们作为参数传递给接收指针的函数时,会执行相同的操作——丢失长度信息,仅留下首元素的内存地址。

2. 它们的三大核心区别#

虽然底层逻辑相同,但在内存布局和使用习惯上,它们有显著差异:

维度普通数组 (如 int[])字符串字面量 (如 "abc")
结束标志。必须额外传递 size 参数。。自带“哨兵” \0 (Null Terminator)。
内存区域通常在 栈 (Stack),可读可写。只读数据区 (Static Data),不可修改。
退化后的类型int[5] \rightarrow int*const char[4] \rightarrow const char*

3. 详细拆解#

区别 A:哨兵机制 (The Terminator)#

  • 普通数组:函数拿到 int* 后,完全不知道哪里是数组的终点。如果你不传 size,函数极易越界。
  • 字符串:虽然也退化成了 const char*,但函数可以通过不断向后寻找 \0 来确定长度(这就是 strlen 的工作原理)。

区别 B:内存安全性#

  • 普通数组
  int arr[] = {1, 2, 3};
  arr[0] = 9; // 完全合法,你拥有这块内存的写权限
  • 字符串字面量:通常在只读数据区(Static/Data Segment)。它虽然退化成了 const char*,但你绝对不能尝试通过指针去修改它,否则程序会直接崩溃。

区别 C:退化后的类型#

  • int a[5] \rightarrow 退化为 int*
  • "abc" (即 const char[4]) \rightarrow 退化为 const char*(注意多了一个 const,因为字面量不可修改)。

string及string引用传参对比#

void test1(const std::string& s ) {}
void test2(const std::string s ) {}

int main()
{
  test1("abc");
  test2("abc");
}
特性const std::string& sconst std::string s
内存身份函数内部的 s 只是外部对象的别名函数内部的 s 是一个全新的副本
拷贝次数仅隐式转换产生的 1 次临时构造转换构造 + 传参拷贝(共 1-2 次)
推荐程度极高(行业标准)不推荐(除非你打算在函数里修改它)
安全性保证不会在函数内产生冗余副本容易引发不必要的性能损耗

53模板

template,能让你定义一个待编译的模版 让编译器用一组规则为你编译代码

当我写一个函数时,我在函数中使用一个模板,我实际做的事类似蓝图的东西,当我决定调用那个函数时,指定一些参数,他们决定了实际放入模板的代码

简单例子#

#include <iostream> 

//template表明这是个编译时求值的模板,这不是实际的函数,只有在调用时才会创建并编译成源代码
//可以从实际参数中隐式获取T
template<typename T>
void Print(T value)
{
	std::cout << value << std::endl;
} 
int main()
{
	Print(5);
	//"5.5"被隐式转为const char[4]
	Print("5.5");
	Print(3.7);

	std::cin.get();
}

额外补充#

模板定义调用 Print(“5.5”) 时 T 的类型说明
void Print(T value)const char*发生退化。传递的是地址。
void Print(T& value)const char[4]不发生退化。传递的是数组的引用。
void Print(const T& value)char[4]不发生退化。T 本身是 char[4]。
// 它代表:我只接受一个含有 4个char的数组的“引用”
void Print(const char (&value)[4]) {
    // sizeof(value) == 4
    // 编译器知道这个数组刚好就是 4 个字节
}
//数组的首地址
void Print(const char* value) {
    // sizeof(value) == 8 (在64位系统上)
    // 编译器不知道这里有 4 个字符
}

例子2#

#include <iostream> 

//template表明这是个编译时求值的模板,这不是实际的函数,只有在调用时才会创建并编译成源代码(才会实际创建函数)
//可以从实际参数中隐式获取T
template<typename T>
//和上面一个意思
//template<class T>
void Print(T value)
{
	std::cout << value << std::endl;
}

int main()
{
	//显示指定T
	Print<int>(5);
	//让编译器自己推断T的类型
	Print(5);

	std::cin.get();
}

例子3#

52处理多返回值

这篇讨论的是如何在函数返回两个值

cherno推荐的方法是创建一个结构体并返回它

代码预备#

为了演示,本篇使用了string,为了顺便探讨性能(视频中没提到),我写了一个string.h 和string.cpp打印相关信息

//String.h
#pragma once // 防止头文件被重复包含
#include <iostream>

class String
{
private:
    char* m_Buffer;
    unsigned int m_Size;

public:
    // 默认构造函数
    String() : m_Buffer(nullptr), m_Size(0) {}

    // 构造函数
    String(const char* string);

    // 拷贝构造函数(深拷贝)
    //拷贝构造函数 (String a = b;):当你在创建一个新的对象,并用已有的对象初始化它时调用。
    String(const String& other);

    // 析构函数
    ~String();

    // 运算符重载
    char& operator[](unsigned int index);

    // 赋值运算符重载 (a = b;):当两个对象都已经存在(已经构造完毕),你只是想把其中一个的值覆盖给另一个时调用。
    String& operator=(const String& other); 

    // 友元函数:重载 << 运算符
    friend std::ostream& operator<<(std::ostream& stream, const String& string);

    // 测试函数
    unsigned int& mytest() { return m_Size; } // 简单的内联函数可以直接写在类内
};
#include "String.h"
#include <cstring> // 必须包含 cstring 才能使用 strlen 和 memcpy

// 构造函数
String::String(const char* string)
{
	m_Size = strlen(string);
	m_Buffer = new char[m_Size + 1];
	memcpy(m_Buffer, string, m_Size);
	m_Buffer[m_Size] = '\0';
}

// 拷贝构造函数
String::String(const String& other)
	: m_Size(other.m_Size)
{
	std::cout << "Copied String!" << std::endl;
	m_Buffer = new char[m_Size + 1];
	memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}

//赋值运算符 
String& String::operator=(const String& other)
{
	std::cout << "Assignment Operator Called!" << other << std::endl;

	// 1. 自赋值检查 (防止 s1 = s1 的骚操作)
	if (this == &other)
		return *this;

	// 2. 释放当前对象旧的内存,防止内存泄漏
	delete[] m_Buffer;

	// 3. 申请新空间并拷贝内容
	m_Size = other.m_Size;
	m_Buffer = new char[m_Size + 1];
	memcpy(m_Buffer, other.m_Buffer, m_Size + 1);

	// 4. 返回对象自身,支持链式赋值 (a = b = c)
	return *this;
}

// 析构函数
String::~String()
{
	delete[] m_Buffer;
}

// 下标运算符重载
char& String::operator[](unsigned int index)
{
	return m_Buffer[index];
}

// 友元函数定义(注意:定义时不需要加 String::,也不需要加 friend 关键字)
std::ostream& operator<<(std::ostream& stream, const String& string)
{
	stream << string.m_Buffer;
	return stream;
}

方法1:使用引用#

#include <iostream>
#include "String.h"

void ParseShader(String& vertexSource, String& fragmentSource)
{
	//假设vs,fs最终计算值是abc、def
	String vs = "abc";
	String fs = "def";

	//这两行代码执行的是 字符串赋值(内部会拷贝)
	vertexSource = vs;
	fragmentSource = fs;
}

int main()
{
	String vs, fs;
	ParseShader(vs, fs);
	std::cout << vs << std ::endl;
	std::cout << fs << std::endl;
	std::cin.get();
}
/*
Assignment Operator Called!abc
Assignment Operator Called!def
abc
def

*/

方法2:使用指针#

#include <iostream>
#include "String.h"

void ParseShader(String* vertexSource, String* fragmentSource)
{
	//假设vs,fs最终计算值是abc、def
	String vs = "abc";
	String fs = "def";

	//这两行代码执行的是 字符串赋值(内部会拷贝)
	if (vertexSource) {
		*vertexSource = vs;
	}
	if (fragmentSource) {
		*fragmentSource = fs;
	} 
}

int main()
{
	String vs, fs;
	ParseShader(&vs, &fs);
	//这种方式允许空指针
	//ParseShader(nullptr, &fs);
	std::cout << vs << std::endl;
	std::cout << fs << std::endl;
	std::cin.get();
}
/*
Assignment Operator Called!abc
Assignment Operator Called!def
abc
def
*/

(同类型)方法3:返回(数组)指针#

#include <iostream>
#include "String.h"

String* ParseShader()
{
	//假设vs,fs最终计算值是abc、def
	//vs,fs先在栈上创建(但String成员m_Buffer是指向堆)
	String vs = "abc";
	String fs = "def";

	//这种语法在c++20才能编译通过
	return new String[]{ vs,fs };
} 

int main()
{ 
	//这里不知道数组有多大
	String* result =ParseShader();

	std::cout << result[0] << std::endl;
	std::cout << result[1] << std::endl;
	std::cin.get();
}
/*
Assignment Operator Called!abc
Assignment Operator Called!def
abc
def
*/

分析:

51创建并使用库

如何在vs中设置多个项目,以及如何创建一个能在所有项目中使用的库

51创建并使用库#

先以Game51命名创建一个项目

文件夹查看

再新建一个项目,项目名为Engine

此时


现在确保设置Game51为应用程序

Engine项目配置为静态库

创建文件夹及文件#

show All Files

目录结构

(静态链接库)Engine项目#

//src/Engine.h
#pragma once

namespace engine {
	void PrintMessage();
}
 
//不能这么写,说明:: 被称为作用域解析运算符。它的逻辑
// 是:你只能引用一个已经存在的名字空间。而这里正在定义
//所以不存在
//void engine::PrintMessage();
///src/Engine.cpp
#include "Engine.h"
#include <iostream>

namespace engine {
	void PrintMessage()
	{
		std::cout << "Hello World!" << std::endl;
	}
}

//或者这么写
//void engine::PrintMessage()
//{
//	std::cout << "Hello World!" << std::endl;
//}

(使用静态链接库)Game51项目中#

处理头文件的两个方法#

//Application.cpp

//添加头文件-方法1
//#include "../../Engine/src/Engine.h"

//直接声明-方法0
//namespace engine {
//	void PrintMessage();
//}


int main()
{

	engine::PrintMessage();
}

处理头文件的第3个方法#

右键项目Game51属性修改

50使用动态库

动态链接是什么,以及如何使用

  • 动态链接是在运行时发生的链接,当你实际启动可执行文件时,动态链接库加载时 它并非可执行文件的一部分,此时动态链接库被载入内存。意味着运行时会动态链接另一个库,然后运行应用程序 载入一个额外文件到内存。
    • 这种情况下,可执行文件实际上需要某些库存在(外部文件)。比如windows系统经常出现“缺少某些dll文件无法启动”
    • 可执行文件了解动态链接库,可执行文件是一个独立文件,在运行时加载的独立模块。但也可以加载动态链接库,他会在运行时查找并加载动态库,然后可以获取函数指针/动态库中任何内容
  • 静态链接是在编译时发生的链接,当你编译一个静态库时,将它链接到可执行文件、应用程序或动态库中。即:直接获取静态库的内容,然后把它和其他二进制数据放到一起,静态库实际存在于可执行文件或动态库中,所以编译器、链接器完全清楚这些代码,并会对此进行优化

当我们说“链接到一个动态库”时,实际上是指在创建(生成)那个 DLL 的时候,把静态库的代码塞进去。

补充:动态链接包括“隐式链接” 需要 header (.h) 和 import library (.lib) 和“显示链接” 编译阶段完全不认识这个 DLL,也不需要 .lib 导入库。用法: 运行时使用 Windows API 函数(如 LoadLibrary 和 GetProcAddress)去手动加载。

50使用动态库#

复制Dependencies文件夹到根目录

λ tree Dependencies /f

CHERNOCPP\HELLOWORLD49\DEPENDENCIES
└─GLFW
    ├─include
      └─GLFW
              glfw3.h
              glfw3native.h
    
    └─lib-vc2022
            glfw3.dll
            glfw3.lib
            glfw3dll.lib
            glfw3_mt.lib

静态链接和动态链接都是用同一部分的头文件

目前按ctrl+f7已经能正常编译

glw3dll.lib glfw3dll.lib不存代码,只有地址 ,这是指向glw3.dll的一系列指针,所以我们在运行时无需实际检测所有内容的位置,这两者同时编译很重要,因为在运行时如果尝试使用不同静态库与DLL链接,你可能会遇到函数不匹配,并且函数指针会出现内存地址错误。但是这里是由glfw分发的,所以glw3.dll和glw3dll.lib是同时编译的,而且他们直接相互关联,这两者不能分开。

接下来设置库文件

右键项目并build成功。但是目前如果会提示找不到dll文件

49使用静态库

49使用静态库#

  • 从github库或其他敌法check out代码块,在那个代码库里能直接得到编译所需的一切,并运行应用程序或项目等,无需使用包管理器
  • 倾向于实际保留依赖项的版本,实际解决方案中二进制的版本。实际项目文件夹中有物理二进制文件的副本,或者是源码

自己编译?或者直接链接预构建的二进制文件

  • 建议直接添加另一个项目(依赖项的源代码),然后编译成静态库或动态库
  • 非大型项目,可以直接链接二进制文件(无需源代码)

本篇要处理的是GLFW的库

选择二进制版本 (32位 vs 64位),这取决于你的目标应用程序架构,而不是你的操作系统。

比如现在我要制作一个32位应用程序

//结构

//里面通常是 HTML 格式的 API 手册。
├─docs
  └─html
      └─search
├─include
  └─GLFW

//含义: 针对 MinGW 编译器的版本(通常配合 GCC 使用)。
//用途: 如果你不在 Windows 上用 Visual Studio,而是用 CLion、Code::Blocks 或者直接在命令行用 g++ 编译,你需要链接这个文件夹里的库(通常是 .a 文件而非 .lib)。
├─lib-mingw-w64 
  └─libglfw3.a //(静态库): 如果你用 MinGW 编译器并想进行静态链接
  └─glfw3.dll //(动态链接库): 这是库的实体。 如果你选择动态链接,这个文件必须在运行时放在你的 .exe 旁边
  └─libglfw3dll.a //给编译器看的“目录”。 当你选择动态链接时,你不是直接链接 .dll,而是链接这个 .a 文件。 它告诉编译器:“真正的代码在 glfw3.dll 里,你先编译通过,运行时再去调它。”

//如果你的项目配置为使用通用的 Windows 10/11 SDK 运行时,有时需要专门链接这个版本的库。
├─lib-static-ucrt

//以下是库文件 (.lib),是给 链接器 用的。之所以有这么多,是因为 C++ 的二进制兼容性比较复杂,库的版本必须尽量与你使用的 Visual Studio 版本匹配
├─lib-vc2013
├─lib-vc2015
├─lib-vc2017
├─lib-vc2019
└─lib-vc2022

库由包含目录includes库目录libraries组成

47优化cpp中stdvector的使用

预习

#include <iostream>
#include <string>
#include <vector>

struct Test
{

};
struct Vertex
{
	float x, y, z;

	//有了其他的构造函数,如果需要用到无参构造函数,
	//就必须手动写一个
	Vertex()
	{

	}

	//因为有构造函数了,所以Vertex 结构体不是一个聚合类型(
	// 有构造函数),所以要使用显示构造函数
	// 使 Vertex v = { 1,2,3 }; 编译通过
	Vertex(float x, float y, float z)
	{
		this->x = x;
		this->y = y;
		this->z = z;
	}

	//参数必须是&,原因如下:
	//1. 当你尝试通过 Vertex a = b; 调用复制构造函数时,你需要将 b 传递给参数 other。
	//2. 在 C++ 中,按值传递(Pass by Value)参数本身就会触发一次复制。
	//3. 为了复制 b 到 other,编译器又需要调用复制构造函数。[死循环了]
	//因为main中使用 vector.push({1,2,3}) 需要将临时对象拷贝到vector中,所以这里的参数
	//必须是const
	Vertex(const Vertex& vertex)
	{
		std::cout << "copied constructor handled" << std::endl;
		this->x = vertex.x;
		this->y = vertex.y;
		this->z = vertex.z;
	}
	/*virtual*/ void test()
	{

	}

	~Vertex()
	{
		std::cout << "destructor handled" << std::endl;
	}
};

std::ostream& operator<<(std::ostream& stream, const Vertex& vertex)
{
	stream << vertex.x << "," << vertex.y << "," << vertex.z;
	return stream;
}

//使用引用避免复制整个数组
void Function(const std::vector<Vertex>& vertices)
{

}

int main()
{
	std::vector<Vertex> vertices;
	//vertices.push_back({ 1,2,3 });
	//std::cout << "==0===" << std::endl;
	//vertices.push_back({ 4,5,6 });

	//对上面两行代码优化
	// 1. 预先分配内存,防止搬家
	vertices.reserve(1);
	// 2. 原地构造,防止临时拷贝
	vertices.emplace_back(1, 2, 3);
	std::cout << "==0===" << std::endl;
	//超出容量,会拷贝原来的{1,2,3}到新数组,并且销毁
	// 原数组中的{1,2,3}
	vertices.emplace_back(4, 5, 6);


	std::cout << "==1===" << std::endl;
	Function(vertices);
	std::cout << "==2===" << std::endl;
	vertices.erase(vertices.begin() + 1);
	std::cout << "==3===" << std::endl;
	for (Vertex& v : vertices)
		std::cout << v << std::endl;
	std::cin.get();
}
/*
copied constructor handled //第一个临时对象复制到vector中之后,马上又销毁了
destructor handled
==0===
copied constructor handled //空间不够了,需要把第一个元素复制到新数组中。第二个元素创建临时对象,并复制到新数组中
copied constructor handled
destructor handled  //旧的数组中的第一个元素被销毁了
destructor handled  //第二个元素的临时对象被销毁了
==1===
==2===
destructor handled
==3===
1,2,3
*/

/*优化后不需要把临时对象复制到vector中,也不需要销毁临时对象了
==1===
==2===
destructor handled
==3===
1,2,3
*/

了解环境是优化过程中最重要的事情之一

46动态数组

  • 动态数组,特别是讨论标准向量类
  • 要习惯CPP的标准库(标准模版库)
  • 标准模版库包含了各种容器、迭代器、算法、仿函数

  • 容器的数据类型由程序员自己决定
  • cpp提供了一个Vector的类,在std命名空间中,可以调整大小,也可以不指定初始大小
  • 本系列会重写C++中的数据结构,优化后比标准模版库中的快很多

std::vector 的实际工作方式:

当超出实际的分配大小时。当创建一个向量,可能会先分配10个元素大小的空间,当超出这个大小时,会在内存中创建一个比原来更大的数组,并把所有内容复制过去,然后删除旧数组。这样就有了一个更大存储空间的新数组

46动态数组#

struct Test
{

};
struct Vertex 
{  
	float x, y, z;
	/*virtual*/ void test()
	{

	}
};

//main()中
Vertex v = { 1,2,3 };

聚合初始化(Aggregate Initialization),Vertex 结构体是一个聚合类型(Aggregate Type)。在 C++ 中,如果一个类或结构体满足以下条件:

  • 没有显式定义的构造函数。
  • 成员变量是公有的(public)。
  • 没有基类(没有继承)。
  • 没有虚函数。

那么你可以使用大括号 {} 直接按顺序为成员变量赋值。

vector的基本使用#

#include <iostream>
#include <string>
#include <vector>

struct Test
{

};
struct Vertex
{
	float x, y, z;

	//有了其他的构造函数,如果需要用到无参构造函数,
	//就必须手动写一个
	Vertex()
	{

	}

	//因为有构造函数了,所以Vertex 结构体不是一个聚合类型(
	// 有构造函数),所以要使用显示构造函数
	// 使 Vertex v = { 1,2,3 }; 编译通过
	Vertex(float x, float y, float z)
	{
		this->x = x;
		this->y = y;
		this->z = z;
	}

	//参数必须是&,原因如下:
	//1. 当你尝试通过 Vertex a = b; 调用复制构造函数时,你需要将 b 传递给参数 other。
	//2. 在 C++ 中,按值传递(Pass by Value)参数本身就会触发一次复制。
	//3. 为了复制 b 到 other,编译器又需要调用复制构造函数。[死循环了]
	//因为main中使用 vector.push({1,2,3}) 需要将临时对象拷贝到vector中,所以这里的参数
	//必须是const
	Vertex(const Vertex& vertex)
	{
		std::cout << "copied constructor handled" << std::endl;
		this->x = vertex.x;
		this->y = vertex.y;
		this->z = vertex.z;
	}
	/*virtual*/ void test()
	{

	}

	~Vertex()
	{
		std::cout << "destructor handled" << std::endl;
	}
};

std::ostream& operator<<(std::ostream& stream, const Vertex& vertex)
{
	stream << vertex.x << "," << vertex.y << "," << vertex.z;
	return stream;
}

int main()
{
	Vertex vertices1[5];
	Vertex* vertices2 = new Vertex[5];

	std::vector<int> myarray;
	//存储Vertex对象,则动态数组的内存是连续的,
	//其中的Vertext对象会按行排列
	//问题是:实际要调整向量大小时得复制所有(类)数据,但内存连续性
	// 带来的读取速度提升通常远超扩容时的开销
	std::vector<Vertex> vertices;
	//存储Vertext指针,实际要调整向量大小是只是复制指针地址(一个数字)
	//,即实际数据的内存地址
	std::vector<Vertex*> vertices4;

	std::cout << "---1---" << std::endl;
	Vertex v = { 1,2,3 };
	//这里演示vertices的使用
	std::cout << "---2---" << std::endl;
	vertices.push_back({ 1,2,3 });
	std::cout << "---3---" << std::endl;
	vertices.push_back({ 4,5,6 });

	std::cout << "---4---" << std::endl;
	for (int i = 0; i < vertices.size(); i++)
	{
		//c++对[]进行了重载
		std::cout << vertices[i] << std::endl;
	}
	std::cout << "---5---" << std::endl;
	//这里会把每个Vertex复制到v
	for (Vertex v : vertices)
		std::cout << v << std::endl;
	std::cout << "---6---" << std::endl;
	//避免复制
	for (Vertex& v : vertices)
		std::cout << v << std::endl;

	//clear() 会移除容器中的所有元素,但通常不一定会立即释放底层内存(容量不变),它只是销毁现有的对象。
	vertices.clear();

	std::cin.get();
}

解释一下vertices.push_back({1, 2, 3});#

当你执行 vertices.push_back({1, 2, 3}); 时,逻辑确实是:在 vector 内部的内存中,通过调用复制构造函数来构造一个属于 vector 自己的对象。

45箭头运算符

在 C++ 中,-> 运算符有一个独特的“递归”特性。当你写下 entity->Print() 时,编译器会查看 entity 的类型:

  • 如果 entity 是原生指针:它直接解引用并访问成员。
  • 如果 entity 是一个对象(类实例):
    1. 编译器会调用你重载的 operator->()。
    2. 这个函数必须返回两样东西之一:要么是一个原生指针,要么是另一个重载了 -> 的对象。
    3. 编译器拿到返回的指针后,自动再次对它使用箭头操作,直到找到最终的成员。

箭头运算符的使用及重载#

#include <iostream>
#include <string>

class Entity
{
public:
	int x;
public:
	void Print() const
	{
		std::cout << "Hello!x=" << x << std::endl;
	}
};

//创建这个类的对象时传入一个堆上的对象,
//可以使用堆对象,且堆对象会被自动释放
class ScopePtr
{
private:
	Entity* m_Obj;
public:
	ScopePtr(Entity* entity)
		:m_Obj(entity)
	{

	}
	~ScopePtr()
	{
		std::cout << "release m_Obj" << std::endl;
		delete m_Obj;
	}

	Entity* GetObject()
	{
		return m_Obj;
	} 
};

int main()
{
	Entity e;
	e.Print();

	//取消引用后用 .函数
	Entity* ptr = &e;
	Entity& entity = *ptr;
	//entity.Print();
	//(*ptr).Print();
	ptr->x = 2;
	ptr->Print();
	{
		ScopePtr entity = new Entity();
		entity.GetObject()->Print(); 
	}

	std::cin.get();
}

重载箭头运算符#

#include <iostream>
#include <string>

class Entity
{
public:
	int x;
public:
	void Print() const
	{
		std::cout << "Hello!x=" << x << std::endl;
	}
};

//创建这个类的对象时传入一个堆上的对象,
//可以使用堆对象,且堆对象会被自动释放
class ScopePtr
{
private:
	Entity* m_Obj;
public:
	ScopePtr(Entity* entity)
		:m_Obj(entity)
	{

	}
	~ScopePtr()
	{
		std::cout << "release m_Obj" << std::endl;
		delete m_Obj;
	}

	Entity* GetObject()
	{
		return m_Obj;
	}

	//1. 告诉编译器:“我把内部的指针给你用,但你只能
	// 看,不能改指针指向的那个 Entity。”
	//2. 右边的const:“这个函数是一个‘只读’函数,它不会修改
	// ScopedPtr 对象内部的任何成员变量。”,而且,他的返回值
	//不能调用非const函数
	const Entity* operator->() const 
	{
		return m_Obj;
	}
};

int main()
{
	Entity e;
	e.Print();

	//取消引用后用 .函数
	Entity* ptr = &e;
	Entity& entity = *ptr;
	//entity.Print();
	//(*ptr).Print();
	ptr->x = 2;
	ptr->Print();
	{
		//隐式转换,编译器会寻找匹配的构造函数
		//等同于:ScopePtr entity(new Entity()); // 直接初始化
		// 或者 ScopePtr entity = ScopePtr(new Entity()); // 显式转换
		ScopePtr entity = new Entity();
		entity.GetObject()->Print();
		entity->Print();
	}

	std::cin.get();
}

对于ScopePtr entity = new Entity();,编译器的逻辑

44_1使用memcpy的注意事项

毁掉多态:篡改虚函数表指针 (vptr)#

#include <iostream>
#include <cstring>

class Entity {
public:
    virtual void SayName() { std::cout << "I am Entity" << std::endl; }
};

class Player : public Entity {
public:
    void SayName() override { std::cout << "I am Player" << std::endl; }
};

void Print(Entity* e) {
    e->SayName(); // 强制程序通过虚表查找
}

int main() {
    Entity base;
    Player p1;

    std::cout << "Before memcpy: ";
    Print(&p1); // 应该输出 I am Player

	//base复制给p1,但是p1的虚函数表却指向的是Entity的
    // 暴力覆盖!把 base 的内存(包含 Entity 的 vptr)强行塞进 p1
    std::memcpy(&p1, &base, sizeof(Entity));

    std::cout << "After memcpy:  ";
    Print(&p1); // 这次,它一定会输出 I am Entity!

    std::cin.get();
}

内存错位#

#include <iostream>
#include <cstring>

struct BaseA {
    int a = 111;
};

struct BaseB {
    int b = 222;
};

// Child 同时拥有 a 和 b,继承顺序决定内存顺序,这里先BaseA-->BaseB-->自己的成员
//在内存里,Child 对象是一块连续的砖,布局如下(假设每个 int 占 4 字节):
// 字节偏移  内容  属于谁
// 0    int a(111)  BaseA的地盘
// 4    int b(222)  BaseB的地盘8int c(3)Child 的地盘
struct Child : public BaseA, public BaseB {
    int c = 333;
};

int main() {
    Child p;
    BaseB sourceB;
    sourceB.b = 999; // 我们准备了一个新的 B,想把它拷进 p 里

    std::cout << "--- 拷贝前 ---" << std::endl;
    std::cout << "p.a = " << p.a << " (BaseA部分)" << std::endl;
    std::cout << "p.b = " << p.b << " (BaseB部分)" << std::endl;

    // --- 致命操作 ---
    // 程序员的意图:把 sourceB 的数据拷贝给 p
    // 程序员认为:p 既然继承了 BaseB,那我就直接拷过去
    std::memcpy(&p, &sourceB, sizeof(BaseB));

    std::cout << "\n--- 运行 memcpy(&p, &sourceB, ...) 后 ---" << std::endl;

    // 错位发生了!
    // 1. p.a 被改成了 999。因为 memcpy 从 p 的开头(BaseA的位置)开始写。
    // 2. p.b 依然是 222。因为它在内存后面,memcpy 根本没写到它。

    std::cout << "p.a = " << p.a << " (被错误覆盖了!)" << std::endl;
    std::cout << "p.b = " << p.b << " (完全没被拷进去!)" << std::endl;

    std::cin.get();

    return 0;
}