面试题库

裸辞,找工作花了一个多月,过程很受打击,曾经胸有成竹,却一次次备受打击,也给看到这篇文章的朋友一些些建议,决定辞职时不过是不是裸辞,先花点时间看看专业性的知识,刷刷算法题目,有意向的公司可以去leetcode查看该公司的笔试题。以下是我面试过程中有遇到的一些问题,仅此提供给各位朋友,祝愿大家能找到诚心如意的工作。

在任务执行期间捕捉到信号并对其进行处理时,进程正在执行的指令序列就被信号处理程序临时中断。如果从信号处理程序返回,则继续执行进程断点处的正常指令序列,从重新恢复到断点重新执行的过程中,函数所依赖的环境没有发生改变,就说这个函数是可重入的,反之就是不可重入的

满足下面条件之一的多数是不可重入函数:

(1)使用了静态数据结构;

(2)调用了malloc或free;

(3)调用了标准I/O函数;标准io库很多实现都以不可重入的方式使用全局数据结构。

(4)进行了浮点运算.许多的处理器/编译器中,浮点一般都是不可重入的 (浮点运算大多使用协处理器或者软件模拟来实现。

 

main函数并不是第一个执行的,ELF会有一个启动的入口函数

大致过程如下

1.操作系统创建好进程之后,把控制权交给程序的入口函数

2.入口函数对运行库和程序环境进行初始化,包括堆,I/O,线程,全局变量构造等

3.入口函数初始化之后,调用main函数,正式开始执行程序主体部分

4.mian函数执行完毕后,返回到入口函数,入口函数进行清理工作,包括全局变量析构,堆销毁,关闭I/O等,然后进行系统调用结束进程

 

gdb -q 启动时不显示提示信息

 

objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,它以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息。

eadelf命令,一般用于查看ELF格式的文件信息,常见的文件如在Linux上的可执行文件,动态库(*.so)或者静态库(*.a) 等包含ELF格式的文件。以下命令的使用是基于android编译出来的so文件上面去运行。

EIP(PC)即call的下一条指令的地址,指向下一条指令的地址

ESP堆栈(Stack)指针寄存器,指向堆栈顶部

EBP基址指针寄存器,指向当前堆栈底部

 

当进程被加载到内存时,会被分成很多段

代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写,如果发生写操作则会提示segmentation fault

数据段:保存初始化的全局变量和静态变量,可读可写不可执行

BSS:未初始化的全局变量和静态变量

堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行

栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,可读可写可执行

环境/参数段(environment/argumentssection):用来存储系统环境变量的一份复制文件,进程在运行时可能需要。例如,运行中的进程,可以通过环境变量来访问路径、shell 名称、主机名等信息。该节是可写的,因此在缓冲区溢出(buffer overflow)攻击中都可以使用该段

 

 

栈大小

Linux 10M ulimit -s查看并修改默认栈大小,ulimit命令用于控制程序的资源

Windows 1M vs的编译属性可以修改程序运行时进程的栈大小

 

产生死锁的必要条件:

互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。

请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。

不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。

环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

解决死锁的基本方法

预防死锁:

资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)

只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)

可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)

资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

 

pstack

可显示每个进程的栈跟踪,pstack $pid即可

 

http超文本传输协议

http请求包含

请求行:描述客户端的请求方式、请求资源的名称、http协议的版本号。 例如: GET/BOOK/JAVA.HTML HTTP/1.1

请求头:(消息头)包含(客户机请求的服务器主机名,客户机的环境信息等)

实体内容:

就是指浏览器端通过http协议发送给服务器的实体数据。

If-Modified-Since:在发送HTTP请求时,把浏览器端缓存页面的最后修改时间一起发到服务器去,服务器会把这个时间与服务器上实际文件的最后修改时间进行比较。

如果时间一致,那么返回HTTP状态码304(不返回文件内容),客户端接到之后,就直接把本地缓存文件显示到浏览器中。

如果时间不一致,就返回HTTP状态码200和新的文件内容,客户端接到之后,会丢弃旧文件,把新文件缓存起来,并显示到浏览器中。

expires:服务端配置,是该资源过期的日期,浏览器会根据该过期日期与客户端时间对比,如果过期时间还没到,则会去缓存中读取该资源,如果已经到期了,则浏览器判断为该资源已经不新鲜要重新从服务端获取

If-Unmodified-Since:如果从某个时间点算起, 文件没有被修改,没有被修改: 则开始`继续'传送文件: 服务器返回: 200 OK,文件被修改: 则不传输, 服务器返回: 412 Precondition failed (预处理错误),可用于断点续传

If-Match:表示这是一个条件请求。在请求方法为 GET 和 HEAD 的情况下,服务器仅在请求的资源满足此首部列出的 ETag值时才会返回资源

If-None-Match 是一个条件式请求首部。对于 GETGET 和 HEAD 请求方法来说,当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端会才返回所请求的资源,响应码为  200  

 

HTTP1.0和HTTP1.1的一些区别

1.缓存处理,在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。

2.带宽优化及网络连接的使用,HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。

3.错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。

4.Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。

5.长连接,HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点

 

nm --列出目标文件(中间文件.o和可执行文件)的符号清单

 

int i = (int)(((int *)0) + 4);

i为16

 

socket

混杂模式选项SOCKET_RAW

端口重用SO_REUSEADDR

端口复用SO_EXECLUSIVEADDRUSE,多个应用复用端口,只有最后一个绑定的socket可以接受数据,所有socket都可以发送数据

tcp_nodelay禁止nagle算法,有需要发送的就立即发送

tcp_cork:它是一种加强的nagle算法,过程和nagle算法类似,都是累计数据然后发送。但它没有 nagle中1的限制,所以,在设置cork后,即使所有ack都已经收到,但我还是不想发送数据,我还想继续等待应用层更多的数据,所以它的效果比nagle更好

 

保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包。而面向流则是指无保护消息保护边界的,如果发送端连续发送数据,接收端有可能在一次接收动作中,会接收两个或者更多的数据包。

 

如果一个类中有虚函数,那么该类就有一个虚函数表。

这个虚函数表是属于类的,所有该类的实例化对象中都会有一个虚函数表指针去指向该类的虚函数表。编译的时候确定了虚函数表。

虚函数表末尾值,在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。在WinXP+VS2003下,这个值是NULL。虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面。父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。通过虚函数表指针,父类指针即可调用该虚函数表中所有的虚函数。

 

 

空类,编译器不会生成任何的成员函数,只会生成1个字节的占位符

有时可能会以为编译器会为空类生成默认构造函数等,事实上是不会的,编译器只会在需要的时候生成6个成员函数:一个缺省的构造函数、一个拷贝构造函数、一个析构函数、一个赋值运算符、一对取址运算符和一个this指针

 

-fpermissive

将类型不一致代码的诊断从错误降级为警告。该选项最好不要使用,因为会降低对于代码检查的严格性。只能用于c++,对c是无效的

 

valgrind 

 -tool= 最常用的选项。运行 valgrind中名为toolname的工具。默认memcheck。

memcheck  重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。Memcheck将内存泄露分为两种,一种是可能的内存泄露(Possibly lost),另外一种是确定的内存泄露(Definitely lost)。能检测到的错误:使用未初始化的内存、使用已经释放了的内存、使用超过 malloc分配的内存空间、对堆栈的非法访问、申请的空间是否有释放、malloc/free/new/delete申请和释放内存的匹配、src和dst的重叠。

callgrind 检测程序代码的运行时间和调用过程,检查程序中函数调用过程中出现的问题,不需要在编译源代码时附加特殊选项,但加上调试选项是推荐的。收集程序运行时的一些数据,建立函数调用关系图。

cachegrind 检查程序中缓存使用出现的问题,模拟CPU中的一级缓存I1,Dl和二级缓存,能够精确地指出程序中cache的丢失和命中。如果需要,它还能够为我们提供cache丢失次数,内存引用次数,以及每行代码,每个函数,每个模块,整个程序产生的指令数。这对优化程序有很大的帮助。

helgrind 检查多线程程序中出现的竞争问题,寻找内存中被多个线程访问,而又没有一贯加锁的区域,这些区域往往是线程之间失去同步的地方,而且会导致难以发掘的错误。

massif 检查程序中堆栈使用中出现的问题,测量程序在堆栈中使用了多少内存,告诉我们堆块,堆管理块和栈的大小。Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速我们程序的运行,减少程序停留在交换区中的几率。

extension 利用core提供的功能,自己编写特定的内存调试工具

Memcheck 能够检测出内存问题,关键在于其建立了两个全局表

  1. Valid-Value 表:

对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。

  1. Valid-Address 表

对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。

检测原理:

  • 当要读写内存中某个字节时,首先检查这个字节对应的 A bit。如果该A bit显示该位置是无效位置,memcheck 则报告读写错误。
  • 内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节对应的 V bit 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 memcheck 会检查对应的V bits,如果该值尚未初始化,则会报告使用未初始化内存错误。

 

infer

进行 C 文件分析时,Infer 使用 gcc 命令并在内部运行Clang来解析,Clang是一个C语言、C++、Objective-C语言的轻量级编译器

Infer 运行的两个阶段

1. 捕获阶段

infer 捕获编译命令,将文件翻译成 Infer 内部的中间语言。这种翻译和编译类似,Infer 从编译过程获取信息,并进行翻译。这就是我们调用 Infer 时带上一个编译命令的原因了,比如: infer -- clang -c file.c, infer -- javac File.java。结果就是文件照常编译,同时被 Infer 翻译成中间语言,留作第二阶段处理。特别注意的就是,如果没有文件被编译,那么也没有任何文件会被分析。

2. 分析阶段

在分析阶段,Infer 分析 infer-out/ 下的所有文件。分析时,会单独分析每个方法和函数。

在分析一个函数的时候,如果发现错误,将会停止分析,但这不影响其他函数的继续分析。

所以你在检查问题的时候,修复输出的错误之后,需要继续运行 Infer 进行检查,知道确认所有问题都已经修复。

错误除了会显示在标准输出之外,还会输出到文件 infer-out/bug.txt 中,我们过滤这些问题,仅显示最有可能存在的。

在结果文件夹中(infer-out),同时还有一个 csv 文件 report.csv,这里包含了所有 Infer 产生的信息,包括:错误,警告和信息

增量模式和非增量模式

运行时,Infer 默认会删除之前产生的 infer-out/ 文件夹,这会导致非增量模式。

如果需要增量模式,加入 --incremental(或者 -i)参数运行,这样 infer-out/ 文件夹将不会被删除。

也有例外的情况,尤其是你只能在上面的一个阶段使用这个参数。

比如, infer -- javac Hello.java 相当于运行了以下两个命令。

infer -a capture -- javac Hello.java infer -- analyze

注意,第二个命令不会删除 infer-out/,因为分析阶段,需要使用文件夹中的进行分析。

你可以通过 infer --help 了解更多关于 Infer 的各种操作模式。

下面我们简单明了地强调一下,什么情况下需要使用增量模式,什么时候使用非增量模式。

非增量模式

非增量模式适用于单编译命令,重复运行 Infer 检测的情况。

infer -- javac Hello.java edit Hello.java # 编译 Hello.java,改动了代码,比如修复了一些问题 infer -- javac Hello.java

如果需要进行全新的一轮的分析,必须:

  1. 删除结果文件夹:

rm -fr infer-out

  1. 删除构建产物。比如,对于基于 make 的项目,运行 make clean。

增量模式

许多软件项目都使用增量编译系统,比如手机应用。Infer 支持好些这样的编译系统,具体的看这个章节.

如果想使用 Infer 进行增量分析,你的编译系统需要是这其中的一个。

运行 Infer 进行检测的时候,只需要简单运行 infer -- <编译命令>,其中编译命令就是我们平时编译的命令。需要注意的是,运行前的项目是清理过的,这样 Infer 才能在捕获阶段捕获所有的编译命令。

比如,一个 gradle 项目:

gradle clean infer -- gradle build

接下来,如果你修改了项目中的一些文件,你可以重复上面的命令,清理并重新分析整个项目。或者通过参数,让 Infer 使用使用增量模式。

edit some/File.java # 修改了 some/File.java 的一些内容。 infer --incremental -- gradle build

当然,你也可以在第一次运行 Infer 进行检测的时候,就使用 --incremental。

检测到的bug:资源泄漏、

infer是基于分离逻辑(Separation logic)和Bi-abduction 来实现的

分离逻辑是霍尔逻辑的一种扩展.霍尔逻辑是广泛应用的程序验证逻辑系统,用于对命令式语言程序 进行推理验证.

基本思想是:在代码段及其调用者之间构建一种合同似的规格说明,由一个前置条件和一个后 置条件构成.前置条件是一个断言,描述这个代码段执行前程序状态必须满足的条件;后置条件也是一个断言, 描述在代码段正确运行后程序状态所需要满足的条件,调用者可以确信在代码段执行结束后这个状态条件 为真。

在分离逻辑中,前置条件和后置条件中的程序状态主要由栈S和堆H构成,栈是变量到值的映射,而堆是有限的地址集合到值的映射.在程序验证时,可以将栈看作对寄存器内容的描述,而堆是对可寻址内存内容的描述。

程序C从满足前置条件P的任何初始状态开始执行, 并且如果该执行终止, 则执行结束时的程序状态满足后置条件Q.部分正确性规范并没有对程序的终止性做任何要求, 所以如果从满足前置条件P的初始状态开始, 程序C的执行不终止, 规范{P} C {Q}也是满足的.

 

C++new失败处理

new 分配内存失败,默认是抛出异常的。

所以,如果分配成功,p == 0 就绝对不会成立;而如果分配失败了,也不会执行 if ( p == 0 ),因为分配失败时,new 就会抛出异常跳过后面的代码。如果你想检查 new 是否成功,应该捕捉异常:

        try {

            int* p = new int[SIZE];

            // 其它代码

        } catch ( const bad_alloc& e ) {

            return -1;

        }

C++ 亦提供了一个方法来抑制 new 抛出异常,而返回空指针:

        int* p = new (std::nothrow) int; // 这样如果 new 失败了,就不会抛出异常,而是返回空指针

        if ( p == 0 ) // 如此这般,这个判断就有意义了

            return -1;

        // 其它代码

 

统计当前目录下所有文件的总行数

find . "(" -name "*" ")" -print | xargs wc -l

find 查找命令

xargs 用于管道,使前面的输出作为后面的输入

wc命令用来统计,-l指统计行数

 

条件变量是互斥锁的补充

thread1/2: while(1) { pthread_mutex_lock(&mutex); iCount++; pthread_mutex_unlock(&mutex); if (iCount >= 100) { pthread_cond_signal(&cond); } } thread4: while (1) { pthread_mutex_lock(&mutex); while(iCount < 100) { pthread_cond_wait(&cond, &mutex); } printf("iCount >= 100\r\n"); iCount = 0; pthread_mutex_unlock(&mutex); }

这样就可以避免线程4一直循环判断iCount是否大于100了。

一个线程等待”条件变量的条件成立”而挂起;另一个线程使“条件成立”。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。线程在改变条件状态前必须首先锁住互斥量,函数pthread_cond_wait把自己放到等待条件的线程列表上,然后对互斥锁解锁(这两个操作是原子操作)。在函数返回时,互斥量再次被锁住

 

TCP连接建立时的初始序号为什么不使用0

如果不是随机产生初始序列号,那么就会以很容易的方式获取到通信双方的初始化序列号,进而伪造序列号进行攻击

 

thread_local

是C++ 11新引入的一种存储类型。它会影响变量的存储周期

有thread_local关键字修饰的变量具有线程周期,在线程开始的时候被生成(allocated),在线程结束的时候被销毁(deallocated)。并且每 一个线程都拥有一个独立的变量实例。各线程的thread_local变量的初始值是该变量最开先的初始值。

哪些变量可以被声明为thread_local

1.命名空间下的全局变量

2.类的static成员变量

3.本地变量

 

TCP面向字节流的传输,因此它能将信息分割成组,并在接收端将其重组;UDP是面向数据报的传输,没有分组开销。

TCP的标准规定,SYN置1的报文段要消耗掉一个序号。

PV操作(信号量)

P操作申请资源:

(1)S减1;

(2)若S减1后仍大于等于零,则进程继续执行;

(3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。

V操作 释放资源:

(1)S加1;

(2)若相加结果大于零,则进程继续执行;

(3)若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。

 

gdb调试正在运行的程序

1. 编译时候带-g选项。

2. 运行程序。

3. ps找到进程号。

4. 启动gdb,使用attach选项,这时gdb会停止在程序的某处。gdb attach pid,pid是正在运行的进程的id

5. 按照GDB调试方法调试。当程序退出之后,依然可以使用run命令重启程序。

 

vmstat查看系统级别的负载情况,包括进程、内存、IO、CPU、系统调用

 

定位占用cpu过高的线程

首先根据top命令,发现占用cpu最高的进程PID

ps显示当前进程下的所有线程列表,找到耗时最高的线程ID

打印线程的堆栈信息,thread dump

jstack pid |grep tid -A 30

 

在top命令中,已经获取到了占用cpu资源较高的线程pid,将该pid转成16进制的值,在thread dump中每个线程都有一个nid,找到对应的nid即可;隔段时间再执行一次stack命令获取thread dump,区分两份dump是否有差别,在nid=0x246c的线程调用栈中,发现该线程一直在执行JstackCase类第33行的calculate方法,得到这个信息,就可以检查对应的代码是否有问题

 

strace命令

strace是一个可用于诊断、调试和教学的Linux用户空间跟踪器。我们用它来监控用户空间进程和内核的交互,比如系统调用、信号传递、进程状态变更等。strace -p pid是正在运行的进程的id

 

多继承

1. 多重继承得到的对象可能拥有“不同的地址!”(无解决方案)

2. 多重继承可能产生冗余的成员(虚继承解决,但不实用)

3. 多重继承可能产生多个虚函数表(C++强制类型转换解决)

虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题,通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。

虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

 

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

变量都有静态类型和动态类型,这两个类型可以一样,也可以不一样

class B

{

void DoSomething();

virtual void vfun();

}

class C : public B

{

void DoSomething();//首先说明一下,这个子类重新定义了父类的no-virtual函数,这是一个不好的设计,会导致名称遮掩;这里只是为了说明动态绑定和静态绑定才这样使用。

virtual void vfun();

}

class D : public B

{

void DoSomething();

virtual void vfun();

}

D* pD = new D();

B* pB = pD;

pD静态类型是D,在编译的时候就确认了,动态类型也是D,在运行时确认。

pB静态类型是B,在编译的时候就确认了,动态类型是D,在运行时确认。

让我们看一下,pD->DoSomething()和pB->DoSomething()调用的是同一个函数吗?

不是的,虽然pD和pB都指向同一个对象。因为函数DoSomething是一个no-virtual函数,它是静态绑定的,也就是编译器会在编译期根据对象的静态类型来选择函数。pD的静态类型是D*,那么编译器在处理pD->DoSomething()的时候会将它指向D::DoSomething()。同理,pB的静态类型是B*,那pB->DoSomething()调用的就是B::DoSomething()。

让我们再来看一下,pD->vfun()和pB->vfun()调用的是同一个函数吗?

是的。因为vfun是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型,pB和pD虽然静态类型不同,但是他们同时指向一个对象,他们的动态类型是相同的,都是D*,所以,他们的调用的是同一个函数:D::vfun()。

当缺省参数和虚函数一起出现的时候情况有点复杂,极易出错。

class B

{

virtual void vfun(int i = 10);

}

class D : public B

{

virtual void vfun(int i = 20);

}

D* pD = new D();

B* pB = pD;

pD->vfun();

pB->vfun();

有上面的分析可知pD->vfun()和pB->vfun()调用都是函数D::vfun(),但是他们的缺省参数是多少?

分析一下,缺省参数是静态绑定的,pD->vfun()时,pD的静态类型是D*,所以它的缺省参数应该是20;同理,pB->vfun()的缺省参数应该是10。编写代码验证了一下,正确。

 

RTTI是”Runtime Type Information”的缩写,意思是运行时类型信息,它提供了运行时确定对象类型的方法.

当类中不存在虚函数时,typeid是编译时期的事情,也就是静态类型

 

C++中哪些函数不能被声明为虚函数

(1)虚函数用于基类和派生类,普通函数所以不能

(2)构造函数不能是因为虚函数采用的是虚调用的方法,

(3)内联成员函数的实质是在调用的地方直接将代码扩展开

(4)继承时,静态成员函数不能被继承的,它只属于一个类,因为也不存在动态联编

(5)友元函数不是类的成员函数,因此也不能被继承

 

redis字符串命令

SET key value

GET key

 

 

重载:多个同名函数同时存在,具有不同的参数个数/类型,返回值类型可以相同可

以不同,调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法

 

隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏

 

重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

 

vim 代码自动格式调整

1,gg 跳转到第一行

2,shift+v 转到可视模式

3,shift+g 全选

4,按下神奇的 =

 

gdb常用命令

用start命令开始执行程序

单步执行(n)

breakpoint,continue和display

info命令可以查看已经设置的断点

delete可以用编号指定删除某个断点

info threads显示的是当前可调试的所有线程,GDB会给每一个线程都分配一个ID。前面有*的线程是当前正在调试的线程

thread ID切换到当前调试的线程为指定为ID的线程。

thread apply all command: 让所有被调试的线程都执行command命令 

thread apply ID1 ID2 … command:这条命令是让线程编号是ID1,ID2…等等的线程都执行command命令 

set scheduler-locking off|on|step:在使用step或continue命令调试当前被调试线程的时候,其他线程也是同时执行的,如果我们只想要被调试的线程执行,而其他线程停止等待,那就要锁定要调试的线程,只让它运行。 

  off:不锁定任何线程,所有线程都执行。 

  on:只有当前被调试的线程会执行。 

  step:阻止其他线程在当前线程单步调试的时候抢占当前线程。只有当next、continue、util以及finish的时候,其他线程才会获得重新运行的。 

show scheduler-locking: 这条命令是为了查看当前锁定线程的模式。 

gdb)help:查看命令帮助,具体命令查询在gdb中输入help + 命令,简写h

(gdb)run:重新开始运行文件(run-text:加载文本文件,run-bin:加载二进制文件),简写r

(gdb)start:单步执行,运行程序,停在第一执行语句

(gdb)list:查看原代码(list-n,从第n行开始查看代码。list+ 函数名:查看具体函数),简写l

(gdb)set:设置变量的值

(gdb)next:单步调试(逐过程,函数直接执行),简写n

(gdb)step:单步调试(逐语句:跳入自定义函数内部执行),简写s

(gdb)backtrace:查看函数的调用的栈帧和层级关系,简写bt

(gdb)frame:切换函数的栈帧,简写f

(gdb)info:查看函数内部局部变量的数值,简写i

(gdb)finish:结束当前函数,返回到函数调用点

(gdb)continue:继续运行,简写c

(gdb)print:打印值及地址,简写p

(gdb)quit:退出gdb,简写q

 

-fstack-protector-all

栈溢出保护选项,检测到缓冲区溢出时(例如,缓冲区溢出攻击)时会立即终止正在执行的程序,并提示其检测到缓冲区存在的溢出的问题。启用该选项后编译器会产生额外的代码来检测缓冲区溢出,例如栈溢出攻击。这是通过在有缺陷的函数中添加一个保护变量来实现的。这包括会调用到alloca的函数,以及具有超过8个字节缓冲区的函数。当执行到这样的函数时,保护变量会得到初始化,而函数退出时会检测保护变量。如果检测失败,会输出一个错误信息并退出程序。

-Wl,-z,relro,-z,now

即Full RELRO全局数据段保护,relro 是一种用于加强对 binary 数据段的保护的技术

-Wl,-z,relro

partial relro ,局部数据段保护

由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读.

 

typeid(*obj).name() 可以获得当前 obj 指针指向的实际的对象类型

关于 dynamic_cast 的注意事项:

  • 只能应用与指针和引用的转换,即只能转化为某一个类型的指针或某一个类型的引用,而不能是某类型本身,dynamic_cast转换后的指针可以访问派生类的非虚成员,如果不加dynamic_cast而直接转换,只能访问派生类的虚函数
  • 要转化的类型中必须包含虚函数,如果没有虚函数,转换就会失败,因为转换时会访问虚函数表,这样就可以知道这个派生类是否继承基类
  • 如果转换成功,返回子类的地址,如果转换失败,返回NULL

关于 typeid 的注意事项:

  • typeid 返回一个 type_info 的对象引用
  • 如果想通过基类的指针指向派生类的数据类型,基类就必须要带有虚函数,否则,在使用typeid 时,就只能返回定义时所使用的数据类型
  • typeid 只能获取对象的实际类型,即便这个类含有虚函数,也只能判断当前对象是基类还是派生类,而不能判断当前指针是基类还是派生类

 

基于TCP的应用层协议有:SMTP(简单邮件传输协议)、TELNET、HTTP、FTP

基于UDP的应用层协议:DNS、TFTP(简单文件传输协议)、RIP(路由选择协议)、DHCP(动态主机配置协议)、BOOTP(是DHCP的前身)、SNMP(简单网络管理协议)

 

TCP为每条连接建立七个定时器:

1、  连接建立定时器在发送SYN报文段建立一条新连接时启动。如果没有在75秒内收到响 应,连接建立将中止。

当TCP实例将其状态从LISTEN更改为SYN_RECV的时侯就会使用这一计时器.服务端的TCP实例最初会等待一个ACK三秒钟.如果在这一段时间没有ACK到达,则认为该连接请求是过期的.

2、  重传定时器在TCP发送数据时设定.如果定时器已超时而对端的确认还未到达,TCP将重传数据.重传定时器的值(即TCP等待对端确认的时间)是动态计算的,取决于TCP为该 连接测量的往返时间和该报文段已重传几次.

3、  延迟ACK定时器在TCP收到必须被确认但无需马上发出确认的数据时设定.TCP等 待时间200MS后发送确认响应.如果,在这200MS内,有数据要在该连接上发送,延迟的ACK响应就可随着数据一起发送回对端,称为稍带确认.

4、  持续定时器在连接对端通告接收窗口为0,阻止TCP继续发送数据时设定.由于连接对端发送的窗口通告不可靠,允许TCP继续发送数据的后续窗口更新有可能丢失.因此,如果TCP有数据要发送,但对端通告接收窗口为0,则持续定时器启动,超时后向对端发送1字节的数据,判定对端接收窗口是否已打开.与重传定时器类似,持续定时器的值也是动态计算的,取决于连接的往返时间,在5秒到60秒之间取值.

5、  保活定时器在应用进程选取了插口的SO_KEEPALIVE选项时生效.如果连接的连续空闲时间超过2小时,保活定时器超时,向对端发送连接探测报文段,强迫对端响应.如果收到了期待的响应,TCP确定对端主机工作正常,在该连接再次空闲超过2小时之前,TCP不会再进行保活测试,.如果收到的是其它响应,TCP确定对端主要已重启.如果连纽若干次保活测试都未收到响应,TCP就假定对端主机已崩溃,尽管它无法区分是主机帮障还是连接故障.

6、  FIN_WAIT-2定时器,当某个连接从FIN_WAIT-1状态变迁到FIN_WAIN_2状态,并且不能再接收任何数据时,FIN_WAIT_2定时器启动,设为10分钟,定时器超时后,重新设为75秒,第二次超时后连接被关闭,加入这个定时器的目的为了避免如果对端一直不发送FIN,某个连接会永远滞留在FIN_WAIT_2状态.

7、  TIME_WAIT定时器,一般也称为2MSL定时器.2MS指两倍MSL.当连接转移到TIME_WAIT状态,即连接主动关闭时,定时器启动.连接进入TIME_WAIT状态时,定时器设定为1分钟,超时后,TCP控制块和INTERNET PCB被删除,端口号可重新使用.

TCP包含两个定时器函数:一个函数每200MS调用一次(快速定时器);另一个函数每500MS调用一次.延迟定时器与其它6个定时器有所不同;如果某个连接上设定了延迟ACK定时器,那么下一次200MS定时器超时后,延迟的ACK必须被发送.其它的定时器每500MS递减一次,计数器减为0时,就触发相应的动作.

 

右值引用

使用&&,如 int &&a=1;相当于给不具名的1取了一个别名,但是int &a=1是错误的。右值引用解决两个问题:第一个问题就是临时对象非必要的昂贵的拷贝操作,第二个问题是在模板函数中如何按照参数的实际类型进行转发

T是类型

左值引用, 使用 T&, 只能绑定左值

右值引用, 使用 T&&, 只能绑定右值

常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值

已命名的右值引用,编译器会认为是个左值

编译器有返回值优化,但不要过于依赖

 

移动语义

完美转换

universal references、引用折叠、move语义

 

纯右值

非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等都是纯右值

将亡值

将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值

std::move

std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。

 

auto能够让你声明一个变量。而decltype则能够从一个变量或表达式中得到类型,都是编译期确定类型。

nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,由于NULL实际上代表的是0

简化的for循环,能够用于遍历数组、容器、string以及由begin和end函数定义的序列(即有Iterator),for (auto p : m)

lambda表达式,能够用于创建并定义匿名的函数对象

 

智能指针

std::auto_ptr,不支持复制(拷贝构造函数)和赋值(operator =),但复制或赋值的时候不会提示出错。所以可能会造成程序崩溃

auto_ptr p1(new string ("auto") ; //#1 auto_ptr p2; //#2 p2 = p1; //#3

在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是好事,可防止p1和p2的析构函数试图刪同—个对象; 但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。如果再访问p1指向的内容则会导致程序崩溃。

unique_ptr,也不支持复制和赋值,但比auto_ptr好,直接赋值会编译出错。实在想赋值的话,需要使用:std::move

shared_ptr,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。

weak_ptr,弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针

 

不可重载的操作符

作用域操作符:::

条件操作符:?:

点操作符:.

指向成员操作的指针操作符:->*,.*

预处理符号:#

sizeof

 

结构体为什么要内存对齐呢?

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

2.硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。

 

static无论是全局变量还是局部变量都存储在全局/静态区域,在编译期就为其分配内存,在程序结束时释放

const全局变量存储在只读数据段,编译期最初将其保存在符号表中,第一次使用时为其分配内存,在程序结束时释放

const局部变量存储在栈中,代码块结束时释放

 

既然有了malloc/free,C++中为什么还需要new/delete呢?

运算符是语言自身的特性,有固定的语义,编译器知道意味着什么,由编译器解释语义,生成相应的代码。

库函数是依赖于库的,一定程度上独立于语言的。编译器不关心库函数的作用,只保证编译,调用函数参数和返回值符合语法,生成call函数的代码。

malloc/free是库函数,new/delete是C++运算符。对于非内部数据类型而言,光用malloc/free无法满足动态对象都要求。new/delete是运算符,编译器保证调用构造和析构函数对对象进行初始化/析构。但是库函数malloc/free是库函数,不会执行构造/析构。

 

多态:不同对象接收相同的消息产生不同的动作。多态包括 编译时多态和 运行时多态

运行时多态是:通过继承和虚函数来体现的。

编译时多态:运算符重载上。

 

const

const修饰类的成员变量,表示常量不可能被修改

const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数

const函数只能调用const函数,非const函数可以调用const函数

 

static

函数体内: static 修饰的局部变量作用范围为该函数体,不同于auto变量,其内存只被分配一次,因此其值在下次调用的时候维持了上次的值

模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内

类中:修饰成员变量,表示该变量属于整个类所有,对类的所有对象只有一份拷贝

类中:修饰成员函数,表示该函数属于整个类所有,不接受this指针,只能访问类中的static成员变量

注意和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象

 

STL中unordered_map和map的区别

map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是O(logN)。

unordered_map和map类似,都是存储key-value对,可以通过key快速索引到value,不同的是unordered_map不会根据key进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,即unoredered_map内部是无序的。

 

构造函数不能声明为虚函数

因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等

虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了

 

voletile修饰,则会从内存中重新装载内容,而不是直接从寄存器中拷贝内容。

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