C++封装常用对象和对头文件以及预编译机制的探索

在C++实际开发中,难免会使用到一些你极为常用的算法(比如笔者经常使用的多线程技术),实现这些算法的类或是全局函数或是命名空间等等经常都要被使用多次,你会有哪些办法来使用呢?笔者有4个办法。

第一个方法就是你直接重新编写一个和原来一样的算法,但是这种方法又费时又费力,效率不高,只有初学者在没有办法的时候才会使用这种方法。第二种方法也是如此,就是复制一份代码到新写的文件中,这种方法的缺点是你不确定能否找到之前的代码,而且一点也不像IT人员的解决方案。

我们重点介绍第三种和第四种方法。

第三种方法是在需要的地方声明这个类或是全局函数。注意声明的时候要使用extern关键字来声明(任何东西都是如此:全局函数、类、结构……)比如下面的代码:

//a.cpp
#include <stdio.h>
void print(){
    pringf("HelloWorld");
}

//b.cpp
extern pingt();
int main(){
    print();
    return 0;
}
这个代码在b.cpp中声明了一个a.cpp文件中的函数并且使用了存储类声明符——extern。在这里我先为大家介绍一下C++的存储类。存储类说明的是一个对象的存储方法,通常有以下4种

1、extern 只声明不定义,比如声明一个变量却不分配内存,一般用于两个文件中的共享(a文件中定义了一个类,想要在b文件中访问,则必须在b文件首部声明一个extern的a文件中的类,上面示例使用的就是这种方法);

2、static 静态,表示把这个东西定义在堆而不是栈里。全局的对象一般都是静态的,但是如果显示声明,则表示我发在其他文件中使用extern来调用它。如果在一个本应该定义在栈的代码块中声明成static,也将声明在堆中(比如全局函数);

3、register 注册,把一个对象声明到寄存器中,但是如果使用&操作符在程序中显示地取地址,编译器也将把这个对象定义到内存而不是寄存器中。使用register一般可以加快处理速度和减少内存使用量。

4、auto 默认、自动,声明一个对象的默认形式,和static正好相反,不能把一个全局对象声明成auto。

那么,我们就明白了刚才那段代码中我们使用extern是为什么了。这种方法很常用,但是并不是最好的方法。

接下来,隆重介绍我们的第四种方法——头文件!什么?你说你没有听说过头文件(head)?不可能,只要你写的不是既不输出也不输入的console程序,也不是窗口中什么也没有的程序,那么你就必须用到头文件!看看你的C++编译器中include目录吧,那就是C++语言的标准头文件库,里边有C编写控制台程序时常用的stdio还有C++新标准的iostream等等。没错,你猜对了,我们第四种方法就是编写头文件。哦哦哦,别把这个方法看的太难!其实就是把第三种方法变了变而已……但是的确方便得多。而且有一些封装之类的思想。

好的,首先我们先来重新了解一下头文件,如果你是高手,请让你的思绪回到最开始编写HelloWorld的时候。那么,我首先要纠正一个问题——头文件的定义。头文件的定义不是“C++提供给程序员的类库,是一些API之类的东西……”云云,而是“将一些类、全局函数、宏等资源集中声明,并在在include后代替声明,类似于类库或叫做封装。”当然,这不是官方的定义,是我根据个人理解而得出的。还有一个误区——文件扩展名,很多windows新手都认为文件扩展名很重要,这其实是windows给我们的一个错觉。因为windows总是以扩展名定义文件并说明文件使用哪种命令打开,但如果你使用过其他的操作系统就知道了,C++源文件并不需要必须是cpp,当然,头文件也不需要都是h。都是文件,里边都存储的是二进制代码,编译器也能编译。比如C++标准的头文件ios啦、cmath等等都没有扩展名。

好了,所有需要的误区也都说完了,我们来说第四种方法。首先看一个例程:

//a.cpp
#include <iostream>
using namespace std;
void print(){
    cout<<"HelloWorld";
}

//main.cpp
int main(){
    print();
}

你有什么办法让编译器不报错呢?现在我们来编写一个头文件。

print();

没错,就一行,这就是一个头文件,我们把它保存成c.h。然后把上面的a.cpp和main.cpp改一下。

//a.cpp
#include <iostream>
#include "c.h" //注意:要把这三个文件放在同一目录

using std::cout;

void print(){
    cout<<"HelloWorld";
}

//main.cpp
#include "c.h"

int main(){
    print();
    return 0;
}

OK!你有没有发现你成功了呢?而且,你可以编写头文件了!(如果你失败了,联系我的qq:2276768747,验证全写本文连接地址)

接下来,我来先讲讲include这个预编译指令。什么?#include吗?我早学过了。是的,我先说的东西80%的人都知道,但是接着,我要说的80%的人不知道:#include我们一般使用两种语法——1、#include <{编译器系统include目录下的文件}>;2、#include “{本目录下或是编译器include目录的头文件}” 注意到他们的不同了吗?他们的不同是<>和””还有,第一种只在include目录下寻找头文件,而第二种在当前目录和include目录下寻找头文件。这个功能使得引入自行编写的头文件成为可能(没有也可以,可以放到include目录下),所以我们可以将声明甚至是定义放到头文件里面。还有一种情况,就是把声明和定义全部放到头文件里面,这样可以避免在使用模板和泛型的时候出现问题,请看下面的代码:

//c.h
//把声明和定义全部放在头文件里,可以使用模板template
#include <iostream>

using namespace std;

void print(){
    cout<<"HelloWorld";
}

//main.cpp
#include "c.h"

int main(){
    print();
    return 0;
}

还有一些我要说:我们可以继续模仿C++开发者——让我们编写的头文件更“正式”(当然,不是给你讲那成山的注释是怎么回事,更不是强逼迫你那么做,我要讲述一些头文件中经常使用的预编译指令)。首先我要给大家一个详细的表格:

#空指令,无任何效果

#include包含一个源代码文件

#define定义宏

#undef取消已定义的宏

#if如果给定条件为真,则编译下面代码

#ifdef如果宏已经定义,则编译下面代码

#ifndef如果宏没有定义,则编译下面代码

#elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码

#endif结束一个#if……#else条件编译块

#error停止编译并显示错误信息

接下来,我一点点的解释上面的宏定义指令,他可以让你的头文件具有判断功能并且使你的头文件变得更加正规和灵活。

首先,我们的老朋友#include,我们已经深入了解过了include的所有“底细”,在此不再赘述。

然后我们来看看#define。#define在C语言编程中很常用,一般用于定义一些具有特殊意义的全局函数或者常量,其正式名称是“宏定义语句”,我们并不多讲,只举一个例子:

#define PI 3.14 //定义π的值,常量

接着看#undef,它是#define的逆运算,即消除宏定义:

#define PI 3.14
//PI可用
#undef PI
//PI不再可用

#if是条件判断语句,这个是判断条件是否为真,为真执行#if到#endif之内的语句然后执行下方语句,否则直接执行下方语句,这个在头文件中传输信息时用的比较多,重点讲一下:

#define DEBUG 0 //定义DEBUG,为0,在C++中,0代表假,1代表真

main()
{
#if DEBUG //所以显然这里的条件是假,那么就不会输出Debugging(当然我不想老调试)
printf("Debugging ");
#endif //之后的语句将执行,也就是说程序执行后会输出Running
printf("Running ");
}

//如果把define改成#define DEBUG 1,将会输出:
//Debugging
//Running

我最开始也不能理解,但是用得多了,就能够很好地理解这个#if,其实它和我们C++的if语句一样,下面会讲到else……(实质上我也不知道会在哪里用到,只是写封装的时候避免一些重复引用的问题云云)

接着说一下ifdef和ifndef,这里注意:#ifdefined等价于#ifdef;#if!defined等价于#ifndef。也就是说ifdef的意思是判断是否定义,如果定义了,就执行下面语句,否则不执行(else除外)而ifndef正好相反。话不多少看示例:

#define DEBUG //注意,这里定义了DEBUG

main()
{
#ifdef DEBUG //很显然,这是真,将会输出yes
printf("yes ");
#endif
#ifnde fDEBUG //DEBUG没定义吗?当然定义了,所以no不会输出
printf("no ");
#endif
}
//输出结果:yes
//如果注释掉第一句,将会输出no

好了,我们终于可以讲else了(我不希望你因为不耐烦和自以为掌握了封装而不看下去,否则你将吃大亏的!你的头文件总会出现重名、重复引用之类的东西,由于头文件没有main主函数,只能通过预处理来进行判断条件等操作)。看看else,前面我就说了,预处理和C++语法一样,所以这代表“否则”的意思。一行代码+注释远远大于100行解释+说明(我的名言,记住了,先记住了,然后仔细看示例):

#define DEBUG //定义了DEBUG

main()
{
#ifndef DEBUG //刚才说过的,不多解释,将不会会输出NoDebugging
printf("Nodebugging ");
#else //否则,也就是说定义了DEBUG的时候,将会输出Debugging
printf("Debugging ");
#endif
printf("Running "); //在endif之后,将会输出RUnning
}
//输出结果:
//Debugging
//Running

这个也很简单吧!接下来,我们回忆一下我们用过的if……else if……这种语句格式。在判断性预编译机制中,我们也有这种类似的语句——#elif。不多说了,我们来看看示例代码:

#define TWO //定义一个TWO,记住了!

main()
{
#ifdef ONE //这里是判断是否定义ONE,所以不会输出1
printf("1 ");
#elifdefined TWO //我不知道这里是否能够缩写成elifdef,但是,的意思是else if defined TWO,也就是会输出2
printf("2 ");
#else //否则,这里不会执行,如果不懂的话就想想else if语句块
printf("3 ");
#endif //预编译判断结束
}

接着是这个:

#error指令将使编译器显示一条错误信息,然后停止编译。

#line指令可以改变编译器用来指出警告和错误信息的文件号和行号。

#pragma指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。

这个很简单了,我就不再多说了,看网上的定义就可以了,这个error还挺常用的,仔细看一下,有兴趣的网上搜搜也无妨。接着我浅谈一下C++预处理机制对我们的封装的作用:

1、由于我们封装的头文件没有main方法,不能对于程序的执行的流程和引用文件等操作进行流程控制;

2、由于C++中头文件会被自动inline,所以需要预处理一些引用以便避免重名和重复引用;

3、我们编写头文件封装的时候难免会使用命名空间和类、结构之类的东西,所以我们一般需要控制生成和调用流程以便保证使用封装时的安全。这里多说一下:在if系列中的预处理语句中,除了可以放输出,还可以放define甚至需要限定某些条件下会执行的语句,所以说,#if系列有的时候也被叫做“头文件的if语句”。

希望大家掌握上述知识,灵活运用,以便使得将来的项目(过去的就算了,先别改了)更加稳定、灵活、高效。

 

在此特别声明:本文中有关于预处理的例程和定义均来自http://www.kuqin.com/language/20090806/66164.html,但是为其添加了注释并修改了格式,笔者为时间仓促没有另行通知而梳表歉意,如原作者不希望笔者使用,请与我联系:zhangyutong@zhangyutong.net。感谢各位,文章最后我将留下我的联系方式。

QQ:2276768747  MSN:xztzrjcxxzz@hotmail.com  email:zhangyutong@zhangyutong.net 感谢阅读金鸡独立提供的计算机技术文章!再见!

posted @ 2014-03-13 20:27  金鸡独立  阅读(861)  评论(0编辑  收藏  举报