C++ 杂记之一从基础到进阶

大纲

范围 for

范围 for 概念

C++ 的 范围 for 循环(Range-based for loop) 是 C++ 11 引入的一种简洁的遍历容器或数组的语法。

  • 特点:

    • 简洁,代码量少
    • 自动推导类型(结合 auto 使用更方便)
    • 无法直接获取索引(如果需要索引,还是用普通 for
  • 适用:

    • 数组
    • vectorlistmap 等 STL 容器
    • 自定义支持 begin() / end() 函数的类

范围 for 使用

  • 基础语法
1
2
3
for (元素类型 变量名 : 容器或数组) {
// 循环体
}
  • 遍历数组
1
2
3
4
int arr[] = {1, 2, 3, 4, 5};
for (int item : arr) {
std::cout << item << " ";
}
  • 遍历容器
1
2
3
4
std::vector<int> v = {6, 7, 8, 9, 10};
for (int item : v) {
std::cout << item << " ";
}
  • 引用遍历(避免拷贝)- 如果元素是大对象,或需要修改元素,建议用引用
1
2
3
4
std::vector<int> v = {6, 7, 8, 9, 10};
for (int &item : v) {
std::cout << item << " ";
}
  • 引用遍历(避免拷贝)- 如果不想修改元素但又不想拷贝,可以用 const&
1
2
3
4
std::vector<int> v = {6, 7, 8, 9, 10};
for (const int &item : v) {
std::cout << item << " ";
}

范围 for 适用场景

适用场景建议用法
简单遍历for (auto x : container)
遍历时需要修改元素for (auto& x : container)
遍历时保证不修改元素for (const auto& x : container)

范围 for 底层原理

  • C++ 编译器会自动把范围 for 循环转换成类似以下形式:
1
2
3
4
for (auto it = std::begin(obj); it != std::end(obj); ++it) {
auto x = *it;
// 循环体
}
  • 所以要求容器或对象:
    • 是原生数组
    • 或者支持 begin()end() 函数(可以是成员函数,也可以是 std::begin()std::end()

auto 关键字

auto 是 C++ 中的一个关键字,用于让编译器自动推导变量的类型(自动类型推导),从而使代码更简洁、灵活,减少重复声明类型的麻烦。它在 C++11 中首次引入,并在 C++14、C++17 中得到了进一步增强。

auto 核心概念

  • auto 的基础语法: auto 变量名 = 初始值;
  • auto 可以根据初始化表达式,自动推导变量的类型,无需显式写出类型。
  • auto 必须在声明时初始化,因为编译器需要通过初始值推导具体的类型。
  • auto 的自动类型推导发生在编译期间,因此使用 auto 不会影响程序的运行效率。
  • auto 支持泛型、模板编程等现代 C++ 风格,常用于范围 for 和 Lambda 表达式中。

auto 使用场景

  • 简单类型声明
1
auto x = 10;
  • 简化复杂类型声明
1
2
3
std::map<std::string, std::vector<int>>::iterator it = myMap.begin();

auto it = myMap.begin(); // 使用 auto 简洁很多
  • 与范围 for 搭配使用
1
2
3
4
5
std::vector<int> vec = {1, 2, 3};

for (auto val : vec) {
std::cout << val << std::endl;
}
  • Lambda 表达式和模板返回值推导
1
auto add = [](int a, int b) { return a + b; };

auto 功能增强

  • C++14 的增强:auto 可以用于函数返回类型
1
2
3
auto add(int a, int b) {
return a + b; // 返回类型自动推导为 int
}
  • C++20 的增强:auto 可以用于范围推导(简化结构绑定)
1
2
std::pair<int, std::string> p = {1, "hello"};
auto [id, name] = p;

auto 使用注意事项

  • 必须在声明时初始化
1
auto x;     // 错误写法:没有初始值,编译器无法推导类型
  • 推导的是值类型
1
2
3
4
5
int x = 5;
int& ref = x;
auto a = ref; // a 是 int,不是 int&

auto& b = ref; // 若想保留引用,需要显式声明,b 是 int&,会改变 x
  • const 会被保留 / 去除
1
2
3
4
const int ci = 100;

auto a = ci; // a 是 int,去掉了 const
auto& b = ci; // b 是 const int&,保留了 const

constexpr 关键字

constexpr 简介

constexpr 是 C++ 11 引入的一个关键字,表示 编译时常量表达式。它用于指示编译器:一个变量、函数或构造函数的值可以在编译期间求值,从而实现更高效的代码(例如:避免运行时计算、提升性能、在模板中使用等)。

constexpr 变量

  • constexpr 变量表示这个变量的值在编译时就已知,类似 const,但更强:
1
2
constexpr int size = 10;  // 编译期常量
int arr[size]; // 可以作为数组大小
  • constexpr 区别于 const
1
2
3
4
const int a = rand();       // 编译通过,但不是编译期常量

constexpr int b = rand(); // 编译失败,不是编译期常量
constexpr int c = 3 + 4; // 编译通过,是编译期常量

constexpr 函数

  • constexpr 函数表示用于定义可以在编译期执行的函数,在 C++ 11 中其定义规则:(1) 函数体必须只有一个 return 表达式; (2) 所有参数和调用都必须是常量表达式;
1
2
3
4
5
constexpr int square(int x) {
return x * x;
}

int arr[square(4)]; // 编译通过,square(4) 在编译期间计算结果为 16
  • 从 C++ 14 开始,constexpr 函数可以有:多条语句、条件判断(if)、循环(forwhile
1
2
3
4
5
6
7
8
// C++14 及以后支持的写法
constexpr int factorial(int n) {
int res = 1;
for (int i = 1; i <= n; ++i) {
res *= i;
}
return res;
}

constexpr 构造函数

从 C++ 11 开始,可以将一个类的构造函数定义为 constexpr,使得该类可以在编译期构造对象:

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
public:
constexpr Point(int x_, int y_) : x(x_), y(y_) {

}

private:
int x, y;
};

constexpr Point p1(1, 2); // p1 为编译期对象

constexpr 应用场景

场景用途
定义静态数组大小constexpr int size = 100; int arr[size];
作为模板参数template<int N> struct Array {}
提高性能避免运行时计算,提升速度
switch 中使用case square(3):
元编程templatetype_traits 等结合实现复杂编译期计算

constexpr 使用注意事项

  • constexpr 不等于 “只在编译期计算”,它也可以在运行期使用,只是如果传入常量,它可以在编译期计算。
  • 从 C++ 20 起,constexpr 支持的功能更强,几乎可用于所有逻辑控制(包括 try/catch 限制性支持)。
  • constexprconstevalconstinit 是不同的概念(见下表)。

constexpr 与相关关键字对比

关键字引入版本含义
constC++ 98 值不可变(可用于编译期或运行期常量)
constexprC++ 11 编译期可求值的常量或函数(从 C++ 14 开始支持更复杂的函数体)
constevalC++ 20 必须在编译期计算的函数(调用时就会被立即求值)
constinitC++ 20 保证用于初始化 staticthread_local 变量时没有静态初始化顺序问题

动态内存分配

野指针、悬空指针、裸指针

  • 野指针(Wild Pointer):指向一块未初始化或非法内存区域的指针,一旦访问就有可能触发未定义行为。
  • 悬空指针(Dangling Pointer):指针仍然指向一块已被释放或无效的内存区域,一旦访问就有可能触发未定义行为。
  • 裸指针(Raw Pointer):普通的 C/C++ 指针,直接指向有效的内存,但不具备自动内存管理能力(非智能指针),也没有生命周期约束,比如平常写的这种指针 int *p = new int(42); 就是裸指针。

C/C++ 五大存储区

C/C++ 程序的存储区有以下几个:

  • 程序代码区(Text Segment)

    • 作用:
      • 存放程序的机器指令(代码)
    • 特点:
      • 只读(防止程序自修改)
      • 多个进程可以共享(操作系统支持代码段共享)
  • 常量存储区(RODATA Segment)

    • 存放:
      • 字符串常量
      • const 修饰的全局常量(在一些编译器实现中)
    • 特点:
      • 只读(通常有内存保护)
      • .data 段分开,防止误修改
      • 注意:局部 const 变量通常在栈上分配,但全局 const 变量可能分配在常量区
  • 全局 / 静态存储区(Data Segment)

    • 已初始化全局 / 静态区(.data 段)
      • 存放:已初始化的全局变量、静态变量
      • 生命周期:程序运行整个过程,程序结束时由系统自动释放
    • 未初始化全局 / 静态区(.bss 段)
      • 存放:未初始化的全局变量、静态变量
      • 系统会自动初始化为 0 或 NULL
  • 堆(Heap)

    • 用途:
      • 程序运行时动态分配内存
    • 分配:
      • C 语言:malloc() / free()
      • C++:new / delete
    • 特点:
      • 程序员手动分配和释放内存(忘记释放会造成内存泄漏)
      • 灵活,但效率比栈稍低
  • 栈(Stack)

    • 存放:
      • 局部变量(非 static
      • 函数参数、返回地址、临时变量
    • 特点:
      • 由编译器自动分配和释放内存
      • 后进先出(LIFO)
      • 每个线程有独立栈空间
      • 栈空间有限,过大可能导致 栈溢出

C/C++ 程序各个存储区的布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
地址低

+-----------------------------+
| 程序代码区(.text) |
+-----------------------------+
| 常量区(.rodata) |
+-----------------------------+
| 已初始化全局变量(.data) |
+-----------------------------+
| 未初始化全局变量(.bss) |
+-----------------------------+
| 堆 |
| (向上增长,从低地址开始) |
+-----------------------------+
| 栈 |
| (向下增长,从高地址开始) |
+-----------------------------+

地址高

C/C++ 程序各个存储区的对比

区域主要内容生命周期典型分配方式
程序代码区可执行代码程序运行期间编译生成
常量区(RODATA)字符串字面量、常量程序运行期间编译生成
静态区(Data / BSS)全局变量、静态变量程序运行期间编译生成
局部变量、函数调用信息函数调用时分配,调用结束时释放自动管理
动态分配内存程序员控制,包括 new/deletemalloc()/free()手动管理

提示

C 语言和 C++ 程序都有上面五大存储区,但它们的使用方式和细节略有不同。

malloc 与 free 使用

基础语法

  • malloc() 动态分配内存

    • 函数原型:void* malloc(size_t size);
    • 参数:size 是要分配的字节数
    • 返回:返回 void* 指针,指向新分配的内存
    • 在 C++ 中,返回值需要强制类型转换为目标类型
  • free() 释放内存

    • 函数原型:void free(void* ptr);
    • 参数:ptrmalloc() 返回的指针
    • 作用:释放指定的内存,防止内存泄漏

使用案例

  • int 数组动态分配内存
1
2
3
4
5
6
7
int *p = nullptr;
p = (int *) malloc(sizeof(int));
if (p != nullptr) {
*p = 5;
std::cout << *p << std::endl;
free(p);
}
  • char 数组动态分配内存
1
2
3
4
5
6
7
8
9
char *c = nullptr;
const int size = sizeof(char) * 20;
c = (char *) malloc(size);
if (c != nullptr) {
memset(c, 0, size); // 建议初始化内存(防止访问越界),char 类型值为 0 时,就等于 '\0' 字符
strcpy(c, "hello world");
std::cout << c << std::endl;
free(c);
}

new 与 delete 使用

基础语法

  • new 的基础语法

    • (1) 指针变量名 = new 类型标识符,比如 int *p = new int;,分配一个 int 类型的内存,但未初始化,值是未定义的。
    • (2) 指针变量名 = new 类型标识符(初始值),比如 int *p = new int(3);,分配一个 int 类型的内存,并将值初始化为 3。
    • (3) 指针变量名 = new 类型标识符[内存单元个数],比如 int *arr = new int[5];,分配一个包含 5 个 int 类型的数组,未初始化(值未定义)。
  • delete 基础语法

    • (1) delete 指针变量名,释放指针内存。
    • (2) delete[] 指针变量名,释放数组内存。

特别注意

  • newdelete 必须结对使用;使用 new[] 申请的内存,必须使用 delete[] 释放内存。
  • delete 不能重复调用,否则会引发未定义行为;正确做法是调用 delete 后将指针设为 nullptr,当 ptr == nullptr 时,delete ptr; 是安全的,不会做任何操作。

使用案例

  • 使用案例一
1
2
3
4
5
int *p = new int;               // 分配一个int类型的内存,但未初始化,值是未定义的
*p = 5; // 手动赋值
std::cout << *p << std::endl; // 输出:5
delete p; // 释放内存
p = nullptr; // 避免悬空指针
  • 使用案例二
1
2
3
4
int *p = new int(3);            // 分配一个int类型的内存,并初始化为3
std::cout << *p << std::endl; // 输出:3
delete p; // 释放内存
p = nullptr; // 避免悬空指针
  • 使用案例三
1
2
3
4
5
6
7
8
9
10
class A {
public:
A(int x) {
std::cout << "A constructor: " << x << std::endl;
}
};

A *obj = new A(10); // 调用 A 的构造函数
delete obj;
obj = nullptr; // 避免悬空指针
  • 使用案例四
1
2
3
4
5
6
7
8
9
10
11
12
int *arr = new int[5];  // 分配一个包含5个int的数组,未初始化(值未定义)

for (int i = 0; i < 5; ++i) {
arr[i] = i * 10;
}

for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " "; // 输出:0 10 20 30 40
}

delete[] arr; // 必须用 delete[] 释放数组内存
arr = nullptr; // 避免悬空指针

malloc 和 new 的区别

  • new

    • C++ 中的运算符(operator new),用于按类型动态分配内存。
    • 底层同样是调用 malloc() 函数开辟内存,但还会调用类对象的构造函数(如果是类对象)进行初始化。
    • 返回值是对应类型的指针,不需要强制类型转换。
    • 开辟内存失败时,会抛出 std::bad_alloc 异常。
  • malloc

    • C 语言中的标准库函数,用于按字节动态分配内存。
    • 不会初始化分配的内存,也不会调用类对象的构造函数(如果分配的内存是用于类对象)。
    • 返回值是 void *,需要强制类型转换。
    • 开辟内存失败时,返回 nullptr(或 C 中返回 NULL),需要手动检查。
区别点mallocnew
所属语言 C / C++C++ 专用
本质库函数(在 <cstdlib> 中)运算符(可重载)
内存分配方式按字节动态分配内存按类型动态分配内存
返回值返回 void *,需强制类型转换返回对应类型的指针,无需类型转换
是否调用构造函数❌ 不会调用构造函数✅ 会调用构造函数
是否调用析构函数free 不会调用析构函数delete 调用析构函数
失败时的行为返回 nullptr(或 C 中返回 NULL),需手动检查抛出 std::bad_alloc 异常
是否可重载❌ 不可重载✅ 可重载 operator newoperator delete
对象初始化❌ 不支持✅ 自动调用构造函数来初始化对象
释放方式使用 free(ptr) 释放内存使用 delete ptrdelete[] ptr 释放内存

free 和 delete 的区别

  • delete 的概述

    • delete 操作符用于释放通过 new 分配的内存。
    • 在释放内存之前,delete 会先调用类对象的析构函数,以确保资源(如文件句柄、网络连接等)正确释放。
    • 如果分配的是数组,必须使用 delete[],否则可能会导致未定义行为。
  • free 的概述

    • free 是 C 标准库函数,用于释放通过 malloc/calloc/realloc 分配的内存。
    • 它只会释放内存,不会执行任何其他操作,例如对象的析构。
  • deletefree 不能混用

    • 通过 new 分配的内存必须用 delete 释放,不能用 free,否则可能会导致未定义行为。
    • 通过 malloc 分配的内存必须用 free 释放,不能用 delete,否则可能会导致未定义行为。
  • deletefree 的内存布局和管理差异

    • newdelete 是 C++ 的操作符,它们了解对象的类型,并能为复杂类型的构造和析构做出正确的处理。
    • mallocfree 是 C 的函数,它们只分配和释放内存,不了解对象的类型。
特性deletefree
适用语言 C++ 专用 C 和 C++
适用对象动态分配的对象(通过 new 分配)动态分配的内存块(通过 malloc/calloc/realloc 分配)
是否调用析构函数会调用类对象的析构函数只释放内存,不调用类对象的析构函数
分配与释放的匹配要求必须和 new 成对使用必须和 malloc/calloc/realloc 成对使用
数组释放使用 delete[] 释放动态数组没有专门的数组释放功能
底层机制 C++ 的运行时库负责,处理更高级的资源管理 C 的运行时库负责,直接释放内存