第二讲:C++ 基础回顾——变量、数据类型、控制流、函数、指针与引用

好,咱们正式开始第二讲。这一讲我打算带大家快速过一遍 C++ 的核心基础。你可能会想:「这些我早就会了,还用学?」嗯,我理解。但说实话,在 Unreal 项目里,很多坑恰恰就出在这些「基础」上。我见过太多人因为指针没搞明白,导致游戏运行时莫名其妙崩溃。所以,咱们还是踏踏实实过一遍。

2.1 变量与数据类型

变量,说白了就是一块内存的名字。你给它起个名,它就在那存着数据。C++ 是静态类型语言,这意味着每个变量在声明时就必须明确它的类型。

核心原则:类型决定了变量能存什么、占多大内存、能做什么操作。

常用的内置类型,我列个表给你看:

类型 大小(通常) 示例
bool 1 字节 bool bIsReady = true;
int 4 字节 int Score = 100;
float 4 字节 float Speed = 1.5f;
double 8 字节 double WorldScale = 0.001;
char 1 字节 char Grade = 'A';

我个人习惯在 Unreal 里尽量用 int32floatbool 这些明确大小的类型。为什么?因为跨平台时,int 在不同编译器下可能是 4 字节也可能是 8 字节,容易出问题。我在项目中就遇到过,一个 int 在 PC 上跑得好好的,打包到主机上就崩了——就是因为大小不一致。

小技巧:Unreal 提供了 int32uint8FString 等类型别名,建议优先使用它们。这样代码更清晰,也更容易维护。

2.2 控制流

控制流就是决定代码走哪条路。C++ 里最常用的就是 ifswitchforwhile。没什么花哨的,但有几个细节我想强调一下。

2.2.1 if 语句

if (Health <= 0)
{
    // 角色死亡
    bIsAlive = false;
}
else if (Health < 30)
{
    // 低血量警告
    PlayLowHealthEffect();
}
else
{
    // 正常状态
}

这里有个坑:if 后面的大括号,千万别省。哪怕只有一行代码,也给我加上。我曾经接手过一个项目,有人写了 if (bDead) return; 结果后来加了一行代码,忘了加括号,逻辑全乱了。嗯,这种 bug 最难查。

2.2.2 switch 语句

switch 适合处理枚举值。比如角色状态:

switch (CharacterState)
{
    case ECharacterState::Idle:
        // 待机动画
        break;
    case ECharacterState::Running:
        // 跑步动画
        break;
    case ECharacterState::Jumping:
        // 跳跃动画
        break;
    default:
        break;
}

注意每个 case 后面都要有 break,否则会「穿透」到下一个 case。有时候穿透是有意为之,但大多数时候是 bug。我建议你养成习惯,每个 case 都写 break,除非你明确知道自己在做什么。

2.2.3 循环

循环嘛,forwhile 最常用。在 Unreal 里,遍历容器时我更喜欢用范围 for

TArray<AActor*> FoundActors;
// ... 填充数组
for (AActor* Actor : FoundActors)
{
    if (Actor)
    {
        Actor->Destroy();
    }
}

这种写法简洁,而且不容易越界。你想想看,用传统 for 循环时,一不小心就把索引写错了,对吧?

2.3 函数

函数是代码复用的基本单元。C++ 的函数声明和定义可以分开,这在大型项目中非常有用。

// 声明(通常在 .h 文件中)
int32 CalculateDamage(int32 BaseDamage, float Multiplier);

// 定义(通常在 .cpp 文件中)
int32 CalculateDamage(int32 BaseDamage, float Multiplier)
{
    return static_cast<int32>(BaseDamage * Multiplier);
}

我个人习惯在函数参数里尽量用 const 引用。为什么?因为传值会拷贝,拷贝大对象(比如 FStringTArray)很费性能。用 const & 既安全又高效。

注意:如果函数参数是基本类型(如 intfloat),传值反而更快。因为引用本质上是指针,有间接寻址的开销。别盲目用引用。

2.4 指针与引用

好,重点来了。指针和引用,是 C++ 里最容易出问题的地方,也是 Unreal 里最常用的东西。

2.4.1 指针

指针存的是地址。你可以通过指针间接访问变量。看个例子:

int32 Value = 42;
int32* Ptr = &Value;  // Ptr 存的是 Value 的地址

*Ptr = 100;  // 通过指针修改 Value
// 现在 Value 变成了 100

指针最怕什么?空指针。你想想看,如果 Ptrnullptr,你直接 *Ptr = 100,程序立马崩溃。我在项目中遇到过无数次这种问题,尤其是网络同步时,对象可能已经被销毁了,但指针还在。

避坑指南:每次使用指针前,先检查是否为 nullptr。Unreal 里可以用 IsValid() 宏来检查 UObject 指针。

2.4.2 引用

引用可以理解为指针的语法糖。它一旦绑定到某个变量,就不能再指向别的变量。引用没有空引用,所以比指针安全一些。

int32 Value = 42;
int32& Ref = Value;  // Ref 是 Value 的引用

Ref = 100;  // 修改 Value
// Value 现在是 100

引用常用于函数参数和返回值。比如:

void AddScore(int32& Score, int32 Delta)
{
    Score += Delta;
}

int32 PlayerScore = 0;
AddScore(PlayerScore, 10);
// PlayerScore 现在是 10

这里 Score 是引用,所以函数内部修改会直接影响外部的 PlayerScore。如果你用传值,那就只是拷贝一份,改不了原变量。

2.4.3 指针 vs 引用

什么时候用指针,什么时候用引用?我个人的经验是:

  • 引用:参数必须存在,且不需要重新绑定。比如函数参数、操作符重载。
  • 指针:参数可能为空,或者需要重新指向别的对象。比如动态分配的对象、数组遍历。

在 Unreal 里,AActor* 指针随处可见。因为 Actor 可能被销毁,指针可能变成 nullptr。而引用则常用于 const & 参数,避免拷贝。

一句话总结:能用引用就用引用,必须用指针时,记得检查空指针。

2.5 实战:在 Unreal 中应用这些基础

咱们来写一个简单的 Unreal 函数,把今天学的都用上:

// 计算角色受到的最终伤害
float UMyGameInstance::CalculateFinalDamage(float BaseDamage, const FDamageInfo& DamageInfo)
{
    // 检查输入
    if (BaseDamage <= 0.0f)
    {
        return 0.0f;
    }

    // 根据伤害类型计算修正
    float DamageMultiplier = 1.0f;
    switch (DamageInfo.DamageType)
    {
        case EDamageType::Fire:
            DamageMultiplier = 1.5f;  // 火焰伤害加成
            break;
        case EDamageType::Ice:
            DamageMultiplier = 0.8f;  // 冰霜伤害减免
            break;
        default:
            break;
    }

    // 计算最终伤害
    float FinalDamage = BaseDamage * DamageMultiplier;

    // 应用护甲减伤(如果有)
    if (DamageInfo.TargetArmor > 0)
    {
        FinalDamage *= (100.0f / (100.0f + DamageInfo.TargetArmor));
    }

    return FinalDamage;
}

你看,这里用到了 ifswitchconst & 引用、基本类型。这些基础组合起来,就能写出健壮的游戏逻辑。

好了,这一讲就到这里。下一讲咱们会深入 Unreal 的反射系统和 UObject,那才是真正进入 Unreal 的核心。到时候你会发现,今天这些基础有多重要。