C++学习记录(六):宏和模板简介

这个博客中,我们将学习:

  • 预处理器简介
  • 关键字#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 模板类和静态成员

略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值