预处理器与编译器
顾名思义,预处理器在编译器之前运行,换句话说,预处理器根据程序员的指示,决定实际要编译的内容。预处理器编译指令都以 # 打头,例如:
#include <iostream>
#define ARRAY_LENGTH 25
上述代码演示了两种预处理器编译指令,一是使用 #define 定义常量,二是使用#define定义宏函数。这两个编译指令都告诉编译器,将每个宏实例(ARRAY_LENGTH 或 SQUARE)替换为其定义的值。
使用宏避免多次包含
C++程序员通常在 .H 文件(头文件)中声明类和函数,并在 .CPP 文件中定义函数,因此需要在 .CPP 文件中使用预处理器编译指令 #include <header> 来包含头文件。
如果在头文件 class1.h 中声明了一个类,而这个类将 class2.h 中声明的类作为其成员,则需要在 class1.h 中包含 class2.h。如果设计非常复杂,即第二个类需要第一个类,则在 class2.h 中也需要包含 class1.h!
然而,在预处理器看来,两个头文件彼此包含对方会导致递归问题。为了避免这种问题,可结合使用宏以及预处理器编译指令 #ifndef 和 #endif。
包含 <header2.h> 的 head1.h 类似于下面这样:
#ifndef HEADER1_H_//多重引用保护
#define HEADER1_H_ // 预处理器将一次性读取此行及其后续行
#include <header2.h>
class Class1 {
// class members
};
#endif // end of header1.h
header2.h 与此类似,但宏定义不同,且包含的是<header1.h>:
#ifndef HEADER2_H_//multiple inclusion guard
#define HEADER2_H_
#include <header1.h>
class Class2 {
// class members
};
#endif // end of header2.h
因此,预处理器首次处理 header1.h 并遇到 #ifndef 后,发现宏 HEADER1_H_还未定义,因此继续处理。#ifndef 后面的第一行定义了宏 HEADER1_H_,确保预处理器再次处理该文件时,将在遇到包含 #ifndef 的第一行时结束,因为其中的条件为 false。header2.h 与此类似。在 C++编程领域,这种简单的机制无疑是最常用的宏功能之一。
使用 #define 编写宏函数
预处理器对宏指定的文本进行简单替换,因此也可以使用宏来编写简单的函数,例如:
#define SQUARE(x) ((x) * (x))
宏函数通常用于执行非常简单的计算。相比于常规函数调用,宏函数的优点在于,它们将在编译前就地展开,因此在有些情况下有助于改善代码的性能。
为什么要使用括号
原因在于宏的计算方式——预处理器支持的文本替换机制。
在省略了括号的情况下,简单的文本替换破坏了编程逻辑!使用括号有助于避免这种问题。
使用 assert 宏验证表达式
编写程序后,立即单步执行以测试每条代码路径很不错,但对大型应用程序来说可能不现实。比较现实的做法是,插入检查语句,对表达式或变量的值进行验证。
assert 宏让您能够完成这项任务。要使用 assert 宏,需要包含<assert.h>,其语法如下:
assert (expression that evaluates to true or false);
下面是一个示例,它使用 assert( ) 来验证指针的值:
#include <assert.h>
int main() {
char* sayHello = new char [25];
assert(sayHello != NULL); // throws a message if pointer is NULL
// other code
delete [] sayHello;
return 0;
}
assert( )在指针无效时将指出这一点:
在 Microsoft Visual Studio 中,assert( ) 让您能够单击 Retry 按钮返回应用程序,而调用栈将指出哪行代码没有通过断言测试。这让 assert( )成为一项方便的调试功能。例如,可使用 assert 对函数的输入参数进行验证。长期而言,assert 有助于改善代码的质量,强烈推荐使用它。
使用宏函数的优点和缺点
宏函数将在编译前就地展开,因此简单宏的性能优于常规函数调用。这是因为函数调用要求创建调用栈、传递参数等,这些开销占用的 CPU时间通常比 MIN 执行的计算还多。
然而,宏不支持任何形式的类型安全,这是一个严重的缺点。另外,复杂的宏调试起来也不容易。如果需要编写独立于类型的泛型函数,又要确保类型安全,可使用模板函数,而不是宏函数。
这将在下一节介绍。另外如果要改善性能,可将函数声明为内联的。
模板简介
模板无疑是 C++语言中最强大却最少被使用的特性之一。
在 C++ 中,模板让程序员能够定义一种适用于不同类型对象的行为。这听起来有点像宏(参见前面用于判断两个数中哪个更大的简单宏 MAX),但宏不是类型安全的,而模板是类型安全的。
模板声明语法
模板声明以关键字 template 打头,接下来是类型参数列表。这种声明的格式如下:
template <参数列表>
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。
各种类型的模板声明
模板函数
假设要编写一个函数,它适用于不同类型的参数,为此可使用模板语法!
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)
注意到调用 GetMax 时使用了,这将模板参数 objType 指定为 int。上述代码将导致编译器生成模板函数 GetMax 的两个版本,如下所示:
// 版本1
const int& GetMax(const int& value1, const int& value2) {
//...
}
//版本2
const double& GetMax(const double& value1, const double& value2) {
// ...
}
然而,实际上调用模板函数时并非一定要指定类型,因此下面的函数调用没有任何问题:
int maxVal = GetMax(num1, num2);
在这种情况下,编译器很聪明,知道这是针对整型调用模板函数。如下面示例代码所示:
#include <iostream>
#include <string>
using namespace std;
template<typename Type>
const Type& GetMax(const Type& value1, const Type& value2) {
if (value1 > value2)
return value1;
else {
return value2;
}
}
template<typename Type>
void DisplayComparison(const Type& value1, const Type& value2) {
cout << "GetMax(" << value1 << ", " << value2 << ") = ";
cout << GetMax(value1, value2) << endl;
}
int main() {
int num1 = -101, num2 = 2011;
DisplayComparison(num1, num2);
double d1 = 3.14, d2 = 3.1416;
DisplayComparison(d1, d2);
string name1("Jack"), name2("John");
DisplayComparison(name1, name2);
return 0;
}
在 main( )函数中的代码表明,可将同一个模板函数用于不同类型的数据:int、double 和 std::string。模板函数不仅可以重用(就像宏函数一样),而且更容易编写和维护,还是类型安全的。
请注意,调用 DisplayComparison 时,也可显式地指定类型,如下所示:
DisplayComparison<int>(num1, num2);
然而,调用模板函数时没有必要这样做。
您无需指定模板参数的类型,因为编译器能够自动推断出类型;但使用模板类时,需要这样做。
模板类
模板类是模板化的 C++ 类,是蓝图的蓝图。使用模板类时,可指定要为哪种类型具体化类。
这让您能够创建不同的 Human 对象,即有的年龄存储在 long long 成员中,有的存储在 int 成员中,还有的存储在 short 成员中。
下面是一个简单的模板类,它只有单个模板参数 T,用于存储一个成员变量:
template<typename T>
class HoldVarTypeT{
private:
T value;
public:
void setValue(const T& newValue){
value = newValue;
}
T& GetValue(){ return value; }
};
类 HoldVarTypeT 用于保存一个类型为 T 的变量,该变量的类型是在使用模板时指定的。下面来看该模板类的一种用法:
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;
因此,这个模板类定义了一种模式,并可针对不同的数据类型实现这种模式。
声明包含多个参数的模板
模板参数列表包含多个参数,参数之间用逗号分隔。因此,如果要声明一个泛型类用于存储两个类型可能不同的对象,可以使用如下所示的代码(这个模板类包含两个模板参数):
template<typename value1, typename value2>
class HoldsPair{
private:
T1 value1;
T2 value2;
};
在这里,类 HoldsPair 接受两个模板参数,参数名分别为 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);
声明包含默认参数的模板
可以修改前面的 HoldsPair <…>,将模板参数的默认类型指定为 int:
template<typename T1=int, typename T2=int>
class HoldsPair{
//...
};
这与给函数指定默认参数值极其类似,只是这里指定的是默认类型。
这样,前述第二种 HoldsPair 用法可以简写为:
// Pair an int with an int (default type)
HoldsPair <> pairInts (6, 500);
一个模板示例
下面使用前面讨论的 HoldsPair 模板来进行开发:
#include <iostream>
#include <string>
using namespace std;
template<typename T1 = int,typename T2=double>
class HoldsPair {
private:
T1 value1;
T2 value2;
public:
HoldsPair(const T1& val1, const T2& val2)
:value1(val1), value2(val2) {}
const T1& GetFirstValue()const {
return value1;
}
const T2& GetSecondValue() const {
return value2;
}
};
int main() {
HoldsPair<> pairIntDbl(300, 10.09);
HoldsPair<short, const char*> pairShortStr(25, "Learn templates, love C++");
cout << "The first object contains -" << endl;
cout << "Value 1 : " << pairIntDbl.GetFirstValue() << endl;
cout << "Value 2 : " << pairIntDbl.GetSecondValue() << endl;
cout << "The second object contains -" << endl;
cout << "Value 1 : " << pairShortStr.GetFirstValue() << endl;
cout << "Value 2 : " << pairShortStr.GetSecondValue() << endl;
return 0;
}
HoldsPair 定义了一种模式,可通过重用该模式针对不同的变量类型实现相同的逻辑。因此,使用模板可提高代码的可复用性。
模板的实例化和具体化
模板类是创建类的蓝图,因此在编译器看来,仅当模板类以某种方式被使用后,其代码才存在。换言之,对于您定义了但未使用的模板类,编译器将忽略它。然而,当您像下面这样通过提供模板参数来实例化模板类(如 HoldsPair)时:
HoldsPair<int, double> pairIntDbl;
就相当于命令编译器使用模板来创建一个类,即使用模板参数指定的类型(这里是 int 和 double)实例化模板。因此,对模板来说,实例化指的是使用一个或多个模板参数来创建特定的类型。
另一方面,在有些情况下,使用特定的类型实例化模板时,需要显式地指定不同的行为。这就是具体化模板,即为特定的类型指定行为。下面是模板类 HoldsPair 的一个具体化,其中两个模板参数的类型都为 int:
template<> class HoldsPair<int, int> {
// implementation code here
};
不用说,具体化模板的代码必须在模板定义后面。
下面的程序是一个模板具体化示例,演示了使用同一个模板可创建不同的具体化版本。
#include <iostream>
#include <string>
using namespace std;
template<typename T1 = int,typename T2=double>
class HoldsPair {
private:
T1 value1;
T2 value2;
public:
HoldsPair(const T1& val1, const T2& val2)
:value1(val1), value2(val2) {}
const T1& GetFirstValue() const;
const T2& GetSecondValue() const;
};
template<> class HoldsPair<int, int> {
private:
int value1;
int value2;
string strFun;
public:
HoldsPair(const int& val1, const int& val2) // constructor
: value1(val1), value2(val2) {}
const int& GetFirstValue() const{
cout << "Returning integer " << value1 << endl;
return value1;
}
};
int main() {
HoldsPair<int, int> pairIntInt(222, 333);
pairIntInt.GetFirstValue();
return 0;
}
对比两次的模板类 HoldsPair 的行为,将发现它们的行为有天壤之别。
事实上,上述代码中的这个模板定义甚至都没有提供存取函数 GetFirstValue() 和 GetSecondValue() 的实现,但程序依然能够通过编译。这是因为编译器只需考虑针对<int, int>的模板实例化,而在这个实例化中,我们提供了完备的具体实现。总之,这个示例不仅演示了模板具体化,还表明根据模板的使用情况,编译器可能忽略模板代码。
模板类和静态成员
前面说过,在编译器看来,仅当模板被使用时,其代码才存在。在模板类中,静态成员属性的工作原理是什么样的呢?
第 9 章介绍过,如果将类成员声明为静态的,该成员将由类的所有实例共享。模板类的静态成员与此类似,由特定具体化的所有实例共享。也就是说,如果模板类包含静态成员,该成员将在针对 int 具体化的所有实例之间共享;同样,它还将在针对 double 具体化的所有实例之间共享,且与针对 int 具体化的实例无关。
#include <iostream>
#include <string>
using namespace std;
template<typename T1>
class TestStatic {
public:
static int staticVal;
};
//静态成员初始化
template<typename T>
int TestStatic<T>::staticVal;
int main() {
TestStatic<int> intInstance;
cout << "Setting staticVal for intInstance to 2011" << endl;
intInstance.staticVal = 2011;
TestStatic<double> dblnstance;
cout << "Setting staticVal for Double_2 to 1011" << endl;
dblnstance.staticVal = 1011;
cout << "intInstance.staticVal = " << intInstance.staticVal << endl;
cout << "dblnstance.staticVal = " << dblnstance.staticVal << endl;
return 0;
}
输出表明,编译器在两个不同的静态成员中存储了两个不同的值,但这两个静态成员都名为 staticVal。也就是说,对于针对每种类型具体化的类,编译器确保其静态变量不受其他类的影响。
参数数量可变的模板
假定您要编写一个将两个值相加的通用函数,为此可编写下面这样的模板函数 Sum():
template <typename T1, typename T2, typename T3>
void Sum(T1& result, T2 num1, T3 num2)
{
result = num1 + num2;
return;
}
这很简单。然而,如果需要编写一个函数,能够计算任意数量值的和,就需要使用参数数量可变的模板。参数数量可变的模板是 2014 年发布的 C++14 新增的,下面的程序演示了如何使用参数数量可变的模板来定义刚才说的函数。
#include <iostream>
#include <string>
using namespace std;
template < typename Res, typename ValType>
void Sum(Res & result, ValType & val){
result = result + val;
}
template < typename Res, typename First, typename... Rest>
void Sum(Res& result, First val1, Rest... valN)
{
result = result + val1;
return Sum(result, valN ...);
}
int main() {
double dResult = 0;
Sum(dResult, 3.14, 4.56, 1.1111);
cout << "dResult = " << dResult << endl;
string strResult;
Sum(strResult, "Hello ", "World");
cout << "strResult = " << strResult.c_str() << endl;
return 0;
}
使用参数数量可变的模板定义的函数 Sum()不仅能够处理不同类型的参数,还能够处理不同数量的参数。
编译期间,编译器将根据调用 Sum() 的情况创建正确的代码,并反复处理提供的参数,直到将所有的参数都处理完毕。
参数数量可变的模板是 C++新增的一项强大功能,可用于执行数学运算,也可用于完成某些简单的任务。通过使用参数数量可变的模板,程序员可避免反复实现执行任务的各种重载版本,从而创建出更简短、更容易维护的代码。
通过支持参数数量可变的模板,C++还打开了支持元组的大门。std::tuple 就是实现元组的模板类,您可使用任意数量的元素来实例化这个模板类,其中每个元素都可为任何类型。要访问这些元素,可使用标准库函数 std::get。如下面程序所示:
#include <iostream>
#include <string>
#include <tuple>
using namespace std;
template<typename tupleType>
void DisplayTupleInfo(tupleType& tup) {
const int numMembers = tuple_size<tupleType>::value;
cout << "Num elements in tuple: " << numMembers << endl;
cout << "Last element value: " << get<numMembers - 1>(tup) << endl;
}
int main() {
tuple<int, char, string> tup1(make_tuple(101, 's', "Hello Tuple!"));
DisplayTupleInfo(tup1);
auto tup2(make_tuple(3.14, false));
DisplayTupleInfo(tup2);
auto concatTup(tuple_cat(tup2, tup1)); // contains tup2, tup1 members
DisplayTupleInfo(concatTup);
double pi;
string sentence;
tie(pi, ignore, ignore, ignore, sentence) = concatTup;
cout << "Unpacked! Pi: " << pi << " and \"" << sentence << "\"" << endl;
return 0;
}
元组是一个高级概念,常用于通用模板编程。这里提到这个主题旨在让您对元组有大致的了解,因为它还在不断发展变化中。
使用 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!");
下面代码演示了一个模板类,它使用 static_assert( ) 禁止针对特定类型进行实例化。
#include <iostream>
using namespace std;
template<typename T>
class EverythingButInt {
public:
EverythingButInt() {
static_assert(sizeof(T) != sizeof(int), "No int please!");
}
};
int main() {
EverythingButInt<int> test;
return 0;
}
没有输出,因为这个程序不能通过编译,它显示一条错误消息,指出您指定的类型不正确。
在实际 C++ 编程中使用模板
模板一个重要而最强大的应用是在标准模板库(STL)中。STL 由一系列模板类和函数组成,它们分别包含泛型实用类和算法。这些 STL 模板类让您能够实现动态数组、链表以及包含键-值对的容器,而 sort 等算法可用于这些容器,从而对容器包含的数据进行处理。
前面介绍的模板语法有助于读者使用本书后面将详细介绍的 STL 容器和函数;更深入地理解 STL将有助于使用 STL 中经过测试的可靠实现,从而编写出更高效的 C++程序,还有助于避免在模板细节上浪费时间。
总结
本章更详细地介绍了预处理器。每当您运行编译器时,预处理器都将首先运行,对#define 等指令进行转换。
预处理器执行文本替换,但在使用宏时替换将比较复杂。通过使用宏函数,可根据在编译阶段传递给宏的参数进行复杂的文本替换。将宏中的每个参数放在括号内以确保进行正确的替换,这很重要。
模板有助于编写可重用的代码,它向开发人员提供了一种可用于不同数据类型的模式。模板可以取代宏,且是类型安全的。学习本章介绍的模板知识后,便为学习如何使用 STL 做好了准备!