C++查缺补漏之头文件

在C++里面,支持所谓的"分别编译",这样程序可以由多个文件组成,这些文件进行分别编译,最后再链接到一起组成可执行的文件(elf文件),我们在程序里面最常见到的就是在一个头文件class.h里面定义一个类,在另外一个源文件class.cc里面定义该类的方法和变量

//class.h

#ifndef _CLASS_H
#define _CLASS_H
class A
{
public:
    void printHello();
};
#endif

//class.cc

#include "class.h"
#include <iostream>
void A::printHello()
{
    std::cout<<"Hello"<<std::endl;
}

//main.cc

#include "class.h"
int main(int argc, char const *argv[])
{
    A a ;
    a.printHello();
    return 0;
}

//下面我们通过makefile来编译上面的程序

//makefile

#下面讲main.cc和class.cc编译生成的.o文件链接到一起,生成可执行文件main

main:main.o class.o
    g++ main.o class.o -o main

#下面是分别编译nain.cc和class.cc 注意这里的-c选项表明只是编译,而不生成可执行文件
main.o:main.cc
    g++ -c main.cc -o main.o
class.o:class.cc
    g++ -c class.cc -o class.o

.PHONY:clean

clean:
    rm -f *.o

虽然上面的程序也可以直接g++ class.cc main.cc -o main,但是这里为了更加突出说明分别编译,故使用makefile,要是源文件数量比较多,makefile的优势久比较明显了

在上面的程序中,main.cc使用了class A这个类,但是这个类printHello()函数的实现却是在class.cc这个源文件里面,main.cc只用知道class A的定义即可,那么这个定义就是在class.h这个头文件中,在预处理阶段就将头文件展开,下面我们来看一下预处理以后的文件

g++ -E main.cc > main.e

生成如下文件:

//main.e

//...

class A
{
public:
 void printHello();
};
# 2 "main.cc" 2
int main(int argc, char const *argv[])
{
 A a ;
 a.printHello();
 return 0;
}


//...

下面我们就来编写一个头文件需要注意那些东西:

1.头文件是用于声明而不是定义

这个比较好理解,如果我们在头文件里面定义了一个变量,而一个程序里面的多个文件又包含了这个头文件,那么这个在头文件里面定义的变量将会在多个文件里面定义,但是我们知道一个程序里面一个变量可以声明多次但是只能定义一次,虽然各个文件在编译阶段不会有任何问题,但是最后链接成可执行文件的时候就会报错。比如在头文件里面这样写就会有问题,除非你的头文件只被包含一次

extern int a = 10;//error,这里不是声明,而是定义

double dNumber;//error,定义

但是在头文件里面不应该有定义有三个例外:

  • 类的定义
  • 常量初始化的const变量
  • inline内联函数

上面的三个中类型(这里也可以加上一个模板)都有一个共同的特征,就是文件在编译的时候就需要知道他们的定义:

1.对于类定义来说,为了产生能定义或使用类的对象的代码,编译器需要知道组成该类型的数据成员。同样还要知道能够在这些对象上执行的操作。

2.常量初始化的const变量,我们知道const变量默认是定义该变量的文件的局部变量,而且一般来说,const变量在编译阶段就会被替换成为常量表达式,所以在实践中不会有任何存储空间用于存储常量表达式初始化的const变量,反正变量都不会改变,还不如直接替换为常量来节省空间

3.内联函数(inline),我们知道inline函数是在程序的每个调用点上“内联的”展开,所以内联函数在编译阶段就必须是有定义的,可见的。


2.避免头文件的多重包含

我们来看一下下面这种情况

//class.h

class A
{
public:
    void printHello();
};

//class1.h

#include "class.h"

/* other code */

//main.cc

#include "class.h"

#include "class1.h"

mian(){}

下面我们编译一下,发现报错了

g++ -c main.cc -o main.o
In file included from class1.h:1:0,
                 from main.cc:2:
class.h:4:7: 错误: ‘class A’重定义
class.h:4:7: 错误: ‘class A’的前一个定义
make: *** [main.o] 错误 1

报错的原因是因为class A的重复定义,因为我们在class.h里面定义了class A,让后class1.h包含了class.h,最后main.cc包含两个class.h和class1.h,最后两个头文件同时展开:

g++ -E main.cc > main.e

//main.e

class A
{
public:
 void printHello();
};
# 2 "main.cc" 2
# 1 "class1.h" 1
# 1 "class.h" 1

class A
{
public:
 void printHello();
};
# 1 "class1.h" 2
# 3 "main.cc" 2
int main(int argc, char const *argv[])
{
 A a ;
 a.printHello();
 return 0;
}

我们这时main.cc里里面就两个class A的定义,这肯定是编译不过的,要避免这种多重包含的错误导致的重复定义的错误,要人工避免那是不可能的,一旦文件变多,要理清有没有多重包含,那就痴人说梦了。但是我们可以借助下面方法解决多重包含的问题:

#ifndef _CLASS_H

#define _CLASS_H

/* 头文件内容 */

#ednif

这里在class.h里面第一次检测没有被包含,那么就define _CLASS_H,在class1.h里面再次#include "class.h"时候,就会检测是否定义了_CLASS_H,如果是,那么就跳过。

当然某些编译器也支持下面这种写法

#program once

/* 头文件内容 */

不过我还是建议使用上一种

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。