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