如何在C语言中使用malloc初始化结构体中的常量?

33

我已经尝试过;

void *malloc(unsigned int);
struct deneme {
    const int a = 15;
    const int b = 16;
};

int main(int argc, const char *argv[])
{
    struct deneme *mydeneme = malloc(sizeof(struct deneme));
    return 0;
}

这是编译器的错误:

gereksiz.c:3:17: error: expected ':', ',', ';', '}' or '__attribute__' before '=' token

还有这个;

void *malloc(unsigned int);
struct deneme {
    const int a;
    const int b;
};

int main(int argc, const char *argv[])
{
    struct deneme *mydeneme = malloc(sizeof(struct deneme));
    mydeneme->a = 15;
    mydeneme->b = 20;
    return 0;
}

这是编译器的错误:

gereksiz.c:10:5: error: assignment of read-only member 'a'
gereksiz.c:11:5: error: assignment of read-only member 'b'

两者都没有被编译。有没有办法在使用malloc分配内存时初始化结构体中的const变量?


你必须去除const属性:*(int*)(&mydeneme->a)=15; - n. m.
调用 malloc 导致未定义的行为,因为原型与库函数不兼容,而库函数是 void *malloc(size_t); - M.M
6个回答

46

你需要去掉const来初始化一个malloc分配的结构体的字段:

struct deneme *mydeneme = malloc(sizeof(struct deneme));
*(int *)&mydeneme->a = 15;
*(int *)&mydeneme->b = 20;

或者,您可以创建一个已初始化的结构体版本并对其进行memcpy:

struct deneme deneme_init = { 15, 20 };
struct deneme *mydeneme = malloc(sizeof(struct deneme));
memcpy(mydeneme, &deneme_init, sizeof(struct deneme));
如果您会频繁使用 deneme_init,则可以将其声明为静态和/或全局变量(这样只需构建一次)。
根据 C11 标准引用, 解释了此代码为什么不像某些评论所暗示的那样是未定义行为:
- 此代码不违反 6.7.3/6,因为 malloc 返回的空间不是“具有 const 限定类型的对象”。表达式 mydeneme->a 不是对象,它是一个表达式。尽管它具有 const 限定类型,但它表示的对象没有被定义为具有 const 限定类型(实际上,根本没有定义为任何类型)。 - 通过写入分配给 malloc 的空间,永远不会违反严格的别名规则,因为每次写入都会更新其“有效类型”(6.5/6)。 - 然而,从由 malloc 分配的空间中读取时可能会违反严格的别名规则。
在 Chris 的代码示例中,第一个示例将整数值的有效类型设置为 int,第二个示例将有效类型设置为 const int,但在两种情况下,通过 *mydeneme 读取这些值都是正确的,因为严格别名规则(6.5/7 第2款)允许通过一个与对象的有效类型相等或更严格限定的表达式来读取对象。由于表达式 mydeneme->a 具有类型 const int,因此可以用它来读取有效类型为 intconst int 的对象。

9
const-cast 访问不是未定义行为吗? - Kerrek SB
13
如果涉及的内存不是const(例如从malloc返回的内存),则const cast是定义良好的。只有在尝试通过转换修改const对象时,才会导致未定义情况。 - Chris Dodd
1
没有定义,这只是意味着程序员要承担责任并知道自己在做什么(对不起女士们)。 我的老师曾经说过,一个人很容易自己给自己惹麻烦。 - Tomas Pruzina
1
标准规定:(6.7.3)如果尝试通过使用具有非const限定类型的lvalue修改使用const限定类型定义的对象,则行为是未定义的。在未定义行为部分中:尝试通过使用具有非const限定类型的lvalue修改使用const限定类型定义的对象(6.7.3)。这将使此示例表现出ub。 - this
@Florian:我的意思是像上面第二个例子中的deneme_init的初始化。这是在具有const字段的本地或全局(非动态)对象中初始化const字段的唯一合法方式。 - Chris Dodd
显示剩余11条评论

11

你尝试过像这样做吗:

int main(int argc, const char *argv[])
{
    struct deneme mydeneme = { 15, 20 };
    struct deneme *pmydeneme = malloc(sizeof(struct deneme));
    memcpy(pmydeneme, &mydeneme , sizeof(mydeneme));
    return 0;
}

我还没有测试过,但是代码看起来是正确的。


1
你无法通过“代码看起来正确”的方式证明某个行为是否是未定义的。在这种情况下,代码确实是正确的,但唯一的方法是阅读标准规范。你无法通过试错来证明它。 - Lundin
@Lundin:标准是基于这样的假设编写的,即因为实现所有强制要求的最简单方法也可以容纳许多其他有用但不受标准强制要求的东西,并且因为市场将推动对这些“流行扩展”的支持,所以没有必要让标准详尽列出它们。因此,唯一受标准未列出特定有用边角情况影响的应该是依赖于该情况的程序是否可以被称为“严格符合”。 - supercat
@supercat 但是人们也有一个习惯,在 SO 上发布代码,明确违反了列出的 UB 或标准的“应该”/约束条件,然后辩称它“似乎运行良好”。每个由软件引起的灾难性失败的历史事件,都有“似乎运行良好”的软件。 - Lundin

2

有趣的是我发现这种 C99 的方法在 clang 中可行但在 gcc 中不行。

int main(int argc, const char *argv[])
{
    struct deneme *pmydeneme = malloc(sizeof(struct deneme));
    *pmydeneme = (struct deneme) {15, 20};
    return 0;
}

2
@NathanTuggy 看起来对我来说是一个答案。 - M.M
@MattMcNabb:也许是这样;“在gcc中无法工作”的提醒让我有些困惑。 - Nathan Tuggy
5
经过进一步检查,发现这段代码是错误的。 *pmydeneme不是一个可修改的左值(C11 6.3.2/1),因为它是一个具有const限定成员的结构体。赋值运算符(6.5.16/2)要求左操作数必须是可修改的左值。如果clang接受它,那么这是clang的一个错误。 - M.M
将以星号开头的那一行替换为 struct deneme temp = {15,20}; memcpy(pmydeneme, &temp, sizeof temp);,代码应该具有定义良好的行为。 - supercat

2

为了进一步解释@Chris Dodd的答案,我已经阅读了标准中“语言律师”的详细信息,看起来这段代码是定义良好的:

struct deneme deneme_init = { 15, 20 };
struct deneme *mydeneme = malloc(sizeof(struct deneme));
memcpy(mydeneme, &deneme_init, sizeof(struct deneme));

或者,要动态创建一个完整的const限定的结构体对象:

const struct deneme deneme_init = { 15, 20 };
struct deneme *mydeneme = malloc(sizeof(struct deneme));
memcpy(mydeneme, &deneme_init, sizeof(struct deneme));

const struct deneme *read_only = mydeneme; 

原理:

当涉及到所谓的lvalue时,首先需要确定它是否具有类型,如果具有类型,那么该类型是否带有限定符。这在C11 6.3.2.1/1中定义:

lvalue是一个表达式(具有对象类型而不是void),可以潜在地指代对象; 如果一个lvalue在评估时不指代任何对象,则行为未定义。当说一个对象具有特定类型时,类型是由用于指代该对象的lvalue指定的。可修改的lvalue是指没有数组类型、不具有不完整类型和不具有const限定类型的lvalue,如果它是结构体或联合体,则没有任何成员(包括所有包含的聚合体或联合体的所有成员或元素)都具有const限定类型。

因此,显然lvalue不仅具有类型,还具有限定符。如果它具有const限定符或者是具有const限定成员的结构体,则不是可修改的lvalue。

接下来看"严格别名"和有效类型的规则,C11 6.5/7:

访问存储值的对象的有效类型是对象的声明类型(如果有的话)。87)如果通过具有非字符类型的类型的lvalue将值存储到没有声明类型的对象中,则lvalue的类型成为该访问和不修改存储值的后续访问的对象的有效类型。如果使用memcpymemmove将值复制到没有声明类型的对象中,或者将其作为字符类型的数组复制,则如果它有一个,则从值复制的对象是修改后对象的有效类型。对于访问没有声明类型的对象的所有其他访问,所使用的lvalue的有效类型只是该对象的类型。

  1. 分配的对象没有声明类型。

这意味着由malloc返回的分配块在通过lvalue写入访问(通过赋值或memcpy)存储在该内存位置中之前没有有效类型。然后,它获得该写访问中使用的lvalue的有效类型。

值得注意的是,指向该内存位置的指针的类型完全无关紧要。它可能与volatile bananas_t*一样,因为它不用于访问lvalue(至少目前还没有)。仅使用lvalue访问的类型才重要。

现在问题变得有些模糊:如果这个写入访问是通过可修改的左值完成的,那么可能会有影响。有效类型规则不涉及限定符,而“严格别名规则”本身也不关心对象是否有限定符(“类型”可以与“限定类型”别名,反之亦然)。
但是,在其他情况下,如果有效类型是只读的,则可能很重要:特别是如果我们稍后尝试对具有const限定的有效类型的对象进行非限定左值访问。从上面引用的左值部分,有效类型具有限定符是有意义的,即使标准没有明确提到。
因此,为了绝对确定,我们必须正确获取用于左值访问的类型。如果整个对象都应该是只读的,则应该使用本帖子顶部的第二个代码片段。否则,如果它是读/写的(但可能具有限定成员),则应该使用第一个代码片段。然后无论您如何阅读标准,都不会出错。

在没有声明类型的情况下覆盖对象值的操作将擦除它可能具有的任何预先存在的有效类型。 - supercat
@supercat 是的,但一旦它具有有效类型,不同类型的后续lvalue写入可能会导致严格别名违规。 - Lundin
访问存储作为它们当前的类型,这样,例如,如果仅存在指向对象的指针被转换为void*并存储在池中,则无需关心以前已使用哪些类型来访问存储,即使在下次写入之前读取了一些字节(例如,因为它们是部分写入的结构的一部分),但在任何情况下,任何可能由未声明类型的存储保持的const限定的Effective Type都将被下一个操作擦除,就像任何其他Effective Type一样。 - supercat
Clang和gcc在禁用优化的情况下可以正常工作。启用优化会使它们在标准完全定义的情况下也变得不可靠。虽然有时需要解决某些编译器的错误或限制,但如果clang和gcc的制造商支持一种模式,该模式可以进行窥孔优化和寄存器缓存优化,但在“棘手”结构周围刷新寄存器并避免不合理的假设,那么编程社区将受益匪浅,而不是只有-O0这种可靠的模式。 - supercat
也许最好的方法是使用宏,在clang或gcc上被定义为内存破坏的asm指令,但在其他编译器上可能会扩展为其他内容或为空,然后在任何clang或gcc可能会搞砸事情的地方使用该指令。没有被故意恶意的编译器应该将内存破坏视为防止涉及相反侧面访问的别名推断的屏障,试图保护代码免受���意恶意编译器的攻击将是愚蠢的行为。 - supercat
显示剩余3条评论

0

标准使用const关键字作为左值限定符和存储类之间的奇怪混合,但并没有明确说明哪种含义适用于结构体成员。

如果有一个类型为struct S的结构体s,其中成员m的类型为T,那么构造s.foo将获取一个类型为struct S的左值,并从中派生出一个类型为T的左值。如果T包含限定符,则该修饰符将影响所产生的左值。

标准当然认识到代码可能会获取一个未被const修饰的lvalue,从而得到一个被const修饰的lvalue,并且还可以从这个被修改了的lvalue派生出原来的不被修饰的lvalue。不清楚的是,在结构体成员上使用const关键字是否会影响对象的基础存储类,或者它是否只会导致在使用成员访问运算符时应用const修饰符。我认为后一种解释更有意义,因为前者会导致许多模糊和无法工作的特殊情况,但我认为标准并没有明确说明应该采用哪种解释。由于在前一种解释下行为已经定义的所有情况都将在后一种解释下被定义为相同,我看不出标准的作者不将后一种解释视为更优秀的理由,但他们可能希望在某些情况下,在某些实现中,前一种解释可能会提供委员会未曾预见的某些优势。

0

我不同意Christ Dodd的答案,因为我认为他的解决方案根据标准会产生未定义行为,正如其他人所说。

为了“解决”const限定符而不会引起未定义行为,我提出以下解决方案:

  1. 定义一个void*变量,并使用malloc()调用进行初始化。
  2. 定义所需类型的对象,即struct deneme,并以某种方式进行初始化,使const限定符不会报错(即在声明行本身中进行初始化)。
  3. 使用memcpy()将struct deneme对象的位复制到void*对象中。
  4. 声明指向struct deneme对象的指针,并将其初始化为先前转换为(struct deneme*)的(void*)变量。

因此,我的代码将是:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
struct deneme {
    const int a;
    const int b;
};
struct deneme* deneme_init(struct deneme data) {
    void *x = malloc(sizeof(struct deneme));
    memcpy(x, &data, sizeof(struct deneme));
    return (struct deneme*) x;
}
int main(void) {
    struct deneme *obj = deneme_init((struct deneme) { 15, 20, } );
    printf("obj->a: %d, obj->b: %d.\n", obj->a, obj->b);
    return 0;
}

1
这与Chris Dodd所发布的示例没有任何区别。指针的类型毫不重要,只有lvalue访问类型才最重要。在这种情况下,它指向&data的类型。 - Lundin
指针指向的动态存储数据并不会因为指针的类型转换(无论是隐式还是显式)而自动获得新的类型。有效类型是通过赋值或memcpy写入对象时设置的。 - tstanisl

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