更改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
一样。 所以这不是直接相关的。
意见如下: printf
和scanf
的整个规范是一个草率的混乱,低于标准其余部分的通常质量标准。 还有许多其他这样的例子,例如根据标准的确切措辞, printf("%lx", 1L);
是未定义的行为而printf("%lx", -1L);
不是未定义的行为。
实际上,实施方案决定了做什么,没有人愿意用驳船杆触及标准的措辞。 所以我想说这个问题没有真正正确的答案。
这是未定义的行为。
正如Michael Walz在评论中提到的那样,编译器接受它的原因是fscanf()
是可变参数,所以放宽了某些编译器检查 – 包括constness。
与所有未定义行为的实例一样,一个可能的结果是按照您的预期工作 – 使用您选择的编译器和库,但不一定与另一个。 这就是为什么测试不是识别未定义行为存在的可靠方法的原因 – 也允许通过任何测试。