void* 与char* 具有相同的表示和内存对齐是什么意思?

gzszwxb4  于 2023-01-08  发布在  其他
关注(0)|答案(3)|浏览(361)

我一直在阅读一些关于void*类型指针的文章,并从标准中找到了这一要求。
6.2.5.27:
指向void的指针应与指向字符类型的指针具有相同的表示和对齐要求。39)类似地,指向兼容类型的合格或不合格版本的指针应具有相同的表示和对齐要求。
我看到标准并不保证所有指针类型都有相同的长度,所以这里的底线是void*指针与char*有相同的长度和对齐规则,对吗?
我不明白的是脚注39),它说
相同的表示和对齐要求意味着函数的参数、函数的返回值以及联合体的成员的可互换性。

    • 我的问题是:**

1."可互换性"是什么意思?它是说函数void* Func(void*)的参数和返回值都可以是char*吗?
1.如果是,它是由编译器进行的隐式转换吗?
1.工会的成员是什么?我真的不明白这是什么意思。谁能给我一个简单的例子?

pbossiut

pbossiut1#

在C语言中,任何数据指针都可以传递给期望void *的函数,并且void *可以存储为任何指针类型。在void *和其他指针类型之间存在隐式转换。但这并不意味着这种转换是无害的。在一些void *int *具有不同表示的体系结构中,从int *转换到void *然后转换回int *被指定为产生相同的指针值,但是相反的情况不成立:将void *转换成int *,再转换回void *,可能产生不同的值,尤其是如果void *不是通过转换int *而获得的。
可互换性意味着此隐式转换不会更改指针的表示形式。转换可以成功地以两种方式操作:将字符指针转换到void *并反向转换产生相同的指针,反之亦然。
下面是一个例子:

#include <assert.h>
#include <stdio.h>
#include <string.h>

int main() {
    char *s = "abc";
    char *s1;
    void *p;
    void *p1;

    assert(sizeof(p) == sizeof(s));
    memcpy(&p, &s, sizeof(p));
    p1 = s;
    assert(p == p1);
    memcpy(&s1, &p1, sizeof(s1));
    assert(s == s1);
    return 0;
}

但是注意这并不意味着!memcmp(&p1, &s, sizeof(p1)),因为指针可以有填充位,也不能违反严格的别名规则,强制转换void *

  • float f = 1.0; unsigned int i = *(int *)(void *)&f;错误。
  • float f = 1.0; unsigned int i; memcpy(&i, &f, sizeof(i));如果sizeof(int) == sizeof(float)正确,但可能产生陷阱值。
uxhixvfz

uxhixvfz2#

1.“可互换性”是什么意思?它是说函数void* Func(void*)的参数和返回值都可以是char*吗?
是的,它是这么说的,但它是与标准的规范性文本相冲突的非规范性文本,让我们讨论问题2,然后回到这个问题。
1.如果是,它是由编译器进行的隐式转换吗?
否,不适用于本说明打算述及的情况。
如果存在void *Func(void *);的可见声明,并且您执行:

char *p = something;
char *q = Func(p);

然后参数p被转换为void *,返回值被转换为char *,但是这些转换作为函数调用和赋值的正常操作的一部分发生;它们与具有相同表示或可互换的类型无关。例如,如果您执行类似上面的代码,但使用int *而不是char *,即使X1 M8 N1 X和X1 M9 N1 X不具有相同的表示并且不可互换,转换也将在X1 M8 N1 X和X1 M9 N1 X之间发生。进行参数转换是因为编译器知道Func的参数类型,所以它按照函数调用规则的要求执行转换。并且由于编译器知道赋值目标的类型,所以它按照赋值规则的要求执行转换,所以进行赋值转换。
但是,假设我们有这样的代码:

char *Func(char *);
char *p = something;
char *q = Func(p);

Func实际上在其库源代码中定义为void *Func(void *);。则C 2018 6.2.5 281中的规则适用。在调用代码中,编译器被告知参数和返回类型为char *,因此在这两种情况下均不执行转换。当传递char *参数时,编译器准确地传递表示char *的字节。在接收函数中,代码期望void *。由于表示char *的字节与表示void *的字节完全相同,对于所表示的地址具有相同的含义,这起作用:函数接收它期望接收的字节,并具有预期的含义。类似地,当函数返回void *的字节,并且调用代码将这些字节解释为char *时,它工作,因为字节是相同的,具有相同的含义。
回到问题1,在这个例子中,Func是用类型char *Func(char *)调用的,但是它是用类型void *Func(void *)定义的,这违反了C标准的规范部分。C 2018 6.5.2.2 6说:
如果函数是用包含原型的类型定义的,并且...升级后的实参类型与形参类型不兼容,则该行为是未定义的。
char *void *不兼容,因此此规则未定义行为。但是,如果调用代码在一个翻译单元中,而被调用代码在另一个翻译单元中,并且没有有关调用代码或被调用函数的信息(特别是没有类型信息)除了将名称链接到函数之外在翻译单元之间传递,那么C实现就不可能区分我们的示例代码和使用与其定义兼容的类型调用函数的代码。特别是,char *具有与void *相同的表示的事实意味着编译调用代码的结果必须相同,无论它使用char *Func(char *)还是void *Func(void *)(注意翻译单元之间不传递类型信息),并且这意味着无论函数定义是使用char *还是void *定义的,编译函数定义的结果必须相同。换句话说,C标准的一条规则说行为没有定义,但是在这种情况下,编译器逻辑上不可能编译与具有定义行为的代码不同的示例代码。
我猜想标准中的这条注解可能是委员会的结果,或者至少是委员会中的一个或多个成员,他们想说,至少在某种意义上,char *可以用来代替void *,反之亦然。但委员会没有时间、动机或其他机会起草正式的措辞,使之成为《公约》的规范性部分。标准的,所以它满足于把它做成一张纸条。
1.工会的成员是什么?我真的不明白这是什么意思。谁能给予我一个简单的例子?
考虑一下这种结合:

union foo
{
    void  *v;
    char  *c;
    float *f;
} u;

当我们写入一个联合体成员时,如u.v = &a;,并从另一个联合体成员读取,如char *p = u.c;,联合体中的字节在新类型中被重新解释(C 2018 6.5.2.3 3和注解99).由于void *char *具有相同的表示,因此重新解释必须产生相同的值.因此,我们保证:

char a;
u.v = &a;
printf("%d\n", u.c == &a);

打印“1”。另一方面,我们不能保证此代码:

float f;
u.v = &f;
printf("%d\n", u.f == &f);

在此代码中,当&f转换为void *时,void *的表示可能与float *不同,所以表示&f的字节可能与表示(void *) &f的字节不同,后者是存储在u.v中的字节。当这些字节作为u.f读取并重新解释为float *时,它们可能表示不同的值,因此比较结果可能不为true。

脚注

1 The question cites “6.2.5.27,” but the quoted passage is found in clause 6.2.5, paragraph 28, of the official 2018 C standard. The note cited as note 39 is found as note 49.

9gm1akwq

9gm1akwq3#

指针只是内存中的一个地址,你可以认为内存是一个字节的连续区域,它非常大(例如,在一个32位的进程上,它将是4GB,但通常进程不能使用整个取决于系统)。
这意味着指针的值实际上是一个整数,表示内存中字节的从零开始的索引(例如,值为0的指针指向内存中的第一个字节,但实际上您将无法取消引用此地址,因为它是空指针)。
当你取消一个指针的引用时,它所做的就是阅读/写那个地址。读/写的大小取决于指针的类型。如果一个指针是int,在那个系统上它的大小是32位,也就是4字节;它将从该地址开始读/写4个字节。对齐意味着值如何存储在内存中。假设存储在内存中的值需要16字节对齐,这意味着其起始地址必须乘以16。
我在这里解释的只是一个高级的指针,这应该足够开始了。在现实中,它有很多事情与它有关,如内存保护,分页等。

相关问题