在C中递增指针地址和值
int main (void){
int num1=2;
int *pnum=NULL;
pnum=&num1;
*pnum++;
printf("%d",*pnum);
}
为什么此代码打印地址,而不是值?不 * 取消引用 pnum 吗?
回答
检查运算符的优先级:后缀一元运算符比前缀一元运算符绑定得更紧密,因此*pnum++等效于*(pnum++),而不是(*pnum)++。
pnum++递增指针pnum并返回 的旧值pnum。递增指针使其指向数组的下一个元素。任何变量都可以被视为一个元素的数组,因此pnum指向位于num1is的单元素数组中第一个之后的元素,我将称之为num1_array[1]。指向数组末尾的指针是有效的,即经过最后一个元素的一个位置。取消引用该指针是无效的:那将是数组溢出。但计算指针是有效的。在 C 中构造无效指针是未定义行为,即使您没有取消引用它;但是这个指针是有效的。
*pnum++取消引用 的旧值pnum。因为那是一个指向 的指针num1,所以这个表达式是完全有效的,它的值是 的值num1。此时,任何半途而废的编译器都会警告该值未使用。如果您没有看到此消息,请配置您的编译器以打印更多警告:不幸的是,许多编译器默认接受错误代码而不是发出错误信号。例如,使用 GCC 或 Clang:
$ gcc -Wall -Wextra -Werror a.c
a.c: In function ‘main’:
a.c:6:5: error: value computed is not used [-Werror=unused-value]
6 | *pnum++;
| ^~~~~~~
cc1: all warnings being treated as errors
调用printf接收参数*pnum。我们之前看到,此时,pnum指向单元素数组的末尾num1_array[1]。这个指针是有效的,但由于它指向对象的末尾,解引用具有未定义的行为。在实践中,这通常要么崩溃,要么打印一些恰好位于特定内存位置的垃圾值。当您调试程序时,有一些工具可以帮助您提高无效指针导致崩溃的可能性,而不是默默地使用垃圾值。例如,对于 GCC 或 Clang,您可以使用AddressSanitizer:
$ export ASAN_OPTIONS=symbolize=1
$ gcc -Wall -Wextra -fsanitize=address a.c && ./a.out
a.c: In function ‘main’:
a.c:6:5: warning: value computed is not used [-Wunused-value]
6 | *pnum++;
| ^~~~~~~
=================================================================
==2498121==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff15ae3e74 at pc 0x55d593978366 bp 0x7fff15ae3e30 sp 0x7fff15ae3e20
READ of size 4 at 0x7fff15ae3e74 thread T0
#0 0x55d593978365 in main (/tmp/stackoverflow/a.out+0x1365)
#1 0x7f525a1380b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)
#2 0x55d59397818d in _start (/tmp/stackoverflow/a.out+0x118d)
Address 0x7fff15ae3e74 is located in stack of thread T0 at offset 36 in frame
#0 0x55d593978258 in main (/tmp/stackoverflow/a.out+0x1258)
This frame has 1 object(s):
[32, 36) 'num1' (line 3) <== Memory access at offset 36 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (/tmp/stackoverflow/a.out+0x1365) in main
Shadow bytes around the buggy address:
0x100062b54770: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100062b54780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100062b54790: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100062b547a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100062b547b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x100062b547c0: 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1[04]f3
0x100062b547d0: f3 f3 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100062b547e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100062b547f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100062b54800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100062b54810: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==2498121==ABORTING
此跟踪告诉您:
- 局部变量 (
stack-buffer-overflow) 中存在缓冲区溢出。 - 溢出访问是尝试读取 4 个字节 (
READ of size 4)。 - 有关问题地址 (
[32, 36) 'num1') 的更多信息。您可以看到程序在num1. - 有问题的指令 (
#0 0x55d593978365)的地址。您可以在调试器中设置断点以检查程序可能在做什么。
在大多数平台上,给定您的程序,堆栈num1上的变量是一个变量,而末尾是堆栈上前一个变量num1的地址。这可以是任何东西,具体取决于您的编译器如何访问内存的细节。这可能是许多事情之一pnum,如果pnum和num1碰巧在您的平台上具有相同的大小(这在 32 位平台上通常是这种情况)并且编译器决定将pnum之前num1放在堆栈上(这在很大程度上取决于编译器、优化级别和程序的细节)。所以你的程序打印 : 的地址是合理的,pnum不是因为*pnum不知何故没有调用解引用运算符,而是因为你的程序已经pnum 指向自己。