网站建设需要哪些网络技术,wordpress 菜单保存在哪里,榆林北京网站建设,设计网站需要什么条件1. C和C有什么区别#xff1f;
C是面向对象的语言#xff0c;而C是面向过程的语言#xff1b;C引入new/delete运算符#xff0c;取代了C中的malloc/free库函数#xff1b;C引入引用的概念#xff0c;而C中没有#xff1b;C引入类的概念#xff0c;而C中没有#xff1…1. C和C有什么区别
C是面向对象的语言而C是面向过程的语言C引入new/delete运算符取代了C中的malloc/free库函数C引入引用的概念而C中没有C引入类的概念而C中没有C引入函数重载的特性而C中没有
2. a和a有什么区别
假设数组int a[10]; int (*p)[10] a;其中a是数组名是数组首元素地址1表示地址值加上一个int类型的大小如果a的值是0x00000001加1操作后变为0x00000005。*(a 1) a[1]。a是数组的指针其类型为int (*)[10]就是前面提到的数组指针其加1时系统会认为是数组首地址加上整个数组的偏移10个int型变量值为数组a尾元素后一个元素的地址。若(int *)p 此时输出 *p时其值为a[0]的值因为被转为int *类型解引用时按照int类型大小来读取
3. static关键字有什么作用
修饰局部变量时使得该变量在静态存储区分配内存只能在首次函数调用中进行首次初始化之后的函数调用不再进行初始化其生命周期与程序相同但其作用域为局部作用域并不能一直被访问修饰全局变量时使得该变量在静态存储区分配内存在声明该变量的整个文件中都是可见的而在文件外是不可见的修饰函数时在声明该函数的整个文件中都是可见的而在文件外是不可见的从而可以在多人协作时避免同名的函数冲突修饰成员变量时所有的对象都只维持一份拷贝可以实现不同对象间的数据共享不需要实例化对象即可访问不能在类内部初始化一般在类外部初始化并且初始化时不加static修饰成员函数时该函数不接受this指针只能访问类的静态成员不需要实例化对象即可访问。
4. #define和const有什么区别
编译器处理方式不同#define宏是在预处理阶段展开不能对宏定义进行调试而const常量是在编译阶段使用类型和安全检查不同#define宏没有类型不做任何类型检查仅仅是代码展开可能产生边际效应等错误而const常量有具体类型在编译阶段会执行类型检查存储方式不同#define宏仅仅是代码展开在多个地方进行字符串替换不会分配内存存储于程序的代码段中而const常量会分配内存但只维持一份拷贝存储于程序的数据段中。定义域不同#define宏不受定义域限制而const常量只在定义域内有效。 5. 静态链接和动态链接有什么区别
静态链接是在编译链接时直接将需要的执行代码拷贝到调用处 优点在于程序在发布时不需要依赖库可以独立执行缺点在于程序的体积会相对较大而且如果静态库更新之后所有可执行文件需要重新链接动态链接是在编译时不直接拷贝执行代码而是通过记录一系列符号和参数在程序运行或加载时将这些信息传递给操作系统操作系统负责将需要的动态库加载到内存中然后程序在运行到指定代码时在共享执行内存中寻找已经加载的动态库可执行代码实现运行时链接优点在于多个程序可以共享同一个动态库节省资源缺点在于由于运行时加载可能影响程序的前期执行性能。
6. 变量的声明和定义有什么区别
变量的定义为变量分配地址和存储空间 变量的声明不分配地址。一个变量可以在多个地方声明 但是只在一个地方定义。加入extern 修饰的是变量的声明说明此变量将在文件以外或在文件后面部分定义。
说明很多时候一个变量只是声明不分配内存空间直到具体使用时才初始化分配内存空间 如外部变量。
int main()
{ extern int A; //这是个声明而不是定义声明A是一个已经定义了的外部变量 //注意声明外部变量时可以把变量类型去掉如extern A; dosth(); //执行函数
}
int A; //是定义定义了A为整型的外部变量
7. 简述#ifdef、#else、#endif和#ifndef的作用
利用#ifdef、#endif将某程序功能模块包括进去以向特定用户提供该功能。在不需要时用户可轻易将其屏蔽。
#ifdef MATH
#include math.c
#endif
在子程序前加上标记以便于追踪和调试。
#ifdef DEBUG
printf (Indebugging......!);
#endif
应对硬件的限制。由于一些具体应用环境的硬件不一样限于条件本地缺乏这种设备只能绕过硬件直接写出预期结果。
「注意」虽然不用条件编译命令而直接用if语句也能达到要求但那样做目标程序长因为所有语句都编译运行时间长因为在程序运行时间对if语句进行测试。而采用条件编译可以减少被编译的语句从而减少目标程序的长度减少运行时间。
8. 写出int 、bool、 float 、指针变量与 “零值”比较的if 语句
首先给个提示题目中要求的是零值比较而非与0进行比较在C里“零值”的范围可就大了可以是0 0.0 FALSE或者“空指针”。
下面是答案。
//int与零值比较
if ( n 0 )
if ( n ! 0 ) //bool与零值比较
if (flag) // 表示flag为真
if (!flag) // 表示flag为假 //float与零值比较
const float EPSINON 0.00001;
if ((x - EPSINON) (x EPSINON) //其中EPSINON是允许的误差即精度。
//指针变量与零值比较
if (p NULL)
if (p ! NULL)
详细解释
intint 是整型可以直接和 0 比较。bool根据布尔类型的语义零值为假记为FALSE任何非零值都是真记为TRUE。TRUE 的值究竟是什么并没有统一的标准。例如Visual C 将TRUE 定义为1而Visual Basic 则将TRUE 定义为 -1。所以我们不可以将布尔变量直接与TRUE、FALSE 或者1、0 进行比较float千万要留意无论是float 还是double 类型的变量都有精度限制都不可以用”或!与任何数字比较应该设法转化成或形式。其中EPSINON 是允许的误差即精度指针指针变量的零值就是NULL
9. 结构体可以直接赋值吗
声明时可以直接初始化同一结构体的不同对象之间也可以直接赋值但是当结构体中含有指针“成员”时一定要小心。
「注意」当有多个指针指向同一段内存时某个指针释放这段内存可能会导致其他指针的非法操作。因此在释放前一定要确保其他指针不再使用这段内存空间。
10. sizeof 和strlen 的区别
sizeof是一个操作符strlen是库函数。sizeof的参数可以是数据的类型也可以是变量而strlen只能以结尾为‘\0’的字符串作参数。编译器在编译时就计算出了sizeof的结果而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小而strlen计算的是字符串实际的长度。数组做sizeof的参数不退化传递给strlen就退化为指针了
11. C 语言的关键字 static 和 C 的关键字 static 有什么区别
在 C 中 static 用来修饰局部静态变量和外部静态变量、函数。而 C中除了上述功能外还用来定义类的成员变量和函数。即静态成员和静态成员函数。
「注意」编程时 static 的记忆性和全局性的特点可以让在不同时期调用的函数进行通信传递信息而 C的静态成员则可以在多个对象实例间进行通信传递信息。
12. volatile有什么作用
状态寄存器一类的并行设备硬件寄存器。一个中断服务子程序会访问到的非自动变量。多线程间被几个任务共享的变量。「注意」虽然volatile在嵌入式方面应用比较多但是在PC软件的多线程中volatile修饰的临界变量也是非常实用的。
13. 一个参数可以既是const又是volatile吗
可以用const和volatile同时修饰变量表示这个变量在程序内部是只读的不能改变的只在程序外部条件变化下改变并且编译器不会优化这个变量。每次使用这个变量时都要小心地去内存读取这个变量的值而不是去寄存器读取它的备份。注意在此一定要注意const的意思const只是不允许程序中的代码改变某一变量其在编译期发挥作用它并没有实际地禁止某段内存的读写特性。
14. 全局变量和局部变量有什么区别操作系统和编译器是怎么知道的
全局变量是整个程序都可访问的变量谁都可以访问生存期在整个程序从运行到结束在程序结束时所占内存释放而局部变量存在于模块子程序函数中只有所在模块可以访问其他模块不可直接访问模块结束函数调用完毕局部变量消失所占据的内存释放。操作系统和编译器可能是通过内存分配的位置来知道的全局变量分配在全局数据段并且在程序开始运行的时候被加载.局部变量则分配在堆栈里面。
15. 简述strcpy、sprintf 与memcpy 的区别
操作对象不同strcpy 的两个操作对象均为字符串sprintf 的操作源对象可以是多种数据类型 目的操作对象是字符串memcpy 的两个对象就是两个任意可操作的内存地址并不限于何种数据类型。执行效率不同memcpy 最高strcpy 次之sprintf 的效率最低。实现功能不同strcpy 主要实现字符串变量间的拷贝sprintf 主要实现其他数据类型格式到字 符串的转化memcpy 主要是内存块间的拷贝。「注意」strcpy、sprintf 与memcpy 都可以实现拷贝的功能但是针对的对象不同根据实际需求来 选择合适的函数实现拷贝功能。
16. 对于一个频繁使用的短小函数应该使用什么来实现有什么优缺点
应该使用inline内联函数即编译器将inline内联函数内的代码替换到函数被调用的地方。
优点
在内联函数被调用的地方进行代码展开省去函数调用的时间从而提高程序运行效率相比于宏函数内联函数在代码展开时编译器会进行语法安全检查或数据类型转换使用更加安全
缺点
代码膨胀产生更多的开销如果内联函数内代码块的执行时间比调用时间长得多那么效率的提升并没有那么大 -如果修改内联函数那么所有调用该函数的代码文件都需要重新编译内联声明只是建议是否内联由编译器决定所以实际并不可控。
17. 什么是智能指针智能指针有什么作用分为哪几种各自有什么样的特点
智能指针是一个RAII类模型用于动态分配内存其设计思想是将基本类型指针封装为模板类对象指针并在离开作用域时调用析构函数使用delete删除指针所指向的内存空间。
智能指针的作用是能够处理内存泄漏问题和空悬指针问题。
分为auto_ptr、unique_ptr、shared_ptr和weak_ptr四种各自的特点
对于auto_ptr实现独占式拥有的概念同一时间只能有一个智能指针可以指向该对象但auto_ptr
在C11中被摒弃其主要问题在于
对象所有权的转移比如在函数传参过程中对象所有权不会返还从而存在潜在的内存崩溃问题不能指向数组也不能作为STL容器的成员。对于unique_ptr实现独占式拥有的概念同一时间只能有一个智能指针可以指向该对象因为无法进行拷贝构造和拷贝赋值但是可以进行移动构造和移动赋值对于shared_ptr实现共享式拥有的概念即多个智能指针可以指向相同的对象该对象及相关资源会在其所指对象不再使用之后自动释放与对象相关的资源对于weak_ptr解决shared_ptr相互引用时两个指针的引用计数永远不会下降为0从而导致死锁问题。而weak_ptr是对对象的一种弱引用可以绑定到shared_ptr但不会增加对象的引用计数。
18. shared_ptr是如何实现的
构造函数中计数初始化为1拷贝构造函数中计数值加1赋值运算符中左边的对象引用计数减1右边的对象引用计数加1析构函数中引用计数减1在赋值运算符和析构函数中如果减1后为0则调用delete释放对象。
19. 右值引用有什么作用
右值引用的主要目的是为了实现转移语义和完美转发消除两个对象交互时不必要的对象拷贝也能够更加简洁明确地定义泛型函数
20.悬挂指针与野指针有什么区别
悬挂指针当指针所指向的对象被释放但是该指针没有任何改变以至于其仍然指向已经被回收的内存地址这种情况下该指针被称为悬挂指针野指针未初始化的指针被称为野指针。
21. 动态多态有什么作用有哪些必要条件
动态多态性
1.虚方法的动态多态性2.抽象方法的动态多态性3.接口方法的动态多态性
动态多态性的作用
1.实现”功能定义 与 功能实现 相脱离实现代码进一步灵活性2.实现系统设计 与 系统代码 相脱离实现代码稳定性减少系统BUG
22.请解析((void ()( ) )0)( )的含义
void (*0)( ) 是一个返回值为void参数为空的函数指针0。(void (*)( ))0把0转变成一个返回值为void参数为空的函数指针。(void (**)( ))0在上句的基础上加表示整个是一个返回值为void无参数并且起始地址为0的函数的名字。((void ()( ))0)( )这就是上句的函数名所对应的函数的调用。
23. C语言的指针和引用和c的有什么区别
指针有自己的一块空间而引用只是一个别名使用sizeof看一个指针的大小是4而引用则是被引用对象的大小作为参数传递时指针需要被解引用才可以对对象进行操作而直接对引 用的修改都会改变引用所指向的对象可以有const指针但是没有const引用具体解释看评论区指针在使用中可以指向其它对象但是引用只能是一个对象的引用不能 被改变指针可以有多级指针**p而引用止于一级指针和引用使用运算符的意义不一样如果返回动态内存分配的对象或者内存必须使用指针引用可能引起内存泄露。
24. typedef 和define 有什么区别
用法不同typedef 用来定义一种数据类型的别名增强程序的可读性。define 主要用来定义 常量以及书写复杂使用频繁的宏。执行时间不同typedef 是编译过程的一部分有类型检查的功能。define 是宏定义是预编译的部分其发生- 在编译之前只是简单的进行字符串的替换不进行类型的检查。作用域不同typedef 有作用域限定。define 不受作用域约束只要是在define 声明后的引用 都是正确的。对指针的操作不同typedef 和define 定义的指针时有很大的区别。「注意」typedef 定义是语句因为句尾要加上分号。而define 不是语句千万不能在句尾加分号。
25. 指针常量与常量指针区别
指针常量指针本身的值是不可改变的但指针指向的变量的值是可以改变的。常量指针指针指向的变量的值是不可改变的但指针本身的值是可以改变的
26. 简述队列和栈的异同
队列和栈都是线性存储结构但是两者的插入和删除数据的操作不同队列是“先进先出”栈是 “后进先出”。
「注意」区别栈区和堆区。堆区的存取是“顺序随意”而栈区是“后进先出”。栈由编译器自动分 配释放 存放函数的参数值局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员 分配释放 若程序员不释放程序结束时可能由OS 回收。分配方式类似于链表。它与本题中的堆和栈是两回事。堆栈只是一种数据结构而堆区和栈区是程序的不同内存存储区域。
27. 设置地址为0x67a9 的整型变量的值为0xaa66
int *ptr;
ptr (int *)0x67a9;
*ptr 0xaa66;
「注意」这道题就是强制类型转换的典型例子绝大部份情况下地址长度和整型数据的长度是一样的(此时的整型指的是 long) 即一个整型数据可以强制转换成地址指针类型只要有意义即可。
28. C语言的结构体和C的有什么区别
C语言的结构体是不能有函数成员的而C的类可以有。C语言的结构体中数据成员是没有private、public和protected访问限定的。而C的类的成员有这些访问限定。C语言的结构体是没有继承关系的而C的类却有丰富的继承关系。「注意」虽然C的结构体和C的类有很大的相似度但是类是实现面向对象的基础。而结构体只可以简单地理解为类的前身。
29. 如何避免“野指针”
指针变量声明时没有被初始化。解决办法指针声明时初始化可以是具体的地址值也可让它指向NULL。指针p被free或者delete之后没有置为NULL。解决办法指针指向的内存空间被释放后指针应该指向NULL。指针操作超越了变量的作用范围。解决办法在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。
30.句柄和指针的区别和联系是什么
句柄和指针其实是两个截然不同的概念。Windows系统用句柄标记系统资源隐藏系统的信息。你只要知道有这个东西然后去调用就行了它是个32bit的uint。指针则标记某个物理内存地址两者是不同的概念。
31. 说一说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这样的情况下也会有用到
32. 对c中的smart pointer四个智能指针shared_ptr,unique_ptr,weak_ptr,auto_ptr的理解
C里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c11支持并且第一个已经被11弃用。
智能指针的作用是管理一个指针因为存在以下这种情况申请的空间在函数结束时忘记释放造成内存泄漏。使用智能指针可以很大程度上的避免这个问题因为智能指针就是一个类当超出了类的作用域是类会自动调用析构函数析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间不需要手动释放内存空间。
auto_ptrc98的方案cpp11已经抛弃 采用所有权模式。
auto_ptr string p1 (new string (I reigned lonely as a cloud.”));
auto_ptrstring p2;
p2 p1; //auto_ptr不会报错.
此时不会报错p2剥夺了p1的所有权但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是存在潜在的内存崩溃问题
unique_ptr替换auto_ptr unique_ptr实现独占式拥有或严格拥有概念保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。
采用所有权模式。
unique_ptrstring p3 (new string (auto)); //#4
unique_ptrstring p4 //#5
p4 p3;//此时会报错
编译器认为p4p3非法避免了p3不再指向有效数据的问题。因此unique_ptr比auto_ptr更安全。
另外unique_ptr还有更聪明的地方当程序试图将一个 unique_ptr 赋值给另一个时如果源 unique_ptr 是个临时右值编译器允许这么做如果源 unique_ptr 将存在一段时间编译器将禁止这么做比如
unique_ptrstring pu1(new string (hello world));
unique_ptrstring pu2;
pu2 pu1; // #1 not allowed
unique_ptrstring pu3;
pu3 unique_ptrstring(new string (You)); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1)这可能导致危害。而#2不会留下悬挂的unique_ptr因为它调用 unique_ptr 的构造函数该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明unique_ptr 优于允许两种赋值的auto_ptr 。
「注意」如果确实想执行类似与#1的操作要安全的重用这种指针可给它赋新值。C有一个标准库函数std::move()让你能够将一个unique_ptr赋给另一个。例如
unique_ptrstring ps1, ps2;
ps1 demo(hello);
ps2 move(ps1);
ps1 demo(alexia);
cout *ps2 *ps1 endl;
shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时当前指针会释放资源所有权计数减一。当计数等于0时资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
成员函数
use_count 返回引用计数的个数unique 返回是否是独占所有权( use_count 为 1)swap 交换两个 shared_ptr 对象(即交换所拥有的对象)reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptrsp(new int(1)); sp 与 sp.get()是等价的weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用不会增加对象的引用计数和shared_ptr之间可以相互转化shared_ptr可以直接赋值给它它可以通过调用lock函数来获得shared_ptr。
class B;
class A
{
public:
shared_ptrB pb_;
~A()
{ coutA delete
;
}
};
class B
{
public:
shared_ptrA pa_;
~B()
{ coutB delete
;
}
};
void fun()
{ shared_ptrB pb(new B()); shared_ptrA pa(new A()); pb-pa_ pa; pa-pb_ pb; coutpb.use_count()endl; coutpa.use_count()endl;
}
int main()
{ fun(); return 0;
}
可以看到fun函数中pa pb之间互相引用两个资源的引用计数为2当要跳出函数时智能指针papb析构时两个资源引用计数会减一但是两者引用计数还是为1导致跳出函数时资源没有被释放A B的析构函数没有被调用如果把其中一个改为weak_ptr就可以了我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下这样的话资源B的引用开始就只有1当pb析构时B的计数变为0B得到释放B释放的同时也会使A的计数减一同时pa析构时使A的计数减一那么A的计数为0A得到释放。
「注意」不能通过weak_ptr直接访问对象的方法比如B对象中有一个方法print(),我们不能这样访问pa-pb_-print(); 英文pb_是一个weak_ptr应该先把它转化为shared_ptr,如shared_ptr p pa-pb_.lock(); p-print();
33. C的顶层const和底层const
底层const是代表对象本身是一个常量不可改变顶层const是代表指针的值是一个常量,而指针的值(即对象的地址)的内容可以改变指向的不可改变
34. 拷贝初始化和直接初始化初始化和赋值的区别?
ClassTest ct1(“ab”); 这条语句属于直接初始化它不需要调用复制构造函数直接调用构造函数ClassTest(constchar *pc)所以当复制构造函数变为私有时它还是能直接执行的。ClassTest ct2 “ab”; 这条语句为复制初始化它首先调用构造函数 ClassTest(const char* pc) 函数创建一个临时对象然后调用复制构造函数把这个临时对象作为参数构造对象ct2所以当复制构造函数变为私有时该语句不能编译通过。ClassTest ct3 ct1;这条语句为复制初始化因为 ct1 本来已经存在所以不需要调用相关的构造函数而直接调用复制构造函数把它值复制给对象 ct3所以当复制构造函数变为私有时该语句不能编译通过。ClassTest ct4ct1;这条语句为直接初始化因为 ct1 本来已经存在直接调用复制构造函数生成对象 ct3 的副本对象 ct4。所以当复制构造函数变为私有时该语句不能编译通过。
要点就是拷贝初始化和直接初始化调用的构造函数是不一样的但是当类进行复制时类会自动生成一个临时的对象然后再进行拷贝初始化。
35. C异常机制
在C中使用异常机制可以提高程序的健壮性和可维护性。异常是在程序运行时发生的一个事件它会打断正在执行的程序的正常流程。C异常处理机制可以使程序在出现异常时进行异常处理而不是退出程序。
基本的异常处理
#include iostream
using namespace std; int main() { try { throw 错误; // 抛出异常 } catch (const char* msg) { cerr msg endl; // 捕获并处理异常 } return 0;
}
使用不同类型的异常 #include iostream
using namespace std; class MyException : public exception { const char* what() const throw() { return MyException occurred!; }
}; int main() { try { throw MyException(); // 抛出异常 } catch (MyException e) { cerr MyException caught: e.what() endl; // 捕获并处理异常 } catch (exception e) { cerr Exception caught: e.what() endl; } return 0;
}
在函数中使用异常声明
#include iostream
using namespace std; double divide(double a, double b) throw(int) { if (b 0) throw 1; // 抛出一个整型异常 return a / b;
} int main() { try { divide(1, 0); } catch (int e) { cerr Divided by zero! e endl; // 捕获并处理异常 } return 0;
}
在函数中使用异常声明
#include iostream
using namespace std; double divide(double a, double b) throw(int) { if (b 0) throw 1; // 抛出一个整型异常 return a / b;
} int main() { try { divide(1, 0); } catch (int e) { cerr Divided by zero! e endl; // 捕获并处理异常 } return 0;
}
使用标准异常类
#include iostream
#include stdexcept
using namespace std; int main() { try { throw out_of_range(数组越界); // 抛出异常 } catch (const out_of_range e) { cerr e.what() endl; // 捕获并处理异常 } return 0;
}
在构造函数中抛出异常
#include iostream
using namespace std; class Test {
public: Test(int x) { if (x 0) throw 负数错误; // 在构造函数中抛出异常 }
}; int main() { try { Test t(-1); } catch (const char* msg) { cerr msg endl; // 捕获并处理异常 } return 0;
}
在析构函数中捕获异常 #include iostream
using namespace std; class Test {
public: ~Test() { try { throw 异常; // 在析构函数中抛出异常 } catch (const char* msg) { cerr msg endl; // 在析构函数中捕获并处理异常 } }
}; int main() { Test t; return 0;
}
以上就是C异常处理的一些基本用法。在实际编程中应该尽量避免使用C异常规范尽可能使用C标准库提供的异常类并在函数声明中明确指出可能抛出的异常类型以保证代码的可维护性和可读性。
二、C面向对象
1. 面向对象的三大特征是哪些
封装将客观事物封装成抽象的类而类可以把自己的数据和方法暴露给可信的类或者对象对不可信的类或对象则进行信息隐藏。继承可以使用现有类的所有功能并且无需重新编写原来的类即可对功能进行拓展多态一个类实例的相同方法在不同情形下有不同的表现形式使不同内部结构的对象可以共享相同的外部接口。
2. C中类成员的访问权限
C通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限它们分别表示公有的、受保护的、私有的被称为成员访问限定符。在类的内部定义类的代码内部无论成员被声明为 public、protected 还是 private都是可以互相访问的没有访问权限的限制。在类的外部定义类的代码之外只能通过对象访问成员并且通过对象只能访问 public 属性的成员不能访问 private、protected 属性的成员
3. 多态的实现有哪几种
多态分为静态多态和动态多态。其中静态多态是通过重载和模板技术实现的在编译期间确定动态多态是通过虚函数和继承关系实现的执行动态绑定在运行期间确定。
4. 动态绑定是如何实现的
当编译器发现类中有虚函数时会创建一张虚函数表把虚函数的函数入口地址放到虚函数表中并且在对象中增加一个指针vptr用于指向类的虚函数表。当派生类覆盖基类的虚函数时会将虚函数表中对应的指针进行替换从而调用派生类中覆盖后的虚函数从而实现动态绑定。
5. 动态多态有什么作用有哪些必要条件
动态多态的作用
隐藏实现细节使代码模块化提高代码的可复用性接口重用使派生类的功能可以被基类的指针/引用所调用即向后兼容提高代码的可扩充性和可维护性。
动态多态的必要条件
需要有继承需要有虚函数覆盖需要有基类指针/引用指向子类对象
6. 纯虚函数有什么作用如何实现
定义纯虚函数是为了实现一个接口起到规范的作用想要继承这个类就必须覆盖该函数。实现方式是在虚函数声明的结尾加上 0即可。
7. 虚函数表是针对类的还是针对对象的同一个类的两个对象的虚函数表是怎么维护的
虚函数表是针对类的类的所有对象共享这个类的虚函数表因为每个对象内部都保存一个指向该类虚函数表的指针vptr每个对象的vptr的存放地址都不同但都指向同一虚函数表。
8. 为什么基类的构造函数不能定义为虚函数
虚函数的调用依赖于虚函数表而指向虚函数表的指针vptr需要在构造函数中进行初始化所以无法调用定义为虚函数的构造函数。
9. 为什么基类的析构函数需要定义为虚函数
为了实现动态绑定基类指针指向派生类对象如果析构函数不是虚函数那么在对象销毁时就会调用基类的析构函数只能销毁派生类对象中的部分数据所以必须将析构函数定义为虚函数从而在对象销毁时调用派生类的析构函数从而销毁派生类对象中的所有数据。
10. 构造函数和析构函数能抛出异常吗
从语法的角度来说构造函数可以抛出异常但从逻辑和风险控制的角度来说尽量不要抛出异常否则可能导致内存泄漏。析构函数不可以抛出异常如果析构函数抛出异常则异常点之后的程序比如释放内存等操作就不会被执行从而造成内存泄露的问题而且当异常发生时C通常会调用对象的析构函数来释放资源如果此时析构函数也抛出异常即前一个异常未处理又出现了新的异常从而造成程序崩溃的问题。
11. 如何让一个类不能实例化
将类定义为抽象类也就是存在纯虚函数或者将构造函数声明为private。
12. 多继承存在什么问题如何消除多继承中的二义性
增加程序的复杂度使得程序的编写和维护比较困难容易出错 在继承时基类之间或基类与派生类之间发生成员同名时将出现对成员访问的不确定性即同名二义性
消除同名二义性的方法
利用作用域运算符::用于限定派生类使用的是哪个基类的成员在派生类中定义同名成员覆盖基类中的相关成员
当派生类从多个基类派生而这些基类又从同一个基类派生则在访问此共同基类的成员时将产生另一种不确定性即路径二义性
消除路径二义性的方法
消除同名二义性的两种方法都可以使用虚继承使得不同路径继承来的同名成员在内存中只有一份拷贝。
13. 如果类A是一个空类那么sizeof(A)的值为多少
sizeof(A)的值为1因为编译器需要区分这个空类的不同实例分配一个字节可以使这个空类的不同实例拥有独一无二的地址。
14. 覆盖和重载之间有什么区别
覆盖是指派生类中重新定义的函数其函数名、参数列表、返回类型与父类完全相同只是函数体存在区别覆盖只发生在类的成员函数中重载是指两个函数具有相同的函数名不同的参数列表不关心返回值当调用函数时根据传递的参数列表来判断调用哪个函数重载可以是类的成员函数也可以是普通函数。
15. 拷贝构造函数和赋值运算符重载之间有什么区别
拷贝构造函数用于构造新的对象
Student s;
Student s1 s; // 隐式调用拷贝构造函数
Student s2(s); // 显式调用拷贝构造函数
赋值运算符重载用于将源对象的内容拷贝到目标对象中而且若源对象中包含未释放的内存需要先将其释放
Student s;
Student s1;
s1 s; // 使用赋值运算符
一般情况下类中包含指针变量时需要重载拷贝构造函数、赋值运算符和析构函数。
16. 对虚函数和多态的理解
多态的实现主要分为静态多态和动态多态静态多态主要是重载在编译的时候就已经确定动态多态是用虚函数机制实现的在运行期间动态绑定。举个例子一个父类类型的指针指向一个子类对象时候使用父类的指针去调用子类中重写了的父类中的虚函数的时候会调用子类重写过后的函数在父类中声明为加了virtual关键字的函数在子类中重写时候不需要加virtual也是虚函数。
虚函数的实现在有虚函数的类中类的最开始部分是一个虚函数表的指针这个指针指向一个虚函数表表中放了虚函数的地址实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表当子类重写父类中虚函数时候会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数会增加访问内存开销降低效率。
17. 请你来说一下C中struct和class的区别
在C中class和struct做类型定义如下区别
默认继承权限不同class继承默认是private继承而struct默认是public继承class还可用于定义模板参数像typename但是关键字struct不能用于定义模板参数 C保留struct关键字主要有如下原因保证与C语言的向下兼容性C必须提供一个structC中的struct定义必须百分百地保证与C语言中的struct的向下兼容性把C中的最基本的对象单元规定为class而不是struct就是为了避免各种兼容性要求的限制对struct定义的扩展使C语言的代码能够更容易的被移植到C中
18. 说说C的四种强制类型转换运算符
1、reinterpret_cast
reinterpret_cast type-id (expression)
type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。
2、const_cast
const_cast (expression)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外 type_id和expression的类型是一样的。用法如下
常量指针被转化成非常量的指针并且仍然指向原来的对象常量引用被转换成非常量的引用并且仍然指向原来的对象const_cast一般用于修改底指针。如const char *p形式
3、static_cast
static_cast type-id (expression)
该运算符把expression转换为type-id类型但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法
用于类层次结构中基类父类和派生类子类之间指针或引用引用的转换进行上行转换把派生类的指针或引用转换成基类表示是安全的进行下行转换把基类指针或引用转换成派生类表示时由于没有动态类型检查所以是不安全的用于基本数据类型之间的转换如把int转换成char把int转换成enum。这种转换的安全性也要开发人员来保证。把空指针转换成目标类型的空指针把任何类型的表达式转换成void类型 注意static_cast不能转换掉expression的const、volatile、或者__unaligned属性。
4、dynamic_cast
有类型检查基类向派生类转换比较安全但是派生类向基类转换则不太安全
dynamic_cast (expression)
该运算符把expression转换成type-id类型的对象。type-id 必须是类的指针、类的引用或者void*
如果 type-id 是类指针类型那么expression也必须是一个指针如果 type-id 是一个引用那么 expression 也必须是一个引用
dynamic_cast运算符可以在执行期决定真正的类型也就是说expression必须是多态类型。如果下行转换是安全的也就说如果基类指针或者引用确实指向一个派生类对象这个运算符会传回适当转型过的指针。如果 如果下行转换不安全这个运算符会传回空指针也就是说基类指针或者引用没有指向一个派生类对象
dynamic_cast主要用于类层次间的上行转换和下行转换还可以用于类之间的交叉转换
在类层次间进行上行转换时dynamic_cast和static_cast的效果是一样的
在进行下行转换时dynamic_cast具有类型检查的功能比static_cast更安全
举个例子
#include bits/stdc.h
using namespace std; class Base
{
public: Base() :b(1) {} virtual void fun() {}; int b;
}; class Son : public Base
{
public: Son() :d(2) {} int d;
}; int main()
{ int n 97; //reinterpret_cast int *p n; //以下两者效果相同 char *c reinterpret_castchar* (p); char *c2 (char*)(p); cout reinterpret_cast输出 *c2 endl; //const_cast const int *p2 n; int *p3 const_castint*(p2); *p3 100; cout const_cast输出 *p3 endl; Base* b1 new Son; Base* b2 new Base; //static_cast Son* s1 static_castSon*(b1); //同类型转换 Son* s2 static_castSon*(b2); //下行转换不安全 cout static_cast输出 endl; cout s1-d endl; cout s2-d endl; //下行转换原先父对象没有d成员输出垃圾值 //dynamic_cast Son* s3 dynamic_castSon*(b1); //同类型转换 Son* s4 dynamic_castSon*(b2); //下行转换安全 cout dynamic_cast输出 endl; cout s3-d endl; if(s4 nullptr) cout s4指针为nullptr endl; else cout s4-d endl; return 0;
}
//输出结果
//reinterpret_cast输出a
//const_cast输出100
//static_cast输出
//2
//-33686019
//dynamic_cast输出
//2
//s4指针为nullptr
从输出结果可以看出在进行下行转换时dynamic_cast安全的如果下行转换不安全的话其会返回空指针这样在进行操作的时候可以预先判断。而使用static_cast下行转换存在不安全的情况也可以转换成功但是直接使用转换后的对象进行操作容易造成错误。
19. 简述类成员函数的重写、重载和隐藏的区别
1重写和重载主要有以下几点不同。
范围的区别被重写的和重写的函数在两个类中而重载和被重载的函数在同一个类中。参数的区别被重写函数和重写函数的参数列表一定相同而被重载函数和重载函数的参数列表一 定不同。virtual 的区别重写的基类中被重写的函数必须要有virtual 修饰而重载函数和被重载函数可以被 virtual 修饰也可以没有。
2隐藏和重写、重载有以下几点不同。
与重载的范围不同和重写一样隐藏函数和被隐藏函数不在同一个类中。参数的区别隐藏函数和被隐藏的函数的参数列表可以相同也可不同但是函数名肯定要相同。当参数不相同时无论基类中的函数是否被virtual 修饰基类的函数都是被隐藏而不是被重写。
「注意」虽然重载和覆盖都是实现多态的基础但是两者实现的技术完全不相同达到的目的也是完 全不同的覆盖是动态态绑定的多态而重载是静态绑定的多态。
20. 类型转换分为哪几种各自有什么样的特点
static_cast用于基本数据类型之间的转换、子类向父类的安全转换、void*和其他类型指针之间的转换const_cast用于去除const或volatile属性dynamic_cast用于子类和父类之间的安全转换可以实现向上向下转换因为编译器默认向上转换总是安全的而向下转换时dynamic_cast具有类型检查的功能dynamic_cast转换失败时对于指针会返回目标类型的nullptr对于引用会返回bad_cast异常reinterpret_cast用于不同类型指针之间、不同类型引用之间、指针和能容纳指针的整数类型之间的转换。
21. RTTI是什么其原理是什么
RTTI即运行时类型识别其功能由两个运算符实现
typeid运算符用于返回表达式的类型可以通过基类的指针获取派生类的数据类型dynamic_cast运算符具有类型检查的功能用于将基类的指针或引用安全地转换成派生类的指针或引用。
22. 说一说c中四种cast转换
C中四种类型转换是static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast
用于将const变量转为非const 2、static_cast用于各种隐式转换比如非const转constvoid*转指针等, static_cast能用于多态向上转化如果向下转能成功但是不安全结果未知
3、dynamic_cast
用于动态类型转换。只能用于含有虚函数的类用于类层次间的向上和向下转化。只能转指针或引用。向下转化时如果是非法的对于指针返回NULL对于引用抛异常。要深入了解内部转换的原理。
向上转换指的是子类向基类的转换向下转换指的是基类向子类的转换 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4、reinterpret_cast
几乎什么都可以转比如将int转指针可能会出问题尽量少用
5、为什么不使用C的强制转换
C的强制转换表面上看起来功能强大什么都能转但是转化不够明确不能进行错误检查容易出错。
23. C的空类有哪些成员函数
缺省构造函数。缺省拷贝构造函数。省析构函数。赋值运算符。取址运算符。取址运算符 const 。
「注意」有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是 空类的默认函数。另外需要注意的是只有当实际使用这些函数的时候编译器才会去定义它们。
24. 模板函数和模板类的特例化
「引入原因」
编写单一的模板它能适应多种类型的需求使每种类型都具有相同的功能但对于某种特定类型如果要实现其特有的功能单一模板就无法做到这时就需要模板特例化
「定义」对单一模板提供的一个特殊实例它将一个或多个模板参数绑定到特定的类型或值上
1模板函数特例化
必须为原函数模板的每个模板参数都提供实参且使用关键字template后跟一个空尖括号对表明将原模板的所有模板参数提供实参举例如下
templatetypename T //模板函数
int compare(const T v1,const T v2)
{ if(v1 v2) return -1; if(v2 v1) return 1; return 0;
}
//模板特例化,满足针对字符串特定的比较要提供所有实参这里只有一个T
template
int compare(const char* const v1,const char* const v2)
{ return strcmp(p1,p2);
}
「本质」特例化的本质是实例化一个模板而非重载它。特例化不影响参数匹配。参数匹配都以最佳匹配为原则。例如此处如果是compare(3,5)则调用普通的模板若为compare(“hi”,”haha”)则调用特例化版本因为这个cosnt char*相对于T更匹配实参类型注意二者函数体的语句不一样了实现不同功能。
「注意」模板及其特例化版本应该声明在同一个头文件中且所有同名模板的声明应该放在前面后面放特例化版本。
2类模板特例化
原理类似函数模板不过在类中我们可以对模板进行特例化也可以对类进行部分特例化。对类进行特例化时仍然用template表示是一个特例化版本例如
template
class hashsales_data
{ size_t operator()(sales_data s); //里面所有T都换成特例化类型版本sales_data //按照最佳匹配原则若T ! sales_data就用普通类模板否则就使用含有特定功能的特例化版本。
};
「类模板的部分特例化」
不必为所有模板参数提供实参可以指定一部分而非所有模板参数一个类模板的部分特例化本身仍是一个模板使用它时还必须为其特例化版本中未指定的模板参数提供实参(特例化时类名一定要和原来的模板相同只是参数类型不同按最佳匹配原则哪个最匹配就用相应的模板)
「特例化类中的部分成员」
可以特例化类中的部分成员函数而不是整个类举个例子
templatetypename T
class Foo
{ void Bar(); void Barst(T a)();
}; template
void Fooint::Bar()
{ //进行int类型的特例化处理 cout 我是int型特例化 endl;
} Foostring fs;
Fooint fi;//使用特例化
fs.Bar();//使用的是普通模板即Foostring::Bar()
fi.Bar();//特例化版本执行Fooint::Bar()
//Foostring::Bar()和Fooint::Bar()功能不同
25. 为什么析构函数一般写成虚函数
由于类的多态性基类指针可以指向派生类的对象如果删除该基类的指针就会调用该指针指向的派生类析构函数而派生类的析构函数又自动调用基类的析构函数这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数则编译器实施静态绑定在删除基类指针时只会调用基类的析构函数而不调用派生类析构函数这样就会造成派生类对象析构不完全造成内存泄漏。所以将析构函数声明为虚函数是十分必要的。在实现多态时当用基类操作派生类在析构时防止只析构基类而不析构派生类的状况发生要将基类的析构函数声明为虚函数。举个例子
#include iostream
using namespace std; class Parent{
public: Parent(){ cout Parent construct function endl; }; ~Parent(){ cout Parent destructor function endl; }
}; class Son : public Parent{
public: Son(){ cout Son construct function endl; }; ~Son(){ cout Son destructor function endl; }
}; int main()
{ Parent* p new Son(); delete p; p NULL; return 0;
}
//运行结果
//Parent construct function
//Son construct function
//Parent destructor function
将基类的析构函数声明为虚函数
#include iostream
using namespace std; class Parent{
public: Parent(){ cout Parent construct function endl; }; virtual ~Parent(){ cout Parent destructor function endl; }
}; class Son : public Parent{
public: Son(){ cout Son construct function endl; }; ~Son(){ cout Son destructor function endl; }
}; int main()
{ Parent* p new Son(); delete p; p NULL; return 0;
}
//运行结果
//Parent construct function
//Son construct function
//Son destructor function
//Parent destructor function
三、C STL
1.什么是C STL
C STL从广义来讲包括了三类算法容器和迭代器。
算法包括排序复制等常用算法以及不同容器特定的算法。容器就是数据的存放形式包括序列式容器和关联式容器序列式容器就是listvector等关联式容器就是setmap等。迭代器就是在不暴露容器内部结构的情况下对容器的遍历。
2. 什么时候需要用hash_map什么时候需要用map?
总体来说hash_map 查找速度会比 map 快而且查找速度基本和数据数据量大小无关属于常数级别;而 map 的查找速度是 log(n) 级别。
并不一定常数就比 log(n) 小hash 还有 hash 函数的耗时明白了吧如果你考虑效率特别是在元素达到一定数量级时考虑考虑 hash_map。但若你对内存使用特别严格希望程序尽可能少消耗内存那么一定要小心hash_map 可能会让你陷入尴尬特别是当你的 hash_map 对象特别多时你就更无法控制了。而且 hash_map 的构造速度较慢。
现在知道如何选择了吗权衡三个因素: 查找速度, 数据量, 内存使用 。
3. STL中hashtable的底层实现
STL中的hashtable使用的是开链法解决hash冲突问题
hashtable中的bucket所维护的list既不是list也不是slist而是其自己定义的由hashtable_node数据结构组成的linked-list而bucket聚合体本身使用vector进行存储。hashtable的迭代器只提供前进操作不提供后退操作
在hashtable设计bucket的数量上其内置了28个质数[53, 97, 193,…,429496729]在创建hashtable时会根据存入的元素个数选择大于等于元素个数的质数作为hashtable的容量vector的长度其中每个bucket所维护的linked-list长度也等于hashtable的容量。如果插入hashtable的元素个数超过了bucket的容量就要进行重建table操作即找出下一个质数创建新的buckets vector重新计算元素在新hashtable的位置。
4. vector 底层原理及其相关面试题
1vector的底层原理
vector底层是一个动态数组包含三个迭代器start和finish之间是已经被使用的空间范围end_of_storage是整块连续空间包括备用空间的尾部。
当空间不够装下数据vec.push_back(val)时会自动申请另一片更大的空间1.5倍或者2倍然后把原来的数据拷贝到新的内存空间接着释放原来的那片空间【vector内存增长机制】。
当释放或者删除vec.clear()里面的数据时其存储空间不释放仅仅是清空了里面的数据。
因此对vector的任何操作一旦引起了空间的重新配置指向原vector的所有迭代器会都失效了。
60a71c3d66223b587e83dd18d57f6f09_70.png
2vector中的reserve和resize的区别
reserve是直接扩充到已经确定的大小可以减少多次开辟、释放空间的问题优化push_back就可以提高效率其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小capacity最少达到参数所指定的大小n。reserve()只有一个参数。
resize()可以改变有效空间的大小也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有多个参数。
3vector中的size和capacity的区别
size表示当前vector中有多少个元素finish – start而capacity函数则表示它已经分配的内存中可以容纳多少元素end_of_storage – start。
4vector的元素类型可以是引用吗
vector的底层实现要求连续的对象排列引用并非对象没有实际地址因此vector的元素类型不能是引用。
5vector迭代器失效的情况
当插入一个元素到vector中由于引起了内存重新分配所以指向原内存的迭代器全部失效。
当删除容器中一个元素后,该迭代器所指向的元素已经被删除那么也造成迭代器失效。erase方法会返回下一个有效的迭代器所以当我们要删除某个元素时需要itvec.erase(it);。
6正确释放vector的内存(clear(), swap(), shrink_to_fit())
vec.clear()清空内容但是不释放内存。vector().swap(vec)清空内容且释放内存想得到一个全新的vector。vec.shrink_to_fit()请求容器降低其capacity和size匹配。vec.clear();vec.shrink_to_fit();清空内容且释放内存。
7vector 扩容为什么要以1.5倍或者2倍扩容?
根据查阅的资料显示考虑可能产生的堆空间浪费成倍增长倍数不能太大使用较为广泛的扩容方式有两种以2倍的方式扩容或者以1.5倍的方式扩容。
以2倍的方式扩容导致下一次申请的内存必然大于之前分配内存的总和导致之前分配的内存不能再被使用所以最好倍增长因子设置为(1,2)之间
929920f50fde91c86bcc25d2ff600525_20160915175318430.png
8vector的常用函数
vectorint vec(10,100); 创建10个元素,每个元素值为100
vec.resize(r,vectorint(c,0)); 二维数组初始化
reverse(vec.begin(),vec.end()) 将元素翻转
sort(vec.begin(),vec.end()); 排序默认升序排列
vec.push_back(val); 尾部插入数字
vec.size(); 向量大小
find(vec.begin(),vec.end(),1); 查找元素
iterator vec.erase(iterator) 删除元素
5.list 底层原理及其相关面试题
list 底层原理及其相关面试题
1list的底层原理
list的底层是一个双向链表以结点为单位存放数据结点的地址在内存中不一定连续每次插入或删除一个元素就配置或释放一个元素空间。
list不支持随机存取适合需要大量的插入和删除而不关心随即存取的应用场景。
2ac9186574aae0cc4d9406e994da19d5_format,png.png
2list的常用函数
list.push_back(elem) 在尾部加入一个数据
list.pop_back() 删除尾部数据
list.push_front(elem) 在头部插入一个数据
list.pop_front() 删除头部数据
list.size() 返回容器中实际数据的个数
list.sort() 排序默认由小到大
list.unique() 移除数值相同的连续元素
list.back() 取尾部迭代器
list.erase(iterator) 删除一个元素参数是迭代器返回的是删除迭代器的下一个位置
6.deque底层原理及其相关面试题
1deque的底层原理
deque是一个双向开口的连续线性空间双端队列在头尾两端进行元素的插入跟删除操作都有理想的时间复杂度。
2什么情况下用vector什么情况下用list什么情况下用deque
vector可以随机存储元素即可以通过公式直接计算出元素地址而不需要挨个查找但在非尾部插入删除数据时效率很低适合对象简单对象数量变化不大随机访问频繁。除非必要我们尽可能选择使用vector而非deque因为deque的迭代器比vector迭代器复杂很多。
list不支持随机存储适用于对象大对象数量变化频繁插入和删除频繁比如写多读少的场景。
需要从首尾两端进行插入或删除操作的时候需要选择deque。
3deque的常用函数
deque.push_back(elem) 在尾部加入一个数据。
deque.pop_back() 删除尾部数据。
deque.push_front(elem) 在头部插入一个数据。
deque.pop_front() 删除头部数据。
deque.size() 返回容器中实际数据的个数。
deque.at(idx) 传回索引idx所指的数据如果idx越界抛出out_of_range。
7.Vector如何释放空间?
由于vector的内存占用空间只增不减比如你首先分配了10,000个字节然后erase掉后面9,999个留下一个有效元素但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。empty()用来检测容器是否为空的clear()可以清空所有元素。但是即使clear()vector所占用的内存空间依然如故无法保证内存的回收。
如果需要空间动态缩小可以考虑使用deque。如果vector可以用swap()来帮助你释放内存。
vector(Vec).swap(Vec); //将Vec的内存清除
vector().swap(Vec); //清空Vec的内存
8.如何在共享内存上使用STL标准库
想像一下把STL容器例如map, vector, list等等放入共享内存中IPC一旦有了这些强大的通用数据结构做辅助无疑进程间通信的能力一下子强大了很多。
我们没必要再为共享内存设计其他额外的数据结构另外STL的高度可扩展性将为IPC所驱使。STL容器被良好的封装默认情况下有它们自己的内存管理方案。
当一个元素被插入到一个STL列表(list)中时列表容器自动为其分配内存保存数据。考虑到要将STL容器放到共享内存中而容器却自己在堆上分配内存。
一个最笨拙的办法是在堆上构造STL容器然后把容器复制到共享内存并且确保所有容器的内部分配的内存指向共享内存中的相应区域这基本是个不可能完成的任务。
假设进程A在共享内存中放入了数个容器进程B如何找到这些容器呢
一个方法就是进程A把容器放在共享内存中的确定地址上fixed offsets则进程B可以从该已知地址上获取容器。另外一个改进点的办法是进程A先在共享内存某块确定地址上放置一个map容器然后进程A再创建其他容器然后给其取个名字和地址一并保存到这个map容器里。
进程B知道如何获取该保存了地址映射的map容器然后同样再根据名字取得其他容器的地址。
9. map插入方式有哪几种
用insert函数插入pair数据
mapStudent.insert(pairint, string(1, student_one));
用insert函数插入value_type数据
mapStudent.insert(mapint, string::value_type (1, student_one));
在insert函数中使用make_pair()函数
mapStudent.insert(make_pair(1, student_one));
用数组方式插入数据
mapStudent[1] student_one;
10.map 、set、multiset、multimap 底层原理及其相关面试题
1map 、set、multiset、multimap的底层原理
map 、set、multiset、multimap的底层实现都是红黑树epoll模型的底层数据结构也是红黑树linux系统中CFS进程调度算法也用到红黑树。
红黑树的特性
每个结点或是红色或是黑色根结点是黑色每个叶结点是黑的如果一个结点是红的则它的两个儿子均是黑色每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。 对于STL里的map容器count方法与find方法都可以用来判断一个key是否出现mp.count(key) 0统计的是key出现的次数因此只能为0/1而mp.find(key) ! mp.end()则表示key存在。
2map 、set、multiset、multimap的特点
set和multiset会根据特定的排序准则自动将元素排序set中元素不允许重复multiset可以重复。
map和multimap将key和value组成的pair作为元素根据key的排序准则自动将元素排序因为红黑树也是二叉搜索树所以map默认是按key排序的map中元素的key不允许重复multimap可以重复。
map和set的增删改查速度为都是logn是比较高效的。
3为何map和set的插入删除效率比其他序列容器高而且每次insert之后以前保存的iterator不会失效
因为存储的是结点不需要内存拷贝和内存移动。
因为插入操作只是结点指针换来换去结点内存没有改变。而iterator就像指向结点的指针内存没变指向内存的指针也不会变。
4为何map和set不能像vector一样有个reserve函数来预分配数据?
因为在map和set内部存储的已经不是元素本身了而是包含元素的结点。也就是说map内部使用的Alloc并不是mapKey, Data, Compare, Alloc声明的时候从参数中传入的Alloc。
5map 、set、multiset、multimap的常用函数
it map.begin() 返回指向容器起始位置的迭代器iterator
it map.end() 返回指向容器末尾位置的迭代器
bool map.empty() 若容器为空则返回true否则false
it map.find(k) 寻找键值为k的元素并用返回其地址
int map.size() 返回map中已存在元素的数量
map.insert({int,string}) 插入元素
for (itor map.begin(); itor ! map.end();)
{ if (itor-second target) map.erase(itor) ; // erase之后令当前迭代器指向其后继。 else itor;
}
11.unordered_map、unordered_set 底层原理及其相关面试题
1unordered_map、unordered_set的底层原理
unordered_map的底层是一个防冗余的哈希表采用除留余数法。哈希表最大的优点就是把数据的存储和查找消耗的时间大大降低时间复杂度为O(1)而代价仅仅是消耗比较多的内存。
使用一个下标范围比较大的数组来存储元素。可以设计一个函数哈希函数一般使用除留取余法也叫做散列函数使得每个元素的key都与一个函数值即数组下标hash值相对应于是用这个数组单元来存储这个元素也可以简单的理解为按照key为每一个元素“分类”然后将这个元素存储在相应“类”所对应的地方称为桶。
但是不能够保证每个元素的key与函数值是一一对应的因此极有可能出现对于不同的元素却计算出了相同的函数值这样就产生了“冲突”换句话说就是把不同的元素分在了相同的“类”之中。 一般可采用拉链法解决冲突
2哈希表的实现
#include iostream
#include vector
#include list
#include random
#include ctime
using namespace std; const int hashsize 12; //定一个节点的结构体
template typename T, typename U
struct HashNode
{ T _key; U _value;
}; //使用拉链法实现哈希表类
template typename T, typename U
class HashTable
{
public: HashTable() : vec(hashsize) {}//类中的容器需要通过构造函数来指定大小 ~HashTable() {} bool insert_data(const T key, const U value); int hash(const T key); bool hash_find(const T key);
private: vectorlistHashNodeT, U vec;//将节点存储到容器中
}; //哈希函数除留取余
template typename T, typename U
int HashTableT, U::hash(const T key)
{ return key % 13;
} //哈希查找
template typename T, typename U
bool HashTableT, U::hash_find(const T key)
{ int index hash(key);//计算哈希值 for (auto it vec[index].begin(); it ! vec[index].end(); it) { if (key it - _key)//如果找到则打印其关联值 { cout it-_value endl;//输出数据前应该确认是否包含相应类型 return true; } } return false;
} //插入数据
template typename T, typename U
bool HashTableT, U::insert_data(const T key, const U value)
{ //初始化数据 HashNodeT, U node; node._key key; node._value value; for (int i 0; i hashsize; i) { if (i hash(key))//如果溢出则把相应的键值添加进链表 { vec[i].push_back(node); return true; } }
} int main(int argc, char const *argv[])
{ HashTableint, int ht; static default_random_engine e; static uniform_int_distributionunsigned u(0, 100); long long int a 10000000; for (long long int i 0; i a; i) ht.insert_data(i, u(e)); clock_t start_time clock(); ht.hash_find(114); clock_t end_time clock(); cout Running time is: static_castdouble(end_time - start_time) / CLOCKS_PER_SEC * 1000 ms endl;//输出运行时间。 system(pause); system(pause); return 0;
}
3unordered_map 与map的区别使用场景
构造函数unordered_map 需要hash函数等于函数;map只需要比较函数(小于函数).
存储结构unordered_map 采用hash表存储map一般采用红黑树(RB Tree) 实现。因此其memory数据结构是不一样的。
总体来说unordered_map 查找速度会比map快而且查找速度基本和数据数据量大小属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n)小hash还有hash函数的耗时明白了吧如果你考虑效率特别是在元素达到一定数量级时考虑考虑unordered_map 。但若你对内存使用特别严格希望程序尽可能少消耗内存那么一定要小心unordered_map 可能会让你陷入尴尬特别是当你的unordered_map 对象特别多时你就更无法控制了而且unordered_map 的构造速度较慢。
4unordered_map、unordered_set的常用函数
unordered_map.begin() 返回指向容器起始位置的迭代器iterator
unordered_map.end() 返回指向容器末尾位置的迭代器
unordered_map.cbegin() 返回指向容器起始位置的常迭代器const_iterator
unordered_map.cend() 返回指向容器末尾位置的常迭代器
unordered_map.size() 返回有效元素个数
unordered_map.insert(key) 插入元素
unordered_map.find(key) 查找元素返回迭代器
unordered_map.count(key) 返回匹配给定主键的元素的个数
12. 迭代器的底层机制和失效的问题
1、迭代器的底层原理
迭代器是连接容器和算法的一种重要桥梁通过迭代器可以在不了解容器内部原理的情况下遍历容器。它的底层实现包含两个重要的部分萃取技术和模板偏特化。
萃取技术traits可以进行类型推导根据不同类型可以执行不同的处理流程比如容器是vector那么traits必须推导出其迭代器类型为随机访问迭代器而list则为双向迭代器。
例如STL算法库中的distance函数distance函数接受两个迭代器参数然后计算他们两者之间的距离。显然对于不同的迭代器计算效率差别很大。比如对于vector容器来说由于内存是连续分配的因此指针直接相减即可获得两者的距离而list容器是链式表内存一般都不是连续分配因此只能通过一级一级调用next()或其他函数每调用一次再判断迭代器是否相等来计算距离。vector迭代器计算distance的效率为O(1),而list则为O(n),n为距离的大小。
使用萃取技术traits进行类型推导的过程中会使用到模板偏特化。模板偏特化可以用来推导参数如果我们自定义了多个类型除非我们把这些自定义类型的特化版本写出来否则我们只能判断他们是内置类型并不能判断他们具体属于是个类型。
template typename T
struct TraitsHelper { static const bool isPointer false;
};
template typename T
struct TraitsHelperT* { static const bool isPointer true;
}; if (TraitsHelperT::isPointer) ...... // 可以得出当前类型int*为指针类型
else ...... // 可以得出当前类型int非指针类型
C
2、一个理解traits的例子
// 需要在T为int类型时Compute方法的参数为int返回类型也为int
// 当T为float时Compute方法的参数为float返回类型为int
template typename T
class Test {
public: TraitsHelperT::ret_type Compute(TraitsHelperT::par_type d);
private: T mData;
}; template typename T
struct TraitsHelper { typedef T ret_type; typedef T par_type;
}; // 模板偏特化处理int类型
template
struct TraitsHelperint { typedef int ret_type; typedef int par_type;
}; // 模板偏特化处理float类型
template
struct TraitsHelperfloat { typedef float ret_type; typedef int par_type;
};
当函数类或者一些封装的通用算法中的某些部分会因为数据类型不同而导致处理或逻辑不同时traits会是一种很好的解决方案。
2、迭代器的种类
输入迭代器是只读迭代器在每个被遍历的位置上只能读取一次。例如上面find函数参数就是输入迭代器。
输出迭代器是只写迭代器在每个被遍历的位置上只能被写一次。
前向迭代器兼具输入和输出迭代器的能力但是它可以对同一个位置重复进行读和写。但它不支持operator–所以只能向前移动。
双向迭代器很像前向迭代器只是它向后移动和向前移动同样容易。
随机访问迭代器有双向迭代器的所有功能。而且它还提供了“迭代器算术”即在一步内可以向前或向后跳跃任意位置 包含指针的所有操作可进行随机访问随意移动指定的步数。支持前面四种Iterator的所有操作并另外支持it n、it – n、it n、 it - n、it1 – it2和it[n]等操作。
3、迭代器失效的问题
插入操作
对于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有效。
对于关联容器map来说如果某一个元素已经被删除那么其对应的迭代器就失效了不应该再被使用否则会导致程序无定义的行为。
13. 为什么vector的插入操作可能会导致迭代器失效
vector动态增加大小时并不是在原空间后增加新的空间而是以原大小的两倍在另外配置一片较大的新空间然后将内容拷贝过来并释放原来的空间。由于操作改变了空间所以迭代器失效。
14. vector的reserve()和resize()方法之间有什么区别
首先vector的容量capacity()是指在不分配更多内存的情况下可以保存的最多元素个数而vector的大小size()是指实际包含的元素个数其次vector的reserve(n)方法只改变vector的容量如果当前容量小于n则重新分配内存空间调整容量为n如果当前容量大于等于n则无操作最后vector的resize(n)方法改变vector的大小如果当前容量小于n则调整容量为n同时将其全部元素填充为初始值如果当前容量大于等于n则不调整容量只将其前n个元素填充为初始值。
15. 标准库中有哪些容器分别有什么特点
标准库中的容器主要分为三类顺序容器、关联容器、容器适配器。
顺序容器包括五种类型
arrayT, N数组固定大小数组支持快速随机访问但不能插入或删除元素vector动态数组支持快速随机访问尾位插入和删除的速度很快deque双向队列支持快速随机访问首尾位置插入和删除的速度很快可以看作是vector的增强版与- - vector相比可以快速地在首位插入和删除元素list双向链表只支持双向顺序访问任何位置插入和删除的速度都很快forward_list单向链表只支持单向顺序访问任何位置插入和删除的速度都很快。
关联容器包含两种类型
map容器
mapK, T关联数组用于保存关键字-值对multimapK, T关键字可重复出现的mapunordered_mapK, T用哈希函数组织的mapunordered_multimapK, T关键词可重复出现的unordered_map
set容器
set只保存关键字multiset关键字可重复出现的setunordered_set用哈希函数组织的setunordered_multiset关键词可重复出现的unordered_set
容器适配器包含三种类型
stack栈、queue队列、priority_queue优先队列。
16. 容器内部删除一个元素
1) 顺序容器序列式容器比如vector、deque
erase迭代器不仅使所指向被删除的迭代器失效而且使被删元素之后的所有迭代器失效(list除外)所以不能使用erase(it)的方式但是erase的返回值是下一个有效迭代器
It c.erase(it);
2) 关联容器(关联式容器比如map、set、multimap、multiset等)
erase迭代器只是被删除元素的迭代器失效但是返回值是void所以要采用erase(it)的方式删除迭代器
c.erase(it)
17. vector越界访问下标map越界访问下标vector删除元素时会不会释放空间
通过下标访问vector中的元素时会做边界检查但该处的实现方式要看具体IDE不同IDE的实现方式不一样确保不可访问越界地址。map的下标运算符[]的作用是将key作为下标去执行查找并返回相应的值如果不存在这个key就将一个具有该key和value的某人值插入这个map。erase()函数只能删除内容不能改变容量大小;
erase成员函数它删除了itVect迭代器指向的元素并且返回要被删除的itVect之后的迭代器迭代器相当于一个智能指针;clear()函数只能清空内容不能改变容量大小;如果要想在删除内容的同时释放内存那么你可以选择deque容器。
18. map中[ ]与find的区别
map的下标运算符[ ]的作用是将关键码作为下标去执行查找并返回对应的值如果不存在这个关键码就将一个具有该关键码和值类型的默认值的项插入这个map。map的find函数用关键码执行查找找到了返回该位置的迭代器如果不存在这个关键码就返回尾迭代器。
19. STL内存优化
STL内存管理使用二级内存配置器。
(1) 第一级配置器
第一级配置器以malloc()free()realloc()等C函数执行实际的内存配置、释放、重新配置等操作并且能在内存需求不被满足的时候调用一个指定的函数。一级空间配置器分配的是大于128字节的空间如果分配不成功调用句柄释放一部分内存如果还不能分配成功抛出异常。
第一级配置器只是对malloc函数和free函数的简单封装在allocate内调用malloc在deallocate内调用free。同时第一级配置器的oom_malloc函数用来处理malloc失败的情况。
(2) 第二级配置器
第一级配置器直接调用malloc和free带来了几个问题
内存分配/释放的效率低当配置大量的小内存块时会导致内存碎片比较严重配置内存时需要额外的部分空间存储内存块信息所以配置大量的小内存块时还会导致额外内存负担 如果分配的区块小于128bytes则以内存池管理第二级配置器维护了一个自由链表数组每次需要分配内存时直接从相应的链表上取出一个内存节点就完成工作效率很高
自由链表数组自由链表数组其实就是个指针数组数组中的每个指针元素指向一个链表的起始节点。数组大小为16即维护了16个链表链表的每个节点就是实际的内存块相同链表上的内存块大小都相同不同链表的内存块大小不同从8一直到128。如下所示obj为链表上的节点free_list就是链表数组。
内存分配allocate函数内先判断要分配的内存大小若大于128字节直接调用第一级配置器否则根据要分配的内存大小从16个链表中选出一个链表取出该链表的第一个节点。若相应的链表为空则调用refill函数填充该链表。默认是取出20个数据块。
填充链表 refill若allocate函数内要取出节点的链表为空则会调用refill函数填充该链表。refill函数内会先调用chunk_alloc函数从内存池分配一大块内存该内存大小默认为20个链表节点大小当内存池的内存也不足时返回的内存块节点数目会不足20个。接着refill的工作就是将这一大块内存分成20份相同大小的内存块并将各内存块连接起来形成一个链表。
内存池chunk_alloc函数内管理了一块内存池当refill函数要填充链表时就会调用chunk_alloc函数从内存池取出相应的内存。
在chunk_alloc函数内首先判断内存池大小是否足够填充一个有20个节点的链表若内存池足够大则直接返回20个内存节点大小的内存块给refill若内存池大小无法满足20个内存节点的大小但至少满足1个内存节点则直接返回相应的内存节点大小的内存块给refill若内存池连1个内存节点大小的内存块都无法提供则chunk_alloc函数会将内存池中那一点点的内存大小分配给其他合适的链表然后去调用malloc函数分配的内存大小为所需的两倍。若malloc成功则返回相应的内存大小给refill若malloc失败会先搜寻其他链表的可用的内存块添加到内存池然后递归调用chunk_alloc函数来分配内存若其他链表也无内存块可用则只能调用第一级空间配置器。
20. 频繁对vector调用push_back()对性能的影响和原因
在一个vector的尾部之外的任何位置添加元素都需要重新移动元素。而且向一个vector添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存并将元素从旧的空间移到新的空间。