变量声明与定义,函数的定义与声明

  变量声明与定义,函数的定义与声明

  在最后一节中,我们将两个程序文件放在一起编译链接。main.c使用的函数push、pop、is_empty都是stack.c提供的其实有点问题。我们可以看到,当我们用-Wall选项编译main.c时:

  $ gcc -c main.c -Wall

  main.c:在函数“main”中:

  main.c:8:警告:函数“push”的隐式声明

  main.c:12:警告:函数“is_empty”的隐式声明

  main.c:13:警告:函数“pop”的隐式声明

  我们在第2节“自定义函数”中讨论了这个问题。由于编译器在处理函数调用代码时没有找到函数原型,所以只能根据函数调用代码进行隐式声明,将这三个函数声明为:

  int push(char);

  int pop(void);

  int是_ empty(void);

  现在,您应该比学习第2节“自定义函数”时更容易理解这条规则了。为什么编译器在处理函数调用代码时需要有函数原型?因为你必须知道参数的类型和个数以及返回值的类型,才能知道生成什么样的指令。为什么隐式声明不可靠?因为隐式声明是从函数调用代码派生出来的,所以实际上函数定义的形参类型可能和函数调用代码传递的实参类型不一样。如果函数定义有可变参数(比如printf),那么从函数调用代码看不出函数有可变参数。另外,从函数调用代码看不出返回值应该是什么类型,所以隐式声明只能规定返回值都是int类型。既然隐式声明不可靠,为什么编译器不自己找函数定义,而要我们先写函数原型再调用?因为编译器经常不知道去哪里找函数定义。像上面的例子,我让编译器编译main.c,但是这些函数的定义都在stack.c里,编译器怎么知道?因此,编译器只能通过隐式声明来猜测函数的原型,这往往是错误的,但在相对简单的情况下仍然可用。比如上一节的例子,即使编译了也能得到正确的结果。

  现在我们在main.c中声明这些函数的原型:

  /* main.c */

  #包含stdio.h

  外部void推送(char);

  外部字符弹出(void);

  extern int是_ empty(void);

  int main(void)

  push( a );

  推( b );

  push( c );

  而(!is_empty())

  putchar(pop());

  putchar( n );

  返回0;

  }

  这样,编译器不会报告警告。这里的external关键字表示这个标识符有一个外部链接。上一章提到了外链接的定义,现在应该更容易理解了。标识符push with External Linkage的意思是:如果main.c和stack.c链接在一起,如果push在main.c和stack.c中都声明了(stack.c中的声明也是一个定义),那么这些声明引用的是同一个函数,后面是同一个全局符号,代表的是同一个地址。函数声明中的extern也可以不写而省略,函数声明不写extern也说明这个函数有外部联动。

  如果用static关键字修饰一个函数声明,就意味着这个标识符有一个内部链接,比如下面两个程序文件:

  /* foo.c */

  静态void foo(void) {}

  /* main.c */

  void foo(void);

  int main(void){ foo();返回0;}

  一起编译会有错误:

  $ gcc foo.c main.c

  /tmp/ccRC2Yjn.o:在函数“main”中:

  main.c:(。text0x12):对“foo”的未定义引用

  集合2: ld返回了1个退出状态

  虽然foo.c中定义了函数foo,但是这个函数只有内部链接。只有在foo.c中多次声明时才表示同一个函数,但在main.c中声明时并不表示如果foo.c编译到目标文件中,函数名foo是局部符号,不参与链接过程,所以链接时在main.c中使用了一个外部链接foo函数,但链接器找不到它的定义和地址,无法做符号分析,只好报错。任何被多次声明的变量或函数必须只有一次声明作为定义。如果有多个定义,或者根本没有定义,链接器将无法完成链接。

  上面描述了用static和extern修改函数声明的情况。现在让我们看看用它们修改变量声明的情况。还是用stack.c和main.c的例子,如果我想在main.c中直接访问stack.c中定义的变量top,可以用extern声明:

  /* main.c */

  #包含stdio.h

  void push(char);

  char pop(void);

  int是_ empty(void);

  extern int top

  int main(void)

  push( a );

  推( b );

  push( c );

  printf(%dn ,top);

  而(!is_empty())

  putchar(pop());

  putchar( n );

  printf(%dn ,top);

  返回0;

  }

  变量top有外部链接,其存储空间分配在stack.c中,所以main.c中的变量声明extern int top不是变量定义,因为它不分配存储空间。上面的函数和变量声明也可以写在主函数体中,这样声明的标识符就有了块作用域:

  int main(void)

  void push(char);

  char pop(void);

  int是_ empty(void);

  extern int top

  push( a );

  推( b );

  push( c );

  printf(%dn ,top);

  而(!is_empty())

  putchar(pop());

  putchar( n );

  printf(%dn ,top);

  返回0;

  }

  注意变量声明和函数声明是有区别的。函数声明的extern可以写也可以不写,而变量声明的意义如果不写extern就完全变了。如果上面的例子中没有写extern,说明在main函数中定义了一个局部变量top。还要注意stack.c中的定义是int top=-1;并且main.c中的声明不能添加初始值设定项,如果上面的例子写成extern int top=-1;编译器将报告一个错误。

  在main.c中,可以通过变量声明访问stack.c中的变量top。但从实现stack.c的模块来看,变量top预计不会被外界访问。变量top和stack都属于这个模块的内部状态。外界应该只允许通过push和pop函数改变模块的内部状态,以保证栈的LIFO特性。如果外界可以随机访问栈或者修改栈顶,那么栈的状态将是混乱的。怎样才能防止外界访问top和stack?答案是用static关键字将它们声明为内部链接:

  /* stack.c */

  静态字符堆栈[512];

  static int top=-1;

  无效推送(字符c)

  stack[top]=c;

  字符弹出(无效)

  返回堆栈[top-];

  int是_empty(void)

  return top==-1;

  }

  这样stack.c的变量top和stack即使在main.c中用extern声明也无法访问从而保护stack.c模块的内部状态,这也是一种封装思想。

  这也是为了用static关键字声明具有内部链接的函数。在一个模块中,提供一些函数供外部使用,也称为导出,这些函数被声明为外部链接。一些只在模块内部使用,不希望外界访问的函数,声明为内部联动。

  继续我们之前关于stack.c和main.c Stack.c的讨论,这个模块封装了top和stack两个变量,派生了push、pop和is_empty三个功能接口,设计的很好。但是用这个模块给每个程序文件写三个函数声明也很麻烦。假设另一个foo.c也使用这个模块,要分别用main.c和foo.c写三个函数声明。应尽可能避免重复代码。过去,我们通过各种方法提取重复代码。例如,在第2节“数组应用示例:计算随机数”中,我们谈到了使用宏定义来避免硬编码的问题。这次有什么解决办法?答案是你可以自己写一个头文件stack.h:

  /* stack.h */

  #ifndef STACK_H

  #定义堆栈_H

  外部void推送(char);

  外部字符弹出(void);

  extern int是_ empty(void);

  #endif

  这样,您只需要在main.c中包含这个头文件,而不是编写三个函数声明:

  /* main.c */

  #包含stdio.h

  #include stack.h

  int main(void)

  push( a );

  推( b );

  push( c );

  而(!is_empty())

  putchar(pop());

  putchar( n );

  返回0;

  }

  首先说为什么# includesdio.h用尖括号,#include stack.h 用引号。对于尖括号中包含的头文件,gcc首先寻找-I选项指定的目录,然后寻找系统的头文件目录(通常是/usr/include,在我的系统上也包括/usr/lib/gcc/I486-Linux-GNU/4 . 3 . 2/include);对于包含在引号中的头文件,gcc首先找到。c文件,然后找到由-I选项指定的目录,然后找到系统的头文件目录。

  如果三个代码文件都放在当前目录中:

  $树

   - main.c

   - stack.c

  `- stack.h

  0个目录,3个文件

  可以用gcc -c main.c编译,gcc会自动在main.c所在的目录下找到stack.h。如果将stack.h移动到子目录中:

  $树

   - main.c

  `-堆栈

   - stack.c

  `- stack.h

  1个目录,3个文件

  你需要用gcc -c main.c -Istack编译。使用-I选项告诉gcc头文件在子目录堆栈中查找。

  可以在#include预处理指令中使用相对路径,比如把上面的代码改成#include stack/stack.h ,那么编译时就不需要添加-Istack选项,因为gcc会自动在main.c所在的目录中寻找,头文件相对于main.c所在目录的相对路径正好是stack/stack.h。

  在stack.h中,我们看到两个新的预处理指令#ifndef STACK_H和#endif,这意味着如果宏STACK_H没有被定义,那么从#ifndef到#endif的代码包含在预处理输出结果中,否则这个代码不会出现在预处理输出结果中。头文件stack.h的内容用#ifndef和#endif括起来。如果在包含这个头文件的时候已经定义了宏STACK_H,就相当于这个头文件里什么都没有,包括一个空文件。这有什么用?如果main.c包含stack.h两次:

  .

  #include stack.h

  #include stack.h

  int main(void)

  .

  那么第一次包含stack.h时没有定义宏STACK_H,所以头文件的内容包含在预处理后的输出结果中:

  .

  #定义堆栈_H

  外部void推送(char);

  外部字符弹出(void);

  extern int是_ empty(void);

  #include stack.h

  int main(void)

  .

  已经定义了宏STACK_H,所以第二次包含stack.h相当于包含了一个空文件,防止头文件的内容被重复包含。这种受保护的头文件称为头文件保护。以后我们写的每一个头文件都要加Header Guard,宏定义名都要用头文件名大写。这是标准做法。

  那为什么需要防止重复收录呢?谁会两次包含一个头文件?没有人会犯上面这么明显的错误,但有时候重复的错误就没那么明显了。例如:

  #include stack.h

  #包含“foo.h”

  但是foo.h包含bar.h,bar.h包含stack.h在大型项目中,头文件包含头文件是很常见的,往往包含四五层。这时候重复包含的问题就很难发现了。例如,在我的系统头文件目录/usr/include中,errno.h包含bits/errno.h,它又包含linux/errno.h,它又包含asm/errno.h,它又包含ASM-generic/errno.h。

  另一个问题是,即使我反复包含头文件,有没有坏处?像上面三个函数声明,在程序中声明两次是没有问题的。对于具有外部链接的函数,任意数量的声明也表示同一个函数。包含重复的头文件存在以下问题:

  第二,如果foo.h包含bar.h,bar.h也包含foo.h,那么预处理器就会陷入死循环(实际上编译器会规定一个包含层数的上限)。

  第三,头文件中有些代码是不允许重复的。虽然变量和函数是允许多次声明的(只要不是多次定义),但是头文件中有些代码是不允许多次出现的,比如typedef类型定义和结构标签定义,在一个程序文件中只能出现一次。

  还有一个问题。既然要#include头文件,不如就在main.c中#include stack.c 这样,把stack.c和main.c合并到同一个程序文件中,就相当于回到了原来的例子12.1,“用stack实现反向打印”。当然这个也可以编译通过,但是在大型项目中做不到。如果有另一个foo.c也使用了模块stack.c怎么办?如果foo.c中也有#include stack.c ,相当于main.c和foo.c中都有push、pop和is_empty的定义,那么main.c和foo.c就不能链接在一起。如果采用包含头文件的方法,那么这三个函数在stack.c中只定义一次,最后main.c,stack.c,foo.c就可以链接在一起了。如下图所示:

  图20.2。为什么要包含头文件而不是?c文件

  同样,头文件中的变量和函数声明也不能是定义。如果变量或函数定义出现在头文件中,并且该头文件由多个。c文件,然后是这些。c文件不能链接在一起。

  以上两节只是介绍了关于定义和声明的基本规则,写代码的时候掌握这些基本规则就足够了。但是C语言中关于定义和声明的规则比较复杂,在分析错误原因或者维护大型项目时需要了解这些规则。本节中的两个表来自[标准C]。

  首先看一下关于函数声明的规则。

  表20.1。存储类关键字在函数声明中的作用

  过去我们说“外部关键字表示该标识符有外部链接”其实是不准确的,准确的说应该是以前的链接。前一个链接的定义是:这个声明的标识符有什么样的链接取决于前一个声明,它有相同的标识符名称,必须是文件范围的声明。如果在程序文件中找不到前面的声明(这个声明是第一个),那么这个标识符有外部链接。例如,在一个程序文件中,同一个函数在文件范围内声明了两次:

  静态int f(void);/*内部联动*/

  extern int f(void);/*以前的链接*/

  那么这里extern修改的标识符有internal链接而不是外部链接。从上表的前两行可以总结出我们前面说的规律:“函数声明有没有extern关键字都一样”。上表还显示,函数允许在文件范围内定义,但不允许在块范围内定义,或者函数定义不能嵌套。另外,不允许在块范围内用static关键字声明函数。

  关于变量声明的规则更加复杂:

  表20.2。存储类关键字在变量声明中的作用

  外部收缩

  静态持续时间

  静态初始值设定项

  暂定定义

  无链接

  自动持续

  动态初始化器

  定义

  先前链接

  静态持续时间

  noinitializer[*]

  not定义

  先前链接

  静态持续时间

  无启动器

  not定义

  内部链接

  静态持续时间

  静态初始值设定项

  暂定定义

  无链接

  静态持续时间

  静态初始值设定项

  定义

  表格中每个单元格分为四行,分别描述变量的链接属性和生存期,以及这个变量是如何初始化的,是否是变量定义。有四种属性:外部链接、内部链接、无链接和先前链接,以及两种生存期:静态持续时间和自动持续时间。请参考本章和上一章中的定义。初始化有两种:静态初始化器和动态初始化器。前者意味着初始化器中只能使用常量表达式,表达式的值必须在编译时确定。后者意味着任何右值表达式都可以在初始化器中使用,表达式的值可以在运行时计算。计算变量的定义有三种:定义(计算变量定义)、非定义(非计算变量定义)和暂定定义(暂定变量定义)。“试探性变量定义”是什么意思?变量声明有一个文件作用域,没有用Storage Class关键字修饰,或者用static关键字修饰,所以如果它有一个初始化器,编译器就认为它是一个变量定义。如果它没有初始化器,编译器会暂时将其定义为一个变量。如果程序文件中有该变量的明确定义,则使用暂定变量定义[32]。在这种情况下,变量用0初始化。【C99】中有一个例子:

  int i1=1;//定义,外部联动

  静态int I2=2;//定义,内部联动

  extern int i3=3;//定义,外部联动

  int i4//暂定定义,外部联动

  静态int i5//暂定定义,内部联动

  int i1//有效的试验性定义,参考以前的

  int i2//6.2.2渲染未定义,链接不一致

  int i3//有效的试验性定义,参考以前的

  int i4//有效的试验性定义,参考以前的

  int i5//6.2.2渲染未定义,链接不一致

  extern int i1//引用上一个,其链接是外部的

  extern int i2//引用上一个,其链接是内部的

  外部整数i3;//引用上一个,其链接是外部的

  外部整数i4;//引用上一个,其链接是外部的

  extern int i5//引用上一个,其链接是内部的

  变量i2和i5第一次声明为内部链接,第二次声明为外部链接,这是不允许的,编译器会报错。注意上表中标有[*]的单元格。对于文件范围内的extern变量的声明,C99是允许带初始化器的,它被认为是一个定义。然而,gcc将报告这种类型的书写的警告,为了兼容性应该避免这种情况。

变量声明与定义,函数的定义与声明