学习方向:C++ 面向对象编程(OOP) · DirectX 12 前置知识
今天完成了 C++ 面向对象的第 3~6 讲,涉及对象指针、构造与析构、继承、虚函数和多态。
这次总结保留你独立编写的代码、存在的问题、修正方式以及重要知识点,可以直接整理到博客或 GitHub 学习日志。
第 3 讲:对象指针与 .、->
1. 我的练习代码
#include <iostream>
using namespace std;
class GPU
{
private:
int temperature = 40;
public:
void SetTemperature(int t)
{
if (t >= 0 && t <= 120)
{
temperature = t;
}
}
void ShowTemperature()
{
cout << "当前温度:" << temperature
<< "C" << endl;
}
};
int main()
{
GPU gpu1;
gpu1.ShowTemperature();
GPU* p = &gpu1;
p->SetTemperature(85);
gpu1.ShowTemperature();
p->ShowTemperature();
return 0;
}
2. 运行结果
当前温度:40C
当前温度:85C
当前温度:85C
3. 核心知识点
对象与对象指针的区别
GPU gpu1;
GPU* p = &gpu1;
GPU gpu1:创建一个 GPU 对象。GPU* p:声明一个指向 GPU 对象的指针。&gpu1:获取对象的地址。p:保存对象的地址,而不是创建一个新对象。
. 和 -> 的区别
gpu1.ShowTemperature(); // 对象访问成员
p->ShowTemperature(); // 指针访问成员
对于普通对象指针:
p->ShowTemperature();
等价于:
(*p).ShowTemperature();
也就是先解引用指针,再访问对象成员。
4. 练习检查
本次代码没有发现需要纠正的错误。
特别是温度范围判断:
if (t >= 0 && t <= 120)
正确包含了两个边界值,也修正了上一讲容易遗漏边界的习惯。
本讲结论:指针能够通过 -> 访问它所指向对象的成员,修改的是原对象的数据。
第 4 讲:构造函数与析构函数
1. 我的原始代码
#include <iostream>
using namespace std;
class Renderer
{
private:
int width;
int height;
public:
Renderer()
{
width = 800;
height = 600;
cout << "默认渲染器创建" << endl;
}
Renderer(int w, int h)
{
width = w;
height = h;
cout << "自定义渲染器创建" << endl;
cout << "当前分辨率:"
<< width << "X" << height << endl;
}
~Renderer()
{
cout << "渲染器销毁";
}
};
int main()
{
Renderer r1;
{
Renderer r2(1920, 1080);
r2.Renderer();
}
r1.Renderer();
return 0;
}
2. 发现的问题
这次主要有一个概念性错误:
r2.Renderer();
r1.Renderer();
构造函数不能通过对象像普通成员函数一样调用。
例如:
Renderer r1;
在这一句中,无参构造函数已经自动执行。
而:
Renderer r2(1920, 1080);
会自动选择带参构造函数:
Renderer(int w, int h)
所以不应该再写 r2.Renderer()。
另外,原代码缺少题目要求的 ShowResolution() 成员函数,析构输出也没有换行。
3. 修正后的代码
#include <iostream>
using namespace std;
class Renderer
{
private:
int width;
int height;
public:
Renderer()
{
width = 800;
height = 600;
cout << "默认渲染器创建" << endl;
}
Renderer(int w, int h)
{
width = w;
height = h;
cout << "自定义渲染器创建" << endl;
}
~Renderer()
{
cout << "渲染器销毁" << endl;
}
void ShowResolution()
{
cout << "当前分辨率:"
<< width << "x" << height << endl;
}
};
int main()
{
Renderer r1;
{
Renderer r2(1920, 1080);
r2.ShowResolution();
}
r1.ShowResolution();
return 0;
}
4. 核心知识点
构造与析构的顺序
Renderer r1;
{
Renderer r2(1920, 1080);
}
执行过程:
- 调用
r1的构造函数 - 调用
r2的构造函数 - 离开内部作用域,调用
r2的析构函数 - 离开
main()作用域,调用r1的析构函数
5. 本讲总结
本次错误源于混淆了构造函数和普通成员函数。
需要重点记忆:
- 构造函数负责对象初始化,不需要手动调用。
- 普通成员函数可以通过对象主动调用。
- 局部对象在生命周期结束时通常会自动析构。
- 构造函数可以重载,编译器根据参数选择适合的版本。
这些知识是后续理解 C++ 的 RAII 资源管理机制的基础。
第 5 讲:继承(Inheritance)
1. 我的练习代码
#include <iostream>
using namespace std;
class GPU
{
private:
int memory = 8;
public:
void SetMemory(int m)
{
if (m > 0)
{
memory = m;
}
}
void ShowMemory()
{
cout << "显存:" << memory
<< "GB" << endl;
}
};
class NvidiaGPU : public GPU
{
public:
void EnableRayTracing()
{
cout << "光线追踪已启用";
}
};
int main()
{
NvidiaGPU gpu1;
gpu1.SetMemory(16);
gpu1.ShowMemory();
gpu1.EnableRayTracing();
}
2. 代码检查
本次代码逻辑正确,能够完成题目要求。
可以优化的是输出:
cout << "光线追踪已启用";
建议添加换行:
cout << "光线追踪已启用" << endl;
此外,main() 末尾省略 return 0; 在 C++ 中是合法的。
3. 核心知识点
继承的基本语法
class NvidiaGPU : public GPU
其中:
GPU:基类。NvidiaGPU:派生类。public:公有继承方式。
派生类可以使用基类中允许访问的成员,并增加自己的成员。
例如:
gpu1.SetMemory(16); // 继承的成员函数
gpu1.ShowMemory(); // 继承的成员函数
gpu1.EnableRayTracing(); // 派生类自己的函数
继承与 private
基类中的私有成员:
private:
int memory = 8;
不能被派生类的成员函数直接访问。
但派生类可以通过基类提供的公有函数间接修改它:
SetMemory(16);
4. 本讲总结
继承允许派生类复用基类的功能,并在此基础上添加自己的功能。
但需要特别注意:
继承不意味着派生类获得了访问基类所有成员的权限。
private 的访问控制依然有效。
第 6 讲:虚函数与多态
这是今天最重要的一节,也是理解 DX12 COM 接口的重要前置知识。
1. 我的练习代码
#include <iostream>
using namespace std;
class GPU
{
public:
virtual void ShowInfo()
{
cout << "普通GPU" << endl;
}
};
class NvidiaGPU : public GPU
{
public:
void ShowInfo() override
{
cout << "NVIDIA RTX 4090" << endl;
}
};
class AMDGPU : public GPU
{
public:
void ShowInfo() override
{
cout << "AMD Radeon" << endl;
}
};
int main()
{
NvidiaGPU nvidia;
AMDGPU amd;
GPU* p1 = &nvidia;
GPU* p2 = &amd;
p1->ShowInfo();
p2->ShowInfo();
return 0;
}
2. 运行结果
NVIDIA RTX 4090
AMD Radeon
本次代码正确,成功实现了 C++ 运行时多态。
3. 核心知识点
virtual:虚函数
virtual void ShowInfo()
表示这个函数支持动态分派。
override:重写
void ShowInfo() override
表示派生类要重写基类中的虚函数,并让编译器检查是否正确重写。
多态:同一接口,不同实现
GPU* p1 = &nvidia;
GPU* p2 = &amd;
两个指针的静态类型都是 GPU*,但指向不同类型的派生类对象。
由于 ShowInfo() 是虚函数:
p1->ShowInfo(); // NVIDIA RTX 4090
p2->ShowInfo(); // AMD Radeon
程序会根据实际对象选择对应的重写版本。
4. 本次概念纠正
你在练习后提出:
删除 virtual 后,两句都会输出普通 GPU,因为基类和派生类存在同名函数时,会默认执行基类的函数。
这里的输出判断在通过 GPU* 调用且同时删除 override 的条件下正确,但原因需要修正。
如果删除 virtual 和派生类中的 override:
NvidiaGPU nvidia;
nvidia.ShowInfo(); // NVIDIA RTX 4090
GPU* p = &nvidia;
p->ShowInfo(); // 普通GPU
为什么两次输出不同?
nvidia.ShowInfo():表达式的静态类型是NvidiaGPU,所以调用派生类函数。p->ShowInfo():p的静态类型是GPU*,由于函数不是虚函数,所以调用基类版本。
如果基类的 ShowInfo() 是虚函数,则通过基类指针调用时会进行动态分派。
nvidia.ShowInfo()NVIDIA RTX 4090NVIDIA RTX 4090p->ShowInfo()普通GPUNVIDIA RTX 4090还有一个容易忽略的点:
如果删除基类的 virtual,却保留派生类的 override,代码会编译失败。
因为 override 要求函数确实重写了基类的虚函数。
5. 本讲总结
本节掌握了三个概念:
- 静态绑定:根据表达式的静态类型决定非虚函数调用。
- 动态绑定:虚函数调用可以根据实际对象的动态类型选择最终重写版本。
- 多态:通过统一的基类指针或引用,调用不同派生类对象提供的实现。
这正是后面理解 COM 接口机制的重要基础。
今天暴露的问题及进步
相比前两讲,今天的代码中没有再出现循环或条件判断边界错误,函数定义、成员访问与继承结构也更加稳定。
目前主要需要强化的是构造函数的特殊性,以及多态调用时静态类型和动态类型的区别。
与 DirectX 12 的联系
今天学的四节课可以直接对应到你之前遇到的 DX12 代码:
ComPtr<IDXGIFactory6> factory;
HRESULT hr = CreateDXGIFactory2(
0,
IID_PPV_ARGS(factory.GetAddressOf())
);
ComPtr 对象对象指针、->通过接口指针调用方法构造与析构ComPtr 的生命周期管理继承DXGI 接口的继承关系虚函数与多态COM 接口的方法调用机制你现在回头看 factory->...、接口类型和成员函数,应该比刚开始接触 DX12 时更容易理解其 C++ 语法结构。
不过,COM 还涉及接口标识符、引用计数和 QueryInterface() 等知识,后续需要单独学习。
下一阶段学习安排
第 7 讲:纯虚函数与抽象类
理解 virtual void Function() = 0;
第 8 讲:智能指针与 COM 基础
模板、ComPtr、IUnknown、引用计数与 RAII
返回 DX12 系统课程
重新理解 DXGI Factory、Adapter 和 Device
今日结论
今天完成了四次独立 Coding 练习,已经能够使用对象指针、继承和虚函数编写简单的多态程序。第 4 讲存在的构造函数调用错误,也已经明确了修正方法。
下一步不必急着扩大学习范围。先通过纯虚函数与抽象类把当前学到的继承、多态知识连起来,再补充 COM 和智能指针,就可以恢复 DX12 学习主线。
今天最重要的收获:C++ 不仅能通过类组织数据和函数,还能利用继承与虚函数,通过统一的类型接口调用不同对象的实际实现。