目录
2.4 模板特化:给特殊类型"开小灶"(了解,未来会进行详细讲解)
0.1 概述&序言
这里是白杨,上篇我们把内存管理的底裤扒干净了(没看过的同学请走传送门:C++入门篇(八):内存管理)。C++入门篇(八):C++内存管理详细精讲-CSDN博客
本篇继续开新地图,聊两件事:
- 模板:泛型编程的基础。一份代码,多种类型,这就是模板的魔法——它也是 STL 的"发动机"。
- STL 初探:标准模板库,C++ 最伟大的财富之一。容器、算法、迭代器三大件先混个脸熟。
内容依旧干货满满,让我们开始吧。
一、泛型编程与函数模板
1.1 什么是泛型编程
先说结论:
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
想写一个交换函数:
void Swap(int& a, int& b) { int t = a; a = b; b = t; }
void Swap(double& a, double& b){ double t = a; a = b; b = t; }
void Swap(char& a, char& b) { char t = a; a = b; b = t; }
逻辑一模一样,只是类型不同,重复代码泛滥成灾。C++ 的解法就是模板——把类型当成参数传。
口诀:模板是图纸,类型是参数;实例化之后,房子才盖好。
1.2 函数模板的定义
template<typename T> // 也可以写 template<class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
template<typename T> 声明了一个模板参数 T。编译器遇到 Swap(a, b) 时,会根据实参类型推导出 T,再生成一份对应的函数——这个过程叫模板实例化。模板本身不是函数,实例化出来的才是函数。
1.3 隐式实例化与显式实例化
int main()
{
int a = 1, b = 2;
Swap(a, b); // 隐式实例化:推导出 T = int
Swap<int>(a, b); // 显式实例化:手动指定 T = int
double d1 = 1.1, d2 = 2.2;
Swap(d1, d2); // T = double
// Swap(a, d1); // error:a是int、d1是double,T 推导不出来
// Swap<int>(a, d1); // error:指定T=int后,d1是double不能传给int&
return 0;
}
1.4 一道错题引入
下面代码输出什么?
#include <iostream>
using namespace std;
template<class T>
void Swap(T& x, T& y)
{
T t = x;
x = y;
y = t;
cout << "模板" << endl;
}
void Swap(int& x, int& y)
{
int t = x;
x = y;
y = t;
cout << "普通函数" << endl;
}
int main()
{
int a = 1, b = 2;
Swap(a, b); // ?
Swap<int>(a, b); // ?
double d1 = 1.1, d2 = 2.2;
Swap(d1, d2); // ?
return 0;
}
答案:普通函数 模板 模板。
1.5 原因与原理:模板参数的匹配原则
- 普通函数和函数模板同时存在、且匹配程度相同时,优先调用普通函数。
- 显式指定模板参数(Swap)时,强制走模板。
- 函数模板不允许自动类型转换(int 不会自动变 double);普通函数可以(int 可以隐式转 double)。
- 模板可以有多个类型参数:
template<class T1, class T2>。
原则:能一套逻辑吃遍所有类型的,写模板;只有一个特殊类型要特事特办的,写普通函数让它优先被调用。
1.6 非类型模板参数
模板参数不只是类型,还可以是常量——称为非类型模板参数:
template<class T, size_t N>
void PrintArray(T(&arr)[N]) // N 由编译器从数组大小推导
{
for (size_t i = 0; i < N; ++i)
cout << arr[i] << " ";
cout << endl;
}
int main()
{
int a[5] = { 1,2,3,4,5 };
PrintArray(a); // T=int, N=5
double d[3] = { 1.1,2.2,3.3 };
PrintArray(d); // T=double, N=3
return 0;
}
先说结论:
非类型模板参数必须是编译期就能确定的整型常量(size_t、int 等),不能传 double、string 和对象。
它的妙处:换普通函数,传数组还得再传一个"数组大小"参数;换成模板,N 编译器帮你数好,一份代码通吃所有类型、所有大小的数组。
二、类模板
2.1 为什么需要类模板
函数模板解决"函数逻辑相同、类型不同",类模板解决"类的结构相同、类型不同"——比如 Stack<int> 和 Stack<char>。
2.2 类模板的定义与实例化
template<class T>
class Stack
{
public:
Stack(size_t capacity = 4)
: _arr(new T[capacity])
, _capacity(capacity)
, _top(0)
{}
~Stack()
{
delete[] _arr;
_arr = nullptr;
_capacity = _top = 0;
}
void Push(const T& x)
{
_arr[_top++] = x;
}
private:
T* _arr;
size_t _capacity;
size_t _top;
};
int main()
{
Stack<int> st1; // 类模板必须显式实例化
st1.Push(2026);
Stack<char> st2; // 编译器生成另一份独立的类
st2.Push('A');
return 0;
}
先说结论:
类模板不是类,实例化出来的才是类。 Stack 只是模板(图纸),Stack<int> 才是真正的类型(房子)。所以类模板必须显式指定类型参数,编译器没法像函数模板那样自动推导。
2.3 类模板的声明与定义分离
函数模板的声明和定义可以分离,类模板理论上也可以,但强烈不建议:
// Stack.h
template<class T>
class Stack
{
public:
void Push(const T& x); // 只声明
// ...
};
// Stack.cpp
template<class T>
void Stack<T>::Push(const T& x)
{
_arr[_top++] = x;
}

如图虽然成功编译。
坑在哪?模板的实例化发生在编译期,编译器必须"看见"函数的完整定义才能生成代码。 如果把定义放在 .cpp 里,使用它的文件只 include 了 .h、看不到定义,就实例化不出来——链接错误。
原则:类模板的声明和定义放在同一个文件(.h / .hpp)里。
2.4 模板特化:给特殊类型"开小灶"(了解,未来会进行详细讲解)
该内容未来在进阶内容会讲,此处不做讲解,只需了解:
模板是"通用流水线",但有些类型天生特殊。比如比较两个字符串指针,通用版本比的是地址而不是内容,这时就要给 char* 单独写一份——这就是模板特化。
三、STL初探
3.1 什么是STL
先说结论:
STL(Standard Template Library,标准模板库),是 C++ 标准库的核心部分,是一套基于模板的通用数据结构和算法库。
它由 Alexander Stepanov 等人在惠普实验室开发,1994 年正式纳入 C++ 标准。学了它意味着:别人帮你把轮子造好了,你要做的就是学会开车。
3.2 STL六大组件
| 组件 | 作用 | 入门必学 |
|---|---|---|
| 容器(Container) | 存数据的结构:vector、list、map… | 必学 |
| 算法(Algorithm) | 对数据操作:sort、find、reverse… | 必学 |
| 迭代器(Iterator) | 容器和算法之间的桥梁,像"指针" | 必学 |
| 仿函数(Functor) | 类似函数的对象,作为算法参数 | 了解 |
| 适配器(Adapter) | 包装已有组件改变接口 | 了解 |
| 空间配置器(Allocator) | 底层内存管理 | 了解 |
一句话记忆:容器装数据,算法干脏活,迭代器当桥梁。
3.3 第一把武器:string(下章详细讲解)
string 是动态字符数组,专治各种 char* 的苦:自动管理内存、自动扩容、支持 + 拼接,还自带一串好用的成员函数。先感受一下"容器:string":(仅举例,下章会对string详细讲解):
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main()
{
string s1 = "hello";
string s2 = "world";
string s3 = s1 + " " + s2; // 拼接:operator+
cout << s3 << endl; // hello world
s3.push_back('!'); // 尾插一个字符
cout << s3 << endl; // hello world!
cout << "长度:" << s3.size() << endl; // 12
if (s3.find("world") != string::npos) // 找子串
cout << "找到了" << endl;
sort(s3.begin(), s3.end()); // 算法:排序(字符串也能排)
cout << s3 << endl; // !dehllloorw(按ASCII序)
return 0;
}
注意对比:换成 C 语言,strcat 要手动管容量、找子串要 strstr 加判空——string 一个 + 一个 find 全搞定,而且永远不会越界。
在学会string后你会发现以前C语言都是过的什么苦日子((*^_^*))。
3.4 常用容器一句话概览
- string:字符串。动态字符数组,C++ 里永远别再手动管理 char*。
- vector:动态数组。随机访问快,尾插尾删快,中间插入删除慢。
- list:双向链表。任意位置插入删除快,不支持随机访问。
- map:键值对,底层红黑树,有序去重。
m["key"] = value直接存。 - unordered_map:键值对,底层哈希表,无序,查询平均更快。
- set:集合,自动去重 + 排序。
四、最终总结
- 泛型编程:与类型无关的代码;模板是图纸,实例化才是房子
- 函数模板:隐式/显式实例化;普通函数优先,模板不自动类型转换
- 非类型模板参数必须是整型常量,N 可以让编译器帮数
- 类模板必须显式实例化;声明和定义放同一个文件
- STL 六大组件:容器装数据、算法干脏活、迭代器当桥梁
好了本期内容就到这里,从下一篇开始,我们逐个拆解 STL 容器——第一站就是 string,欢迎继续围观。如果对你有帮助,不要忘记点赞三连一波哦,我是白杨,我们下期见。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/Bai_YangSQ/article/details/166643289




