这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

c++笔记

描述

C++ 是一种广泛使用的编程语言,其标准由国际标准化组织(ISO)维护。C++ 标准的发展 经历了多个版本,每个版本都引入了新的特性和改进。以下是 C++ 标准的主要版本及其主 要特性:

主要版本

  1. C++98 (ISO/IEC 14882:1998)

    • 第一个正式的 C++ 标准。
    • 引入了标准模板库(STL),包括容器、算法和迭代器等。
    • 支持模板、异常处理和运行时类型识别(RTTI)。
  2. C++03 (ISO/IEC 14882:2003)

    • 对 C++98 的小修小补,主要是修复了一些已知的问题。
    • 提供了更好的库支持和一些语言细节的改进。
  3. C++11 (ISO/IEC 14882:2011)

    • 引入了许多重要的新特性,大大增强了语言的功能和表达能力。
    • 主要特性包括:
      • 自动类型推导 (auto 和 decltype)
      • lambda 表达式
      • 右值引用 和 移动语义
      • 智能指针 (std::unique_ptr, std::shared_ptr)
      • 多线程支持 (std::thread, std::mutex)
      • 范围 for 循环
      • nullptr
      • 静态断言 (static_assert)
  4. C++14 (ISO/IEC 14882:2014)

    • 对 C++11 的进一步改进,增加了更多的便利性和功能。
    • 主要特性包括:
      • 泛型 lambda 表达式
      • 返回类型推导 (auto 函数)
      • 变量模板
      • 二进制字面量
      • 数字分隔符 (')
  5. C++17 (ISO/IEC 14882:2017)

    • 继续改进 C++14 的特性,并引入了一些新的特性。
    • 主要特性包括:
      • 结构化绑定 (auto [x, y] = f();)
      • if 和 switch 语句中的初始化 (if (int x = f(); x > 0))
      • 文件系统库 (<filesystem>)
      • 折叠表达式 (用于模板参数包)
      • 内联变量
      • constexpr 改进
  6. C++20 (ISO/IEC 14882:2020)

    • 引入了许多现代化的语言特性和库。
    • 主要特性包括:
      • 概念 (concept 关键字,用于模板约束)
      • 范围 (std::ranges 库)
      • 协程 (std::coroutine)
      • 模块 (module 关键字,用于替代头文件)
      • 日历和时间 zone 库
      • 三重引号字符串字面量
      • 模板参数列表中的 auto
  7. C++23 (ISO/IEC 14882:2023)

    • 最近发布的一个版本,继续扩展和完善 C++。
    • 主要特性包括:
      • 标准化的协程 (std::coroutine 的进一步改进)
      • 改进的范围库 (std::views 的扩展)
      • 标准化的格式化库 (std::format)
      • 标准化的 span 类型 (std::span)
      • 改进的模块支持
      • 标准化的预期类型 (std::expected)

编译器支持

不同的编译器对这些标准的支持程度不同。以下是一些常见编译器的最新版本对 C++ 标准 的支持情况:

  • GCC (GNU Compiler Collection):从 GCC 4.8 开始支持 C++11,GCC 7 开始支持 C++17,GCC 10 开始支持 C++20,GCC 13 开始支持 C++23。
  • Clang:从 Clang 3.1 开始支持 C++11,Clang 5 开始支持 C++17,Clang 10 开始 支持 C++20,Clang 16 开始支持 C++23。
  • Microsoft Visual Studio:从 VS 2015 开始支持 C++11,VS 2017 开始支持 C++17,VS 2019 开始支持 C++20,VS 2022 开始支持 C++23。

include 的细节

#include “headername” 和 #include <headername> 的区别

#include "headername" 和 #include <headername> 的主要区别在于头文件的搜索路 径。虽然在包含标准库文件(如 iostream)时,两者的行为通常没有区别,但在实际开发 中,两者有不同的语义,尤其是在包含自定义头文件时。

1.#include “headername”

  • 含义:首先在当前目录(即源文件所在的目录)中搜索头文件。如果没有找到该头文件, 编译器会在标准的系统路径中继续查找。

  • 用法:这种方式常用于包含用户自定义的头文件(非标准库头文件)。

  • 搜索路径:

    1. 当前源文件所在目录。
    2. 如果在当前目录找不到头文件,编译器会在标准的包含路径(如 /usr/include/ 或 C:\Program Files...)中继续查找

2.#include <headername>

  • 含义:编译器会直接在系统的标准路径中搜索头文件,而不检查当前目录。

  • 用法:这种方式通常用于包含标准库头文件,例如 iostream、vector 等。

  • 搜索路径:直接在系统的标准头文件路径中查找,不查找当前目录。

区别

  1. 主要区别是搜索路径:
    • #include "headername":先查找当前目录,然后再查找系统标准目录。
    • #include <headername>:只查找系统标准目录,不查找当前目录。
  2. 使用场景:
    • #include "headername":主要用于用户自定义的头文件,开发者可以将头文件放在 当前项目目录中。
    • #include <headername>:主要用于标准库头文件或系统级头文件,编译器会从系统 的标准路径中查找。

对于标准库头文件(如 #include <iostream>),两者的区别在很多编译器中不大,因 为即使你使用 #include "iostream",编译器也会在标准路径中找到标准库头文件。但 是,规范上推荐使用 #include <iostream> 来包含标准库头文件,因为它更明确且可 以避免潜在的冲突。

pragma 指令

描述

在 C++ 中,#pragma 是一种编译器指令,用于向编译器提供特定的编译指示。它并不是 C++ 标准的一部分,而是由不同编译器实现的,因此它的作用和支持的指令可能会因编译器 的不同而有所差异。

#pragma 是一种编译器指令,用于执行特定的编译器行为,常用于内存对齐、警告控制、优 化等任务。由于它不是 C++ 标准的一部分,因此其使用和功能通常依赖于具体的编译器。 在跨平台开发时需要特别注意,尽量避免编译器特定的 #pragma 指令,以提高代码的可移 植性

1. #pragma once

  • 用于防止头文件被多次包含。
  • 它告诉编译器,在编译过程中只包含一次该文件的内容。相比传统的 include guard (#ifndef、#define、#endif),它更简洁。

示例

#pragma once
// 头文件内容

等同于

#ifndef HEADER_FILE_NAME
#define HEADER_FILE_NAME
// 头文件内容
#endif

2. #pragma pack

  • 用于修改结构体或类的字节对齐方式。通常编译器会根据平台的要求为结构体字段进行内 存对齐,但通过 #pragma pack,你可以改变默认的对齐方式

下面的示例会让 MyStruct 使用 1 字节对齐,避免结构体字段之间的填充字节。

示例

#pragma pack(1) // 设置为1字节对齐
struct MyStruct {
    char a;
    int b;
};
#pragma pack()  // 恢复默认对齐

3. #pragma warning

  • 用于控制编译过程中是否显示某些警告,通常是在禁用特定警告或启用更多警告信息时使 用。

示例

#pragma warning(push)       // 保存当前警告状态
#pragma warning(disable:4996) // 禁用某个警告(如过时的函数使用)

// 这里可以包含会触发该警告的代码,但警告会被忽略
#pragma warning(pop)        // 恢复之前的警告状态

4. #pragma GCC(GCC 编译器专有)

  • 针对 GCC 编译器的 #pragma,如 #pragma GCC optimize,用于控制优化级别。

示例

#pragma GCC optimize ("O2") // 使用O2优化级别

5. #pragma region / #pragma endregion

  • 这个指令用于组织代码的区域,尤其在 IDE(如 Visual Studio)中,有助于折叠和展开 代码段。它主要用于改善代码可读性和导航

示例

#pragma region MyRegion
// 该区域内的代码可以被折叠
int a = 10;
int b = 20;
#pragma endregion MyRegion

C 动态分配二维数组的内存

指针数组

使用指针数组, 每个指针指向一维数组的起始位置

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3;
    int cols = 4;

    // 分配行指针数组
    double **array = (double **)malloc(rows * sizeof(double *));
    if (array == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // 为每一行分配内存
    for (int i = 0; i < rows; i++) {
        array[i] = (double *)malloc(cols * sizeof(double));
        if (array[i] == NULL) {
            printf("Memory allocation failed\n");
            // 释放已分配的内存
            for (int j = 0; j < i; j++) {
                free(array[j]);
            }
            free(array);
            return 1;
        }
    }

    // 初始化并打印二维数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j;
            printf("%f ", array[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    for (int i = 0; i < rows; i++) {
        free(array[i]);
    }
    free(array);

    return 0;
}

连续内存块

分配一个连续的内存块,并使用指针数组管理。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3;
    int cols = 4;

    // 分配行指针数组
    double **array = (double **)malloc(rows * sizeof(double *));
    if (array == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // 分配一个连续的内存块
    double *data = (double *)malloc(rows * cols * sizeof(double));
    if (data == NULL) {
        printf("Memory allocation failed\n");
        free(array);
        return 1;
    }

    // 使每个行指针指向连续内存块的对应位置
    for (int i = 0; i < rows; i++) {
        array[i] = data + i * cols;
    }

    // 初始化并打印二维数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j;
            printf("%f ", array[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    free(data);
    free(array);

    return 0;
}

指向指针的指针

分配一个指向指针的指针,然后分配每一行的内存.

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3;
    int cols = 4;

    // 分配指向指针的指针
    double **array = (double **)malloc(rows * sizeof(double *));
    if (array == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // 为每一行分配内存
    for (int i = 0; i < rows; i++) {
        array[i] = (double *)malloc(cols * sizeof(double));
        if (array[i] == NULL) {
            printf("Memory allocation failed\n");
            // 释放已分配的内存
            for (int j = 0; j < i; j++) {
                free(array[j]);
            }
            free(array);
            return 1;
        }
    }

    // 初始化并打印二维数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j;
            printf("%f ", array[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    for (int i = 0; i < rows; i++) {
        free(array[i]);
    }
    free(array);

    return 0;
}

总结

  • 方法一:适合简单的二维数组分配,但每行的内存不一定是连续的。
  • 方法二:内存是连续的,适合需要频繁访问二维数组元素的情况,性能较好。
  • 方法三:类似于方法一,但更灵活,可以用于更复杂的结构。

在 C 函数中建议 move data to heap 时

在 C 语言中,将数据移动到堆内存(heap)上,可以通过动态内存分配函数来实现。这些函数包括 malloc、calloc、realloc

malloc

malloc(memory allocation)函数用于分配指定字节数的内存,并返回指向这块内存的指针。内存未初始化。

#include <stdlib.h>

int main() {
    // 分配内存用于存储10个整数
    int *array = (int *)malloc(10 * sizeof(int));
    if (array == NULL) {
        // 处理内存分配失败的情况
        return 1;
    }

    // 使用分配的内存
    for (int i = 0; i < 10; i++) {
        array[i] = i;
    }

    // 释放内存
    free(array);

    return 0;
}

calloc

calloc(contiguous allocation) 函数用于分配指定数量的元素, 每个元素大小为指定字节数, 并将分配的内存初始化为 zero;

#include <stdlib.h>
int main()
{
    // 分配内存用于存储 10 个整数, 并初始化为 zero
    int *array = (int*)calloc(10, sizeof(int));
    if(array == NULL)
    {
        // 处理内存分配失败的情况
        return 1;
    }

    // 使用分配的内存
    for(int i = 0; i < 10; i++>)
    {
        // array[i] 已初始化为 0
    }

    // 释放内存
    free(array);

    return 0;
}

realloc

realloc (re-allocation) 函数用于调整之前分配的内存块的大小, 如果新的内存大于旧的内存, 新的内存部分未初始化

#include <stdlib.h>

int main()
{
    // 分配内存用于存储 10 个整数
    int *array = (int *)malloc(10 * sizeof(int));
    if(array == NULL)
    {
        // 处理内存分配失败的情况
        return 1;
    }

    // 调整 内存块的大小以存储 20 个整数
    array = (int *)realloc(array, 20 * sizeof(int));
    if(array == NULL)
    {
        // 处理内存分配失败的情况
        return 1;
    }

    // 使用调整后的内存
    for(int i = 0; i < 20; i++)
    {
        array[i] = i;
    }

    // 释放内存
    free(array);

    return 0;
}

Note: 注意事项

  1. 内存泄漏:确保每个通过 malloc、calloc 或 realloc 分配的内存块最终都被 free 释放,否则会导致内存泄漏。
  2. 空指针检查:在使用分配的内存之前,始终检查返回的指针是否为 NULL,以确保内存分配成功。
  3. 内存初始化:使用 calloc 可以确保分配的内存被初始化为零,而 malloc 分配的内存则未初始化。

比较 boost 和 stl

Boost 和 STL(Standard Template Library)是两个非常重要的 C++ 库,它们各自提供了 丰富的功能和工具,但在用途、设计哲学和使用场景上有一些显著的区别。下面是对 Boost 和 STL 的详细比较:

STL(Standard Template Library)

  1. 标准库的一部分:STL 是 C++ 标准库的一部分,由 ISO/IEC 标准化组织定义。所 有符合标准的 C++ 编译器都必须提供 STL。
  2. 核心组件:STL 主要包含容器、算法和迭代器三个核心组件。
    • 容器:如 vector、list、map、set 等,用于存储和管理数据。
    • 算法:如 sort、find、copy 等,用于对容器中的数据进行操作。
    • 迭代器:用于遍历容器中的元素,提供了统一的接口。
  3. 轻量级:STL 设计简洁,专注于基本的数据结构和算法,适合大多数常见的编程任 务。
  4. 性能优化:STL 的实现经过高度优化,性能通常非常好。

使用场景

  • 通用数据结构和算法:适用于大多数常见的数据管理和操作需求。
  • 学习和教学:STL 是学习 C++ 标准库和模板编程的好起点。

Boost

  1. 第三方库:Boost 是一个第三方库集合,由 C++ 社区开发和维护,不是标准库的一 部分。
  2. 功能丰富:Boost 提供了比 STL 更多的功能,涵盖了网络编程、线程管理、正则表 达式、智能指针、日期和时间处理等多个领域。
  3. 模块化:Boost 包含许多独立的模块,每个模块解决特定的问题。用户可以根据需 要选择和使用这些模块。
  4. 高级特性:Boost 提供了许多高级特性,如元编程、函数对象、信号槽机制等,适 合复杂的编程任务。
  5. 社区支持:Boost 有一个活跃的社区,提供了丰富的文档、示例和讨论论坛。

使用场景

  • 复杂应用开发:适用于需要高级功能和复杂数据结构的项目。
  • 高性能计算:许多 Boost 模块针对高性能计算进行了优化。
  • 跨平台开发:Boost 提供了许多跨平台的解决方案,适合多平台项目。

对比总结

功能对比

  • STL:主要提供基本的数据结构和算法,适合大多数常见的编程任务。
  • Boost:提供更丰富的功能和高级特性,适合复杂的编程需求。

性能对比

  • STL:经过高度优化,性能通常非常好。
  • Boost:某些模块也经过优化,但整体性能取决于具体的模块和实现。

易用性对比

  • STL:设计简洁,易于学习和使用。
  • Boost:功能丰富,但可能需要更多的时间来学习和掌握。

社区和支持

  • STL:作为标准库的一部分,有广泛的文档和教程支持。
  • Boost:有活跃的社区,提供了丰富的资源和支持。

结论

  • STL 是 C++ 标准库的一部分,提供了基本的数据结构和算法,适合大多数常见的编 程任务。
  • Boost 是一个功能丰富的第三方库,提供了许多高级特性,适合复杂的编程需求。

move函数的实现原理

C++ 是如何实现面向对象编程的

面向对象的三个特性

  • 封装:将对象的状态(数据成员)和行为(成员函数)捆绑在一起,并控制对它们的访问。封装的主要目的是隐藏类的内部实现细节,并仅暴露必要的接口供外部使用。这样可以保护数据,防止外部代码意外或恶意地改变对象的状态。
    • 数据成员可以被声明为 private、protected 或 public。
    • 成员函数通常也是 public 的,以便用户可以调用这些函数来操作对象。
    • 使用 private 和 protected 修饰符可以限制对数据成员的直接访问。
  • 继承:允许创建新的类(称为派生类或子类)从现有的类(称为基类或父类)继承属性和方法。这样可以复用代码,同时也可以扩展或修改基类的行为。
    • 派生类可以继承基类的所有非私有成员。
    • 派生类可以添加新的成员变量和方法,或者重写基类的方法。
    • 继承支持层次化的分类结构,便于建模现实世界的关系
  • 多态: 允许使用一个接口来表示多种类型的特性。在 C++ 中,多态通常是通过 虚函数 (virtual function)来实现的。当一个函数被声明为虚函数时,可以在派生类中重写该函数,并且可以通过基类的指针或引用调用派生类中的重写版本。
    • 支持运行时多态性(runtime polymorphism),即可以在运行时确定调用哪个版本的函数。
    • 通常与虚函数和接口有关

虚函数 和 纯虚函数

虚函数的实现机制

C++ 中多态的实现原理

多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。

重载 | 重写 | 隐藏

  • 函数重载: 同一可访问区 内被声明几个具有 不同 参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
  • 函数重写(覆盖): 派生类中存在 重新定义 的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数 必须 有 virtual 修饰。
  • 函数隐藏: 在面向对象编程中,特别是在 C++ 中,当一个派生类(derived class)中定义了一个与基类(base class)中相同名称的成员函数时,如果没有特别标记为 virtual,那么派生类中的成员函数就会“隐藏”掉基类中的同名成员函数。这意味着当你通过派生类的对象调用这个函数时,调用的是派生类中的版本,而不是基类中的版本。
class Base
{
public:
    void fun(int tmp, bool tag ) { cout << "Base::fun(int tmp, bool tmp1)" << endl; }
    virtual void vir_fun(int* array) { cout << "Base::vir_fun(int* array)" << endl; }
}

class Derived: public Base
{
public:
    void fun(int tmp);              // 隐藏基类中的同名函数
    void fun(float tmp);            // 重载 参数类型不同(相对于上一个函数)
    void fun(int tmp, float tmp1);  // 重载 参数个数不同(相对于上一个函数)
    void fun(float tmp, int tmp1);  // 重载 参数顺序不同(相对于上一个函数)
    int  fun(int tmp);               // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型

    virtual void vir_fun(int* array) { cout << "Derived::vir_fun(int* array)"<< endl; }
};

int main()
{

    Derived ex;
    ex.fun(1);         // Derive::fun(int tmp)
    ex.fun(1, false);  // error: candidate expects 1 argument, 2 provided
    ex.fun.Base.fun(1, true); // Base::fun(int bool)

    Base *base = new Derived();
    base->vir_fun({1,3,4}); // Derived::vir_fun(int* array)

    return 0;
}

重载和重写的区别

  • 范围区别:对于作用函数的 空间 而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。
  • 参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都必须和原函数相同,父类中被重写的函数需要有 virtual 修饰。
  • virtual:重写的函数基类中必须有 virtual 关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有(取决于重载的函数是否被派生)。

隐藏和重写, 重载的区别

  • 范围区别:隐藏与重载 范围不同,隐藏发生在不同类中。
  • 参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,只要派生类中有一个同名函数, 基类函数都是被隐藏,而不是重写。
  • 利用重写可以实现多态,而隐藏不可以: 如果使用基类指针 p 指向派生类对象,利用这个指针调用函数时,对于隐藏的函数,会根据指针的类型去调用函数;对于重写的函数,会根据指针所指对象的类型去调用函数。重写必须使用 virtual 关键字,此时会更改派生类虚函数表的表项。
  • 隐藏是发生在编译时,即在编译时由编译器实现隐藏,而重写一般发生运行时,即运行时会查找类的虚函数表,决定调用函数接口

sizeof 和 strlen 的区别

编译与链接

什么是编译

C++ 是一种静态类型的编程语言,CPU 无法直接执行文件中的各行命令, 这意味着在程序执行之前需要通过 编译过程 将其源代码转换为机器码. 编译器会读取程序的源文件 .cpp, 将其翻译为可执行文件 ELF, ELF 文件可以经过操作系统进行加载执行

编译的过程

常见的编译过程分为四个过程: 编译预处理, 编译, 汇编, 链接

  • 编译预处理:在预编译过程中主要处理源代码中的预处理指令,比如引入头文件#include,去除注释,处理所有的条件编译指令#ifdef, #ifndef, #else, #elif, #endif,宏的替换#define,添加行号,保留所有的编译器指令;
  • 编译:针对预处理后的文件进行词法分析、语法分析、语义分析、符号汇总、汇编代码生成,并针对程序的结构或者特定的 CPU 平台进行优化,其中涉及的过程较为复杂。简单来说编译的过程即为将 .cpp 源文件翻译成 .s 的汇编代码;
  • 汇编:将汇编代码 .s 翻译成机器指令 .o 文件,一个 .cpp 文件只会生成一个 .o 文件;
  • 链接:汇编程序生成的目标文件即为 .o 文件,单独的 .o 文件可能无法执行。因为一个程序可能由多个源文件组成,此时就存在多个 .o 文件。文件 A 中的函数引用了另一个文件 B 中定义的符号或者调用了某个库文件中的函数,这就需要 链接处理。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成一个可被操作系统加载执行的 ELF 程序文件。链接的过程较为复杂,如有兴趣可以参考:「Computer Systems: A Programmer’s Perspective」。

什么是链接

链接的种类

静态链接

动态链接

大端和小端

什么是字节序(Endian-ness)

两种字节序的形式

小端(Little-Endian)

大段(Bit-Endian)

字节序的转换

内存泄漏

什么是内存泄漏

程序块在[堆]中申请的[动态内存], 在程序块使用完成后没有得到[及时释放], 从而导致了堆中可使用的内存越来越少的现象

  • 内存泄漏是逻辑上的概念, 指在程序运行期间, 总内存的可复用率变低的问题
  • 内存泄漏主要指在[堆]上分配的变量所占用的内存, 因为[栈]中分配的变量, 会随着函数退出时自动回收. 而堆是动态分配的
  • 不仅要考虑 malloc, calloc, realloc, new 等分配的内存是否得到了释放, 也要考虑使用系统资源或者在[堆]中创建的对象

内存泄漏的检测与预防的实践

通常会利用各种[内存检测工具]来检测内存泄漏, 其原理也是统计和分析内存的申请和释放的相关日志

以下通过 valgrind 的实践来介绍

valgrind 实践

valgrind 是 Linux 下, 开源代码 (GPL V2) 的仿真调试工具的集合, 包括以下工具:

  • Memcheck:

c++ 内存管理

ELF 文件:

可执行与可链接格式(Executable and Linkable Format)是一种用于可执行文件, 目标代码, 共享库和核心转储(core dump)的标准文件格式, 每个ELF文件都由一个ELF header 和紧跟其后的文件数据部分组成, 可以参考 ELF 文件的构成如下:

alt text

我们可以看见可执行程序内部都是分段进行存储的.

  • .text section: 代码段, 通常存放已编译程序的机器代码, 一般操作系统加载后, 这部分是只读的
  • .rodata section: 只读数据段, 此段的数据不可修改, 存放程序中会使用的常量
  • data section: 数据段, 主要用于存放已初始化的全局变量, 常量
  • bss section: 未初始化的全局标量, 仅是占位符, 不占据任何实际磁盘空间

目标文件格式区分初始化和非初始化是为了空间效率

操作系统在加载 ELF 文件时, 会按照文件结构, 依次读取每个 section 中的内容, 并将其加载到内存中, 同时为该进程分配空间, 并将 pc寄存器 指向代码段的起始位置, 然后启动进程

内存分区

C++ 程序在运行时, 会按照不同的功能划分不同的段: 栈, 堆, 全局/静态存储区, 常量存储区, 代码区

  • 栈: 目前绝大多数CPU体系是基于栈来运行程序的, 栈中主要存放函数的局部变量, 函数参数, 返回地址等, 栈空间一般由操作系统进行默认分配或者程序指定分配, 栈空间在进程生存周期一直都存在, 当进程退出时, 操作系统才会对栈空间进行回收
  • 堆: 动态申请的内存空间, 就是由 malloc 函数或者 new 函数分配的内存块, 由系统控制它的分配和释放, 可以在程序运行周期随时进行申请和释放, 如果进程结束后还没收释放, 操作系统会自动回收
  • 全局区/静态存储区: 主要为 .bss 和 .data 段, 存放全局变量和静态变量, 程序运行结束, 操作系统自动释放, 在 C 中, 未初始化的放在 .bss 段中, 初始化的放在 .data 段中
  • 常量存储区: .rodata 段, 存放常量, 不允许修改, 程序运行结束后自动释放
  • 代码区: .text 段, 存放代码, 不允许修改, 但可以执行, 编译后的二进制文件存放在这里

参考 Linux 操作系统的内存分布如下:

从操作系统的本身而言, 以上存储区在该程序内存中的虚拟地址分布是如下形式(虚拟地址从低地址 到 高地址, 实际的物理地址可能是随机的): text->.data->.bss->heap->unused->stack->…

alt text

程序实例

以下为详细的程序实例, 也可以通过 GDB 工具来查看具体的每个变量的存放地址

#include <iostream>
using namespace std;

/*
* 说明: C++ 中不再区分初始化和未初始化的全局变量, 静态变量的存储区, 如果非要区分, 在下述程序标注在了括号中
*/

int g_var = 0; // g_var 在全局区(.data 段)
char * gp_var; // gp_var 在全局区(.bss 段)

int main()
{
    int var;    // var 在 栈区
    char * p_var;   // p_var 在栈区
    char arr[] = "abc"; // arr 为数组变量, 存储在栈区: "abc" 为字符串常量, 存储在常量区
    char *p_var1 = "12345"; // p_var1 在栈区: "123456" 为字符串常量, 存储在常量区
    static int s_var = 0; // s_var 为静态变量, 存在静态存储区(.data 段)
    p_var = (char*)malloc(10) // 分配得来的10个字节的区域在堆区
    free(p_var);
    return 0;
}

变量定义与生命周期

作用域 与 生命周期

这两个属性, 以 空间(作用域) 和 时间(生命周期) 两个不同的维度来描述一个 变量

作用域

一个变量可以被引用的范围

  • 全局变量: 全局变量只需在一个源文件中定义, 就可以作用域所有的源文件, 其他不包含全局变量定义的源文件需要用 extern 关键字再次声明这个全局变量
  • 静态全局变量: 与全局变量的区别在于如果程序包含多个文件的话, 它作用域定义它的文件里, 不能作用到其他文件里, 即被 static 关键字修饰过的变量具有文件作用域, 即使两个不同的文件都定义了相同的名字的静态全局变量, 它们也是不同的变量
  • 局部变量: 它是自动对象(auto), 在程序运行期间不是一直存在,而是只在函数执行区间存在, 函数的依次调用执行结束后, 变量被撤销, 其所占的内存也被收回, 局部变量对于函数外部的程序说是不可见的, 实际是以 {} 为所用与
  • 静态局部变量: 它直被初始化依次, 自从第一次被初始化知道程序运行结束都一直存在, 它只对定义自己的函数体始终可见, 只有定义该变量的函数内部可以使用和访问该变量

生命周期

变量可以存在(被引用)的时间段

  • 全局变量: 在真个程序运行期间都会一直存在, 都可以随时访问, 当程序结束时, 对应的变量会自动销毁, 内存被系统回收
  • 局部变量: 仅限于函数被调用期间, 当函数调用结束时, 该变量会自动销毁
  • 静态局部变量: 在程序运行期间一直存在, 局部静态变量只能被初始化一次

从分配内存空间看

不同生命周期的变量, 在程序内存中的分布位置是不一样的, 内存分为 代码区(.text), 全局数据区(.data, .bss, .rodata), 堆区(heap), 栈区(stack), 不同的内存区域, 对应不同的生命周期

alt text

  • 静态变量一般存储在数据段, 包括 data 段. bss 段, rodata 段, 其中 data 存储已经初始化的静态变量和全局变量, bss 存储未初始化的静态变量与全局变量
  • 局部变量一般存储在 栈区 或者 堆区

Note

  • 静态变量和栈变量(存储在栈中的变量), 堆变量(存储在堆中的变量)的区别: 静态变量会被放在程序的静态存储区(.data 段, bss 段, rodata 段)中(静态变量会自动初始化, 这样可以在下一次调用的时候还可以保持原来的赋值), 而栈变量或堆变量不能保证在下一次调用的时候依然保持原来的值
  • 静态变量和全局变量的区别: 静态变量仅在变量的作用范围内可见, 实际是依靠编译器来操控作用域, 全局变量在整个程序范围内都可见, 只需要声明该全局变量, 即可使用
  • 全局变量的定义不要再头文件中定义: 如果在头文件中定义全局变量, 当该头文件被多个文件 #include 时, 该头文件中的全局变量就会被多次定义, 编译时会应为重复定义而报错, 一般情况下我们将变量的定义放在 .cpp 文件中, 在 .hpp 文件使用 extern 对变量进行声明

堆与栈

内存对齐

内存对齐

计算机中内存的地址空间是按照 byte 来划分的, 从理论上讲对任何类型变量的访问, 可以从内存中的任意地址开始, 但实际情况是: 在访问特定类型变量的时候通常在特定的内存地址访问, 这就需要对这些数据在内存中存放的位置进行限制, 各种类型数据按照一定的规划在空间上排列, 而不是顺序地一个接一个地排放, 这就是对齐, 编译器将程序中地每个 数据单元 的地址安排在机器字的整数倍的地址指向的内存之中

为什么要内存对齐

主要是由于 CPU 的访问内存的特性决定, CPu 访问内存时并不是以字节为单位来读取内存, 而是以机器字长为单位, 实际机器字长由 CPU 数据总线宽度决定的, 实际 CPU 运行时, 每一次控制内存读写信号发生时, CPU 可以从内存中读取数据总线宽度的数据, 并将其写入到 CPU 的通用寄存器中

比如, 32位cpu, 机器字长为 4字节, 数据总线宽度为 32 为, 如果该 CPU 的地址总线宽度也为 32 位, 则其可以访问的地址空间为[0, 0xffffffff]

内存对齐的主要目的是为了减少 CPU 访问内存的次数, 加大 CPU 访问内存的吞吐量

假设: 读取 8 个字节的数据, 按照每次读取 4 个字节的速度, 则 8 个字节需要 CPU 耗费 2 次读取操作

CPU 始终以字节访问内存, 如果不进行内存对齐, 很可能增加 CPU 访问内存的次数

alt text

比如: 以上在读取变量 b 时, 如果不进行内存对齐的话, 会导致 CPU 读取次数为 2, 在内存对齐的情况下, 只需读取一次即可, 当然实际的读取非对齐的内存处理更为复杂, 我们参考下图中读取非对齐内存时的数据处理过程

alt text

除了能够减少内存访问次数, 增加内存读取的吞吐量以外, 还有其他原因

  • 比如某些特性的硬件设备只能存取对齐数据, 存取非对齐的数据可能会引发一场, 比如 CPU 中 SIMD 指令, 则必须要求内存严格对齐
  • 每次内存访问是原子的, 如果变量的大小不超过字长, 那么内存对齐后, 对该变量的访问就是原子的. 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作, 因此此时 CPU 可能需要读取多次内存, 这样就破坏了 变量的原子性
  • 相比于存取对齐的数据, 存取非对齐的数据需要花费更多的时间, 提高内存的访问效率, 因为 CPU 在读取内存时, 是一块一块地读取
  • 某些处理器虽然支持非对齐数据的访问, 但会引发 对齐陷阱(alignment trap)
  • 某些硬件设备只支持简单数据指令而非对齐存取, 不支持复杂数据指令的非对齐存取

内存对齐的原则

程序中的内存对齐大部分是由编译器来处理, 编译器会自动在内存之间填充字节, 结构体中的变量对齐的基本规则如下:

  • 结构体变量的首地址能够被其最宽的基本类型成员的长度和对齐基数二者中的较小者整除;
  • 结构体中的 static 成员变量不占用结构体的空间, 由于静态成员变量在程序初始化时已经在静态存储区分配完成, 所有该结构体实例中的静态成员都指向同一个内存区域;
  • 结构体中每个成员相对于结构体首地址的偏移量(offset)都是该成员大小与对齐基数中的较小者的整数倍, 如有需要, 编译器会在成员之间加上 填充字节(internal padding)
  • 结构体的总大小为结构体中最宽基本类型成员的长度与对齐基数二者中的较小者的整数倍, 如有需要编译器会在最末尾的成员之后加上 填充字节(internal padding)

指定程序对齐规则

我们可以指定结构体的对齐规则, 在某些特定场景下, 我们需要指定结构体内存进行对齐

例如: 在发送特定网络协议报文, 硬件协议控制, 消息传递, 硬件寄存器访问时, 这时, 就需要避免内存对齐, 因为双方均按照预定义的消息格式来进行交互, 从而避免不同的硬件平台造成的差异, 同时能够将双方传递的数据进行空间压缩, 避免不必要的空间浪费

programpack: 通过使用 #progma pack(x) 指定结构体以 x 为单位进行对齐

#pragma pack (push)
#pragma pack (x)
// 存放需要 x 对齐方式的数据块
#pragma pack (pop)

attribute((aligned(n))): __attribute__((aligned (n))) 让所作用的结构成员对齐在 n 字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。需要注意的是:内存对齐的对齐数取决于对齐系数和成员的字节数两者之中的较小值。对齐属性的有效性会受到链接器(linker)固有限制的限制,即如果你的链接器仅仅支持 8 字节对齐,即使你指定16字节对齐,那么它也仅仅提供 8 字节对齐。__attribute__((packed)) 表示取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,这部分属于 gcc 中的用法,详细了解详情可以参考 gcc 的手册

alignof: C++ 11 以后新增 alignof 的特性,通过调用 alignof 返回当前变量的字节对齐方式

小结

内存对齐使得程序便于在不同的平台之间进行移植, 因为有些硬件平台不能够支持任意地址的数据访问, 只能在某些地址处取某些特定的数据, 否则会抛出异常; 另一方面提高内存的访问效率, 因为 CPU 在读取内存时, 是以块为单位进行读取的

智能指针的创建

make_unique 和 make_share

优先使用 std:make_unique 和 std:make_shared, 而非使用 new;

  • make_unique: 减少代码量, 能够加快编译速度, 定义两边类型时, 编译器需要进行类型推导, 会降低编译速度, 某些意外情况下可能会导致内存泄漏, 但是 make_unique 不允许自定析构器, 不接受 std:initializer_list 对象
  • make_shared: 这个主要是可以减少对堆中申请内存的次数, 只需要申请一次即可

智能指针简介与使用

智能指针

为了解决动态内存分配时带来的内存泄露以及多次释放同一块内存空间而提出的

传统的指针在申请完成后, 必须要调用 free 或 delete 来释放指针, 否则容易产生内存泄漏的问题

smart pointer 遵循 RAII 原则, 当 smart pointer 对象创建时, 即为该指针分配了相应的内存, 当对象销毁时, 析构函数会自动释放内存, 需要注意的是, 智能指针不能像普通指针那样支持加减运算(迭代操作)

智能指针的类型

  • unique_ptr: 独享所有权的智能指针, 资源智能被一个指针占有, 该指针不能拷贝构造和赋值, 但可以进行移动构造和移动赋值构造(调用 move() 函数), 即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象, 可以通过该方法进行赋值

alt text

  • shared_ptr: 与 unique_ptr 不同的是, shared_ptr 中的资源可以被多个指针共享, 但是多个指针指向同一个资源不能被释放多次, 因此使用计数机制表明资源被几个指针共享; 通过 use_count() 查看资源的所有者的个数, 可以通过 unique_ptr, weak_ptr 来构造, release 释放资源的所有权, 同时将计数减一, 当计数减为 0 时候,会自动释放内存空间, 从而避免了泄露 需要注意的是, shared_ptr 并不是线程安全的, 但 shared_ptr 的计数是原子操作实现的, 利用 atmoic CAS 指令实现 通过 shared_ptr 的内存模型, 当引用计数和 weak cout 同时为 0 时, shared_ptr 对象才会被最终释放掉 通过查看 shared_ptr 的源代码可以看到如下, shared_ptr 实际成员包含两个指针, 一个指向对象资源的指针 ptr, 另一个指向管理区域的指针 ___cntrl_, 具体 __cntrl_ 指针的区域包括 deleter, allocator, shared_ptr 对象的引用计数, weak_ptrs 的对象的引用计数

  • weak_ptr: 指向 shared_ptr 指向的对象, 能够解决 shared_ptr 带来的循环引用的问题, 与 shard_ptr 配合使用, 将 weak_ptr 转换为 shared_ptr 时, 虽然它能访问 shared_ptr 所指向的资源, 但却不享有资源的所有权, 不影响该资源的引用计数, 有可能资源已经被释放, 但 weak_ptr 仍然存在, shared_ptr 必须等待所有引用 weak_ptr 全部被释放才会进行释放, 因此, 每次访问资源时, 都需要判断资源是否有效.

什么是类

c++ 在visual studio和XCode上的差异

描述

Visual Studio 和 Xcode 在处理 C++ 方面的主要差异在于它们所使用的编译器、工具链以及它们各自的集成开发环境(IDE)特性。以下是一些关键的不同点:

1. 编译器

  • Visual Studio 使用的是 Microsoft 的 C++ 编译器(MSVC),该编译器有自己的语法扩展和特性,虽然它也支持大部分的 C++ 标准。
  • Xcode 使用的是 LLVM/Clang 编译器,这是一个开源编译器,其设计目标之一就是提供清晰的诊断信息,并且它对现代 C++ 标准的支持非常好。

2. IDE 特性

  • Visual Studio 提供了丰富的功能,如智能感知(IntelliSense)、实时模板编辑、重构工具等,这些都是针对微软的开发环境优化的。
  • Xcode 也有类似的功能,如自动补全(Code Completion)、实时错误提示(Live Issue Identification)等,这些功能针对 macOS 和 iOS 开发进行了优化。

3. API 和库支持

  • 在 Visual Studio 中,可以使用 Windows API 来开发应用程序,以及 Microsoft 提供的其他库。
  • 在 Xcode 中,更可能使用 Cocoa 或 Cocoa Touch(对于 iOS 开发)API,以及 BSD 和 POSIX 的相关库。

4. 构建系统

  • Visual Studio 使用 MSBuild 构建系统来管理项目。
  • Xcode 使用自己的构建系统,不过也可以配置使用外部构建工具如 CMake。

5. 调试工具

  • Visual Studio 自带强大的调试工具,如 WinDbg。
  • Xcode 使用 LLDB 作为调试器,LLDB 提供了现代化的命令行接口和 GUI 调试体验。

6. 用户界面

  • Visual Studio 的用户界面设计适合 Windows 用户,具有熟悉的 Windows 风格界面。
  • Xcode 的用户界面则更适合 macOS 用户,遵循苹果的设计指南。

7. 跨平台支持

  • 如果在 Visual Studio 中开发跨平台的 C++ 代码,可能会使用像 CMake 这样的工具来帮助管理跨平台的构建过程。
  • 在 Xcode 中,虽然主要面向 macOS 和 iOS 开发,但也可以通过配置支持跨平台的开发,尤其是在使用像 CMake 这样的工具时。

总结

尽管两者在很多方面有所不同,但在编写符合 C++ 标准委员会定义的标准 C++ 代码时,这些差异并不显著。如果目标是编写可移植的 C++ 代码,应该尽量避免使用特定于编译器的特性,并遵循 C++ 标准。此外,使用像 CMake 这样的跨平台构建工具可以在多个平台上保持一致性。

如何避免写 c 代码的时候 Unhandled Exception:Stack overflow

避免 C 程序出现堆栈溢出异常(Stack Overflow Exception)通常需要注意以下几点

  1. 优化递归算法:如果程序中使用了递归,确保递归深度可控,避免无限递归或递归层次过深。
  2. 减少局部变量的数量和大小:函数内部声明的局部变量会占用堆栈空间,特别是对于大型数组或结构体。优化代码,尽量减少或优化局部变量的使用。
  3. 动态内存分配:考虑使用动态内存分配函数(如 malloc()、calloc()、realloc())来管理大型数据结构,而不是将它们放在栈上。
  4. 避免过度递归:如果递归是必须的,确保递归调用的深度受到控制,并考虑是否可以改写为迭代方法。
  5. 增加堆栈空间限制:虽然不推荐频繁增加堆栈空间的大小,但是可以通过编译器或操作系统的设置,适当增加堆栈空间的限制。这种方法应谨慎使用,并仅当确实需要时才考虑。
  6. 使用静态分配:对于较小的数据结构或变量,优先使用静态分配(即在函数外部或静态存储区声明变量)而不是动态分配或放在堆栈上。
  7. 编译器优化:现代编译器通常会进行优化,包括函数调用和参数传递的优化。确保使用编译器的优化选项,以提高程序的效率和性能。
  8. 定期检查和测试:对于可能存在堆栈溢出风险的代码,定期进行检查和测试是很重要的,以发现潜在的问题并进行优化和改进。

左值和右值

stl

描述

STL(Standard Template Library,标准模板库)是 C++ 标准库的重要组成部分,提供了 高效且通用的数据结构和算法。

STL 的核心要素主要包括以下几个方面

  • 容器(Containers): 容器是用于存储和组织数据的数据结构。STL 提供了多种类型的 容器,每种容器都有其特定的特性和使用场景。
  • 迭代器(Iterators): 迭代器是用于遍历容器中元素的指针或指针类似的对 象。STL 提供了多种类型的迭代器,以适应不同容器的需求。
  • 算法(Algorithms): 算法是用于处理容器中数据的函数模板。STL 提供了大量的算法 ,涵盖了排序、查找、修改、数值计算等多种操作。
  • 函数对象(Function Objects): 函数对象是重载了函数调用运算符 operator() 的对 象,可以像普通函数一样调用。STL 提供了一些预定义的函数对象,用于算法中的比较和 操作
  • 适配器(Adapters): 适配器用于改变现有容器的行为,使其符合特定的需求。

迭代器相关

描述

容器相关

描述

stl 提供的数据类型

序列容器

序列容器存储元素的顺序集合,支持随机访问或顺序访问。

  • vector: array

    • 特点: 动态数组, 支持随机访问, 末尾插入和删除效率高
    • 使用场景:适用于需要频繁随机访问和在末尾进行插入和删除操作的场景。
      #include <vector>
      #include <iostream>
    
      int main() {
          std::vector<int> vec = {1, 2, 3, 4, 5};
          vec.push_back(6); // 在末尾添加元素
          std::cout << "Vector size: " << vec.size() << std::endl; // 输出: Vector size: 6
          return 0;
      }
    
  • list:

    • 特点: 双向链表,支持高效的插入和删除操作,但不支持随机访问。

    • 使用场景:适用于需要频繁在任意位置进行插入和删除操作的场景

      #include <list>
      #include <iostream>
      
      int main() {
          std::list<int> lst = {1, 2, 3, 4, 5};
          lst.push_front(0); // 在头部添加元素
          lst.push_back(6);  // 在末尾添加元素
          for (int val : lst) {
              std::cout << val << " "; // 输出: 0 1 2 3 4 5 6
          }
          return 0;
      }
      
  • deque:

    • 特点:双端队列,支持高效的前端和后端插入和删除操作,也支持随机访问

    • 使用场景:适用于需要在两端进行插入和删除操作的场景。

      #include <deque>
      #include <iostream>
      
      int main() {
          std::deque<int> deq = {1, 2, 3, 4, 5};
          deq.push_front(0); // 在头部添加元素
          deq.push_back(6);  // 在末尾添加元素
          for (int val : deq) {
              std::cout << val << " "; // 输出: 0 1 2 3 4 5 6
          }
          return 0;
      }
      

关联容器

关联容器存储键值对,支持高效的查找操作

  • set:

    • 特点:有序集合,不允许重复元素,支持高效的查找、插入和删除操作。

    • 使用场景:适用于需要唯一键值且有序存储的场景。

      #include <set>
      #include <iostream>
      
      int main() {
          std::set<int> s = {5, 3, 1, 4, 2};
          s.insert(6); // 插入元素
          for (int val : s) {
              std::cout << val << " "; // 输出: 1 2 3 4 5 6
          }
          return 0;
      }
      
  • map:

    • 特点:有序键值对集合,不允许重复键,支持高效的查找、插入和删除操作。
    • 使用场景:适用于需要唯一键值且有序存储键值对的场景。
      #include <map>
      #include <iostream>
    
      int main() {
          std::map<std::string, int> m = {{"apple", 1}, {"banana", 2}, {"cherry", 3}};
          m["date"] = 4; // 插入键值对
          for (const auto& pair : m) {
              std::cout << pair.first << ": " << pair.second << std::endl;
          }
          return 0;
      }
    
  • multiset: -特点:有序集合,允许重复元素,支持高效的查找、插入和删除操作。

    • 使用场景:适用于需要唯一键值但允许重复且有序存储的场景。
      #include <set>
      #include <iostream>
    
      int main() {
          std::multiset<int> ms = {5, 3, 1, 4, 2, 3};
          ms.insert(6); // 插入元素
          for (int val : ms) {
              std::cout << val << " "; // 输出: 1 2 3 3 4 5 6
          }
          return 0;
      }
    
  • multimap:

    • 特点:有序键值对集合,允许重复键,支持高效的查找、插入和删除操作。
    • 使用场景:适用于需要唯一键值但允许重复且有序存储键值对的场景
      #include <map>
      #include <iostream>
    
      int main() {
          std::multimap<std::string, int> mm = {{"apple", 1}, {"banana", 2}, {"apple", 3}};
          mm.insert({"cherry", 4}); // 插入键值对
          for (const auto& pair : mm) {
              std::cout << pair.first << ": " << pair.second << std::endl;
          }
          return 0;
      }
    

无序容器

无序容器存储键值对,支持高效的查找操作,但不保证元素的顺序。

  • unordered_set:

    • 特点:无序集合,不允许重复元素,支持高效的查找、插入和删除操作。
    • 使用场景:适用于需要唯一键值且不需要有序存储的场景。
      #include <unordered_set>
      #include <iostream>
    
      int main() {
          std::unordered_set<int> us = {5, 3, 1, 4, 2};
          us.insert(6); // 插入元素
          for (int val : us) {
              std::cout << val << " "; // 输出顺序不确定
          }
          return 0;
      }
    
  • unordered_map

    • 特点:无序键值对集合,不允许重复键,支持高效的查找、插入和删除操作。
    • 使用场景:适用于需要唯一键值且不需要有序存储键值对的场景。
      #include <unordered_map>
      #include <iostream>
    
      int main() {
          std::unordered_map<std::string, int> um = {{"apple", 1}, {"banana", 2}, {"cherry", 3}};
          um["date"] = 4; // 插入键值对
          for (const auto& pair : um) {
              std::cout << pair.first << ": " << pair.second << std::endl;
          }
          return 0;
      }
    
  • unordered_multiset

    • 特点:无序集合,允许重复元素,支持高效的查找、插入和删除操作。

    • 使用场景:适用于需要唯一键值但允许重复且不需要有序存储的场景

      #include <unordered_set>
      #include <iostream>
      
      int main() {
          std::unordered_multiset<int> ums = {5, 3, 1, 4, 2, 3};
          ums.insert(6); // 插入元素
          for (int val : ums) {
              std::cout << val << " "; // 输出顺序不确定
          }
          return 0;
      }
      
  • unordered_multimap

    • 特点:无序键值对集合,允许重复键,支持高效的查找、插入和删除操作。
    • 使用场景:适用于需要唯一键值但允许重复且不需要有序存储键值对的场景。
      #include <unordered_map>
      #include <iostream>
    
      int main() {
          std::unordered_multimap<std::string, int> umm = {{"apple", 1}, {"banana", 2}, {"apple", 3}};
          umm.insert({"cherry", 4}); // 插入键值对
          for (const auto& pair : umm) {
              std::cout << pair.first << ": " << pair.second << std::endl;
          }
          return 0;
      }
    

容器适配器

容器适配器基于其他容器构建,提供特定的功能。

  • stack

    • 特点:后进先出(LIFO)的数据结构,基于 deque、vector 或 list 实现。
    • 使用场景:适用于需要栈操作的场景。
      #include <stack>
      #include <iostream>
    
      int main() {
          std::stack<int> stk;
          stk.push(1);
          stk.push(2);
          stk.push(3);
          std::cout << "Top element: " << stk.top() << std::endl; // 输出: Top element: 3
          stk.pop();
          std::cout << "Top element after pop: " << stk.top() << std::endl; // 输出: Top element after pop: 2
          return 0;
      }
    
  • queue

    • 特点:先进先出(FIFO)的数据结构,基于 deque 或 list 实现。
    • 使用场景:适用于需要队列操作的场景。
      #include <queue>
      #include <iostream>
    
      int main() {
          std::queue<int> q;
          q.push(1);
          q.push(2);
          q.push(3);
          std::cout << "Front element: " << q.front() << std::endl; // 输出: Front element: 1
          q.pop();
          std::cout << "Front element after pop: " << q.front() << std::endl; // 输出: Front element after pop: 2
          return 0;
      }
    
  • priority_queue

    • 特点:最大堆(默认)或最小堆的数据结构,基于 vector 实现。
    • 使用场景:适用于需要优先级队列操作的场景。
      #include <queue>
      #include <iostream>
    
      int main() {
          std::priority_queue<int> pq;
          pq.push(3);
          pq.push(1);
          pq.push(2);
          std::cout << "Top element: " << pq.top() << std::endl; // 输出: Top element: 3
          pq.pop();
          std::cout << "Top element after pop: " << pq.top() << std::endl; // 输出: Top element after pop: 2
          return 0;
      }
    

stl 算法分析

描述

函数对象

描述

  • 比较函数对象: less, greater, equal_to, not_equal_to 等。
  • 算术函数对象: plus, minus, multiplies, divides, modulus, negate 等。
  • 逻辑函数对象: logical_and, logical_or, logical_not 等