腾讯后端开发C++
腾讯后端开发 C++
来自伟大的ACM集训队群前群主
32位机和64位机中数据类型的区别
- linux下,long和指针所占字节不一致,都从4byte变成8byte
- windows下,指针所占字节不一致,从4byte变成8byte
代码在内存中的分布都有哪些区?宏定义存在刚才你说的哪个区域?堆栈有什么区别啊?堆中的数据会回收吗?
- 5个区,堆、栈、自由存储区、全局/静态存储区、常量存储区
- 栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数、函数返回信息等。 对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
- 堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制。堆内存用来保存类和对象。一般一个new就要对应一个delete。如果没有被释放掉,那么在程序结束后,操作系统会自动回收。
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向。堆都是动态分配的,没有静态分配的堆。堆是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间,就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
- 全局/静态存储区,全局变量和静态变量被分配到同一块内存中
- 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)
- 宏定义没有分配在哪个区,因为宏只是在编译前的预处理做文本替换
- 堆中的数据在程序运行过程中不会自动回收,全靠程序逻辑回收,程序结束后被操作系统回收。
引用
- 引用是C++语法做的优化,引用的本质还是靠指针来实现的。引用相当于变量的别名。
- 引用可以改变指针的指向,还可以改变指针所指向的值。
- 声明引用的时候必须初始化,且一旦绑定,不可把引用绑定到其他对象;即引用必须初始化,不能对引用重定义;对引用的一切操作,就相当于对原对象的操作。
顶层const,底层const
这个是常识了
const int* p=&a; //底层 int* const p=&a; //顶层
volatile
- volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt;
- 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
- volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i 的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i 是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。
- 一般说来,volatile用在如下的几个地方:
- 中断服务程序中修改的供其它程序检测的变量需要加volatile
- 多任务环境下各任务间共享的标志应该加volatile
- 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义
- volatile跟const一样,也有顶层底层之分
- 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
左值、右值
- 左值代表一个在内存中占有确定地址的对象,右值可以在内存也可以在CPU寄存器。一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
- 函数并不是只能返回右值,也可以返回左值。C++从函数中返回左值的能力对于实现一些运算符重载时很重要。
- 不是所有的左值都可修改,比如有const限定的就不行
- 通常来说,语言构造一个对象的值要求右值作为参数
- 解引用可以把右值转化为左值,而取地址符&拿左值作为参数得到一个右值
左值引用、右值引用
- 左值引用的语法是
type &Name = lvalueExpression
- 右值引用的语法是
type &&Name = rvalueExpression
- 右值引用的意义在于延长右值的生存期。因为右值在表达式结束后就会消亡。如果想继续使用右值,那就会动用昂贵的拷贝构造函数。
- 右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
- 右值引用是对临时对象的一种引用,它是在初始化时完成引用的,但是右值引用不代表引用临时对象后,就不能改变右值引用所引用对象的值。仍然可以在初始化后改变临时对象的值。
- 如果一个右值引用有名字,那么它是左值,否则为右值
- 左值引用的语法是
malloc/free和new/delete有什么区别?(malloc: 咩力克)
- malloc/free是C语言提供的系统函数,需要头文件支持;new/delete是C++关键字(运算符),需要编译器支持。
- 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
- new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void* ,需要通过强制类型转换将void*指针转换成我们需要的类型。
- new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现),然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作,所以得到的一片新内存中,其值将是随机的。
- new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存,操作系统中有一个记录空闲内存地址的链表,当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
- C++允许重载new/delete操作符,malloc不允许重载。
- new内存分配失败时,会抛出bad_alloc异常。malloc分配内存失败时返回NULL。
- 如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,结果也会导致程序出错。
- 空指针可以释放无数次,相当于什么都没有做
- delete指针之后应重设指针的值为nullptr,否则指针会指向之前的地址,变成悬垂指针。
- 内存泄漏对于new和malloc都能检测出来,而new可以指明是哪个文件的哪一行,malloc不可以。
重写(override)和重载(overload)的区别
- override是指派生类重写基类的虚函数,重写的函数必须有一致的参数表和返回值。
- overload约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。
- 相同参数不同返回值可以重载吗?不能
函数隐藏、函数覆盖
- 函数隐藏是指派生类中函数与基类中的函数同名,但是这个函数在基类中并没有被定义为虚函数,这种情况就是函数的隐藏。所谓隐藏是指使用常规的调用方法,派生类对象访问这个函数时,会优先访问派生类中的这个函数,基类中的这个函数对派生类对象来说是隐藏起来的。 但是隐藏并不意味这不存在或完全不可访问。
- 函数覆盖特指由基类中定义的虚函数引发的一种多态现象。在某基类中声明为
virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual
函数返回类型 函数名(参数表)
{函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。函数覆盖条件有三:
- 基类中的成员函数被virtual关键字声明为虚函数
- 生类中该函数必须和基类中函数的名称、参数类型和个数等完全一致
- 将派生类的对象赋给基类指针或者引用,实现多态
- 函数覆盖(多态)实现了一种基类访问(不同)派生类的方法。我们把它称为基类的逆袭。
类和结构体的区别(博主一般以类是引用类型,结构体是值类型入手)
- 最本质的区别是继承访问权限。到底默认是public继承还是private继承,取决于子类而不是基类。
- struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的
- class这个关键字还用于定义模板参数,就像typename。但关键字struct不用于定义模板参数
怎么禁止隐式转换
- 使用explicit关键字,在类的构造函数前面加上该关键字就能禁止隐式转换。
字节对齐,怎么让编译器按指定大小对齐的?
- 许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4的倍数,这就是所谓的字节对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。
- 内置类型的自身对齐模数(有符号无符号相同) char 1 short 2 int 4 float 4 double 8
- 自定义类型的自身对齐模数等同于其成员中最大的自身对齐模数
- 通过预编译命令#pragma pack(n)来指定对齐模数,n为2的整数幂
- 有效对齐模数:指定对齐模数与类型自身对齐模数的较小的值,就是实际生效的对齐模数。
- 字节对齐的细节和具体编译器实现相关,但一般而言,满足三个准则:
- 结构体变量的首地址能够被其最宽基本类型成员的大小所整除
- 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节
- 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节
- 在C++类里在没有任何数据类型变量的时候,会有一个字节的空间占用,如果有数据类型变量就会有字节对齐
C++当中一个class,拥有一个char和int,加起来占用多大内存空间?
- 我的电脑跑出来是8byte,因为字节对齐,所以4+4==8
- 首先,类大小的计算遵循结构体的对齐原则
- 类的大小与普通数据成员有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响
- 虚函数对类的大小有影响,是因为虚函数表指针带来的影响。有虚函数表会有一个指向虚函数表的指针,这个指针受字节对齐影响。不管有没有发生虚函数的覆盖或者增加,大小计算方法不变。继承了多少个类,就会有多少个虚函数表,就会有多少个指针。
- 虚继承对类的大小有影响,是因为虚基表指针带来的影响。不管是否虚继承,gcc都是将虚表指针在整个继承关系中共享的,不共享的是指向虚基类的指针。更多看这里。
- 空类的大小是一个特殊情况,空类的大小为1。C++标准规定,凡是一个独立的(非附属)对象都必须具有非零大小,不同的对象不能具有相同的地址。
- 静态数据成员之所以不计算在类的对象大小内,是因为类的静态数据成员被该类所有的对象所共享,并不属于具体哪个对象,静态数据成员定义在内存的全局区
- 有两种特殊情况需要注意:
- 第一种情况,涉及到空类的继承。当派生类继承空类后,派生类如果有自己的数据成员,而空基类的一个字节并不会加到派生类中去。
- 第二种情况,一个类包含一个空类对象数据成员。这种情况下空类的1字节会被算进去,而且受到字节对齐的影响。
vector
- vector底层实现为数组。有三个指针:_Myfirst, _Mylast, _Myend。first和end指向数组的一头一尾,last指向现在用到的位置。size=last-first, capacity=end-first,分别对应于resize()和reserve()这两个函数
- vector是一个封装了动态大小数组的顺序容器,按照严格的线性顺序排序。可以通过元素在序列中的位置访问对应的元素。
- vector提供了在序列末尾相对快速地添加/删除元素的操作。
- push_back是O(c)的,这是因为vector扩容是按照倍数扩大来保证的。如果扩容是固定大小扩容,push_back就会变成O(n)的。如果以成倍方式增长,假定插入n个元素,倍增因子为m。那么要完成这n个元素的push_back,需要重新分配\(\log_mn\)次内存,第i次重新分配会复制\(m^i\)个旧空间的元素,总时间复杂度约为\(\frac{nm}{m-1}\)。均摊为常量。如果以大于等于2倍的方式扩容,下一次申请的内存会大于之前分配内存的总和,导致之前分配的内存不能再被使用。所以其实大于1小于2是最合适的。
- vector预留空间不足时,push_back之前会先调整vector内存大小,效率会变得很低。
- vector使用一个内存分配器对象来动态地处理它的存储需求。vector的内存管理策略是:一旦空间不足,则增长一倍。重新分配内存时会拷贝当前已有的所有元素到新的内存区域。如果已有元素很多,这个操作将变得非常昂贵。如何避免重新分配内存?使用reserve。该函数会分配一块指定大小的空间,但不进行任何初始化,所以分配出来的空间不包含元素,也就不能访问。然后用同样的方式使用push_back函数,此时只要不超过之前reserve的空间,vector不会进行内存重新分配,只是简单的依次往后摆放。
- 创建一个
vector
,里面存了5个元素1 2 3 4 5
,把迭代器指向 5,然后在 vector 的最前面插入一个 0 ,问刚才那个迭代器指向几?写了,答案是4。插入前后迭代器地址不变。 - vector的容量增长的题目,vector a; push_back八次对象,求总共调用多少次拷贝构造函数。答案是4次。
list
- list底层是一个双向链表(某些版本的STL里是双向循环链表),支持快速增删。相比双向链表结构的好处是在构建 list 容器时,只需借助一个指针即可轻松表示 list 容器的首尾元素。
- list节点包含指向上一个节点的prev指针,指向下一个节点的next指针,存储值的成员变量myval
- list每分配一个元素都会从内存中分配,每删除一个元素都会释放它占用的内存.
- 迭代器的移动就是通过操作节点的指针实现的。
- 为了更方便地实现 list 模板类提供的函数,该模板类在构建容器时,会刻意在容器链表中添加一个空白节点,并作为 list 链表的首个节点(又称头节点)。使用双向链表实现的 list 容器,其内部通常包含 2 个指针,并分别指向链表中头部的空白节点和尾部的空白节点(也就是说,其包含 2 个空白节点)
- list<指针>完全是性能最低的做法,这种情况下还是使用vector<指针>好,因为指针没有构造与析构,也不占用很大内存指针>指针>
set
- set是有序集合,内部数据结构为红黑树
- 插入和查找效率都是\(\log{N}\)的,仅仅需要指针操作节点即可完成,不涉及到内存移动和拷贝,map也一样。插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点也OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。
- 每次insert之后,以前保留的迭代器不会失效。删了才会失效,而且删了的那部分,迭代器指向的内存和值都没变。
- 存储自定义类型时需要重载<
map
- 内部数据结构也是红黑树,查询很快,插入较慢(因为要维护红黑树)
- 存储自定义类型时需要重载<
unordered_map (C++11)
- unordered_map记录元素的hash值,根据hash值判断元素是否相同。即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储(用红黑树实现),进行中序遍历会得到有序遍历。所以使用时map的key需要定义operator<。而unordered_map需要定义hash_value函数并且重载operator==。
deque
双端队列。底层数据结构为一个中央控制器和多个缓冲区,支持首尾(中间不能)快速增删,也支持随机访问。
deque也是在堆中保存内容的.它的保存形式如下:
[堆1] --> [堆2] -->[堆3] --> ...
每个堆保存好几个元素,然后堆和堆之间有指针指向。
stack和queue
- 他们都是对容器的再封装,所以应该叫适配器。底层一般用list或deque实现,然后封闭头部即可。
priority_queue
- 优先队列,底层用堆来实现,队首元素一定是优先级最高的一个
STL容器的数据实际存在什么位置?
- 比如vector,指针会存在栈里,元素会存在堆里。
STL迭代器什么情况下会失效,各个容器都说一下
- vector在push_back和pop_back时都会引起迭代器失效。erase迭代器失效是在删除一个元素的时候,后面的元素要向前挪动,所以迭代器指向的位置就会被前面的覆盖,这时候++迭代器,就会跳过删除元素的后一个。正确用法是不要在循环里it++,erase方法会返回被删除的元素的下一个元素的迭代器。
- list迭代器失效也发生在erase中。当迭代器指向的节点被删除后,迭代器++会失效
- 增加元素时,对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效;对于list和forward_list,所有的iterator,pointer和refercnce有效。
- 删除元素时,对于vector和string,插入点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的;对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失效,其他的iterators,pointers,references有效;对于list和forward_list,所有的iterator,pointer和refercnce有效。
- 在循环中refresh迭代器,当处理vector,string,deque时,当在一个循环中可能增加或移除元素时,要考虑到迭代器可能会失效的问题。一定要refresh迭代器。
- 在循环不变式中不要store off-the-end迭代器。增加或移除元素之后,off-the-end失效了,不store的话,每次从end()函数中取的都是最新的off-the-end,自然不会失效。
Algorithm里面有哪些内容
- 挑一些常用的说
- 不修改序列的:all_of, for_each
- 修改序列的:fill, swap, reverse, unique
- 排序:sort
- 二分查找:upper_bound, lower_bound, binary_search
- 堆操作:is_heap, make_heap, push/pop/sort_heap
- min/max
- next/prev_permutation
深拷贝浅拷贝
- 简单来说,浅拷贝是增加了一个指针,指向原来已经有的内存;深拷贝是增加一个指针,开辟一块新的内存,让指针指向这块新内存。
- 在多个指针指向同一块内存时,若该内存被释放之后再次被释放,就会出错。
- 这个问题写个重载赋值就懂了
Main函数执行之前还会执行什么?
- main函数执行之前,主要就是初始化系统相关资源
- 操作系统创建进程后,把控制权交给程序的入口函数,这个函数往往是运行时库的某个入口函数。
- 入口函数对运行库和程序运行环境进行初始化,包括堆,I/O,线程,全局变量构造(constructor)等。
- 调用MAIN函数,正式开始执行程序主体。
- 执行MAIN完毕,返回入口函数,进行清理工作,包括全局变量析构,堆销毁,关闭I/O等,然后进行系统调用介绍进程
C++ 友元函数、友元类
- 一个类中可以有 public、protected、private 三种属性的成员,通过对象可以访问 public 成员,只有本类中的函数可以访问本类的 private 成员。现在,我们来介绍一种例外情况——友元(friend)。借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的 private 成员。
- 在当前类以外定义的、不属于当前类的函数也可以在类中声明,但要在前面加 friend 关键字,这样就构成了友元函数。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。
- 注意,友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象。必须通过参数传递对象(可以直接传递对象,也可以传递对象指针或对象引用),并在访问成员时指明对象。
- friend 函数不仅可以是全局函数(非成员函数),还可以是另外一个类的成员函数。一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的 private 成员。
- 不仅可以将一个函数声明为一个类的“朋友”,还可以将整个类声明为另一个类的“朋友”,这就是友元类。友元类中的所有成员函数都是另外一个类的友元函数。
- 友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
- 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。
C++继承
- 搞清楚公有继承、保护继承、私有继承就行这里
虚继承
- 为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题。
- 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
- 实例化顺序是,首先执行虚基类的构造函数,多个虚基类的构造函数按照被继承的顺序构造;执行基类的构造函数,多个基类的构造函数按照被继承的顺序构造;执行成员对象的构造函数,多个成员对象的构造函数按照申明的顺序构造;执行派生类自己的构造函数。析构以与构造相反的顺序执行。
- 在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。
- 声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。
- 观察类构造函数的构造顺序,拷贝也只有一份。
- 必须在虚派生的真实需求出现前就已经完成虚派生的操作。虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。
虚函数原理,虚函数与纯虚函数
- 虚函数是C++中用于实现多态(polymorphism)(同一代码可以产生不同效果的特点,被称为多态)的机制,核心理念就是通过基类访问派生类定义的函数。
- 虚函数虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。
- 虚函数只能借助于指针或者引用来达到多态的效果
- 虚函数实际上是如何被编译器处理的呢?编译器发现一个类中有被声明为virtual的函数,就会为其创建一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写。
- 基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。
- 在基类中仅仅给出声明,不对虚函数实现定义,而是在派生类中实现。这个虚函数称为纯虚函数。普通函数如果仅仅给出它的声明而没有实现它的函数体,这是编译不过的。纯虚函数没有函数体。纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。
- private虚函数的语意是:最好重写这个函数,但是不要管它如何使用,也不要调用这个函数。
- 一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了。也就是说不能在构造函数和析构函数中让自己“多态”。
- 在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。
- 其实虚函数表的本质就是一种迟后联编的过程,正常编译都是先期联编的,但是当代码遇到了virtual时,就会把它当做迟后联编,但是为了迟后编译,我么就生成了局部变量–虚函数表,这就增大了一些空间上的消耗。(前提是两个函数的返回类型,参数类型,参数个数都得相同,不然就起不到多态的作用)
- 有一种特殊的情况,那就是如果基类中虚函数返回一个基类指针或引用,派生类中返回一个派生类的指针或引用,则c++将其视为同名虚函数而进行迟后联编
- 使用虚函数的一些限制:
- 只有类成员函数才能声明为虚函数,这是因为虚函数只适用于有继承关系的类对象中。
- 静态成员函数不能说明为虚函数,因为静态成员函数不受限与某个对象,整个内存中只有一个,所以不会出现混淆的情况
- 内联函数不可以被继承,因为内联函数是不能子啊运行中动态的确认其位置的。
- 构造函数不可以被继承。
- 析构函数可以被继承,而且通常声明为虚函数。
抽象类
- 含有纯虚函数的类被称为抽象类。抽象类只能作为派生类的基类,不能定义对象,但可以定义指针。在派生类实现该纯虚函数后,定义抽象类对象的指针,并指向或引用子类对象。
- 在定义纯虚函数时,不能定义虚函数的实现部分;
- 在没有重新定义这种纯虚函数之前,是不能调用这种函数的。
- 抽象类的唯一用途是为派生类提供基类,纯虚函数的作用是作为派生类中的成员函数的基础,并实现动态多态性。继承于抽象类的派生类如果不能实现基类中所有的纯虚函数,那么这个派生类也就成了抽象类。因为它继承了基类的抽象函数,只要含有纯虚函数的类就是抽象类。纯虚函数已经在抽象类中定义了这个方法的声明,其它类中只能按照这个接口去实现。
- 含有纯虚函数的类被称为抽象类。抽象类只能作为派生类的基类,不能定义对象,但可以定义指针。在派生类实现该纯虚函数后,定义抽象类对象的指针,并指向或引用子类对象。
C++接口和抽象类的区别
- 一般说的接口,表示对外提供的方法,提供给外部调用。是沟通外部跟内部的桥梁。也是以类的形式提供的,但一般该类只具有成员函数,不具有数据成员。
- 抽象类可以既包含数据成员又包含方法。
基类指针和派生类指针之间的转换
- 基类指针指向基类对象、派生类指针指向派生类对象。这是正常的。
- 基类指针指向派生类对象。这种情况是允许的,通过定义一个基类指针和一个派生类对象,把基类指针指向派生类对象,但是需要注意,通常情况这时的指针调用的是基类的成员函数。分四种情况:
- 函数在基类和派生类中都存在。这时通过指向派生类对象的基类指针调用成员函数,调用的是基类的成员函数。
- 函数在基类中不存在,在派生类中存在。编译器报错
- 将基类指针强制转换为派生类指针。这种是向下的强制类型转换,转换之后“指向派生类的基类指针”就可以访问派生类的成员函数
- 如果基类中的成员函数被定义为虚函数,并且在派生类中也实现了该函数,则通过“指向派生类的基类指针” 访问虚函数,访问的是派生类中的实现。允许“基类指针指向派生类”这个操作,最大的意义也就在此,通过虚函数和函数覆盖,实现了“多态”(指向不同的派生类,实现不同功能)。
- 派生类指针指向基类对象,会编译错误。基类对象无法被当作派生类对象,派生类中可能具有只有派生类才有的成员或成员函数。即便是使用强制转换,将派生类指针强制转换成基类指针,通过这个“强制指向基类的派生类指针”访问的函数依然是派生类的成员函数。
- 综上,可以通过基类指针访问派生类方法(强制转换和虚函数),不存在通过派生类指针调用基类成员函数的方法(即便是强制转换)。
C++编译时多态,运行时多态
- 编译时多态又叫静态联编,主要通过函数重载和运算符重载来实现
- 运行时多态又叫动态联编,主要通过继承和虚函数来实现
c++11新特性
- 右值引用与std::move()避免右值对象拷贝
- 初始化列表
- explicit
- auto
- auto不会有任何的效率损失,都是基于编译期的推导
- auto还会带来更好的安全性
- decltype
- nullptr(与nil等价)
- default启用编译器提供的默认函数实现
- delete关键字禁止生成默认方法实现
- static_assert()提供编译期断言
- range for
- constexpr
- lambda表达式
- enum class
构造函数和析构函数能否抛出异常
- 构造函数可抛出异常。动态创建对象要进行两个操作:分配内存和调用构造函数。若在分配内存时出错,会抛出bad_alloc异常;若在调用构造函数初始化时出错,会不会存在内存泄漏呢?答案是不会。
- 析构函数也可抛出异常,但不推荐抛出。原因如下:
- 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。 [正常情况下调用析构函数抛出异常导致资源泄露]
- 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。 [在发生异常的情况下调用析构函数抛出异常,会导致程序崩溃]
- 如果非要抛出异常,解决方案如下:
- 如果某个操作可能会抛出异常,class应提供一个普通函数(而非析构函数),来执行该操作。目的是给客户一个处理错误的机会。
- 如果析构函数中异常非抛不可,那就用try catch来将异常吞下,必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外。
构造函数中有哪些注意事项?
- 默认情况下,c++编译器至少为我们写的类增加3个函数
- 默认构造函数(无参,函数体为空)、默认析构函数(无参,函数体为空)、默认拷贝构造函数(对类中非静态成员属性简单值拷贝)
- 如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数
- 如果用户定义了普通构造函数(非拷贝),c++不再提供默认无参构造,但是会提供默认拷贝构造
- 默认情况下,c++编译器至少为我们写的类增加3个函数
构造函数和析构函数可以是虚函数吗?
- 构造函数不能为虚函数,而析构函数可以且常常是虚函数。
- 当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。
- 析构函数也可以是虚的,甚至是纯虚的。纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类,而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。
子类析构会调用父类的析构函数吗?执行顺序是什么?
- 析构情况需要分类讨论
- 父类析构函数不是虚函数,并使用父类指针指向子类对象,析构该子类对象时,只会调用父类析构函数,因为不具多态性。如何解决?父类析构函数改为虚函数。
- 父类析构函数不是虚函数,并使用子类指针指向子类对象,那么会先调用子类析构函数,再调用父类析构函数,子类释放子类中分配的,父类分配父类中分配的。
- 析构情况需要分类讨论
C++默认成员函数
- 默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值运算符、取址运算符和取址运算符const
- 注意,如果一个类中只存在一个参数为&ClassName的拷贝构造函数,那么就不能使用const ClassName或volatile ClassName的对象实行拷贝初始化。
sizeof和strlen
- sizeof是运算符,strlen是函数
- sizeof操作符的结果是size_t,该类型保证能容纳实现所建立的最大对象的字节大小
- sizeof可以用类型做参数,strlen只能用char*做参数,且必须以“\0”结尾
- 数组作为sizeof的参数不退化,但是传递给strlen时就退化为指针了
- 大部分编译程序在编译的时候sizeof就被计算过了,这就是sizeof(x)可以作为定义数组维数的原因。strlen的结果要在运行的时候才能计算出来,它用来计算字符串的长度,不是类型占内存的大小。
- sizeof后如果是类型必须加括弧,如果变量名可以不加括弧。这是因为sizeof是个操作符,不是个函数。
- sizeof计算的是分配的数组所占内存空间的大小,不受里面存储内容的改变而改变。
- sizeof用途
- 与存储分配和I/O系统的例程进行通信
- 查看某个类型的对象在内存中所占的单元字节
- 动态分配对象时,可以使系统知道要分配多少内存。
- 便于一些类型的扩充,在Windows中很多结构类型有一个专用的字段是用来存放该类型的字节大小。
- 由于操作数的字节数在实现时可能出现变化,建议在涉及到操作数字节大小时用sizeof来代替常量计算。
- 如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小。
内联函数和宏定义的区别
- 使用宏和内联函数都可以节省在函数调用方面所带来的时间和空间开销。二者都采用了空间换时间的方式,在其调用处进行展开
- 在预编译时期,宏定义在调用处执行字符串的原样替换。在编译时期,内联函数在调用处展开,同时进行参数类型检查。
- 内联函数可以作为某个类的成员函数,这样可以使用类的保护成员和私有成员。而当一个表达式涉及到类保护成员或私有成员时,宏就不能实现了(无法将this指针放在合适位置)。
- 在编写内联函数时,函数体应该短小而简洁,不应该包含循环等较复杂结构,否则编译器不会将其当作内联函数看待,而是把它决议成为一个静态函数。
- 频繁的调用内联函数和宏定义容易造成代码膨胀,消耗更大的内存而造成过多的换页操作。
C++ shared_ptr
循环引用,weak_ptr
程序编译过程、静态链接和动态链接等
C++ RAII
- RAII是Resource Acquisition Is Initialization(wiki上面翻译成 “资源获取就是初始化”)的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。RAII是用来管理资源、避免资源泄漏的方法。
- 当我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就被销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。RAII就是这样去完成的。
- 由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。
- 作者:zhaozhengcoder 链接:https://www.jianshu.com/p/b7ffe79498be 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
C++ static
- static关键词作用于成员变量和成员函数
- 静态变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为0,使用时可以改变其值。静态变量或静态函数只有本文件内的代码才能访问它,它的名字在其它文件中不可见。
- 函数内部声明的static变量,可作为对象间的一种通信机制。如果一局部变量被声明为static,那么将只有唯一的一个静态分配的对象,它被用于在该函数的所有调用中表示这个变量。这个对象将只在执行线程第一次到达它的定义使初始化。
- 对于局部静态对象,构造函数是在控制线程第一次通过该对象的定义时调用。在程序结束时,局部静态对象的析构函数将按照他们被构造的相反顺序逐一调用,没有规定确切时间。
- 如果一个变量是类的一部分,但却不是该类的各个对象的一部分,它就被成为是一个static静态成员。一个static成员只有唯一的一份副本,而不像常规的非static成员那样在每个对象里各有一份副本。同理,一个需要访问类成员,而不需要针对特定对象去调用的函数,也被称为一个static成员函数。
- 类的静态成员函数只能访问类的静态成员(变量或函数)。
shared_ptr 线程安全,引用计数如何实现的,原子操作的原理
- shared_ptr在C++11版本之后提供,包含在头文件
<memory>
中,有shared_ptr,unique_ptr和weak_ptr。
- shared_ptr在C++11版本之后提供,包含在头文件
有关于内存分配的问题,怎么实现自主在堆中进行内存分配(因为我自己实现了malloc函数,所以就是简单回答用sbrk() 的系统调用对于堆进行内存分配)
1G内存,void * p = malloc(1.2g) 可行吗,用 for 循环对所分配的内存依次写入,到后面会发生什么,哪些会被置换, 32位系统的进程空间分布,malloc的内存在哪里, p 呢
实现一个C++string operator=()函数, 白纸写. 这个写的还可以, 要注意的点 : 自身复制, 异常安全.
实现strncpy函数, 没啥毛病, 注意鲁棒性
空对象的大小,加虚析构函数又怎样呢。
- 空对象大小为1,加了虚析构函数就是指针大小,32位系统大小为4,64位系统大小为8
假设我现在开辟了一片共享内存,然后我想在这块共享内存上使用stl库,该怎么做呢。比如说我使用vector,我想要它的元素全部在共享内存上,就算是新添加的元素也是被分配在共享内存上。(我们可以重写一个allocator,把共享内存划分给它,用这些共享内存实现一个内存池,让allocator来对它进行管理)
把C++多态的实现讲一下吧(从虚表表、虚函数表、虚函数表指针去具体介绍,然后介绍了构造析构过程中虚函数表指针的变化过程,然后从这些变化过程去解释语言级别的现象)
gcc选项知道哪些(-O优化选项、-W加强警告...还有分阶段编译:-E预编译生成.i文件,-S预编译+编译生成.s文件,-c生成.o文件,-o指定输出文件,-l指定链接库,差不多用得多的就这些了)加调试信息(-gstabs)多线程编译呢(不支持)
介绍一下STL allocator
介绍一下迭代器与容器之间的耦合关系(在SGI STL中只有容器对迭代器的依赖关系,而迭代器并没有对容器的耦合关系。所以,比如说vector扩容之后,迭代器会失效,解引用这样的迭代器可能会造成非法访问。但是以前用VisualStudio使用它的C++的STL库CRT的时候,如果容器进行了扩容,然后解引用它们已失效的迭代器的时候,会引发异常。所以我猜想它们的实现里,一定是将迭代器与容器进行了关联,每次对迭代器进行操作时候,都会根据容器检验迭代器的有效性,如果无效就抛出异常。)
类型萃取有什么作用
- C++模板中的类型参数T是抽象的,我们并不能在模板内部直接获得它的具体特征。类型萃取(抽取)技术就是要抽取类型的一些具体特征(trait),比如它是哪种具体类型,它是引用类型,内建类型,还是类类型等。可见,类型萃取技术其实就是trait模板技术的具体体现。
- 类型信息是编译期的实体,现在要针对类型来进行编程,这其实就是模板元编程的一个方面。我们平常使用的if/else,while,for等基本的逻辑结构都是运行期的行为,在面向类型的编程中并不能使用,这就需要用到一些特殊的模板技术。实现类型萃取要用到的基本思想一个是特化,一个就是用typedef来携带类型信息。实际上,我们在用模板做设计时,一般建议在模板定义内部,为模板的每个类型参数提供typedef定义,这样在泛型代码中可以很容易地访问或抽取这些类型。
- 在C和C++中,普通的函数可以称为值函数,它们接受的参数是某些值,返回的结果也是值。而所谓的类型函数接受的实参是类型,返回的是被抽取出来的类型或常量值等(即用typedef定义的类型别名,一般不同的具体类型都定义统一的别名)。如类模板就是类型函数,sizeof是内建的类型函数,返回给定类型实参的大小。在类型编程中,很多地方都要用到sizeof。
hashmap底层实现原理,hashmap存储结构怎么样,怎么处理的hash冲突,当查询时,其时间复杂度怎么样
gdb用过吗?可以,那来讲下死锁应该怎么调试吧
查内存泄露用什么工具?自己使用过么
Java和C++的最主要的区别是什么
- 第一点是,在C++中,支持面向过程,函数可以与类隔离单独存在,而Java的函数必须在类里面。第二点是内存管理,C++需要程序员自己去管理内存,而Java是通过垃圾回收自动管理内存。(关于多继承和单继承的区别忘记回答了。。接口也忘了回答了,有点紧张)
怎么弄出一个不能被继承的类
- https://www.cnblogs.com/yanenquan/p/4006691.html