C++ 面试知识点总结

1. new,delete 与 malloc,free的区别:melonstreet

  • 1. 申请的内存所在位置
  • 2.返回类型安全性
  • 3.内存分配失败时的返回值
  • 4.是否需要指定内存大小
  • 5.是否调用构造函数/析构函数
  • 6.对数组的处理
  • 7.new与malloc是否可以相互调用
  • 8.是否可以被重载
  • 9. 能够直观地重新分配内存
  • 10. 客户处理内存分配不足
  • new是运算符,malloc是C语言库函数
  • new可以重载,malloc不能重载
  • new的变量是数据类型,malloc的是字节大小
  • new可以调用构造函数,delete可以调用析构函数,malloc/free不能
  • new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化
  • malloc分配的内存不够的时候可以使用realloc扩容,new没有这样的操作
  • new内存分配失败抛出bad_malloc,malloc内存分配失败返回NULL值

2. 虚函数与多态:Harttle Land

3. static:疾风中的劲草

修饰全局函数时会 限制其作用域只在本文件内

4. 指针和引用的区别

  • 指针保存的是指向对象的地址,引用相当于变量的别名
  • 引用在定义的时候必须初始化,指针没有这个要求
  • 指针可以改变地址,引用必须从一而终
  • 不存在空应引用,但是存在空指针NULL,相对而言引用更加安全
  • 引用的创建不会调用类的拷贝构造函数

4. 函数重载

在同一个作用域内,可以声明几个功能类似的同名函数,但同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。不能仅返回类型不同来重载函数

5 C++内存分配(堆栈)

  1. 栈:是分配给函数局部变量的存储单元,函数结束后,该变量的存储单元自动释放,效率高,分配的空间有限。

  2. 堆:由new创建,由delete释放的动态内存单元。如果用户不释放该内存,程序结束时,系统会自动回收。

  3. 自由存储区:由new创建,由delete释放的动态内存单元,与堆类似。

  4. 全局(静态)存储区:全局变量和静态变量占一块内存空间。

  5. 常量存储区:存储常量,内容不允许更改。

堆和栈的区别

实例程序

 1 //main.cpp    
 2 int a = 0;   //全局初始化区    
 3 char *p1;   //全局未初始化区    
 4 int main()
 5 {
 6     int b;   //
 7     char s[] = "abc"; //
 8     char *p2;  //
 9     char *p3 = "123456";   //123456 / 0在常量区,p3在栈上。
10     static int c = 0;   //全局(静态)初始化区
11     p1 = (char*)malloc(10);
12     p2 = (char*)malloc(20);
13     //分配得来得10和20字节的区域就在堆区。
14     strcpy(p1, "123456");   //123456 / 0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
15 }
  • 申请方式

    • 栈:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间

    • 堆:需要程序员自己申请,并指明大小,在c中malloc函数

      如p1 = (char *)malloc(10);
      在C++中用new运算符
      如p2 = new char[10];
      但是注意p1、p2本身是在栈中的。

  • 申请后系统的响应

    • 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

    • 堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

  • 申请大小的限制

    • 栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

    • 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

  • 申请效率的比较:

    • 栈: 由系统自动分配,速度较快。但程序员是无法控制的。

    • 堆: 是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

  • 堆和栈中的存储内容

    • 栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

    • 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

  • 存取效率的比较

    • 堆:由C/C++函数库提供,机制很复杂。所以堆的效率比栈低很多。

    • 栈:是系统提供的数据结构,计算机在底层对栈提供支持,分配专门寄存器存放栈地址,栈操作有专门指令。

6.  构造函数不能为虚函数而析构函数可以:woyaowenzi

  • 虚函数的执行依赖于虚函数表。而虚函数表需要在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。
  • 在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。

7. 内存泄漏: 内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

  • 程序员要养成良好习惯,保证malloc/new和free/delete匹配;
  • 检测内存泄漏的关键原理就是,检查malloc/new和free/delete是否匹配,一些工具也就是这个原理。要做到这点,就是利用宏或者钩子,在用户程序与运行库之间加了一层,用于记录内存分配情况。

8. 纯虚函数

  1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
  2、虚函数声明如下:virtual ReturnType FunctionName(Parameter);虚函数必须实现,如果不实现,编译器将报错,错误提示为:
error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"
  3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
  4、实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
  5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。


9. 静态,动态链接

  • 静态链接
    所谓静态链接就是在编译链接时直接将需要的执行代码拷贝到调用处,优点就是在程序发布的时候就不需要依赖库,也就是不再需要带着库一块发布,程序可以独立执行,但是体积可能会相对大一些。
  • 动态链接
    所谓动态链接就是在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的。优点是多个程序可以共享同一段代码,而不需要在磁盘上存储多个拷贝,缺点是由于是运行时加载,可能会影响程序的前期执行性能

10. 内存对齐

对齐单位(有效对齐值):min(#pragma pack(n), 结构体中最长数据类型大小)

(1) offset对齐:结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。

(2) 整体对齐:结构体的总大小为有效对齐值的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

为什么要对齐?大部分处理器并不是按单字节来存取内存的。它一般会以2字节, 4字节, 8字节, 16字节甚至32字节为单位(存取粒度)来存取内存

如果随意存放,则会带来不必要的存取开销,处理器需要按存取粒度读取,剔除不必要的字节后进行合并

结论:编码中定义结构体时需要考虑成员变量定义的先后顺序

 

11. 静态绑定和动态绑定(多态基础)

对象的静态类型:对象在声明时采用的类型。是在编译期确定的。

对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。

 1 class B{
 2 }
 3 class C : public B{
 4 }
 5 class D : public B{
 6 }
 7 D* pD = new D();//pD的静态类型是它声明的类型D*,动态类型也是D*
 8 B* pB = pD;//pB的静态类型是它声明的类型B*,动态类型是pB所指向的对象pD的类型D*
 9 C* pC = new C();
10 pB = pC;//pB的动态类型是可以更改的,现在它的动态类型是C*

静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。

动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。

只有虚函数使用的是动态绑定,其他的全部是静态绑定

绝对不要重新定义继承而来的非虚(non-virtual)函数(《Effective C++ 第三版》条款36),因为这样导致函数调用由对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态

 1 class B
 2 {
 3     void DoSomething();
 4     virtual void vfun();
 5 }
 6 class D : public B
 7 {
 8     void DoSomething();//首先说明一下,这个子类重新定义了父类的no-virtual函数,这是一个不好的设计,会导致名称遮掩;这里只是为了说明动态绑定和静态绑定才这样使用。
 9     virtual void vfun();
10 }
11 D* pD = new D();
12 B* pB = pD;

pD->DoSomething()和pB->DoSomething()调用的是同一个函数吗?
不是,虽然pD和pB都指向同一个对象。因为函数DoSomething是一个no-virtual函数,它是静态绑定的,也就是编译器会在编译期根据对象的静态类型来选择函数。pD的静态类型是D*,那么编译器在处理pD->DoSomething()的时候会将它指向D::DoSomething()。同理,pB的静态类型是B*,那pB->DoSomething()调用的就是B::DoSomething()。

让我们再来看一下,pD->vfun()和pB->vfun()调用的是同一个函数吗?
是的。因为vfun是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型,pB和pD虽然静态类型不同,但是他们同时指向一个对象,他们的动态类型是相同的,都是D*,所以,他们的调用的是同一个函数:D::vfun()。

上面都是针对对象指针的情况,对于引用(reference)的情况同样适用。

绝对不要重新定义一个继承而来的virtual函数的缺省参数值,因为缺省参数值都是静态绑定(为了执行效率),而virtual函数却是动态绑定《Effective C++ 第三版》 条款37

 

12:std:move

move操作减少不必要的copy,move后的变量变为空数据,值转移到左值

自定义类型的使用move需要定义move copy和move 赋值构造函数

https://www.cnblogs.com/shadow-lr/p/Introduce_Std-move.html

 

智能指针:https://www.cnblogs.com/douzujun/p/10803365.html

 

参考自:

C++内存分配

吴秦

hackbuteer1

内存对齐

动态绑定

posted @ 2018-07-09 01:31  demianzhang  阅读(4619)  评论(0编辑  收藏  举报