一、多线程
std::thread()、join() 的用法:使用std::thread()可以创建一个线程,同时指定线程执行函数以及参数,同时也可使用lamda表达式。
cpp
#include <iostream>
#include <thread>
void threadFunction(int num) {
std::cout << "Hello from thread with number: " << num << std::endl;
}
int main() {
int threadNum = 42;
std::thread t(threadFunction, threadNum); // 将threadNum传递给线程函数
t.join(); // 将会阻塞主线程的执行等待t线程执行完毕
// 如果没有使用join可能造成主线程结束而t线程没执行完毕提前消亡
return 0;
}
除了使用join以外,也可使用detach()
在C++中,std::thread是一个用于创建线程的类,而detach()是std::thread类的一个成员函数。detach()函数用于将一个std::thread对象与其底层的线程分离,从而允许线程在后台运行,不再与原始std::thread对象关联。
当一个线程被分离后,它的生命周期将不再受到std::thread对象的控制,这意味着在线程执行完成之前,你不再能够对其进行join()操作,也不能检查其是否已经执行完毕。分离线程后,线程的资源将在其执行完成后自动释放,而不需要显式地调用join()函数。
cpp
#include <iostream>
#include <thread>
void threadFunction() {
// 假设这是在一个后台线程中执行的函数
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(threadFunction); // 创建线程对象t,并指定线程函数threadFunction
t.detach(); // 将线程与线程对象分离,使得线程在后台执行
// 注意:在此处不能使用t.join(),因为线程已经被分离,没有与t相关联的线程了
// 主线程继续执行其它任务
std::cout << "Main thread continues..." << std::endl;
// 这里可能会发生线程在后台执行的输出,也可能不会,因为线程已经分离了
// 线程可能在主线程结束前执行,也可能在主线程结束后执行
return 0;
}
1.1 std::lock_guard()的用法
std::lock_guard
是C++标准库中的一个RAII(资源获取即初始化)类模板,用于在多线程环境中实现互斥锁(std::mutex
)的自动上锁和解锁。它提供了一种简单的方式来确保在互斥锁保护的代码块中,获取锁和释放锁的正确顺序和时机,从而避免了因异常或提前返回而导致的锁无法释放的情况。
std::lock_guard
的用法如下:
cpp
#include <iostream>
#include <mutex>
std::mutex mtx; // 创建一个互斥锁
void criticalSection() {
std::lock_guard<std::mutex> lock(mtx); // 在函数内部创建std::lock_guard对象,并传入互斥锁
// 在此处放置需要保护的临界区代码
// 在临界区代码执行期间,互斥锁会被自动上锁
// 当std::lock_guard对象超出作用域时,会自动释放互斥锁,无需手动解锁
}
int main() {
std::thread t1(criticalSection);
std::thread t2(criticalSection);
t1.join();
t2.join();
return 0;
}
在上面的例子中,std::lock_guard<std::mutex> lock(mtx);
这一行创建了一个std::lock_guard
对象lock
,并传入了互斥锁mtx
。当lock_guard
对象被创建时,它会自动调用互斥锁的lock()
方法来上锁。当lock_guard
对象的作用域结束时,无论是通过函数正常返回、抛出异常或是因其他原因退出作用域,它都会自动调用互斥锁的unlock()
方法来解锁,确保临界区代码执行完成后互斥锁一定会被正确释放。
使用std::lock_guard
的好处是它简化了互斥锁的使用,避免了忘记解锁或异常处理不当导致的死锁问题。它是一种比较安全和推荐的方式来处理多线程的互斥访问问题。
1.2 std::atomic_bool 的用法
std::atomic_bool
是 C++ 标准库中的一个原子布尔类型,用于支持多线程编程中的原子操作。原子操作是一种确保操作不会被其他线程中断的操作,从而避免竞态条件(Race Condition)和数据竞争(Data Race)的发生。
什么是原子操作?
原子操作(Atomic operations)是在计算机科学中的一种操作,指的是不能被中断、分割或交错执行的操作。在多线程或并发环境中,原子操作是为了防止竞态条件(Race Condition)和数据竞争(Data Race)而设计的。竞态条件指的是多个线程在访问共享资源时的不确定性行为,数据竞争则是多个线程在访问内存位置时引发的未定义行为。
原子操作保证了操作的执行是不可分割的,即使在多线程环境下也不会被其他线程的操作干扰。这种操作通常是在硬件级别实现的,使用特定的机器指令或锁机制。
在编程中,原子操作可以确保在并发情况下对共享数据的访问是线程安全的。一些常见的原子操作包括:
-
加载(Load): 从内存中读取值,确保读取的操作是原子的,不会在读取过程中被其他线程的写操作干扰。
-
存储(Store): 将值写入内存中,确保写操作是原子的,不会在写入过程中被其他线程的读或写操作干扰。
-
交换(Exchange): 原子地交换两个值,通常用于实现一些同步机制。
-
比较交换(Compare and Swap,CAS): 检查某个内存位置的值是否等于预期值,如果相等,则将新值写入该位置,这个操作用于实现一些锁和同步机制。
-
递增和递减(Increment and Decrement): 原子地增加或减少某个内存位置的值。
在 C++ 中,标准库提供了 std::atomic
类模板,用于实现原子操作。您可以使用 std::atomic
来创建原子类型的变量,从而在多线程环境中进行线程安全的操作。例如,std::atomic_int
表示原子整数类型,std::atomic_bool
表示原子布尔类型等。
原子布尔类型 std::atomic_bool
支持以下特性:
-
原子操作:
std::atomic_bool
支持各种原子操作,包括加载(load)、存储(store)、交换(exchange)、比较交换(compare_exchange_strong 和 compare_exchange_weak)等。 -
原子加载和存储: 使用
load
方法可以原子地获取std::atomic_bool
的值,而使用store
方法可以原子地设置新的值。 -
原子交换: 使用
exchange
方法可以原子地交换std::atomic_bool
的值,并返回之前的值。 -
比较交换: 使用
compare_exchange_strong
和compare_exchange_weak
方法可以原子地比较当前值与期望值,并在匹配时更新为新值。
以下是一个简单的示例,展示了如何使用 std::atomic_bool
来实现线程安全的标志变量:
cpp
#include <iostream>
#include <atomic>
#include <thread>
std::atomic_bool flag(false);
void worker() {
while (!flag.load(std::memory_order_relaxed)) {
// 在这里执行一些工作
}
std::cout << "Worker thread finished." << std::endl;
}
int main() {
std::thread t(worker);
// 模拟一些工作
std::this_thread::sleep_for(std::chrono::seconds(2));
flag.store(true, std::memory_order_relaxed);
t.join();
return 0;
}
在上面的示例中,主线程通过 store
方法将 flag
设置为 true
,通知工作线程停止工作。工作线程通过 load
方法定期检查 flag
的值,以判断是否继续工作。这样可以避免了使用标准布尔变量时可能出现的数据竞争问题。
总之,std::atomic_bool
是 C++ 中的原子布尔类型,用于在多线程环境下执行操作,确保操作的原子性以避免竞态条件和数据竞争。
1.3 std::condition_variable的用法
当涉及到多线程编程时,有时候需要让一个线程等待,直到某个条件满足。std::condition_variable
是 C++ 中为这种目的提供的一种机制。
std::condition_variable
通常与 std::mutex
一起使用,来同步线程的执行和等待特定条件的出现。
基本概念如下:
- 一个或多个线程可以在特定条件上等待。
- 另一个线程可以用来通知等待的线程该条件已满足。
主要方法:
wait()
: 使线程等待,直到条件满足。这通常与std::unique_lock
和std::mutex
一起使用。notify_one()
: 唤醒一个等待该条件的线程(如果存在)。notify_all()
: 唤醒所有等待该条件的线程。
简单示例:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lock(mtx);
while (!ready) { // 为防止假唤醒,我们使用一个循环来检查条件
cv.wait(lock);
}
std::cout << "thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lock(mtx);
ready = true;
cv.notify_all(); // 唤醒所有等待的线程
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
go();
for (auto &th : threads)
th.join();
return 0;
}
在上面的示例中,我们有10个线程都在等待"比赛"开始。主线程会设置 ready
为 true
,然后使用 cv.notify_all()
通知所有等待的线程。这样,所有线程都会开始执行。
使用 std::condition_variable
时需要注意:
- 可能会发生"假唤醒"(即没有任何线程调用
notify_*
的情况下,等待的线程可能被唤醒)。为了安全地处理这种情况,应该始终在一个循环中检查条件。 - 当使用
wait()
方法时,必须提供一个std::unique_lock
,该锁应该在调用wait()
之前上锁。wait()
会自动释放锁,允许其他线程进入临界区,并在条件满足后重新获取锁。
二、基础语法知识
2.1 static的用途
当应用于不同上下文中,static
关键字在C++中具有不同的含义和用法。下面是对 static
的几种常见用法的总结:
-
静态成员变量:
- 在类中声明的静态成员变量是类的所有实例共享的,而不是每个实例独立拥有的。
- 静态成员变量在类的所有实例之间保持相同的值。
- 静态成员变量可以通过类名或类的实例来访问。
-
静态成员函数:
- 静态成员函数与类的实例无关,只能访问类的静态成员变量和其他静态成员函数。
- 静态成员函数在调用时不需要通过类的实例来调用,可以直接使用类名调用。
-
静态局部变量:
- 静态局部变量是在函数内部声明的变量,但只在第一次进入该函数时初始化,之后函数退出再次进入时保持上次的值。
- 静态局部变量在函数调用之间保持状态,可用于保留跨多次函数调用的信息。
-
静态全局变量:
- 在函数外部声明的静态全局变量只能在声明它的文件内可见,不会受到其他文件的影响。
- 静态全局变量在整个程序运行期间保持其值,不会受到函数调用的限制。
-
静态类成员(C++17起):
- 在类中声明的静态成员可以用
inline
关键字指定为内联。 - 静态类成员可以在类的定义中直接初始化,无需在类外进行初始化。
- 在类中声明的静态成员可以用
总之,static
在C++中有多种用法,可以用于创建静态成员、静态函数、静态局部变量以及限定全局变量的作用域。根据不同的上下文,static
提供了不同的功能和特性。
其中单例模式中用到了静态成员函数
在C++中,你可以在类中定义一个静态成员函数,并且让这个静态成员函数返回该类的引用。下面是一个示例代码来展示如何实现这一点:
cpp
class MyClass {
public:
// 静态公有函数,返回类的引用
static MyClass& GetInstance() {
// 在这里可以进行一些初始化操作,如果需要的话
// 返回类的引用
static MyClass instance;
return instance;
}
// 其他类成员和函数...
private:
// 私有构造函数,防止外部直接实例化
MyClass() {
// 构造函数的初始化操作
}
// 防止复制和赋值
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
// 私有成员变量和函数...
};
int main() {
// 通过静态函数获取类的引用
MyClass& myInstance = MyClass::GetInstance();
// 使用myInstance进行操作...
return 0;
}
在这个示例中,GetInstance()
静态成员函数创建并返回了一个静态局部变量 instance
的引用,确保了该类只有一个实例,并且在首次调用该函数时初始化。私有的构造函数和删除复制构造函数及赋值运算符重载函数都有助于防止直接的实例化和复制。
三、C++ 11 新特性
1.std::function<void()>
std::function<void()>
是 C++11 中引入的标准库模板类,它是一个通用的函数包装器,可以用来存储和调用任意可调用对象,如函数、函数对象、Lambda 表达式等,它们的返回类型是 void
,且没有参数。
这个模板类是定义在 <functional>
头文件中的,可以通过使用 std::function<void()>
来声明一个函数包装器,使其能够存储返回类型为 void
,无参数的可调用对象。
以下是 std::function<void()>
的一些重要特性和用法:
-
声明函数包装器:
cpp#include <functional> std::function<void()> myFunction;
这里声明了一个名为
myFunction
的std::function
,它可以包装返回类型为void
,无参数的可调用对象。 -
赋值函数对象或Lambda表达式:
cppstruct MyFunctor { void operator()() { std::cout << "Hello from functor!" << std::endl; } }; myFunction = MyFunctor(); // 使用函数对象 // 或者 myFunction = []() { std::cout << "Hello from lambda!" << std::endl; }; // 使用Lambda表达式
-
调用函数包装器:
cppmyFunction(); // 将会调用所包装的可调用对象
-
判断函数包装器是否为空:
cppif (myFunction) { // myFunction 不为空,可以调用 myFunction(); }
-
重设或清除函数包装器:
cppmyFunction = nullptr; // 或 myFunction = std::function<void()>(); // 现在 myFunction 变为空,不能再调用
-
使用
std::bind
绑定带参数的函数:cpp#include <iostream> #include <functional> void greet(const std::string& name) { std::cout << "Hello, " << name << "!" << std::endl; } int main() { std::function<void(const std::string&)> greetFunction = std::bind(greet, "Alice"); greetFunction(); // 输出:Hello, Alice! return 0; }
这里我们通过
std::bind
将一个带参数的函数greet
绑定到了greetFunction
上,使得它成为一个没有参数的函数包装器,但在调用时会传递预先绑定的参数。
总之,std::function<void()>
是一个功能强大的工具,可以用于在运行时存储和调用不同类型的可调用对象,特别是在需要根据运行时条件来动态地选择和执行函数时非常有用。
对于简单的情况,直接调用函数 initA()
和 initB()
是更加直观和方便的做法,没有必要使用 std::function<void()>
这样的函数对象。
std::function<void()>
更适用于以下场景:
-
函数对象的运行时选择: 当需要在运行时根据条件或配置选择不同的初始化函数时,可以使用
std::function<void()>
来存储并动态调用相应的函数。例如,可以根据配置文件或用户输入来选择执行不同的初始化函数,而不需要修改代码逻辑。 -
作为参数传递: 如果某个函数需要接受一个可调用对象作为参数,但这个可调用对象的具体类型是不确定的,那么可以使用
std::function<void()>
作为参数类型。这样,调用者可以传递任意的函数、函数对象或 Lambda 表达式,而函数内部可以通过std::function
来调用这个传递进来的可调用对象。 -
作为返回值: 类似地,如果某个函数需要返回一个可调用对象,但具体返回哪个函数或函数对象是根据一些条件决定的,可以使用
std::function<void()>
作为返回类型,灵活返回不同的函数或函数对象。 -
函数指针替代: 在一些历史遗留代码或与 C 接口交互的情况下,可能需要将函数指针封装成更安全的
std::function<void()>
对象,以方便使用和管理。
总结来说,std::function<void()>
更适合需要在运行时动态选择或处理可调用对象的情况。对于固定、静态的函数调用,直接调用函数更简单明了。使用 std::function
主要是为了更大的灵活性和通用性。
2. =default(), =delete
在 C++ 中,当类的成员函数被声明时,我们可以使用 = default
和 = delete
来指定它们的默认行为或删除该函数。这些用法是 C++11 中引入的特性。
-
= default
: 当我们在类中的成员函数声明后面使用= default
,表示我们希望编译器生成默认的函数实现。这主要用于特殊成员函数(默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数)。当我们显式地声明类的某个特殊成员函数时,编译器不会再自动生成该函数。但是,如果我们在函数声明后使用= default
,编译器将会自动生成该函数的默认实现。cppclass MyClass { public: // 默认构造函数 MyClass() = default; // 拷贝构造函数 MyClass(const MyClass& other) = default; // 拷贝赋值运算符 MyClass& operator=(const MyClass& other) = default; // 默认析构函数 ~MyClass() = default; // 其他函数声明 };
-
= delete
: 当我们在类的成员函数声明后面使用= delete
,表示我们禁用了该函数,使得它不能被调用。这通常用于阻止某些不合适的操作,或者是防止某些函数的隐式调用。使用= delete
可以让编译器在尝试调用该函数时产生编译错误。cppclass MyClass { public: // 禁用默认构造函数 MyClass() = delete; // 禁用拷贝构造函数 MyClass(const MyClass& other) = delete; // 禁用拷贝赋值运算符 MyClass& operator=(const MyClass& other) = delete; // 其他函数声明 };
使用 = default
和 = delete
可以在设计类的接口时,更加精确地控制特殊成员函数的生成和可用性,从而增加代码的可读性和安全性。
3.std::enable_shared_from_this
在C++中,std::enable_shared_from_this
是一个模板类,它提供了一种方式,使得在一个继承了该类的对象中,能够安全地获取一个指向其 std::shared_ptr
的引用。这在处理基于共享指针的资源管理和对象生命周期控制时非常有用,特别是在对象之间存在互相引用的情况下。
这个模板类通常与 std::shared_ptr
一起使用,以确保在对象删除之前,共享指针的引用计数能够正确地管理。
以下是 std::enable_shared_from_this
的基本用法和示例:
cpp
#include <iostream>
#include <memory>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::shared_ptr<MyClass> getShared() {
return shared_from_this(); // 获取指向当前对象的 shared_ptr
}
void print() {
std::cout << "Hello from MyClass" << std::endl;
}
};
int main() {
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2 = ptr1->getShared();
ptr1->print();
ptr2->print();
return 0;
}
在这个例子中,MyClass
继承自 std::enable_shared_from_this<MyClass>
,这就允许 MyClass
对象内部调用 shared_from_this()
方法来获得指向自身的 std::shared_ptr
。通过这种方式,可以在不增加引用计数的情况下获得对象的共享指针,避免出现循环引用导致的内存泄漏。
需要注意以下几点:
- 必须使用
std::shared_ptr
来管理对象,否则shared_from_this()
将导致未定义行为。 - 对象必须通过
std::shared_ptr
进行共享,而不能通过裸指针或其他智能指针进行管理。 - 在对象的生命周期中,只能调用一次
shared_from_this()
,否则会导致未定义行为。
总之,std::enable_shared_from_this
为共享指针的资源管理提供了一种更加安全和便捷的方法,尤其适用于涉及循环引用的情况下。