基本语言面经

static关键字的作用

  1. 全局静态变量,自动初始化为0。
  2. 局部静态变量,自动初始化为0。
  3. 静态函数,这个函数只可在本cpp内使用。
  4. 类的静态成员,实现多个对象之间数据的共享(智能指针的引用计数)。
  5. 类的静态函数,多个对象之间函数共享,引用的时候不需要带对象,<类名>::<静态成员函数名>(<参数表>)

C/C++的区别

思想上:C++是面向对象的语言,C是面向过程的结构化编程语言。

语法上:

1.C++具有封装、继承和多态三种特性;

2.C++相比于C,增加了许多类型安全的功能,比如强制类型转换;

3.C++支持范式编程,比如模板类、函数模板等。

C++中四种cast转换

  1. const_cast:讲const变量转为非const。
  2. static_cast:用于各种隐式转换,比如非const转const,void*转指针等。
  3. dynamic_cast:用于动态类型转换。只能用于含有虚函数的类。
  4. reinterpret_cast:几乎什么都可以转,比如int转指针。

指针和引用的区别

  1. 指针有一块自己的空间,引用只是一个别名;
  2. 使用sizeof看指针的大小是4,而引用是被引用对象的大小;
  3. 指针可以初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
  4. 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
  5. 可以由const指针,但是没有const引用;
  6. 指针在使用中可以指向其他对象,但是引用只能死一个对象的引用,不能被修改;
  7. 指针可以有多级指针(**p),但是引用只有一级;
  8. 指针和引用使用++运算符的意义不一样;
  9. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

C++中的智能指针

四个智能指针:

1
2
3
4
auto_ptr	//已经被C++11弃用	
shared_ptr
weak_ptr
unique_ptr

1.auto_ptr

采用所有权模式

1
2
3
auto_ptr<string>p1(new string("hello world"));
auto_ptr<string>p2;
p2=p1;

这段代码不会报错,但是p2剥夺了p1的所有权,所以放访问p1时会报错。所以他的缺点是存在潜在的内存崩溃问题。

2.unique_ptr(替换auto_ptr)

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有

一个智能指针可以指向该对象。主要还是避免资源泄露。

1
2
3
unique_ptr<string>p1(new string("hello world"));
unique_ptr<string>p2;
p2=p1;

这个时候会报错,编译器认为p2=p1是非法的,避免了跑不再指向有效数据的问题。所以他比aotu_ptr安全。

而且当程序试图讲一个unique_ptr赋值给另一个时,如果源unique_ptr是个临时右值,编译器允许这样做,如果源unique_ptr将存在一段时间,编译器将禁止这么做。

3.shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其他相关资源会在最后一个引用被销毁的时候释放。他有个计数机制表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。

4.weak_ptr

weak_ptr是一种不控制对象生命周期的智能指针,他指向一个shared_ptr管理的对象,进行该对象的内存管理的是哪个强引用的shared_ptr。weak_ptr是用来解决shared_ptr相互引用时造成的死锁问题,如果两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能降为0,资源永远不可能释放。它是对对象的弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

说一说C++中析构函数的作用

析构函数和构造函数相互对应,当对象结束其生命周期,系统会自动执行析构函数。

析构函数名和类名相同,只是在函数名前加一个位取反符~,以区别于构造函数。他不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。

如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),他不进行任何操作。所以许多简单的类中没有用显示的析构函数。

如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免了内存泄露。

类析构的顺序:

1)派生类本身的析构函数

2)对象成员析构函数

3)基类析构函数

析构函数为什么必须是虚函数?为什么C++默认的析构函数不是虚函数?

将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄露。

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当做父类的时候,设置为虚函数。

说一说静态函数和虚函数的区别

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。

说一说你理解的虚函数和多态

多态的实现主要分为静态多态和动态多态。

静态多态主要是重载,在编译期就已经确定。

动态多态使用虚函数机制实现的,在运行时期动态绑定。

举个例子:一个父类的类型的指针指向一个子类对象的时候,使用父类的指针去调用子类中重写了父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写的时候不需要加virtual也是虚函数。

虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数的时候,会将其继承到的虚函数表中的地址替换成重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

说一说函数指针

1.定义

函数指针是指向函数的指针变量。

函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。

C在编译的时候,每个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他变量一样。

2.用途

调用函数和做函数的参数,比如回调函数。

说一说fork函数

创建一个和当前进程映像一样的进程可以通过fork()系统调用:

1
2
3
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);

成功调用fork()会创建一个新的进程,他几乎与调用fork()的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork()会返回0。在父进程中fork()返回子进程的pid。如果出现错误,fork()会返回一个负值。

最常见的fork()用法是创建一个新的进程,然后使用exec载入二进制映像,替换当前进程的映像。这种情况下,派生了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。

早起的Unix系统创建进程时,会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到进程。这种方式是十分耗时的。现在的Linux采用写时拷贝的方法,而不是对父进程空间进程整体复制。

说一说重载、重写和覆盖

重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在统一作用域中。

覆盖:子类继承父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写。

重写:

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

说一说strcpy和strlen

strcpy是字符串拷贝函数,原型:

1
char *strcpy(char *dest,const char *src);

从src逐字节拷贝到dest,直到遇到’\0’结束,因为没有固定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是strncpy。

strlen函数是计算字符串长度的函数,返回从开始到’\0’之间的字符个数。

写个函数在main函数执行前先运行

1
2
3
4
__attribute((constructor))void before()
{
cout<<"hello world"<<endl;
}

const修饰成员函数的目的是什么?

const修饰的成员函数表明函数调用不会对对象做出任何更改。事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。

const用法的区别

1
2
3
4
5
6
7
8
const char *a="123";
//字符串123保存在常量区,const本来是修饰a指向的值不能通过a区修改,但是字符串123在常量区,本来就不能改变,所以加不加const效果都一样。
char *b="123";
//字符串123保存在常量区,这个和a指针指向的是同一个位置,同样不能通过b区修改123的值
const char c[]="123";
//这里的123本来是在栈上的,但是编译器会做某些优化,将其放在常量区
char d[]="123";
//保存在栈区,可以通过d来修改

如果同时定义了两个函数,一个带const,一个不带,会有问题吗?

不会,相当于函数的重载。

说一说C++里是怎么定义常量的?常量存放内存的那个位置?

常量在C++里的定义就是一个top-level const加上对象类型。常量定义必须初始化。

对于局部常量,存放在栈区;对于全局常量,编译器一般不分配内存,放在符号表中以提高访问效率;字面值常量比如字符串存放在常量区。

说一说隐式类型转换

首先,对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换。

其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。

说一说extern “C”

C++调用C函数需要extern “C”,因为C语言没有函数重载。

加上extern “C”后,会指示编译器这部分代码按C语言的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

new/delete和malloc/free的区别

new/delete是C++关键字,而malloc/free是C语言的库函数,后者使用必须指明申请申请空间的大小,对于类的类型的对象后者不会调用构造函数和析构函数。

new操作符内存分配成功时,返回的是对象类型的指针,类型严格匹配对象,无需进行类型转换,所以new是符合类型安全性的操作符。而malloc内存分配成功则是返回void,需要通过强制类型转换将void*指针转换成我们需要的类型。

说一说C语言是怎么进行函数调用的?

每一个函数调用都会分配函数栈,在栈内进行函数执行过程,调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。

C语言参数是从右到左进行压栈的。

C++如何处理返回值

生成一个临时变量,把他的引用作为函数参数传入函数内。

C++中拷贝赋值函数的形参能否进行值传递?

不能,如果是这种情况下,调用拷贝函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。如此循环,无法完成拷贝,栈也会满。

说一说select

select在使用前,先将需要监测的描述符对应的bit位置1,然后将其传select,当有任何一个事件发生时,select将会返回所有的描述符,需要在应用程序自己遍历去检查那个描述符上有事件发生,效率很低,并且其不断在内核态和用户态进行描述符的拷贝,开销很大。

说一说fork,wait,exec函数

父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用了写时拷贝机制分配内存,exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork从父进程返回子进程的pid,从子进程返回0。调用了wait的父进程会发生阻塞,知道有子进程状态改变,执行成功返回0,错误返回-1.exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1。

类和结构体的区别

结构体是一种值类型,而类是引用类型。区别在于复制方式,值类型的数据是值复制,引用类型的数据是引用复制。

结构体使用栈存储,而类使用堆存储。

栈的空间相对较小,但是存储在栈中的数据访问效率相对较高。

堆的空间相对较大,但是存储在堆中的数据访问效率相对较低。

结构体使用完之后就自动解除内存分配,类实例有垃圾回收机制来保证内存的回收处理。

如何选择结构体还是类

  1. 堆栈的空间有限,对于大量的逻辑对象,创建类比创建结构体好一些。
  2. 结构体表示如点,矩阵,颜色这样的轻量级对象。例如,如果声明一个含有1000个点对象的数组,则将为引用每个对象分配附加的内存。在此情况下,结构体的成本较低,
  3. 在表现抽象和多级别的对象层次时,类是最好的选择,因为结构体不支持继承
  4. 大多数情况下该类型只是一些数据时,结构体是最佳的选择

堆和栈的区别

1)首先给出 x86 32 位 linux 系统进程地址空间上堆和栈的描述,先 是.text,.data,.bss,堆,栈,命令行参数,环境变量等。。。来说说在虚拟地址空间上,堆和栈的划分是完全两块不同的内存,不能混为一谈的。

2)栈内存是由系统分配,系统释放的;以函数为单位进行栈内存分配,函数栈帧,局部变量,形参变量等都存放在栈内存上。堆内存是由用户自己分配的,C 语言用 malloc/free进行分配释放,C++用 new/delete 进行分配释放,由于堆需要用户自己管理,因此堆内存很容易造成内存泄露,而栈内存不会。
3)栈的内存分配释放速度快效率高,内存都是连续的;堆内存的分配释放相对于栈来说效率低一些,内存不一定连续,容易产生内存碎片,但是灵活性高。
4)栈是由高地址向低地址扩展的连续内存,栈的大小一般为 2M 或者 10M(大家的redhat 系统可以用 ulimit -s 命令来查看,是 10M);堆是由低地址向高地址扩展的非连续内存,堆的大小影响的因素比较多,和系统虚拟内存的大小有关系。

I/O复用

1) Select 和 poll 每轮循环都需要将描述符和事件传给内核,而 epoll 每个描述符只需要传一次,不需要每轮都传。而且 select 事件集合相对较小。
2) Select 和 poll 在内核中是以轮询的方式实现的,事件复杂度 O(n) 而 epoll 是采用回调函数的方式监测,事件复杂度为 O(1)

3) Select 和 poll 返回后,为了找到就绪描述符,需要遍历所有元素,时间复杂度为 O(n), 而 epoll 直接拿到了就绪的描述符,不需要遍历所有元素,事件复杂度为 O(1)

epoll的et模式和lt模式

LT 模式下:描述符上事件就绪后,如果没有把数据处理完成,或者没有处理,下一次 epoll 会继续提醒应用程序,直到把数据读完。
ET 模式下:描述符上事件就绪后,如果没有把数据处理完成,或者没有处理,下一次 epoll 不会提醒应用层序,所以要求应用程序在收到一次提醒时,必须当下将所有数据处理完成。
内核实现:epoll_wait 每次将收集到的就绪事件和描述符返回给应用程序,过程是这样,当检测到 rdlist 不为空时,就说明有事件就绪,使用 ep_collect_ready_items 方法将就绪队列 rdlist 中的数据挪到 txlist 中,此时,rdlist 为空。再通过 ep_send_events 方法将就绪事件返回给应用程序,同时又会掉用 ep_reinject_items 方法将一些有设置EPOLLET 的事件的描述符又放回到 rdlist,这样下一轮,epoll_wait 会发现有就绪事件,但会再次检查是否有数据,有就返回,没有继续阻塞。但设置了 EPOLLET 事件,相应的描述符在 ep_reinject_items 方法中不会被放回 rdlist,只有等设备驱动程序检查到有事件产生才会再次将事件放到 rdlist。所以,如果 ET 模式数据没有读完不会放回 rdlist,那么也就不会再去检查是否有数据,提醒应用程序了,直到下次设备驱动程序检查到了事件才会放入 rdlist。