NDK 基础(五)—— C++ 高级特性2

news2024/11/19 3:49:52

1、左值右值

在 C++ 中,左值(lvalue)和右值(rvalue)是用于描述表达式的术语,它们与赋值操作和内存中对象的生命周期有关。

**左值(lvalue)**是指可以出现在赋值操作符左侧的表达式,它通常具有持久的身份(identity)和内存位置。左值可以是变量、对象、函数或表达式,它们具有可寻址(addressable)的属性,可以获取其地址。例如:

int x = 42;  // x 是左值
int* ptr = &x;  // &x 是左值,表示 x 的地址
int& ref = x;  // ref 是左值引用,绑定到 x

**右值(rvalue)**是指不能出现在赋值操作符左侧的表达式,它通常是临时的、没有持久身份的值。右值可以是字面量、临时对象、表达式的结果等。右值不能直接获取其地址,因为它们可能没有明确的内存位置。例如:

int y = 10 + 20;  // 10 + 20 是右值
int&& rref = 10;  // 10 是右值,rref 是右值引用,绑定到 10

在 C++11 中引入了右值引用(rvalue reference)的概念,它允许我们绑定到右值,并提供了移动语义和完美转发的能力。通过使用右值引用,我们可以对临时对象进行高效的转移语义操作,如移动构造和移动赋值。例如:

std::string str1 = "Hello";  // "Hello" 是右值,str1 是左值
std::string str2 = std::move(str1);  // std::move(str1) 是右值,使用移动构造将 str1 的内容转移到 str2

左值和右值的区分对于理解 C++ 的语义和语法非常重要。例如,在函数重载和模板参数推导时,左值和右值的区别可以决定调用哪个函数或模板实例。在 C++11 之后,右值引用的引入使得我们能够更好地利用右值,并实现高效的资源管理和移动语义操作。

为了加深理解,再来看下面的例子:

class Student {
private:
    string info = "AAA";

public:
    // 返回 info 的副本,而不是 info 成员本身
    string getInfo() {
        return this->info;
    }

    // 返回 info 的引用
    string &getInfoRef() {
        return this->info;
    }
};

int main() {
    Student student;
    // 第一种情况,修改 getInfo() 返回的副本
    student.getInfo() = "BBB";
    cout << "第一种情况结果:" << student.getInfo() << endl;

    // 第二种情况,修改 getInfoRef() 返回的引用
    student.getInfoRef() = "CCC";
    cout << "第二种情况结果:" << student.getInfoRef() << endl;

    return 0;
}

输出结果:

第一种情况结果:AAA
第二种情况结果:CCC

第一种情况,由于 getInfo() 实际上返回的是一个临时变量,是右值,不能被修改;第二种情况,由于 getInfoRef() 返回的是对象本身,是左值,因此修改后会直接影响 info 成员本身。

2、线程

C++ 的线程大致分为两种,原始的 pthread 和 C++11 新增的 std::thread:

  • pthread(POSIX Threads)是跨平台的线程库,不是 C++ 标准库的一部分。它是基于 POSIX 标准定义的线程接口,提供了一组函数用于创建、控制和同步线程;std::thread 是 C++ 标准库提供的跨平台线程支持的一部分,它提供了一种面向对象的方式来创建和管理线程,使得多线程编程更加方便和可移植
  • pthread 是 C 的库,而 std::thread 是 C++ 库中的类
  • pthread 是面向过程的,你需要使用函数指针作为线程的入口点,并手动管理线程的生命周期;std::thread 提供了面向对象的方式来创建和管理线程。你可以使用函数对象、成员函数、Lambda 表达式等作为线程的入口点,线程的生命周期由 std::thread 对象管理
  • pthread 是一个跨平台的 POSIX 标准库,可以在支持 POSIX 接口的操作系统上使用,但在某些平台上可能需要进行适当的配置和设置;std::thread 是 C++ 标准库的一部分,因此在支持 C++11 的编译器上是可用的,可以在不同的操作系统和平台上运行
  • pthread 需要手动处理线程中的异常;std::thread 提供了对异常的支持,如果在线程函数中抛出了异常并未被捕获,std::thread 会自动终止线程并将异常传播到主线程
  • pthread 具有更广泛的使用,AndroidNDK、Linux、JDK、JVM、Java 线程和 Native C++ 基本上还是在用 pthread;std::thread 本质上是对 pthread 的封装,并且封装的还不是很好
  • pthread 在 AndroidNDK 和 Linux 下是默认就支持的环境,在 CLion 配置了 Cygwin 的环境下也可使用,但是在 VS 和 mingw 就需要手动配置才能使用了

2.1 std::thread 的使用

thread 简单使用示例:

// 相当于 Java 中运行任务的 run()
void run(int number) {
    for (int i = 0; i < 5; ++i) {
        cout << "run:" << number << endl;
    }
}

int main() {
    // 第一种方式:main() 通过 sleep() 等待一段时间
    thread thread1(run, 100);
    sleep(3); // unistd.h
    cout << "main 弹栈了" << endl;
}

运行结果:

run:100
run:100
run:100
run:100
run:100
main 弹栈了
terminate called without an active exception

第二种方式:

int main() {
    // 第二种方式:main() 等 thread2 执行完毕后再执行
    thread thread2(run, 666);
    thread2.join();
    cout << "main 弹栈了" << endl;
}

运行结果:

run:666
run:666
run:666
run:666
run:666
main 弹栈了

使用方式跟 Java 非常像。不过注意,正式开发不要使用第一种方式让主线程等待子线程,要用第二种。

2.2 pthread 的使用

通过 pthread_create() 创建 pthread 对象:

int pthread_create(pthread_t *th, const pthread_attr_t *attr, void *(* func)(void *) start_routine, void *arg);

该函数的四个参数说明如下:

参数描述
th指向 pthread_t 类型的指针,用于存储新创建线程的标识符。创建成功后,该标识符可用于对线程进行操作,如等待或终止
attr指向 pthread_attr_t 类型的指针,用于指定新线程的属性。可以通过该参数设置线程的属性,例如线程的栈大小、调度策略等。如果不需要特别的线程属性,可以将该参数设置为 nullptr
start_routine指向函数指针,表示新线程的入口点函数。该函数必须具有以下签名:void *(*start_routine)(void *)。新线程将从该函数开始执行
arg传递给 start_routine 函数的参数。可以使用该参数向新线程传递任意类型的数据。通常可以将参数封装为一个结构体,并进行类型转换运行函数的参数。必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 nullptr

第三个参数的类型为 void *(* func)(void *) 是一个参数和返回值类型均为 void *,名称为 func 的函数指针。void * 可以接收任何数据类型的指针。

pthread_create() 的返回值是一个整数,表示创建线程的成功与否。如果成功创建新线程,返回值为 0;否则,返回的是一个非零的错误码,用于指示发生的错误类型。

使用 pthread_create() 函数时,一般的步骤如下:

  1. 创建一个 pthread_t 类型的变量,用于存储新线程的标识符。

  2. 可选地创建一个 pthread_attr_t 类型的变量,并设置线程的属性。如果不需要特殊的属性,可以将其设置为 nullptr

  3. 定义一个函数作为新线程的入口点函数,并确保其具有正确的参数和返回值。

  4. 调用 pthread_create() 函数,传递相应的参数。在成功创建线程后,thread 指向的变量将包含新线程的标识符。

  5. 处理创建线程时可能发生的错误,检查 pthread_create() 的返回值。

  6. 在必要时,使用线程标识符(pthread_t)对新线程进行操作,如等待线程完成、取消线程等。

示例代码:

#include <iostream>
#include <pthread.h>

using namespace std;

// 线程要执行的函数,相当于 Java 中的 run()
void *pthreadTask(void *pVoid) {
    // 将 pVoid 静态转换成 int *,然后再 * 取出 int
    int number = *static_cast<int *>(pVoid);
    cout << "异步线程执行了:" << number << endl;
    // 要返回 nullptr 或者 0,否则可能会出现未知错误
    return nullptr;
}

int main() {
    pthread_t pthreadId;
    int number = 666;
    pthread_create(&pthreadId, nullptr, pthreadTask, &number);
    // 让 pthreadId join 到主线程中,使主线程等待 pthreadId 执行
    // 完毕后再执行,否则可能看不到 pthreadId 的运行结果
    pthread_join(pthreadId, nullptr);
    return 0;
}

在使用 pthread 时要注意如下几点:

  1. pthread_t 相当于线程句柄,在 pthread_create() 创建线程成功后,pthreadId 才有一个有效值
  2. pthreadTask 这个函数指针,接收的参数和返回值类型都为 void *(因为要传给 pthread_create() 作为第三个参数,所以类型要符合 pthread_create() 的参数声明),那么函数内部就需要把参数的 void * 转换成真实的数据类型,并且在最后要有返回值 nullptr 或 0(nullptr 更好)
  3. number 作为 pthread_create() 的第四个参数,实际上是传给 pthreadTask() 作为其参数的。如果 pthreadTask() 没有形参,则第四个参数就可以传 nullptr
  4. nullptr 是 C++11 标准引入的关键字,是 C++ 标准库的一部分,用来表示空指针的字面值常量。在早期的 C++ 标准中,使用 NULL0 来表示空指针。然而,nullptr 的引入提供了更好的类型安全性,因为它没有与整数类型之间的隐式转换。使用 nullptr 可以明确指示一个指针不指向任何对象,在条件判断中,nullptr 可以和指针进行比较,判断指针是否为空
  5. main() 中使用 pthread_join() 让主线程在 pthreadId 线程执行完毕后再执行,这样可以避免 main() 先于 pthreadId 执行完而看不到 pthreadId 内输出的文字

2.3 分离线程与非分离线程

在 std::thread 多线程编程中,线程可以分为分离线程(detached thread)和非分离线程(joinable thread)两种类型。这两种类型的线程在生命周期和资源管理方面有所不同。

  1. 非分离线程(joinable thread)

    • 非分离线程是默认类型的线程。当线程创建时,它是非分离的。
    • 非分离线程在其执行结束后仍然存在并保持其状态,直到其他线程调用 std::thread::joinstd::thread::detach 来回收其资源。
    • 通过调用 std::thread::join,主线程可以等待非分离线程执行完成,以确保线程的执行完毕。
    • 如果不调用 std::thread::joinstd::thread::detach,并且线程对象的析构函数被调用,则程序会终止并引发 std::terminate
    • 非分离线程可以访问主线程栈上的局部变量,但需要注意线程安全问题。
  2. 分离线程(detached thread)

    • 分离线程是通过调用 std::thread::detach 将非分离线程转换为分离线程。
    • 分离线程在其执行结束后会自动释放所有资源,无需其他线程调用 std::thread::joinstd::thread::detach
    • 分离线程不会阻塞主线程的执行,主线程也不会等待分离线程的结束。
    • 分离线程在执行完毕后自行回收资源,因此不能访问主线程栈上的局部变量。

下面是一个示例,展示了分离线程和非分离线程的用法:

#include <iostream>
#include <thread>

void ThreadTask() {
    std::cout << "执行线程任务" << std::endl;
}

int main() {
    // 非分离线程
    std::thread nonDetachedThread(ThreadTask);
    nonDetachedThread.join();  // 等待非分离线程执行完成

    // 分离线程
    std::thread detachedThread(ThreadTask);
    detachedThread.detach();  // 转换为分离线程

    // 主线程继续执行其他任务...

    return 0;
}

在上述示例中,首先创建了一个非分离线程 nonDetachedThread,并调用 join 等待其执行完成。然后,创建了一个分离线程 detachedThread,并调用 detach 将其转换为分离线程。在主线程中,可以继续执行其他任务,而不需要等待分离线程的结束。

如果需要等待线程执行完成并获取其结果,或者希望确保线程的执行顺序,那么非分离线程是更合适的选择。而如果线程的执行与主线程无关,且不需要等待其执行完成,那么可以选择分离线程以简化资源管理。

2.4 线程安全 —— 互斥锁

互斥锁英文以为 mutex lock,其中这个 mutex 是互斥量的简写,我们先简单了解一下互斥量。

2.4.1 什么是互斥量

mutex 是互斥量(mutual exclusion)的缩写,是一种用于实现线程同步的机制。它是一种可以协调多个线程对共享资源进行访问的工具,以避免并发访问导致的数据竞争和不一致问题。

互斥量的作用是确保在任意时刻只有一个线程可以访问被保护的共享资源。当一个线程想要访问共享资源时,它必须先获取互斥量的锁(lock),以确保其他线程不能同时访问该资源。当线程完成对共享资源的访问后,它释放互斥量的锁,以便其他线程可以获取锁并访问资源。

在 std::thread 中,互斥量的锁操作通常由两个函数完成:

  1. std::mutex::lock():尝试获取互斥量的锁。如果锁已被其他线程占用,则当前线程被阻塞,直到锁被释放。

  2. std::mutex::unlock():释放互斥量的锁,允许其他线程获取锁并访问共享资源。

下面是一个简单的示例,展示了互斥量的使用:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;  // 创建一个互斥量

void ThreadTask() {
    mtx.lock();  // 获取互斥量的锁

    // 访问共享资源
    std::cout << "线程执行开始" << std::endl;
    // ...
    std::cout << "线程执行结束" << std::endl;

    mtx.unlock();  // 释放互斥量的锁
}

int main() {
    std::thread t1(ThreadTask);
    std::thread t2(ThreadTask);

    t1.join();
    t2.join();

    return 0;
}

在上述示例中,创建了一个互斥量 mtx,然后在 ThreadTask 函数中使用 mtx.lock() 获取互斥量的锁,以确保线程之间互斥地访问共享资源。在访问共享资源结束后,通过 mtx.unlock() 释放互斥量的锁。

需要注意的是,获取互斥量的锁后,其他线程将被阻塞,直到锁被释放。这样可以确保同一时间只有一个线程访问共享资源,从而避免了数据竞争和不一致问题。

2.4.2 互斥锁

pthread 互斥锁使用步骤:

  1. 声明一个互斥锁对象 pthread_mutex_t mutex
  2. 初始化互斥锁 pthread_mutex_init(&mutex, nullptr)
  3. 对共享资源上锁 pthread_mutex_lock(&mutex),使用完毕后解锁 pthread_mutex_unlock(&mutex)
  4. 销毁互斥锁 pthread_mutex_destroy(&mutex)

示例代码:

// 互斥锁对象,在 Cygwin 平台中该对象不能有野指针
pthread_mutex_t mutex;
// 子线程数量
int num_thread = 10;
// 线程的共享资源 —— 队列
queue<int> threadQueue;

void *task(void *pVoid) {
    // 上锁,与解锁操作 pthread_mutex_unlock 配对
    pthread_mutex_lock(&mutex);
    int thread_num = *static_cast<int *>(pVoid);
    if (!threadQueue.empty()) {
        cout << thread_num << "号线程正在消费队列元素" << threadQueue.front() << endl;
        threadQueue.pop();
    } else {
        cout << "队列数据已被消费完毕~" << endl;
    }
    // 解锁
    pthread_mutex_unlock(&mutex);

    return nullptr;
}

int main() {
    // 构造队列中的数据
    for (int i = 0; i < num_thread; ++i) {
        threadQueue.push(i);
    }

    // 初始化互斥锁,与销毁互斥锁的 pthread_mutex_destroy 要成对出现
    pthread_mutex_init(&mutex, nullptr);

    // 创建 10 个子线程去访问队列中的数据
    pthread_t pthreadIds[num_thread];
    for (int i = 0; i < num_thread; ++i) {
        pthread_create(&pthreadIds[i], nullptr, task, &i);
    }

    // 等这 10 个子线程全执行完再执行主线程
    for (int i = 0; i < num_thread; ++i) {
        pthread_join(pthreadIds[i], nullptr);
    }

    // 销毁互斥锁,与 pthread_mutex_init 成对出现
    pthread_mutex_destroy(&mutex);
    cout << "main函数即将弹栈..." << endl;
    return 0;
}

输出结果:

3号线程正在消费队列元素0
5号线程正在消费队列元素1
6号线程正在消费队列元素2
6号线程正在消费队列元素3
7号线程正在消费队列元素4
8号线程正在消费队列元素5
8号线程正在消费队列元素6
9号线程正在消费队列元素7
10号线程正在消费队列元素8
10号线程正在消费队列元素9
main函数即将弹栈...

可以看到队列中的元素按照顺序被消费了,没有发生线程安全问题。

2.4.3 互斥锁 + 条件变量

通过互斥锁 + 条件变量可以实现 Java 中 wait-notify 的效果。以生产者消费者为例:

template<typename T>
class SafeQueue {
private:
    queue<T> queue;
    int capacity = 10; // 队列容量,默认为 10
    pthread_mutex_t mutex; // 定义互斥锁(不允许有野指针)
    // 条件变量,用于实现等待、读取等功能(不允许有野指针)
    pthread_cond_t condFull; // 队列已满时的条件
    pthread_cond_t condEmpty; // 队列为空时的条件
public:
    SafeQueue() {
        // 初始化锁和条件对象
        pthread_mutex_init(&mutex, nullptr);
        pthread_cond_init(&condFull, nullptr);
        pthread_cond_init(&condEmpty, nullptr);
    }

    SafeQueue(int capacity) : SafeQueue()/*, capacity(capacity)*/ {
        this->capacity = capacity;
    }

    ~SafeQueue() {
        // 回收锁和条件对象
        pthread_mutex_destroy(&mutex);
        pthread_cond_destroy(&condFull);
        pthread_cond_destroy(&condEmpty);
    }

    // 数据入队
    void enqueue(T data);

    // 队首数据出队
    void dequeue(T &t);
};

在 safe_queue.h 中初始化了一个锁 mutex 和两个条件 condFull、condEmpty,队列入队出队操作实现如下:

// 数据入队
template<typename T>
void SafeQueue<T>::enqueue(T data) {
    pthread_mutex_lock(&mutex);
    // 要用 while 循环而不是 if
    while (queue.size() >= capacity) {
        cout << "队列已满" << endl;
        // 让线程在 condFull 条件上等待,相当于 Java 的 wait
        pthread_cond_wait(&condFull, &mutex);
    }
    queue.push(data);
    // 已经成功将数据入队,广播通知等待在 condEmpty 条件
    // 上的所有线程,相当于 Java 中的 notifyAll
    pthread_cond_broadcast(&condEmpty);
    pthread_mutex_unlock(&mutex);
}

// 队首数据出队
template<typename T>
void SafeQueue<T>::dequeue(T &t) {
    pthread_mutex_lock(&mutex);
    while (queue.empty()) {
        // 队列为空时,外部线程无法消费数据,让它们等待
        cout << "队列为空,等待中..." << endl;
        pthread_cond_wait(&condEmpty, &mutex);
    }
    t = queue.front();
    cout << "消费了队首元素:" << t << endl;
    queue.pop();
    // 消费了元素,队列中有空位了,通知所有在队列已满
    // 条件下等待的线程
    pthread_cond_broadcast(&condFull);
    // 唤醒一个线程
//        pthread_cond_signal(&condFull);
    pthread_mutex_unlock(&mutex);
}

最后测试:

SafeQueue<int> safeQueue;

void *consume(void *) {
    int value;
    while (true) {
        safeQueue.dequeue(value);
        // 如果消费了 -1 就结束循环
        if (value == -1) {
            break;
        }
    }
    return nullptr;
}

void *product(void *) {
    int value;
    while (true) {
        cout << "输入要生产的信息:"<<endl;
        cin >> value;
        safeQueue.enqueue(value);

        // 如果生产了 -1 就结束循环
        if (value == -1) {
            break;
        }
    }
    return nullptr;
}

int main() {
    pthread_t consumerThread;
    pthread_t producerThread;
    pthread_create(&consumerThread, nullptr, consume, nullptr);
    pthread_create(&producerThread, nullptr, product, nullptr);
    pthread_join(consumerThread, nullptr);
    pthread_join(producerThread, nullptr);
    return 0;
}

3、智能指针

3.1 概述

C++11 开始提供了智能指针用来管理动态分配的对象的指针,它们可以自动处理内存的分配和释放,帮助避免内存泄漏和悬挂指针等问题。在 C++ 标准库中,有三种主要的智能指针类型:

  1. std::unique_ptr: std::unique_ptr 是独占指针,它拥有对动态分配对象的唯一所有权。当 std::unique_ptr 被销毁时(例如,超出其作用域),它会自动释放所拥有的对象。std::unique_ptr 不能被复制,但可以通过移动语义转移所有权。示例:

    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    std::cout << *ptr << std::endl;  // 输出: 42
    ```
    
  2. std::shared_ptr: std::shared_ptr 是共享指针,它可以多个指针共享对同一个对象的所有权。它使用引用计数来管理对象的生命周期,当最后一个 std::shared_ptr 被销毁时,它会自动释放所拥有的对象。std::shared_ptr 可以被复制和赋值。示例:

    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
    std::shared_ptr<int> ptr2 = ptr1;
    std::cout << *ptr1 << std::endl;  // 输出: 42
    std::cout << *ptr2 << std::endl;  // 输出: 42
    ```
    
  3. std::weak_ptr: std::weak_ptr 是一种弱引用指针,它指向一个由 std::shared_ptr 管理的对象,但不会增加引用计数。因此,它不影响对象的生命周期。std::weak_ptr 可以通过 std::shared_ptr 转换为临时的 std::shared_ptr 以访问对象。示例:

    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
    std::weak_ptr<int> weakPtr = sharedPtr;
    
    // 使用 weak_ptr 访问对象
    if (auto shared = weakPtr.lock()) {
        std::cout << *shared << std::endl;  // 输出: 42
    }
    ```
    

这些智能指针类型提供了更安全和方便的内存管理方式,可以减少手动管理内存的复杂性和错误。在使用智能指针时,应避免使用裸指针直接操作拥有智能指针的对象,以免破坏智能指针的引用计数和导致不确定的行为。

3.2 工作原理

三种指针与引用计数的关系:

  1. shared_ptr 修饰的对象,其引用计数会加 1,只要对象的引用计数不为 0,该对象就不会执行析构函数被回收
  2. weak_ptr 只能指向 shared_ptr 管理的对象,不会增加引用计数
  3. unique_ptr 独占一个对象,也就不需要引用计数了

shared_ptr 和 weak_ptr 可以通过 use_count() 获取到一个对象的引用计数,示例如下:

#include <iostream>
#include <memory> // 智能指针的头文件引入

using namespace std;

class Person {
public:
    ~Person() {
        cout << "执行 Person 析构函数" << endl;
    }
};

int main() {
    // 在堆区创建 Person 并在栈中分配指针指向它们
    Person *person1 = new Person();
    Person *person2 = new Person();

    // 栈区开辟智能指针
    // unique_ptr 独占 person1 对象
    unique_ptr<Person> uniquePtr(person1);
    // shared_ptr 会使 person2 对象的引用计数加 1
    shared_ptr<Person> sharedPtr(person2);
    weak_ptr<Person> weakPtr = sharedPtr;
    cout << "person2 引用计数:" << sharedPtr.use_count() << ","
         << "person2 添加 weak_ptr 后的引用计数:" << weakPtr.use_count() << endl;
    return 0;
    // main 弹栈后会自动执行 person1 和 person2 的析构函数
}

流程分析:

  • unique_ptr 独占式的持有 person1,当 main() 弹栈时,unique_ptr 的生命周期结束,它就会自动释放 person1
  • shared_ptr 共享式的持有 person2,每有一个 shared_ptr 持有 person2 时,person2 的引用计数就会加 1,因此这里 person2 的引用计数为 1;weak_ptr 不会增加对象的引用计数
  • 当 main() 弹栈时,shared_ptr 会结束自己的生命周期,同时对 person2 的引用计数减 1,使得 person2 的引用计数变为 0,此时就会调用 person2 的析构函数对其进行回收

测试结果:

person2 引用计数:1,person2 引用计数:1
执行 Person 析构函数
执行 Person 析构函数

3.3 shared_ptr 的循环引用问题

shared_ptr 通过引用计数的方式决定是否释放对象有一个问题,就是两个互相引用的对象会因为引用计数永远不会为 0 而无法被释放:

class Person2;

class Person1 {
public:
    shared_ptr<Person2> sharedPtr;

    ~Person1() {
        cout << "执行 Person1 的析构函数" << endl;
    }
};

class Person2 {
public:
    shared_ptr<Person1> sharedPtr;

    ~Person2() {
        cout << "执行 Person2 的析构函数" << endl;
    }
};

int main() {
    // 创建 Person1、Person2 对象
    auto *person1 = new Person1();
    auto *person2 = new Person2();
    // 创建 person1、person2 的 shared_ptr 指针
    shared_ptr<Person1> sharedPtr1(person1);
    shared_ptr<Person2> sharedPtr2(person2);
    // 查看引用计数
    cout << "前 sharedPtr1的引用计数是:" << sharedPtr1.use_count() << endl;
    cout << "前 sharedPtr2的引用计数是:" << sharedPtr2.use_count() << endl;
    // 让 person1 的 sharedPtr 指向 sharedPtr2,反之亦然
    person1->sharedPtr = sharedPtr2;
    person2->sharedPtr = sharedPtr1;
    // 再次查看引用计数
    cout << "后 sharedPtr1的引用计数是:" << sharedPtr1.use_count() << endl;
    cout << "后 sharedPtr2的引用计数是:" << sharedPtr2.use_count() << endl;
    return 0;
}

输出结果:

前 sharedPtr1的引用计数是:1
前 sharedPtr2的引用计数是:1
后 sharedPtr1的引用计数是:2
后 sharedPtr2的引用计数是:2

你可以看到,并没有执行 Person1、Person2 的析构函数,原因是在进行互相引用之后,main() 弹栈之后两个对象的引用计数由 2 减为 1。由于不是 0,因此并不会调用析构函数回收这些对象。画个图便于理解:

请添加图片描述

以 Person1 为例,它会被 main() 中的 person1 指针引用,还会被 Person2 对象中的成员指针 sharedPtr 引用,使得它的引用计数为 2。main() 弹栈后,person1 指针被销毁,销毁时会对 Person1 对象的引用计数减 1,但是因为 Person2 的 sharedPtr 还保持着对 Person1 的引用,因此 Person1 的引用计数为 1 不会被销毁。但实际上已经没有栈的指针指向 Person1 对象了,此时它应该被回收,但是因为在堆上的相互引用而导致 Person1 无法被回收,Person2 同理。

3.4 使用 weak_ptr 解决循环引用问题

解决方法很简单,既然是堆上的相互引用导致了对象无法被回收,那么就利用 weak_ptr 不会增加引用计数的特性,让两个类内部的智能指针从 shared_prt 改为 weak_prt:

class Person2;

class Person1 {
public:
//    shared_ptr<Person2> sharedPtr;
    // 使用 shared_ptr 可能会导致循环引用而无法回收,改用 weak_prt
    weak_ptr<Person2> weakPtr;

    ~Person1() {
        cout << "执行 Person1 的析构函数" << endl;
    }
};

class Person2 {
public:
//    shared_ptr<Person1> sharedPtr;
    // 使用 shared_ptr 可能会导致循环引用而无法回收,改用 weak_prt
    weak_ptr<Person1> weakPtr;

    ~Person2() {
        cout << "执行 Person2 的析构函数" << endl;
    }
};

int main() {
    // 创建 Person1、Person2 对象
    auto *person1 = new Person1();
    auto *person2 = new Person2();
    // 创建 person1、person2 的 shared_ptr 指针
    shared_ptr<Person1> sharedPtr1(person1);
    shared_ptr<Person2> sharedPtr2(person2);
    // 查看引用计数
    cout << "前 sharedPtr1的引用计数是:" << sharedPtr1.use_count() << endl;
    cout << "前 sharedPtr2的引用计数是:" << sharedPtr2.use_count() << endl;
    // 使用 weakPtr 互相引用
    person1->weakPtr = sharedPtr2;
    person2->weakPtr = sharedPtr1;
    // 再次查看引用计数
    cout << "后 sharedPtr1的引用计数是:" << sharedPtr1.use_count() << endl;
    cout << "后 sharedPtr2的引用计数是:" << sharedPtr2.use_count() << endl;
    return 0;
}

再次运行程序:

前 sharedPtr1的引用计数是:1
前 sharedPtr2的引用计数是:1
后 sharedPtr1的引用计数是:1
后 sharedPtr2的引用计数是:1
执行 Person2 的析构函数
执行 Person1 的析构函数

3.5 自己实现一个 shared_ptr

手写智能指针的目的在于夯实前面的 C++ 基础,因为这里会用到很多前面学到过的知识。我们一步一步来。

最基本功能

基本功能有如下几点:

  1. 我们需要一个能存放任意类型的指针对象 object 以及一个引用计数器 count
  2. 当智能指针本身结束了声明周期时,应该将引用计数器减 1,并且如果引用计数为 0 还要释放所持有的指针所指向的对象
  3. 提供获取引用计数的函数 use_count()

代码如下:

template<typename T>
class custom_shared_ptr {
private:
    // 智能指针持有的普通数据指针
    T *object;
    // 对 object 的引用计数
    int *count;

public:
    custom_shared_ptr(T *obj) : object(obj) {
        count = new int(1);
    }

    ~custom_shared_ptr() {
        if (--(*count) == 0) {
            // 回收 object 指向的对象
            if (object) {
                delete object;
                object = nullptr;
            }

            // 回收在堆上声明的 count
            delete count;
            count = nullptr;
        }
    }

    int use_count() {
        return *count;
    }
};

重写拷贝构造函数

除了声明时直接传入一个指针之外,还可以通过如下形式为智能指针赋值:

	auto *student2 = new Student();
    custom_shared_ptr<Student> sharedPtr3(student2);
    custom_shared_ptr<Student> sharedPtr4 = sharedPtr3;

sharedPtr4 不会调用构造函数,而是通过拷贝构造函数获取到 sharedPtr3 的内容。这种情况下,是 sharedPtr4 指向了 sharedPtr3,因此 sharedPtr3 的引用计数是要加 1 的,这个应该重写拷贝构造函数:

	// 拷贝构造函数,传入的是被拷贝的指针对象
    custom_shared_ptr(const custom_shared_ptr<T> &ptr) {
        cout << "执行 custom_shared_ptr 的拷贝构造函数" << endl;
        if (count == nullptr) {
            count = new int(0);
        }
        *count = ++(*ptr.count);
        object = ptr.object;
    }

重载 = 运算符

还有一种赋值的情况会对引用计数造成影响:

	auto *student1 = new Student();
    auto *student2 = new Student();
    custom_shared_ptr<Student> sharedPtr1(student1);
    custom_shared_ptr<Student> sharedPtr2(student2);
    sharedPtr2 = sharedPtr1;

// 视频里将 custom_shared_ptr<Student> sharedPtr2; 也算作一种情况,实际上和上述代码的处理方式是一样的。
// 只不过是调用了有参还是无参的构造函数的区别,但是在重载 = 的处理方式上是一样的。而且我自定义的例子中
// 根本也没有提供 custom_shared_ptr 的空参构造函数,因为空参会导致内部的 object 没有具体的指向对象,容易
// 出现野指针的问题……

sharedPtr2 = sharedPtr1 使得 sharedPtr2 指向 sharedPtr1,导致 student1 的引用计数加 1,student2 的引用计数减 1,需要通过重载运算符 = 来实现更新引用计数的逻辑:

	custom_shared_ptr &operator=(const custom_shared_ptr &ptr) {
        cout << "= 运算符重载" << endl;
        // 当前 custom_shared_ptr 会指向 ptr,因此
        // 给 ptr 的引用计数加 1
        ++(*ptr.count);

        // 当前 custom_shared_ptr 从所持有的指针的对象
        // 上移走了,因此引用计数要减 1
        if (--(*count) == 0) {
            if (object) {
                delete object;
                object = nullptr;
            }
            delete count;
            count = nullptr;
        }
        // 更新 object 和 count 为 ptr 的
        object = ptr.object;
        count = ptr.count;

        return *this;
    }

4、类型转换

C++ 的类型转换可以分为隐式转换和显式转换,隐式转换是指在某些情况下,C++ 编译器会自动执行的类型转换,比如隐式的将 int 转换为 double:

int num = 10;
double value = num; 

而显式转换需要通过 C++ 提供的四种显式类型转换操作符进行转换。

4.1 const_cast

常量转换,用于添加或移除变量的常量性:

void sample1() {
    const Person *p1 = new Person();
    // 报错:不能通过常量指针去修改对象
//    p1->name = "person1";
    // 解决方法:通过 const_cast 将 p1 转换为非常量指针的 p2
    Person *p2 = const_cast<Person *>(p1);
    p2->name = "person2";
    // 输出 p1.name = person2,成功修改了 p1 对象的内容
    cout << "p1.name = " << p1->name << endl;
}

4.2 static_cast

静态转换,用于基本类型之间的转换,以及具有继承关系的类型之间的转换:

class Parent {
public:
    void show() {
        cout << "Parent show" << endl;
    }
};

class Child : public Parent {
public:
    void show() {
        cout << "Child show" << endl;
    }
};

void sample2() {
    // 1.将 void * 转换为 int *
    int number = 99;
    void *pVoid = &number;
    int *pInt = static_cast<int *>(pVoid);
    cout << *pInt << endl;

    // 2.父类转换为子类
    auto *parent = new Parent;
    parent->show();
    // 静态转换是在编译期,转换后的类型是 = 左边声明的类型
    Child *child = static_cast<Child *>(parent);
    child->show();
    // 回收规则,一定是谁 new 了就回收谁
    delete parent;
}

输出:

99
Parent show
Child show

静态转换发生在编译期,因此在使用 static_cast 进行转换时,= 左侧声明的类型就是转换后的实际类型。

4.3 dynamic_cast

动态转换,用于在继承层次结构中进行安全的向下转型(downcast)。

使用时需要注意如下几点:

  1. 进行动态转换的前提是父类开启了多态,否则编译会报错 'Parent' is not polymorphic

    void test() {
        auto *parent = new Parent;
        // 'Parent' is not polymorphic
        auto *child = dynamic_cast<Child *>(parent);
    }
    
  2. 能够成功向下转型的前提是,对象本身就是子类对象,只不过前面用父类的指针指向了子类对象,在需要时才将父类的指针转换为子类指针。例如:

    void test() {
        // 1.不行,对象本身是父类,而不是子类
        auto *parent = new Parent;
        auto *child = dynamic_cast<Child *>(parent);
        
        // 2.可以,对象本身是子类
        auto *parent1 = new Child;
        auto *child1 = dynamic_cast<Child *>(parent1);
    }
    
  3. 动态转换有返回值,返回 null 表示转换失败

  4. 动态转换是运行时转换,对象的实际类型都要看 = 右侧的类型:

    void sample3() {
        // *parent 的实际类型是 = 右侧的 Parent
        auto *parent = new Parent;
        // auto *parent = new Child;
        // 试图将 *Parent 转换为 *Child,编译不会报错,但是
        // 转换会失败,只有将上面的注释打开,即 parent 本身
        // 实际上就是 Child 才能转换成功
        auto *child = dynamic_cast<Child *>(parent);
        if (child) {
            cout << "转换成功" << endl;
            child->show();
        } else {
            cout << "转换失败" << endl;
        }
    }
    

以下是正确的示例代码:

class Parent {
public:
    // 动态转换需要父类开启了多态
    virtual void show() {
        cout << "Parent show" << endl;
    }
};

class Child : public Parent {
public:
    void show() {
        cout << "Child show" << endl;
    }
};

void sample3() {
    auto *parent = new Child;
    auto *child = dynamic_cast<Child *>(parent);
    if (child) {
        cout << "转换成功" << endl;
        child->show();
    } else {
        cout << "转换失败" << endl;
    }
}

另外,动态转换子类转父类是可以的,还是以运行时看 = 右侧的标准为准,转换后实际上是子类型(即 = 右侧类型):

void sample4() {
    auto *child = new Child;
    auto *parent = dynamic_cast<Parent *>(child);
    if (parent) {
        cout << "转换成功" << endl;
        // 输出:转换成功 Child show
        parent->show();
    } else {
        cout << "转换失败" << endl;
    }
}

4.4 reinterpret_cast

用于将指针或引用转换为不同类型的指针或引用,通常用于低级操作或与底层代码的交互。需要谨慎使用,因为它不会进行任何类型检查。

示例代码:

void sample5() {
    int value = 42;

    // 使用 reinterpret_cast 进行指针类型转换
    int *intValuePtr = &value;
    char *charValuePtr = reinterpret_cast<char *>(intValuePtr);
    cout << charValuePtr << endl;

    // 使用 reinterpret_cast 进行引用类型转换
    int &intValueRef = value;
    char &charValueRef = reinterpret_cast<char &>(intValueRef);
    cout << charValueRef << endl;
}

两个输出都是 42 对应的 ASCII 字符 *。

reinterpret_cast 还有一种常用方法,就是在数值和对象之间相互转换:

void sample6() {
    // 将对象转换成值
    auto *parent = new Parent;
    auto parentValue = reinterpret_cast<uintptr_t >(parent);
    // 将值转换成对象
    auto *anotherParent = reinterpret_cast<Parent *>(parentValue);
    cout << parent << "," << anotherParent << endl;
}

输出的 parent 和 anotherParent 是同一个地址,证明转换前后是同一个 Parent 对象。此外,将对象转换成数值时使用了 uintptr_t 类型,而没有使用 long,因为在某些平台上,long 可能只能存储较小范围的整数值,无法容纳一个指针的完整地址。如果强行使用 long 会编译报错:

	auto *parent = new Parent;
	// Cast from pointer to smaller type 'long' loses information
    long parentLongValue = reinterpret_cast<long >(parent);

为了避免这个问题,我们才使用 uintptr_t 类型,它是一个无符号整数类型,足够大以容纳指针的完整地址。

5、nullptr

C++11 引入的关键字,用于表示空指针。它是一个字面常量,用于代表指针不指向任何有效对象或函数。

在早期的 C++ 版本中,通常使用整数常量 0 来表示空指针。然而,这样的表示方式可能引起一些问题,因为 0 也可以被解释为整数类型,导致在某些情况下发生歧义。

为了解决这个问题,C++11 引入了 nullptr。它具有以下特点:

  1. 类型安全:nullptr 是一个特殊的空指针类型,不会被隐式地转换为其他类型,从而避免了与整数类型的混淆
  2. 可用于指针和布尔上下文:nullptr 可以在指针上下文和布尔上下文中使用。在指针上下文中,nullptr 表示空指针;在布尔上下文中,nullptr 被解释为 false

nullptr 本意是替代 NULL 的,除此之外还有额外功能:

void show(int *i) {
    cout << " show(int * i) " << endl;
}

void show(int i) {
    cout << " show(int  i) " << endl;
}

int main() {
    show(6);
    show(nullptr);
    return 0;
}

输出:

 show(int  i) 
 show(int * i) 

这是因为 nullptr 被解释为空指针常量,其类型是 nullptr_t。在函数重载的情况下,编译器会选择最匹配的函数版本。由于 nullptr 可以隐式转换为任何指针类型,包括 int*,因此应该调用 void show(int* i) 函数。

此外,nullptr 还有一个重要作用就是作为指针的初始值。因为很多指针在初始化的时候并不知道要指向哪个对象,此时可以使用 nullptr 为其初始化,避免野指针的同时在后续判断指针是否已经初始化过也很方便:

	const char *name = nullptr;
	...
    if (name == nullptr) {
        *name = "xxx";
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1629559.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Vue3+Tres 三维开发】02-Debug

预览 介绍 Debug 这里主要是讲在三维中的调试,同以前threejs中使用的lil-gui类似,TRESJS也提供了一套可视化参数调试的插件。使用方式和之前的组件相似。 使用 通过导入useTweakPane 即可 import { useTweakPane, OrbitControls } from "@tresjs/cientos"const {…

PotatoPie 4.0 实验教程(21) —— FPGA实现摄像头图像二值化(RGB2Gray2Bin)

PotatoPie 4.0开发板教程目录&#xff08;2024/04/21&#xff09; 为什么要进行图像的二值化&#xff1f; 当我们处理图像时&#xff0c;常常需要将其转换为二值图像。这是因为在很多应用中&#xff0c;我们只对图像中的某些特定部分感兴趣&#xff0c;而不需要考虑所有像素的…

机器视觉系统-工业光源什么是同轴光

光路描述&#xff1a;反射光线与镜头平行&#xff0c;称为同轴光。 效果分析&#xff1a;光线经过平面反射后&#xff0c;与光轴平行地进入镜头。此时被测物相当于一面镜子&#xff0c;图像体现的是光源的信息&#xff0c;当“镜子“出现凹凸不平时&#xff0c;将格外地明显。 …

Win32 API 光标隐藏定位和键盘读取等常用函数

Win32 API 光标隐藏定位和键盘读取等常用函数 一、Win32 API二、控制台程序指令modetitlepausecls 三、控制台屏幕上坐标的结构体COORD四、句柄获取函数GetStdHandle五、控制台光标操作1.控制台光标信息结构体CONSOLE_CURSOR_INFO2.得到光标信息函数GetConsoleCursorInfo3. 设置…

会跳舞的网站引导页HTML源码

源码介绍 这套引导页源码非常好看&#xff0c;网址也不会不停的动起来给人一种视觉感很强烈 简单修改一下里面的地址就行看&#xff0c;非常简单&#xff01; 效果预览 源码下载 会跳舞的网站引导页HTML源码

排序FollowUp

FollowUp 插入排序 直接插入排序 时间复杂度:最坏情况下:0(n^2) 最好情况下:0(n)当数据越有序 排序越快 适用于: 待排序序列 已经基本上趋于有序了! 空间复杂度:0(1) 稳定性:稳定的 public static void insertSort(int[] array){for (int i 1; i < array.length; i) {int…

64位整数高低位的数据获取与赋值操作探讨

参考本篇->LOWORD和HIWORD函数_hidword-CSDN博客 一&#xff0c;如何获取一个64位整数的高32位和低32位 原理其实很简单&#xff1a; 解释一些概念 ①十六进制和二进制直接挂钩 一个十六位的十六进制数【0XAABBCCDD12345678】转为二进制的过程是把其中的每个数转为对应的二…

构建中小型企业网络-单臂路由

1.给IP地址配置好对应的IP和网关 2.配置交换机 3.路由配置 在交换机ge0/0/1中配置端口为trunk是可以允许多个vlan通过的&#xff0c;但路由器是不能够配置vlan&#xff0c;而交换机和路由器间连接的只有一根线&#xff0c;一个端口又只能配置一个ip地址&#xff0c;只有一个ip地…

人脸识别概念解析

目录 1. 概述 2. 人脸检测 3. 人脸跟踪 4. 质量评价 5. 活体检测 6. 特征提取 7. 人脸验证 8. 人脸辨识 1. 概述 人脸识别在我们的生活中随处可见&#xff0c;例如在大楼门禁系统中&#xff0c;它取代了传统的门禁卡或密码&#xff0c;提高了进出的便捷性和安全性。在商…

如何通过4G DTU实现现场仪表的分布式采集并发布到MQTT服务器

提供一份资料文档以一个具体的工程案例来讲解&#xff0c;如何通过4G DTU实现现场仪表的分布式采集并发布到MQTT服务器。采用的数据采集模块是有人物联的边缘采集4G DTU&#xff0c;采集多个多功能电表和远传水表的数据&#xff0c;通过MQTT通讯的型式传送给MQTT服务器&#xf…

Elsevier(爱思唯尔)期刊—Latex模板下载、使用、投稿过程

目录 一、Elsevier期刊-Latex模板下载 1. Elsevier 通用latex模板下载&#xff1a;【elsarticle.zip】 2. Elsevier 复杂版式latex模板下载&#xff1a;【els-cas-templates.zip】&#xff0c;有单栏和双栏版本 二、Elsevier期刊-Latex模板使用 1. 通用模板【elsarticle.zi…

【源码】IM即时通讯源码/H5聊天软件/视频通话+语音通话/带文字部署教程

【源码介绍】 IM即时通讯源码/H5聊天软件/视频通话语音通话/带文字部署教程 【源码说明】 测试环境&#xff1a;Linux系统CentOS7.6、宝塔、PHP7.2、MySQL5.6&#xff0c;根目录public&#xff0c;伪静态laravel5&#xff0c;根据情况开启SSL 登录后台看到很熟悉。。原来是…

如何在 Ubuntu 12.04 上使用 Apache 配置 WebDAV 访问

简介 WebDAV 是内置在 HTTP 中的分布式网络编辑实现&#xff0c;允许您轻松共享文件并与他人协作。 我们可以在 Web 服务器中安装此扩展&#xff0c;以允许通过 Web 浏览器远程读写访问本地文件。在本指南中&#xff0c;我们将在带有 Apache Web 服务器的 Ubuntu 12.04 VPS 上…

Mysql--创建数据库

一、创建一个数据库 “db_classes” mysql> create database db_classes; mysql> show databases; -------------------- | Database | -------------------- | db_classes | | information_schema | | mysql | | performance_schema | |…

PeLK: 大卷积核强势回归,高达101 × 101,提出了外围卷积

paper&#xff1a;https://arxiv.org/pdf/2403.07589 code&#xff1a;暂无 目录 0. 摘要 1. 引言 2. 相关工作 2.1. Large Kernel Convolutional Networks 2.2. Peripheral Vision for Machine Learning 3. 密集卷积优于条纹卷积 4. 参数高效的大核卷积神经网络 4.1. …

【Vue】如何使用Webpack实现打包操作

一、Webpack介绍 Webpack最主要的作用就是打包操作&#xff0c;由两个核心部分构成分别是“出口”与“入口”。wbepack是现在比较热门的打包工具了&#xff0c;它可以将许多松散耦合的模块按照依赖和规则打包成符合生产环境部署的前端资源。说的直白一点&#xff0c;通过webpac…

ansible-playbook离线升级centos内核

目录 概述实践ansible目录结构关键代码执行效果 结束 概述 内核离线包官网下载地址如下&#xff1a; 地址 实践 ansible目录结构 如对 ansible 不熟悉&#xff0c;离线包下载有问题&#xff0c;请至此地址下载&#xff0c;按本文操作可直接使用。 相关文章链接如下 文章地…

Java基础_集合类_List

List Collection、List接口1、继承结构2、方法 Collection实现类1、继承结构2、相关类&#xff08;1&#xff09;AbstractCollection&#xff08;2&#xff09;AbstractListAbstractSequentialList&#xff08;子类&#xff09; 其它接口RandomAccess【java.util】Cloneable【j…

Android Studio gradle 默认sourceSets配置

一. AS默认的sourceSets配置 sourceSets在Android插件中如何使用的&#xff1a;android {sourceSets {main {manifest.srcFile AndroidManifest.xmljava.srcDirs [src]resources.srcDirs [src]aidl.srcDirs [src]renderscript.srcDirs [src]res.srcDirs [res]assets.srcD…

企业智能名片小程序:AI智能跟进功能助力精准营销新篇章

在数字化浪潮的推动下&#xff0c;企业营销手段不断迭代升级。如今&#xff0c;一款集手机号授权自动获取、智能提醒、访客AI智能跟进及客户画像与行为记录于一体的企业智能名片小程序&#xff0c;正以其强大的AI智能跟进功能&#xff0c;助力企业开启精准营销的新篇章。 通过深…