避免空指针解引用
空指针解引用会导致标准未定义的行为。
使用 *、->、.*、->*、[]、() 等运算符,通过指针的值访问指针指向的数据称为“解引用(dereference)”。使用 nullptr、NULL、0 等常量初始化的指针是空指针,未指向任何对象或函数,解引用空指针属于逻辑错误。
空指针与任何已指向对象或函数的指针均不相等,通过空指针访问对象或函数是非法的,其后果在语言标准中是未定义的。执行环境可以拒绝程序访问空指针对应的地址,终止程序运行,但并非所有环境都具备相关机制。
示例:
int foo(int i) {
int* p = NULL;
if (cond) {
p = &i;
}
return *p; // Non-compliant
}
如果例中条件表达式 cond 为假,p 为空指针,*p 可以引发“段错误”,使程序无法继续运行。
对于服务类程序,如果攻击者能够掌握导致空指针解引用的外部操作,会使系统遭受“拒绝服务攻击”。在特殊环境中,如某些嵌入式系统或某些超级计算机等,空指针对应的地址是可以被访问的,但其后果是不可预期的,甚至会导致恶意代码的执行。
例外:
int* p = NULL;
int* q = &*p; // Compliant in C
根据 C 标准,不论指针 p 是否为空,&*p 均等同于 p,故例中 q 也是空指针,虽然 C++ 编译器会兼容 C 标准,但应注意 C++ 标准并无此规定。
在 C++ 理论体系中,如果 P 为指针类型的表达式, *P 总应指代有效的对象或函数,形如 P->M、P->*M 的表达式等同于 (*P).M、(*P).*M,形如 P[N] 的表达式等同于 *(P + N),如果 P 或 P + N 没有指向有效的对象或函数,程序的行为是未定义的。在任何情况下,解引用运算符均不应作用于空指针等未指向有效对象的指针。
如通过空指针访问静态成员是不符合要求的:
struct T {
static int bar() { return 1; }
};
T* p = nullptr;
cout << p->bar(); // Non-compliant, use ‘T::bar()’ instead
访问类的静态成员不需要对象实例,通过空指针访问静态成员一般不会造成实际错误,但这种情况下程序的行为是未定义的,可参见“CWG issue 315”、“CWG issue 2823”。另外,通过 . 或 -> 访问的静态成员会被误认作非静态成员,例中 p->bar() 应改为 T::bar(),以遵循标准并提高可读性。
避免空指针解引用的措施:
- 非必要不使用指针,或使用引用、迭代器、智能指针等方式代替指针
- 封装指针,避免外界直接操作指针,并进行充分的单元测试
- 接口文档应明确标注是否接受空指针作为参数,以及返回值是否有可能为空指针
- 对于有可能为空的指针,在使用前均需判断其是否为空指针
- 利用代码分析工具定期检查代码,并将这种检查作为开发流程的一部分
示例:
size_t len(const T* p) {
return p->size(); // Bad, missing documentation or validation
}
例中函数不接受空指针作为参数,如果是设计使然,应使用断言进行标注,否则应对参数为空指针的情况作出处理。
断言也是接口文档的重要组成部分,可显著提高可读性,并提供运行时检查,如:
size_t len(const T* p) {
assert(p != NULL); // Good
return p->size();
}
在 C++ 代码中也可以使用引用代替不为空的参数:
size_t len(const T& r) { // Safe and brief
return r.size();
}