StringStream
stream作为数据类型和最终输出的中间单位,每个obj有一个buffer
stringstream分为ostringstream和istringstream,使用流运算符<<,>>来进行操作,并用两个指针(get和put)来管理输入输出流,会自动进行数据类型转换。
在istringstream里,要输入到一个变量里,则从当前get指针位置开始读取对应变量类型(自动进行类型转换),直到读取的字符为空(空格,换行,tab)或不能够转换成对应数据类型。
例如:A.str()="12.3"; 时,若要输入到一个整型变量B里,则最终B的值是12,而A的get指针停留在 ' . ' 上。
流对象执行输入输出操作时会维护一组状态标志,包括eofbit,failbit,badbit,例如,当将缓冲区字符串为”hello“的流输入到整型变量时,会出现输入失败的情况,这时流被标记为fail,只有进行clear操作后才可以继续操作。
ostringstream和istringstream的操作符与功能可类比cin和cout
含有stream operator(<< or >>)的表达式返回流对象本身,并且可以当作bool值(若statebit不是fail或者eof,则返回true,反之则为false)。
cout流有一个缓冲区,所有通过<<输入的数据对象需要等到flush的时候才被打印在屏幕上,endl也算是一个flush,会影响运行速度,但在有些时候不进行flush操作会导致错误或者不便。
cin每次读取会读取一行(直到回车),而其作为输入流在程序里和流的行为一样(按照空白符号分段读取),有可能会引起问题。另外,一个cin结束后指针在空白字符之前,而在下一个cin语句的时候,指针才会跳过这些空白字符
解决:getline(cin,stringname);替换cin>>表达式,直接读到换行符(并且consume换行符,获得的字符串中不包括换行符)
但>>和getline混用可能会出现问题,因为>>读取后指针停在空白符之前,而若此空白符是\n,那么下一次getline就只会返回一个空字符串。
Type
type aliases:类型别名
使用typedef或者using语句可以简化复杂的类型名
auto关键字:自动推断
需要初始化(以便推断类型)
不加&则去引用。若需要引用类型就要显式声明auto&
去掉const和volatile修饰符。若需要const就要显式声明const auto
用于初始化变量的数据类型如果含有指针,则保留constness,例如:
const int* px = 100;
auto py = px; // py 的类型是 const int* (const 被保留)
又例如:
auto d = "world!"; // d 的类型是 const char*
由于“world”字面量默认被推导为c风格字符串(char*),含有指针,故保留constness,而若有如下操作:
auto e = string("world!");
则类型推断为string,不含有指针,故不保留constness
引用&
使用pair和tuple将多个值打包,用auto可以简化操作
使用struct可以为变量取名,加以区分
使用结构绑定可以简化从pair或tuple或struct取值的操作
Containers&Iterator
Initialization
Uniform Initialization:使用{}初始化列表初始化大部分的对象,从而避免了初始化方法过多的麻烦,如:
Vector<int> vec{12,12,12,45,46,23};
本质上是调用了vector类的带有初始化列表参数的构造函数
Sequence Containers
Vector(在前面插入速度慢,但访问速度比deque快)
Deque(能够实现前后都比较快的插入速度,但访问速度比vector慢)
else:
list:链表
Forward_list:单向链表
array:定长数组
Container Adaptors
stack:基于vector/deque实现
queue:基于deque实现
Associative Containers
Set:存储单个值,自动排序,快速查找。
Map:存储不同数据类型的键值对映射,相当于把sequence里的index变成了key(但存储方式和sequence不同),元素按照key有特定顺序,可以自定义<运算符来定义排序。能够进行快速查找(二叉树结构)。
[key]方法:若没有key对应的值,则新建键值对
.get(key)方法:若没有key对应的值,则throw an error
判断key是否存在:
.count(key)方法,返回key在map里出现的次数(只可能是0或者1)
.find(key)方法,返回key第一次出现的迭代器,若key不存在则返回.end()迭代器
multimap:允许一个键对应多个元素,但不再有[key]运算符,.find(key)返回key对应的第一个元素,.range(key)返回一个pair<begin_iterator,end_iterator>,表示对应key的元素范围,使用iterator从begin到end进行遍历,可获取key对应的全部元素
map和multimap存储的都是pair,意味着iterator的解引用结果是一个pair
set和map在使用for each遍历的时候是只读的,因为修改元素会破坏容器结构,导致for循环不安全
sequence和associative对比:associative查找超快,sequence可以随机访问
Iterator
可以对所有Container进行遍历,以int型的set为例:
Set<int>::iterator it=mySet.begin();
容器可调用相应函数返回iterator值
.begin()
.end()
对容器使用操作符
++it:将iterator移动到下一个元素
*it:对iterator进行解引用(it不一定是指针,但相似)
Types(低级到高级)
Input/output
Forward
Bidirectional
Random access
根据层级不同,可使用的操作不一样,如set的iterator不支持随机访问(+3),只能进行递增(++),而vector就可以,具体要考虑到容器的底层数据存储方式
Template
用于泛型编程,使一个函数能接收不同类型的参数而无需重载(以及更多功能)
语法:
template <typename T,typename U,…… > // 其中,template是声明模板的标识,<>内是模板参数列表,typename代表T是一个类型名变量
函数声明定义语句(用T代表要使用泛型的类型)
调用的时候和普通情况一样,也可以用显式实例化(explicit instantiation),即在调用的函数名后面用<类型名> 传入模板参数
隐式实例化时,字符串字面量会被认为是char*而非string,所以可能会出问题
声明模板类时,一般要求把声明和实现都放在.h文件里,如果分离声明和实现会报LNK错误
如果需要分离,则需要用到显式实例化,即在.cpp文件里先通过template class className<typename>来提前对typename类型的类的成员函数进行实例化,并且只有在.cpp里实例化过的类型才能使用模板函数
Implicit interface
使用模板并不是传入任何类型的参数都能工作,模板函数里对泛型变量的操作对传入参数的类型作了限制
concept(explicit interface)
c++20:显式声明参数的限制,不用看函数定义就能知晓限制
模板函数可以传入参数函数(谓词函数)
函数作为参数传入模板函数时,实例化出一个有函数指针类型参数的函数原型
谓词函数是对返回布尔值的函数的叫法,仅仅为了方便,和平常函数并没有分别,但由于模板化的需要,这类函数使用得比较频繁
使用stl库时所需要的谓词函数,往往是一元或者二元的(倘若库函数需要一元谓词函数,但又有再传入其他参数的需求,就可以考虑lambda的捕获或者仿函数)
谓词可以是:函数,函数对象(仿函数),lambda表达式(实际也是仿函数),函数指针
Lambda
谓词函数可以用lambda表达式替代,lambda表达式实际上是仿函数,是一个重载了()运算符的类
lambda表达式语法:
[capture](parameter)->return type{
Statements;
}
capture: 捕获列表,用于指定哪些外部变量可以在 lambda 表达式中使用。捕获方式有值捕获([x])、引用捕获([&x])以及捕获所有变量([=]、[&])等。
parameters: 函数参数列表,和普通函数类似。(这里的参数列表可以使用auto)
return_type: 返回类型,可以省略,编译器会自动推导。
可以使用auto objectname=lambda;来把函数对象存起来
Algorithm
sort:按照条件排序
Stable partition:按照条件将容器元素分组,分别在前面和后面,但同组元素相对顺序不变
Copy_if:按照条件复制,需要传入一个output iterator,并且output it不能自动扩容,如需要扩容,则传入back_inserter(容器名)
Back_inserter是一个iterator adaptor,用于生成可扩容的迭代器
相似的,还有ostream_iterator,可以生成流的迭代器
Remove_if:按照条件移除,但是注意:STL函数并不是容器的成员函数,所以并不能修改容器的大小,而只是把移除的元素都放在了容器的最后面,并返回指向最前面的要移除元素的指针,而容器end指针不变
并且已经使用remove指定移除的元素,虽然没有被完全移除,但并不可用,只是垃圾,需要手动清理
完全移除元素的组合拳:↑
find(find_if):
Const
1.限制函数参数,一般和&一起,防止传入的参数被改变,如果函数内部试图改变此参数,编译器会报错。
同时也限制了函数内调用此参数的其他函数
2.使用const_cast转换constness,不常用
3.和class协同,const member function
声明方法:在函数原型声明后加const
类的const对象只能调用const member function
非const对象能调用所有类型的成员函数
4.const pointer
5.const iterator
6.auto不自动包含const和&,故需要显式声明const auto &p=something
Operators
1.当使用运算符的时候,c++实际上在调用函数
这里运算符是作为成员函数被调用,实际上,也可以使用非成员函数形式(有些运算符只有成员函数形式)
2.重载
syntax:(类外)returnType className::operator specificOperator(parameter list){func body;}
通常,算术运算符返回的是运算结果的引用,标准格式如下
this:一个指向调用当前函数对象的指针,类型为className* const,*this返回对象的引用
作为成员函数的二元运算符重载只需要一个参数,因为另一个参数是对象本身
非成员函数的二元运算符重载则需要两个参数
对于非成员函数的运算符重载,如需要访问privata成员变量,可在类内将该函数声明为friend
SpecialMemberFunction
Special Functions
Constructor
当声明一个类的成员变量为常量时,不允许在其构造函数体内对变量进行初始化,这时候需要用到初始化列表(适用于所有只初始化一次,而不再后续改变的情况)(非常量的成员函数也可以使用这个语法)
并且初始化列表里的变量名自动指代本对象里的成员变量,而无需使用this指针
若一个类的对象被声明为常量,对其非常量成员变量的初始化,既可以在初始化列表进行,也可以在构造函数体内进行
Copy
Copy constructor
创建新对象时,若使用另一个已有的对象来初始化此对象,则调用此函数
自己实现的方法为重载一个参数为本类型的构造函数
要分清楚简单拷贝和深层拷贝
考虑使用no
Copy assignment
对于一个已有的对象,用另一个已有对象赋值给它,调用此函数
实现方法为重载 = 运算符
若一个类内存在不能复制的元素,如文件流,可将copy constructor和copy assignment显式声明为delete
当涉及到手动分配的内存时,就考虑实现上面两者和destructor
Rule of three(five):copy constructor,copy assignment,destructor三者同时实现或者 不实现
Move Semantics
函数间传输对象的时候,往往会产生不必要的copy,如参数传入,返回结果,可以优化
aside:vector的emplace_back方法,相比于push_back,本方法直接传入构造一个对象的参数,而无需传入一个已经构造好的对象,避免不必要的copy
左值/右值(L/R value):
左值为有地址的值,一般有名字,解引用的指针也是左值
右值为无地址的临时量,如字面量,不返回引用的函数返回值
引用:
&表示左值引用,比较常用;&&表示右值引用,使得被引用的右值生命周期增长
常量类型的左值引用也可以绑定到右值上
原型:
即设计参数为右值引用版本的copy constructor和copy assignment,由于常量类型的左值引用也可以绑定到右值上,所以先前的copy函数也能用
实现上,相比于原先的深度拷贝,对于右值引用,可以直接窃取其数据,而无需创建新的数据
对于以右值引用传入的参数,其本身却是左值,因此进行拷贝时可能不会按照预期进行,此时可以使用std::move()函数,该函数返回传入参数的右值版本
Rule of five:
copy constructor,copy assignment,destructor,move constructor,move assignment五者同时实现或者不实现
Namespace&Inheritance
Inheritance from the first tutorial
Inheritance:类声明第一行后面加上
:public 父类名
父子类构造函数调用顺序和重载问题
不论有无输入参数,先调用父类默认(即不带输入参数的)构造函数,在调用本类对应输入参数的重载构造函数
如果使用以下语法(假设创建Dog实例时传入了下图三个参数)
则会调用父类对应参数的重载构造函数,而不调用无参数的构造函数
如果在Dog的构造函数体内调用了Animal构造函数(而不是使用冒号初始化列表),则先调用Animal的默认构造函数,再调用Dog的重载构造函数,再调用Animal的重载构造函数(并且最后调用的这个函数并不会影响到实际对象,而是创建了一个虚拟对象来操作),可以说是错误用法。
Private:仅限类内
Protected:仅限类内和派生类
Public:都行
关键字继承:(不写关键字则类默认private,结构体默认public)
例:class Dog : protected Animal {} ;
Animal类中所有public变量都变成protected,其余不变
Virtual:
非静态成员函数前加上virtual关键字
派生类即可重写(override)此函数,方法为(例)void Speak(){}(不用加virtual),并且此函数在派生类中依旧可以被重写(隐式标注了virtual)
为了可读性,可以在重写函数前加上virtual(代表此函数可以被重写),也可以在重写函数后加上override(代表此函数是基类一个函数的重写)
可以使用完全限定符来调用基类的被重写的原函数
Polymorphism
一个类实例的指针可以指向其所有子类的实例,并且调用继承函数时会调用相应的重写函数
Multi-Inheritance
使用“,”分隔开父类即可,每个父类前都要加关键字
对于菱形继承:
使用virtual关键字,确保只存在一个基类实例
Namespace
使用namespace name{}定义自己的命名空间
好的习惯:
不要总是using namespace std,在大项目里应用using std::something
在header里也尽量别用using
inheritance
Interface:另一种理解继承的方式,即
父类不提供具体实现,而是提供接口,因此标记为virtual
子类继承父类,继承了接口并实现接口,这样就有了统一的方法
Virtual void functionName()=0;(纯虚函数)
在接口类声明函数时赋值为0,可强制子类实现该函数
当一个类包含纯虚函数时,该类(抽象类)不能实例化
当子类新定义了一个名字和父类相同的变量时,父类里的那个变量会被隐藏
若一个类想要被继承,最好将其析构函数声明为virtual
若基类析构函数没有被声明为virtual,则在使用多态时(一个基类的指针指向派生类),派生类的析构函数不会被正确调用,delete只会调用基类的析构函数,导致内存泄漏
多态性:template vs. derived classes
template是静态多态,在编译时就已经决定好了;derived是动态多态,直到运行时才知道对象类型
template多种对象实现的功能类似;derived多种对象实现的功能可以完全不同
RAII&SmartPointer
程序在抛出异常的时候可能不会正确释放内存,导致内存泄漏
如:new,文件流,lock
RAII
鉴于析构函数在每次退出作用域的时候都会被调用,因此可以直接将释放内存的操作写在析构函数里
Constructor builds, destructor releases
例如,ifstream就内置了RAII,使其在作用域末尾无需手动调用close方法
对于lock,在作用域开头定义一个lock_guard类,用lock将其初始化;而在lock_guard类的构造函数中lock被锁上,在lock_guard的析构函数中lock被解锁
Smart pointers
原理如上面lock_guard,标准库提供了用于管理指针内存的类
Unique_ptr
不允许复制操作
初始化:unique_ptr<node> p=make_unique<node>;
Shared_ptr
允许复制(多个指针指向同一个数据地址),有一个计数器,内存在计数器为零的时候被释放
初始化:shared_ptr<node> p=make_shared<node>;
Weak_ptr
使用try,catch语句来捕获异常
MultiThreading
概述
使用atomic类型,尤其是在注重顺序的情况下,防止数据竞争data race
Condition variable进行线程间通信
使用thread类名声明线程(就像其他类一样)
thread没有copy函数,不能被拷贝
使用join()方法来等待线程结束