理解C语言中的变量声明和定义

前言

在C语言中,变量的声明和定义是两个常被提及但有着重要区别的概念。本文将对这两者进行深入解释,并结合官方文档的相关说明,以帮助大家更全面地理解这些概念。

1.基本概念

变量的声明声明是一种引入一个或多个标识符到程序中,并指定其含义及属性的 C 语言构造。

声明可以出现在任何作用域中。每个声明以分号结束(类似语句),并由两 (C23 前)三 (C23 起)个独立部分组成:

  • 说明符与限定符 声明符与初始化器(可选) ;
  • 属性说明符序列 说明符与限定符 声明符与初始化器
  • 属性说明符序列

简单的来说就是:变量的声明告诉编译器变量的类型和名称,但并不为其分配内存空间。它相当于告诉编译器:“嘿,这个东西我会用到,它的名字是这样,类型是那样,记得为它分配内存和空间哦”。简而言之,声明就是预告,让编译器知道某个标识符(比如变量名)的存在,但并不实际创建它。

变量的定义:变量定义将为标识符分配存储空间、引入一组实现定义的属性,并且根据初始化器(如果有提供)对对象进行初始化。

通俗的说就是:为变量分配内存并为其指定一个初始值。

**声明是定义的一个子集,也就是说,所有的定义都是声明,但并非所有的声明都是定义。**我们先记住这个结论,后续我们会进行论证。

2.语法解析

1、对于对象,分配其存储的声明(自动或静态存储持续期,但不是 extern )即是定义,而一个不分配存储的声明(外部声明)不是。

extern int n; // 声明  不分配存储空间,只是告诉编译器变量x在其他地方定义了
int n = 10; // 定义  定义了一个整型变量a,并初始化为10  

这里的 extern 关键字用于声明一个变量,而不是定义它。

对于对象(变量)来说,在C语言中,声明并不总是定义。只有在分配存储空间的情况下才是定义。但有一些情况例外,即在某些特定上下文中,比如静态存储持续期(static storage duration)或自动存储持续期(automatic storage duration)的情况下:

  • 静态存储持续期:当在全局作用域内定义变量时,或使用static关键字在函数内部定义静态变量时,这些声明既是声明也是定义。因为它们为变量分配了静态存储空间。

    • 全局作用域内定义静态变量

      // 在全局作用域内定义静态变量,这既是声明也是定义
      static int globalStaticVariable; // 这里声明并定义了一个静态变量
      
    • 在函数内部使用static关键字定义静态变量

      void someFunction() {
          // 在函数内部定义静态变量,也是声明也是定义
          static int localStaticVariable; // 这里声明并定义了一个静态变量
      }
      
  • 自动存储持续期:当在函数内部定义非静态变量时,它们是声明也是定义,因为它们分配了自动存储空间。

    void Func() {
        // 在函数内部定义非静态变量,这里声明并定义了一个自动变量
        int localVar; // 这里声明并定义了一个自动变量
        // 相当于 auto int localVar,只不过省略了auto关键字
    }
    

2、每个 enum 或 typedef 声明都是定义。

  • enum声明:enum(枚举)声明定义了一个新的枚举类型,并为该类型定义可能的值。它创建了一个具有指定名称的新类型,并指定了该类型可能的取值范围。

    enum Weekday {
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday,
        Saturday,
        Sunday
    };
    

    这里,Weekday就是一个新类型的声明,而且在C/C++中这也被视为定义

  • typedef声明:typedef声明为一个现有类型创建一个新名称。它不会创建新的类型,只是为现有类型提供了一个别名。比如:

    typedef int myInt;
    

    这里的myInt现在可以被用作int的别名,在代码中可以用myInt代替int,但它并没有创建一个新的数据类型。

    那为什么官方文档里说typedef的声明是定义呢?

    在严格的语言定义中,typedef更倾向于被认为是声明而不是定义,因为它并没有创建一个全新的类型,只是为现有类型引入了一个别名。但在实际使用中,人们可能会将typedef视为创建了一个新类型,并将其视为定义。

    在大多数情况下,typedef创建的别名被视为原类型的同义词,而不是创建了一个新类型。但有些情况下,特别是在代码审查、静态分析或对类型安全性有更高要求的情境下,typedef的别名可能被视为一种新的类型。

3、对于函数,包含函数体的声明即是函数定义。

在C语言中,函数的声明描述了函数的接口,包括函数名称、参数列表和返回类型,但不包括函数体。

函数的定义则包括了函数的声明,并提供了函数体,即函数的实际操作和逻辑。所以,包含函数体的声明即是函数的定义。

例子:

// 这是函数的声明
int add(int a, int b);

// 这是函数的定义
int add(int a, int b) {
    return a + b;
}

在C23标准之前可以使用无名形参来声明函数,但不能定义函数(C23起可以)。补充:仅有的例外是形参列表 (void) 可以定义函数

int f(int, int); // 声明
// int f(int, int) { return 7; } // 无名形参函数定义C23 前错误,C23 起 OK

int g(void) { return 8; } //函数定义  OK:void 不声明参数

4、对于结构体和联合体,指定其成员列表的声明是定义。

struct X; // 声明
struct X { int n; }; // 定义

3.深入理解

当涉及编译和链接时,变量的声明和定义也具有重要影响。

**编译过程:**在C语言的编译过程中,编译器会检查源代码中的语法错误,并为定义的变量分配内存。但是,对于仅声明而未定义的变量,编译器不会为其分配内存,而是会检查该变量是否在其他编译单元中定义。

链接过程:

  • 链接器的任务:链接器将编译生成的目标文件(.o文件,包含机器代码和符号表)组合成一个可执行文件。它负责解析程序中各个部分之间的引用关系。
  • 处理变量的定义和声明:链接器会解析所有外部符号(例如在一个文件中声明但在其他地方定义的变量)。如果一个变量被声明但在链接阶段找不到具体的定义,链接器将会报错。

多文件项目和多模块编译:

  • 变量的声明和定义在多文件项目中:在多文件项目中,头文件中的声明用于在不同源文件中共享变量的信息。定义只能出现在一个源文件中,多个文件对同一个变量的定义会导致链接时的重复定义错误。同时在头文件中,我们通常只声明变量而不定义它。这是因为头文件可能被多个源文件包含,如果我们在头文件中定义变量,那么每个包含该头文件的源文件都会有该变量的一个副本,从而导致重复定义的错误。

    • 当涉及多文件项目时,通常会有多个源文件(.c文件)和头文件(.h文件)来组织代码。在这种情况下,变量的定义和声明需要在不同文件之间进行正确的管理,以避免重复定义和符号冲突的问题。

      示例代码:

      file1.c

      int globalVar; // 在file1.c中定义全局变量globalVar
      
      void funcFile1() {
          globalVar = 10;
      }
      

      file2.c

      #include 
      extern int globalVar; // 在file2.c中声明file1.c中定义的全局变量globalVar
      
      void funcFile2() {
          printf("%d\n", globalVar);
      }
      

      在这个示例中,file1.c中定义了一个全局变量globalVar,而file2.c则通过extern关键字声明了在file1.c中定义的全局变量globalVar

    • 当在头文件中定义变量时,在多个源文件中包含该头文件并在不同源文件中定义相同名称的变量会导致重复定义的错误。这是一个常见的错误示例。

      header.h

      int globalVar = 5; // 在头文件中定义全局变量globalVar并赋初值为5
      

      file.c

      #include "header.h"
      
      int globalVar = 10;
      

      在这种情况下,header.h中定义的globalVar在每个包含该头文件的源文件中都会有自己的一份定义。这将导致链接时的重复定义错误,因为编译器无法确定应该使用哪个定义。

      要避免这种错误,应该在头文件中声明变量而不是定义它们。在头文件中使用extern关键字声明变量,并在一个源文件中进行定义。如下面所示:

      header.h

      // 在头文件中声明全局变量globalVar
      extern int globalVar;
      

      file.c

      #include "header.h"
      
      // 在file.c中定义全局变量globalVar并赋初值为5
      int globalVar = 5;
      
      
  • 编译单元和符号冲突:编译器在编译单元中处理变量的声明和定义,当同一变量在多个编译单元中出现定义,链接器会遇到符号冲突问题,需要解决重复定义的情况。

从编译和链接的角度来理解变量的声明和定义有助于了解编译器和链接器如何处理变量,以及在多文件项目中如何正确使用声明和定义以避免链接错误。

4.总结

当谈及C语言中的变量声明和定义时,我们要注意以下要点:

  1. 声明与定义的区别:
    • 声明引入标识符并指定其类型,告诉编译器这个标识符的存在及其类型,但并不为其分配内存空间。
    • 定义包括声明,并且为标识符分配内存空间,可以附加初始值。
  2. 变量声明和定义的例子:
    • 对于对象(变量),声明并不总是定义。只有在分配存储空间时才是定义。
    • typedef声明为类型创建别名,虽然语法上理解偏向于声明,但实际在官方文档中定性为定义。
    • 函数声明描述接口,函数定义包括声明并提供函数体。
    • 对于结构体和联合体,指定成员列表的声明是定义。
  3. 多文件项目中的注意事项:
    • 在多文件项目中,头文件通常包含变量声明而不是定义。如果在头文件中定义变量,可能导致重复定义错误。
    • 应该在一个源文件中定义变量,然后在头文件中声明它们,以避免重复定义和符号冲突。
  4. 编译和链接过程中的作用:
    • 编译器在编译过程中为定义的变量分配内存空间,但对于仅声明而未定义的变量,编译器不会分配内存空间。
    • 链接器负责解析程序中各个部分之间的引用关系,当出现重复定义时会报错。

理解变量声明和定义的区别有助于编写更清晰、避免冲突的代码,并确保程序正确编译和链接。

你可能感兴趣的:(C,c语言)