c++知识点4

STL

C++泛型编程与STL深入解析-CSDN博客

STL标准模板库_stl标准库-CSDN博客

c++容器的常用函数_c++容器常用函数-CSDN博客

c++遍历容器(vector、list、set、map_vector、string、array、list、map、set等标准库的使用和迭代器遍历 练习题-CSDN博客

c++中的deque容器_c++deque容器-CSDN博客

c++中的vector容器-CSDN博客

priority_queue 优先队列(Priority Queue)-CSDN博客

C++中的 string 类_c++中string类-CSDN博客

STL- 函数对象

简单来说,谓词就是一个"做判断"的工具 。它接收数据,然后告诉你"是"还是"否"(truefalse)。

  • 仿函数(函数对象) :本质上是一个重载了 () 运算符的类对象。
  • 谓词 :特指那些返回 bool 类型的仿函数(或普通函数)。

概念

  • 返回bool类型的仿函数称为谓词
  • 如果operator()接受一个参数,那么叫做一元谓词
  • 如果operator()接受两个参数,那么叫做二元谓词
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm> // sort 需要这个头文件

using namespace std;

// 1. 定义二元谓词(仿函数)
class MyCompare {
public:
    // operator() 接收两个参数
    bool operator()(int val1, int val2) {
        return val1 > val2; // 如果前者大于后者,返回 true
    }
};

int main() {
    vector<int> v = {10, 30, 20, 50};
    
    // 2. 使用二元谓词
    // sort 会根据 MyCompare 的返回值来决定元素的顺序
    sort(v.begin(), v.end(), MyCompare());
    
    for(int i : v) {
        cout << i << " "; // 输出: 50 30 20 10
    }
    cout << endl;
    
    return 0;
}

内建函数对象意义

C++ STL 内建函数对象(也称为仿函数)

它们被定义在 <functional> 头文件中,使得开发者无需为基本操作重复编写仿函数,可以直接将其作为参数传递给 STL 算法,从而让代码更加简洁、清晰和通用。

STL 内建函数对象主要分为三大类,涵盖了绝大多数基础运算需求:

1. 算术仿函数

用于执行基本的数学运算。

  • plus<T>:加法 (a + b)
  • minus<T>:减法 (a - b)
  • multiplies<T>:乘法 (a * b)
  • divides<T>:除法 (a / b)
  • modulus<T>:取模 (a % b)
  • negate<T>:取负 (-a)

2. 关系仿函数

用于执行数值或对象间的比较。

  • equal_to<T>:等于 (a == b)
  • not_equal_to<T>:不等于 (a != b)
  • greater<T>:大于 (a > b)
  • less<T>:小于 (a < b)
  • greater_equal<T>:大于等于 (a >= b)
  • less_equal<T>:小于等于 (a <= b)

3. 逻辑仿函数

用于执行布尔逻辑运算。

  • logical_and<T>:逻辑与 (a && b)
  • logical_or<T>:逻辑或 (a || b)
  • logical_not<T>:逻辑非 (!a)

STL- 常用算法

  • 算法主要是由头文件<algorithm> <functional> <numeric>组成。
  • <algorithm>是所有STL头文件中最大的一个,范围涉及到比较、 交换、查找、遍历操作、复制、修改等等
  • <numeric>体积很小,只包括几个在序列上面进行简单数学运算的模板函数
  • <functional>定义了一些模板类,用以声明函数对象。

Lambda, 闭包是什么?

Lambda, 闭包是什么?-CSDN博客

std::bind

std::bind 是 C++11 引入的一个函数适配器 ,定义在 <functional> 头文件中。

简单来说,它的作用是**"定制"或"改造"一个现有的函数** 。它能把一个函数的某些参数提前固定下来 ,或者调整参数的顺序,从而生成一个新的、可调用的对象。

你可以把它想象成一个函数模板 :原函数是一个需要3个原料的机器,你用 std::bind 预先塞进去1个固定原料,剩下的2个位置留空(用占位符表示)。这样你就得到了一个只需要2个原料的新机器。

核心机制:占位符 (std::placeholders)

要使用 std::bind,必须先了解它的搭档------占位符

它们位于 std::placeholders 命名空间下,通常写作 _1, _2, _3...

  • _1 代表新生成的函数被调用时的第1个参数
  • _2 代表第2个参数,以此类推。
  1. 固定参数(部分应用)

这是 std::bind 最常见的用法。当你有一个函数,但只想固定它的某些参数,生成一个新函数时,就非常有用。

cpp 复制代码
#include <iostream>
#include <functional> // std::bind 和 std::placeholders

using namespace std::placeholders; // 为了使用 _1, _2

// 一个普通的加法函数
int add(int a, int b, int c) {
    return a + b + c;
}

int main() {
    // 1. 固定第一个参数为 10
    // 新函数 add_10 只需要接收2个参数,它们会依次填入 _1 和 _2 的位置
    auto add_10 = std::bind(add, 10, _1, _2);
    
    std::cout << add_10(20, 30) << std::endl; // 输出: 60 (10 + 20 + 30)

    // 2. 固定第一个和第三个参数
    auto add_10_30 = std::bind(add, 10, _1, 30);
    
    std::cout << add_10_30(5) << std::endl; // 输出: 45 (10 + 5 + 30)
    
    return 0;
}
  1. 调整参数顺序

std::bind 可以通过占位符灵活地改变原函数参数的接收顺序。

cpp 复制代码
// 一个打印函数
void print(int a, int b) {
    std::cout << "a=" << a << ", b=" << b << std::endl;
}

int main() {
    // 交换参数顺序
    // 新函数接收的第一个参数 (_1) 会被传给 print 的第二个参数 (b)
    // 新函数接收的第二个参数 (_2) 会被传给 print 的第一个参数 (a)
    auto print_swapped = std::bind(print, _2, _1);
    
    print_swapped(10, 20); // 输出: a=20, b=10
    
    return 0;
}
  1. 绑定成员函数

在 C++11 时代,std::bind 常被用来将类的成员函数转换为普通的可调用对象,尤其适合用作回调函数。

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

using namespace std::placeholders;

class Calculator {
public:
    int multiply(int x, int y) {
        return x * y;
    }
};

int main() {
    Calculator calc;
    
    // 绑定成员函数
    // 第一个参数是成员函数指针
    // 第二个参数是对象指针或引用 (作为 this 指针)
    // 后面的 _1, _2 是将来调用时传入的参数
    auto mult = std::bind(&Calculator::multiply, &calc, _1, _2);
    
    std::cout << mult(5, 6) << std::endl; // 输出: 30
    
    return 0;
}

虽然 std::bind 功能强大,但在现代 C++(C++11 及以后)中,Lambda 表达式通常被认为是更优的选择

特性 std::bind Lambda 表达式
可读性 较差,需要理解占位符 _1, _2 的含义 极佳,代码逻辑一目了然
灵活性 功能强大但语法晦涩 灵活且语义清晰
性能 可能产生额外的开销 编译器更容易优化,性能通常更好

用 Lambda 改写上面的例子:

cpp 复制代码
// 固定参数
auto add_10 = [](int b, int c) { return 10 + b + c; };

// 调整顺序
auto print_swapped = [](int a, int b) { print(b, a); };

// 绑定成员函数
auto mult = [&calc](int x, int y) { return calc.multiply(x, y); };

锁的机制及管理

volatile关键字 作用?

volatile关键字-CSDN博客

volatile 是 C/C++ 中一个极易被误解的关键字。它的核心作用只有一个:禁止编译器优化

它告诉编译器:"这个变量随时可能被程序之外的因素(如硬件、中断、其他线程)修改,请不要自作聪明地把它缓存到寄存器里,也不要省略读取指令,每次必须直接从内存地址读取最新的值。"

核心误区:它不是为多线程设计的!

在深入之前,必须先纠正一个最大的误区:

  • volatile 不能保证原子性 (例如 i++ 依然不是原子的)。
  • volatile 不能保证线程安全
  • 在多线程编程中,请使用 std::atomic 或互斥锁(std::mutex)。

volatile 的真正战场是嵌入式硬件交互底层系统编程

struct,union,Class,bitfield各自的作用和区别

struct,union,Class,bitfield各自的作用和区别-CSDN博客

内存资源及管理

内存被分成五个区:栈、堆、静态存储区、常量区、代码区。

new,delete与malloc,free

new/deletemalloc/free 是 C++ 中管理动态内存的两种方式。虽然它们都用于在堆上申请和释放内存,但设计理念、功能特性和底层机制有着本质的区别。

简单来说,malloc/free 是 C 语言的遗产,负责管理原始内存;而 new/delete 是 C++ 的现代化方式,负责管理对象的生命周期

特性 malloc / free new / delete
本质 C 标准库函数 C++ 运算符
构造/析构 ❌ 不会调用 ✅ 自动调用
类型安全 ❌ 返回 void*,需强转 ✅ 返回具体类型指针
内存大小 需手动计算字节数 编译器自动计算
失败处理 返回 NULL 抛出 std::bad_alloc 异常
数组支持 不支持,需手动计算 有专门的 new[] / delete[]

本质区别:函数 vs 运算符

  • malloc/free :它们是 <cstdlib> 头文件中声明的普通函数。编译器并不知道它们的特殊含义,只是像调用其他函数一样调用它们。
  • new/delete :它们是 C++ 的内置运算符,编译器会为其生成特殊的代码。new 的底层通常会调用 operator new 函数(它内部封装了 malloc),delete 则会调用 operator delete 函数(内部封装了 free)。

核心差异:对象生命周期管理

这是两者最根本的区别。new/delete 不仅仅是分配内存,它们还负责对象的完整生命周期。

new 的执行流程

  1. 调用 operator new 函数分配足够大小的原始内存(底层调用 malloc)。
  2. 在这块内存上调用对象的构造函数进行初始化。

delete 的执行流程

  1. 在对象上调用析构函数,清理资源。
  2. 调用 operator delete 函数释放内存(底层调用 free)。

malloc/free 只负责纯粹的内存分配与释放,完全不关心内存里存放的是什么,因此不会调用任何构造或析构函数。

cpp 复制代码
#include <iostream>
#include <cstdlib> // malloc, free

class MyClass {
public:
    MyClass() { std::cout << "Constructor called" << std::endl; }
    ~MyClass() { std::cout << "Destructor called" << std::endl; }
};

int main() {
    std::cout << "--- Using malloc/free ---" << std::endl;
    // 1. 只分配内存,不调用构造函数
    MyClass* obj1 = (MyClass*)malloc(sizeof(MyClass));
    // 2. 只释放内存,不调用析构函数
    free(obj1);

    std::cout << "\n--- Using new/delete ---" << std::endl;
    // 1. 分配内存并调用构造函数
    MyClass* obj2 = new MyClass();
    // 2. 调用析构函数并释放内存
    delete obj2;

    return 0;
}

输出结果:

cpp 复制代码
--- Using malloc/free ---

--- Using new/delete ---
Constructor called
Destructor called

什么是不可重入函数?有哪些

不可重入函数(Non-reentrant Function) 是指那些在执行过程中如果被中断或再次调用,会导致数据错误或程序崩溃的函数。

简单来说,这类函数"记性不好"且"独占资源"。如果它在处理任务 A 时被打断,转而去处理任务 B(恰好也是调用这个函数),它可能会忘记任务 A 做到哪一步了,或者把任务 A 的数据给覆盖了。

为什么函数会"不可重入"?

一个函数变成"不可重入",通常是因为它依赖了全局共享的资源来保存状态。当多个执行流(如中断、信号、多线程)同时访问这些资源时,就会发生冲突。

主要原因包括:

  1. 使用了静态(static)变量或全局变量:这是最常见的原因。因为这些变量的内存是共享的,第二次调用会覆盖第一次调用的数据。
  2. 调用了标准 I/O 函数 :如 printf,它们内部通常使用全局缓冲区。
  3. 调用了内存分配函数 :如 malloc/free,它们管理的是全局堆内存。
  4. 返回了静态数据的指针:多个调用者会拿到同一块内存地址。

在 C/C++ 标准库和系统调用中,有很多经典的不可重入函数。如果你在中断服务程序(ISR)信号处理函数或多线程环境中使用它们,必须格外小心。

函数类别 典型函数 为什么不可重入? 安全替代方案 (可重入版本)
字符串处理 strtok() 内部使用静态指针记录下一次分割的位置。 strtok_r() (Linux) strtok_s() (Windows)
随机数生成 rand() 内部使用静态变量保存种子状态。 rand_r() (需手动传入种子)
时间获取 localtime() 返回指向静态结构体的指针。 localtime_r()
输入输出 printf() / scanf() 内部使用全局锁和缓冲区。 底层系统调用 (如 write) 或使用局部缓冲区的封装
内存管理 malloc() / free() 操作全局堆链表,若被打断可能导致堆损坏。 嵌入式中常用内存池替代
网络/文件 gethostbyname() 返回静态缓冲区。 gethostbyname_r()
函数名 主要作用 是否可重入 原因与说明
strtok 字符串分割。将字符串按分隔符切分。 ❌ 否 内部使用静态变量保存下一次分割的位置(上下文),多线程或中断调用会破坏状态。应使用 strtok_r
strerror 错误码转字符串。将 errno 转换为描述文本。 ❌ 否 返回指向内部静态缓冲区的指针,多次调用会覆盖内容。应使用 strerror_r
setlocale 设置区域信息。控制数字、时间格式等。 ❌ 否 修改的是全局状态,且通常没有锁保护,并发调用会导致数据损坏。
tmpnam 生成临时文件名。 ❌ 否 返回指向内部静态缓冲区的指针。
socket 创建网络套接字。用于网络通信。 ✅ 是 属于异步信号安全的系统调用,不依赖共享的全局状态。
gethostbyaddr IP转域名。根据 IP 地址查询主机信息。 ❌ 否 返回指向静态数据区的指针。应使用 gethostbyaddr_r
srand 设置随机数种子。 ❌ 否 修改全局状态(种子值),与 rand() 配合使用时是非线程安全的。
rand 生成伪随机数。 ❌ 否 读取并修改全局状态(当前种子),并发调用会产生重复序列。应使用 rand_r
getenv 获取环境变量。 ⚠️ 视情况 在大多数现代实现(如 glibc)中通过加锁实现线程安全,但在严格定义的"可重入"标准下(如信号处理中),它可能不安全。
asctime 时间转字符串。将 tm 结构转为 "Wed Jan 02..." 格式。 ❌ 否 返回指向内部静态缓冲区的指针。应使用 asctime_r
ctime 时间戳转字符串。结合了 localtimeasctime ❌ 否 同样返回指向内部静态缓冲区的指针。应使用 ctime_r
gethostbyname 域名转IP。根据域名查询 IP 地址。 ❌ 否 返回指向静态数据区的指针。应使用 gethostbyname_r
inet_ntoa IP格式转换。将二进制 IP 转为点分十进制字符串 (x.x.x.x)。 ❌ 否 返回指向内部静态缓冲区的指针,连续调用会覆盖上一次的结果。
fork 创建子进程。复制当前进程。 ✅ 是 系统调用,通常是异步信号安全的。
read 读取文件/流。从文件描述符读取数据。 ✅ 是 系统调用,通常是异步信号安全的。
strcpy 字符串复制。 ✅ 是 只操作传入的参数(指针)和栈内存,不使用全局变量,是可重入的。
相关推荐
墨染千千秋1 小时前
C++输入输出全解
c++
云qq1 小时前
C++ 原子操作
开发语言·c++·算法
Aurorar0rua1 小时前
CS50 x 2024 Notes C - 08
c语言·开发语言·学习方法
froginwe111 小时前
SQL GROUP BY 详解
开发语言
A charmer1 小时前
第一章:基础语法破冰|从 C++ 无缝切换 OC 语法
c++·objective-c
wangl_922 小时前
C#性能优化完全指南 - 从原理到实践
开发语言·性能优化·c#·.net·.netcore·visual studio
xyq20242 小时前
Redis 哈希(Hash)
开发语言
fffzd2 小时前
C++入门(一)
开发语言·c++·命名空间·输入输出·缺省参数
小妖同学学AI2 小时前
架构图即代码:GitHub星标41.9k的Diagrams,用Python解放你的画图生产力
开发语言·python·github