编写代码不只是确保代码能够计算正确的值,代码应该容易阅读和维护
1 模板参数
- 类型参数:例如int 、std::string 、 Box* 等
- 非类型参数:整数类型字面量(如200)、整数常量表达式 、只想对象的指针或引用 、 函数指针或空指针
- 参数还可以是一个模板
2 定义类模板
Array.h
#pragma once
template <typename T>
class Array
{
public:
explicit Array(size_t arraySize);
Array(const Array& array);
~Array();
T& operator[](size_t index);
const T& operator[](size_t index) const;
// Array& operator=(const Array& rhs);
Array& operator=(Array rhs);
size_t getSize() const { return size; }
template <typename T>
void swap(Array<T>& other) noexcept
{
std::swap(elements, other.elements);
std::swap(size, other.size);
}
private:
T* elements;
size_t size;
};
Array.cpp
#include "Array.h"
#include<iostream>
template <typename T>
Array<T>::Array(size_t arraySize) : elements{ new T[arraySize] }, size{ arraySize }
{
std::cout << "constructor" << std::endl;
}
template <typename T>
Array<T>::Array(const Array& array) : Array{ array.size }
{
std::cout << "copy constructor" << std::endl;
for (size_t i{}; i < size; ++i)
elements[i] = array.elements[i];
}
template <typename T>
Array<T>::~Array()
{
std::cout << "destructor" << std::endl;
delete[] elements;
}
/*
template <typename T>
T& Array<T>::operator[](size_t index)
{
if (index >= size)
throw std::out_of_range{ "Index too large" };
return elements[index];
}
*/
template <typename T>
T& Array<T>::operator[](size_t index)
{
return const_cast<T&>(std::as_const(*this)[index]);
}
template <typename T>
const T& Array<T>::operator[](size_t index) const
{
if (index >= size)
throw std::out_of_range{ "Index too large" };
return elements[size];
}
/*
template <typename T>
Array<T>& Array<T>::operator=(const Array& rhs)
{
if (&rhs != this)
{
delete[] elements;
size = rhs.size;
elements = new T[size];
for (siez_t i{}; i < size; ++i)
elements[i] = rhs.elements[i];
}
return *this;
}
*/
template <typename T>
Array<T>& Array<T>::operator=(Array rhs)
{
swap(rhs);
return *this;
}
template <typename T>
void swap(Array<T>& one, Array<T>& other) noexcept
{
one.swap(other);
}
- **构造函数 **
这里使用new在自由存储区分配内存,T如果是类类型就必须包含一个公共构造函数
template <typename T>
Array<T>::Array(size_t arraySize) : elements{new T[arraySize]}, size(arraySize) {}
- 拷贝构造函数
这里调用了构函数重新分配了elements的空间,并使用array给新的数组赋值,所以这里是深拷贝
template <typename T>
Array<T>::Array(const Array &array) : Array{array.size}
{
for (size_t i{}; i < size; ++i)
elements[i] = array.elements[i];
}
- 析构函数
因为elements使用new创建,分配在堆区,不会自动释放内存空间,所以这里需要使用delete[]释放内存
template <typename T>
Array<T>::~Array()
{
delete[] elements;
}
- 下标运算符模板
size_t 是一个不带符号的整型,所以这里只需要校验index是否过大
// 非const版本
template <typename T>
T &Array<T>::operator[](size_t index)
{
if (index >= size)
throw out_of_range{"Index to large: " + to_string(index)};
return elements[index];
}
// const版本
template <typename T>
const T &Array<T>::operator[](size_t index) const
{
if (index >= size)
throw out_of_range{"Index to large: " + to_string(index)};
return elements[index];
}
这里出现代码重复,重复代码会不便维护,可以使用const版本实现非const版本
template <typename T>
T &Array<T>::operator[](size_t index)
{
return const_cast<T &>(static_cast<const Array<T> &>(*this)[index]);
}
// as_const
template <typename T>
T &Array<T>::operator[](size_t index)
{
return const_cast<T &>(as_const(*this)[index]);
}
这一部分将当前对象(*this)强制转换为其 const 引用版本(const Array&)。这样可以调用const 版本的 operator[] 函数
由于 const 版本的 operator[] 返回 const T&,为了使其在非 const 上下文中可修改,我们使用 const_cast<T&> 进行类型转换
C++17 引入 as_const 将 static_cast<const Array<T> &>
这段代码缩减
- 赋值运算符
检查左操作数和右操作数是否相同,避免在赋值前就释放了右操作数的elements成员的内存,如果二者相同也能避免重复赋值这种冗余操作
template <typename T>
Array<T> &Array<T>::operator=(const Array &rhs)
{
if (&rhs != this)
{
delete[] elements;
size = rhs.size;
elements = new T[size]; // may throw std::bad_alloc
for (size_t i{}; i < size; ++i)
elements[i] = rhs.elements[i]; // may throw any exception (depends on type T)
}
return *this;
}
- 赋值运算符——异常安全性
针对上一份代码可能产生的问题:
1、处于某种原因,不能分配自由存储区中的内存,这人运算符就会抛出bad_alloc异常——内存分配失败产生悬挂指针,后续析构函数又会delete[] elements导致错误等
2、for循环中也使用了赋值运算符=,如果类类型T的赋值运算出问题,那么这种嵌套调用也会出问题,此时要保证“要么都成功,要么都失败”
// Array.h
void swap(Array &other) noexcept;
Array &operator=(Array rhs);
// Array.cpp
// 1、向Array中添加一个额外的成员函数swap()
template <typename T>
void Array<T>::swap(Array &other) noexcept
{
std::swap(elements, other.elements);
std::swap(size, other.size);
}
// 2、使用该成员函数版本实现传统的非成员函数swap()
template <typename T>
void swap(Array<T> &one, Array<T> &other)
{
one.swap(other);
}
template <typename T>
Array<T> &Array<T>::operator=(Array rhs)
{
swap(rhs);
return *this;
}
解析:值传递完成拷贝 swap完成交换
1、这里为Array提供了全局的swap函数重载,在使用std::swap时会有优先使用它
2、operator=
这里使用了值传递,值传递会创建一个副本,如果发生异常,原始对象this不会被修改因为副本实在进入函数之前生成的
3、swap函数使用了std::swap完成值或指针的交换,并且是noexcept的(如果要确保T也是noexcept的需要使用is_nothrow_swappable检查)
4、自赋值问题,即使发生自赋问题,由于操作的是副本对象,所以没有影响
3 创建类模板的实例
1、编译器只编译程序使用的成员函数,而不是(一次使用模板参数替代生成)整个类
2、需要把指针和引用作为模板类型实参来测试模板
3、(针对2)可以使用显示实例化,快速测试是否能够为一个或多个类型实例化新的类模板及其所有成员,这样就不必编写代码来调用所有的成员函数了
- 在创建特定模板类型的对象时,类模板只能隐式实例化,声明对象类型的指针不会创建模板的实例Array* pObject;,而Array<string*> pMessage {10};则会创建
- 类模板的成员函数在分离定义(就是你的模板同时写.h 与 .cpp文件)时必须显示实例化
- 解决方式一:将所有的定义都放在头文件中
- 解决方式二:显示实例化模
// Array.cpp
#include "Array.h"
// 显式实例化 Array<int>
template class Array<int>;
4 非类型模板参数
1、非类型模板参数必须是常量值,不能是运行时的变量
2、每次给定一个参数就会生成一个新的类 Array<int,1> 和 Array<int,2>是不同的
简单的使用示例:
#include <iostream>
template <int N>
class Array {
public:
void printSize() {
std::cout << "Array size: " << N << std::endl;
}
};
int main() {
Array<5> arr; // 实例化 Array,模板参数 N 为 5
arr.printSize(); // 输出: Array size: 5
Array<10> arr2; // 实例化 Array,模板参数 N 为 10
arr2.printSize(); // 输出: Array size: 10
return 0;
}
5 模板特化
模板特化需要放到模板声明之后
5.1 全特化
假设我们有一个模板类 Array,它接受一个类型参数 T 和一个大小参数 N:
template <typename T, size_t N>
class Array {
public:
Array() {
std::cout << "Generic Array\n";
}
};
如果我们希望为 T = int 和 N = 5 的情况提供一个特殊的实现,我们可以使用全特化:
// 全特化:针对 T = int 和 N = 5 的情况
template <>
class Array<int, 5> {
public:
Array() {
std::cout << "Specialized Array for int and size 5\n";
}
};
现在,模板类 Array<int, 5> 会使用我们特化的版本,而其他 Array<T, N> 仍然使用通用版本。
int main() {
Array<double, 10> arr1; // 输出: Generic Array
Array<int, 5> arr2; // 输出: Specialized Array for int and size 5
}
5.2 偏特化
1、偏特化是指只为模板的某些类型参数提供特化,而不是针对所有参数进行特化。它使得我们可以对模板的某些类型参数进行修改,同时保留其他类型参数的灵活性。
假设我们有一个模板类 Array,它接受两个类型参数 T 和 U:
template <typename T, typename U>
class Array {
public:
Array() {
std::cout << "Generic Array\n";
}
};
如果我们希望针对 U = int 的情况提供一个特化版本,而保持 T 可以是任何类型,我们可以使用偏特化:
// 偏特化:当 U = int 时,提供特化
template <typename T>
class Array<T, int> {
public:
Array() {
std::cout << "Specialized Array for type T and U = int\n";
}
};
在这个例子中,我们通过偏特化针对第二个模板参数 U 为 int 的情况进行了特化,而第一个参数 T 仍然可以是任何类型。
int main() {
Array<float, double> arr1; // 输出: Generic Array
Array<char, int> arr2; // 输出: Specialized Array for type T and U = int
}
5.3 指针特化
指针特化是偏特化的一种
// 通用模板
template <typename T>
class Wrapper {
public:
Wrapper(T value) {
std::cout << "Generic Wrapper: " << value << std::endl;
}
};
// 偏特化:专门处理指针类型
template <typename T>
class Wrapper<T*> {
public:
Wrapper(T* value) {
std::cout << "Pointer Wrapper: " << *value << std::endl;
}
};
int main() {
int x = 10;
Wrapper<int> w1(x); // 输出: Generic Wrapper: 10
Wrapper<int*> w2(&x); // 输出: Pointer Wrapper: 10
}
6 在类模板中使用 static_assert()
在类模板中的类型实参不合适时,使用static_assert可以使编译器输出一条信息
模板 | 结果 | 含义 |
is_integral | std::is_integral::value == true; | 是否为整数类型 |
is_floating_point | std::is_floating_point::value == true; | 是否为浮点类型 |
is_pointer | std::is_pointer<int*>::value == true; | 是否为指针类型 |
is_array | std::is_array<int[5]>::value == true; | 是否为数组类型 |
is_class | std::is_classstd::string::value == true; | 是否为类类型 |
is_enum | enum Color { Red, Green, Blue }; std::is_enum::value == true; | 是否为枚举类型 |
is_void | std::is_void::value == true; | 是否为 void 类型 |
is_function | std::is_function<void(int)>::value == true; | 是否为函数类型 |
is_same | std::is_same<int, int>::value == true; | 判断类型 T 和 U 是否相同 |
is_const | std::is_const::value == true; | 是否为常量类型 |
is_volatile | std::is_volatile::value == true; | 是否为易变类型 |
is_const_v | std::is_const_v == true; | 与 std::is_const 功能相同,但以变量方式提供结果 |
is_default_constructible_v | std::is_default_constructible_v == true; | 是否含有默认构造器 |
type_traits 头文件中还有很多有用的模板,例如类型变换的模板可以将类型修改或移除某些特性
7 带有嵌套类的类模板 (带有嵌套模板的类模板暂时不考虑)
template <typename T>
class Stack
{
private:
class Node
{
public:
T item{};
Node *next{};
explicit Node(const T &item) : item{item} {}
};
Node *head{};
public:
Stack() = default;
Stack(const Stack &stack);
~Stack();
Stack &operator=(const Stack &rhs);
void push(const T &item);
T pop();
bool isEmpty() const;
void swap(Stack &other) noexcept;
};
#include <stdexcept>
#include <utility>
#include <iostream>
#include <string>
#include <array>
#include "Stack.h"
using namespace std;
// 拷贝构造函数 深拷贝
template <typename T>
Stack<T>::Stack(const Stack &stack)
{
if (stack.head)
{
// 这里使用了系统默认的拷贝构造,拷贝构造内使用了浅拷贝,
// 复制了item及next指针,所以此时head(指向的节点)存放jumps head.next 存放的 fox
head = new Node(*stack.head);
// test
// cout << "head next item : " << (head->next)->item << endl;
Node *oldNode = stack.head;
Node *newNode = head;
while (oldNode = oldNode->next)
{
// test
// cout << "copy constructor : node item = " << oldNode->item << endl;
newNode->next = new Node(*oldNode);
newNode = newNode->next;
}
}
}
// 判断栈是否为空
template <typename T>
bool Stack<T>::isEmpty() const
{
return head == nullptr;
}
// 实现复制交换
template <typename T>
void Stack<T>::swap(Stack &other) noexcept
{
std::swap(head, other.head);
}
// 重载赋值运算符 (原理:拷贝交换,利用拷贝构造实现复制,然后交换复制体和this指针)
// 1、函数声明没有带模板类型T,但这里需要加上
// 2、如果去掉赋值运算符形参中的const即可变成值传递,值传递的过程会调用拷贝构造函数
template <typename T>
Stack<T> &Stack<T>::operator=(const Stack &rhs)
{
auto copy{rhs};
swap(copy);
return *this;
}
// push
// 1、创建一个指针指向一个使用item创建的节点
// 2、将原来的head指针指向的节点拼接在此节点后面
// 3、将head指针指向此节点
template <typename T>
void Stack<T>::push(const T &item)
{
Node *node = new Node{item};
node->next = head;
head = node;
}
// pop
// 1、如果没有元素需要抛出异常
// 2、记录head指针指向节点的下一个节点
// 3、记录head指向节点的值
// 4、删除head指针指向的节点
// 5、head指针指向原节点的下一个节点
template <typename T>
T Stack<T>::pop()
{
if (isEmpty())
throw std::logic_error{"Stack Empty"};
auto *next = head->next;
T item = head->item;
delete head;
head = next;
return item;
}
// 析构函数 遍历清空指针指向的内存
template <typename T>
Stack<T>::~Stack()
{
while (head)
{
auto *next = head->next;
delete head;
head = next;
}
}
int main()
{
std::string words[]{"The", "quick", "brown", "fox", "jumps"};
Stack<string> wordStack;
// 不知道为什么不支持 std::size
int size = sizeof(words) / sizeof(words[0]);
// test
// cout << "size = " << size << endl;
for (size_t i{}; i < size; ++i)
wordStack.push(words[i]);
// 这里执行拷贝构造
Stack<string> newStack{wordStack};
// 遍历打印栈
while (!newStack.isEmpty())
cout << newStack.pop() << " ";
cout << endl;
// 相当于倒序了
while (!wordStack.isEmpty())
newStack.push(wordStack.pop());
while (!newStack.isEmpty())
cout << newStack.pop() << " ";
cout << endl;
cout << "Enter a line of text: " << endl;
string text;
getline(cin, text);
Stack<const char> characters;
for (size_t i{}; i < text.length(); ++i)
characters.push(text[i]);
cout << endl;
while (!characters.isEmpty())
cout << characters.pop();
cout << endl;
system("pause");
}
// out:~
size = 5
jumps fox brown quick The
The quick brown fox jumps
Enter a line of text: