本文的目的是让每个人(尤其是C程序员)说:“我不知道C”。
我想证明C的暗角比看起来更近,甚至琐碎的代码行也可能包含未定义的行为。
本文按一系列问题和答案进行组织。所有示例都是源代码的单独文件。
1.
int i;
int i = 10;
问:此代码正确吗?(是否会出现与变量定义两次有关的错误?提醒您,它是一个单独的源文件,而不是函数主体或复合语句的一部分)
回答
答:是的,此代码是正确的。第一行是临时定义,在编译器处理该定义后成为“定义”(第二行)。
2.
extern void bar(void);
void foo(int *x)
{
int y = *x; /* (1) */
if(!x) /* (2) */
{
return; /* (3) */
}
bar();
return;
}
问:事实证明,即使x是空指针,bar()也会被调用(并且程序不会崩溃)。是优化程序的错误,还是一切正确?
回答
答:一切正确。如果x是空指针,则在第(1)行中发生未定义的行为,并且没有人欠程序员任何东西:程序不必在第(1)行中崩溃,也不必在第(2)行中返回以防万一。设法执行第(1)行。如果我们谈论编译器所遵循的规则,则所有这些都将通过以下方式发生。在对第(1)行进行分析之后,编译器认为x不能为空指针,并消除了(2)和(3)作为无效代码。变量y被删除为未使用。由于* x类型没有限定为易失性,因此也消除了从内存中的读取。
这就是未使用的变量删除空指针检查的方式。
3.
有一个功能:
#define ZP_COUNT 10
void func_original(int *xp, int *yp, int *zp)
{
int i;
for(i = 0; i < ZP_COUNT; i++)
{
*zp++ = *xp + *yp;
}
}
他们想通过以下方式对其进行优化:
void func_optimized(int *xp, int *yp, int *zp)
{
int tmp = *xp + *yp;
int i;
for(i = 0; i < ZP_COUNT; i++)
{
*zp++ = tmp;
}
}
问:是否可以调用原始函数和优化函数,以便在zp中获得不同的结果?
回答
答:有可能,让yp == zp。
4.
double f(double x)
{
assert(x != 0.);
return 1. / x;
}
问:此函数可以返回inf吗?假定浮点数是根据IEEE 754(大多数计算机)实现的,并且启用了断言(未定义NDEBUG)。
回答
答:可以。传递一个非规格化的x就足够了,例如1e-309。
5.
int my_strlen(const char *x)
{
int res = 0;
while(*x)
{
res++;
x++;
}
return res;
}
问:上面提供的函数应返回以空值结尾的行的长度。查找错误。
回答
答:使用int类型存储对象大小是错误的,因为不能保证int能够存储任何对象的大小。我们应该使用size_t
6.
#include <stdio.h>
#include <string.h>
int main()
{
const char *str = "hello";
size_t length = strlen(str);
size_t i;
for(i = length - 1; i >= 0; i--)
{
putchar(str[i]);
}
putchar('\n');
return 0;
}
问:循环是无限的。怎么来的?
回答
答:size_t是无符号类型。如果i是无符号的,则i> = 0始终为true。
7.
#include <stdio.h>
void f(int *i, long *l)
{
printf("1. v=%ld\n", *l); /* (1) */
*i = 11; /* (2) */
printf("2. v=%ld\n", *l); /* (3) */
}
int main()
{
long a = 10;
f((int *) &a, &a);
printf("3. v=%ld\n", a);
return 0;
}
该程序由两个不同的编译器编译,并在little-endian计算机上运行。获得了两个不同的结果:
1. v=10 2. v=11 3. v=11
1. v=10 2. v=10 3. v=11
问:您如何解释第二个结果?
回答
答:给定的程序具有未定义的行为。即,违反了严格的别名规则。在第(2)行中更改了int。因此,我们可以假设任何时间都没有改变。(我们不能取消引用别名另一个不兼容类型的指针的指针)。这就是为什么编译器可以传递在执行第(1)行时读取的相同long(第(3)行)的原因。
8.
#include <stdio.h>
int main()
{
int array[] = { 0, 1, 2 };
printf("%d %d %d\n", 10, (5, array[1, 2]), 10);
}
问:此代码正确吗?如果没有未定义的行为,那么它将显示什么?
回答
答:是的,这里使用逗号运算符。首先,计算并丢弃逗号的左引数。然后,计算正确的自变量并将其用作整个运算符的值。输出为10 2 10。
请注意,函数调用中的逗号符号(例如f(a(),b()))不是逗号运算符,因此,它不保证计算顺序:a(),b()可以以任何顺序调用。
9.
unsigned int add(unsigned int a, unsigned int b)
{
return a + b;
}
问:add(UINT_MAX,1)的结果是什么?
回答
答:定义了无符号数字的溢出,它是由2 ^(CHAR_BIT * sizeof(unsigned int))计算的。结果为0。
10.
int add(int a, int b)
{
return a + b;
}
问:add(INT_MAX,1)的结果是什么?
回答
答:带符号的数字溢出–未定义的行为。
11.
int neg(int a)
{
return -a;
}
问:这里是否存在未定义的行为?如果是这样,根据什么理由?
回答
答:neg(INT_MIN)。如果ECM在附加代码中表示负数(二进制补码),则INT_MIN的绝对值比INT_MAX的绝对值大一。在这种情况下,-INT_MIN调用带符号的溢出,这是未定义的行为。
12.
int div(int a, int b)
{
assert(b != 0);
return a / b;
}
问:这里是否存在未定义的行为?如果是这样,根据什么理由?
回答
答:如果ECM在附加代码中表示负数,则div(INT_MIN,-1) –请参考上一个问题。
-Dmitri Gribenko gribozavr@gmail.com
https://kukuruku.co/post/i-do-not-know-c/