C++零基础到工程实战(四) - for循环与容器

文章内容

本章节目标:

  • 用循环控制重复执行的流程
  • 用数组或 vector 连续存放数据
  • 用位运算拆分和重组二进制位。

三者合在一起,完成一个可运行的 Base16 编码与解码程序。

一、批量任务:用 for 驱动确定次数

1.1 初始化、条件与迭代

for 适合循环次数已知,或需要沿着下标、迭代器逐项访问数据的任务。它的括号内有三个部分:初始化只在进入循环时执行一次;条件在每轮开始前判断;迭代在循环体完成后执行。条件为假时,循环立即结束。

初始化部分定义的变量属于 for 语句的作用域。在现代 C++ 中,循环结束后不能继续访问这里的 i。循环体内部定义的局部变量则会在每轮进入代码块时重新创建。

for 循环执行流程

下面的课件代码先输出 0 到 9,再用 continue 跳过偶数。
第二段代码先输出奇数,再判断它是否大于 50,因此会在输出 51 后执行 break。

cpp
    // 代码位置 1test_for.cpp 第 10—29 行    // 包含输入输出流头文件    #include <iostream>    // 使用标准命名空间中的名称    using namespace std;    // 定义程序入口并接收参数与环境变量    int main(int argc, char* argv[], char* env[])    // 开始主函数体    {        // 依次令 i 取零到九        for (int i = 0; i < 10; i++)        // 开始基础循环体        {            // 输出当前下标            cout << "i = " << i << endl;        // 结束基础循环体        }        // 遍历零到九十九        for (int i = 0; i < 100; i++)        // 开始流程控制示例        {            // 判断当前数是否为偶数            if (i % 2 == 0)                // 跳过本轮剩余语句                continue;            // 输出没有被跳过的奇数            cout << i << " ";            // 判断当前奇数是否已经超过五十            if (i > 50)                // 结束整个循环                break;        // 结束流程控制示例        }        // 输出换行        cout << endl;

实际编译运行(g++ -std=c++14)得到的输出:

plain text
i = 0i = 1i = 2i = 3i = 4i = 5i = 6i = 7i = 8i = 91 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51

第一段没有外部输入,输出为 i = 0 到 i = 9。第二段输出 1 3 5 ... 49 51。其中,continue 只结束当前轮,仍会执行 for 的迭代表达式;break 会越过迭代,直接离开循环。

1.2 嵌套、无限循环与环境变量

双层循环可以把行、列两个维度映射为一组坐标,常见于矩阵和图像像素处理。嵌套太深会使分支和状态难以追踪,此时可将内层逻辑提取为独立函数。

for (;;) 省略三个表达式后成为无限循环,因此循环体必须提供可靠的退出路径。
主函数的第三个参数 env 是课件所用实现提供的环境变量指针数组,以空指针结束。

cpp
    // 代码位置 1test_for.cpp 第 31—55 行    // 遍历三行    for (int i = 0; i < 3; i++)    // 开始外层循环体    {        // 遍历当前行的三列        for (int j = 0; j < 3; j++)        // 开始内层循环体        {            // 输出行列坐标            cout << i << "-" << j << " ";        // 结束内层循环体        }        // 每行结束后换行        cout << endl;    // 结束外层循环体    }    // 在矩阵输出后再换一行    cout << endl;    // 初始化无限循环的计数器    int index = 0;    // 建立无限循环    for (;;)    // 开始无限循环体    {        // 判断计数器是否越过十        if (index > 10)            // 满足退出条件时结束循环            break;        // 输出当前计数器        cout << "index:" << index << endl;        // 推进计数器        index++;    // 结束无限循环体    }    // 遍历以空指针结尾的环境变量数组    for (int i = 0; env[i] != nullptr; i++)    // 开始环境变量循环体    {        // 输出一条环境变量字符串        cout << env[i] << endl;    // 结束环境变量循环体    }

实际编译运行输出(环境变量部分因机器而异,仅截取矩阵与无限循环部分):

plain text
0-0 0-1 0-21-0 1-1 1-22-0 2-1 2-2 index:0index:1index:2index:3index:4index:5index:6index:7index:8index:9index:10

嵌套循环输出九组 行-列 坐标;无限循环输出 index:0 到 index:10,随后由 break 结束。最后一段逐项输出当前进程收到的环境变量,数量由末尾的 nullptr 决定。

二、while 表达式

2.1 条件、退出标志与条件表达式

while 只有条件,没有专门的初始化与迭代位置。它更适合命令循环、消息循环这类无法预先确定次数的任务。continue 会直接回到条件判断,因此若计数器更新写在循环体尾部,前面的 continue 可能使更新永远无法执行。

while 循环控制流

示例代码:

cpp
    // 代码位置 2test_while.cpp 第 78—116 行    // 初始化循环计数器    int index{ 0 };    // 建立常量条件的无限循环    while (1)    // 开始无限循环体    {        // 先增加计数器        index++;        // 判断计数器是否为偶数        if (index % 2 == 0)            // 偶数直接进入下一轮            continue;        // 判断奇数是否已经超过十        if (index > 10)            // 结束无限循环            break;        // 输出不大于十的奇数        cout << index << " ";    // 结束无限循环体    }    // 输出换行    cout << endl;    // 初始化退出标志    bool is_exit{ false };    // 重置计数器    index = 0;    // 在退出标志为假时循环    while (!is_exit)    // 开始标志控制的循环体    {        // 增加计数器        index++;        // 判断计数器是否超过十        if (index > 10)            // 设置退出标志            is_exit = true;        // 输出本轮计数器        cout << index << " ";    // 结束标志控制的循环体    }    // 输出换行    cout << endl;    // 重置计数器    index = 0;    // 先取 index 的旧值比较,再执行自增    while (index++ < 10)        // 输出已经自增后的值        cout << index << " ";    // 输出换行    cout << endl;    // 再次重置计数器    index = 0;    // 先自增,再把新值用于比较    while (++index < 10)        // 输出满足条件的新值        cout << index << " ";    // 输出换行    cout << endl;    // 重置计数器    index = 0;    // 每次判断时声明 i,并以非零值表示真    while (int i = ++index)    // 开始带条件声明的循环体    {        // 输出当前计数器        cout << index << " ";        // 判断本轮的 i 是否超过十        if (i > 10)            // 主动结束循环            break;    // 结束带条件声明的循环体    }    // 输出换行    cout << endl;

编译运行输出:

plain text
1 3 5 7 91 2 3 4 5 6 7 8 9 10 111 2 3 4 5 6 7 8 9 101 2 3 4 5 6 7 8 91 2 3 4 5 6 7 8 9 10 11

2.2 一个支持 ls 的最小 shell

这个练习把 while 用作命令主循环:读取命令,识别 ls,将其改写为 Windows 的 dir,再交给 system 执行;输入 exit 或 quit 后设置退出标志。

支持 ls 的 shell 交互流程

cpp
    // 代码位置 2test_while.cpp 第 10—54 行    // 包含输入输出流头文件    #include <iostream>    // 包含字符串类型头文件    #include <string>    // 使用标准命名空间中的名称    using namespace std;    // 定义程序入口    int main()    // 开始主函数体    {        // 建立局部作用域隔离 shell 变量        {            // 初始化退出标志            bool is_exit{ false };            // 未收到退出命令时持续读取            while (!is_exit)            // 开始命令循环体            {                // 保存用户输入的命令名                string cmd;                // 保存交给系统执行的命令                string execcmd;                // 输出 shell 提示符                std::cout << "xshell>>";                // 读取第一个以空白分隔的单词                std::cin >> cmd;                // 判断是否为 ls 命令                if (cmd == "ls")                // 开始 ls 分支                {                    // 将 ls 映射为 Windows 的 dir                    execcmd = "dir";                    // 保存可选参数                    string arg;                    // 检查缓冲区中是否还有换行之外的内容                    if (std::cin.rdbuf()->in_avail() > 1)                    // 开始参数读取分支                    {                        // 读取一个参数                        std::cin >> arg;                        // 将参数拼接到 dir 后面                        execcmd += " " + arg;                    // 结束参数读取分支                    }                    // 把 C++ 字符串转换为 C 字符串并执行                    system(execcmd.c_str());                // 结束 ls 分支                }                // 判断是否为两种退出命令之一                else if (cmd == "exit" || cmd == "quit")                // 开始退出分支                {                    // 请求循环在下一次判断时退出                    is_exit = true;                    // 跳过本轮剩余逻辑                    continue;                // 结束退出分支                }                // 处理不支持的命令                else                // 开始错误分支                {                    // 向标准错误输出提示                    cerr << "命令" << cmd << "不支持!" << endl;                // 结束错误分支                }            // 结束命令循环体            }        // 结束局部作用域        }        // 正常结束程序        return 0;    // main() 的收尾大括号实际在源文件第 117 行,中间是课件保留的早期练习代码

输入 ls 时,execcmd 变成 dir。输入 ls C:\\ 时,示例试图组成 dir C:\\;输入 exit 或 quit 时循环结束。

三、数组:连续内存上的批量数据

3.1 地址、下标

数组是一段连续内存。已知首地址、元素类型和元素数量后,程序可以计算任意元素的位置:&arr[i] 等价于 arr + i,实际字节偏移是 i * sizeof(元素类型)。C++ 数组下标从 0 开始。

数组内存布局

固定大小的局部数组随作用域结束而销毁;new[] 取得的动态数组必须配对 delete[];vector 自己管理动态存储,并提供大小、容量和增删接口。

三种数组对比

3.2 栈区数组

固定数组的长度必须是编译期常量表达式,变长数组不属于标准 C++。sizeof(arr1) 在数组尚未退化为指针时得到整个数组的字节数,因此可用总字节数除以单个元素字节数计算元素数量。

课件在数组长度之外,还对比了三种"看起来都像常量"的写法:const int csize = 5 由常量表达式初始化,可用于数组长度;constexpr int cesize = 5 是显式的编译期常量,同样可用;const int csize2 = size 由运行时变量 size 初始化,标准 C++ 不把它视为常量表达式,不能用于数组长度——所以源码只拿 csize 和 cesize 定义数组,没有用 csize2。

cpp
    // 代码位置 3test_arr.cpp 第 11—74 行    // 包含输入输出流头文件    #include <iostream>    // 使用标准命名空间中的名称    using namespace std;    // 定义程序入口    int main()    // 开始主函数体    {        // 建立栈区数组示例的作用域        {            // 定义四个元素并全部初始化为零            int arr1[4]{ 0 };            // 按值遍历数组元素            for (int a : arr1)                // 输出当前元素                cout << a << " ";            // 输出换行            cout << endl;            // 修改第一个元素            arr1[0] = 99;            // 修改第三个元素            arr1[2] = 88;            // 再次按值遍历数组            for (int a : arr1)                // 输出修改后的元素                cout << a << " ";            // 输出换行            cout << endl;            // 输出整个数组占用的字节数            cout << "sizeof(arr1) = " << sizeof(arr1) << endl;            // 根据总字节数计算元素数量            for (int i = 0; i < sizeof(arr1) / sizeof(int); i++)            // 开始下标遍历            {                // 输出当前下标对应的元素                cout << arr1[i] << ",";            // 结束下标遍历            }            // 输出换行            cout << endl;            // 定义未初始化的局部数组            int arr2[4];            // 按值遍历未初始化数组            for (int a : arr2)                // 输出不确定值仅用于演示                cout << a << " ";            // 输出换行            cout << endl;            // 定义运行时变量            int size = 5;            // 定义可作常量表达式的长度            const int csize = 5;            // 定义由运行时值初始化的常量            const int csize2 = size;            // 定义显式编译期常量            constexpr int cesize = 5;            // 使用编译期常量定义数组            int arr3[csize];            // 使用 constexpr 常量定义数组            int arr4[cesize];            // 用部分初值初始化十个元素            int arr5[10] = { 1, 2, 3, 4, 5 };            // 由初值数量推导数组长度            int arr6[] = { 3, 4, 5, 6, 7, 8 };            // 定义包含字符串终止符的字符数组            char arr7[] = "test string";            // 输出 arr6 的总字节数            cout << "sizeof(arr6) = " << sizeof(arr6) << endl;            // 输出 arr7 的总字节数            cout << "sizeof(arr7) = " << sizeof(arr7) << endl;            // 输出数组首地址            cout << "arr1 = " << (long long)arr1 << endl;            // 输出第一个元素地址            cout << "&arr1[0] = " << (long long)&arr1[0] << endl;            // 输出第二个元素地址            cout << "&arr1[1] = " << (long long)&arr1[1] << endl;            // 输出第三个元素地址            cout << "&arr1[2] = " << (long long)&arr1[2] << endl;            // 输出第四个元素地址            cout << "&arr1[3] = " << (long long)&arr1[3] << endl;            // 输出向后移动两个元素后的地址            cout << "arr1 + 2 = " << (long long)(arr1 + 2) << endl;            // 输出向后移动三个元素后的地址            cout << "arr1 + 3 = " << (long long)(arr1 + 3) << endl;            // 解引用第三个元素的位置            cout << "*(arr1 + 2) = " << *(arr1 + 2) << endl;        // 结束栈区数组作用域        }

编译运行(g++ -std=c++14):

plain text
0 0 0 099 0 88 0sizeof(arr1) = 1699,0,88,0,2135110968 32764 -2042182760 28896sizeof(arr6) = 24sizeof(arr7) = 12arr1 = 140722443596672&arr1[0] = 140722443596672&arr1[1] = 140722443596676&arr1[2] = 140722443596680&arr1[3] = 140722443596684arr1 + 2 = 140722443596680arr1 + 3 = 140722443596684*(arr1 + 2) = 88

arr2 那一行的四个数字就是读取未初始化内存得到的垃圾值,每次运行、每台机器都可能不同;地址那几行同理,仅反映本次运行时的栈布局,不具有可重复性,列出只是为了验证相邻元素地址确实按 sizeof(int)(这里是 4 字节)递增。

arr1 初始输出四个 0,修改后输出 99 0 88 0。在常见的四字节 int 环境中,sizeof(arr1) 是 16,相邻地址相差 4。

3.3 堆区数组与 memcpy

动态数组的长度可以在运行时决定。变量 arr1 本身是指针,所以 sizeof(arr1) 得到指针大小,并非堆上数组的总大小。释放数组时要使用 delete[];随后置为 nullptr 可以避免误把悬空指针当作有效地址继续使用。

cpp
    // 代码位置 3test_arr.cpp 第 79—105 行    // 建立堆区数组示例的作用域    {        // 分配四个整数并初始化为零        int* arr1 = new int[4] { 0 };        // 分配四个整数并给出初值        auto arr2 = new int[4] { 1, 2, 3, 4 };        // 输出指针变量自身的字节数        cout << "sizeof(arr1) = " << sizeof(arr1) << endl;        // 定义运行时数组长度        int size = 10;        // 按运行时长度分配并清零        auto arr3 = new int[size] { 0 };        // 遍历动态数组的有效下标        for (int i = 0; i < size; i++)        // 开始动态数组遍历        {            // 输出当前元素            cout << arr3[i] << "|";        // 结束动态数组遍历        }        // 输出换行        cout << endl;        // 释放第一块数组内存        delete[] arr1;        // 清空第一根指针        arr1 = nullptr;        // 释放第二块数组内存        delete[] arr2;        // 清空第二根指针        arr2 = nullptr;        // 释放第三块数组内存        delete[] arr3;        // 清空第三根指针        arr3 = nullptr;        // 定义复制来源数组        int arr5[5] = { 1, 2, 3, 4, 5 };        // 为复制目标分配等量空间        auto* arr6 = new int[5];        // 按来源数组的总字节数复制        memcpy(arr6, arr5, sizeof(arr5));        // 遍历复制结果        for (int i = 0; i < 5; i++)            // 输出目标数组元素            cout << arr6[i] << "-";        // 输出换行        cout << endl;        // 释放复制目标        delete[] arr6;        // 清空目标指针        arr6 = nullptr;    // 结束堆区数组作用域    }

实际编译运行得到的输出:

plain text
sizeof(arr1) = 80|0|0|0|0|0|0|0|0|0|1-2-3-4-5-

sizeof(arr1) 得到的是指针本身的大小(本机是 8 字节,64 位环境),并非堆上数组占用的字节数。

四、vector:自动管理动态连续空间

4.1 类型、定义与三种遍历

std::vector 是类模板,vector<int>、vector<float> 和 vector<string> 是三个不同的实际类型。它们的元素仍连续存放,但内存申请、扩容和释放由容器管理。

vector 模板类型生成

下面的代码展示空容器、指定元素数量、初始化列表和尾部插入。遍历可使用下标、迭代器或 C++11 范围 for;需要修改元素时使用引用 auto&,只读取时可用 const auto& 避免复制。

cpp
    // 代码位置 4test_vector.cpp 第 2—45 行    // 包含输入输出流头文件    #include <iostream>    // 包含 vector 容器头文件    #include <vector>    // 包含算法头文件    #include <algorithm>    // 包含字符串头文件    #include <string>    // 使用标准命名空间中的名称    using namespace std;    // 定义程序入口    int main()    // 开始主函数体    {        // 定义空的整数 vector        std::vector<int> vi;        // 定义空的字符串 vector        vector<string> vs;        // 定义空的浮点数 vector        vector<float> vf;        // 创建含十个零值元素的整数 vector        vector<int> vd1(10);        // 用初始化列表创建整数 vector        vector<int> vd2{ 1, 2, 3, 4, 6 };        // 用初始化列表创建字符串 vector        vector<string> vs1{ "ss1", "ss2", "ss3" };        // 建立基础操作的局部作用域        {            // 定义空的数据容器            std::vector<int> datas;            // 在末尾插入十            datas.push_back(10);            // 在末尾插入十一            datas.push_back(11);            // 在末尾插入十二            datas.push_back(12);            // 通过下标修改第一个元素            datas[0] = 99;            // 使用下标遍历全部元素            for (int i = 0; i < datas.size(); i++)                // 输出当前下标的元素                cout << datas[i] << " ";            // 输出换行            cout << "\n";            // 保存起始迭代器            std::vector<int>::iterator itr = datas.begin();            // 从起始迭代器走到尾后迭代器            for (auto itr = datas.begin(); itr != datas.end(); itr++)                // 解引用并输出当前元素                cout << *itr << ",";            // 输出换行            cout << "\n";            // 通过引用进行范围遍历            for (auto& d : datas)            // 开始范围循环体            {                // 输出当前元素                cout << d << "|";            // 结束范围循环体            }            // 输出换行            cout << "\n";        // 结束基础操作作用域        }

三种遍历都读取同一组状态 99、11、12,区别只在访问方式。end() 指向末尾元素之后的位置,不能解引用;循环在迭代器等于 end() 时停止。

4.2 查找、删除、插入与排序

标准算法 std::find 返回迭代器。找到元素时,迭代器减去 begin() 可得到下标;没有找到时返回 end()。erase 和 insert 会移动操作点之后的元素,并可能使已有迭代器失效。

cpp
    // 代码位置 4test_vector.cpp 第 46—98 行    // 建立增删查排示例的局部作用域    {        // 用初始数据创建容器        std::vector<int> datas{            // 写入第一段初始元素            7, 2, 3, 6, 5, 6, 7, 8, 0, 10,            // 写入第二段初始元素            5, 12, 3312, 334, 11        // 结束初始化列表        };        // 查找第一个七        auto f = find(datas.begin(), datas.end(), 7);        // 确认查找成功        if (f != datas.end())        // 开始查找成功分支        {            // 输出元素值及其下标            cout << "find " << *f << " in " << f - datas.begin() << endl;            // 删除找到的元素            datas.erase(f);        // 结束查找成功分支        }        // 查找三千三百一十二        f = find(datas.begin(), datas.end(), 3312);        // 在找到的位置前插入三千三百一十一        datas.insert(f, 3311);        // 在第三个位置插入九十九        datas.insert(datas.begin() + 2, 99);        // 指定需要删除的数值        int fdata = 6;        // 保存所有匹配位置        vector<int> datapos;        // 从末尾向开头查找        for (int i = datas.size() - 1; i >= 0; i--)        // 开始反向下标遍历        {            // 判断当前元素是否匹配            if (datas[i] == fdata)            // 开始匹配分支            {                // 按从大到小的顺序保存下标                datapos.push_back(i);            // 结束匹配分支            }        // 结束反向下标遍历        }        // 遍历已记录的位置        for (auto p : datapos)            // 输出匹配位置            cout << p << " ";        // 输出换行        cout << "\n";        // 按从后到前的顺序遍历位置        for (auto p : datapos)        // 开始删除循环体        {            // 删除对应位置的元素            datas.erase(datas.begin() + p);        // 结束删除循环体        }        // 遍历删除后的数据        for (auto d : datas)        // 开始结果输出循环体        {            // 输出当前元素            cout << d << "-";        // 结束结果输出循环体        }        // 输出换行        cout << endl;        // 按升序排列全部元素        std::sort(datas.begin(), datas.end());        // 遍历升序结果        for (auto d : datas)            // 输出升序元素            cout << d << "-";        // 输出换行        cout << endl;        // 使用 greater 比较器按降序排列        std::sort(datas.begin(), datas.end(), greater<int>());        // 遍历降序结果        for (auto d : datas)            // 输出降序元素            cout << d << "-";        // 输出换行        cout << endl;    // 结束增删查排作用域    }

第一个 7 位于下标 0,删除后数据整体前移。所有值为 6 的位置按降序保存并删除,后面的删除便不会改变尚未处理的前方下标。

4.3 size、capacity 与扩容

size() 是已经构造的元素数量,capacity() 是当前存储无需重新分配便能容纳的元素数量。resize(8) 会把元素数量改为 8,并构造新增元素;reserve(16) 只申请至少能容纳 16 个元素的空间,不增加 size()。

size 与 capacity 对比及扩容过程

cpp
    // 代码位置 4test_vector.cpp 第 101—135 行    // 建立容量观察的局部作用域    {        // 定义空容器        vector<int> v1;        // 创建十个零值元素        vector<int> vdata(10);        // 输出空容器的元素数量        cout << "v1.size()" << v1.size() << endl;        // 输出已初始化容器的元素数量        cout << "vdata.size()" << vdata.size() << endl;        // 输出空容器的容量        cout << "v1.capacity():" << v1.capacity() << endl;        // 输出已初始化容器的容量        cout << "vdata.capacity():" << vdata.capacity() << endl;        // 将元素数量改为八        v1.resize(8);        // 预留至少十六个元素的空间        v1.reserve(16);        // 输出调整后的元素数量        cout << "v1.size():" << v1.size() << endl;        // 输出调整后的容量        cout << "v1.capacity():" << v1.capacity() << endl;        // 在八个零值元素后插入九十九        v1.push_back(99);        // 遍历当前元素        for (auto v : v1)            // 输出当前元素            cout << v << "|";        // 输出换行        cout << endl;        // 销毁并移除全部元素        v1.clear();        // 请求收缩未使用的容量        v1.shrink_to_fit();        // 输出清空后的元素数量        cout << "v1.size():" << v1.size() << endl;        // 输出收缩请求后的容量        cout << "v1.capacity():" << v1.capacity() << endl;        // 输出分隔线        cout << "----------------\n";        // 保存上一次观察到的容量        int c = 0;        // 连续插入二百个整数        for (int i = 0; i < 200; i++)        // 开始扩容观察循环        {            // 在尾部插入当前整数            v1.push_back(i);            // 判断容量是否发生变化            if (c != v1.capacity())            // 开始容量变化分支            {                // 输出变化前保存的容量                cout << "v1.capacity():" << c << endl;            // 结束容量变化分支            }            // 保存当前容量供下一轮比较            c = v1.capacity();        // 结束扩容观察循环        }    // 结束容量观察作用域    }

resize(8) 后插入的 99 位于第九个元素。clear() 把 size() 变为 0,但通常保留容量;shrink_to_fit() 只是收缩请求,标准不保证一定释放,也不保证容量恰好等于元素数量。增长倍率属于标准库实现细节,不能依赖某组固定序列。已知数据规模时提前 reserve,可以减少重新分配以及由此引起的移动和迭代器失效。

4.4 push_back 与 emplace_back

push_back 接收一个已经存在或临时构造的元素,再把它复制或移动进容器;emplace_back 接收构造参数,在容器的元素位置直接构造对象。差异取决于实参:emplace_back(string("test")) 已经显式创建临时对象,仍需移动;emplace_back("test") 才直接用字符串字面量构造容器内的 string。

push_back 与 emplace_back 对象创建位置对比

cpp
    // 代码位置 4test_vector.cpp 第 139—154 行    // 建立插入方式对比的局部作用域    {        // 定义字符串容器        vector<string> vs;        // 显式创建临时 string 并移动到容器        vs.push_back(string("test"));        // 显式创建临时 string 后交给原位构造接口        vs.emplace_back(string("test"));        // 隐式构造临时 string 后插入        vs.push_back("test");        // 直接用字符指针构造容器内的 string        vs.emplace_back("test");        // 在容器内默认构造空 string        vs.emplace_back();    // 结束插入方式对比作用域    }

执行结束后,vs 含五个元素:前四个是 "test",最后一个是空字符串。若构造参数可以直接生成目标元素,原位构造能够省去临时对象;push_back 的实参类型和插入对象则更醒目。二者应按调用语义选择。

五、Base16:循环、容器与位运算

5.1 一个字节怎样变成两个字符

Base16 使用 0123456789ABCDEF 这 16 个字符表示二进制数据。四个二进制位有 16 种取值,因此一个八位字节要拆成高、低两个四位值,每个值映射为一个字符。编码后的长度是原始字节数的两倍。

字节拆分为高四位与低四位

编码表与解码表

高四位通过右移四位得到,低四位通过与掩码 0b00001111 做按位与得到。两个结果都落在 0 到 15,可直接充当编码表下标。

课件用 for (unsigned char c : teststr) 遍历,而不是 char:char 在多数平台上有符号,UTF-8 中文字节最高位为 1 时会被解释成负数,对负数做 >> 是算术右移,高位补 1,结果会落在表外。unsigned char 保证右移高位补 0,结果稳定落在 0 到 15。

位运算拆分与重组

cpp
    // 代码位置 5base16.cpp 第 10—33 行    // 包含输入输出流头文件    #include <iostream>    // 包含字符串头文件    #include <string>    // 包含 vector 头文件    #include <vector>    // 使用标准命名空间中的名称    using namespace std;    // 定义不可修改的 Base16 编码表    static const string base16_enc_tab{ "0123456789ABCDEF" };    // 定义程序入口    int main()    // 开始主函数体    {        // 准备待编码字符串        string teststr = "测试用于base16的字符串";        // 输出原始字符串        cout << "ori:\t" << teststr << endl;        // 创建编码结果字符串        string base16str;        // 按无符号字节遍历原始字符串        for (unsigned char c : teststr)        // 开始编码循环体        {            // 右移四位取得高四位            char h = c >> 4;            // 使用掩码取得低四位            char l = c & 0b00001111;            // 将高四位映射为字符并追加            base16str += base16_enc_tab[h];            // 将低四位映射为字符并追加            base16str += base16_enc_tab[l];        // 结束编码循环体        }        // 输出 Base16 编码结果        cout << "Base16:\t" << base16str << endl;

输入是 teststr 中的原始字节序列,输出是只含数字与大写 A 到 F 的 base16str。以字节 0100 0001(ASCII 的 'A')为例:右移四位得 0000 0100,即数值 4,映射为字符 '4';与 0000 1111 做按位与得 0000 0001,即数值 1,映射为字符 '1'。

5.2 用 ASCII 映射表解码

解码每次读取两个字符。程序定义了一张按 ASCII 码索引的映射表:字符 '0' 到 '9' 的编码为 48 到 57,对应数值 0 到 9;'A' 到 'F' 的编码为 65 到 70,对应数值 10 到 15。无效区间映射为 -1。

cpp
    // 代码位置 5base16.cpp 第 34—62 行    // 创建解码后的字符串    string ostr;    // 建立按 ASCII 码索引的解码表    const vector<char> base16_dec_tab{        // 为 ASCII 码零到九填入无效标记        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,        // 为 ASCII 码十到十九填入无效标记        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,        // 为 ASCII 码二十到二十九填入无效标记        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,        // 为 ASCII 码三十到三十九填入无效标记        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,        // 为 ASCII 码四十到四十七填入无效标记        -1, -1, -1, -1, -1, -1, -1, -1,        // 将字符零到九映射为数值零到九        0, 1, 2, 3, 4, 5, 6, 7, 8, 9,        // 为 ASCII 码五十八到六十四填入无效标记        -1, -1, -1, -1, -1, -1, -1,        // 将字符 A 到 F 映射为数值十到十五        10, 11, 12, 13, 14, 15    // 结束解码表初始化    };    // 每次读取两个 Base16 字符    for (int i = 0; i < base16str.size(); i += 2)    // 开始解码循环体    {        // 取得高四位对应的字符        char ch = base16str[i];        // 取得低四位对应的字符        char cl = base16str[i + 1];        // 查表得到高四位数值        unsigned char h = base16_dec_tab[ch];        // 查表得到低四位数值        unsigned char l = base16_dec_tab[cl];        // 左移高四位并与低四位合并        ostr += (h << 4 | l);    // 结束解码循环体    }    // 输出解码结果    cout << "Base16Decode:" << ostr << endl;

这里的输入由前一段代码生成,长度为偶数,字符也限定为大写 Base16 字符。每两个字符都能安全查表,输出 ostr 与 teststr 的字节序列一致。

外部输入还要检查长度是否为偶数、字符下标是否处于表内、表项是否为 -1,也可以补充小写 a 到 f 的映射。缺少这些检查时,奇数长度或非 ASCII 字符可能造成越界访问。


本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:c++

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

C++零基础到工程实战(四) - for循环与容器 · Yinchen 的博客