C++开发工程师面经总结
C++语言基础
-
C++语言特点及与C语言的区别
C++在C语言基础上引入面向对象的特性,并兼容c语言;
c++代码质量好,执行效率高;
c++增加许多高级特性,使c++使用更加便捷安全,例如引用,4类cast转换,类与对象,模板,智能指针等,stl模板库等。
c++是面向对象的,c语言主要是面向过程的语言。
-
c++中struct与class的区别
struct一般用于一个数据结构的集合,class一般用于对象数据的封装;
struct默认访问权限是public,class默认是private;
struct与class均可以做继承,但是struct默认公有继承可修改,class默认私有继承;
class关键字能用于定义模板,struct不可以。
-
c++中什么是模板
template
模板是一种泛型编程,能独立与各种类型绑定编码;
模板通常有函数模板与类模板;
函数模板可以隐式调用或者显示调用,即自动绑定类型或者直接尖括号显示指定类型。
函数模板会有两次编译,一个在声明处编译,一个在调用处对替换类型后的代码再编译。
类模板有高复用性,编译时检查数据类型比较安全,然后移植性高。
-
模板类的实现
模板类隐式实例化:在编译过程中编译器决定使用什么类型实例化一个模板,className<int> P; 在尖括号中直接写入类型名去实例化一个类对象。
模板类显式实例化:程序员直接代码中明确模板类使用的类型,template class className<int>; 直接在程序中尖括号类显式明确类采用的模板类型。
模板具体化:当模板使用某种类型生成的类或者函数不能满足需要,可以通过模板具体化时修改类中定义,template<> class className<int> {}; 花括号体内可以直接重新定义类的某些函数等。
类什么时候实例化通常看是否需要分配内存空间,例如声明不需要,引用是操作同一块空间则也不需要实例化,指针使用一块固定地址的话则也不需要。
-
程序是怎么执行的
预处理:宏定义删除并展开宏,引入头文件代码,删除注释,添加行号和文件名标识等,预处理后依然是.cpp源文件;
编译:对预处理的源文件进行代码分析与优化,(词法分析,语法分析,语义分析)生成汇编程序.s文件,也是ASCII文件;
汇编:对汇编程序进行转换为二进制.o文件,链接才能执行。
链接:将所有二进制文件与链接库包括动态链接库与静态链接库链接在一起,生成exe可执行文件(window),linux下为.out文件。
静态链接在链接时将函数与过程链接到可执行文件中,就算删除链接库也不影响程序;生成的静态链接库,Windows下以.lib,Linux下以.a。
动态链接是执行中再找链接的方法与过程,可执行文件中只有链接库的定位信息,window下是dll,linux下是.so。
-
static关键字作用
全局静态变量:作用域为该文件中,其它文件不可见,存储在全局静态变量静态存储区
局部静态变量:在静态存储区初始化,作用域依然为局部作用域,但离开作用域不会被销毁,依然在内存中,下次调用函数时直接取出该变量,不用再分配内存。
静态函数:只在声明其的文件中可见,外部文件不可见,函数实现需要static修饰,不会与同名函数冲突。
类静态成员:可以实现多个对象的数据共享,并保证了数据的隐藏性,类外定义无需加static,可以被普通成员函数或静态成员函数使用,注意静态类成员变量要在类外初始化。
类静态成员函数:与类绑定而非对象,实现中可使用静态成员,但不能直接引用非静态成员,使用非静态成员时需要采用对象来引用,使用类静态成员函数直接通过类名引用或者对象使用。类静态成员函数定义可在类中也可在类外,类外定义不可加static。
-
extern用法
extern可以用于变量和函数前面,用来说明该变量或者函数的定义在其它地方,若声明与定义处有不符,则会导致运行报错。
-
c++四种cast强制转换
static_cast:可以用于执行类相关类型转换,例如基类向派生类,派生类向基类的转换,不会执行安全检查,主要执行非多态的类型转换;还有一般类型转换,如基本数据类型转换,void*与其它类型指针的转换。
const_cast:一般用于const属性的转化,例如增加const属性或者删除const属性,唯一一个操作const操作符的转换。
dynamic_cast:专门用于类之间的转换,即转换类型必须是类指针、类引用或者void*,派生类转换为基类与隐式转换相同,基类转换为派生类仅当基类指向对象为派生类时才可转换成功,也即基类最开始的指向是转换目标的完整有效对象才可转换成功,转换成功返回转换类型,失败则返回空指针;另外也可用将任何类型指针转化为void指针。
reinterpret_cast:几乎可以做任何类型转换,如int转指针,但是不检查指向内容,也不检查指针类型自身,容易出问题。
-
函数指针与指针函数
函数指针:即指向函数的指针变量,存放着函数的入口地址,通常用于函数回调,即别人的库函数里调用我们的函数即回调,如sort排序可以自定义排序规则函数。
指针函数:一个函数,返回值是指针。
-
nullptr能否调用成员函数
如果是编译则可以通过,因为编译期间绑定地址与是否为空无关,但是调用时若解引用了这个空指针则报错。
-
野指针怎么产生和避免
野指针就是指向位置不可知不正确的指针,产生原因通常有:指针指向释放后指针未置nullptr、指针指向变量在指针之前被销毁(如指针指向一个函数局部变量)、局部指针变量未初始化(局部变量初始化随机)等等。
避免野指针方法:定义时初始化为空、释放之后置空、申请内存后判定是否为空、使用智能指针等等。
-
智能指针
头文件memory,智能指针是一个类,在构造函数中传入一个普通指针,析构函数中释放传入的指针,智能指针的类是栈上的对象,函数结束或者程序结束时后自动释放。
unique_ptr:是一种独占型指针,在c11中用来代替c98里的auto_ptr,以避免auto_ptr可能存在的内存崩溃问题;unique_ptr保证了同一时间只有一个智能指针可以指向某个对象,无法直接对其复制,在编译器就会进行检查。
unique_ptr<string> p3 (new string ("auto")); //#4 unique_ptr<string> p4; //#5 p4 = p3;//此时会报错!!在c98中则可以,因此两个智能指针指向同一内存可能会内存崩溃 unique_ptr<string> pu3; pu3 = unique_ptr<string>(new string ("You")); // #2 对于临时的右值对象则可以复制
shared_ptr:是共享型指针,可以使用多个shared_ptr指向一块内存空间,其使用引用计数的方式管理内存,计数为0时自动释放空间。但是当两个对象使用share_ptr指向对方且对象内又有对方的share_ptr时,可能导致循环引用导致死锁,引用计数可能会失效,从而导致内存泄漏。
use_count 返回引用计数的个数 unique 返回是否是独占所有权( use_count 为 1) swap 交换两个shared_ptr 对象(即交换所拥有的对象) reset 指针置空,放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少 get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.
weak_ptr:是一种不控制对象声明周期的指针,即其指向一个share_ptr管理的对象,就是协助share_ptr进行对象管理,可以用来放置share_ptr出现循环引用导致内存泄漏的风险;weak_ptr是一种对对象的弱引用,即它不会增加share_ptr的引用计数,并且可以与share_ptr互相转化,share_ptr可以直接赋值给weak_ptr,同时可以对weak_ptr调用lock函数来获取到share_ptr。
注意weak_ptr不能直接访问其指向对象的方法,需要先通过lock函数转化为share_ptr再进行访问。
-
内联函数与宏函数的区别
宏定义本质不是函数,预处理时复制宏代码无函数压栈退栈过程,提高效率;内联函数则是一个函数,编译时在调用处将函数体展开,省了调用开销,提高了效率。宏定义无类型检查,内联函数则满足函数提醒,也有类型检查,通常用于比较短使用频繁的函数,一般定义也在头文件,如果内联函数太冗长,则导致代码量过大消耗过多内存空间。
-
++i与i++
均是做i的加1操作,前者先加再赋值,后者先赋值再加,后者效率更慢;i++不能做左值,++i可以,两者均不是原子操作。
-
new与malloc的区别
new为操作符,而malloc为函数,new调用时会先分配内存,再调用构造函数,释放时调用析构函数,malloc无构造与析构;
new无需指定内存大小,也不用进行指针类型转换,malloc需要指定分配内存大小,返回的指针也需要强转。
new发生错误会抛出异常,malloc会返回null,new会更加安全。
new操作符可以重载,但是malloc不可以重载。
-
const与define的区别
两者均可以定义常量,但const生效于编译阶段,define在预处理阶段生效;
const常量需要额外内存空间,define常量是直接作为操作数使用,无需内存空间;
const常量有类型,而define没有类型,不利于做类型检查。
-
c++几种传值方式
值传递:值传递会拷贝对象,效率低,形参变化也不会影响实参的值。
引用传递:直接绑定对象,效率高,形参变化会影响实参。
指针传递:同理与引用但是操作指针不如操作引用更安全。
-
指针常量与常量指针
int const *a:常量指针,内容为常量,地址可以改变,相当int const (*a)
int *const a:指针常量,内容可以变,但是指向地址不变,相当于int * (const a)。
-
内存分配的方式
c++中内存主要分为5个区。从上到下在内存中是从高到低
常量存储区:通常存放常量,不允许修改。
栈:通常存放函数局部变量,函数执行结束自动释放内存;
堆:通常是程序员分配的空间,如new,malloc等,需要由程序员自己释放内存;
全局/静态存储区:通常存放全局变量和静态变量。通常会分为数据段(存放已初始化的全局变量与静态变量)与BSS段(存放未初始化或者初始化为0的全局变量和静态变量)。
程序代码区:用于存放二进制代码。
内存的分配注意判断以及初始化为nullptr,堆区内存要注意释放并及时置空,以免产生野指针。
-
什么是内存对齐?作用是什么
内存通常按照字节划分,让各种类型数据按照一定规则存放在某个位置,使对其的访问更加高效即内存对齐,如一个变量内存中存放的地址正好是其长度的整数倍,则称为自然对齐。
如一个int型数据存放地址正好是4个字节的整数倍,则可以一次取出数据,如果地址为奇数则可能会通过一次char查询一次int查询才取出数据。通常64位机器中char为1个字节,short为2个字节,int为4个字节,float为4个字节,double为8个字节。通过内存对齐使数据的访问更加高效,在某些机器上如果没有内存对齐甚至可能会出错。
-
面向对象与面向过程区别
面向对象是一种编程思想,即将一切事物看做一个对象,一种种类,他们均有各自的属性以及行为方法,将这些属性与方法封装可以成为一个类。
面向过程是根据业务逻辑从上到下写代码。
面向对象是进行类的封装,提高代码复用性,提高开发效率。
-
面向对象的特征与继承方式
封装:将类的属性与方法封装起来,隐藏对象的一些属性与实现细节,仅提供对外接口与外界对象交互,提高程序安全性。对象只能访问类的public成员,protected与private成员均不可访问。
继承:通过对基类进行拓展,在能使用现有功能前提下,无需重新编写原有功能进行新的拓展。
对于基类的各个成员
public继承:private不可见,protected仍为protected成员,public仍为public成员,派生类对象只能访问基类的public成员。
protected继承:private不可见,protected仍为protected成员,public变为protected成员,派生类对象对于基类成员均不可访问。
private继承:private不可见,protected变成private成员,public变成private成员,派生类对象对于基类成员均不可访问。
多态:即不同对象对于同一行为会有多种状态,如父类指针可以根据指向的子类对象,相同的函数会有不同的状态。实现方法通常为重写和重载。
-
c++中重写和重载
重写:指派生类中重新定义的函数,其函数名,参数列表,返回值类型均与基类中的一致,只有函数体会不一样,派生类对象调用时会调用派生类中的重写函数,基类的被重写函数不会调用,并且基类中的被重写函数必须有virtual修饰。
重载:一些函数的功能相似但有细节不同,如数值类型不同,这时通过使用相同的函数名,而参数类型、顺序、个数有区别(返回值类型不同不可以构成重载),使两个函数有区分,实现了函数重载。
-
多态原理与使用
原理:
1、当类中存在虚函数时,编译器会在类中自动生成一个虚函数表
2、虚函数表是一个存储类成员函数指针的数据结构
3、虚函数表由编译器自动生成和维护
4、virtual 修饰的成员函数会被编译器放入虚函数表中
5、存在虚函数时,编译器会为对象自动生成一个指向虚函数表的指针(通常称之为 vptr 指针)继承了父类虚函数的子类,编译器会自动为重写函数添加一个virtual关键字,编译器会找到指针指向对象的vptr指针,然后找到其对应虚函数表中的虚函数调用,实现了多态,如果父类指针指向子类对象,则编译器会找到子类对象中的虚函数表里的虚函数并调用。
-
c++构造函数类型
默认构造函数:编译器自动生成的构造函数,函数体为空。
普通构造函数:可以有多个,但是参数数目或类型需要不同,也能通过该构造函数进行类成员初始化。
拷贝构造函数:函数参数为对象的引用,用于根据一个已经存在的对象复制出一个新的对象,没有显式写拷贝构造时候系统会自动生成一个拷贝构造函数,默认拷贝是浅拷贝,浅拷贝通过值传递两个变量同时操作一个内存,可能导致内存泄漏风险,深拷贝需要额外开辟一块内存空间,导致效率降低。
移动构造函数:参数类型为右值引用(&&)的拷贝构造函数,避免了深拷贝与浅拷贝的问题,无需新开辟内存避免了深拷贝问题,如果传入对象时临时值,则会选择使用移动拷贝构造,将临时变量的资源做浅拷贝就避免了左值引用浅拷贝的问题,move函数是强制将左值对象转换为右值。
-
一个类默认生成哪些函数,多重继承时类的初始化顺序及析构顺序
默认生成:无参构造函数,拷贝构造函数,赋值运算符重载,析构函数。
创建派生类对象时,优先调用基类构造函数;
如果类中有成员类,则成员类的构造函数也优先调用;
父类构造函数->成员类对象构造函数->自身构造函数;
派生类Derive(): public base1, virtual public base2, public base3, virtual public base4{
Obj1 obj1;
Obj2 obj2;
};
派生类对象内包含多个子类成员,同时继承了多个基类,基类中加virtual的则为虚继承,该基类为虚基类,目的是让继承虚基类的派生类能共享虚基类中命名变量,即防止多次继承同一个基类的派生类会产生命名冲突,通过虚继承使派生类对同一命名的虚基类成员只持有一个,不会同时继承多个同命名变量。
此时构造函数顺序为:base2, base4, base1, base3, obj1, obj2, derive;
即优先构造虚基类,顺序以继承顺序排序,其次构造普通继承的基类,顺序以继承顺序,再构造子类构造,顺序以初始化顺序,最后构造自身构造函数。
析构函数的顺序则与构造函数的相反。
-
虚析构和虚构造
虚析构:将可能被继承的基类的析构函数设置为虚函数,当使用基类指针指向子类对象时,释放基类指针也能释放掉子类的空间,能防止内存泄漏,如果基类析构函数不是虚函数,则可能导致子类空间无法被析构。默认情况下析构函数不是虚函数,因为虚函数需要虚函数表与虚表指针,需要额外空间。
不能虚构造:虚函数需要在虚函数表里调用,但是如果有虚构造函数的话,对象没有实例化,没内存空间分配,那就无法调用虚函数表,自相矛盾了,另外虚构造函数也没有什么实际的意义。
-
c++类中定义引用数据成员
1.不能使用默认构造函数去初始化,要提供构造函数去初始化引用数据成员,否则发生未初始化错误。
2.构造函数的形参也必须是引用类型。
3.不能在构造函数内初始化,必须在初始化列表中进行初始化。
初始化列表一般用于对那些定义时必须赋初始值的变量进行初始化,如引用,const类型变量。采用初始化列表方式进行变量初始化效率更高,因为采用构造函数内初始化是先定义,再初始化,初始化列表是定义与初始化同时进行,同时构造函数内初始化可能再类实例化时会先进行默认初始化构造,再进入自定义的构造函数中进行变量初始化。
-
类中常函数是什么
类的成员函数的最后面加上一个const关键字,表明这个函数不会对类的非静态成员变量做修改(可以修改类静态成员变量),只能对数据成员进行读取,另外const修饰的常量对象可以调用常函数,不可调用无const修饰的函数(可以调用静态成员函数)。
-
虚继承以及作用以及虚基类
虚继承一般用于解决类的多重继承中基类的成员存在二义性的问题以及存储空间浪费的问题,一般出现原因也是菱形继承导致。
在继承基类时在继承类型前加上virtual关键字可以实现虚继承,被继承的类也叫做虚基类,在派生类中对于同一份基类成员只会存在一份拷贝,也不会出现二义性的问题。
-
虚函数与纯虚函数
虚函数主要是实现多态的机制,虚函数必须是基类的非静态成员函数,通过基类指针指向子类对象,调用虚函数的重写函数时,不同的子类对象会展现不同的调用效果,虚函数绑定是在运行时选择,即动态多态,虚函数是通过虚函数表来进行调用,实例化对象时,会生成一个虚表指针来找到虚函数表。
纯虚函数是在基类中声明的虚函数,即在虚函数声明后加上"= 0",通过声明纯虚函数使基类成为一个抽象类,对于纯虚函数,子类必须要进行重写以实现多态性,抽象类同时也不可以进行实例化。一般纯虚函数是为了给子类提供一个函数接口,同时必须要进行重写实现,但自身无定义也不可调用,抽象类也可通过创建基类指针或引用来实现多态。
虚函数与纯虚函数均不可使用static标识,因为static修饰的函数是在编译期间绑定,但是虚函数是动态绑定,并且两者修饰的函数生命周期也不同。
构造函数可以调用虚函数,但是一般没什么意义,虚函数一般用于多态,基类中构造函数调用虚函数和多态没什么关联。
-
拷贝构造函数的传参类型
传参必须是引用传递。
如果是值传递的话会再次调用该类的拷贝构造函数,从而无穷无尽地递归调用拷贝构造。
注意指针方式也是传值。
-
拷贝赋值与移动赋值区别
拷贝赋值是通过拷贝构造函数来赋值,通过同一类创建过的对象来初始化新的对象。
移动赋值是通过移动构造函数来赋值,二者区别在于:
拷贝构造函数形参是一个左值引用,移动构造的形参一般是右值引用;
拷贝构造一般是对整个对象或者变量的拷贝,移动构造生成一个指针指向源对象的地址,接管其内存,效率高也无浅拷贝的风险。
-
仿函数以及作用
仿函数又称函数对象,是一个能行使函数功能的类。仿函数的类中必须重载operator()运算符。
仿函数比一般函数更加灵活,能够行使类的功能同时又能行使函数功能,例如在一些回调函数中如sort排序里进行一些特殊的排序筛选操作,第三个参数一般传入一个函数,如果需要在这个函数内加上一个阈值变量就不方便提前设定,而仿函数可以通过类的特性提前初始化一个类成员变量,通过通过函数一样的调用代替一般函数,实现特殊要求的操作。
class ShorterThan { public: explicit ShorterThan(int maxLength) : length(maxLength) {} bool operator() (const string& str) const { return str.length() < length; } private: const int length; }; shorterthan s(10); //仿函数初始化 bool flag = s("abc"); //类似于函数的调用
-
STL中map, hashTable, deque, list的实现原理
map:map实现原理是红黑树(结点时红色或者黑色,叶子结点都是黑色空结点,是平衡的二叉搜索树,即左右子树高度相差不高于1,同时结点值左小右大),所以map中元素有序,对map的操作都是对红黑树的一系列操作。
hashTable:原理是采用函数映射思想将存储位置与记录关键字记录下来,能快速定位查找的内容。
deque:实现原理是双向队列。元素在内存中连续存放,所有适用于vector的操作都适用于deque,随机存储元素是常数时间,速度仅次于vector。
list:实现原理是双向链表。元素在内存中不连续排列,任何位置增删都是常数时间,不支持随机存取。
-
STL的空间配置器allocator介绍
allocator封装了stl容器在内存管理的底层细节,头文件为memory,allocator一般使用的函数有:
allocate:分配内存(底层为malloc函数);construct:调用已分配对象的构造函数;destory:调用析构函数;deallocate:用于释放内存(底层为free函数)。
allocator底层采用malloc分配内存可能存在内存碎片问题,所以其采用了双层级配置器。第一级对于分配较大的内存则采用malloc与free进行分配与释放;当配置内存大小小于128bytes时使用第二级配置器,其内存申请与释放通过一个内存链表来维护内存池,该内存链表通过union结构(联合体,类似struct结构,但是union内所有成员共用一块内存,也即只能使用一个变量,对一个变量修改则就只能使用这个变量,union内存大小取决于union中最大的一个变量所占内存)实现,空闲内存相互挂接在一起,被使用的内存则被链表剔除。
-
STL中容器及各个时间复杂度
vector:采用一维数组实现,内存连续,插入(ON),查看(O1),删除(ON)。
deque:采用双端队列实现,内存连续,插入(ON),查看(O1),删除(ON)。
list:采用双向链表实现,元素存放在堆中,内存一般不连续,插入(O1),查看(ON),删除(O1)。
map, set, multimap, multiset:均采用红黑树实现,插入(OlogN),查看(OlogN),删除(OlonN)
map:关联容器,提供1对1的哈希,采用红黑树实现所以元素排列有序,元素都是pair,但是建立红黑树导致内存消耗较大,对于有顺序要求的问题可使用map,键不能重复且不能修改,map无法插入相同键的键值对。
multimap:与map区别在于提供了1对多的哈希,可以插入相同键的键值对。
set:集合,元素不会重复,同时是有序的。
multiset:与set区别在于允许元素重复。
unordered_map, unordered_set, unordered_multimap, unordered_multiset: 均采用哈希表(也即散列表)实现,时间复杂度为:插入(O1, 最坏情况为ON), 查看(O1, 最坏情况为ON), 删除(O1, 最坏情况为ON)。哈希表通过散列函数来对key值进行空间映射,最简单的方法是余数法,将key与一个数组长度取余,余数作为数组下标,数组存放某个链表头指针,用链表去存放值,通过链表也可以依次存放哈希冲突后的值。所以最坏情况下,所有键值均冲突,此时哈希表变成一个普通链表,插入,查看,删除均要依次遍历,所以复杂度为ON。
原理采用哈希表实现的结构查询更快,查询问题可以哈希表,但是注意哈希表的建立比较耗时。
-
STL各个容器使用场景
vector:随机访问与存取时,随机插入删除比较少。
list:随机插入删除较多,随机访问存取较少。
deque:类似排队场景时,例如可能对队列头部或者尾部进行插入删除,可以使用双端队列这种。
set:主要负责查找不重复元素,并且有顺序要求,插入删除效率高。
map:对于大量数据中的元素查询,实现高速查询,插入删除效率高。
-
STL中迭代器作用以及与指针区别
用于指向顺序容器和关联容器之中的元素,并进行访问与修改。
迭代器不是指针,是一个模板类,其内部封装了指针,并且模拟了指针的一些功能,例如一些指向,自增自减运算符,并且迭代器隐藏了容器元素的内部结构,只提供了对外接口,符合隐藏原则,迭代器返回的是对象的引用而不是对象的值,所以对值的访问需要对迭代器进行解引用。
-
STL中迭代器怎么删除元素
对于序列容器如vector,deque来说,使用erase删除后,后面的元素迭代器全部失效,每个元素往前移动一位,并返回下一个有效的迭代器。
对于关联容器map,set来说,使用erase后,当前元素迭代器失效,由于内部为红黑树,则不会影响后面的迭代器,也会返回下一个有效迭代器,或者需要在使用erase前记录下下一个元素迭代器。
list的earse也会返回下一个有效迭代器。
stack,queue,priority_queue均不支持迭代器。
-
STL中resize与reserve的区别
capacity:该值在容器初始化时赋值,表示容器容纳的最大元素数量,但创建时并未创建元素对象。
size:表示容器当前实际元素数目,可以通过下标访问,因为此时已经有元素对象了。
resize既分配了空间也创建了对象;reserve只增加了容器预留空间,但是没有创建对象,需要使用insert或者push等操作加入对象。
resize既修改了size大小也修改了capacity的大小,但是reserve只修改了capacity大小。
两者形参个数不同,resize有两个参数分别是修改后容器大小以及元素初始值(默认为0),reserve参数只有修改后容器预留的大小。
注意resize中修改容器大小的参数如果小于当前大小,则会删除多余的元素,保留前面部分。
-
push_back与emplace_back的区别
push_back一般是先构造对象,然后将该对象拷贝到容器末尾,emplace_back直接在容器的末尾构造对象,省了拷贝过程,效率更高。
-
auto与decltype的区别
两者均是关键字,decltype用法更像函数,两者功能类似,都是在编译时期进行自动类型推导。
进行auto推导是通过右值来推导变量类型,所以必须要初始化,同时auto推导的是值类型,对于引用类型会自动去掉引用标识符。
decltype是根据括号内的变量或者表达式类型来推导出类型,所以无需进行初始化,而且decltype可以推导出引用,无需再次添加引用标识符。
decltype(auto)这种写法就必须进行初始化,先根据右值类型推导替换掉auto,然后用decltype推导出该类型。
decltype(x1) x;
-
null与nullptr的区别
在c++中nullptr是一个空指针类型,主要能用来解决null二义性的问题,比如null在c++中表示为0,在一些重载的隐式转化中可能被绑定为int类型而不是void*类型,而nullptr可以很好避免这种问题。
-
正则表达式贪婪模式与非贪婪模式
贪婪匹配:默认是贪婪模式,正则表达式一般趋向于最大长度匹配,也就是所谓的贪婪匹配。如上面使用模式p匹配字符串str,结果就是匹配到。
非贪婪匹配:在量词后面直接加上一个问号?就是非贪婪模式,就是匹配到结果就好,就少的匹配字符。如上面使用模式p匹配字符串str,结果就是匹配到。
-
lambda表达式
就是一种没有名称的函数,语法格式是:
[capture](parameters)mutable throw() ->ret{body}
capture表示作用域内的捕获列表,[]表示什么也不捕获,函数体无法使用任何作用域变量;[=]表示按值捕获所有变量包括this指针;[&]按引用方式捕获所有变量包括this指针;[=, &a,&b]表示变量a,b按引用捕获,其余全按值捕获;[&, a,b,c]表示a,b,c按值捕获,其余全按引用捕获;[a, &b]表示a按值捕获,b按引用捕获;[this]表示只捕获this指针。
parameters表示lambad形参列表,可为空,一般情况括号也可以省略。
mutable是修饰符,是可选的,如果写上则表示取消lambda的常量性,默认lambda是常函数,即不可修改作用域内的值,写上的时候必须要有参数列表的括号。
throw()是异常说明,也是可选的,用来指示lambda不会引发异常,该修饰符尽量不用。
->ret表示返回类型,一般情况下lambda会自动推导返回类型,是可选的。
body表示函数体,可以访问的变量有:捕获变量,形参变量,局部声明的变量,类数据成员(this被捕获时),具有静态存储持续时间的任何变量,如全局变量。
lambda短小精悍,让代码紧凑,但是难以实现函数复用。
编译器会将lambda生成一个匿名类对象,并在其中重载operator()运算符,将lambda函数体中代码复现到operator方法里。
auto fun1 = [](int a, int b){return a+b;} auto p = fun1(1,2);
-
运算符重载
返回值类型 operator 运算符名称 (形参表列){
//TODO:
}本质就是函数重载,通过operator关键字实现,注意不能改变原运算符优先级,不能创建新的运算符,不能重载作用域运算符、四类类型转换、sizeof、条件运算符等等。
当运算符作为类成员函数时,形参参数一般为一个(后置单目运算符除外),也就是操作符的右操作数,这是因为类的this隐式访问了对象本身,默认充当了左操作数。
当运算符作为全局函数时,两个操作数均由形参传递,另外在类中会将全局运算符重载声明为friend友元函数,因为可能会访问类中的private变量。
-
流运算符的重载
流运算符重载不可以成为类的成员函数,这是因为类的成员运算符重载一般左操作数是类本身,但是流运算符重载要求第一个操作数是流对象。
所以针对流运算符重载一般放在类外重载,然后在类中声明为友元函数。
void operator<<(ostream& out,const Date& d) { //流对象需要在操作符左边,然后在对应的类中使用友元 out << d._year << "-" << d._month << "-" << d._day << endl; }
操作系统
-
常用Linux命令
-
怎么以root权限运行某个程序
sudo chown root filename sudo chmod a+s filename 修改文件权限
-
软链接与硬链接的区别
软链接:就是符号链接,链接文件中就包含了目标文件的路径名,可以是任意文件或者目录。
硬链接:就是一个文件的多个文件名,相当于两个指针指向同一块内存,可以用多个文件名与同一文件进行链接,并可以存在同一目录或者不同目录。
软链接可以对不存在的目录或者文件进行链接,可以交叉文件系统;硬链接只能对存在的文件进行链接,不能对目录进行创建,不能交叉文件系统创建硬链接。
删除一个硬链接不影响其它相同硬链接的文件,删除软链接也不影响被指向的文件。
-
进程调度算法有哪些
先来先服务调度:每次调度都从后备进程队列里选择一个或者多个前面的进程,然后分配内存与资源,放入就绪队列里。属于非抢占式调度,优点是公平,实现简单;缺点是不利于短作业。
短作业优先调度:从后备队列中选择一个或者多个估计运行时间最短的进程,进行调度运行。降低作业的平均等待时间,提高系统吞吐量。对长作业不利;未考虑作业的紧迫程度;对进程估计执行时间难以预测。
高优先级优先调度:在后备队列中优先级最高的进行优先分配资源进行创建。常被用于批处理系统中,还可用于实时系统中。
时间片轮转:在后备队列中按照时间片轮转进行调度,一个时间片可能几个ms到几百ms,时间片结束后计时器发送中断请求,系统停止当前进程并且放入就绪队列列尾,然后资源再分给下个进程。优点是兼顾长短作业;缺点是平均等待时间较长,上下文切换较费时。适用于分时系统。
多级反馈队列调度算法:有多个进程队列,每个队列优先级不同,在队列中采用时间片轮转调度,优先级越高的队列则时间片越短,当该时间片运行完进程未结束则该进程进入次优先级队列末尾,最低优先级队列的时间片比较长,所以大作业也能执行完。优点是兼顾长短作业,有较好的响应时间,可行性强,适用于各种作业环境。
调度算法可设计为抢占式和非抢占式,其中非抢占式(Nonpreemptive):让进程运行直到结束或阻塞的调度方式,容易实现,适合专用系统,不适合通用系统。 抢占式(Preemptive):允许将逻辑上可继续运行的在运行过程暂停的调度方式,可防止单一进程长时间独占,CPU系统开销大(降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序)。
先来先服务是非抢占式的,优先级调度两种都可以设计,时间片轮转是抢占式的。
-
linux系统态和用户态
内核态与用户态是操作系统两种运行级别,内核态有最高权限,可以访问所有系统指令;用户态只能访问一部分指令。
进入内核态的三种方式:1.系统调用;2.异常;3.设备中断。其中系统调用是主动的,另外两种是被动的。
cpu所有指令中一些指令可能导致一些危险,如内存清理,时钟设置等核心操作不可让用户程序随意进行。
-
LRU算法(最近最少使用页面置换算法)
一般用于缓存淘汰,将缓存中最近最少使用的对象删除掉。
实现方式一般用链表和hashmap
-
什么是页表,其作用是什么
页表是虚拟内存的概念。操作系统虚拟内存到物理内存的映射表,即页表。
虚拟内存的每个字节的映射非常多,通过分页能够减少虚拟内存页到物理内存页的映射表大小。
-
什么是虚拟内存
是一种内存管理机制,可以通过页表寻址完成虚拟地址到物理地址的转换。
对大进程通过虚拟内存调度仍然能让其执行;对进程对物理地址的访问进行控制,提高了内存安全性;能实现内存共享,方便进程通信。
但是虚拟内存创建页表占用效率与空间,地址转换也会耗时。
-
进程,线程,协程区别
进程是系统资源分配的最小单元;线程是CPU调度的最小单元;协程是一种轻量型线程,由程序控制,即在用户态进行。
进程与线程均可并发执行。
线程是非抢占式的,协程是抢占式的。协程能够保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用状态,协程也无需锁机制,因为相当于是串型执行。
-
进程通信方式与同步方式
进程通信包括:管道,信号量,共享内存,消息队列,信号,套接字socket。
进程同步方式:信号量,管道,消息队列。
计算机网络
-
TCP三次握手与四次挥手
第一次握手:建立连接时,客户端向服务器发送一个SYN(请求连接)数据包,请求连接,等待确认;
第二次握手:服务端收到SYN包,回复一个确认字符ack,同时发送一个SYN数据包给客户端;
第三次握手:客户端收到SYN+ack包,再回复一个ack表示自己已经收到,然后连接建立开发发送数据。
第一次挥手:客户端发送一个FIN(关闭连接)数据包,表示数据发送完毕,请求关闭连接,然后客户端不再发送数据但是能接受数据;
第二次挥手:服务端发送一个ack表示自己收到了fin包,此时等待剩余数据传输完毕;
第三次挥手:服务端等待数据传输完毕,向客户端发送一个fin包,表示可以断开连接;
第四次挥手:客户端收到fin包后回复一个ack表示确认,等待一段时间后确保服务端没数据发送过来后断开连接。
-
为什么TCP握手要3次,2次不可以
一方面TCP连接是双向的,只有经过3次握手才能确保双方都接收到了对方的数据,两次握手只能保证客户端知道服务端收到了自己的数据了。
另一方面,能防止已经失效的请求连接报文又发送给服务端导致又再次连接,例如客户端第一次请求连接滞留在网络中,然后重发一次给服务端建立起了连接,等关闭连接后这时失效报文重新送到了服务端,如果两次握手则服务端收到后直接给客户端发送一条数据就直接单方面建立连接了导致资源浪费,如果三次握手,服务端向客户端发送确认报文时客户端是不会回应的,因为客户端已经断开等待了,服务端超时没收到回复也会停止等待。
-
TCP和UDP的区别,它们头部结构如何
TCP面向连接,即数据传送之前需要经过3次握手建立连接,会话结束后也要结束连接。UDP无需连接可以发送数据。
TCP保证数据按序发送按序到达,提供超时重传保证可靠,UDP是不可靠的传输协议,虽然是按序发送,但是不保证按序到达也不保证能到达。
TCP数据首部需要20个字节,UDP只需要8个字节。
TCP有流量控制与拥塞控制,UDP没有,网络的拥堵也不会影响发送端的发送速率。
TCP是一对一的连接,而UDP是支持1对1,多对1,多对多的通信。
TCP面向的是字节流的服务,UDP面向的是报文的服务。面向报文指应用层交付UDP多长的报文,UDP就照样发送,既不合并,也不拆分。面向字节流指应用层与TCP的交互是一次一个数据段,TCP可以对数据段进行划分与合并。
数据结构与算法
-
介绍各类排序算法以及时间复杂度
排序的稳定性要看排序中元素之间的相对位置是否改变。
冒泡排序:稳定的排序算法,平均时间复杂度O(n2),最优为O(n),空间复杂度O(1);对于n个元素的数组,首先遍历第一和第二个元素,若为逆序则交换两者位置;然后比较第二和第三个元素,双重循环重复n次,每次将当前n-i个元素中最大者放在n-i的位置上,n次遍历后完成排序。
选择排序:不稳定的排序算法,时间复杂度O(n2),空间复杂度O(1);每次循环,在无序数组中找到最小值,然后与无序数组中第一个值交换位置,然后在剩下的无序数组中重复该操作,n次遍历后完成排序。
插入排序:稳定的排序算法,平均时间复杂度O(n2),最优为O(n),空间复杂度O(1);首先认定第一个元素为已排序过了,然后对第二个元素开始在已排序数组中遍历一遍,插入到正确位置,然后重复操作。
希尔排序:不稳定的排序算法,平均时间复杂度为O(nlogn),最坏为O(n2),最优为O(n),空间复杂度为O(1);类似于插入排序,首先设定一个初始变量k,通常可以为数组长度的一半,然后将数组中间隔为k的元素划分为一组,对每组进行插入排序,然后k减少例如再减半,然后重复间隔为k的元素分一组进行插入排序,重复操作使数组基本有序,最后一次k一定为1,这时再做一次插入排序,使数组有序,多数情况每次插入都是微调。
归并排序:稳定的排序算法,时间复杂度为O(nlogn),空间复杂度为O(n);归并排序采用了分治的思想,通过空间换取时间,首先将数组划分为n/2个长度为2或者1的子序列,每个子序列自身进行基本排序,然后子序列两两进行归并,归并也类似双指针,两个指针均从起点开始比较,顺序排列,重复两两归并直到全部合并。
堆排序:不稳定的排序算法,时间复杂度为O(nlogn),空间复杂度为O(1);采用完全二叉树的结构实现,大顶堆指根结点以及子树的根结点均大于等于左右结点,小顶堆指根结点以及子树的根结点均小于等于左右结点。首先建立一个堆,然后交换堆顶元素与最后一个元素再重新调整堆结构,直到删除堆中全部元素。
快速排序:不稳定的排序算法,平均时间复杂度为O(nlogn),最坏为O(n2),最优为O(logn),空间复杂度为O(nlogn)。采用分治的思想,一般取出数组第一位作为基准数,然后双指针从左往右找一个比基准数大的,从右往左找一个比基准数小的,然后交换这两位,当双指针在某个元素位置相遇,将基准数与该元素交换,使基准数左边都比基准数小,右边都比他大;然后对左序列和右序列做同样的操作,直到数组升序排序成功。
设计模式
-
单例模式
属于创建型模式,该类不能被复制,一个类中一般只有一个实例对象,并提供唯一一个全局访问接口,该实例被所有程序模块共享,一般可以用于写日志时创建唯一一个日志实例。该类的构造函数无法被公开调用,实例对象一般被定义为静态类成员。
主要用于节省资源提高效率,因为无需频繁创建对象,只需要创建一次对象即可。
一般的实现比较简单,但遇到多线程开发时对于单例模式就需要考虑线程安全的问题,一般可通过在单例类中进行加解锁来提高线程安全。
实现方式一般有懒汉模式和饿汉模式
懒汉模式:第一次用到类实例时才会实例化,也是最简单的实现。遇到多线程时为了提高线程安全则可以通过加解锁方式,但是频繁加解锁可能带来性能损耗,通常可以双重判断实例是否为空,为空再加锁再判断是否创建,因为锁开销较大,避免了频繁加解锁。也可以使用把类实例创建为静态局部变量返回,但是同样也有多线程问题,可以在主线程一开始就调用创建静态局部变量的方法。
饿汉模式:单例类定义的时候就进行实例化,没有多线程问题。但是可能影响程序启动效率。
-
工厂模式
定义一个创建对象的接口,让子例决定实例化哪个类,对象的创建统一让工程类去管理。
简单工厂模式:实现比较简单,一般是在工程类中做判断,然后创建相应对象。当需要增加新的产品对象时,就需要修改工程类。优点是实现简单,能降低系统耦合性。缺点是违背了开放封闭原则:软件实体可以拓展,但是不可修改,因为需要增加新产品时就需要修改工程类。
工厂方法模式:定义一个用于创建对象的接口,一般在工厂父类中创建一个创建对象的纯虚函数,然后让子类来决定实例化哪个类。优点是拓展性好,符合开放封闭原则,新增加一个新产品对象时,只需要增加产品类和工程子类即可。缺点是每增加一个新产品就需要增加一个产品类和工厂子类,实现时需要更多的类定义。
抽象工厂模式:与工厂方法模式主要区别在于工厂方法模式只有一个抽象产品类来派生多个产品类,而抽象工厂模式有多个抽象产品类来派生多个产品类,例如原来只有一个生产水杯的抽象类,现在抽象工厂模式新增了一个生产水壶的抽象类,工厂方法模式中具体工厂类只能创建一种产品对象,而抽象工厂模式可以创建多种,例如生产水杯同时也能生产水壶。