私人学习笔记

DMA

DMA,全称Direct Memory Access,即直接存储器访问。

DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。当CPU初始化这个传输动作,传输动作本身是由DMA控制器来实现和完成的。DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场过程,通过硬件为RAM和IO设备开辟一条直接传输数据的通道,使得CPU的效率大大提高。

DMA工作流程:

  • 用户应用进程调用read函数,向操作系统发起IO调用,进入阻塞状态,等待数据返回。
  • CPU收到指令后,对DMA控制器发起指令调度。
  • DMA收到IO请求后,将请求发送给磁盘;
  • 磁盘将数据放入磁盘控制缓冲区,并通知DMA
  • DMA将数据从磁盘控制器缓冲区拷贝到内核缓冲区。
  • DMA向CPU发出数据读完的信号,把工作交换给CPU,由CPU负责将数据从内核缓冲区拷贝到用户缓冲区。
  • 用户应用进程由内核态切换回用户态,解除阻塞状态

开启步骤:

初始化DMA控制器

首先,需要初始化DMA控制器,以确保它能够正确地控制数据传输。这通常涉及设置DMA控制寄存器和地址寄存器等参数。

配置外设和存储器

接下来,需要配置外设和存储器,以指定数据传输的源地址和目的地址。外设可能有特定的寄存器或接口,用于配置DMA传输的相关参数。

开启DMA传输

一旦DMA控制器和外设、存储器都已正确配置,就可以开始DMA传输了。这可以通过向DMA控制器发送一个启动传输的命令来实现。

数据传输

DMA控制器将负责管理数据的传输,它会直接从外设读取数据,并将其写入存储器,或者从存储器读取数据,并将其发送到外设。

零拷贝

含义:零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及CPU的拷贝时间。它是一种I/O操作优化技术。

实现方式:

mmap+write

  • 用户进程通过mmap方法向操作系统内核发起IO调用,上下文从用户态切换为内核态
  • CPU利用DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
  • 上下文从内核态切换回用户态,mmap方法返回。
  • 用户进程通过write方法向操作系统内核发起IO调用,上下文从用户态切换为内核态
  • CPU将内核缓冲区的数据拷贝到的socket缓冲区。
  • CPU利用DMA控制器,把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write调用返回。

节省一次CPU拷贝,发生了4次上下文切换和3次数据拷贝

sendfile

  1. 用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
  2. DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
  3. CPU将读缓冲区中数据拷贝到socket缓冲区
  4. DMA控制器,异步把数据从socket缓冲区拷贝到网卡,
  5. 上下文(切换2)从内核态切换回用户态,sendfile调用返回。

2次用户空间与内核空间的上下文切换,以及3次数据拷贝

sendfile() 系统调用不需要将数据拷贝或者映射到应用程序地址空间中去,所以 sendfile() 只是适用于应用程序地址空间不需要对所访问数据进行处理的情况。

带DMA收集功能的sendfile

SG-DMA技术:实现直接从内核空间读取数据到网卡。

  1. 用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
  2. DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
  3. CPU把内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到socket缓冲区
  4. DMA控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
  5. 上下文(切换2)从内核态切换回用户态,sendfile调用返回。

可以发现,sendfile+DMA scatter/gather实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及2次数据拷贝。其中2次数据拷贝都是包DMA拷贝。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过CPU来搬运数据,所有的数据都是通过DMA来进行传输的。

连接池

 数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并讲这些连接组成一个连接池(简单说:在一个“池”里放了好多半成品的数据库联接对象),由应用程序动态地对池中的连接进行申请、使用和释放。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。并且应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。 连接池技术尽可能多地重用了消耗内存地资源,大大节省了内存,提高了服务器地服务效率,能够支持更多的客户服务。通过使用连接池,将大大提高程序运行效率,同时,我们可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。 
不使用连接池情况:

使用连接池情况:

 

 工作原理:连接池建立-使用管理-关闭

       第一、连接池的建立。一般在系统初始化时,连接池会根据系统配置建立,并在池中创建了几个连接对象,以便使用时能从连接池中获取。连接池中的连接不能随意创建和关闭,这样避免了连接随意建立和关闭造成的系统开销。Java中提供了很多容器类可以方便的构建连接池,例如Vector、Stack等。

        第二、连接池的管理。连接池管理策略是连接池机制的核心,连接池内连接的分配和释放对系统的性能有很大的影响。其管理策略是:

        当客户请求数据库连接时,首先查看连接池中是否有空闲连接,如果存在空闲连接,则将连接分配给客户使用;如果没有空闲连接,则查看当前所开的连接数是否已经达到最大连接数,如果没达到就重新创建一个连接给请求的客户;如果达到就按设定的最大等待时间进行等待,如果超出最大等待时间,则抛出异常给客户。

        当客户释放数据库连接时,先判断该连接的引用次数是否超过了规定值,如果超过就从连接池中删除该连接,否则保留为其他客户服务。

        该策略保证了数据库连接的有效复用,避免频繁的建立、释放连接所带来的系统资源开销。

        第三、连接池的关闭。当应用程序退出时,关闭连接池中所有的连接,释放连接池相关的资源,该过程正好与创建相反。

weak_ptr

1、检查有效性:

lock():若weak_ptr已过期,返回空的share_ptr指针,反之则返回和当前weak_ptr指向系统的share_ptr指针

expired():判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。

weak_ptr<T> 模板类没有重载 * 和 -> 运算符,因此 weak_ptr 类型指针只能访问某一 shared_ptr 指针指向的堆内存空间,无法对其进行修改。

TCP除常见机制外保证可靠的方式

除了TCP本身提供的可靠传输机制即序号和确认号、超时重传、滑动窗口、确认机制、拥塞控制之外,还有其他层上的方式可以保证数据的可靠传输。

比如在数据链路层和物理层上,常用的技术包括循环冗余校验(CRC)、帧检验序列(FCS)等,用于检测和纠正数据传输中的错误。

在应用层上,常用的方法包括数据重传、数据校验等。例如,HTTP协议通常会在应用层上进行数据重传,以保证数据的可靠传输。另外,应用层协议也可以使用一些校验算法,如MD5、SHA等,来验证数据的完整性,以保证数据在传输过程中不被篡改。

虚函数表深入

1、所有类的对象共享一个虚函数表,但是有各自的虚表指针

2、只要父类有虚函数,子类不写virtual也是虚函数

3、子类可以有多个虚函数表,只要继承多个父类即可

4、子类中没有新的虚函数,子类的虚函数表和父类相同,但是还是两个不一样的表,只是内容相同,但是地址不同

5、虚函数表创建时间:编译期间  虚表指针创建时间:运行时,即类的创建时间

实例化含虚函数的子类对象步骤:

  1. 开辟内存空间
  2. 构造父类
  3. 在类的首地址处,填入编译时创建完毕的虚函数表的地址,即虚函数表指针
  4. 进入类的构造函数,执行初始化列表
  5. 执行构造函数body部分

在父类的构造函数中,通过指向子类的父类this指针,调用了虚函数,且子类中重写了该虚函数。但是,此时子类的虚函数表并未明确(虚函数表指针尚未填入),所以不会触发多态,还是会调用父类的虚函数。

move使用场景

1、转移对象资源所有权,避免进行不必要的数据拷贝

2、对容器进行插入或删除操作的时候使用move避免额外拷贝

std::vector<std::string> sourceVec = {"A", "B", "C"};

// 将 sourceVec 的元素移动到 targetVec
std::vector<std::string> targetVec;
for (auto&& element : sourceVec) {
    targetVec.push_back(std::move(element));
}
// 在从 sourceVec 移动后,sourceVec 不再拥有原有的元素

注意for循环中的&&

3、返回右值

在函数返回时,可以使用 std::move 将局部对象的所有权转移给返回的右值,避免不必要的拷贝。

std::string createLargeString() {
    std::string largeString = "Very large string";
    // 其他操作
    return std::move(largeString);
}

将局部对象的所有权转移到函数返回的右值,避免拷贝大字符串开销

4、确保资源正确释放和避免资源泄露

削峰和限流

1、消息队列削峰

要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。

2、分层过滤

就是对请求进行分层过滤,从而过滤掉一些无效的请求

  • 通过在不同的层次尽可能地过滤掉无效请求。
  • 通过CDN过滤掉大量的图片,静态资源的请求。
  • 再通过类似Redis这样的分布式缓存,过滤请求等就是典型的在上游拦截读请求。

HTTPS加密过程

HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。

确保传输安全过程(其实就是rsa原理):

  1. Client给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。
  2. Server确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。
  3. Client确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给Server。
  4. Server使用自己的私钥,获取Client发来的随机数(Premaster secret)。
  5. Client和Server根据约定的加密方法,使用前面的三个随机数,生成”对话密钥”(session key),用来加密接下来的整个对话过程。

delete和trancate区别

delete和truncate都是用来删除数据或表的命令,但是它们之间有一些区别。

  • delete属于数据库DML操作语言,只删除数据不删除表的结构,会走事务,执行时会触发trigger;
  • 在InnoDB中,DELETE其实并不会真的把数据删除,mysql实际上只是给删除的数据打了个标记为已删除,因此delete删除表中的数据时,表文件在磁盘上所占空间不会变小,存储空间不会被释放,只是把删除的数据行设置为不可见。虽然未释放磁盘空间,但是下次插入数据的时候,仍然可以重用这部分空间(重用→覆盖);
  • DELETE执行时,会先将所删除数据缓存到rollback segement中,事务commit之后生效;
  • delete from table_name删除表的全部数据,对于MyISAM会立刻释放磁盘空间,InnoDB不会释放磁盘空间;
    truncate用于删除表中的所有数据,但是保留表的结构。使用truncate命令后,表的结构和数据都会被删除,无法恢复 。

RPC协议

RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

TCP send函数

1、sendto函数单次发送的最大业务数据是65507字节

从报文角度解析(报文字段已经硬性规定了这个限制):
        1、IP报文最大是:IP包头有一个16bit的长度,最大值是2^16 -1,也就是说一个IP包整个长度的最大值是: 2^16 - 1  = 65535字节

        2、因为是UDP,65535字节除去IP头的20个字节,除去UDP头的8个字节,那么:最大可发送字节:2^16 - 1 - 20 - 8 字节

send函数能超过65535个字节, 实际上, 如果send函数的长度过大, 那么会分为多个tcp包来发

2、如果buffer中的数据过大,我也只需要调用一次send函数,而底层到底是一次传输成功还是陆续传输我不用管了吗?

答:recv到的数据流可能是断断续续的,你要把他们放在一起然后解码。

3、Send分为阻塞和非阻塞,

阻塞模式下,如果正常的话,会直到把你所需要发送的数据发完再返回;

非阻塞模式,会根据你的socket在底层的可用缓冲区的大小,来将你的缓冲区当中的数据拷贝过去,有多大缓冲区就拷贝多少,缓冲区满了就立即返回,这个时候的返回值,只表示拷贝到缓冲区多少数据,但是并不代表发送多少数据,同时剩下的部分需要你再次调用send才会再一次拷贝到底层缓冲区。

4、send函数具体过程

在这里插入图片描述

send函数并不是直接将数据传输到网络中,而是负责将数据写入输出缓冲区,数据从输出缓冲区发送到目标主机是由TCP协议完成的。数据写入到输出缓冲区之后,send函数就可以返回了,数据是否发送出去,是否发送成功,何时到达目标主机,都不由它负责了,而是由协议负责。

recv函数也是一样的,它并不是直接从网络中获取数据,而是从输入缓冲区中读取数据。

输入输出缓冲区,系统会为每个socket都单独分配,并且是在socket创建的时候自动生成的。一般来说,默认的输入输出缓冲区大小为8K。套接字关闭的时候,输出缓冲区的数据不会丢失,会由协议发送到另一方;而输入缓冲区的数据则会丢失。
int send( SOCKET s, const char FAR *buf, int len, int flags );  

其中,第一个参数为套接字描述符,第二个为指明应用程序要发送数据的缓冲区,第三个为实际要发送数据的字节数,第四个一般为0.

调用该函数时:

1、首先比较发送数据长度len和套接字缓冲区s的长度,若len大于s,该函数返回SOCKET_ERROR。

2、若len小于或者等于s,send先检查是否协议是否正在发送s的缓冲区数据,若有数据,就等数据发送完,若发送还没开始或者没有,就比较s的发送缓存区属于空间和len的大小

3、如果len大于s剩余空间,send就一直等待协议把s的发送缓存中的数据发送完

4、如果len小于s的剩余空间,send把buf中的数据copy到s剩余空间中,注意不是另一端。如果copy成功,返回copy的字节数,否则返回SOCKET_ERROR

要注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执 行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR)

2. recv函数
int recv( SOCKET s, char FAR *buf, int len, int flags);   
    不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。该函数的第一个参数指定接收端套接字描述符;
    第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
    第三个参数指明buf的长度;
    第四个参数一般置0。
    这里只描述同步Socket的recv函数的执行流程。当应用程序调用recv函数时,
    (1)recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,
    (2)如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),
    recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
注意:在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

对于在send和recv过程中数据接受不完整的问题

1、利用SetSocketOpt()函数将接收方套接子接收缓冲设为足够大小

 3.设置为阻塞方式:
阻塞就是干不完不准回来!

2.基于winsock API包装send和recv

4:在发送方进行数据发送时判断发送是否成功,如果不成功重发;
5:要求接收方收到数据后给发送方回应,发送方只在收到回应后才发送下一条数据。

基础语法-01-20 | 阿秀的学习笔记 (interviewguide.cn)

字节对齐

对齐要求:起始地址为其长度的整数倍即可。如,int类型的变量起始地址要求为4的整数倍。

两个规则

  1. 数据类型自身的对齐值:为指定平台上基本类型的长度。对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
  2. 结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
  3. 指定对齐值:#pragma pack (value)时的指定对齐值value。
  4. 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值

对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐:也就是地址整除以对齐值
数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
联合 :按其包含的长度最大的数据类型对齐。
结构体: 结构体中每个数据类型都要对齐。

typedef与define区别

typedef是定义了类型的新别名,不同于define,它不是简单的字符串替换

  先定义:

  typedef char* PSTR;

  然后:

  int mystrcmp(const PSTR, const PSTR);

  const PSTR实际上相当于const char*吗?不是的,它实际上相当于char* const。

  原因在于const给予了整个指针本身以常量性,也就是形成了常量指针char* const。

  简单来说,记住当const和typedef一起出现时,typedef不会是简单的字符串替换就行。

宏定义只是简单的字符串代换(原地扩展),而typedef则不是原地扩展,它的新名字具有一定的封装性,以致于新命名的标识符具有更易定义变量的功能。请看上面第一大点代码的第三行:

  typedef (int*) pINT;

  以及下面这行:

  #define pINT2 int*

  效果相同?实则不同!实践中见差别:pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量。而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b。

为什么用成员初始化列表会快一些? 

简单的来说:
对于用户定义类型:
1)如果使用类初始化列表,直接调用对应的构造函数即完成初始化
2)如果在构造函数中初始化,那么首先调用默认的构造函数,然后调用指定的构造函数

有哪些情况必须用到成员列表初始化?作用是什么?

  1. 必须使用成员初始化的四种情况

① 当初始化一个引用成员时;

② 当初始化一个常量成员时;

③ 当调用一个基类的构造函数,而它拥有一组参数时:基类的有参构造器必须在初始化列表中调用。

class A
{
public:
    A(int i)
    :i_(i){}

private:
    int i_;
};

class B : public A
{
public:
    B(int x)
    :A(x){}     //调用父类的有参构造器

};

int main()
{
    B b(100);

    return 0;
}

④ 当调用一个成员类的构造函数,而它拥有一组参数时:类数据成员是类对象,且需要调用其有参构造器,那么只能在初始化列表中调用。

  1. 成员初始化列表做了什么

① 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;

② list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;

内存交换、覆盖、虚拟内存

虚拟存储技术和交换技术很像,都是换入换出,把暂时不需要用的数据换出内存,将需要用到的数据换入内存,从而实现逻辑上内存的扩充。

二者之间的区别是:

1、虚拟存储技术是在一个作业运行的过程中,将作业的数据进行换入换出。比如玩游戏,停留在场景A的时候,场景B的数据不需要用到,所以不放在内存,转换到场景B的时候再把场景B的数据放入内存。(段页式管理)

2、交换技术是内存紧张时,换出某些进程,腾出内存空间,换入其他进程。换而言之,交换技术是在不同的进程(作业)间的,虚拟存储技术是在一个作业间的。另外提一嘴,覆盖技术也是在同一个程序或进程中的。

交换技术问题:

  1. 交换时机的确定:只有当内存空间不够或又不够的危险时换出;
  2. 交换区的大小:必须足够大存放所有用户进程的所有内存映像的拷贝,必须对这些内存映像进行直接存取;
  3. 程序换入的重定位:换出后再换入内存的位置不一定一样,寻址可能会出现问题。所以最好采用动态地址映射的方法。

交换技术对于程序员来说是透明的,减轻了程序员的负担,但是系统的开销变大了。

覆盖:

把程序按照其自身逻辑结构,划分为若干个功能相对独立的程序模块;那些不会同时执行的模块共享同一内存区域,按时间先后来运行:
必要部分(常用功能)的代码和数据常驻内存;
可选部分(不常用功能)在其他程序模块中实现,平时存放在外存中,在需要用时才装入内存;
不存在调用关系的模块不必同时装入到内存,从而可以相互覆盖,即这些模块共用一个分区。
在这里插入图片描述

存在问题

最大问题开销问题非常大:

  • 设计开销大,设计者需要考虑最优设计,如何安排模块;
  • 运行过程中不停地涉及到硬盘读写。

1、引用在C++中的内部实现是一个常指针。
Type& name <--> Type* const name
2、C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小 与指针相同。

3、当执行语句“a = fun1();”的时候就会把临时变量的值再拷贝给a,假设这个临时变量是t,相当于做了这两个赋值的步骤:t = temp; a = t;

引用作为函数返回值时:

1、返回局部对象引用:函数执行完局部对象被销毁,返回地址不存在

2、返回堆空间的引用(函数内new一个对象):堆空间得不到释放,可能造成内存泄漏

HTTPS

1、Http缓存
    1)缓存分类
缓存分为强缓存和协商缓存,其中强缓存优先级较高,命中强缓存失败情况下才会执行协商缓存
强缓存:访问浏览器缓存,若命中缓存,则直接从缓存中获取数据,不在访问服务器(命中返回码200)
协商缓存:访问浏览器缓存,同时访问服务器查看缓存是否有效,进而判断是重新发起请求还是从本地获取资源(命中返回码304:
304Not Modified未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
强缓存实现:Expires和cache-control
Expires是一个时间戳,接下来再向服务器请求资源的时候就会对比本地时间和时间戳,若本地时间小于设定的过期时间,就直接从缓存中获取资源。
缺陷:expires由服务器定义,本地时间取自于客户端,客户端和本地时间一致性要求高,且expires是HTTP1.0产物,所以实现缓存大多依赖cache-control,但是这两个都有
Cache-control:Http 1.1产物,包含值很多:
public:表示响应可以被任何对象(如发送请求客户端,代理服务器)缓存
private:响应只能被客户端缓存
no-cache:跳过强缓存,直接进入协商缓存
max-age:设置缓存的最大周期
协商缓存:控制字段:Last-Modifed和if-Modified-since(Http 1.0)
Last-Modif:资源最后修改时间,是一个时间戳,首次请求和response返回
if-Modified-since:只能用于get和head请求,记录缓存中资源最后修改时间,是再次请求时请求头包含
服务端收到请求发现此请求头中有If-Modified-Since字段,会与被请求资源的最后修改时间进行对比,如果一致则会返回304和响应报文头,浏览器从缓存中获取数据即可。从字面上看,就是说从某个时间节点开始看,是否被修改了,如果被修改了,就返回整个数据和200 OK,如果没有被修改,服务端只要返回响应头报文,304 Not Modified,Response Headers不会再添加Last-Modified字段。
缺陷:
1、如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为 If-Modified-Since 只能检查到以秒为最小计量单位的时间差。
2、如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。
3、我们编辑了文件,但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求。
改进Etag和if-None-Match
Etag:响应头部字段,根据实体内容生成hash字符串,标志资源状态,由服务端产生
if-None-Match:请求式首部,请求资源是请求头部加上该字段,值为Etag,若服务器上有资源与该Etag值相符,服务器返回304,不带实体,否则返回200且带实体

引用动态绑定

引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数。注意只能调用虚函数。

Base为基类,Son为派生类

    Son s;
	Base& b = s; // 基类类型引用绑定已经存在的Son对象,引用必须初始化
	s.fun(); //son::fun()
	b.fun(); //son :: fun()
	return 0;

全局变量和局部变量区别

生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;

使用方式不同:通过声明后全局变量在程序的各个部分都可以用到;局部变量分配在堆栈区,只能在局部使用。

操作系统和编译器通过内存分配的位置可以区分两者,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面 

判断浮点数是否相等

对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。与浮点数的表示方式有关

变量的传递只能传递给权限更严格的变量

组合和继承相比的优缺点

继承

继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。

继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。

继承的缺点有以下几点:

①:父类的内部细节对子类是可见的。

②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。

③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

组合

组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

组合的优点:

①:当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。

②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。

③:当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。

组合的缺点:①:容易产生过多的对象。②:为了能组合多个对象,必须仔细对接口进行定义。

函数指针

函数指针的声明方法

int (*pf)(const int&, const int&); (1)

上面的pf就是一个函数指针,指向所有返回类型为int,并带有两个const int&参数的函数。注意*pf两边的括号是必须的,否则上面的定义就变成了:

int *pf(const int&, const int&); (2)

而这声明了一个函数pf,其返回类型为int *, 带有两个const int&参数。

为什么有函数指针

函数与数据项相似,函数也有地址。我们希望在同一个函数中通过使用相同的形参在不同的时间使用产生不同的效果。

一个函数名就是一个指针,它指向函数的代码。

一个函数地址是该函数的进入点,也就是调用函数的地址。函数的调用可以通过函数名,也可以通过指向函数的指针来调用。函数指针还允许将函数作为变元传递给其他函数;

两种方法赋值:

指针名 = 函数名; 指针名 = &函数名

普通成员变量在类中初始化;静态成员变量在类外初始化;

当程序中有函数重载时,函数的匹配原则和顺序是什么?

  1. 名字查找

  2. 确定候选函数

  3. 寻找最佳匹配

Cout和printf的区别

C++的iostream库和C中的stdio库中分别的cout/cin和printf/scanf相比优势

1、首先是类型处理更加安全,更加智能,我们无须应对int、float中的%d、%f

2、扩展性极强,对于新定义的类,printf想要输入输出一个自定义的类的成员是天方夜谭的,而iostream中使用的位运算符都是可重载的

3、cout将清空缓冲区的自由交给了用户(在printf中的输出是没有缓冲区的),而且流风格的写法也更加自然和简洁。

4、printf("%d",x);

在此其中,您实质上在调用一个名为printf的函数,其返回值为int类型。调用过程中,程序会产生控制台输出,并返回一个值。

cout<<x;

此时,其实是cout在与x进行运算,运算过程中会产生控制台输出,并返回ofstream类型(this指针,实现连续输出),这也是为什么cout后面可以使用多个<<。

如果您会定义运算符,那么您在定义operator <<的时候,会更直观地感受到。

总之,cout是变量,使用其输出时是在进行运算;而printf输出是调用函数。

行缓冲清空缓冲区

因为cout是行缓冲的,所以其实有以下几种方式(我们需要知道的是,下面任何会清空缓冲区的条件中都的确会导致输出,但是仅仅表明是在该条语句要清空缓冲区之间的某一时间点会导致输出,但是并没有说是具体什么时间点,具体时间点可能依据操作系统和具体编译环境而定):

   1、缓冲区满;

   2、用户手动刷新,即显示地清空,比如像上面的使用操纵符的方式;

   3、程序结束(这种情况非常常见),见下面例1代码;

   4、程序的下一步将要从标准输入流读入数据,则会将之前的缓冲区清空

为什么模板的声明都放在.h文件中

1、编译单元:一个编译单元(translation  unit)是指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件

2、编译a.cpp文件时,调用了b.cpp的函数(a函数),编译器不知道该函数的实现,需要连接器寻找a的实体。在编译b.cpp的时候,编译器找到了a函数的实现,所以a的实现(二进制代码)出现在b.obj中,连接时获取b.cpp的实现代码的地址,然后将a.cpp 调用f的地址填入

3、在分离式编译情况下,某个cpp文件不知道其他cpp文件的存在,且C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来,(template<…> 处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知,typename 实参不明,

模板是在需要的时候,才会去生成一个具体化的实例的,比如,你只要一个int型的实例,模板就只会给你生成一个int型的实例,模板本身是不会被执行的

)但是模板不被实例化的话整个工程的.obj中就找不到一行模板实例的二进制代码。所以模板应该放在.h文件中与cpp文件一同编译

volatile关键字

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。

如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)

如果有一个空类,它会默认添加哪些函数?

1)  Empty(); // 缺省构造函数//
2)  Empty( const Empty& ); // 拷贝构造函数//
3)  ~Empty(); // 析构函数//
4)  Empty& operator=( const Empty& ); // 赋值运算符//

什么情况使用指针什么情况使用引用

一般的原则: 对于使用引用的值而不做修改的函数:

如果数据对象很小,如内置数据类型或者小型结构,则按照传递;

如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针

如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间;

如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递)

对于修改函数中数据的函数:

如果数据是内置数据类型,则使用指针

如果数据对象是结构,则使用引用或者指针

如果数据是类对象,则使用引用

引用作为返回值

优点:内存中不产生被返回值的副本。

但是有以下的限制:

1)不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁

2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak

3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值