C++基础要点摘记
1.定义是创建与符号关联的实体。声明是让符号为程序所知。
声明变量的同时也就完成了变量的定义,只有声明外部全局变量的情况是例外,当一个文件想要使用其他文件定义的全局变量,则必须声明。
函数和变量的声明不会分配内存, 但是定义会分配相应的内存空间,变量名就是对相应的内存单元的命名
函数和变量的声明可以有很多次, 但是定义最多只能有一次
2.将static全局变量写在头文件中,所有头文件的操作函数都会共享这个变量。但如果是在源文件中去操作这个静态全局变量,则这个静态全局变量只能在当前文件有效,但是在另外一个文件访问此静态变量,会是该变量初始的默认值,不会是其他文件中修改的值,虽然它们有相同的初始内容,但是存储的物理地址并不一样。
一般用法:static修饰全局变量或函数,并未改变其存储位置及生命周期,而是改变了其作用域,好处如下:(1)不会被其他文件所访问(2)其他文件中可以使用相同名字的变量,不会发生冲突(3)全局函数变成静态函数。
static修饰局部变量,一般用在函数中,生命周期从程序开始到结束,作用域是函数的编译单元,每次调用函数操作的是同一个静态变量。
静态成员变量不可以在类内初始化,因为这样会为每个对象创一个,不可以在头文件的类外初始化,因为这样会为每个include了它的源文件创一个(参考此点开头的说明)。在类外初始化时不加static。但是可以用const修饰static数据成员在类内初始化,因为常数据成员。
静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数参数不含this指针。
const成员变量可以在类声明中初始化(类似普通成员变量),或者只能在构造函数的初始化列表初始化,因为const定义的时候必须初始化,不能进行赋值。
static const成员变量可以在类声明中初始化,也可以用类中的无名的private enum替换它定义int的作用。
3. 重载、覆写(重写override)、隐藏、(从名称空间上作答)实例化=生成对象、抽象类一般用作声明函数接口供继承
4. struct和union都是由多个不同的数据类型成员组成,union中只存放了一个被选中的成员,所有成员共用一段内存空间,即对于union的某成员赋值,将会覆盖其它成员,而struct的所有成员都占有自己的内存空间。一个struct变量的总长度等于所有成员长度之和,Union变量的长度等于最长的成员的长度。
类、Struct的对齐:首先根据结构体内部成员的对齐值和系统指定对齐值得到结构体的自身对齐值,取较小的,以此对齐值进行数据放置,与内存补齐,连续的数据可以和并再补齐。(如果一个数据大小不足对齐值的整数倍则放入内存中的时候对它进行补齐,使它占有对齐值整数倍的空间,使得每个数据的开头地址都是补齐值的整数倍)
1.数据类型自身的对齐值 2.结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
3.指定对齐值:#pragma pack(n),n=1,2,4,8,16改变系统的对齐值
4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
5. 多态。动态多态(运行期多态):程序在运行时才决定调用哪个函数(函数地址在运行时而不是编译时根据引用或指针指向的对象与函数符号绑定即动态绑定,动态联编),一个接口,多种方法,实现了父子类的接口复用。封装通过struct\class和三个访问控制权来完成,为了保护内部数据成员,隔离外部干扰,也使代码模块化方便复用。继承可以扩展已存在的代码,为了代码复用。静态多态(编译期多态)是通过重载和模板技术实现。
动态多态由虚函数和继承,虚函数主要通过虚函数表(V-Table)实现。如果一个类包含虚函数,那么就会拥有一张虚函数表(在编译器就建立),虚函数表存储的每一项是一个虚函数的入口地址。对象不包含虚函数表,只有隐藏成员--虚指针(在运行期生成,指向虚表,方便在运行期根据对象查询到底执行哪个函数,即动态绑定。虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),派生类会生成一个兼容基类的虚函数表。
继承的虚函数和自己创建的虚函数,可以看到虚表放在基类对象虚表的位置上。
具体看https://www.cnblogs.com/LUO77/p/5771237.html
www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064更详对象模型,有adjustor。
这些图都是编译器可以查看的类内存布局
多(重)继承:派生类继承多个基类,为每个基类(显式或隐式地默认私有)指定了访问级别。派生类的对象包含每个基类的对象,派生类构造函数的初始化列表(因为要先构造出基类对象)调用基类构造函数(否则将调用基类的默认构造函数,然后可以在函数体内对基类对象部分进行初始化,或者不初始化),基类构造函数按照基类构造函数在类派生声明(而不是初始化列表)中的出现次序调用,析构总是按构造函数运行的逆序调用析构函数。
而基类的析构函数最好写成virtual,否则用基类指针对子类对象delete的时候,无法销毁子类对象资源造成资源泄露(effective C++的建议是在类有至少一个virtual函数时,给类声明一个virtual析构,还说即使没有其他virtual,如果会被继承,还是要virtual析构)。有时候想让一个类成为抽象类但苦于没有哪个函数好作为纯虚函数,可以让析构成为纯虚,而且要定义它,否则链接器会抱怨,既解决了子类对象不销毁问题,又使类成为抽象类。
虚基类(虚继承)让子类对象中只拥有一个共享的基类对象副本(如在菱形继承中,子类的父类都继承了同一个基类(公共基类),导致子类对象中可能出现多个基类对象),解决二义性,即解决了公共基类的多份拷贝的问题。
子类对象中包括各基类对象副本,其中每个基类(MyClassA和MyClassB)和公共基类(MyClass)对象有自己的虚函数表指针,每个虚拟继承了MyClass的父类对象还需要记录一个指向虚基类表vbtable的指针vbptr
注:最下面两层是虚基类,虚基类表第一项记录着当前子对象相对于虚基类表指针的偏移,第二项是虚基类对象地址(即虚基类的虚指针)相对于虚基类表指针的偏移。这样一来每个子类对象都能找到那唯一的虚基类。
用法:istream和ostream类对它们的基类ios进行虚继承,使基类成为虚基类,如果其他类如iostream同时继承它们两个,则派生类对象中仅有一个ios对象副本。继承可以写成:virtual public或者:public virtual。
6.拷贝/赋值构造:如果要为派生类编写拷贝构造(/赋值)函数,则对于拷贝构造来说需要调用基类相应拷贝构造函数并为其传递参数(在初始化列表中),否则将调用基类的默认构造(即无参数构造),使基类对象部分只有默认赋值;对于拷贝赋值来说需要在拷贝赋值函数体中调用A::operator=(B);将参数B的基类对象部分赋给*this。
深拷贝与浅拷贝:
浅拷贝:默认的拷贝构造(/赋值)函数只是完成了对象之间的值的拷贝,也就是把对象里成员的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了堆内存,那A中的那个成员变量也指向同一块内存,这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝:自定义复制构造函数需要将对象成员指向的资源也进行复制,即A有5个空间,B也应该申请5个堆空间,而不是指向A的5个空间。
7.各个排序算法的时间复杂度和稳定性,快排的原理。
冒泡排序改进1:在某次遍历中如果没有数据交换,说明整个数组已经有序。因此通过设置标志位来记录此次遍历有无数据交换就可以判断是否要继续循环(原做法是重复n次)。
冒泡排序改进2:记录某次遍历时最后发生数据交换的位置,这个位置之后的数据显然已经有序了。因此通过记录最后发生数据交换的位置就可以确定下次循环的范围了。
快排优化:①STL里用1of 3,一般用随机基准点。②有大量重复元素的数组中(考虑一个全是重复元素的数组),可能会把数组分割为长度差距很大的两部分,又变成了O(n^2),所以用双路快排,即使碰到基准元素也停下来交换(与快排相反)③三路快排又进行了优化,只需要对小于和大于基准点的范围进行排序即可,中间是等于基准点的,代码:
https://blog.****.net/k_koris/article/details/80585979
选择排序相对于插入排序来说有元素交换,所以不稳定,如[5,8,5,2,9],只有它比较次数与初始排列无关。
调整法建堆时间复杂度是O(n)(推起来很复杂),插入法是O(nlogn)(和红黑树一样)。为什么不稳定:交换堆头尾元素(pop_heap其实可以直接根与最后一个节点交换,再下沉即可。而STL先拿出最后一个节点,将根节点放进最后的洞,让其他下面的节点先上溯,最后一个空洞放原最后一个叶节点,再让它上溯,这样变麻烦了,不知为何)
为什么STL不一开始就用heapsort?因为它每次比较和交换都是将一个元素变得有序,所以次数比快速排序多,常数因子比快速排序大,而快排每一次交换都会使数据更加有序;元素交换的位置相对远,对缓存不友好;快排发生恶化的情况很少发生。
introsort对一段排序前先判断递归深度是否大于logn恶化,如恶化则heapsort并返回,再判断序列长度是否小于16,如小于则放置不排并返回,再进行此段的快排,对左右两端递归此过程,全部返回后再对整个序列进行插入排序,因为基本有序的序列用插入性能比较好。
归并在两个有序序列合成一个的环节,要设置前面序列元素等于后面的时候先放前面的才能实现稳定。
基数排序第一步根据数字的个位放置(分配)到每个桶里,然后将数字按照桶的次序输出(串起来);然后根据十位分桶,继续按照桶的次序再串起来。直至所有位被用完,所有数字已经有序。
8.各容器的特点
allacator是“内存配置器”,通常它是一个类,负责内存管理,STL容器使用拥有层次结构的、高效率的STL配置器类,类中有函数负责内存的分配释放。考虑到小型区块可能造成内存碎片问题,设计了双层配置器,第一级配置器直接malloc和free,第二级配置器利用内存池视申请大小采用不同策略,超128字节则用一级,小于则用二级的内存池,维护16个链表负责16种大小内存块8-128的存储与分配,取决于某个_USE_MALLOC预定义宏是否被定义(默认未被定义,用一级)来辨别目前是用哪种配置器,它定不定义其实是启动一个条件编译,给一级或二级一个typedef成alloc。而STL又给配置器包装了一个simple_alloc接口,接口中对配置器中的allocate和deallocate函数进行了包装,容器都是用这个alloc作为Alloc模板参数的缺省参数值,然后用它作为simple_alloc的模板参数实例化接口。(第二配置器内存池不足就找第一配置器分配,第一配置器内存不足也是类似new的set_new_handler设置一个异常处理函数,函数也没办法就bad_alloc异常。
而对象的构造是全局函数construct(调用调用placement new的new关键字)析构是全局函数destroy(调用类型T的析构函数)。
vector采用线性空间存储数据。如果空间不够,则另外分配新的两倍大小的空间,然后把旧空间释放掉。不适合push_front,不适合中间插入,适合随机访问。需要高效的随机存取,而不在乎插入和删除的效率,使用vector。
vector的reserve只改变capacity。vector.size()表示的这个vector里容纳了多少个元素,capacity表示容器在必须分配存储空间之前可以存储的元素总数(即vector能够容纳多少元素),vector的capacity是2倍增长的。如果vector的大小不够了,比如现在的capacity是4,插入到第五个元素的时候,发现不够了,此时会给他重新分配8个空间,把原来的数据及新的数据复制到这个新分配的空间里,所以会有迭代器失效(它的迭代器就是普通指针)的问题。如果resize()比原size小则删去超过的,如果大则用value的默认构造函数构造对象填充空间。
list内部数据结构是双向环状链表。物理存储空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它没有提供[]操作符的重载,随机存取变的非常没有效率,但由于这个特点,它可以以很好的效率支持任意地方的删除和插入。需要大量的插入和删除,而不关心随机存取,则应使用list。
deque物理上是分段连续的存储空间,由缓冲区buff和一个中控器组成(指向buff的指针的数组),通过中控器将buff连接起来,给上层用户造成一种连续空间的假象。不适合中间插入但适合头尾,也可以随机访问数据,是list和vector的折中方案。需要随机存取,而且关心两端数据的插入和删除,则应使用deque。
deque插入删除中间元素时,会比较到首近还是尾近,来进行数据移动。而在首尾增加数据也可能使迭代器失效的原因是,可能中控器map的首或尾节点(buff指针)指向的buff已经没位置了,且map的首或尾已经没有了节点备用空间,这时将换一个更大的map将节点转移过去。
map是STL的一个关联容器,它提供一对一的数据映射能力,底层是红黑树,键值对用存储在红黑树结点中的pair对象实现。要存储一个数据字典,并要求方便地根据key找value,那么map是较好的选择。要查找一个元素是否在某集合内,则使用set存储这个集合比较好。
表中有误,set和map都不能随机访问,map的以下标访问也是用查找实现的,rbtree迭代器是bidirectional_iterator_tag双向迭代器。rbtree最大\小元素迭代器++\--都指向head,head的left是最小right是最大,parent是root。head迭代器减减是最大元素,++则会向右一下再一直向左,因此到中间某个不能事先确定的元素。
Input Iterator和Output Iterator不同的是,Forward Iterator可以对访问过的元素进行访问(如果前面保存了迭代器的话),但输入输出迭代器就不保证解引用得到的是正确结果。
map/hash_map迭代器指向的是底层RBTree/hash table存储的key-value pair。
hash底层需要设置(通过模板参数,key为内置类型除外)hash()和equal_key(),RBTree需要设置compare()(默认是less<key>,这个模板里面还是用的’<’ ’>’,所以只要重载过符号就行),所以还是map而非hash_map方便一点。
9.makefile
一个文件依赖库a,库a依赖库b,写makefile的时候,a要放在b的前面还是后面
a放b前面如gcc -o bin A B,链接时会把a所需要的符号找出来,然后继续往后走,找这些符号有没有出现,如果走到末尾还没出现,就报错(前后上下顺序都需如此,所以后面的makefile例子是可执行文件在最上面)。如果a和b互相依赖呢?尝试:bin a b a
makefile关系到了整个工程的编译规则,(一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中),需要makefile定义一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要依赖哪些文件编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以编写操作系统的命令。
makefile带来的好处就是——“自动化编译”,极大的提高了软件开发的效率,一旦写好,只需要一个make命令,整个工程完全自动编译。make是一个命令解释工具,是一个makefile中指令的解释器。make命令执行时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接程序。
首先,我们用一个示例来说明Makefile的书写规则。我们的规则是:
1)如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
2)如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
3)如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。
只要我们的Makefile写得够好,把这个内容保存在文件为“Makefile”或“makefile”的文件中,然后在该目录下直接输入命令“make”,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。
target:dependence下面一行是命令,表示如果有文件比target文件要新的话,command所定义的命令就会被执行。
clean可以写成clean:rm prog.o code.o(内容任加),是执行make clean将执行的命令。
gcc -c test.c将生成test.o的目标文件
gcc -o app test.c将生成可执行程序app
gcc -c a.c -o a.o表示把源文件a.c编译成指定文件名
10. 非静态的成员函数必须被绑定到一个类的对象或者指针上,才能得到被调用对象的this指针,然后才能调用指针所指的成员函数(所有类的对象都有自己成员变量,但是成员函数都是共用的,为了区分是谁调用了成员函数,就必须有this指针,this指针是隐式的添加到函数参数列表里去的)。
11. A* arr=new A[10];此时A的默认构造函数就被调用了10次,因为之后A[n]是对一个已经创建的具体对象的操作。
12.
每个进程都有各自独立的4G逻辑地址,其中0~3G是用户态空间,3~4G是内核空间
静态区域:
text segment(代码段):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
data segment(数据段):存储程序中已初始化的全局变量和静态变量
bss segment:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一初始化为0。
动态区域:
heap(堆):需要自己动态开辟,和释放,可运行时确定大小,调用malloc时分配一个堆,允许在程序运行时才决定需要多大内存(动态分配内存),从低地址向高地址增长。 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,系统会在这块内存空间中的首地址处记录本次分配的大小,这样,delete语句才能正确的释放内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
stack(栈):由系统自动分配,程序员无法控制,使用栈空间存储局部变量、函数的返回地址、参数、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小。
13. C++函数中参数的传递方式有指针传递、值传递、引用传递(1.比指针更安全2.节省了赋值给形参的时间空间开销)(C中无论是指针、数组都是值传递,实参的值赋给形参)
14. struct和class区别在于默认的继承访问控制权限、默认的成员访控权限、struct可以用{}初始化、class还可以作为模板参数,比如template <class T, int i>。
15. C和C++有什么不同?1.设计思想2.特性3.语法4.场景、效率
设计思想上:C是面向过程的结构化编程语言,侧重于解决问题的逻辑设计,基本单元是模块(即函数把功能细分,一个一个功能编写),C++是面向对象的的语言,侧重于类的设计,基本单元是类/对象(把复杂业务抽象为类,让类相互产生关系,进行编程,更灵活,复用与扩展性更强)。 C++兼容C,是它的扩充。
语法上:C++具有封装、继承和多态三种特性。C++有new delete、引用、类、函数重载。C++相比C,增加多许多类型安全的功能,比如类型转换。C++支持范式编程,比如模板类、函数模板等。
场景:C语言用在效率高的场景(一般是直接访问硬件),如嵌入式开发、操作系统的内核;C++用在比较上层、复杂的场景,如桌面应用软件、服务器后台。
要用特定的语言完成它所擅长适合的场景的任务。
16. ::operator new是一个关键字(C++语言预留的标识符)或者说运算符,在堆上分配一块内存,并会自动调用类的构造函数。它包括负责堆内存分配的可重载的类内operator new函数,和调用类构造函数,两个部分。 重载类内operator new,并且增加额外的参数,但是必须保证第一个参数必须是size_t类型,它指明了分配内存块的大小。
类内operator new可同时重载placement版本和普通版,它们不同就是size_t参数之后的参数。
内存不足时在抛出bad_alloc异常前如果客户set_new_handler()了一个异常处理函数,那么就先调用它。
调用格式都是 new(size_t之后的参数)类型名
17. 全局对象的构造函数会在main函数之前执行,以此可以实现在main前调用函数。
18. 语法是inline与函数定义体放一起,调用处直接展开,免除调用(寻址入口、运行代码区跳转),回调的开销,提高效率。内联函数(C++)会做参数类型检查与就地展开(编译期),带参数的宏(C)只做文本替换(预编译期),所以内联安全。宏不是函数而内联是函数。宏在定义时要小心处理宏参数,(一般情况是把参数用括弧括起来)。
类内定义的函数都为内联,编译器也会进行一些自动优化,将一些短小函数内联。
18. 具体的:
- define不会做类型检查,const拥有类型,会执行相应的类型检查
- define仅仅是文本替换,不为它分配内存,而const参与编译运行,会为它分配内存,常量在程序的常量表中。
19. C++预定义宏__FILE__ __DATA__ __TIME__ __LINE__ __CPLUSPLUS。
19. 局部变量,定义时创建,编译单元结束时销毁。全局变量从定义到源程序结束。
静态变量伴随整个源程序。操所系统通过内存分配的段(栈、数据段/bss)来辨别。
20. malloc与free是C/C++标准库函数,new/delete是C++关键字,都可以申请/释放动态内存,但前者是库函数,没有调用非内部数据类型的构造/析构的部分,C++是面向对象的语言,需要调用进行对象的创建和销毁,所以C++需要后者。free可能会造成资源的泄漏,特别是在继承链中。
new的参数是数据类型,malloc的是字节大小。new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
new内存分配失败抛出bad_malloc,malloc内存分配失败返回NULL值。
malloc分配的内存不够的时候可以使用realloc扩容,new没有这样的操作。
21.extern用于声明文件外部的一个全局变量。
21. ①访问寄存器要比访问内存要块,因此CPU会优先访问该数据在寄存器中的存储结果,但是内存中的数据可能已经发生了改变,而寄存器中还保留着原来的结果。为了避免这种情况的发生将该变量声明为volatile,告诉CPU变量是易被改变的,每次都应该从内存去读取数据。②一个参数可以即是const又是volatile的吗?可以,一个例子是只读状态寄存器,是const告诉程序不应该试图去修改,是volatile因为它可能被意想不到的被改变。
21. sizeof(p)比sizeof(int)来判断OS的位数更靠谱,因为64位OS的int也是4,但p为8,(但是还和编译器模式有关)。如果不允许用sizeof则用unsigned int a=~0;
22. 很多时候直到运行时才知道需要多大内存(动态分配内存),所以需要堆。
23. Debug为调试版,包含大量调试信息(比如断点),不做任何优化,体积较大便于调试程序。
Release称为发布版,进行了优化使程序代码大小和运行速度都最优,便于用户直接运行使用。
24. ASCII有些字符不能从键盘输入,用\+八进制数又不方便记忆,所以有\+字母如n的转义字符。
25. 引用与指针,本质上指针是地址,引用是别名1.引用定义时必须被初始化,指针不必,所以引用比指针使用起来更安全2.引用初始化后不能改变,指针可以3.不存在空引用但存在空指针4.指针作为参数是值传递,引用是引用传递
场景:能用引用尽量用,相对更安全高效(指针有可能变野,引用可能指向被释放的栈空间),引用作为函数参数不用将实参对象拷贝为形参传入,节省开销,作为返回值,如重载输出运算符函数的返回值(返回),也是节省开销。如果使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,或者改变指向的对象,比如链表用指针实现,这时应该把变量声明为指针,因为这样可以赋空值给该变量。
26. 用常量方便修改多个地方的变量值,如果直接在表达式中写数字,会使可读性变差或出现书写错误,且更改麻烦。
27. 构造函数为什么不能是虚的。构造函数是虚函数就需要通过指向虚函数表的vptr来调用,可是对象还没有实例化,也就是内存空间还没有,找不到虚表指针,这是一个死锁。
28. 设计一个类只能在堆上创建,可以将构造、复制构造、赋值设为私有,再创建类内静态函数return new。设计一个类只能在栈上创建,可以将operator new和delete设为private或=delete,或者类似只在堆上创建的类的设计。
29. virtual函数返回值可以是基类虚函数返回值的子类或子类指针,定义了缺省参数值的参数的值是静态绑定,所以不要重新定义virtual函数的缺省参数值。
30. C++不具备类型安全(不同类数据不能相互转化),即不是强类型语言(如int/char可以作为char/int操作,指针可以强转)
31. virtual--多态、抽象类。
32. const ①修饰局部/全局变量,常量,需要初始化 ②常量指针或常量的指针 ③const修饰函数形参使参数不能改变、const修饰引用作为函数参数使得不能改变(可以处理const和非const实参)且如果实参不匹配(或者为一个左值如一个数字或表达式)可以创建临时变量转换成参数规定类型(如double转int,这是函数参数的普通隐式转换所做不到的)
类中:④const修饰成员变量,需要在构造函数中初始化或类定义中默认初始化⑤const修饰成员函数为常函数,隐式传入const *this,不可修改非静态成员变量 ⑥成员函数返回const表明返回的不是左值
33. 空类sizeof为1,因为要为对象申请内存空间,而申请的最小单位为1。
34. 函数模板模板参数只能作为形参、返回参,实例化时的模板参数是由编译器在处理函数调用时根据调用传入的参数自动指定的(模板参数自动推导)(也可以显式指明)。类模板可以将成员变量、成员函数作为模板参数,实例化的模板参数必须由程序员在类声明中显示指定。
35. 重载主要靠函数签名(函数名、形参)区分、调用函数,而与返回值无关。
函数参数不匹配时都会隐式转换成正确类型(如果可能的话,如int转double,反之不行)。
36. 模板是C++实现泛型编程的机制(不考虑数据类型只考虑逻辑),编译时检查数据类型,保证安全性,有很高的复用性,体现了通用和泛化的思想。容器是STL中特定的模板类。
38. a++不能作为左值,而++a可以。
39. return的对象不能是指向栈内存的指针或引用,否则出现野指针或引用一个不确定的量,返回对象要考虑效率,因为会复制临时变量。
40. 不能重载’.’,不能重载非运算符,重载不能改变运算符优先级。
41. void*可以转为任意,但任意不能转为void*,必须强制。void*因为不知类型,所以不能++。 函数参数可以用void* 。
42. 内联函数(调用处直接展开成一段代码,不存在寻址,编译期要确定,写了virtual编译器就不把它当内联了)、静态成员函数(属于整个类而不是具体对象,不被继承,在子类中也可调用但受权限控制,即和在类外访问无区别)、友元函数(不是成员不涉及继承)、构造函数、非成员函数都不可以是虚函数。
43. 空类默认产生:缺省构造、缺省复制、析构、赋值函数
44. return string(s1+s2);编译器直接把这个临时对象创建并初始化到外部存储单元中,省去了拷贝和析构的成本。因为移动语义。
45. vector本质上是一个会自动增加长度的动态数组。
46. 异常--运行时产生的问题,在C++中表示为异常对象,throw的异常必须事先定义,是一个值或对象。用try、catch捕获。抛出异常时,将停止当前函数的执行,释放try块内存,检查与该try相关的catch子句,看是否可以处理该异常。如果不能处理,就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到上层的调用函数中查找,直到找到一个可以处理该异常的catch。这个过程称为栈解退。找到匹配再运行catch编译单元中的代码。
47. C++和Java之间的区别?
Java的应用在高层,C++在中间层和底层
运行效率上比不过C++
Java语言简洁;取消了指针带来更高的代码质量;
完全面向对象,java运行在jvm上(jvm可以做到与OS或CPU无关),独特的运行机制使其具有可移植性非常高。
Java在web应用上具有C++无可比拟的优势
垃圾回收机制的区别。C++用析构函数回收垃圾,Java自动回收,写C和C++程序时一定要注意内存的申请和释放。
Java用接口(Interface)技术,C++程序中需要继承抽象类
48. 从语法上来说,构造函数和析构函数都可以抛出异常。但从逻辑上和风险控制上,构造函数不建议,析构函数不可以抛出异常。
①构造函数中抛出异常,会导致析构函数不能被调用,可能导致堆内存泄漏或系统资源未释放(此时成员变量的内存已释放了),必须保证在构造函数抛出异常之前,把资源释放掉(可以用shared_ptr),防止内存泄露。
①如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。②某一个异常发生时,c++的栈解退(寻找匹配异常类型的catch)会调用局部对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序过早结束或不明确行为。
①如果某个操作可能会抛出异常,class应提供一个普通函数(而非析构函数),来执行该操作,此操作可以客户执行,给客户一个处理错误的机会。②如果析构函数中异常非抛不可,那就用try catch来将异常吞下,必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外。
49. 智能指针auto_ptr采用所有权模式,对象只能被一个指向,可能赋值产生悬挂指针,被摈弃。
unique也是所有权模式,但不能赋值,是auto的替代,比它更安全。
shared共享所有权模式,计数机制,引用计数为0时,资源被释放,可以另外三种指针构造,.get()返回真实指针,解决了unique的局限性。
weak指向一个 shared_ptr 管理的对象,只是提供了对管理对象的一个访问手段,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用计数的增加或减少。目的是为配合 shared_ptr工作, 解决shared_ptr环形引用(如相互引用)时的死锁问题。
50. 类析构顺序:派生类本身的析构函数;对象成员析构函数;基类析构函数。
51. 必须在构造函数初始化列表进行初始化的const成员、引用成员、没有默认构造函数的类成员。
52. 四种类型转换
const_cast用于将const变量转为非const
static_cast用的最多,用于各种编译期隐式转换,非const转const,void*转指针等, static_cast能用于多态指针的向上转化,因为是编译期向下转能成功,但是不安全,没有运行时类型检查;
dynamic_cast用于动态类型转换,有rtti运行时类型检查。只能用于包含虚函数的类层次结构,用于类层次间的转指针或引用的向上转化和安全的向下转化(指向一个比将要转化为的指针的类更下层的对象时)。不安全则返回空指针,引用则返回bad_cast异常。
reinterpret_cast几乎什么都可以转,比如将int转指针,但这是非常危险的,可能会出问题,尽量少用;
为什么不使用C的强制转换?C的强制转换表面上看起来功能强大什么都能转,但是转化太过松散不够明确,不能进行类型安全性检查。
53. RTTI是运行时类型识别,只能用于包含虚函数的类层次结构,对于没有虚函数的层次结构,编译器一律按照静态联编(rtti通过虚函数表指针对应到虚函数表根据表中的type_info对象的指针判断类型,所以必须要虚函数),有3个支持的运算符dynamic_cast已经说了。typeid(对象或类)函数,返回一个重载了==的type_info对象,可判断指针指向的俩对象是否同类。多态类的对象的类型信息保存在虚函数表的索引的-1的项中,该项是一个type_info对象的地址,该type_info对象保存着该对象对应的类型信息,每个多态性质的类都对应着一个type_info对象。
54. C++中的NULL就是0或者void*(0),会有问题,如fun(NULL);会匹配到参数为int的重载函数,所以要nullptr。
55. 命名空间可作为附加信息来区分不同库中相同名称的函数、类、变量等。
56. explicit关键字用在构造函数前面,使带一个参数的构造函数的参数类型对应的变量不能隐式转换为该类对象。
57. 野指针指向非法的内存地址:指向的对象已消亡且指针未被置空,如被释放的堆,或超出作用域已被释放的栈,或未初始化时乱指一气,或指向不可访问的系统空间。
58. 用C实现C++多态,父struct中有个函数指针,可以在子struct里有一个父struct放在第一位,其中的父struct里的函数指针指向定义为需要重写的子函数,将父struct指针指向子struct再调用函数就是子struct的函数了。
59. 脚本语言,计算机对代码进行解释执行而不是编译,这是在运行时完成的,每次运行都要跑一遍这个解释的过程。这样修改程序起来很方便:直接改代码就好了,不需要编译。常见的python、shell脚本、javascript。
60. strcpy是C语言里的标准库函数,strcpy把从src地址开始且含有’\0’结束符的字符串复制到以dest开始的地址空间,返回值的类型为char*。原型为
char* strcpy(char* dest,const char *srcstr);
说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
返回指向dest的指针。
61. LRU页面置换算法C++模拟实现
https://blog.****.net/Monster7559/article/details/99297407
list.splice();
62. python是脚本语言,比较方便地跨平台,适合写小工具小程序,C++是编译语言,运行效率高,稳定安全,一般不跨平台,因为用到了OS的系统调用、库、中断调用。
63. C++11新特性 部分具体化==偏特化
①统一的初始化方式,大括号初始化列表可以用于所有对象(对应构造函数的参数列表)、STL容器、可以用于new表达式。
②auto变量自动类型推导,decltype(x) y编译期推导表达式类型。auto、->和decltype使返回类型后置。
④智能指针 ⑤加入explicit关键字
⑥可在类定义中初始化成员
⑦委托构造函数:类构造函数初始化列表中调用自己类内的另一个构造函数。
继承构造函数:using让子类同名函数不会隐藏父类函数。而在子类中用using 基类名::基类名,继承基类的构造函数,实例化时子类可直接将基类参数列表相同的构造函数当做自己的构造函数用。如果子类中定义了参数列表相同的构造函数,那就不会用到继承的版本。
基于范围的for循环 for(auto& x:vector);
lambda表达式可以在被调位置直接创建匿名函数对象,并加以定义,或有名的,一般用在定义比较短小的函数。
都是为了简化编码工作。
⑧可变参数模板,使模板类/模板函数可接受可变数量的参数,
template<class T,typename...Args> void show(T value,Arg...arg); 配合递归
模板参数包Arg是一个类型列表与任意数量的类型匹配,函数参数包arg是一个值列表。
⑨允许默认模板参数
⑩using MyVec = std::vector<T, myalloc<T>>;已经实例化的模板的别名
11.左值引用关联左值,右值引用可关联右值(字面值、表达式的字面值、函数返回的临时变量),用&&表示,关联之后该右值被存储到特定位置,可用&看地址。主要为了实现移动语义。
移动语义,在一些对象的构造时可以获取到右值对象已有的资源(如堆)而不需要通过拷贝到临时变量对象,申请新的内存,这样将拷贝加销毁替换成直接接管资源的做法,将会避免额外的工作,大幅度提升性能。例如有些右值即将消亡析构,这个时候我们用移动构造函数而不是复制构造函数,可以接管他们的资源,移动构造函数内还是像复制构造一样赋值,只不过传进来的右值对象中的指针要置空,防止delete堆两次。移动赋值函数也是一样的道理,只不过一个是赋值时遇到右值,一个是调用拷贝构造时遇到右值就用移动构造函数(可以回忆一下用到拷贝构造的三个情况)。
std::move()强行使一个左值变为右值,方便调用移动构造函数接管资源。
12.可以用=delete而非private禁用函数,可用=default显式声明默认函数
13.override加在参数列表后,表示自己是重写版本,final表示子类不可重写了。
64.什么时候调用拷贝构造函数:①用一个对象初始化另一个时②函数传参参数③函数返回
65.函数调用压栈过程:参数入栈:将参数从右向左依次压入系统栈中。返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。EBP ESP:跳转指令进入函数后,当前EBP值入栈,然后把当前ESP的值给EBP。函数内:执行函数内的代码,如局部变量入栈。调用执行函数完毕,局部变量var3,var2,var1一次出栈,EBP恢复原值,返回地址出栈,找到原执行地址,param1,param2,param3依次出栈,函数调用执行完毕。 ESP EBP https://www.cnblogs.com/roy-blog/p/6367093.html 函数栈帧
66. 如何避免内存泄露(堆内存、系统内存(文件描述符未close释放))①在C++中避免内存泄漏的最好方法是尽可能少地在程序级别上进行new和delete调用。②任何动态内存的需求都应该隐藏在一个RAII对象中,在构造函数中分配资源并在析构函数中释放资源,这样当变量离开当前范围时内存就可以被释放。③始终在new和delete之间编写代码,通过new关键字分配内存,通过delete关键字释放内存。④尽量不要手动管理内存,在适用的情况下使用智能指针(它是外部分配资源,构造函数中获取的RAII对象)。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
66. 内存溢出:1、数组越界。2、栈空间不够用溢出。
67. 检测内存泄露,_CrtDumpMemoryLeaks();通常应恰好放在程序退出位置之前,来转储内存泄漏信息,将在“输出”窗口中显示内存泄漏信息。 Memcheck 检测工具
68. 程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫做Core Dump。诊断和调试程序是非常有帮助的,因为对于有些程序错误是很难重现的,例如指针异常,而core dump文件可以再现程序出错时的情景。
写log方便查错,变量、环境变量、外部参数等等。二分法查错。
69. 零拷贝和对象复用
服务器网络编程,不熟,大概说一下,将磁盘上的文件传输到网络上,操作系统则首先将磁盘上的数据拷贝到内核缓冲区,这一步目前主要依靠DMA来传输,然后再read系统调用把内核缓冲区上的内容拷贝到用户缓冲区中,接下来,write系统调用再把用户缓冲区的内容拷贝到socket缓冲区中,最后socket再把内核缓冲区的内容发送到网卡上。两个系统调用是需要cpu拷贝数据的,但没必要,零拷贝技术直接把内核缓冲区描述符和数据长度传到socket缓冲区,DMA控制器直接将页缓存中的数据打包发送到网络中。
https://www.jianshu.com/p/d4dc636591bd第二十题
70. C++语言本身并没有提供多线程机制,当然目前C++ 11新特性中,已经可以使用std::thread来创建线程了,但Windows系统为我们提供了相关API,我们可以使用他们来进行多线程编程。CreateThread可以指定线程函数起始地址(线程被创建则开始执行),返回线程句柄或NULL,CloseHandle关闭句柄。
71. C++允许程序员通过指针以极大的灵活性处理内存。C++极大的优点是几乎所有东西都可以改写为程序员自定义的形式或行为,如运算符重载。
72.当hashtable中元素的总数大于bucket的数量时进行扩容到质数表中下一个质数与rehash。
73.作为类的成员函数,重载运算符只能有一个参数。
当重载的类运算符需要考虑运算符左右参数的顺序,如把*this对象放在运算符后面时,需要将运算符函数定义为友元函数,提供类成员函数对对象成员的访问权限并且不是成员函数。