知识点汇总2

weak_ptr

std::weak_ptr 是 std::shared_ptr 的一个伴随类,它允许你观察一个 std::shared_ptr 所管理的对象,但不会延长该对象的生命周期。换句话说,std::weak_ptr 不控制其指向对象的引用计数。它主要用于解决 std::shared_ptr 之间的循环引用问题,以避免内存泄漏。
你不能使用 std::weak_ptr 来直接构造 std::shared_ptr 的原因主要有以下几点:
引用计数机制:std::shared_ptr 通过引用计数来管理对象的生命周期。当最后一个 std::shared_ptr 指向的对象被销毁或重置时,对象本身也会被销毁。而 std::weak_ptr 不参与引用计数,它仅仅观察 std::shared_ptr 的状态。如果允许用 std::weak_ptr 来构造 std::shared_ptr,那么就会破坏 std::shared_ptr 的引用计数机制,可能导致内存泄漏或其他未定义行为。
所有权问题:std::shared_ptr 表示对对象的共享所有权,而 std::weak_ptr 不表示所有权。如果允许从 std::weak_ptr 构造 std::shared_ptr,那么就会引入所有权模糊的问题。这意味着你无法清晰地知道哪个 std::shared_ptr 实际“拥有”对象,这会导致代码维护和调试变得困难。
安全性问题:如果 std::weak_ptr 观察的 std::shared_ptr 已经被销毁(即其指向的对象不再存在),那么尝试从这样的 std::weak_ptr 构造 std::shared_ptr 是不安全的,因为这将导致悬垂指针(dangling pointer),访问这样的指针会导致未定义行为。
不过,虽然你不能直接用 std::weak_ptr 来构造 std::shared_ptr,但你可以使用 std::weak_ptr 的 lock 成员函数来尝试获取一个 std::shared_ptr。这个函数会尝试获取对对象的共享所有权。如果对象仍然存在(即至少还有一个 std::shared_ptr 指向它),那么 lock 会返回一个指向该对象的 std::shared_ptr;否则,它会返回一个空的 std::shared_ptr。这种方式既保证了安全性,又允许你在需要时获取对象的共享所有权。
示例代码:

std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;

// 尝试从weak_ptr获取shared_ptr
std::shared_ptr<int> new_shared = weak.lock();
if (new_shared) {
    // new_shared 现在有效,可以安全使用
} else {
    // weak 观察的shared_ptr已经被销毁,new_shared 是空的
}

回调函数

在C++中,回调函数是一种通过函数指针或可调用对象(如函数对象、lambda表达式等)实现的机制,它允许一个函数(或可调用对象)作为参数传递给另一个函数,并在需要时由后者调用。回调函数在事件处理、异步编程、多线程、插件架构等场景中非常有用。
下面是一个简单的C++回调函数示例:

#include <iostream>
#include <functional>

// 定义一个回调函数类型
typedef std::function<void(int)> Callback;

// 一个接受回调函数的函数
void processData(int data, const Callback& callback) {
    // 对数据进行一些处理...
    std::cout << "Processing data: " << data << std::endl;

    // 调用回调函数
    callback(data);
}

// 一个简单的回调函数实现
void handleData(int data) {
    std::cout << "Handling data: " << data << std::endl;
}

int main() {
    // 将handleData作为回调函数传递给processData
    processData(10, handleData);

    // 使用lambda表达式作为回调函数
    processData(20, [](int data) {
        std::cout << "Lambda handling data: " << data << std::endl;
    });

    return 0;
}

在这个示例中,我们首先定义了一个回调函数类型Callback,它是一个std::function对象,接受一个int参数并返回void。然后,我们定义了一个processData函数,它接受一个整数和一个回调函数作为参数。在processData函数内部,我们对数据进行处理,并调用传递进来的回调函数。
在main函数中,我们展示了两种使用回调函数的方式:一种是将一个普通的函数(handleData)作为回调函数传递给processData;另一种是使用lambda表达式作为回调函数。这两种方式都允许我们在processData函数处理完数据后执行自定义的操作。

function包装器

function包装器在C++中通常指的是std::function,它是一个通用的、多态的函数包装器,能够对任何可调用对象(如函数、函数对象、lambda表达式等)进行包装。std::function提供了一种类型安全的、统一的方式来处理各种可调用对象,使它们能够被存储、复制和调用。
std::function的模板参数指定了被包装的可调用对象的返回值类型和形参类型。这使得std::function能够存储不同类型的可调用对象,并在需要时调用它们。
使用std::function包装器,你可以轻松地将可调用对象作为参数传递给其他函数,或者将它们存储在容器中,以实现更灵活和可重用的代码。此外,std::function还可以与std::bind或lambda表达式结合使用,以创建更复杂的可调用对象。
以下是一个简单的示例,演示了如何使用std::function包装器来包装一个函数并调用它:

#include <iostream>
#include <functional>

// 一个普通的函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 使用std::function包装add函数
    std::function<int(int, int)> func = add;
    
    // 调用包装后的函数
    int result = func(3, 4);
    std::cout << "The result is: " << result << std::endl;
    
    return 0;
}

在这个例子中,我们定义了一个名为add的函数,它接受两个整数参数并返回它们的和。然后,我们创建了一个std::function对象func,并使用add函数来初始化它。之后,我们可以通过调用func来间接地调用add函数,并将结果存储在result变量中。最后,我们打印出结果。
通过使用std::function,我们可以轻松地更改func所引用的可调用对象,而无需修改调用它的代码。这使得我们的代码更加灵活和可维护。

move

在C++中,std::move是一个函数模板,它并不真的移动任何东西,而是将其参数转换为右值引用。这是为了启用移动语义,允许对象通过移动而非复制来转移其资源。std::move在语义上表明,调用者不再需要当前对象的值,因此编译器可以安全地使用该对象的资源来进行优化,例如通过调用移动构造函数或移动赋值运算符。
std::move的典型用法如下:

#include <utility> // for std::move

// 假设有一个可以移动的对象,比如std::vector
std::vector<int> vec1 = {1, 2, 3, 4, 5};

// 创建另一个vector,并尝试通过移动vec1的内容来初始化它
std::vector<int> vec2(std::move(vec1));

// 现在,vec1不再包含有效数据,并且它的状态是未定义的,
// 但它是安全的,因为我们不再使用它,并且它的析构函数可以被安全调用。
// vec2现在包含原先vec1的数据。

需要注意的是,std::move并不保证一定会发生移动操作。它只是给编译器一个暗示,即可以尝试使用移动构造函数或移动赋值运算符。编译器仍然会检查移动操作是否安全。如果移动操作不安全(例如,源对象是一个左值,或者没有定义移动操作),那么编译器将回退到复制操作(如果可能)。
此外,std::move之后的对象仍然是一个有效的对象,但其值已经不再是确定的(除非通过其他方式重新赋值)。这是因为移动操作通常将源对象置于有效但未定义的状态,这意味着它可以被安全销毁,但不应再被使用。
最后,std::move不会改变其参数的物理位置或内存地址,它只改变参数的类别(从左值变为右值),从而允许使用移动语义。

右值引用

C++11中引入的右值引用是C++语言的一个重要特性,它极大地增强了C++在资源管理、性能优化和代码简洁性方面的能力。右值引用主要用于移动语义和完美转发。
1 . 右值引用和左值引用
在C++中,左值是可以取得地址的实体,例如变量、数组元素等。右值则是不能取得地址的临时对象,例如字面量、返回值等。左值引用是对左值的引用,而右值引用则是对右值的引用。
2. 移动语义
右值引用是实现移动语义的关键。在C++11之前,我们只能通过复制来传递或返回对象,这可能会涉及大量的内存复制操作,特别是对于大型对象或容器,性能开销很大。通过移动语义,我们可以避免不必要的复制,而是直接“窃取”资源(如内存)从一个对象到另一个对象。
C++11中,一些类库(如std::vector、std::string等)重载了移动构造函数和移动赋值运算符,这些函数接受右值引用作为参数,从而实现了移动语义。使用右值引用作为参数,编译器可以自动将临时对象(右值)绑定到右值引用,然后调用移动构造函数或移动赋值运算符,从而实现资源的快速转移。
3. 完美转发
完美转发是右值引用的另一个重要应用。在模板函数中,我们可能希望将参数原封不动地转发给其他函数,包括参数的类型和值类别(左值或右值)。通过使用右值引用和std::forward函数模板,我们可以实现完美转发。
4. 使用方法
在代码中,右值引用使用&&符号表示。例如:

void foo(int&& x) {
    // x 是一个右值引用
}

当传递一个右值给这个函数时,x就会绑定到这个右值。在函数内部,我们可以通过std::move将左值转换为右值,从而利用移动语义。
总的来说,C++11中的右值引用是一个强大的工具,它让我们能更高效地管理资源,优化性能,并写出更简洁、更易于理解的代码。

多态

多态的原理主要包括继承和重写两个方面。
继承允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。子类可以拥有父类的所有属性和方法,并可以在此基础上进行扩展。这种特性使得代码得以复用,提高了代码的灵活性和可维护性。
重写则是指子类可以重新实现或覆盖父类中已有的方法。这意味着当子类对象调用某个方法时,会执行子类中的方法实现,而不是父类中的方法。这种机制使得同样的方法调用在不同的对象上可能会产生不同的行为。
多态的核心在于“多种形式”,它相当于一个公共接口。当我们通过父类的引用或指针来操作子类对象时,这个引用或指针就可以表现出多种形态。具体来说,多态的实现依赖于虚函数表(vtable)和虚函数指针(vptr)。父类中声明为虚的函数会在vtable中存储其地址,子类重写父类的虚函数时,会更新vtable中对应的函数地址。当通过父类的引用或指针调用虚函数时,会根据实际对象的类型(即vptr所指向的vtable)来查找并执行正确的函数。
这种机制使得多态能够实现运行时多态性,即程序在运行时能够根据实际对象的类型来动态地决定执行哪个函数。这种动态性使得代码更加灵活和可扩展,能够适应不同的需求和场景。
总结来说,多态的原理是通过继承和重写机制,以及虚函数表和虚函数指针的配合使用,实现了运行时多态性。这使得程序能够根据对象的实际类型来执行不同的行为,提高了代码的灵活性和可维护性。

组合

使用组合而非继承:在某些情况下,使用组合(即一个类包含另一个类的对象)而不是继承可能更加合适。组合可以提供更灵活和可维护的代码结构,避免不必要的多态性。

自旋锁

自旋锁(Spin Lock)是一种专为防止多处理器并发而引入的锁机制,用于保护共享资源。它与互斥锁类似,都是为了解决对某项资源的互斥使用问题。在任何时刻,自旋锁最多只能有一个持有者,即只有一个执行单元能够获得锁。
自旋锁的核心特性在于,如果锁已被其他执行单元持有,当前请求获取锁的执行单元不会进入睡眠状态,而是会持续循环检查锁是否可用,进行忙等待,直到锁被释放并成功获取。这种持续循环检查的行为就像“自旋”一样,因此得名自旋锁。
自旋锁适用于锁持有时间较短,且线程切换开销较大的场景。然而,如果锁持有时间过长,或者线程切换开销较小,自旋锁的忙等待方式可能会浪费大量的CPU资源,降低系统性能。因此,在选择使用自旋锁时,需要根据具体的应用场景和需求进行权衡。
在操作系统或并发编程中,自旋锁常用于中断处理、临界区互斥保护等场景,以确保对共享资源的正确访问和避免数据竞争。

文件描述符

文件描述符是一个非负整数,它是内核为每一个进程所维护的、该进程打开文件的记录表的索引值。当程序打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符。在程序设计中,文件描述符常用于底层程序编写,特别是在涉及文件或socket的I/O操作中。文件描述符的作用是高效管理已被打开的文件,确保执行I/O操作的系统调用都能通过正确的文件描述符来指向被打开的文件。
具体来说,文件描述符在形式上是一个简单的整数,用以标明每一个被进程所打开的文件和socket。在程序刚刚启动时,0、1和2这三个文件描述符分别对应标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。随着更多的文件或socket被打开,它们的文件描述符会依次递增。
需要注意的是,Unix操作系统通常会给每个进程能打开的文件数量设定一个限制,这有助于系统资源的管理和优化。如果文件描述符用尽,可能会导致服务被拒绝或其他性能问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦想很美

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值