一、多线程
std::thread()、join() 的用法:使用std::thread()可以创建一个线程,同时指定线程执行函数以及参数,同时也可使用lamda表达式。
#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()函数。
#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
的用法如下:
#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
来实现线程安全的标志变量:
#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()
: 唤醒所有等待该条件的线程。
简单示例:
#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()
会自动释放锁,允许其他线程进入临界区,并在条件满足后重新获取锁。
假唤醒(Spurious Wakeup)是多线程编程中的一种现象,它发生在一个线程在等待条件变量时,即使没有明确的通知,也会偶尔被唤醒。
主要特征包括以下几点:
-
未预期的唤醒:在使用条件变量的
wait()
函数等待条件变为真时,有时候线程可能会在条件没有满足的情况下被唤醒,尽管没有其他线程调用notify()
或notify_all()
来通知等待线程。 -
不可预测性:假唤醒是不可预测的,因此无法在代码中准确地控制或避免。这意味着您不能简单地依赖于假唤醒,而需要额外的条件检查来验证等待的条件是否真的满足。
为什么假唤醒会发生?
假唤醒通常是由于操作系统或线程库的内部实现所导致的。在一些操作系统中,条件变量的实现可能会有一些限制或不确定性,导致了假唤醒的发生。这通常是为了在某些情况下提高性能或简化实现,但也需要程序员注意和处理。
如何防止假唤醒?
为了防止假唤醒,通常需要在等待条件变量的同时进行额外的条件检查,以确保条件变量的满足是真实的,而不仅仅是一个假唤醒。这可以通过一个循环来实现,直到条件满足为止:
std::unique_lock<std::mutex> lock(mutex);
while (!condition_is_met) {
condition_variable.wait(lock);
}
// 现在条件满足,继续执行
这种方式会在每次唤醒后重新检查条件,如果条件不满足,线程会继续等待,从而避免了假唤醒引起的问题。
另外一种防止假唤醒的方式,和上面原理一样:
std::unique_lock<std::mutex> lock(mutex);
condition_variable.wait(lock, [&]() {
//回调 lambda 表达式用于检查条件
return !condition_is_met;
});
// 现在条件满足,继续执行
总之,假唤醒是多线程编程中的一种常见现象,可以通过在等待条件时进行额外的条件检查来防止。注意处理假唤醒是确保多线程程序正确性和可靠性的重要一步。
二、基础语法知识
2.1 static的用途
当应用于不同上下文中,static
关键字在C++中具有不同的含义和用法。下面是对 static
的几种常见用法的总结:
-
静态成员变量:
- 在类中声明的静态成员变量是类的所有实例共享的,而不是每个实例独立拥有的。
- 静态成员变量在类的所有实例之间保持相同的值。
- 静态成员变量可以通过类名或类的实例来访问。
-
静态成员函数:
- 静态成员函数与类的实例无关,只能访问类的静态成员变量和其他静态成员函数。
- 静态成员函数在调用时不需要通过类的实例来调用,可以直接使用类名调用。
-
静态局部变量:
- 静态局部变量是在函数内部声明的变量,但只在第一次进入该函数时初始化,之后函数退出再次进入时保持上次的值。
- 静态局部变量在函数调用之间保持状态,可用于保留跨多次函数调用的信息。
-
静态全局变量:
- 在函数外部声明的静态全局变量只能在声明它的文件内可见,不会受到其他文件的影响。
- 静态全局变量在整个程序运行期间保持其值,不会受到函数调用的限制。
-
静态类成员(C++17起):
- 在类中声明的静态成员可以用
inline
关键字指定为内联。 - 静态类成员可以在类的定义中直接初始化,无需在类外进行初始化。
- 在类中声明的静态成员可以用
总之,static
在C++中有多种用法,可以用于创建静态成员、静态函数、静态局部变量以及限定全局变量的作用域。根据不同的上下文,static
提供了不同的功能和特性。
其中单例模式中用到了静态成员函数
在C++中,你可以在类中定义一个静态成员函数,并且让这个静态成员函数返回该类的引用。下面是一个示例代码来展示如何实现这一点:
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
的引用,确保了该类只有一个实例,并且在首次调用该函数时初始化。私有的构造函数和删除复制构造函数及赋值运算符重载函数都有助于防止直接的实例化和复制。
2.2 枚举类型
在C++中,枚举(enumeration)类型是一种用户定义的类型,它用于为程序中的整数值指定更具有可读性的名字。枚举类型的内存分配和它们是对象所有还是类所有的问题,可以从几个方面来解析。
枚举类型的内存分配
-
定义阶段:当你定义一个枚举类型时,实际上并没有分配内存。这是因为枚举类型定义仅仅是在编译期间为编译器提供了一种方式,以知道枚举中的名字对应的整数值。内存分配发生在你创建枚举类型的变量时。
-
变量声明阶段:当你声明一个枚举类型的变量时,内存会为该变量分配。枚举变量的大小通常是足够存储最大整数值的基本数据类型的大小。在大多数平台上,这通常是和
int
类型相同的大小,即通常为4字节。但这并不是强制性的,编译器可以选择最合适的大小以优化存储和性能。
枚举类型和枚举变量的所有权
-
枚举类型:枚举类型是全局定义的或在命名空间、类等中定义的。它们本身并不属于某个对象或类,而是定义了一组命名的整数常量。因此,可以说它们既不是对象所有也不是类所有,而是属于定义它们的作用域。
-
枚举变量:枚举变量是根据枚举类型声明的变量,它们可以是全局变量、局部变量、类的成员变量等。如果枚举变量被声明为类的成员变量,则它们是对象的一部分,每个类的对象都会拥有自己的枚举变量副本(除非该枚举变量被声明为
static
,在这种情况下,它是类所有的,所有对象共享同一个枚举变量)。
总结
- 枚举类型本身在定义时不分配内存,内存分配发生在枚举类型的变量声明时。
- 枚举类型不是对象或类所有,它们属于定义它们的作用域。
- 枚举变量可以是对象所有(作为类的非静态成员变量)或类所有(作为类的静态成员变量)。
这种方式使得枚举类型非常适合在程序中定义一组命名的常量,提高代码的可读性和维护性。
2.3 const char* m_file修饰不同的位置
const char* m_file
在C++中是一个指针声明,其中m_file
是指向char
类型的指针,用于存储字符串或字符数组的地址。const
关键字用于修饰指针指向的内容是常量,意味着通过这个指针不能修改指向的内容。然而,const
可以放在不同的位置来修饰指针的不同方面,这会影响你能否修改指针本身和指针所指向的内容。
-
const char* m_file
: 这表示m_file
是一个指针,指向const char
类型的数据。这意味着你不能通过这个指针修改所指向的字符内容,但是可以修改指针本身指向另一个地址。 -
char* const m_file
: 这里,const
修饰m_file
指针本身,意味着指针m_file
一旦初始化后,就不能再指向另一个地址,但是你可以通过这个指针修改它所指向的内容。 -
const char* const m_file
: 在这个声明中,第一个const
修饰指针所指向的内容,表示内容是不可修改的,而第二个const
修饰指针本身,意味着指针也是不可修改的。这样,既不能通过指针修改所指向的内容,也不能修改指针本身指向其他地址。
总结:
- 如果
const
位于*
之前,它修饰的是指针所指向的内容,使内容成为常量。 - 如果
const
位于*
之后或指针名称之前,它修饰的是指针本身,使指针成为常量。 - 使用
const
可以增加程序的安全性,防止意外修改数据,特别是在处理字符串和数组时非常有用。
2.4 虚析构函数的作用
虚析构函数在C++中是一个非常重要的概念,主要用于面向对象编程中的多态性管理。具体来说,虚析构函数的作用主要体现在以下几个方面:
-
确保对象的正确析构:在继承关系中,当使用基类指针(或引用)指向派生类对象,并通过基类指针删除对象时,若基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这将导致派生类中分配的资源没有被正确释放,引发资源泄露。如果基类的析构函数是虚析构函数,那么在删除对象时,会首先调用派生类的析构函数,然后再调用基类的析构函数,从而确保所有相关资源都被正确释放。
-
支持多态性:多态性是面向对象编程中的一个核心概念,允许通过基类的指针或引用来操作派生类的对象。虚析构函数是实现多态性的一种机制,它确保无论对象的实际类型是什么,对象的析构都能通过基类的指针或引用来正确进行。
-
避免未定义行为:如果基类的析构函数不是虚的,而我们通过基类的指针来删除派生类的对象,这将是未定义行为(Undefined Behavior),可能导致程序崩溃或其他不可预期的结果。通过将析构函数声明为虚函数,可以避免这种未定义行为。
简而言之,虚析构函数的作用是确保在多态使用场景下,当通过基类指针或引用删除对象时,能够正确地调用到派生类的析构函数,从而正确地释放对象及其所占用的资源,保证程序的稳定性和资源的有效管理。这是C++中管理动态分配对象生命周期的重要机制之一。
2.5 volatile指针
volatile
关键字在C和C++编程语言中用于告诉编译器,一个变量的值可能以程序未明确指定的方式改变。这可能是由于硬件设备、操作系统或在同一程序中运行的其他线程的操作。因此,编译器不应对这些变量的访问做任何优化,每次访问时都应该直接从其内存地址读取它们的值。
当我们谈论volatile
指针时,主要有两种用法:
-
指向
volatile
类型的指针:这意味着指针指向的数据是volatile
的,即数据可能会在程序的控制之外改变。因此,每次通过指针访问该数据时,程序都会直接从内存中读取数据,而不是使用可能已存储在寄存器中的任何缓存值。语法示例为volatile int *ptr;
,这里ptr
是一个指针,指向一个volatile int
类型的变量。 -
volatile
修饰的指针:这种情况下,指针本身被认为是volatile
的。这意味着指针的值(即它指向的地址)可能会意外地改变。这在嵌入式系统中尤其有用,其中指针可能由硬件更改。语法示例为int * volatile ptr;
,这里ptr
本身是volatile
的,但它指向的int
类型数据不是volatile
的。
在实际应用中,volatile
指针的使用场景包括但不限于嵌入式系统编程、硬件寄存器访问、中断服务程序中和多线程应用中,以确保程序的正确执行和数据的一致性。
使用volatile
关键字时应当谨慎,因为它会禁用编译器优化,可能会影响程序的性能。只有在确实需要时才使用它,例如在需要直接与硬件交互或处理并发编程中共享资源的情况下。
普通的指针访问和volatile
修饰的指针访问在底层访问机制上本质相同,区别主要在于编译器对它们的处理。让我们先回顾一下普通指针的工作方式,然后比较volatile
修饰的指针和普通指针的区别。
普通指针的访问
当你使用普通指针访问内存时,编译器会根据需要进行优化。这些优化可能包括:
- 缓存值:如果编译器认为一个变量的值在连续的几次访问中不会改变,它可能只从内存中读取一次值,然后在后续的几次访问中使用这个缓存的值。
- 指令重排序:为了提高执行效率,编译器可能会改变指令的顺序,只要这种重排序不违反执行逻辑的前提下。
volatile
修饰的指针的区别
当指针被volatile
关键字修饰时,它告诉编译器被指向的数据或指针本身的值可能会意外改变。因此,编译器会在处理这些指针时禁用某些优化策略:
- 禁止使用缓存值:编译器会在每次通过
volatile
指针访问数据时直接从内存地址读取数据,而不使用任何缓存值。 - 禁止指令重排序:与
volatile
变量相关的读写操作不会被重排序,这保证了程序按照源代码中的顺序访问volatile
变量。
总结
- 普通指针访问允许编译器进行优化,可能会缓存数据值或重排序指令,这样做是为了提高程序的执行效率。
volatile
修饰的指针访问禁止了这些优化,确保每次访问都直接从内存中读取数据,以及保持访问的顺序,这对于硬件编程、中断处理和多线程环境中的数据一致性至关重要。
使用volatile
关键字主要是为了处理那些可能由程序外部因素(如硬件或其他线程)改变的数据。如果你的数据不会遇到这样的情况,使用普通指针和变量就足够了,这样可以让编译器优化你的程序,提高执行效率。
2.6 虚函数
当通过基类的指针或引用调用虚函数时,虽然操作看似是通过基类进行的,但实际上涉及的虚指针(vptr)和虚表(vtable)是基于对象的实际类型的,而不仅仅是基于你通过哪个类型(基类或派生类)的引用或指针进行调用。这是多态的核心所在。这里是详细的解释:
虚指针(vptr)和虚表(vtable)
- 虚指针(vptr):每个含有虚函数的对象都会在其内存布局中隐含一个指针(vptr),指向对应的虚函数表(vtable)。这个指针在对象构造时被自动设置,以指向该对象实际类型的vtable。
- 虚表(vtable):每个含有虚函数的类(包括基类和所有派生类)都有自己的虚表。虚表中包含了该类的所有虚函数的地址。如果派生类重写了基类的虚函数,则派生类的虚表中该函数的条目会被更新为指向派生类中该函数的实现。
多态的工作流程
- 对象实例化:当创建派生类的对象时,该对象内部的vptr被设置为指向派生类的vtable。即使这个对象是通过基类的引用或指针来引用的,它的vptr仍然指向派生类的vtable。
- 虚函数调用:当通过基类的引用或指针调用虚函数时,程序首先访问对象的vptr以找到当前对象实际类型的vtable。
- 查找虚表(vtable):通过vptr访问到的vtable已经是根据对象的实际类型(派生类)调整过的,因此包含了指向派生类中重写的虚函数的地址。
- 函数调用:最后,程序通过vtable中的地址调用正确的函数实现,即使是通过基类的指针或引用进行的调用。
实际例子
假设有基类Shape
和派生类Circle
,Circle
重写了Shape
的虚函数draw()
。
- 当创建
Circle
对象时,该对象的vptr被自动设置为指向Circle
的vtable,即使这个Circle
对象是通过Shape
类型的指针或引用来处理的。 - 调用
draw()
时,通过Shape
指针或引用,程序查看对象的vptr,找到Circle
的vtable,从而找到Circle::draw()
的地址,并调用这个函数。
因此,即使是通过基类的指针或引用进行调用,由于对象的vptr指向的是根据对象实际类型调整过的vtable,程序仍然能够找到并调用派生类中重写的虚函数。这就是动态多态如何工作的。
三、C++ 11 新特性
1.std::function<void()>
std::function<void()>
是 C++11 中引入的标准库模板类,它是一个通用的函数包装器,可以用来存储和调用任意可调用对象,如函数、函数对象、Lambda 表达式等,它们的返回类型是 void
,且没有参数。
这个模板类是定义在 <functional>
头文件中的,可以通过使用 std::function<void()>
来声明一个函数包装器,使其能够存储返回类型为 void
,无参数的可调用对象。
以下是 std::function<void()>
的一些重要特性和用法:
-
声明函数包装器:
#include <functional> std::function<void()> myFunction;
这里声明了一个名为
myFunction
的std::function
,它可以包装返回类型为void
,无参数的可调用对象。 -
赋值函数对象或Lambda表达式:
struct MyFunctor { void operator()() { std::cout << "Hello from functor!" << std::endl; } }; myFunction = MyFunctor(); // 使用函数对象 // 或者 myFunction = []() { std::cout << "Hello from lambda!" << std::endl; }; // 使用Lambda表达式
-
调用函数包装器:
myFunction(); // 将会调用所包装的可调用对象
-
判断函数包装器是否为空:
if (myFunction) { // myFunction 不为空,可以调用 myFunction(); }
-
重设或清除函数包装器:
myFunction = nullptr; // 或 myFunction = std::function<void()>(); // 现在 myFunction 变为空,不能再调用
-
使用
std::bind
绑定带参数的函数:#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
,编译器将会自动生成该函数的默认实现。class MyClass { public: // 默认构造函数 MyClass() = default; // 拷贝构造函数 MyClass(const MyClass& other) = default; // 拷贝赋值运算符 MyClass& operator=(const MyClass& other) = default; // 默认析构函数 ~MyClass() = default; // 其他函数声明 };
-
= delete
: 当我们在类的成员函数声明后面使用= delete
,表示我们禁用了该函数,使得它不能被调用。这通常用于阻止某些不合适的操作,或者是防止某些函数的隐式调用。使用= delete
可以让编译器在尝试调用该函数时产生编译错误。class 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
的基本用法和示例:
#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
为共享指针的资源管理提供了一种更加安全和便捷的方法,尤其适用于涉及循环引用的情况下。