c++声明与定义/内部链接与外部链接


 读完需要明白的问题:

  (1)   如何避免程序报重复定义错误?

(2)在头文件中可以定义些什么?

(3)什么是内部链接与外部链接,为什么不在头文件中定义具有外部链接的实体?

(4) 为什么类的定义放在.h文件中。而类的实现放在同名的cpp文件中?

 

在c或c++中,头文件重复包含问题是程序员必须避免的问题,也是很容易犯错的问题 .

 (1)为什么要避免头文件重复包含呢?

      1.在编译c或c++程序时候,编译器首先要对程序进行预处理,预处理其中一项工作便是将源程序中 #include的头文件完整的展开,如果多次包含相同的头文件,会导致编译器在后面的编译步骤多次编译该头文件,工程代码量小还好,工程量一大会使整个项目编译速度变的缓慢,后期的维护修改变得困难。
       2.第一点讲的头文件重复包含的坏处其实还能忍,但是头文件重复包含带来的最大坏处是会使程序在编译链接的时候崩溃

 1 //a.h
 2 #include<stdio.h>
 3 int A=1;
 4  
 5  
 6 //b.h
 7 #include "a.h"
 8 void f(){printf("%d",A);}
 9  
10 //main.c
11 #include<stdio.h>
12 #include"a.h"
13 #include"b.h"
14 void main(){f();}

此时输入gcc -c main.c进行编译,会提示A重复定义,程序崩溃:

 

 然后输入gcc -E main.c -o main.i看下预处理内容:

 

 可以看到6015行和6021行重复出现int A=1;的定义,违背了一次定义的原则,所以会出错。

(2)那么如何避免它呢?
通常有两种做法:条件编译和#pragma once

         1.  使用宏定义避免重复引入

       

        其中,_NAME_H 是宏的名称。需要注意的是,这里设置的宏名必须是独一无二的,不要和项目中其他宏的名称相同。

        当程序中第一次 #include 该文件时,由于 _NAME_H 尚未定义,所以会定义 _NAME_H 并执行“头文件内容”部分的代码;当发生多次 #include 时,因为前面已经定义了 _NAME_H,所以不会再重复执行“头文件内容”部分的代码

           2.  使用#pragma once避免重复引入

         除了前面第一种最常用的方式之外,还可以使用 #pragma one 指令,将其附加到指定文件的最开头位置,则该文件就只会被 #include 一次

         

        注意:#pragma once 只能作用于某个具体的文件,而无法向 #ifndef 那样仅作用于指定的一段代码

          3.  两种方式得区别

          #ifndef 是通过定义独一无二的宏来避免重复引入的,这意味着每次引入头文件都要进行识别,所以效率不高。但考虑到 C 和 C++ 都支持宏定义,所以项目中使用 #ifndef 规避可能出现的“头文件重复引入”问题,不会影响项目的可移植性。

          和 ifndef 相比,#pragma once 不涉及宏定义,当编译器遇到它时就会立刻知道当前文件只引入一次,所以效率很高。但值得一提的是,并不是每个版本的编译器都能识别 #pragma once 指令,一些较老版本的编译器就不支持该指令(执行时会发出警告,但编译会继续进行),即 #pragma once 指令的兼容性不是很好

 

(3)避免头文件的重复包含是否一定可以避免变量、函数、类、结构体的重复定义?
         答案当然是否!

         继续上面的例子:

1 //a.h
2 #include<stdio.h>
3 #ifndef _A_H
4 #define _A_H
5  
6 int A = 1;
7  
8 #endif;
//b.h

#include<stdio.h>
#include "a.h"
void f();
 
//b.c

#include"b.h"
void f()
{
   printf("%d",A+1);
}
 1 //c.h
 2  
 3 #include<stdio.h>
 4 #include "a.h"
 5 void fc();
 6  
 7 //c.c
 8  
 9 #include"c.h"
10 void fc()
11 {
12    printf("%d",A+2);
13 }
//main.c
 
#include<stdio.h>
#include "b.h"
#include "c.h"
void main()
{
    fb();
    fc();
}

然后分别编译gcc -c b.c -o b.o和gcc -c main.c -o main.o,并未提示任何错误。
但是当生成可执行文件时候gcc b.o main.o -o main,编译器提示出错:

 

 

        为什么会出错呢?按照条件编译,a.h并没有重复包含,可是还是提示变量A重复定义了。
        在这里我们要注意一点,变量,函数,类,结构体的重复定义不仅会发生在源程序编译的时候,在目标程序链接的时候同样也有可能发生。我们知道c/c++编译的基本单元是.c或.cpp文件,各个基本单元的编译是相互独立的,#ifndef等条件编译的作用域仅在单个文件中,因此只能保证在一个基本单元(单独的.c或.cpp文件)中头文件不会被重复编译,但是无法保证两个或者更多基本单元中相同的头文件不会被重复编译

出错本质:编译器在编译.c或.cpp文件时,有个很重要的步骤,就是给这些文件中含有的已经定义了的变量分配内存空间,在a.h中A就是已经定义的变量,由于b.c和c.c独立,所以A相当于定义了两次,分配了两个不同的内存空间。在main.o链接b.o和c.o的时候,由于main函数调用了fb和fc函数,这两个函数又调用了A这个变量,对于main函数来说,A变量应该是唯一的,应该有唯一的内存空间,但是fb和fc中的A被分配了不同的内存,内存地址也就不同,main函数无法判断那个才是A的地址,产生了二义性,所以程序会出错】

那么到底怎么样才能避免重复定义呢?
    其实避免重复定义关键是要避免重复编译,防止头文件重复包含是有效避免重复编译的方法,但是最好的方法是记住:头文件尽量只有声明,不要有定义

 

(4)声明与定义

         “声明”:只是声明某个符号(变量或函数)的存在,即告诉编译器,这个符号是在其他文件中定义的,我这里先用着,你链接的时候再到别的地方去找找看它到底是什么吧。

         “定义”:则是要按C++语法完整地定义一个符号(变量或者函数),告诉编译器在此处分配存储空间建立变量和函数。

        头文件的作用:就是被其他的.cpp包含进去的, 本身并不参与编译。但实际上,它们的内容却在多个.cpp文件中得到了编译。通过"定义只能有一次”的规则,很容易可以得出:头文件中应该只放变量和函数的声明,而不能放它们的定义。因为一个头文件的内容实际上是会被引 入到多个不同的.cpp文件中的,并且它们都会被编译。放声明当然没事,如果放了定义,那么也就相当于在多个.cpp文件中出现了对同一个符号(变量或函数)的定义,因此就会报“重复定义的错误”。

   

          总结:声明是将一个名称引入程序;定义提供了一个实体(类型、变量、对象、函数)在程序中的唯一描述

          所以:一个符号,在整个程序中可以被声明多次,但只允许被定义一次

         大多数情况下,声明与定义是相同的,但是有少些情况下,声明并非定义。

声明:

 1  #ifndef _DEMO_H_
 2  #define _DEMO_H_
 3  
 4  void declaration(int a,int b);   //声明一个全局函数; 
 5  extern int number;               //声明一个全局变量;
 6  class test{                      
 7       ...; 
 8       static int a;               //类内声明一个静态类数据成员;
 9       void func(int ,int);        //类内声明一个成员函数 10       int b;                      //类内声明一个普通数据成员
11       ...}; 
12  
13 class A;               //类的声明;  
14 tepedef int INT;       //typedef声明; 
15 class B{
friend func(class *B); //类内友元函数声明 16 } 17 #endif

定义:

1) 定义了一个静态类数据成员;  int test::a = 4 或者 static int a=4;
2) 定义了一个non-inline成员函数; void declaration(int a,int b){return (a<b?a:b)}

 

(5)C/C++编译过程

         编译一个.cpp文件时, 经过处理、编译、汇编和链接 个步骤,生成一个可执行程序:

                                 

 

                                                                                              gcc编译过程

     1. 预处理:将(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码              

     2. 编译:   将预处理得到的源代码文件,进行“翻译转换”, 生成汇编代码(编译阶段要检查代码的规范性、是否有语法错误,如函数、变量是否被声明等)

     3. 汇编:   将汇编代码翻译成了机器码,表现为二进制文件

     4. 链接:    将汇编生成的.o文件及其他函数库文件链接起来,生成能在特定平台上运行的可执行程序(在链接程序时,链接器会在所有的目标文件中找寻函数的实现。如果找不到,那到就会报链接错误码(LinkerError))

 

(6)内部链接与外部链接

        1. 内部链接:内部链接意味着对符号名的访问仅限于当前编译单元。即:对于任何其他编译单元都是不可见的,在链接的时候不会与其它编译单元中同样的名称相冲突,则这个符号具有内部链接。   

        具体有:

                     1)静态(static)全局变量的定义、静态自由函数的定义、静态友元函数的定义
                     2)类的声明与定义
                     3)内联函数定义
                     4)Union共同体/结构体/枚举类型定义
                     5)const常量定义
                     6)各种声明

                    C++又补充规定,extern const联合修饰时,extern将压制const的内部链接属性。

         举例:

1 static int x;            //静态全局变量定义
2 static void func(){...};//静态自由函数定义 3 //静态友元函数函数定义 4 class A{...}; //类定义
class A; //类声明 5 inline void func(){...};//内联函数定义 6 Union AA{...}; //Union共同体定义 7 const int y; //const常量定义 8 enum Boolean{No, Yes}; //枚举类型定义
9 extern int z; //全局变量声明

        用内部链接定义的一个重要的例子就是类的定义。类的定义如下。因此,它不能够在同一作用域的编译单元内重复定义。如果需要在其他编译单元使用,类必须被定义在头文件且被其他文件包含。仅仅在其他文件中使用class Point;声明是不行的,原因就是类的定义是内部链接,不会在目标文件导出符号。也就不会被其他单元解析它们的未定义符号

class Point{
    int d_x;                              //内部链接 int d_y;
  public:
     Point(int x,int y):d_x(x),d_y(y){}   //内部链接
     int x() const{return d_x;}           //内部链接
     int y() const{return d_y;}           //内部链接
};

        因此:具有内部链接的符号无法作用于当前文件外部,要让其影响程序的其他部分,可以将其放在.h文件中。此时在所有包含此.h文件的源文件都有自己的定义且互不影响。

        2.  外部链接:外部链接意味着这个定义不局限于单个的编译单元。在.o文件中,具有外部链接的定义产生外部符号,这些外部符号可以被所有其他编译单元访问用来解析其他编译单元中未定义的符号,即:一个名称在链接时可以和其他编译单元交互,那么这个名称就具有外部链接。

              因此:因此它们在整个程序中必须是唯一的,否则将会导致重复定义

             具体有:

                       1.类的非内联函数(包括成员函数和静态成员函数)的定义
                       2.类的静态成员变量的定义
                       3.名字空间或全局非静态的自由函数,非静态变量,非友元函数的定义

             举例:

 class A
 {
     static int a;   //类的静态成员声明,内部链接
void fun(); //类的非内联成员函数声明,内部链接
static void fun2(); //类的非内联静态成员函数声明,内部链接
void fun2(){...}; //类内实现函数定义,若为内联则为内部链接,若为非内联则为外部链接
} int A::a = 1; //类的静态成员定义,外部链接
void A::fun(){...}; //类的非内联成员函数定义,外部链接
static void A::fun2(){...}; //类的非内联静态成员函数定义,外部链接

namespace A{...} //名字空间定义,外部链接
void fun3(){...}; //全局非静态自由函数定义,外部链接
int b; //全局非静态变量,外部链接

        有一些名字定义所表示的实体拥有外部链接,这样就意味着他可以跨越编译单元去进行代码的链接。

        所以,拥有外部链接的实体如果放在头文件中并且被多个.cpp文件包含,可能就会出现链接冲突错误,因为每个包含这个拥有外部链接实体的.cpp都会分配空间,当多个编译单元链接的时候,连接器就会面对多个相同的名字,无法正常链接到正确的对象。

       因此:由于cpp文件中存储的是成员函数的实现,而成员函数具有外部链接特性,会在目标文件产生符号。在此文件中此符号是定义过的。其他调用此成员函数的目标文件也会产生一个未定的符号。两目标文件连接后此符号就被解析

       判断一个符号是内部链接还是外部链接的一个很好的方法就是看该符号是否被写入.o文件,由于声明只对当前编译单元有用,因此声明并不将任何东西写入.o文件

 

(7)总结:头文件中应该写什么

 1 //test.h
 2 #ifndef TEST_H
 3 #define TEST_H
 4 int a;           //外部链接,不能在头文件中定义(是声明也是定义)
 5 extern int b=10; //外部链接,不能在头文件中定义(是定义)
extern int bb; //声明,内部链接,可以定在头文件中(是声明)
6 const int c=2; //内部链接,可以定在头文件中但应该避免 7 static int d=3; //内部链接,可以定在头文件中但应该避免 8 static void func(){} //同上 9 void func2(){} //同a 10 void func3(); //可以,仅仅是声明。并不会导致符号名被写入目标文件。 11 class A //可以,类定义,内部链接 12 { 13 public: 14 static int e; //可以,声明,内部链接 15 int f; //同上 16 void func4(); //同上 17 }; 18 int A::e=10; //不可以,外部链接,符号名会写入目标文件 19 void A:func4()//不可以,外部链接 20 { 21 //...... 22 } 23 #endif

 

         声明本身不会影响到.o文件的内容,在源文件中每一个声明都只是命名一个外部符号,使当前的编译单元在需要的时候可以访问相应的外部定义

         函数调用会导致一个未定义的符号被写入到.o文件。如果func在该文件中没有被使用,那么不会被写入到.o文件。而有对此函数的调用,就会将此符号写入目标文件,此后该.o文件与定义此符号的.o文件被连接在一起,前面未定义的符号被解析。

 

       宏是内部链接还是外部链接

       答:都不是,宏在预处理环节时就被替换掉了,而内部链接与外部链接是针对编译环节与链接环节而言的

 

(8)总结

       基于以上的分析,我们可以知道:
       1. 将具有外部链接的定义放在头文件中几乎都是编程错误。因为如果该头文件中被多个源文件包含,那么就会存在多个定义,链接时就会出错。
       2. 在头文件中放置内部链接的定义却是合法的,但不推荐使用的。因为头文件被包含到多个源文件中时,不仅仅会污染全局命名空间,而且会在每个编译单元中有自己的实体存在。大量消耗内存空间,还会影响机器性能
  非模板类型 模板类型
.h

全局变量声明(带extern字符)

全局自由函数声明

内联函数声明与定义(inline)

static/const变量声明与定义(声明可以,定义不建议)

typedef的声明

namespace的定义

带inline限定符的全局模板函数的声明与定义

类的定义与声明

类属性与方法的声明(类内)

类内函数定义(相当与inline)

带static/const限定符的数据成员初始化(类内)

带inline限定符的类成员函数定义(类外)

模板类的定义

模板类成员的声明与定义(定义可以放在类内或类外,类外不需要写inline)

.cpp

全局变量定义及初始化

全局自由函数的定义

类成员函数的定义

带static类成员属性的初始化

最后再给出一个C++编程建议,慎重考虑在头文件中定义有外部链接的实体:

      1. 如果头文件是像int a=1;这样的定义,被包含在多个.cpp文件后肯定会报出链接错误。

      2. 如果是static int a = 2;这样的定义就会在所有包含他的.cpp文件中生成一个副本,如果被大量源文件include的话,就会占据大量的空间,造成内存浪费。

       总之:头文件尽量只有声明,不要有定义

 

参考:

https://blog.csdn.net/iteye_21199/article/details/82438044?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-4.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-4.control

 附:

(1)在.h中和.cpp中include头文件有什么区别

 1 在 .h 里面 include 的好处是:
 2 如果很多.c,.cpp文件,都包含一批头文件,
 3 如果复制很容易遗漏
 4 如果输入,很容易出错
 5 
 6 如果全部在一个.h, include  那么每个.c,.cpp文件只需要一个#include 语句
 7 这样不仅输入量减少,
 8 而且代码也美观多了
 9 代码也主次分明了
10 毕竟,.c.cpp, 里面
11 要实现的函数,才是主要代码
12 
13 2)主要缺陷,
14 可能会包含完全不需要的头文件,
15 增加编译工作量
例子:
(1)如果你在a.h头文件中include了“stdio.h”,“iostream”,……一大堆
     那么你的a.cpp源文件只要include你的a.h,就相当于include了“stdio.h”,“iostream”,……一大堆
但是当其他文件include你的a.h的同时也就包含了“stdio.h”,“iostream”,……一大堆
     如果a.cpp包含了头文件a.h,a.h包含了头文件b.h,b.c也包含了b.h,那么当b.h发生改变时,a.c和b.c都会重新编译,增加编译工作量
所以:
1. 如果你需要让其他文件也include一大堆,那么写在a.h中就可以;
2. 如果只有a.cpp需要include一大堆,那么建议在a.cpp中include一大堆
3. 有时只需要前置声明就可以满足使用变量或者函数的需求,建议不要包含头文件

(2)头文件中可 以写内联函数(inline)的定义

          内联函数之所有具有内部链接,因为编译器在可能的时候,会将所有对函数的调用替换为函数体,不将任何符号写入.o文件

          因为inline函数是需要编译器在遇到它的地方根据它的定义把它内联展开的,而并非是普通函数那样可以先声明再链 接的(内联函数不会链接),所以编译器就需要在编译时看到内联函数的完整定义才行。如果内联函数像普通函数一样只能定义一次的话,这事儿就难办了。因为在 一个文件中还好,我可以把内联函数的定义写在最开始,这样可以保证后面使用的时候都可以见到定义;但是,如果我在其他的文件中还使用到了这个函数那怎么办 呢?这几乎没什么太好的解决办法,因此C++规定,内联函数可以在程序中定义多次,只要内联函数在一个.cpp文件中只出现一次,并且在所有的.cpp文 件中,这个内联函数的定义是一样的,就能通过编译。那么显然,把内联函数的定义放进一个头文件中是非常明智的做法。

(3)头文件中可以写类 (class)的定义

          因为在程序中创建一个类的对象时,编译器只有在这个类的定义完全可见的情况下,才能知道这个类的对象应该如何布局,所以,关于类的 定义的要求,跟内联函数是基本一样的。所以把类的定义放进头文件,在使用到这个类的.cpp文件中去包含这个头文件,是一个很好的做法。在这里,值得一提 的是,类的定义中包含着数据成员和函数成员。数据成员是要等到具体的对象被创建时才会被定义(分配空间),但函数成员却是需要在一开始就被定义的,这也就 是我们通常所说的类的实现。一般,我们的做法是,把类的定义放在头文件中,而把函数成员的实现代码放在一个.cpp文件中。这是可以的,也是很好的办法。 不过,还有另一种办法。那就是直接把函数成员的实现代码也写进类定义里面。在C++的类中,如果函数成员在类的定义体中被定义,那么编译器会视这个函数为 内联的。因此,把函数成员的定义写进类定义体,一起放进头文件中,是合法的。注意一下,如果把函数成员的定义写在类定义的头文件中,而没有写进类定义中, 这是不合法的,因为这个函数成员此时就不是内联的了。一旦头文件被两个或两个以上的.cpp文件包含,这个函数成员就被重定义了

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM