C++基础概念(上)1

简介: C++基础概念(上)1

C++基础概念

C++基础概念 1


1.linux中静态库和动态库的异同 3

2.是否了解动态规划 11

3.STL源码中的map实现(红黑树的五点要求) 12

B+树 13

4.map和unorder_map(hash_map)的区别 14

5.new一个对象涉及几个步骤?其中哪个步骤可以通过重载new操作符来修改 15

6.malloc一次性最大能申请多大内存空间 16

7.new和malloc的区别 17

8.delete和free的区别 19

9.什么时候必须用delete[] 20

10.静态多态 动态多态 21

11.动态绑定(后期绑定,运行时绑定),静态绑定(前期绑定,编译时绑定) 21

12.学过的设计模式? 状态机模式,简单工厂,策率模式,享元模式,模板模式 1次 24

13.指针和引用的区别: 24

14.C语言的代码内存布局详解 26

15.内存中堆和栈的区别 29

16.用户进程内存空间 30

17.类型转换有哪些?(四种类型转换,分别举例说明) 30

18.内存对齐原则 33

19.内联函数(讲了一下内联函数的优点以及和宏定义的区别) 35

20.typedef和#define的用法与区别 37

21.const 和宏定义#define的区别 39

22.链接指示:extern “C” 40

23.extern与include 44

24.继承机制中对象之间,引用和指针之间如何转换 45

25.如何实现只能动态分配类对象,不能定义类对象 46

26模板的优缺点 47

27.模板特化 48

28.explicit作用: Primer P264 50

29.strcpy函数 53

30.memcpy的缺陷 55

31.内存溢出,内存泄漏 56

32.查看内存泄漏的软件都有什么 58

33.初始化列表 59

34.public、protected、private访问属性及继承 60

35.智能指针 61

36.虚函数和纯虚函数 64

37.构造函数为什么不能是虚函数,虚析构函数是用来做什么的 65

38.pthread_create()的函数(Linux环境中的一个创建线程函数)原型写出来 66

39.C中volatile关键字 67

40.为什么父类指针可以指向子类反之则不行 68

41.stl中的list中的size函数是如何实现的?是遍历还是设置一个变量来保存?让你实现,你倾向哪一种,为什么? 69

42.子类a多重继承于b和c,b和c都有虚函数,此时a中有几张虚函数表?有几个虚函数指针?当依次调用分属b和c的虚函数时,虚函数指针怎么变化? 70

43.浅拷贝还是深拷贝 72

44.拷贝构造函数和赋值运算符的区别 76

45.系统调用里有sleep(),usleep()函数,usleep()函数号称自己是微秒级,你相信它真的能达到这么快吗?或者说系统调用可以达到微秒速度吗? 77

46.重载、覆盖、隐藏 79

47.this, const成员函数、mutable类型 80

48.STL内存管理 81

49.static 全局变量 和 非static全局变量的区别 83

50.编写makefile文件 84

51.coredump产生的几种可能情况 85

52.网络连接中的长链接和短链接。 86

53.同步IO和异步IO,阻塞和非阻塞 87

54.实现栈和队列 89

55.大端小端 90

56.编译器都会做哪些优化 92

57.回调一个对象的成员函数,但是这个对象可能已经不存在了,怎么办? 93

58.介绍一下C++11的特性 94

59.硬盘拷贝速度,内存拷贝速度 95

60.Gdb调试,调试core dump文件 96

61.构造函数可以调用虚函数吗 98

62.dynamic_cast转换失败时,会出现什么情况? 100

63.为什么析构函数不能够抛出异常 101

1.linux中静态库和动态库的异同

本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。

所谓静态、动态是指链接。

回顾一下,将一个程序编译成可执行程序的步骤:

a158a2304da440c189392c09311cf338.png

图:编译过程

之所以成为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。

1802c1a078ff4bcaba63a148bb23e899.png

图:创建静态库过程

Linux静态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.a。

通过上面的介绍发现静态库,容易使用和理解,也达到了代码复用的目的,那为什么还需要动态库呢?

为什么还需要动态库?

为什么需要动态库,其实也是静态库的特点导致。

 空间浪费是静态库的一个问题

e29cd4aa56e94954ba03d19aad033e81.png

 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。

9abe7593f07e48d1a6066fc2ee0c5435.png

动态库特点总结:

 动态库把对一些库函数的链接载入推迟到程序运行的时期。

 可以实现进程之间的资源共享。(因此动态库也称为共享库)

 将一些程序升级变得简单。

 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。

与创建静态库不同的是,不需要打包工具(ar、lib.exe),直接使用编译器即可创建动态库。

linux动态库的命名规则

动态链接库的名字形式为 libxxx.so,前缀是lib,后缀名为“.so”。

Linux下库相关命令

g++(gcc)编译选项

-shared:指定生成动态链接库。

-static:指定生成静态链接库。

-fPIC :表示编译为位置独立的代码,用于编译共享库。目标文件需要创建成位置无关码, 念上就是在可执行程序装载它们的时候,它们可以放在可执行程序的内存里的任何地方。

-L. :表示要连接的库所在的目录。

-l:指定链接时需要的动态库。编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.a/.so来确定库的名称。

nm命令

有时候可能需要查看一个库中到底有哪些函数,nm命令可以打印出库中的涉及到的所有符号。库既可以是静态的也可以是动态的。nm列出的符号有很多,常见的有三种:

 一种是在库中被调用,但并没有在库中定义(表明需要其他库支持),用U表示;

 一种是库中定义的函数,用T表示,这是最常见的;

一种是所谓的弱态”符号,它们虽然在库中被定义,但是可能被其他库中的同名符号覆盖,用W表示。

例如:nm libhello.a

ldd命令

ldd命令可以查看一个可执行程序依赖的共享库,例如我们编写的四则运算动态库依赖下面这些库:

ee2af9b2d0454b31b005a36ffc882a02.png

总结:

一.不同

1.名字: libxxx.a libxxx.so

2.链接时间

静态库在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中 比较大 更新时(即使修改了一点)也要全部更新。

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入 比较小 有修改时,只需要更新对应的动态库即可。

3.生成的过程不同,.a用ar工具生成,而.so用gcc即可。

生成静态库

de24241639a54318b161aae4e96e861f.png

将DynamicMath.cpp生成动态库

8f6c2fda6653462d98c1f49a4bfbfdda.png

以下不是区别:

使用静态库,动态库都想下面这样:

19e00baad94c4f4d98f1dc9a1f93b0bf.png

但动态库在上述命令后可能会报错所找不到动态库文件libname.so。因为程序在运行时,会在/usr/lib和/lib等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。我们将文件 libname.so复制到目录/usr/lib中即可。


二、相同

都是由*.o目标文件生成

都是为了代码重用


这篇写的特别好

这个链接中对整个链接动态库的整个过程做了实验

.a 静态库 archive(用ar工具生成) 档案馆的意思,就像档案馆这个名字一样,其实他就是一组目标文件(.o)的集合而已。即许多目标文件经过压缩打包后形成的一个文件。


2.是否了解动态规划

动归,本质上是一种划分子问题的算法,站在任何一个子问题的处理上看,当前子问题的提出都要依据现有的类似结论,而当前问题的结论是后 面问题求解的铺垫。任何DP都是基于存储的算法,核心是状态转移方程。


3.STL源码中的map实现(红黑树的五点要求)

我们什么时候用到了红黑树?

C++STL中map,set的底层实现全是用的红黑树,java,C#等语言同样如此。

为什么需要红黑树?

map,set底层都提供了排序功能,且查找速度快。红黑树实际上是AVL的一种变形,但是其比AVL(平衡二叉搜索树)具有更高的插入效率,当然查找效率会平衡二叉树稍微低一点点,毕竟平衡二叉树太完美了。但是这种查找效率的损失是非常值得的。它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。


  我说用散列表也可以,unordered_map底层就是用的散列表。红黑树可以保证有序(O(logn)),散列表不保证有序,但由于是通过hash到桶实现的,自然复杂度是O(n). 红黑树需要两个指针,散列表冲突的情况下也只需要一个指针,所以需要的空间少一些。

若考虑有序,查询速度稳定,容器元素量少于1000,非频繁查询那么考虑使用map。

若非常高频查询(100个元素以上,unordered_map都会比map快),内部元素可非有序,数据大超过1k甚至几十万上百万时候就要考虑使用unordered_map

何为红黑树?

这里二叉平衡树的概念我就不提了。红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。

性质1 节点是红色或黑色。

性质2 根节点是黑色。

性质3 每个叶节点(NIL节点,空节点)是黑色的。

性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

性质5 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(部分平衡)。

这些约束的好处是:保持了树的相对平衡,同时又比AVL的插入删除操作的复杂性要低许多。

红黑树和AVL树的区别:

平衡二叉树的追求的是全局均衡,如在做插入,删除操作时,需要调整整棵树,显然这是费时的,因此希望在做调整时,是局部调整,因此提出了红黑树。

红黑树并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。

红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构,能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。

B+树

待整理

见数据库索引


4.map和unorder_map(hash_map)的区别

1.实现原理,红黑树,散列表

2.速度,O(logn),O(1)。自然红黑树占用CPU高。

map具有稳定性,底层存储为树,这种算法差不多相当与list线性容器的折半查找的效率一样,都是O (log2N)。而hash_map使用hash表来排列配对,hash表是使用关键字来计算表位置。当这个表的大小合适,并且计算算法合适的情况下,hash表的算法复杂度为O(1)的,但是这是理想的情况下的,如果hash表的关键字计算与表位置存在冲突,那么最坏的复杂度为O(n)。 map在一次查找中,你可以断定它最坏的情况下其复杂度不会超过O(log2N)。而hash表就不一样,是O(1),还是O(N),或者在其之间,你并不能把握。

3.内存,红黑树至少两指针,散列表用冲突的话用链表解决的话也就一个指针,内存占用小。

4.map保证有序,需要保证有序的话必须使用map。若是问统计关键字出现次数map用什么实现:hash map,它效率高内存小,不需要保存有序。

5.数据小的时候大家差不多,数据量大用hash_map

2,3,4,5都是由于实现不同导致的差异。


unorder_map和hash_map都是用散列表实现的,但前者是c++标准之一,后者是一些库对STL的扩展,推荐用前者,支持的编译器多,好移植。


5.new一个对象涉及几个步骤?其中哪个步骤可以通过重载new操作符来修改

当用户用new构造一个对象的时候,其实内含两种操作:1)调用::operator new申请内存;2)调用该对象的构造函数构造此对象的内容

当用户用delete销毁一个对象时,其实内含两种操作:1)调用该对象的析构函数析构该对象的内容;2)调用::operator delete释放内存


分配足够的内存以容纳对象,然后调用构造函数初始化上一步所分配的内存。

你所能改变的只是第一步的行为,如何为对象分配RAW内存。


new操作符会调用operator new函数,new操作符进行两步:分配内存,调用构造函数。第一步就是通过调用operator new函数来完成的,可以通过重载operator new函数来改变第一步。


第二步是编译器调用的,程序员可以手动调用吗?可以通过placement new来做到:

详细请见


6.malloc一次性最大能申请多大内存空间

32位最大是4G。有一部分要给内核。比如32位下windows除内核外剩余大概2g,而Linux3G,其余的都是给了内核。Malloc是在堆上,栈还要占用一部分。

详细请见


7.new和malloc的区别

1、new是c++中的操作符,malloc是c 中的一个函数

2、new不止是分配内存,而且会调用类的构造函数,同理delete会调用类的析构函数,而malloc则只分配内存,不会进行初始化类成员的工作,同样free也不会调用析构函数

3、new自动分配大小,malloc要用户指定。

new出来的指针是直接带类型信息的。而malloc返回的都是void指针

4、new和malloc效率比较

new可以认为是malloc加构造函数的执行。


malloc原理:

malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。


malloc()到底从哪里得来了内存空间:

1、malloc()到底从哪里得到了内存空间?答案是从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。就是这样!

2、什么是堆:堆是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程 初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。

什么是栈:栈是线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立。每个函数都有自己的栈,栈被用来在函数之间传递参数。操作系统在切换线程的时候会自动的切换栈,就是切换SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

通过上面对概念的描述,可以知道:

栈是由编译器自动分配释放,存放函数的参数值、局部变量的值等。操作方式类似于数据结构中的栈。

堆一般由程序员分配释放,若不释放,程序结束时可能由OS回收。注意这里说是可能,并非一定。所以我想再强调一次,记得要释放!

注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。


8.delete和free的区别

delete 用于释放 new 分配的空间,free 有用释放 malloc 分配的空间

delete [] 用于释放 new [] 分配的空间

delete 释放空间的时候会调用 相应对象的析构函数

顺便说一下new在分配空间的时候同时会调用对象的构造函数,对对象进行初始化,使用malloc则只是分配内存

调用free 之前需要检查 需要释放的指针是否为空,使用delete 释放内存则不需要检查指针是否为NULL

free 和 delete 不能混用,也就是说new 分配的内存空间最好不要使用使用free 来释放,malloc 分配的空间也不要使用 delete来释放

补充一个问题,free和delete 是如何知道需要释放的内存块的大小的?

在调用malloc或new 分配内存空间的时候,实际分配的空间会比程序员申请的空间要大。实际分配的内存空间前面有一部分空间用于保存所分配内存的大小,校验和等信息。当分配函数返回时,将会返回实际可操作的地址(也就是实际分配空间加上前面用于记录分配信息的空间之后的地址)。下面举个例子,例子通过破坏 new 返回地址的前面四个字节的数据导致内存空间释放出问题。如果不破坏前面的数据则不会出现内存不能释放的情况。

9.什么时候必须用delete[]

int a = new int[10] delete a; 上述代码是否有问题,会内存泄漏吗?(不会)

class A {public: int a[100]; }; A pa = new A[100]; delete pa;上述代码是否有问题?会内存泄漏吗?(不会)

delete 跟 delete []到底内在有何区别?什么情况下必须用delete [],否则内存泄漏。


对于像int/char/long/int*/struct等等简单数据类型,由于对象没有destructor,所以用delete 和delete [] 是一样的


delete跟delete[]都会释放指针所指内存区域(地址会记录区域的大小、数目等,所以无论单个对象还是数组,都能正确释放内存),只不过delete只调用第一个对象的析构函数,而delete[]会调用后续所有对象的析构函数。对于对象数组,只要内存是连续的,用delete来释放是完全没有问题的。delete[]只有在对象中有分配在堆上的数据时才必须使用,因为堆上的数据只有各自的析构函数知道如何释放。


你一定会问,反正不管怎样都是把存储空间释放了,有什么区别。

答:关键在于调用析构函数上。此程序的类没有使用操作系统的系统资源(比如:Socket、File、Thread等),所以不会造成明显恶果。如果你的类使用了操作系统资源,单纯把类的对象从内存中删除是不妥当的,因为没有调用对象的析构函数会导致系统资源不被释放,如果是Socket则会造成Socket资源不被释放,最明显的就是端口号不被释放,系统最大的端口号是65535(216 _ 1,因为还有0),如果端口号被占用了,你就不能上网了,呵呵。如果File资源不被释放,你就永远不能修改这个文件,甚至不能读这个文件(除非注销或重器系统)。如果线程不被释放,这它总在后台运行,浪费内存和CPU资源。这些资源的释放必须依靠这些类的析构函数。

详细请见


10.静态多态 动态多态

编译期多态(静态多态,早绑定)----------重载和模板

运行期多态(动态多态,晚绑定)----------虚函数

11.动态绑定(后期绑定,运行时绑定),静态绑定(前期绑定,编译时绑定)

C++中,非虚函数都是静态绑定,而虚函数却是动态绑定。

2.什么是早绑定?

早绑定是指在程序运行之前,即编译和链接是执行的绑定就是早绑定!


3.什么事迟绑定?

迟绑定就是指发生在运行时,基于不同类型的对象。对函数调用不同的函数体是发生的绑定。当一种语言要实现迟绑定是,必须有某种机制去确定对象的具体类型,然后调用适当的成员函数!


其次我们来看一个小例子:

4442c3a012164f10b3eec3467e5f7806.png

fef07720b1754a5c90eac65fe876b9c0.png


程序运行结果:

Animall breath;

Fish sleep;


我们可以看到虽然a为指向Animal的指针,但是当调用sleep是却是Fish sleep:


这其实就是多态的性质,当父类指针指向子类对象时,这是会调用子类虚表中的虚函数,


当然如果是非虚函数,则没有该类没有虚表,则只会按指针去找代码区中的函数实现。


当然父类指针就会找到父类里的函数,所有输出会是animal breath!


4.这种特性是怎么实现的呢?(虚函数机制)


首先,在C++中,虚函数用virtual关键字标示。当子类重写父类的虚函数时,在子类中自动的该函数也是虚函数!


对于带有虚函数的类,在数据区里会有其对应的一个虚函数表。而且每一个虚函数表都会有一个虚指针VTPR。如果是同一类型的对象,则虚函数指针指向同一地址。


那让我们再来看看多态的实现机制


首先父类会有一个虚函数表,子类对象也对应一个虚函数表,虚函数指针是在对象的构造函数调用时生成的。


当把一个子类对象赋值给父类指针时,由于虚函数指针只是属于对象的,所以父类指针中的虚函数指针则是子类的!


但调用虚函数时,会根据这个虚函数指针去虚函数表里寻找。所以调用的代码和对象的类型相关。


12.学过的设计模式? 状态机模式,简单工厂,策率模式,享元模式,模板模式 1次

13.指针和引用的区别:

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;

而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已,可以理解为操作受限了的指针(仅容许取内容操作)。如:

int a=1;int *p=&a;

int a=1;int &b=a;

上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。

而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。

(2)可以有const指针,但是没有const引用;

(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

(6)"sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;

(7)指针和引用的自增(++)运算意义不一样;


相关文章
|
4天前
|
存储 算法 NoSQL
【C/C++ 数据结构 概念】计算机数据结构基础:探索核心概念与术语
【C/C++ 数据结构 概念】计算机数据结构基础:探索核心概念与术语
41 0
|
4天前
|
设计模式 存储 缓存
【C++ 基本概念】深入探索C++ RTTI 特性
【C++ 基本概念】深入探索C++ RTTI 特性
65 0
|
4天前
|
安全 编译器 C++
【C++20概念】编译时多态性的力量
【C++20概念】编译时多态性的力量
52 0
|
4天前
|
编译器 API C++
c++ 新特性 概念和约束 “无规矩 难成方圆”
c++ 新特性 概念和约束 “无规矩 难成方圆”
|
4天前
|
算法 安全 编译器
【C++20 新特性Concepts 概念】C++20 Concepts: Unleashing the Power of Template Programming
【C++20 新特性Concepts 概念】C++20 Concepts: Unleashing the Power of Template Programming
49 0
|
6月前
|
存储 C语言 C++
66 C++ - 流的概念和流类库的结构
66 C++ - 流的概念和流类库的结构
42 0
|
6月前
|
Java 程序员 C语言
62 C++ - 异常基本概念
62 C++ - 异常基本概念
28 0
|
6月前
|
编译器 C++
31 C++ - 运算符重载基本概念
31 C++ - 运算符重载基本概念
27 0
|
4天前
|
编译器 Linux C语言
C/C++ 常见函数调用约定(__stdcall,__cdecl,__fastcall等):介绍常见函数调用约定的基本概念、用途和作用
C/C++ 常见函数调用约定(__stdcall,__cdecl,__fastcall等):介绍常见函数调用约定的基本概念、用途和作用
24 0
|
4天前
|
存储 人工智能 编译器
【重学C++】【指针】一文看透:指针中容易混淆的四个概念、算数运算以及使用场景中容易忽视的细节
【重学C++】【指针】一文看透:指针中容易混淆的四个概念、算数运算以及使用场景中容易忽视的细节
31 1