前言

五一比较闲,就看了看 CS106L 这门课

懒得等今年的课,看的是 Spring 2021 的,因为没代码就只看了课件

记一点学到的新东西(其实只是凑篇blog


类似于一个普通变量,但有一个“没有值”的状态

类似 std::optional,但可选择含多种类型(同一时刻只有一种值,类似 union

类似 std::any,可含任意类型的值


  • 统一初始化(Uniform Initialization)(C++11)
    • 内部实现为 std::initializer_list
  • 结构化绑定(Structured binding) (C++17)

语法糖,简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
std::pair<int, double> v{1, 3.14};
auto [x, y] = v;
std::cout << x << ' ' << y << std::endl;
//1 3.14

struct Info
{
int a, b;
std::pair<int, char> c;
std::vector<int> d;
};
Info e{1, 2, {3, 's'}, {4, 5, 6}};

  • std::stringstream

类似 C 的 sscanfsprintf,在 std::string 上做流操作


  • std::vector<bool>

尽量不使用,底层实现为 bitset,导致部分功能无法正常使用

可使用 std::dequestd::bitset 代替


捕获当前作用域中部分变量值,以供 Lambda 函数使用

1
2
3
4
5
6
7
8
9
10
11
12
auto Lambda = [capture-values](arguments)
{
//do something
}

[x](arguments) //捕获 x 为常量
[&x](arguments) //捕获 x 为引用
[&x, y](arguments) //捕获 x 为引用, y 为常量
[&](arguments) //捕获所有作为引用
[&, x](arguments) //捕获除 x 以外的变量为引用, 捕获 x 为常量
[=](arguments) //捕获所有作为常量
//...

  • 特殊迭代器
    • std::inserterstd::front_inserterstd::back_inserter
    • std::istream_iteratorstd::ostream_iterator
1
2
std::copy(vec.begin(), vec.end(), std::back_inserter(vec2));//连接两个 vector
std::copy(vec.begin(), vec.end(), std::ostream_iterator<int>(std::cout, "\n"));//输出 vector

  • Anonymous namespace

匿名空间,可以将变量作用域限制在本文件内

1
namespace{...}

由编译器自动生成唯一名字,类似于:

1
2
namespace _UNIQUE_NAME{...}
using namespace _UNIQUE_NAME;

  • 内存泄漏?(这里只是简单记录一下,可能有错,仅供了解,实际问题更加复杂)

学 OI 的时候不用管,指针随便开,现在才感觉非常麻烦,C++ 中手动申请的内存不会自动回收,需要手动处理

简单例子(重载等于号时 struct 中的数组复制):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Info
{
int *a, Size;

Info(int _Size = 0): Size(_Size), a(new int[Size]){}

Info& operator=(const Info& o)
{
//a = o.a, Size = o.Size
//浅拷贝,无法复制内容,实质是指针共用一块地址
if(&o == this)return *this; //特殊情况
Size = o.Size;
delete[] a; //清除原有内存
a = new int[Size];
std::copy(o.a, o.a + Size, a); //深拷贝
return *this;
}

~Info(){delete[] a;} //析构函数中释放内存
};
  • 函数后加 = 运算符
1
2
3
4
5
6
7
8
struct Info
{
Info& operator=(const Info& o) = delete;
//删除函数,在有内存泄漏的风险下可用于防止直接复制 struct / class 等

virtual void Function() = 0;
//强制子类必须实现此函数
};

  • Move Semantics

考虑一段代码:

1
2
3
4
5
6
std::vector<int> Func()
{
//return something
}

std::vector<int> v = Func();

这段代码中 v 首先复制 Func()的内容,然后 Func() 销毁,比较浪费(copy 行为)

考虑有没有一种方法直接将 v 指向 Func() 的内容?(move 行为)

  • lvalue / rvalue(左值 / 右值)
    • lvalue:在内存中有地址,可置于 = 运算符的左边,一般生命周期为定义域
    • rvalue:在内存中无地址,只存在于 = 运算符的右边,一般生命周期为当前语句

For example:

1
2
3
4
5
6
int v1 = 2;			    //2: rvalue
int v2 = v1 * 2; //v1 * 2: rvalue
char v3 = getchar(); //getchar(): rvalue
int v4 = v1 += v2; //v1 += v2: lvalue
int *p = &v4;
v1 = *p; //*p: lvalue

通常引用只能指向 lvalue:

1
2
3
4
void Func(int& x){/*...*/}
int x = 3;
Func(x);
Func(3);//Compile Error

对 rvalue 的引用使用 &&:

1
void Func(int&& x){/*...*/}

那么 rvalue 的引用有什么用?

可以用于对 rvalue 实现 move 行为,以构造函数为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Info
{
int *a, Size;

Info(const Info& o): Size(o.Size)
{
a = new int[Size];
std::copy(o.a, o.a + Size, a);
}

Info(Info&& o): Size(o.Size)
{
a = o.a; //直接指向 rvalue 的地址,因为后续不再需要 rvalue
o.a = nullptr; //保证 rvalue 调用析构函数的正常销毁,不影响内存上的值
o.Size = 0;
}

~Info(){delete[] a;}
};

但是......注意到里面 o.a 是 lvalue,a = o.a 这一语句执行的依然是 copy 行为,尽管复制一个指针对效率影响不大,但是如果是复制一些特殊变量可能还会达不到目的,怎么样进行 move 行为?

头文件 <utility>

std::move 并不移动什么,实际上 std::move 作用是转为 rvalue 的引用,等同于 std::static_cast<T&&>(...)

于是可以将代码改为:

1
2
3
4
5
6
Info(Info&& o): Size(std::move(o.Size))
{
a = std::move(o.a);
o.a = nullptr;
o.Size = 0;
}

其他例子:

1
2
3
4
std::vector<int> v1{1, 2, 3}, v2;
v2 = std::move(v1);
std::cout << v2[2] << std::endl;
std::cout << v1[2] << std::endl; //v1 转为 rvalue 并 move 后销毁,不再可用

需要注意的是,一个对 rvalue 的引用本身是 lvalue:

1
2
3
4
5
std::vector<int> v1{1, 2, 3}, v2;
auto&& r = std::move(v1); //此处 r 为 lvalue
v2 = r; //执行 copy
std::cout << v2[2] << std::endl;
std::cout << v1[2] << std::endl; //未执行 move,依然可用

一种避免内存泄漏的方法,即所有的初始化由构造函数执行,销毁由析构函数执行(总会执行)

1
2
3
4
std::ifstream in;
in.open("data.txt");
//something might throw exception
in.close();

此语句不保证 in 能够正常关闭,可改为:

1
2
std::ifstream in("data.txt");
//something might throw exception

程序会自动调用析构函数,可以正常释放资源

  • Smart Pointers(智能指针)

C++ 提供的智能指针可以自动释放内存,避免内存泄漏

1
2
int* p = new int;
delete p;

可变为:

1
2
std::unique_ptr<int> p(new int);
//生命周期结束后自动释放

std::unique_ptr 不可复制,std::shared_ptr 可复制,std::shared_ptr 在同一对象的所有指针都销毁后才销毁对象。

std::weak_ptr 可指向 std::shared_ptr,但不影响 std::shared_ptr 的销毁机制

如何避免使用 newdelete 运算符呢?可使用 std::make_unique (C++14),std::make_shared (C++11) 代替(并且推荐)


  • Variadic templates (C++11)

可变参数模板,以求和函数为例:

1
2
3
4
5
6
7
8
9
10
template<typename T>
T Sum(T v){return v;}

template<typename T, typename... Args>
T Sum(T v, Args... args)
{
return v + Sum(args...);
}

//Sum(1, 2, 3, 4)

  • Spaceship operator / Three-way comparison (C++20)

在定义 operator<=> 后,编译器自动生成 ==!=<<=>>= 运算符

其用法类似于 strcmp,返回 -1,0,1 表示小于 / 等于 / 大于,在 C++ 实现中为三种特殊类型

auto operator<=>(...) = default; 生成默认函数


  • Designated Initializers / Aggregate Initialization (C++20)

类似 Python 中的指定特殊变量初始化:

1
2
struct Test{int a, b, c;};
Test x{.a = 1, .c = 2};

  • Attribute specifier sequence (C++11)

[[likely]][[unlikely]] (C++20)

人为提高分支预测准确率(怀疑效果如何

。。以及其他标志


后记

还是学到不少东西

不过因为我太摸了导致这预计 20 学时的东西摸了一星期,还没做Assignments

完结撒花~