C Programming Language

Basics #06: Declaration and Definition

# Basic Tutorial,  # Programming Language,  # C Programming Language

Last Updated: 2020-07-01


前置知识

在阅读本文档前,请确保你已经掌握函数的基础知识,详见 Basics #02: Function

在阅读本文档前,请确保你已经掌握类型的基础知识,详见 Basics #03: Types


前言

在 C 语言中,一个标识符(例如变量或函数)的声明和定义是不同的,很多初学者不清楚这其中的区别。这不是一个复杂的问题,但却也不好归类,因此就出现了这篇极为简短的文章。


基本概念

通俗地讲,一个标识符的 声明 (declaration) 的作用是告诉编译器在程序的某个位置定义了一个如声明所述的变量或函数,而 定义 (definition) 则为变量或函数确立了实体,会为其保留相应的内存空间。

声明和定义之间的区别是非常明确的:声明不会为标识符所描述的对象分配内存。在只有声明没有定义的情况下,这个变量或函数实际上可能是“不存在的”,或者说,至少不存在于此处。在程序后续的编译过程中,编译器会通过某种途径在整个程序(可能横跨多个源文件)中查找其实际定义,如果查找失败则会触发编译错误。

另外,定义本身就包含了声明。当你定义了一个变量或函数的时候,本身也同时声明了该变量或函数。事实上,根据 C 语言标准,定义就是一种满足特殊条件的声明。

标准指出,一个标识符的定义,是满足下列条件的、对该标识符的声明:

  • 对于一个对象(例如变量),为该对象保留了内存空间;

  • 对于一个函数,描述了其函数主体(即其代码);

  • 对于枚举常量或 typedef 名称,是标识符的(唯一)声明。


变量的声明和定义

变量的声明和定义通常是一体的,但并非总是如此。当我们用最简单的语句定义一个变量时,实际上已经为该变量保留了相应的内存空间:

void f() {
    int x;     // definition, or equally called "declaration and definition"
    int y = 0; // definition with initialization
}

但如果使用了 extern 关键字,则该语句仅声明了变量,而没有将其定义,因为 extern 关键字告诉编译器该变量是由外部定义的,编译器不会为其分配相应的内存空间,而会从其它位置(通常是联合编译的其它源文件中)寻找其定义:

extern int n;  // only declaration

不过其实这样也是合法的,只是没什么意义:

extern int x;
int main(void) {
    x = 1;
    return 0;
}
int x;

但如果你在使用 extern 关键字的同时对变量进行初始化,则该语句会被认为是定义:

extern int x = 2;
int main(void) {
    x = 1;
    return 0;
}

并且你会收到编译器发出的警告:

|1| warning: 'x' initialized and declared 'extern'

函数的声明和定义

读者对函数的声明和定义应该是比较熟悉的。下面的代码不能通过编译,因为在编译到 main 函数时,编译器在之前未能扫描到任何有关标识符 f 的定义:

#include <stdio.h>
 
int main(void) {
    f(1);
    return 0;
}
 
void f(int x) {
    printf("call f(%d)\n", x);
}

需要先声明:

#include <stdio.h>
 
void f(int x);
 
int main(void) {
    f(1);
    return 0;
}
 
void f(int x) {
    printf("call f(%d)\n", x);
}

或是将定义移至 main 函数前(实质上是移至该函数的所有调用之前),前文已经提到,定义本身就有声明的含义:

#include <stdio.h>
 
void f(int x) {
    printf("call f(%d)\n", x);
}
 
int main(void) {
    f(1);
    return 0;
}

结构体相关的声明和定义

当我们“定义”一个结构体类型时,实际上根据 C 标准这是一个声明而不是定义,只是有些人习惯于说成“定义一个结构体”,不过这不重要:

struct Foo {
    int a;
    double b;
};

当然,这就是在声明 Foo 类型的同时定义了一个 Foo 类型的变量了:

struct Foo {
    int a;
    double b;
} obj;