内存管理

C++

多态 虚函数

  • 什么是多态?C++的多态是如何实现的?

所谓多态,就是同一个函数名具有多种状态,或者说一个接口具有不同的行为;C++的多态分为编译时多态和运行时多态,编译时多态也称为为静态联编,通过重载和模板来实现,运行时多态称为动态联编,通过继承和虚函数来实现。

img

  1. 对于非虚函数,三个类中虽然都有一个叫 func2 的函数,但他们彼此互不关联,因此都是各自独立的,不存在重载一说,在调用的时候也不需要进行查表的操作,直接调用即可。
  2. 由于子类B和子类C都是继承于基类A,因此他们都会存在一个虚指针用于指向虚函数表。注意,假如子类B和子类C中不存在虚函数,那么这时他们将共用基类A的一张虚函数表,在B和C中用虚指针指向该虚函数表即可。但是,上面的代码设计时子类B和子类C中都有一个虚函数 vfunc1,因此他们就需要各自产生一张虚函数表,并用各自的虚指针指向该表。由于子类B和子类C都对 vfunc1 作了重载,因此他们有三种不同的实现方式,函数地址也不尽相同,在使用的时候需要从各自类的虚函数表中去查找对应的 vfunc1 地址。
  3. 对于虚函数 vfunc2,两个子类都没有进行重载操作,所以基类A、子类B和子类C将共用一个 vfunc2,该虚函数的地址会分别保存在三个类的虚函数表中,但他们的地址是相同的。
  4. 从上图可以发现,在类对象的头部存放着一个虚指针,该虚指针指向了各自类所维护的虚函数表,再通过查找虚函数表中的地址来找到对应的虚函数。
  5. 对于类中的数据而言,子类中都会包含父类的信息。如上例中的子类C,它自己拥有一个变量 m_data1,似乎是和基类中的 m_data1 重名了,但其实他们并不存在联系,从存放的位置便可知晓。

动态绑定有以下三项条件要符合:

  1. 使用指针进行调用
  2. 指针属于up-cast后的
  3. 调用的是虚函数

与动态绑定相对应的是静态绑定,它属于编译的时候就确定下来的,如上文的非虚函数,他们是类对象直接可调用的,而不需要任何查表操作,因此调用的速度也快于虚函数。

C++中的虚指针与虚函数表 - 知乎 (zhihu.com)

内存分配

内存区域 描述 分配和释放方式 特性
使用malloc、free动态分配和释放空间,能分配较大的内存 malloc/free 1. 需要手动申请和手动释放
2. 能分配的内存较大
3. 分配和释放内存可能产生内存碎片
4. 分配效率较低
5. 地址从低向上
为函数的局部变量分配内存,能分配较小的内存 由操作系统自动分配和释放 1. 自动申请和自动释放
2. 能分配的内存较小
3. 不会产生内存碎片
4. 分配效率高
5. 地址由高向下
全局/静态存储区 用于存储全局变量和静态变量 编译器分配和释放 存储全局和静态变量的生命周期
常量存储区 专门用来存放常量 编译器分配和释放 存储常量,不可修改
自由存储区 通过new和delete分配和释放空间的内存,具体实现可能是堆或者内存池 new/delete 1. 是C++的术语,抽象概念
2. 可以通过重载操作符使用其他内存实现
3. 对于自定义对象,能调用构造函数和析构函数

关于堆和栈的内存区别:

区别点
内存申请和释放 手动申请和释放 自动申请和释放
内存大小 较大(4G(32位机器)) 较小(1M)
内存碎片 可能产生 不产生
分配效率 较低 较高
地址方向 从低向上 由高向下

C++和C分别使用什么函数来做内存的分配和释放:

语言 分配和释放函数 描述
C malloc/free 库函数,只进行内存分配和释放,无法调用构造函数和析构函数
C++ new/delete 运算符,能调用构造函数和析构函数,完成对象的空间分配、初始化、销毁和释放空间

C++和C的内存管理函数的区别:

  1. new分配内存空间无需指定分配内存大小,malloc需要。
  2. new返回类型指针,类型安全,malloc返回void*,再强制转换成所需要的类型。
  3. new是从自由存储区获得内存,malloc从堆中获取内存。
  4. 对于类对象,new会调用构造函数和析构函数,malloc不会。

不能混用C和C++的内存管理函数,因为它们的处理方式和内存管理机制不同。

内存布局

在C++中类对象的内存布局是如何分布的呢?

在这里,影响对象大小的有哪些因素呢?

成员变量的类型与数量、虚函数表的指针(_vftptr)、虚基类表指针(_vbtptr)–>产生虚函数表、单一继承、多重继承、重复继承、虚拟继承

类型转换

类型转换运算符 描述 示例
const_cast 转换const属性,可以添加或移除const属性,仅适用于指针或引用,只能改变对象的底层const。 const int a = 10; int* p = const_cast<int*>(&a);
static_cast 实现隐式类型转换,支持基本数据类型、枚举、结构体、类之间的转换,以及类层次间的向上和向下转换(向下转换不安全)。 double d = 3.14; int i = static_cast<int>(d);
dynamic_cast 动态类型转换,用于将基类安全地转换为派生类(或向上转换),失败时返回NULL或抛出异常。需要基类有虚函数。 class Base { virtual ~Base() {} }; class Derived : public Base {}; Base* b = new Derived(); Derived* d = dynamic_cast<Derived*>(b);
reinterpret_cast 重新解释二进制数据,可以转换任何类型到任何类型,不保证类型安全。 int a = 10; float* f = reinterpret_cast<float*>(&a);

请注意,使用const_castreinterpret_castdynamic_cast时需要特别小心,因为它们可能会破坏类型安全或引入其他问题。在大多数情况下,使用static_cast进行类型转换是更安全的选择。

智能指针

智能指针主要解决一个内存泄露的问题,它可以自动地释放内存空间。因为它本身是一个类,当函数结束的时候会调用析构函数,并由析构函数释放内存空间。

智能指针类型 名称 描述
shared_ptr 共享指针 多个shared_ptr可以指向相同的对象,采用引用计数机制。当最后一个引用销毁时,释放内存空间。
unique_ptr 独占指针 保证同一时间段内只有一个unique_ptr能指向该对象,可通过move操作来传递unique_ptr。
weak_ptr 弱指针 用来解决shared_ptr相互引用时的死锁问题,是对对象的一种弱引用,不会增加对象的引用计数。
  • shared_ptr的实现原理是什么?构造函数、拷贝构造函数和赋值运算符怎么写?shared_ptr是不是线程安全的?

(1)shared_ptr是通过引用计数机制实现的,引用计数存储着有几个shared_ptr指向相同的对象,当引用计数下降至0时就会自动销毁这个对象;

(2)具体实现:

1)构造函数:将指针指向该对象,引用计数置为1;

2)拷贝构造函数:将指针指向该对象,引用计数++;

3)赋值运算符:=号左边的shared_ptr的引用计数-1,右边的shared_ptr的引用计数+1,如果左边的引用技术降为0,还要销毁shared_ptr指向对象,释放内存空间。

(3)shared_ptr的引用计数本身是安全且无锁的,但是它指向的对象的读写则不是,因此可以说shared_ptr不是线程安全的。shared_ptr是线程安全的吗? - 云+社区 - 腾讯云 (tencent.com)

  • weak_ptr是为了解决shared_ptr的循环引用问题,那为什么不用raw ptr来解决这个问题?

答:一个weak_ptr绑定到shared_ptr之后不会增加引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使weak_ptr指向对象,也还是会释放;raw指针,当对象销毁之后会变成悬浮指针。

关键字

  • const的作用?指针常量和常量指针?const修饰的函数能否重载?
描述 细节
const修饰符 用来定义常量,表示不可变性。
常量 被const修饰的成员函数不能修改类中的数据成员。 具有不可变性
指针常量 指针本身不可修改,但指向的对象可以修改。 指针本身的常量性
常量指针 指针指向的对象不可修改,但指针本身可以修改。 指针指向对象的常量性。
const成员函数 不能改变类内的数据成员,也无法调用非const的成员函数。
const类对象函数调用const函数 只能调用const成员函数。
非const类对象函数调用const函数 可以调用const和非const成员函数,但优先调用非const函数(如果有重载)。
  • static的作用?static变量什么时候初始化?
修饰对象 作用范围 描述
文件作用域的变量 本文件 static修饰的变量和函数仅在本文件可见,其他文件无法访问和使用,有助于避免重定义问题。
函数作用域的变量 函数内部 作为局部静态变量时,该变量在函数调用期间保持其值,只进行一次初始化,不会因函数调用而重置,但仅在该函数内部可见。
类的静态数据成员 所有类对象 static修饰的静态数据成员是所有类对象共享的,而非每个类对象独有的。这些静态数据成员在类的声明中不占用内存,必须在.cpp文件中定义以分配内存。
类的静态成员函数 所有类对象 static修饰的静态成员函数->静态数据成员和函数->x非静态数据成员和函数。
它们属于类本身而非类的某个特定对象。
初始化过程 文件域的静态变量和类的静态成员变量在main函数执行之前的静态初始化过程中分配内存并初始化;局部静态变量在第一次使用时分配内存并初始化。
  • 其他关键字
关键字
extern 它与”C”一起连用时,如: extern “C” void fun(int a, int b);则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的;当它作为一个对函数或者全局变量的外部声明,提示编译器遇到此变量或函数时,在其它模块中寻找其定义。
explicit 标明类的构造函数是显式的,不能进行隐式转换。
constexpr 这个关键字明确的告诉编译器应该去验证(函数或变量)在编译期是否就应该是一个常数(这样编译器就可以大胆进行优化)。
volatile 跟编译器优化有关,告诉编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的备份。
mutable 可变的意思,使类中被声明为const的函数可以修改类中的非静态成员
auto 用于实现类型自动推导,让编译器来操心变量的类型,auto不能用于函数传参和推导数组类型
deltype 用于实现类型自动推导,让编译器来操心变量的类型,deltype可以解决这个问题

左值右值 构造函数

左值就是具有可寻址的存储单元,并且能由用户改变其值的量,比如常见的变量:一个int,float,class等。左值具有持久的状态,直到离开作用域才销毁;右值表示即将销毁的临时对象,具有短暂的状态,比如字面值常量“hello”,返回非引用类型的表达式int func()等,都会生成右值;

右值引用就是必须绑定到右值的引用,可以通过&&(两个取地址符)来获得右值引用;右值引用只能绑定到即将销毁的对象,因此可以自由地移动其资源;

右值引用是为了支持移动操作而引出的一个概念,它只能绑定到一个将要销毁的对象,使用右值引用的移动操作可以避免无谓的拷贝,提高性能。使用std::move()函数可以将一个左值转换为右值引用。(可以通过两个很长的字符串的直接赋值和移动赋值来测试一下性能的差距)。

  • 为什么要自己定义拷贝构造函数?什么是深拷贝和浅拷贝?

(1)拷贝构造函数的作用就是定义了当我们用同类型的另外一个对象初始化本对象的时候做了什么,在某些情况下,如果我们不自己定义拷贝构造函数,使用默认的拷贝构造函数,就会出错。比如一个类里面有一个指针,如果使用默认的拷贝构造函数,会将指针拷贝过去,即两个指针指向同个对象,那么其中一个类对象析构之后,这个指针也会被delete掉,那么另一个类里面的指针就会变成野指针(悬浮指针);

(2)这也正是深拷贝和浅拷贝的区别,浅拷贝只是简单直接地复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。 但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

  • 什么是移动构造函数,和拷贝构造函数的区别?

就是拷贝传递内存后就销毁他

C++11 移动构造函数_项脊轩-CSDN博客_c++ 移动构造

答:移动构造函数需要传递的参数是一个右值引用,移动构造函数不分配新内存,而是接管传递而来对象的内存,并在移动之后把源对象销毁;拷贝构造函数需要传递一个左值引用,可能会造成重新分配内存,性能更低。

内联函数和宏

内联函数就是在编译的时候在函数调用点直接展开函数 类似与匿名函数但是可以作为函数变量可以反复使用

define 是在预处理作为命令进行文本性切换

杂项

  • C++11的新特性

(1)auto关键字,可以自动推断出变量的类型;

(2)nullptr来代替NULL,可以避免重载时出现的问题(一个是int,一个是void*);

(3)智能指针,那三个智能指针,对内存进行管理;

(4)右值引用,基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率;

(5)lambda表达式,可以理解为一个匿名的内联函数。

不足之处:没有GC(垃圾回收机制)、没有反射机制等。

  • 指针和引用的区别

(1)指针本质是一个地址,有自己的内存空间,引用只是一个别名;

(2)指针可以指向其他的对象,但是引用不能指向其他的对象,初始化之后就不能改变了;

(3)指针可以初始化为nullptr,而引用必须被初始化为一个已有对象的引用;

(4)指针可以是多级指针,引用只能是一级。

  • 重载、重写和隐藏的区别

(1)重载指的是同一个名字的函数,具有不同的参数列表(参数类型、个数),根据参数列表决定调用哪一个函数;

(2)重写(覆盖)指的是,派生类中的函数重写了基类中的虚函数,重写的基类的中函数必须被声明为virtual,并且返回值,参数列表和基类中的函数一致;

(3)隐藏是指,派生类中的同名函数把基类中的同名函数隐藏了,即基类同名函数被屏蔽掉;此时基类函数不能声明为virtual。

  • Delete和Delete[]的区别,delete[]如何知道要delete多少次,在类的成员函数中能否Delete This?

【游戏开发面经汇总】- 计算机基础篇 - 知乎 (zhihu.com)