巧用头文件,多文件编译少发愁 | 浅谈实用“StdAfx” !

      本文旨在分享我在编程过程中对于多文件编译的一点经验。文中谈到了因头文件重复包含而引起的重复定义问题及其解决方案,另外由此引伸出我对于StdAfx的相关认识。

      如果你同我一样,有颗强烈的好奇心;如果你同我一样,对使用AppWizard(应用程序向导)创建程序模板时生成的“StdAfx.cpp”、“StdAfx.h”感到疑惑不解。那么我会很开心,不要误会,我是开心于下面这些文字也许对你有点帮助。如果你还没有接触Visual C++ 6.0,那也没关系,只要你在编程中使用过自定义头文件,有过多文件编译的经历,那么下面这些文字同样值得一阅。

一、缘于重复包含的 重复定义 错误

1、 “重复包含”是个啥?

       “重复包含”顾名思义,即头文件在一个源文件或多个源文件中被多次包含,使得在编译时需要对该头文件中的代码进行重复编译,显然这是一种重复劳动,缺乏效率,而且还可能引起一系列问题。如果对我给出的解释感到晕乎,那么先来看几个重复包含引发错误的例子:

1)C语言中的重复包含 

例1.1.1(单源文件的重复包含):
创建一个源文件main.c,一个头文件head.h,如下:
//头文件:head.h
int g_iNum = 1;
//********************
 
//源文件:main.c
//********************
#include <stdio.h>
#include <stdlib.h>
#include "head.h"
#include "head.h"   // 重复包含头文件head.h
int main()
{
       printf(“%d/n”, g_iNum);
       system(“pause”);
       return 0;
}

对例1.1.1的进行编译,编译器报错,提示类似“redefined”(重复定义)这样的错误。这是因为头文件head.h在源文件main.c中重复包含两次,编译器处理时,头文件head.h在源文件main.c中被展开,由于是展开两次,因此语句“int g_iNum = 1;”出现两次,大致情况如下:

int g_iNum = 1;
int g_iNum = 1;     // 重复定义
int main()
{
       printf(“%d/n”, g_iNum);
       system(“pause”);
       return 0;
}

显然这样是错误的,int型变量g_iNum在源文件main.c中连续定义两次。当然,这种错误一般人都不会犯,谁会故意将一个头文件在同一个源文件中包含两次呢?但是,如果将一个头文件包含到另一个头文件里,再将这两个头文件分别包含在一个源文件中,这样所产生的错误就比较常发生了,而且不容易察觉到,也许你就曾经犯过这样的错误。来看一下这种错误的例子。

例1.1.2(单源文件的头文件嵌套包含):
创建一个源文件main.c,两个头文件head1.h和head2.h,如下:
//头文件:head1.h
int g_iNum1 = 1;
 
//头文件:head2.h
#include “head1.h” // 嵌套包含头文件head1.h
int g_iNum2 = 2;
 
//源文件:main.c
#include <stdio.h>
#include <stdlib.h>
#include "head1.h"
#include "head2.h"
int main()
{
       printf(“%d/n”, g_iNum);
       system(“pause”);
       return 0;
}

在例1.1.2中,如果单独观察每个文件,不容易发现问题所在,需要假想编译器处理时的情况。首先头文件head1.h在源文件main.c中展开,于是源文件main.c中有拥有了语句“int g_iNum = 1;”。接着展开头文件head2.h,由于头文件head2.h中包含了头文件head1.h,故再次对头文件head1.h进行展开。此时,源文件main.c中将拥有两条“int g_iNum = 1;”语句,显然int型变量“g_iNum”被重复定义,编译器报错。

 一般的解决方案是将头包含命令“#include “head1.h””从头文件head2.h中删除。

(2)C++中的重复包含

因为C++是兼容C语言的,所以上面的两个例子产生的问题在C++中同样会发生。而较之于单纯的C语言,C++中重复包问题更复杂。 

我们通常在定义类时,将类的声明和实现分开,声明部分放在头文件中,实现部分放在源文件中。这是一种好习惯,但是没处理好却也会让人大伤脑筋。来看下面的几个例子:

例1.1.3(单源文件的 类声明头文件 嵌套包含):

创建工程,定义一个点类CPoint,将声明和实现分别放在CPoint.h和CPoint.cpp中,定义一个线类CLine,将声明和实现分别放在CLine.h和CLine.cpp中,主函数在源文件main.cpp中:

//头文件:CPoint.h * 描  述:点类CPoint的声明
class CPoint
{
protected:
       double m_dX;
       double m_dY;
public:
       CPoint(double x = 0, double y = 0);
       virtual void set(double x, double y);
       double getX();
       double getY();
};
 
//*   源文件:CPoint.cpp * 描  述:点类CPoint的实现
#include "CPoint.h"
CPoint::CPoint(double x, double y):m_dX(x), m_dY(y)
{
}
void CPoint::set(double x, double y)
{
       m_dX = x;
       m_dY = y;
}
double CPoint::getX()
{
       return m_dX;
}
double CPoint::getY()
{
       return m_dY;
}
 
//*头文件:CLine.h * 描述:线类CLine的声明,CLine由CPoint的派生而来。
#include "CPoint.h"       // 头文件嵌套包含
class CLine:public CPoint
{
protected:
       double m_dLength;
public:
       CLine(double x = 0, double y = 0, double l = 0);
       void set(double x, double y, double l);
       double getLength();
};
 
//源文件:CLine.cpp  *   描  述:线类CLine的实现
#include "CLine.h"
CLine::CLine(double x, double y, double l):CPoint(x, y)
{
       m_dLength = l;
}
void CLine::set(double x, double y, double l)
{
       CPoint::set(x, y);
       m_dLength = l;
}
double CLine::getLength()
{
       return m_dLength;
}
 
//* 源文件:main.cpp
#include <iostream>
#include <stdlib.h>
#include "CPoint.h"
#include "CLine.h"
using namespace std;
int main()
{
       CLine line;
       line.set(1.2, 2.3, 3.4);
       cout << line.getX() << endl;
       cout << line.getY() << endl;
       cout << line.getLength() << endl;
       system(“pause”);
       return 0;
}

对例1.1.3的工程进行编译,编译器报错,提示“CPoint”类重复定义。问题出在头文件CLine.h中,由于在其中嵌套包含了头文件CPoint.h,使得在编译源文件main.cpp时出错。首先,编译器遇到头包含命令“#include "CPoint.h"”,将头文件CPoint.h在源文件main.cpp中展开。接着,遇到头包含命令“#include "CLine.h"”,也将头文件CLine.h在源文件main.cpp中展开。由于头文件CLine.h中有头包含命令“#include "CPoint.h"”,于是再次将头文件CPoint.h源文件main.cpp中展开。此时,点类CPoint的声明部分在源文件main.cpp中出现了两次,重复了,于是报错。

对于这种错误,一般有两种解决方案。一是将头文件CLine.h中的头包含命令“#include "CPoint.h"”删除,再在源文件CLine.cpp中的头包含命令“#include “CLine.h””之上添加头包含命令“#include "CPoint.h"”。二是直接将源文件main.cpp中的头包含命令“#include "CPoint.h"”删除。对于简单的几个文件而言,此方案足矣。然而对于一个包含很多文件的大工程工程,此法并非良策。在文章后面将会介绍到一种优越的解决方案。

如果是将类的声明和实现都放在一个文件中会怎样呢?看下面例1.1.4:

例1.1.4(类的声明和实现不分开):

修改例1.1.3中的错误,并将类的声明和定义都放在同一个头文件中。

//*   头文件:CPoint.h *  描  述:点类CPoint的声明和实现
// 类的声明部分
class CPoint
{
protected:
       double m_dX;
       double m_dY;
public:
       CPoint(double x = 0, double y = 0);
       virtual void set(double x, double y);
       double getX();
       double getY();
};
// 类的实现部分
CPoint::CPoint(double x, double y):m_dX(x), m_dY(y)
{
}
void CPoint::set(double x, double y)
{
       m_dX = x;
       m_dY = y;
}
double CPoint::getX()
{
       return m_dX;
}
double CPoint::getY()
{
       return m_dY;
}
 
//*   头文件:CLine.h  *   描  述:线类CLine的声明和实现,CLine由CPoint的派生而来 
#include "CPoint.h"
// 类的声明部分
class CLine:public CPoint
{
protected:
       double m_dLength;
public:
       CLine(double x = 0, double y = 0, double l = 0);
       void set(double x, double y, double l);
       double getLength();
};
// 类的实现部分
CLine::CLine(double x, double y, double l):CPoint(x, y)
{
       m_dLength = l;
}
void CLine::set(double x, double y, double l)
{
       CPoint::set(x, y);
       m_dLength = l;
}
double CLine::getLength()
{
       return m_dLength;
}
 
//*源文件:main.cpp
#include <iostream>
#include <stdlib.h>
// 不再包含头文件CPoint.h
#include "CLine.h"
using namespace std;
 
int main()
{
       CLine line;
       line.set(1.2, 2.3, 3.4);
       cout << line.getX() << endl;
       cout << line.getY() << endl;
       cout << line.getLength() << endl;
       system(“pause”);
       return 0;
}

将例1.1.4同例1.1.3比较,似乎例1.1.4更加简洁方便,可以少写些头包含命令。既然如此,那为什么平常我们会被建议将类的声明和实现分开来呢?这并不是空穴来风、混淆视听。当工程中含有多个源文件,而有两个或两个以上的源文件中包含了同一个类的头文件(包括声明和实现),那么问题就会凸显出来。为了演示这个问题,我们在例1.1.4的基础上,再加入一个源文件test.cpp和对应的头文件test.h,并在其中引用“CLine”这个类。

//********************
//*   源文件:test.cpp   *
//********************
 
#include <iostream>
#include "CLine.h"        // 在源文件main.c中也包含头文件CLine.h
 
using namespace std;
 
void test()
{
       CLine line;
       line.set(2.4, 1.5, 4.7);
       cout << line.getX() << endl;
       cout << line.getY() << endl;
       cout << line.getLength() << endl;
}
 
//********************************
//*   头文件:test.h                            *
//*   描  述:test.cpp中的函数声明   *
//********************************
void test();

如果对源文件test.cpp单独编译不会出错,而当构建整个工程时却出错了,错误提示大意是CPoint类和CLine类的成员函数已经在目标文件main.obj中定义了。为什么会这样呢?让我们追随编译器的脚步来看个究竟:编译器先对每个源文件单独编译,由于源文件main.cpp和源文件你test.cpp中都有头包含命令“#include “CLine.h””,对头文件CLine.h进行展开后,在这两个源文件文件中都存在点类CPoint和线类CLine的声明和实现代码,这一点并不影响各个文件单独编译。单独编译之后得到两个目标文件main.obj和test.obj。接下来将目标文件连接为一个文件,连接过程中发现在该文件中点类CPoint和线类CLine都被定义了两次,于是就报错。

此时只得把按更正后的例1.1.3将点类CPoint和线类CLine的声明和实现分开来。因此,“将类的声明部分放在头文件中,而将实现部分放在源文件中”是很有必要的,特别是在多文件编程时。

尽管如此,然而对于类模板却是另一番景象,类模板的声明和实现需放在同一个头文件中。从网络上的资料得知,如果要将类模板的声明部分和实现部分分开的话,需要使用关键字export。但是VC6等编译器都不支持关键字export,所以目前只能将二者放在一个头文件中。

2、解决重复包含的优化方案 

当一个工程的文件不多时,上述几个重复定义的解决方案并不会表现出明显的缺陷。而随着工程中文件的不断增加,这些解决方案便越发显得笨拙而费时,甚至让人无从下手。有没有更好的解决方案呢?当然是有的,否则就不会有这篇文章了!

接触过VC的朋友可能会发现,在使用AppWizard(应用程序向导)创建程序模板时会自动生成源文件StdAfx.cpp和头文件StdAfx.h。如果你的好奇心够强烈的话,也许你会百度一下,了解有关StdAfx的相关资料。在此期间,或许你对阅读这些资料时感到吃力,不知所云。既然这样,那就先撇开那堆晦涩难懂的文字,来了解一下StdAfx对我们解决重复包含问题的巨大贡献吧。下面不讲理论,只浅显地模仿StdAfx的模式来解决重复包含问题。若是理论,我也谈不来。

对于工程,我们大可不必借助程序模板,自己动手,一步步地完善一个空工程。这里就以修正后的例1.1.3为基础,进行完善。

步骤一:

再另外创建两个文件,其中一个源文件StdAfx.cpp,一个头文件StdAfx.h。(这两个文件的文件名可随意取,并不是固定的,我只是仿照了VC中自动创建的那两个文件,这样来得亲切,而且有助于理解“StdAfx”。 

步骤二:

在每个头文件的首尾处添加条件编译命令,使得每个头文件中的代码都被夹在中间,像下面这样:

//********************************
//*   头文件:CPoint.h                       *
//*   描  述:点类CPoint的声明      *
//********************************
// 条件编译
#ifndef CPOINT_H
#define CPOINT_H
class CPoint
{
protected:
       double m_dX;
       double m_dY;
public:
       CPoint(double x = 0, double y = 0);
       virtual void set(double x, double y);
       double getX();
       double getY();
};
#endif
 
//********************************************************
//*   头文件:CLine.h                                                                  *
//*   描  述:线类CLine的声明,CLine由CPoint的派生而来。*
//********************************************************
// 条件编译
#ifndef CLINE_H
#define CLINE_H
class CLine:public CPoint
{
protected:
       double m_dLength;
public:
       CLine(double x = 0, double y = 0, double l = 0);
       void set(double x, double y, double l);
       double getLength();
};
#endif

看到了吧,红色部分就是条件编译命令。这些命令的作用是防止头文件重复包含。拿头文件CLine.h为例来稍微描述一下过程:当编译器在源文件(cpp文件)中第一次遇到头包含命令“#include “CLine.h””时,打开头文件CLine.h,准备对其在源文件中进行展开、编译。此时发现宏“CLINE_H”没有被定义,于是先定义这个宏,再将其后的代码进行编译,直到遇到“#endif”命令为止。当编译器在源文件中第二次遇到头包含命令“#include “CLine.h””时,再次打开头文件CLine.h,准备对其在源文件中进行展开、编译。然而此时发现宏“CLINE_H”已经被定义过了,于是后面的代码被跳过,不编译,直接跳至“#endif”命令。这样中间的代码就不会被再次编译了,也就不会出现重复定义的问题。 

另外补充一下,除了“#ifndef”“#endif”这对条件编译命令外,还可以使用其他类似功能的条件编译指令。下面几种方式都可用于避免重复包含:

#ifndef MACRO
#define MACRO
// ......
// ............代码
// ……
#endif
 
#if !define(MACRO)
#define MACRO 
// ……
// 代码
// ……
#endif
 
#pragma once
// ……
// 代码
// …… 

前两种是等价的,后一种中“#pragma once”的意思是:在编译一个源文件时,只对该文件包含(打开)一次。至于它于前两种的区别,不是本文主题所在,这里就不讲了。

步骤三:

 删除所有文件(源文件和头文件)中的头包含命令。并在头文件StdAfx.h中包含当前工程所需要的所有头文件。如下:

//****************************
//*   头文件:StdAfx.h               *
//*   描  述:包含所有头文件    *
//****************************
// 条件编译
#ifndef STDAFX_H
#define STDAFX_H
 
// 包含工程所需所有头文件
#include <iostream>
#include <stdlib.h>
using namespace std;
#include "CPoint.h"
#include "CLine.h"
#include “test.h”
 
#endif STDAFX_H

步骤四:

在所有源文件(cpp文件或者c文件)首部包含头文件StdAfx.h,且仅包含这个头文件。

 到此,工程已经可以很好的工作了,而且上面的四个步骤对于C语言和C++,对于其他编译环境(如gcc、g++,不只在vc中)都适用。我们来整体看一下我们修改后的各个文件:

//********************************
//*   头文件:CPoint.h                       *
//*   描  述:点类CPoint的声明      *
//********************************
// 条件编译
#ifndef CPOINT_H
#define CPOINT_H
class CPoint
{
protected:
       double m_dX;
       double m_dY;
public:
       CPoint(double x = 0, double y = 0);
       virtual void set(double x, double y);
       double getX();
       double getY();
};
#endif
 
//********************************************************
//*   头文件:CLine.h                                                                  *
//*   描  述:线类CLine的声明,CLine由CPoint的派生而来。*
//********************************************************
// 条件编译
#ifndef CLINE_H
#define CLINE_H
class CLine:public CPoint
{
protected:
       double m_dLength;
public:
       CLine(double x = 0, double y = 0, double l = 0);
       void set(double x, double y, double l);
       double getLength();
};
#endif
 
 
//****************************
//*   头文件:StdAfx.h               *
//*   描  述:包含所有头文件    *
//****************************
// 条件编译
#ifndef STDAFX_H
#define STDAFX_H
 
// 包含工程所需所有头文件
#include <iostream>
#include <stdlib.h>
using namespace std;
#include "CPoint.h"
#include "CLine.h"
#include “test.h”
 
#endif STDAFX_H 
 
//********************************
//*   头文件:test.h                            *
//*   描  述:test.cpp中的函数声明   *
//********************************
 
#ifndef TEST_H
#define TEST_H
 
void test();
 
#endif 
 
//************************
//*   源文件:main.cpp        *
//************************
#include "StdAfx.h"
int main()
{
       CLine line;
       line.set(1.2, 2.3, 3.4);
       cout << line.getX() << endl;
       cout << line.getY() << endl;
       cout << line.getLength() << endl;
       test();
       system(“pause”);
       return 0;
}
 
//********************
//*   源文件:test.cpp   *
//********************
 #include "StdAfx.h"
 
void test()
{
       CLine line;
       line.set(2.4, 1.5, 4.7);
       cout << line.getX() << endl;
       cout << line.getY() << endl;
       cout << line.getLength() << endl;
}
//************************
//*   源文件:StdAfx.cpp     *
//************************
 
#include "StdAfx.h"

二、神行太保——预编译头文件

1、剥开StdAfx的神秘面纱

如果你潜意识里感觉有什么东西不太搭调的话,那么你直觉是正确的。到此为止,都看不出源文件StdAfx.cpp有何功用,难道它是多余的?其实不然,它在VC中可以发挥更大的作用,可以使得一个庞大的工程在一次漫长的编译过后,接下来的编译更畅快,大大提高编译效率。不过要发挥它的作用必须借助于VC的环境设置,并不是所有开发环境都支持该功能。

下面是摘用“百度百科”的几段话来说明StaAfx的作用。如果没看明白也没关系,接下来不讲理论,只谈浅显的运用。

“Microsoft C 和 C++ 编译器提供了用于预编译任何 C 或 C++ 代码(包括内联代码)的选项。利用此性能特性,可以编译稳定的代码体,将已编译状态的代码存储在文件中,以及在随后的编译中,将预编译的代码与仍在开发的代码结合起来。由于不需要重新编译稳定代码,因此后面每次编译的速度都要快一些。”

“VC创建项目时自动创建的预编译头文件,在编译其他文件之前,VC先预编译此文件。头文件stdafx.h引入了项目中需要的一些通用的头文件,比如window.h等,在自己的头文件中包括stdafx.h就包含了那些通用的头文件。

所谓头文件预编译,就是把一个工程(Project)中使用的一些MFC标准头文件(如Windows.H、Afxwin.H)预先编译,以后该工程编译时,不再编译这部分头文件,仅仅使用预编译的结果。这样可以加快编译速度,节省时间。

预编译头文件通过编译stdafx.cpp生成,以工程名命名,由于预编译的头文件的后缀是“pch”,所以编译结果文件是projectname.pch。

编译器通过一个头文件stdafx.h来使用预编译头文件。stdafx.h这个头文件名是可以在project的编译设置里指定的。编译器认为,所有在指令#include "stdafx.h"前的代码都是预编译的,它跳过#include "stdafx. h"指令,使用projectname.pch编译这条指令之后的所有代码。”

如果你想从理论上了解StdAfx的作用的话,可以参阅以下文章:

《关于#include "stdafx.h"》

http://blog.csdn.net/magicsutra/archive/2007/10/24/1842301.aspx

《百度百科——stdafx》

http://baike.baidu.com/view/1499221.htm?fr=ala0_1

如果你对上述参考阅读不感兴趣,那么接下来我们就来看一下如何使StdAfx.cpp发挥作用。

2、领头羊StdAfx.cpp——创建预编译头文件

步骤一:

       鼠标左键单击Visual C++6.0菜单栏上的“Project”(工程),在弹出的菜单中选择“Settings”(设置),弹出“Project Settings”(工程设置)对话框。另外也可以鼠标右键单击“FileView”(文件视图)中的项目图标,在弹出的菜单中选择“Settings”,同样也可以打开“Project Settings”对话框。

步骤二:

       在“Project Settings”对话框中选择“C/C++”选项卡。在该选项卡菜单中的“Category”(类别)列表框中选择“Precompiled Headers”(预编译头)项,出现对应选项。

步骤三:

       在“Precompiled Headers”的对应选项中选择“Use pecompiled hader file (.pch)”(使用预编译头文件)单选按钮。在对应的“Through header”(通过头文件)编辑框中输入我们前面创建的头文件StdAfx.h。(前面已经提到,这个头文件可以任意命名,所以输入时依你自己的情况而定。) 

步骤四:

       在“Project Settings”对话框左侧展开工程的文件树。接着展开“Source Files”(源文件)文件夹,选择我们之前创建的源文件StdAfx.cpp(该文件名依情况而定)。

步骤五:

       在“Project Settings”对话框右侧与源文件StdAfx.cpp 对应的选项中选择“C/C++”选项卡。在该选项卡菜单中的“Category”(类别)列表框中选择“Precompiled Headers”(预编译头)项,于是出现对应选项。

步骤六:

       在“Precompiled Headers”对应选项中选择“Create pecompiled hader file (.pch)”(创建预编译头文件)单选按钮。在对应的“Through header”(通过头文件)编辑框中输入我们前面创建的头文件StdAfx.h。

步骤七:

       单击“OK”(确定)按钮,完成设置。

       此后便可使用预编译头文件,发挥StdAfx.cpp的作用。重新编译整个工程,在工程目录下的“Debug”文件夹下会发现一个以该工程命名的.pch文件,它便是预编译头文件。看得出,其实预编译头文件并不是.h头文件。

需要注意的是,其后在对头文件StdAfx.h进行改动后,须将整个StdAfx.cpp再重新编译一次,以更新预编译头文件,否则可能在单独编译其他源文件时可能会报错!

三、篇后语

       不知我的描述是否让你明白,希望本文对你有所帮助。如果你大致明白上述文字的意思,那么不妨动手实践一下。实践过程中如果发现您的结果与本文有所出入,敬请指出,欢迎一起交流讨论。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值