【问题标题】:Why this C program complies and runs为什么这个 C 程序符合并运行
【发布时间】:2013-05-14 03:42:11
【问题描述】:

出于对typedef 的定义和范围的好奇,我在下面的 2 个 .c 文件中编写了 C 代码:

main.c

#include <stdio.h>

int main()
{
    int a = 5, b = 6;
    printf("a = %d, b = %d\n", a, b);
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
}

swap.c

typedef T;

void swap(T* a, T* b)
{
    T t = *a;
    *a = *b;
    *b = t;
}

令我惊讶的是,代码文件可以使用 Visual Studio C 编译器 (cl.exe /Tc main.c swap.c) 进行编译

程序运行正常!据我了解,typedef 需要 2 个参数,但为什么这段代码会编译并运行呢?

为了进一步分析,在主函数中,我声明了另外 2 个浮点变量,并在交换了 2 个整数后也尝试交换两者,但这一次编译失败(使用 cl.exe)。令人惊奇的是,代码可以用 Tiny C (tcc.exe main.c swap.c) 编译和运行,所以它就像模板方法一样工作!

【问题讨论】:

  • 这完全不像模板。

标签: c typedef c99 implicit c89


【解决方案1】:

Typedef 实际上是一个声明(它为现有类型创建别名),绝不限于两个“参数”。请参阅Typedef Declarations (C)Fundamental typedef operand syntax

如果你写typedef T;,你就是在声明 T 是一个未指定的类型(在 C89 规范中称为“无指定类型”)。这有点像#define X 定义了X(也只是非常少,但从概念上讲这可能会对您有所帮助),但预处理器会将其替换为空字符串(即删除X)。

所以,typedefing T 是未指定的,这使得 swap 函数的参数为​​未指定类型。

您在此处看到的是 在 C89 中(但不是 C99,它会导致未定义的行为 - 对比 ANSI 3.5.2 与 ISOC99 6.7.2),未指定类型默认为int,这就是为什么您的方法适用于整数但不适用于 Visual Studio 中的浮点数(可能默认情况下它不允许隐式整数类型)。不过,如果您不使用 -Werror,GCC 将使用浮点数编译它,您可能应该使用它。

我强烈建议打开一些警告:gcc 中的-Wall 会吐出以下内容

swap.c:1:9: warning: type defaults to ‘int’ in declaration of ‘T’ [-Wimplicit-int]

它在浮点数上“工作”的原因是因为浮点数和整数在您的机器上可能具有相同的大小(32 位)。试试双倍。或字符。或短。

swap真的是这样的:

void swap(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

有你的呼唤

swap((int*)&a, (int*)&b);

尝试一下并自己比较结果。


编辑:我刚刚在 tcc 中尝试过。遗憾的是,-Wall 不会提醒您注意隐式 int 类型。

【讨论】:

  • 试试看是一种危险的哲学,它会导致未定义和不可移植的代码。也许您可以使用 C 标准作为参考......更具体地说,6.3.2.1p2 说“如果左值的类型不完整并且没有数组类型,则行为未定义。”因此,行为不是未指定的,而是未定义的。
  • 我同意重新。 try it and see 是一种哲学,但我的意思是 if you don't believe me, please observe the results before discussing it further。然而,重新。未定义与未指定类型,我相信 C89 中的“无类型说明符”与未定义相同。 “无类型说明符”被视为int,尽管我手头没有 ISO 出版物。 FWIW Google 告诉我基于 ANSI 的 '89 草案 (flash-gordon.me.uk/ansi.c.txt) 的 3.5.2 同意(是的,编号不同,与 ISO C89/C90 不完全相同,也不权威,我知道)。
  • 哦,我明白了。我把它和前向声明混淆了。在这种情况下,typedef foo; 会是 C99 和 C11 中的语法错误吗?
  • 嗯,ISO C99 的 6.7.2 没有在类型说明符列表中包含 "no type specifier" 并且还说 "至少一个类型说明符应该在每个声明的声明说明符中给出...",并且违反 "shall" 会导致未定义的行为(参见第 4 节)。例如,带有std=c99 的 GCC 似乎已经(看起来不是很深入,不要引用我的话)使用 C89 行为实现了所述未定义行为,并且默认情况下抱怨隐式 int 类型(即没有 -Wimplicit-int启用)。我怀疑 Visual Studio 已将其作为语法错误实现。
  • @IskarJarak 感谢您的出色回答!出于学习目的,有时我使用 Tiny C 编译器,用它我可以交换 2 个浮点数而不会丢失抽取。但从你的解释来看,参数被转换为(int *),它应该松散抽取。为什么?还是应该避免使用 Tiny C 编译器而使用更标准的编译器?
【解决方案2】:

在 C90 中(这是 MSVC 在编译 C 代码时遵循的基准),可能的类型说明符之一是(C90 6.5.2 “类型说明符” - 已添加重点):

  • int、signed、signed int 或 无类型说明符

因此,如果声明中没有提供类型说明符(包括 typedef),则类型默认为 int。这通常称为“隐式 int”声明。请注意,C99 删除了对隐式 int 的支持(默认情况下,GCC 仅在 C99 模式下编译时对此发出警告)。

你的类型定义:

typedef T;

相当于:

typedef int T;

所以你的swap() 定义相当于:

void swap(int* a, int* b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

当调用尚未声明或原型化的函数时(如在 main.c 中调用 swap() 时发生的情况),编译器会将默认参数提升应用于算术参数并假设函数返回int。您对swap() 的调用传递了两个int* 类型的参数,因此不会发生提升(它们是指针参数,而不是算术)。这恰好是 swap() 的定义所期望的,因此函数调用有效(并且是明确定义的行为)。

现在,调用代码期望 swap() 返回 int,因为没有看到任何声明,并且您的 swap() 函数没有返回任何内容 (void)。这是未定义的行为,但在这种情况下没有明显的问题(尽管它仍然是代码中的错误)。但是,如果您更改swap() 的定义,使其返回int

int swap(int* a, int* b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

未定义的行为消失了,即使swap() 似乎没有返回任何东西。由于在调用站点没有对结果进行任何处理,因此 C90 允许函数以表达式返回。 C90 允许这样做是为了支持没有 void 类型之类的预标准代码。

【讨论】:

  • 感谢您的快速回答!
猜你喜欢
  • 2023-03-15
  • 1970-01-01
  • 1970-01-01
  • 2018-05-17
  • 2021-05-19
  • 2020-11-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多