STL
c++遍历容器(vector、list、set、map_vector、string、array、list、map、set等标准库的使用和迭代器遍历 练习题-CSDN博客
c++中的deque容器_c++deque容器-CSDN博客
priority_queue 优先队列(Priority Queue)-CSDN博客
C++中的 string 类_c++中string类-CSDN博客
STL- 函数对象
简单来说,谓词就是一个"做判断"的工具 。它接收数据,然后告诉你"是"还是"否"(
true或false)。
- 仿函数(函数对象) :本质上是一个重载了
()运算符的类对象。- 谓词 :特指那些返回
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, 闭包是什么?
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个参数,以此类推。
- 固定参数(部分应用)
这是
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; }
- 调整参数顺序
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; }
- 绑定成员函数
在 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::bindLambda 表达式 可读性 较差,需要理解占位符 _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 是 C/C++ 中一个极易被误解的关键字。它的核心作用只有一个:禁止编译器优化。
它告诉编译器:"这个变量随时可能被程序之外的因素(如硬件、中断、其他线程)修改,请不要自作聪明地把它缓存到寄存器里,也不要省略读取指令,每次必须直接从内存地址读取最新的值。"
核心误区:它不是为多线程设计的!
在深入之前,必须先纠正一个最大的误区:
volatile不能保证原子性 (例如i++依然不是原子的)。volatile不能保证线程安全。- 在多线程编程中,请使用
std::atomic或互斥锁(std::mutex)。
volatile 的真正战场是嵌入式硬件交互 和底层系统编程。
struct,union,Class,bitfield各自的作用和区别
struct,union,Class,bitfield各自的作用和区别-CSDN博客
内存资源及管理
内存被分成五个区:栈、堆、静态存储区、常量区、代码区。
new,delete与malloc,free
new/delete和malloc/free是 C++ 中管理动态内存的两种方式。虽然它们都用于在堆上申请和释放内存,但设计理念、功能特性和底层机制有着本质的区别。简单来说,
malloc/free是 C 语言的遗产,负责管理原始内存;而new/delete是 C++ 的现代化方式,负责管理对象的生命周期。
特性 malloc/freenew/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的执行流程
- 调用
operator new函数分配足够大小的原始内存(底层调用malloc)。- 在这块内存上调用对象的构造函数进行初始化。
delete的执行流程
- 在对象上调用析构函数,清理资源。
- 调用
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 的数据给覆盖了。
为什么函数会"不可重入"?
一个函数变成"不可重入",通常是因为它依赖了全局共享的资源来保存状态。当多个执行流(如中断、信号、多线程)同时访问这些资源时,就会发生冲突。
主要原因包括:
- 使用了静态(static)变量或全局变量:这是最常见的原因。因为这些变量的内存是共享的,第二次调用会覆盖第一次调用的数据。
- 调用了标准 I/O 函数 :如
printf,它们内部通常使用全局缓冲区。- 调用了内存分配函数 :如
malloc/free,它们管理的是全局堆内存。- 返回了静态数据的指针:多个调用者会拿到同一块内存地址。
在 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 时间戳转字符串。结合了 localtime和asctime。❌ 否 同样返回指向内部静态缓冲区的指针。应使用 ctime_r。gethostbyname 域名转IP。根据域名查询 IP 地址。 ❌ 否 返回指向静态数据区的指针。应使用 gethostbyname_r。inet_ntoa IP格式转换。将二进制 IP 转为点分十进制字符串 (x.x.x.x)。 ❌ 否 返回指向内部静态缓冲区的指针,连续调用会覆盖上一次的结果。 fork 创建子进程。复制当前进程。 ✅ 是 系统调用,通常是异步信号安全的。 read 读取文件/流。从文件描述符读取数据。 ✅ 是 系统调用,通常是异步信号安全的。 strcpy 字符串复制。 ✅ 是 只操作传入的参数(指针)和栈内存,不使用全局变量,是可重入的。