C++基础知识

(C/C++)语言基础

目录

  • (C/C++)语言基础
    • (1)C++和Python的区别
    • (2)C和C++的区别 或 面向对象和面向过程的区别
    • (3)用过C11吗,知道C11新特性吗?(有面试官建议熟悉C11)
    • (4)C++的调用惯例(简单一点C++函数调用的压栈过程)
    • (5)定义和声明的区别
    • (6)指针和引用的区别
    • (7)传递的时候,什么时候使用指针,引用,按值传递?
    • (8)struct和class的区别
    • (9)堆和栈的区别
    • (10)既然有了malloc/free,C++中为什么还需要new/delete呢?
    • (11)new/delete 和malloc/free的区别
    • (12)new和 new[] / delete和delete[]的区别
    • (13)在C++中const的用法(定义,用途)
    • (14)宏定义(#define)和const定义的联系与区别(编译阶段、安全性、内存占用等)
    • (15)C/C++中的static用法和意义
    • (16) C++的STL介绍(这个系列也很重要),包括内存管理,函数,实现机理,多线程实现等
    • (17) STL源码中的hash表的实现
    • (18) STL中unordered_map和map的区别
    • (19)如何解决哈希冲突?
    • (20) STL中vector的实现
    • (21)C++中vector和list的区别
    • (22) C++中的重载和重写的区别:
    • (23) C ++内存管理(热门问题)
    • (24) 介绍面向对象的三大特性,并且举例说明每一个。
    • (25) 多态的实现(和下个问题一起回答)
    • (26) C++虚函数相关(虚函数表,虚函数指针),虚函数的实现原理(热门,重要)
    • (27) 基类的析构函数一般写成虚函数的原因
    • (28) 构造函数为什么一般不定义为虚函数
    • (29) 构造函数或者析构函数中调用虚函数会怎样
    • (30) 纯虚函数
    • (31) 静态绑定和动态绑定的介绍
    • (32) 深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)
    • (33) 介绍C++所有的构造函数
    • (34) 什么情况下会调用拷贝构造函数(三种情况)
    • (35) 结构体内存对齐方式和为什么要进行内存对齐?
    • (36) 内存泄露的定义,如何检测与避免?
    • (37) C++的智能指针有哪些
    • (38) 调试程序的方法
    • (39) 什么是内联函数(inline),和宏定义有什么区别
    • (40) 模板的用法与适用场景 实现原理
    • (41) 成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?
    • (42) C++的四种强制转换
    • (43)string的底层实现
    • (44)一个函数或者可执行文件的生成过程或者编译过程是怎样的
    • (45)set,map和vector的插入复杂度
    • (46)typdef和define区别
    • (47)被free回收的内存是立即返还给操作系统吗?为什么
    • (48)引用作为函数参数以及返回值的好处
    • (49)友元函数和友元类
    • (50) 说一下volatile关键字的作用
    • (51) STL中的sort()算法是用什么实现的,stable_sort()呢
    • (52)vector会迭代器失效吗?什么情况下会迭代器失效?
    • (53)为什么C++没有实现垃圾回收?
    • (54)移动构造函数
    • (55)栈溢出的情况与解决方法

(1)C++和Python的区别

  包括但不限于:
    ① Python是一种脚本语言,是解释执行的,C++是编译语言,编译完成后需要在特定的平台上运行, python跨平台很方便,但是效率没C++高。
    ② Python使用缩进区分不同的代码块,C++使用花括号
    ③ C++需要事先定义变量的类型,而Python不需要。Python的基本类型只有:数字,布尔值,字符串, 列表,元组,字典等。
    ④ Python的库函数远多于C++,调用起来非常方便
    ⑤ 但它们都属于面向对象的语言

 

(2)C和C++的区别 或 面向对象和面向过程的区别

  我们都知道C面向过程,C++面向对象,说两者的区别其实就是比较面向过程和面向对象的区别
  ① C 与 C++最大的区别在于,它们用于解决问题的思想方法不一样

  ② 面向过程以函数驱动,通过函数调用先后次序来完成事情
  优点:流程比较清晰,设计相对而言比较简单
  缺点:不太适合大型工程,程序后期的可维护性也比较差

  ③ 面向对象不会关心具体的过程,关心的是有哪些对象,对象如何来进行交互,更符合对事物的认知过程,适合大型的工程或者复杂的问题,可扩展性/维护性比较友好

  ④ 面向对象就是高度实物抽象化(功能划分)、面向过程就是自顶向下的编程(步骤划分)

 

(3)用过C11吗,知道C11新特性吗?(有面试官建议熟悉C11)

自动类型推导auto:auto的自动类型推导用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作

nullptr
:nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0,而nullptr是void*类型的

lambda(匿名函数):它可以用于创建并定义匿名的函数对象,以简化编程工作。
就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或函数对象,避免了代码膨胀和功能分散。有更好的可读性和可维护性。在需要的时间和地点实现功能闭包。
Lambda的语法如下:

[ capture ] ( params )   opt ->   ret    { body; };
 捕获列表     参数     函数选项   返回值类型  函数主体
auto f = [](int a) -> int {return a + 1;};
cout << f(3) << endl;  //输出4

新的智能指针 unique_ptr和shared_ptr
 

(4)C++的调用惯例(简单一点C++函数调用的压栈过程)

函数的调用过程:

1)从栈空间分配存储空间

2)从实参的存储空间复制值到形参栈空间

3)进行运算

形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。

数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。

当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/指针传递

(5)定义和声明的区别

声明是告诉编译器变量的类型和名字,不会为变量分配空间

定义就是对这个变量和函数进行内存分配和初始化。需要分配空间,同一个变量可以被声明多次,但是只能被定义一次
 

(6)指针和引用的区别

  • 指针是一个新的变量,有独立的空间,指向另一个变量的地址
  • 而引用不是新定义一个变量,是给已存在的变量取一个别名,没有独立的空间,和其引用实体共用一块空间,对引用的操作就是对变量的本身进行操作
  • 有多级指针,但没有多级引用
  • 传参的时候,使用指针的话需要解引用才能对参数进行修改,而使用引用可以直接对参数进行修改
  • 引用定义时必须初始化,指针没有要求,
  • 指针可以为空,引用不可以
  • 引用用起来比指针更安全(指针要判空)

 

(7)传递的时候,什么时候使用指针,引用,按值传递?

  • 数据对象是内置数据结构,且需要修改传递过来的值,使用指针
  • 数据对象是数组,只能使用指针
  • 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式
  • 数据量小且不对值进行修改的内置数据类型或小型结构,使用按值传递。
     

(8)struct和class的区别

  • C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。和class是定义类是一样的,区别是struct的成员默认访问方式是public,class的成员默认访问方式是private。
  • 使用struct时,它的限默认访问权限和继承方式都是public的,而class默认继承和访问方式则都是private的
  • class可以用作模板,而struct不能

 

(9)堆和栈的区别

首先我明白堆和栈在不同的场景下对应的含义完全不同,一般情况下有两种含义

  • 程序内存场景下,堆与栈表示两种内存管理方式;
  • 数据结构场景下,堆与栈表示两种常用的数据结构;


内存场景下:堆与栈实际上是操作系统对进程占用的内存空间的两种管理方式,主要有如下几种区别:
(1)管理方式不同,栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;

(2)空间大小不同,每个进程拥有的栈大小要远远小于堆大小。

(3)生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。

(4)分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配和动态分配。

(5)分配效率不同(栈更快) 栈由操作系统自动分配,会在硬件层级对栈提供支持,堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的分配效率比栈要低得多。

(6) 存放内容不同,栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。
 
数据结构场景下:

  • 栈是一种提供后进先出存储方式的线性表,只允许在表的一端操作数据,存取其他项很慢。在输入数据规模可预知的情况下使用数组实现的栈叫顺序栈,未知就用链表实现叫链栈,并且数组实现栈的效率更高。但是顺序栈创建大小不当会造成栈溢出。
  • 堆是一种常用的树形结构,它总是一棵完全二叉树,堆中任意节点总是不大于或不小于其子节点的值。它插入删除较快,对最值的存取极快,对其他项存取很慢
     

(10)既然有了malloc/free,C++中为什么还需要new/delete呢?

  • malloc / free 和 new / delete都是用来动态申请内存和回收内存的。
  • 首先malloc / free是库函数,new / delete是操作符
  • 在对非内置类型的对象使用的时候,对象创建的时候需要执行构造函数进行初始化操作,销毁的时候要执行析构函数。而malloc/free是库函数,创建和释放空间的时候不会调用构造和析构函数。
     

(11)new/delete 和malloc/free的区别

  • 在申请自定义类型的空间时,new申请空间会调用构造函数对空间进行初始化,delete释放空间会调用析构函数,而malloc与free不会
  • malloc申请出来的不是一个自定义类型的对象,而是与该类型大小相同的一段空间
  • malloc需要给定空间大小,而new只需要对象名

 

(12)new和 new[] / delete和delete[]的区别

  • 申请和释放单个元素的空间,使用new T (T是空间中存储元素的类型) 和 delete Ptr

  • 申请和释放连续的空间,使用new T [n] 和 delete []ptr

  • delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数

  • 用new分配的内存用delete释放,用new[]分配的内存用delete[]释放
     

(13)在C++中const的用法(定义,用途)

在C++中,提出建议:尽量使用const修饰的常量替换宏常量
注意:
 在c语言中,被const修饰的变量不能够被修改,但其并不是常量
 在C++中,被const修饰的已经是常量,而且具有宏替换属性(但是替换实际在程序编译时)

  • const修饰类的成员变量时,表示该成员变量不能被修改

const修饰的成员函数与普通成员函数的区别?

  • const修饰类的成员函数(const放函数后面)称为const成员函数,表明该成员函数不能修改类中的任何成员变量。
    (PS:但是有些情况下,可能需要在const成员函数中修改个别属性,要在成员变量前加mutable)

  • 普通成员函数中:const在返回值类型之前,修饰的该函数的返回值类型,返回值不能修改,可以修改this指向的当前对象的属性信息
    C++基础知识_第1张图片

     

(14)宏定义(#define)和const定义的联系与区别(编译阶段、安全性、内存占用等)

联系:它们都是定义常量的一种方法。
区别:

  • 宏是一改全改,降低了代码出错概率,提高了代码的可读性
  • define定义的常量没有类型,只是进行了简单的替换,可能会有多个拷贝,占用的内存空间大,const定义的常量是有类型的,存放在静态存储区,只有一个拷贝,占用的内存空间小。
  • define定义的常量是在预处理阶段进行替换,而const在编译阶段确定它的值。
  • define不会进行类型检测,安全性低,而const会进行类型安全检查,安全性更高。

 

(15)C/C++中的static用法和意义

static的意思是静态的,可以用来修饰变量,函数和类成员。
变量:

  • static修饰局部变量,改变了它的存储方式,即改变了它的生命周期,函数调用后不会被立刻销毁,程序结束后才会。
  • static修饰全局变量,改变了其链接属性,可以达到改变它作用域的目的,使全局变量只能在被其定义的源文件下访问

函数:

  • static修饰函数,改变函数链接属性,表明:该函数只能在当前文件中进行使用,不能被其他文件调用。

类:而在类中,被static修饰的成员变量或成员函数是类静态成员,静态成员变量在类中声明,类外定义。静态成员不属于某个具体的对象,被类的所有对象共用。通过类名::静态成员变量名字(Date::name)来进行访问,静态成员存放在静态存储区,不占用类的大小, 且静态成员函数无法访问非静态成员变量,非静态成员函数(但是非静态成员函数可以访问静态成员函数),不能使用const修饰(const修饰的是this指针)
核心本质:静态成员函数没有this指针

空类的大小是1, 在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。
 

(16) C++的STL介绍(这个系列也很重要),包括内存管理,函数,实现机理,多线程实现等

STL大体分为6大组件:算法,容器,迭代器,仿函数,适配器,空间配置器。

  • 算法:包括排序,复制,查找等常用算法,以及不同容器特定的算法。
  • 容器:就是数据的数据结构,包括序列式容器:vector,List 和关联式容器:set,map等,
  • 迭代器:就是在不暴露容器内部结构的情况下对容器的遍历类似一个指针
  • 仿函数:行为类似函数,可作为算法的某种策略。
  • 适配器:一种用来修饰容器,迭代器或仿函数接口的东西。
  • 空间配置器:专门用来进行空间的申请,释放与管理。
    C++基础知识_第2张图片
     

(17) STL源码中的hash表的实现

  • STL中的hash表就unordered_map。底层采用哈希表实现。它记录的键是元素的哈希值,通过哈希函数对比元素的哈希值来确定元素的值。
  • 它采用开链法(也就是用桶)来解决哈希冲突,当桶的大小超过8时,就自动转为红黑树进行组织。

 

(18) STL中unordered_map和map的区别

  • map底层是采用红黑树实现的,所以它的key是有序的,查询增删的时间复杂度都是O(log(n)),,因此需要实现比较操作符(<)。
  • unordered_map底层是使用哈希实现的,占用内存比较多,在已知key的前提下,查询和增删速度极快,O(1)时间复杂度。它的key是无序的,需要实现==操作符。

 

(19)如何解决哈希冲突?

  • 拉链法:在每一个桶中维护一个链表,由元素哈希值寻找到这个桶,然后将元素插入到对应的链表中,STL的hashtable就是采用这种实现方式。
    C++基础知识_第3张图片

  • 线性探测法:该元素的哈希值对应的桶不能存放元素时,循序往后一 一查找,直到找到一个空桶为止,在查找时也一样,当哈希值对应位置上的元素与所要寻找的元素不同时,就往后一 一查找,直到找到吻合的元素,或者空桶。(PS:要求tableSize一定要大于dataSize,不然哈希表上就没有空置的位置来存放冲突的数据了)

 

(20) STL中vector的实现

  • STL中的 vector是封装了动态数组的顺序容器。不过与动态数组不同的是,vector可以根据需求,自动扩大容器的大小。具体策略是每次容量不够用时重新申请一块大小为原来容量两倍的内存,并将原容器的元素拷贝至新容器,并释放原空间,返回新空间的指针。

  • 频繁调用push_back()会使得程序花费很多时间在vector扩容上,这个过程是十分耗时和耗内存的,这种情况可以考虑使用list。

 

(21)C++中vector和list的区别

  • vector和数组类似,vector申请的是一段连续的内存。能很好的支持随机存取,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。

  • list是由双向链表实现的,因此内存空间是不连续的。它不支持随机访问,只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n); 但由于链表的特点,能高效地进行插入和删除。

  • 总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;如果需要大量的插入和删除,而不关心随机存取,则应使用list。

 

(22) C++中的重载和重写的区别:

  • 重载(overload)是指函数名相同,参数列表不同的函数实现方法。它们的返回值可以不同,但返回值不可以作为区分不同重载函数的标志。
  • 重写(overwide)是指函数名相同,参数列表相同,只有方法体不相同的实现方法。一般用于子类继承父类时对父类方法的重写。子类的同名方法屏蔽了父类方法的现象称为隐藏。

 

(23) C ++内存管理(热门问题)

在C++中,内存分成5个区,他们分别是堆、栈、全局/静态存储区和常量存储区和代码区。
C++基础知识_第4张图片

  • 栈:栈是由操作系统自动分配与释放的,栈内存分配效率很高,但是分配的内存容量有限。主要用来存储和函数调用相关的数据,如返回值类型,形参,局部变量,寄存器等。
  • 堆:由程序员手动开辟与释放,如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。堆上存储的是程序员动态开辟的空间。
  • 全局/静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据(局部static变量,全局static变量)、全局变量和常量。
  • 常量存储区:里面存放的是常量字符串,不允许修改。
  • 代码区:存放程序的二进制代码

 

(24) 介绍面向对象的三大特性,并且举例说明每一个。

面向对象的三大特性是:封装,继承和多态。

  • 封装:将数据和操作数据的方法进行有机结合,隐藏了对象的属性和实现细节,仅对外公开接口来和对象进行交互,如类里面的private和public;
  • 继承使得子类可以复用父类的成员和方法,实现了代码重用;
  • 多态则是“一个接口,多个实现”,通过父类调用子类的成员,实现了接口重用,如父类的指针指向子类的对象。

 

(25) 多态的实现(和下个问题一起回答)

  • C++ 多态包括静态多态(编译时)和动态多态(运行时),静态多态(编译时多态)体现在函数重载和模板上,动态多态体现在派生类和虚函数上。

  • 静态多态的地址早绑定——编译阶段确定函数地址

  • 动态多态的地址晚绑定——运行时确定函数地址

  • 虚函数:在父类的函数前加上virtual关键字,在子类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用子类的函数;如果对象类型是基类,就调用父类的函数.

 

(26) C++虚函数相关(虚函数表,虚函数指针),虚函数的实现原理(热门,重要)

  • C++的虚函数是实现多态的机制,它是通过虚函数表实现的。
  • 虚函数表是每个类中存放虚函数地址的指针数组。
  • 类的实例在调用函数时会在虚函数表中寻找函数地址进行调用,如果子类重写了这个虚函数表,它会将自身的虚函数表替换掉,会覆盖父类的函数,则子类的虚函数表会指向子类实现的函数地址,否则指向父类的函数地址。
  • 一个类的所有实例都共享同一张虚函数表。
#include
using namespace std;
class Animal
{
public:
   // 加virtual变成虚函数
	virtual void speak()
	{
	    cout << "动物在说话" << endl;
	}
};

class Cat : public Animal
{
public:
	virtual void speak()// 派生类中virtual可加可不加
	{
	    cout << "小猫在说话" << endl;
	}
};

class Dog : public Animal
{
public:
	void speak()
	{
	    cout << "小狗在说话" << endl;
	}
};

// 地址早绑定 在编译阶段确定函数地址执行的永远执行的是父类的speak
// 如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,地址晚绑定
void Dospeak(Animal& animal)
{
	animal.speak();
}

int main()
{
	Cat cat;
	Dospeak(cat);// Animal类中不加virtual执行静态多态 "动物说话"
 	Dog dog;
	Dospeak(dog);// 加了virtual后变为虚函数动态多态 "小狗在说话"
	system("pause");
	return 0;
}
/*
总结:
多态满足条件       有继承关系
    	       子类重写父类中的虚函数

多态使用条件
    	       父类指针或引用指向子类对象, Dospeak(Animal& animal)  Dospeak(cat) 常用引用
    	       重写:函数返回值类型 函数名 参数列表 完全一致称为重写*/

 

(27) 基类的析构函数一般写成虚函数的原因

  • 首先析构函数可以为虚函数,当析构一个指向子类的父类指针时,编译器可以根据虚函数表寻找到子类的析构函数进行调用,从而正确释放子类对象的资源。

  • 如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向子类的父类指针时,只会调用父类的析构函数,这样就会造成子类对象析构不完全造成内存泄漏。
     

(28) 构造函数为什么一般不定义为虚函数

  • 因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等

  • 虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数。
     

(29) 构造函数或者析构函数中调用虚函数会怎样

  • 在构造函数中调用虚函数,由于当前对象还没有构造完成,此时调用的虚函数指向的是基类的函数实现方式。

  • 在析构函数中调用虚函数,此时调用的是子类的函数实现方式。

 

(30) 纯虚函数

纯虚函数语法: virtual 返回值类型 函数名(参数列表) = 0 ;

  • 使用场景:在多态中,当一个类本身产生的实例没有意义的,主要都是调用子类重写的内容,因此可以把这个类的函数实现为纯虚函数,比如动物可以派生出老虎兔子,但是实例化一个动物对象就没有什么意义。

  • 纯虚函数是只有声明没有实现的虚函数,是对子类的约束,是接口继承

  • 包含纯虚函数的类是抽象类,它不能被实例化,只有实现了这个纯虚函数的子类才能生成对象

 

(31) 静态绑定和动态绑定的介绍

  • 静态绑定也就是将该对象相关的属性或函数绑定为它的静态类型,也就是它在声明的类型,在编译的时候就确定。在调用的时候编译器会寻找它声明的类型进行访问。

  • 动态绑定就是将该对象相关的属性或函数绑定为它的动态类型,具体的属性或函数在运行时确定,通常通过虚函数实现动态绑定。

 

(32) 深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)

  • 浅拷贝就是将对象的指针进行简单的复制,原对象和副本指向的是相同的资源。

  • 而深拷贝是新开辟一块空间,将原对象的资源复制到新的空间中,并返回该空间的地址。

  • 深拷贝可以避免重复释放和写冲突。例如使用浅拷贝的对象进行释放后,对原对象再进行释放会导致内存泄漏或程序崩溃。

 

(33) 介绍C++所有的构造函数

C++中的构造函数主要有三种类型:默认构造函数、重载构造函数和拷贝构造函数

  • 默认构造函数是当类没有实现自己的构造函数时,编译器默认提供的一个构造函数。
  • 重载构造函数也称为一般构造函数,一个类可以有多个重载构造函数,但是需要参数类型或个数不相同。可以在重载构造函数中自定义类的初始化方式。
  • 拷贝构造函数是在对象复制的时候调用的。

 

(34) 什么情况下会调用拷贝构造函数(三种情况)

  • 对象以值传递的方式传入函数参数

  • 对象以值传递的方式从函数返回

  • 对象需要通过另外一个对象进行初始化

 

(35) 结构体内存对齐方式和为什么要进行内存对齐?

因为结构体的成员可以有不同的数据类型,所占的大小也不一样。同时,由于CPU读取数据是按块读取的,内存对齐可以使得CPU一次就可以将所需的数据读进来。

对齐规则:

  • 结构体的第一个成员,永远放在与结构体变量偏移量为0的地址
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    (对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值, linux 中默认为4,vs 中的默认值为8 )
  • 结构体总大小是结构体的所有成员的对齐数中最大的那个对齐数的整数倍。

 

(36) 内存泄露的定义,如何检测与避免?

动态开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。

造成内存泄漏的几种原因

① 类的构造函数和析构函数中new和delete没有配套

② 在释放对象数组时没有使用delete[ ],而是使用的delete

③ 没有将父类的析构函数定义为虚函数,当父类指针指向子类对象时,如果父类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有得到正确释放,因此造成内存泄露

④ 没有正确的清除嵌套的对象指针

避免方法:

  • malloc/free要配套
  • 使用智能指针;
  • 将基类的析构函数设为虚函数;

 

(37) C++的智能指针有哪些

C++中的智能指针有auto_ptr,unique_ptr, shared_ptr, weak_ptr。智能指针其实是将指针进行了封装,可以像普通指针一样进行使用,同时可以自行进行释放,避免忘记释放指针指向的内存造成内存泄漏。

  • auto_ptr是较早版本的智能指针,在进行指针拷贝和赋值的时候,新指针直接接管旧指针的资源并且将旧指针指向空,但是这种方式在需要访问旧指针的时候,就会出现问题。
  • unique_ptr是auto_ptr的一个改良版,不能赋值也不能拷贝,保证一个对象同一时间只有一个智能指针。
  • shared_ptr可以使得一个对象可以有多个智能指针,当这个对象所有的智能指针被销毁时就会自动进行回收。(内部使用计数机制进行维护)
  • weak_ptr是为了协助shared_ptr而出现的。它不能访问对象,只能观测shared_ptr的引用计数,防止出现死锁。

 

(38) 调试程序的方法

  • 通过设置断点进行调试
  • 打印log进行调试
  • 打印中间结果进行调试

 

(39) 什么是内联函数(inline),和宏定义有什么区别

inline是内联的意思,可以定义比较小的函数。因为函数频繁调用会占用很多的栈空间,进行入栈出栈操作也耗费计算资源,所以可以用inline关键字修饰频繁调用的小函数。

1、宏不是函数,而inline是函数

2、内联函数在编译时展开,而宏在预处理时展开

3、在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。

4、内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。

5、宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。

 

(40) 模板的用法与适用场景 实现原理

模板就是建立一个通用的模具,大大提高复用性。
用template < typename T >关键字进行声明,接下来就可以进行模板函数和模板类的编写了。

用户对模板实例化后,编译器将会对实参类型进行推演,用于确定参数列表中T的实际类型。

编译器会对函数模板进行两次编译:

  • 在声明的地方对模板代码本身进行编译,这次编译只会进行一个语法检查,并不会生成具体的代码。
  • 在运行时对代码进行参数替换后再进行编译,生成具体的函数代码。

 

(41) 成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?

成员初始化列表就是在类或者结构体的构造函数中,在参数列表后以冒号开头,逗号进行分隔的一系列初始化字段。如下:

class Date
{
public:
	A(int year, int month, int day):_year(2022),_month(8),_day(4)// 成员初始化列表
	{} 
private:
	int _year;
	int _month;
	int _day;	
};

因为使用成员初始化列表进行初始化,会直接使用传入参数的拷贝构造函数进行初始化,省去了一次执行传入参数的默认构造函数的过程,所以使用成员初始化列表效率会高一些。

有三种情况是必须使用成员初始化列表进行初始化的:

  • const修饰的常量成员(因为常量成员只能初始化不能赋值)
  • 引用成员变量
  • 自定义类型的成员(该类没有默认构造函数)

 

(42) C++的四种强制转换

四种强制类型转换操作符分别为:static_cast、dynamic_cast、const_cast、reinterpret_cast

1)static_cast :
用于各种隐式转换。具体的说,就是用户各种基本数据类型之间的转换,比如把int换成char,float换成int等。以及派生类(子类)的指针转换成基类(父类)指针的转换。

特性与要点:
它没有运行时类型检查,所以是有安全隐患的。
在子类指针转换到父类指针时,是没有任何问题的,在父类指针转换到子类指针的时候,会有安全问题。
static_cast不能转换const,volatile等属性

2)dynamic_cast:
用于动态类型转换。具体的说,就是父类到子类指针,或者子类到父类指针的转换。
dynamic_cast能够提供运行时类型检查,只用于含有虚函数的类,如果不能转换会返回NULL。

3)const_cast:
用于去除const常量属性,使其可以修改 ,也就是说,原本定义为const的变量在定义后就不能进行修改的,但是使用const_cast操作之后,可以通过这个指针或变量进行修改; 另外还有volatile属性的转换。

4)reinterpret_cast
几乎什么都可以转,用在任意的指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换等。但是不够安全。

 

(43)string的底层实现

string,其实是对char* 进行了封装,封装的string包含了char* 数组,容量,长度等属性。

string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间,然后将原字符串拷贝过去,并加上新增的内容。

 

(44)一个函数或者可执行文件的生成过程或者编译过程是怎样的

预处理,编译,汇编,链接

  • 预处理: 对预处理命令进行替换等预处理操作
  • 编译:代码优化和生成汇编代码
  • 汇编:将汇编代码转化为机器语言
  • 链接:将目标文件彼此链接起来

 

(45)set,map和vector的插入复杂度

  • set,map的插入复杂度就是红黑树的插入复杂度,是log(N)。

  • unordered_map,unordered_set的插入复杂度是常数,最坏是O(N).

  • vector的插入复杂度是O(N),最坏的情况下就要对所有其他元素进行移动。

 

(46)typdef和define区别

  • #define是预处理命令,在预处理时执行简单的文本替换,不做正确性的检查

  • typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名

 

(47)被free回收的内存是立即返还给操作系统吗?为什么

不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。

 

(48)引用作为函数参数以及返回值的好处

对比值传递,引用传参的好处:

1)在函数内部可以对此参数进行修改

2)提高函数调用和运行的效率(因为没有了传值和生成副本的时间和空间消耗)

用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。

但是有以下的限制:

  • 不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁

  • 不能返回函数内部new出来的内存的引用。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成内存泄漏

  • 可以返回类成员的引用,但是最好是const

 

(49)友元函数和友元类

友元提供了不同类的成员函数之间,或和一般函数之间进行数据共享的机制。通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

1)友元函数

  • 友元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员。但是需要在类的定义中声明所有可以访问它的友元函数。
  • 一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数。
#include 

using namespace std;

class A
{
public:
    friend void set_show(int x, A &a);      //该函数是友元函数的声明
private:
    int data;
};

void set_show(int x, A &a)  //友元函数定义,为了访问类A中的成员
{
    a.data = x;
    cout << a.data << endl;
}
int main(void)
{
    class A a;

    set_show(1, a);

    return 0;
}

2)友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
但是另一个类里面也要相应的进行声明

#include 

using namespace std;

class A
{
public:
   friend class C;                         //这是友元类的声明
   
private:
   int data;
};

class C             //友元类定义,为了访问类A中的成员
{
public:
   void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}
};

int main(void)
{
   class A a;
   class C c;
   c.set_show(1, a);
   return 0;
}

使用友元类时注意:

(1) 友元关系不能被继承。

(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元。

 

(50) 说一下volatile关键字的作用

volatile的意思是“脆弱的”,表明它修饰的变量的值十分容易被改变,所以编译器就不会对这个变量进行优化(CPU的优化是让该变量存放到CPU寄存器而不是内存),进而提供稳定的访问。每次读取volatile的变量时,系统总是会从内存中读取这个变量,并且将它的值立刻保存。

 

(51) STL中的sort()算法是用什么实现的,stable_sort()呢

STL中的sort是用快速排序和插入排序结合的方式实现的,stable_sort()是归并排序。

 

(52)vector会迭代器失效吗?什么情况下会迭代器失效?

会, 当vector在插入的时候,如果原来的空间不够,会将申请新的内存并将原来的元素移动到新的内存,此时指向原内存地址的迭代器就失效了,begin和end迭代器都失效
当vector在插入的时候,end迭代器肯定会失效
当vector在删除的时候,被删除元素以及它后面的所有元素迭代器都失效。

 

(53)为什么C++没有实现垃圾回收?

首先,实现一个垃圾回收器会带来额外的空间和时间开销。你需要开辟一定的空间保存指针的引用计数和对他们进行标记mark。然后需要单独开辟一个线程在空闲的时候进行free操作。
垃圾回收会使得C++不适合进行很多底层的操作。

 

(54)移动构造函数

C++11之后,新增加了两个函数:移动构造函数(Move Constructor)和 移动赋值运算符(Move Assignment operator)
有时候我们会遇到这样一种情况,我们用对象a初始化对象b后,对象a就不外使用了,但是对象a的空间还在(析构之前),既然拷贝构造函数就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷。
移动构造可以减少不必要的复制,带来性能上的提升。

Str(Str &&s)
{
    cout<<"移动构造函数..."<<endl;
    str = NULL;
    str = s.str;
    s.str = NULL;
}

 

(55)栈溢出的情况与解决方法

  • 局部数组过大:当函数内部的数组过大时,有可能导致堆栈溢出。

  • 递归调用层次太多:递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。

  • 指针或数组越界:这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。

解决这类问题的办法有两个,

一是增大栈空间,
二是改用动态分配,使用堆而不是栈。

你可能感兴趣的:(java,c++,面试,开发语言,经验分享)