前言
不少刚从 C 语言转场 C++ 的开发者,写代码时总带着浓重的 "C 味":裸数组满天飞、手动处理指针边界、靠特殊数值标记空返回值...... 代码臃肿、隐藏着大量越界、类型截断、野指针隐患。 C++11 至 C++20 迭代新增了大量现代化语法、安全容器与工具类,从根源规避传统写法的各类坑。今天我们就从零梳理一套规范、优雅、低 bug 的现代 C++ 基础体系,覆盖容器、语法糖、面向对象、内存初始化四大核心板块,附带可直接运行的实操代码,帮大家彻底抛弃过时的 C 风格编码习惯✨
Bilibili 同步视频
一、告别裸 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):优雅处理 "空值" 场景
以往我们靠-1、nullptr、EOF这类特殊值标记函数无返回值,可读性差、极易误判。std::optional<T>完美解决,它只有两种状态:持有有效值 / 空。 核心 API:
-
has_value():布尔判断是否存在有效值 -
.value():取值,空状态直接抛异常std::bad_optional_access -
*opt:解引用取值,和 value 行为一致 -
.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、自定义聚合结构体都能一键拆分为独立变量,大幅简化取值逻辑。 硬性规则:
-
必须搭配
auto,不能替换 int/string 等具体类型 -
变量数量必须和容器 / 结构体成员数量严格匹配
-
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 循环是遍历容器首选,简洁干净无下标越界风险。
-
while:先判断条件,满足再执行
-
do-while:先执行一次,再判断条件,至少运行 1 次
-
for:初始化、终止条件、迭代更新集中一行,计数循环专用
-
基于范围 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 读写接口,隔离内部数据与外部调用,后续修改内部存储逻辑无需改动上层业务代码。
关键概念梳理
-
访问限定符
-
public:外部可自由调用,存放对外接口 -
private:仅类内部访问,存储数据成员,统一加m_前缀区分 -
protected:继承场景专用,子类可访问
-
-
构造函数:与类同名、无返回值,创建对象自动调用
-
析构函数:
~类名,对象销毁自动执行,用于释放文件、堆内存等资源 -
三种成员初始化方式:构造初始化列表 > 类内就地初始化 > 构造函数内赋值
模块化完整代码示例(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;
}
作用域解析运算符::避坑指南
代码中同名变量、函数随处可见,依靠::区分不同作用域:
-
命名空间::函数:指定命名空间内接口 -
::函数名:强制调用全局域同名函数
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 之前,结构体、类、基础变量初始化语法割裂,{}统一初始化语法抹平所有类型差异,优势拉满:
-
全类型通用,等号可省略,书写统一
-
{}空括号自动零初始化:数值 0、字符串空、指针nullptr -
禁止窄化隐式转换,double 转 int、long 转 short 直接编译报错,杜绝隐蔽数值 bug
-
支持 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 指派初始化器(聚合类型专属)
针对结构体、数组这类聚合类型,.成员名=值显式赋值,可读性大幅提升: 优势:
-
可跳过部分成员,自动填充类内默认值或 0
-
结构体新增成员时,旧代码无需修改,向下兼容
-
一眼看懂每个字段赋值逻辑,对比纯数字初始化列表可读性碾压
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 风格写法,用安全容器、标准化语法、编译期校验规避底层内存隐患。给大家整理一份开发优先级规范,日常编码直接遵循:
-
容器选择:
std::vector(动态) /std::array(定长) >> 裸 C 数组 -
初始化:统一
{}初始化 > 括号 / 等号赋值;聚合类型优先 C++20 指派初始化 -
空值处理:
std::optional替代 - 1、nullptr 特殊标记 -
复合数据拆解:结构化绑定简化 pair / 结构体 / 数组取值
-
面向对象:私有数据 + public 读写接口,严格封装
-
循环遍历:范围 for 循环优先,减少下标操作降低越界风险

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