C++ STL——模板


注:原创不易,转载请务必注明原作者和出处,感谢支持!

注:内容来自某培训课程,不一定完全正确!

一 函数模板的特性

模板技术:类型参数化,编写代码可以忽略类型

为了让编译器区分函数是模板函数还是普通函数,模板函数需要以template <class T>开头,或者以template <typename T>开头。每个模板函数都需要一个上述的开头,一个开头不能对应多个模板函数。

template<class T>
void MySwap(T &a, T &b)
{
	T temp = a;
	a = b;
	b = temp;
}

void callMySwap(void)
{
	// 不指定变量类型,则模板函数会根据你所传递的值进行自动类型推导
	int a = 3, b = 4;
	cout << "a = " << a << " b = " << b << endl;
    // 自动类型推导
	MySwap(a, b);
	cout << "a = " << a << " b = " << b << endl;
	cout << endl;

	double c = 3.14, d = 6.28;
	cout << "c = " << c << " d = " << d << endl;
    // 自动类型推导
	MySwap(c, d);
	cout << "c = " << c << " d = " << d << endl;
	cout << endl;

	// 显式地指定变量类型
	char i = 'A', j = 'B';
	cout << "i = " << i << " j = " << j << endl;
	MySwap<char>(i, j);
	cout << "i = " << i << " j = " << j << endl;

}

模板函数和普通函数在一起的调用规则

  • 模板函数可以像普通函数那样被重载
  • C++编译器优先考虑普通函数
  • 如果函数模板可以产生一个更好的匹配,那么选择模板函数
  • 可以通过空模板实参列表的语法限定编译器只能通过模板匹配

模板函数和普通函数的区别

  • 函数模板不允许自动类型转化
  • 普通函数能过自动进行类型转换
// 函数模板
template<typename T>
int MyAdd(T a, T b)
{
	return a + b;
}

// int char
int MyAdd(int a, char c)
{
	return a + c;
}

// int int
int MyAdd(int a, int b)
{
	return a + b;
}

void callMyAdd(void)
{
	int a = 10, b = 20;
	char c1 = 'A', c2 = 'B';

	// 调用函数模板,因为函数模板是更好的匹配
	cout << MyAdd(a, b) << endl;
	
    // 调用普通函数 int char
	cout << MyAdd(a, c1) << endl;
	
    // 调用普通函数,因为普通函数能够进行参数类型转换,而函数模板不行
	cout << MyAdd(c1, b) << endl;
	
    // 调用普通函数,因为编译器优先考虑普通函数
	cout << MyAdd(a, b) << endl;
	
    // 通过空模板实参列表强制要求编译器调用函数模板
	cout << MyAdd<>(a, b) << endl;
}

模板函数的重载:

// 函数模板的重载
template<class T>
void Print(T a)
{
	cout << a << endl;
}

template<class T>
void Print(T a, T b)
{
	cout << "a = " << a << " b = " << b << endl;
}

void callPrint(void)
{
	Print(3);
	Print(7, 8);
}

下面是一个函数模板的具体使用案例。

// 函数模板使用案例
// 打印数组
template<typename T>
void PrintArray(T *arr, int len)
{
	for (int i = 0; i < len; ++i)
		cout << arr[i] << " ";
	cout << endl;
}

// 对数组按从大到小顺序排序
template<typename T>
void MySort(T *arr, int len)
{
	for (int i = 0; i < len; ++i)
	{
		for (int j = i + 1; j < len; ++j)
		{
			if (arr[i] < arr[j])
			{
				T tmp = arr[i];
				arr[i] = arr[j];
				arr[j] = tmp;
			}
		}
	}
}

void call(void)
{
	int iArr[] = { 9, 1, 5, 2, 8, 6 };
	int ilen = sizeof(iArr) / sizeof(int);

	char cArr[] = { 'j', 'b', 'e', 'i', 'k' };
	int clen = sizeof(cArr) / sizeof(char);

	cout << "排序前:" << endl;
	PrintArray<int>(iArr, ilen);
	MySort<int>(iArr, ilen);
	cout << "排序后:" << endl;
	PrintArray<int>(iArr, ilen);

	cout << "排序前:" << endl;
	PrintArray<char>(cArr, clen);
	MySort<char>(cArr, clen);
	cout << "排序后:" << endl;
	PrintArray<char>(cArr, clen);
}

二 模板的实现机制

C++文件编译过程

输入 编译过程 输出
hello.cpp 预编译(g++ -E) hello.i
hello.i 编译(g++ -S) hello.s
hello.s 汇编(g++ -c) hello.o
hello.o 链接(g++) hello.out

hello.cpp中的内容

#include <iostream>

using namespace std;

#define MAX 1024

int main(int argc, char **argv)
{
	cout << "MAX = " << MAX << endl;
    
    return 0;
}

可以看到hello.i中的头文件不见了,取而代之的是头文件iostream中的内容被插入到了hello.i中,源代码中的define语句不见了,宏MAX被替换成了1024。hello.s则是对应的汇编语言文件,hello.o是二进制文件,最后一步链接之后,生成了可执行文件hello.out。

hello.cpp中的内容为:

#include <iostream>
using namespace std;

// 函数模板
template<class T>
T MyAdd(T a, T b)
{
    return a + b;
}

int main(int argc, char **argv)
{
    int a = 10;
    int b = 20;
    double da = 3.14;
    double db = 6.28;

    MyAdd(a, b);	// 对应的汇编入口地址:_Z5MyAddIiET_S0_S0_
    MyAdd(da, db);	// 对应的汇编入口地址:_Z5MyAddIdET_S0_S0_

    return 0;
}

得到它对应的汇编文件:

g++ -S hello.cpp -o hello.s

打开hello.s文件,可以看到在main函数中的两次函数调用分别对应着如下的两条汇编命令。注意到它们的调用地址是完全不一样的。

call    _Z5MyAddIiET_S0_S0_

call    _Z5MyAddIdET_S0_S0_

这说明同一个函数模板生成了两个不同的模板函数。这两个模板函数对应的汇编内容分别如下所示:

_Z5MyAddIiET_S0_S0_:
.LFB973:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -8(%rbp), %eax
    movl    -4(%rbp), %edx
    addl    %edx, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE973:
    .size   _Z5MyAddIiET_S0_S0_, .-_Z5MyAddIiET_S0_S0_
    .section    .text._Z5MyAddIdET_S0_S0_,"axG",@progbits,_Z5MyAddIdET_S0_S0_,comdat
    .weak   _Z5MyAddIdET_S0_S0_
    .type   _Z5MyAddIdET_S0_S0_, @function

_Z5MyAddIdET_S0_S0_:
.LFB974:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movsd   %xmm0, -8(%rbp)
    movsd   %xmm1, -16(%rbp)
    movsd   -8(%rbp), %xmm0
    addsd   -16(%rbp), %xmm0
    movsd   %xmm0, -24(%rbp)
    movq    -24(%rbp), %rax
    movq    %rax, -24(%rbp)
    movsd   -24(%rbp), %xmm0
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE974:
    .size   _Z5MyAddIdET_S0_S0_, .-_Z5MyAddIdET_S0_S0_
    .text
    .type   _Z41__static_initialization_and_destruction_0ii, @function

将hello.cpp的内容改成如下的形式,也即是加了两条代码。

#include <iostream>
using namespace std;

// 函数模板
template<class T>
T MyAdd(T a, T b)
{
    return a + b;
}

int main(int argc, char **argv)
{
    int a = 10;
    int b = 20;
    double da = 3.14;
    double db = 6.28;

    MyAdd(a, b);
    MyAdd(da, db);
    MyAdd(3, 4);
    MyAdd(1.0, 2.3);

    return 0;
}

再次得到它对应的hello.s文件。可以看到main函数里的四次函数调用汇编语句为:

1  call    _Z5MyAddIiET_S0_S0_
2  call    _Z5MyAddIdET_S0_S0_
3  call    _Z5MyAddIiET_S0_S0_
4  call    _Z5MyAddIdET_S0_S0_

可以看到,第1次和第3次调用是相同的,第2次和第4次调用也是相同的。也就是说MyAdd(a, b)MyAdd(3, 4)调用的是同一个模板函数,MyAdd(da, db)MyAdd(1.0, 2.3)调用的是同一个模板函数。

结论:
(1) 编译器并不是把函数模板处理成能够处理任何类型的函数
(2) 函数模板可以通过具体类型产生不同的函数
(3) 编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。

三 类模板

类模板与之前的函数模板非常类似,下面是一个类模板的具体小例子。

// 类模板Person
template<typename T>
class Person
{
public:
	Person(T id, T age)
	{
		mId = id;
		mAge = age;
	}

	void Show(void)
	{
		cout << "ID = " << mId << endl;
		cout << "AGE = " << mAge << endl;
	}

private:
	T mId;
	T mAge;
};

void call(void)
{
	// 函数模板在调用时可以进行自动类型推导
	// 而类模板则只能显示地指定变量类型

	// 不显式地指定类模板的变量类型会报错
	// Person p(10, 20);

	Person<int> p(10, 20);
	p.Show();
}

需要注意的地方在于,之前的函数模板可以进行自动类型推导而无需显式地指定变量类型。类模板就不行了,必须指定参数类型,否则无法通过编译。

四 类模板如何派生子类

类模板派生普通类
类模板的派生和普通类的派生很类似,需要注意的是,类模板在派生普通类的时候需要指定类模板的具体数据类型。


// 类模板Person
template<typename T>
class Person
{
public:
	Person(T id, T age)
	{
		mId = id;
		mAge = age;
	}

	void Show(void)
	{
		cout << "ID = " << mId << endl;
		cout << "AGE = " << mAge << endl;
	}

private:
	T mId;
	T mAge;
};

// 类模板派生普通类需要指定类模板的具体数据类型
class Student : public Person<int>
{

};

类模板派生类模板
Animal的数据类型就是实例化Cat时指定的数据类型T。

template<typename T>
class Animal
{
public:
	void Jiao(void)
	{
		cout << mAge << "岁的动物在叫!" << endl;
	}

private:
	T mAge;
};

// Animal的数据类型就是实例化Cat时指定的数据类型T(Cat的T传递给了Animal)
template<typename T>
class Cat : public Animal<T>
{

};

当然你也可以跟上面派生普通类时那样,为父类模板Animal指定特定的数据类型。


template<typename T>
class Animal
{
public:
	void Jiao(void)
	{
		cout << mAge << "岁的动物在叫!" << endl;
	}

private:
	T mAge;
};

// 指定Animal的数据类型为int
template<typename T>
class Cat : public Animal<int>
{
public:
	void eat() {}

private:
	T color;
};

五 普通类的.h和.cpp文件分离

Person.h里只做声明,具体实现放在Person.cpp当中。Person.h中的内容如下:

#pragma once	// 防止头文件被重复包含
#include <iostream>
#include <string>

using namespace std;

class Person
{
public:
	Person(string name, int age);
	void Show(void);

private:
	string mName;
	int mAge;
	int mID;
};

Person.cpp中的内容如下。

#include "Person.h"

Person::Person(string name, int age)
{
	this->mName = name;
	this->mAge = age;
}

void Person::Show()
{
	cout << "Name = " << this->mName << endl;
	cout << "Age = " << this->mAge << endl;
}

在main.cpp中使用该类。

int main(int argc, char **argv)
{
	Person p("Jone", 20);
	p.Show();

	getchar();
	return 0;
}

六 类模板在类内类外的实现

类模板在类内部实现

// 类模板在类内部实现
template<typename T1, typename T2>
class Person
{
public:
	Person(T1 name, T2 age)
	{
		this->mName = name;
		this->mAge = age;
	}

	void Show(void)
	{
		cout << "Name = " << mName << endl;
		cout << "Age = " << mAge << endl;
	}

private:
	T1 mName;
	T2 mAge;
};

void call(void)
{
	Person<string, int> p("Jone", 20);
	p.Show();
}

类模板在类外部实现
模仿普通类那样声明和实现分离的方式来写类模板,也即类模板在类外部实现。Person.h和Person.cpp分离的方式,在Person.h只声明不实现。

#pragma once

template<typename T>
class Person
{
public:
	Person(T age);
	void Show(void);
public:
	T mAge;
};

在Person.cpp中实现。

#include "Person.h"
#include <iostream>

template<typename T>
Person<T>::Person(T age)
{
	this->mAge = age;
}

template<typename T>
void Person<T>::Show(void)
{
	std::cout << "Age = " << mAge << std::endl;
}

在main.cpp中调用。

#include <iostream>
#include "Person.h"

using namespace std;

int main(int argc, char **argv)
{
	Person<int> p(23);
	p.Show();

	getchar();
	return 0;
}
像上面这样写是不行的!

首先,上述代码能够通过编译步骤但无法通过链接步骤!也就是说上述代码无法达到目的!

原因在于C++模板的二次编译特性!模板的初次编译只是对模板本身进行编译,它不生成具体类型对应的类的目标代码。具体类型对应的类代码只有在指定了变量类型的调用时后才能够生成。而在链接main.cpp时,因为找不到数据类型为int的具体类Person<int>的目标代码,所以链接器会报一个无法解析外部符号的错误,无法完成编译!

怎样解决这个问题?

很简单,在main.cpp中不再包含Person.h而是包含Person.cpp,利用include指令强行将类的定义内容给包含到main.cpp当中。这样,虽然模板类是分开写的,但是编译时却是在main.cpp当中一起被编译的。为了以示区别,一般将Person.cpp改名为Person.hpp。这样,Person.h里包含了模板类的定义,Person.hpp里包含了它的实现,而在main.cpp当中将Person.hpp给include进来即可。

模板类里的static变量
每一个由同一个模板类派生出来的具体类都将拥有与模板类里相同的static变量。所派生出来的不同的具体类所有的同名static变量互不影响,如下面的例子所示。

// 模板类内定义static变量a
template <typename T>
class Person
{
public:
	static int a;
};

// 模板类外进行static变量初始化
template <typename T> int Person<T>::a = 0;

int main(int argc, char **argv)
{
	// Person<int>和Person<char>将拥有不同的static变量a
	Person<int> p1, p2;
	Person<char> pp1, pp2;

	// p1.a的修改只影响Person<int>里的a的值
	p1.a = -1;

	// p1和p2共享Person<int>类的static变量a
    /*
    	输出:
        p1.a = -1
        p2.a = -1
        pp1.a = 0
        pp2.a = 0
    */
	cout << "p1.a = " << p1.a << endl;
	cout << "p2.a = " << p2.a << endl;
	cout << "pp1.a = " << pp1.a << endl;
	cout << "pp2.a = " << pp2.a << endl;

	// pp1.a的修改只影响Person<char>里的a的值
	pp1.a = 10;

    /*
    	输出:
        p1.a = -1
		p2.a = -1
		pp1.a = 10
		pp2.a = 10
    */
	cout << "p1.a = " << p1.a << endl;
	cout << "p2.a = " << p2.a << endl;
	// pp1和pp2共享Person<char>类里的static变量a
	cout << "pp1.a = " << pp1.a << endl;
	cout << "pp2.a = " << pp2.a << endl;
    
	getchar();
	return 0;
}

七 模板的应用实例

下面是一个C++模板的应用实例。MyArray.hpp中实现模板类的声明和定义

#pragma once

template <typename T>
class MyArray
{
public:
	// 构造函数和析构函数
	MyArray(int capacity);
	MyArray(const MyArray<T> &arr);
	~MyArray(void);

	// 运算符重载
	T& operator[] (int index);
	MyArray<T> operator= (const MyArray<T> &arr);

	// 追加数据
	void PushBack(const T &data);

	// 辅助函数
	int Capacity(void);
	int Size(void);

private:
	int mCapacity;	// 数组最大容量
	int mSize;		// 数组当前元素个数
	T *pAddr;		// 数组首地址指针
};

template <typename T>
MyArray<T>::MyArray(int capacity)
{
	this->mCapacity = capacity;
	this->mSize = 0;
	this->pAddr = new T[this->mCapacity];
}

template <typename T>
MyArray<T>::MyArray(const MyArray<T> &arr)
{
	this->mSize = arr.mSize;
	this->mCapacity = arr.mCapacity;

	this->pAddr = new T[this->mCapacity];
	for (int i = 0; i < this->mSize; ++i)
		this->pAddr[i] = arr.pAddr[i];
}

template <typename T>
MyArray<T>::~MyArray(void)
{
	if (this->pAddr != nullptr)
	{
		delete[] this->pAddr;
		this->mCapacity = 0;
		this->mSize = 0;
	}
}

template <typename T>
T& MyArray<T>::operator[] (int index)
{
	return this->pAddr[index];
}

template <typename T>
MyArray<T> MyArray<T>::operator= (const MyArray<T> &arr)
{
	if (this->pAddr != nullptr)
		delete[] this->pAddr;

	this->mCapacity = arr.mCapacity;
	this->mSize = arr.mSize;
	this->pAddr = new T[this->mCapacity];
	for (int i = 0; i < this->mSize; ++i)
		this->pAddr = arr.pAddr[i];

	return *this;
}

template <typename T>
void MyArray<T>::PushBack(const T &data)
{
	if (this->mSize >= this->mCapacity)
		return;

	this->pAddr[this->mSize] = data;
	this->mSize++;
}

template <typename T>
int MyArray<T>::Capacity(void)
{
	return this->mCapacity;
}

template <typename T>
int MyArray<T>::Size(void)
{
	return this->mSize;
}

在main.cpp中调用。

#include <iostream>
#include "MyArray.hpp"

using namespace std;

void test(void)
{
	MyArray<int> marray(20);
	int a = 10, b = 20, c = 30, d = 40;
	marray.PushBack(a);
	marray.PushBack(b);
	marray.PushBack(c);
	marray.PushBack(d);
	marray.PushBack(100);
	marray.PushBack(200);
	marray.PushBack(300);

	// marray.PushBack(10);
	// marray.PushBack(20);

	cout << "marray.mCapacity = " << marray.Capacity() << endl;
	cout << "marray.mSize = " << marray.Size() << endl;
	for (int i = 0; i < marray.Size(); ++i)
	{
		cout << marray[i] << " ";
	}
	cout << endl;
}

int main(int argc, char **argv)
{
	test();

	getchar();
	return 0;
}
posted @ 2019-10-31 20:26  wallace-rice  阅读(268)  评论(0编辑  收藏  举报