1. C++ 中的 C
1. 认识 volatile
1.1 volatile 的作用
每次都从内存中读取该值,而不会因编译器优化从缓存的地方读取(如寄存器中,由于访问寄存器的速度比 RAM 快),从而保证 volatile 变量被正确读取。
- 问题示例:从寄存器中读取值,被修改的变量值不能及时得到反应。
#include <iostream>
using namespace std;int main() {int i = 10;int a = i;cout << a << endl;_asm {mov dword ptr[ebp - 4], 80}int b = i;cout << b << endl;return 0;
}
VS 2022 环境下生成 x86 Release 版本(Release 模式下才会对程序代码进行优化),输出结果如下,可见 i 的值还是 10,并没有变成 80。
- 解决方法:volatile
VS 2022 环境下生成 x86 Release 版本,用 volatile 定义变量 i, 输出结果如下。
1.2 volatile 的应用场景
1.2.1 并行设备的硬件寄存器
假如要初始化一个设备,这个设备的某个寄存器为 0xff800000:
int* output = (unsigned int*)0xff800000;int init() {for (int i = 0; i < 10; i++) {*output = i;}
}
编译器经过优化后认为前面的循环是无用的,对最后的结果毫无影响,因为最终是将 output 这个指针的内容赋值为 9,所以最后编译器编译的代码结果相当于:
int init() {*output = 9;
}
但如果对此设备进行初始化的过程必须像开头一样顺序地对其赋值,则优化过程显然不行。需要加 volatile。
1.2.2 一个中断服务子程序中会访问到的变量
static int i = 0;int main() {while (1) {if (i) {dosomething();}}
}// 中断服务程序
void IRS() {i = 1;
}
代码本意是产生中断时,IRS 响应,i 被置为 1,然后 main 中 dosomething()。但是,编译器判断在 main 中没有动过 i,因此可能只执行一次把 i 读到寄存器的操作,然后 if 判断时都只用这个寄存器里的值,导致 dosomething 永远无法执行。
1.2.3 多线程应用中被几个任务共享的变量
volatile bool bStop = false;void threadFunc1() {while (!bStop) {......}
}void theadFunc2() {bStop = true;
}
如果没有 volatile 修饰,threadFunc1 中将是个死循环,因为 bStop 已经被读到寄存器中。
1.3 volatile 的常见问题
1.3.1 一个参数既可以是 const 也可以是 volatile 吗?为什么?
YES!例如只读的状态寄存器。
1.3.2 一个指针可以是 volatile 吗?为什么?
YES!例如:这里需要注意两点:
volatile int *p;
p 是指向 volatile int 的普通指针,*p = 10;
每次访问都会真正读写内存,因为 *p 的内容可能随时被硬件或其他线程修改,所以不能优化。int * volatile p;
p 是一个 volatile 指针,指针变量本身可能随时被外部改变(例如通过 DMA、硬件寄存器映射)- 还有
volatile int * volatile p;
1.3.3 下面的函数有什么错误?
int square(volatile int* ptr) {return *ptr * *ptr;
}
因为加了 volatile,所以上述代码变成下面:
int square(volatile int* ptr) {int a, b;a = *ptr;b = *ptr;return a * b;
}
由于 *ptr 可能被改变,所以 a 和 b 可能不相同,导致代码不符合预期。可以修改为如下:
int square(volatile int* ptr) {int a;a = *ptrreturn a * a;
}
2. 数组与指针详解
2.1 数组
2.1.1 数组名
数组名,本质是一个文字常量(指针常量),代表第一个元素的地址和数组的首地址。数组名本身不可寻址(理解为不能类似自增这种操作,改变其值,也呼应了这句开头的文字常量)。
int a[10];int* &r = a; // 错误
int* const &r = a; // 正确
2.1.2 数组类型
- 对于一个整型变量,其类型是 int。那么对于数组 a,它的类型是怎样的?其类型有两部分,一个是元素类型,一个是元素个数。所以对于 sizeof 来说,能够计算出来占用的空间大小就是数组类型的大小,也即 元素个数 乘上 元素类型的大小。
- 对于数组 a 来说,a、&a[0]、a+0 是等价的。
2.1.3 数组的引用
- 对于数组 a,在数据区开辟一个无名临时变量,将数组 a 的地址常量复制到该变量中,再将常引用 r 与此变量进行绑定。即先有 int *,表示复制数组 a 的地址,然后是一个常引用 const &。
- 另一种就比较直观了,直接数组类型 int [4],然后用一个引用 &。
int a[4];int* const &r = a;
int (&ra)[4] = a;
2.1.4 数组指针
数组指针,数组 a 和 &a 的类型是不一样的,a 的类型就是数组类型 int[4],在表达式中退化为 int*;而 &a 是取数组的地址,也即它的类型是 int(*)[4],所以 a+1 表示第 2 个元素的地址,而 &a + 1 是跳过一个数组大小的地址。示例如下:
#include <iostream>
using namespace std;int main()
{int a[4] = {};int* const &r = a;int (&ra)[4] = a;cout << a << endl;cout << &a << endl;cout << a+1 << endl;cout << &a+1 << endl;return 0;
}
$ g++ test.cpp
$ ./a.out
0x7ffe02deeeb0
0x7ffe02deeeb0
0x7ffe02deeeb4
0x7ffe02deeec0
2.2 指针
任何指针类型所占用的空间一般都是 4 字节(在 32 位平台上)。比如 sizeof(int*),sizeof(float*),等等。
野指针:指向非法内存地址的指针
2.2.1 使用未初始化的指针
int *p;
cout << *p << endl;
2.2.2 指针所指向的对象已经消亡
#include <iostream>
using namespace std;int* retAddr() {int num = 10; // 局部变量,存放在栈上return # // ❌ 返回局部变量的地址
}int main() {int* p = NULL;p = retAddr(); // p 接收了一个已经无效的地址cout << &p << endl; // 打印 p 这个指针变量自己的地址cout << *p << endl; // ❌ 解引用一个悬空指针,未定义行为
}
2.2.3 指针释放之后未置空
#include <iostream>
using namespace std;int main() {int* p = NULL;p = new int[10]; // 在堆上分配一个大小为 10 的 int 数组delete p; // ❌ 错误释放方式cout << "p[0]:" << p[0] << endl; // ❌ 使用已释放的内存
}
2.2.4 realloc() 函数使用不当
#include <malloc.h>void main() {char *p, *q;p = (char *)malloc(10);q = p;p = (char *)realloc(p,20);// ...
}
p = malloc(10)
向堆申请 10 字节内存,返回首地址赋给p
。此时q = p
,所以q
和p
指向同一块 10 字节内存。p = realloc(p, 20)
试图把p
指向的内存扩展到 20 字节。
如果原地址后面刚好有足够空闲空间,realloc
会直接在原地扩展,返回的地址和p
相同,q
依然有效。
如果原地址后面没有足够空间,realloc
会:- 在堆中找一块新的 20 字节内存(相当于
malloc(20)
), - 把原来 10 字节的数据拷贝到新内存,
- 释放原来的 10 字节内存,
- 返回新内存的地址。
- 此时
p
被更新为新地址,但q
依旧保存着旧地址——而旧内存已经被释放。
- 在堆中找一块新的 20 字节内存(相当于
- 所以
q
成为了 悬空指针(dangling pointer)。
如果再用q
访问数据,就会导致 未定义行为,可能导致崩溃或数据错误。
- 正确的写法:如果真的要保留旧数据的引用,就要明确判断
realloc
的返回值,并避免让q
成为野指针。例如:
char *p, *q;
p = malloc(10);
q = p;char *tmp = realloc(p, 20);
if (tmp != NULL) {p = tmp;// q 现在不能再安全使用旧地址q = p; // 如果还要 q 继续指向新地址,就必须更新
} else {// realloc 失败,p 仍然指向原来的 10 字节
}
3. 文字常量和常变量
3.1 文字常量
文字常量:编译后写在代码区,不可寻址不可更改。包括 数值常量,字符常量和符号常量。
int &r = 5; // ❌ 错误
因为数值常量没办法寻址。但下面的却是可以的,编译器将数值常量转变为了常变量,在数据区开辟了一个值为 5 的无名整型常变量,然后引用绑定。
const int &r = 5;
3.2 常变量
常变量:定义时必须显式初始化且值不可修改的变量。可以寻址。例如 字符串常量。
全局常变量存储在只读存储区,不可修改;局部常变量存储在栈上,可以间接修改。