C++作为我们的老朋友,伴随我们从18岁的懵懂无知到23岁的找不到工作,今天就让我们来手撕一下基本的STL!
模板函数与函数模板
我现在写两个函数:
void MySwap(int &a,int &b) {int temp;temp = a;a = b;b = temp;
}
void MySwap(char &c,char &d) {char temp;temp = c;c = d;d = temp;
}
这是我们基本的函数重载,然后我们在main函数中分别使用这两个函数:
int main() {int a = 1, b = 2;cout << a << " " << b << endl;MySwap(a,b);cout << a << " " << b << endl;char c = 'c', d = 'd';cout << c << " " << d << endl;MySwap(c, d);cout << c << " " << d << endl;
}
输出如图:
很好,可是问题是:假如我们要处理的数据有很多很多类型呢?难道我们每种类型都要进行重载吗?
答案是否定的,我们使用函数模板:
template<typename T>
void MySwap(T& a,T& b ) {T temp;temp = a;a = b;b = temp;
}
我们的关键字template<>括号中可以定义多个元素,我们定义类型T,然后我们的函数就可以使用这个T来替代之前的类型了。
我们的main函数不变:
int main() {int a = 1, b = 2;cout << a << " " << b << endl;MySwap(a,b);cout << a << " " << b << endl;char c = 'c', d = 'd';cout << c << " " << d << endl;MySwap(c, d);cout << c << " " << d << endl;
}
输出呢?
一模一样。
很好,我们一下就学会了函数模板和模板函数,函数模板就是允许你使用泛型编程的T,使用这个模板的函数就是模板函数。
那么假如我们同时有这三个函数呢?
#include<iostream>
using namespace std;
template<typename T>
void MySwap(T& a,T& b ) {cout << "调用模板函数" << endl;T temp;temp = a;a = b;b = temp;
}
void MySwap(int &a,int &b) {cout << "调用普通函数1" << endl;int temp;temp = a;a = b;b = temp;
}
void MySwap(char &c,char &d) {cout << "调用普通函数2" << endl;char temp;temp = c;c = d;d = temp;
}
int main() {int a = 1, b = 2;cout << a << " " << b << endl;MySwap(a,b);cout << a << " " << b << endl;char c = 'c', d = 'd';cout << c << " " << d << endl;MySwap(c, d);cout << c << " " << d << endl;
}
运行后可以看到:
是的,普通函数与模板函数同时存在时,我们优先调用普通函数。
为什么呢?
啊什么,居然是C++的标准,那确实没办法。但假如我就是想用我们的模板函数可以实现吗?
其实是可以的:
int main() {int a = 1, b = 2;cout << a << " " << b << endl;MySwap<int>(a,b);cout << a << " " << b << endl;char c = 'c', d = 'd';cout << c << " " << d << endl;MySwap<char>(c, d);cout << c << " " << d << endl;
}
我们在函数后加一个中括号,括号内写明数据类型,这样相当于显示地调用我们的模板函数。
结果如图:
模板函数还有什么性质呢?
可以看到我们重新定义了double类型的变量,然后我们显式调用模板函数,但是这个时候报错告诉你不可以这样,因为你指定的是int类型而aa和bb是double类型,换句话说,模板函数不支持基本的类型隐式转换。
我们还需要介绍一下模板的特化,事实上,我们刚才的函数就是一种特化:
#include<iostream>
using namespace std;
template<typename T>
void MySwap(T& a,T& b ) {cout << "调用模板函数" << endl;T temp;temp = a;a = b;b = temp;
}
void MySwap(int &a,int &b) {cout << "调用普通函数1" << endl;int temp;temp = a;a = b;b = temp;
}
void MySwap(char &c,char &d) {cout << "调用普通函数2" << endl;char temp;temp = c;c = d;d = temp;
}
int main() {int a = 1, b = 2;double aa = 11, bb = 22;cout << a << " " << b << endl;MySwap(a, b);cout << a << " " << b << endl;char c = 'c', d = 'd';cout << c << " " << d << endl;MySwap(c, d);cout << c << " " << d << endl;cout << aa << " " << bb << endl;MySwap(aa, bb);cout << aa << " " << bb << endl;
};
运行后得到:
说白了我们的特化就是在已有模板函数的基础上加入普通函数的重载去覆盖基本的模板函数,这样可以针对某些特定类型的数据进行处理。
之前我们的模板中总是只有孤零零的T,假如我加一些东西会怎么样?
template<typename T,int type_int,char type_char>
可以看到,假如我们在模板定义时加入了多个参数,那么使用这个函数模板的模板函数的参数就必须对齐这个模板的参数。
这样就不会报错了。
运算符重载
我们来看下面这段代码:
我们定义了一个类,类中有两个int成员变量实部和虚部,我现在希望可以让这两个数的实部和虚部进行相加得到一个整体,我们可以在类中定义好这个方法。
#include<iostream>
using namespace std;
class MyClass {
public:int real, image;MyClass(int real, int image) :real (real), image (image){}pair<int,int> Add(MyClass& a,MyClass& b) {int real = a.real + b.real;int image = a.image + b.image;return { real,image };}
};
int main() {int a = 1, b = 2;MyClass me(a, b), you(b,a);MyClass her(0, 0);cout <<"实部是:"<< her.Add(me, you).first << " 虚部是:" << her.Add(me, you).second << endl;
};
运行后得到:
但显然这太麻烦了,有没有更快捷更粗暴的方法呢?
有的兄弟有的。
#include<iostream>
using namespace std;
class MyClass {
public:int real, image;MyClass(int real, int image) :real (real), image (image){}MyClass operator+(MyClass& other) {return MyClass(other.real+this->real,other.image+this->image);}
};
int main() {int a = 1, b = 2;MyClass me(a, b), you(b,a);MyClass her = me + you;cout <<"实部是: " <<her.real << " 虚部是: " << her.image << endl;
};
这就是我们今天的主角:操作符重载。
简单地说,我们的操作符允许我们对特定的运算符进行重定义,变成我们想要的形式。
我重新写了两个操作符重载:
#include<iostream>
using namespace std;
class MyClass {
public:int real, image;MyClass(int real, int image) :real (real), image (image){}MyClass operator+(MyClass& other) {return MyClass(other.real+this->real,other.image+this->image);}int operator * (MyClass& other) {return this->real*other.real + this->image*other.image;}
};
ostream& operator<<(ostream& str,const MyClass& other) {str << "实部是: " << other.real << " 虚部是: " << other.image << endl;return str;
}
int main() {int a = 1, b = 2;MyClass me(a, b), you(b,a);MyClass her = me + you;cout <<"实部是: " <<her.real << " 虚部是: " << her.image << endl;int res = me * you;cout << "乘积是:" << res << endl;cout << me << endl;
};
我们现在总的有三个操作符重载,+,*与<<。可以看到我们的操作符重载是可以在类中也可以在类外实现的,,返回的类型也是可以是int也可以是类,非常的方便。(注意我们返回ostream的时候得添加引用符号,因为string类型的拷贝构造函数是删除的,无法值传递)
手写简单Vector
现在让我们来写第一个我们最熟悉的容器:vector。
既然要写vector,那当然得首先知道vector的工作原理是什么。对于vector来说,其底层实现基于数组,但比起数组来说最大的特点就是多了一个扩容的内容。
其原理就是:当我们发现vector的容量已满时,我们会去重新开辟一块新的空间作为存储的地方,然后把现有的数据全部搬到这块新的空间里。
除此之外,vector还需要一个迭代器来代替指针,迭代器的特点就是不会出现越界的情况,但是这并不代表着迭代器就是永远正确的:比如我们的vector进行扩容之后,我们的vector整体会“搬家”,那么这个时候旧的迭代器就会失效。
让我们来写这部分的代码:
T* m_data; // 数据存储指针size_t m_size; // 当前元素数量size_t m_capacity; // 当前分配的内存容量// 扩容策略:双倍增长void resize(size_t new_capacity) {T* new_data = new T[new_capacity];std::copy(m_data, m_data + m_size, new_data);delete[] m_data;m_data = new_data;m_capacity = new_capacity;}
vector总体来说由三个变量来控制:最大内存,当前元素数量以及数组指针,可以注意的是这里我们的容量我们不是用int而是用size_t,这是为什么呢?
一句话以概括:size_t就是一个可以包含任何类型对象的size大小的整型变量。
resize函数中,我们要new一个数组,然后直接利用copy把原数组的东西搬到新数组中后把原来的这三个变量进行更新,这里我们就得说说copy的用法了:
简单地说,copy更安全的同时使用范围更广,但是memcpy可以实现更极致的性能。
接下来我们来写一下基本的构造函数和析构函数:
// 构造函数Vector() : m_data(nullptr), m_size(0), m_capacity(0) {}explicit Vector(size_t n, const T& val = T()): m_data(new T[n]), m_size(n), m_capacity(n) {std::fill(m_data, m_data + n, val);}// 拷贝构造函数Vector(const Vector& other): m_data(new T[other.m_capacity]),m_size(other.m_size),m_capacity(other.m_capacity) {std::copy(other.m_data, other.m_data + other.m_size, m_data);}// 拷贝赋值运算符Vector& operator=(const Vector& other) {if (this != &other) {delete[] m_data;m_size = other.m_size;m_capacity = other.m_capacity;m_data = new T[m_capacity];std::copy(other.m_data, other.m_data + m_size, m_data);}return *this;}// 析构函数~Vector() {delete[] m_data;}
这里我们首先要注意的是我们的explicit关键字,这个关键字并不常见:
一般来说我们只有在修饰构造函数还有运算符重载的时候才会去添加这个explicit关键字,让我们只有在明确表示使用这个构造函数或者这个重载了的运算符时才会使用,以防止混淆。在我们的代码中:
explicit Vector(size_t n, const T& val = T()): m_data(new T[n]), m_size(n), m_capacity(n) {std::fill(m_data, m_data + n, val);}
我们只有显示地写vector<T>(n)这样我们才会是调用这个构造函数,可以看到我们的第二个参数是const T& val = T(),这个的含义就是第二个参数是泛型类型的且如果不显示提供的话就使用该类的默认值,如int就是0,类就是默认的构造函数。
比如:vector<int>(n)就是一个n大小的int数组且初始值为0,vector<int>(n,2)则是一个n大小的int数组且初始值为2。
fill函数就是帮我们用val去填充这个数组。
剩下的拷贝构造,拷贝赋值运算符以及析构好像就没什么好说的,都是基本的操作且都用了copy函数。
写完了构造函数那就开始写数组的功能了,什么增删查改不就来了吗:
先写对于vector来说最简单的查:
// 元素访问T& operator[](size_t index) {return m_data[index];}const T& operator[](size_t index) const {return m_data[index];}
因为vector是基于数组实现的,所以对于vector来说我们只用去重载[]这个运算符就可以,然后返回数组里对应小标的值,需要注意的是我们提高了两个版本:非const和const版本的,这个的主要区别就在于我们是否可以修改访问的值。
然后是增:
我们都知道vector提供了push_back方法(其实还有emplace_back),具体是怎么做的呢?
// 添加元素void push_back(const T& val) {if (m_size >= m_capacity) {resize(m_capacity == 0 ? 1 : m_capacity * 2);}m_data[m_size++] = val;}
我们本身是有一个变量来记录当前元素存储的位置的,如果这个位置大于等于我们的容量我们就要去触发动态扩容,否则我们只需要像数组的增加元素一样操作即可。
然后是删,对应vector的pop_back()方法:
// 删除末尾元素void pop_back() {if (m_size > 0) --m_size;}
是的,也非常简单,我们删除vector的末尾元素的方法就是把表示当前存储元素的下标向前移动一位即可。
这样我们的增删查改就都完成了(查和改是一起的),我们还差什么呢?
当然是迭代器:
// 迭代器(简化版)T* begin() { return m_data; }T* end() { return m_data + m_size; }const T* begin() const { return m_data; }const T* end() const { return m_data + m_size; }
当然还有一些基本的信息比如empty(),size()等等:
// 容器信息size_t size() const { return m_size; }size_t capacity() const { return m_capacity; }bool empty() const { return m_size == 0; }
这样我们的整个vector类就完成了,我们可以假如一些测试用例:
int main() {Vector<int> vec;vec.push_back(10);vec.push_back(20);vec.push_back(30);vec[2] = 40;std::cout << "Size: " << vec.size()<< ", Capacity: " << vec.capacity() << std::endl;std::cout << "Elements: ";for (const auto& num : vec) {std::cout << num << " ";}vec.pop_back();std::cout << "Elements: ";for (const auto& num : vec) {std::cout << num << " ";}}
结果如图: