C++ 宏和模板简介

摘要:
在C++编程领域,这种简单的机制是最常用的宏功能应用。而#ifndef,#define,#endif是C/C++语言中的宏定义,所有支持C/C++语言的编译器上都是有效的,如果编写跨平台程序,最好使用宏的方式来避免头文件的重复包含。在C++中,模板允许程序员定义一种适用于不同类型的对象的行为,有一点类似宏,但宏不是类型安全的,而模板是类型安全的。

参考《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


免责声明:文章转载自《C++ 宏和模板简介》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Android中ListView的总结(1)如何在C#中使用全局鼠标、键盘Hook下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

OpenCASCADE 参数曲面面积

OpenCASCADE 参数曲面面积 eryar@163.com Abstract. 本文介绍了参数曲面的第一基本公式,并应用曲面的第一基本公式,结合OpenCASCADE中计算多重积分的类,对任意参数曲面的面积进行计算。 Key Words. Parametric Curve, Parametric Surface, Gauss Integration,...

【问题解决记录】无法识别的标志“-sdlMode”,在“p2”中

本文为大便一箩筐的原创内容,转载请注明出处,谢谢:http://www.cnblogs.com/dbylk/这是本人第一次使用MarkDown编辑器,试试看效果—w—,下面是正文: ## 一、报错原因 昨天用SIMD指令编写了几个矩阵计算函数,想要替换掉DX的函数达到优化性能的目的。 因为公司项目使用的编译器是VS2008,而VS2013编译出来的SI...

stop slave 卡住模拟--大事务场景

目录 1.主库数据准备 2.从库停止复制stop slave,只开启io thread 3.在主库上执行大事务 4.开启复制start slave sql_thread,等待2s,接着stop slave 上一篇文章stop slave卡住,初步介绍了stop slave的问题现象以及一些原因。 本文介绍另外一种情况:大事务。 下面将演示遇到大事务...

将元素移动到列表末尾

列表的操作在日常编程中很常见。人们可能会遇到希望仅使用单衬纸执行的各种问题。一个这样的问题可能是将列表元素移动到后面(列表末尾)。让我们讨论可以做到这一点的某些方法。 方法#1:使用append() + pop() + index()通过组合这些功能,可以在一行中执行此特定功能。append 函数使用 index 函数提供的索引添加 pop 函数删除的元素...

模块与包的导入

1.模块什么是模块: #常见的场景:一个模块就是一个包含了python定义和声明的文件(文件名就是模块名字加上.py的后缀),模块可以被导入使用。 #但其实import加载的模块分为四个通用类别:  使用python编写的.py文件 已被编译为共享库或DLL的C或C++扩展 把一系列模块组织到一起的文件夹(注:文件夹下有一个__init__.py文件,该...

PCRE函数简介和使用示例【转】

PCRE函数简介和使用示例 标签:正则表达式listbuffercompilationnullperl 原文地址:http://blog.csdn.net/sulliy/article/details/6247155 PCRE是一个NFA正则引擎,不然不能提供完全与Perl一致的正则语法功能。但它同时也实现了DFA,只是满足数学意义上的正则。 P...