news 2026/8/11 4:45:19

C++从重复代码到标准库:模板、STL与string入门

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++从重复代码到标准库:模板、STL与string入门

学完类和对象、运算符重载与动态内存后,我们已经可以自己设计一个类,也知道对象的构造、拷贝、赋值和销毁不是凭空发生的。

但继续写代码,很快会碰到两个问题:

  1. 两个函数的逻辑完全一样,只是参数类型不同,难道每换一种类型都要重写一次?
  2. 字符串明明只是“一串字符”,为什么使用字符数组时还要反复处理空间、拷贝和\0

模板和 STL 正是在解决这类重复劳动。模板把“类型不同、逻辑相同”的代码抽出来,STL 则把常用的数据结构和算法整理成了一套能够协同工作的标准工具。string是我们接触这套工具时很合适的第一站,因为它既像前面学过的字符数组,又是一个真正的类。

本文不追求一次讲完模板和 STL,而是沿着学习时间线解决三个问题:

  • 模板为什么能减少重复代码?
  • STL 中的容器、迭代器和算法如何配合?
  • string比字符数组多替我们管理了什么?

一、同一个比较逻辑,为什么要写很多遍

假设我们需要求两个数中的较小值。对于int,可以这样写:

intMin(intleft,intright){returnleft<right?left:right;}

后来又要比较两个double,最直接的办法是再写一个函数:

doubleMin(doubleleft,doubleright){returnleft<right?left:right;}

两个函数真正不同的只有类型,比较过程没有改变。继续为longfloat和其他类型重写,只是在复制代码。

函数模板允许我们暂时把具体类型写成一个占位符:

template<classT>TMyMin(constT&left,constT&right){returnleft<right?left:right;}

这里的T不是某一种固定类型,而是模板参数。调用时,编译器会根据实参尝试推导T

std::cout<<MyMin(3,5)<<'\n';std::cout<<MyMin(2.5,1.8)<<'\n';

第一处调用中,T被推导为int;第二处调用中,T被推导为double。可以把这一过程理解为:编译器根据实际使用情况生成对应类型的函数实例。

1. 模板不是“什么类型都一定能用”

模板只是把类型暂时参数化,并没有取消代码对类型能力的要求。MyMin的函数体使用了<,所以传入的类型必须支持符合要求的比较操作。

这与前面学过的运算符重载正好连起来。自定义类型如果定义了合适的operator<,也可以使用这个模板:

classDate{public:Date(intyear,intmonth,intday):_year(year),_month(month),_day(day){}booloperator<(constDate&other)const{if(_year!=other._year)return_year<other._year;if(_month!=other._month)return_month<other._month;return_day<other._day;}private:int_year;int_month;int_day;};

MyMin(Date(...), Date(...))能否工作,不取决于模板“认识不认识日期”,而取决于模板使用的操作能否作用于Date

这给模板入门阶段留下了一个很重要的判断:

阅读一个模板时,不只看模板参数叫什么,还要看函数体对这个类型做了哪些操作。

2. 为什么参数写成const T&

如果写成按值传参:

template<classT>TMyMin(T left,T right);

传入类类型时,通常需要构造两个形参副本。改成const T&后,可以在不修改实参的前提下引用原对象,减少不必要的拷贝。

这正好承接前面学过的拷贝构造和const:引用避免额外副本,const又限制函数不能通过形参修改原对象。

不过当前版本仍然按值返回,因为返回的是两个实参中的一个值。更通用的模板返回设计还会涉及对象生命周期和引用悬空问题,这一阶段先不提前展开。

二、STL不是一堆互不相干的接口

模板解决了“同一逻辑适配不同类型”的问题。标准模板库 STL 则进一步利用这种能力,提供了一批通用容器和算法。

初学 STL 时,最容易把它学成函数名清单:今天背push_back,明天背find,后天背sort。这样虽然暂时会用几个接口,却不容易理解它们为什么能够组合。

先抓住三个角色:

  • 容器负责保存数据,例如vectorlist
  • 迭代器用来定位容器中的元素,并描述一个操作区间。
  • 算法对迭代器给出的区间进行处理,例如sortfindreverse

例如:

std::vector<int>values={4,1,3,2};std::sort(values.begin(),values.end());

这里:

  • values是保存整数的容器;
  • values.begin()指向第一个元素;
  • values.end()指向最后一个元素的下一个位置;
  • [begin, end)表示左闭右开的有效区间;
  • sort处理这个区间中的元素。

为什么是左闭右开区间

假设容器有 4 个元素,有效下标是0、1、2、3begin()对应第 0 个元素,end()位于第 3 个元素之后,因此[begin, end)恰好覆盖全部 4 个元素,却不会访问end()所在位置。

这和前面学习数组、排序以及循环边界时的思想是一致的:边界不是凭记忆写出来的,而是根据“哪些位置有效、循环体会访问哪里”推导出来的。

为什么算法不直接只为vector设计

如果算法只接收一种固定容器,它就会与容器紧密绑定。STL 让算法主要面向迭代器区间,只要迭代器提供算法需要的能力,同一个算法就可能服务于不同容器。

因此,STL 的重点不只是“标准库帮我们写好了代码”,更是把数据的保存方式和处理逻辑尽量分开。

三、从字符数组过渡到string

C 风格字符串通常存放在字符数组中,并以\0标记结束:

chartext[20]="hello";

如果要在后面拼接内容,我们需要考虑:

  • 数组剩余空间是否足够;
  • 原字符串在哪里结束;
  • 新内容应从哪个位置开始复制;
  • 最后是否正确保留\0
  • 使用动态空间时由谁释放。

这些问题并不是字符本身的业务逻辑,而是字符串存储管理带来的负担。

std::string把一串字符封装成了类:

#include<string>std::string text="hello";text+=" C++";

我们不再直接管理底层字符缓冲区,而是通过对象的成员函数和运算符完成操作。这并不是说内存管理消失了,而是string对象在内部负责维护资源,并通过构造、拷贝、赋值和析构等机制管理自己的生命周期。

这与前面学过的类和对象不是两套知识:string正是一个已经设计好的标准库类。

四、string如何描述和修改一串字符

下面不按接口表机械罗列,而是用一段字符串逐步完成“创建、读取、查找和修改”。

1. 构造与拼接

std::string first="hello";std::stringsecond(" C++");first+=second;first.push_back('!');std::cout<<first<<'\n';

预期输出:

hello C++!

+=能够用于string,可以联系前面学过的运算符重载来理解:对于类类型,运算符最终对应这个类型提供的操作,而不是编译器天然知道如何拼接所有对象。

push_back每次在末尾添加一个字符。如果添加的是一段字符串,应使用+=append等适合字符串的操作,不能把字符和字符串混为一谈。

2.size()capacity()不是一回事

std::cout<<first.size()<<'\n';std::cout<<first.capacity()<<'\n';
  • size()表示当前有效字符的数量;
  • capacity()表示在下一次需要重新分配存储空间前,当前实现能够容纳的字符数量。

对于"hello C++!"size()可以根据字符数量确定为 10;但capacity()的具体值与标准库实现和对象当前状态有关,不能把某次机器上的结果写成所有环境都固定相同。

因此不能这样遍历:

for(std::size_t i=0;i<first.capacity();++i){std::cout<<first[i];// 错误:capacity不是有效字符个数}

正确边界应来自size()

for(std::size_t i=0;i<first.size();++i){std::cout<<first[i];}

这仍然是数组下标边界问题,只是数组换成了类对象。

3.operator[]at()的差别

std::cout<<first[0]<<'\n';std::cout<<first.at(0)<<'\n';

二者都可以访问指定位置的字符,但越界处理不同:

  • operator[]不提供与at()相同的越界异常检查,使用者必须保证下标合法;
  • at()越界时会抛出std::out_of_range

初学时不要把这简化成“at()永远更好”。需要明确检查时它更直接;能够通过程序逻辑保证下标合法时,operator[]也很常见。无论使用哪一个,最根本的工作仍是判断下标是否处于有效范围。

4. 查找时为什么要检查npos

std::size_t pos=first.find("C++");if(pos!=std::string::npos){std::cout<<"找到的位置:"<<pos<<'\n';}

find找到目标时返回起始位置,未找到时返回std::string::npos。不要直接把返回值存进int后再与-1形成习惯性判断;接口返回的是size_type,使用std::size_tauto并与std::string::npos比较更清晰。

autopos=first.find("Java");if(pos==std::string::npos){std::cout<<"没有找到"<<'\n';}

5. 截取、删除和插入其实都依赖区间

std::string text="hello C++!";std::string part=text.substr(6,3);std::cout<<part<<'\n';

substr(6, 3)表示从下标 6 开始取 3 个字符,所以得到:

C++

再看删除和插入:

text.erase(5,4);// 从下标5开始删除4个字符:空格、C、+、+text.insert(5," STL");std::cout<<text<<'\n';

预期输出:

hello STL!

学习这些接口时,与其分别死记参数,不如每次都画出下标:

字符: h e l l o C + + ! 下标: 0 1 2 3 4 5 6 7 8 9

erase(5, 4)从位置 5 开始,连续删除 4 个字符,保留下标 0 到 4 的hello和原下标 9 的!

五、string也能进入STL的算法区间

string保存的是连续字符序列,也提供begin()end()。因此,标准算法可以通过迭代器处理它的字符区间:

#include<algorithm>#include<string>std::string number="12345";std::reverse(number.begin(),number.end());std::cout<<number<<'\n';

预期输出:

54321

这段代码把前面的三个部分连起来了:

  • string负责保存字符;
  • begin()end()给出[begin, end)区间;
  • reverse对区间执行反转。

算法并不需要我们手动传入字符个数,也不必知道string内部如何扩容。它只需要通过迭代器访问区间中的元素。

如果只反转中间三个字符,也可以缩小区间:

std::string number="12345";std::reverse(number.begin()+1,number.begin()+4);std::cout<<number<<'\n';

参与反转的是下标[1, 4),也就是2、3、4,所以预期结果为:

14325

这里最重要的仍然不是背代码,而是能根据左闭右开区间判断哪些元素参与操作。

六、string替我们管理内存,但仍有边界

1.size()变化时,底层存储可能发生什么

当不断向字符串末尾添加字符时,现有容量可能不够。string的实现通常需要取得更大的存储空间,把原字符转移或复制过去,再释放旧存储。

std::string text;for(inti=0;i<100;++i){text.push_back('a');}

我们不需要像手写动态字符数组那样亲自完成扩容,但这不代表扩容没有成本。因此:

  • size()表示当前有多少有效字符;
  • capacity()与当前可用存储能力有关;
  • 容量如何增长属于实现策略,不应假定每次固定翻倍;
  • 扩容可能使原来指向内部字符的指针、引用或迭代器失效,具体应根据相应操作的规则判断。

最后一点先建立风险意识即可,完整的迭代器失效规则可以放到后续容器学习中系统展开。

2.reserve()只是预留容量,不是增加字符

std::string text;text.reserve(100);std::cout<<text.size()<<'\n';

reserve(100)用于请求至少能够容纳一定数量字符的容量,但不会凭空创建 100 个有效字符。因此,text.size()仍然是 0。

如果提前知道会追加大量字符,合理使用reserve可以减少反复扩容的可能;但具体容量值和分配策略仍由实现决定。

3. 与C接口交互时使用c_str()

有些旧接口接收const char*,而我们手中是std::string

std::string filename="data.txt";constchar*ptr=filename.c_str();

c_str()提供以空字符结尾的字符序列视图,便于与需要 C 风格字符串的接口交互。

但要注意:

  • 返回的指针指向string对象管理的内部数据;
  • 不应通过这个指针修改内部字符;
  • 当字符串执行某些修改操作后,之前取得的指针可能不再有效;
  • 指针的有效性依赖原string对象仍然存在。

这与前面动态内存部分的思想相同:看到一个指针时,不只看它保存了什么地址,还要追问那块数据由谁拥有、能用到什么时候。

七、几个容易混淆的写法

1. 字符与字符串字面量混淆

std::string text="abc";text.push_back('d');// 一个字符text+="ef";// 一段字符串

单引号表示字符,双引号表示字符串字面量。push_back接收一个字符,不能把"d"直接当成'd'

2. 把capacity()当作遍历边界

for(std::size_t i=0;i<text.capacity();++i)// 错误思路

容量可能大于有效字符数。遍历有效字符应使用size(),或者直接使用范围for

for(charch:text){std::cout<<ch<<' ';}

3. 不检查find结果就继续使用

autopos=text.find("xyz");text.erase(pos,3);// 如果没找到,pos就是npos

正确做法是先判断:

autopos=text.find("xyz");if(pos!=std::string::npos){text.erase(pos,3);}

4. 保存内部指针后继续修改string

std::string text="hello";constchar*ptr=text.c_str();text+=" world";// 此时不要继续假定ptr仍然指向有效的原内部存储

字符串修改可能改变内部存储位置。需要指针时,应在修改完成后重新获取,或者重新设计代码以避免长期保存内部地址。

5. 混合类型导致模板推导失败

MyMin(3,4.5);// 一个是int,一个是double

原模板的两个参数都要求推导为同一个T,但这里分别得到intdouble,推导会发生冲突。可以明确统一类型:

MyMin(3.0,4.5);// 或MyMin<double>(3,4.5);

这也说明模板不是“把所有类型问题自动抹平”,编译器仍然需要得到明确、合法的实例。

八、完整示例:把模板、string和STL连起来

#include<algorithm>#include<iostream>#include<string>#include<vector>// T必须支持operator<template<classT>TMyMin(constT&left,constT&right){returnleft<right?left:right;}voidPrint(conststd::string&text){std::cout<<"text = "<<text<<'\n';std::cout<<"size = "<<text.size()<<'\n';std::cout<<"empty = "<<std::boolalpha<<text.empty()<<'\n';}intmain(){std::string first="hello";std::stringsecond(" C++");first+=second;std::cout<<first<<'\n';first.push_back('!');std::cout<<first<<'\n';std::cout<<"find C++: "<<first.find("C++")<<'\n';std::cout<<"substr: "<<first.substr(6,3)<<'\n';first.erase(5,4);first.insert(5," STL");std::cout<<first<<'\n';std::string number="12345";std::reverse(number.begin(),number.end());std::cout<<number<<'\n';std::cout<<MyMin(3,5)<<'\n';std::cout<<MyMin(std::string("apple"),std::string("banana"))<<'\n';std::vector<int>values={4,1,3,2};std::sort(values.begin(),values.end());for(intvalue:values){std::cout<<value<<' ';}std::cout<<'\n';Print("");return0;}

按代码规则推导,预期输出如下:

hello C++ hello C++! find C++: 6 substr: C++ hello STL! 54321 3 apple 1 2 3 4 text = size = 0 empty = true

说明:本文撰写时,当前本机环境未找到g++,所以上述内容是依据 C++17 代码行为给出的预期输出,不冒充本机实测结果。发布前应在 Visual Studio、Clang 或安装好 GCC 的环境中再次编译,并将真实控制台结果作为验证截图。

九、沿着学习时间线重新看这三个知识点

今天学到的内容并不是突然出现的一套新语法,而是对前面知识的继续抽象。

从重载到模板

函数重载可以让同名函数处理不同参数类型,但每一种类型通常仍要有对应实现。模板则在“算法逻辑相同”时,把类型差异抽成参数。模板内部能使用哪些操作,又取决于类型是否支持相应运算符或成员函数。

从类和动态内存到string

手写一个资源管理类,需要考虑构造、拷贝、赋值、析构和扩容。string已经把这些工作封装起来,让使用者主要表达“我要拼接、查找还是删除字符”。但资源依然存在,所以内部指针有效期、扩容成本和对象生命周期仍然值得注意。

从数组区间到STL算法

以前写循环和排序时,我们一直在处理下标与区间。STL 没有抛弃这种思想,而是用迭代器把区间表达得更通用。[begin, end)仍然是左闭右开,只是边界从整数下标变成了迭代器。

十、这一阶段真正需要形成的判断

学完模板、STL 简介和string,不必一次背下所有成员函数。更重要的是形成下面几条判断:

  1. 代码只是类型不同、逻辑相同时,考虑能否使用模板减少重复。
  2. 模板能否实例化,要看模板内部所需操作是否被实参类型支持。
  3. 遇到 STL 代码时,先分清谁保存数据、谁表示区间、谁执行算法。
  4. 使用string时,size()表示有效字符数,capacity()不能当作遍历边界。
  5. 看到find就考虑npos,看到下标就检查有效范围。
  6. string取得内部指针后,要继续追问对象生命周期和后续修改是否会让指针失效。
  7. 学习接口不能脱离前面的数组、类、运算符重载和动态内存;标准库只是把这些机制组织成了更稳定、更通用的工具。

下一阶段继续学习string时,可以进一步实现一个简化版string,亲自处理容量、扩容、深拷贝和迭代器。到那时,今天看到的接口就不再只是“会调用”,而会与前面学过的类和对象真正连成一条线。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/11 4:44:40

Simulink实现两区域电力系统二次调频与AGC控制

1. 项目概述&#xff1a;两区域系统二次调频的Simulink实现这个Simulink项目构建了一个经典的两区域电力系统模型&#xff0c;重点模拟了自动发电控制(AGC)中的二次调频过程。系统包含火电机组和储能装置两种调频资源&#xff0c;通过合理的控制策略实现区域间的频率稳定。对于…

作者头像 李华
网站建设 2026/8/11 4:43:38

RAID 5配置全流程详解:从原理到实战的存储基石搭建

1. 项目概述&#xff1a;为什么RAID 5依然是存储配置的经典选择在数据存储和服务器运维的领域里&#xff0c;RAID&#xff08;独立磁盘冗余阵列&#xff09;是一个绕不开的话题。而RAID 5&#xff0c;作为平衡了性能、容量和成本三者关系的“甜点”方案&#xff0c;至今仍在大量…

作者头像 李华
网站建设 2026/8/11 4:42:56

Unity集成海康威视RTSP视频流:基于UMP插件的跨平台监控方案

1. 项目概述与核心价值最近在做一个智慧园区或者安防监控相关的Unity项目&#xff0c;需要把海康威视的网络摄像头画面实时拉取到Unity场景里&#xff0c;这需求听起来简单&#xff0c;但真动起手来&#xff0c;坑是一个接一个。直接用海康的SDK&#xff1f;那意味着你得处理不…

作者头像 李华
网站建设 2026/8/11 4:42:02

Elasticsearch核心架构与实战:从倒排索引到生产部署

1. 从“搜索”到“洞察”&#xff1a;为什么我们需要Elasticsearch&#xff1f; 如果你在过去十年里做过任何与数据检索相关的开发&#xff0c;大概率听说过Elasticsearch&#xff08;简称ES&#xff09;。我第一次接触它&#xff0c;是在一个需要从几百万条日志里快速定位某个…

作者头像 李华
网站建设 2026/8/11 4:38:21

高效文件管理:从根目录批量处理到自动化工作流实践

这次我们来看一个名为“文件夹 根”的项目。从标题和有限的材料来看&#xff0c;这很可能是一个专注于本地文件系统管理、目录结构分析或批量文件处理的工具。对于经常需要处理大量文件、整理杂乱目录或进行自动化文件操作的用户来说&#xff0c;一个高效、直观的工具至关重要。…

作者头像 李华
网站建设 2026/8/11 4:36:26

Selenium无头浏览器实战:从原理到生产环境部署与优化

1. 项目概述&#xff1a;为什么我们需要无头浏览器&#xff1f;如果你正在用Selenium做自动化测试或者网页数据抓取&#xff0c;大概率遇到过这样的场景&#xff1a;脚本在本地跑得好好的&#xff0c;一放到服务器上就报错&#xff0c;或者你只想在后台默默执行任务&#xff0c…

作者头像 李华