记录: C 语言标识符查找与命名空间以及结构体定义引发的问题

版权声明:署名-非商业性使用-相同方式共享

多年来写惯 C++ 了, 突然遇到一个 C 语言的问题, 还真有点摸不着头脑

在此感谢 Sakura酱, 二位大佬的解惑, 也不禁感叹 C 语言的博大精深与自我知识的匮乏

现将问题记录如下

问题

// test.c
struct person_t {
    unsigned char* name;
    int age;
};
typedef struct person_t* pperson_t;

#if 1 
// interface
pperson_t person_new(unsigned char* name, unsigned int age);
int       person_age(person_t* self);

// implementation
pperson_t person_new(unsigned char* name, unsigned int age) {
    return 0;
}

int person_age(person_t* self) {
    return 0;
}

int main() {
    return 0;
}

#else

// interface
pperson_t person_new(unsigned char* name, unsigned int age);
int       person_age(pperson_t self);

// implementation
pperson_t person_new(unsigned char* name, unsigned int age) {
    return 0;
}

int person_age(pperson_t self) {
    return 0;
}
#endif

int main() {
    return 0;
}

上面的代码在 msvc 中编译会报下面的错误:

cl.exe test.c
用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.29.30153 版
版权所有(C) Microsoft Corporation。保留所有权利。

test.c
test.c(11): error C2143: 语法错误: 缺少“)”(在“*”的前面)
test.c(11): error C2143: 语法错误: 缺少“{”(在“*”的前面)
test.c(11): error C2059: 语法错误:“)”
test.c(18): error C2143: 语法错误: 缺少“)”(在“*”的前面)
test.c(18): error C2143: 语法错误: 缺少“{”(在“*”的前面)
test.c(18): error C2059: 语法错误:“)”
test.c(18): error C2054: 在“self”之后应输入“(”

但只要我将 #if 1 修改为 #if 0, 就可以编译通过了, 它们之间的区别仅仅在于

第一个使用 person_t* self
第二个使用 pperson_t self

但两者其实是同一个类型: typedef struct person_t* pperson_t;

这是哪里出现了问题?

另外 gcc 也会出现同样的错误, 而以 C++ 模式编译都一切正常!

gcc test.c
test.c:11:22: error: unknown type name 'person_t'; did you mean 'pperson_t'?
   11 | int       person_age(person_t* self);
      |                      ^~~~~~~~
      |                      pperson_t
test.c:18:16: error: unknown type name 'person_t'; did you mean 'pperson_t'?
   18 | int person_age(person_t* self) {
      |                ^~~~~~~~
      |                pperson_t

原因及解决方案

在 C 程序中遇到标识符时,会查找并定位引入该标识符的声明(在当前作用域内)。若同一标识符的多个声明属于不同的 “命名空间” 类别,则 C 允许它们同时存在于作用域内(不包含 C23 引入的内容):

  1. 标号命名空间:所有声明为标号的标识符。
  • goto 语句的目标。
  • switch 语句的 case 标号。
  • switch 语句的默认标号。
  1. 标签名:所有声明为结构体、联合体及枚举类型名称的标识符,注意它们共享同一命名空间。
  2. 成员名:所有声明为结构体或联合体的成员的标识符。
  3. 通常标识符: 所有其他标识符如 函数名、对象名、typedef 名、枚举常量。

在查找时,根据使用方式确定标识符所属的命名空间

参考 C 语言 查找与命名空间: https://zh.cppreference.com/w/c/language/name_space

struct A { int c; }; // 名称 A 引入标签命名空间
typedef struct A A;  // 第一个 A 从标签命名空间中查找, 然后第二个 A 则将名称 A 引入通常命名空间
struct A* p;         // A 从标签命名空间中查找
A* q;                // A 从通常命名空间中查找
q->c;                // c 从A成员名命名空间中查找
goto __LABEL1;       // __LABEL1 从标号命名空间中查找

根据以上可知,最初的问题 int person_age(person_t* self) 中的标识符 person_t 尝试从通常命名空间中查找, 而这个标识符在标签名命名空间,因此编译器提示: 找不到标识符的错误。

将声明代码修改如下, 上面代码的两个分支都可以正常编译(C/C++):

typedef struct person_t { // 这个 person_t 定义在标签名命名空间
    unsigned char* name;
    int age;
} person_t, *pperson_t;   // 这个 person_t 与 pperson_t 定义在通常命名空间

而到了这里,也解释了我很久以前的另一个疑惑,为什么会出现很多这样的结构体声明:

typedef struct _WIN32_FILE_ATTRIBUTE_DATA {
    DWORD dwFileAttributes;
    FILETIME ftCreationTime;
    FILETIME ftLastAccessTime;
    FILETIME ftLastWriteTime;
    DWORD nFileSizeHigh;
    DWORD nFileSizeLow;
} WIN32_FILE_ATTRIBUTE_DATA, *LPWIN32_FILE_ATTRIBUTE_DATA;