文章

c++高频总结十

new可以重载吗,可以改写new函数吗

是的,C++ 中 newdelete 操作符是可以重载(overload)的。重载这些操作符允许程序员自定义内存分配和释放的行为。C++ 标准库的内存管理函数(如 mallocfree)仍然可以被调用,但通过重载,程序员可以插入自己的逻辑,比如自定义内存池或调试日志。


1. 重载 newdelete 的基本规则

全局重载 newdelete

全局重载会影响程序中所有调用 newdelete 的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <cstdlib> // 包含 malloc 和 free
using namespace std;

// 全局重载 new 操作符
void* operator new(size_t size) {
    cout << "Custom global new: allocating " << size << " bytes" << endl;
    return malloc(size);
}

// 全局重载 delete 操作符
void operator delete(void* ptr) noexcept {
    cout << "Custom global delete" << endl;
    free(ptr);
}

int main() {
    int* p = new int; // 使用重载的 new
    delete p;         // 使用重载的 delete
    return 0;
}

输出示例:

1
2
Custom global new: allocating 4 bytes
Custom global delete

类级别重载 newdelete

类级别的重载只影响该类对象的动态分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <cstdlib> // 包含 malloc 和 free
using namespace std;

class MyClass {
public:
    // 类级别重载 new
    void* operator new(size_t size) {
        cout << "Class-specific new: allocating " << size << " bytes" << endl;
        return malloc(size);
    }

    // 类级别重载 delete
    void operator delete(void* ptr) noexcept {
        cout << "Class-specific delete" << endl;
        free(ptr);
    }
};

int main() {
    MyClass* obj = new MyClass; // 使用类级别的 new
    delete obj;                // 使用类级别的 delete
    return 0;
}

输出示例:

1
2
Class-specific new: allocating 1 bytes
Class-specific delete

2. 常见的重载形式

普通 newdelete 重载

最常见的形式是重载参数为 size_tnew 和参数为 void*delete

1
2
void* operator new(size_t size);
void operator delete(void* ptr) noexcept;

带额外参数的 newdelete

可以为 newdelete 提供额外参数,适用于需要传递额外信息的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

// 带额外参数的 new
void* operator new(size_t size, const char* msg) {
    cout << "Custom new with message: " << msg << endl;
    return malloc(size);
}

// 普通 delete 重载
void operator delete(void* ptr) noexcept {
    free(ptr);
}

int main() {
    int* p = new ("Debug Info") int; // 传递额外的字符串参数
    delete p;
    return 0;
}

输出示例:

1
Custom new with message: Debug Info

3. 注意事项

  1. 必须返回有效的指针

    • new 必须返回一个有效的指针,通常通过 malloc 或其他内存分配函数实现。
    • 如果分配失败,应抛出 std::bad_alloc 异常,而不是返回 nullptr
  2. 匹配的 newdelete

    • 自定义的 new 必须与自定义的 delete 一起使用,确保释放的内存正确。
  3. 对齐问题

    • C++17 引入了对齐的概念,支持重载对齐版本的 newdelete
    1
    2
    
    void* operator new(size_t size, std::align_val_t alignment);
    void operator delete(void* ptr, std::align_val_t alignment) noexcept;
    
  4. 线程安全

    • 如果程序是多线程的,确保自定义的 newdelete 是线程安全的。

4. 重载 newdelete 的应用场景

  1. 内存池(Memory Pool)
    • 自定义 newdelete 以使用内存池,提高内存分配效率。
  2. 调试和日志
    • 重载 newdelete 打印日志,帮助定位内存分配和释放的问题。
  3. 对齐要求
    • 在 SIMD(单指令多数据)或硬件相关开发中,可以重载对齐版本的 newdelete
  4. 性能优化
    • 针对特定场景优化内存分配策略。

5. newmalloc 的对比

特性newmalloc
构造函数调用调用构造函数不调用构造函数
自定义重载支持通过 operator new 重载通过 malloc 的封装间接实现
额外参数支持额外参数不支持

6. 总结

  • 可以重载 newdelete,无论是全局还是类级别。
  • 通过重载可以实现自定义内存管理,如调试日志、内存池或对齐优化。
  • 需要注意分配失败的处理和 new/delete 配对使用。

C++中的map和unordered_map的区别和使用场景

mapunordered_map 是 C++ 标准库中常用的关联容器,它们主要的区别在于底层实现、性能特点和使用场景:


区别

特性mapunordered_map
底层实现基于 红黑树(有序二叉搜索树)基于 哈希表
键值存储顺序按键值有序存储(默认使用 < 比较)无序存储,顺序不可预测
查找/插入时间复杂度O(log n)平均 O(1),最坏 O(n)(发生哈希冲突时)
内存占用较少较多(由于需要存储哈希表和可能的冲突链)
自定义排序规则支持,通过提供比较函数不支持排序
迭代器失效插入/删除操作后,迭代器依然有效插入/删除可能导致迭代器失效

使用场景

使用 map 的场景

  1. 需要排序的数据 如果需要按键值自动排序存储,可以直接使用 map,无需额外的排序操作。

    1
    2
    3
    4
    5
    6
    7
    8
    
    std::map<int, std::string> ordered_map;
    ordered_map[2] = "B";
    ordered_map[1] = "A";
    ordered_map[3] = "C";
    // 输出有序:1->A, 2->B, 3->C
    for (const auto& [key, value] : ordered_map) {
        std::cout << key << " -> " << value << std::endl;
    }
    
  2. 需要范围查询的数据 map 提供高效的范围查询功能,例如 lower_boundupper_bound,这在需要查找某个范围内的键值时非常实用。

    1
    2
    
    auto it = my_map.lower_bound(10); // 返回第一个 >= 10 的元素
    auto it2 = my_map.upper_bound(20); // 返回第一个 > 20 的元素
    
  3. 内存有限的场景 由于 map 内存占用较少,更适合内存敏感的场景。

使用 unordered_map 的场景

  1. 高效查找/插入操作 如果数据量大,且不需要按键值排序,unordered_map 在大多数情况下比 map 速度更快。

    1
    2
    3
    4
    
    std::unordered_map<int, std::string> hash_map;
    hash_map[2] = "B";
    hash_map[1] = "A";
    hash_map[3] = "C";
    
  2. 无顺序要求 如果数据不需要按顺序访问,unordered_map 是更好的选择。

  3. 需要常量时间查找的场景 在频繁查找操作的场景下(例如缓存、计数等),unordered_map 是性能最佳的选择。

  4. 自定义哈希函数的场景 当键是复杂类型(如结构体)时,可以通过提供自定义哈希函数来扩展 unordered_map 的功能。

    1
    2
    3
    4
    5
    6
    7
    8
    
    struct Key {
        int x, y;
        bool operator==(const Key& other) const { return x == other.x && y == other.y; }
    };
    struct Hash {
        size_t operator()(const Key& key) const { return std::hash<int>()(key.x) ^ std::hash<int>()(key.y); }
    };
    std::unordered_map<Key, std::string, Hash> custom_hash_map;
    

总结

  • 如果需要 按顺序存储键值执行范围查询,使用 map
  • 如果需要 高效查找和插入,且对顺序没有要求,使用 unordered_map

根据具体的性能需求和应用场景选择合适的容器可以显著提高程序效率和可读性。

他们是线程安全的吗

std::mapstd::unordered_map 不是线程安全的。在多线程环境中,以下几点需要特别注意:


线程安全性分析

  1. 读操作 如果多个线程同时执行 只读操作,例如遍历或查找操作(findcount 等),是线程安全的,前提是没有其他线程对容器进行修改。
  2. 写操作 如果有线程对容器进行写操作(例如 inserterase、修改键值对),则所有其他线程(无论是读还是写)都会面临竞态条件,可能导致未定义行为。
  3. 并发读写 并发读写操作(例如一个线程读取,一个线程插入/删除)也不是线程安全的,需要额外的同步机制(如互斥锁)。
  4. 迭代器安全性 写操作(如 inserterase)可能使迭代器失效,导致其他线程使用的迭代器不安全。

如何实现线程安全?

在多线程环境下,如果需要使用 std::mapstd::unordered_map,可以采用以下方法确保线程安全:

1. 使用互斥锁保护容器

使用互斥锁(如 std::mutex)对容器的操作进行同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <map>
#include <mutex>
#include <thread>

std::map<int, int> my_map;
std::mutex map_mutex;

void thread_safe_insert(int key, int value) {
    std::lock_guard<std::mutex> lock(map_mutex);
    my_map[key] = value;
}

void thread_safe_read(int key) {
    std::lock_guard<std::mutex> lock(map_mutex);
    if (my_map.find(key) != my_map.end()) {
        std::cout << "Key: " << key << ", Value: " << my_map[key] << std::endl;
    }
}

2. 使用读写锁

如果读操作远多于写操作,可以使用读写锁(如 std::shared_mutex)提高性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <map>
#include <shared_mutex>

std::map<int, int> my_map;
std::shared_mutex map_mutex;

void thread_safe_write(int key, int value) {
    std::unique_lock<std::shared_mutex> lock(map_mutex);
    my_map[key] = value;
}

void thread_safe_read(int key) {
    std::shared_lock<std::shared_mutex> lock(map_mutex);
    if (my_map.find(key) != my_map.end()) {
        std::cout << "Key: " << key << ", Value: " << my_map[key] << std::endl;
    }
}

3. 使用并发容器(如 tbb::concurrent_hash_map

如果需要更高效的并发操作,可以使用 Intel TBB 提供的并发容器,例如 tbb::concurrent_hash_map,避免手动加锁的复杂性。

4. 自定义分区锁

如果容器较大,且读写冲突发生在不同键上,可以将容器划分为多个部分,每部分用一个锁,减少锁争用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unordered_map>
#include <mutex>

std::unordered_map<int, int> buckets[10];
std::mutex bucket_mutexes[10];

void partitioned_write(int key, int value) {
    int bucket_index = key % 10;
    std::lock_guard<std::mutex> lock(bucket_mutexes[bucket_index]);
    buckets[bucket_index][key] = value;
}

void partitioned_read(int key) {
    int bucket_index = key % 10;
    std::lock_guard<std::mutex> lock(bucket_mutexes[bucket_index]);
    if (buckets[bucket_index].find(key) != buckets[bucket_index].end()) {
        std::cout << "Key: " << key << ", Value: " << buckets[bucket_index][key] << std::endl;
    }
}

注意事项

  • 无锁方案:如果性能需求非常高,且可以容忍一定的复杂性,可以考虑无锁编程(如原子操作或自旋锁),但实现起来较复杂。
  • 多线程适配:C++ 标准库的容器(如 std::mapstd::unordered_map)设计目标是单线程优先,在多线程环境中,手动加锁或使用专门的并发容器更合适。

结论

std::mapstd::unordered_map 默认情况下不是线程安全的。需要根据实际需求,通过加锁或使用并发容器等手段来实现线程安全操作。

C++标准库里优先队列是怎么实现的?

C++ 标准库中的优先队列是通过 std::priority_queue 实现的,它基于 堆(Heap) 数据结构,通常使用 最大堆最小堆 来组织元素。


实现原理

  1. 底层数据结构 std::priority_queue 底层依赖 std::vector 作为存储容器。堆操作(如插入、删除)直接在 std::vector 上执行,通过维护堆的性质来确保优先队列的功能。
  2. 堆性质
    • 最大堆:堆中每个节点的值都大于或等于其子节点的值,队首元素(堆顶)是最大值。
    • 最小堆:堆中每个节点的值都小于或等于其子节点的值,队首元素(堆顶)是最小值。
  3. 主要算法
    • 插入元素:新元素插入堆尾,然后通过 上浮(sift-up) 操作恢复堆的性质,时间复杂度为 O(log⁡n)O(\log n)。
    • 删除队首元素:将堆顶元素移除,用堆尾元素替换堆顶,然后通过 下沉(sift-down) 操作恢复堆的性质,时间复杂度为 O(log⁡n)。
  4. 使用比较器
    • 默认情况下,std::priority_queue 是一个最大堆,比较器使用 std::less<T>
    • 可以通过自定义比较器来实现最小堆或其他优先级规则。

优先队列的定义和用法

1. 默认最大堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <queue>
#include <vector>
#include <iostream>

int main() {
    std::priority_queue<int> pq;
    pq.push(10);
    pq.push(20);
    pq.push(5);

    while (!pq.empty()) {
        std::cout << pq.top() << " "; // 输出 20 10 5
        pq.pop();
    }
    return 0;
}

2. 最小堆

通过自定义比较器实现最小堆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <queue>
#include <vector>
#include <iostream>

int main() {
    std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap;
    min_heap.push(10);
    min_heap.push(20);
    min_heap.push(5);

    while (!min_heap.empty()) {
        std::cout << min_heap.top() << " "; // 输出 5 10 20
        min_heap.pop();
    }
    return 0;
}

3. 自定义比较器

使用结构体或 lambda 表达式实现自定义优先级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <queue>
#include <vector>
#include <iostream>

struct Task {
    int priority;
    std::string name;
};

// 自定义比较器
struct Compare {
    bool operator()(const Task& t1, const Task& t2) {
        return t1.priority > t2.priority; // 按优先级升序排列
    }
};

int main() {
    std::priority_queue<Task, std::vector<Task>, Compare> pq;
    pq.push({1, "Low priority"});
    pq.push({3, "High priority"});
    pq.push({2, "Medium priority"});

    while (!pq.empty()) {
        auto t = pq.top();
        std::cout << t.name << " "; // 输出:Low priority Medium priority High priority
        pq.pop();
    }
    return 0;
}

复杂度分析

操作时间复杂度备注
插入元素O(log⁡n)需要调整堆结构
访问最大/最小元素O(1)直接访问堆顶
删除最大/最小元素O(log⁡n)删除堆顶元素后,重新调整堆结构

总结

  • 底层实现:基于堆结构,通常使用 std::vector 存储元素。
  • 默认性质std::priority_queue 默认是最大堆。
  • 灵活性:通过模板参数支持自定义比较规则,从而实现最小堆或其他优先级规则。
  • 适用场景:用于需要动态维护最大值或最小值的场景,如任务调度、图算法中的优先级管理(如 Dijkstra 算法)。

gcc编译的过程

GCC(GNU Compiler Collection)是一个强大的编译器工具链,用于编译 C、C++ 等多种语言。它的编译过程通常分为以下四个主要阶段:


1. 预处理(Preprocessing)

预处理器的任务是处理源代码中的预处理指令,如宏定义(#define)、头文件包含(#include)以及条件编译(#ifdef 等)。

  • 命令gcc -E source.c -o source.i
  • 输入:原始 C 源文件(source.c
  • 输出:预处理后的文件(source.i

具体操作:

  • 展开宏(#define
  • 处理头文件(#include),将头文件内容插入到代码中
  • 处理条件编译指令(如 #ifdef#ifndef
  • 删除注释

示例代码

1
2
3
4
5
#include <stdio.h>
#define PI 3.14
int main() {
    printf("PI: %f\n", PI);
}

预处理结果(source.i):

1
2
3
int main() {
    printf("PI: %f\n", 3.14);
}

2. 编译(Compilation)

编译器将预处理后的代码(source.i)转换为汇编代码(Assembly code)。

  • 命令gcc -S source.i -o source.s
  • 输入:预处理后的文件(source.i
  • 输出:汇编代码文件(source.s

具体操作:

  • 分析语法和语义
  • 进行优化(如常量折叠、无用代码删除等)
  • 生成对应的汇编代码

示例汇编代码(source.s):

.LC0:
    .string "PI: %f\n"
main:
    pushq   %rbp
    movq    %rsp, %rbp
    movsd   .LC1(%rip), %xmm0
    movl    $.LC0, %edi
    movl    $1, %eax
    call    printf
    movl    $0, %eax
    popq    %rbp
    ret
.LC1:
    .double 3.14

3. 汇编(Assembly)

汇编器将汇编代码(source.s)转换为机器代码,生成目标文件(Object file)。

  • 命令gcc -c source.s -o source.o
  • 输入:汇编代码文件(source.s
  • 输出:目标文件(source.o

具体操作:

  • 将汇编代码转换为二进制机器指令
  • 生成可重定位的目标文件(包括符号表、段信息等)

4. 链接(Linking)

链接器将目标文件(source.o)与库文件(如标准库)链接在一起,生成最终的可执行文件。

  • 命令gcc source.o -o executable
  • 输入:目标文件(source.o
  • 输出:可执行文件(executable

具体操作:

  • 合并多个目标文件
  • 解析符号(如函数调用、全局变量)
  • 加载库文件(静态库 .a 或动态库 .so
  • 生成最终的可执行文件

完整编译命令

通常,使用以下命令可以直接从源文件生成可执行文件:

1
gcc source.c -o executable

这将隐式完成所有阶段:预处理、编译、汇编和链接。


GCC 编译流程总结

阶段命令输入文件输出文件
预处理gcc -E source.c -o source.i.c 源文件预处理后的 .i 文件
编译gcc -S source.i -o source.s预处理后的 .i 文件汇编代码 .s 文件
汇编gcc -c source.s -o source.o汇编代码 .s 文件目标文件 .o
链接gcc source.o -o executable目标文件 .o可执行文件

调试和优化选项

  • 调试选项
    • -g:生成调试信息,用于调试器(如 GDB)。
    • 示例:gcc -g source.c -o executable
  • 优化选项
    • -O0:无优化(默认)
    • -O1:基本优化
    • -O2:较高优化
    • -O3:最高优化
    • 示例:gcc -O2 source.c -o executable

高级用法

  • 查看每个阶段的输出:使用 -save-temps 保存所有中间文件。

    1
    
    gcc -save-temps source.c
    

    这会生成 source.isource.ssource.o 等文件。

  • 动态链接库:

    1
    2
    
    gcc source.c -o executable -lm      # 链接数学库
    gcc source.c -o executable -L. -lmycustomlib  # 链接自定义库
    

通过理解 GCC 的编译过程,可以更好地分析和优化 C/C++ 程序的构建过程。

本文由作者按照 CC BY 4.0 进行授权