更改const对象 – 没有警告? 还有,在哪种情况下它是UB?

为什么以下代码中没有警告?

int deserialize_students(const Student *dest, const int destCapacityMax) { FILE *ptr_file; int i=0; ptr_file =fopen("output.txt","r"); if (!ptr_file) return -1; if(destCapacityMax==0) return -2; while (!feof (ptr_file)) { fscanf (ptr_file, "%d", &dest[i].id); // UB? fscanf (ptr_file, "%s", dest[i].name); fscanf (ptr_file, "%d", &dest[i].gender); i++; if(i==destCapacityMax) return 0; } fclose(ptr_file); return 0; } 

这就是我所说的:

 Student students[5]; deserialize_students(students,5); 

另外我有以下问题:我做了什么未定义的行为? 注意:

  • 我认为传授students很好,因为函数需要const Student*而且我可以传递非const 。 对?
  • 但是当我在fscanf修改该对象时,我是否触发了UB? 或者它取决于students是否首先被声明为const(在该函数之外)?

代码中没有未定义的行为。

调用者传递的是一个可变对象。 因此可以直接或通过显式转换来修改它:

 int func(const int *p) { int *q = (int*)p; *q = 5; } 

只要传递给func()的对象是可变的,就可以了(可能是不明智的方式,但合法)。

但是如果传递的对象是const限定的,那么它将是未定义的行为。 所以在你的情况下,它并不是未定义的。

限定符const只是函数不应该修改dest的契约。 它与对象的实际可变性无关。 因此,修改const-qualified调用UB是否取决于传递给的对象是否具有任何此类限定符。

至于警告,海湾合作委员会(5.1.1)警告:

 int func(const int *p) { fscanf(stdin, "%d", p); } 

有:

 warning: writing into constant object (argument 3) [-Wformat=] 

可能VS不会识别fscanf()修改对象。 但是C标准只是说如果你修改一个const限定的对象它是未定义的:

(C11选秀,6.7.3,6种类型资格赛)

如果尝试通过使用具有非const限定类型的左值来修改使用const限定类型定义的对象,则行为未定义。

如果代码调用未定义的行为,则C标准不需要诊断。 一般情况下,如果您的代码导致UB并且编译器可能无法在所有原因中帮助您,则您自行处理。

恕我直言,这是正式的UB。

函数deserialize_students声明了一个const Student *dest参数。 从那里开始, dest[i].id是一个const int,而&dest[i].id是一个const int *

你没有得到任何警告,因为fscanf是一个可变函数,并且编译器无法控制常量(即使gcc使用它作为一种特殊情况)但是如果你使用了临时中间变量,你会得到一个错误:

 int id; fscanf (ptr_file, "%d", &id); dest[i].id = id; // here you get an error 

所以你将const指针传递给修改指针对象的函数( fscanf )和恕我直言,它足以将其限定为正式的UB。 可以想象一个编译器的实现,它将指向值的副本的指针传递给fscanf ,因为你承诺它是const。 或者那会传递一个指向students数组副本的指针,因为deserialize_students其参数声明为const。

有真正的风险吗? 恕我直言,因为当你将一个可修改的dest传递给函数时,正常的编译器实现只会传递原始地址,并且会将dest[i].id的地址传递给fscanf 。 因此,整个过程将以正确修改原始数组结束。 但正如Peter已经说过的那样, 就像所有未定义行为的实例一样,一个可能的结果就像你期望的那样工作,所以使用所有经过测试的编译器并不能保证不会出现未定义的行为。

注意:由于原始数组不是const,因此对象未定义为const,因此我不确定6.7.3,§6是否适用于此。 但6.7.3§9仍然说: 对于兼容的两种限定类型,两者都应具有兼容类型的相同限定版本,因此int * (fscanf要求)和const int * (实际传递)不是。

该标准尚不清楚。 从C11 7.21.6.2/12开始:

转换说明符及其含义是:

d匹配可选带符号的十进制整数,其格式与strtol函数的主题序列的预期格式相同,基本参数的值为10。 相应的参数应该是指向有符号整数的指针

现在,您可以争辩说,因为它没有说“指向非const有符号整数的指针”,它实际上意味着int *const int *都可以。 或者你可以说它们只是指向非const有符号整数的指针。

请注意,vararg传递(7.16)的定义明确表示使用va_arg(int *)来检索类型为const int *的参数是未定义的。 但是,未指定fscanf函数的行为就像使用了va_arg一样。 所以这不是直接相关的。


意见如下: printfscanf的整个规范是一个草率的混乱,低于标准其余部分的通常质量标准。 还有许多其他这样的例子,例如根据标准的确切措辞, printf("%lx", 1L); 是未定义的行为而printf("%lx", -1L); 不是未定义的行为。

实际上,实施方案决定了做什么,没有人愿意用驳船杆触及标准的措辞。 所以我想说这个问题没有真正正确的答案。

这是未定义的行为。

正如Michael Walz在评论中提到的那样,编译器接受它的原因是fscanf()是可变参数,所以放宽了某些编译器检查 – 包括constness。

与所有未定义行为的实例一样,一个可能的结果是按照您的预期工作 – 使用您选择的编译器和库,但不一定与另一个。 这就是为什么测试不是识别未定义行为存在的可靠方法的原因 – 也允许通过任何测试。