C++模板
C++提供了function template.
function template:实际上是建立一个通用函数,其函数类型和形参类型不具体制定,用一个虚拟的类型来代表。这个通用的函数就称为函数模版。
是不是可以这样理解,函数模版就是给了一种功能,能够适合多种数据的功能?
- C++提供两种模版机制:函数模版和类模版。
- 类属-类型参数化,又称参数模版。
总结:
- 模版把函数或类要处理的数据类型参数化,表现为参数的多态性,称为类属。
- 模版用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为。
1 函数模板
用函数模版,是为了实现泛型,减轻编程的工作量,增强函数的重要性
#include <iostream>template<typename T>
void Myswap(T& a, T& b){T temp = a;a = b;b = temp;
}int main(){int a = 10, b = 20;Myswap(a, b);std::cout << "a = " << a << " b = " << b << std::endl;double x = 1.5, y = 2.5;Myswap(x, y);std::cout << "x = " << x << " y = " << y << std::endl;char c = 'a', d = 'b';Myswap(c, d);std::cout << "c = " << c << " d = " << d << std::endl;return EXIT_SUCCESS;
}
在上述的代码中,我们写了一个基于模板的交换函数,可以用于交换int类型、double类型和char类型,如果不使用模板函数,就需要写三个交换函数。
void Myswap(T& a, T& b);
void Myswap(int& a , int& b);
void Myswap(double& a, double& b);
void Myswap(char& a , char& b);
1.1 函数模板和普通函数区别
- 函数模板不允许自动类型转化,必须严格匹配类型;
- 普通函数能够自动进行类型转化
#include <iostream>template<typename T>
T MyPlus(T a, T b){std::cout << "int TemplateT + T = " << a + b << std::endl;T ret = a + b;return ret;
}int MyPlus(int a, char b){std::cout << "int noTemplate char + int = " << a + b << std::endl;int ret = a + b;return ret;
}int main(){std::cout << "Hello World!" << std::endl;int a = 10;char b = 'a';MyPlus(a,a); // function template MyPlus<int>(a, a) is calledMyPlus(b,b); // function template MyPlus<char>(a, b) is calledMyPlus(a,b); // function MyPlus(int, char) is called// 普通函数可以隐式类型转换MyPlus(b,a); //function template MyPlus<char>(b, a) is calledreturn EXIT_SUCCESS;
}/*
Hello World!
int TemplateT + T = 20
int TemplateT + T = 194
int noTemplate char + int = 107
int noTemplate char + int = 107
*/
/* why not define template
template<typename T1, typename T2>
T1 MyPlus(T1 a, T2 b) {std::cout << "T1,T2 Template T2 + T1 = " << a + b << std::endl;T1 ret = a + b;return ret;
}
MyPlus(a,b); // template<typename T1, typename T2> is called
MyPlus(b,a); // template<typename T1, typename T2> is called
*/
1.1.1 为什么函数模板不允许自动类型转换
在C++中,函数模板不允许自动类型转换的主要原因是为了保持类型安全和编译时的类型检查。以下是几个关键原因:
- 类型安全:函数模板的设计初衷是提供一种泛型编程的方式,使得代码可以处理多种数据类型。为了确保类型安全,编译器需要确保模板参数的类型是精确匹配的。如果允许自动类型转换,可能会导致意外的类型转换和潜在的类型错误。
- 编译时类型检查:C++是一种静态类型语言,编译器在编译时会进行严格的类型检查。函数模板参数的精确匹配确保了编译器可以在编译时发现类型不匹配的错误,而不是在运行时才发现问题。
- 避免二义性和不确定性:如果允许自动类型转换,可能会导致函数调用的二义性。例如,如果有多个函数模板可以匹配某个调用,编译器将难以确定应该选择哪个模板实例。通过要求精确匹配,可以避免这种二义性。
- 模板特化和重载:C++允许对模板进行特化和重载,这提供了更灵活的类型处理方式。如果允许自动类型转换,可能会干扰这些机制的正常工作,使得模板特化和重载的规则变得复杂和难以管理。
例如,考虑以下函数模板:
template <typename T>
T add(T a, T b) {return a + b;
}
如果允许自动类型转换,调用 add(5, 3.2)
可能会导致编译器尝试将 int
转换为 double
或反之,这不仅会增加编译器的复杂性,还可能导致意外的结果。
因此,为了保持类型安全、编译时类型检查和避免二义性,C++中的函数模板不允许自动类型转换。如果需要处理不同类型的参数,可以使用多个类型参数的模板、模板特化或重载函数来实现。
1.1.2 自动类型转换
在C++中,自动类型转换(也称为隐式类型转换)是指编译器在需要时自动将一种数据类型转换为另一种数据类型的过程。这种转换通常发生在不同数据类型的变量进行运算或赋值时,以确保操作能够顺利进行。
C++中的自动类型转换遵循一定的规则和优先级,常见的自动类型转换包括:
- 整数提升:较小的整数类型(如
char
和short
)在表达式中会被自动提升为int
类型。 - 整数和浮点数之间的转换:整数类型可以自动转换为浮点数类型,但可能会导致精度损失。
- 派生类到基类的转换:派生类对象可以自动转换为基类对象。
- 算术转换:在算术运算中,操作数会被转换为相同的类型,通常是表达式中最高级别的类型。
以下是一些C++中自动类型转换的示例:
#include <iostream>int main() {int a = 5; // 整数类型double b = 3.2; // 浮点数类型double c = a + b; // 自动将整数 a 转换为浮点数类型,然后进行加法运算std::cout << c << std::endl; // 输出 8.2char ch = 'A'; // 字符类型int i = ch; // 自动将字符类型转换为整数类型std::cout << i << std::endl; // 输出 65('A' 的 ASCII 值)return 0;
}
在这个示例中,整数 a
被自动转换为浮点数类型,以便与浮点数 b
进行加法运算。字符 ch
被自动转换为整数类型,以便赋值给整数变量 i
。
1.2 函数模板和普通函数在一起调用规则
在C++中,函数模板和普通函数可以共存,并且在调用时遵循一定的规则来确定调用哪个函数。这些规则包括函数模板实例化、普通函数的重载解析以及模板参数推导等。以下是一些关键规则:
- 普通函数的优先级:如果存在一个普通函数,其参数类型与调用参数完全匹配,那么编译器会优先选择这个普通函数,而不是实例化一个函数模板。
- 模板参数推导:如果调用参数与普通函数不匹配,编译器会尝试推导函数模板的参数类型,并实例化一个模板函数。
- 重载解析:如果存在多个函数模板或普通函数可以匹配调用参数,编译器会进行重载解析,选择最匹配的函数。
以下是一个示例,展示了函数模板和普通函数在一起调用的规则:
#include <iostream>// 普通函数
int add(int a, int b) {return a + b;
}// 函数模板
template <typename T>
T add(T a, T b) {return a + b;
}int main() {int a = 5,b = 3;double c = 3.2, d = 2.1;// 调用普通函数,因为参数类型完全匹配std::cout << add(a, b) << std::endl; // 输出 8// 调用函数模板,因为普通函数不匹配 double 类型std::cout << add(c, d) << std::endl; // 输出 5.3// 调用函数模板,因为普通函数不匹配 double 和 int 的组合std::cout << add(a, c) << std::endl; // 输出 8.2(注意:这里会进行隐式转换)return 0;
}
在这个示例中:
add(a, b)
调用普通函数int add(int, int)
,因为参数类型完全匹配。add(c, d)
调用函数模板T add(T, T)
,因为普通函数不匹配double
类型。add(a, c)
调用函数模板T add(T, T)
,因为普通函数不匹配int
和double
的组合。在这种情况下,编译器会尝试推导模板参数类型,但由于模板参数必须一致,a
会被隐式转换为double
类型。
总结来说,函数模板和普通函数在一起调用时,编译器会根据参数类型匹配、模板参数推导和重载解析等规则来确定调用哪个函数。普通函数如果参数类型完全匹配,会优先被调用;否则,编译器会尝试实例化函数模板。
1.3 模板的编译过程
?为什么函数模板可以和普通函数放在一起?
编译过程:预处理(Pre-processing)->编译(Compiling)-> 汇编(Assembling)-> 链接(Linking)
函数模板机制理论
- 编译器并不是把函数模板处理成能够处理成任何类型的函数
- 函数模板通过具体类型产生不同的函数
- 编译器对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
局限性:函数模板很有可能无法处理某些类型,比如两数相加的模板就无法处理两个数组相加。此时可以使用模板重载的方式解决。
1.4 函数模板重载
在C++中,函数模板可以像普通函数一样进行重载。函数模板重载允许定义多个具有相同名称但参数类型或数量不同的模板函数。编译器会根据调用时的参数类型和数量来选择合适的模板实例。
以下是一些函数模板重载的示例:
#include <iostream>// 函数模板 1:接受两个相同类型的参数
template <typename T>
T add(T a, T b) {return a + b;
}
// 函数模板 2:接受三个相同类型的参数
template <typename T>
T add(T a, T b, T c) {return a + b + c;
}
// 普通函数:接受两个不同类型的参数
double add(int a, double b) {return a + b;
}
int main() {int a = 5,b = 3,c = 2;double d = 3.2;// 调用函数模板 1std::cout << add(a, b) << std::endl; // 输出 8// 调用函数模板 2std::cout << add(a, b, c) << std::endl; // 输出 10// 调用普通函数std::cout << add(a, d) << std::endl; // 输出 8.2return 0;
}
在这个示例中:
add(a, b)
调用第一个函数模板T add(T, T)
,因为参数类型相同。add(a, b, c)
调用第二个函数模板T add(T, T, T)
,因为参数类型相同且数量为三个。add(a, d)
调用普通函数double add(int, double)
,因为参数类型不同且数量为两个。
函数模板重载的规则与普通函数重载类似,编译器会根据参数类型和数量进行匹配,选择最合适的函数模板实例或普通函数。如果存在多个匹配的模板实例,编译器会进行重载解析,选择最匹配的实例。
需要注意的是,函数模板的重载并不意味着编译器会自动进行类型转换。模板参数必须精确匹配,否则编译器会报错。如果需要处理不同类型的参数,可以使用多个类型参数的模板或模板特化来实现。
1.5 函数模板特化
在C++中,template<>
语法用于模板特化。模板特化允许为特定类型提供专门的实现。你提到的 template<> void mySwap<persion>(persion &p1, persion &p2)
是一个函数模板的显式特化,用于处理 persion
类型的交换操作。
假设你有一个通用的 mySwap
函数模板,用于交换两个对象:
template <typename T>
void mySwap(T &a, T &b) {T temp = a;a = b;b = temp;
}
如果你希望为 persion
类型提供一个专门的实现,可以使用模板特化:
#include <iostream>
#include <string>// 定义 persion 类
class persion {
public:persion(const std::string& name, int age) : name(name), age(age) {}void print() const {std::cout << "Name: " << name << ", Age: " << age << std::endl;}std::string name;int age;// 友元声明,允许 mySwap 函数访问私有成员void mySwap(persion &p1, persion &p2);
};// 通用的 mySwap 函数模板
template <typename T>
void mySwap(T &a, T &b) {T temp = a;a = b;b = temp;
}// 针对 persion 类型的 mySwap 函数模板特化
template<> void mySwap<persion>(persion &p1, persion &p2) {using std::swap;swap(p1.name, p2.name);swap(p1.age, p2.age);
}int main() {persion p1("Alice", 30);persion p2("Bob", 25);std::cout << "Before swap:" << std::endl;p1.print(); p2.print();mySwap(p1, p2);std::cout << "After swap:" << std::endl;p1.print(); p2.print();return 0;
}
/*
Before swap:
Name: Alice, Age: 30
Name: Bob, Age: 25
After swap:
Name: Bob, Age: 25
Name: Alice, Age: 30
*/
在这个示例中:
- 定义了一个通用的
mySwap
函数模板,用于交换两个对象。 - 定义了一个
persion
类,包含name
和age
成员变量。 - 使用
template<>
语法为persion
类型提供了一个专门的mySwap
实现,交换persion
对象的name
和age
成员变量。
通过这种方式,你可以为特定类型提供定制的交换操作,而不影响其他类型的交换行为。
类模板
类模板和函数模板的定义和使用类似。
类模板用于实现类所需数据的类型参数化
在C++中,类模板(class template)是一种强大的工具,用于实现类所需数据的类型参数化。类模板允许你定义一个通用的类,其中的数据类型可以作为参数传递,从而使得类可以处理多种不同类型的数据。
以下是一个简单的类模板示例,展示了如何使用类模板实现类型参数化:
#include <iostream>
#include <vector>// 定义一个类模板,用于存储和管理数据
template <typename T>
class Container {
public:// 添加元素到容器中void add(const T& item) {data.push_back(item);}// 打印容器中的所有元素void print() const {for (const T& item : data) {std::cout << item << " ";}std::cout << std::endl;}private:std::vector<T> data; // 使用 vector 存储数据
};int main() {// 创建一个存储 int 类型数据的 Container 实例Container<int> intContainer;intContainer.add(1);intContainer.add(2);intContainer.add(3);intContainer.print(); // 输出: 1 2 3// 创建一个存储 double 类型数据的 Container 实例Container<double> doubleContainer;doubleContainer.add(1.1);doubleContainer.add(2.2);doubleContainer.add(3.3);doubleContainer.print(); // 输出: 1.1 2.2 3.3// 创建一个存储 std::string 类型数据的 Container 实例Container<std::string> stringContainer;stringContainer.add("Hello");stringContainer.add("World");stringContainer.print(); // 输出: Hello Worldreturn 0;
}
/*
1 2 3
1.1 2.2 3.3
Hello World
*/
在这个示例中:
- 定义了一个名为
Container
的类模板,其中的T
是一个类型参数。 Container
类包含两个成员函数:add
用于添加元素到容器中,print
用于打印容器中的所有元素。- 使用
std::vector<T>
来存储数据,这样可以方便地管理动态数组。 - 在
main
函数中,创建了三个不同类型的Container
实例:一个用于存储int
类型数据,一个用于存储double
类型数据,一个用于存储std::string
类型数据。
通过这种方式,类模板允许你编写一次代码,然后使用不同的类型参数来实例化类,从而实现类型参数化。这大大提高了代码的复用性和灵活性。
类模板做函数参数
在C++中,类模板可以作为函数参数,这使得函数能够处理不同类型的类模板实例。你可以通过模板参数来传递类模板实例,或者直接在函数参数中使用类模板。
以下是一些示例,展示了如何将类模板作为函数参数:
示例 1:通过模板参数传递类模板实例
#include <iostream>
#include <vector>// 定义一个类模板
template <typename T>
class Container {
public:void add(const T& item) {data.push_back(item);}void print() const {for (const T& item : data) {std::cout << item << " ";}std::cout << std::endl;}
private:std::vector<T> data;
};// 定义一个函数模板,接受一个 Container 实例
template <typename T>
void processContainer(const Container<T>& container) {container.print();
}
int main() {Container<int> intContainer;intContainer.add(1); intContainer.add(2); intContainer.add(3);processContainer(intContainer); // 输出: 1 2 3Container<double> doubleContainer; doubleContainer.add(1.1); doubleContainer.add(2.2); doubleContainer.add(3.3);processContainer(doubleContainer); // 输出: 1.1 2.2 3.3return 0;
}
示例 2:直接在函数参数中使用类模板
// 定义一个函数,接受一个 Container 实例
void processIntContainer(const Container<int>& container) {container.print();
}
void processDoubleContainer(const Container<double>& container) {container.print();
}
在这两个示例中,我们展示了如何将类模板实例作为函数参数传递。第一个示例通过模板参数传递类模板实例,使得函数可以处理不同类型的 Container
实例。第二个示例直接在函数参数中使用类模板,为每种类型定义了一个单独的函数。
通过这种方式,你可以编写灵活的函数,处理不同类型的类模板实例
类模板派生普通类
在C++中,可以从类模板派生出普通类。这种情况下,普通类会固定使用类模板中的某个特定类型。以下是一个示例,展示了如何从类模板派生出普通类:
#include <iostream>
#include <vector>// 定义一个类模板
template <typename T>
class BaseContainer {
public:void add(const T& item) {data.push_back(item);}void print() const {for (const T& item : data) {std::cout << item << " ";}std::cout << std::endl;}
protected:std::vector<T> data;
};// 从类模板派生出一个普通类
class IntContainer : public BaseContainer<int> {
public:void addMultiple(int count, int value) {for (int i = 0; i < count; ++i) {add(value);}}
};int main() {IntContainer intContainer;intContainer.add(1); ntContainer.add(2); ntContainer.add(3);intContainer.print(); // 输出: 1 2 3intContainer.addMultiple(3, 4);intContainer.print(); // 输出: 1 2 3 4 4 4return 0;
}
在这个示例中:
- 定义了一个名为
BaseContainer
的类模板,其中的T
是一个类型参数。 BaseContainer
类包含两个成员函数:add
用于添加元素到容器中,print
用于打印容器中的所有元素。- 从
BaseContainer
类模板派生出一个普通类IntContainer
,并固定使用int
类型。 IntContainer
类添加了一个新的成员函数addMultiple
,用于添加多个相同值的元素。
通过这种方式,你可以从类模板派生出普通类,并固定使用某个特定类型。这使得代码更加简洁和易于维护。
类模板派生类模板
类模板派生类模板是指从一个类模板派生出另一个类模板。这种技术在C++中非常有用,可以实现代码的重用和泛化。下面是一个简单的示例,展示了如何从一个类模板派生出另一个类模板。
#include <iostream>// 基类模板
template <typename T>
class Base {
public:Base(T value) : data(value) {}void display() const {std::cout << "Base: " << data << std::endl;}
protected:T data;
};// 派生类模板
template <typename T>
class Derived : public Base<T> {
public:Derived(T value) : Base<T>(value) {}void display() const {std::cout << "Derived: " << this->data << std::endl;}
};int main() {Base<int> base(10);base.display();Derived<int> derived(20);derived.display();return 0;
}
在这个示例中,我们定义了一个基类模板 Base
,它包含一个类型为 T
的数据成员 data
和一个显示数据的成员函数 display
。然后,我们定义了一个派生类模板 Derived
,它继承自 Base
模板,并重写了 display
函数。
在 main
函数中,我们创建了一个 Base<int>
对象和一个 Derived<int>
对象,并分别调用它们的 display
函数来显示数据。
通过这种方式,我们可以灵活地使用类模板派生类模板,实现更复杂和通用的代码结构。
类模板类内实现
在C++中,类模板的成员函数可以在类内实现,也可以在类外实现。类内实现通常用于简单的成员函数,这样可以使得代码更加紧凑和易读。下面是一个示例,展示了如何在类模板中实现成员函数。
#include <iostream>template <typename T>
class MyClass {
public:// 构造函数MyClass(T value) : data(value) {}// 成员函数在类内实现void display() const {std::cout << "Data: " << data << std::endl;}private:T data;
};int main() {MyClass<int> obj(10);obj.display();MyClass<double> obj2(3.14);obj2.display();return 0;
}
在这个示例中,我们定义了一个类模板 MyClass
,它包含一个类型为 T
的数据成员 data
和一个显示数据的成员函数 display
。display
函数在类内实现,直接在类定义中给出其实现。
在 main
函数中,我们创建了一个 MyClass<int>
对象和一个 MyClass<double>
对象,并分别调用它们的 display
函数来显示数据。
类内实现的优势在于代码更加紧凑,适合简单的成员函数。如果成员函数较为复杂,或者需要在多个地方使用相同的实现,可以考虑在类外实现成员函数。
类模板类外实现
在C++中,类模板的成员函数也可以在类外实现。这种做法通常用于成员函数较为复杂,或者需要在多个地方使用相同的实现。下面是一个示例,展示了如何在类模板中实现成员函数。
#include <iostream>template <typename T>
class MyClass {
public:// 构造函数MyClass(T value);// 成员函数声明void display() const;private:T data;
};// 构造函数在类外实现
template <typename T>
MyClass<T>::MyClass(T value) : data(value) {}// 成员函数在类外实现
template <typename T>
void MyClass<T>::display() const {std::cout << "Data: " << data << std::endl;
}int main() {MyClass<int> obj(10);obj.display();MyClass<double> obj2(3.14);obj2.display();return 0;
}
在这个示例中,我们定义了一个类模板 MyClass
,它包含一个类型为 T
的数据成员 data
和一个显示数据的成员函数 display
。display
函数在类外实现,需要在类定义之外给出其实现。
在 main
函数中,我们创建了一个 MyClass<int>
对象和一个 MyClass<double>
对象,并分别调用它们的 display
函数来显示数据。
类外实现的优势在于可以将复杂的成员函数实现分离出来,使得类定义更加简洁。同时,相同的实现可以在多个地方使用,提高了代码的重用性。
类模板头文件和源文件分离问题
==注意:==类模板的声明和实现放到一个文件中,我们把这个文件命名为.hpp(这个是约定的规则,并不是标准,必须这么写)。
原因:
- 类模板需要二次编译,在出现模板的地方编译一次,在调用模板的地方再次编译。
- C++编译规则为独立编译。
模板类与友元函数
友元函数是可以访问类的私有变量(private)和保护变量(protected)。
在C++中,模板类可以声明友元函数,以便友元函数能够访问类的私有成员。友元函数可以是普通函数、类成员函数,甚至是另一个模板类的成员函数。下面是一个示例,展示了如何在模板类中声明友元函数。
#include <iostream>// 模板类
template <typename T>
class MyClass {
public:MyClass(T value) : data(value) {}// 声明友元函数friend void display(const MyClass<T>& obj);private:T data;
};// 友元函数的实现
template <typename T>
void display(const MyClass<T>& obj) {std::cout << "Data: " << obj.data << std::endl;
}int main() {MyClass<int> obj(10);display(obj);MyClass<double> obj2(3.14);display(obj2);return 0;
}
在这个示例中,我们定义了一个模板类 MyClass
,它包含一个类型为 T
的数据成员 data
。我们在类中声明了一个友元函数 display
,该函数可以访问 MyClass
的私有成员 data
。
友元函数 display
的实现位于类外,并且需要使用模板参数 T
来匹配类模板。在 main
函数中,我们创建了一个 MyClass<int>
对象和一个 MyClass<double>
对象,并分别调用友元函数 display
来显示数据。
通过这种方式,模板类可以与友元函数协同工作,实现对私有成员的访问和操作。
template template parameters
在C++中,模板模板参数(template template parameters)允许你在定义模板时使用另一个模板作为参数。这种技术在需要参数化容器类型时特别有用。下面是一个示例,展示了如何使用模板模板参数。
#include <iostream>
#include <vector>
#include <deque>template <template <typename...> class Container, typename T>
class MyContainer {
public:MyContainer() {std::cout << "Constructor" << std::endl;}void display() {// 假设容器有一个 size() 方法和一个 operator[] 方法std::cout << "Size: " << container.size() << std::endl;for (size_t i = 0; i < container.size(); ++i) {std::cout << container[i] << " ";}std::cout << std::endl;}void push_back1(const T& value) {container.push_back(value);std::cout << "Push back " << value << std::endl;std::cout << "Size: " << container.size() << std::endl;std::cout << "Capacity: " << container.capacity() << std::endl;std::cout << "Front: " << container.front() << std::endl;}void dequeue1() {if (!container.empty()) {container.pop_front();}}
private:Container<T> container;
};int main() {// 使用 vector 作为容器类型MyContainer<std::vector, int> vecContainer;vecContainer.push_back1(1); vecContainer.push_back1(2);vecContainer.push_back1(3);vecContainer.display();// 使用 deque 作为容器类型MyContainer<std::deque, int> dequeContainer;dequeContainer.push_back1(1);dequeContainer.push_back1(2);dequeContainer.push_back1(3);dequeContainer.display();return 0;
}
在这个示例中,我们定义了一个模板类 MyContainer
,它接受两个模板参数:一个模板模板参数 Container
和一个类型参数 T
。Container
是一个模板类,而 T
是容器中元素的类型。
在 MyContainer
的构造函数中,我们使用模板模板参数 Container
创建了一个容器,并向其中添加了一个默认构造的 T
类型的元素。
在 main
函数中,我们创建了两个 MyContainer
对象,一个使用 std::vector
作为容器类型,另一个使用 std::deque
作为容器类型,并分别调用它们的 display
函数来显示容器中的元素。
通过这种方式,模板模板参数允许我们在定义模板时参数化容器类型,从而实现更灵活和通用的代码。