文章

c++高频总结三

一个对象=另一个对象会发生什么

在 C++ 中,当一个对象被赋值给另一个对象(即使用赋值运算符 =)时,具体会发生什么取决于对象的类型以及是否为赋值操作定义了特殊行为。

1. 默认的赋值行为(浅拷贝)

如果类没有明确定义赋值运算符,编译器会生成一个默认的赋值运算符。默认的赋值操作会逐个成员地复制(浅拷贝)对象的所有非静态数据成员。

示例:

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

class MyClass {
public:
    int a;
    std::string b;
};

int main() {
    MyClass obj1;
    obj1.a = 10;
    obj1.b = "Hello";

    MyClass obj2;
    obj2 = obj1; // 默认赋值操作
    std::cout << obj2.a << ", " << obj2.b << std::endl; // 输出: 10, Hello
    return 0;
}

2. 用户定义的赋值运算符

如果类定义了赋值运算符,则赋值操作会执行用户定义的逻辑。这在需要深拷贝或特殊操作时非常有用。

示例:

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
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <cstring>

class MyClass {
private:
    char* data;
public:
    MyClass(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }

    // 用户定义赋值运算符
    MyClass& operator=(const MyClass& other) {
        if (this == &other) return *this; // 自赋值保护

        delete[] data; // 释放旧内存

        data = new char[strlen(other.data) + 1]; // 分配新内存
        strcpy(data, other.data); // 拷贝数据

        return *this; // 返回 *this
    }

    void print() const {
        std::cout << data << std::endl;
    }

    ~MyClass() {
        delete[] data;
    }
};

int main() {
    MyClass obj1("Hello");
    MyClass obj2("World");
    obj2 = obj1; // 调用用户定义的赋值运算符
    obj2.print(); // 输出: Hello
    return 0;
}

3. 移动赋值运算符

在现代 C++ 中(C++11 及更高版本),可以定义移动赋值运算符,用于避免不必要的深拷贝。

示例:

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
30
#include <iostream>
#include <string>

class MyClass {
private:
    std::string data;
public:
    MyClass(const std::string& str) : data(str) {}

    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        if (this == &other) return *this; // 自赋值保护

        data = std::move(other.data); // 转移资源

        return *this; // 返回 *this
    }

    void print() const {
        std::cout << data << std::endl;
    }
};

int main() {
    MyClass obj1("Hello");
    MyClass obj2("World");
    obj2 = std::move(obj1); // 移动赋值
    obj2.print(); // 输出: Hello
    return 0;
}

4. 赋值时的特殊情况

  • 自赋值:对象赋值给自己时(如 obj = obj;),需要特别处理,避免释放或修改自身资源。
  • 静态成员:静态成员不会被赋值运算符处理,因为它们属于类而不是实例。
  • 继承:在继承体系中,基类的赋值运算符可能需要被显式调用,否则派生类可能需要手动处理基类成员的赋值。

5. 赋值 vs. 拷贝构造

赋值运算符与拷贝构造函数不同:

  • 赋值运算符:用于两个已经存在的对象之间的赋值。
  • 拷贝构造函数:用于新对象的初始化时。

示例区分:

1
2
3
MyClass obj1("Hello");
MyClass obj2 = obj1; // 调用拷贝构造函数
obj2 = obj1;         // 调用赋值运算符

总结

对象赋值的行为取决于类的定义:

  • 默认赋值运算符(浅拷贝)。
  • 用户定义赋值运算符(深拷贝、自定义逻辑)。
  • 可选的移动赋值运算符(高效资源转移)。 确保在类中管理资源(如动态内存)时正确处理赋值运算,避免内存泄漏或未定义行为。

如果new了之后出了问题直接return。会导致内存泄漏。怎么办

在 C++ 中,如果在 new 分配内存后由于某种原因直接 return,确实会导致内存泄漏,因为分配的内存没有被释放。为了避免这种情况,可以采取以下策略:


1. 智能指针 (推荐)

使用智能指针(如 std::unique_ptrstd::shared_ptr),让内存的生命周期由智能指针管理,即使发生异常或提前 return,智能指针也会自动释放内存。

示例

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 <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor\n"; }
    ~MyClass() { std::cout << "MyClass destructor\n"; }
};

std::unique_ptr<MyClass> createObject(bool fail) {
    auto obj = std::make_unique<MyClass>(); // 使用智能指针管理内存

    if (fail) {
        std::cout << "Error occurred, returning early.\n";
        return nullptr; // 自动释放内存,无内存泄漏
    }

    return obj;
}

int main() {
    auto obj = createObject(true); // 测试提前返回
    return 0;
}

2. RAII 原则

将动态分配的资源封装到类中,并通过析构函数释放资源。在函数中如果出现提前 return 或异常,封装类的析构函数会自动释放资源。

示例

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
30
#include <iostream>

class ResourceGuard {
    int* data;
public:
    ResourceGuard() : data(new int[10]) { 
        std::cout << "Resource allocated.\n"; 
    }
    ~ResourceGuard() { 
        delete[] data; 
        std::cout << "Resource released.\n"; 
    }
};

void process(bool fail) {
    ResourceGuard guard; // RAII 管理资源

    if (fail) {
        std::cout << "Error occurred, returning early.\n";
        return; // 自动释放资源
    }

    std::cout << "Processing...\n";
}

int main() {
    process(true);  // 测试提前返回
    process(false); // 正常处理
    return 0;
}

3. 显式释放内存

如果不使用智能指针或 RAII,务必要在返回前手动释放内存。虽然这更容易出错,但在某些场景下可能是唯一的选择。

示例

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
#include <iostream>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor\n"; }
    ~MyClass() { std::cout << "MyClass destructor\n"; }
};

MyClass* createObject(bool fail) {
    MyClass* obj = new MyClass();

    if (fail) {
        std::cout << "Error occurred, releasing memory.\n";
        delete obj; // 显式释放内存
        return nullptr;
    }

    return obj;
}

int main() {
    MyClass* obj = createObject(true); // 测试提前返回
    delete obj; // 如果返回的不是 nullptr,记得释放内存
    return 0;
}

4. 异常处理

结合智能指针或 try-catch 块,在分配内存后发生错误时捕获异常并释放资源。

示例

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 <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor\n"; }
    ~MyClass() { std::cout << "MyClass destructor\n"; }
};

std::unique_ptr<MyClass> createObject(bool fail) {
    try {
        auto obj = std::make_unique<MyClass>();

        if (fail) {
            throw std::runtime_error("Error occurred.");
        }

        return obj;
    } catch (...) {
        std::cout << "Caught exception, cleaning up.\n";
        delete obj;
        return nullptr; // 返回空指针,无泄漏
    }
}

int main() {
    auto obj = createObject(true); // 测试异常处理
    return 0;
}

5. 总结与建议

  • 优先使用智能指针(推荐):例如 std::unique_ptrstd::shared_ptr,避免手动管理内存。
  • RAII 原则:将资源封装到类中,由析构函数负责释放。
  • 如果必须手动管理内存,确保在每个可能的提前 return 或异常路径中释放资源。
  • 对于复杂场景,结合智能指针和异常处理机制可以最大程度地减少错误。

这些方法不仅避免了内存泄漏,还让代码更易于维护和调试。

C++11的智能指针有哪些。weak_ptr的使用场景。什么情况下会产生循环引用

C++11 提供了三种主要的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr。它们通过 RAII 原则自动管理动态分配的内存,减少了内存泄漏的风险。以下是它们的用途、特性以及 std::weak_ptr 的使用场景和循环引用的讨论。


1. C++11 提供的智能指针

1.1. std::unique_ptr

  • 特点

    • 独占所有权(一个对象只能被一个 unique_ptr 拥有)。
    • 无法复制,只能转移所有权(通过 std::move)。
    • 轻量级,适合单一所有权的场景。
  • 用途

    • 管理动态分配的资源,避免手动 delete
    • 避免资源在多个所有者间共享。
  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    #include <iostream>
    #include <memory>
      
    class MyClass {
    public:
        MyClass() { std::cout << "MyClass constructed\n"; }
        ~MyClass() { std::cout << "MyClass destroyed\n"; }
    };
      
    int main() {
        std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
        // std::unique_ptr<MyClass> ptr2 = ptr1; // 错误,不能复制
        std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 转移所有权
        return 0;
    }
    

1.2. std::shared_ptr

  • 特点

    • 支持共享所有权(多个 shared_ptr 可以管理同一个对象)。
    • 使用引用计数机制,只有当最后一个 shared_ptr 被销毁时,资源才会释放。
    • 线程安全的引用计数,但资源本身需要额外同步。
  • 用途

    • 适合多个对象共享所有权的场景。
    • 动态资源生命周期由多个对象共同管理。
  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    #include <iostream>
    #include <memory>
      
    class MyClass {
    public:
        MyClass() { std::cout << "MyClass constructed\n"; }
        ~MyClass() { std::cout << "MyClass destroyed\n"; }
    };
      
    int main() {
        std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
        std::shared_ptr<MyClass> ptr2 = ptr1; // 共享所有权
        return 0; // 引用计数为 0,资源被释放
    }
    

1.3. std::weak_ptr

  • 特点

    • 弱引用,不影响对象的生命周期(不增加引用计数)。
    • 必须结合 std::shared_ptr 使用。
    • 用于打破循环引用或观察共享资源的状态。
  • 用途

    • 防止循环引用(常见于树形、图形结构)。
    • 检查共享资源是否仍然存在(如缓存)。
  • 示例

    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
    
    #include <iostream>
    #include <memory>
      
    class MyClass {
    public:
        MyClass() { std::cout << "MyClass constructed\n"; }
        ~MyClass() { std::cout << "MyClass destroyed\n"; }
    };
      
    int main() {
        std::shared_ptr<MyClass> shared = std::make_shared<MyClass>();
        std::weak_ptr<MyClass> weak = shared; // 弱引用,不增加引用计数
      
        if (auto locked = weak.lock()) { // 检查对象是否还存在
            std::cout << "Resource is still alive\n";
        }
      
        shared.reset(); // 手动释放共享所有权
      
        if (weak.expired()) { // 检查对象是否已销毁
            std::cout << "Resource has been destroyed\n";
        }
      
        return 0;
    }
    

2. 什么情况下会产生循环引用

循环引用 通常发生在 std::shared_ptr 互相引用时,因为引用计数永远不会降到零,导致对象无法释放,造成内存泄漏。

示例

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

class Node {
public:
    std::shared_ptr<Node> next; // 循环引用
    ~Node() { std::cout << "Node destroyed\n"; }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();

    node1->next = node2;
    node2->next = node1; // 循环引用

    return 0; // 内存泄漏,引用计数无法归零
}

3. 使用 std::weak_ptr 打破循环引用

示例

通过将一个引用改为 std::weak_ptr,避免循环引用。

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

class Node {
public:
    std::weak_ptr<Node> next; // 弱引用打破循环
    ~Node() { std::cout << "Node destroyed\n"; }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();

    node1->next = node2;
    node2->next = node1; // 循环被弱引用打破

    return 0; // 对象可以正确释放
}

4. 总结

智能指针的选择:

  • std::unique_ptr:单一所有权,资源不共享。
  • std::shared_ptr:共享所有权,多对象共同管理资源。
  • std::weak_ptr:弱引用,防止循环引用或观察共享资源。

循环引用场景:

  • 循环引用通常发生在两个或多个 std::shared_ptr 互相引用。
  • 使用 std::weak_ptr 打破循环引用,确保资源能正确释放。

在设计复杂数据结构(如树、图)时,合理使用智能指针及其组合,可以避免内存泄漏和生命周期管理问题。

sizeof是在编译期还是在运行期确定

sizeof 操作符在 编译期 确定结果,具体来说:


1. 编译期确定

在大多数情况下,sizeof 的结果是 编译时常量,因为编译器可以在编译阶段解析类型或对象的大小。例如:

示例

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

struct MyStruct {
    int a;
    double b;
    char c;
};

int main() {
    std::cout << sizeof(MyStruct) << std::endl; // 编译期确定
    return 0;
}
  • sizeof(MyStruct) 的值在编译期就已确定,因为 MyStruct 的大小是已知的。
  • 编译期计算的情况:
    • sizeof 基本数据类型(如 intdouble)。
    • sizeof 自定义类型(类、结构体、联合体)。
    • sizeof 数组(只要数组大小是固定的)。

2. 运行期的影响

尽管 sizeof 通常在编译期确定,但有些情况下,sizeof 的行为可能看起来像在运行期计算,主要是因为动态内存分配或指针类型的模糊性

2.1 动态分配的数组

sizeof 并不计算动态分配内存的大小,它仅返回指针类型本身的大小,而不是所指向的数据的大小。

示例

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
    int* arr = new int[10];
    std::cout << sizeof(arr) << std::endl; // 返回指针的大小,而不是数组的大小
    delete[] arr;
    return 0;
}
  • sizeof(arr) 返回指针类型的大小(例如在 64 位系统上通常为 8 字节)。
  • 动态分配的数组大小需要运行时跟踪(比如通过显式变量记录)。

2.2 不完全类型(Incomplete Types)

对于指向不完全类型(如 void* 或前向声明的类)的指针,sizeof 返回指针的大小,而不是类型大小。

示例

1
2
3
4
5
6
7
8
9
#include <iostream>

class MyClass; // 不完全类型

int main() {
    MyClass* obj = nullptr;
    std::cout << sizeof(obj) << std::endl; // 返回指针大小
    return 0;
}

3. 特殊情况:C++11 中的变长数组(VLA)

在 C++ 标准中,不支持 C 风格的变长数组(Variable-Length Array, VLA),但一些编译器(如 GCC 的扩展)允许使用。这种情况下,sizeof 的结果是在运行时确定的,因为数组的大小只有在运行时才能知道。

示例(GCC 扩展支持)

1
2
3
4
5
6
7
8
9
#include <iostream>

int main() {
    int n;
    std::cin >> n;
    int arr[n]; // VLA,GCC 扩展支持
    std::cout << sizeof(arr) << std::endl; // 运行时确定大小
    return 0;
}
  • 如果输入 n = 5,则 sizeof(arr) 可能返回 5 * sizeof(int)

注意:这种扩展不是标准 C++,建议避免使用。


4. 总结

  • 编译期确定:
    • 大多数情况下,sizeof 是编译时常量,结果在编译阶段确定。
    • 适用于固定大小的类型、数组和对象。
  • 运行期影响:
    • 动态内存分配(如 new)或变长数组(非标准扩展)可能需要运行时确定大小。
    • 指针类型仅返回指针的大小,而非其所指向数据的大小。

因此,在标准 C++ 中,sizeof 通常是 编译期常量,但理解它的运行期影响对编写高效和可靠的代码很重要。

多线程里线程的同步方式有哪些

在多线程编程中,线程同步是为了协调多个线程对共享资源的访问或任务的执行顺序,以避免数据竞争和不确定行为。在 C++ 中,线程同步有多种方式,以下是常见的同步方式及其特点:


1. 互斥锁 (Mutex)

1.1 std::mutex

  • 作用:

    • 用于保护共享资源的访问,防止多个线程同时访问同一资源。
  • 用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    #include <iostream>
    #include <thread>
    #include <mutex>
      
    std::mutex mtx;
      
    void print(int id) {
        mtx.lock();
        std::cout << "Thread " << id << " is running\n";
        mtx.unlock();
    }
      
    int main() {
        std::thread t1(print, 1);
        std::thread t2(print, 2);
      
        t1.join();
        t2.join();
        return 0;
    }
    

1.2 std::lock_guard

  • 作用:

    • 自动管理互斥锁的生命周期,防止忘记解锁。
  • 用法:

    1
    
    std::lock_guard<std::mutex> guard(mtx);
    

1.3 std::unique_lock

  • 作用:

    • 提供灵活的锁控制,例如延迟加锁、条件变量的使用等。
  • 用法:

    1
    
    std::unique_lock<std::mutex> lock(mtx);
    

2. 条件变量 (Condition Variable)

  • 作用:

    • 用于线程之间的通知和等待机制,通常与 std::unique_lock 配合使用。
  • 用法:

    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
    
    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <condition_variable>
      
    std::mutex mtx;
    std::condition_variable cv;
    bool ready = false;
      
    void worker() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return ready; }); // 等待条件为 true
        std::cout << "Thread is working\n";
    }
      
    int main() {
        std::thread t(worker);
      
        std::this_thread::sleep_for(std::chrono::seconds(1));
        {
            std::lock_guard<std::mutex> lock(mtx);
            ready = true; // 修改条件
        }
        cv.notify_one(); // 通知等待的线程
        t.join();
        return 0;
    }
    

3. 读写锁 (Shared Mutex)

  • 作用:

    • 提供对资源的共享访问(多个读者)和独占访问(单个写者)。
  • 用法:

    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 <thread>
    #include <shared_mutex>
      
    std::shared_mutex rw_lock;
      
    void reader() {
        std::shared_lock<std::shared_mutex> lock(rw_lock);
        std::cout << "Reader is reading\n";
    }
      
    void writer() {
        std::unique_lock<std::shared_mutex> lock(rw_lock);
        std::cout << "Writer is writing\n";
    }
      
    int main() {
        std::thread t1(reader);
        std::thread t2(writer);
      
        t1.join();
        t2.join();
        return 0;
    }
    

4. 原子操作 (Atomic)

  • 作用:

    • 使用 std::atomic 进行无锁操作,适用于简单的计数或标志位操作。
  • 优点:

    • 高效,无需显式加锁。
  • 用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    #include <iostream>
    #include <thread>
    #include <atomic>
      
    std::atomic<int> counter(0);
      
    void increment() {
        for (int i = 0; i < 1000; ++i) {
            counter.fetch_add(1); // 原子递增
        }
    }
      
    int main() {
        std::thread t1(increment);
        std::thread t2(increment);
      
        t1.join();
        t2.join();
        std::cout << "Counter: " << counter << std::endl;
        return 0;
    }
    

5. 信号量 (Semaphore)

  • 作用:

    • 控制对资源的访问数量(如线程池、资源池)。
  • 用法

    (C++20 引入 std::counting_semaphore):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    #include <iostream>
    #include <thread>
    #include <semaphore>
      
    std::counting_semaphore<5> sem(2); // 初始许可数量为 2
      
    void worker(int id) {
        sem.acquire(); // 请求许可
        std::cout << "Thread " << id << " is working\n";
        std::this_thread::sleep_for(std::chrono::seconds(1));
        sem.release(); // 释放许可
    }
      
    int main() {
        std::thread t1(worker, 1);
        std::thread t2(worker, 2);
        std::thread t3(worker, 3);
      
        t1.join();
        t2.join();
        t3.join();
        return 0;
    }
    

6. 屏障 (Barrier, C++20)

  • 作用:

    • 线程之间的同步点,所有线程达到屏障后才能继续执行。
  • 用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    #include <iostream>
    #include <thread>
    #include <barrier>
      
    std::barrier sync_point(3); // 需要 3 个线程同步
      
    void worker(int id) {
        std::cout << "Thread " << id << " reached barrier\n";
        sync_point.arrive_and_wait(); // 等待其他线程到达
        std::cout << "Thread " << id << " passed barrier\n";
    }
      
    int main() {
        std::thread t1(worker, 1);
        std::thread t2(worker, 2);
        std::thread t3(worker, 3);
      
        t1.join();
        t2.join();
        t3.join();
        return 0;
    }
    

7. 线程局部存储 (Thread-Local Storage)

  • 作用:

    • 提供每个线程独立的变量,避免多线程间数据竞争。
  • 用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    #include <iostream>
    #include <thread>
      
    thread_local int local_var = 0;
      
    void worker(int id) {
        local_var = id;
        std::cout << "Thread " << id << " local_var: " << local_var << std::endl;
    }
      
    int main() {
        std::thread t1(worker, 1);
        std::thread t2(worker, 2);
      
        t1.join();
        t2.join();
        return 0;
    }
    

总结

同步方式适用场景特点
std::mutex简单加锁解锁保护资源需要手动管理
std::lock_guard自动管理锁生命周期简化锁管理
std::shared_mutex读写锁,多个读线程共享资源,写线程独占资源适合读多写少场景
std::atomic无锁操作,适用于简单计数或标志位操作高效,适合基本同步任务
std::condition_variable线程等待和通知配合 std::unique_lock 使用
std::counting_semaphore控制对资源的访问数量C++20 引入,适合资源池
std::barrier多线程同步点C++20 引入,适合并行算法的同步
thread_local每线程独立的局部变量无需加锁,避免数据竞争

根据场景选择合适的同步方式,可以提高程序的性能和代码可读性。

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