C++ 入门(下)
关联型容器 Associative Containers
关联型容器通常需要键满足 <
,否则可以自定义一个:
|
关联性容器定义了三种类型:
key_type
mapped_type
value_type
set
:key_type
map
:pair<const key_type, mapped_type>
set 的迭代器都是 const_iterator
.lower_bound()
>=,.upper_bound()
>,.equal_range()
除此之外,还有键可以重复的 multiset
和 multimap
unordered_set
和 unordered_map
使用 hash,不保证顺序
动态内存 Dynamic Memory
智能指针:
shared_ptr<T> sp
和unique_ptr<T> up
p.get()
返回 p 中的指针make_shared<T>(args)
返回指向动态分配类型 T 的对象的shared_ptr
,使用args
初始化对象
.reset
更新引用计数并可能会删除指向的对象
也可以显式指定析构函数:shared_ptr<T, D> s2(d)
或 unique_ptr<T, D> u2(d)
,一个例子:
|
转移所有权的方法:
|
weak_ptr
是不会控制指向对象的生命周期的智能指针,其通常指向由 shared_ptr
管理的对象
因为有可能指向已经被释放的值,故不能直接访问,而是通过 .lock()
:如果已经 w.expired() == true
,则返回 nullptr
;否则返回指向的对象的 shared_ptr
也支持动态分配数组:unique_ptr<T[]> u
但注意当使用 shared_ptr
时,要指定析构函数:
|
allocator
可以将分配从构造中分离出来
- 定义
allocator<T> a
a.allocate(n)
分配原始的,为构造的内存以存放 n 个 T 类型的对象a.construct(p, args)
:p
必须指向 T 类型原始内存的指针,args
是用来构造的参数a.destroy(p)
:运行析构函数a.deallocate(p, n)
:归还allocate
的内存
一个例子:
|
类似于 copy
和 fill
的算法,allocator
也有相对应的算法:uninitialized_copy(b, e, b2)
和 uninitialized_fill(b ,e t)
复制控制 Copy Control
复制,赋值和销毁
copy constructor:Foo(const Foo&);
复制初始化:
|
有的类使用 explicit
指定必须使用直接初始化:
|
copy assignment:Foo& operator=(const Foo&);
析构函数 destructor:~Foo();
以上三者必须同时定义
使用 = default
来显式要求编译器生成默认的版本
极少数类(如 iostream
)会禁止复制,使用 = delete
复制控制和资源管理
尽管很少见,但赋值运算符必须支持自己给自己赋值,故其一定要先复制 rhs
操作数,再删除 lhs
操作数
尽管 swap
不是必须的,但是定义 swap
可以有更好的优化:
|
Swap
可以在赋值中使用 swap
,这种做法比较安全:
|
移动对象
IO
和 unique_ptr
不可以被复制,但可以被 move
左值引用和右值引用
可以使用 std::move
将左值转换为右值
相对应的,就有了 move constructor:
|
因为 move constructor 没有分配资源,所以并不会抛出异常,故要加上 noexcept
push_back()
保证了如果发生异常,则原有数据保持不变。那么如果 move constructor 发生了异常,则旧数据和新数据都会损坏,故只能选择 copy constructor。所以需要显式指明不会抛出异常
对应的也就有 move assignment:
|
只有当一个类没有定义自己的复制控制的成员时,编译器才会合成 move 成员
一种同时定义了 copy 和 move 赋值的方法:
|
一般的迭代器在解引用时返回的是左值引用,但是 move iterator 返回的却是一个右值引用
将迭代器转为移动迭代器的做法:make_move_iterator()
区分复制和移动参数的重载函数:
|
类似于 const *this
,我们也可以指定 *this
是左值还是右值:
|
重载运算和转换 Overloaded Operations and Conversions
可以显式调用运算符:
|
成员函数与非成员函数
输入流和输出流重载
算术运算符通常使用符号赋值重载:
|
同时定义
支持初始化列表的赋值:
|
定义下标运算符时,通常要定义两个版本:const 和非 const 的:
|
自增/自减分为前缀/后缀版本,用一个为 0
的参数来区分:
|
甚至可以重载函数调用运算符,像函数一样使用,叫函数对象:
|
事实上,lambda 就是函数对象
对于一个捕获了局部变量的 lambda,其相当于构造函数带该参数,并保存在其成员变量中
functional
头文件中定义了很多类型,如:plus<T>
、greater<T>
等
有多种可调用对象,如函数,函数指针,lambda,重载了 ()
的对象等,该头文件中还定义了函数原型以统一这些表达:
|
类型转换函数的基本形式:operator 类型名() const
前面可以加上 explicit
,强制要求显式转换
面向对象编程 Object-Oriented Programming
概览
基类 base class:
|
派生类 derived class:
|
动态绑定(运行时绑定):根据运行时是对基类还是派生类的引用,调用不同类中的函数
基类一般会定义一个 virtual
析构函数,使得其动态绑定
virtual
关键字表示希望派生类覆盖该成员
定义基类和派生类
派生类会自动转换为基类,如:
|
基类先初始化,然后才是派生类的成员:
|
对于 static
成员,则整个层次结构中只有一个该成员实例
使用 final
来防止一个类被继承:class NoDerived final { ... };
静态类型(编译时)和动态类型(运行时)
基类向派生类没有隐式转换
注意这只在引用和指针之间讨论,在基类和派生类对象之间没有隐式转换
虚函数
只有在通过指针或引用调用虚函数时,才会在运行时处理,否则在编译时解决
一旦某个函数被定义为 virtual
,则会在所有派生类中都定义为 virtual
可以使用 override
显式指明覆盖了基类中的某个虚函数
注意只有 virtual
函数可以 override
,final
函数不会被 override
可以绕开虚函数机制
抽象基类
纯虚函数 pure virtual 不会被定义,方法是在声明时在后面加 = 0
有纯虚函数的类是抽象基类 abstract base class,不能创建抽象基类的对象,其相当于提供了一个要求派生类覆盖的接口
派生类的构造函数只初始化它的直接基类
访问控制和继承
public
和 private
继承不影响该类的访问控制,但会影响这个类的用户(包括其派生类)能否访问到其基类的成员
friend 不会被继承
类内部的 using
声明可以使其 private
继承来的被访问到
继承下的类作用域
名字的查找发生在编译时
构造函数和复制控制
析构函数必须是 virtual
的
构造时先构造基类,析构时反过来
容器与继承
如果我们想要容纳以继承关联的对象,一般放智能指针,这样使用的就是动态类型
模板和泛型编程 Templates and Generic Programming
面向对象编程和泛型变成都是处理编写程序时不知道的类型的,但是 OOP 在运行时处理,泛型则在编译时处理
定义一个模板
函数模板:
|
我们也可以定义非类型参数 nontype parameters,即表示的是一个值而不是类型,在模板函数内部一定是一个常量表达式:
|
为了生成实例化,编译器需要定义函数模板或类模板成员函数,所以这两者一般都放在头文件中
在类的外部定义类模板成员函数:
|
只有当某个成员被使用时,它才会被实例化
可以让友元的成为模板的所有实例的朋友,也可以一对一成为友元
可以使模板类型参数成为友元:
|
可以定义类模板的类型别名:
|
类模板和函数参数支持设置默认值,如:
|
可以显式实例化来避免开销:
|
unique_ptr
在编译时绑定了 deleter,而 shared_ptr
在运行时绑定了 deleter
模板参数推断
模板类型参数的仅有的自动转换是 const 转换和数组或函数指针转换
而常规的转换应用于非模板参数的类型上
函数模板可以显式指定参数类型:
|
这是一个返回未知类型的函数:
|
其返回的是一个引用,但是如果是想要按值返回,则可以显式类型改变 type transformation:remove_reference<>
引用合并:
X& &
、X& &&
、X&& &
都合并为X&
X&& &&
合并为X&&
故即使参数的形式是右值 &&
,也可能合并为左值 &
因此,函数参数中的对模板类型的右值引用可以绑定到左值上,即若一个左值传递给这个参数,则会被实例化为左值引用
使用右值的函数模板通常被重载为:
|
实际上,std::move
的原理如下:
|
std::forward
可以在保持原始参数类型的情况下传递参数,其返回的类型是 T&&
,例如实现一个交换调用参数顺序的模板函数:
|
重载和模板
当有几个重载函数都能够匹配调用时,会选择最详细的那个
如果还有非模板函数也能匹配,则更优先匹配非模板函数
可变模板
可变模板 variadic template 是接受可变数量的参数的模板函数或类:
|
可以使用 sizeof...()
获取包中的元素数量
通常使用递归实现展开:
|
通常和 std::forward<Argss>(args)...
一起使用转发给别的函数
模板特化
我们可以接管编译器做的工作,即模板特化 template specialization:
|
模板特化也适用于类模板,但要注意定义在同一个命名空间中
类模板可以部分特化,但函数模板不行
专业化的库设施 Specialized Library Facilities
tuple
类型
tuple
和 pair
类似,但是支持容纳多个元素
定义:
|
或者使用 make_tuple
访问其中的元素:
|
tuple 支持关系比较
bitset
类型
定义 32 位:bitset<32> bitvec(1U);
,其中低位为 1
也可以从字符串中获取:bitset<32> bitvec4("1100");
,注意最低位为 0
其支持丰富的操作位的函数
以字符串格式 IO
正则表达式
随机数
使用到了两个类——随机数引擎 random-number engines 和随机数分布类 random-number distribution classes
生成均匀 [0, 9]
的均匀的随机数:
|
还支持生成小数 uniform_real_distribution<>
、正态分布 normal_distribution<>
再探 IO 库
操作符 manipulator 用于控制输出的格式
如十进制、八进制,精度
还支持更低级的 IO:
is.get(ch)
读下一个字节到 ch 中os.put(ch)
把下一个字符 ch 放到 os 中is.get()
返回下一个字节为 int,因为可能读到的是 EOFis.unget()
放回一个字节is.peek()
返回下一个字节为 int,但不会移除is.putback(ch)
把 ch 放回 is
还有支持多字节的版本:
is.getline(sink, size, delim)
读至多size
个字节至 EOF 或 delim 到 sink 指向的位置,注意 delim 不会被存储。delim 也会被保存的版本:get
is.ignore(size, delim)
os.write(source, size)
随机读写(p
代表写,g
代表读,但实际上操作的是同一个标志,本没有什么区别):
tellg()
获取当前位置seekg(pos)
移动当前位置seekg(off, from)
用于大型程序的工具 Tools for Large Programs
异常处理
当一个异常发生时,函数会返回并向上传递异常,直到被捕捉或退出程序,这个过程叫堆栈展开 stack unwinding,在该过程中,局部对象的析构函数会被调用,所以要注意向异常传递的指针所指向的对象在异常处理时是否存在
如果一个 catch
无法完全解决问题,需要再抛出异常 rethrow,方法很简单:throw;
捕捉所有异常的方法 catch(...)
函数 try 块 function try block 能够将 catch
与语句和构造函数的初始化阶段结合起来:
|
noexcept(true)
表示这个函数不会抛出异常,noexcept(false)
表示这个函数可能抛出异常,noexcept(函数)
可以判断某个函数是否会抛出异常,通常用于定义一个是否抛出异常取决于函数 g()
的函数:
|
异常也有继承的层次结构,可以自定义异常类型
命名空间
为了防止全局命名空间造成的命名空间污染 namespace pollution,引入了命名空间 namespace 用于防止命名冲突
命名空间的定义中包含着各命名的声明:
|
使用:
|
命名空间的定义可以被分到多个文件中
在全局作用域定义的名字被定义在全局命名空间 global namespace 中,显式的使用方法为:::成员名
命名空间可以嵌套
内联命名空间 inline namespace 中的名字可以不需要命名空间名限定即可使用
匿名命名空间 unnamed namespace 中的名字只会局限于当前文件,无法显式指定,用于替代 static
来是名称局限于当前文件中
命名空间别名 namespace alias 用于定义某命名空间的缩写:
|
- using 声明 using declaration 每次只引入一个命名空间中的成员
- using 指令 using directive 一次引入了所有的成员
- 推荐使用前者,因为如果发生了名称冲突,则会在编译时报错,而后者是在运行时报错,且更容易发生冲突
注意到这种写法是允许的:
|
其中的 operator
前并没有加上 std::
也能正常运行,因为首先会在对象 s
和 cin
定义的作用域中查找相关的 operator
因此,std::move
和 std::forward
通常要连着前面的限定一起写,因为如果使用 using std::move
的话,一开始并不是在 std
命名空间中查找 move
函数的,而 move
是个非常常见的名称,容易被误定义过。为了避免这种事,一定要连上 std::
多重和虚拟继承
一个多继承的例子:
|
其构造函数的写法和平常的没什么区别:
|
最终基类一定第一个初始化,然后按声明的顺序
如果有不同的基类的相同名字的成员,则必须指定是哪个基类的成员
注意到 Bear
和 Endangered
继承了相同的基类 ZooAnimal
,为了防止同时继承了这两个类的派生类存在多个 ZooAnimal
部分,需要将这两个类声明为虚继承 virtual inheritance,那个共同的基类叫虚基类 virtual base class
声明方法如下:
|
虚继承中构造函数保证了虚基类最先初始化,且只被初始化一次
专门的工具和技术 Specialized Tools and Techniques
控制内存分配
new 表达式构造了对象并调用 new 函数分配内存
delete 同理
运行时类型识别
运行时类型识别 run-time type identification(RTTI) 通过两个运算符提供:
typeid
返回给定表达式的类型dynamic_cast
操作符将指向基类的指针或引用转换为指向派生类的指针或引用
对于指针,dynamic_cast
失败会返回 nullptr
,故一般用法:
|
对于引用,则抛出 bad_cast
异常
注意指针的 typeid
返回的是编译时的类型
枚举
|
默认情况下从 0 开始,每次加 1,可以在定义时指定每个枚举量的值
枚举量的值是一个 constexpr
的量
我们可以指定枚举量的类型:
|
类成员指针
针对数据成员的指针:
|
当然,也可以是成员函数的指针:
|
可以通过 function<>
将其转换为一个可调用对象,或者直接使用 mem_fn
:
|
嵌套类
在类中声明另一个类:
|
然后在外部定义:
|