第5章 基础——5.3. C++项目组成

[回到目录]

白话C++

 

5.3. C++项目组成

首先我们知道了,写一个C++程序,可能需要多个源文件,比如a.cpp、b.cpp。

有没有可能只用一个源文件呢?似乎是可以的,比如我们之前写的“Hello world”经典版等项目,不就只有一个main.cpp吗。

其实,就算是“Hello world”经典版这样一个小程序,我们也要支付链接器同志的出场费。因为,我们在代码中使用了std::cout,std::cout来自于C++标准库,而C++标准库,又可能调用了C的标准库——“标准库”文件,其实也是一种“目标文件”,通常它就是多个“目标文件”的打包——结论是,就算我们只写了一个“main.cpp”,编译器只编译出一个“main.o”,但是,仍然需要链接器将“main.o”和一些必要的标准库文件进行链接。

5.3.1.项目文件

项目文件并不是必须的,直接使用g++编译器写命令行,就可以完成C++源文件的编译或链接,但我们刚提过,C++的编译器是单个单个源文件编译的,非常不方便,所以,需要项目文件。

在Unix下,最流行的C++项目文件,称作“Makefile/制作文件”。那相当于是一种“批量处理文件”,并不需要任何IDE。在Windows下,通常每个不同的IDE都会制定自己的项目文件格式。Code::Blocks可以使用Makefile,但默认的,也是更方便的方式是使用Code::Blocks自定义的项目文件,扩展名为:“.cbp”(Code::Blocks Project)。

hint〖小提示〗:工作空间:管理多个项目

除了提供项目文件以外,Code::Blocks还提供了“工作空间文件”,用于同时管理多个相关的项目,其扩展名为:“.workspace”。

 

5.3.2.源文件、头文件

作为一种笼统的说法,我们往往将程序员所写的一切代码文件,都称为“源文件”,不过如果具体到“编译器是单个单个源文件进行编译”时,这里的“源文件”就仅限于扩展名为“.cpp”或“.c”,或“.cxx”等文件,而“.hpp”、“.h”、“.hxx”等文件被称为“头文件”。

从扩展名来区分有些本末倒置。我们刚刚说过,如果要代码的某处需要用到某个函数,而该函数在代码当前位置之前还未有定义,我们可以通过“声明”这个函数长什么样子,来骗过编译器(链接器会帮我们寻找那个函数的真实位置);那么,假设我们“a.cpp”里实现了100个函数,而在“b.cpp”和“c.cpp”里都要全部用上,是否意味着我们要写上200次“函数声明”呢?

当然不必,再往前的课程,我们提过:“函数声明”就像函数的名片,而“头文件”就是含有多个声明的名片夹,因为,我们可以将“a.cpp”中的100个函数声明,全都写到一个头文件中(通常就叫“a.hpp”),以后在任意需要用到相关函数的源文件里,通过:“#include a.hpp”导入所有全部声明。

由于“头文件”不是编译单元,所以类似像C++标准库这样主要以“头文件”形式提供的库,我们只需要让编译可以找到这些头文件即可;而对于另外一些,直接以“源文件”形式的扩展库,如果我们要在某个项目中使用它,就必须将它所提供“源文件”,最好先复制一份,然后加入项目文件,参与编译。

在《准备》章节中,我们安装了很多扩展库。我们注意到,几乎所有扩展库,都提供一个include子目录,通常这个目录之下(可能还会有子目录),会存在该扩展库的头文件。比如MySQL++库的include目录为:E:/cpp_ex_libs/MySQL++/3.0.6/include。

xczy〖课堂作业〗:查找MySQL的头文件

请进入MySQL++中的include目录,然后查找“mysql++.h”文件。

 

现在,假设以我们要用到MySQL++库,于是要包含它的一头文件,名为“mysql++”,源代码中是否写成如下?

#include “E:/cpp_ex_libs/MySQL++/3.0.6/include/mysql++.h”

用绝对路径不仅麻烦,而且让源代码变得很不通用。如果不写绝对路径,该如何让编译器找到mysql++.h这个文件呢?

原来,编译允许我们通过在命令行中,指定参数来告诉它,如果某个头文件找不到了,可以上哪里寻找。对于g++,这个参数是“-I头文件路径”,本例解决方法写出来类似:

g++.exe –I”E:/cpp_ex_libs/MySQL++/3.0.6/include/” ……

不过,我们并不直接写命令行,而是通过Code::Blocks调用编译器,因此,Code::Blocks要求我在项目文件中配置该项目所需要用到的扩展库的路径,具体的配置方法,我们将在IDE章节详谈。

5.3.3.使用头文件

头文件就像“名片”或“名片夹”,通常包含了一些数据声明、函数声明、类型定义(典型的如:struct/class)等内容。一个人的名片可以分发给很多人,一个头文件通常也要被多个源文件包含;再者,头文件之间还可以相互包含,这就带来了一种不太好的可能性:相同头文件往往会在同一个项目中被重复包含。

  • 唯一包含

生活中我们不会喜欢拥有同一个人的多张重复的名片。对C++来说,重复的包含一个头文件,不仅造成编译速度降低,还会带来编译错误:数据、函数声明允许重复,但一个类型不允许重复定义。

#define可以定义一个“宏符号”,并且可以使用 #ifndef来判断一个“符号”是否已经定义:

#define ABCD //定义一个宏符号:名为ABCD

#ifdef ABCD  //判断ABCD是否“已定义”
  /*
     这里的代码,仅当ABCD有定义
     才会接受编译,否则被直接略过
  */

#endif 

和本例中的 #ifdef指示符正好相反,#ifndef多出来的字母‘n’为not,它用来判断指定的符号是否“未定义”。我们可以方便地使用以下方法,来保证一个头文件只被实际包含一次。假设当前头文件名为:“my_header.h”

001 #ifndef  _MY_HEADER_H_
002 #define _MY_HEADER_H_
003

// 所要声明或定义的内容,放在此处


xxx #endif //_MY_HEADER_H_

我们首先假设这是一个项目中第一次包含到“my_eader.h”这个文件:

预编译器在处理“myHeader.h”时,首先就碰上下面这行预处理指令(通常是第一行):

#ifndef _MY_HEADER_H_

它判断宏符号“_MY_HEADER_H_”是否“未定义”,因为现在是第一次包含本文件,所以确实还没有定义过它,于是002行立即定义这个符号。然后才是本头文件的实质内容(从003行,一直到xxx前一行)。

接着,我们假设某一处代码,再次包含了这个头文件,但这回预编译器发现符号_MY_HEADER_H_ 已经定义过了,于它直接跳到xxx行(#endif)之后。

窍门显然在于:我们应该为每一个文件都取一个唯一的宏符号——通常也称为:“保护符”。 要让每一个头文件都“挂着”一个唯一的保护符,方法是让这个符号的名字和头文件名字有一个映射关系,习惯上是:将所有字母都改成大写,再把扩展名之前的‘.’改成‘_’,如果还想再酷一点,可以在前后分别再加一个下划线。

〖小提示〗:同名头文件怎么办?

有时候,位于不同目录下的两个头文件,确实有可能同名,这时我们需要使用更长一些的保护符名称。

 

  • 使用自定义的头文件

#include 接受两种形式的头文件指示

#include <library_header.h>
   #include "my_header.h"

通常我们对标准库头文件,使用尖括号(<>)形式,对当前项目我们自己所写的头文件,使用双引号("")形式。至于第三方库,如果我们已经在IDE中配置它的全局路径,也可以使用尖括号的形式,典型的如wxWidgets或boost库。

Code::Blocks提供了方便的“文件向导”用于生成头文件或源文件,但本节我们将学习如何“纯手工”地项目添加头文件及源文件。

先用向导创建一个控制台项目(命名为IncludeDemo1),一开始它只有一个文件:main.cpp。

点击主菜单“文件”-> “新建” ->“空白文件”(或热键:Ctrl + Shift + N),出现提问框:

“是否将新文件加入到当前项目(加入项目前,须先保存)?”

选择“是”,然后将文件存为:“my_file.hpp”。由于一个项目默认会有两个构建目标:Debug和Release版,所以接下来IDE会询问新建的文件要加入到哪些构建目标,请选择全部目标。

再新建一个文件,保存为“my_file.cpp”,同样加入全部目标。现在,项目文件树如下:

图 5-4 添加了my_file.cpp/.hpp之后的项目树
图 5-4 添加了my_file.cpp/.hpp之后的项目树

然后,我们将——

  • 在my_file.hpp中,定义MyStruct类,声明my_function函数;
  • 在my_file.cpp中,实现MyStruct类,实现my_function函数;
  • 在main.cpp中,使用MyStruct类,使用my_function函数。

 

请分别完成以下代码,为了方便排除输入代码不小心造成的错误,请每完成一个文件的内容之后,就按Ctrl + F9 进行编译,确实编译无误后,再进行下一步。

第一、my_file.hpp中的代码——声明、定义:

#ifndef _MY_FILE_HPP_
#define _MY_FILE_HPP_

//定义一个类
struct MyStruct
{
    MyStruct();
    ~MyStruct();
};


//声明一个函数:
void my_function(int year, int month, int day);

#endif //my_file.hpp

第二、my_file.cpp中的代码——实现

#include "my_file.hpp" //包含自定义的头文件

#include <iostream> //包含标准库文件

using namespace std;

MyStruct::MyStruct()
{
    cout << "MyStruct Construct." << endl;
}

MyStruct::~MyStruct()
{
    cout << "MyStruct Destruct." << endl;
}

void my_function(int year, int month, int day)
{
    cout << year << '-' << month << '-' << day << endl;
}

第三、main.cpp中的代码——使用

#include <iostream>

#include "my_file.hpp" //引入MyStruct和my_function 

using namespace std;

int main()
{
    MyStruct myStruct;
    
    my_function(1974, 4, 20);
    
    return 0;
}

5.3.4.库文件

C++扩展库的提代方式,可以是普通源文件,也可以是已经编译成目标文件的“库文件”。对于前者,使用起来和我们自己写的源文件没有两样,需要加入项目,参加编译;对于后者,只需要参加链接。链接形式上,又分成两种方式:静态链接库、动态链接库。之前我们在《准备》章节已经介绍过二者的区别。

  • 静态链接库

在“构建期”(也经常笼统地称为“编译期”)完成链接。即,当源文件编译完成之后,库文件和其它中间文件统一参加链接。因此,库文件也被合并到可执行文件(程序)。

静态链接库的文件扩展名,通常是“.lib”或“.a”。

  • 动态链接库

动态链接库的文件扩展名,通常是“.dll”或“.so”、“.o”。

这类库采用特定技术,允许在程序运行时,才将库与程序在内存中实现合并。合并所完成的主要任务,是“定位”。比如在主程序k.exe中,需要用到动态库m.dll中的一个签名为“void foo()”的函数,自然的,k.exe就需要知道“foo()”函数在m.dll中“地址”。由于程序(包括动态库)运行时,需要加载到内存中,因此这个地址,是一个“内存地址”。

如何定位函数(或其它内存对象,下面仅以函数为例)在动态库中的地址,又分为两种方法:

第一、自动导入

对于C++编程,当我们写一个复杂的程序,往往会将程序分成一个主项目(用于生成可执行文件)和好些子项目(用于生成动态库)。此时常用的方法是由可执行程序在启动时自动加载所需要的动态库,并完成定址。

程序如何知道需要加载哪些函数?又如何知道这些函数的地址?这就需要第三种库出现:“导入库/import library”(全称符号导入库)。“导入库”保存了某一动态库中全部(需要导出的)函数等符号的偏移地址。

偏移地址不是“内存地址”,它是一种各个函数在DLL文件中的地址,而当动态库被加载到内存时,整个动态库有一个起始地址。函数内存地址=DLL起始地址+函数偏移地址。

通常我们在编译一个动态库项目时,除了生成动态库文件以外,还会同时产生“导入库”。而当我们需要在一个执行文件使用这个动态库,并且想采用“自动导入”的方法,则需要将“导入库”以“静态链接”的方式,加入项目。对于g++,导入库通常以“.a”为扩展名。

C++对动态库自动导入实现没有统一标准,因此,这项技术通常无法在不同编译器之间使用。比如Borland C++ 或Visual C++编译出来的动态库,则g++编译出来的可执行文件无法以自动导入的方式调用。

第二、手工导入

手工导入动态库中函数等数据,其实是C语言的一项标准。C++因为兼容而获得了这项功能。要使用这项技术,要求必须以C语言的相关标准来导出一个函数或数据,通常被称为“C 语言接口”。C语言接口是操作系统暴露其编程接口时的事实标准。

程序在运行时,仅当在需要时,才通过一些特定的语句,将指定动态库加载到内存,再查找到所需的函数,然后调用这个函数。用完之后,还可以从内存中卸载掉这个动态库。这就是手工导入动态库这项技术最大的特色。

 

C++库的形式及用法有哪些,是时候重新看一眼了:

图 5-5 C++库形式及使用方法
图 5-5 C++库形式及使用方法

 

不管采用什么形式,提供“头文件”,以免使用者自己去写声明,这是最基本的要求了。对于C++标准库,及boost中的很多子库,由于使用了“泛型”技术,所以采用的是“纯头文件”的形式。

最后,我们一直没有提到的,但对于库的使用非常重要的内容是:库的说明文档。通常开源的库都可以在共官方网站上找到说明文档的链接

 

[回到目录]

白话C++
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南郁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值