在读取值时,未定义行为(UB)的一个明显示例是:
int a;
printf("%dn", a);
下面的例子怎么样?
int i = i; // `i` is not initialized when we are reading it by assigning it to itself.
int x; x = x; // Is this the same as above?
int y; int z = y;
上面的三个例子都是UB吗,还是有例外?
这三行中的每一行都会触发未定义的行为。解释这一点的C标准的关键部分是关于转换的第6.3.2.1p2节:
除非它是
sizeof
运算符的操作数_Alignof
运算符、一元&
运算符、++
运算符、--
运算符或.
运算符的左操作数或赋值运算符,一个没有数组类型的左值转换为存储在指定对象中的值(不再是左值);这被称为左值转换。如果左值具有限定类型,则该值具有左值类型的非限定版本;另外,如果左值具有原子类型,则该值具有非原子版本左值类型;否则,该值具有左值的类型。如果左值具有不完整类型,并且没有数组类型,行为未定义如果左值指定一个具有自动存储持续时间的对象,该对象可以已使用register
存储类声明(从未使用获取地址),并且该对象未初始化(未声明具有初始值设定项,但尚未对其进行赋值在使用之前执行),行为是未定义的
在这三种情况中的每一种情况下,一个未初始化的变量都被用作赋值或初始化(为此目的,它相当于赋值)的右侧,并进行左值到右值的转换。粗体部分适用于此处,因为有问题的对象尚未初始化。
这也适用于int i = i;
的情况,因为右侧的左值尚未初始化。
在一个相关的问题中,有人争论int i = i;
的右侧是UB,因为i
的寿命尚未开始。然而,事实并非如此。来自第6.2.4节p5和p6:
5一个对象的标识符是在没有链接和没有存储类说明符
static
的情况下声明的,它具有自动存储持续时间,一些复合文字也是如此。的结果尝试使用自动存储间接访问对象对象所在线程以外的线程的持续时间关联的是定义的实现。6 对于这样一个没有可变长度数组类型的对象,其生存期从进入块开始延长与之关联,直到该块的执行以任何方式(进入封闭的块或调用函数挂起但不结束当前块的执行。)如果块是递归输入的,对象的一个新实例是每次创建。对象的初始值为不确定的如果为对象,每次声明或复合时执行在块的执行中达到文字;否则每次到达时,该值都会变得不确定
因此,在这种情况下,i
的生存期在遇到声明之前开始。所以int i = i;
仍然是未定义的行为,但不是因为这个原因。
然而,6.3.2.1p2中粗体部分确实为使用未初始化的变量而不是打开了大门,该变量是未定义的行为,也就是说,如果有问题的变量的地址被占用了。例如:
int a;
printf("%pn", (void *)&a);
printf("%dn", a);
在这种情况下,如果:,则不是未定义的行为
- 实现没有给定类型的陷阱表示,OR
- 为
a
选择的值恰好不是陷阱表示
在这种情况下,a
的值未指定。特别是,在本例中,GCC和Microsoft Visual C++(MSVC)将出现这种情况,因为这些实现没有整数类型的陷阱表示。
使用未初始化的自动存储持续时间对象会调用UB。
未初始化的静态存储持续时间对象的使用被定义为它们被初始化为0s
int a;
int foo(void)
{
static int b;
int c;
int d = d; //UB
static int e = e; //OK
printf("%dn", a); //OK
printf("%dn", b); //OK
printf("%dn", c); //UB
}
如果对某个类型的对象的操作可能会在该类型具有陷阱表示的平台上产生不可预测的后果,但对没有陷阱表示的类型至少有一些可预测的行为,则标准将通过将所有内容放入"捕获"类别来避免区分定义行为的平台;未定义的行为";。
关于未初始化或部分初始化对象的行为,我认为对于哪些角落的情况必须像用未指定的位模式初始化对象一样处理,以及哪些情况不需要以这种方式处理,从来没有达成共识。
例如,给定以下内容:
struct ztstr15 { char dat[16]; } x,y;
void test(void)
{
struct zstr15 hey;
strcpy(hey.dat, "Hey");
x=hey;
y=hey;
}
根据x
和y
的使用方式,至少有四种方法可能对实现过程有用——上面的代码:
如果试图复制任何未完全初始化的自动持续时间对象,则发出Squawk。在必须避免泄露机密信息的情况下,这可能非常有用。
零填充
hey
的所有未使用部分。这将防止堆栈上的机密信息泄露,但如果数据不是零填充的,则不会标记可能导致此类泄露的代码。确保
x
和y
的所有部分都是相同的,而不考虑是否写入了hey
的相应成员。写入
x
和y
的前四个字节,以与hey
的字节相匹配,但保留一些或所有剩余部分,保留它们在调用test()
之前所保留的内容。
我不认为该标准旨在判断其中一些方法是比其他方法更好还是更差,但在考虑选项#3的情况下,以定义test()
行为的方式编写该标准会很尴尬。只有当程序员能够在客户端代码不关心x.dat[4..15]
和y.dat[4..15]
内容的情况下安全地编写如上所述的代码时,#3所促进的优化才会有用。如果保证该函数行为的唯一方法是写入hey
的所有部分,包括那些值与程序行为无关的部分,那么这将使#3方法所能提供的任何优化优势失效。