我不知道C

本文的目的是让每个人(尤其是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/

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注