指针运算与下标访问

指针运算与下标访问

在《容器与数组简介》中,我们指出数组元素在内存中连续存放。本章将深入探讨数组下标的数学原理。

尽管后续章节不会再直接使用这些下标计算,但本节内容有助于理解范围 for 循环的实现机制,并在后续学习迭代器时再次派上用场。

何谓指针运算指针运算允许我们对某类型的指针施加特定的整数算术运算(加、减、自增或自减),以产生新的内存地址。

给定指针 ptr,表达式 ptr + 1 返回指向内存中下一个对象的地址(步长取决于指针所指向类型)。例如,若 ptr 为 int* 且 int 占 4 字节,则 ptr + 1 为 ptr 后 4 字节处的地址,ptr + 2 为后 8 字节处的地址。

#include

int main()

{

int x {};

const int* ptr{ &x }; // 假设 int 占 4 字节

std::cout << ptr << ' ' << (ptr + 1) << ' ' << (ptr + 2) << '\n';

return 0;

}

在作者机器上输出:

00AFFD80 00AFFD84 00AFFD88

可见相邻地址相差 4 字节。

指针运算亦支持减法。ptr - 1 返回前一个对象的地址,步长同理。

#include

int main()

{

int x {};

const int* ptr{ &x }; // 假设 int 占 4 字节

std::cout << ptr << ' ' << (ptr - 1) << ' ' << (ptr - 2) << '\n';

return 0;

}

输出:

00AFFD80 00AFFD7C 00AFFD78

每步递减 4 字节。

关键洞察指针运算返回的是“下一个/上一个对象”的地址,而非简单的“下一个/上一个字节地址”。

若对指针使用前置自增 ++ 或自减 --,其效果与指针加减 1 相同,但会修改指针自身存储的地址。

类似于整型变量:

int x;

++x; // 等价于 x = x + 1

对于指针:

int* ptr;

++ptr; // 等价于 ptr = ptr + 1

#include

int main()

{

int x {};

const int* ptr{ &x };

std::cout << ptr << '\n';

++ptr;

std::cout << ptr << '\n';

--ptr;

std::cout << ptr << '\n';

return 0;

}

输出:

00AFFD80

00AFFD84

00AFFD80

警告严格来说,上述示例属于未定义行为。C++ 标准规定,指针运算只有当指针本身及其结果均位于同一数组(或指向数组尾后一位)时才为良定义。不过,现代实现通常不会强制检查,允许在数组外进行运算。

下标运算通过指针运算实现上一章《C 风格数组退化》提到可对指针使用 operator[]:

#include

int main()

{

const int arr[] { 9, 7, 5, 3, 1 };

const int* ptr{ arr }; // 指向元素 0 的普通指针

std::cout << ptr[2]; // 输出 5

return 0;

}

深入解析:下标表达式 ptr[n] 是 *((ptr) + (n)) 的简写形式。编译器先做指针运算,再隐式解引用。

初始化 ptr 时,arr 退化为首元素地址。ptr[2] 等价于 *(ptr + 2):– ptr + 2 得到首元素后 2 个对象的地址;– 解引用后取得数组下标 2 的元素。再举一例:

#include

int main()

{

const int arr[] { 3, 2, 1 };

std::cout << &arr[0] << ' ' << &arr[1] << ' ' << &arr[2] << '\n';

std::cout << arr[0] << ' ' << arr[1] << ' ' << arr[2] << '\n';

std::cout << arr << ' ' << (arr + 1) << ' ' << (arr + 2) << '\n';

std::cout << *arr << ' ' << *(arr + 1) << ' ' << *(arr + 2) << '\n';

return 0;

}

输出:

00AFFD80 00AFFD84 00AFFD88

3 2 1

00AFFD80 00AFFD84 00AFFD88

3 2 1

由于数组元素在内存中顺序存放,若 arr 指向元素 0,则 *(arr + n) 即为数组第 n 个元素。

这也是数组采用 0 基下标而非 1 基下标的主因:运算更高效,无需每次下标时额外减 1。

附注由于 ptr[n] 被翻译为 *((ptr) + (n)),因此甚至可以写成 n[ptr]!编译器会将其变为 *((n) + (ptr)),与前者行为一致,但请勿如此书写,因其可读性极差。

指针运算与下标均表示相对位置初学者常误以为下标表示数组中的固定元素:0 永远是首元素,1 永远是次元素……这是一种错觉。下标实则表示相对位置。它们看似固定,是因为我们通常从元素 0 开始索引。

给定指针 ptr,*(ptr + 1) 与 ptr[1] 均返回“下一个对象”。若 ptr 指向元素 0,则二者都指向元素 1;若 ptr 指向元素 3,则二者指向元素 4。

#include

#include

int main()

{

const int arr[] { 9, 8, 7, 6, 5 };

const int *ptr { arr }; // 指向元素 0

std::cout << *ptr << ptr[0] << '\n'; // 输出 99

std::cout << *(ptr+1) << ptr[1] << '\n'; // 输出 88

ptr = &arr[3]; // 指向元素 3

std::cout << *ptr << ptr[0] << '\n'; // 输出 66

std::cout << *(ptr+1) << ptr[1] << '\n'; // 输出 55

return 0;

}

为避免混淆,建议仅在从数组首元素(元素 0)开始索引时使用下标;若需相对定位,则使用指针运算。

最佳实践

以元素 0 为基准时,优先使用下标,使下标与元素序号一致。需相对定位时,使用指针运算。负下标上一章曾提及,与标准库容器不同,C 风格数组的下标可为有符号或无符号整数。这不仅出于便利,更因为负下标的确可行。

已知 *(ptr + 1) 指向下一对象,那么上一对象对应的下标形式为何?正是 ptr[-1]。

#include

#include

int main()

{

const int arr[] { 9, 8, 7, 6, 5 };

const int* ptr { &arr[3] }; // 指向元素 3

std::cout << *ptr << ptr[0] << '\n'; // 输出 66

std::cout << *(ptr-1) << ptr[-1] << '\n'; // 输出 77

return 0;

}

使用指针运算遍历数组指针运算最常见的用途之一是无显式索引地遍历 C 风格数组:

#include

int main()

{

constexpr int arr[]{ 9, 7, 5, 3, 1 };

const int* begin{ arr }; // begin 指向首元素

const int* end{ arr + std::size(arr) }; // end 指向尾后一位

for (; begin != end; ++begin) // 从 begin 到 end(不含)遍历

{

std::cout << *begin << ' '; // 解引用获取当前元素

}

return 0;

}

输出:

9 7 5 3 1

end 设为“尾后一位”地址,虽不可解引用,但便于边界判断。只要指针运算结果位于数组元素或尾后一位内,即为良定义;否则导致未定义行为。在上一章中,我们指出数组退化使函数重构困难,因为某些操作对非退化数组有效,对退化数组则不然。然而,上述遍历方式可原封不动地提取为独立函数,且仍能正常工作:

#include

void printArray(const int* begin, const int* end)

{

for (; begin != end; ++begin)

{

std::cout << *begin << ' ';

}

std::cout << '\n';

}

int main()

{

constexpr int arr[]{ 9, 7, 5, 3, 1 };

const int* begin{ arr };

const int* end{ arr + std::size(arr) };

printArray(begin, end);

return 0;

}

该程序正确编译并输出,且我们并未显式把数组传给函数,而是通过 begin 与 end 提供了遍历所需的全部信息。后续学习迭代器与算法时,将看到标准库大量采用“begin/end”对来界定操作范围。

范围 for 循环基于指针运算实现考虑以下范围 for 循环:

#include

int main()

{

constexpr int arr[]{ 9, 7, 5, 3, 1 };

for (auto e : arr)

{

std::cout << e << ' ';

}

return 0;

}

根据文档,范围 for 循环大致展开如下:

{

auto __begin = begin-expr;

auto __end = end-expr;

for ( ; __begin != __end; ++__begin)

{

range-declaration = *__begin;

loop-statement;

}

}

将示例手动展开:

#include

int main()

{

constexpr int arr[]{ 9, 7, 5, 3, 1 };

auto __begin = arr; // begin-expr 为 arr

auto __end = arr + std::size(arr); // end-expr 为 arr + size

for ( ; __begin != __end; ++__begin)

{

auto e = *__begin; // range-declaration

std::cout << e << ' '; // loop-statement

}

return 0;

}

与前例几乎一致,区别仅在于将 *__begin 赋给 e 后再使用。

测验问题 1a) 为何 arr[0] 与 *arr 等价?[显示答案]

相关内容下一章《C 风格字符串》还将包含指针运算的更多测验题。

✨ 相关推荐

B&O BEOPLAY S3 蓝牙便携音箱开箱
下载旧版本彩票365软件

B&O BEOPLAY S3 蓝牙便携音箱开箱

📅 07-18 👀 8670
什么颜色搭配条幅
下载旧版本彩票365软件

什么颜色搭配条幅

📅 12-06 👀 2824
演艺之路:演员证与学历要求全解析
365bet网络娱乐

演艺之路:演员证与学历要求全解析

📅 09-12 👀 8550