C++20 高级编程 003:从初始化、内存到const系列关键字
- 前言
- [Bilibili 同步视频](#Bilibili 同步视频)
- [一、✨ 指派初始化器:结构体初始化的优雅解法](#一、✨ 指派初始化器:结构体初始化的优雅解法)
-
- 三大核心优势(完全贴合原文)
- [正反代码对比演示(复刻书中 Employee 结构体)](#正反代码对比演示(复刻书中 Employee 结构体))
- [二、📦 内存两大阵营:栈帧 VS 自由存储区(堆)](#二、📦 内存两大阵营:栈帧 VS 自由存储区(堆))
-
- [1. 栈(栈帧)运行规则](#1. 栈(栈帧)运行规则)
- [2. 自由存储区 + 裸指针全解](#2. 自由存储区 + 裸指针全解)
-
- 开发规范警告(原文摘抄)
- [① 指针基础与空指针安全](#① 指针基础与空指针安全)
- [② 对象指针箭头运算符 ->](#② 对象指针箭头运算符 ->)
- [③ 动态分配堆数组](#③ 动态分配堆数组)
- [④ nullptr 取代老旧 NULL](#④ nullptr 取代老旧 NULL)
- [三、🔒 const 全家桶:常量修饰的多层玩法](#三、🔒 const 全家桶:常量修饰的多层玩法)
-
- 1. const 修饰普通变量,替代 #define
- [2. const 与指针的四种组合(原文核心难点)](#2. const 与指针的四种组合(原文核心难点))
- [3. const 修饰函数参数](#3. const 修饰函数参数)
- [4. const 类成员函数(const 正确性)](#4. const 类成员函数(const 正确性))
- [四、⚡ constexpr & consteval:编译期求值双兄弟](#四、⚡ constexpr & consteval:编译期求值双兄弟)
-
- [1. constexpr(C++11)](#1. constexpr(C++11))
- [2. consteval(C++20 立即函数)](#2. consteval(C++20 立即函数))
- [五、📌 收尾开发避坑清单(原文警告汇总)](#五、📌 收尾开发避坑清单(原文警告汇总))
- 结语
前言
不少深耕 C++ 的开发者常会陷入一种尴尬:能写业务逻辑,却在底层语法细节栽跟头 ------ 指派初始化分不清、栈堆内存乱释放、const指针看得头皮发麻、constexpr与consteval傻傻分不清。
本篇结合《C++20 高级编程(第 5 版)上册》第一章完整知识点,把书中原文重点提炼,搭配可直接运行的代码示例,兼顾趣味性与实用性,帮你一次性吃透底层核心逻辑,告别模糊认知~
Bilibili 同步视频
一、✨ 指派初始化器:结构体初始化的优雅解法
原文表述:"使用指派初始化器的好处是,与使用统一初始化语法相比,它更容易理解指派初始化器正在初始化的内容。使用指派初始化器,如果对某些成员的默认值感到满意,则可以跳过它们的初始化。"
三大核心优势(完全贴合原文)
-
语义清晰可读:显式标注每个待初始化成员,阅读代码不用对照结构体定义核对参数顺序;
-
可跳过成员初始化:跳过的成员若无类内初始化器,自动初始化为 0;若存在类内默认值,则沿用预设值;
-
扩展兼容性强:原文提到 "当新成员被添加到数据结构时,使用指派初始化器的现有代码将继续起作用。新的数据成员将使用其默认值进行初始化。"
反观传统统一初始化语法,原文明确限制:"如果使用统一初始化语法,这是不可以的,必须像下面这样指定 employeeNumber 为 0",不能省略中间成员,拓展性极差。
正反代码对比演示(复刻书中 Employee 结构体)
cpp
#include <iostream>
using namespace std;
struct Employee {
char firstInitial;
char lastInitial;
int employeeNumber = 0; // 无显式赋值则默认0
double salary = 75000; // 类内初始化默认薪资75000
};
int main() {
// 书中标准指派初始化写法:打乱顺序、跳过employeeNumber
Employee anEmployee{
.lastInitial = 'D',
.firstInitial = 'J',
.salary = 80000
};
// 统一初始化语法:必须按成员顺序完整传参,不能跳过中间employeeNumber
Employee emp2{'J','D',0,80000};
cout << "指派初始化员工工号:" << anEmployee.employeeNumber << ",薪资:" << anEmployee.salary << endl;
cout << "统一初始化员工工号:" << emp2.employeeNumber << ",薪资:" << emp2.salary << endl;
return 0;
}
运行输出:
Plain
指派初始化员工工号:0,薪资:80000
统一初始化员工工号:0,薪资:80000
对应原文场景:后续给Employee新增成员,指派初始化代码无需改动;但统一初始化代码必须补充参数,否则编译报错。
二、📦 内存两大阵营:栈帧 VS 自由存储区(堆)
原文划分两大内存区域:栈(栈帧)、自由存储区,二者完全隔离,绝大多数内存泄漏、程序崩溃问题都来源于对两块内存规则混淆。
1. 栈(栈帧)运行规则
原文定义:"栈帧很好,因为它为每个函数提供了独立的内存空间。如果在 foo () 栈帧中声明了一个变量,那么除非专门要求,否则调用 bar () 函数不会更改该变量。此外,foo () 函数执行完毕时,栈帧就会消失,该函数中声明的所有变量都不会再占用内存。在栈上分配内存的变量不需要由程序员释放内存 (删除),这个过程是自动完成的。"
配套书中示例代码:
cpp
#include <iostream>
using namespace std;
void foo() {
// 存储在foo函数独立栈帧中
int i = 7;
int j = 11;
cout << "foo栈内变量 i=" << i << " j=" << j << endl;
}
int main() {
foo();
// 函数执行结束,i、j内存自动销毁,main无法访问
return 0;
}
2. 自由存储区 + 裸指针全解
原文描述自由存储区:"自由存储区是与当前函数或栈帧完全独立的内存区域。如果想在函数调用结束之后仍然保存其中声明的变量,可以将变量放到自由存储区中。必须确保释放 (删除) 在自由存储区上分配的任何内存,这个过程不会自动完成,除非使用了智能指针,智能指针将在第 7 章详细讨论。"
开发规范警告(原文摘抄)
这里介绍指针是因为你将会遇到它们,尤其是在遗留代码中,但是,在新代码中,仅在不涉及所有权的情况下,才允许使用此类原始 / 裸指针。否则,应该使用第 7 章介绍的智能指针之一。
① 指针基础与空指针安全
原文强调:"未初始化的指针,因为它们会指向内存中的某个随机位置。使用这种指针很可能使程序崩溃。这就是总是应同时声明和初始化指针的原因。如果不希望立即分配内存,可以把它们初始化为空指针 nullptr" ;
同时警示:"在解引用之前指针必须有效,对 null 或未初始化的指针解引用会导致未定义的行为。程序可能崩溃,也可能继续运行,却给出奇怪的结果。"
cpp
#include <iostream>
using namespace std;
int main() {
// 规范写法:初始化空指针
int* myIntegerPointer = nullptr;
if (!myIntegerPointer) {
cout << "当前指针为空,禁止解引用!" << endl;
}
// new分配堆内存
myIntegerPointer = new int;
*myIntegerPointer = 8; // *解引用修改堆内数值
cout << "堆内整数:" << *myIntegerPointer << endl;
// 使用完成释放内存,后置空避免野指针
delete myIntegerPointer;
myIntegerPointer = nullptr;
return 0;
}
② 对象指针箭头运算符 ->
原文给出两种对象成员访问方式:
cpp
// 写法1:解引用后使用点运算符
cout << (*anEmployee).salary << endl;
// 写法2:箭头运算符(推荐,可读性更强)
cout << anEmployee->salary << endl;
同时推荐短路逻辑规避空指针崩溃,原文示例:
cpp
bool isValidSalary = ((anEmployee != nullptr) && anEmployee->salary>0);
逻辑规则:仅当anEmployee不为空时,才会执行解引用获取薪资,空指针直接短路,不会触发危险操作。
③ 动态分配堆数组
原文说明:"指针变量仍在栈中,但动态创建的数组在自由存储区中。"
释放强制规则:"delete 后的方括号表明所删除的是一个数组!每次调用 new \[\] 时,都必须相应地调用 delete \[\],以避免内存泄漏。"
示例代码完全复刻书中逻辑:
cpp
#include <iostream>
using namespace std;
int main() {
int arraySize = 8;
int* myVariableSizedArray = new int[arraySize];
myVariableSizedArray[3] = 2;
cout << "数组下标3的值:" << myVariableSizedArray[3] << endl;
// 数组必须搭配delete[]释放
delete[] myVariableSizedArray;
myVariableSizedArray = nullptr;
return 0;
}
④ nullptr 取代老旧 NULL
原文点明NULL的设计缺陷:"NULL 只是被简单地定义为常量 0,这会导致一些问题" ,重载场景下会错误匹配 int 形参;
nullptr是 C++11 标准空指针常量,原文案例:
cpp
#include <iostream>
using namespace std;
void func(int i) {
cout << "匹配int类型参数版本" << endl;
}
int main() {
func(NULL); // NULL等价整数0,错误调用func(int)
// func(nullptr); // 编译报错,无指针重载,精准暴露问题
return 0;
}
三、🔒 const 全家桶:常量修饰的多层玩法
原文开篇定义:"基本上,关键字 const 是 constant 的缩写,它表示某些内容保持不变。编译器通过将任何试图将其更改的行为标记为错误,用来保证此要求。此外,启用优化后,编译器可以利用此知识生成更好的代码。"
1. const 修饰普通变量,替代 #define
原文推荐:"在 C++ 中,鼓励程序员使用 const 取代 #define 定义常量。使用 const 定义常量就像定义变量一样,只是编译器保证代码不会改变这个值。"
书中示例常量定义:
cpp
const int versionNumberMajor = 2;
const int versionNumberMinor = 1;
const std::string productName = "Super Hyper Net Modulator";
const double PI = 3.14159265358979323842;
2. const 与指针的四种组合(原文核心难点)
原文核心判断规则:"const 关键字作用于其直接左侧的内容" ,同时补充记忆技巧:"从右向左读复杂声明"
-
const int* ip;/int const* ip;:指向 const int 的指针,不能修改指向的值,指针本身可重新赋值原文验证:
ip[4]=5; // DOES NOT COMPILE! -
int* const ip = nullptr;:指向 int 的 const 指针,指针地址不可修改,声明必须初始化原文验证:
ip=new int[10]; // DOES NOT COMPILE! -
const int* const ip = nullptr;:双重 const,指针、指向的值均不可修改
完整测试代码:
cpp
#include <iostream>
using namespace std;
int main() {
int num = 10;
int num2 = 20;
// 场景1:值不可改,指针可换指向
const int* p1 = #
// *p1 = 99; // 编译失败
p1 = &num2;
// 场景2:指针固定,值可修改
int* const p2 = #
*p2 = 99;
// p2 = &num2; // 编译失败
// 场景3:全部锁定
const int* const p3 = #
// *p3 = 99; // 编译失败
// p3 = &num2; // 编译失败
return 0;
}
3. const 修饰函数参数
原文说明:"可将非 const 变量转换为 const 变量。为什么想这么做呢?这提供了一定程度的保护,防止其他代码修改变量。"
分为两类参数:
- const 值参数:函数内无法修改参数副本
cpp
void func(const int param) {
// param = 100; 编译报错
}
- const 指针参数:保护外部原始数据,禁止函数修改外部变量
cpp
#include <string>
#include <iostream>
using namespace std;
void mysteryFunction(const string* someString) {
// *someString = "Test"; // Will not compile
}
int main() {
string myString = "The string";
mysteryFunction(&myString);
return 0;
}
4. const 类成员函数(const 正确性)
原文定义区分两种成员函数:
-
const 成员函数:检查器 (inspector) ,只读,不能修改类数据成员,声明末尾加
const; -
普通非 const 成员函数:赋值函数 (mutator),可修改成员变量。
书中 AirlineTicket 完整示例:
cpp
#include <string>
#include <iostream>
using namespace std;
class AirlineTicket
{
public:
// const只读方法
double calculatePriceInDollars() const;
string getPassengerName() const;
void setPassengerName(string name);
int getNumberOfMiles() const;
void setNumberOfMiles(int miles);
bool hasEliteSuperRewardsStatus() const;
void setHasEliteSuperRewardsStatus(bool status);
private:
string m_passengerName = "Unknown Passenger";
int m_numberOfMiles = 0;
bool m_hasEliteSuperRewardsStatus = false;
};
string AirlineTicket::getPassengerName() const
{
// m_passengerName = "测试"; // 编译报错,const函数禁止修改成员
return m_passengerName;
}
int main() {
AirlineTicket ticket;
cout << ticket.getPassengerName() << endl;
ticket.setPassengerName("开发者");
cout << ticket.getPassengerName() << endl;
return 0;
}
四、⚡ constexpr & consteval:编译期求值双兄弟
1. constexpr(C++11)
原文说明:"C++ 一直有常量表达式的概念,即在编译器求值的表达式。在某些情况下,必须使用常量表达式。例如,定义数组时,数组的大小需要为常量表达式。"
普通函数无法作为数组长度,constexpr函数可以在编译期求值,书中示例:
cpp
// 普通函数,不能作为数组长度
// const int getArraySize(){return 32;}
// int myArray[getArraySize()]; // Invalid in C++
// constexpr函数,编译期可计算
constexpr int getArraySize(){ return 32;}
int myArray[getArraySize()];// OK
int myArray2[getArraySize()+1];// OK
同时支持constexpr构造函数自定义常量对象:
cpp
#include <iostream>
using namespace std;
class Rect
{
public:
constexpr Rect(size_t width,size_t height) :m_width(width),m_height(height){}
constexpr size_t getArea() const { return m_width*m_height;}
private:
size_t m_width=0,m_height=0;
};
int main() {
constexpr Rect r{8,2};
int myArray[r.getArea()];// 合法,编译期计算数组长度
return 0;
}
原文补充特性:constexpr不强制编译期执行,传入运行时动态变量会延迟到运行时计算。
2. consteval(C++20 立即函数)
原文核心区分二者:"constexpr 关键字指定函数可以在编译期执行,但不能保证一定在编译期执行;如果确实希望保证始终在编译时对函数进行求值,则需要使用 C++20 的 consteval 关键字将函数转换为所谓的立即函数 (immediate function)。"
书中单位转换示例:
cpp
#include <iostream>
using namespace std;
// 立即函数,强制编译期求值
consteval double inchToMm(double inch){ return inch*25.4;}
int main() {
constexpr double const_inch = 6.0;
double mm1 = inchToMm(const_inch);// 正常编译,编译期计算
double dynamic_inch = 8.0;
// double mm2 = inchToMm(dynamic_inch); // 编译报错,动态变量无法编译期求值
return 0;
}
五、📌 收尾开发避坑清单(原文警告汇总)
-
内存管理规范:原文明确 "避免使用 C 中的 malloc 和 free,而使用 new 和 delete,或者使用 new \[\] 和 delete \[\]" ;
new与delete、new[]与delete[]必须成对,否则内存泄漏;新项目优先智能指针,裸指针仅用于无所有权场景; -
指针安全:禁止使用未初始化、空指针直接解引用,指针释放后赋值
nullptr;使用短路逻辑判空,规避程序崩溃; -
const 规范:遵循 const 正确性,所有只读参数、只读成员函数统一添加
const修饰; -
初始化方案:结构体优先指派初始化,兼顾可读性与代码拓展性;
-
空指针常量:抛弃老式
NULL,统一使用 C++11 标准nullptr消除重载歧义; -
编译期优化:固定常量计算选用
constexpr,需要强制编译执行、零运行时开销场景使用 C++20consteval。
结语
全文知识点全部取自《C++20 高级编程(第 5 版)上册》第一章原文,没有脱离标准教材的基础语法拓展。C++ 基础语法看似简单,但每一条规则、每一处限制都有明确的底层设计原因,线上绝大多数内存错误、编译歧义、性能损耗,根源都是对这些基础细节一知半解。

吃透指派初始化、栈堆内存管理、指针体系、const 修饰体系、编译期关键字constexpr/consteval这几大块内容,既能从容应对面试中高频的语法提问,也能写出规范、安全、高性能的现代化 C++20 工程代码。