c++学习文档汇总
[TOC]
c++ primer部分
c++学习笔记
// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
// 调试程序: F5 或调试 >“开始调试”菜单
// 入门使用技巧:
// 1. 使用解决方案资源管理器窗口添加/管理文件
// 2. 使用团队资源管理器窗口连接到源代码管理
// 3. 使用输出窗口查看生成输出和其他消息
// 4. 使用错误列表窗口查看错误
// 5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
// 6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件
引用(左值引用) int &refVal = val; 引用即别名,给val起另外一个名字,想找val也可以访问refVal,但是并不存在想找refVal的情况
空指针: int *p = nullptr;或者 int *p=0;
const引用: int &ref = val; 常量引用,不能通过ref去改变val的值(val = 10;这种是可以的)
const指针同理:const *p = &val;不能通过操作指针去 改变val的值(其他方式改变val的值是可以的)
声明是可以复合的,读的顺序从右往左 ←
// const(指针) 顶层const
int* const ref = &val;
// 指针(指向const) 底层const
const int* ref = &val;
顶层const实际不约束对象,只约束引用,在对象拷贝时不受影响,底层const需要校验对象是否也是一个底层const对象
指针声明:
一条指针声明,如int *p, 其中int是数据类型,星号是指针的声明符,在同一条定义语句中,数据类型可以只有一个,但是声明符确可以不同
const相关:
可以理解为const限定紧随在后的数据类型,int、(int *)这类才是数据类型,注意区分 const int *p 和const (int *p):
- const p 代表const限定p的数据类型,如果p的数据类型是int表示常量int,p是指针表示常量指针
- const int代表const限定的是明确的数据类型int,如果const int *p表示声明指针p的数据类型是const int
常量引用:const int a = 10; const int &b = a;就代表b是对常量a的引用,常量引用 = const 引用,类似的,const int* c = &a; 表示c是一个指向常量a的指针
顶层const和底层const:
// 顶层const = 当前声明的变量是常量
int *const p;
// const p <=> p是一个常量
// * const p <=> 声明一个指针p,p是常量
// int *const p <=> 声明一个指针p,p是常量,p的数据类型是int
const int *p;
// *p <=> 声明一个指针p
// int *p <=> 声明一个指针p,p的数据类型是int
// const int *p <=> 声明一个指针p,p的数据类型是const int
const int &p;
// 常量引用(const 引用)代表这个引用不能被更改值,也就是底层被const了
//特殊的情况
typedef int *p;
const p q;
// const p q<=> const (int *) q
constexpr:constexpr是真正的常量,const只是在编译的时候的对限定变量的限制
constexpr可能是手动告诉编译器这是个常量的表达式,对于复杂的表达式编译器推断起来压力很大,所以这样手动声明可以最大限度让编译器进行优化,constexpr的类型就是"字面值类型",constexpr声明指针的时候同样表明当前变量是常量,即顶层const,所以constexpr指针不能指向函数体内的变量(因为它们在内存中并没有固定的地址)
auto和decltype,auto多了个推断的过程,decltype是原封不动的照搬
auto:本质是计算右侧的表达式后,将表达式的数据类型赋值给左侧变量,会忽略顶层const,底层const会保留
const int p = i;
auto q = p; //q是int型
decltype:本质是对右侧的表达式(单个的变量也是表达式的一种,加不加括号效果不同)不计算,将表达式的返回类型赋值给左侧,如果是单个的变量,将它的原型(原来是const int返回const int,原来是常量引用,返回常用引用,auto则计算具体的值然后根据具体的值推断类型)作为数据类型赋值给左侧
特别的
- decltype( 解引用如 *p),这里得到的是一个引用类型int &而非解引用后的数据类型int
- decltype((variable))的结果永远是一个引用
对于声明修饰符的作用: 设想编译器去解析一段复合声明, xxxxx value ; 有两种方式从左至右,从右到左,如果从左至右的话则可能需要到达变量才能明确语法错误(实际也可能不需要),但是如果想直观一点,还是把复合声明看成运算符性质的比较好,从右到左,也就是所谓的自顶向下
拷贝初始化 a = b
string
string类型的size()尽量不要与int混合运算,因为size()返回的是无符号数(为了保持与机器无关的属性,unsigned是与机器相关的)
string两个对象之间 == > <= 等都是比较字面量,大于小于比较的是挨个字符的字典顺序
字面值"str"并不是标准库string的对象(历史原因)
c++的标准库兼容c的标准库,在c中的 name.h相当于c++中的cname
字符串的下标运算符 [] 接受的参数是 string::size_type(无符号)类型的值,如果给他一个带符号类型的值将会被自动转为 string::size_type无符号类值
vector:
头文件
#include <vector>
using std::vector;
c++既有类模板也有函数模板,vector是一个类模板,模板本身不是函数或者类,是编译器为了生成类或者函数编写的声明,编译器根据模板创建类或者函数的过程称为实例化,所以在使用模板的时候要指出编译器应该把类或者函数实例化成何种类型的对象(引用不是对象,vector内部无法存放引用)
C++11之前的标准,应该写成 vector<vector<int> >需要有个空格
类的定义
struct ClassName{
};//这里必须有分号
struct Person{
} zhangsan, lisi, *wangwu;//可以将类的定义与对象的定义放在一起写,但不推荐
类通常在头文件中定义,头文件改变的时候相关的源文件也需要重新编译
头文件保护符:
-
define
-
ifdef
-
ifndef
-
endif
初始化vector对象的方法:
vector<T> v1;
vector<T> v2(v1);
vector<T> v2 = v1;
vector<T> v3(n, val);
vector<T> v4(n);
//对于初始值的列表,需要使用{}而不是()
vector<T> v5 {a, b, c...};
vector<T> v5 = {a,b,c....};
不同初始化方式的区别
vector<int> v1(10);//size = 10,全部初始化为0
vector<int> v1{10};
vector<int> v1(10,1);//10个1
vector<int> v1{10,1};
圆括号表示 构造construct vector对象,传入的参数会被当成容量 size
花括号表示 列表初始化list initialize vector对象,传入的参数会被当成每个元素的初始值
编译器会在默认条件不满足的时候尝试另一个条件
vector<string> v1{10}; //默认使用列表初始化,但是类型不匹配,所以转为construct初始化
vector<string> v2{10,"hi"};
//确认无法执行列表初始化后,编译器会尝试使用默认值初始化vector对象
vector在定义的时候尽量不要定义大小
vector的size()返回 vector<int>::size_type类型
迭代器:
所有的标准库容器都可以使用迭代器,但是只有少数几种才同时支持下标运算符,string虽然不属于容器,但是很多操作和容器很类似
auto b = v.begin();
auto e = v.end();
//end返回的是 指向最后一个位置之后的一个位置的迭代器,这个位置上实际不存在元素,所以无法使用 ++e, --e去操作不存在的元素
迭代器运算符
- *iter
- iter->men 等价于 (*iter).mem
- ++iter; --iter;
- iter1 == iter2 ; iter1 != iter2;
for(auto it = s.begin(); it != s.end() ; ++it){
//使用 != 而不是 < 主要是因为所有标准库容器的迭代器都定义了 == 和!=,但是大多数没有定义<,所以<可能会出现不兼容的情况
}
迭代器的类型:
vector<int>::iterator it1;
string::iterator it2;
//const型只能读不能写
vector<int>::const_iterator it3;
string::const_iterator it4;
// c++11,得到一个const的迭代器
auto it3 = v.cbegin();
// ->
it->empty();
//等价于
(*it).empty();
遍历text文本第一段话
for(auto it=text.cbegin(); it!=text.cend() && !it->empty(); ++it){
cout<< *it << endl;
}
push_back操作会使得vector对象的迭代器失效=> so不要在迭代器循环内添加元素
迭代器支持 it1-it2,it1 >= it2;这种算术比较操作,算术运算的结果it1-it2是difference_type类型的带符号整数
数组:
数组的长度在编译的时候必须是一个常量表达式constexpr
int cnt = 10;
string fail[cnt];//error
constexpr cnt = 10;
string success[cnt];
列表初始化的时候可以忽略维度,编译器会自动推算长度,如int a[] = {1, 2, 3};
int[5] arr = {1,2}; //{1,2,0,0,0}
字符数组的特殊性
char a1[] = {'c', '+', '+'}; //len =3
char a1[] = {'c', '+', '+', '\0'};//len =4
char a1[] = "c++";//len=4,默认添加一个'\0'空字符
char a1[3] = "c++"//error
不允许使用数组的拷贝和赋值
int a[] = {1,2};
int a2[] = a;//error
a3 = a;//error
指针、引用与数组
int *ptrs[10];//10个指针的数组
int (*ptrs)[10] = &arr;//一个指针指向长度为10的数组
int (&arrRef)[10] = arr;//引用
数组声明的读法与之前的有所区别,总体是由内往外,从右到左,例如
/*
&arr arr是一个引用
(&arr)[10] arr是一个大小为10的数组的引用
int *(&arr)[10] arr引用的数组每个元素类型是int型的指针
*/
int *(&arr)[10] = ptrs;
数组的下标是 size_t类型,是一种机器相关的无符号类型
数组的下标运算符[]是由C++语言直接定义的,这个运算符能用在数组类型的运算对象上,vector的下标运算符是由库模板vector定义的,只能用于vector类型的运算对象
for可以进行遍历数组的所有元素
数组与指针:在很多用到数组名字的地方,编译器会自动将其替换为一个指向数组首元素的指针(拿到这个指针 = 拿到可操作的数组对象)
string *p = nums ; // string *p = &nums[0];
所以在一些对数组这个字面值进行操作的时候 实际上为指针的操作(使用decltype时除外,编译器不会自动替换)
int arr[] = {1,2,3};
auto arr2(arr); //arr2是一个指针类型,且是int型的指针,指向arr的第一个元素
int i = arr[1];// arr是一个指针,arr[1]等价于 (arr+1)的元素
//只要指针指向的是数组中的元素,都可以执行下标运算
int *p = &arr[1];
int j = p[1];//p[1]等价于 *(p+1) 等价于 *(arr+2) 等价于 arr[2]
可以使用指针遍历数组,同样可以使用尾元素之后的空元素的地址
int arr[10];
int *end = &arr[10];
//遍历
for(int *begin = arr; begin != end ; ++begin){
cout<< *begin <<endl;
}
c++11中引入新的函数,非成员函数因为数组不是类类型,而是
int *beg = begin(arr);
int *end = end(arr);
在对数组的指针进行操作时,与vector的迭代器类似,指向同一个数组元素的指针可以进行下标运算,结果是由符号数值(同样适用于其他指针的使用场景)
数组的下标运算符和vector以及string不同的一点是 数组的下标运算可以使用索引值为负的(vector和string的下标运算符是无符号类型):**p[1]等价于 (p+1) 等价于 (arr+2) 等价于 arr[2]
int arr[] = {1,2,3};
//只要指针指向的是数组中的元素,都可以执行下标运算
int *p = &arr[1];
int j = p[1];//p[1]等价于 *(p+1) 等价于 *(arr+2) 等价于 arr[2]
int k = p[-1];//等价于arr[0]
不允许数组a赋值给数组b,不允许使用vector对象初始化数组,但是可以用数组来初始化vector对象
int arr[] = {1,2,3,4,5};
vector<int> ivec(begin(arr), end(arr));
//或者
vector<int> ivec(arr+1, arr+4);
现在的C++程序应该尽量使用vector和迭代器,避免使用内置数组和指针
尽量使用string,避免使用C风格的基于数组的字符串
多维数组:严格上C++没有多维数组,通常的数组其实是数组的数组
int arr[3][4] = {
{0},
{1, 2, 3},
{4, 5, 6, 7},
};
二维数组的处理
constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt];
for(size_t i=0 ; i != rowCnt ; i++){
for(size_t j=0 ; j != colCnt ; j++){
ia[i][j] = i*rowCnt + j;
}
}
c++11的标准:
使用for处理多维数组,除了最内层的循环外,其他循环的控制变量都应该是引用类型,否则编译器会自动转成指针类型
for(auto &row : ia){
for(auto &col : row){
col = val;
}
}
由于编译器的自动转换,所以多维数组的名字 = 指向第一个内层数组的指针,对多维数组名字解引用会得到内部数组的指针
arr[2][5] = {
{0, 1, 2, 3, 4},
{5, 6, 7, 8, 9}
}
arr 也就是 arr[0] 代表指向 {0, 1, 2, 3, 4}的指针
*arr 代表指向 {0}的指针
使用标准库函数的迭代方法:
for(auto p = begin(arr); p != end(arr); ++p){
for(auto q = begin(*p) ; q!=end(*p) ; ++q){
do something;
}
}
缓冲区溢出:试图通过一个越界的索引访问容器(string、vector和数组)内容
第四章-表达式
运算对象转换:小整数类型(bool、char、short)通常会被提升为较大的整数类型,主要是int
运算符重载:当运算符作用于类类型的对象的时候,用户可以自行定义其含义,实际上为已经存在的运算符赋予 另一层含义,即运算符的重载。IO库的>>和<<以及string对象、vector对象和迭代器使用的运算符已经由c++重载过,重载可以自定义运算对象的类型、返回值的类型,但是无法更改运算对象的个数、优先级、结合律
左值和右值:当一个对象被用作右值的时候,用的是对象的值,当对象被用作左值的时候,用的是对象在内存中的位置。原则:当一个左值被当成右值使用时,使用的是它的值val,右值不能被当成左值使用,如:
- 赋值运算符,需要一个左值(非常量)作为左侧运算对象,得到的结果是一个左值
- 取地址符,作用于左值运算对象,返回指向该运算对象的指针是右值(指向该运算对象的指针是常量,无法改变的)
- 内置解引用符、下标运算符、迭代器解引用运算符、string和vector下标运算符的求值结果都是左值
- ++和--作用于左值运算对象
使用decltype的时候,如果表达式的求值结果是左值,decltype作用该表达式得到一个引用类型
int *p = &val;
decltype(*p) q; //q是int& 类型
decltype(&p) q; //q是int**类型
求值顺序(给编译器充分的自由来优化并发执行求值):大部分(除了 && || ,?:)之外的运算符的求值顺序是不明确的,所以 cout<< i << " " << ++i <<endl这种表达式是未定义的,行为不可预知
c++11新标准规定商一律向0取整,即直接删去小数部分
赋值和初始化:赋值的左侧运算对象必须是一个可以修改的左值
int i=0 , j = 1, k = 2;//这是初始化不是赋值
赋值运算符的结合律和其他二元运算符不太一样,赋值运算符是右结合,且返回左侧运算对象
//以下是合法的
int a, b;
a = b = 0;// b=0先执行,然后 a = b
//int a = b ;这个=是初始化运算符而不是赋值运算符
表达更清晰的赋值:
int i;
while( (i = getValue() ) != 42){
do something;
}
++和--运算符:
- 可以用于部分不支持算术运算的迭代器
- 除非必须不要使用 i++
- 因为 i ++运算的对象和返回的对象不同,且多生成的对象容易造成内存泄漏
成员访问运算符:
- 点运算符 p.men
- 箭头运算符 (*p).men 等价于 p->mem
位运算符和移位运算符尽管不同但是优先级和结合律是一样的
sizeof运算符,右结合律,所得的值是一个size_t类型的常量表达式:有两种形式,sizeof(type),sizeof expr,在sizeof expr中返回的是表达式结果类型的大小,但是并不实际计算其运算对象的值(类似decltype),这有点类似于编译器返回给你变量表中该类型占用空间的大小
sizeof (数组名字)的时候,编译器不会自动将数组名转换为指向第一个元素的指针,而是对数组每个元素求sizeof然后求和
逗号运算符(comma):两个运算对象,从左到右,先对左侧求值,然后丢掉左值,真正的结果是右侧表达式的值
无符号类型的算术转换
1、整形提升,char、short等提升为int,布尔值提升为unsigned int类型 2、如果这时候为 带符号型 和 无符号型的运算,则小的向大的提升
- 如果 int > unsigned int (这里也可以是long等),如果unsigned占用的位数<signed占用的位数,也就是unsigned可以无损迁移成signed,这也是默认的转换情况
- 如果 int > unsigned int (long),如果unsigned占用的位数 > signed占用的位数,signed -> unsigned
也就是:unsigned准备向signed提升的时候,默认是短的向长的转
显式类型转换:
- static_cast : double dval = static_cast<double>(j),可以包含所有不含底层const的类型转换
- dynamic_cast
- const_cast
- reinterpret_cast
Code- const_cast:
#include <iostream>
using namespace std;
int main() {
const int constVal = 10;
int a = constVal;
cout<< a <<endl;
// const_cast只能改变底层const,也就是说无法作用于对象本身是个常量的情况(顶层const)
// int &b = const_cast<int>(constVal);编译不通过
const int *p = &constVal;//来一个底层const
// *p = 20;无法执行
int *q = const_cast<int *>(p);
*q = 20;
cout << *q << endl;
int val = 11;
const int &constRef = const_cast<const int&>(val);
cout<< constRef <<endl;
val = 22;
//constRef = 33; 仅通过ref无法改变const,也就是const只给当前赋值的引用一种约束
cout << val << " ,ref = " << constRef << endl;
}
第五章-语句
范围for语句:for(declaration : expression) statement,可以遍历容器或者其他序列的所有元素,例如string、vector、甚至是{1,2,3,5}这类拥有能返回迭代器的begin和end成员
第六章-函数
函数返回类型:不能返回数组或者函数,但是可以返回指向数组或函数的指针
函数声明也叫函数原型,函数和变量应该在头文件中声明,在源文件中定义
分离式编译:
参数传递:每次调用函数都会重新创建它的形参,并用传入的实参对形参进行初始化。如果形参是引用类型,则会绑定到对应的实参上,也叫做实参被引用传递或者函数被传引用调用,传值时是值传递和传值调用。指针形参 = 非引用类形参 = 拷贝指针的值 = 可以修改对象内存中的值
//参考前面的引用
int &i = val;
//函数的传引用很类似
void function(int &i){
do something;
}
function(val);
//当执行function(val)的时候,等价于实参i执行了引用绑定int &i = val
//所以 i在函数作用域内是一个val的引用
引用的另一个作用是避免拷贝,有些大对象或者容器对象拷贝低效,有些(IO类型等)不支持拷贝,可以使用引用避免拷贝,可以使用引用或者const引用
使用引用可以使得函数返回多个值,超过1个的return通过引用将值隐式的返回
void reset(int *p){
}
void reset(int &p){
}
reset(&i);//形参是指针
reset(i);//调用形参是int &的函数
数组形参:
因为数组有两个特殊的性质,不允许拷贝并且使用数组的时候会将其转换为指针。所以数组无法值传递,也就是说形式上是数组的形式但是实际上是传递指针
//下面三个print函数式等价的
void print(const int*);
void print(const int[]);
void print(const int[10]);//由于指针的转换,只检查是否是指针,所以10不影响实际结果
//三个函数都是接受int* 类型的形参
int *arr[10];//arr是一个10个元素的数组,每个元素类型都是int型指针
int (*arr)[10];//arr是一个指向10个int型元素数组的指针
f( int &arr[10]);//arr是一个10个元素的数组,每个元素类型都是int型引用
f( int (&arr)[10]);//arr是一个指向10个int型元素数组的引用
交换指针值
void swapPointer(int *&p , int *&q){
int *tmp = p;
p = q;
q = tmp;
}
int main() {
int v1 = 10, v2 = 30;
int *p = &v1;
int *q = &v2;
cout<< *p << " - " << *q <<endl;
swapPointer(p, q);
cout<< *p << " - " << *q <<endl;
return 0;
}
含有可变形参的函数
initializer_list函数,initializer_list是一种模板类型,类似于vector
initializer_list<T> list;
initializer_list<T> list{a, b, c};
list2(list);//拷贝
list2 = list;
list.size();
list.begin();
list.end();
函数调用返回一个非常量引用的结果时,可以对它进行赋值,如get_val(s, 0) = 'A';
第七章-类
类的作用域
在类的外部定义成员函数必须同时提供类名加函数名,显式的告诉编译器现在所在的作用域,若返回值也处于类的作用域类,则要先处理返回值
Window_mgr::ScreenIndex//先声明返回值的作用域
Window_mgr::addScreen(xx){
do something;
}
名字查找(name lookup)在类中有些不一样,这里先编译成员的声明,知道类全部可见后才编译函数体
成员函数中使用的名字查找路线
- 在函数体内,类作用域内成员函数之前的
- 在类内全体成员查找
- 类作用域之外查找
如果函数作用域内的同名成员屏蔽了函数体外类内的同名成员,可以通过this指针显式的调用
void Screen::fcn(pos height){
cursor = width * this->height;//类的成员height被屏蔽了,但是仍可以通过this显式访问
cursor = width * ::height;//全局作用域下的height
}
构造函数初始化和赋值
Person(string n, int a):name(n),age(a){}//初始化
Person(string n, int a){//赋值
name = n;
age = a;
}
初始化和赋值大致没有区别,但是有些类型不接受赋值必须先进行初始化,例如const和引用,是无法执行默认初始化的
在初始化的时候,name(n),age(a)的初始化顺序仅和name、age在类中的顺序有关,可能编译器考虑到在书写成员属性的时候会有些顺序
委托构造函数:一个委托构造函数使得它所在类的其他构造函数执行它自己的初始化过程例如:
Sales_data(string s):Sales_data(s, 0 , 0){}//委托给后面的构造函数
Sales_data(string s, unsigned cnt, double price):bookNo(s)....{
xxx;
do something;//执行完毕后交还控制权给Sales_data(string s)
}
e.g:
class Person{
public:
string name;
unsigned age;
double score;
Person(string s, unsigned a, double sc):name(s), age(a), score(sc){
cout<< "this is three para init...."<<endl;
}
Person():Person(" "){
cout<< "this is default intit..."<<endl;
}
Person(string s):Person(s, 0, 0){
cout<< "this is one para init" <<endl;
}
};
int main(){
Person p = Person();
string str = "this is a str";
Person formStr = str;
Person formStr = "this is a str";//这是错误的
}
//output:
this is three para init....
this is one para init
this is default intit...
this is three para init....
this is one para init
隐式的类型转换只允许一步,上述的Person formStr = "this is a str"是上面的两步转换,所以不被编译器接受,explicit关键字可以阻止隐式转换(只允许出现在类内声明)
explicit构造函数也不能用于拷贝初始化
聚合类:
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类、没有virtual函数
如:
struct Data{
int val;
string s;
}
Data d = {10, "hello"};
静态成员:类静态成员不与任何对象绑定在一起,不包含this指针,不能声明成const的,访问使用类的作用域运算符, double r = Account::rate();
静态成员可以使用特殊类型:
class Bar{
static Bar mem1;
Bar *mem2;
}
Code- 函数返回指向函数的指针
#include <iostream>
#include <vector>
using namespace std;
int fun1(int a, int b);
int add(int a, int b){
return a+b;
}
int minusFun(int a, int b){
return a-b;
}
int multiply(int a, int b){
return a*b;
}
int divide(int a, int b){
return b==0?0:a/b;
}
int main() {
vector<decltype(fun1)*> vec;
vector<int(*)(int,int)>vec2;
vec2.push_back(add);
vec2.push_back(minusFun);
vec2.push_back(multiply);
vec2.push_back(divide);
int a = 10, b =2;
for(auto fun: vec2){
cout<< fun(a, b) << endl;
}
vec.push_back(add);
vec.push_back(minusFun);
vec.push_back(multiply);
vec.push_back(divide);
for(auto fun: vec){
cout<< fun(a, b) << endl;
}
}
第八章-IO
string str;
cin >> str ;//cin是一个istream对象, >>标识从标准输入读取数据
cout << str ;
iostream:
- istream、wistream从流读取
ostream、wostream
iostream、wiostream
fstream:
- ifstream、wifstream
- ofstream、wofstream
- fstream、wfstream
sstream:
- istringstream、wistringstream从string读取
- ostringstream、wostringstream向string写入
- stringstream、wstringstream读写string
w开头的位宽字符版本
IO对象无拷贝或赋值:it doesnot make any sense,流像是一种连接,如果想拷贝数据可以拷贝源文件,同时读取一个IO对象会改变其状态,因此传递和返回的引用不能是const的
IO条件状态:
iostate、badbit(崩溃)、failbit(IO操作失败)、eofbit(文件结束)、goodbit
s.eof()、s.fail()、s.bad()、s.good()、s.clear()状态复位、s.clear(flags)选中状态复位、s.setstate(flags)、srdstate()
一个流一旦发生错误,其上后续的IO操作都会失败
- badbit为不可恢复的读写错误,流无法使用
- failbit(期望读int实际读char)可恢复,流还可以继续使用
- 文件结束为止时,eofbit和failbit都会被置位
while(cin >> str){
//当badbit、failbit和eofbit任一个被置位,检测流状态的条件会失败
}
/*
badbit => badbit、failbit被置位
failbit => failbit被置位
文件结束 => eofbit、failbit被置位
*/
cin.clear( cin.rdstate() & ~cin.failbit & ~cin.badbit )//复位failbit和badbit
输出缓冲
每个输出流都管理一个缓冲区,用来保存读写的数据,由于设备写可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作
可以导致缓冲刷新的原因
- 程序结束,return操作
- 缓冲区满
- endl操作符显式刷新
- 每个输出结束后unitbuf设置流的内部状态,清空缓冲区,cerr是默认设置unitbuf的
- 一个输出流关联到另一个流,当读写被关联的流时关联到的流的缓冲区会立即刷新,默认cin和cerr关联到cout,读cin或者写cerr都会导致cout的缓冲区被刷新
cout<< "hello" << endl;//换行然后刷新缓冲区
cout<< "hello" << flush;//只刷新
cout<< "hello" << ends;//空字符,刷新缓冲区
cout<< unitbuf;//所有输出操作后立即刷新缓冲区(无缓冲模式)
cout<< nounitbuf;//返回
程序异常终止时,输出缓冲区是不会被刷新的
标准库将cin和cout关联在一起,所以当执行cin>>ival的时候cout的缓冲区被刷新(在用于输入前,所有的输出都要被打印出来)
tie函数:
- 不带参数:返回指向输出流的指针
- 带参数(指向ostream的指针):将自己关联到ostream,x.tie(&o)将流x关联到o
一个输出流关联到另一个流,当读写被关联的流时关联到的流的缓冲区会立即刷新,也可以cin.tie(nullptr)标识cin不再与其他流关联
文件的输入输出:
- ifstream:从给定文件读
- ofstream:向给定文件写
- fstream:可以读写给定文件
正确的写法:
ofstream out;
out.open(ifle + ".copy");
if(out){
//通过检测badbit、failbit等检测是否open成功
do something;
}else{
cerr << "could not open :" <<end;''
}
文件模式file mode:每个流都有一个关联的文件模式
out(ofstream默认以out模式打开文件) | 以写方式 |
in(ifstream默认以in模式打开文件) | 以读方式 |
app | 每次写之前定位到文件末尾 |
ate | 打开文件后定位到文件末尾 |
trunc(ofstream默认模式) | 截断文件 |
binary | 以二进制打开文件 |
ofstream app("fileName", ofstream:app);//默认ofstream会清空文件内容,app模式保留文件内容
ofstream out;
out.open(fileName, ofstream::app);
out.write("hello world", 11);
out.close();
第九章-顺序容器
顺序容器:与元素加入容器时的位置相对应
vector | |
deque | 双端队列,支持快速随机访问 |
list | 双向链表 |
forward_list | 单向链表 |
array | 固定大小 |
string | 与vector相似的容器,专门用于保存字符 |
容器库操作的层次:
- 所有容器都支持的操作
- 仅针对顺序容器、关联容器、无序容器
- 仅适用一小部分 容器
顺序容器在初始化的时候可以提供大小
vector<int> vec(10, init);
类型别名 | |
---|---|
iterator | |
const_iterator | |
size_type | |
difference_type | |
value_type | |
reference | 元素的左值类型,等价于value_type& |
const_reference | const value_type& |
构造函数 | |
---|---|
C c; | |
C c1(c2); | |
C c(b, e); | b和e是迭代器 |
C c{a, b, ....} | |
赋值与swap | |
---|---|
c1 = c2; | 数组(非容器)不支持 |
c1 = {a, b, ....}; | |
a.swap(b); | 交换a和b的元素 |
swap(a, b); | 等价操作 |
大小 | |
---|---|
c.size() | |
c.max_size() | |
c.empty() | |
添加删除元素 | |
---|---|
c.insert(args) | |
c.emplace(inits) | |
c.erase(args) | 删除指定元素 |
c.clear() | 返回void |
所有容器都支持 ==, != ,关系运算符 < , <=, >, >= 等无序关联容器不支持
迭代器 | |
---|---|
c.begin(), c.end() | |
c.cbegin(), c.cend() | 返回const_iterator,底层const |
reverse_iterator | 逆序寻址的迭代器 |
const_reverse_iterator | |
c.rbegin(), c.rend() | 逆序的迭代器 |
c.crbegin(), c.crend() | |
迭代器有着公共的接口,如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式是相同的,例如 *iterator, ++iterator
迭代器要使用 beg != end,不要用 beg < end
6种顺序容器(Sequential Container)类型:vector,deque,list,forward_list,array,string
容器定义和初始化:顺序容器(不包括array,array其实也是可以接受大小参数初始化的)的构造函数才能接受大小参数进行初始化,如 C seq(n), C seq(n, t),初始化为n个默认值初始化,初始化为n个值为t的元素(这个构造函数式explicit的)
array在顺序容器内是一个很特别的存在,不接受迭代器的拷贝(感觉array的构建与后面这些精密设计过的容器不同,后续可以在stl源码中看看),在迭代器的拷贝过程中甚至可以元素不同
标准库array,可以设置固定大小 array<int, 10>,标准库的array是可以赋值的,arr1 = arr2,但不能赋值给一个列表(初始化是可以的),不允许 arr2 = {1,2};(arr2已经定义或者初始化了)
利用迭代器拷贝顺序容器的优势在于去除了容器本身的耦合,由于迭代器是公共的接口,所以只要是具体元素的类型是可以转换的,就可以遍历每一个值并创建一个新的容器
赋值相关运算会导致左边容器内部的迭代器、引用和指针失效,swap操作将容器内容交换不会导致直线容器的迭代器、引用和指针失效(todo)
vector<int> v1 = {1,2,3};
vector<int> v2 = {4,5,6};
auto p = v1.begin();
auto q = v2.end()-1;
cout<< *p << " " << *q << endl;
swap(v1, v2);
cout<< *p << " " << *q << endl;
output:
1 6
1 6
swap不是拷贝交换,而是从根本上swap
标准库的容器支持关系运算符:> <...,比较的是前缀的大小,类似string的比较
容器的相等时利用内部元素的 == 运算符来实现比较的,其他关系运算符是使用元素的<运算符
顺序容器添加元素:
/*
array不支持,因为会改变容器大小
forward_list不支持push_back和emplace_back有自己专有版本的insert和emplace
vector和string不支持push_front和emplace_front
*/
c.push_back(t)
c.emplace_back(args)
c.push_front(t)
c.emplace_fornt(args) //返回void,args用后一个?
c.insert(p, t)
c.emplace(p, args)
c.insert(p, n, t) //插入n个值为t的元素,返回指向新添加的第一个元素的迭代器,n = 0 时返回p
c.insert(p, b, e) //迭代器b,e之间的元素插入,返回同上
c.insert(p, il) //il是一个{1, 2, 3}
//向一个vector、string或deque插入元素会使得所有指向容器的迭代器、引用和指针失效
string也是支持push_back的(但是只能push_back 字符类型)
容器元素是拷贝:当我们用一个对象来初始化容器时,实际上放入容器中的是对象值得一个拷贝,而不是容器本身
vector<string> vec;
string str = "hello world";
vec.push_back(str);
vec[0] = "ni hao";
cout<< str << endl; //hello world
cout<< vec[0] << endl;//ni hao
list、forward、forward_list和deque还支持push_front的类似操作(vector不支持)
insert操作提供了更一般的添加功能,vector、deque、list和string都支持insert
//vector不支持push_front,但是可以用insert实现
vec.insert(vec.begin(), val);
注意insert插入的迭代器不能是当前顺序容器本身的迭代器,如 vec.insert(vec.begin(), vec.begin(), vec.end())
c++11标准下insert会返回第一个新加入元素的迭代器,以前的版本会返回void,相当于push_front:
vector<int> vec;
auto it = vec.begin();
for(int i=0 ; i<10 ; ++i){
it = vec.insert(it, i);//每次返回begin()迭代器
//如果这里是it = vec.insert(it, i)+1,这返回的是顺序0 1 2 3...
}
for(const auto &n: vec){
cout<< n << " ";//9 8 7 6 5 4 3 2 1 0
}
cout<<endl;
emplace_back相当于直接在容器内创建对象,而不是拷贝一份存在容器中,实际上提高了效率并解耦
vector<Person> vec ;
Person zhangSan = Person("zhangSan", 18);
vec.push_back(zhangSan);
vec.emplace_back("liSi", 22);
cout<< vec.size() << endl;
vec[0].name = "ZZZZZ";
vec[1].name = "LLLLL";
cout<< zhangSan.name << endl;//zhangSan
//vec = ["zzz", "lll"],vec[0]是zhangsan的拷贝
vec.push_back(Person("zhangSan", 18));//创建了一个临时的对象并将这个临时对象压入容器
vector<Person> vec ;
vec.push_back(Person("zhangSan", 18));//创建了一个临时的对象并将这个临时对象压入容器
vec.emplace_back("liSi", 22);
vec.emplace_back();//调用默认构造函数
auto beg = vec.begin();
vec.emplace(beg, "wangWu", 100);
for(const auto & p: vec){
cout<< p.name << " " << p.age<< endl;
}
//wangWu 100
//zhangSan 18
//liSi 22
// 0
front和back:包括array在内的每个顺序容器都有front成员函数,除了forward_list之外的所有顺序容器都有一个back成员函数,这两个操作分别返回首元素和尾元素的引用(迭代器返回的是指针)
cout<< (vec.front().name == (*vec.begin()).name) <<endl;//1
front、back函数与迭代器其他区别还有:back返回的是最后一个元素的引用,而end()返回的是指向最后一个元素之后的位置的迭代器的指针
vector<Person> vec = {Person("zhangSan", 18), Person("liSi", 111)};
vec.front().name = "changeFrontName";
auto p = vec.back();//返回引用当右值,普通变量做左值只是拷贝一份
p.name = "changeBackName1";
cout<<vec[0].name<<" "<<vec[1].name<<endl; //changeFrontName liSi
auto &q = vec.back();
q.name = "changeBackNameByRef";
cout<<vec[0].name<<" "<<vec[1].name<<endl; //changeFrontName changeBackNameByRef
vec.at(index)可以跑出out_of_range异常
顺序容器的删除操作:
//array不适用
//forward_list 不支持pop_back,特殊的erase
//vector和string不支持pop_front
c.pop_back()
c.pop_front()
c.erase(p)//删除迭代器p所指的元素,返回一个指向被删除元素之后的迭代器,若p是尾迭代器,则返回尾后(off-the-end)迭代器,若p是尾后迭代器,函数行为未定义
c.erase(b, e)//与上面的区别是若e是尾后迭代器,则返回尾后迭代器
c.clear()
返回下一个迭代器是的操作可以连续,例如 it = vec.erase(it),则删除当前it指向的元素并使得迭代器指向下一个位置(相当于it++)
forward_list是一个单向链表,所以是无法获取当前迭代器的前一个元素的,(考虑删除,是否是通过cur = next,然后删除next链表节点?),链表的操作基于前一个元素,所以相应的操作替换为insert_after、emplace_after和erase_after的操作,如果想插入链表的头部,则需要在before_begin的地方(off-the-begining迭代器)插入
lst.before_begin()
lst.cbefore_begin()
lst.insert_after(p, t)
lst.insert_after(p, n ,t)
lst.insert_after(p, b, e)
lst.insert_after(p, il) //返回指向最后一个插入元素的迭代器,若p为尾后,则未定义
emplace_after(p, args)//返回一个指向新元素的迭代器,若p为尾后,则未定义
lst.erase_after(p)//若p为尾后,则未定义
lst.erase_after(b, e)//若p为尾后,则未定义
forward_list在迭代的时候需要用pre记录上一个节点来通过after对当前节点操作
改变容器大小:resize,如果容量不足,后部的元素会被删除,如果容量富足,可以添加新元素
vector<int> vec(10, -1);
display(vec);
vec.resize(5);
display(vec);
vec.resize(18, 33);
display(vec);
//-1 -1 -1 -1 -1 -1 -1 -1 -1 -1
//-1 -1 -1 -1 -1
//-1 -1 -1 -1 -1 33 33 33 33 33 33 33 33 33 33 33 33 33
resize缩小容器的时候,同样的指向被删除元素的迭代器、引用和指针都会失效
迭代器相关:
- insert和erase之后需要重新计算迭代器
- 在添加或者删除元素时,不要保存end返回的迭代器(插入和删除操作会导致迭代器失效)
可以这样写 while(beg != vec.end())
vector的容量有两个成员函数:大小size()和容量capacity(),因为vector底层是一段连续的内存空间
string构造方法:
string s(s1, startIndex, endIndex);//endIndex超了就截断
string s(s2, startIndex);
substr操作:与Java的substring不同,substr(startIndex, len),而不是endIndex
string src = "abcdefghijk";
cout<< src.substr(2, 4) << endl;//cdef
vector<char> vec = {'a', 'b', 'c'};
string vec2str(vec.cbegin(), vec.cend());
cout << vec2str << endl;//abc
//如果每次读入一个字符,读取固定长度很长的字符,可以使用vector或者arr实现,然后一次性拷贝生成string,性能应该会较高
string操作:
string str1 = "hello_world_test_this";
str1.insert(1, 3, 'W');// hWWWello_world_test_this
cout<< str1 << endl;
string str2 = "_INSERT";
str1.insert(str1.size(), str2); //hWWWello_world_test_this_INSERT
cout<< str1 << endl;
str1.erase(1, 20);// hhis_INSERT
cout<< str1 << endl;
str1 = "hello";
str2 = "world";
str1.insert(0, str2, 1, 3);// orlhello
cout<< str1 << endl;
string的另外操作函数:append(), replace() P323
s2.replace(11, 3, "5thxx");//从位置11开始,删除3个字符然后插入"5thxx"
string的搜索操作:均返回string::size_type值标识匹配位置的下标,如果搜索失败,返回string::npos的static成员,标准库将npos定义为一个const string::size_type类型,并初始化为-1
string str1 = "hello";
auto ret = str1.find("think");
cout<< ret << endl; //18446744073709551615
cout<< static_cast<bool>(ret == string::npos) << endl; //1
string str2 = "abcde";
auto r1 = str1.find_first_of(str2); //1,第一个被找到的是e
cout<< r1 << endl;
auto r2 = str1.find_first_not_of(str2); //0
cout<< r2 << endl;
/**
* first_pos
* last_pos
* find_first_of(target, startIndex)
*/
compare函数,s1.compare(s2)
string转换:
double convert2number(string str){
string numberAndSign = "-+.0123456789";
return stod(str.substr(str.find_first_of(numberAndSign)));//会截断一部分3.14159
}
int main(){
double a = 3.14;
string s1 = to_string(a); //3.140000
cout<< s1 << endl;
double b = stod(s1);
cout<< b +1 << endl;
string str1 = "number val = 3.1415926";
cout << convert2number(str1)<< endl;
}
容器适配器:
除了顺序容器之外,标准库定义了3个顺序容器适配器:stack、queue和priority_queue,适配器是标准器的一个通用概念,一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型
适配器支持的操作和类型:
size_type
value_type
container_type //底层容器类型
A a;
A a(c); //创建适配器a,带有容器c的一个拷贝
支持关系运算符 == != > < >= <=
a.empty()
a.size()
swap(a,b)
a.swap(b)
默认情况下,stack和queue是基于deque实现的(Java其实是同样的实现方法),priority_queue是在vector之上实现的(利用堆的下标进行上滤和下滤?)但可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。但是在适配器上只能使用适配器的操作,例如一个底层是vector的stack适配器,添加元素需要用push而不是push_back
适配器可能试图在抽象的更高层定义一个统一的数据抽象模型,但同时允许一定程度的自定义底层结构
s.pop()
s.push(item)
s.emplace(args)
s.top() //不是s.peek()
s.empty()
q.pop()
q.front() //首元素或尾元素
q.back() //只用于queue
q.top()
// priority queue
pq.push(item)
q.emplace(args)
第十章-泛型算法
标准库定义了一组泛型算法(generic algorithm),这些泛型算法实现了一些经典的公共接口,如排序和搜索,它们可以作用于不同类型的元素和多种容器类型(包括内置的数组类型等)
code-二维数组排序
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>
#include <numeric>
#include <iterator>
using namespace std;
constexpr int col = 2;
void displayTwoDimensionalArray(const int (*p)[col], int row){
for(auto i=0 ; i<row ; ++i){
for(auto &n: *(p+i)){
cout<< n << " ";
}
cout<< endl;
}
}
int isLarger(const int arr1[2], const int arr2[2]){
return arr1[0] == arr2[0] ? arr1[1] - arr2[1] : arr1[0] - arr2[0];
}
int main() {
constexpr int row = 3;
int arr[row][col] = {
{1,2},
{3,1},
{2,2}
};
displayTwoDimensionalArray(arr, row);
sort(begin(arr), end(arr), isLarger);
displayTwoDimensionalArray(arr, row);// not work
}
code-二维vector排序
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>
#include <numeric>
#include <iterator>
using namespace std;
constexpr int col = 2;
void displayTwoDimensionalVector(const vector<vector<int>> vec){
for(auto &v: vec){
for(auto n : v){
cout<< n << " ";
}
cout<<endl;
}
}
//返回值是bool类型
bool isSmaller(const vector<int> v1, const vector<int> v2){
return v1[0] == v2[0] ? v1[1] < v2[1] : v1[0] > v2[0];
}
int main() {
vector<vector<int>> vec = {
{1,2},
{3,1},
{2,4},
{1,1},
{2,2}
};
displayTwoDimensionalVector(vec);
// vector定义了<这种运算,且与string原理相同,是挨个元素字典序的比较
sort(vec.begin(), vec.end());
cout<<" =====sort==== "<<endl;
displayTwoDimensionalVector(vec);
sort(vec.begin(), vec.end(), isSmaller);
cout<<" =====isSmaller sort==== "<<endl;
displayTwoDimensionalVector(vec);
}
Code-数组拷贝
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>
#include <numeric>
#include <iterator>
using namespace std;
void display(int *arr, int size){
for (int i = 0; i < size; ++i) {
cout<< *(arr++) << " ";
}
cout<<endl;
}
int main() {
constexpr int n = 10;
int arr[n] = {1,2,3,4,5,6,7,8,9,10};
// copy form index 4
constexpr int startIndex = 4;
int arr2[ sizeof(arr)/sizeof(*arr) - startIndex];
auto ret = copy(begin(arr)+startIndex, end(arr), arr2);
cout <<(ret == end(arr2) )<< endl;//ret等于插入的尾迭代器位置
display(arr2, end(arr2)-begin(arr2));
}
Code-vector逆序拷贝
/*
* 将3-7之间的元素逆序拷贝到list中去
*/
int main() {
vector<int> vec;
for(int i=0 ; i<10 ; ++i){
vec.push_back(i);
}
auto end = vec.crend() - 3, beg = vec.crend() - 7;
list<int> lst(beg, end); //6 5 4 3
for_each(lst.begin(), lst.end(), [](const int &n)->void{
cout << n << " ";
});
cout << endl;
}
第十一章-关联容器
标准库提供8个关联容器,有:
- set or map
- 去重 or 不去重
- 有序 or 无序
关键字保存 | |
---|---|
map | |
set | |
multimap | 可重复的 |
multiset | 可重复的 |
unordered_map | 哈希函数组织的无序集合,下同 |
unordered_set | |
unordered_multimap | |
unordered_multiset | |
关联容器的实现方式有两种,一种是有序利用比较(不是等于,是小于),另一种是利用哈希函数实现散列,后一种类似我们常在Java中使用的类型,对于第一种而言,存入的关键字key必须要支持这种比较(key1<key2),例如int,string,vector
对于那些关键字上不支持小于运算符的,可以提供一个自定义的比较操作函数,在定义关联容器中提供这个函数的指针;例如
bool compareIsbn(const Sales_data &s1, const Sales_data &s2){
return s1.isbn() < s2.isbn();
}
// multiset<,> name(function);构造函数传入function的地址,这里function的name会被编译器自动转为指针,等价于传 &function
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
错误的示例
map<vector<int>::iterator, int> m ;//vector的迭代器是可以的
map<forward_list<int>::iterator, int> m1 ;//这个不可以,不支持<
forward_list<int> lst = {1,2,3};
m1[lst.begin()] = 1;
m1[lst.end()] = 3;
cout<<m1[lst.begin()]<<endl;
error: no match for 'operator<' (operand types are 'const std::_Fwd_list_iterator<int>' and 'const std::_Fwd_list_iterator<int>')
{ return __x < __y; }
传参e.g
struct Person{
string name;
int age;
Person() = default;
Person(string name, int age):name(name),age(age){
}
};
bool compareAge(const Person &p1, const Person &p2){
return p1.age < p2.age;
}
int main(){
set<Person, decltype(compareAge)*> personSet(compareAge);//使用decltype传
personSet.insert(Person("zhangsan", 18));
personSet.insert(Person("wwww", 1));
personSet.insert(Person("lisi", 11));
for_each(personSet.cbegin(), personSet.cend(), [](const Person &p)->void {
cout << p.name << " "<< p.age << " "<<endl;
//wwww 1
//lisi 11
//zhangsan 18
});
set<Person, bool (*)(const Person &, const Person &)> set2(compareAge);//第二种写法
set2.insert(Person("zhangsan", 18));
set2.insert(Person("wwww", 1));
set2.insert(Person("lisi", 11));
for_each(set2.cbegin(), set2.cend(), [](const Person &p)->void {
cout << p.name << " "<< p.age << " "<<endl;
//wwww 1
//lisi 11
//zhangsan 18
});
}
pair类型,定义在头文件utility中,一个pair保存两个数据成员,类似容器,pair是一个用来生成特定类型的模板
pair<T1, T2> p;
pair<T1, T2> p(v1, v2);
pair<T1, T2> p = {v1, v2};
make_pair(v1, v2);// 返回一个初始化后的pair类型
p.first;
p.second;
p1 relop p2 比较大小,看情况类似于vector的比较操作,make_pair可能比二维数组更方便在某些时候
p1 == p2
p1 != p2
返回一个初始化为空的容器,返回一个空的vector,返回一个空的pair:
vector<int> funReturnVec(){
// return {}; 值初始化
return vector<int>(); //传参构造
}
pair<string, vector<int>> funReturnPair(){
return pair<string, vector<int>>();//这种事隐式构造返回值
// return make_pair("", vector<int>());
}
关联容器额外的类型别名
set<string>::value_type v1;
map<string, int>::value_type v3; //v3是一个pair<const string, int>
解引用一个关联容器迭代器时我们会得到一个类型为容器的value_type的值得引用,注意此时对map而言first成员保存的key是const的,也就是说不能这样赋值:
mapIt->first = "new key"; //错误,迭代器解引用的key是const的
同样的set的迭代器也是const的(可以认为这两种迭代器对key的操作都是const的,只读)
我们通常是不对关联容器使用泛型算法的,关键字是const的这一特性意味着不能将关联容器传递给修改或者重排容器元素的算法,泛型算法在关联容器中总是会顺序搜索,使用关联容器定义的专用的find成员会比调用泛型find快得多
插入元素
set<string> set1;
set1.insert("hello");
vector<string> vec = {"1", "2", "3"};
set1.insert(vec.cbegin(), vec.cend());
display(set1);// 1 2 3 hello
map<string,int> map1;
map1["amazing"] = 666;//如果下标不存在的话,会创建一个元素并插入到map中
map1.insert({"zhangSan", 1});
map1.insert(make_pair("liSi", 1));
map1.insert(pair<string, int>("wangWu", 1));
auto res = map1.insert(map<string,int>::value_type("zhaoLiu", 1));
for_each(map1.cbegin(), map1.cend(), [](pair<string,int> p)->void {
cout<< p.first << " : " <<p.second << endl;
});
//1<-insert pair's value | insert result->1
cout << res.first->second << "<-insert pair's value | insert result->" << res.second << endl;
emplace也可以执行插入操作,emplace返回的是一个pair<指向插入key的迭代器, bool值>,insert和emplace返回的第二个值都是一个bool值,显示插入成功或者失败,对于multi这类的容器插入的时候因为不会出现重复的key所以不会返回bool值
删除元素:
如果使用泛型算法erase(迭代器,一对迭代器),则会返回指向最后删除元素的后一个元素的迭代器,如果提供的参数是key的话,则会返回删除的元素的数量(可以作为判断条件)
下标运算符(multimap不支持):如果下标不存在的话,会创建一个元素并插入到map中,但是目测这种方式会有性能上的损耗,因为它会先去搜索key,然后插入key并默认初始化,最后写入value,(但是在某些特殊场合会使得程序更加简洁明了),下标和insert的另一个区别是下标总是保留最后一个,insert总是保留第一个
//下标的和insert的区别
map<int,int> map2 = {
{1,2}
};
map2.insert({1,3}); //重复key,不能插入,1 : 2
for_each(map2.cbegin(), map2.cend(), [](pair<int,int> p)->void {
cout<< p.first << " : " <<p.second << endl;
});
map2[1] = 4; //重新给key赋值value 1 : 4
for_each(map2.cbegin(), map2.cend(), [](pair<int,int> p)->void {
cout<< p.first << " : " <<p.second << endl;
});
这里要主要下标运算符得到的结果和迭代器解引用不同
查找元素:
// 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
cout << set1.count(1) << endl; // 2
cout << (set1.find(1) == set1.cend()) << endl;//0
cout << *set1.lower_bound(2) << endl;//2 返回第一个关键字不小于2的迭代器
cout << *set1.upper_bound(2) << endl;//3 返回第一个关键字大于2的迭代器
auto v = set1.equal_range(2);//返回的是关键字等于2一个迭代器的范围,if不存在则返回两个set1.end()
for(auto i=v.first ; i!=v.second ; ++i){
cout << *i << " "; // 2 2
}
count和find组合可以实现:
cnt = count();
it = find();
while(cnt){
cout<<it->second;//对一个map而言
++it;
--cnt;
}
或者是:
for(auto beg = m.lower_bound(key), end = m.upper_bound(key) ; beg != end ; ++beg){
cout << beg->second << endl;
}
//或者是
for(auto pos=m.range(key); pos.first!=pos.second; ++pos.first){
cout << pos.first->second <<endl;
}
无序容器unordered的效率通常会更高,无序容器甚至可以直接操作桶的数量,P395
由于比较实用 == ,以及hash需要,对于自定义的类型需要定义这两个重载函数,或提供两个函数来代替,例如书中的例子:
size_t hasher(const Sale &s){
return hash<string>()(s.isbn());
}
bool eqOp(const Sale &s1, const Sale &s2){
return s1.isbn() == s2.isbn();
}
using s_set = unordered_multiset<Sale, decltype(hasher) *, decltype(eqOp) *>;
s_set bookstore(42, hasher, eqOp);
c++异常处理机制:
- throw表达式
- try语句块
- 异常类
函数:
函数的调用:通过调用运算符'('和')'来执行函数,调用运算符作用于一个表达式,该表达式时函数或者指向函数的指针
内联函数:在每个调用点上内联的展开,在返回类型之前加上inline.内联函数适合规模小、流程直接、频繁调用的函数
Constexpr函数:要求的是函数内全部使用字面量,并非常量,返回值可能是常量也可能不是常量,这个函数被隐式的指定为内联函数
含有多个形参的函数匹配:
当有且只有一个函数满足:每一个都必须是最优选之一,至少一个形参是最优没有之一
类
类的基本思想是数据抽象和封装,数据抽象是将接口和实现进行分离,接口包括用户所能执行的操作,实现包括成员属性和方法的实现,封装实现了类的接口和实现的分离,用户只能访问接口而不知道实现的细节
成员函数的声明必须在类的内部,定义可以在内的内部也可以在类的外部。如果是定义在类的内部的成员函数是隐式的inline函数
成员函数通过一个隐式的this参数来访问调用它的对象,当我们调用一个函数时,用请求该函数对象的地址来初始化this对象,比如 Sales_data::isbn(&obj)
class关键字和struct关键字的唯一区别就是默认访问级别,struct默认public,而class默认是private
友元:要想让fun函数访问当前类的私有成员,只需要在类定义中增加 friend fun(xx);的函数声明即可,但是这个声明只是权限声明,而非函数声明(所以最好在类定义外再做一次函数的声明--无须权限的函数声明)
在一个const成员函数内改变成员属性:可以将成员属性声明成mutable,这样可以通过const检查
泛型算法
这些泛型算法不直接操作容器,而是遍历迭代器,这样就屏蔽了不同容器之间的差异,甚至提供者可以不是容器.算法本身不会执行容器的任何操作,所有的操作都是基于迭代器的,这样就有一个前提,那就是算法的操作永远不会向容器中增加或是删除元素
equal以及一些其他的算法在比较两个容器的时候都会采用这种形式:functionName(container1.begin(), container1.end(), container2.begin() )确保container2的迭代器有足够的长度是程序员所要确保的事,也就说长度小的放在前面
Back_inserter函数
#include <iterator>
vector<int> vec;
auto it = back_inserter(vec);
for (int i = 0; i < 3; ++i) {
*it = i;// 每次调用it的时候会更新迭代器
}
display(vec);// 0 1 2
vector<int> vec2;
fill_n(back_inserter(vec2), 10, 22);
display(vec2);// 22 22 22 22 22 22 22 22 22 22
Array_copy函数
constexpr int n = 10;
int arr[n] = {1,2,3,4,5,6,7,8,9,10};
// copy form index 4
constexpr int startIndex = 4;
int arr2[ sizeof(arr)/sizeof(*arr) - startIndex];
auto ret = copy(begin(arr)+startIndex, end(arr), arr2);
cout <<(ret == end(arr2) )<< endl;//ret等于插入的尾迭代器位置
display(arr2, end(arr2)-begin(arr2));
vector_copy函数
vector<string> v1 ={"abc", "def", "iop"};
vector<string> v2;
copy(v1.cbegin(), v1.cend(), back_inserter(v2));//abc def iop
display(v2);
去重函数
vector<int> vec = {1,2,3,4,1,1,2,2,3,1};
//去重
sort(vec.begin(), vec.end());
auto it = unique(vec.begin(), vec.end());
vec.erase(it, vec.end());
display1(vec);//1 2 3 4
二维vector自定义排序
//返回值是bool类型
bool isSmaller(const vector<int> v1, const vector<int> v2){
return v1[0] == v2[0] ? v1[1] < v2[1] : v1[0] > v2[0];
}
int main() {
vector<vector<int>> vec = {
{1,2},
{3,1},
{2,4},
{1,1},
{2,2}
};
displayTwoDimensionalVector(vec);
// vector定义了<这种运算(sort的默认排序方式),且与string原理相同,是挨个元素字典序的比较
sort(vec.begin(), vec.end());
cout<<" =====sort==== "<<endl;
displayTwoDimensionalVector(vec);
sort(vec.begin(), vec.end(), isSmaller);
cout<<" =====isSmaller sort==== "<<endl;
displayTwoDimensionalVector(vec);
}
output:
1 2
3 1
2 4
1 1
2 2
=====sort====
1 1
1 2
2 2
2 4
3 1
=====isLarger sort====
3 1
2 2
2 4
1 1
1 2
stable_sort
sort算法接受的第三个参数叫做谓词,谓词的意思是一个可调用的表达式,返回结果是一个能用作条件的值,根据接受参数的个数分为一元谓词和二元谓词,sort用这个谓词代替<(小于号)来比较元素(这里是因为sort默认是按照升序来排序的),在上个例子中相当于判断 a<b 等价于 isSmaller(a,b)的返回值
partition方法
bool isMoreThanFive(const string &str) {
return str.size() >= 5;
}
int main() {
vector<string> vec;
auto it = back_inserter(vec);
string str;
while (cin >> str && str != "Quit") {
*it = str;
}
displayVector(vec);
auto start = partition(vec.begin(), vec.end(), isMoreThanFive);
vec.erase(start, vec.end());//只保留输入长度大于5的
displayVector(vec);
}
lambda表达式:我们可以向一个算法传递任何类别的可调用对象(如果可以使用'()'这个调用运算符调用的就是可调用对象,例如sort() )
可调用对象一共有四种:
- 函数
- 函数指针
- 重载了函数调用符的类
- lambda表达式
一个lambda表达式的形式如下
[capture list捕获列表不可省略](para list) -> return type {function body不可省略}
捕获列表只用于当前函数内lambda函数外的局部变量,对于当前函数外的变量不需要捕获就可以直接使用
void findBigSize(vector<string> &vec, int minimum){
auto it = partition(vec.begin(), vec.end(), [minimum](const string& str)->bool{
return str.size() >= minimum;
});
vec.erase(it, vec.end());
for_each(vec.cbegin(), vec.cend(), [](const string& s)->void{
cout<< s << " ";
});
cout<<endl;
}
int main() {
vector<string> vec;
string str;
cout<< "Enter strings, 'quit' for Quit! "<<endl;
while(cin >> str && str != "quit"){
vec.push_back(str);
}
cout<< "Enter minimum size:"<<endl;
string num;
cin >> num;
findBigSize(vec, stoi(num));
}
定义一个lambda表达式时,编译器生成一个与之相对应的类类型,使用lambda的时候实际上在使用这种类型的对象,默认情况下生成的类都包含一个在lambda所捕获的变量的数据成员,数据成员在创建lambda的时候被初始化,捕获的变量是在创建的时候拷贝,而不是调用的时候拷贝,lambda捕获的值也可以是引用,但是必须保证在lambda表达式调用的时候这个变量没有消亡
隐式捕获:
[=]值
[&]引用
[=, &val]
[&, val]混合
如果一个lambda表达式返回return之外的任何语句(如if..),编译器会假定这个lambda返回void,需要制定返回类型
lambda表达式一个很大的优势在于,它可以灵活的适配那些需求指定了形参个数以及类型的地方,可以将一部分的形参移动到捕获列表中,使用bind也可以达到同样的效果,
using namespace std::placeholders::_1;
auto bindFunction = bind(oldFunction, _1, arg2);//这里标识以_开头标识调用bindFunction要传入的参数
迭代器的其他几种:
- 插入迭代器:back_inserter, front_inserter, inserter,只有容器支持的情况下push_back,push_front才可以用
- 流迭代器
- 反向迭代器
- 移动迭代器
反向迭代器:
- 首先要理解反向迭代器的前进方向是相反的,这是其他的条件的基础
- 反向迭代器的操作是相反的:例如 ++it实际上是递减的移动,(考虑前进方向)
- 如果想对一个反向迭代器截取,( it, vec.rend() )也就是 (it, vec.begin() )才是默认的截取(考虑前进方向),如果想按照正常的方向截取( it, vec.end() )会得到一个错误的结果,因为反向迭代器并不向容器的尾部前进,这时候需要用base()进行重定位
- 由于左闭右开的截取原则,反向迭代器进行base的时候并不是原来的位置了而是相邻的位置,标准库对此参照我们的直觉做了优化
迭代器类别:
输入迭代器 | 只读 | 单遍 | 只能递增 | ||
输出迭代器 | 只写 | 单遍 | 只能递增 | ||
前向迭代器 | 只能递增 | Forward_list | |||
双向迭代器 | |||||
随机访问迭代器 | 算术运算 > < <= >=<br />+= -= <br />两个迭代器相减 |
算法的样式:
- 算法的形参 如: algorithm( src范围, dest范围, 其他参数),其中src范围一般是两个迭代器,dest范围如果是一个迭代器则表示从这个迭代器开始一直执行
- 算法命名的规范:
- 算法默认比较的方式有是否相等,默认使用"==",排序,默认使用"<"(参考sort)
- 算法的重载版本可以传入谓词参数(自定义相等的条件,大小的条件),如sort(v.begin(), v.end(), lambda表达式 )
- _if类似算法的重载版本,find_if(beg, end, 谓词arg)
- _copy返回的值拷贝到dest中去,reverse(beg, end, dest)
对于list和forward_list应该尽量使用成员函数的算法(针对数据结构做了优化)而不是通用的算法,调用方法 lst.merge(lst2)
类的定义
struct ClassName{
};//这里必须有分号
struct Person{
} zhangsan, lisi, *wangwu;//可以将类的定义与对象的定义放在一起写,但不推荐
类通常在头文件中定义,头文件改变的时候相关的源文件也需要重新编译
头文件保护符:
-
define
-
ifdef
-
ifndef
-
endif
析构函数:
- 无法重载
- 析构的时候先执行函数体,然后执行与构造函数相反的析构顺序(先构造的后析构)
- 在对每个对象析构的时候,如果是基本类型,直接析构,如果是其他类型,执行自己的析构函数:例如对于普通指针直接销毁,但是对于智能指针,由于它有自己的析构函数,所以会执行delete方法
- 何时销毁,例如:对象销毁的时候成员变量都会被销毁;容器销毁的时候内部元素都会被销毁;动态分配的对象,对该指针delete的时候被销毁;临时对象表达式结束的时候被销毁
关于拷贝构造函数和拷贝赋值运算符:
- 编译器是可以绕过拷贝构造函数的
/**
* 关于:
* 构造函数
* 拷贝构造函数
* 拷贝赋值运算符
*
* 在何时调用
*
* 结论:
* 有Person这种才是构造函数或者拷贝构造函数
* a = b 这种没有类名的是拷贝赋值运算符
* 编译器可以绕过拷贝构造函数
*/
#include <iostream>
using namespace std;
class Person{
public:
Person(){cout << "default " << endl;}
Person(const Person & p){ cout << " copy "<< endl;}
Person operator=(const Person &p){cout << " operator " << endl;}
};
void f1(Person p){ return ;}
Person f2(){ Person p; return p;}
int main() {
Person p1; //default
cout<< "=========p1 end========="<< endl;
Person p2 = p1;// copy
cout<< "=========p2 end========="<< endl;
Person p3(p2);// copy
cout<< "=========p3 end========="<< endl;
Person p4;//default
p4 = p3;//operator
cout<< "=========p4 end========="<< endl;
f1(p4);// copy
cout<< "=========f1 end========="<< endl;
f2();//default
cout<< "=========f2 end========="<< endl;
Person p5 = f2();//default 编译器绕过了拷贝构造函数而使用的是构造函数
cout<< "=========p5 end========="<< endl;
}
三五法则:析构函数、拷贝赋值运算符和拷贝构造函数应该被视为一个整体
- 定义特定的析构函数,则必须要定义另外两个:否则内存被重复释放
- 拷贝赋值运算符和拷贝构造函数是一体的:因为当你需要一个 特定的拷贝过程的时候需要构造函数和运算符配合执行
阻止拷贝:
NoCopy(const NoCopy &) = delete;
使用delete和 private拷贝构造函数都可以阻止拷贝,但是前者更彻底,后者不能阻止类的友元函数和成员函数进行拷贝(可以通过声明但不定义的方式,但是可能会导致某种链接错误),所以推荐使用delete
赋值运算符:
- 保证将一个对象赋值给它自身时也是正常的
- 大多数时候结合了析构函数和拷贝构造函数的工作
struct Mycopy{
string *p;
}
MyCopy& MyCopy::operator=(const MyCopy &m){
//拷贝的时候需要先释放当前p指向的内存,然后拷贝m的p指针指向的内存
//这时候不能直接delete p,因为可能是 Mycopy a = a;delete p会导致两个a的p指针指向的内存同时失效
//需要一个tmp
auto tmp = new string(*m.p);//深度copy m的p指针指向的对象
delete p;
p = tmp;
return *this;
}
e.g:注意(递增运算符优先级高于解引用运算符 ++运算符优先于*)
事实上Student s2 = s1;和Student s2 (s1);是等价的,在赋值运算符的函数体中非常容易出错
/**
* 拷贝一个行为像指针的类,底层是可以共享数据的
* 如果使用拷贝构造函数赋值,则会共享数据,use记录当前数据被多少个对象共享
* 析构函数需要在use == 0的时候进行析构
*/
class Student{
public:
string name;
int *score;
size_t *use;
Student()=default;
Student(string name, int score):name(name), score(new int(score)), use(new size_t(1)){ cout << "default with name" << endl;}
Student(const Student &s):name(s.name), score(s.score), use(s.use){
//这里用s的use指针对this.use进行初始化,两个指针指向同一个地址
//增加 计数
//递增运算符优先级高于解引用运算符 ++运算符优先于*
++*use;
cout << "copy" << endl;
}
Student& operator=(const Student &s){
//同样是拷贝,但是要加上引用计数的考虑
++*s.use;
if(--*use == 0){
//对原来分配的所有动态内存都需要进行析构,不光是use指针
delete use;
delete score;
}
use = s.use;
name = s.name;
score = s.score;
cout << "operator=" << endl;
return *this;
}
~Student(){
if (--*use == 0){
delete use;
delete score;
}
}
};
void display(const Student & s){
cout << s.name << " " << *s.score << " " << *s.use << " " << endl;
}
int main() {
Student s1 = {"zhangSan", 20};
display(s1);
cout << "============= s1 end ===========" <<endl;
Student s2 = s1;
display(s2);
cout << "============= s2 end ===========" <<endl;
Student s3(s2);
display(s3);
cout << "============= s3 end ===========" <<endl;
Student s4;
s4 = s3;
display(s4);
cout << "============= s4 end ===========" <<endl;
s4.name = "lisi";
*s4.score = 33; //score是共享的指针
display(s1);
display(s2);
display(s3);
display(s4);
}
output:
default with name
zhangSan 20 1
============= s1 end ===========
copy
zhangSan 20 2
============= s2 end ===========
copy
zhangSan 20 3
============= s3 end ===========
operator=
zhangSan 20 5
============= s4 end ===========
zhangSan 33 5
zhangSan 33 5
zhangSan 33 5
lisi 33 5
Process finished with exit code 0
定义自己的swap函数可以避免在swap的时候进行多次拷贝赋值,而仅仅是交换底层的指针
void swap(Student &s1, Student &s2){
using std::swap;//没有namespace,这个swap只会交换指针而不是拷贝到tmp
swap(s1.name, s2.name);
swap(s1.score, s2.score);
// use是代表原来的数据有多少个共享,在swap之后每个底层数据的引用数量是不变的,如果使用标准库的swap会重复加减
}
赋值运算符可以依靠swap函数以一种非常安全的方式构造:
/*对于a = b,来说
函数执行
b1 = b;//第一步是拷贝保存,所以保证了a = a这种自赋值的绝对安全
swap(a, b1);
return a;
*/
Person& operator=(Person p){
//传入的是p的副本
swap(*this, p);
return *this;
}
移动构造函数:
对于string这类值拷贝的对象,如果移动则可能会将原string拷贝到一个新的地址,然后销毁原地址的string对象,标准库提供了一种移动来代替这种拷贝-销毁的动作,std::move,并且标准库保证移动后的对象也是可以正常析构的
右值引用:左值相当于对象本身,右值相当于值或者表达式(本质还是用它的值),对于val = 10 * v1 ;这个表达式来说val是左值而10*v1是右值;右值存在一种 特殊的情况时可以将const左值引用绑定到右值上。右值引用只能绑定到将要被销毁,或者没有其他使用者的对象上
int &&rr3 = std::move(rr1); //除了对rr1赋值或者销毁以外,将不在使用rr1
第十二章-动态内存
静态内存用来保存局部static对象、类的static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁,栈对象仅在程序块运行时才存在,static对象在使用之前分配,在程序结束时销毁。
动态内存-即堆,程序用堆来存储动态分配的对象,动态对象的生存期由程序控制,要显式的销毁动态对象
动态内存的管理:
- new,分配空间返回一个指向该对象的指针
- delete,接受一个动态对象的指针,销毁该对象,释放内存
- 智能指针(负责自动释放所指向的对象)
- shared_ptr
- unique_ptr
- weak_ptr
智能指针也是模板,创建时需要显式给出可以指向的类型
shared_ptr<string> p1;
shared_ptr<vector<int>> p2;
swap(p, q);
p.swap(q);
//shared_ptr独有的操作,使用的类似引用计数的原理,左值引用--,右值引用++;当引用计数等于0的时候则会释放内存
make_shared<T>(args);
shared_ptr<T>p (q);
p = q;
p.unique(); //p.use_count() == 1返回true,否则返回false
p.use_count();//返回共享对象的智能指针数量
auto p = make_shared<int>(42);
r = q;//给r赋值,指向另一个地址
// q的计数++
// r的计数--
// r原来指向的对象如果没有引用者,会自动释放
引用计数是与智能指针(还是与智能指针指向的对象)相关联的,通常当指针被销毁的时候对象并不会随之销毁,但是智能指针在销毁的时候递减引用计数如果为0则销毁指向的对象
程序使用动态内存的原因:
- 程序不知道自己要使用多少对象:容器
- 程序不知道所需对象的准确类型
- 程序需要在多个对象之间共享数据
当多个对象共享同一份数据的时候,一个对象被销毁不能销毁这份数据,直到这份被复用的数据没人使用的时候才可以
new分配空间失败时会跑出一个类型为bad_alloc的异常,可以阻止抛出异常
int *p2 = new (nothrow) int;//分配失败则返回空指针
delete的指针必须指向动态分配的内存,或者一个空指针。释放一块并非new分配的内存,或者将相同的内存释放多次,其行为是未定义的
常见问题:忘记delete内存,使用已经释放的对象,同一块内存释放两次(delete一个指针后,指针值就变得无效了,但是指针内部的内存地址在很多机器上还会保存,因此会delete一个没有new的内存区域)
智能指针的构造函数式explicit的,因此:
shared_ptr<int> p1 = new int(1024);//无法隐式转换,必须使用构造的形式
shared_ptr<int> p2(new int(1024));//正确
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存(new对应delete),因为智能指针默认使用delete释放它所关联的对象
shared_ptr<T> p(q);//q指向new分配的内存
shared_ptr<T> p(u);//从unique_ptr u处接管对象的所有权,u置为空
shared_ptr<T> p(q, d);//接管内置指针q, p使用d代替delete
shared_ptr<T> p(p2, d);//拷贝shared_ptr p2, p使用d代替delete
p.reset(); //释放对象
p.reset(q); //p指向q
p.reset(q, d); //调用d释放
void process(shared_ptr<int> ptr){
}
// ptr离开作用域,被销毁
int *X(new int(1024)); // 操作动态内存
process(x); //shared_ptr要求显式explicit的构造函数
process(shared_ptr<int>(x)); //执行完后shared_ptr会执行delete释放内存,当普通指针将一块内存托管给智能指针后就不应该再使用普通指针来访问这块内存了
智能指针的get方法,只有确定代码不会delete指针的情况下才能使用get,永远不要用get初始化另一个智能指针
智能指针比动态内存更好的另一个地方是如果在new 和delete之间发生异常的话,动态内存则不会触发delete来释放内存,而智能指针可以在销毁这个局部对象的时候检查引用计数触发delete
对于某些没有析构函数的类来说,使用智能指针可以确保这些类的对象正确的被释放(因为可以使用指向删除器函数的参数)
unique_ptr在初始化的时候必须要绑定到一个new返回的指针上,不支持拷贝和赋值,
//转移控制权
unique_ptr<string> p2(p1.release());
p2.reset(p3.release());
weak_ptr:指向由一个shared_ptr管理的对象,将weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
if( shared_ptr<int> np = wp.lock() ){
//在if中访问共享对象是安全的,否则对象可能已经被释放了
}
allocator类(头文件memory中)
模板类(类似vector)帮助将内存分配和对象构造分离开来,定义参考vector
allocator<string> alloc;//可以分配string的allocator对象
auto const p = alloc.allocate(n);//分配n个未初始化的string
auto q = p; //尾部之后的一个位置方便插入
alloc.construct(q++, "hi");
*p;// hi
*q;// q指向未构造的内存
//销毁
while( q!= p ){
alloc.destroy(--q);//从end向start
}
第十三章-拷贝控制
拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数,如果没有定义拷贝构造函数,编译器也会默认定义一个合成拷贝构造函数,这个函数对类类型的成员会使用构造函数来拷贝,对于内置类型的成员会直接拷贝
在容器insert和emplace操作的时候,insert的操作是有个拷贝初始化的过程,emplace则是直接初始化
class Person{
Person(const Person &);//这里如果不是引用,则调用拷贝构造函数需要先调用构造函数...循环调用
}
因为拷贝构造函数的存在,所以可以使用 { ... }来给要赋值的对象的成员属性赋值
Person p = "name";//右侧就算是一个对象同样也是提取它的成员属性进行赋值,但是对explicit不适用
重载运算符:本质是函数,其名字由operator关键字后接表示要定义的运算符的符号组成,赋值运算符就是一个名为operator=的函数,类似于任何其他函数,运算符函数也有一个返回类型和一个参数列表
重载运算符的参数表示运算符的运算对象,如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数,二元运算符的右侧运算对象作为显式参数传递
Foo & operator=(const Foo &);//赋值运算符,返回一个指向其左侧运算对象的引用
Code-拷贝控制与赋值运算符等
/**
* 关于:
* 构造函数
* 拷贝构造函数
* 拷贝赋值运算符
*
* 在何时调用
*
* 结论:
* 有Person这种才是构造函数或者拷贝构造函数
* a = b 这种没有类名的是拷贝赋值运算符
* 编译器可以绕过拷贝构造函数
*/
#include <iostream>
using namespace std;
class Person{
public:
Person(){cout << "default " << endl;}
Person(const Person & p){ cout << " copy "<< endl;}
Person operator=(const Person &p){cout << " operator " << endl;}
};
void f1(Person p){ return ;}
Person f2(){ Person p; return p;}
int main() {
Person p1; //default
cout<< "=========p1 end========="<< endl;
Person p2 = p1;// copy
cout<< "=========p2 end========="<< endl;
Person p3(p2);// copy
cout<< "=========p3 end========="<< endl;
Person p4;//default
p4 = p3;//operator
cout<< "=========p4 end========="<< endl;
f1(p4);// copy
cout<< "=========f1 end========="<< endl;
f2();//default
cout<< "=========f2 end========="<< endl;
Person p5 = f2();//default 编译器绕过了拷贝构造函数而使用的是构造函数
cout<< "=========p5 end========="<< endl;
}
Code-拷贝一个行为像指针的类
/**
* 拷贝一个行为像指针的类,底层是可以共享数据的
* 如果使用拷贝构造函数赋值,则会共享数据,use记录当前数据被多少个对象共享
* 析构函数需要在use == 0的时候进行析构
*/
class Student{
public:
string name;
int *score;
size_t *use;
Student()=default;
Student(string name, int score):name(name), score(new int(score)), use(new size_t(1)){ cout << "default with name" << endl;}
Student(const Student &s):name(s.name), score(s.score), use(s.use){
//这里用s的use指针对this.use进行初始化,两个指针指向同一个地址
//增加 计数
//递增运算符优先级高于解引用运算符 ++运算符优先于*
++*use;
cout << "copy" << endl;
}
Student& operator=(const Student &s){
//同样是拷贝,但是要加上引用计数的考虑
++*s.use;
if(--*use == 0){
//对原来分配的所有动态内存都需要进行析构,不光是use指针
delete use;
delete score;
}
use = s.use;
name = s.name;
score = s.score;
cout << "operator=" << endl;
return *this;
}
void swap(Student &s1, Student &s2){
using std::swap;//没有namespace,这个swap只会交换指针而不是拷贝到tmp
swap(s1.name, s2.name);
swap(s1.score, s2.score);
// use是代表原来的数据有多少个共享,在swap之后每个底层数据的引用数量是不变的
}
~Student(){
if (--*use == 0){
delete use;
delete score;
}
}
};
void display(const Student & s){
cout << s.name << " " << *s.score << " " << *s.use << " " << endl;
}
int main() {
Student s1 = {"zhangSan", 20};
display(s1);
cout << "============= s1 end ===========" <<endl;
Student s2 = s1;
display(s2);
cout << "============= s2 end ===========" <<endl;
Student s3(s2);
display(s3);
cout << "============= s3 end ===========" <<endl;
Student s4;
s4 = s3;
display(s4);
cout << "============= s4 end ===========" <<endl;
s4.name = "lisi";
*s4.score = 33; //score是共享的指针
display(s1);
display(s2);
display(s3);
display(s4);
}
第十四章-重载运算与类型转换
重载运算符函数的参数数量与运算符的运算作用对象一致,运算符函数是成员函数的参数数量比运算符的运算对象总数少一个(默认左侧绑定this),也就是说重载要么将运算符定义为一个类的成员函数,要么给它类类型的参数
data1 + data2 ;
operator+(data1, data2);//是等价的调用
因为重载后会变成函数调用,所以重载会丢失求值顺序,例如 &&这类的短路求值是无法保留的
重载的设计:使用与内置类型同样的语义,IO、相等性== !=、 单序比较 <,返回的类型应该兼容,如 "+" 运算符返回左侧对象的一个引用,且应相应的重载 "+="
成员函数or非成员函数:
- 成员函数:"=", "[]", "()", "->"
- "+="可以是成员函数或者非成员函数
- 改变对象状态的通常应该是成员,例如"++", "--", "*"
- 相等性、算术、关系和位运算符通常是非成员函数
成员函数的运算符重载左侧必须是运算符所属的一个对象
//重载<< 类似tostring
//非成员函数,第一个参数是ostream的引用也就是调用方(cout),第二个参数是const 引用对象,返回的是ostream的引用(ostream写入内容会改变内容所以不是const)
ostream &operator<<(ostream &os, const Sales &item){
os << item.name() << " " << item.isbn();
return os;
}
输入运算符重载的时候要考虑输入可能失败的情况,如果输入失败则重置或者自定义的状态
//如果 == 重载过了,重载 != 可以使用更简洁的方法
bool operator!=(const Sale &s1, const Sale &s2){
return !(s1 == s2);
}
关系运算符应该:
- 顺序与容器中关键字的要求一致
- 如果类同时也含有 == 运算符的话,特别是,如果两个对象是!=的,那么一个对象应该<另一个
下标运算符可以有常量版本和非常量版本的,常量版本的返回常量引用仅仅能用作下标访问读,非常量版本的可以读写,常量版本如下:
//返回const引用无法写
const string& operator[](size_t) const{ //末尾的const表示const this指针
return elements[n];
}
"++"后置版本重载时候接受一个额外的int类型形参(仅做区分可以不使用),后置版本 "i++"实际也是调用前置版本"++i"来简化实现
重载函数调用运算符"()"
class PrintStr{
public:
//构造函数
PrintStr(ostream &o = cout, char ch = '-'):os(o), seq(ch){
}
void operator()(const string &str){
for (int i = 0; i < str.size(); ++i) {
os << str[i] << seq;
}
}
private:
ostream &os;
char seq;
};
int main(){
PrintStr ps;
ps("hello");
}
lambda表达式:编译器将该表达式翻译成一个未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符
lambda表达式、函数、函数对象类都如果接受同样的参数返回同样的类型,实际使用中可能希望把它们当成一类处理如set<这一类>,或者是函数表(vector、map的函数集合)但是实际上它们并不能简单归为一类混用,但是可以都当成function类型处理(标准库类型),例如
set<function<int (int, int)>> functionSet;
类型转换函数:
operator type() const;
//例如:
//成员函数、无返回类型(有实际返回)、参数列表为空
operator int() const{
return val; //可以隐式的将所在的类对象转换为int类型
}
explicit会让这种类型转换除了在做条件(while、for、逻辑&&等)之外不成立(不发生隐式转换)
第十五章-面向对象程序设计
继承:
class Student : public Person{
public:
string getName() const override; // override表示改写基类的虚函数
};
动态绑定又称为运行时绑定(run-time binding),如果基类希望它的某些函数被派生类覆盖,则定义成虚函数virtual,且该函数在派生类中隐式的也是虚函数。非虚函数在编译期间进行解析,虚函数在运行时进行解析(runtime binding)
[?Java没有指针和引用,是可以直接使用对象的, Java是不是在运行时通过反射查找当前对象是哪个类对象,是否有这个方法,]
基类通常应该定义一个虚析构函数,即使该函数不执行任何操作
可以认为派生类有两个部分,继承而来的部分和自定义的部分,当一个基类的指针指向派生类的时候,其实它只是指向了派生类继承而来的部分(同样的,这部分的初始化也是使用的基类的构造函数,遵循基类的接口,对基类部分使用统一的构造函数)
派生类的作用域嵌套在基类的作用域之内
用作基类的类必须已经定义而非仅仅声明
final的类不能继承:
class NoDerived final {
//NoDerived不能被继承
};
当使用基类指针时,实际上我们并不清楚该指针所绑定对象的真是类型,该对象可能是基类的对象也可能是派生类的对象
在继承中,要将变量或表达式的静态类型和该表达式表示内存中的对象的动态类型区分开
// Student 继承 Person 重写getName
string name = zhangSan.getName();
//这里zhangSan这个指针 or 引用
//无论是Student类还是Person类静态类型都是基类Person
//因为指针和引用对于派生类可以绑定到其中的基类部分上
//动态类型运行时才知道
如果表达式既不是指针也不是引用(例如仅仅是一个对象),则它的动态类型 恒等于 静态类型(因为只有指针和引用对于派生类可以绑定到其中的基类部分上)
在很多过程中其实是不存在对象之间的类型转换的,内部使用的拷贝初始化和赋值运算符的函数来操作引用
Student s;
//下面两个操作s中的非Person部分都被cut掉了
Person p(s); // 调用 Person(const Person&)拷贝构造函数,传入一个Person的引用
p = s; //调用operator=(const Person&)赋值运算符
当我们用一个派生类对象为一个基类对象初始化或者赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉(意味着不会初始化这部分,不会有隐式的类型转换)
多态:运行时绑定: 基类的指针or引用 + 虚函数 缺一不可,否则就会在编译时绑定
虚函数的默认实参是使用基类的默认实参(使用基类的构造函数进行初始化)
纯虚函数:string getName() = 0;//pure virtual
含有纯虚函数的类是抽象基类,无法创建抽象基类的对象
类一旦被重构就需要重新编译含有类对象的代码了,因为很多都是使用的静态类型
protect:可以通过派生类对象访问基类的受保护成员P543[?]
struct Pub_Derv : private Base{} //private不影响派生类的访问权限,只对派生类继承的那一部分做了访问限定
struct默认是共有继承、class默认是私有继承
名字查找与继承的顺序:确定p的静态类型 =》在静态类型的类中查找mem,如果找不到递归直到继承链的顶端=》类型检查调用是否合法=》是否虚函数决定是常规函数调用还是运行时动态绑定
名字查找优于类型检查(参数等签名),并且派生类同名函数会屏蔽基类的同名函数,所以基类同名函数就算签名一致也不会被调用
对于容器这些需要存储同样类型对象的来说,需要存放指针而非对象(类是没有转换的)
第十六章-模板与泛型编程
实例化函数模板:编译器用推断出的模板参数来实例化一个特定版本的参数,类型参数之前必须使用关键字class或者typename,使用的参数使用const的引用可以保证函数可以用于那些不能被拷贝的类型(仍可以引用)
编译器只有在实例化模板的时候才会生成代码
类模板:
template <typename T> class Blob{
public:
typedef T value_type;
//以T作为模板类型参数,用来保存Blob保存的元素类型
}
在实例化类模板的时候需要提供一个显式的模板实参,编译器使用这些模板实参来实例化出特定的类,编译器在实例化的时候对类型进行替换,例如Blog<int>,上述的模板就会被重写替换
引用折叠:
int&&可以表示成右值,或者左值引用的引用,重复的引用被"折叠"
- X& &、 X& &&和 X&& &都折叠成X&
- X&& &&折叠成 X&&
so
- 如果参数是 T&&,可以传左值,且实例化出的是一个普通的左值引用
- T&&,可以接受左值和右值
如果一个函数参数是指向模板类型参数的右值引用(T &&),它对应的实参的const属性和左值右值属性得到保持
可变参数模板使用Args表示零个或多个额外的类型函数
sizeof...(Args)//可以得出类型参数的数目
sizeof...(args)//可以得出函数参数的数目
tuple类型
tuple<int, string, int> t = {1, "hello", 2};
auto n = get<1>(t);//返回第二个成员
笔记部分
gather函数:priority_queue<int, vector<int>,greater<int>>();
头文件是<functional>
建堆的时候,默认是大根堆,第三个参数用greater<T>会变成小根堆;
排序的时候,默认是从小到大,但是第三个参数用greater<T>会变成从大到小
网友评论