C++面试复习总结
C++面试
本人20年3到4月内面了近十家公司,整理一下C++客户端问的多的基础问题
另:操作系统面试总结,OpenGL面试总结,计算机网络面试总结
代码到可执行程序
- 预处理:条件编译,头文件包含,宏替换的处理,生成.i文件。
- 编译:将预处理后的文件转换成汇编语言,生成.s文件
- 汇编:汇编变为目标代码(机器代码)生成.o的文件
- 链接:连接目标代码,生成可执行程序
C++内存结构
- 栈区: 由编译器自动分配释放,像局部变量,函数参数,都是在栈区。会随着作用于退出而释放空间。
- 堆区:程序员分配并释放的区域,像malloc(c),new(c++)
- 全局数据区(静态区):全局变量和静态量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束释放。
- 代码区
静态库和动态库
静态库和动态库最本质的区别就是:该库是否被编译进目标(程序)内部
- 静态库
一般扩展名为.a
或.lib
,在编译的时候会直接整合到目标程序中,所以利用静态函数库编译成的文件会比较大,最大的优点就是编译成功的可执行文件可以独立运行,而不再需要向外部要求读取函数库的内容;但是从升级难易度来看明显没有优势,如果函数库更新,需要重新编译。 - 动态库
动态函数库的扩展名一般为.so
或.dll
,这类函数库通常名为libxxx.so或xxx.dll 。
与静态函数库被整个捕捉到程序中不同,动态函数库在编译的时候,在程序里只有一个“指向”的位置而已,也就是说当可执行文件需要使用到函数库的机制时,程序才会去读取函数库来使用;也就是说可执行文件无法单独运行。这样从产品功能升级角度方便升级,只要替换对应动态库即可,不必重新编译整个可执行文件。
dll加载
隐式加载(静态调用
在程序编译的时候就将dll编译到可执行文件中,这种加载方式调用方便,程序发布的时候可以不用带着dll
缺点是程序会很大
显示加载(动态调用
在程序运行过程中,需要用到dll里的函数时,再动态加载dll到内存中
这种加载方式因为是在程序运行后再加载的,所以可以让程序启动更快,而且dll的维护更容易,使得程序如果需要更新,很多时候直接更新dll,而不用重新安装程序
这种加载方式,函数调用稍微复杂一点
C++多态
静态多态
静态多态:也称为编译期间的多态
- 函数重载:包括普通函数的重载和成员函数的重载
- 函数模板的使用
函数签名
为什么C语言中没有重载呢?
C编译器的函数签名不会记录参数类型和顺序,
C++中的函数签名(function signature):包含了一个函数的信息——包括函数名、参数类型、参数个数、顺序以及它所在的类和命名空间,普通函数签名并不包含函数返回值部分。所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数。
调用协议
stdcall是Pascal方式清理C方式压栈,通常用于Win32 Api中,参数入栈规则是从右到左,自己在退出时清空堆栈。
cdecl是C和C++程序的缺省调用方式,参数入栈规则也是从右至左,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用stdcall函数的大。
fastcall调用的主要特点就是快,通过寄存器来传送参数,从左开始不大于4字节的参数放入CPU的ECX和EDX寄存器,其余参数从右向左入栈
C语言:
__stdcall | 编译后,函数名被修饰为“_functionname@number” |
---|---|
__cdecl | 编译后,函数名被修饰为“_functionname” |
__fastcall | 编译后,函数名给修饰为“@functionname@nmuber” |
C++:
__stdcall | 编译后,函数名被修饰为“?functionname@@YG******@Z” |
---|---|
__cdecl | 编译后,函数名被修饰为“?functionname@@YA******@Z” |
__fastcall | 编译后,函数名给修饰为“?functionname@@YI******@Z” |
函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。C语言和C++语言间如果不进行特殊处理,也无法实现函数的互相调用。
动态多态
虚函数表
当一个类在实现时,如果存在一个或以上的虚函数,这个类便会包含一张虚函数表。而当一个子类继承了基类,子类也会有自己的一张虚函数表。
当我们在设计类的时候,如果把某个函数设置成虚函数时,也就表明我们希望子类在继承的时候能够有自己的实现方式;如果我们明确这个类不会被继承,那么就不应该有虚函数的出现。
对于虚函数的调用是通过查虚函数表来进行的,每个虚函数在虚函数表中都存放着自己的一个地址。这张虚函数表是在编译时产生的,否则这个类的结构信息中也不会插入虚指针的地址信息。
每个类使用一个虚函数表,每个类对象用一个虚表指针。
多重继承的派生类有多个虚函数表,就像一个二维数组
虚析构
如果析构函数不被声明成虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,只会调用基类析构函数,而不调用派生类析构函数,这样就会导致基类指针指向的派生类对象析构不完全。
虚构造
实例化一个对象时,首先会分配对象内存空间,然后调用构造函数来初始化对象。
vptr变量是在构造函数中进行初始化的。又因为执行虚函数需要通过vptr指针来调用。如果可以定义构造函数为虚函数,那么就会陷入先有鸡还是先有蛋的循环讨论中。
重载,重写,重定义
重载(overload):函数名相同,参数列表不同,override只是在类的内部存在
重写(override):也叫覆盖。子类重新定义父类中有相同名称和参数的虚函数(virtual)。
- 被重写的函数不能是static的,且必须是virtual的
- 重写函数必须有相同的类型,名称和参数列表
- 重写函数的访问修饰符可以不同。尽管父类的virtual方法是private的,派生类中重写改写为public,protected也是可以的。这是因为被virtual修饰的成员函数,无论他们是private/protect/public的,都会被统一放置到虚函数表中。
对父类进行派生时,子类会继承到拥有相同偏移地址的虚函数标(相同偏移地址指的是各虚函数先谷底与VPTR指针的偏移),因此就允许子类对这些虚函数进行重写
重定义(redefining),也叫隐藏。子类重新定义父类有相同名称的非虚函数(参数列表可以不同)。
子类若有和父类相同的函数,那么,这个类将会隐藏其父类的方法。除非你在调用的时候,强制转换成父类类型。在子类和父类之间尝试做类似重载的调用时不能成功的。
菱形继承
虚继承解决菱形继承二义性问题,虚继承只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。
在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。
C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。
函数调用的过程
int func(int param1 ,int param2,int param3){
int var1 = param1;
int var2 = param2;
int var3 = param3;
...
}
int result = func(1,2,3);
在堆栈中变量分布是从高地址到低地址分布
EBP是指向栈底的指针,在过程调用中不变,又称为帧指针。
ESP指向栈顶,程序执行时移动,ESP减小分配空间,ESP增大释放空间,ESP又称为栈指针。
函数执行时,参数从右向左逐步压入栈中(stdcall),最后压入RET返回地址。
通过跳转指令进入函数后(func(1,2,3);
),函数地址入栈,EBP入栈,然后把当前ESP的值给EBP,对应的汇编指令
push ebp
mov ebp esp
此时栈顶和栈底指向同一位置
之后再将调用的函数的临时变量压入,最后通过EAX寄存器保存函数的返回值
调用执行函数完毕,局部变量var3,var2,var1一次出栈,EBP恢复原值,返回地址出栈,找到原执行地址,param1,param2,param3依次出栈,函数调用执行完毕。
调用执行函数完毕,局部变量var3,var2,var1一次出栈,EBP恢复原值,返回地址出栈,找到原执行地址,param1,param2,param3依次出栈,函数调用执行完毕。图略
类的类型大小
static
静态局部变量:变量属于函数本身,仅受函数的控制。保存在全局数据区,而不是在栈中,每次的值保持到下一次调用,直到下次赋新值。
静态全局变量:定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见,不能被其它文件所用(全局变量可以)
静态函数:静态函数不能被其它文件所用,其它文件中可以定义相同名字的函数,不会发生冲突
静态数据成员:静态数据成员的生存期大于 class 的实例(静态数据成员是每个 class 有一份,普通数据成员是每个 instance 有一份)
- 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
- 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
- 静态成员函数不能访问非静态成员函数和非静态数据成员;
- 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以用类名::函数名调用(因为他本来就是属于类的,用类名调用很正常)
const
修饰普通类型的变量
修饰指针:顶层const(指针本身是个常量)和底层cosnt(指针指向对象是一个常量)
修饰函数参数:
- 函数参数为值传递:值传递(pass-by-value)是传递一份参数的拷贝给函数,因此不论函数体代码如何运行,也只会修改拷贝而无法修改原始对象,这种情况不需要将参数声明为const。
- 函数参数为指针:指针传递(pass-by-pointer)只会进行浅拷贝,拷贝一份指针给函数,而不会拷贝一份原始对象。因此,给指针参数加上顶层const可以防止指针指向被篡改,加上底层const可以防止指向对象被篡改。
- 函数参数为引用:引用传递(pass-by-reference)有一个很重要的作用,由于引用就是对象的一个别名,因此不需要拷贝对象,减小了开销。这同时也导致可以通过修改引用直接修改原始对象(毕竟引用和原始对象其实是同一个东西),因此,大多数时候,推荐函数参数设置为pass-by-reference-to-const。给引用加上底层const,既可以减小拷贝开销,又可以防止修改底层所引用的对象。
修饰函数返回值:令函数返回一个常量,可以有效防止因用户错误造成的意外,比如 “=” 。
const成员函数:const成员函数不可修改类对象的内容(指针类型的数据成员只能保证不修改该指针指向)。原理是cost成员函数的this指针是底层const指针,不能用于改变其所指对象的值。
当成员函数的 const 和 non-const 版本同时存在时:const object 只能调用 const 版本,non-const object 只能调用 non-const 版本。
const object(data members不得变动) | non-const objectdata members可变动) | |
---|---|---|
const member function(保证不更改data members) | 可 | 可 |
non-const member function(不保证) | 不可 | 可 |
extern "C"
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。
加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。
- C++代码调用C语言代码
- 在C++的头文件中使用
- 在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++
inline
类内定义成员函数默认为inline
优点:
- inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高。
- 内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。(宏替换不会检查参数类型,安全隐患较大)
- inline函数可以作为一个类的成员函数,与类的普通成员函数作用相同,可以访问一个类的私有成员和保护成员。内联函数可以用于替代一般的宏定义,最重要的应用在于类的存取函数的定义上面。
缺点:
- 内联函数具有一定的局限性,内联函数的函数体一般来说不能太大,如果内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。(换句话说就是,你使用内联函数,只不过是向编译器提出一个申请,编译器可以拒绝你的申请)这样,内联函数就和普通函数执行效率一样了。
- inline说明对编译器来说只是一种建议,编译器可以选择忽略这个建议。比如,你将一个长达1000多行的函数指定为inline,编译器就会忽略这个inline,将这个函数还原成普通函数,因此并不是说把一个函数定义为inline函数就一定会被编译器识别为内联函数,具体取决于编译器的实现和函数体的大小。
和宏定义的区别
宏是由预处理器对宏进行替代,而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。内联函数与带参数的宏定义进行下比较,它们的代码效率是一样,但是内联欢函数要优于宏定义,因为内联函数遵循的类型和作用域规则,它与一般函数更相近,在一些编译器中,一旦关联上内联扩展,将与一般函数一样进行调用,比较方便。
stl容器
序列容器
vector
动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放;如果新值大于当前大小时才会重新分配内存。
push_back()
,pop_back()
,insert()
,访问时间是O1,erase()
时间是On,查找On
扩容方式:倍数开辟二倍的内存,旧的数据开辟到新内存,释放旧的内存,指向新内存。
- 对头部和中间进行添加删除元素操作需要移动内存,如果元素是结构或类,那么移动的同时还会进行构造和析构操作
- 对任何元素的访问时间都是O1,常用来保存需要经常进行随机访问的内容,并且不需要经常对中间元素进行添加删除操作
- 属性与string差不多,同样可以使用capacity看当前保留的内存,使用swap来减少它使用的内存,如push_back 1000个元素,capacity返回值为16384
- 对最后元素操作最快(在后面添加删除元素最快),此时一般不需要移动内存,只有保留内存不够时才需要
list
双向循环链表
元素存放在堆中,每个元素都是放在一块内存中,他的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随机存取变得非常没有效率,因此它没有提供[]
操作符的重载。但是由于链表的特点,它可以很有效率的支持任意地方的删除和插入操作。
增删erase()
都是O1,访问On,查找头O1,其余查找On。
deque
双端队列 deque 是对 vector 和list 优缺点的结合的一种容器。
由于分段数组的大小是固定的,并且他们的首地址被连续存放在索引数组中,因此可以对其进行随机访问,但是效率比 vector 低很多。向两端加入新元素时,如果这一端的分段数组未满,则可以直接加入,如果这一端的分段数组已满,只需要创建新的分段数组,并把该分段数组的地址加入到索引数组中即可,这样就不需要对已有的元素进行移动,因此在双端队列的两端加入新的元素都具有较高的效率
insert()
和erase()
时间是On,查找On,其余O1。
vector:快速的随机储存,快速的在最后插入删除元素
需要高效随机储存,需要高效的在末尾插入删除元素,不需要高效的在其他地方插入和删除元素
list:快速在任意位置插入删除元素,快速访问头尾元素
需要大量插入删除操作,不关心随机储存的场景
deque:比较快速的随机存储(比vector的慢),快速的在头尾插入删除元素
需要随机储存,需要高效的在头尾插入删除元素的场景
deque可以看作是vector和list的折中方案
关联容器
底层都是红黑树
set
元素有序,无重复元素,插入删除操作的效率比序列容器高,因为对于关联容器来说,不需要做内存的拷贝和内存的移动。
增删改查Ologn
multiset
multiset和set相同,只不过它允许重复元素,也就是说multiset可包括多个数值相同的元素。
map
map由红黑树实现,其元素都是键值对,每个元素的键是排序的准则,每个键只能出现一次,不允许重复
增删改查Ologn
multimap
multimap和map相同,但允许重复元素,也就是说multimap可包含多个键值(key)相同的元素。
unordered_map和map
map内部实现了一个红黑树(所有元素都是有序的),unordered_map内部实现了一个哈希表(元素的排列是无序的)
map
优点:有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作;内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高
缺点:空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间
适用:对于那些有顺序要求的问题,用map会更高效一些
unordered_map:
优点:因为内部实现了哈希表,因此其查找速度非常的快
缺点:哈希表的建立比较耗费时间
适用:对于查找问题,unordered_map会更加高效一些
智能指针
将基本类型指针封装为类对象指针,并在析构函数里编写delete语句删除指针指向的内存空间。
auto_ptr<string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; vocaticn = ps;
如果 ps 和 vocation 是常规指针,则两个指针将指向同一个 string 对象。这是不能接受的,因为程序将试图删除同一个对象两次,一次是 ps 过期时,另一次是 vocation 过期时。要避免这种问题,方法有多种:
- 定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
- 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的析构函数会删除该对象。然后让赋值操作转让所有权。这就是用于 auto_ptr 和 unique_ptr 的策略,但 unique_ptr 的策略更严格。
- 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加 1,而指针过期时,计数将减 1,当减为 0 时才调用 delete。这是 shared_ptr 采用的策略。
所有的智能指针类都有一个explicit构造函数,以指针作为参数。因此不能自动将指针转换为智能指针对象,必须显式调用:
shared_ptr<double> pd;
double *p_reg = new double;
pd = p_reg; // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg); // allowed (explicit conversion)
shared_ptr<double> pshared = p_reg; // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg); // allowed (explicit conversion)
对全部三种智能指针都应避免的一点:
string vacation("I wandered lonely as a cloud.");
shared_ptr<string> pvac(&vacation); // No
pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。
auto_ptr
在 auto_ptr 对象销毁时,他所管理的对象也会自动被 delete 掉。
auto_ptr 采用 copy 语义来转移指针资源,转移指针资源的所有权的同时将原指针置为 NULL,拷贝后原对象变得无效,再次访问原对象时会导致程序崩溃。
unique_ptr
由 C++11 引入,旨在替代不安全的 auto_ptr。
unique_ptr 则禁止了拷贝语义,但提供了移动语义,即可以使用std::move() 进行控制权限的转移,如下代码所示:
它持有对对象的独有权——两个 unique_ptr 不能指向一个对象,即 unique_ptr 不共享它所管理的对象。
内存资源所有权可以转移到另一个 unique_ptr,并且原始 unique_ptr 不再拥有此资源。实际使用中,建议将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。因此,当需要智能指针用于存 C++ 对象时,可使用 unique_ptr,构造 unique_ptr 时,可使用 make_unique 函数。
//智能指针的创建
unique_ptr<int> u_i; //创建空智能指针
u_i.reset(new int(3)); //绑定动态对象
unique_ptr<int> u_i2(new int(4));//创建时指定动态对象
unique_ptr<T,D> u(d); //创建空 unique_ptr,执行类型为 T 的对象,用类型为 D 的对象 d 来替代默认的删除器 delete
//所有权的变化
int *p_i = u_i2.release(); //释放所有权
unique_ptr<string> u_s(new string("abc"));
unique_ptr<string> u_s2 = std::move(u_s); //所有权转移(通过移动语义),u_s所有权转移后,变成“空指针”
u_s2.reset(u_s.release()); //所有权转移
u_s2=nullptr;//显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价
当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
使用move后,原来的指针仍转让所有权变成空指针,可以对其重新赋值。
shared_ptr
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销:
- shared_ptr 对象除了包括一个所拥有对象的指针外,还必须包括一个引用计数代理对象的指针;
- 时间上的开销主要在初始化和拷贝操作上, * 和 -> 操作符重载的开销跟 auto_ptr 是一样;
- 开销并不是我们不使用 shared_ptr 的理由,,永远不要进行不成熟的优化,直到性能分析器告诉你这一点。
环形引用:智能指针互相指向了对方,导致自己的引用计数一直为1,所以没有进行析构,这就造成了内存泄漏。
weak_ptr
weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数,当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效。
内存泄漏
内存泄漏是指应用程序分配某段内存后,失去了对该段内存的控制,因而造成了内存的浪费。
- 在类的构造函数和析构函数中没有匹配的调用new和delete函数
- 没有正确地清除嵌套的对象指针
- 在释放对象数组时在中没有使用delete[]
- 指向对象的指针数组不等同于对象数组
- 缺少拷贝构造函数或重载赋值运算符:隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。
- 没有将基类的析构函数定义为虚函数
野指针:指向被释放的或者访问受限内存的指针。
造成野指针的原因:
- 指针变量没有被初始化(如果值不定,可以初始化为NULL)
- 指针被free或者delete后,没有置为NULL,free和delete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为NULL.
- 指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针。
避免内存泄漏
RAII 资源获取即初始化,是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。智能指针即RAII最具代表的实现
RTTI
即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。
RTTI典型的应用需求
1、类型的识别,即能在运行时判断出某对象、表达式等的类型,能判断它们是基本类型(int、string),还是对象,以及它们区别于其它类型的标识;
2、对象的继承关系的运行时判断;
3、在出错处理、内存诊断等处理时的输出信息;
4、基于字符型名称的运行时对象访问、方法调用;
5、对象的自动保存和读入;
6、基于ID或名称的对象自动生成;
7、环境配置的保存和读入;
8、程序自动生成;
指针和引用
指针是存放内存地址的一种变量,指针的大小不会像其他变量一样变化。声明时可以暂时不初始化
指针在声明周期内随时可能会为Null,所以使用时一定要做检查,防止出现空指针、野指针的情况
引用的本质是“变量的别名”,就一定要有本体。声明时就必须初始化
C++中的引用本质上是一种被限制的指针,引用是占据内存的
new和malloc
malloc实际是返还一段内存区域的中间地址,我们可用的内存只可以是这个地址之后的,地址前面的内存是根据系统固定规则存放着该内存区域的信息,这样free就可以知道malloc出来的地址前面的内容,从而确定整个内存区域的大小。
1.类型安全
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图方法自己没被授权的内存区域。关于C++的类型安全性可说的又有很多了。
2.分配失败返回
new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
在使用C语言时,我们习惯在malloc分配内存后判断分配是否成功:
3.调用ctor
new的过程
使用new操作符来分配对象内存时会经历三个步骤:
- 调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
- 编译器运行相应的构造函数以构造对象,并为其传入初值。
- 对象构造完成后,返回一个指向该对象的指针。
使用delete操作符来释放对象内存时会经历两个步骤:
- 调用对象的析构函数。
- 编译器调用operator delete(或operator delete[])函数释放内存空间。
总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构,而malloc则不会。
placement new 本质上是对 operator new 的重载,定义于#include 中,它不分配内存,调用合适的构造函数在 ptr 所指的地方构造一个对象,之后返回实参指针ptr
重载
opeartor new /operator delete可以被重载,而malloc/free并不允许重载。
总结
特征 | new/delete | malloc/free |
---|---|---|
分配内存的位置 | 自由存储区 | 堆 |
分配成功 | 返回完整类型指针 | 返回void* |
分配失败 | 默认抛出异常 | 返回NULL |
分配内存的大小 | 由编译器根据类型计算得出 | 必须显式指定字节数 |
处理数组 | 有处理数组的new版本new[] | 需要用户计算数组的大小后进行内存分配 |
已分配内存的扩充 | 无法直观地处理 | 使用realloc简单完成 |
是否相互调用 | 可以,看具体的operator new/delete实现 | 不可调用new |
分配内存时内存不足 | 客户能够指定处理函数或重新制定分配器 | 无法通过用户代码进行处理 |
函数重载 | 允许 | 不允许 |
构造函数与析构函数 | 调用 | 不调用 |
strcpy和memcpy
- 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。企业中使用memcpy很平常,因为需要拷贝大量的结构体参数。memcpy通常与memset函数配合使用。
- 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。因此strcpy会复制字符串的结束符“\0”,而memcpy则不会复制。
C++11,C++14
11:初始化列表,auto Type,foreach,nullptr,enum class,委托构造,禁止重写final,显示重写override,成员初始值,默认构造函数default,删除构造函数delete,常量表达式constexpr,Lambda函数
14:auto返回类型,泛型lambda
排序
堆排序、快速排序、希尔排序、直接选择排序是不稳定的排序算法,
而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。
归并
- 如果给的数组只有一个元素的话,直接返回(也就是递归到最底层的一个情况)
- 把整个数组分为尽可能相等的两个部分(分)
- 对于两个被分开的两个部分进行整个归并排序(治)
- 把两个被分开且排好序的数组拼接在一起
void merge(int arr[], int l, int m, int r)
{
int i, j, k;
int n1 = m - l + 1;
int n2 = r - m;
vector<int> L(n1);
vector<int> R(n2);
for (i = 0; i < n1; i++)
L[i] = arr[l + i];
for (j = 0; j < n2; j++)
R[j] = arr[m + 1 + j];
i = 0;
j = 0;
k = l;
while (i < n1 && j < n2){
if (L[i] <= R[j]){
arr[k] = L[i];
i++;
}
else{
arr[k] = R[j];
j++;
}
k++;
}
while (i < n1){
arr[k] = L[i];
i++;
k++;
}
while (j < n2){
arr[k] = R[j];
j++;
k++;
}
}
void mergeSort(int arr[], int l, int r)
{
if (l < r)
{
int m = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
}
堆排
基本思想:把待排序的元素按照大小在二叉树位置上排列,排序好的元素要满足:父节点的元素要大于等于其子节点;这个过程叫做堆化过程。
根据这个特性(大根堆根最大,小根堆根最小),就可以把根节点拿出来,然后再堆化下,再把根节点拿出来,循环到最后一个节点,就排序好了。
- 最大堆中的最大元素值出现在根结点(堆顶)
- 堆中每个父节点的元素值都大于等于其孩子结点
下图是最小堆:
建立堆函数:
void heapify(int arr[], int n, int i)
{
int largest = i; // 将最大元素设置为堆顶元素
int l = 2*i + 1; // left = 2*i + 1
int r = 2*i + 2; // right = 2*i + 2
// 如果 left 比 root 大的话
if (l < n && arr[l] > arr[largest])
largest = l;
// I如果 right 比 root 大的话
if (r < n && arr[r] > arr[largest])
largest = r;
if (largest != i)
{
swap(arr[i], arr[largest]);
// 递归地定义子堆
heapify(arr, n, largest);
}
}
堆排序的方法如下,把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。
堆排序函数:
void heapSort(int arr[], int n)
{
// 建立堆
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// 一个个从堆顶取出元素
for (int i=n-1; i>=0; i--)
{
swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}
快排
void quickSort(int a[],int left,int right)
{
int i=left;
int j=right;
int temp=a[left];
if(left>=right)
return;
while(i!=j)
{
while(i<j&&a[j]>=temp)
j--;
if(j>i)
a[i]=a[j];//a[i]已经赋值给temp,所以直接将a[j]赋值给a[i],赋值完之后a[j],有空位
while(i<j&&a[i]<=temp)
i++;
if(i<j)
a[j]=a[i];
}
a[i]=temp;//把基准插入,此时i与j已经相等R[low..pivotpos-1].keys≤R[pivotpos].key≤R[pivotpos+1..high].keys
quickSort(a,left,i-1);/*递归左边*/
quickSort(a,i+1,right);/*递归右边*/
}
哈希冲突
https://blog.csdn.net/u011109881/article/details/80379505
图的联通
https://blog.csdn.net/weixin_44646116/article/details/95523884
红黑树和AVL
平衡二叉树类型 | 平衡度 | 调整频率 | 适用场景 |
---|---|---|---|
AVL树 | 高 | 高 | 查询多,增/删少 |
红黑树 | 低 | 低 | 增/删频繁 |
设计模式
单例模式
保证一个类只有一个实例,并提供一个访问它的全局访问点,使得系统中只有唯一的一个对象实例。
常用于管理资源,如日志、线程池
工厂模式
工厂模式的主要作用是封装对象的创建,分离对象的创建和操作过程,用于批量管理对象的创建过程,便于程序的维护和扩展。