由高到低分别是:
按照每层序号编号:
TCP是面向连接的、基于数据流的可靠传输。
在数据传输前,需要经过3次握手握手建立连接;在数据传输结束后,需要经过4次挥手断开连接。
在数据传输过程中,需要保证可靠传输、流量控制和拥塞控制。
TCP在传输数据准确、对速度没有硬性要求的场合有很好的表现。
UDP是无连接的、基于数据报的不可靠传输。
在数据传输前,不需要建立连接;在数据传输结束后,不需要断开连接。
在数据传输过程中,不需要保证可靠传输、流量控制、拥塞控制,少了很多开销,因此传输速度快。
UDP在对传输速度要求较高的场景中有很好的表现,如视频通话、网络直播等。
面向字节流的含义:
虽然应用程序和TCP之间的交互是一次一个数据块,但TCP把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。具体表现是:发送端发送数据的次数和接收端接收数据的次数不一致。
面向数据报的含义:
UDP对应用层传下来的报文,既不分割也不合并,而是保留这些报文的边界。应用层交给UDP多长的数据,UDP就照样发送,即一次发送一个报文。因此,应用程序必须选择合适大小的报文;如果报文太长,UDP添加首部后交给IP层,IP层就要在传送时进行分片,降低了IP层效率;如果报文太短,UDP交给IP层后,IP数据报的首部就相对太长,这也降低了IP层效率。
以客户端服务器为例,
第一次握手:客户端向服务器发送SYN同步报文,表示请求建立连接,同时发送序列号,进入SYN_SEND状态;
第二次握手:服务器向客户端发送SYN同步报文和确认,表示同意建立连接,进入SYN-RECVD状态;
第三次握手:客户端收到服务器的同步报文,向服务器发送确认报文,进入ESTABLISHED状态。
第一次挥手:客户端向服务器发送FIN结束报文,表示请求断开连接;
第二次挥手:服务器向客户端发送ACK确认报文,此时还未断开连接,因为服务器还有数据发送给客户端;
第三次挥手:服务器向客户端发送FIN结束报文,表示服务器的数据发送完了,请求断开连接;
第四次挥手:客户端向服务器发送确认报文。
防止已失效的连接请求报文突然又发送至服务器端,导致占用服务器资源。
有这种情况:
客户端第一次发送的连接请求报文,因为网络的原因被滞留了;
于是客户端触发超时重传机制,第二次发送连接请求;
当第二次连接请求正常断开后,第一次的连接请求又到达服务器;
服务器会误认为客户端又重新请求建立连接,于是同一客户端的连接请求;
然而客户端并没有建立连接的意向,服务器只能等待客户端的确认报文,白白占用客户端的资源。
为什么需要四次挥手?
因为TCP连接是全双工的,客户端发送断开请求之后,表示客户端没有数据发送给服务器了;但是服务器可能还有数据发送至客户端。
TIME-WAIT的作用?
流量控制是指,接收方告诉发送方发送的速度不要太快,要让接收方来得及处理。
TCP发送方有个发送缓冲区,接收方有个接收缓冲区。如果发送方发送的太快,会导致接收方接收缓冲区很快被填满,出现丢包现象和造成网络资源浪费的现象。
方法是:接收方通过设置确认报文的“窗口”字段值,动态地改变发送方的发送速率。
拥塞控制是指,防止过多的数据注入到网络中,造成网络中的路由器和链路出现过载的现象。拥塞控制是一个全局性的过程,涉及所有的主机、路由器,以及与降低网络传输性能有关的所有因素。
拥塞控制的算法有4种:慢开始、拥塞避免、快重传和快恢复。
慢开始:
TCP连接刚建立时,并不知道网络当前的负荷情况,不能将大量的数据注入到网络中,这样会造成网络拥塞;正确做法是先设置较小的拥塞窗口值试探,然后以指数的形式增大。
拥塞避免:
指数形式会使拥塞窗口膨胀的很快,因此设定一个慢开始阶段阈值。当拥塞窗口大小超过阈值时,进入拥塞避免阶段。
拥塞避免阶段会降低拥塞窗口的增长速度,让拥塞窗口以线性方式缓慢增长。当网络出现拥塞时,将慢开始阶段拥塞窗口阈值设为拥塞峰值的一半,然后再次执行慢开始算法。
快重传:
有时,个别报文段会在网络中意外的丢失,但实际上网络并没有发生拥塞;发送方迟迟收不到丢失报文的确认,误认为网络发生拥塞,于是发送方错误地启动慢开始算法,因此不必要的降低了传输效率。
快重传算法要求,接收方收到了失序的报文段后,要立即发送对已经收到的报文段的重复确认。
快重传算法是指,当发送方连续收到3个重复的确认报文,就知道网络并没有发生拥塞,只是丢失了个别报文段;因而立即重新发送丢失的报文段。
快恢复:
如果发送方知道了只是丢失个别报文段,没有发生网络拥塞,于是不执行慢启动算法;而是将拥塞窗口值设为拥塞峰值的一半,然后直接执行拥塞避免算法。
产生原因:
发送方有个发送缓冲区,接收方有个接收缓冲区,发送方在发送数据时,会将应用层传下来的多个数据包放在一起,这样数据与数据之间的边界就丢失了;接收方在接收数据时不知道哪些数据是属于同一个包,于是不能正确解析发送方发送的数据。
解决方法:
使用标准的应用层协议(比如http、https协议)来封装要发送的数据;
发送方按照固定模式封装数据,接收方按照固定模式解析数据;
在每条数据的尾部添加一个标识作为结束符;
接收方识别到一个标识符,就说明一条数据接收完毕,有点像C语言中字符串的末尾加个’\0’表示标识当前字符串的结尾。
缺点:效率低,需要一个字节一个字节判断当前字符是不是结束符。
在发送数据块的前面,添加一个固定大小的数据头,即整个数据包括:数据头+数据块;
数据头存储了当前数据的总长度,接收方先接收数据头,再根据数据头中的长度接收指定大小的字节;
数据块,要发送的数据本体。
UDP传输层无法保证数据的可靠传输,只能通过应用层去实现。
UDP只是尽最大努力完成数据交付,而不关心接收方是否受到数据
提供超时重传和确认机制。
发送方在发送报文中添加首部字段:序号和时间戳。发送报文时,携带发送序号和时间戳。接收方接收数据后,提取时间戳和序号,添加到确认报文首部后返回给发送方。发送方根据时间戳计算RTT,从而计算出合适的RTO(超时重传时间),用于超时重传。
地址解析协议,根据IP地址,获取其对应的MAC地址。
主机有一个ARP高速缓存,缓存中存放了一个从IP地址到MAC地址的映射表,这个映射表是动态更新的。
当主机向局域网中的其它主机发送IP分组时,先在ARP高速缓存中查找有无目标主机的IP地址,如果命中,则从ARP高速缓存中取出其对应的MAC地址;
如果没有命中,
网际控制报文协议,ICMP允许主机或路由器报告差错情况,属于网络层协议。
ICMP报文有两种,ICMP差错报告报文和ICMP询问报文。
ping就是ICMP询问报文的一种应用。
tracert就是ICMP差错报告报文的一种应用。
将监听多个文件描述符状态的任务委托给操作系统内核(这个过程是阻塞的),一旦操作系统内核检测到有文件描述就绪(可能是读缓冲区就绪,也可能是写缓冲区就绪)程序的阻塞就会被解除,之后基于这些就绪的文件描述符进行通讯。
与基于多进程、多线程的服务器并发相比,IO多路复用的最大优势就是系统开销小。
//函数原型
int select(int nfds, fd_set* read_fds, fd_set* write_fds, fd_set* exceptfds, struct timeval* timeout);
其中:
select的优点是跨平台,降低系统并发的开销,提高效率;
缺点:
poll的特点:
什么是死锁:
各进程互相等待其它进程所拥有的资源,导致各进程都被阻塞的现象。
死锁产生的必要条件:
预防死锁:
避免死锁:
安全序列:进程的一种执行顺序,按照这个顺序,不会发生死锁现象。
银行家算法:在进行资源分配前,先判断此次资源分配是否会导致系统进入不安全状态;如果会导致系统进入不安全状态的话,则暂时不为该进程分配资源。
死锁的解除:
跟两个因素相关:
1. 系统的位数
32位机下,一个进程的虚拟地址空间为4G,其中用户空间占3G,内核空间占1G。如果创建线程时分配的栈空间为10M,则一个进程最多能创建300个左右的线程。
64位机下,用户空间的虚拟地址128T,理论上一个进程可以创建很多线程,但会手动系统参数的限制。
2. 系统参数的限制
/proc/sys/kernel/threads-max(系统支持的最大线程数);
/proc/sys/kernel/pid_max(系统全局的PID号数值限制,进程和线程都会占用ID,当ID超过pid_max时,创建线程失败);
/proc/sys/vm/max_map_count(一个进程可以拥有的虚拟内存区域限制)。
进程的空间是独立的,因此不能直接访问其它进程的内存空间,需要通过其它的方式实现进程间的通信。
进程间通讯主要指:数据传输、共享数据、通知事件和进程控制。
1. 管道
匿名管道:
是一种半双工的通信方式,数据只能单向流动,而且只能在父子进程间使用。
命名管道:
是一种半双工的通信方式,数据只能单向流动,但允许在没有亲缘关系的进程间使用。
2. 消息队列
消息队列是进程间传递数据块的一种方法,每个数据块都有一个特定的类型,接收方根据类型有选择地接收数据。
消息队列的本质就是存放在操作系统内核中的链表,其中每个节点对应用户定义的数据结构。发送数据相当于往链表中插入节点,接收数据相当于从链表中删除节点。
主要步骤是:
创建消息队列msgget()、将消息添加到消息队列msgsnd()、从消息队列中获取消息msgrcv()、查看/设置/删除消息队列msgctl()。
消息队列在收发数据时,数据一定要按照规定的格式来,如下所示:
struct msgStruct{
long msgType; //消息类型
char msgText[n]; //消息正文,n为字节数,具体多少有程序员控制
...
}
上图中,0记录了消息队列中的第一个消息,1对应一种类型的消息,2对应另一种类型的消息,以此类推…
消息队列的缺点:
进程往消息队列中放数据时,会发生用户态拷贝数据到内核态的过程;进程往消息队列中取数据时,会发生内核态拷贝数据到用户态的操作;即都需要操作系统内核的频繁介入,效率较低。
3. 共享内存
共享内存是指多个进程通过访问同一块物理内存,从而达到通讯的效果。
共享内存是最高效的IPC机制,因为共享内存一旦建立起来,所有的访问都是常规的访存,无需借助内核;即数据不会在进程间来回拷贝。
因为存在多个进程对同一块内存空间的访问,所以涉及到进程的同步互斥问题。
主要步骤是:
创建/获取共享内存shmget()、关联当前进程和共享内存shmat()、通过指针访问共享内存、分离当前进程和共享内存shmdt()、删除共享内存shmctl()。
注意:在操作共享内存时,会更新结构体变量shmid_ds,其内容如下所示:
shm_nattch是共享内存的引用计数,只有shm_nattch值为0时,才会真正删除共享内存(和shared_ptr类似)。
select
/*
提取对象:生产者对象、消费者对象、缓冲区;
分析关系:生产者和消费者访问缓冲区是互斥的;
因为缓冲区大小是有限制的,故只有生产者生产了产品,消费者才能拿走产品;
如果缓冲区为空,消费者进程就会被阻塞,这是同步关系;
如果缓冲区满了,生产者进程就会被阻塞;只有消费者进程消费了产品,生产者才能生产产品,这也是同步关系。
*/
//用于互斥访问缓冲区
Semophore mutex = 1;
//用于产品和容量同步
Semophore capacity = n;
Semophore product = 0;
void Producer()
{
while(1){
//生产产品
P(capacity);
P(mutex);
//将产品放入缓冲区
V(mutex);
V(product);
}
}
void Consumer()
{
while(1){
P(product);
P(mutex);
//从缓冲区中取出产品
V(mutex);
V(capacity);
//消费产品
}
}
//注意:用于访问缓冲区的P操作一定要放在用于同步的P操作之后,否则会造成死锁现象。
读者写者模型
写进程与写进程互斥、写进程和读进程互斥、读进程和读进程不互斥。
如果在读、写前后都进行上锁、解锁操作,那么读进程和读进程就是互斥的了。像下面这样:
Semophore mutex = 1;
void Reader()
{
while(1){
P(mutex);
//读数据...
V(mutex);
}
}
读者写者问题就是要解决,取消读进程和读进程之间的互斥关系。
只让第一个读进程进行上锁操作,最后一个读进程进行解锁操作。想到一个方法,搞个计数器count,用于记录当前是第几个读进程。
int count = 0;
Semophore mutex = 1;
void Reader()
{
while(1){
//先检查当前读进程是否为第一个读进程,如果是,则上锁;否则,不上锁,读与读之间不互斥
if(count == 0)
P(mutex);
count++;
//读数据...
count--;
//检查是否为最后一个读进程,如果是,则解锁
if(count == 0)
V(mutex);
}
}
上面的代码并非完全正确,因为存在多个进程并发访问count变量的问题。
原因:检查count变量和更新count变量不是一气呵成的
这造成的结果是:当count=0,读进程A执行到P(mutex)后,count++前发生进程切换;读进程B检查到count=0,仍然会执行上锁操作,这就造成了读进程B阻塞的现象。
解决办法:所有的读进程互斥地访问count计数器,搞一把锁,锁住count计数器。代码如下:
int count = 0;
Semophore mutex = 1;
//新增互斥量
Semophore mutex_count = 1;
void Reader()
{
while(1){
//先检查当前读进程是否为第一个读进程,如果是,则上锁;否则,不上锁,读与读之间不互斥
P(mutex_count);
if(count == 0)
P(mutex);
count++;
V(mutex_count);
//读数据...
P(mutex_count);
count--;
//检查是否为最后一个读进程,如果是,则解锁
if(count == 0)
V(mutex);
V(mutex_count);
}
}
上述代码已经做了很大改进,实现了读进程与读进程不互斥的功能,但并不完美。
如果系统中创建很多对该资源的读进程,就会造成写进程饥饿的现象。因为只要有进程在读临界资源,写进程就会被阻塞(等待最后一个读进程释放锁);而新来的读进程却不需要等待直接可以读临界资源,这样写进程等待时间变得更长了。
读写公平
解决办法如下:
int count = 0;
Semophore mutex = 1;
//新增互斥量
Semophore mutex_count = 1;
Semophore rw = 1;
void Reader()
{
while(1){
P(rw);
//先检查当前读进程是否为第一个读进程,如果是,则上锁;否则,不上锁,读与读之间不互斥
P(mutex_count);
if(count == 0)
P(mutex);
count++;
V(mutex_count);
V(rw)
//读数据...
P(mutex_count);
count--;
//检查是否为最后一个读进程,如果是,则解锁
if(count == 0)
V(mutex);
V(mutex_count);
}
}
void Writer()
{
while(1){
P(rw);
P(mutex);
//写数据...
v(mutex);
V(rw);
}
}
用来描述和控制进程运行的一种数据结构,是进程实体的一部分,是进程存在的唯一标识。
PCB中包含以下几种信息:
1. 进程标识符
进程标识符用于唯一的标识一个进程,每个进程都被操作系统赋予了一个唯一的数字标识符
2. 处理机状态
处理机的状态信息主要是由处理机各种寄存器中的内容组成。包括:通用寄存器、指令计数器、程序状态字PSW和用户栈指针。当进程被中断时,必须将这些寄存器的值保存到PCB中,以便重新运行时能够恢复到中断前的状态。
3. 进程调度信息
进程调度信息主要包括:进程的状态、进程优先级、阻塞原因等,这些信息都和进程的切换(即调度)相关。
4. 进程控制信息
主要包括程序和数据的地址信息、进程同步和通信机制信息、资源清单(分配到该进程的资源)、链接指针(指向队列中相同状态的下一个PCB的地址)。
PCB的组织方式:
PCB组织方式是指如何组织和管理多个PCB。有两种方式:
1. 链接方式
把处在同一状态的PCB,用链表的形式组织成一个队列,于是形成了:就绪队列、阻塞队列、空白队列等。就绪队列中常按进程优先级的高低排列,优先级较高的PCB放在队列前面。
根本区别:
进程是资源分配的基本单位;线程是处理机调度的基本单位。
资源开销:
每个进程都有独立的代码和数据空间,进程之间的切换开销较大;
线程可以看作是轻量级进程,同一进程内的线程共享代码和数据空间,每个线程都有自己独立的栈和程序计数器,线程之间的切换开销较小。
包含关系:
一个进程中可以包含多个线程。
内存分配:
同一进程的线程共享本进程的地址空间和资源;进程之间的地址空间和资源是相互独立的。
什么时候使用多线程?(多线程优点)
线程的使用方法是:创建线程对象,向线程对象中传递线程函数,因此线程的运行本质是函数的执行。函数运行时的信息保存在栈帧中,这些信息包括:函数的参数、局部变量、返回值以及函数使用的寄存器信息,因此每个线程都拥有自己独立的栈空间。
为什么使用线程?
线程状态
现代的处理器使用的是虚拟地址寻址,CPU需要将虚拟地址转换成物理地址后才可以进行访存;在CPU中负责虚拟地址到物理地址转换的是MMU(内存管理单元)。虚拟指的是每个进程的起始地址都被虚拟化为0。
为什么要使用虚拟地址空间?
如果不使用虚拟地址空间,说明进程是直接访问物理内存的,这就会出现几个问题:
1. 进程地址空间不隔离
由于进程直接访问物理内存,那么在多任务处理系统中,一个进程就可能有意无意地去修改了其它进程的内存数据,导致其它进程运行出现异常。这也不满足进程的独立性特点。
引入虚拟地址空间后,就相当引入了一个中间层,进程首先访问的是虚拟地址,访问虚拟地址时可以确保不会越界;然后操作系统负责完成虚拟地址到物理内存地址的映射,这样进程间就达到了隔离的效果。
2. 内存使用效率低。
如果直接使用物理地址,一个进程就对应连续的内存块;在进程的换入换出操作中,就要将整个进程搬走,导致效率低下。
进程虚拟空间对应的各个分区:
每个进程的虚拟地址空间都被分成两部分:内核区 + 用户区。
注意:系统中所有进程虚拟地址空间中的内核区都会映射到同一块物理内存上,因为操作系统内核只有一个。
智能指针是一个类,类中包含一个指针指向在堆空间上动态分配的对象。当智能指针对象生命周期结束时,自动释放动态分配对象的空间。
共享指针,允许多个智能指针指向同一个动态分配对象;
每当一个新的智能指针指向该对象时,该对象的引用计数就会+1;每当析构一次该对象时,该对象的引用计数就会-1。当引用计数减为0时,自动释放该动态分配的资源。
shared_ptr的核心就是引用计数:
以下为shared_ptr的代码简易实现:
tempplate<typename T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)){}
SharedPtr(const SharedPtr& s) : _ptr(s.ptr), _pcount(s._pcount){(*_pcount)++;}
SharedPtr<T>& operator=(const SharedPtr& s){
//自我检测,防止出错
if(this != &s){
if(--(*(this->_pcount)) == 0){
delete this->_ptr;
delete this->_pcount;
}
this->_ptr = s._ptr;
this->_pcount = s._pcount;
//引用计数+1
*(this->_pcount)++;
}
return *this;
}
T& operator*(){
return *(this->_ptr);
}
T* operator->(){
return this->_ptr;
}
//析构函数
~SharedPtr(){
//每调用一次析构函数,引用计数就需要减1
--(*(this->_pcount));
//当引用计数减为0时,释放堆空间上动态分配的对象和引用计数
if(*(this->_pcount) == 0 ){
delete this->_ptr;
this->_ptr = nullptr;
delete this->_pcount;
this->_pcount = nullptr;
}
}
private:
//指向堆空间上动态分配的对象
T* _ptr;
//指向引用计数
int* _pcount;
}
shared_ptr ptr(new T) 和 make_shared()的区别:
前者是先在堆上开辟一块空间存放T,再由shared_ptr的构造函数在堆上开辟一块空间存放引用计数;这两步是不连续的,容易产生内存碎片;分配两次内存。
后者是一次性为对象和引用计数分配一块连续的内存,只分配一次内存,不容易产生内存碎片。
注意:
-不能使用原始指针初始化多个shared_ptr对象
int* rawPt = new int(5);
shared_ptr<int> shpt(rawPt);
shared_ptr<int> shpt2(rawPt);
/*
上面的做法是错误的,如果用同一个原始指针初始多个shared_ptr对象,在释放内存的时候,会出现double free现象。
为什么会出现double free呢?
看一下shared_ptr的初始化构造函数就知道了
*/
//下面是简易版的构造函数:
SmartPtr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)){}
//观察SmartPtr的构造函数发现,如果传入的是对象的原始指针,会在堆空间上重新开辟一块空间保存引用计数
//shpt, shpt2的问题在于,两个智能指针指向的是堆上的同一个对象,然而没有共享一份引用计数;
//那么在shpt、shpt2超出作用域时,指针所指对象会被释放两次,出现错误!
unique_ptr是一个独占型的智能指针,同一时刻只能有一个unique_ptr指向堆空间上动态分配的对象。因此,unique_ptr不支持普通的拷贝构造和赋值操作,但可以通过std::move将控制权转移至其它的unique_ptr。
weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针。它指向了shared_ptr管理的堆上内存,但是只引用不计数。
shared_ptr有以下几个缺点:
对于循环引用问题:
template<typename T>
class Node
{
public:
Node(const T& val) : m_val(val), m_pPre(shared_ptr<Node<T>>()), m_pNext(shared_ptr<Node<T>>()){
cout << "Node constructor..." << endl;
}
~Node(){
cout << "Node destructor..." << endl;
}
int m_val;
shared_ptr<Node<T>> m_pPre;
shared_ptr<Node<T>> m_pNext;
}
int main()
{
shared_ptr<Node<int>> shptr1(new Node<int>(1));
shared_ptr<Node<int>> shptr2(new Node<int>(2));
cout << "shptr1指向对象的引用计数为:" << shptr1.use_count() << endl;
cout << "shptr2指向对象的引用计数为:" << shptr2.use_count() << endl;
//相互引用
shptr1->m_pNext = shptr2;
shptr2->m_pPre = shptr1;
cout << "shptr1指向对象的引用计数为:" << shptr1.use_count() << endl;
cout << "shptr2指向对象的引用计数为:" << shptr2.use_count() << endl;
return 0;
}
上面代码的执行结果是:
shptr1指向对象的引用计数为:1
shptr2指向对象的引用计数为:1
shptr1指向对象的引用计数为:2
shptr2指向对象的引用计数为:2
函数退出时,shptr1和shptr2的引用计数变成1,堆空间上的内存没有被释放掉,出现内存泄漏!
解决方法:使用weak_ptr解决循环引用,因为weak_ptr指向堆空间上动态分配的对象时,只会引用不会改变引用计数。如下所示:
template<typename T>
class Node
{
public:
Node(const T& val) : m_val(val), m_pPre(weak_ptr<Node<T>>()), m_pNext(weak_ptr<Node<T>>()){
cout << "Node constructor..." << endl;
}
~Node(){
cout << "Node destructor..." << endl;
}
int m_val;
weak_ptr<Node<T>> m_pPre;
weak_ptr<Node<T>> m_pNext;
}
Lambda表达式的优点:
语法形式如下:
[ capture ] ( params ) opt -> ret { body; };
[capture] 为捕获列表,可以捕获一定范围内的变量,有以下几种:
通过引入右值引用优化性能,即通过移动语义来避免不必要的拷贝问题。要实现移动语义,必须要采取某种方式告诉编译器什么时候需要拷贝对象,什么时候不需要。因此,我们需要定义一个移动构造函数,它使用右值引用作为参数。
实现移动语义的步骤:
移动构造函数不会执行深拷贝,而是将所有权转移至其它对象。
//++i代码实现,前置返回一个引用,效率较高
int& operator++()
{
(*this) += 1;
return (*this);
}
//i++代码实现,后置返回一个拷贝对象,效率较低
int operator++(int)
{
int temp = *this;
++(*this);
return temp;
}
//字符串字面值是左值,可以取地址,这是个例外。因为C++用char类型数组实现了字符串字面值
cout << &("hello world") << endl;
x + y;
//x + y是得到的是一个不具名的临时对象,是一个纯右值
什么是野指针?
野指针是指指针指向的位置是随机的、不确定的。
野指针产生的原因?
int* ptr;
cout << *ptr << endl;
//以上代码编译时就会出错,引用了未初始化的指针(即野指针)
int* p = new int(11);
delete p;
cout << *p << endl;
//以上代码在运行时报错
int* func()
{
int a = 11;
int* p = &a;
return p;
}
int main()
{
int* ptr = func();
cout << *ptr << endl;
return 0;
}
以上代码虽然不会报错,但是会返回一个随机的、不确定的值。
如何避免野指针?
定义指针时一定要初始化;
指针释放后一定要置空。
迭代器的定义:提供一种方法,在不暴露容器内部结构的情况下,遍历容器中的各个元素。
STL的设计思想是将容器和算法分开单独设计,然后用迭代器将其撮合起来。
迭代器的本质:迭代器本质是一种智能指针,对原始的指针进行封装。迭代器的最重要的两个操作:解引用和成员访问操作。
迭代器的分类:输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器。
迭代器的类型推导:STL的设计思想是将算法和容器分开,在算法设计中,可能要用到迭代器所指对象的类型去定义中间变量。因此需要一种方法提取迭代器所指对象的数据类型,STL用的方法是traits。
traits技法
STL专门设计了一个iterator_traits模板类(其实是模板结构体)用来萃取迭代器or指针的数据类型。
对于迭代器,在设计迭代器时声明内嵌数据类型:
template <class T>
struct MyIterator{
typedef T value_type; //声明内嵌数据类型
T* ptr; //封装原生指针
MyIterator(T* p) : ptr(p){}
//解引用
T& operator*(){
return *ptr;
}
//成员访问
T* operator->(){
return ptr;
}
//......
}
于是,针对某个算法传入的迭代器,可以通过如下方法提取迭代所指向的数据类型:
template <class T>
struct iterator_traits{
typedef typename T::value_type value_type;
}
以上设计有一个问题,如果iterator_traits的模板参数是指针类型,那么iterator_traits无法提取该指针对应的数据类型。解决这种问题的方法是利用模板偏特化(template partial specialization)。
所谓模板偏特化,是指将参数的类型进一步限制。
于是,iterator_traits可以修改成:
//如果模板参数是迭代器,则可以通过迭代器的内嵌类型提取出迭代器的value_type
template <class T>
struct iterator_traits{
typedef typename T::value_type value_type;
//......
}
//如果iterator_traits的模板参数是指针类型
template <class T>
struct iterator_traits<T*>{
typedef T value_type;
}
//如果iterator_traits的模板参数是常量指针类型
template <class T>
struct iterator_traits<const T*>{
typedef T value_type;
}
上面最后两个iterator_traits的定义就是模板偏特化。