C语言 - 指针偏移量异常

3
我有一个指向数组的指针,我想使用像memcpy这样的函数来进行特定偏移量的操作,但是当我对指针地址进行偏移时,得到的值比偏移量大,我不明白为什么会这样。有人能解释一下这里发生了什么吗?
#include <stdio.h>
#include <stdint.h>

int main()
{
    uint8_t *source[5];

    // Initial
    printf("%p\n", (void *)source); // 786796896

    // Offset by 2
    printf("%p\n", (void *)(source + 2)); // 786796912 (unexpected, more than 2)
}

4
我的编译器(使用默认设置)给出了7个警告。对于一个20行的程序来说还不错。 - pmacfarlane
@pmacfarlane 如果11不算差的话,那么0有多好呢? - user16217248
1
"%d" 不是打印指针的正确方式。请尝试使用 "%p"。 - pmacfarlane
2
uint8_t *ptr = source; 这是一个错误,你的编译器会告诉你。@user16217248 解释了你看到的值,但这可能是未定义的行为,只是碰巧做了你想要的事情。 - pmacfarlane
1
谢谢您指出这些警告,我在制作示例时使用了在线编译器,没注意到它们。我已经修复了这些警告,简化了示例,并根据@pmacfarlane的建议更新了printf,以使用%p来处理指针。 - koga73
3个回答

0
问题在于当你将2添加到source时,数组会衰变成指针类型uint8_t **。当你对指针执行算术运算时,添加的偏移量是添加的元素数量而不是字节数量,如果指针元素的大小大于一个字节。从source + 2的字节偏移量实际上是2*sizeof(*source)字节,即16个字节。

为了绕过这种行为,将source转换为char *,执行加法,然后再转换回来。但要注意,如果操作不正确,可能会导致未对齐访问,这可能是个坏消息。


1
创建未对齐指针也是UB。 - M.M
@Gerhardh 不会的。但至少它不会是即时未定义行为。 - user16217248
1
陷阱表示法将在存储第二次转换的结果时“触发”,如果它实际上是一个陷阱表示法。但是,实现可以定义未对齐的值不是陷阱表示法。 - M.M
指针衰减仅发生在声明为数组的参数变量上,并仅限于第一级,因此类似double matrix[3][5]参数定义 衰减为 double (*matrix)[5],而不是 double ***matrix 或者您对衰减的任何想法。 - Luis Colorado
在这种情况下,声明的对象不是指针,而是指针数组,并且没有进行衰减,因为它是一个局部变量,从未成为函数参数。该变量中有五个指向int8_t的指针,未初始化,但从未有单个指向数组的指针。我们疯了吗?K&R中有一整章(甚至更多)介绍类型定义表达式的相对晦涩的语法。请阅读它。 - Luis Colorado
显示剩余9条评论

0
指针运算需要尽量避免。对于上述情况,应该尽量避免使用指针运算。
#include <stdio.h>
#include <stdint.h>

int main() {
    uint8_t* source[5]; // array of 5 pointers of uint8_t* type

    printf("%p\n", &source[2]); // address of 3rd element place in array source
}

需要注意的一个非常重要的点是,将2添加到源地址并不会导致地址增加+2,而是增加+10,因为2被解释为char *[5]而不是char *。
// using without casting
char * arr[5];
char * parr = malloc(sizeof(int));
    
printf("%p\t%p\n", arr, parr);
printf("%p\t%p\n", arr+2, parr+2);

0x7ffde2925fb0  0x55b519f252a0
 +10             +2
0x7ffde2925fc0  0x55b519f252a2

//using with casting

char * arr[5];
char * parr = malloc(sizeof(int));
    
printf("%p\t%p\n", arr, parr);
printf("%p\t%p\n", (void*)arr+2, parr+2);

0x7ffde2925fb0  0x55b519f252a0
 +2               +2
0x7ffde2925fb2  0x55b519f252a2


3
我看不出避免指针算术的任何优势。而且实际上很难避免,特别是如果您曾经使用过数组。这是因为数组索引*就是 *指针算术+解引用。 - John Bollinger
鉴于数组语法只是另一种编写指针算术的方式,我不认为有任何理由严格避免使用它。&source[2]source+2完全相同。在这种用例中,我发现后者甚至比使用数组语法更易读。顺便说一下:您正在调用未定义的行为,因为读取指针元素的未确定值。而且您忘记了转换为应传递给%pvoid* - Gerhardh
这个可以运行,但我不喜欢强制类型转换: memcpy((uint8_t *)buf_header + offset1 + offset2, val, val_size); - koga73
1
“尽可能避免使用指针算术”不是对C语言学习者的好建议。如果您不理解指针算术,请不要用您的陈述来误导人们。您还没有意识到int8_t *source[5]不是指向数组的指针,而是指针数组(一个非常不同的对象)。 - Luis Colorado
@JohnBollinger,数组索引是一种掩盖的指针算术方式。你是对的,所以不要责怪我,我也已经在这方面添加了注释,请在抱怨之前先阅读它。 - Luis Colorado
显示剩余3条评论

0
我有一个指向数组的指针。
    uint8_t *source[5];

抱歉,但这不是指向数组的指针。它是一个包含5个未初始化的指向uint8_t的指针的数组,因此您应该对其进行初始化。

这是一个指向数组的指针:

    uint8_t (*source)[5];

(在类型表达式中,[] 运算符的优先级高于 * 运算符)

...这里到底发生了什么?

嗯,基于你错误的假设,所有的东西都是不正确的。

    // Initial
    printf("%p\n", (void *)source); // 786796896

    // Offset by 2
    printf("%p\n", (void *)(source + 2)); // 786796912 (unexpected, more than 2)

source + 2 是数组 source 的第三个元素的地址,它是一个指针数组,因此它比 source 本身偏移了两个指针位置(在64位架构中为16字节--您得到的值--,在32位架构中为8字节)

只需尝试使用所提议的声明,您将获得预期结果。

$ a.out
0x560bee9df060
0x560bee9df06a
$ _

(10个位置,正如预期的两个5 uint8_t元素数组)

但您可以自行检查。

如果您在source本身上使用sizeof运算符,则会得到一个数组的大小(在64位架构上为5个指针或40个字节),而正如您在问题中所述,它应该是单个指针的大小(在64位架构中为8个字节)。如果您使用建议的声明进行声明,则将获得8,因为这是预期的,而如果您执行sizeof *source(指向source的对象的大小),在这种情况下,您将获得5(五个uint8_t值的数组的大小)。试一试,看看吧。


关于在问题评论中讨论的对齐问题,这是最后的说明。
声明的对象是指针数组,而不是指向包含五个uint8_t的数组的指针,因此其地址(传递给printf的值确实对齐且有效(它确实是在堆栈中创建变量source的地址),并且在使用该地址时不会产生U.B.,因为数组内容本身未被使用,因此保持未初始化状态并不会引起U.B.)
如果正确声明为指向数组的指针,则打印其未初始化的值---唯一不可预测的是该值本身---但这不需要解引用指针或对齐(这不是问题,请参见下一个)。在这种情况下,应避免对未初始化指针变量进行指针算术运算,因为不能保证指针的目标值是有效地址,因此存在一些(通常不会有害的)U.B.(这是因为原始未初始化值本身也可能是无效地址,并且在仅处于未初始化状态时不应引起U.B.。 (void *)转换本身通常不会引起任何U.B.)
如果声明为指向数组的指针,则再次没有对齐问题,因为指针的基本类型是一个具有1(一个)对齐的uint8_t数组,因此任何地址都应该是可接受的。由于未解引用指针,因此它的行为类似于任何未初始化变量,因此不会引起U.B.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接