C++开发岗基础面试题(校招)
1、extern关键字作用
第一:当一个函数或者变量在一个文件中声明时使用extern关键字修饰,则该函数或者变量可以在其他文件中调用,但是要include使用extern声明函数或者变量的文件。比如在头文件中声明一个extern修饰的变量,在源文件中定义它。
第二:当使用extern "C"时,是告诉编译器在编译这段代码块时按照C的规则去识别。
2、static关键字作用
第一:static修饰局部变量时,使被修饰变量成为静态变量,存储在静态区。该变量在main函数前初始化,在程序退出时销毁。局部静态变量使得变量在函数退出后不会被销毁,因此可以使得再次调用该函数时,保证数据不变。
第三:static修饰函数,则使得函数只能在包含该函数定义的文件中被调用。如果将static函数定义在头文件中,则每个包含该头文件的文件都实现了这个函数,因此static实现了不同文件中定义同名函数这一目的,而不发生冲突。在多人协同的项目中,为了避免出现同名函数冲突,可以把函数定义为static,这样可以避免同名冲突。
第四:static修饰类中的成员变量和成员函数,可以使不同对象之间实现数据共享。
3、volatile关键字作用
首先说一下volatile的三个特性:易变性、不可优化性、顺序性。
因为CPU对寄存器的访问速度比对内存的访问速度要快得多,所以CPU会优先访问在寄存器中存储的结果或者备份,但是此时内存中的东西可能发生了改变,而寄存器中的内容还是之前遗留下来的,当出现我们希望CPU优先访问内存时(内存管理)而不是自己去访问寄存器,就可以使用volatile关键字,指明希望CPU读取数据的位置。
一个参数可以同时使用const和volatile修饰吗?答案是可以的,比如只读状态寄存器。
4、const关键字作用
第一:const修饰基本数据类型,修饰基本数据类型或者数组,则说明该数据已经被定义为常量型,不可以改变数据的值。
第二:const修饰指针或引用变量,const位于*左边,则标识const修饰指针所指向内存的变量类型,即指针指向一个常量,指针可以指向不同的地方,但是指针所指向的内存中数据不可以改变;const位于*右边,标识const修饰指针,即指针本身是个常量,表示指针不可指向其他内存地址,而指针指向的内存中的内容可以改变。const修饰引用变量一般在类的拷贝构造函数的参数中和重载函数返回值和参数中使用。
第三:const在函数中的使用,const修饰形参说明对形参常量化,参数成为只读参数。const修饰函数返回值,也是起到相应的保护作用。
第四:const在类中的使用,不能在类声明中初始化const成员变量,只能在构造函数的初始化表中对其初始化。const修饰类对象说明对象为常量对象,只能调用常量方法,其他类方法不可调用。
5、new和malloc
第一:new分配内存按照数据类型分配,malloc分配内存按照大小分配,但都是在堆区上分配存储空间。
第二:new是C++关键字,malloc是一个C函数,new在分配内存时会调用构造函数,而malloc底层是使用操作系统的系统调用实现的。
第三:new返回的是指定对象的指针,malloc返回的是void*类型,因此使用malloc多需要进行类型转换。
第四:new分配内存使用delete销毁,malloc对应free,new[]对应delete[],使用delete时会调用对象的析构函数。
第五:malloc分配内存不足时可以用realloc扩容,new没有扩容操作。new分配内存失败时会抛出异常,malloc分配失败时返回NULL。
第六:malloc申请内存后不可赋初值,而new可以。
6、C++特性
封装、继承、多态
7、讲讲C++多态和虚函数、纯虚函数
第一:C++多态分为静态多态和动态多态,静态多态通过模板和重载技术实现,在编译时确定。动态多态通过虚函数和继承关系实现,在运行时动态确定。动态多态实现有两个条件,一是虚函数,二是基类指针或引用指向派生类的对象。
第二:虚函数是指在类中用virtual关键字修饰的成员函数,它提供一种接口界面,允许在派生类中重定义。在声明虚函数的同时,C++内部会维护一个虚函数表,虚函数表的地址在每个对象的首地址,对象调用虚函数是查找该表中的函数指针来获取函数的地址。每个对象中保存的不是一个完整的虚函数表,而是一个指向该表的指针,同类的不同对象保存相同的表指针。
第三:纯虚函数的作用是只在基类中声明一个接口而不去实现它,这些接口应该被派生类所实现,当一个类中至少有一个纯虚函数时,该类被称为抽象类。抽象类中也可以包含虚函数,而抽象类只能作为基类,不可用于创建对象。子类从基类中继承来的纯虚函数,在子类中仍然是虚函数。
8、为什么对于存在虚函数的类中析构函数要定义为虚函数
因为如果析构函数没有定义为虚函数,则在销毁对象的时候只会调用子类的析构函数,只能销毁子类中定义的数据,而不会向下实现多态。析构函数的调用次序是先调用派生类的析构函数再调用基类的析构函数,于调用构造函数顺序相反。
9、析构函数是否可以抛出异常
不能。C++标准明确规定析构函数不可抛出异常,因为当对象操作有异常抛出时说明对象在抛出异常的同时已经失效,这样对象就超出了它的作用域,此时对象会调用析构函数释放空间,如果析构函数可以抛出异常,则在上一个异常没有处理完的情况下,又在析构函数中抛出了另一个异常,会导致程序崩溃。
10、指针和引用的区别
第一:指针保存的时对象的地址,引用是对象的别名,指针作为变量会对其分配空间,而引用不会。
第二:指针使用时可以改变它所指向区域,而引用在初始化以后不可再改变。
第三:指针可以用NULL初始化,而引用只能使用实体对象初始化。
第四:指针有常量指针(可用const修饰),引用没有常量引用。
第五:指针自增操作是改变所指地址,引用自增改变所代表的变量数据。
11、指针与数组的区别
第一:修改指针所指存储区域的内容和修改数组中的内容的方式不同,当指针指向常量区数据时不可使用下标访问,只有当指针指向数组首地址时可用使用。
第二:使用sizeof计算指针大小和数组大小的结果不相同,计算指针时返回指针本身的大小,比如int*为4,而计算数组时返回整个数组的大小,比如char a[10]返回10。
第三:数组名做参数时会退化为指针(sizeof除外)。
12、什么是智能指针
智能指针本身是个类,程序员在为指针进行内存分配后,可能忘记使用delete销毁内存,为了避免这个问题,出现了智能指针。智能指针在创建时调用构造函数,在消亡时(超出使用范围以后)自动调用析构函数,这样就起到一个内存回收的作用。
第一:auto_ptr(C++98方案,C++11已经弃用),该指针采用所有权模式,假设有auto_ptr p1和p2,此时p1已经初始化,将p1赋值给p2,则p2剥夺了p1的所有权,当程序运行时访问p1会报错,因为此时p1所指位置并不确定,因此存在潜在内存问题。
第二:unique_ptr(替代auto_ptr),unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个只能指针可以指向该对象,同时智能指针本身是一个类,在对象生命周期结束以后会自动调用析构函数释放内存,从而避免了内存泄漏问题。那么刚才说的有关auto_ptr出现的情况如果用unique_ptr替换则无法通过编译。
第三:shared_ptr实现共享式拥有,多个智能指针可以指向一个相同对象,该对象和其相关资源会在最后一个引用被销毁时释放。shared_ptr是为了解决auto_ptr在对象所有权上的局限性,在使用引用计数的机制上提供了可以共享所有权的智能指针。
第四:weak_ptr是一中不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象,其设计的目的是为了配合shared_ptr的使用,它只能从一个shared_ptr或另一个weak_ptr对象构造,其构造和析构不会引起引用计数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题(如果两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能降到0,资源永远不能释放)。
13、什么是深拷贝和浅拷贝
浅拷贝是指对对象指针的拷贝,拷贝后,两个指针指向同一个内存。深拷贝不但对指针进行拷贝,还对指针所指向的内容进行拷贝,拷贝后的指针指向的内存地址和之前的指针指向的内存地址不同,但两个地址中的内容相同。
14、C++的四种类型转换
第一:const_cast用于将const转换成非const。
第二:static_cast用于各种隐式转换,比如非const转const,void*转指针等。
第三:dynamic_cast用于动态类型转换,只能用于含有虚函数的类,只能转指针或引用。
第四:reinterpre_cast万能转换,比如int转指针。但容易出现问题,一般很少使用。
为什么C中有强转这个操作,还要诞生C++中的类型转换呢?因为C的转换不够明确,不能进行错误检查,容易出现错误。
15、内存对齐的原则是什么
第一:从0位置开始存储;
第二:变量存储的起始位置是该变量大小的整数倍;
第三:结构体总大小是结构体中最大元素的整数倍,长度较小的元素需要补齐内存空间;(嵌套结构也是如此)
16、内联函数与宏定义的区别
第一:宏定义是在预编译时候进行替换,内联函数在编译阶段对调用内联函数的地方进行替换,减少了调用过程,但是使得编译文件变大,因而内联函数适合函数体简单的函数。
第二:内联函数比宏替换更加安全,使代码更易于调试。因为宏定义是简单的文本替换,不会被编译器所认识,而内联函数要通过编译就要受到编译器的检测。
第三:宏定义函数要注意给所有单元加上括号,不然在复杂语句中做替换容易发生危险。
17、C++内存区域分为哪几块
第一:栈区,用于存储函数的参数,局部变量的值等,由编译器自动分配和释放。
第二:堆区,程序员分配和释放,分配方式类似链表,在内存中属于不连续存储。
第三:全局和静态区,存储全局变量和静态变量,其中已初始化和未初始化的变量会分开存储,存储区域由系统自由管理。
第四:文字常量区,存储常量字符,程序结束后释放。
第五:程序代码区,存放函数体的二进制代码。
18、重载(overload)和重写(override)的区别
重载:指允许多个函数同名,而这些函数的参数表不同(类型或个数都可)。重载时,编译器根据函数不同的参数表判断调用哪个方法,是静态的。方法调用和函数地址在编译期已经绑定好了。
重写:指子类重新定义父类虚函数的方法。子类重新定义父类的虚函数后,父类指针根据赋给它的不同子类指针,动态的调用属于子类的此类函数,这样函数的地址在编译期无法确定,是动态的。
19、动态链接和静态链接的区别
动态链接是只建立一个引用的接口,而真正的代码和数据存放在另外一个可执行模块中,在运行时再装入;而动态链接是把所有的代码和数据都复制到本模块中,运行时就不再需要库了。
二、STL
1、STL的六大组件是什么
第一:容器,各种数据,如序列式容器和关联式容器。
第二:算法,各种常用算法,如sort、search、erase等。
第三:迭代器,容器和算法之间的胶合剂,一种泛型指针,每个容器有自己的迭代器,但都继承自源生迭代器。
第四:仿函数,类似函数,是一种算法策略。
第五:配接器,一种用来修饰容器、仿函数、迭代器接口的东西。例如queue和stack虽然看似容器但其底层操作都借助于deque,所以queue和stack也可看成配接器。
第六:配置器,负责空间配置和管理,是动态的。
交互关系:容器通过配置器取得存储空间,算法通过迭代器存取容器内容,仿函数协助算法完成不同的策略变化,配接器修饰或套接仿函数利用底层接口。
2、STL容器中vector和map的实现原理
第一:vector属于顺序容器,它是一个动态数组,支持插入、删除、查找等操作。vector在内存中是一块连续的存储空间,在旧内存空间不够的情况下,vector会自动分配一个比原空间大两倍的新空间,将原数据拷贝到新空间,然后释放原空间。由于vector空间的重新分配,导致旧vector的迭代器全部失效需要重新配置。vector中数据的读操作效率极高,而插入操作一般只使用尾插,使用随机插入insert时需要移动插入位置以后的所有元素,效率就比较低了。
第二:map属于关联容器,以键值对的方式存储数据,而关键字便起到了索引的作用,一般关联容器的底层使用红黑树结构实现,插入和删除操作都在O(logN)的时间复杂度。当然有其他结构比如mulitmap使用哈希思想实现,查找效率极高,一般在常数级,但是前期哈希表的建立耗时较多。
3、部分关联容器的底层是红黑树,那么红黑树的特点是什么
第一:红黑树是一种平衡二叉查找树,与AVL树的区别是AVL树是完全平衡,红黑树是基本平衡。
第二:选用红黑树的原因是其插入删除的效率都是O(logN),而在其结构上,红黑树想比AVL树插入和删除时旋转的次数相差较大,红黑树至多旋转三次,而AVL树为了保证完全平衡需要旋转多次。
第三:红黑树的定义,(1)节点是红色或者黑色;(2)父节点是红色则子节点不可为红色;(3)根是黑色的那么NULL节点被认为是黑色的;(4)从根节点到每个叶子节点路径上的黑色节点数量相同。
4、为何每次insert之后,以前保存的迭代器不会失效
迭代器这里就相当于指向节点的指针,内存没有改变,指向内存的指针也就不会失效。而相对于vector来说,每次删除和插入指针都有可能失效,调用push_back在尾部插入也是如此,因为为了保证内存数据的连续存放,vector迭代器指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经释放,在插入操作中可能出现分配的空间不足情况,则同上面所说vector内部会自动重新分配空间,此时之前旧空间上建立的迭代器就会失效。特别是在和find等算法在一起使用时,应该牢记不要使用失效的iterator。
5、hash_map和map的区别有哪些
第一:构造函数不同,hash_map需要哈希函数、等于函数;而map只需要使用比较函数。
第二:存储结构不同,hash_map采用哈希表存储,map一般采用红黑树实现。
6、请讲一下STL中各个容器在使用erase方法后迭代器的状态
第一,对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;
第二,对于关联容器map,set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
第三,对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
三、网络原理(TCP/IP和http协议)
1、TCP和UDP的区别
第一:TCP基于有连接,UDP基于无连接。有链接就是tcp在传输前先发送连连接请求和应答包,确定客户端与服务器双方已经连接上了再开始传输数据,而无连接的udp在发送数据前并不考虑对方能否接收到,甚至目的地址无效也能单方传输。
第二:TCP保证可靠传输,UDP属于不可靠传输。tcp能够确保数据一定可送到目的地址,因为当tcp连接没有建立的时候不会传输数据,为了实现可靠传输,tcp有超时重传、应答等机制,而udp不可以保证数据一定送达。
第三:TCP基于流模式,UDP基于数据报模式。tcp把数据看成无结构字符流,无边界,通过客户端和服务器通过建立缓冲区来存储数据,而udp的每个数据报属于一个独立对象,有大小限制。
第四:TCP连接只能点对点,UDP则可以一对一、一对多或多对多。这样一来广播和多播便只能使用udp。
第五:TCP结构复杂,建立过程较慢,UDP结构简单,建立过程较快。
第六:TCP有确认、重传、拥塞控制机制,在没有建立连接时不会传输数据,所以不会导致通信流量的浪费,而UDP在没有建立连接或者对方已经退出的情况下依然会继续发送数据,导致通信流量的浪费。
2、什么是TCP连接的三次握手和四次挥手
3、什么是套接字(Socket)
套接字本质上是个二元组,由IP地址和端口号组成。互联网通信至少需要一对套接字,分别为Client Socket和Server Socket,两类套接字内部存储目的IP地址和目的端口号及源IP地址和源端口号。套接字之间的连接过程分为三部:服务器监听、客户端请求、连接确定。
4、http协议的特点有哪些
第一:无连接,http限制每次连接只处理一个请求,服务器处理完客户端的请求,收到客户应答后立即断开,这样可以节省传输时间。
第二:无状态,指协议对于事务处理没有记忆功能,服务器根据请求发送完数据后不会记录信息,这就衍生出cookie机制解决无状态问题。
第三:媒体独立,当客户端向服务器发送请求时,只需要简单的填写请求路径和方法即可,请求的发送通过浏览器或某些程序中的发送语句发送,并允许传输任意类型和格式的数据对象。
5、讲一讲get/post的区别
第一:get重点是从服务器上获取资源,post是向服务器发送资源。
第二:get传输数据时通过url请求,该过程用户可见;post传输数据通过http的post机制,将字段与对应值封存在请求实体中发送,该过程用户不可见。
第三:get传输数据量小,收到url的长度限制,但效率高;post可以传输大量数据,所以上传文件只能用post方法。
第四:get不安全,因为url可见,post较get安全性较高。
6、https与http的区别
第一:https是在http与传输层之间奖赏了一个ssl。
第二:https使用对称加密与非对称加密。
7、http的结构是什么样的
http数据由请求行,首部字段,空行,报文主体四个部分组成。首部字段又分为:通用首部字段、请求首部字段、响应首部字段、实体首部字段。
8、浏览器中输入一个url之后发送了什么,需要用到哪些协议
浏览器中输入URL,首先浏览器要将URL解析为IP地址,解析域名就要用到DNS协议,首先主机会查询DNS的缓存,如果没有就给本地DNS发送查询请求。DNS查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的DNS服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。得到IP地址后,浏览器就要与服务器建立一个http连接。因此要用到http协议,http协议报文格式上面已经提到。http生成一个get请求报文,将该报文传给TCP层处理。如果采用https还会先对http数据进行加密。TCP层如果有需要先将HTTP数据包分片,分片依据路径MTU和MSS。TCP的数据包然后会发送给IP层,用到IP协议。IP层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如PPP,SLIP),以太网协议需要直到目的IP地址的物理地址,有需要ARP协议。
四、操作系统
1、进程和线程的区别及联系
第一:定义。进程是操作系统进行资源分配的一个独立单位,是具有一定独立功能的程序在某个数据集合上的一次执行过程。线程是进程内部的一个实体,是CPU调度的基本单位。
第二:关系。一个进程可以创建或撤销多个线程,同一进程中的所有线程共享该进程的数据,同一进程中的一个线程崩溃,则该进程中的所有线程崩溃,一般使用一个进程中创建多个线程来模拟进程通信。
第三:区别。进程由操作系统管理,线程由cpu管理;进程具有独立地址空间,一个进程崩溃后在操作系统的保护模式下不会对其他进程产生影响,而线程共享该线程所在进程的地址空间,一个线程崩溃则所有线程崩溃。因此多进程比多线程健壮,但多线程比多进程效率高;对于程序要求同时执行并且又需要共享数据的并发操作只使用多线程,不适用多进程。
2、讲一下线程回收
线程回收需要用到pthread_join(),该函数用于等待其他线程结束,当调用pthread_join时,当前线程会处于阻塞状态,直到被调用的线程结束,当前线程才会重新开始。如果一个线程是非分离的(默认情况下创建的线程都是非分离的)并且没有对该线程使用pthread_join,则该线程结束后并不会释放其内存空间,这会导致该线程变成“僵尸线程”。
3、线程同步的方式有哪些
使用互斥量、条件变量和信号量进行线程控制。
4、谈一下死锁
死锁是指多个进程或者线程为了争夺某种资源而又互相等待其他进程或线程释放它的状态的现象。死锁产生的必要条件有互斥条件、请求和保持条件、不剥夺条件、环路等待条件。预防死锁可以使用资源一次性分配、可剥夺资源、资源有序分配三种方法;然而预防死锁的几种策略会严重损害系统性能,因此再避免死锁时,要施加较弱的限制,从而获得较满意的系统性能。解除死锁的方法有两个:进程终止(终止所有死锁进程和一次只终止一个进程直至死锁消失)和资源抢占(从一个或多个死锁进程里抢占一个或多个资源)。
5、进程调度的方法有哪些
第一:先来先服务
第二:短作业优先调度(平均等待时间最短)
第三:优先级调度
第四:时间片轮询调度
第五:多级队列
第六:多级反馈队列
6、什么是虚拟内存,虚拟内存的优点
第一:虚拟内存允许执行进程不必完全再内存中,每个进程拥有独立的地址空间,这个进程空间被分为大小相等的多个块,称为页。每个页都是一段连续的地址,这些页被映射到物理内存。当程序引用到一部分在物理内存中的地址空间时,由硬件立即进行必要的映射;当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败命令。
第二:使用虚拟内存的优点有,在内存中可以保留多个进程,系统的并发度得到提高;解除了用户与内存之间的紧密约束,进程可以比内存的全部空间还大。
7、讲一下epoll
https://blog.csdn.net/baidu_41388533/article/details/110134366
五、数据库
1、什么是聚集索引,什么是非聚集索引
第一:聚集索引是该索引中键值的逻辑顺序决定了表中相应行的物理顺序。
第二:非聚集索引是数据存储和索引存储分开,索引带有指针指向数据的存储位置。
第三:两者区别。聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个;聚集索引存储记录是物理上的连续存在,而非聚集索引是逻辑上的连续。
2、现在普遍关系数据库用的数据结构是什么
答:B树或者B+树
3、索引的优点和缺点
第一:建立索引的优点。大大加快数据的检索速度;创建唯一性索引,保证数据库表中每一行数据的唯一性;加速表和表之间的连接;在使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间。
第二:索引的缺点。索引需要占物理空间;当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,降低了数据的维护速度。
4、关系数据库和非关系数据库
简单来说,关系模型指的就是二维表格模型,而一个关系型数据库就是由二维表及其之间的联系所组成的一个数据组织。非关系型数据库提出另一种理念,例如,以键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,这样就不会局限于固定的结构,可以减少一些时间和空间的开销。
5、什么是悲观锁与乐观锁
第一:悲观锁是假定会发送并发冲突,屏蔽一切可能违反数据完整性的操作。
第二:乐观锁假定不会发生并发冲突,只在提交操作时检查是否违反数据完整性。