在 C90 中使用部分初始化的结构时的未定义行为
Undefined behavior when working with partially initialized struct in C90
让我们考虑以下代码:
struct M {
unsigned char a;
unsigned char b;
};
void pass_by_value(struct M);
int main() {
struct M m;
m.a = 0;
pass_by_value(m);
return 0;
}
在函数pass_by_value
中m.b
在使用前进行了初始化。
但是,由于 m
是按值传递的,因此编译器已经将其复制到堆栈中。
这里没有变量有存储classregister
。 a
和 b
属于 unsigned char
.
类型
在 C90 中是否必须将其视为 UB? (请注意:我特意要C90)
这个问题与非常相似,但实际上是相反的。
C 1990 标准(和 C 199 标准)不包含 C 2011 中首次出现的句子,该句子使使用某些未初始化值的行为未定义。
C 2011 6.3.2.1 2 包括:
… If the lvalue has an incomplete type and does not have array type, the behavior is undefined. If the lvalue designates an object of automatic storage duration that could have been declared with the register
storage class (never had its address taken), and that object is uninitialized (not declared with an initializer and no assignment to it has been performed prior to use), the behavior is undefined.
C 1990 中第 6.2.2.1 条第二段的整个对应段落是:
Except when it is the operand of the sizeof
operator, the unary &
operator, the ++
operator, the --
operator, or the left operand of the .
operator or an assignment operator, an lvalue that does not have array type is converted to the value stored in the designated object (and is no longer an lvalue). If the lvalue has qualified type, the value has the unqualified version of the type of the lvalue; otherwise, the value has the type of the lvalue. If the lvalue has an incomplete type and does not have array type, the behavior is undefined.
因此,问题中代码的行为似乎已定义,因为它传递存储在结构中的值。
在标准中没有明确声明的情况下,惯例有助于指导解释。不初始化结构的所有成员以期望结构表示有用数据是完全正常的,因此如果至少有一个成员被初始化,则必须定义将结构用作值的行为。 在其答案之一中包含对标准 struct tm
的提及(来自 C 缺陷报告)。 struct tm
可用于通过填写所有日期字段(年、月、日)和可能的时间字段(小时、分钟、秒,甚至夏令时指示)来表示特定日期,但留下星期几和年份字段未初始化。
在 3.16 中定义未定义的行为时,1990 年的标准确实说它是“使用时的行为……不确定值的对象,本国际标准对此没有强加任何要求。”并且 6.5.7 说“......如果一个具有自动存储持续时间的对象没有明确初始化,它的值是不确定的......”但是,一个具有自动存储持续时间的结构,其中一个成员,而不是另一个,已经被初始化既没有完全初始化也没有初始化。考虑到结构的预期用途,我会说我们不应该考虑使用部分初始化的结构的值被 3.16 定义为未定义。
在 C90 下,如果对象持有不确定值,则每一位都可以独立为零或一,无论它们组合起来是否代表对象类型的有效位模式。如果一个实现指定了尝试读取一个对象可能持有的 2ⁿ 个可能位模式中的每一个的行为,那么读取具有不确定值的对象的行为将等同于读取任意选择的位模式的值。如果有任何位模式的实现未指定尝试读取的效果,那么尝试读取可能包含此类位模式的对象的效果同样未指定。
在某些情况下,可以通过更松散地指定未初始化对象的行为来提高代码生成效率,这种方式在其他方面与指定的顺序程序执行不一致,但仍能满足程序要求。例如,给定如下内容:
struct foo { short dat[16]; } x,y,z;
void test1(int a, int b, int c, int d)
{
struct foo temp;
temp.dat[a] = 1;
temp.dat[b] = 2;
temp.dat[c] = 3;
temp.dat[d] = 4;
x=temp;
y=temp;
}
void test2(int a, int b, int c, int d)
{
test1(a,b,c,d);
z=x;
}
如果客户端代码只关心写入的temp
值对应的x
和y
值,效率可能会提高,同时仍然满足要求,如果代码被重写为:
void test1(int a, int b, int c, int d)
{
x.dat[a] = 1;
y.dat[a] = 1;
x.dat[b] = 2;
y.dat[b] = 1;
x.dat[c] = 3;
y.dat[c] = 1;
x.dat[d] = 4;
y.dat[d] = 1;
}
原始函数 test1
不执行任何初始化 temp
的事实表明它不会关心任何个人尝试读取它所产生的结果。另一方面,test2 代码中的任何 都不会暗示客户端代码不关心 x
的所有成员是否与 [=] 的相应值具有相同的值13=]。因此,这样的推断在那里更有可能是危险的。
C 标准没有尝试定义在优化可能产生程序行为的情况下的行为,这些行为虽然有用,但与非优化代码的顺序处理不一致。相反,优化绝不能影响任何已定义行为的原则被认为意味着标准必须将其行为将明显受到优化影响的所有行为描述为未定义,让实施者自行决定应该或不应该定义行为的哪些方面的问题在什么情况下。具有讽刺意味的是,标准在这种行为方面的松懈将允许在人为场景之外更有效地生成代码的唯一一次是在实现将行为视为至少松散定义的情况下,并且程序员能够利用它。如果程序员必须显式初始化 temp
的所有元素以避免让编译器以完全无意义的方式运行,那将消除优化对 x
和 [=13 的未使用元素的不必要写入的任何可能性=].
让我们考虑以下代码:
struct M {
unsigned char a;
unsigned char b;
};
void pass_by_value(struct M);
int main() {
struct M m;
m.a = 0;
pass_by_value(m);
return 0;
}
在函数pass_by_value
中m.b
在使用前进行了初始化。
但是,由于 m
是按值传递的,因此编译器已经将其复制到堆栈中。
这里没有变量有存储classregister
。 a
和 b
属于 unsigned char
.
在 C90 中是否必须将其视为 UB? (请注意:我特意要C90)
这个问题与
C 1990 标准(和 C 199 标准)不包含 C 2011 中首次出现的句子,该句子使使用某些未初始化值的行为未定义。
C 2011 6.3.2.1 2 包括:
… If the lvalue has an incomplete type and does not have array type, the behavior is undefined. If the lvalue designates an object of automatic storage duration that could have been declared with the
register
storage class (never had its address taken), and that object is uninitialized (not declared with an initializer and no assignment to it has been performed prior to use), the behavior is undefined.
C 1990 中第 6.2.2.1 条第二段的整个对应段落是:
Except when it is the operand of the
sizeof
operator, the unary&
operator, the++
operator, the--
operator, or the left operand of the.
operator or an assignment operator, an lvalue that does not have array type is converted to the value stored in the designated object (and is no longer an lvalue). If the lvalue has qualified type, the value has the unqualified version of the type of the lvalue; otherwise, the value has the type of the lvalue. If the lvalue has an incomplete type and does not have array type, the behavior is undefined.
因此,问题中代码的行为似乎已定义,因为它传递存储在结构中的值。
在标准中没有明确声明的情况下,惯例有助于指导解释。不初始化结构的所有成员以期望结构表示有用数据是完全正常的,因此如果至少有一个成员被初始化,则必须定义将结构用作值的行为。 struct tm
的提及(来自 C 缺陷报告)。 struct tm
可用于通过填写所有日期字段(年、月、日)和可能的时间字段(小时、分钟、秒,甚至夏令时指示)来表示特定日期,但留下星期几和年份字段未初始化。
在 3.16 中定义未定义的行为时,1990 年的标准确实说它是“使用时的行为……不确定值的对象,本国际标准对此没有强加任何要求。”并且 6.5.7 说“......如果一个具有自动存储持续时间的对象没有明确初始化,它的值是不确定的......”但是,一个具有自动存储持续时间的结构,其中一个成员,而不是另一个,已经被初始化既没有完全初始化也没有初始化。考虑到结构的预期用途,我会说我们不应该考虑使用部分初始化的结构的值被 3.16 定义为未定义。
在 C90 下,如果对象持有不确定值,则每一位都可以独立为零或一,无论它们组合起来是否代表对象类型的有效位模式。如果一个实现指定了尝试读取一个对象可能持有的 2ⁿ 个可能位模式中的每一个的行为,那么读取具有不确定值的对象的行为将等同于读取任意选择的位模式的值。如果有任何位模式的实现未指定尝试读取的效果,那么尝试读取可能包含此类位模式的对象的效果同样未指定。
在某些情况下,可以通过更松散地指定未初始化对象的行为来提高代码生成效率,这种方式在其他方面与指定的顺序程序执行不一致,但仍能满足程序要求。例如,给定如下内容:
struct foo { short dat[16]; } x,y,z;
void test1(int a, int b, int c, int d)
{
struct foo temp;
temp.dat[a] = 1;
temp.dat[b] = 2;
temp.dat[c] = 3;
temp.dat[d] = 4;
x=temp;
y=temp;
}
void test2(int a, int b, int c, int d)
{
test1(a,b,c,d);
z=x;
}
如果客户端代码只关心写入的temp
值对应的x
和y
值,效率可能会提高,同时仍然满足要求,如果代码被重写为:
void test1(int a, int b, int c, int d)
{
x.dat[a] = 1;
y.dat[a] = 1;
x.dat[b] = 2;
y.dat[b] = 1;
x.dat[c] = 3;
y.dat[c] = 1;
x.dat[d] = 4;
y.dat[d] = 1;
}
原始函数 test1
不执行任何初始化 temp
的事实表明它不会关心任何个人尝试读取它所产生的结果。另一方面,test2 代码中的任何 都不会暗示客户端代码不关心 x
的所有成员是否与 [=] 的相应值具有相同的值13=]。因此,这样的推断在那里更有可能是危险的。
C 标准没有尝试定义在优化可能产生程序行为的情况下的行为,这些行为虽然有用,但与非优化代码的顺序处理不一致。相反,优化绝不能影响任何已定义行为的原则被认为意味着标准必须将其行为将明显受到优化影响的所有行为描述为未定义,让实施者自行决定应该或不应该定义行为的哪些方面的问题在什么情况下。具有讽刺意味的是,标准在这种行为方面的松懈将允许在人为场景之外更有效地生成代码的唯一一次是在实现将行为视为至少松散定义的情况下,并且程序员能够利用它。如果程序员必须显式初始化 temp
的所有元素以避免让编译器以完全无意义的方式运行,那将消除优化对 x
和 [=13 的未使用元素的不必要写入的任何可能性=].