【RM前置】C / C++ 基本特性

对竞赛代码中不太熟悉的地方做了补充

【RM前置】C / C++ 基本特性

零、速查

&   → 取地址
*   → 解引用

volatile
→ 防止编译器对变量访问进行不当优化
→ 常用于硬件寄存器

static
→ 局部:延长生命周期
→ 全局:限制文件作用域

public
→ 外部可访问

protected
→ 自己 + 子类可访问

private
→ 类内部可访问

virtual
→ 支持运行时多态

override
→ 明确表示子类重写父类虚函数

virtual ~Base()
→ 多态基类通常需要虚析构函数

new
→ 动态申请对象

delete
→ 释放 new 得到的对象

malloc
→ 动态申请内存

free
→ 释放 malloc 得到的内存

一、整数类型与 <stdint.h>

<stdint.h> 提供了一些明确位宽的整数类型,避免不同平台上 int 等类型大小不同带来的问题。

 | `uint8_t`  | unsigned,8 bit,无符号  |
 | `int32_t`  | signed,32 bit,有符号   |
 | `uint32_t` | unsigned,32 bit,无符号 |
 | `float`    | 浮点数                   |

为什么使用 stdint.h?

不同平台上 int 等基础类型的大小可能存在差异,因此使用:

uint8_t
int32_t
uint32_t

可以明确规定变量的位宽。


二、指针

1. 定义指针

int *p = &a;
  • p 是一个 int* 类型的指针。
  • &a:取变量 a 的地址。
  • p 中存放的是 a 的地址。

可以理解为:

a       → 存放数据
&a      → a 的地址
p       → 存放 a 的地址

2. 解引用

*p

*p 表示解引用,即访问 p 所指向地址中的数据。

例如:

int a = 10;
int *p = &a;

printf("%d", *p);   // 10

3. 野指针

野指针:指针指向了一个未知、无效或者不可访问的内存地址。

例如:

int *p;
*p = 10;    // 危险

因为 p 没有指向一个确定的有效地址。

因此,指针暂时不用时可以:

int *p = NULL;

4. 常见表示

十六进制

0x

表示后面的数字是十六进制。

例如:

0x40020000

u

例如:

100u

表示 unsigned int 类型的整数。


三、typedef struct —— 结构体类型定义

常见写法:

typedef struct
{
    volatile uint32_t DDR;
    ...
} GPIO_TypeDef;

这样之后:

GPIO_TypeDef gpio;

就可以直接使用 GPIO_TypeDef 作为结构体类型。


GPIO 寄存器映射

笔记中的例子类似:

typedef struct
{
    volatile uint32_t DDR;
    ...
} GPIO_TypeDef;

#define GPIOA ((GPIO_TypeDef *)0x40020000)

其中:

GPIOA

本质上是一个指向 GPIO_TypeDef 的指针。

可以理解成:

GPIOA
  ↓
0x40020000
  ↓
GPIO 寄存器区域

GPIO

GPIO:

General-Purpose Input/Output

即:

通用输入/输出接口


四、volatile 与 static

1. volatile

volatile 的核心作用:

告诉编译器:这个变量的值可能在程序代码之外发生改变,不要随意进行优化。

典型应用:

  • 硬件寄存器
  • 中断相关变量
  • 多线程/异步修改的变量等

例如:

volatile uint32_t DDR;

这里的 DDR 很可能对应硬件寄存器。

如果没有 volatile,编译器可能认为某个变量的值不会莫名其妙发生变化,从而进行优化。


五、static

static 主要涉及两个概念:

生命周期 + 作用域

1. 局部 static

例如:

void test()
{
    static int count = 0;
    count++;
}

虽然 count 是在函数内部定义的,作用域仍然是局部的,但是它的生命周期会持续到整个程序结束。

也就是说:

普通局部变量
→ 函数结束
→ 生命周期结束

static 局部变量
→ 函数结束
→ 变量仍然存在
→ 程序结束才释放

2. 全局 static

在全局位置:

static int value;

static 可以限制变量/函数的可见范围,使其只在当前源文件中可见。

因此可以简单记:

局部 static
→ 延长生命周期

全局 static
→ 限制作用域 / 文件内可见

六、程序的内存区域

笔记最后画了一个大致的内存分类:

程序运行时的内存
│
├── 栈(Stack)
│     └── 自动管理 / 自动释放
│
├── 静态区(Static / Global)
│     └── 程序运行期间一直存在
│
└── 堆(Heap)
      └── 手动申请、手动释放

栈 Stack

主要存放:

  • 普通局部变量
  • 函数调用相关数据等

特点:

自动分配、自动释放。


静态区 Static / Global

主要包括:

  • 全局变量
  • static 变量

特点:

生命周期通常贯穿整个程序运行过程。


堆 Heap

动态申请的内存:

malloc()

申请:

int *p = (int *)malloc(sizeof(int));

释放:

free(p);

C++ 中常见:

new
delete

即:

堆内存
→ malloc / new
→ 手动申请

→ free / delete
→ 手动释放

七、C++ 面向对象

1. 类 class

笔记中的例子:

class Motor
{
public:
    void setOutput(float out)
    {
        output = out;
    }

    float getSpeed() const
    {
        return speed;
    }

private:
    int id;
    float speed;
    float output;
};

八、访问权限

C++ 中常见三种访问权限:

public
private
protected

public

public:

表示:

外部可以直接访问。

例如:

motor.setOutput(0.8);

private

private:
    int id;
    float speed;
    float output;

表示:

类的内部可以访问,外部不能直接访问。

主要用于封装数据。


protected

protected:

表示:

类自身以及子类可以访问,类外部不能直接访问。

可以记:

public
→ 外部也能访问

protected
→ 自己 + 子类

private
→ 自己

九、构造函数

笔记中的构造函数:

Motor(int id)
    : id(id), speed(0), output(0)
{
}

当创建对象时:

Motor motor(1);

就会自动调用构造函数。

初始化列表

: id(id), speed(0), output(0)

表示初始化成员变量:

id     ← 参数 id
speed  ← 0
output ← 0

十、析构函数

析构函数:

~Motor()
{
}

对象销毁时自动调用。

如果类中涉及动态申请的资源,需要在析构阶段进行相应的资源释放。


十一、继承

例如:

class M3508 : public Motor
{
    ...
};

表示:

Motor
  ↑
  │ 继承
  │
M3508

M3508 是 Motor 的子类。


十二、virtual 虚函数

例如:

virtual void setOutput(float out) = 0;

virtual 表示这是一个虚函数,允许子类重新实现。

子类:

class M3508 : public Motor
{
public:
    void setOutput(float out) override
    {
        ...
    }
};

这里:

override

表示:

子类明确表示自己正在重写父类的虚函数。


十三、纯虚函数

如果写成:

virtual void setOutput(float out) = 0;

那么这是一个:

纯虚函数

含义是:

父类只规定接口,不提供具体实现,具体怎么实现由子类决定。

拥有纯虚函数的类通常称为:

抽象类

抽象类一般不能直接创建对象。


十四、虚析构函数

如果一个类可能作为父类被继承,通常应该考虑:

virtual ~Motor() = default;

这样通过父类指针 delete 子类对象时,可以按照正确的继承关系调用析构函数。

例如:

Motor *motor = new M3508(1);

delete motor;

如果父类析构函数不是虚函数,可能无法正确执行子类析构过程。

所以可以记:

作为多态基类时,析构函数通常应该声明为 virtual。


十五、多态

笔记中的核心总结是:

同一个调用,根据对象实际类型的不同,得到不同的实现。

例如父类:

class Motor
{
public:
    virtual void setOutput(float out) = 0;
};

子类:

class M3508 : public Motor
{
public:
    void setOutput(float out) override
    {
        ...
    }
};

不同子类可以拥有不同的 setOutput() 实现。


十六、动态多态示例

笔记中的代码大致是:

Motor* motors[4] =
{
    new M3508(1),
    new M3508(2),
    new M3508(3),
    new M3508(4)
};

for (int i = 0; i < 4; i++)
{
    motors[i]->setOutput(0.8);
}

这里的关键点是:

Motor*

虽然指针类型都是父类 Motor*,但实际指向的对象可以是不同的子类。

通过:

virtual
+
override

实现运行时多态。


十七、静态方式 vs 动态方式

笔记中还写到了另一种写法:

M3508 motorObjects[4] =
{
    M3508(1),
    M3508(2),
    M3508(3),
    M3508(4)
};

以及:

Motor* motors[] =
{
    &motorObjects[0],
    ...
};

这里需要注意两种情况。

1. 静态/栈上创建对象

M3508 motorObjects[4];

对象本身直接存在于当前作用域对应的存储区域中,不需要:

new
delete

2. 动态创建对象

Motor *motor = new M3508(1);

对象通过 new 动态分配,需要考虑:

delete motor;

互动

0 人点赞 · 0 条评论


© 2026 By Catkin

Powered by Astro