参考《21天学通C++》第14章节,对C++中的宏和模板进行了学习,总结起来其主要内容如下:

(1) 预处理器简介

(2) 关键字#define与宏

(3) 模板简介

(4) 如何编写函数模板和模板类

(5) 宏和模板之间的区别

(6) 使用static_assert进行编译阶段检查

************************************************************************************************************************************

1 预处理器与编译器

预处理器:在编译器之前运行,根据程序员的指示,决定实际要编译的内容,预编译指令都以#打头

典型的应用有如下几种:

(1) 使用#define定义常量

通常在定义数组的长度时用到。其定义的常量只是进行文本替换,并不进行类型检测,比如定义#define PI 3.1416来讲,这个宏PI的类型编译器并不检测,也不知道其到底是float还是double。

定义常量时,更好地选择是使用关键字const和数据类型,比如const double PI = 3.1416 就比使用宏进行定义要好得多。

(2) 使用宏避免多次包含

C++典型的做法是将类和函数的声明放在头文件(.h)中,而在源文件(.cpp)中定义函数,因此需要在.cpp文件中使用#include <header>来包含头文件。如果两个头文件需要相互包含时,在预处理器看来会导致递归问题,解决方案之一就是使用宏以及预处理器编译指令#ifndef和#endif。举例如下:

/*<header1.h>*/

#ifndef HEADER1_H_

#define HEADER1_H_

#include <header2.h>

....................................

....................................

....................................

#endif

header2.h与此类似,但宏定义不同,且包含 header1.h :

/*<header2.h>*/

#ifndef HEADER2_H_

#define HEADER2_H_

#include <header1.h>

....................................

....................................

....................................

#endif

解释说明:#ifndef是一个条件处理命令,让预处理器仅在标识符未定时才继续,#endif告诉预处理器,条件处理指令到此结束。因此,预处理器首次处理header1.h并遇到#ifndef后,发现HEADER1_H_还未定义,因此继续处理。#ifndef后面的第一行定义了宏HEADER1_H_,确保预处理器再次处理该文件时,将在遇到包含#ifndef的第一行时结束,因为其中的条件已变为false。header2.h与此类似。在C++编程领域,这种简单的机制是最常用的宏功能应用。

除此之外,还有另一种保证头文件只被编译一次的方法:#pragma once。只需要在开头写上这条预编译指令,就可以保证所有头文件只被包含编译一次。但是#pragma once是编译器相关的,有的编译器支持,有的编译器则不支持,不过大部分编译器都支持这个预编译指令了。而#ifndef,#define,#endif是C/C++语言中的宏定义,所有支持C/C++语言的编译器上都是有效的,如果编写跨平台程序,最好使用宏的方式来避免头文件的重复包含。

(3) 使用assert断言宏进行调试

编写程序后,立即单步执行测试每种路径似乎很不错,但可能不现实,比较好的做法是插入检查语句,对表达式或变量的值进行验证。assert宏就是用来完成这项任务,使用assert宏需包含<assert.h>,语法如下:

assert(expression that evaluates to true or false);举例:char * test = new char[25]; assert(test != NULL);

assert在指针无效时将指出这一点,在Microsoft Visual Studio 中,assert能够返回应用程序,而调用栈将指出哪行代码没有通过断言测试,这让assert成为一项方便的调试功能。

在大多数开发环境中,assert在发布模式下被禁用,因此它仅在调试模式下显示错误消息。另外在有些开发环境中assert被实现为函数,而不是宏。

(4) 使用#define定义宏函数

预处理器对宏指定的文本进行简单替换,因此可以使用编写简单的函数。宏函数通常用于执行非常简单的计算,相比于常规函数调用,宏函数的优点是它们将在编译前就地展开,有助于改善代码的性能(是不是有点内联函数的感觉?)。因为宏不考虑数据类型,因此使用宏就比较危险,这一点需要考虑到。另外,宏是简单的替换,对于宏函数一定要使用括号保证替换后的功能逻辑正确,这是使用宏函数中经常犯错误的点。

正是宏不进行类型检查,所以使用宏函数可以作用于不同的变量类型。宏函数不像常规函数那样在函数调用时需要创建调用栈、传递参数等,这些开销占用CPU的时间通常比常规函数的执行时间还多。所以,对于简单的函数,通常可以使用宏函数进行。但是宏不支持任何形式的类型安全,而且复杂的宏调试起来不方便。如果比那些独立于类型的泛型函数,又要保证类型安全,可使用模板函数,而不是宏函数,如要改善性能,可以定义为内联函数,使用关键字inline,这样就完全覆盖了宏函数的优势。

小结

尽可能不要自己编写宏函数;尽可能使用const变量,而不是宏常量;请牢记宏并非类型安全的,预处理器不进行类型检查;在宏函数定义中要使用括号将每个变量括起来;避免头文件重复包含编译,可使用宏来解决;别忘了在调试中可以大量使用assert断言,对提高代码质量很有帮助。

************************************************************************************************************************************

2. 模板

模板可能是C++语言中最强大却最少被使用(或被理解)的特性之一。

在C++中,模板允许程序员定义一种适用于不同类型的对象的行为,有一点类似宏,但宏不是类型安全的,而模板是类型安全的。

(1) 模板声明语法

template <parameter list>

template function / class declaration

关键字template标志模板声明的开始,接下来是模板参数列表。该参数列表包含关键字typename ,它定义了模板参数objectType,而objectType是一个占位符,针对对象实例化模板时,将使用对象类型替换它。

(2) 模板声明的类型

模板声明可以是:函数的声明或定义;类的声明或定义;类模板的成员函数或成员类的声明或定义;类模板的静态数据成员的定义;嵌套在类模板中的类的静态数据成员定义;类或类模板的成员模板的定义。

(3) 模板函数

调用模板函数时并非一定要指定类型。举例如下:

template <typename objectType>

const objectType& GetMax(const objectType& value1,const objectType& value2)

{

if(value1 > value2)

return value1;

else

return value2;

}

具体使用该模板的示例:

int Integer1 = 25;

int Integer2 = 50;

使用 int MaxValue  =  GetMax <int> (Integer1 , Integer2 );

与使用int MaxValue  =  GetMax (Integer1 , Integer2 );效果是一样的。这种情况下编译器会进行数据类型检查。但对于模板类则必须显式的指明类型。但是并不能进行像GetMax
(Integer1 , “some string” )这样的混杂类型。这种调用将导致编译器错误。

(4) 模板类

类是一种编程单元,封装属性以及使用这些属性的方法。属性通常是私有成员。当某一属性可以是int型,也可以是long型等时,模板类可派上用场。使用模板类时,可指定哪种类型的具体化。举例如下:

template <typename T>

class Test

{

public:

void SetValue(const T& newValue) { Value = newValue;}

const T& GetValue() const { return Value;}

private:

T Value;

};

模板类的使用方法:

Test <int> Test1;

Test1.SetValue(5);

这样就实现了模板类中同一个属性可以具有不同的数据类型实现,只要在实例化对象时指定实例化对象需要的属性的数据类型就好了。在术语上,使用模板时,实例化指的是根据模板声明以及一个或多个参数创建特定的类型,而实例化创建的特定类型称为具体化。

(5) 声明包含多个参数的模板

模板参数列表包含多个参数,参数之间使用逗号分割。因此,如果要声明一个泛型类用于存储两个类型可能不同的对象,可以使用如下代码:

template <typename T1, typename T2>

class Test

{

private:

T1 Value1;

T2 Value2;

public:

/*some methods*/

/*constructor*/

Test ( const T1& value1, const T2& value2)

{Value1 = value1; Value2 = value2;}

};

使用方法如下:

Test <int, int> Test1(5, 6);//通过构造函数来进行初始化。

(6) 声明包含默认参数的模板

举例如下:

template <typename T1 = int, typename T2 = int>

class Test

{

private:

T1 Value1;

T2 Value2;

public:

/*some methods*/

/*constructor*/

Test ( const T1& value1, const T2& value2)

{Value1 = value1; Value2 = value2;}

};

这与给函数指定默认参数值及其类似。所以,这种指定了默认类型的模板,可以简化实例化过程:

Test < > Test1(5, 6);//通过构造函数来进行初始化。

(7) 模板类和静态成员

如果将类成员声明为静态,该成员将由类的所有实例共享,模板类的静态成员也类似,由特定具体化的所有实例共享。如果模板类T包含静态成员X,该成员将针对int具体化的所有实例之间共享。同样,还将针对double具体化的所有实例之间共享,且针对int具体化的实例无关。编译器创建了两个版本的X,X_int用于针对int具体化的实例,而X_double则针对double具体化的实例。也就是说,对于针对每种类型具体化的类,编译器保证其静态变量不受其他类的影响。模板类的每个具体化都有自己的静态成员。举例如下:

template <typename T>

class TestStatic

{

private:

public:

static int StaticValue;

/*some methods*/

};

// static member initialization

template <typename T> int TestStatic<T>:: StaticValue;

(8) 使用static_assert执行编译阶段检查

可以用来进行设置挑剔的模板类,屏蔽针对某种类型的具体化,使用static_assert进行编译阶段的检查。举例如下:

template <typename T>

class Test

{

private:

public:

/*some methods*/

EverythingButInt()

{

static_assert(sizeof(T) != sizeof(int), " No int please!");

}

};

int main()

{

Test<int> test;

return 0;

}

编译结果输出:

error:No int please!

这个编译结果是由模板类中的static_assert指定的。

小结

务必使用模板来实现通用概念,而不是宏;编写模板函数和模板类时尽可能使用const;模板类的静态成员由特定具体化的所有实例共享。

************************************************************************************************************************************

总结

本文详细介绍了预处理器,在运行编译器时,预处理器都将首先运行,对#define等指令进行转换;预处理器执行文本替换,但在使用宏时替换将比较复杂。通过使用宏函数,可以在编译阶段传递给宏的参数进行复杂的文本替换。将宏中的每个参数放在括号内以确保进行正确的替换,这很重要。模板有助于编写可重用的代码,它向开发人员提供了一种可用于不同数据类型的模式。模板可以取代宏,而且是类型安全的。学习好模板,对于后续使用C++标准模板库STL有着极为重要的概念性基石意义。

************************************************************************************************************************************

2015-7-30

最新文章

  1. 常用JS汇总
  2. PHP面向对象的一些深入理解
  3. Sequence 分类: 栈和队列 2015-08-05 10:10 2人阅读 评论(0) 收藏
  4. 不要将缓存服务器与Tomcat放在单台机器上,否则出现竞争内存问题
  5. Objective-C Runtime 运行时之六:拾遗
  6. VirtualBox的四种网络连接方式详解
  7. Linq 更改主键值
  8. 计算机程序的思维逻辑 (66) - 理解synchronized
  9. Sublime 常用快捷键
  10. 【javascript】您好, 您要的ECMAScript6速记套餐到了
  11. Ta-lib函数功能列表
  12. Ubuntu下通过makefile生成静态库和动态库简单实例
  13. linux CentOS6.5 yum安装mysql 5.6
  14. 浅谈UML中常用的几种图——用例图
  15. docker 镜像运行问题
  16. JS中的call、apply、bind方法详解
  17. 关于vb代码复制到其他地方出现乱码的问题
  18. HeadFirst Ruby 第七章总结 references
  19. POJ1013 称硬币
  20. salt常用命令(一)

热门文章

  1. 共享库的使用(.so)文件
  2. luogu 1714
  3. SB的SDOISB记
  4. Pycharm使用技巧:Split Vertically/Horizontally(垂直/水平拆分窗口)
  5. Vue.js中 watch的理解以及深度监听
  6. 【caffe Blob】caffe中与Blob相关的代码注释、使用举例
  7. tomcat启动startup.bat一闪而过(分析与解答)
  8. 01_tf和numpy的区别
  9. 在Ubuntu 18.04上安装OpenCV 4(C ++和Python)
  10. JMS与消息队列