这个博客中,我们将学习:
- 预处理器简介
- 关键字#define与宏
- 模板简介
- 如何编写函数模板与模板类
- 宏与模板之间的区别
- 使用C++11新增的static_assert进行编译阶段检查
6.1 预处理器与编译器
我们之前接触过预处理器。顾名思义,预处理器在编译器之前运行,换句话说,预处理器可以根据程序员的指示,决定实际要编译的内容。预处理器编译指令都以#打头,例如:
// tell the preprocessor to insert the contents of header iostream here
#include <iostream>
// define a macro constant
#define ARRAY_LENGTH 25
int MyNumbers[ARRAY_LENGTH]; //array of 25 integers
// define a macro function
#define SQUARE(x) ((x)*(x))
int TwentyFive = SQUARE(5);
本博客重点介绍上述代码演示的两种预处理其编译指令,一是使用#define 定义常量,二是是用#define定义宏函数。这两个编译指令都告诉编译器,将每个宏实例(ARRAY_LENGTH或AQUARE)替换为其定义的值。
注意:宏也进行文本替换,预处理器只是就将标识符替换为指定的文本。
6.2 使用#define定义常量
使用#define定义常量的语法非常简单:
#define identifier value
例如,要定义将被替换为25的常量ARRAY_LENGTH,可使用如下代码:
#define ARRAY_LENGTH 25
这样,每当预处理器遇到标识符ARRAY_LENGTH时,都会将其替换为25.
int MyNumbers [ARRAY_LENGTH] = {0};
double Radiuses [ARRAY_LENGTH] = {0.0};
std::string Names [ARRAY_LENGTH];
对于上述三行代码,预处理器运行完毕后,编译器看到的如下代码:
int MyNumbers [25] = {0}; // an array of 25 integers
double Radiuses [25] = {0.0}; //an array of 25 doubles
std::string Names [25]; //an array of 25 std::strings
替换将在所有代码中进行,包括下面这样的for循环:
for {int Index = 0;Index<25;++Index}
MyNumbers[Index] = Index;
编译器将看到如下代码:
for (int Index = 0;Index<25;++Index)
MyNumbers[Index] = Index;
且看下面的代码:
#include <iostream>
#include <string>
using namesapce std;
#define ARRAY_LENGTH 25
#define PI 3.1416
#define MY_DOUBLE double
#define FAV_WHISKY "Jack Daniels"
int main()
{
int MyNumbers [ARRAY_LENGTH] = {0};
cout<<"Array's length: "<<sizeof(MyNumbers)/sizeof(int)<<endl;
cout<<"Enter a Radius: ";
MY_DOUBLE Radius = 0;
cin>>Radius;
cout<<"Area is: "<<PI*Radius*Radius<<endl;
string FavoriteWhisky(FAV_WHISKY);
cout<<"My favorite drink is: "<<FAV_WHISKY<<endl;
return 0;
}
上面定义了:ARRAY_LENGTH、PI、MY_DOUBLE和FAV_WHISKY。
上面的都是死板的文本替换,这种文本替换优缺点。
提示:定义常量时,更好的选择是使用关键字const和数据类型,因此下面的定义更好:
const int ARRAY_LENGTH = 25;
const double PI = 3.1416;
const char* FAV_WHISKY = "Jack Daniels";
typedef double MY_DOUBLE; //use typedef to alias a type
6.2.1 使用宏避免多次包含
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_ //multiple inclusion guard;
#define HEADER1_H_ //preprocessor will read this and following lines once
#include <header2.h>
class Class1
{
// class members
};
#endif //end of header1.h
header2.h与此类似,但宏定义不同,且包含的是<header1.h>
#ifndef HEADER2_H_
#define HEADER2_H_
#inlude <header1.h>
class Class2
{
// class members
};
#endif //end of header2.h
注意:
#ifndef 可读作 if-not-defined。这是一个条件处理命令,让预编译器仅在标识符未定义时才继续。
#endif告诉预处理器,条件处理指令到此为止。
因此,预处理器首次处理header1.h并遇到#ifndef后,发现宏HEADER1_H_还未定义,因此继续处理。#ifndef后面的第一行定义了宏HEADER1_H_,确保预处理器再次处理该文件时,将在遇到包含#ifndef的第一行时结束,因为其中的条件为false。header2.h与此类似。在C++编程领域,这种简单的机制无疑是最常用的宏功能之一。
6.3 使用#define编写宏函数
预处理器对宏指定的文本进行简单替换,因此也可以使用宏来编写简单的函数,例如:
#define SQUARE(x) ((x)*(x))
这个宏计算平方值。同样,计算圆面积的宏类似于下面:
#define PI 3.1416
#define AREA_CIRCLE(r) (PI*(r)*(r))
宏函数通常用于执行非常简单的计算。相比于常规函数调用,宏函数的优点在于,它们将在编译前就地展开,因此有些情况下有助于改善代码的性能。且看下面的代码:
#include <iostream>
#include <string>
using namespace std;
#define SQUARE(x) ((x)*(x))
#define PI 3.1416
#define AREA_CIRCLR(r) (PI*(r)*(r))
#define MAX(a,b) (((a)>(b)) ? (a):(b))
#define MIN(a,b) (((a)<(b)) ? (a):(b))
int main()
{
cout<<"Enter an integer: ";
int Input1 = 0;
cin>>Input1;
cout<<"SQUARE("<<Input1<<") = "<<SQUARE(Input1)<<endl;
cout<<"Area of a circle with radius "<<Input<<" is: ";
cout<<AREA_CIRCLE(Input1)<<endl;
cout<<"Enter another integer: ";
int Input2 = 0;
cin>>Input2;
cout<<"MIN("<<Input1<<", "<<Input2<<") = ";
cout<<MIN(Input1,Input2)<<endl;
cout<<"MAX("<<Input1<<", "<<Input2<<") = ";
cout<<MAX(Input1,Input2)<<endl;
return 0;
}
6.3.1 为什么要使用括号
再看一下计算圆面积的宏:
#define AREA_CIRCLE(r) (PI*(r)*(r))
上面的代码比较古怪,使用了大量的括号。
而在常规的代码中,Area()的代码如下:
//Function definitions (implementation)
double Area(double InputRadius)
{
return Pi*InputRadius*InputRadius; // look, no brackets?
}
编写宏时使用了大量括号,而在函数中,同样的公式看起来完全不同。为啥?原因在于宏的计算方式——预处理器支持的文本替换机制。
请看下面的宏:
#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
根据运算符优先级,将先乘除后加减,因此编译器将这样计算面积:
cout<<(PI*4+24+6);
所以,知道为啥这么爱在宏函数使用括号了么?
6.3.2 使用assert宏验证表达式
编写程序后,立即单步执行以测试每种路径很不错,但可能不现实。比较现实的做法时,插入检查语句,对表达式或者变量的值进行验证。
assert宏能够完成这项任务。要使用assert宏,需要包含<assert.h>,语法如下:
assert(experssion that evaluate to true or false);
且看下面代码:
#include <assert.h>
int main()
{
char* sayHello = new char[25];
assert(sayHello != NULL); //throws up a message if pointer is NULL
//other code
delete[] sayHello;
return 0;
}
assert()在指针无效时将指出这一点。
6.3.3 使用宏函数的优点和缺点
宏函数可以用于不同的变量类型。再看一下下面的代码:
#define MIN(a,b) (((a)<(b)) ? (a):(b))
可以将宏函数MIN用于整型
cout<<MIN(25,10)<<endl;
也可以用于双精度数:
cout<<MIN(0.1,0.2)<<endl;
如果MIN()为常规函数,必须编写两个不同版本:MIN_INT()和MIN_DOUBLE(),前者接受int参数并返回int,后者接受double参数并返回double。使用宏减少了代码行,这是一种细微的优势,诱使程序员用宏来定义一些简单的函数。且宏函数将在编译前就展开,因此简单的宏性能优于常规函数调用。这是因为函数调用要求创建调用栈、传递参数等,这些开销占用的CPU时间通常比MIN执行的计算还多。
虽然有以上有优点,但也存在严重问题,就是不支持任何形式的类型安全。另外,复杂的宏调试起来也不容易。
6.4 模板简介
模板时C++语言中最强大却最少被使用(或被理解)的特性之一。
在C++中,模板让程序员 能够定义一种适用于不同类型的对象的行为。听起来有点像宏,但宏是不安全的,而模板是安全的。
6.4.1 模板声明语法
模板声明以template打头,接下来是类型参数列表。这种声明的格式如下:
template <parameter list>
template function / class declaration..
关键字template标志着模板声明的开始,接下来是模板参数列表。该参数列表包含关键字typename,它定义了模板参数objectType,objectType是一个占位符,针对对象实例化模板时,将使用对象的类型替换它。
template <typename T1,typename T2>
bool TemplateFunction(const T1& param1,const T2& param2);
//A template class
template <typename T1,typename T2 = T1>
class Template
{
private:
T1 m_Obj1;
T2 m_Obj2;
public:
T1 GetObj1() {return m_Obj1;}
// ... other members
};
上面函数演示了一个模板函数和模板类,他们都接受两个模板参数:T1和T2,其中T2的类型默认为T1.
6.4.2 各种类型的模板声明
模板声明可以是:
- 函数的声明或者定义
- 类的声明或定义
- 类模板的成员函数或者类成员的声明或定义
- 类声明的静态数据成员的定义
- 嵌套在类模板中的类的静态数据成员的定义
- 类或类模板的成员模板的定义
6.4.3 模板函数
假设要编写一个函数,它适用于不同类型的参数,谓词可使用模板语法!
下面来分析一个模板声明,它与前面讨论的MAX宏等价——返回两个参数这种较大的一个:
template <typename objectType>
const objectType& GetMax(const objectType& value1,const objectType& value2)
{
if (value1>value2)
return value1;
else
return value2;
}
调用模板函数时并非一定要指定类型。
但是对于类模板,必须显示地指定类型。
#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 DisplayComparision(const Type& value1,const Type& value2)
{
cout<<"GetMax("<<value1<<", "<<value2<<") = ";
cout<<GetMax(value1,value2)<<endl;
}
int main()
{
int Int1 = -101, Int2 = 2011;
DisplayComparision(Int1,Int2);
double d1 = 3.14, d2 = 3.1416;
DisplayComparision(d1,d2);
string Name1("Jack"),Name2("John");
DisplayComparision(Name1,Name2);
return 0;
}
6.4.4 模板与类型安全
上面地代码中地类型安全地,但是这不意味着能像下面这样进行无意义的调用:
DisplayComparison(Integer,"Some string");
这种调用将报错。
6.4.5 模板类
模板类是模板化的C++类,是蓝图的蓝图。使用模板类时,可指定要为哪种类型具体化类。这让我们能够创建不同的Human对象,即有的年龄存在long long成员中,有的存储在int成员中,还有的存储在short中。
下面是一个简单的类模板,它只有一个模板参数T:
template <typename Type>
class MyFirstTemplateClass
{
public:
void SetValue (const Type& newValue){Value = newValue;}
const Type& GetValue() const {return Value;}
private:
Type Value;
};
6.4.6 模板的实例化和具体
对于模板,术语实例化的含义稍有不同。用于类时,实例化通常指的是根据类创建对象。
但用于模板时,实例化指的是根据模板声明以及一个或多个参数创建特定的类。
因此,对于下面的模板声明:
template <typename T>
class TemplateClass
{
T m_member;
};
使用该模板时将编写这样的代码:
TemplateClass <int> IntTemplate;
这种实例化创建的特定类型称为具体化。
6.4.7 声明包含多个参数的模板
6.4.8 声明包含默认参数的模板
template <typename T1=int,typename T2=int>
class HoldsPair
{
// ...method declarations
};
6.4.10 模板类和静态成员
略。