2

只是对工作有点好奇,在这里。在处理一些危险的事情时,我开始思考各种编译器及其相关标准库的实现。以下是我的想法的进展:

  1. 某些类别的标识符保留用于 C++ 和 C 中的实现使用。

  2. 编译器必须执行编译阶段(预处理、编译、链接),就好像它们是按顺序执行的一样。

  3. C 预处理器不知道标识符的保留状态。

  4. 因此,当且仅当

    1. 使用的保留标识符都是预处理器符号。

    2. 预处理结果不包括保留标识符。

    3. GNUC标识符与编译器(等)预定义的符号不冲突

这是有效的吗?我不确定第 3 点和第 4.3 点。此外,有没有办法测试它?

4

5 回答 5

6

(对问题的评论解释说,我们正在讨论C99 第 7.1.3 节意义上的保留标识符,即在文件范围内与外部链接等/^_[A-Z_]/任何地方匹配的标识符。所以这是我的猜测至少一部分你在问什么......)/^_//^str[a-z]/

从某种意义上说,它们不是保留的(任何特定阶段)编译器应该诊断它们的误用。相反,它们是保留的,因为如果您愚蠢到自己(误用)它们,那么如果您的程序停止工作或在以后停止编译,您就不会抱怨。

我们都看到了当只有少量知识的人查看系统头文件然后编写自己的头文件保护时会发生什么:

#ifndef _MYHEADER_H
#define _MYHEADER_H
// ...
#endif

他们正在调用未定义的行为,但没有将其诊断为“错误:最终用户代码使用的保留标识符”。相反,大多数情况下他们很幸运,一切都很好。但有时它们会与实现感兴趣的标识符发生冲突,并且会发生令人困惑的事情。

strip()同样,我经常有一个名为左右的外部可见函数:

char *strip(char *s) {
  // remove leading whitespace
  }

通过阅读 C99 的 7.1.3、7.26 和 7.26.11,这会引发未定义的行为。但是我决定不关心这个。标识符的保留不是因为预计今天会发生任何不好的事情,而是因为标准保留了str-ip()在未来修订中发明新标准例程的权利。而且我已经决定我认为string - ip,无论它可能是什么,都不太可能是将来添加的字符串操作的名称——所以万一发生这种情况,我会在到达时越过那座桥它。从技术上讲,我正在调用未定义的行为,但我不希望被咬。

最后,针对您的第 4 点举一个反例:

#include <string.h>
#define memcpy(d,s,n)  (my_crazy_function((n), (s)))
void foo(char *a, char *b) {
  memcpy(a, b, 5);  // intends to invoke my_crazy_function
  memmove(a, b, 5); // standard behaviour expected
}

这符合您的 4.1、4.2、4.3(如果我理解您对最后一个的意图)。但是,如果memmove另外实现为用 编写的宏(通过 7.1.4/1)memcpy,那么您将遇到麻烦。

于 2010-09-28T21:09:24.747 回答
2

这个故事比这更复杂,我认为,至少对于当且仅当。我从 C99 中回忆的内容:

例如 3. 为假,即使在预处理阶段也保留令牌,并且也可能不会重新定义诸如 等的defined伪宏。__LINE____func__

然后,标识符的保留取决于范围。

  • 一些标识符明确地保留给外部符号,例如 setjmp.
  • 以下划线开头,然后是另一个下划线或大写字母的标识符在 C 中的任何地方都保留。即使使用预处理器,也不应该触摸它们。
  • 在文件范围内禁止以下划线开头然后是小写字母的标识符,因为它们可能引用外部符号。它们可以在函数范围内自由使用。

4.2 也不完全正确。首先,在以下条件下定义一个以关键字作为名称的宏只是未定义的行为(也就是非常邪恶的):

包含标准标头,同时使用与关键字相同的名称定义宏 (7.1.2)。

然后,在其扩展中包含自己名称的宏是“安全的”,因为保证扩展不是递归的。尽管不推荐,但以下内容将是有效的:

#define if(...)                                         \
for(int _i = 0; _i < 1; ++_i)                           \
  for(int _cond = (__VA_ARGS__);                        \
      _i < 1;                                           \
      printf("line %d val %d\n", __LINE__, _cond),      \
        ++_i)                                           \
    if(_cond)

(顺便说一句,不要有人使用那个宏,它编译并执行它的外观,但有一些极端情况让它爆炸。)

于 2010-09-28T20:24:08.307 回答
2

C 预处理器不知道标识符的保留状态。

我不确定您所说的“意识到”是什么意思,但我认为您不一定可以假设这一点- 7.1.3 说

所有以下划线开头的标识符,无论是大写还是另一个下划线,始终保留用于任何用途

预处理器(或编译器)实现可以将这些保留的标识符用于任何适合它的目的 - 如果您滥用这些标识符,它不需要警告您。

我建议“当且仅当”标准(例如一组预定义的宏)或实现在其文档中如此说明时,“程序可以使用保留的标识符”。

当然,我认为你会在很多情况下使用保留的标识符而侥幸——实现不会特意给你带来问题。大量代码使用保留的名称,我猜想如果没有足够的理由,实现宁愿不破坏该代码。但是,如果您没有实现编译器工具链,最好完全避免使用该命名空间。

于 2010-09-28T20:28:55.713 回答
1

标识符 like_UNDERSCORE_CAPdouble__underscore保留供实现使用,因为它认为合适。如果实现使用它们不是问题,例如在 中具有_File标识符或宏<stdio.h>,这就是保留的目的。如果用户使用一个,这是一个潜在的问题。

因此,为了诊断这一点,编译器必须跟踪标识符的来源。仅检查不在 中的代码是不够的<angle_bracket_files.h>,因为它们可以定义可能使用的宏,并且可能使用实现保留字扩展为某些内容。例如,isupper可以定义<ctype.h>

#define isupper(x) (_UPPER_BIT & _CHAR_TRAITS[x])

或一些这样的。(我已经很久没有看到基于上述定义的定义了。)

因此,为了跟踪这一点,预处理器必须维护记录,其中包括哪些宏来自那里。跟踪会使预处理器相当复杂,编译器编写者似乎认为没有相应的收益。

于 2010-09-28T21:45:08.903 回答
0

如果您问是否可以#define if while并使您的代码不可读,那么可以。这是混淆 C 竞赛中的常见做法。不过,这实际上会违反您的 4.2。

对于像 GNUC 这样的东西,这些是预定义的,但您通常可以重新定义它们并取消定义它们。这样做并不是一个好主意,但你可以。更有趣的是重新定义或取消定义__LINE__,__FILE__和类似的预处理器符号(b/c 它们会自动更改)。

于 2010-09-28T19:42:31.580 回答