cmu15445-bootcamp Note

前言

最近打算写写cmu15445课程的project,但是对于c++的很多新特性和深入用法不够熟悉,这里从cmu15445-bootcamp入手,重新熟悉c++

仓库地址:https://github.com/cmu-db/15445-bootcamp


一、文件结构 

仓库README中介绍了files分类,一组内的files覆盖了c++一方面的知识点。 

其中对于笔者来说难点在于移动语义、智能指针和锁。

二、References and Move Semantics

1. Move semantics(移动语义)

Move semantics(移动语义)是 C++11 引入的核心特性之一,它允许资源(如动态内存、文件句柄等)从一个对象高效地"移动"到另一个对象,而非进行昂贵的拷贝操作。其是一种优化技术,通过"窃取"临时对象(右值r-value)的资源来避免不必要的深拷贝。当源对象是临时对象(即将被销毁)时,直接接管其资源比创建新副本更高效。由于移动的特性,源对象则会处于有效但未指定状态。 

特性拷贝语义移动语义
操作深拷贝所有数据转移资源所有权
开销高(分配新内存+复制数据)低(仅指针/句柄交换)
源对象保持不变处于有效但未指定状态(通常为空)
适用场景需要两个独立副本时源对象是临时对象或不再需要时

std::move 是 C++11 引入的一个标准库函数,用于将一个对象显式地转换为右值引用(rvalue reference),从而允许资源的“移动”而不是“复制”。它本质上并不移动任何东西,而是告诉编译器:这个对象可以被“移走”,你可以调用移动构造函数或移动赋值运算符来处理它。启用移动语义

右值(rvalue) 传统上指的是只能出现在赋值表达式右侧的值。右值通常是临时对象或字面量,没有持久的内存地址。其不可寻址生命周期短暂(通常在表达式结束后就会被销毁)。C++11引入了右值引用(用&&表示),使得可以区分左值和右值:

void func(int& lvalue);      // 接受左值
void func(int&& rvalue);     // 接受右值

简而言之,std::move的主要作用为两点:类型转换启用移动语义

一些注意事项:

(1)不是实际移动操作std::move本身不移动任何东西,它只是将对象标记为可移动,对于一些没有移动语义的类型,实际上进行的操作还是拷贝。

int x = 10;
int y = std::move(x);  // 实际上还是拷贝,因为int没有移动语义

再比如仓库里move_semantics.cpp文件中,这样两行代码。这里并不会报错,因为std::move本身不移动数据,它只是将左值转换为右值引用,真正的移动发生在移动构造函数或移动赋值操作时。左侧为右值引用类型时,并没有触发移动操作。

std::vector<int> &&rvalue_stealing_ints = std::move(stealing_ints);

std::cout << "Printing from stealing_ints: " << stealing_ints[1] << std::endl;

但是如下情况则会报错:

std::vector<int> stealing_ints = {1, 2, 3};
std::vector<int> new_ints = std::move(stealing_ints);  // 实际移动发生

// 现在访问stealing_ints是危险的!
std::cout << stealing_ints[1] << std::endl;  // 未定义行为

(2)移动后的对象状态 

  • 对象仍处于有效但未定义状态

  • 基本类型不受影响

  • 标准库类型通常变为空状态

(3)不要重复使用

auto obj = std::move(x);
auto obj2 = std::move(x);  // 危险!x可能已被移动

文件move_semantics.cpp中设计这样一个函数:

void move_add_three_and_print(std::vector<int> &&vec) {
  std::vector<int> vec1 = std::move(vec);
  vec1.push_back(3);
  for (const int &item : vec1) {
    std::cout << item << " ";
  }
  std::cout << "\n";
}

其中函数参数设计为右值引用类型(&&),主要作用为明确函数意图"我将从这个参数中窃取资源"。且此时限定函数参数为右值引用,避免意外输入普通引用类型,而导致输入的vector被清空的情况。

2. Move constructors and Move assignment operators

移动构造函数和移动赋值运算符是在类内部实现的方法,用于有效地将资源从一个对象移动到另一个对象,通常使用 std::move。这些类方法接受另一个相同类型的对象,并将其资源移动到调用该方法的实例。

文件中示例:

Person(Person &&person)
      : age_(person.age_), nicknames_(std::move(person.nicknames_)),
        valid_(true) {
    std::cout << "Calling the move constructor for class Person.\n";
    // The moved object's validity tag is set to false.
    person.valid_ = false;
}

// Move assignment operator for class Person.
Person &operator=(Person &&other) {
    std::cout << "Calling the move assignment operator for class Person.\n";
    age_ = other.age_;
    nicknames_ = std::move(other.nicknames_);
    valid_ = true;

    // The moved object's validity tag is set to false.
    other.valid_ = false;
    return *this;
}

nicknames_为std::vector<std::string>类型,在移动构造函数中需要通过std::move以右值的方式传递,实现移动。

另外的注意点在于文件中通过= delete的方式禁用了负载构造函数和赋值运算,使得该类只能进行移动构造进行“复制”。

// We delete the copy constructor and the copy assignment operator,
// so this class cannot be copy-constructed.
Person(const Person &) = delete;
Person &operator=(const Person &) = delete;

三、C++ Standard Library (STL) Memory

1. unique_ptr

unique_ptr 是 C++11 引入的一种智能指针,用于管理动态分配的内存,它提供了独占所有权的语义,确保指针指向的对象有且只有一个所有者。其提供了更加安全的内存管理方法。当 unique_ptr 离开作用域时,会自动删除它所管理的对象,这是与常规指针有所不同的、更加安全的内存管理方法。独占则能避免双重释放等问题。

创建方法:

#include <memory>

// 创建一个指向int的unique_ptr
std::unique_ptr<int> ptr1(new int(10));

// 使用make_unique (C++14推荐)
auto ptr2 = std::make_unique<int>(20);

访问对象:

*ptr1 = 30;  // 解引用
int x = *ptr2;

ptr1->method();  // 如果指向的是类对象

所有权转移。unique_ptr 不能复制,但可以通过移动语义转移所有权:

auto ptr3 = std::move(ptr1);  // ptr1现在为nullptr

在 C++ 中,如果有两个 std::unique_ptr,分别为 ab,并且两者都不为空,当执行以下语句:

a = std::move(b);

会发生以下几件事:

1. 原来 a 拥有的资源会被释放

在赋值之前,a 原本所拥有的对象会被销毁(如果不是空指针)。这是因为 std::unique_ptr 的赋值操作符会先释放自己当前拥有的资源。

2. a 获得 b 的所有权

赋值语句中的 std::move(b)b 转换为右值引用,使得 a 可以接管 b 所拥有的资源。

3. b 变为空

b 失去其资源的所有权,并被置为空(即内部指针变为 nullptr),这是 unique_ptr 的语义:只允许一个所有者。

 2. shared_ptr

std::shared_ptr 是 C++11 引入的智能指针,用于管理动态分配对象的共享所有权。多个 shared_ptr 可以指向同一个对象,并通过引用计数自动管理对象的生命周期。基本用法与unique_ptr类似,但其可以进行复制构造,且有引用计数统计指向该对象的shared_ptr数量:

std::cout << p1.use_count();  // 输出当前引用计数

当其引用计数为0时,自动销毁指针指向的对象。

四、C++ Standard Library (STL) Synch Primitives

1. mutex

互斥锁,用于实现资源访问的互斥。一般使用lock()与unlock()操作。

2. scoped_mutex

std::scoped_lock 是一个互斥锁包装类,它提供了一种 RAII 风格的获取和释放锁的方法。这意味着,当对象被构造时,会获取锁;当对象被析构时,会释放锁。

int count = 0;
std::mutex m;

void add_count() {
  // The constructor of std::scoped_lock allows for the thread to acquire the
  // mutex m.
  std::scoped_lock slk(m);
  count += 1;

  // Once the function add_count finishes, the object slk is out of scope, and
  // in its destructor, the mutex m is released.
}

与其他锁的对比: 

类名多锁支持可手动 unlock用途
std::lock_guard简单作用域锁
std::unique_lock灵活控制锁生命周期
std::scoped_lock同时锁多个互斥量

由此可知,scoped_lock特点在于一次性锁定多个互斥量(mutex),并在作用域结束时自动释放,其可以避免死锁。这是作用域锁(scoped lock)模式的一种实现。

3. condition variable

condition_variable(条件变量)是C++标准库中用于线程同步的一种机制,定义在<condition_variable>头文件中。它允许线程在某个条件不满足时进入等待状态,直到其他线程通知条件可能已改变。

主要作用:

  • 一个线程可以在某个条件不满足时进入等待状态(阻塞),释放锁。

  • 另一个线程在改变条件后,通知等待的线程继续执行。

关键成员函数:

函数说明
wait(std::unique_lock<mutex>&, Predicate pred)阻塞当前线程,直到被通知(并且条件满足)
notify_one()唤醒一个等待的线程
notify_all()唤醒所有等待的线程

文件condition_variable.cpp中的例子:

void add_count_and_notify() {
  std::scoped_lock slk(m);
  count += 1;
  if (count == 2) {
    cv.notify_one();
  }
}

// This function, ran by the waiting thread, waits on the condition 
// count == 2. After that, it grabs the mutex m and executes code in
// the critical section.
// Condition variables need an std::unique_lock object to be constructed.
// std::unique_lock is a type of C++ STL synchronization primitive that
// gives more flexibility and features, including the usage with
// condition variables. Particularly, it is moveable but not copy-constructible
// or copy-assignable.
void waiter_thread() {
  std::unique_lock lk(m);
  cv.wait(lk, []{return count == 2;});

  std::cout << "Printing count: " << count << std::endl;
}

 其中,要使用 unique_lock 而不是 lock_guard,是因为 condition_variable::wait 需要 unique_lock(它可以解锁和重新加锁)。

对于 cv.wait ( lk, [ ]{ return count == 2; } ) 语句,在等待时,wait() 会:

  1. 自动释放互斥锁 m,让其他线程可以修改 count。

  2. 被唤醒时重新获取锁,并再次检查条件 count == 2(如果是虚假唤醒也能避免继续执行)。

4. std::shared_lock and std::unique_lock

这两个都是C++标准库中提供的锁管理类,定义在<mutex>头文件中,用于更安全和方便地管理互斥锁。简单来说就是一个是进行锁的独占管理,一个是锁的共享管理。

特性unique_lockshared_lock
所有权独占共享
配合的互斥量mutextimed_mutexshared_mutexshared_timed_mutex
锁定方式独占锁定共享锁定
灵活性高(可手动控制)中等
引入版本C++11C++14

文件rwlock.cpp中的实例为:

int count = 0;
std::shared_mutex m;

// This function uses a std::shared_lock (reader lock equivalent) to gain
// read only, shared access to the count variable, and reads the count
// variable.
void read_value() {
  std::shared_lock lk(m);
  std::cout << "Reading value " + std::to_string(count) + "\n" << std::flush;
}

// This function uses a std::unique_lock (write lock equivalent) to gain
// exclusive access to the count variable and write to the value.
void write_value() {
  std::unique_lock lk(m);
  count += 3;
}

其中可以看到,使用了shared_mutex共享锁。shared_mutex 是 C++17 引入的同步原语(定义在 <shared_mutex> 头文件中),它提供了比普通互斥量更精细的并发控制,特别适用于读多写少的场景。其主要使用场景就是上方代码中的读写同步问题,主要与shared_lock一起使用,实现共享锁。

需要注意:shared_lock 不能管理 std::mutex

五、Misc(杂项)

1. wrapper class

C++ 包装类是管理资源的类。资源可以是内存、文件套接字或网络连接。包装类通常使用 RAII(资源获取即初始化)C++编程技术。使用此技术意味着资源的生命周期与其作用域绑定。当包装类的实例被构造时,这意味着它所管理的底层资源可用;而当此实例被析构时,该资源也不可用。简单总结就是“用一个类来管理一个资源,资源与类同生死”。

文件中举了一个int*指针的包装类,类似于一种智能指针的简单模拟实现。

2. iterator

文件iterator.cpp中主要介绍迭代器的实现,实现了基本双向链表的迭代器。

六、demo files

文件s24_my_ptr.cpp中对移动语义、智能指针举了更多的例子,进行了更多讲解。可以在最开始先看这个文件对一些概念有整体理解,也可以最后看当做总结。其中有一些关于临时变量的销毁、悬挂引用的问题,未来可以再通过看这部分代码来熟悉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值