家人们好呀如果你要把全班50个学生的成绩存起来难道要定义score1、score2、score3……一直到score50吗那你的代码会像超市小票一样长得让人绝望。幸运的是C早就帮你准备好了解决方案——数组Array。你可以把它想象成一排带编号的储物柜一个名字就能管理一整排柜子通过编号下标直接找到对应的那个。而字符串String本质上也是一种特殊的数组——字符的数组。但由于字符串实在太常用了C为它准备了专门的“VIP待遇”。本文就是你的“数组与字符串完全指南”。我们将从C风格的“老古董”讲起一直到C标准库里的现代“利器”。准备好了吗让我们开始给数据排排坐。一、数组1.1 什么是数组数组是一块连续的内存空间里面存放着一串相同类型的数据。它的核心特点有三个类型相同一个数组里只能放同一种类型的数据全是int、全是double……。大小固定对于内置数组 一旦创建数组的长度就不能改变。连续存储所有元素在内存中是紧密相邻的这赋予了数组极高的访问效率。数组就像一列首尾相接的火车车厢。你有一列int号列车它的每节车厢都只能装整数。你可以通过“第几节车厢”下标立刻找到里面的东西元素因为这列火车的车厢是连续编号的。1.2 C风格数组虽然C11之后有了更现代的std::array但C风格的原始数组仍然是很多底层代码的基石也是理解指针和内存的第一步。声明与初始化// 声明并初始化方式1指定大小随后赋值其他元素自动为0intscores[5]{95,88,76,92,100};// 声明并初始化方式2让编译器自动算大小doubleprices[]{9.99,19.99,29.99};// 自动推导出大小为3// 声明方式3先声明后赋值所有元素初值为垃圾值intdata[10];// 局部数组元素值是随机的重要提醒局部作用域的C风格数组在函数内部声明的如果只声明不初始化里面的值是垃圾值内存中残留的随机数。访问元素下标操作符 []数组用下标从0开始来访问元素intscores[5]{95,88,76,92,100};cout第一个人的成绩scores[0]endl;// 输出95cout第三个人的成绩scores[2]endl;// 输出76scores[1]90;// 把第二个人的成绩改成90数组编序号是从0开始的这被称为“零基索引”。所以有5个元素的数组有效下标是0, 1, 2, 3, 4。为什么程序员数数总是从0开始因为他们习惯了——数组的第一个元素在偏移量为0的位置就这么简单。这导致程序员在生活中也经常“从0开始”去餐厅点菜可能会说“我要第0道菜”……数组越界这是C风格数组最危险的特性访问数组时C不检查下标是否有效intdata[5]{1,2,3,4,5};coutdata[10];// 越界了但编译能通过运行时可能崩溃也可能读到随机值这种错误被称为未定义行为UB。结果可能是程序崩溃运气好也可能是悄悄读到/写入了不该动的内存导致程序在另一个完全不相关的地方出错运气差调试到你怀疑人生。1.3 数组与指针的“纠缠”这是C/C中最核心也最令人头疼的关系之一数组名在大多数情况下会被视为指向数组首元素的指针。intarr[5]{10,20,30,40,50};int*ptrarr;// arr被隐式转换为指向arr[0]的指针cout*ptrendl;// 输出10cout*(ptr1)endl;// 输出20指针偏移到arr[1]有四个例外情况数组名不会被转换为指针作为sizeof的操作数sizeof(arr)返回整个数组的大小5 × 4 20字节。作为取地址操作符的操作数arr的类型是int(*)[5]指向包含5个int的数组的指针而不是int**。作为字符串字面量用于初始化字符数组。作为decltype的操作数。指针算术指针加1实际上加的是指针所指向类型的大小而不是1个字节。对于int*加1就是地址加4字节。数组作为函数参数当你把数组名传给函数时实际上传的是指针数组首地址函数并不知道数组有多大。所以通常需要把大小也一并传过去或者用一个“哨兵值”标记数组结束C风格字符串用的就是’\0’。// 两种写法完全等价voidprintArray(intarr[],intsize);// arr本质上是指针voidprintArray(int*arr,intsize);// 和上面一模一样1.4 std::array来自C11的现代化“改良版”std::array定义在array头文件中是对C风格数组的C封装大小固定但功能更丰富。它支持迭代器、有size()方法、不会退化为指针是固定大小数组的首选。#includearrayusingnamespacestd;arrayint,5scores{95,88,76,92,100};// 大小在签名中指定// 遍历方式1标准for循环for(inti0;iscores.size();i){coutscores[i] ;}// 遍历方式2范围for推荐for(intx:scores){coutx ;}// 实用成员函数scores.size();// 获取元素个数scores.at(0);// 带越界检查的访问越界会抛出异常scores.front();// 第一个元素scores.back();// 最后一个元素scores.fill(0);// 所有元素填充为0scores.empty();// 判断是否为空std::array的优势是它不会退化为指针当作函数参数传递时类型信息得以保留使用迭代器遍历也更加安全。在现代C中能用std::array的地方就不要用C风格数组。二、多维数组套娃的艺术当一维数组不能满足需求时比如要存一个矩阵或者一张图像的数据就需要二维甚至更高维的数组。2.1 二维数组基础// 声明一个3行4列的矩阵intmatrix[3][4]{{1,2,3,4},{5,6,7,8},{9,10,11,12}};coutmatrix[0][2]endl;// 输出3第0行第2列二维数组的本质是“数组的数组”。在内存中它是按行连续存储的行优先。2.2 std::array二维版本arrayarrayint,4,3matrix2{{{1,2,3,4},{5,6,7,8},{9,10,11,12}}};for(constautorow:matrix2){// 外层遍历每一行for(intval:row){// 内层遍历行中的每个元素coutval ;}coutendl;}2.3 多维数组作为函数参数这是最需要注意的地方——当你把多维数组作为参数传递给函数时除了第一维其他维度的大小必须明确指定// 正确指定了列数voidprintMatrix(intmatrix[][4],introws){/* ... */}// 错误编译器无法确定行的大小// void printMatrix(int matrix[][]) { ... }对于更高维数组也是除了最左边的第一维其余维度大小都需要在形参声明中指定。三、C风格字符串以’\0’为终点的时代在C早期字符串就是用字符数组来处理的并以一个特殊字符’\0’空字符ASCII码为0作为结束标志。这种字符串被称为C风格字符串。3.1 基础用法charname1[]Alice;// 编译器自动在末尾加\0数组大小为6charname2[]{B,o,b,\0};// 手动添加\0charname3[20]Charlie;// 预留足够空间coutname1endl;// 输出Alice遇到\0才停关键点Alice这个字符串字面量实际占用6个字符5个字母 1个’\0’。忘记给’\0’留空间是新手常犯的错误。3.2 常用操作函数定义在cstring函数用途注意strlen(s)获取长度不含’\0’遍历到’\0’为止strcpy(dst, src)复制字符串确保dst空间足够strcat(dst, src)拼接字符串同上strcmp(s1, s2)比较大小返回0表示相等#includecstringusingnamespacestd;charbuffer[50];strcpy(buffer,Hello);// buffer现在是Hellostrcat(buffer, World);// buffer现在是Hello Worldcoutstrlen(buffer)endl;// 输出11coutstrcmp(abc,abd)endl;// 输出负数abc abdC风格字符串的优点是简单、与C语言API兼容。但缺点也很明显操作麻烦、容易越界忘了留’\0’的空间可能导致缓冲区溢出这是很多安全漏洞的根源、不能直接用比较内容。四、std::string现代C的字符串王者std::string定义在string头文件是C标准库提供的字符串类型自动管理内存、支持动态扩容使用起来像是给字符串操作开了“外挂”。4.1 基础操作#includestringusingnamespacestd;string s1;// 空字符串string s2Hello;// 初始化strings3(World);// 另一种初始化方式string s4s2 s3!;// 直接拼接输出Hello World!长度与访问string strHello;coutstr.length()endl;// 输出5coutstr[0]endl;// 输出H不检查越界coutstr.at(0)endl;// 输出H检查越界越界会抛异常str[0]h;// 修改字符str变成hello比较string可以直接用、、进行比较按字典序逐个字符比对if(str1str2){/* ... */}if(str1str2){/* 按字典序比较 */}子串与查找string strHello World;string substr.substr(0,5);// Hello从索引0开始取5个字符size_t posstr.find(World);// 返回6首次出现的位置size_t pos2str.find(o,5);// 从索引5开始找o返回7size_t pos3str.find(xyz);// 返回string::npos表示没找到str.replace(6,5,C);// 从索引6开始把5个字符替换成Cstr.erase(5,1);// 从索引5开始删除1个字符str.insert(0,Say: );// 在索引0处插入遍历stringstring strC;for(charch:str){// 范围for遍历coutch ;}for(size_t i0;istr.size();i){// 传统下标遍历coutstr[i] ;}4.2 string与数值互转C11起// 字符串转数值intastoi(42);// string to intdoublebstod(3.14);// string to doublelonglongcstoll(12345678);// string to long long// 数值转字符串string s1to_string(42);// int to string → 42string s2to_string(3.14);// double to string → 3.140000注意默认6位小数4.3 string vs C风格字符串对比维度C风格字符串char[]std::string内存管理手动容易溢出自动扩容安全拼接strcat需手动管理空间直接用号比较strcmp不能直接直接用获取长度strlenO(n)遍历.size()O(1)作为函数参数退化为指针可以传引用保留类型信息学习建议理解原理知道怎么用就行日常开发首选五、std::vectorC动态数组如果数组的大小不确定——比如你需要存一个班的学生成绩但这个班的人数随时可能变化——那么std::vector就是你的最佳选择。std::vector定义在vector头文件中是一个动态数组可以随时增长或缩小。#includevectorusingnamespacestd;vectorintv;// 空vectorvectorintv2(5);// 5个元素初始值为0vectorintv3(5,42);// 5个元素初始值为42vectorintv4{1,2,3};// 初始化列表核心操作操作代码说明添加元素v.push_back(10);在末尾添加删除末尾v.pop_back();删除最后一个元素访问v[0]、v.at(0)[]不检查越界.at()会检查大小v.size()当前元素个数容量v.capacity()已分配的内存能容纳多少个元素清空v.clear()删除所有元素size变0判空v.empty()是否为空首/尾元素v.front()、v.back()获取第一个/最后一个元素在任意位置插入v.insert(it, val)在迭代器it位置前插入删除任意位置v.erase(it)删除迭代器it指向的元素遍历vectorvectorintv{10,20,30,40,50};for(intx:v){coutx ;}// 范围forfor(size_t i0;iv.size();i){coutv[i];}// 下标for(autoitv.begin();it!v.end();it){cout*it;}// 迭代器vector是动态数组向其中添加元素可能触发重新分配内存当size()超过capacity()时。如果你大致知道会用多少元素可以用v.reserve(n)预先分配空间来避免多次重新分配。六、字符串输入再探getline与cin混用的终极解决方案在上一篇文章中我们提到过这个“千古谜题”这里再系统梳理一遍。问题cin 读取数字后紧接getline(cin, str)getline会被跳过。intage;string name;cinage;// 输入25按回车getline(cin,name);// 读到了回车符直接返回空字符串原因cin 读取了数字但把行尾的换行符’\n’留在了输入缓冲区。getline一上来就看到换行符以为读到了一行空行。终极解决方案cinage;cin.ignore(numeric_limitsstreamsize::max(),\n);// 清掉换行符getline(cin,name);// 现在正常工作了如果前面有多次cin 也可以用循环来清空缓冲区但最稳妥的做法就是在每次cin 之后、getline之前加cin.ignore()。七、现代C新特性C17到C267.1 C17std::string_viewstd::string_view定义在string_view头文件相当于一个“窗口”它指向一个已存在的字符串的某一段但不拥有内存因此创建开销极小。#includestring_viewusingnamespacestd;string strHello World;string_view svstr;// 不复制字符串内容string_view subsv.substr(0,5);// Hello也没有复制voidprint(string_view sv){// 可以同时接受string和const char*coutsvendl;}使用场景函数只需要读取字符串内容而不需要持有它时用string_view可以避免不必要的拷贝。但要注意string_view不拥有内存原字符串被销毁后不能继续使用。7.2 C20starts_with 和 ends_withstring strHello World;boolb1str.starts_with(Hell);// trueboolb2str.ends_with(rld);// trueboolb3str.contains(lo Wo);// C23true7.3 C20/23数组相关的其他改进C20起部分编译器已支持constexpr vectorC23全面支持。std::spanC20提供了对连续内存区域的安全视图访问。八、最佳实践能用std::string就别用char[]。能用std::array就别用C风格数组固定大小。能用std::vector就别用new出来的动态数组大小可变。需要只读访问字符串时考虑std::string_view。提交代码前检查所有数组下标是否可能越界。cin 后紧接getline记得清缓冲区。九、动手实践打开Visual Studio把下面的代码跑起来#includeiostream#includestring#includearray#includevectorusingnamespacestd;intmain(){// 1. std::array 演示cout std::array endl;arrayint,5scores{95,88,76,92,100};for(intx:scores)coutx ;coutendl;cout最高分*max_element(scores.begin(),scores.end())endl;// 2. std::string 演示cout\n std::string endl;string greetingHello;greeting C;coutgreeting长度greeting.length()endl;cout子串greeting.substr(0,5)endl;// 3. std::vector 演示cout\n std::vector endl;vectorstringnames;names.push_back(张三);names.push_back(李四);names.push_back(王五);for(constautoname:names)coutname ;coutendl;system(pause);return0;}十、总结恭喜你现在你已经拿下了批量存储数据的基本技能。快速回顾· C风格数组固定大小、连续内存、下标从0开始、容易越界· std::arrayC风格的安全升级版固定大小但不退化指针· 多维数组数组的数组参数传递时除第一维外都要指定大小· C风格字符串以’\0’结尾的字符数组提供操作函数· std::string现代C首选自动管理、支持拼接、比较· std::vector动态数组随时增减元素· std::string_viewC17轻量级只读字符串视图思考题为什么C风格数组作为函数参数时往往还需要传一个“大小”参数Hello这个字符串字面量实际占用几个字节为什么std::string可以直接用比较内容那char[]可以吗如果可以它比较的是什么在什么场景下你会选择std::vector而不是std::arraystd::string_view和std::string的区别是什么下一篇文章我们将学习C的函数——如何把代码组织成一个个可以重复使用的“功能模块”让程序结构更清晰。到时候你会发现main函数只是整个程序的“总导演”精彩的戏都在你自己写的函数里—— 一个曾经因为数组越界而让程序“乱码乱飞”的C学习者——————呵呵哒——————家人们真的很感谢你们的支持有幸刷到我的文章也是一种不可磨灭的缘分我还只是个命苦的学生如果你的手指还没有残废的话麻烦点一下点赞收藏关注让我不解的是为什么有的人点了赞却不收藏或者随便评论一下也行能给双方加积分。我的专栏里还有很多有趣的内容呃如果不想买的话可以看里面的试读文章我真的好良心一堆试读的我也会不断更新当然买下来我会大大滴感谢泥真的想赚点零花钱呜呜呜T_T