宏和模板简介
1.1 预处理器和编译器
预处理器在编译器之前运行,换句话说,预处理器根据程序员的指示,决定实际要编译的内容。预处理器编译指令都以#打头,例如:
// instruct preprocessor to insert contents of iostream here
#include <iostream>
// define a macro constant
#define ARRAY_LENGTH 25
int numbers[ARRAY_LENGTH]; // array of 25 integers
// define a macro function
#define SQUARE(x) ((x) * (x))
int TwentyFive = SQUARE(5);
本小节重点介绍两种预处理器编译指令,一是使用#define定义常量,二是使用#define定义宏函数。这两个编译指令都告诉编译器,将每个宏实例替换为其定义的值。
1.2 使用#define 定义常量
使用#define定义常量的语法非常简单:
#define identifier value
C++程序员通常在 .h 文件(头文件)中声明类和函数,并在 .cpp 文件中定义函数,因此需要在 .cpp 文件中使用预处理器编译指令 #include<header> 来包含头文件。如果在头文件 class1.h中声明了一个类,而这个类将 class2.h 中声明的类作为其成员,则需要在 class1.h 中包含 class2.h。如果设计非常复杂,即第二个类需要第一个类,则在 class2.h 中也需要包含 class1.h。
然而,在预处理器看来,两个头文件彼此包含对方会导致递归问题。为了避免这种问题,可结合使用宏以及预处理器编译指令 #ifndef 和 #endif。
1.3 使用#define 编写宏函数
预处理器对宏指定的文本进行简单替换,因此也可以使用宏来编写简单的函数。例如:
#define SQUARE(x) ((x) * (x))
这个宏计算平方值。
1.3.1 为什么要使用括号
计算圆面积的宏定义如下:
#define AREA_CIRCLE(r) (PI*(r)*(r))
上述代码使用了大量的括号。如果省略括号:
#define AREA_CIRCLE(r) (PI*r*r)
如果用下面的语句调用这个宏:
cout << AREA_CIRCLE (4+6);
编译器看到的语句将会如下:
cout << (PI*4+6*4+6); // not the same as PI*10*10
所以通过使用括号,能让宏代码不受运算符优先级的影响。
1.3.2 使用assert宏验证表达式
编写程序后,立即单步执行以测试每条代码路径很不错,但对大型应用程序来说可能不现实。比较现实的做法是,插入检查语句,对表达式或变量的值进行验证。
assert宏能够完成这项任务,要使用asser宏,需要包含<assert.h>,其语法如下:
assert (expression that evaluates to true or false);
1.3.3 使用宏函数的优点和缺点
尽可能不要自己编写宏函数。尽可能使用const常量,而不是宏常量。宏并非类型安全的,预处理器不执行类型检查。
1.4 模板简介
1.4.1 模板声明语法
模板声明以关键字template打头,接下来是类型参数列表。这种声明的格式如下:
template <parameter list>
template function / class declaration..
关键字template标志着模板声明的开始,接下来是模板参数列表。该参数列表包含关键字typename,它定义了模板参数objType,objType是一个占位符,针对对象实例化模板时,将使用对象的类型替换它。
template <typename T1, typename T2 = T1>
bool TemplateFunction(const T1& param1, const T2& param2);
// A template class
template <typename T1, typename T2 = T1>
class MyTemplate
{
private:
T1 member1;
T2 member2;
public:
T1 GetObj1() {return member1; }
// ... other members
};
上述代码演示了一个模板函数和一个模板类,它们都接受两个模板参数:T1和T2,其中T2的类型默认为T1。
1.4.2 各种类型的模板声明
模板声明可以是:
- 函数的声明或定义;
- 类的定义或声明;
- 类模板的成员函数或成员类的声明或定义;
- 类模板的静态数据成员的定义;
- 嵌套在类模板中的类的静态数据成员的定义;
- 类或类模板的成员模板的定义。
1.4.3 模板函数
假设要编写一个函数,它适用于不同类型的参数,为此可使用模板语法!
template <typename objType>
const objType& GetMax(const objType& value1, const objType& value2)
{
if (value1 > value2)
return value1;
else
return value2;
}
下面是一个使用该模板的示例:
int num1 = 25;
int num2 = 40;
int maxVal = GetMax <int> (num1, num2);
double double1 = 1.1;
double double2 = 1.001;
double maxVal = GetMax <double>(double1, double2);
1.4.4 模板类
类是一种编程单元,封装类属性以及使用这些属性的方法。使用模板类,可指定要为哪种类型具体化类。下面是一个简单的模板类,它只有单个模板参数T,用于存储一个成员变量:
template <typename T>
class HoldVarTypeT
{
private:
T value;
public:
void SetValue (const T& newValue) { value = newValue; }
T& GetValue() {return value;}
};
下面来看该模板类的一种用法:
HoldVarTypeT <int> holdInt; // template instantiation for int
holdInt.SetValue(5);
cout << "The value stored is: " << holdInt.GetValue() << endl;
这里使用该模板类来存储和检索类型为int的对象,即使用int类型的模板参数实例化Template类。同样,这个类也可以用于处理字符串,其用法类似:
HoldVarTypeT <char*> holdStr;
holdStr.SetValue("Sample string");
cout << "The value stored is: " << holdStr.GetValue() << endl;
因此,这个模板类定义了一种模式,并可针对不同的数据类型实现这种模式。
1.4.5 声明包含多个参数的模板
模板参数列表包含多个参数,参数之间用逗号分隔。因此,如果要声明一个泛型类用于存储两个类型可能不同的对象,可使用如下所示的代码:
template <typename T1, typename T2>
class HoldsPair
{
private:
T1 value1;
T2 value2;
public:
// Constructor that initializes member variables
HoldsPair (const T1& val1, const T2& val2)
{
value1 = val1;
value2 = val2;
};
// ... Other member functions
};
在这里,类HoldPair接受两个模板参数,参数名分别为T1和T2。可使用这个类来存储两个类型相同或不同的对象,如下所示:
// A template instantiation that pairs an int with a double
HoldsPair <int, double> pairIntDouble (6, 1.99);
// A template instantiation that pairs an int with an int
HoldsPair <int, int> pairIntDouble (6, 500);
1.4.6 声明包含默认参数的模板
可以修改前面的<HoldsPair<...>,将模板参数的默认类型指定为int:
template <typename T1=int, typename T2=int>
class HoldsPair
{
// ... method declarations
};
这与给函数指定默认参数值及其类似,只是这里指定的是默认类型。
1.4.7 模板的实例化和具体化
模板类是创建类的蓝图,因此在编译器看来,仅当模板类以某种方式被使用后,其代码才存在。换言之,对于定义了但未使用的模板类,编译器将忽略它。
当像下面这样通过提供模板参数来实例化模板类时:
HoldsPair<int, double> pairIntDbl;
就相当于命令编译器使用模板来创建一个类,即使用模板参数指定的类型实例化模板。因此,对模板来说,实例化指的是使用一个或多个模板参数来创建特定的类型。
另一方面,在有些情况下,使用特定的类型实例化模板时,需要显式地指定不同的行为。这就是具体化模板,即为特定的类型指定行为。下面是模板类HoldsPair的一个具体化,其中两个模板参数的类型都为int:
template<> class HoldsPair<int, int>
{
// implementation code here
};
1.4.8 模板类和静态成员
如果将类成员声明为静态的,该成员将由类的所有实例共享。模板类的静态成员与此类似,由特定具体化的所有实例共享。也就是说,如果模板类包含静态成员,该成员将在针对int具体化的所有实例之间共享;同样,它还将在针对double具体化的所有实例之间共享,且与针对int具体化的实例无关。换句话说,可以认为编译器创建了两个版本的x: x_int用于针对int具体化的实例,而x_double针对double具体化的实例。
1.4.9 参数数量可变的模板
假如要编写一个将两个值相加的通用函数,为此可编写下面这样的模板函数Sum():
template <typename T1, typename T2, typename T3>
void Sum(T1& result, T2 num1, T3 num2)
{
result = num1 + num2;
return;
}
这很简单。然而,如果需要编写一个函数,能够计算任意数量值的和,就需要使用参数数量可变的模板。参数数量可变的模板是2014年发布的C++14新增的。
1.4.10 使用static_assert执行编译阶段检查
static_assert是C++11新增的一项功能,能够在不满足指定条件时禁止编译。例如,可能想禁止针对int实例化模板类,为此可使用static_assert,它是一种编译阶段断言,可用于在开发环境显示一条自定义消息:
static_assert(expression being validated, "Error message when check fails");
要禁止针对类型int实例化模板类,可使用static_assert(),并将sizeof(T)与sizeof(int)进行比较,如果它们相等,就显示一条错误消息:
static_assert(sizeof(T) != sizeof(int), "No int please!");
1.4.11 在实际C++编程中使用模板
模板是一个重要而最强大的应用是在标准模板库(STL)中。STL由一系列模板类和函数组成,它们分别包含泛型实用类和算法。这些STL模板类能够实现动态数组、链表以及包含键-值对的容器,而sort等算法可用于这些容器,从而对容器包含的数据进行处理。