C++20 高级编程 003:吃透现代基础语法与标准容器

前言

不少刚从 C 语言转场 C++ 的开发者,写代码时总带着浓重的 "C 味":裸数组满天飞、手动处理指针边界、靠特殊数值标记空返回值...... 代码臃肿、隐藏着大量越界、类型截断、野指针隐患。 C++11 至 C++20 迭代新增了大量现代化语法、安全容器与工具类,从根源规避传统写法的各类坑。今天我们就从零梳理一套规范、优雅、低 bug 的现代 C++ 基础体系,覆盖容器、语法糖、面向对象、内存初始化四大核心板块,附带可直接运行的实操代码,帮大家彻底抛弃过时的 C 风格编码习惯✨

Bilibili 同步视频

C++20 高级编程 003:吃透现代基础语法与标准容器

一、告别裸 C 数组:std::array 定长容器完胜传统数组

1. 原生 C 数组的先天缺陷

传统栈数组写法简单,但处处暗藏陷阱:

cpp 复制代码
// C风格数组初始化:仅首个元素赋值,其余自动置0
int myArray[3]{2};

痛点 1:获取长度方式丑陋且有局限

老式依靠sizeof计算元素数量,仅栈数组可用,数组退化为指针后直接失效:

cpp 复制代码
// 老式写法
size_t arraySize = sizeof(myArray) / sizeof(myArray[0]);
// C++17标准写法,推荐!<array>头文件
size_t arraySize = std::size(myArray);

痛点 2:多维数组可读性差、无内置长度接口

以井字棋棋盘二维数组举例,访问逻辑晦涩,没有自带 size 接口:

cpp 复制代码
char ticTacToeBoard[3][3];
ticTacToeBoard[1][1] = 'o'; // 棋盘中心填充字符

官方明确建议:工程开发尽量规避 C 风格数组,优先选用std::array

2. std::array:编译期定长安全容器

std::array定义于<array>,本质是对原生数组的轻量化封装,编译期固定容量,运行时无法扩容 ,对比裸数组优势拉满: ✅ 自带.size()接口,随时获取元素总数 ✅ 不会隐式转换为裸指针,杜绝数组退化 bug ✅ 完整支持迭代器、范围 for 遍历 ✅ 支持 C++17 CTAD 类模板参数推导,省略冗余模板参数

完整示例代码

cpp 复制代码
#include <array>
#include <format>
#include <iostream>

int main()
{
    // 传统完整模板写法
    std::array<int, 3> arr{9, 8, 7};
    std::cout << std::format("容器总长度:{}n", arr.size());
    std::cout << std::format("第二个元素:{}n", arr[1]);

    // CTAD自动推导,极简写法,无需填写<类型,长度>
    std::array arr2{10, 20, 30, 40};
    std::cout << std::format("CTAD数组长度:{}n", arr2.size());
    return 0;
}

关键区分:std::array容量编译锁定,如果需要运行时动态增减元素,请看下方std::vector

二、动态万能容器 std::vector,运行时自由伸缩

std::vector<vector>头文件,是 C++ 最常用动态数组,自动管理堆内存,无需手动 new/delete,完美适配长度不确定的业务场景。

  • push_back():尾部追加元素,自动扩容内存

  • 支持下标[]随机访问

  • 同样支持 CTAD 推导,但空声明不能省略模板参数

实操代码演示

cpp 复制代码
#include <vector>
#include <format>
#include <iostream>

int main()
{
    // 初始化:创建容器,填充两个11
    std::vector<int> myVector{11, 22};
    // 动态追加元素
    myVector.push_back(33);
    myVector.push_back(44);
    // CTAD简化写法
    std::vector vec{100, 200, 300};

    std::cout << std::format("vector首个元素:{}n", myVector[0]);
    return 0;
}

⚠️ 踩坑提醒:vector vec; 直接空定义会编译报错,CTAD 必须依靠初始化列表推导类型。

三、两大实用工具模板:pair & optional

3.1 std::pair:打包两个不同类型数据

<utility>头文件下的std::pair,专门用于捆绑两组异构数据,通过.first.second访问成员,CTAD 加持简化代码:

cpp 复制代码
#include <utility>
#include <format>
#include <iostream>

int main()
{
    std::pair<double, int> myPair{1.23, 5};
    // CTAD极简写法
    std::pair pairDemo{3.14, 666};
    std::cout << std::format("first值:{},second值:{}n", myPair.first, myPair.second);
    return 0;
}

3.2 std::optional (C++17):优雅处理 "空值" 场景

以往我们靠-1nullptrEOF这类特殊值标记函数无返回值,可读性差、极易误判。std::optional<T>完美解决,它只有两种状态:持有有效值 / 空。 核心 API:

  1. has_value():布尔判断是否存在有效值

  2. .value():取值,空状态直接抛异常std::bad_optional_access

  3. *opt:解引用取值,和 value 行为一致

  4. .value_or(val):空值时返回自定义兜底值,无异常风险

完整示例代码

cpp 复制代码
#include <optional>
#include <format>
#include <iostream>

// 可能返回有效值,也可能返回空
std::optional<int> getData(bool giveIt)
{
    if (giveIt)
        return 42;
    return std::nullopt;
}

int main()
{
    std::optional<int> data1 = getData(true);
    std::optional<int> data2 = getData(false);

    std::cout << std::format("data1是否有值:{}n", data1.has_value());
    // 直接if判断optional
    if (!data2)
        std::cout << "data2为空,无存储数据n";

    std::cout << std::format("data1取值:{}、解引用取值:{}n", data1.value(), *data1);
    // 空值兜底,返回0
    std::cout << std::format("data2兜底取值:{}n", data2.value_or(0));
    return 0;
}

限制说明:optional<T&>非法,无法存储引用,但可以存放指针。

四、C++17 结构化绑定:一键拆解复合数据

auto [变量列表]结构化绑定堪称代码清爽神器,数组、pair、自定义聚合结构体都能一键拆分为独立变量,大幅简化取值逻辑。 硬性规则:

  1. 必须搭配auto,不能替换 int/string 等具体类型

  2. 变量数量必须和容器 / 结构体成员数量严格匹配

  3. auto&可绑定引用,直接修改原数据;const auto&只读不拷贝

三段场景代码

cpp 复制代码
#include <array>
#include <utility>
#include <format>
#include <iostream>

// 自定义聚合结构体
struct Point
{
    double m_x, m_y, m_z;
};

int main()
{
    // 1. 拆解std::array
    std::array arr{11, 22, 33};
    auto [x, y, z] = arr;
    std::cout << std::format("数组拆解:{} {} {}n", x, y, z);

    // 2. 拆解pair
    std::pair strIntPair{"hello", 99};
    auto [strVal, intVal] = strIntPair;
    std::cout << std::format("pair拆解:{} {}n", strVal, intVal);

    // 3. 拆解自定义结构体
    Point point{1.0, 2.0, 3.0};
    auto [px, py, pz] = point;
    std::cout << std::format("坐标点:{} {} {}n", px, py, pz);
    return 0;
}

五、四种循环全解,C++20 范围 for 新增初始化语法

C++ 提供四类循环,适配不同业务场景,其中基于范围的 for 循环是遍历容器首选,简洁干净无下标越界风险。

  1. while:先判断条件,满足再执行

  2. do-while:先执行一次,再判断条件,至少运行 1 次

  3. for:初始化、终止条件、迭代更新集中一行,计数循环专用

  4. 基于范围 for:遍历数组 / 容器;C++20 新增循环内初始化器

全套演示代码

cpp 复制代码
#include <array>
#include <iostream>

int main()
{
    // 1. while循环
    int i{0};
    while (i < 3)
    {
        std::cout << "while循环执行n";
        ++i;
    }

    // 2. do-while 必定执行一次
    int j{100};
    do
    {
        std::cout << "do-while强制执行一次n";
        j++;
    } while (j < 3);

    // 3. 传统for计数循环
    for (int k{0}; k < 3; ++k)
    {
        std::cout << "传统for循环n";
    }

    // 4. 基础范围for循环
    std::array arr{1, 2, 3, 4};
    for (int val : arr)
    {
        std::cout << val << " ";
    }
    std::cout << "n";

    // C++20 范围for内置初始化器语法
    for (std::array temp{10, 20, 30}; int num : temp)
    {
        std::cout << num << " ";
    }
    return 0;
}

六、initializer_list:实现可变参数函数

<initializer_list>提供类型安全的可变参数列表,函数可接收任意数量同类型参数,编译器强制校验元素类型,杜绝类型混乱。

示例:实现多数字求和工具

cpp 复制代码
#include <initializer_list>
#include <iostream>

int makeSum(std::initializer_list<int> values)
{
    int total{0};
    for (int v : values)
        total += v;
    return total;
}

int main()
{
    int a = makeSum({1, 2, 3});
    int b = makeSum({10, 20, 30, 40});
    std::cout << std::format("求和1:{},求和2:{}n", a, b);
    // makeSum({1, 2, 3.14}); 编译报错,禁止double混入int列表
    return 0;
}

七、std::string 现代字符串替代 C 字符数组

C 语言依靠char[]实现字符串,操作繁琐、极易缓冲区溢出;C++ 标准库std::string封装完整字符串能力,使用逻辑和基础类型一致,支持下标随机访问,定义于<string>头文件:

cpp 复制代码
#include <string>
#include <format>
#include <iostream>

int main()
{
    std::string myStr{"Hello Modern C++20"};
    std::cout << std::format("完整字符串:{}n", myStr);
    std::cout << std::format("第二个字符:{}n", myStr[1]);
    return 0;
}

八、C++ 面向对象基础:封装思想落地实践

C++ 是完全面向对象语言,核心封装准则:所有数据成员私有,对外仅开放 get/set 读写接口,隔离内部数据与外部调用,后续修改内部存储逻辑无需改动上层业务代码。

关键概念梳理

  1. 访问限定符

    • public:外部可自由调用,存放对外接口

    • private:仅类内部访问,存储数据成员,统一加m_前缀区分

    • protected:继承场景专用,子类可访问

  2. 构造函数:与类同名、无返回值,创建对象自动调用

  3. 析构函数:~类名,对象销毁自动执行,用于释放文件、堆内存等资源

  4. 三种成员初始化方式:构造初始化列表 > 类内就地初始化 > 构造函数内赋值

模块化完整代码示例(C++20 module 新特性)

airline_ticket.cppm 模块接口文件

cpp 复制代码
export module airline_ticket;
import <string>;
import <format>;

export class AirlineTicket
{
public:
    // 构造、析构声明
    AirlineTicket();
    ~AirlineTicket() = default;

    // 对外接口
    double calculatePriceInDollars();
    std::string getPassengerName();
    void setPassengerName(std::string name);
    int getNumberOfMiles();
    void setNumberOfMiles(int miles);
    bool hasEliteSuperRewardsStatus();
    void setHasEliteSuperRewardsStatus(bool status);

private:
    // 类内就地初始化
    std::string m_passengerName{"Unknown Passenger"};
    int m_numberOfMiles{0};
    bool m_hasEliteSuperRewardsStatus{false};
};

// 类内直接实现方法
double AirlineTicket::calculatePriceInDollars()
{
    if (hasEliteSuperRewardsStatus())
        return 0.0;
    return getNumberOfMiles() * 0.1;
}

std::string AirlineTicket::getPassengerName()
{
    return m_passengerName;
}

void AirlineTicket::setPassengerName(std::string name)
{
    m_passengerName = std::move(name);
}

int AirlineTicket::getNumberOfMiles()
{
    return m_numberOfMiles;
}

void AirlineTicket::setNumberOfMiles(int miles)
{
    m_numberOfMiles = miles;
}

bool AirlineTicket::hasEliteSuperRewardsStatus()
{
    return m_hasEliteSuperRewardsStatus;
}

void AirlineTicket::setHasEliteSuperRewardsStatus(bool status)
{
    m_hasEliteSuperRewardsStatus = status;
}

// 构造函数,初始化列表写法
AirlineTicket::AirlineTicket()
: m_passengerName("Unknown Passenger"), m_numberOfMiles(0), m_hasEliteSuperRewardsStatus(false)
{}

main.cpp 使用模块

cpp 复制代码
import airline_ticket;
import <format>;
import <iostream>;

int main()
{
    AirlineTicket ticket;
    ticket.setPassengerName("Sherman T. Socketwrench");
    ticket.setNumberOfMiles(700);
    double cost = ticket.calculatePriceInDollars();
    std::cout << std::format("机票价格:${}n", cost);
    return 0;
}

作用域解析运算符::避坑指南

代码中同名变量、函数随处可见,依靠::区分不同作用域:

  1. 命名空间::函数:指定命名空间内接口

  2. ::函数名:强制调用全局域同名函数

cpp 复制代码
#include <iostream>

class Demo
{
public:
    int get() { return 5; }
};

// 全局get函数
int get() { return 10; }

namespace NS
{
    int get() { return 20; }
}

int main()
{
    Demo d;
    std::cout << d.get() << "n";     // 输出5 类内方法
    std::cout << NS::get() << "n";   // 输出20 命名空间内
    std::cout << ::get() << "n";     // 输出10 全局函数
    std::cout << get() << "n";       // 输出10 默认优先全局
    return 0;
}

踩坑提醒:匿名命名空间若存在与全局同名函数,直接调用会触发编译歧义错误。

九、统一初始化 {} + C++20 指派初始化器,根治类型截断 bug

9.1 统一初始化(C++11 核心特性)

在 C++11 之前,结构体、类、基础变量初始化语法割裂,{}统一初始化语法抹平所有类型差异,优势拉满:

  1. 全类型通用,等号可省略,书写统一

  2. {}空括号自动零初始化:数值 0、字符串空、指针nullptr

  3. 禁止窄化隐式转换,double 转 int、long 转 short 直接编译报错,杜绝隐蔽数值 bug

  4. 支持 new 动态数组、类内数组成员、标准容器初始化

对比演示

cpp 复制代码
#include <iostream>

int main()
{
    // 多种初始化方式对比
    int a = 3;
    int b(3);
    int c{3};  // 统一初始化推荐写法
    int d{};   // 零初始化,d=0

    // 窄化转换直接报错,规避隐患
    // int err{3.14};  编译失败!禁止double截断为int
    return 0;
}

若业务确实需要窄化转换,官方推荐 GSL 库gsl::narrow_cast显式转换,语义清晰。

9.2 C++20 指派初始化器(聚合类型专属)

针对结构体、数组这类聚合类型,.成员名=值显式赋值,可读性大幅提升: 优势:

  1. 可跳过部分成员,自动填充类内默认值或 0

  2. 结构体新增成员时,旧代码无需修改,向下兼容

  3. 一眼看懂每个字段赋值逻辑,对比纯数字初始化列表可读性碾压

cpp 复制代码
#include <format>
#include <iostream>

struct Employee
{
    char firstInitial;
    char lastInitial;
    int employeeNumber;
    int salary{75000}; // 类内默认薪资
};

int main()
{
    // 传统统一初始化,全靠顺序记忆字段
    Employee emp1{'J', 'D', 42, 80000};

    // C++20指派初始化,显式绑定成员
    Employee emp2{
        .firstInitial = 'J',
        .lastInitial = 'D',
        .employeeNumber = 42,
        .salary = 80000
    };

    // 跳过employeeNumber,自动置0;salary使用默认75000
    Employee emp3{
        .firstInitial = 'L',
        .lastInitial = 'M'
    };

    std::cout << std::format("emp3默认薪资:{}n", emp3.salary);
    return 0;
}

限制:赋值顺序必须和结构体成员声明顺序保持一致,不能混用普通初始化与指派初始化。

结尾总结

从 C++11 到 C++20,语言本身一直在弱化老式 C 风格写法,用安全容器、标准化语法、编译期校验规避底层内存隐患。给大家整理一份开发优先级规范,日常编码直接遵循:

  1. 容器选择:std::vector(动态) / std::array(定长) >> 裸 C 数组

  2. 初始化:统一{}初始化 > 括号 / 等号赋值;聚合类型优先 C++20 指派初始化

  3. 空值处理:std::optional替代 - 1、nullptr 特殊标记

  4. 复合数据拆解:结构化绑定简化 pair / 结构体 / 数组取值

  5. 面向对象:私有数据 + public 读写接口,严格封装

  6. 循环遍历:范围 for 循环优先,减少下标操作降低越界风险

善用这套现代 C++ 体系,既能保留底层性能,又能大幅减少内存越界、类型截断、空值判断失误等经典 bug,写出简洁、易维护、高健壮性工程代码。

相关推荐
码匠许师傅41 分钟前
【C++ 面试真题】聊聊 C++ 的关联容器
开发语言·c++·面试
Hhy_11072 小时前
《C++深度解构02》类和对象(上)——定义规范、内存对齐与 this 指针全解
c语言·开发语言·c++·学习·visual studio
晚风叙码2 小时前
C++哈希表实现:开放定址法和链地址法 (哈希桶)
数据结构·c++·哈希算法·散列表
呜喵王阿尔萨斯2 小时前
C/C++ 杂记-1
c++
不会代码的小猴2 小时前
2. 了解Qt
开发语言·c++·笔记·qt·算法
兵哥工控3 小时前
mfc实现在数值范围内实时改变静态文本控件字体颜色实例
c++·mfc
程序喵大人5 小时前
【C++进阶】STL算法与函数对象 - 09 函数对象保存状态并复用规则
开发语言·c++·算法·stl·函数对象
ShineWinsu10 小时前
对于C++:auto_ptr、unique_ptr、shared_ptr的模拟实现
c++·面试·笔试·智能指针·unique_ptr·shared_ptr·auto_ptr
XLYcmy12 小时前
京东 算法实习一面 下+手撕
c++·python·llm·概率论·数据处理·训练·codebert