C++基础知识
c和c++的区别
C面向过程,C++面向对象
- 面向过程语言:性能比面向对象高,因为类调用时需要实例化,比较消耗资源,但是没有面向对象易维护,易复用,易扩展。
- 面向对象语言:易维护,易复用,易扩展,由于具有面向对象的特性,可以设计出低耦合的系统使得系统更加灵活。但是性能低。
- c++仍然是以c位基础:区块、语句、预处理器、内置数据类型、数组、指针等都来自c。但是c++有模板、异常、重载、stl库、分装继承多态等部分。
后缀名不同
一些关键字有区别:
- C中的struct中不能有函数,但是C++中可以有
- malloc:返回值为void*,C中可以直接赋值给任何类型指针,但是C++中必须强制类型转换。
返回值
- C中如果一个函数没有返回值默认返回int,C++中没有返回值需要指定void
缺省参数
- C不支持缺省参数
函数重载
- C不支持函数重载,C++支持函数重载,常用于处理实现功能类似但是类型不同的问题。
C++和java联系和区别
- 都是面向对象的语言,都支持封装,继承和多态
- java不提供指针直接访问内存,程序更加安全
- java的类是单继承的,C++支持多重继承;但是java的接口可以多继承
- java有自动内存管理机制,不需要程序员手动释放无用内存。
指针引用
为什么使用指针
- 能够动态分配内存,实现内存自由管理
- 方便使用数组和字符串
- 值传递不如指针传递高效
指针和引用的区别
- 指针是地址引用是别名
- 指针本身也是一个对象占有一块内存;引用不占有内存,指向的对象可以更改,引用初始化时必须指向一个已经存在的对象
const&static
const和define区别
- 编译器处理不同:
- 宏定义在预处理阶段展开,不能对宏定义进行调试,生命周期结束于编译时期
- const是一个“编译运行时”概念,在程序运行中使用起作用
- 起作用的方式不同:
- 宏定义只是简单的字符替换,没有类型检查,存在边界的错误;const对应数据类型,进行类型检查
- 存储方式:
- 宏定义进行展开,它定义的宏常量在内存中有若干个备份,占用代码段空间;const定义的只读变量在程序运行过程中只有一份备份,占用数据段空间。
- 宏定义不能调试,因为在预编译阶段会被替换;const可以进行调试
- 宏定义可以使用#undef取消符号定义;const不能重定义
- define可以用来防止头文件重复引用;const不能
- define不可以用于类成员变量的定义,但是可以用于全局变量;const可以用于类成员变量的定义,只要一定义,不可修改
- define可以用表达式作为名称;const采用一个普通的常量名称。
const 作用
- 修饰变量,说明该变量不可以被改变;
- 修饰指针,分为指向常量的指针(pointer to const)和自身是常量的指针(常量指针,const pointer);
- 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改;
- 修饰成员函数,说明该成员函数内不能修改成员变量。
const成员函数和static成员函数的区别
- const成员函数:由于传入函数内部的this指针是一个const,所以在函数内部不能通过this指针访问类的非静态成员变量,当然如果硬要访问也可以,在类成员变量前加mutable修饰就行了。
- static成员函数:对于这个类和他的所有对象来说只有一个static成员函数实体。由于static成员函数不在内部传入this指针,所以它也无法访问类的非静态成员变量。另外,static成员函数可以直接由类名或者对象名调用。
const与指针
- 常量指针:const修饰的是指针,指针本身是一个常量地址不可以被改变,但是指针所指向的对象是可以改变的。
- 指向常量的指针:const修饰的是指针指向的对象,对象不可以被改变,但是指针可以改变指向的对象。
- 指向常量的常量指针:不论是指针本身还是指向的对象都不可以被改变。
static的作用
- 对于全局变量:改变全局变量的作用域,使其他的编译单元不可见(data全局初始化区)
- 对于局部变量:改变他的存储位置(由栈到静态存储区)和生命周期(函数结束到程序结束)
- 成员变量:成为类的全局变量,需要在类外初始化,只有唯一的实例,被所有的类对象共享。
- 成员函数:成为类的静态成员函数被所有类对象共享,可以直接用类名调用,没有隐含this指针
static const int a
·static const int a可以在类内进行初始化,而static int a不可以
this指针
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。- 当对一个对象调用成员函数时,编译程序先将对象的地址赋给
this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用this
指针。 - 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
this
指针被隐含地声明为:ClassName *const this
,这意味着不能给this
指针赋值;在ClassName
类的const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能对this
指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);this
并不是一个常规变量,而是个右值,所以不能取得this
的地址(不能&this
)。- 在以下场景中,经常需要显式引用指针:
- 为实现对象的链式引用;
- 为避免对同一对象进行赋值操作;
- 在实现一些数据结构时,如
list
。
inline 内联函数
特征
- 相当于把内联函数里面的内容写在调用内联函数处;
- 相当于不用执行进入函数的步骤,直接执行函数体;
- 相当于宏,却比宏多了类型检查,真正具有函数特性;
- 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
使用
inline 使用
// 声明1(加 inline,建议使用)
inline int functionName(int first, int second,...);
// 声明2(不加 inline)
int functionName(int first, int second,...);
// 定义
inline int functionName(int first, int second,...) {
/****/};
// 类内定义,隐式内联
class A {
int doA() {
return 0; } // 隐式内联
}
// 类外定义,需要显式内联
class A {
int doA();
}
inline int A::doA() {
return 0; } // 需要显式内联
编译器对 inline 函数的处理步骤
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
inline优缺点
优点
- 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
- 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
- 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
- 内联函数在运行时可调试,而宏定义不可以。
缺点
- 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
- inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
- 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
C++ 中 struct 和 class
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
区别
- 最本质的一个区别就是默认的访问控制
- 默认的继承访问权限。struct 是 public 的,class 是 private 的。
- struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。
union联合体类型
- 联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
- 默认访问控制符为 public
- 可以含有构造函数、析构函数
- 不能含有引用类型的成员
- 不能继承自其他类,不能作为基类
- 不能含有虚函数
- 匿名 union 在定义所在作用域可直接访问 union 成员
- 匿名 union 不能包含 protected 成员或 private 成员
- 全局匿名联合必须是静态(static)的
enum枚举类型
- 限定作用域的枚举类型
enum class open_modes{input, output, append};
- 不限定作用域的枚举类型
enum color {red, yellow, green};
enum {floatPrec=6, doublePrec=10;
extern关键字
- extern c :告诉编译器在编译的时候按着c的规则去编译
- extern :所声明的函数和变量可以在本模块和其他模块一起使用
volatile关键字
volatile int i = 10;
volatile
关键字是一种类型修饰符,用它声明的类型变量可以被某些编译器未知的因素改变(操作系统,硬件等)。所以使用该关键字告诉编译器不应对这样的对象进行优化volatile
关键字声明的变量,每次访问都必须从内存中取值(没有被volatile
修饰的变量,可能由于被编译器优化,从CPU寄存器中取值)const
(编译期保证代码中没有被修改的地方,运行的时候不受限制)可以是volatile
(编译期告诉编译器不优化该变量,在运行期每次都从内存中取值):表示一个变量在程序编译期不能被修改并且不能被优化,在程序运行期变量值可能会被修改,每次使用到该变量值都需要从内存中读取,防止意外错误- 指针可以是
volatile
的
explicit关键字
explicit
修饰构造函数时,可以防止隐式转换或者复制初始化explicit
修饰转换函数时,可以防止隐式转换,但按语境转换除外
使用
struct A{
A(int){
}
operator bool() const {
return true;}
};
struct B{
explicit B(int){
}
explicit operator bool() const {
return true;}
};
int main{
B b1(1); //OK,直接初始化
B b2 = 1; //Error,被explict修饰的构造函数不能复制初始化
}
using
- 一条
using
声明语句一次只引入命名空间的一个成员,这使得我们可以清楚知道程序所引用的到底是哪个命名空间里的函数,比如
using namespace_name::name;
- 构造函数的
using
声明:在C++11中,可以直接重用其直接基类定义的构造函数,比如
class Derived:Base{
public:
using Base:Base;
};
- 如上,对于基类的每个构造函数,编译器都生成一个与之对应(形参列表完全相同)的派生类构造函数,生成如下类型构造函数:
Derived(params):Base(args){}
using
指示使得某个特定命名空间中所有名字都可见,这样就无需再为他们都加上任何前缀了,比如
using namespace std;
应当尽量少使用
using
指示污染命名空间,尽量多使用using
声明
::范围解析运算符
- 分类
- 全局作用域符(
::name
):用于类型名称(类,类成员,成员函数,变量等)前,表示作用域为全局命名空间 - 类作用域符(
class::name
):用于表示指定类型的作用域范围是具体某个类的 - 命名空间作用域符(
namespace::name
):用于表示指定类型的作用域范围是具体某个命名空间的
- 全局作用域符(
右值引用
-
左值和右值的概念
- 左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
- 右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。
- C++11:右值是由两个概念构成,将亡值和纯右值。纯右值是用于识别临时变量和一些不跟对象关联的值,比如1+3产生的临时变量值,2、true等,而将亡值通常是指具有转移语义的对象,比如返回右值引用T&&的函数返回值等
-
它实现了转移语义和精确传递。它的主要目的有两个方面:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
- 能够更简洁明确地定义泛型函数
-
总结一个类型T
- 左值引用, 使用
T&
, 只能绑定左值 - 右值引用, 使用
T&&
, 只能绑定右值 - 常量左值, 使用
const T&
, 既可以绑定左值又可以绑定右值 - 已命名的右值引用,编译器会认为是个左值
编译器有返回值优化
,但不要过于依赖
- 左值引用, 使用
野指针
-
访问一个已销毁或者访问受限的内存区域的指针
-
产生的原因
- 指针定义时未被初始化
- 指针被释放时没有置空
- 指针操作超越变量作用域:不要返回指向栈内存的指针或者引用,因为栈内存在函数结束的时候会被释放
函数指针
- 定义:函数指针是指向函数的指针变量,c在编译的时候每一个函数都会有一个入口地址,函数指针就是指向这个地址。
- 用途:调用函数和做函数的参数,比如回调函数
重载和覆盖
- 重载:两个函数名相同,但是参数列表不同的函数,在同一个作用域中
- 覆盖:子类继承父类,重写父类中的虚函数
strcpy和strlen
- strlen计算字符串长度,返回长度
- sltcpy逐个拷贝字符,因为没有指定长度所以会导致拷贝越界,安全版本为strncpy
assert()
断言,是宏,而非函数。assert 宏的原型定义在 <assert.h>
(C)、<cassert>
(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG
来关闭 assert,但是需要在源代码的开头,include <assert.h>
之前。
assert() 使用
#define NDEBUG // 加上这行,则 assert 不可用
#include <assert.h>
assert( p != NULL ); // assert 不可用
函数的调用过程
使用到了三个常用的寄存器,EIP
为指令指针,即指向下一条即将执行的指令的地址;EBP
为基址地址,常用来指向栈底;ESP
为栈顶指针,常用来指向栈顶。
- 函数调用
- 将调用函数的参数从右往左(第一个参数在栈顶,可以动态变化参数个数,出栈的时候最左边的先出来)压入帧栈中,call指令跳转到子函数起始地址 - 保存现场
- 将call指令后一条指令的地址压入栈中,实际上就是把EIP
指针入栈
- 将EDP
入栈,因为每个函数都有自己的栈区域,所以栈基址不是一样的,现在需要进入新函数,为了不覆盖调用函数的EDP
,将它压入栈中。
- 将ESP
作为EBP
,即将此时的栈顶地址作为该函数的栈基址
- 再分别将EBX,ESI,EDI
压入栈中,他们分别为基址寄存器,源变址寄存器,目的变址寄存器
- 执行子函数 - 恢复现场
- 分别弹出EDI,ESI,EBX
的值
- 将EDP
的值赋给ESP
,弹出EBP
的值
- 执行call返回断点
pragram pack(n)
- 设定
struct
,union
以及类成员变量以n字节方式对齐,也就是让数据在内存中连续存储,同时以n字节对齐
使用
#pragram pack(push) //保持对齐状态
#pragram pack(4) //设定为4字节对齐
struct test{
char m1;
double m4;
int m3;
};
#pragram pack(pop) //恢复对齐状态
使用C实现C++的类
- 使用C实现C++的对象特性(封装、继承、多态)
- 封装:使用函数指针把属性和方法封装到结构体中
- 继承:结构体嵌套
- 多态:父类和子类方法的函数指针不同
friend友元类和函数
- 能访问私有成员
- 破环封装性
- 友元关系不能传递
- 友元关系的单向性
- 友元声明的形式和数量不受限制
构造函数和初始化
- **默认构造函数(无参数):**如果创建一个类没有写任何构造函数,系统自动生成默认的构造函数,或者写一个不带任何参数的构造函数。
- **一般构造函数:**可以有各种参数形式,一个类可以有多个构造函数,前提是参数的个数或者类型不同
- **拷贝构造函数:**参数为类对象本身的引用,用于根据一个已经存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。参数是(const &)类型的。
- 用类的一个对象取初始化另一个对象时
- 当函数的形参是类的对象时(值传递),如果是引用则不会使用拷贝构造函数
- 当函数的返回值是类的对象时,如果是引用则不会使用拷贝构造函数
=dufault:将拷贝控制成员定义为=default显式要求编译器生成合成的版本
=delete:将拷贝构造函数和拷贝赋值运算符定义为删除的函数,阻止拷贝
=0:将虚函数定义为纯虚函数
构造函数/拷贝构造函数/赋值运算符
- 构造函数:对象不存在,自己进行初始化工作
- 拷贝构造函数:对象不存在,使用其他对象进行初始化
- 赋值运算符:对象存在,用其他的对象给他赋值
直接初始化和复制初始化
直接初始化直接调用于实参匹配的构造函数
复制初始化总是调用复制构造函数,复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。
初始化和赋值
而初始化是要创建一个新的对象,并且其初值来自于另一个已存在的对象。
赋值操作是在两个已经存在的对象间进行的。
初始化列表
- C++构造函数的初始化列表可以使得代码更加简洁,并且少了一次调用默认构造函数的过程,可以用于全部成员变量,也可以只用于部分成员变量
- 成员变量的初始化顺序和初始化列表中列出的变量的顺序无关,只和成员变量再类中声明的顺序有关
- 还有一个重要的功能 就是初始化
const
成员变量。初始化const
成员变量的唯一方法就是使用初始化列表 - 引用类型,引用必须再定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
initializer_list列表初始化
- 使用花括号初始化器列表初始化一个对象,其中对应构造函数接受一个
std::initializer_list
参数
C++异常处理
C++中异常事件发生时,程序使用throw关键字抛出异常表达式,抛出点称为异常出现点,有操作系统为程序设置当前异常对象,然后执行程序的当前异常处理代码块,在包含了异常出现点的最内层的try块,一次匹配catch语句中的异常对象(只进行类型匹配,catch的参数有时候在catch语句中并不会使用到)。
若异常匹配成功,则执行catch块内的异常处理语句,然后执行try…catch之后的代码。如果当前的try…catch找不到匹配该异常对象的catch语句,则有更外层的try…catch块来处理该异常;如果当前函数内所有的try…catch块都不能匹配该异常,则递归回退到调用栈的上一层处理该异常。
如果一直回退到主函数都无法处理,则调用系统函数terminate()终止程序。
// 一些异常类型
exception 最常用的异常类,只报告异常的发生而不报告任何额外的信息<exception>
runtime_error 只有在运行的时候才能检测该异常<stdexcept>
overflow_error 运行时错误:计算上溢
underflow_error 运行时错误:计算下溢
logic_error 逻辑错误
invalid_argument 逻辑错误:参数无效
out_of_range 逻辑错误:使用一个超出有效范围的值
bad_alloc 内存动态分配错误
try{
语句
}
catch(异常类型){
异常处理代码
}
catch(异常类型){
异常处理代码
}
C++11 新特性
- nullptr:替代null,专门用来区分空指针
- 类型推导:auto(变量) ,decltype(表达式)
- 区间迭代:for(auto i :arr)
- 初始化列表:initializer list
vector<int> num{1,2,3}
- 智能指针
- lambda表达式
[捕获区](参数区){代码区};
比如sort(arr.begin(),arr.end(),[](int a, int b){return a > b;});
- 捕获一般有以下几种用法:
[a,&b]
:其中a以复制捕获,b以引用捕获[this]
:以引用捕获当前对象(*this)[&]
:以引用捕获所有用于lambda体内的自动变量,并以引用捕获当前对象,如果存在[=]
:以复制捕获[]
:不捕获,大多数情况下适用
- 右值引用和移动
move
语义:- C++中的变量要么是左值(通俗指非临时变量,&引用),要么是右值(通俗指临时对象,&&引用)。
- 右值引用实现了转移语义和完美转发,主要目的有两个方面
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
- 能够更简洁的定义泛型函数
- 移动语义:可以将资源(堆,系统对象等)从一个对象转移到另一个对象,这样可以减少不必要的临时对象的创建、拷贝及销毁
std::move
:将左值变成一个右值,它是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。- 完美转发:
std::forward()
函数可以对左值引用,右值引用,常左值引用,常右值引用进行完美转发。
- 新增容器:array,forward_list,无序容器(通过哈希实现)(unordered_map/multimap,unordered_set/multiset)
C++ STL
Standard Template Library,标准模板库。
主要由六大组件组成:
- 容器(Containers):各种数据结构如(顺序容器)vector,deque,forward_list,list;(关联容器)set,multiset,map,multimap;(无序关联容器)unordered_set,unordered_multiset,unordered_map,unordered_multimap
- 算法(Algorithm):各种常用算法如sort,search,copy等
- 迭代器(Iterators):泛型指针。
- 仿函数(functors):行为类似函数,可以作为算法的某种策略
- 适配器(Adapters):一种来修饰容器或仿函数或迭代器接口的东西如stack,queue,priority_queue
- 分配器(Allocators):负责空间配置和管理
分配器(Allocators)
构造和析构的基本工具
construct()
接收一个指针和初值value,该函数的用途是将初值设定到指针所指的空间上。C++的placement new
运算符可用来完成。
destory()
有两个版本,第一个版本接收一个指针,准备将该指针所指之物释放;第二版本接受first和last两个迭代器,将之间的对象析构掉。
空间配置和释放
对象构造前的空间配置和对象析构后的空间释放,由<stl_alloc.h>
负责:
- 向内存堆要求空间
- 考虑多线程状态
- 考虑内存不足的应变措施
- 考虑内存碎片问题
C++内存配置由::operator new()
和::operator delete()
完成
为了考虑到内存碎片问题,SGI设计了双层级配置器,第一层配置器(__malloc_alloc_template)直接使用malloc()
和free()
;第二级配置器(__default_alloc_template)视情况使用不同的策略:
- 如果配置区块大于128bytes,视之为足够大,直接调用第一级配置器
- 如果小于,视之为过小,为了降低额外负担和内存碎片,使用内存池(memory pool)方式
- 维护16个自由链表(free lists),负责16种小型区块的次配置能力,内存池以malloc()配置而得
STL内存池(Memory Pool)
chunk_alloc()
函数以end_free-start_free
来判断内存池的大小,如果大小充足,就直接调用20个区块返回给free list,如果不足就提供一个以上的区块,这时候引用传递的nobjs参数会被修改为实际能够调用的区块数。如果一个也无法提供,就利用malloc()函数从heap中配置内存,增加内存池大小。新大小为需求量的两倍,再加上随着配置次数增加而越来越大的附加量。
迭代器(Iterators)和traits编程技巧
一种方法,能够依序巡访某个容器所含的各个元素,而又无需暴露该容器的内部表述方式。
迭代器是一种行为类似指针的对象,而指针的各种行为中最常见也最重要的便是解引用(dereference)和成员访问(member access)。
- 可以使用模板函数的参数推导功能来进行迭代器的关联类型(associated type),但是参数推导功能只能推导参数,而无法推导函数的返回值类型;
- 可以声明内嵌类型,但是并不是所有的迭代器都是class type,比如原生指针就不行。所以如果不是class type,就无法为它定义内嵌类型。
- 所以还需要针对**原生指针T*和const原生指针const T***定义特化版
但是迭代器的关联类型并不只是”迭代器所指对象的类型“一种而已,最常用的关联类型有5种。
traits(特性)
也被称为特性萃取技术,就是指提取”被传进的对象“对应的返回类型,让同一个接口实现对应的功能。
在STL种,算法和容器是分离的,两者通过迭代器连接,算法在实现时并不知道传进来的是什么,traits技法相当于在接口和实现之间加了一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(比如偏特化)。
根据经验,最常用的迭代器关联类型有5种:
// 内嵌类型
template<class T>
struct iterator_traits{
// 由于C++语言默认情况下,假定通过作用域运算符访问的名字不是类型,所以想要访问的是类型的时候
// 必须使用typename告诉编译器这是一个类型
typedef typename T::iterator_category iterator_category;
typedef typename T::value_type value_type;
typedef typename T::difference_type difference_type;
typedef typename T::pointer pointer;
typedef typename T::reference reference;
};
// T*特化版
template<class T>
struct iterator_traits<T*>{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& refernce;
};
// const T*特化版
template<class T>
struct iterator_traits<const T*>{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
tyepdef const T* pointer;
typedef const T& reference;
};
通过iterator_traits
的封装,则对任意类型(不论是迭代器,还是原生指针int*或const int*),都可以通过它获取正确的关联类型信息。
value_type
:指迭代器所指对象的类型difference_type
:用来表示两个迭代器之间的距离reference_type
:指引用类型const T&
pointer_type
:指针类型T*
iterator_category
:迭代器根据移动特性和操作,可以分为五类Input Iterator
:这种迭代器所指的对象,不允许改变,只读Output Iterator
:只写Forward Iterator
:允许”写入型“算法(例如replace())在此种迭代器所形成的区间上进行读写操作Bidirectional Iterator
:可双向移动,某些算法需要逆向走访某个迭代器区间(例如逆向拷贝某范围内的元素)Random Access Iterator
:前四种迭代器都只提供一部分指针算术能力(前三支持opertor++
,第四还支持operatr--
),第五种则涵盖所有指针算术能力(p+n,p-n,p[n],p1-p2,p1<p2)
顺序容器(sequential containers)
- vector:
- 底层数组
- 随机读改,尾部增删O(1),头部增删O(N),支持随机访问
- 无序可重复
- list:
- 底层双向链表
- 指定位置增删O(1),不支持随机访问
- 无序可重复
- forward_list:
- 底层单向链表
- 指定位置增删O(1),不支持随机访问
- 无序可重复
- deque:
- 底层双端队列(一个中央控制器+多个缓冲区)
- 头尾增删O(1),支持随机访问
- 无序可重复
vector
相比于数组的静态空间,Vector是动态空间,随着元素的加入,内部机制会自动扩充空间以容纳新元素。
vector支持随机存取,提供的是Random_access_iterators
,也就是vector的迭代器是普通指针。
vector的扩容(在 VS 下是 1.5倍,在 GCC 下是 2 倍)
-
如果原大小为0,则配置1(各元素大小);
-
如果原大小不为1,则配置原大小的两倍(前半段用来放原数据,后半段用来放新数据);(开辟一块新的空间而不是在原来空间的后面添加)
-
将原来vector的内容拷贝到新的vector中;再新的vector插入新元素
-
析构原来的vector,并调整迭代器指向新的vector。(一旦引起空间重新配置,所有指向原来vector的迭代器就都失效了)
插入时首先检查备用空间大小是否大于新增元素个数m
- 如果大于,则先把插入点之后的所有元素向后移动m位,然后在插入点插入新增元素;
- 如果小于,则先按照上述扩容步骤(max(old_size,n))进行扩容,然后将插入点之前的数据复制到新空间,再把新增元素填入新空间,最后把旧vector中插入点之后的元素复制到新空间。
使用vector的注意事项
在一个vector的尾部之外的任何位置添加元素,都需要重新移动元素。同时可能会引起整个对象存储空间的重新分配。
频繁调用push_back()可能会引起对象的重新分配,并将旧的空间元素移动到新的空间,这十分耗费时间。
list
任何位置的元素插入和删除的时间复杂度都是常数时间。
// 底层实现为双向链表
template <class T>
struct __list_node{
typedef void* void_pointer;
void_pointer prev;
void_pointer next;
T data;
};
由于底层实现为双向链表,迭代器必须具有前后移动的能力,所以list提供的迭代器为bidirectional_iterator
双向迭代器,并且插入和结合操作都不会导致原有的list迭代器的失效,删除操作时只会导致”指向被删除的那个元素“的迭代器失效。
在SGI STL中,list的实现不仅是双向链表,同时还是一个环状双向链表,所以只需要一个指针就可以完整的遍历整个链表。此时只需要在最后一个节点的后面加上一个空白节点,就可以实现STL要求的**”前闭后开“**区间的要求。
List的排序算法不能使用STL的算法sort(),必须使用自己的sort()成员函数,因为STL的sort()函数只接受RandomAccessIterator
forward_list
单向链表,任何位置的插入和删除的时间复杂度都是O(1),不支持随机访问。迭代器为forward_iterator
。
元素插入使用push_front(),所以其元素次序于插入次序相反。
deque队列
vector是一个单向开口的连续线性空间,deque是一个双向开口的连续线性空间(双端队列),即可以在头尾两端分别进行插入和删除操作。
相对vector的区别:
- deque允许以常数时间内对头部进行元素的插入和删除操作
- deque没有容量(capacity)概念,因为是以动态的分段连续空间组合而成,随时可以增加一段新的空间并链接起来
虽然deque也提供Random Access Iterator
,但是他的迭代器并不是普通的指针,这影响了他的计算效率。
对deque进行排序操作时,为了最高效率,可以将它完整的复制到一个vector中,排序之后在复制回deque。
deque的中控器
deque由一段一段的定量连续空间组成,一旦有必要在deque的头部或者尾部添加新空间,便配置一定量的连续空间,串接在整个deque的头部或者尾部。同时使用主控维护其整体连续的假象。
deque采用一块所谓的map
来作为主控,其中每个元素都是一个指针,指向另一端连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。
deque的迭代器
T* cur; // 指向所指缓冲区中的当前元素
T* first; // 指向所指缓冲区中的头
T* last; // 指向所指缓冲区的尾(含备用空间)
map_pointer node; // 指向map中指向该缓冲区的节点
deque::begin() //返回迭代器start,指向deque中第一个元素
deque::end() //返回迭代器end,指向deque中最后一个元素
deque的插入与扩容
当进行push_back()操作时,当插入的元素正好使缓冲区满时,进行扩容操作,新开辟一个缓冲区,并将end指向新开辟的缓冲区
进行push_front()操作时,首先使用start.cur != start.first
判断第一缓冲区是否还有备用空间,如果有就直接在备用空间上构造元素;如果没有就配置一个新的缓冲区,并将start指向新开辟的缓冲区。
在扩容过程中,如果map的尾部的节点备用空间不足,就配置一个更大的,拷贝原来的过去,然后释放;如果头部节点空间不足,也是如此。
容器适配器(container adapter)
stack
是一种先进后出(FILO)的数据结构,只有一个出口,只能在其顶端操作O(1),不允许遍历,stack没有迭代器。
其底层使用deque或者list实现。因为其以底层容器完成其所有工作,而具有这种”修改某物接口,形成另一种风貌“的性质的结构称为适配器。
queue
是一种先进先出(FIFO)的数据结构,其有两个出口,允许在头尾两端进行操作(尾部插入,头部取出)O(1),不允许遍历,没有迭代器。
底层使用deque或者list实现。
priority_queue
是一种有序的queue,允许在头尾两端进行操作(尾部插入,头部取出)O(logN),不允许遍历,没有迭代器。
底层使用vector(max-heap或min-heap)实现。
有序关联容器
STL的关联式容器主要有set(集合)和map(映射)两大类,以及他们的衍生体multiset和multimap,底层均以RB-tree(红黑树)实现。RB-tree也是一个独立容器,但是不开放给外界使用。
关联式容器即每个元素都有一个键(key)和一个值(value)。当元素被插入到关联式容器中时,容器内部结构(RB-tree)便依据键值大小,将其放置在合适的位置。关联式容器没有头部和尾部的概念。
set
所有元素都会根据元素的键值自动被排序,set元素的键值就是value实值。
set不允许存在两个相同的元素。
底层使用RB-tree实现,存在constant bidirectional iterators
,即不允许通过迭代器对集合内元素进行修改。
与list类似,当对元素进行增删操作时,除了被删除的那个迭代器之外,其他迭代器全部有效。
map
所有元素都会按照元素的键值自动排序。map的所有元素都是pair,同时拥有键值(key)和实值(value)。
pair的第一个元素被视为键值,第二个元素为实值。map不允许存在两个相同的键值。
底层使用RB-tree实现,存在constant bidirectional iterators
,即不允许通过迭代器对集合内元素进行修改。
与list类似,当对元素进行增删操作时,除了被删除的那个迭代器之外,其他迭代器全部有效。
multiset
与set的唯一差别就是允许键值重复,底层的插入操作使用的是RB-tree的insert_equal()
而不是insert_unique()
迭代器为constant bidirectional iterators
multimap
与map的唯一差别就是允许键值重复,底层的插入操作使用的是RB-tree的insert_equal()
而不是insert_unique()
迭代器为constant bidirectional iterators
无序关联容器
底层使用hash_table实现,其中哈希表里的元素节点被称为桶(bucket),桶内维护的是链表,但不是STL中的链表,而是自行维护的一个node。bucket聚合体也即hash_table使用vector实现。
template <class Value>
struct __hashtable_node{
__hashtable_node* next;
Value val;
};
所有无序关联容器的迭代器为forward iterators
unordered_set
是含有键值类型唯一对象集合的关联容器。搜索、插入和删除都拥有平均O(1)时间复杂度。
在内部,元素并不以任何特别顺序排序,而是组织进桶中。元素被放进哪个桶完全依赖其值的哈希。这允许对单独元素的快速访问,因为哈希一旦确定,就准确指代元素被放入的桶。
不可修改容器元素(即使通过非 const 迭代器),因为修改可能更改元素的哈希,并破坏容器。
unordered_map
一种关联容器,含有带唯一键的键值对pair。搜索、插入和删除都拥有平均常数时间复杂度。
元素在内部不以任何特定顺序排序,而是组织进桶中。元素放进哪个桶完全依赖于其键的哈希。这允许对单独元素的快速访问,因为一旦计算哈希,则它准确指代元素所放进的桶。
unordered_multiset
是关联容器,含有可能非唯一 Key 类型对象的集合。搜索、插入和删除拥有平均常数时间复杂度。
元素在内部并不以任何顺序排序,只是被组织到桶中。元素被放入哪个桶完全依赖其值的哈希。这允许快速访问单独的元素,因为一旦计算哈希,它就指代放置该元素的准确的桶。
插入操作使用insert_equal(),set使用的是insert_unique()
unordered_multimap
一种关联容器,含有可能非唯一键的键值对pair。搜索、插入和删除都拥有平均常数时间复杂度。
元素在内部不以任何特定顺序排序,而是组织到桶中。元素被放进哪个桶完全依赖于其关键的哈希。这允许到单独元素的快速访问,因为哈希一旦计算,则它指代元素被放进的准确的桶。
算法
以有限的步骤解决逻辑或者数学上的问题,称为算法。Algorithm。
STL算法的一般形式:所有泛型算法的前两个参数都是一对迭代器(iterators),通常称为first和last,用于标示算法的操作区间。
STL中习惯采用前闭后开区间表示法,写成[first,last),表示区间涵盖first至last(不含last)之间的所有元素。当last==last时,表示一个空区间。
sort()
排序算法接受两个RandomAccessIterators
(随机存取迭代器),然后将区间内的所有元素以递增方式由小到大重新排列;也可以接受一个仿函数作为排序标准。
可以对vector,deque进行排序,因为他们的迭代器都属于RandomAccessIterators
。对list进行排序需要使用他们自己的sort()成员函数。
STL中的sort()
算法,数据量大时使用快速排序,分段递归排序。数据量小于某个门槛(16)时,为了避免快排的递归调用带来过大的额外负荷,使用插排。如果递归层次过深,还会使用堆排。
插入排序
以双层循环的形式进行。外循环遍历整个序列,每次迭代决定出一个子区间;内循环遍历子区间,将子区间内的每一个“逆转对”倒转过来。
当数据量很少时效果很好,原因是因为STL中实现了一些技巧,同时不需要进行递归调用等操作,会减少额外负担。
void __insertion_sort(iterator first, iterator last){
if(first == last) return;
for(iterator i = first + 1; i != last; i++) // 外循环
__linear_insert(first, i, value_type(first)); // [first, i)形成一个子区间
}
// 辅助函数
void __linear_insert(iterator first, iterator last, T*){
T value = *last