【问题标题】:How to initialize const in a struct in C (with malloc)如何在 C 中的结构中初始化 const(使用 malloc)
【发布时间】:2012-03-30 06:56:08
【问题描述】:

我试过了;

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 变量?

【问题讨论】:

  • 你必须抛弃 constness:*(int*)(&mydeneme->a)=15;
  • 调用malloc会导致未定义的行为,因为原型与库函数void *malloc(size_t);不兼容

标签: c struct initialization malloc constants


【解决方案1】:

您需要抛弃 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 标准参考解释为什么此代码不是某些 cmets 建议的未定义行为:

  • 此代码不违反 6.7.3/6,因为malloc 返回的空间不是“具有 const 限定类型的定义对象”。表达式mydeneme->a 不是一个对象,它是一个表达式。虽然它具有const-qualified 类型,但它表示一个未使用 const-qualified 类型定义的对象(实际上,根本没有使用任何类型定义)。

  • 写入malloc分配的空间永远不会违反严格的别名规则,因为每次写入都会更新有效类型 (6.5/6)。

(但是,从malloc 分配的空间中读取可能会违反严格的别名规则。

在 Chris 的代码示例中,第一个将整数值的有效类型设置为 int,第二个将有效类型设置为 const int,但是在这两种情况下都通过 @987654331 读取这些值@ 是正确的,因为严格别名规则(6.5/7 项目符号 2)允许通过与对象的有效类型同等或更多限定的表达式来读取对象。由于表达式mydeneme->a 具有const int 类型,因此可以使用它来读取有效类型intconst int 的对象。

【讨论】:

  • 不是 const-cast 访问未定义行为吗?
  • 否——如果所讨论的内存不是 const(例如从 malloc 返回的内存),则 const cast 是明确定义的。仅当您尝试通过强制转换修改 const 对象时,它才未定义。
  • 没有定义,它只是意味着程序员承担责任并且知道他(对不起女士们)在做什么。我的老师曾经说过,一个人可以很容易地射中他的腿思想。
  • 标准说:(6.7.3) 如果尝试通过使用具有非 const 限定类型的左值来修改使用 const 限定类型定义的对象,则行为未定义。在未定义的行为部分:尝试通过使用具有非 const 限定类型的左值 (6.7.3) 来修改使用 const 限定类型定义的对象。这将使这个例子展示 ub。
  • @self.: 但是这里没有用 const 限定类型定义的对象——该对象是用 malloc 分配的(它永远不是 const),然后转换为 const 类型。因此,强制转换只是丢弃了一个由先前(隐式)强制转换(来自 malloc 的返回值)应用的 const,并且对象本身不是 const。
【解决方案2】:

您是否尝试过这样做:

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;
}

我没有测试,但代码似乎是正确的

【讨论】:

  • 您无法通过“代码似乎正确”来真正证明某事是否是未定义的行为。在这种情况下,代码实际上是正确的,但唯一的判断方法是阅读标准所说的内容。您无法通过反复试验来证明这一点。
  • @Lundin:标准是基于这样的假设编写的,因为执行所有规定的最简单方法也将适应标准未规定的许多其他有用的东西,并且因为市场会驱动器对此类“流行扩展”的支持,标准没有必要详尽地列出它们。因此,唯一应该受到标准未能列出特定有用极端案例的影响是依赖该案例的程序是否可以被称为“严格符合”。
  • @supercat 但是人们也有一个习惯,在 SO 上发布明确违反列出的 UB 或标准的“应”/约束的代码,然后争辩说它“似乎正在工作”。每一个由软件引起的灾难性故障的历史事件都有“似乎在工作”的软件。
【解决方案3】:

有趣的是,我发现这种 C99 方式在 clang 中有效,但在 gcc 中无效

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

【讨论】:

  • @NathanTuggy 它看起来像是对我的回答
  • @MattMcNabb:也许是这样; “在 gcc 中不起作用”的提及让我失望。
  • 经过进一步检查...此代码不正确。 *pmydeneme 不是 可修改的左值 (C11 6.3.2/1),因为它是一个具有 const 限定成员的结构。赋值运算符 (6.5.16/2) 需要一个可修改的左值作为左操作数。如果 clang 接受它,那么它是 clang 中的一个错误。
  • 将以星号开头的行替换为struct deneme temp = {15,20}; memcpy(pmydeneme, &temp, sizeof temp);,代码应该已经定义了行为。
【解决方案4】:

我不同意 Christ Dodd 的回答,因为正如其他人所说,我认为他的解决方案根据标准给出了 Undefined Behaviour

为了以不调用未定义行为的方式“解决”const 限定符,我提出以下解决方案:

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

所以,我的代码是:

#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;
}

【讨论】:

  • 这与 Chris Dodd 发布的示例没有任何不同。指针的类型无关紧要,只有左值访问类型很重要。在这种情况下&amp;data指向的类型。
【解决方案5】:

为了扩展@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),它可能 指定一个对象;如果左值在评估时未指定对象,则行为未定义。当一个对象被称为具有特定类型时,该类型由用于指定该对象的左值指定。 可修改的左值是一个左值,它没有数组类型,没有不完整类型,没有 const 限定类型,如果它是结构体或联合体,则没有任何成员(包括, 递归地, 所有包含的聚合或联合的任何成员或元素) 具有 const 限定类型。

很明显,左值不仅有类型,还有限定符。如果它是 const 限定的或者是具有 const 限定成员的结构,则它不是可修改的左值。

继续讨论“严格别名”和有效类型的规则,C11 6.5/7:

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

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

这意味着 malloc 返回的分配块没有有效类型,直到通过左值写入访问(通过赋值或memcpy)将某些内容存储在该内存位置内。然后它获取在该写访问中使用的左值的有效类型。

值得注意的是,指向该内存位置的指针的类型完全不相关。它也可能是volatile bananas_t*,因为它不用于访问左值(至少现在还没有)。 只有用于左值访问的类型很重要。

现在这是变得模糊的地方:这个写访问是否通过可修改的左值完成可能很重要。上面的有效类型规则没有提到限定符,因此“严格别名规则”不关心对象是否合格(“type”可能别名为“qualified键入”,反之亦然)。

但在其他情况下,有效类型是否为只读可能很重要:最值得注意的是,如果我们稍后尝试对有效类型为 const 限定的对象进行非限定左值访问。 (C11 6.7.3/6 “如果尝试通过使用具有非 const 限定类型的左值来修改使用 const 限定类型定义的对象,则行为未定义。”)来自上面之前引用的部分左值,它有意义有效类型有一个限定符,即使标准没有明确提及。

因此,为了绝对确定,我们必须获得用于左值访问权限的类型。如果整个对象是只读的,那么应该使用本文顶部的第二个 sn-p。否则,如果它是读/写的(但可能有合格的成员),则应使用第一个 sn-p。那么无论您如何阅读标准,它都不会出错。

【讨论】:

  • 覆盖没有声明类型的对象的值的操作将删除它可能拥有的任何预先存在的有效类型。
  • @supercat 是的,但是一旦它有一个有效的类型,不同类型的后续左值写入很可能是一个严格的别名违规。
  • 从 6.5p6 开始,“如果通过非字符类型的左值将值存储到没有声明类型的对象中,则左值的类型将成为用于该访问和后续访问不修改存储值的对象。”书面规则导致不可行的极端情况,无论是 clang 还是 gcc 都不能正确处理,但是一个缺陷报告建议斜体文本被拒绝。一个正确编写的规则应该将“有效类型”的持久性基于将要使用的指针的存在......
  • ...以当前类型访问存储,这样如果例如唯一现存的指向对象的指针被转换为void* 并存储在池中,没有理由关心以前用于访问存储的类型,即使在下次写入之前读取了某些字节(例如,因为它们是部分编写的结构的一部分),但在任何情况下,存储可能持有的没有声明类型的任何 const 限定的有效类型都将被下一个修改它的操作擦除,就像任何其他有效类型。
  • @supercat 就是这样。 C 有许多严重的缺陷,这只是众多缺陷之一。与严格别名相关的所有各种问题和极端情况正是我们应该尽可能避免更改存储对象类型的原因。如果您采用安全的子集并禁止使用各种语言功能,则通常只能专业地使用 C。
【解决方案6】:

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

如果一个结构 s 类型为 struct S 且成员 m 类型为 T,则构造 s.foo 采用 struct S 类型的左值并从它派生一个 @987654328 类型的左值@。如果T 包含限定符,则该修饰符将影响由此产生的左值。

标准当然认识到代码可能采用不符合const-qualified 的左值,派生自具有const-qualified 的左值,派生自与原始值一样的左值的可能性不是,然后使用后一个左值来修改对象。尚不清楚的是,在结构成员上使用 const 修饰符是否会影响对象的底层存储类,或者它是否只会导致 const 修饰符应用于使用成员访问运算符形成的任何左值.我认为后一种解释更有意义,因为前者会导致许多模棱两可和不可行的极端情况,但我认为标准并没有明确说明应该适用哪种解释。由于在前一种解释下定义行为的所有情况在后一种解释下的定义都是相同的,我认为标准的作者没有理由不认为后一种解释是优越的,但他们可能希望保留这种可能性在某些情况下,在某些实施中,前一种解释可能会提供一些委员会没有预见到的优势实施。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-06-08
    • 2013-01-17
    • 1970-01-01
    • 1970-01-01
    • 2016-02-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多