CppCoreGuidelines ES.42 确保指针的使用简单明了
15 April 2023
“Keep use of pointers simple and straightforward”
理由
复杂的指针操作是导致故障的根源。
注意
请使用gsl::span
。指针应该只指向一个对象。指针的数值计算很容易出错误,是大量严重 bug 和安全隐患的根源。用 span
就能以边界检测、类型安全的方式访问数组数据。如果我们通过常量访问已知边界长度的数组,编译器能够帮助我们验证数组下标是否合法。
坏例子
void f(int* p, int count) { if (count < 2) return; int* q = p + 1; // BAD ptrdiff_t d; int n; d = (p - &n); // OK d = (q - p); // OK int n = *p++; // BAD if (count < 6) return; p[4] = 1; // BAD p[count - 1] = 2; // BAD use(&p[0], 3); // BAD }
好例子
void f(span<int> a) // BETTER: use span in the function declaration { if (a.size() < 2) return; int n = a[0]; // OK span<int> q = a.subspan(1); // OK if (a.size() < 6) return; a[4] = 1; // OK a[a.size() - 1] = 2; // OK use(a.data(), 3); // OK }
注意
对于用变量作为下标访问数组的操作,不管是工具还是程序员都很难验证其正确性与安全性。 span
在运行时会被边界检测,就能以类型安全的方式访问数组数据。at()
是另一个解决方案,可以确保单次访问是被边界检测的。如果需要迭代器访问数组数据,请使用从数组的 span
生成的迭代器。
坏例子
void f(array<int, 10> a, int pos) { a[pos / 2] = 1; // BAD a[pos - 1] = 2; // BAD a[-1] = 3; // BAD (but easily caught by tools) -- no // replacement, just don't do this a[10] = 4; // BAD (but easily caught by tools) -- no // replacement, just don't do this }
好例子
使用 span
:
void f1(span<int, 10> a, int pos) // A1: Change parameter type to use span { a[pos / 2] = 1; // OK a[pos - 1] = 2; // OK } void f2(array<int, 10> arr, int pos) // A2: Add local span and use that { span<int> a = {arr.data(), pos}; a[pos / 2] = 1; // OK a[pos - 1] = 2; // OK }
使用 at()
void f3(array<int, 10> a, int pos) // ALTERNATIVE B: Use at() for access { at(a, pos / 2) = 1; // OK at(a, pos - 1) = 2; // OK }
坏例子
void f() { int arr[COUNT]; for (int i = 0; i < COUNT; ++i) arr[i] = i; // BAD, cannot use non-constant indexer }
好例子
使用 span
#include <gsl/gsl> using namespace gsl; constexpr int COUNT = 10; void f1() { int arr[COUNT]; span<int> av = arr; for (int i = 0; i < COUNT; ++i) { av[i] = i; cout << av[i] << ", "; } } int main() { f1(); return EXIT_SUCCESS; }
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
使用 span
和带范围的 for
#include <gsl/gsl> using namespace gsl; constexpr int COUNT = 10; void f1a() { int arr[COUNT]; span<int, COUNT> av = arr; int i = 0; for (auto& e : av) { e = i++; cout << e << ", "; } } int main() { f1a(); return EXIT_SUCCESS; }
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
使用at()
访问
#include <gsl/gsl> using namespace gsl; constexpr int COUNT = 10; void f2() { int arr[COUNT]; for (int i = 0; i < COUNT; ++i) { at(arr, i) = i; cout << at(arr, i) << ", "; } } int main() { f2(); return EXIT_SUCCESS; }
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
使用带范围的 for
constexpr int COUNT = 10; void f3() { int arr[COUNT]; int i = 0; for (auto& e : arr) { e = i++; cout << e << ", "; } } int main() { f3(); return EXIT_SUCCESS; }
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
注意
工具可以用at()
替换掉用动态索引访问数组的代码:
#include <gsl/gsl> using namespace gsl; static int a[10]; void f(int i, int j) { a[i + j] = 12; // BAD, could be rewritten as ... at(a, i + j) = 12; // OK -- bounds-checked cout << at(a, i + j); } int main() { f(2, 3); return EXIT_SUCCESS; }
12
例子
如果用指针传递数组,则没有机会进行边界检测了。所以尽量避免。
void g(int* p); void f() { int a[5]; g(a); // BAD: are we trying to pass an array? g(&a[0]); // OK: passing one object }
如果你要传递一个数组,可以这样写:
void g(int* p, size_t length); // old (dangerous) code void g1(span<int> av); // BETTER: get g() changed. void f2() { int a[5]; span<int> av = a; g(av.data(), av.size()); // OK, if you have no choice g1(a); // OK -- no decay here, instead use // implicit span ctor }
强化
- 标记计算结果为指针类型,针对指针的算术操作。
- 标记索引下标不是有效范围内的常量,且针对表达式或数组类变量的下标操作。
- 标记隐式地将数组类型转换成指针的操作。
此规则是边界安全规则集的一部分。