C++从入门到放弃

C++学习笔记--目录

  • C++学习笔记
    • 1. C++命名空间
    • 2. 继承 与 多态
    • 3. 函数重载
    • 4. 引用
    • 5. 构造函数 与 析构函数
    • 6. 初始化列表
    • 7. explicit关键字
    • 8. static静态成员 和 友元函数与友元类、内部类
    • 9. 模板
    • 10. string
    • 11. vector
    • 12. List
    • 13. vector和list的区别及使用场景
    • 14. deque
    • 15. stack
    • 16. queue
    • 17. priority_queue
    • 18. set | map | multiset | multimap
    • 19. 右值引用
    • 20. lambda表达式

C++学习笔记

1. C++命名空间


如何定义和使用命名空间

// 命名格式:
namespace 命名空间名称 
{
	变量;
	函数;
}
// 命名空间的使用

namespace MYNS
{
	int a = 1;
	void fun()
	{
		printf("MYNS::fun()\n");
	}
}

1、加命名空间名称及作用域限定符::
int main()
{
	printf("MYNS::a = %d\n", MYNS::a);
	MYNS::fun();
	return 0;
}

2、使用using将命名空间中成员引入
using MYNS::a;
using MYNS::fun;
int main()
{
	printf("MYNS::a = %d\n", a);
	fun();
	return 0;
}

3、使用using namespace 命名空间名称引入
using namespace MYNS;
int main()
{
	printf("MYNS::a = %d\n", a);
	fun();
	return 0;
}

2. 继承 与 多态

继承的概念
继承:继承是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类的特性的基础上进行扩展,增加功能。产生新的一个类。

父类与子类的赋值
(1)在子类赋值给父类中,支持子类赋值给父类、支持子类指针赋值给父类指针、也支持子类引用赋值给父类引用;
(2)在父类赋值给子类中,不支持父类赋值给子类、不支持父类指针直接赋值给子类指针、也不支持父类引用直接赋值给子类引用。但是支持父类指针经过强转赋值给子类指针、支持父类引用经过强转赋值给子类引用。但是这种方式不安全,会产生非法访问问题

派生类的默认成员函数

  • 构造函数
    (1)如果父类中有默认构造函数,编译器会自动调用父类的构造函数;
    (2)在子类的初始化列表中,不能直接初始化父类的成员;
    (3)如果要初始化父类的成员或者父类没有默认构造,必须在初始化列表中显示调用父类的构造函数;
    (4)调用构造函数的顺序:先调用父类的构造函数,再调用子类的构造函数。
  • 拷贝构造
    (1)子类的默认拷贝构造会自动调用父类的拷贝构造;
    (2)在子类的拷贝构造函数初始化列表中,不能直接初始化父类的成员;
    (3)如果子类显示定义了拷贝构造,子类的拷贝构造会自动调用父类的默认构造;
    (4)在子类显示定义的拷贝构造中,显示调用父类的拷贝构造,此时就不会自动调用父类的默认构造函数了。
  • 赋值运算符重载函数
    (1)子类的默认赋值运算符重载函数会自动调用父类的赋值运算符重载;
    (2)在子类定义的赋值运算符重载函数中,不会自动调用父类的赋值运算符重载函数,且不能显示调用operaot=(stu),因为显示调用会发生同名隐藏,此时会无线调用子类的赋值运算符重载函数。必须加上作用域就可以显示调用Person::operator(stu)。
  • 析构函数
    (1)子类中默认的析构函数会自动调用父类的析构函数;
    (2)即使子类中的析构函数显示定义了,也会调用父类的析构函数;
    (3)子类中的析构函数和父类中的析构函数存在同名隐藏,原因是析构函数在底层的函数名都是destructor;
    (4)析构函数调用顺序,与构造顺序相反,先调用子类的析构函数,再调用父类的析构函数。

继承与友元
子类不会继承父类的友元关系

继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一 个static成员实例 ,它们都会共享这个静态成员。
  
多态的概念
不同类的对象对同一消息作出不同的响应就叫做多态。

实现多态的前提(缺一不可):
(1)多态是体现在基类和派生类中的,多态必须有继承;
(2)该函数必须是虚函数;
(3)虚函数需要被子类重写;(派生类中的虚函数要与基类中的虚函数的函数名、参数列表、返回值都要完全相同)
(4)通过父类的指针或者引用调用虚函数。

override
格式:重写的函数 override ----用在派生类中的虚函数;
作用:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

final
格式1:class 类名 final ----用在基类中;
格式2:函数名 final ----用在基类中的虚函数;
作用:1、被final修饰的类不能被继承;2、被final修饰的虚函数不能被重写。

抽象类
纯虚函数的定义:在虚函数的后面写上=0 ,则这个函数为纯虚函数。
抽象类的定义:包含纯虚函数的类叫做抽象类(也叫接口类)。
注意:抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

多态的原理

  • 虚函数表指针
    虚函数表指针指向的是一个虚表----vftable,也就是说虚函数表指针是虚表的首地址 ,而虚表中存放的是虚函数指针,虚表也就是一个指针数组;
  • 虚函数指针
    虚函数指针也就是就是虚函数的地址;
  • 虚函数
    虚函数和普通函数一样。

  派生类中也有一个虚表指针,派生类继承了基类的虚表,也就是将基类中的虚表拷贝一份,再用一个新的虚表指针指向该虚表。派生类当存在同名、返回值、参数列表都相同的虚函数时,会将虚表中的相同虚函数指针给覆盖掉。例如派生类中的fun1重写基类中的fun1,此时派生类中的虚表存放的就是派生类fun1的虚函数指针。所以,之所以可以产生多态行为,其实就是派生类中的虚函数指针覆盖了基类中的指定的虚函数指针,从而调用时就会调用派生类的虚函数了。

如何找到虚函数?
(1)从对象中获取虚表指针;
(2)通过虚表指针找到虚表;
(3)从虚表中找到虚函数的地址;
(4)执行虚函数的指令。

多态零碎知识汇总
(1)virtual关键字只在声明时加上,在类外实现时不能加
(2)static和virtual是不能同时使用的
(3)静态成员函数属于整个类,不能被重写,不能设置为虚函数,虚表指针是存在对象中的,通过类名是拿不到虚表指针的
(4)编译时的多态性可通过函数重载和模板实现;运行时的多态性可通过虚函数实现
(5)一个类的不同对象共享该类的虚表
(6)虚表是在编译期间生成的
(7)多继承的时候,就会可能有多张虚表
(8)纯虚函数不一定是空函数,只是写函数体的意义不大
(9)内联函数不能是虚函数,因为inline函数没有地址,无法把地址放到虚函数表中
(10)如果存在虚函数和虚拟继承,对象的前4个字节依然是虚表指针,紧接后面的是虚基表指针
(11)构造函数是不能是虚函数的,虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行
(12)如果是普通对象,调用普通函数和虚函数的速度是一样快的;如果是引用或者指针,由于构成多态,运行调用虚函数需要到虚函数表中去查找,则普通函数更快


3. 函数重载


函数重载的要求:
(1)函数参数的个数不同;
(2)函数参数的类型不同;
(3)函数参数的类型顺序不同;
注意:函数重载不参考函数的返回值。

那为什么C语言不支持函数重载,而C++支持函数重载呢? --通过编译时对函数名字的修饰

extern "C"作用: 在函数前加extern “C”,意思是告诉编译器, 将该函数按照C语言规则来编译。

// 在C++工程中,不想让这个函数重载,使用  extern “c”
extern "C" int funtion(int a, int b)
{
    return a + b;
}

4. 引用


引用不同类型的变量

int main()
{
	int a = 10;
	double b = 12.34;
	a = b; //创建一个临时变量tmp,将tmp的值赋给a

	// int& c = b;	 //不加const,无法通过编译
	const int& d = b;	//ok
	return 0;
}

临时变量具有常性
  涉及到不同类型赋值会创建临时变量,通过临时变量再赋值给目标值。假如在int前加上const,d不会直接引用b,而是引用了他们之间的临时变量tmp,b和d没有任何关系。而引用后的临时变量在交换完后不会被归还,而是一直存在栈中,d的地址就是临时变量的地址,d的真正引用是这个临时变量tmp。

引用的底层实现其实就是指针

引用和指针的区别(重要):
(1)引用必须在定义时初始化,指针没有要求。
(2)引用在初始化时引用一个实体后,不能更改引用实体而指针可以。
(3)没有NULL引用,但是有NULL指针
(4)有多级指针,但是没有多级引用
(5)访问实体方式不同,指针需要解引用,引用是编译器自行处理
(6)引用比指针用起来相对安全
(7)引用自加自减都是实体值的改变,而指针自加自减是地址的偏移


5. 构造函数 与 析构函数


  在C++类中,默认会有6个默认的成员函数,即使我们不写,也至少会有6个成员函数,分别是:构造函数、析构函数、拷贝构造、赋值操作符重载、取地址操作符重载、const修饰的取地址操作符重载。
  
· 构造函数
(1)一个对象只能调用一次构造函数。
(2)构造函数是对象创建的时候编译器会自动调用的默认成员函数。完成的任务是对对象的初始化,非创建对象。
(3)构造函数可以重载,分为有参构造和无参构造
(4)如果我们没有写构造函数,编译器会自动生成一个无参的构造函数
(5)无参的构造函数和全缺省的构造函数都称为默认构造函数,且他们中只能存在一个。
(6)无参构造函数只会对自定义类型做初始化,内置类型不做初始化。
(7)当成员变量有自定义类型的变量时,所有的构造函数都会自定调用自定义类型的默认构造函数,必须是默认构造函数,如果没有,编译器会报错。

· 析构函数
(1)析构函数只能有一个,如果没有显示定义,编译器会自动生成
(2)析构函数只能被调用一次,且不能显示调用,只能在对象生命周期结束时会自动调用。
(3)当类没有资源释放和清理,可以不写析构函数。
(4)编译器默认生成的析构函数并非没有作用,当成员变量存在自定义类型时,会自动调用自定义类型的析构函数。

构造顺序:全局变量->静态变量->普通变量
析构顺序:普通变量->静态变量->全局变量

· 拷贝构造

拷贝构造函数格式:类名(const 类名& 变量名)

编译器自动生成的拷贝构造是浅拷贝,浅拷贝就是 按字节拷贝,只拷贝内容不拷贝资源
  当我们一个对象是在栈上开辟了内存的对象,有指针指向该内存。当我们利用拷贝构造拷贝时,成员变量的内容是一样的,指针的指向也是一样的,同时指向一块内存空间。当一个对象生命周期结束调用析构函数,会释放该空间,另一个拷贝对象生命周期结束也会自动调用析构函数,此时同一个内存空间被释放两次,程序就会崩溃。
  
· 赋值操作符重载

// 函数名:operator+需要重载的运算符,返回值类型和参数自己决定,例如:
bool operator==(const Date& d1, const Date& d2)
{}

(1)内置类型不能使用的运算符,自定义类型也不可以重载,例如operator#;
(2)运算符中有5个自定义类型不能重载,.*, ::, sizeof, ?: ,.
(3)不能重载内置类型的运算符;
(4)重载操作符必须有一个类类型或者枚举类型的操作数;
(5)为了保证类的封装性,我们的运算符重载一般写在类里面,当成成员函数。

  赋值运算符( = )的重载是为了让自定义类型可以像内置类型一样去使用赋值运算符。是默认的成员函数,即使我们不写,编译器也会自动生成。(浅拷贝–按字节拷贝)

拷贝构造 和 赋值运算符重载的区别?
(1)拷贝构造是一个对象不存在,要实例化它,用另一个已经存在的 对象去实例化 Date d2(d1)
(2)赋值重载是两个对象都已经被初始化了,已经给存在的,是把一个对象的值拷贝到另一个对象中 d2 = d1
(3)如果类中没有资源需要释放,这两个函数都可以不用写。
  
· 取地址操作符重载、const修饰的取地址操作符重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况(如想让别人获取到指定的内容),才需要重载。

class A
{
public:
	//取地址操作符重载
	A* operator&() 
	{
		return this;
	}
	//const取地址操作符重载
	const A* operator&() const
	{
		return this;
	}
};

6. 初始化列表

(1)成员变量的初始化是在构造函数中完成的;
(2)构造函数的函数体内写的赋值操作,是普通的赋值操作。

// 格式: 以一个冒号开始,接着是一个以逗号分隔的数据成员列表,
// 每个"成员变量"后面跟一个放在括号中的初始值或表达式

class Date 
{
public: 
	Date(int year, int month, int day) 
		//初始化列表
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private: 
	int _year;
	int _month;
	int _day;
};

三种特殊的成员变量必须在初始化列表进行初始化
(1)引用类型的成员变量
  因为引用类型的变量在定义的时候必须初始化,不能进行空引用。而真正初始化的地方是在初始化列表,所以引用类型的成员变量必须在初始化列表进行初始化。
(2)const成员变量
  一个const变量也是必须在定义的时候初始化,初始化后的值不能在进行变化,所以必须在初始化列表进行初始化。
(3)没有默认构造函数的自定义类型成员
  当类的成员变量存在自定义类型成员时,会自动调用自定义类型成员的默认构造函数,如果没有默认构造函数就会编译报错,可以在初始化列表中给该类型默认值。

建议:不管是不是以上三种特殊的成员变量,只要涉及到成员变量的初始化都用初始化列表

注意:
(1)当存在自定义成员变量时,先初始化自身的初始化列表,再执行自定义成员变量的初始化列表和函数体,最后再执行自身的构造函数的函数体
(2)初始化列表中初始化顺序是按照声明的顺序进行的,并不是按照初始化列表依次从上往下初始化的


7. explicit关键字

class A
{
public:
	//构造函数
	A(int a = 0)
		:_a(a)
	{}
	//拷贝构造
	A(const A& a)
	{
		_a = a._a;
	}

private:
	int _a;
};

int main()
{
	A b(10); //构造
	A c = b; //拷贝构造
	A d = 20; //单参构造隐式类型转换
}

在main函数中可以用一个int类型的常量来实例化d对象,这是编译器先用“20”调用构造函数创建一个匿名对象,再通过拷贝构造,将匿名对象拷贝到d中。如果不想编译器进行隐式类型转换,可以在构造函数前加上 explicit 关键字 —— 禁止单参构造的隐式类型转换

class A
{
public:
	explicit A(int a)
		:_a(a)
	{}

	A(const A& a)
	{
		_a = a._a;
	}

private:
	int _a;
};

int main()
{
	A b(10); 
	A c = b;	
	A d = 20; 	// 编译报错,
}

8. static静态成员 和 友元函数与友元类、内部类

· static静态成员变量

class A
{
public:
	static int getCount()  // 静态成员函数
	{
		return _count;
	}

private:
	static int _count;	// 静态成员变量
};

//类外定义
int A::_count = 0;

static成员特性:
(1)静态成员为所有类对象所共享,不属于某个具体的实例
(2)静态成员变量只能在类外定义
(3)静态成员函数没有隐藏的this指针,不能访问任何非静态成员
(4)静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
  注意:static静态成员函数中,不能访问非静态成员(成员变量 + 成员函数)

访问static成员的方式
1、对象.static成员
2、类型::static成员

· 友元函数
  在类中声明要成为友元函数的函数

class Date 
{
public: 
	//友元函数的声明:规定此函数可以访问当前类的所有成员
	friend istream& operator>>(ostream& _cin, Date d);
	friend ostream& operator<<(ostream& _cout, Date d);
	
	Date(int year, int month, int day) 
		:_year(year)
		, _month(month)
		, _day(day)
	{}

private: 
	int _year;
	int _month;
	int _day;
};

istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year >> d._month >> d._day;
	return _cin;
}

ostream& operator<<(ostream& _cout, Date d)
{
	_cout << d._year << "-" << d._month << "-" << d._day << endl;
	return _cout;
}

// main.cpp
int main()
{
	Date d1;
	Date d2;
	cout << d1 << d2 << endl;
	cin >> d1 >> d2;
	cout << d1 << d2 << endl;
	return 0;
}

· 友元类
  友元类与友元函数相似。A是B的友元类,那么A就可以访问B的私有成员,但是B不能访问A的私有成员。友元类是单向而非双向的

· 内部类
  在一个类的内部定义一个类,则定义的类为内部类。但是内部类也是一个独立的类,外部类对内部类也没有任何优越的访问权限,更不能在类外直接创建内部类。内部类其实就是更有优势的友元类。他们的功能区别就是内部类可以直接访问外部类的静态成员。
  在计算类的大小时,内部类并不参与外部类的计算、外部类也不参与内部类的计算


9. 模板

· 函数模版

//声明模板
template 
void Swap(T& left, T& right)
{
	T tmp = left;
	left = right;
	right = tmp;
}

//使用模版
void test()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	cout << a << " " << b << endl;
	double c = 1.1;
	double d = 2.2;
	Swap(c, d);
	cout << c << " " << d << endl;
	char e = 'a';
	char f = 'b';
	Swap(e, f);
	cout << e << " " << f << endl;
}

  在编译阶段,编译器会将你传入的实参来推演出模板函数的类型,产生一个新的函数,并将T替换成相对应的类型,再调用这个新的函数。
(1)当一个非模板函数和一个同名模板函数同时存在时,会优先调用非模板函数;
(2)非模板函数和同名模板函数同时存在,编译器会根据传入的参数匹配更好的函数;
(3)如果知名需要进行实例化,则直接实例化,不管是否存在同名非模板函数。

· 类模版

//声明模板
template 
class seqList
{
public:
	seqList(int n)
		:_data(new T[n])
		, _size(0)
		, _capacity(n)
	{}
	
	//声明成员模板函数
	T seqListAt(size_t pos);
private:
	T* _data;
	size_t _size;
	size_t _capacity;
};

//类外定义成员函数
template 
T seqList::seqListAt(size_t pos)
{
	return _data[pos];
}

//使用模版
//在我们实例化对象时,因为是自定义类型,编译器无法推导出我们想要的类的类型,所以我们必须显示实例化。
//格式:类名<类型> 对象名(参数)
seqList sq1(2);
seqList sq2(4);
seqList sq3(6);

10. string

· 基础知识
string字符串是表示字符序列的类,标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
string在底层实际是:basic_string模板类的别名,typedef basic_string string;
使用string类,需要引入头文件< string >;
  
迭代器是一种设计模式,是元素访问的一种设计模式,所有容器都是需要遵循相同的设计规范。
迭代器的使用方式和指针类似:
(1)begin迭代器:指向第一个元素的位置
(2)end迭代器:指向最后一个元素的下一个位置
(3)访问数据方式:*、->
(4)迭代器的移动:++移动到下一个元素的位置;–移动到上一个元素的位置
(5)迭代器支持比较:!=、 ==
  
特殊的反向迭代器:
(1)rbegin迭代器:指向最后一个元素的位置
(2)rend迭代器:指向第一个元素的前一个位置

· string类对象的常见构造
功能:构造一个字符串对象,根据所使用的构造函数版本来初始化其值
string() 创建一个空对象
string (const string& str) 拷贝一个字符串对象
string (const char* s) 给予字符串常量来创建字符串
string (const string& str, size_t pos, size_t len = npos) 截取字符串str,从pos位置开始截取len个
string (const char* s, size_t n) 给予字符串常量并从头开始截取n个字符来创建字符串对象
string (size_t n, char c) 创建字符串,内容为n个字符c
string& operator= (const string& str) 拷贝一个字符串对象
string& operator= (const char* s) 拷贝一个字符串常量

void test()
{
	string str; // ""
	string copy(str); // ""
	string str2("White"); // "White"
	string substr(str2, 1, 2); //"hi"
	string str3("White ShirtI", 5);//white
	string str4(5, 'w'); //"wwwww"
	str3 = "White ShirtI"; //"White ShirtI"
	str4 = str3; //"White ShirtI"
}

· 常见的访问字符串操作
char& operator[] (size_t pos) 和数组一样通过下标去访问字符串内容,并且可以修改字符串内容(越界会报断言错误)
const char& operator[] (size_t pos) const 和上面接口一样,但是不能修改字符串的内容
char& at (size_t pos) 与[]一样通过索引访问字符串,且可以修改字符串内容(越界会抛异常)
const char& at (size_t pos) const 和上面接口一样,但是不能修改字符串的内容
char& front() 访问字符串第一个有效字符,可修改
const char& front() const 访问字符串第一个有效字符,不可修改
char& back() 访问字符串最后一个有效字符,可修改
const char& back() const 访问字符串最后一个有效字符,不可修改

void test()
{
	string str = "White shirtI";
	char& ref = str[7];
	ref = 'S'; //ok
	str.at(0) = 'w'; //ok

	const string str2 = "White ShirtI";
	const char& ref2 = str2[0];
	ref2 = 'w';  //error 表示必须是可修改的左值
	str2.at(0) = 'w';//error 表示必须是可修改的左值
}

· string中迭代器的使用
正向迭代器
iterator begin() 指向字符串第一个有效元素,并可以修改
const_iterator begin() const 指向字符串第一个有效元素,不能修改
iterator end() 指向字符串最后一个有效元素的下一个位置,并可以修改
const_iterator end() const 指向字符串最后一个有效元素的下一个位置,不能修改

void test()
{
	string str = "WhiteShirtI";
	string::iterator it = str.begin();
	for (; it != str.end(); ++it)
	{
		cout << *it; //WhiteShirtI
		*it = 'w';
	}

	const string str2 = "WhiteShirtI";
	string::const_iterator it2 = str.begin();
	for (; it != str.end(); ++it)
	{
		cout << *it2;
		*it2 = 'w'; // error 表达式必须为可修改的左值
	}
}

反向迭代器
reverse_iterator rbegin() 指向字符串最后一个有效字符的位置,并且能修改
const_reverse_iterator rbegin() const 指向字符串第一个有效字符的位置,不能修改
reverse_iterator rend() 指向字符串第一个有效位置的前一个位置,并且能修改
const_reverse_iterator rend() const 指向字符串第一个有效位置的前一个位置,不能修改

void test()
{
	string str = "12345";
	string::reverse_iterator rit = str.rbegin();
	while (rit != str.rend())
	{
		cout << *rit << " "; //5 4 3 2 1
		*rit = 'w';
		//反向迭代器,什么都是反的
		++rit;
	}

	const string str2 = "12345";
	string::const_reverse_iterator rit2 = str2.rbegin();
	while (rit2 != str2.rend())
	{
		cout << *rit2 << " "; //5 4 3 2 1
		*rit2 = 'w';//error 表达式必须为可修改的左值
		//反向迭代器,什么都是反的
		++rit2;
	}
}

遍历字符串可以通过数组方式通过下边遍历,也可以通过范围for来遍历,范围for底层也是通过迭代器来实现的,获取头尾元素以及迭代器的自增

void test()
{
	string str = "WhiteShirtI";
	for (const auto& ch : str)
	{
		cout << ch;
	}
}

· string有关容量的常见接口
size_t size() const 获取有效字符的个数
size_t length() const 获取有效字符的个数
void resize (size_t n) 修改有效字符的个数,如果修改后比原有的有效字符个数多,多出的部分都置为字符’\0’(’\0’并非为有效字符的结束标志,这里’\0’也是有效字符串的其中一个)
void resize (size_t n, char c) 修改有效字符的个数,如果修改后比原有的有效字符个数多,多出的部分都置为字符c

void test()
{
	string str = "White";
	cout << str.size() << endl;
	cout << str.length() << endl;
	str.resize(10);
	cout << str.size() << endl;
	str.resize(2);
	cout << str.size() << endl;

	string str2 = "Shirt";
	cout << str.size() << endl;
	str2.resize(10, 'I'); //"ShirtIIIII"
	cout << str.size() << endl;
	str2.resize(5, 'I'); //有效字符为5,后面参数不起作用
}

capacity为已经开辟的空间大小容量,当前string中最多可以存放的元素个数,增容规则:增容到原来容量的1.5倍。
size_t capacity() const 获得字符串的容量
void reserve (size_t n = 0) 修改字符串的容量(只能调大不能调小,调小相当于没修改)

void test()
{
	string str = "White";
	cout << str.capacity() << endl; //默认为15
	cout << str.size() << endl; //5
	//修改容量不会影响有效元素个数
	str.reserve(30);
	cout << str.capacity() << endl; //30以上
	cout << str.size() << endl; //5
	//修改有效元素个数会影响容量,并且 cap > size
	str.resize(60);
	cout << str.capacity() << endl; //60以上
	cout << str.size() << endl; //60
}

void clear() 清空有效字符,置size为0,但不影响容量大小
bool empty() const 判断字符串是否为空,为空返回NULL
void shrink_to_fit() 缩小容量为适合size的大小,优化。

void test()
{
	string str = "White";
	cout << str.size() << endl;
	str.clear();
	cout << str.size() << endl;

	str.reserve(1000);
	cout << str.capacity() << endl;
	str.shrink_to_fit();
	cout << str.capacity() << endl;
}

· 常见修改string字符串的接口
string& operator+= (const string& str) 在原来字符的后面添加str字符串的内容
string& operator+= (const char* s) 在原来字符的后面添加常量字符串s
string& operator+= (char c) 在原来字符的后面添加常量字符c

void test()
{
	string str;
	string str2 = "abc";
	str += str2; //"abc"
	str += "def"; //"abcdef"
	str += 'g';//"abcdefg"
}

string& append (const string& str) 追加一个字符串的内容
string& append (const string& str, size_t subpos, size_t sublen) 追加一个字符串的内容,从该字符串的第subpos位置起追加sublen个字符
string& append (const char* s) 追加一个常量字符串
string& append (const char* s, size_t n) 追加一个常量字符串的前n个
string& append (size_t n, char c) 追加n个字符c
template string& append (InputIterator first, InputIterator last) 从一个字符串的first位置追加到last位置

void test()
{
	string str;
	string str2 = "abc"; 
	str.append(str2);//"abc"
	str.append(str2, 1, 2);//"abcbc"
	str.append("abc"); //"abcbcabc"
	str.append("123456789", 6);//"abcbcabc123456"
	str.append(5, 'w'); // "abcbcabc123456wwwww"

	char arr[] = "abc";
	str.append(arr, arr + sizeof(arr) / sizeof(arr[0])); //"abcbcabc123456wwwwwabc"
	str.append(str2.begin(), str2.end()); //"abcbcabc123456wwwwwabcabc"
}

string& assign (const string& str) 将内容修改为与str内容一样
string& assign (const string& str, size_t subpos, size_t sublen) 将内容修改为str字符串从subpos位置起添加sublen个字符
string& assign (const char* s) 将内容修改为与常量字符串内容一样
string& assign (const char* s, size_t n) 将内容修改为常量字符串的前n个
string& assign (size_t n, char c) 将内容修改为n个字符c
template string& assign (InputIterator first, InputIterator last) 将字符内容设置为从first位置带last位置的内容一样

void test()
{
	string str;
	string str2 = "abc";
	str.assign(str2); //abc
	str.assign(str2, 1, 2);// bc
	str.assign("abc"); //abc
	str.assign("abcde", 4); //abcd
	str.assign(5, 'w'); //wwwww
	str.assign(++str2.begin(), str2.end()); // bc
}

string& insert (size_t pos, const string& str) 在pos位置前插入字符串str的内容
string& insert (size_t pos, const string& str, size_t subpos, size_t sublen) 在pos位置前插入字符串subpos位置起sublen个字符
string& insert (size_t pos, const char* s) 在pos位置前插入常量字符串s的内容
string& insert (size_t pos, const char* s, size_t n) 在pos位置前插入常量字符串s的n个字符
string& insert (size_t pos, size_t n, char c) 在pos位置前添加n个字符c
void insert (iterator p, size_t n, char c) 在迭代器之前插入n个字符c
template void insert (iterator p, InputIterator first, InputIterator last) 在迭代器p位置之前插入first到last迭代器的内容

void test()
{
	string str = "123";
	string str2 = "abc";
	str.insert(0, str2);//abc123
	str.insert(3, str2, 1, 1);//abcb123
	str.insert(str.size(), "www"); // abcb123www
	str.insert(7, "abc", 2);//abcb123ab3www
	str.insert(str.size(), 5, 'a');//abcb123abwwwaaaaa
	str.insert(str.begin(), 2, '1');//11abcb123abwwwaaaaa
	str.insert(str.end(), str2.begin(), str2.end());//11abcb123abwwwaaaaaabc
}

string& erase (size_t pos = 0, size_t len = npos) 从pos位置开始删除,删除npos个元素
iterator erase (iterator p) 删除迭代器所指向的位置的元素
iterator erase (iterator first, iterator last) 删除first到last之间的元素

void test()
{
	string str = "Hello WhiteShirtI";
	str.erase(1, 2);//"Hlo WhiteShirtI"
	str.erase(str.begin());//"lo WhiteShirtI"
	str.erase(++str.begin(), --str.end());//"lI"
}

string& replace (size_t pos, size_t len, const string& str) 将pos位置开始,共len个元素全部替换成str
string& replace (size_t pos, size_t len, const string& str,size_t subpos, size_t sublen) 将pos位置开始,共len个元素全部替换成str中从subpos位置开始,sublen个元素
void swap (string& str) 调用者与str交换内容
void pop_back() 删除最后一个元素

void test()
{
	string str = "abc";
	string str2 = "123";
	str.replace(0, 1, str2);//123bc
	str.replace(0, 1, str2, 0, 2);//1223bc
	str.swap(str2); //str:123  str2:1223bc
	str.pop_back();//12
}

· 常见的string查找元素接口
const char* c_str() 返回一个C语言中带有\0的字符串的首地址
size_t find (const string& str, size_t pos = 0) const 查找一个字符串,从pos位置开始,找到字符串的第一个位置
size_t find (const char* s, size_t pos = 0) 查找一个常量字符串,从pos位置开始,找到字符串的第一个位置
size_t find (const char* s, size_t pos, size_t n) 查找一个常量字符串,从pos位置开始,匹配常量字符串的前n个字符就可以,返回第一个字符的位置
size_t find (char c, size_t pos = 0) const 查找字符c,返回第一次查到的位置
rfind 与上面函数类似,也有4个接口,从后往前找

void test()
{
	char* c = new char[20];
	string str = "abc";
	strcpy(c, str.c_str()); //c:"abc\0"
	string str2 = "123abc123abc";
	int pos = str2.find(str, 1); //3
	pos = str2.find("bc", 1);//4
	pos = str2.find("bcd", 1, 3); //-1
	pos = str2.find("bcd", 1, 2); //4
	pos = str2.find('c', 0); //5

	pos = str2.rfind(str);//9
	pos = str2.rfind("bc");//10
	pos = str2.rfind("bcd", str2.size(), 3); //-1
	pos = str2.rfind('b'); // 10
}

size_t find_first_of (const char* s, size_t pos, size_t n) 从前往后找,找到s中能匹配n个字符的位置,返回第一次遇到的位置
size_t find_last_of (const char* s, size_t pos, size_t n) const 从后往前,找到s中能匹配n个字符的位置,返回第一次遇到的位置
size_t find_first_not_of (const char* s, size_t pos = 0) 从前往后找,找到的第一个不出现在s中的第一个字符
size_t find_last_not_of (const char* s, size_t pos = npos) 从后往前找,找到的第一个不出现在s中的第一个字符
string substr (size_t pos = 0, size_t len = npos) const 从pos位置开始截取len个字符串,赋予新的字符串

void test()
{
	//size_t find_first_of (const string& str, size_t pos = 0) const
	string str = "abccba123abccba";
	int pos = str.find_first_of("fbfc", 0, 2);//1
	pos = str.find_last_of("fffb"); //13
	pos = str.find_first_not_of("abc");//6
	pos = str.find_last_not_of("abc");//8

	string substr = str.substr(3, 3);//cba
	//使用find与substr获得网站的域名
	str = "https://blog.csdn.net/qq_44443986?spm=1011.2124.3001.5113";
	int pos1 = str.find("://");
	int pos2 = str.find("net");
	substr = str.substr(pos1 + 3, pos2 - pos1);//blog.csdn.net
}

· 字符串常用的输入输出流
istream& operator>> (istream& is, string& str) 与内置类型一样去输入字符串,以换行为结束
istream& getline (istream& is, string& str) 与内置类型一样去输入字符串,以换行为结束
istream& getline (istream& is, string& str, char delim) 与上面函数相似,但是可以指定输入结束标志
ostream& operator<< (ostream& os, const string& str) 像内置类型一样输出字符串的内容

void test()
{
	string str;
	cin >> str; //换行结束
	getline(cin, str); //换行结束
	//输入WhirtShirtI
	getline(cin, str, 'S'); //White
	cout << str << endl;//White
}

· 代码实现

#define _CRT_SECURE_NO_WARNINGS
#include 
#include 
using namespace std;

class String
{
public:
	typedef char* iterator;		//迭代器
	typedef const char* const_iterator;		//const迭代器

	iterator begin()			//第一个元素的位置
	{
		return _str;
	}

	iterator end()		//最后一个元素的下一个位置
	{
		return _str + _size;
	}

	const_iterator begin() const		//第一个元素的位置
	{
		return _str;
	}

	const_iterator end() const 		//最后一个元素的下一个位置
	{
		return _str + _size;
	}

	String(const char* str = "")		//默认构造函数
	{
		assert(str != nullptr);
		_size = strlen(str);
		_str = new char[_size + 1];
		_capacity = _size;
		strcpy(_str, str);
	}

	String(const String& s)		//拷贝构造函数(深拷贝)
		:_str(nullptr)
		,_size(0)
		,_capacity(0)
	{
		String tmp(s._str);
		Swap(tmp);
	}

	void Swap(String& s)		//交换函数,交换两个对象的所有内容
	{
		swap(_str, s._str);
		swap(_size, s._size);
		swap(_capacity, s._capacity);
	}

	String& operator=(String s)		//赋值运算符重载
	{
		if (this != &s)
		{
			Swap(s);
		}
		return *this;
	}

	~String()		//析构函数
	{
		if (_str != nullptr)
		{
			delete[] _str;
			_str = nullptr;
		}
	}

	const char* c_str() const
	{
		return _str;
	}

	size_t size() const
	{
		return _size;
	}

	char& operator[](size_t pos)
	{
		assert(pos < _size);
		return _str[pos];
	}

	const char& operator[](size_t pos) const 
	{
		assert(pos < _size);
		return _str[pos];
	}

	void reserve(size_t n)
	{
		if (n > _capacity)
		{			
			char* tmp = new char[n + 1];	//申请空间 
			strcpy(tmp, _str);				//拷贝原来的数据			
			delete[] _str;					//释放原有空间			
			_str = tmp;						//将拷贝的内容重新赋值给_str			
			_capacity = n;					//更新容量大小
		}
	}

	void push_back(const char& ch)
	{
		if (_size == _capacity)		//检查空间
		{
			size_t newCapcity = _capacity == 0 ? 15 : 2 * _capacity;
			reserve(newCapcity);
		}
		_str[_size++] = ch;
		_str[_size] = '\0';
	}

	void resize(size_t n, const char& ch = '\0')
	{
		//n > capacity 增容
		if (n > _capacity)
		{
			reserve(n);
		}
		//_size < n <= capacity  填充ch
		if (n > _size)
		memset(_str + _size, ch, (n - _size) * sizeof(char));

		//n < size
		_size = n;
		_str[_size] = '\0';
	}

	void append(const char* str)
	{
		//检查容量
		int len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_size + len);
		}

		//尾插
		memcpy(_str + _size, str, sizeof(char) * len);

		//更新
		_size += len;
		_str[_size] = '\0';
	}

	String& operator+=(const String& str)
	{
		append(str._str);
		return *this;
	}
	
	String& operator+=(const char* str)
	{
		append(str);
		return *this;
	}
	
	String& operator+=(const char ch)
	{
		push_back(ch);
		return *this;
	}

	void insert(size_t pos, const char& ch)
	{
		//检查位置有效性
		assert(pos <= _size);
		//检查容量
		if (_size == _capacity)
		{
			size_t  newCapacity = _capacity == 0 ? 15 : 2 * _capacity;
			reserve(newCapacity);
		}

		//移动元素,从后往前移
		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}
		//插入新元素并更新有效字符个数
		_str[pos] = ch;
		++_size;
	}

	void insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		int len = strlen(str);
		if (_size + len >= _capacity)
		{
			reserve(_size + len);
		}
		size_t end = _size + len;
		while (end > pos + len - 1)
		{
			_str[end] = _str[end - len];
			--end;
		}
		memcpy(_str + pos, str, sizeof(char) * len);
		_size += len;
	}

	void erase(size_t pos, size_t len)
	{
		//检查位置有效性
		assert(pos < _size);
		//要删除的个数大于剩下的字符个数
		if (pos + len >= _size)
		{
			_size = pos;
			_str[_size] = '\0';
		}
		else
		{
			//移动元素,从前向后移动
			size_t start = pos + len;
			while (start <= _size)
			{
				_str[pos++] = _str[start];
				++start;
			}
			_size -= len;
		}
	}

	size_t find(const char& ch, size_t pos = 0)
	{
		//检查位置的有效性
		assert(pos < _size);
		
		for (size_t i = pos; i < _size; ++i)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}
		return npos;
	}

	size_t find(const char* str, size_t pos = 0)
	{
		//返回值:
		//NULL:未找到
		//有效地址:子串出现的首地址
		char* ptr = strstr(_str, str);
		if (ptr != NULL)
		{
			return ptr - _str;
		}
		return npos;
	}

private:
	char* _str;
	size_t _size;
	size_t _capacity;
	static const size_t npos;
};

const size_t String::npos = -1;

String operator+(const String& str1, const String& str2)
{
	String tmp = str1;
	tmp += str2;
	return tmp;
}

String operator+(const String& str1, const char* str2)
{
	String tmp = str1;
	tmp += str2;
	return tmp;
}

String operator+(const char& ch, const String& str1)
{
	String tmp = str1;
	tmp += ch;
	return tmp;
}

ostream& operator<<(ostream& out, const String& str)
{
	for (const auto& ch : str)
	{
		cout << ch;
	}
	return out;
}

istream& operator>>(istream& in, String& str)
{
	char ch;
	while (ch = getchar())
	{
		if (ch == ' ' || ch == '\n' || ch == '\t')
		{
			break;
		}
		str += ch;
	}

	return in;
}

bool operator==(const String& str1, const String& str2)
{
	int res =  strcmp(str1.c_str(), str2.c_str());
	if (res == 0)
		return true;
	else
		return false;
}

bool operator==(const String& str1, const char*& str)
{
	int res = strcmp(str1.c_str(),str);
	if (res == 0)
		return true;
	else
		return false;
}

bool operator!=(const String& str1, const String& str2)
{
	return !(str1 == str2);
}

bool operator!=(const String& str1, const char*& str)
{
	return !(str1 == str);
}

· 迭代器失效问题
(1)操作已释放的空间造成迭代器失效

#include 
#include 
using namespace std;

void test()
{
	vector v(5, 1);
	vector::iterator it = v.begin();
	cout << *it << endl;
	v.resize(10);
	it = v.begin();	//如果不重新对迭代器进行赋值,会导致程序的崩溃和不安全
	cout << *it << endl;
}

int main()
{
	test();
	return 0;
}


  当容量发生变化时会重新开辟一个更大的空间,然后将原来空间的数据拷贝到新的空间中,再释放原有空间。但是此时的迭代器还指向着原有空间,已经是野指针了,对野指针进行解引用当然是不可取的,会导致程序的崩溃和不安全。
  所以说只要涉及到扩容,就会开辟新的空间,此时迭代器就会失效,下面这些函数都是会有可能涉及到扩容的:resize、reserve、insert、assign、 push_back
  如果存在迭代器的情况下,进行了以上函数的操作,我们只要对迭代器重新赋值就可以解决迭代器失效问题。
  
(2)迭代器的指向发生了错位

#include 
#include 
using namespace std;

void test()
{
	vector v(5, 1);
	vector::iterator it = v.begin();
	cout << *it << endl;
	//头删
	v.erase(it);
	cout << *it << endl;
}

int main()
{
	test();
	return 0;
}


  假如我们删除了最后一个元素,此时的迭代器就指向了end这个位置,但是end这个位置是没有元素的,所以再对迭代器进行操作那么就会导致程序的崩溃,而在vs中的编译器就简单粗暴,无论是在哪里删除元素,迭代器就都会失效,但是在g++编译器中,并没有这么简单粗暴,只有在删除最后元素才会导致程序崩溃
  如果存在迭代器的情况下,进行了以上述的操作,我们只要对迭代器重新赋值就可以解决迭代器失效问题


11. vector

template
class Vector
{
public:
	typedef T* iterator;
	typedef const T* const_iterator;
	
	iterator begin()
	{
		return _start;
	}

	iterator end()
	{
		return _finish;
	}

	const_iterator begin() const
	{
		return _start;
	}

	const_iterator end() const
	{
		return _finish;
	}

	//无参默认构造
	Vector()
		:_start(nullptr)
		,_finish(nullptr)
		,_endOfStorage(nullptr)
	{}

	//n个val的构造函数
	Vector(int n, const T& val = T())
		:_start(new T[n])
		,_finish(_start +n)
		,_endOfStorage(_finish)
	{
		for (int i = 0; i < n; ++i)
		{
			_start[i] = val;
		}
	}

	//通过迭代器产生的构造函数
	template
	Vector(InputIterator first, InputIterator last)
		:_start(nullptr)
		, _finish(nullptr)
		, _endOfStorage(nullptr)
	{
		while (first != last)
		{
			pushBack(*first);
			++first;
		}
	}
	
	~Vector()
	{
		if (_start)
		{
			delete[] _start;
			_start = _finish = _endOfStorage = nullptr;
		}
	}

	size_t size() const
	{
		return _finish - _start;
	}

	size_t capacity() const
	{
		return _endOfStorage - _start;
	}

	T& operator[](size_t pos)
	{
		assert(pos < size());
		return _start[pos];
	}

	const T& operator[](size_t pos) const
	{
		assert(pos < size());
		return _start[pos];
	}

	void reserve(size_t n)
	{
		//reserve只能扩大空间不能缩小空间
		if (n > capacity())
		{
			//保存有效元素
			size_t sz = size();
			//申请空间
			T* tmp = new T[n];
			//将数据拷贝到新的空间
			if (_start != nullptr)
			{
				//拷贝有效元素
				memcpy(tmp, _start, sizeof(T) * size());
				delete[] _start;
			}
			//更新
			_start = tmp;
			_finish = _start + sz;
			_endOfStorage = _start + n;
		}
	}

	void pushBack(const T& val)
	{
		//检查容量
		if (_finish == _endOfStorage)
		{
			size_t newC = _endOfStorage == nullptr ? 1 : 2 * capacity();
			reserve(newC);
		}

		//插入数据
		*_finish = val;
		//更新finish
		++_finish
	}

	void resize(size_t n, const T& val = T())
	{
		//3.n >= capacity
		if (n > capacity())
		{
			reserve(n);
		}
		//2.size < n <= capacity
		if (n > size())
		{
			while (_finish != _start + n)
			{
				*_finish = val;
				++_finish;
			}
		}
		//1.n<=size
		_finish = _start + n;
	}

void insert(iterator pos, const T& val)
	{
		//检查位置有效性
		assert(pos >= _start || pos < _finish);
		//检查容量
		if (_finish == _endOfStorage)
		{
			//增容会导致迭代器失效
			//保存pos和_start的偏移量
			size_t offset = pos - _start;
			size_t newC = _endOfStorage == nullptr ? 1 : 2 * capacity();
			reserve(newC);
			//更新pos
			pos = _start + offset;
		}
		//移动元素
		iterator end = _finish;
		while (end != pos)
		{
			*end = *(end - 1);
			--end;
		}
		//插入
		*pos = val;
		//更新
		++_finish;
	}

	iterator erase(iterator pos)
	{
		//检查位置有效性
		assert(pos >= _start || pos < _finish);
		//移动元素,从前往后
		iterator start = pos + 1;

		while (start != _finish)
		{
			*(start - 1) = *start;
			++start;
		}
		//更新
		--_finish;
	}

	void popBack()
	{
		if (size() > 0)
			erase(end() - 1);
	}
	

private:
	iterator _start;
	iterator _finish;
	iterator _endOfStorage;
};


12. List

template 
struct ListNode
{
	T _data;
	ListNode* _next;
	ListNode* _prev;

	ListNode(const T& val = T())
		:_data(val)
		, _next(nullptr)
		, _prev(nullptr)
	{}
};

template
struct ListIterator
{
	typedef ListNode Node;
	typedef ListIterator Self;
	Node* _node;

	ListIterator(Node* node = nullptr)
		: _node(node)
	{}

	ListIterator(const Self& s)
		: _node(s._node)
	{}

	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	Self operator++(int)
	{
		Self tmp(_node);
		_node = _node->_next;
		return tmp;
	}

	Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	Self operator--(int)
	{
		Self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &(_node->_data);
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

	bool operator==(const Self& s)
	{
		return _node == s._node;
	}
};


template 
class List
{
public:
	typedef ListNode Node;
	typedef ListIterator iterator;
	typedef ListIterator const_iterator;

	iterator begin()
	{
		return iterator(_header->_next);
	}

	iterator end()
	{
		return iterator(_header);
	}

	const_iterator cbegin() const
	{
		return const_iterator(_header->_next);
	}

	const_iterator cend() const
	{
		return const_iterator(_header);
	}

	List()
		:_header(new Node())
	{
		//循环结构
		_header->_next = _header->_prev = _header;
	}

	List(int n, const T& val = T())
		:_header(new Node())
	{
		_header->_next = _header->_prev = _header;
		for (size_t i = 0; i < n; ++i)
		{
			pushBack(val);
		}
	}

	template
	List(inputIterator first, inputIterator last)
		:_header(new Node())
	{
		_header->_next = _header->_prev = _header;
		while (first != last)
		{
			pushBack(*first);
			++first;
		}
	}

	List(List& l)
		:_header(new Node())
	{
		_header->_next = _header->_prev = _header;
		List tmp(l.begin(), l.end());
		swap(tmp);
	}

	List& operator=(List tmp)
	{
		swap(tmp);
		return *this;
	}

	void swap(List& l)
	{
		Node* tmp = l._header;
		l._header = this->_header;
		this->_header = tmp;
	}

	void pushBack(const T& val)
	{
		/*Node* tail = _header->_prev;
		Node* newNode = new Node(val);

		tail->_next = newNode;
		newNode->_prev = tail;

		newNode->_next = _header;
		_header->_prev = newNode;*/
		insert(end(), val);
	}

	void pushFront(const T& val)
	{
		insert(begin(), val);
	}

	void popBack()
	{
		erase(--end());
	}

	void popFront()
	{
		erase(begin());
	}

	T& font()
	{
		return _header->_next->_data;
	}

	const T& font()const 
	{
		return _header->_next->_data;
	}

	T& back()
	{
		return _header->_prev->_data;
	}

	const T& back()const
	{
		return _header->_prev->_data;
	}

	bool empty()const 
	{
		return _header->_next == _header;
	}

	size_t size()const
	{
		int sz = 0;
		Node* cur = _header->_next;
		while (cur != _header)
		{
			++sz;
			cur = cur->_next;
		}
		return sz;
	}

	iterator erase(iterator pos)
	{
		if (pos != end())
		{
			Node* next = pos._node->_next;
			Node* prev = pos._node->_prev;

			prev->_next = next;
			next->_prev = prev;
			delete[] pos._node;
			return iterator(next);
		}
		return pos;
	}

	void insert(iterator pos, const T& val)
	{
		Node* newNode = new Node(val);
		Node* cur = pos._node;
		Node* prev = cur->_prev;

		prev->_next = newNode;
		newNode->_prev = prev;
		newNode->_next = cur;
		cur->_prev = newNode;
	}

	void clear()
	{
		if (_header != nullptr)
		{
			Node* cur = _header->_next;
			while (cur != _header)
			{
				Node* next = cur->_next;
				delete[] cur;
				cur = nullptr;
				cur = next;
			}
		}
	}

	~List()
	{
		clear();
		delete _header;
		_header = nullptr;
	}

private :
	Node* _header;
};

13. vector和list的区别及使用场景

区别:
(1)vector底层是连续结构;list底层是非连续结构;
(2)vector支持随机访问;list不支持随机访问;
(3)vector迭代器是原生指针;list迭代器是封装结点的一个类;
(4)vector在插入和删除时可能会导致迭代器失效;list在删除的时候会导致当前迭代器指向的结点失效;
(5)vector不容易造成内存碎片,空间利用率高;list容易造成内存碎片,空间利用率低;
(6)vector在非尾插,删除的时间复杂度为O(n),list在任何地方插入和删除的时间复杂度都为O(1)。

使用场景:
vecotr需要高效存储,支持随机访问,不关心插入删除效率;list大量插入和删除操作,不关心随机访问。


14. deque

双端队列是动态大小的序列式容器,其可以像两端进行伸缩。
双端队列中实际上是一个vector,这个vector中的元素都是指针,而指针指向的地方,才是真正存放数据的地方。

deque的迭代器
cur:指向当前访问的元素
first:指向当前数据存在的缓冲区的首地址
last:指向当前数据存在的缓冲区的末尾
node:指向容器中的某一节点,表示该迭代器属于某一指针的

deque与其他容器比较的优缺点
与vector比较
deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,其效率比vector高。
与list比较
deque的优势是:底层是连续空间,空间利用率比较高,不需要存储额外字段。
deque缺陷
不适合遍历,因为在遍历时,deque的迭代器要频繁地检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历。因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。


15. stack

stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其只能从容器的一端进行元素的插入与提取操作。stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:empty:判空操作; back:获取尾部元素操作; push_back:尾部插入元素操作; pop_back:尾部删除元素操作。

stack的实现
stack可以通过 vector< T >list< T >deque< T >
默认使用deque< T >
原因:
(1)与vector相比:stack不需要随机访问,也没有迭代器,deque的增容代价小;
(2)与list相比:stack不需要在任意位置的插入和删除,deque不容易造成内存碎片。

template>
class Stack
{
public:
	Stack()
	{}
	void push(const T& x)
	{
		con.push_back(x);
	}
	void pop()
	{
		con.pop_back();
	}
	T& top() 
	{
		return con.back();
	} 
	const T& top()const
	{
		return con.back();
	}
	size_t size()const
	{
		return con.size();
	}
	bool empty()const
	{
		return con.empty();
	}
private:
	Container con;
};

16. queue

queue是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。queue的底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类,该底层容器应至少支持以下操作:empty:检测队列是否为空 ;size:返回队列中有效元素的个数; front:返回队头元素的引用 ;back:返回队尾元素的引用;push_back:在队列尾部入队列;pop_front:在队列头部出队列。

queue的实现
queue可以通过 list< T >deque< T >
默认使用deque< T >
原因:
(1)vector没有pop_front()接口;
(2)与list相比:queue不需要在任意位置的插入和删除,deque不容易造成内存碎片。

template>
class Queue
{
public:
	Queue() 
	{} 
	void push(const T& x) 
	{ 
		con.push_back(x);
	} 
	void pop() 
	{ 
		con.pop_front();
	} 
	T& back() 
	{ 
		return con.back();
	}  
	const T& back()const
	{ 
		return con.back();
	} 
	T& front() 
	{ 
		return con.front();
	} 
	const T& front()const 
	{ 
		return con.front();
	}
	size_t size()const 
	{ 
		return con.size();
	} 
	bool empty()const 
	{ 
		return con.empty();
	}
private: 
	Container con;
};

17. priority_queue

优先级队列是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。

优先级队列的特点:
(1)优先级队列是0个或多个元素的集合,每个元素都有一个优先权或值;
(2)当给每个元素分配一个数字来标记其优先级时,可设较小的数字具有较高的优先级,这样更方便地在一个集合中访问优先级最高的元素,并对其进行查找和删除操作;
(3)对优先级队列,执行的操作主要有:查找,插入,删除;
(4)在最小优先级队列(min Priority Queue)中,查找操作用来搜索优先权最小的元素,删除操作用来删除该元素;
(5)在最大优先级队列(max Priority Queue)中,查找操作用来搜索优先权最大的元素,删除操作用来删除该元素;
(6)插入操作均只是简单地把一个新的元素加入到队列中;
(7)注:每个元素的优先级根据问题的要求而定。当从优先级队列中删除一个元素时,可能出现多个元素具有相同的优先权。在这种情况下,把这些具有相同优先权的元素视为一个先来先服务的队列,按他们的入队顺序进行先后处理。

优先级队列的使用
头文件:#include < queue >
优先级队列默认是最大优先级队列

接口介绍
priority_queue() / priority_queue(first, last) 构造一个空的优先级队列
empty( ) 判断优先级队列是否为空,为空返回true
top( ) 获取优先级队列中最大或者最小的元素,即堆顶元素
push(x) 将x元素插入到优先级队列中
pop() 删除优先级队列中最大或者最小的元素, 即删除堆顶元素

#include 

using std::swap;

//用大最小优先级队列
template
struct Less
{
	bool operator()(const T& a, const T& b)
	{
		return a < b;
	}
};

//用于最小优先级队列
template
struct Greater
{
	bool operator()(const T& a, const T& b)
	{
		return a > b;
	}
};

template, class Compare = Less>
class PriorityQueue
{
public:
	//向下调整
	void shiftDown()
	{
		int parent = 0;
		int child = 2 * parent + 1;
		while (child < con.size())
		{
			if (child + 1 < con.size() && cmp(con[child], con[child + 1]))
			{
				++child;
			}
			if (cmp(con[parent], con[child]))
			{
				swap(con[parent], con[child]);
				parent = child;
				child = 2 * parent + 1;
			}
			else
			{
				break;
			}
		}
	}

	//向上调整
	void shiftUp(int child)
	{
		int parent = (child - 1) / 2;
		while (child > 0)
		{
			if (cmp(con[parent], con[child]))
			{
				swap(con[parent], con[child]);
				child = parent;
				parent = (child - 1) / 2;
			}
			else
			{
				break;
			}
		}
	}

	void push(const T& val)
	{
		//尾插
		con.push_back(val);
		shiftUp(con.size() - 1);
	}

	void pop()
	{
		//交换
		swap(con[0], con[con.size() - 1]);
		con.pop_back();
		shiftDown();
	}

	T& top()
	{
		return con.front();
	}

	size_t size() const
	{
		return con.size();
	}

	bool empty() const
	{
		return con.empty();
	}
private:
	Container con;
	Compare cmp;
};

// main.cpp
#include 
#include 
#include "myQueue.h"

using namespace std;

int main()
{
    PriorityQueue> pq;
    pq.push(123);
    pq.push(14);
    pq.push(13);
    pq.push(23);
    pq.push(88);

    while (!pq.empty())
    {
        cout << pq.top() << " ";
        pq.pop();
    }
    return 0;
}

18. set | map | multiset | multimap

set
set中的key就是value,就是 key=value 的一种关联容器。
(1)set容器中不能存在重复元素,底层二叉树不允许存在重复元素;
(2)使用set的迭代器遍历set中的元素,可以得到有序序列,底层通过中序遍历得到;
(3)set中插入元素时,只需要插入value即可,不需要构造键值对;
(4)set中的元素不允许修改,二叉搜索树不允许修改。

  • 构造函数
    explicit set (const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type()); // 空构造
    template < class InputIterator >
    set (InputIterator first, InputIterator last, const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type()); // 通过迭代器创建set容器
  • 大小容量
    size() // 求set元素个数
    empty() // 判断set是否为空
  • 插入
    pair insert (const value_type& val); // 返回值pair插入数据的位置以及插入是否成功的数据
    terator insert (iterator position, const value_type& val);
  • 删除
    size_type erase (const value_type& val); // 返回值是删除的个数,存在则删除并返回1,不存在返回0
    void erase (iterator position);
    void erase (iterator first, iterator last);
  • 查找
    iterator find (const value_type& val) const; // 找到返回数据的迭代器,找不到返回set容器的end()迭代器
    size_type count (const value_type& val) const; // 查看是否存在某个数据

map
map提供一对一的hash,自动建立 key-value 的对应。key和value可以是任意类型,包括自定义类型。
(1)并且在map的内部,key与value通过成员类型value_type绑定在一起, 为其取别名称为pair;
(2)map中的元素总是按照键值key进行比较排序的,默认为递增排序;
(3)map支持下标访问符,即在[]中放入key,就可以找到与key对应的value;
(4)map中的key不可以重复,但是value可以重复。不能修改key,但是可以修改value。

  • 构造函数
    explicit map (const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type()); // 空构造
    template < class InputIterator>
    map (InputIterator first, InputIterator last, const key_compare& comp = key_compare()); // 通过迭代器创建map容器
  • [] 方括号
    mapped_type& operator[] (const key_type& k);
    和我们平时使用数组时使用方法类似,key相当于索引,value就是索引对应的值 map[key] == value。但是map中的[]具有新的功能,即使key不存在,也可以通过[]来插入新的一个键值对并输出对应的value值。
  • at接口
    mapped_type& at (const key_type& k);
  • insert
    pair insert (const value_type& val);
  • erase
    size_type erase (const key_type& k);

multiset
与set类似,但是存的值可以重复。
(1)multiset是按照特定顺序存储元素的容器,其中元素是可以重复的;
(2)multiset元素的值不能在容器中进行修改(因为元素 总是const的),但可以从容器中插入或删除;
(3)默认排序依然是递增。

multimap
与map类似,但是存的key值可以重复。
(1)Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对,其中 多个键值对之间的key是可以重复的;
(2)默认排序依然是递增。


19. 右值引用

C++引入右值的原因
(1)实现移动语义(移动构造和移动赋值)
(2)给中间临时变量取名字
(3)实现完美转发

左值引用和右值引用
左值引用就是我们平时用的引用,左值引用就是在类型后面加&,则定义该变量为引用类型。左值引用及可以引用左值,也可以引用右值。

void test()
{
	int a = 10;
	int& ra = a;//引用左值
	const int& ri = 10;//引用右值
}

右值引用则需要在变量类型加上&&,则为右值引用。右值引用不能引用左值,只能引用右值。

void test()
{
	int a = 10;
	int&& rri = 10;//ok
	int&& rra = a;//error;
}

右值引用的作用
引入右值引用,目的就是为了在某些特定场景下(创建拷贝以及释放空间的开销)提高代码运行的效率------通过不进行深拷贝来提高代码拷贝效率


20. lambda表达式

lambda表达式是一个匿名的函数

完整表达式:[capture-list](parameters)mutable->return-type{statement}

  • []中的变量就是可以将上下文中的变量引入到匿名函数中去使用,默认属性为const
  • ()中的变量为函数的参数列表,定义方式和普通函数一样,可省略,但是如果添加mutable属性后即使()为空也需要显示写出;
  • mutable的作用是将[]的变量属性修改为非const,可省略;
  • ->return-type为函数的返回值,但是编译器可以自动推导,可省略;
  • {}为函数体,可以使用[]()内的变量。
void test()
{
	int a = 10;
	int b = 20;
	[a, b] {return a + b; };
	[a, b](int c) {return a + b + c; };
	[a, b](int c) ->int{return a + b + c; };
	[a, b]()mutable{a = 100; b = 200; };
	[] {};
}

如果要使用lambda表达式,可以定义一个auto变量来接收这个匿名函数

void test()
{
	int a = 10;
	int b = 20;
	auto fun = [a, b](int c) ->int{return a + b + c; };
	int ret = fun(10);
	cout << ret << endl;
}

[]如果要捕捉多个变量,如果一个一个写会非常麻烦,此时C++也引入了需要的语法来便捷操作

  • [=]:以值的形式捕捉上下文的所有变量(包括this指针);
  • [&]:以引用的形式捕捉上下文的所有变量(包括this指针);
  • [&x]:以引用博主的方式捕捉x;
  • [=, &x]:x变量以引用方式来捕捉,其他变量都以值形式捕捉;
  • [&, x]:x变量以值方式来捕捉,其他变量都以引用形式捕捉;
  • [this]:以值形式来捕捉当前的this指针。

错误捕捉方式:[=, x]、[&, &x]重复捕捉

class A
{
public:
	void fun(int a)
	{
		[this]() {return this->_a; };
	}
private:
	int _a;
};

void test()
{
	int a = 10;
	int b = 20;
	auto fun = [=](int c) ->int{return a + b + c; };
	auto fun1 = [&](int c) ->int {return a + b + c; };
	auto fun2 = [&a](int c) ->int {return a + c; };
	auto fun3 = [=, &a](int c) ->int {return a + b + c;};
	auto fun4 = [&, a](int c) ->int {return a + b + c; };
}

不能捕捉不在同一作用域中的变量或者是非局部变量的变量

int g = 10;

void test()
{
	int t = 20;
	auto f = [g] {};//error
}

int main()
{
	auto f1 = [t] {};//error
	test();
	return 0;
}

不能进行赋值,但是可以进行拷贝

void test()
{
	int t = 20;
	auto f = [t](int a) {return a + t; };
	auto f1 = [t](int a) {return a + t; };
	f = f1;//error
	auto f2(f);//ok
}

你可能感兴趣的:(C++,c++)