零、速查
& → 取地址
* → 解引用
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;