【VC++开源代码栏目提醒】:本文主要为网学会员提供“C++工程的文件组织 - 编程语言”,希望对需要C++工程的文件组织 - 编程语言网友有所帮助,学习一下!
这篇文章题目叫“
VC工程的文件组织”,其实内容不光是文件组织了,内容还还很广,我很早就想写这么篇文章,一方面是总结这几年下来的经验,另一方面就是能和别人交流交流,为了不让读者在阅读中丧失兴趣,我将在文章中加入大量生动的例子,所以这篇文章内容很散,但知识本身就是一种离散的积累之后才形成关系的连贯, 难道不是吗?此文的观点并不“权威”,只是我个人的观点,欢迎来信和留言,图共同进步。
1、全局变量的声明与定义一般来说,.h 文件是存放定义(Definition)的地方,而.cpp 文件这是存放实现的地方(Implementation)。
简单说是这样的,不过问题来了,如果你需要一个全局变量:HWND g_hwndMain那么应该放在.h 文件中,还是在.cpp 文件中?对单个变量来说,这既是声明也是定义也是实现。
按照我做法,把它放到.cpp 文件中,比如放到 main.cpp 中去。
至于.h 里,根本不需要提及它。
那如果需要在别的文件中使用到这个全局变量,那应该怎么办?那就声明一下吧:extern HWND g_hwndMain加上了 extern 前缀,就变成了声明,而不是定义。
如果工程中有声明 g_hwndMain,但不存在“HWND g_hwndMain”这个定义的话,在 link 的时候就会出错。
这里顺便提一下面试时候经常问到的一个问题,在全局变量前面加上关键字static,起到什么作用?这个问题不懂就是不懂,想不出来的。
其实很简单,就是让这个全局变量只能在这个模块(文件)中使用。
为什么?因为 static,extern两者不同同时修饰一个变量,尝试“extern static HWND g_hwndMain”,这样会导致编译错误。
“extern HWND g_hwndMain”作为一个声明,是不是就一定把它放在.h 文件中,不是,应该在哪里需要使用到 g_hwndMain,就在哪里声明。
大家想到了,其实使用全局变量会降低程序可读性,但开发中全局变量确实又能提供很多便利,因此用不着盲目排斥它。
2、包含关系习惯上来说把一个类写在一个.h 和一个.cpp 文件中,这样维护起来比较简单,比如我要写一个类叫 CRS232Comm,一个串口通信类,那么我就创建两个文件RS232Comm.h 和 RS232Comm.cpp,一个是 class CRS232Comm 这个类的定义,一个实现。
RS232Comm.cpp 中有“include RS232Comm.h”。
一般来说,总是.cpp 包含.h,那有没有包含.cpp 的时候呢?你见过吗?我不知道你见没见过,但我确实见过 J。
那就是在使用 IDL(接口描述语言,写过 COM都应该知道)的时候,MIDL(IDL 的编译器)会把 IDL 变成四个文件,其中有一个为“xxxx_i.c”,对就是它,你得 include 它,于是就出现了比较尴尬的“includexxxx_i.c”。
为什么生成的是“xxxx_i.c”而不是“xxxx_i.h”呢?我也不知道,如果你知道,你可以告诉我。
但除了 IDL 这种情况,我们还是没有什么理由需要包含.c 或者.cpp。
那除了.h 和上面这个特殊的情况,我们还能包含些什么?也许你马上想到了,比如:include哈,这个文件既不是.c 也不是.h,那我们能不能不用它,改用 iostream.h?一般来说只是为了使用 cout 这种对象是可以的。
但意义上有差别,iostream 和iostream.h 是不一样的,一个是模板库,一个是 C库头文件。
这里顺便提起一下以前的一件事,我刚进入上上家公司的时候,头要我们写个类模板,于是我就跟写一般的类一样把它分为两个文件,一个.h,一个.cpp,一个放定义,一个放实现。
你们认为我的做法有问题么?写过模板的人都应该知道,这样是不行的,因为 C中的实现是针对类的实现, 而不是针对模板的实现,分开写会导致连接时候找不到实现的。
因此必须将模板的“定义”与“实现”写在同一个文件中,那这个文件究竟叫“.h”好呢还是“.cpp”好呢?都不好,它既不是类的定义也不是类的实现,它是模板,模板就是模板,那干脆就不用文件扩展名,STL 就怎么干的,That’s OK!另外,个人认为,我们似乎没有什么理由创建自己的模板库了,模板的90的用途在于“容器”,而这一切 STL 都做好了,自己写个模板出来容易令人费解,况且真的有必要吗?我确实没想出太多的理由。
最近我甚至对 STL 有些排斥态度,我在一个类中用到了 STL 的 vector,程序在_DBCS 选项下编译运行没有任何问题,但如果用_UNICODE 选项编译,在程序关闭时候就会出现运行时错误,这个错误发生在“”之后,无影无踪,令我不知所措,把 vector 拿掉之后就没看到这个错误,我没看出我的代码有什么问题, 而只要不用_UNICODE, 就是正常的,实在是难题。
所以有时我觉得 STL 还不如 MFC 的集合类来得好用,可惜我并不怎么喜欢用 MFC。
3、进一步讨论包含关系,兼提及_UNICODE 和 UNICODE先提起一下一个最最最常见的头文件,是什么?stdio.h?不是不是,现在用windows.h 也不用它,那是不是 windows.h?也不是,写 MFC 程序的时候根本就不需要 windows.h。
猜对了!就是 stdafx.h。
这个文件曾经让我如此憎恨,我不明白为什么每个.cpp 都需要把它作为 first include,要是不 include 它,就会出现惊人的错误,但现在想想这个文件有它可爱之处,由于每个 cpp 都需要包含,并且第一包含它,(如果你没改变默认编译选项的话)那它就有它的作用了。
大家都知道,编译选项中可以指定_UNICODE 和 UNICODE,来编译 Unicode 程序。
有了_UNICODE 选项,_TEXTmy string宏就变成了 Lmy string;有了UNICODE 选项,著名的函数 CreateWindow 就变成了 CreateWindowW。
其实查看一下那些
VC的头文件就不难发现, 通常_UNICODE 和 UNICODE 都是同时需要的,那如果在编译选项里同时指定_UNICODE 和 UNICODE 又有些别扭,那怎么办?这个时候 stdafx.h 就可以好好利用起来了,在 stdafx.h 里(在包含其它文件前)写道:ifdef _UNICODEifndef UNICODEdefine UNICODEendifendififdef UNICODEifndef _UNICODEdefine _UNICODEendifendif这样只需要在编译选项里指定_UNICODE 或者 UNICODE 就可以了。
另外可以把一些到处都有可能用到的头文件在这里 include 掉,但一般来说没必要这样,在哪里需要用,在哪里 include 就好了。
这里提一下反重复包含的问题,如何反重复包含?当然是用宏了。
这是用
VC向导添加的文件的宏:if definedAFX_STDAFX_H__2E6B6299_78FC_4514_953C_D3F5DA24A99E__INCLUDED_defineAFX_STDAFX_H__2E6B6299_78FC_4514_953C_D3F5DA24A99E__INCLUDED_//……endif而自己创建的文件(不是用向导添加的)没有用 GUID,一般可以这样写,假如文件名叫 IOCPSocket.h,那么就这样写:ifndef __IOCPSOCKET_H__define __IOCPSOCKET_H__//……endif这是我长期养成的习惯,不强求怎样,只是一种风格,如文件名的命名,我是用大小写区分,这也是 windows 的风格,而 linux 的风格这是全小写,下划线区分。
顺便提提风格而已。
OK,重点了,前段时间碰到的一个问题,我要改写一个类库,其中一个任务就是把原来全部合并一起的文件拆分开来,让一个类使用一个.h 文件和一个.cpp 文件,但这样就有问题了,情况是这样:基类为 CBase,子类有 CDerivedA,CDerivedB,这倒没什么,但 CBase 中竟然有这种函数:class CBase //…… virtual CDerivedA GetA virtual CDerivedB GetBDerivedA.h 和 DerivedB.h 中需要 include Base.h,而 CBase 竟然也用到了它的子类……那根据哪里用到就哪里包含的法则, Base.h 是不是也要 include DerivedA.h和 DerivedB.h?这岂不是形成了“互相包含”?是的,如果出现了这种互相包含,
VC就会给出编译警告。
当然如果你做了“反重复包含” 的工作,编译警告就不会出现, 也不会出现“重复定义”,而取而代之的是“未定义”,程序还是通不过的。
比如下面这个简单的例子:基类//Base.hifndef __BASE_H__define __BASE_H__include DerivedA.hclass CBasepublic: CBase CBase virtual CDerivedA GetAreturn NULLendif子类//DerivedA.hifndef __DERIVED_A_H__define __DERIVED_A_H__include Base.hclass CDerivedA:public CBasepublic: CDerivedA CDerivedA virtual CDerivedA GetAreturn thisendif编译出现的错误大致如下,但并非一定,甚至每次都有可能不同,这和编译的顺序有关系:error C2143: syntax error : missing before error C2433: CDerivedA : virtual not permitted on data declarationserror C2501: CDerivedA : missing storage-class or type specifierserror C2501: GetA : missing storage-class or type specifiers总之出现了这种基类“需要”子类的情况的话,就不能这样 include 了。
取而代之的是使用一个类的声明:在 Base.h 中把“include DerivedA.h”去掉,用“classCDerivedA”取代它。
这样编译就没有问题了。
OK OK, 可能你又有问题了, 如果基类中的函数不是“virtual CDerivedA GetA”,而是“virtual CDerivedA GetA”,那怎么又通不过了?哇哈哈……老兄,你别扯了,我保证你找遍全世界的高手,也没有人能解决这个问题的,因为它逻辑上已经错误了,父在诞生的时候需要子,而父没诞生,哪来的子?又一个典型的鸡生蛋,蛋生鸡的问题。
至于指针为什么就可以,因为指针在 Win32中归根到底只是一个 long 型,它并不需要理解 CDerivedA 究竟何方神圣,只需要连接的时候找到就行了,反过来如果不是指针,CBase 就要尝试寻找 CDerivedA 并生成实例,这可能吗?4、
VC中的“文件视图”又到轻松点的话题了,最早我开始学
VC的时候是……上上上份工作以前,当时手头有一本书叫《
VC 6.0编程指南》 ,忘记谁写的了,和别的
VC教材差不多,这本书一开始也是教我们如何用向导生成 MFC 对话框程序,厄……本人认为这种教学方法不好,MFC 本来就封装了很多技术细节,再加上一开始就“向导”,就很难了解其内幕,看到一些 Win32 API 调用就更加不知所言。
但其中最尴尬的,是我企图按照书上讲的步骤去创建一个应用程序而屡不成功,为什么?因为
VC的类视图有 bug,经常出些问题,尤其是你使用向导添加派生类的时候,常常有一些本来存在的类在类视图消失,书上说“类视图中右击 XXX 类,在弹出菜单中选择 YYY”,我就是找不到该类,于是工程重做了一遍又一遍,快把我气炸了。
后来我看了《Windows Programming》才知道:“程序,不是这样编的。
”我开始使用文件视图,一个个文件地创建,手动编写,再一个个加入工程,一个个编译,终于恍然大悟,原来
VC如此用法。
所以之后我就习惯了文件视图,文件视图有个好处,就是从来就不会有文件莫名其妙地消失。
下面这张图是我做的一个小工程的文件组织:该 WorkSpace 下有3个 project,我只展开了一个,因为文件比较多,全部展开太长,我采用了很典型的存放方式, 一个 project 下有两个目录,一个是 Source Files,一个是 Header Files,一个放.cpp 文件,一个放.h 文件,当然 Source Files 中除了cpp,通常还有 rs(Resource Script)文件,如果写 COM,那还有 idl 文件,总之需要编译的,都放在 Source Files 目录下。
我们还可以看到,需要编译的文件的图标有个向下的箭头。
这样分开目录来存放而不是全部都放在同一个目录下的好处是便于查看。
OK,我承认这并没有什么技术含量,但如果组织得好,能给你开发带来些便利。
那么是否可以创建更多的目录呢?可以,但这又有什么必要呢?还让人难以理解,不如按照默认的做好了。
文件视图还可以这样用,即选定单个文件来编译,这样总比直接点“Build”生成一大堆的错误好看点,嗯,从排错的意义上来说。
结论:可以再 fileList 中对每个文件进行单独地编译。
5、目录结构我看过不少别人写的代码, 其中还有一些是
开源代码, 里边的内容可谓错综复杂,尤其一个叫“PGPnet”的开源项目,使用纯 C 编写,(包括界面)代码十分晦涩,大量使用的全局变量令人不知所措,程序何始何终让人无法捉摸,其中目录结构大约有六层之多,有些目录竟然是空的,我不知道其意义何在。
总之最后就是看不懂,摸不透。
也难怪这个开源项目早就停止维护了, 除了作者,谁还愿意去看?我认为,目录层次不宜过多,一层可能太少,两层合适,最多三层,四层太多,五层炼狱,六层地狱。
大致如下: LibProject.dsp LibProject.cpp LibProject.h OtherLib.cpp OtherLib.h DllProject.dsp DllProject.cpp DllProject.h OtherDll.cpp OtherDll.h MainWorkspace.dsw MainProject.dsp MainProject.cpp MainProject.h OtherMain.cpp OtherMain.h Common.h这是个典型的目录结构示意,一共两层, ,主目录通常 (除了 Debug 和 Release)用来存放 dsw 和主工程的文件。
什么是主工程?书上可没提这个概念,我这里为了方便描述提出来的, 比如带有程序界面的工程,生成 exe 能直接运行的工程,总之可以自己来定。
有主就有副,区别于主工程,别的工程就叫副工程,没人反对吧,如果有副工程,那么最好给他们各自创建一个目录,一个位于主工程目录下的子目录。
然后将这些副工程也添加到 Workspace 去,这样在 Workspace 看来,主和副是同一级别的,所以你清楚哪个是主就行了,
VC可不知道。
我们的最终目的是为了让主工程能顺利运行,所有的副工程都是为主工程服务的,那如何把它们的“劳动成果”结合起来呢?比如主工程需要一个 Lib Project 生成的“aaa.lib”, 还需要头文件“aaa.h”,而“aaa.h”是在目录下,“aaa.lib”是在下的或者目录下,那我们就需要对主工程进行些设置,让它知道它要的文件在哪里。
在文件视图中右击主工程,弹出菜单中选“Setting…”,在 C标签中,Category 选“Preprocessor”,“Additional include directories”中填入需要额外寻找头文件的目录,比如填入“LibProject”,这样在主工程中“include aaa.h”就会自动寻找到LibProjectaaa.h。
库的设置在 Link 标签,Category 中选择“Input”,在“Additionallibrary path” 中 填 入 “LibProjectDebug” ( 如 果 是 Release 版 本 的 话 就 填 ,“LibProjectRelease”) 然后在上边的“Object/Library”中加入“aaa.lib”,这样在 link的时候,
VC就会自动从“LibProjectDebug”目录下找到“aaa.lib”。
如果是 Dll Project 呢?Dll 的导入分为两种,一种是静态导入,一种是动态导入,我偏向于使用静态导入,当然我以前做过一个文件分类系统的项目,是使用动态导入的,因为对文件的类别有可能会增加,我不确定一共需要多少个 dll,所以动态导入。
先说静态导入。
我觉得静态导入和使用静态库基本相同,除了 exe 运行时候需要在同个目录底下有那个 dll 之外。
那么操作就基本同静态 lib,但如何把这个 dll复制到 exe 的那个目录去呢?手动复制么?一次两次还行,经常改呢?麻烦!所以
VC的 Custom Build 功能这个地方就用上了。
假设 dll 名字叫“bbb.dll”,打开dll 工程的 Project Setting,选择 Custom Build 标签,在 Commands 中输入:“copy 这是 Debug 选项的,OutDirbbb.dll ProjDir..Debugbbb.dll”, 如果是 Release,那就对应改一下目录。
也许你有话说了: dll 放在系统的 system32目录下不也 “把可以么?”啊,当然可以,不过我不推荐这种做法,因为我们的软件最好也能做到“绿色”。
_那动态导入的情况呢?我通常会在 exe 文件的目录下创建一个“dll”目录,把所有的要动态导入的 dll 都往那个目录放,那 LoadLibrary 的时候就很方便了,这样让 exe 所在的这个目录也干净一些。
说了这么多我也觉得其实这都算不上什么技术,只是对
VC这个 IDE 使用上的一些心得。
但希望对大家有用,本来还想多写些东西,但现在觉得换个文章标题,或发表在别处会更合适些。