21天学通C++:第十三、十四章节

第十三章:类型转换运算符

类型转换是一种机制,让程序员能够暂时或永久性改变编译器对对象的解释。注意,这并不意味着程序员改变了对象本身,而只是改变了对对象的解释。可改变对象解释方式的运算符称为类型转换运算符。

为何需要类型转换

如果 C++应用程序都编写得很完善,其处于类型是安全的且是强类型的世界,则没有必要进行类型转换,也不需要类型转换运算符。然而,在现实世界中,不同模块往往由不同的人编写,而使用不同开发环境的厂商需要协作。因此,程序员经常需要让编译器按其所需的方式解释数据,让应用程序能够成功编译并正确执行

在 C++的发展过程中,不断有新的 C++类型转换运算符出现,这导致 C++编程社区分裂成两个阵营:一个阵营继续在其 C++应用程序中使用 C 风格类型转换;另一个阵营转而使用 C++编译器引入的类型转换关键字。前一个阵营认为,C++类型转换难以使用,且有时候功能变化不大,只有理论意义。后一个阵营则显然由 C++语法纯粹论者组成,他们通过指出 C 风格类型转换的缺陷以支持其论点。在现实世界中,这两个观点各行其道,读者最好通过阅读本章以了解每种风格的优缺点,然后形成自己的见解。

为何有些 C++程序员不喜欢 C 风格类型转换

当前,C++编译器仍需向后兼容,以确保遗留代码能够通过编译,因此支持下面这样的语法:

char* staticStr = "Hello World!";
int* intArray = (int*)staticStr; // Cast one problem away, create another

这种 C 风格类型转换实际上强迫编译器根据程序员的选择来解释目标对象。对不希望类型转换破坏其倡导的类型安全的 C++程序员来说,这是无法接受的。

C++类型转换运算符

虽然类型转换有缺点,但也不能抛弃类型转换的概念。在很多情况下,类型转换是合理的需求,可解决重要的兼容性问题。C++提供了一种新的类型转换运算符,专门用于基于继承的情形,这种情形在 C 语言编程中并不存在。

4 个 C++类型转换运算符如下:

• static_cast
• dynamic_cast
• reinterpret_cast
• const_cast

这 4 个类型转换运算符的使用语法相同:

destination_type result = cast_operator<destination_type> (object_to_cast);

使用 static_cast

static_cast 用于在相关类型的指针之间进行转换,还可显式地执行标准数据类型的类型转换—这种转换原本将自动或隐式地进行。用于指针时,static_cast 实现了基本的编译阶段检查,确保指针被转换为相关类型。

这改进了 C 风格类型转换,在 C 语言中,可将指向一个对象的指针转换为完全不相关的类型,而编译器不会报错。使用 static_cast 可将指针向上转换为基类类型,也可向下转换为派生类型,如下面的示例代码所示:

Base* objBase = new Derived();
Derived* objDer = static_cast<Derived*>(objBase); //OK

//类 Unrelated 和 Base 父类没有关系,因此下面的转换报错
Unrelated* notRelated = static_cast<Unrelated*> (objBase); //Error
//static_cast 不允许转换成不相干的类型

在这里插入图片描述

然而,static_cast 只验证指针类型是否相关,而不会执行任何运行阶段检查。因此,程序员可使用static_cast 编写如下代码,而编译器不会报错:

Base* objBase = new Base();
Derived* objDer = static_cast<Derived*>(objBase); //仍然是错误的

其中 objDer 实际上指向一个不完整的 Derived 对象,因为它指向的对象实际上是 Base()类型。

由于 static_cast 只在编译阶段检查转换类型是否相关,而不执行运行阶段检查,因此 objDer -> DerivedFunction()能够通过编译,但在运行阶段可能导致意外结果。

除用于向上转换和向下转换外,static_cast 还可在很多情况下将隐式类型转换为显式类型,以引起程序员或代码阅读者的注意:

double Pi = 3.14159265; 
int num = static_cast<int>(Pi); // Making an otherwise implicit cast, explicit

在上述代码中,使用 num = Pi 将获得同样的效果,但使用 static_cast 可让代码阅读者注意到这里使用了类型转换并指出(对知道 static_cast 的人而言)编译器根据编译阶段可用的信息进行了必要的调整,以便执行所需的类型转换。对于使用关键字 explicit 声明的转换运算符和构造函数,要使用它们,也必须通过 static_cast。

使用 dynamic_cast 和运行阶段类型识别

顾名思义,与静态类型转换相反,动态类型转换在运行阶段(即应用程序运行时)执行类型转换可检查 dynamic_cast 操作的结果,以判断类型转换是否成功。使用 dynamic_cast 运算符的典型语法如下:

destination_type* Dest = dynamic_cast<class_type*>(Source);

if(Dest) // Check for success of the casting operation 
	Dest->CallFunc ();

例如:

Base* objBase = new Derived();

//演示 dynamic_cast
Derived* objDer = dynamic_cast<Derived*>(objBase);

if(objDer) //查看转换是否成功
	objDer->CallDerivedFunction();

如上述代码所示,给定一个指向基类对象的指针,程序员可使用 dynamic_cast 进行类型转换,并在使用指针前检查指针指向的目标对象的类型。在上述示例代码中,目标对象的类型显然是 Derived,因此这些代码只有演示价值。然而,情况并非总是如此,例如,将 Derived*传递给接受 Base*参数的函数时。该函数可使用 dynamic_cast 判断基类指针指向的对象的类型,再执行该类型特有的操作。总之,可使用 dynamic_cast 在运行阶段判断类型,并在安全时使用转换后的指针。下面的程序使用了一个您熟悉的继承层次结构—Tuna 和 Carp 类从基类 Fish 派生而来,其中的函数 DetectFishtype( )动态地检查 Fish 指针指向的对象是否是 Tuna 或 Carp。

在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>

using namespace std;

class Fish {
public:
	virtual void Swim() {
		cout << "Fish swims in water" << endl;
	}

	//基类总是应该保证有一个虚析构函数
	virtual ~Fish(){}
};

class Tuna : public Fish{
public:
	void Swim() {
		cout << "Tuna swims real fast in the sea" << endl;
	}

	void BecomeDinner() {
		cout << "Tuna became dinner in Sushi" << endl;
	}
};

class Carp : public Fish {
public:
	void Swim() {
		cout << "Carp swims real slow in the lake" << endl;
	}

	void Talk() {
		cout << "Carp talked Carp" << endl;
	}
};

void DetectFishType(Fish* objFish) {
	//确保是 Tuna 类型才执行后面的事情
	Tuna* objTuna = dynamic_cast<Tuna*>(objFish);
	if (objTuna) { //检查是否转换成功
		cout << "Detected Tuna. Making Tuna dinner: " << endl;
		objTuna->BecomeDinner();
	}

	Carp* objCarp = dynamic_cast<Carp*>(objFish);
	if (objCarp) {
		cout << "Detected Carp. Making carp talk: " << endl;
		objCarp->Talk();
	}

	cout << "Verifying type using virtual Fish::Swim: " << endl;
	objFish->Swim(); // calling virtual function Swim
}

int main() {
	Carp myLunch;
	Tuna myDinner;

	DetectFishType(&myDinner);
	cout << endl;
	DetectFishType(&myLunch);

	return 0;
}

输出:

在这里插入图片描述

这个示例的独特之处在于,给定一个基类指针(Fish*),您可动态地检测它指向的是否是 Tuna 或 Carp。

这种动态检测(运行阶段类型识别)是在第 38~54 行定义的函数 DetectFishType( )中进行的。在第 40 行,使用 dynamic_cast 传入的基类指针(Fish*)参数指向的是否是 Tuna 对象。如果该 Fish*指向的是 Tuna 对象,该运算符将返回一个有效的地址,否则将返回 NULL。因此,总是需要检查 dynamic_cast 的结果是否有效。如果通过了第 40 行的检查,您便知道指针 objTuna 指向的是一个有效的 Tuna 对象,因此可以使用它来调用函数 Tuna::BecomeDinner( ),如第 43 行所示。如果传入的 Fish*参数指向的是 Carp 对象,则使用它来调用函数 Carp::Talk( ),如第 49 行所示。返回之前,DetectFishType( )调用了 Swim( ),以验证对象类型;Swim( )是一个虚函数,这行代码将根据指针指向的对象类型,调用相应类(Tuna 或 Carp)中实现的方法 Swim( )。

在这里插入图片描述

使用 reinterpret_cast

reinterpret_cast 是 C++中与 C 风格类型转换最接近的类型转换运算符。它让程序员能够将一种对象类型转换为另一种,不管它们是否相关;也就是说,它使用如下所示的语法强制重新解释类型:

Base* objBase = new Base (); 
Unrelated* notRelated = reinterpret_cast<Unrelated*>(objBase); 
// The code above compiles, but is not good programming!

这种类型转换实际上是强制编译器接受 static_cast 通常不允许的类型转换,通常用于低级程序(如驱动程序),在这种程序中,需要将数据转换为 API(应用程序编程接口)能够接受的简单类型。

由于其他 C++类型转换运算符都不允许执行这种有悖类型安全的转换,因此除非万不得已,否则不要使用 reinterpret_cast 来执行不安全(不可移植)的转换

在这里插入图片描述

使用 const_cast

const_cast 让程序员能够关闭对象的访问修饰符 const。

您可能会问:为何要进行这种转换?在理想情况下,程序员将经常在正确的地方使用关键字 const。不幸的是,现实世界并非如此,像下面这样的代码随处可见:

class SomeClass { 
public: 
	// ... 
	void DisplayMembers(); //problem - display function isn't const 
};

在下面的函数中,以 const 引用的方式传递 object 显然是正确的。毕竟,显示函数应该是只读的,不应调用非 const 成员函数,即不应调用能够修改对象状态的函数。然而,DisplayMembers()本应为 const 的,但却没有这样定义。如果 SomeClass 归您所有,且源代码受您控制,则可对 DisplayMembers()进行修改。然而,在很多情况下,它可能属于第三方库,无法对其进行修改。在这种情况下,const_cast 将是您的救星。

void DisplayAllData (const SomeClass& object) { 
	object.DisplayMembers (); // Compile failure 
	// reason: call to a non-const member using a const reference 
}

在这种情况下,调用 DisplayMembers()的语法如下:

void DisplayAllData (const SomeClass& object) { 
	SomeClass& refData = const_cast<SomeClass&>(object); 
	refData.DisplayMembers(); // Allowed! 
}

除非万不得已,否则不要使用 const_cast 来调用非 const 函数。一般而言,使用 const_cast 来修改 const 对象可能导致不可预料的行为。

另外,const_cast 也可用于指针:

void DisplayAllData (const SomeClass* data) { 
	// data->DisplayMembers(); Error: attempt to invoke a non-const function! 
	SomeClass* pCastedData = const_cast<SomeClass*>(data); 
	pCastedData->DisplayMembers(); // Allowed! 
}

总结

在这里插入图片描述

本章介绍了各种 C++类型转换运算符以及支持和反对类型转换运算符的根据。一般而言,应避免使用类型转换

第十四章:宏和模板简介

预处理器与编译器

顾名思义,预处理器在编译器之前运行,换句话说,预处理器根据程序员的指示,决定实际要编译的内容。预处理器编译指令都以 # 打头,例如:

#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 做好了准备!

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 《21天学通C 第8版》是一本系统介绍C语言基础知识的教材。全书分为21章节,旨在帮助读者在21天内掌握C语言的基本语法、数据类型、运算符、流程控制语句、数组和指针、函数与递归等知识点,并能完成简单的编程练习。 该书的优点在于系统性强、简明易懂、内容涵盖面广。每一章节都设置了知识点总结和习题练习,读者在看完后可以通过练习加深对知识点的理解和掌握。同时,《21天学通C 第8版》还附带了光盘,内含配套的编程软件和课程视频,方便读者边学边练,在实践中掌握C语言的应用。 不过,《21天学通C 第8版》也有一些缺点。书中部分内容略显单薄,有些知识点缺乏深度讲解。同时,随着C语言发展和应用的日益广泛,该书的内容已经有些落后,没有覆盖到一些现代C语言编程的技术和工具。此外,对于初学者来说,21天的学习时间有些紧张,对于基础薄弱的读者可能需要更长时间的学习和练习。 总体而言,如果你是初学者,并且想快速入门C语言编程,那么《21天学通C 第8版》是一本不错的教材。但如果你已经有了一定的编程基础,或者想学习更深入的C语言编程知识,那么建议结合其他教材和实践项目进行学习。 ### 回答2: 《21天学通C》第8版是一本深入浅出、适合初学者学习C语言的经典教程。本书从C语言基础开始,系统地介绍了语言的各个方面,包括语法、数据类型、运算符、表达式、循环、分支、函数、指针、结构体、数据存储和IO操作等内容,每一章节都有丰富的例子和练习题,帮助读者更好地理解和掌握知识点。 这本书的特点是结构清晰、示例详细、实现灵活、应用广泛。它不仅讲解了C语言的基本语法和编程技巧,还涉及了一些高级话题,例如控制台程序、文件操作、数据结构和算法等。此外,本书还介绍了一些实际应用中常用的C库函数和工具,例如stdio.h、stdlib.h、string.h、time.h、make和gcc等。 学习这本书不仅能够帮助初学者快速掌握C语言,还能够提高他们的编程能力和实践经验。本书的内容深入浅出,涵盖全面,适合广大学生、初学者、自学者和编程爱好者使用,它也是一本值得长期保留和经常翻阅的经典教材。 ### 回答3: 《21天学通C》是一本经典的编程入门教材,旨在帮助初学者快速掌握C语言编程基础知识,从而能够编写简单的C程序。第8版在原有基础上进行了全面更新和完善,内容更加丰富、实用。 本书共分为21天的学习计划,每天均有相应的课程安排和练习题。第一天介绍了C语言的概述及编译器的使用,第二天开始介绍变量、数据类型、表达式等基础知识,随后每天的内容都逐渐加深,包括运算符、流程控制、函数、数组、指针等重要的C语言特性。每一章节都采用了清晰而易懂的语言和实例说明,让初学者能够更快的理解和掌握C语言。 值得一提的是,本书的第21天还特别介绍了一些高级的C语言编程技巧和应用,如结构体、文件操作、内存动态分配、多线程等,这些知识对于想进一步深入学习C语言的人来说非常有帮助。 总体而言,《21天学通C》第8版是一本经典而实用的编程入门教材,很好地贯穿了C语言的基础和高级知识点,能够让初学者快速掌握并灵活运用C语言编程技能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在地球迷路的怪兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值