C++面向对象核心知识点实战-游戏角色战斗系统(详细讲解指针/函数指针/继承/多态)
文章目录
前言
本文的主要作用是对C++中比较核心的几个知识点进行分析和理解,其主要作用也是为了让本人的学习成果进行一个阶段性的总结,为此我将从相关的知识点讲解,结合部分代码进行实战,最后通过一个小项目进行整体的知识点运用,如果想直接看项目的话可以翻到最后一页
一、指针
在C/C++语言学习的过程当中,指针就是一个很难跨越的鸿沟,那么我将从最基础的进行分析讲解
1.1指针的定义
指针是一个变量,它的值是另一个变量的内存地址
通俗理解:
指针就像一个内存地址的 “门牌号”:
普通变量存储的是数据本身(比如 int a = 10; 中 a 存的是数值 10);
指针变量存储的是另一个变量的内存地址(比如 int *p = &a; 中 p 存的是 a 在内存中的地址,通过这个地址就能找到 a)。
专业定义:
指针是 C/C++ 中的一种数据类型,对应的变量称为 “指针变量”,
它的核心特征是:
存储的不是数据值,而是内存单元的地址(通常是某个变量 / 对象 / 函数的起始地址);
通过指针可以间接访问(读写)该地址指向的内存空间中的数据;
指针的类型(如 int*、char*)决定了 “解引用” 时能操作的内存大小(比如 int* 对应 4 字节,char* 对应 1 字节)。
#include <stdio.h>
int main() {
// 1. 普通变量:存储数据本身
int a = 10;
printf("变量a的值:%d\n", a); // 输出:10
printf("变量a的内存地址:%p\n", &a); // 输出:a的地址(比如 0x7ffeefbff5ac)
// 2. 指针变量:存储a的地址(定义格式:类型* 指针名 = &变量名)
int *p = &a; // *表示p是指针变量,int表示p指向的是int类型变量,&a是取a的地址
printf("指针p的值(即a的地址):%p\n", p); // 输出:和&a完全相同的地址
// 3. 解引用:通过指针访问指向的变量(*p 等价于 a)
*p = 20; // 等价于 a = 20,通过指针修改a的值
printf("修改后a的值:%d\n", a); // 输出:20
printf("通过指针访问a的值:%d\n", *p); // 输出:20
return 0;
}
C++中的指针:
int a = 10;
int* p = &a; // p 存储了 a 的地址
// 访问
cout << p; // 输出地址 (如 0x7ffee...)
cout << *p; // 输出值 (10),这叫“解引用”
// 修改
*p = 20; // a 现在变成了 20
其本质是没有什么变化的,所以只要理解了其基本的定义和使用方法,任何时候都能灵活使用
1.2变量指针
1.2.1概念:即指向普通变量的指针
定义:指针是一个变量,其存储的值是另一个变量的内存地址。
本质:它是连接“变量名”与“内存物理地址”的桥梁。
大小:指针本身的大小取决于系统架构(32位系统为4字节,64位系统为8字节),与它指向的变量类型(int, double, struct)无关。
1.2.2三大基本操作符
| 操作符 | 名称 | 作用 | 示例 |
|---|---|---|---|
& |
取地址符 | 获取变量在内存中的地址 | int* p = &a; (p 存了 a 的地址) |
* |
解引用符 | 访问指针所指向地址上的值 | *p = 10; (修改 a 的值为 10) |
= |
赋值符 | 改变指针的指向或修改目标值 | p = &b; (p 改指 b) |
1.2.3指针与变量的关系模型
理解这一层是掌握指针的关键:
间接访问:通过指针修改变量,效果等同于直接修改变量。
别名机制:指针可以看作变量的“别名”或“遥控器”。
传递机制:
传值:函数接收变量副本,原变量不变。
传址(指针):函数接收地址,可以直接修改原变量(这是指针最核心的用途之一)。
void swap(int* x, int* y) {
int temp = *x;
*x = *y;
*y = temp;
}
// 调用:swap(&a, &b); -> a 和 b 的值真正交换了
1.2.4关键修饰符:const与指针
这是最容易混淆的知识点,口诀:“左定值,右定向”(const 在 * 左边还是右边)。
| 声明方式 | 含义 | 能否修改变量值? | 能否改变指针指向? | 典型场景 |
|---|---|---|---|---|
int* p |
普通指针 | 能 | 能 | 通用 |
const int* p (或 int const* p) |
常量指针 (Pointer to Constant) |
不能 ( *p = 5 报错) |
能 ( p = &b 合法) |
函数参数,承诺不修改传入变量 |
int* const p |
指针常量 (Constant Pointer) |
能 | 不能 (初始化后不可改指向) |
固定指向某个硬件寄存器或配置项 |
const int* const p |
双常指针 | 不能 | 不能 | 极度安全的只读访问 |
1.2.5生命周期
虚空指针:当指针指向的变量已经销毁(超出作用域或被 delete),指针仍保留该地址。
int* getDangling() {
int local = 10;
return &local; // 错误!local 函数结束后销毁,返回的地址无效
}
野指针:定义了指针但未初始化,它指向随机内存地址。
int* p; // 危险!p 指向未知位置
*p = 5; // 可能直接覆盖系统内存导致崩溃
1.3指针与数组(重点)
这个部分是最容易混淆的部分,主要是很像,所以我将由浅入深进行分析理解
1.3.1联系
数组名(如arr)本身就是一个常量指针(地址不可修改),指向数组第一个元素的内存地址:
int arr[5] = {1,2,3,4,5};
// 以下两行等价:arr 等价于 &arr[0](数组首元素的地址)
int *p = arr; // 正确:数组名直接赋值给指针
int *p = &arr[0]; // 等价写法
//数组arr是一排房子(地址:101、102、103、104、105);
//数组名arr就是 “101 室的门牌号”(首元素地址),是固定的(不能改);
//指针p是 “可移动的门牌号标签”,可以贴到 101、102… 任意房间。
数组下标访问 ≡ 指针偏移 + 解引用
数组的下标访问arr[i],编译器会自动转换为*(arr + i)(指针偏移后解引用),两者完全等价:
int arr[5] = {1,2,3,4,5};
cout << arr[2]; // 输出3:下标访问第3个元素
cout << *(arr+2); // 输出3:指针偏移2位后解引用,等价于arr[2]
cout << *(p+2); // 输出3:p是指向arr首元素的指针,等价写法
//arr + i:数组首地址向后偏移i个元素的地址(不是字节!比如int占 4 字节,arr+1实际地址 + 4);
//*(arr + i):解引用偏移后的地址,取对应位置的值。
指针遍历数组
数组遍历既可以用下标i,也可以用指针偏移++,指针方式更贴近内存操作,效率更高:
int arr[5] = {1,2,3,4,5};
int *p = arr;
// 指针遍历:无需下标,直接移动指针
while(p < arr + 5) { // arr+5 是数组末尾的下一个地址(终止条件)
cout << *p << " "; // 解引用取当前元素
p++; // 指针右移,指向下一个元素
}
// 输出:1 2 3 4 5
1.3.2指针与数组的关键区别
| 特征 | 数组名 (如 int arr[5]) |
指针变量 (如 int *p) |
|---|---|---|
| 本质 | 常量地址 (非左值) (注:虽常被通俗称为“常量指针”,但严格来说它不是指针变量,而是一个代表固定地址的符号) |
变量指针 (是一个存储地址的普通变量) |
| 可赋值性 | 不可修改/不可赋值arr = arr + 1; (错误)arr = p; (错误) |
可修改/可赋值p = p + 1; (正确)p = &a; (正确)p = nullptr; (正确) |
sizeof 结果 |
数组总字节数 例: int arr[5] → 5 * 4 = 20 字节 |
指针本身大小 32位系统:4 字节 64位系统:8 字节 |
| 存储内容 | 直接存储元素数据 (内存中连续存放 5 个 int 值) |
存储内存地址 (内存中存放的是某个变量的地址值) |
| 典型存储区 | 栈区 (局部数组) 全局/静态区 (全局数组) |
栈区 (指针变量本身) (它指向的目标可以在栈、堆或全局区) |
取地址 & |
&arr 指向整个数组(类型: int (*)[5]) |
&p 指向指针变量本身(类型: int ) |
实例展示
#include <iostream>
using namespace
void pointer_array_demo_cpp() {
cout << "===== 2. 指针与数组 (C++ Style) =====" << endl;
// 数组定义
int arr[5] = {1, 2, 3, 4, 5};
cout << "数组名arr的值(首元素地址):" << static_cast<void*>(arr) << endl;
cout << "数组首元素arr[0]的地址:" << static_cast<void*>(&arr[0]) << endl;
cout << "arr[2]的值:" << arr[2]
<< ",通过指针访问*(arr+2):" << *(arr+2) << endl;
// 指针遍历数组
int *p_arr = arr;
cout << "指针遍历数组:";
for (int i = 0; i < 5; i++)
{
cout << *p_arr << " ";
p_arr++;
}
cout << endl;
// 指针数组:注意字符串字面量是 const char*
const char *str_arr[3] = {"Apple", "Banana", "Cherry"};
cout << "指针数组(字符串数组):";
for (int i = 0; i < 3; i++)
{
cout << str_arr[i] << " ";
}
cout << endl;
// 数组指针:指向整个数组的指针
int (*p_entire_arr)[5] = &arr;
cout << "数组指针指向的数组首元素:" << (*p_entire_arr)[0] << endl;
cout << endl;
}
int main() {
pointer_array_demo_cpp();
return 0;
}
//输出为:
//===== 2. 指针与数组 (C++ Style) =====
//数组名arr的值(首元素地址):0x6100000000000000
//数组首元素arr[0]的地址:0x6100000000000004
//arr[2]的值:3,通过指针访问*(arr+2):3
//指针遍历数组:1 2 3 4 5
//指针数组(字符串数组):Apple Banana Cherry
//数组指针指向的数组首元素:1
//
1.3.3指针与数组的应用场景
指针修改数组元素:
通过指针解引用*p可以直接修改数组元素(原地修改,无需拷贝):
int arr[3] = {10,20,30};
int *p = arr;
*p = 100; // 修改arr[0]为100
*(p+1) = 200; // 修改arr[1]为200
// 数组变为:{100,200,30}
数组作为函数参数(自动退化为指针)
数组传参时,编译器会自动将数组名转为指针(丢失数组长度信息),因此函数中无法用sizeof获取数组总长度,需手动传长度:
// 错误写法:函数内sizeof(arr)是指针字节数(8),不是数组总长度
void print_arr(int arr[]) {
int len = sizeof(arr)/sizeof(arr[0]); // 错误!len=8/4=2(错误)
}
// 正确写法:手动传数组长度
void print_arr(int arr[], int len) {
for(int i=0; i<len; i++) {
cout << arr[i] << " ";
}
}
int main() {
int arr[5] = {1,2,3,4,5};
print_arr(arr, 5); // 正确:传数组名(指针)+ 长度
}
字符数组与字符指针(字符串场景)
字符串是特殊的字符数组(以’\0’结尾),字符指针常用来操作字符串:
// 字符数组(可修改)
char str1[10] = "hello";
str1[0] = 'H'; // 正确:修改数组元素,str1变为"Hello"
// 字符指针(指向字符串常量,不可修改)
char *str2 = "hello";
// str2[0] = 'H'; // 错误!字符串常量存放在只读区,修改会崩溃
// 指针遍历字符串(利用'\0'终止)
char *p = str1;
while(*p != '\0') { // 遍历到结束标志为止
cout << *p;
p++;
}
1.4指针与函数(重点PrpMax)
函数指针和指针函数
这俩虽然就换了个顺序,但是这俩根本就不是一个玩意儿,所以我们得先区分好这两个东西
核心概念:
指针函数(Pointer Function):
定义:本质是一个函数,其返回值类型是指针。
侧重点:它是一个函数,只是返回了一个地址。
声明格式:数据类型 *函数名(参数列表);
示例:
// 返回一个指向 int 的指针的函数
int *getPointer(int x) {
static int val = x;
return &val;
}
函数指针 (Function Pointer):
定义:本质是一个指针变量,它指向一段函数的入口地址。
侧重点:它是一个指针,指向的是代码段中的函数。
声明格式:返回值类型 (*指针变量名)(参数列表)
示例:
示例:
// 定义一个函数
int add(int a, int b) { return a + b; }
// 定义一个函数指针,指向接受两个 int 并返回 int 的函数
int (*funcPtr)(int, int) = &add; // &add 可省略为 add
// 调用
int result = funcPtr(3, 4); // 或 (*funcPtr)(3, 4)
区别要点:
简单理解:
int *p():() 优先级高于 ,p 先与 () 结合,说明 p 是函数,返回 int → 指针函数。
int (*p)():(*p) 被括号强制优先结合,说明 p 是指针,指向返回 int 的函数 → 函数指针。
对比分析
| 特征维度 | 指针函数 (Pointer Function) | 函数指针 (Function Pointer) |
|---|---|---|
| 本质定义 | 函数(其返回值类型是指针) | 指针变量(其存储的内容是函数的入口地址) |
| 核心作用 | 执行特定逻辑后,返回一个内存地址 (常用于返回动态分配内存、静态变量地址等) |
存储函数的地址,实现间接调用 (常用于回调函数、策略模式、跳转表) |
| 声明语法结构 | 返回类型 *函数名(参数列表)(例: int* getData(int id);) |
返回类型 (*指针变量名)(参数列表)(例: int (*funcPtr)(int id);) |
* 号结合对象 |
* 与 返回值类型 结合表示返回的是一个指针类型 |
* 与 变量名 结合(需括号优先)表示该变量本身是一个指针 |
| 优先级关键 | () 优先级高于 *先结合成函数,再修饰返回值 |
(*...) 强制优先级先结合成指针,再修饰指向的函数类型 |
| 调用/使用方式 | 像普通函数一样调用:int* p = getData(1); |
像函数一样调用指针:int res = funcPtr(1);或 (*funcPtr)(1); |
| 常见陷阱 | 悬空指针:严禁返回局部自动变量(栈内存)的地址 | 类型不匹配:参数列表或返回值类型必须严格一致,否则编译失败 |
| 现代 C++ 替代 | 推荐返回智能指针:std::unique_ptr<T> 或 std::shared_ptr<T> |
推荐使用:std::function 或 Lambda 表达式 |
| 记忆口诀 | “函”数返回“指”针(重点是函数) | “指”针指向“函”数(重点是指针) |
实例展示:
#include <iostream>
#include <vector>
#include <functional>
#include <cassert>
using namespace std;
// ============================================================================
// 第一部分:指针函数 (Pointer Function)
// 本质:是一个函数,返回值是指针。
// 场景:返回数组中最大值的地址、返回动态分配内存的地址等。
// ============================================================================
/**
* @brief 查找整型数组中最大值的地址
* @param arr 数组首地址
* @param len 数组长度
* @return 指向最大值元素的指针 (int*)
*
* 安全提示:
* 1. 这里返回的是传入数组内部的地址,生命周期由调用者管理,是安全的。
* 2. 严禁返回局部自动变量 (local automatic variable) 的地址,否则会导致悬空指针。
*/
int* find_max(int arr[], int len) {
if (len <= 0) return nullptr; // 边界检查,返回空指针
int* p_max = &arr[0]; // 初始化指向第一个元素
for (int i = 1; i < len; ++i) {
if (arr[i] > *p_max) {
p_max = &arr[i]; // 更新指针指向更大的元素
}
}
return p_max; // 返回地址,而非值
}
// ============================================================================
// 第二部分:普通函数 (用于被函数指针指向)
// 本质:普通的逻辑函数,作为回调策略的具体实现。
// ============================================================================
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
// 第三部分:函数指针 (Function Pointer) 与 回调模式
// 本质:是一个指针变量,存储函数的入口地址。
// 场景:回调函数、策略模式、跳转表。
/**
通用计算器 (回调函数演示)
这里的 op 是一个“插件接口”,主函数不需要知道具体是加法还是减法,
只需要知道它接受两个 int 并返回一个 int。
*/
int calculate(int a, int b, int (*op)(int, int)) {
if (op == nullptr) {
cerr << "Error: Operation function pointer is null!" << endl;
return 0;
}
return op(a, b); // 间接调用:通过指针执行函数
}
void function_pointer_demo() {
cout << "===== C++ 指针与函数深度解析 =====" << endl << endl;
// 场景 1:指针函数的使用
cout << "[1] 指针函数演示 (Pointer Function)" << endl;
int arr[] = {8, 3, 9, 2, 7};
int len = sizeof(arr) / sizeof(arr[0]);
// 调用指针函数,接收返回的地址
int* p_max = find_max(arr, len);
if (p_max != nullptr) {
cout << " -> 最大值地址: " << static_cast<void*>(p_max) << endl; // 强制转换为 void* 打印地址
cout << " -> 最大值内容: " << *p_max << endl;
cout << " -> 验证:原数组对应位置值: " << *(arr + (p_max - arr)) << endl;
}
cout << endl;
// 场景 2:函数指针的基础定义与调用
cout << "[2] 函数指针基础 (Function Pointer Basics)" << endl;
// 定义格式:返回值类型 (*指针变量名) (参数列表)
// 括号 (*) 是必须的,否则会被解析为返回指针的函数
int (*p_func)(int, int);
// 赋值:函数名即地址,&add 和 add 等价
p_func = add;
cout << " -> 调用 add(5, 3): " << p_func(5, 3) << endl; // 写法 A: 直接像函数一样调用
cout << " -> 调用 add(5, 3): " << (*p_func)(5, 3) << endl; // 写法 B: 显式解引用 (等价)
// 切换指向:指针的可变性
p_func = sub;
cout << " -> 切换指向 sub, 调用 sub(5, 3): " << p_func(5, 3) << endl;
cout << endl;
// 场景 3:函数指针作为参数 (回调函数)
cout << "[3] 回调函数模式 (Callback Pattern)" << endl;
cout << " -> 计算 10 + 4 (传入 add): " << calculate(10, 4, add) << endl;
cout << " -> 计算 10 - 4 (传入 sub): " << calculate(10, 4, sub) << endl;
cout << " -> 计算 10 * 4 (传入 multiply): " << calculate(10, 4, multiply) << endl;
cout << endl;
// 场景 4:现代 C++ 的替代方案 (Best Practice)
cout << "[4] 现代 C++ 视角 (Modern C++ Alternative)" << endl;
cout << " -> 使用 std::function 支持 Lambda 表达式 (捕获上下文):" << endl;
int factor = 10;
// std::function 可以容纳任何可调用对象,包括带状态的 Lambda
// 这是传统函数指针做不到的 (传统指针无法捕获局部变量 factor)
function<int(int, int)> modern_op = [factor](int a, int b) {
return (a + b) * factor;
};
cout << " -> Lambda 计算 (10 + 4) * 10: " << modern_op(10, 4) << endl;
cout << " (传统函数指针无法直接实现上述带状态的计算)" << endl;
}
int main() {
// 设置输出格式
ios_base::sync_with_stdio(false);
cin.tie(NULL);
try {
function_pointer_demo();
} catch (const exception& e) {
cerr << "Exception caught: " << e.what() << endl;
return 1;
}
return 0;
}
//输出结果:
/*===== C++ 指针与函数深度解析 =====
[1] 指针函数演示 (Pointer Function)
-> 最大值地址: 0x5ffde8
-> 最大值内容: 9
-> 验证:原数组对应位置值: 9
[2] 函数指针基础 (Function Pointer Basics)
-> 调用 add(5, 3): 8
-> 调用 add(5, 3): 8
-> 切换指向 sub, 调用 sub(5, 3): 2
[3] 回调函数模式 (Callback Pattern)
-> 计算 10 + 4 (传入 add): 14
-> 计算 10 - 4 (传入 sub): 6
-> 计算 10 * 4 (传入 multiply): 40
[4] 现代 C++ 视角 (Modern C++ Alternative)
-> 使用 std::function 支持 Lambda 表达式 (捕获上下文):
-> Lambda 计算 (10 + 4) * 10: 140
(传统函数指针无法直接实现上述带状态的计算)
*/
以上就大概包含了关于指针知识点,当然,这不是全部,还有指针的动态内存分配,多级指针等,在这里就不进行过多的描述了,这里有相关的博客链接大家如果想进行拓展的话可以去看看,他们都整理的非常详细 链接一文搞懂一级指针二级指针,三级指针
二、类
上面对指针进行了大概的讲解,那么现在就开始进入到面向对象设计的范围了,这也是c++的核心所在
在正式开始之前,建议先搞懂什么是对象,什么是类,在进行C++的几大特征进行了解
我会提供相关链接,这些博主总结的也十分的到位,
C++特征
对象和类
通过上面两个链接相信大家也有了一点了解那么我这里就会进行一下我自己的总结和理解
类的定义和访问控制
类通过访问限定符来实现封装(Encapsulation),这是 OOP(面向对象编程)的第一大支柱。C++ 提供了三种访问权限:public、protected 和 private。
| 限定符 | 类内部访问 | 类外部访问 (通过对象) | 派生类 (子类) 访问 | 典型用途 |
|---|---|---|---|---|
public |
可 | 可 | 可 | 对外接口、构造函数、析构函数 |
protected |
可 | 否 | 可 | 供子类复用的内部实现细节 |
private |
可 | 否 | 否 | 核心数据成员、内部辅助函数 |
默认权限:在 class 中,默认访问权限是 private;而在 struct 中,默认是 public。除默认权限外,两者的功能在现代 C++ 中几乎等价。
友元(friend):friend 关键字可以打破封装限制,允许特定函数或类访问 private 和 protected 成员。应谨慎使用,以免破坏封装性。如果想拓展一下参考一下下面的链接C++友元
三、构造函数与析构函数
在 C++ 中,构造函数(Constructor)和析构函数(Destructor)是类生命周期管理的“守门人”。它们不仅负责对象的初始化和清理,更是 RAII(资源获取即初始化) 机制的核心载体。理解并正确使用它们,是编写无内存泄漏、异常安全代码的关键
构造函数特征:
名称:与类名完全相同。
返回值:无返回值(连 void 都不能写)。
调用时机:对象创建时自动调用(包括栈对象、堆对象 new、临时对象、容器扩容时的元素构造)。
重载:支持重载,允许通过不同参数列表提供多种初始化方式。
示例:
class Widget {
public:
Widget(int id); // 自定义构造
Widget() = default; // 显式请求默认构造,清晰且高效
};
析构函数特征:
名称:~ 加上类名。
参数与返回:无参数,无返回值,不可重载。
调用时机:对象生命周期结束(出作用域、delete、容器销毁)时自动调用。
异常规范:C++11 起,析构函数默认是 noexcept(true)。严禁在析构函数中抛出异常,否则会导致程序直接终止 (std::terminate)。
示例:
// 错误示范
class Base {
public:
~Base() { /* ... */ } // 非虚析构
};
class Derived : public Base {
int* data;
public:
Derived() { data = new int[100]; }
~Derived() { delete[] data; } // 永远不会被调用!内存泄漏
};
// 正确示范
class Base {
public:
virtual ~Base() = default; // 虚析构,确保多态删除安全
};
区别和使用:
| 对比维度 | 构造函数 (Constructor) | 析构函数 (Destructor) |
|---|---|---|
| 核心作用 | 初始化对象 • 赋值成员变量 • 申请动态资源(内存、文件句柄等) |
销毁对象 • 清理动态资源 • 释放内存、关闭文件或网络连接 |
| 调用时机 | 对象创建时自动调用 • 栈对象定义时 ( Class obj;)• 堆对象 new 时 (new Class())• 临时对象或容器扩容时 |
对象生命周期结束时自动调用 • 栈对象离开作用域时 • 堆对象被 delete 时• 程序结束或容器销毁时 |
| 函数名规则 | 与类名完全相同(区分大小写) | 类名前加 ~(波浪号),如 ~ClassName() |
| 返回值 | 无返回值 (连 void 也不能写,写了会报错) |
无返回值 (连 void 也不能写) |
| 参数列表 | 可以有参数 • 支持重载(无参、有参、拷贝/移动构造) • 可通过默认参数实现多种初始化 |
绝对不能有参数 • 无法重载 • 一个类只能有一个析构函数 |
| 手动调用 | 禁止手动调用 (仅由编译器在对象创建时自动触发) |
严禁手动调用 (手动调用会导致资源重复释放,引发崩溃/未定义行为) |
| 默认生成机制 | 若用户未定义任何构造函数,编译器生成隐式默认构造函数(无参)。 (注:一旦定义了其他构造,默认构造不再自动生成) |
若用户未定义,编译器生成默认析构函数。 (注:通常为空操作,但若成员有析构函数则会调用它们) |
| 核心使用场景 | • 初始化 const 或引用成员(必须用初始化列表)• 分配堆内存 ( new)• 打开文件/数据库连接 |
• 释放堆内存 (delete)• 关闭文件描述符/网络套接字 • 释放锁资源 (RAII 模式核心) |
| 特殊注意事项 | • 推荐使用成员初始化列表而非函数体赋值 • C++11 支持 = default 和委托构造 |
• 基类析构函数必须是 virtual(若需多态删除)• 严禁抛出异常(C++11 起默认为 noexcept) |
直观展示其差异:
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
// 1. 构造函数:与类名相同,支持重载(多个构造)
// 无参构造
Person() {
name = "未知";
age = 0;
// 申请动态资源
id = new int(0);
cout << "构造函数(无参):初始化对象,id=" << *id << endl;
}
// 有参构造(重载)
Person(string n, int a) {
name = n;
age = a;
// 申请动态资源
id = new int(a + 1000); // 模拟id生成规则
cout << "构造函数(有参):初始化对象,id=" << *id << endl;
}
// 2. 析构函数:类名前加~,无参数、无重载
~Person() {
// 释放构造函数中申请的动态资源
delete id;
cout << "析构函数:销毁对象,释放id内存(name=" << name << ")" << endl;
}
void show() {
cout << "姓名:" << name << ",年龄:" << age << ",id:" << *id << endl;
}
private:
string name;
int age;
int* id; // 动态资源(堆内存)
};
int main() {
cout << "===== 创建对象p1(无参)=====" << endl;
Person p1; // 触发无参构造函数
p1.show();
cout << "\n===== 创建对象p2(有参)=====" << endl;
Person p2("张三", 20); // 触发有参构造函数
p2.show();
cout << "\n===== main函数结束,对象开始销毁 =====" << endl;
// 离开main作用域时,p2先销毁(后进先出),再销毁p1 → 依次触发析构函数
return 0;
/*
===== 创建对象p1(无参)=====
构造函数(无参):初始化对象,id=0
姓名:未知,年龄:0,id:0
===== 创建对象p2(有参)=====
构造函数(有参):初始化对象,id=1020
姓名:张三,年龄:20,id:1020
===== main函数结束,对象开始销毁 =====
析构函数:销毁对象,释放id内存(name=张三)
析构函数:销毁对象,释放id内存(name=未知)
}
*/
四、虚函数
虚函数是 C++ 实现多态(Polymorphism) 的核心机制,主要用于基类指针 / 引用指向派生类对象时,调用派生类的重写方法
核心定义与作用:
定义:在基类中用 virtual 关键字声明的成员函数,允许派生类重写(override)该函数,且通过基类指针 / 引用调用时,会根据实际指向的对象类型(而非指针 / 引用类型)执行对应类的函数版本。
核心作用:实现运行时多态(动态绑定) —— 函数调用的具体版本在程序运行时确定,而非编译时。
对比非虚函数:非虚函数是编译时多态(静态绑定),调用版本由指针 / 引用的类型决定。
语法规则与使用要求:
| 规则维度 | 具体说明与严谨细节 |
|---|---|
| 声明位置 | • 基类:必须在函数声明处添加 virtual 关键字。• 派生类:重写时 virtual 关键字可选(编译器自动识别),但强烈建议保留以增强代码可读性和维护性。• 最佳实践:在 C++11 及以后,派生类应使用 override 说明符代替或配合 virtual,以便编译器检查签名匹配。 |
| 重写要求 (Override) | 派生类函数必须严格满足以下条件才能构成有效重写: 1. 函数签名一致:函数名、参数列表完全相同。 2. 返回值兼容:返回值类型必须相同,或是基类返回类型的协变(即返回指向派生类的指针/引用)。 3. CV 限定符一致: const / volatile 属性必须完全匹配。4. 访问权限:访问控制( public/protected/private)不影响重写的发生,但会影响调用的合法性(即能否通过该指针访问)。 |
| 调用条件 (动态绑定) | • 触发多态:必须通过基类的指针或基类的引用来调用虚函数。 • 静态绑定:若直接通过对象实例(如 obj.func())调用,编译器会在编译期确定调用版本,不会触发动态绑定(多态)。• 本质:多态依赖于运行时生成的虚函数表 (vtable) 和对象中的 虚函数指针 (vptr)。 |
| 析构函数特例 | • 黄金法则:若类设计为基类且可能被多态删除,必须将析构函数声明为 virtual。• 风险:若基类析构非虚,通过基类指针 delete 派生类对象时,仅调用基类析构函数,派生类的析构逻辑被跳过,导致资源泄漏和未定义行为。 |
| 禁止虚函数的情况 | 以下三类函数不能也不应声明为 virtual:1. 静态成员函数 ( static):属于类而非具体对象,没有 this 指针,无法参与多态。2. 构造函数:对象尚未创建完成, vptr 未初始化,无法进行动态绑定。3. 内联函数 ( inline):- 原理冲突:内联旨在编译期展开,而虚函数需运行期查表。 - 特殊情况:声明为 virtual inline 时,仅在直接通过对象调用时可能内联;通过指针/引用调用时,内联失效,退化为普通虚函数调用。 |
关键扩展:纯虚函数与抽象类
纯虚函数:基类中声明但不实现的虚函数,语法为 virtual 返回值 函数名(参数) = 0;,作用是强制派生类重写该函数。
抽象类:包含纯虚函数的类称为抽象类,特点:
无法实例化对象(不能直接创建 Animal a;);
可作为基类,派生类必须实现所有纯虚函数,否则派生类也为抽象类;
常用于定义接口(仅声明功能,不实现)。
示例:
class Animal {
public:
// 纯虚函数:抽象接口
virtual void makeSound() = 0;
virtual ~Animal() = default;
};
// 派生类必须实现纯虚函数
class Bird : public Animal {
public:
void makeSound() override {
cout << "鸟叫:叽叽叽" << endl;
}
};
int main() {
// Animal a; // 错误:抽象类无法实例化
Animal* b = new Bird();
b->makeSound(); // 输出:鸟叫:叽叽叽
delete b;
return 0;
}
底层原理(简易理解)
虚函数的动态绑定依赖虚函数表(vtable) 和虚表指针(vptr):
每个包含虚函数的类会生成一个虚函数表(vtable),存储该类所有虚函数的地址;
每个对象会包含一个隐藏的虚表指针(vptr),指向所属类的虚函数表;
运行时,通过指针 / 引用调用虚函数时,先通过 vptr 找到 vtable,再根据函数位置调用对应版本。
五、继承与多态
上面已经对类,以及类当中的在继承中涉及到的相关函数进行了一些介绍,那么结合上面的知识点,就可以引出继承与多态的内容,继承和多态是C++ 面向对象编程(OOP)的两大核心特性:继承解决代码复用问题,多态解决接口统一与动态行为问题。
5.1继承
核心定义与作用
定义:允许一个类(派生类 / 子类)继承另一个类(基类 / 父类)的属性和方法,子类可复用父类代码,也可扩展或重写父类功能。
核心作用:
代码复用:避免重复编写相同的成员变量 / 函数;
类的层次化:构建 “一般 - 特殊” 的类关系
基本语法:
// 基类
class 基类名 { ... };
// 派生类:继承方式 基类名
class 派生类名 : 继承方式 基类名 { ... };
规则:
| 继承方式 | 基类成员原权限 | 派生类中可见性 (调整后权限) | 外部访问性 (通过派生类对象) | 语义含义与应用场景 |
|---|---|---|---|---|
public(公有继承) |
public |
public |
可访问 | “Is-A” 关系 最常用。保持接口不变,派生类对象拥有基类的全部公有接口。 |
protected |
protected |
不可访问 | 供派生类内部使用,对外隐藏。 | |
private |
不可见 | 不可访问 | 基类的私有实现细节,派生类无法直接访问。 | |
protected(保护继承) |
public |
protected |
不可访问 | “部分 Is-A” 关系 较少用。将基类的所有公有接口降级为保护成员,仅允许更下层的子类访问,对外完全隐藏。 |
protected |
protected |
不可访问 | 保持保护属性,仅限子类链内部访问。 | |
private |
不可见 | 不可访问 | 依然不可见。 | |
private(私有继承) |
public |
private |
不可访问 | “Has-A” / 实现细节 极少直接用(通常用组合替代)。将基类所有成员变为私有,派生类内部可用,但派生类的子类及外部均不可访问。 |
protected |
private |
不可访问 | 降级为私有,仅限当前派生类内部使用。 | |
private |
不可见 | 不可访问 | 依然不可见。 |
继承中的重定义(隐藏)vs 重写(override)
重定义(隐藏:派生类定义与基类同名但非虚的函数,覆盖基类函数(编译时绑定,仅影响派生类调用;
重写(override):派生类重写基类的虚函数(运行时绑定,多态的核心)。
5.2多态
核心定义与分类
定义:同一接口(基类函数),不同对象(派生类)表现出不同行为;
分类:
| 特性维度 | 编译时多态 (静态多态) | 运行时多态 (动态多态) |
|---|---|---|
| 类型名称 | 静态多态 (Static Polymorphism) | 动态多态 (Dynamic Polymorphism) |
| 绑定时机 | 编译期 (Compile-time) 编译器在编译阶段直接确定调用哪个函数。 |
运行期 (Run-time) 程序运行时,根据对象的实际类型动态决定调用哪个函数。 |
| 实现方式 | 1. 函数重载 (Function Overloading) 2. 运算符重载 (Operator Overloading) 3. 模板 (Templates / CRTP) 4. constexpr 函数 |
1. 虚函数 (virtual keyword)2. 必须通过 基类指针 或 基类引用 调用 |
| 底层机制 | 符号解析与内联 编译器根据参数列表直接匹配函数地址,通常可内联优化,无额外开销。 |
虚函数表 (vtable) + 虚指针 (vptr) 对象内部维护指向虚函数表的指针,调用时查表获取函数地址(间接寻址)。 |
| 性能特点 | 高效 无运行时开销,支持内联,代码执行速度快。 |
轻微开销 涉及间接寻址(查表),通常无法内联,占用额外内存(vptr + vtable)。 |
| 灵活性 | 低 行为在编译时已固定,无法根据运行时状态改变。 |
高 支持插件化架构、策略模式,可根据运行时对象类型灵活切换行为。 |
| 主要应用 | 泛型编程、数学库、性能敏感的基础设施代码。 | 图形界面框架、游戏实体系统、插件系统、需要多态行为的业务逻辑。 |
运行时多态的实现条件(缺一不可)
基类中声明虚函数(virtual 关键字);
派生类重写(override)该虚函数(函数签名完全一致:名、参数、返回值、const 属性);
拓展:
纯虚函数与抽象类(多态的接口设计)
纯虚函数:基类声明但不实现的虚函数,语法:virtual 返回值 函数名(参数) = 0;;
作用:强制派生类实现该函数,定义统一接口。
抽象类:包含纯虚函数的类;
特性:
无法实例化对象(Animal a; 报错);
可作为基类,派生类必须实现所有纯虚函数,否则仍为抽象类;
核心用途:定义 “接口规范”,不关注具体实现(如Shape类的draw()方法)
5.3继承和多态的关联
继承是多态的前提:多态依赖基类与派生类的继承关系;
多态是继承的高级应用:继承解决复用,多态解决 “接口统一但行为不同” 的问题。
示例:
#include <iostream>
using namespace std;
// 抽象基类(纯虚函数)
class Shape {
public:
// 纯虚函数:定义接口
virtual double getArea() = 0;
// 虚析构函数(必须)
virtual ~Shape() = default;
};
// 派生类1:圆形
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 重写纯虚函数
double getArea() override {
return 3.14 * radius * radius;
}
};
// 派生类2:矩形
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
// 重写纯虚函数
double getArea() override {
return width * height;
}
};
// 统一接口函数(多态核心:参数为基类引用)
void printArea(Shape& shape) {
cout << "面积:" << shape.getArea() << endl;
}
int main() {
Circle c(5);
Rectangle r(4, 6);
// 同一接口,不同行为
printArea(c); // 输出:面积:78.5
printArea(r); // 输出:面积:24
return 0;
//输出
//面积:78.5
//面积:24
}
六、实践演练
综合上面的知识点,现在正式进入到知识点的总结运用,直接展示代码,具体的思路等,都通过注释进行了展示
代码:
/*
项目名称:游戏角色战斗系统
思路:
多角色,多攻击策略,控制台显示
定义一个基类baseCharacter,包含角色的基本属性和函数
保护:角色姓名name,角色id(string),角色生命值health(string),角色攻击attack(string)
静态:角色总数count
函数:
构造函数:初始化角色属性
析构函数:释放角色资源
纯虚函数:攻击attack,防御defense
普通成员函数,获取角色属性(getName,getId,getHealth,getAttack),显示角色信息(getInfo),受伤扣血(damage)
静态函数,获取角色总数getCount()
使用函数指针来实现不同角色的攻击策略
先定义一个指针类型,指向攻击方法指针,
再使用普通函数,参数为指针,实现不同的攻击策略
在主函数中定义一个函数指针数组*attackPtrs,指向不同角色的攻击方法
根据角色类型调用不同的攻击方法
定义一个子类,战士Warrior,继承自baseCharacter
定义独有属性,防御值defense,暴击率critRate
重写攻击attack方法,根据暴击率随机是否暴击,
构造函数:初始化战士属性
析构函数:释放战士资源
普通成员函数,显示战士信息(getInfo)
定义一个子类,法师Mage,继承自baseCharacter
定义独有属性,魔法值magic,魔法攻击magicAttack
重写攻击attack方法,根据魔法攻击计算伤害,
构造函数:初始化法师属性
析构函数:释放法师资源
普通成员函数,显示法师信息(getInfo)
定义一个子类,射手Rogue,继承自baseCharacter
定义独有属性,敏捷值agility,攻击速度attackSpeed
重写攻击attack方法,根据攻击速度随机是否快速攻击,
构造函数:初始化射手属性
析构函数:释放射手资源
普通成员函数,显示射手信息(getInfo)
主函数:
创建不同角色对象
调用攻击方法
显示角色信息
*/
#include<iostream>
#include<cstdlib>//随机数
#include<ctime>//时间
using namespace std;//标准命名空间
/*
定义一个基类baseCharacter,包含角色的基本属性和函数
保护:角色姓名name,角色id(string),角色生命值health(string),角色攻击attack(string)
函数:
构造函数:初始化角色属性
析构函数:释放角色资源
纯虚函数:攻击attack,防御defense
普通成员函数,获取角色属性(getName,getId,getHealth,getAttack),显示角色信息(getInfo),受伤扣血(damage)
静态函数,获取角色总数getCount()
*/
class baseCharacter
{
protected:
string name;//角色姓名
string id;//角色id
int health;//角色生命值
int attackValue;//角色攻击值
public:
baseCharacter(string n,string i,int h,int a):name(n),id(i),health(h),attackValue(a)//初始化角色属性
{
count++;//角色总数增加
}
virtual ~baseCharacter()//释放角色资源
{
count--;//角色总数减少
}
string getName()//获取角色姓名
{
return name;
}
string getId()//获取角色id
{
return id;
}
int getHealth()//获取角色生命值
{
return health;
}
int getAttack()//获取角色攻击值
{
return attackValue;
}
void damage(int dmg)//受伤扣血
{
health -= dmg;
}
bool isAlive()//判断角色是否存活
{
return health > 0;
}
virtual void attack(baseCharacter *target) = 0;//攻击attack,baseCharacter* :定义target为指针,指向目标角色
virtual void defense() = 0;//防御defense
virtual void getInfo() = 0;//显示角色信息
static int count;//角色总数
};
/*
使用函数指针来实现不同角色的攻击策略
先定义一个指针类型,指向攻击方法指针,
再使用普通函数,参数为指针,实现不同的攻击策略
在主函数中定义一个函数指针数组*attackPtrs,指向不同角色的攻击方法
根据角色类型调用不同的攻击方法
*/
int baseCharacter::count = 0;//角色总数初始化为0,静态成员变量(只能在类外部初始化),所有对象共享一个count变量
//攻击函数指针类型,typedef定义一个函数指针类型,指向攻击方法指针
//typedef可以定义一个新的类型名,用于表示已有的类型,或者可以定义一个新的类型
//baseCharacter *attacker :定义attacker为指针,指向攻击角色
//baseCharacter *target :定义target为指针,指向目标角色
typedef double (*AttackFunc)(baseCharacter *attacker, baseCharacter *target);
//定义了指针类型后,写不同的攻击方法
double normalAttack(baseCharacter *attacker, baseCharacter *target)//普通攻击
{
if(!target || !target->isAlive())//为什么不使用.isAlive(),因为target是一个指针,不能直接调用成员函数
{
cout<<"目标无效或已死亡"<<endl;
return 0.0;
}
double damage = attacker->getAttack();//获取攻击值
target->damage(static_cast<int>(damage));//将double值转换为int值,作为伤害扣血参数
cout<<attacker->getName()<<" 对 "<<target->getName()<<" 造成了 "<<damage<<" 点伤害"<<endl;
return damage;
}
double critAttack(baseCharacter *attacker, baseCharacter *target)//暴击攻击
{
if(!target || !target->isAlive())
{
cout<<"目标无效或已死亡"<<endl;
return 0.0;
}
double damage = attacker->getAttack() * 2.0;//获取攻击值,乘以2,得到暴击伤害
target->damage(static_cast<int>(damage));//将double值转换为int值,作为伤害扣血参数
cout<<attacker->getName()<<" 对 "<<target->getName()<<" 造成了 "<<damage<<" 点暴击伤害!"<<endl;
return damage;
}
double skillAttack(baseCharacter *attacker, baseCharacter *target)//技能攻击
{
if(!target || !target->isAlive())
{
cout<<"目标无效或已死亡"<<endl;
return 0.0;
}
double damage = attacker->getAttack() * 3.0;//获取攻击值,乘以3,得到技能伤害
target->damage(static_cast<int>(damage));//将double值转换为int值,作为伤害扣血参数
cout<<attacker->getName()<<" 对 "<<target->getName()<<" 造成了 "<<damage<<" 点技能伤害!"<<endl;
return damage;
}
/*
定义一个子类,战士Warrior,继承自baseCharacter
定义独有属性,防御值defense,暴击率critRate
重写攻击attack方法,根据暴击率随机是否暴击,
构造函数:初始化战士属性
析构函数:释放战士资源
普通成员函数,显示战士信息(getInfo)
*/
class Warrior:public baseCharacter//战士角色
{
private:
int defenseValue;//防御值,用于计算防御值
double critRate;//暴击率,用于判断是否暴击
public:
Warrior(string n,string i,int h,int a,int d,double c):baseCharacter(n,i,h,a),defenseValue(d),critRate(c){}//初始化战士属性
~Warrior(){}//释放战士资源
void attack(baseCharacter *target) override//重写攻击attack方法,override确保子类方法与父类方法签名一致
{
if(rand() % 100 < critRate * 100)//判断是否暴击
{
critAttack(this, target);//用this指向当前对象,调用暴击攻击函数
}
else
{
normalAttack(this, target);//普通攻击,调用普通攻击函数
}
}
void defense() override//重写防御defense方法
{
cout << name << "防御,防御值为 " << defenseValue << endl;
}
void getInfo() override//重写显示角色信息getInfo方法
{
cout<<"战士"<<endl;
cout<<"名称: "<<name<<endl;
cout<<"ID: "<<id<<endl;
cout<<"生命值: "<<health<<endl;
cout<<"攻击力: "<<attackValue<<endl;
cout<<"防御值: "<<defenseValue<<endl;
cout<<"暴击率: "<<critRate*100<<"%"<<endl;
cout<<endl;
}
};
class Mage:public baseCharacter//法师角色
{
private:
int magic;//魔法值,用于计算魔法值
int magicAttack;//魔法攻击,用于计算魔法攻击
public:
Mage(string n,string i,int h,int a,int m,int ma):baseCharacter(n,i,h,a),magic(m),magicAttack(ma){}
~Mage(){}
void attack(baseCharacter *target) override
{
if(magic > 20)//判断魔法值是否足够
{
magic -= 20;
skillAttack(this, target);//用this指向当前对象,调用技能攻击函数
}
else
{
normalAttack(this, target);
}
}
void defense() override//重写防御defense方法
{
cout << name << "护盾,魔法值为 " << magic << endl;
}
void getInfo() override
{
cout<<"法师"<<endl;
cout<<"名称: "<<name<<endl;
cout<<"ID: "<<id<<endl;
cout<<"生命值: "<<health<<endl;
cout<<"攻击力: "<<attackValue<<endl;
cout<<"魔法值: "<<magic<<endl;
cout<<"魔法攻击: "<<magicAttack<<endl;
cout<<endl;
}
};
class Rogue:public baseCharacter//射手角色
{
private:
int agility;//敏捷值,用于计算敏捷值
double attackSpeed;//攻击速度,用于判断是否快速攻击
public:
Rogue(string n,string i,int h,int a,int ag,double as):baseCharacter(n,i,h,a),agility(ag),attackSpeed(as){}
~Rogue(){}
void attack(baseCharacter *target) override//重写攻击attack方法
{
int attacks = 1;
if(rand() % 100 < attackSpeed * 100)//判断是否快速攻击
{
attacks = 2;
cout<<name<<" 触发快速攻击!"<<endl;
}
for(int i=0;i<attacks;i++)
{
normalAttack(this, target);
if(!target->isAlive()) break;
}
}
void defense() override//重写防御defense方法
{
cout << name << " 进行闪避,敏捷值为 " << agility << endl;
}
void getInfo() override
{
cout<<"射手"<<endl;
cout<<"名称: "<<name<<endl;
cout<<"ID: "<<id<<endl;
cout<<"生命值: "<<health<<endl;
cout<<"攻击力: "<<attackValue<<endl;
cout<<"敏捷值: "<<agility<<endl;
cout<<"攻击速度: "<<attackSpeed*100<<"%"<<endl;
cout<<endl;
}
};
int main()
{
srand(time(0));
cout<<"=== 游戏角色战斗系统 ==="<<endl<<endl;
cout<<"当前角色总数: "<<baseCharacter::count<<endl;//调用静态成员变量count,显示当前角色总数
//创建角色,通过指针指向对象,调用对象的方法和属性
Warrior *war = new Warrior("战士1","1001",100,15,10,0.3);
Mage *mage = new Mage("法师1","1002",80,10,100,25);
Rogue *rogue = new Rogue("射手1","1003",70,12,15,0.4);
cout<<"创建角色后,角色总数: "<<baseCharacter::count<<endl<<endl;
//显示角色信息,使用指针调用对象的方法getInfo
war->getInfo();
mage->getInfo();
rogue->getInfo();
//定义攻击函数指针数组,存放普通攻击、暴击攻击、技能攻击
AttackFunc attackPtrs[] = {normalAttack, critAttack, skillAttack};
cout<<"=== 战斗开始 ==="<<endl<<endl;
cout<<"--- 第1轮 ---"<<endl;
war->attack(mage);
mage->attack(rogue);
rogue->attack(war);
cout<<endl;
cout<<"--- 使用函数指针调用攻击 ---"<<endl;
attackPtrs[1](war, mage);
attackPtrs[2](mage, rogue);
cout<<endl;
cout<<"--- 战斗后状态 ---"<<endl;
war->getInfo();
mage->getInfo();
rogue->getInfo();
cout<<"--- 防御演示 ---"<<endl;
war->defense();
mage->defense();
rogue->defense();
cout<<endl;
//释放角色资源
delete war;
delete mage;
delete rogue;
cout<<"删除角色后,角色总数: "<<baseCharacter::count<<endl;
cout<<"=== 战斗结束 ==="<<endl;
return 0;
}
总结
以上就是这篇文章的全部内容了,希望能够给各位提供一点帮助,当然如果有没有表述明白的地方,请大家谅解,我也一定会去改进,此外还需要感谢一下文章链接中的博主,也给我提供了不少的帮助。
最后欢迎大家在评论区中交流学习
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)