博主头像
小雨淅沥

Some things were meant to be.

OOP课程笔记:第零章 补充资料

第零章 补充资料

1.C++中的输入输出流

在C++中,输入输出流(I/O Streams)主要通过 <iostream> 库提供的标准流对象(如 std::coutstd::cinstd::cerr 等)和流操作符(<<>>)来实现数据的输入和输出。

C++ 的 I/O 流主要分为:

  • 标准输出流std::cout(控制台输出)、std::cerr(错误输出)、std::clog(日志输出)
  • 标准输入流std::cin(控制台输入)
  • 文件流std::ifstream(读文件)、std::ofstream(写文件)、std::fstream(读写文件)
  • 字符串流std::istringstream(从字符串读取)、std::ostringstream(写入字符串)、std::stringstream(读写字符串)
#include <iostream>
using namespace std;

int main() {
    int num;
    cout << "Enter a number: ";  // 输出到控制台
    cin >> num;                  // 从控制台读取输入
    cerr << "Error message!";    // 输出错误信息(无缓冲)
    clog << "Log message!";      // 输出日志信息(有缓冲)
    return 0;
}

2.不同风格强制类型转换

C语言使用简单的强制转换语法,形式为:

(target_type)expression
  • 简单直接:将所有转换视为显式强制行为,不区分转换的语义。
  • 无类型检查:编译器不会检查转换是否安全(例如,可能丢失精度的转换)。
  • 适用于所有类型:包括基本类型、指针、结构体等。
int a = 10;
double b = (double)a;       // 基本类型转换
void* p = (void*)&a;        // 指针转为void*
int* q = (int*)p;           // void*转回int*

float f = 3.14;
int c = (int)f;             // 截断小数部分(无警告)

C++引入了4种显式类型转换运算符,提供更精细的控制和安全性:

转换类型语法用途
static_caststatic_cast<target>(expr)安全的显式转换(如基本类型、父子类指针)。
dynamic_castdynamic_cast<target>(expr)运行时多态类型转换(需虚函数,失败返回nullptr或抛异常)。
const_castconst_cast<target>(expr)移除或添加const/volatile属性。
reinterpret_castreinterpret_cast<target>(expr)低层指针/整数间的危险转换(如int*char*)。
  • 语义明确:每种转换有特定用途,避免滥用。
  • 编译时检查:如static_cast会检查继承关系,const_cast仅处理const
  • 安全性:比C风格转换更安全(例如dynamic_cast支持运行时类型检查)。
double d = 3.14;
int i = static_cast<int>(d);      // 基本类型转换(明确意图)

Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // 多态转换

const int x = 10;
int* px = const_cast<int*>(&x);   // 移除const(慎用)

int* p = new int(42);
char* ch = reinterpret_cast<char*>(p); // 危险:重新解释指针类型

关键区别

特性C风格转换C++风格转换
语法(type)exprxxx_cast<type>(expr)
安全性无检查,易出错部分操作有编译/运行时检查
用途区分明确区分4种语义
多态类型支持不支持dynamic_cast支持运行时类型检查
代码可读性较差更清晰,意图明确

何时使用?

  • C风格转换:仅在C代码或简单场景(如基本类型转换)中使用,但需谨慎。
  • C++风格转换:优先使用,尤其是:

    • static_cast:替代大多数C风格转换。
    • dynamic_cast:处理多态类型。
    • const_cast:需要修改const时(罕见)。
    • reinterpret_cast:底层操作(如序列化、硬件访问)。

3.匿名函数 (lambda)

在 C++ 中,Lambda 函数(匿名函数)是一种快速定义并使用的轻量级函数对象,特别适合用作回调函数、算法参数(如 std::sort 的比较规则)等场景


基本语法

[capture-list](parameters) -> return-type { 
    // 函数体
}
  • capture-list:捕获外部变量(可选)
  • parameters:参数列表(可选)
  • return-type:返回值类型(可选,可自动推导)
  • 函数体:具体的实现逻辑

用法示例

无捕获,无参数

auto hello = []() {
    std::cout << "Hello, Lambda!" << std::endl;
};
hello(); // 输出: Hello, Lambda!

带参数的 Lambda

auto add = [](int a, int b) {
    return a + b;
};
std::cout << add(3, 4); // 输出: 7

在算法中作为回调(如 std::sort

std::vector<int> nums = {3, 1, 4, 1, 5};
std::sort(nums.begin(), nums.end(), [](int a, int b) {
    return a > b; // 降序排序
});
// 结果: {5, 4, 3, 1, 1}

捕获外部变量(Capture List)

Lambda 可以通过捕获列表访问外部作用域的变量:

捕获方式效果
[]不捕获任何外部变量
[x, &y]按值捕获 x,按引用捕获 y
[=]按值捕获所有外部变量
[&]按引用捕获所有外部变量
[=, &x]默认按值捕获,但 x 按引用捕获
[&, x]默认按引用捕获,但 x 按值捕获

按值 vs 按引用捕获

int x = 10, y = 20;
auto foo = [x, &y]() {
    y = 30;  // 修改外部变量 y(引用捕获)
    // x = 5; // 错误!x 是按值捕获的,不可修改
};
foo();
std::cout << y; // 输出: 30(y 被修改)

指定返回值类型(可选)

当函数体复杂时,可以显式指定返回类型:

auto divide = [](double a, double b) -> double {
    if (b == 0) return 0; // 避免歧义
    return a / b;
};

4.异常处理 (exception)

在 C++ 中,异常处理(Exception Handling)是一种重要机制,用于应对运行时错误,使程序具有更强的健壮性

C++ 的异常处理主要涉及三个关键字:

关键字含义
try用于捕捉异常的代码块。放置可能抛出异常的语句。
throw抛出异常对象。可以是基本类型、类对象、指针等。
catch用于捕捉并处理异常。必须紧跟在 try 之后。

异常处理的基本结构

try {
    // 可能抛出异常的代码
    throw SomeException();  // 抛出异常
} catch (const SomeException& e) {
    // 捕获异常并处理
}
#include <iostream>
#include <stdexcept>

void testFunction() {
    throw std::runtime_error("An error occurred in testFunction!");
}

int main() {
    try {
        testFunction();  // 抛出异常
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    } catch (...) {
        std::cerr << "Caught unknown exception." << std::endl;
    }
    return 0;
}
  • throw std::runtime_error(...):抛出异常。
  • try 块中执行该函数,程序遇到异常会跳到对应的 catch
  • catch (const std::runtime_error& e):捕获该类型的异常对象。
  • catch (...):捕获所有未被匹配的异常(兜底方案)。

exception 类重写

C++ 中的 std::exception 是标准异常类的基类,如果想自定义一个异常类型,通常只需要继承 std::exception 并重写 what() 函数即可

#include <exception>
#include <string>

class MyException : public std::exception {
public:
    // 构造函数支持传入自定义消息
    explicit MyException(const std::string& message)
        : msg_(message) {}

    // 重写 what() 函数,返回错误信息
    virtual const char* what() const noexcept override {
        return msg_.c_str();
    }

private:
    std::string msg_;  // 用于保存错误信息
};
#include <iostream>

int main() {
    try {
        throw MyException("自定义错误信息:操作失败!");
    } catch (const std::exception& e) {
        std::cout << "捕获异常: " << e.what() << std::endl;
    }
    return 0;
}

exception 注意事项

项目说明
virtual const char* what() const noexcept override必须使用 noexcept(否则不符合标准库接口),必须为 const,并标记为 override 保证正确覆盖。
返回值类型为 const char*所以 std::string.c_str() 非常合适
不应返回临时字符串返回值必须是有效的 C 字符串指针,生命周期由异常对象本身管理
建议用 std::string 保存消息可灵活构造,自动管理内存

使用添加错误码的 exception

class MyExceptionWithCode : public std::exception {
public:
    MyExceptionWithCode(int code, const std::string& message)
        : code_(code), msg_(message) {}

    virtual const char* what() const noexcept override {
        return msg_.c_str();
    }

    int code() const noexcept {
        return code_;
    }

private:
    int code_;
    std::string msg_;
};

try {
    throw MyExceptionWithCode(404, "资源未找到!");
} catch (const MyExceptionWithCode& e) {
    std::cout << "错误码: " << e.code() << ",消息: " << e.what() << std::endl;
}

throw 的用法

抛出基本类型异常

throw 42;  // 抛出int类型

抛出自定义异常类

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "My custom exception!";
    }
};
throw MyException();

抛出指针(不推荐)

throw new MyException();  // 不推荐,因为需要手动释放资源

catch 的使用方式

  • catch 会按顺序尝试匹配异常类型,从最具体的类型开始匹配
  • 如果没有匹配的 catch,程序会调用 std::terminate() 并中止。
  • 多态类型(如 std::exception)可以作为父类捕获多个子类异常。

捕获异常时推荐使用 const 引用:

catch (const std::exception& e) {
    std::cerr << e.what();
}

避免对象拷贝,支持多态。

异常可以嵌套(C++11 开始支持):

try {
    try {
        throw std::runtime_error("Inner error");
    } catch (...) {
        std::throw_with_nested(std::runtime_error("Outer context"));
    }
} catch (const std::exception& e) {
    std::cerr << e.what() << std::endl;
}

函数声明可能抛出异常(C++11 后已弃用):

void foo() throw();      // 不会抛出异常(已废弃)
void foo() noexcept;     // C++11 推荐写法

注意事项

  1. C++ 中异常处理 开销较大,不能滥用,尤其是性能敏感场景(如嵌入式开发)。
  2. 不建议在构造函数中抛出异常除非你完全控制对象生命周期。
  3. 不建议使用裸指针抛异常,除非特别需要(因为资源管理问题)。

5. 多线程库 <thread>

C++11 引入的 <thread> 库为多线程编程提供了标准化的支持,使得编写跨平台的多线程程序变得更加简单。

基本线程创建

使用函数创建线程

#include <iostream>
#include <thread>

void thread_function() {
    std::cout << "子线程执行中..." << std::endl;
}

int main() {
    std::thread t(thread_function);  // 创建并启动线程
    t.join();                       // 等待线程结束
    
    std::cout << "主线程结束" << std::endl;
    return 0;
}

使用lambda表达式创建线程

#include <iostream>
#include <thread>

int main() {
    std::thread t([](){
        std::cout << "Lambda线程执行中..." << std::endl;
    });
    
    t.join();
    return 0;
}

带参数的线程函数

#include <iostream>
#include <thread>
#include <string>

void print_message(const std::string& msg) {
    std::cout << "消息: " << msg << std::endl;
}

int main() {
    std::string message = "Hello, Thread!";
    std::thread t(print_message, message);
    t.join();
    return 0;
}

线程管理

  • join(): 阻塞当前线程,直到被调用的线程执行完毕
  • detach(): 将线程与线程对象分离,线程在后台独立运行
#include <iostream>
#include <thread>
#include <chrono>

void long_running_task() {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "任务完成" << std::endl;
}

int main() {
    std::thread t(long_running_task);
    
    // t.join();  // 主线程会等待t完成
    t.detach();   // t会在后台独立运行
    
    std::cout << "主线程继续执行" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(3)); // 确保程序不会过早退出
    
    return 0;
}

检查线程是否可join

if (t.joinable()) {
    t.join();
    // 或 t.detach();
}

线程标识和当前线程

获取线程ID

#include <iostream>
#include <thread>

void print_thread_id() {
    std::cout << "线程ID: " << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t1(print_thread_id);
    std::thread t2(print_thread_id);
    
    std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
    
    t1.join();
    t2.join();
    
    return 0;
}
#include <thread>
#include <chrono>

// 让当前线程休眠
std::this_thread::sleep_for(std::chrono::milliseconds(100));

// 让出当前线程的时间片
std::this_thread::yield();

线程传参

参数传递方式:参数默认是按值传递的,如果需要传递引用,需要使用 std::ref

#include <iostream>
#include <thread>

void modify_value(int& x) {
    x = 42;
}

int main() {
    int value = 0;
    // std::thread t(modify_value, value);  // 错误,不能直接传递引用
    std::thread t(modify_value, std::ref(value));  // 正确
    
    t.join();
    std::cout << "修改后的值: " << value << std::endl;  // 输出42
    
    return 0;
}

传递指针

void process_data(int* data) {
    // 处理数据
}

int main() {
    int data[100];
    std::thread t(process_data, data);
    t.join();
    return 0;
}

线程异常处理

如果线程函数抛出异常且未被捕获,程序会调用 std::terminate()

#include <iostream>
#include <thread>
#include <exception>

void might_throw() {
    throw std::runtime_error("线程中发生错误");
}

int main() {
    std::thread t([](){
        try {
            might_throw();
        } catch (const std::exception& e) {
            std::cerr << "捕获到异常: " << e.what() << std::endl;
        }
    });
    
    t.join();
    return 0;
}

硬件并发数

#include <iostream>
#include <thread>

int main() {
    unsigned int n = std::thread::hardware_concurrency();
    std::cout << "此系统支持 " << n << " 个并发线程" << std::endl;
    return 0;
}

线程池基础实现

下面是一段实现线程池的简单代码:

#include <iostream>
#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>

class ThreadPool {
public:
    ThreadPool(size_t num_threads) {
        for (size_t i = 0; i < num_threads; ++i) {
            workers.emplace_back([this] {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock, [this] { 
                            return this->stop || !this->tasks.empty(); 
                        });
                        if (this->stop && this->tasks.empty()) return;
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }
                    task();
                }
            });
        }
    }

    template<class F>
    void enqueue(F&& f) {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            tasks.emplace(std::forward<F>(f));
        }
        condition.notify_one();
    }

    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread &worker : workers)
            worker.join();
    }

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop = false;
};

int main() {
    ThreadPool pool(4);
    
    for (int i = 0; i < 8; ++i) {
        pool.enqueue([i] {
            std::cout << "任务 " << i << " 由线程 " 
                      << std::this_thread::get_id() << " 执行" << std::endl;
        });
    }
    
    return 0;
}

注意事项

  1. 线程安全:多线程访问共享数据时需要使用同步机制(如互斥锁)
  2. 生命周期管理:确保线程对象在销毁前被join或detach
  3. 异常安全:线程中的异常不会自动传播到主线程
  4. 性能考量:创建过多线程可能导致性能下降
  5. 平台差异:虽然<thread>是跨平台的,但某些实现细节可能不同

6.多线程常用库

在 C++ 中,线程同步机制(如互斥锁、自旋锁、条件变量)的实现涉及操作系统底层支持和标准库的封装。


互斥锁 (Mutex)

  • 操作系统依赖:通常基于内核提供的同步原语(如 Linux 的 futex、Windows 的 CRITICAL_SECTION)。
  • 阻塞机制:当线程尝试获取已被锁定的互斥锁时,会进入阻塞状态(由内核调度),让出 CPU 资源。
#include <mutex>

std::mutex mtx;

void safe_function() {
    mtx.lock();   // 阻塞直到获取锁
    // 临界区代码
    mtx.unlock(); // 释放锁
}

更安全的 RAII 方式:

void safe_function() {
    std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时自动解锁
    // 临界区代码
}

自旋锁 (Spinlock)

实现原理:

  • 忙等待 (Busy-waiting):线程在获取锁时不会阻塞,而是通过循环不断尝试获取锁。
  • 适用场景:临界区非常小(纳秒级),且线程不希望被调度器挂起(避免上下文切换开销)。
#include <atomic>

class spinlock {
    std::atomic_flag flag = ATOMIC_FLAG_INIT;

public:
    void lock() {
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 可插入 CPU 暂停指令(如 x86 的 _mm_pause())减少能耗
        }
    }

    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

使用示例:

spinlock slock;

void thread_func() {
    slock.lock();
    // 临界区代码
    slock.unlock();
}

与互斥锁对比:

特性自旋锁互斥锁
等待方式忙等待(消耗 CPU)阻塞(不消耗 CPU)
开销低(无上下文切换)高(需内核介入)
适用场景极短临界区较长临界区

条件变量 (Condition Variable)

实现原理:

  • 结合互斥锁使用:用于线程间的条件等待和通知。
  • 操作系统支持:通常基于内核的等待/唤醒机制(如 Linux 的 futex)。
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

// 等待线程
void wait_thread() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; }); // 阻塞直到 ready == true
    // 条件满足后的操作
}

// 通知线程
void notify_thread() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one(); // 唤醒一个等待线程
}

读写锁 (Read-Write Lock)

实现原理:

  • 多读单写:允许多个线程同时读,但写操作独占。
  • 基于原子计数器:记录当前读线程数量。

C++17 的 std::shared_mutex

#include <shared_mutex>

std::shared_mutex rw_mutex;

// 读操作
void read_data() {
    std::shared_lock<std::shared_mutex> lock(rw_mutex);
    // 多个读线程可同时进入
}

// 写操作
void write_data() {
    std::unique_lock<std::shared_mutex> lock(rw_mutex);
    // 独占访问
}

原子操作 (Atomic)

实现原理:

  • CPU 指令级支持:通过 LOCK 前缀指令(x86)或类似的原子指令实现。
  • 无锁编程:避免锁的开销,直接操作内存总线。
#include <atomic>

std::atomic<int> counter(0);

void increment() {
    counter.fetch_add(1, std::memory_order_relaxed);
}

RALL机制

RAII(Resource Acquisition Is Initialization)在 C++ 中用于管理互斥锁 (std::mutex) 的 ,它结合了 互斥锁的自动加锁和解锁,确保线程安全。

1. 基本语法

std::unique_lock<std::mutex> lock(mtx);
  • std::mutex mtx:定义一个互斥锁(用于保护临界区)。
  • std::unique_lock<std::mutex>:是一个模板类,用于管理 std::mutex 的锁。
  • lock(mtx):构造函数,在构造时自动对 mtx 加锁(调用 mtx.lock())。

2. 核心功能

自动加锁

  • std::unique_lock 对象 lock 被创建时,它会立即对 mtx 加锁:

    std::unique_lock<std::mutex> lock(mtx);  // 构造时自动调用 mtx.lock()

自动解锁(RAII)

  • lock 离开作用域(如函数结束或异常抛出)时,它的析构函数会自动调用 mtx.unlock(),避免忘记解锁导致的死锁:

    {
        std::unique_lock<std::mutex> lock(mtx);  // 加锁
        // 临界区操作...
    }  // 离开作用域,自动解锁

手动控制

  • 可以手动加锁/解锁(比 std::lock_guard 更灵活):

    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);  // 延迟加锁
    lock.lock();   // 手动加锁
    lock.unlock(); // 手动解锁

4. 与条件变量配合使用

条件变量 (std::condition_variable) 的 wait() 必须接收 std::unique_lock,因为:

  • wait() 会在等待时 临时释放锁(避免死锁),被唤醒后 重新加锁
  • std::unique_lock 支持这种灵活的锁管理,而 std::lock_guard 不能。
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; });  // 等待时会释放锁,唤醒后重新加锁

5. 典型应用场景

  1. 保护临界区:

    std::mutex mtx;
    {
     std::unique_lock<std::mutex> lock(mtx);
        // 临界区代码...
    }  // 自动解锁
  2. 条件变量等待:

    std::condition_variable cv;
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return data_ready; });
  3. 延迟加锁:

    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
    // ...其他操作...
    lock.lock();  // 手动加锁

总结

  1. 互斥锁:适合保护较长的临界区,由内核调度阻塞线程。
  2. 自旋锁:适合极短临界区,避免上下文切换开销。
  3. 条件变量:用于线程间条件同步,必须与互斥锁配合使用。
  4. 读写锁:优化读多写少场景,C++17 直接支持。
  5. 原子操作:最轻量级的同步,适合简单变量操作。
OOP课程笔记:第零章 补充资料
https://www.rainerseventeen.cn/index.php/Code-Basic/11.html
本文作者 Rainer
发布时间 2025-10-18
许可协议 CC BY-NC-SA 4.0

评论已关闭