c++ 和c11
内存管理
C++
多态 虚函数
- 什么是多态?C++的多态是如何实现的?
所谓多态,就是同一个函数名具有多种状态,或者说一个接口具有不同的行为;C++的多态分为编译时多态和运行时多态,编译时多态也称为为静态联编,通过重载和模板来实现,运行时多态称为动态联编,通过继承和虚函数来实现。
- 对于非虚函数,三个类中虽然都有一个叫 func2 的函数,但他们彼此互不关联,因此都是各自独立的,不存在重载一说,在调用的时候也不需要进行查表的操作,直接调用即可。
- 由于子类B和子类C都是继承于基类A,因此他们都会存在一个虚指针用于指向虚函数表。注意,假如子类B和子类C中不存在虚函数,那么这时他们将共用基类A的一张虚函数表,在B和C中用虚指针指向该虚函数表即可。但是,上面的代码设计时子类B和子类C中都有一个虚函数 vfunc1,因此他们就需要各自产生一张虚函数表,并用各自的虚指针指向该表。由于子类B和子类C都对 vfunc1 作了重载,因此他们有三种不同的实现方式,函数地址也不尽相同,在使用的时候需要从各自类的虚函数表中去查找对应的 vfunc1 地址。
- 对于虚函数 vfunc2,两个子类都没有进行重载操作,所以基类A、子类B和子类C将共用一个 vfunc2,该虚函数的地址会分别保存在三个类的虚函数表中,但他们的地址是相同的。
- 从上图可以发现,在类对象的头部存放着一个虚指针,该虚指针指向了各自类所维护的虚函数表,再通过查找虚函数表中的地址来找到对应的虚函数。
- 对于类中的数据而言,子类中都会包含父类的信息。如上例中的子类C,它自己拥有一个变量 m_data1,似乎是和基类中的 m_data1 重名了,但其实他们并不存在联系,从存放的位置便可知晓。
动态绑定有以下三项条件要符合:
- 使用指针进行调用
- 指针属于up-cast后的
- 调用的是虚函数
与动态绑定相对应的是静态绑定,它属于编译的时候就确定下来的,如上文的非虚函数,他们是类对象直接可调用的,而不需要任何查表操作,因此调用的速度也快于虚函数。
C++中的虚指针与虚函数表 - 知乎 (zhihu.com)
构造函数与析构函数
构造和析构能是虚函数吗
- 为什么父类析构函数必须是虚函数?为什么C++默认析构函数不是虚函数。
通常将父类的析构函数设为虚函数。如果父类的析构函数不是虚函数,则不会触发动态绑定(多态),结果就是只会调用父类的析构函数,而不会调用子类的析构函数,造成内存泄漏。
C++默认析构函数不是虚函数,是因为虚函数需要虚函数表和虚表指针,会占用额外内存。如果一个类没有子类,就没有必要将析构函数设为虚函数。
- 构造函数不能是虚函数,因为在对象构造过程中其内存布局和虚函数表(vtable)尚未完全初始化,无法进行虚函数调用。
- 析构函数可以也是应当为虚函数,以确保通过基类指针删除派生类对象时能够调用正确的析构函数,从而实现正确的资源管理和释放。
构造函数和析构函数是否能调用虚函数
答:在C++ primer中说到过是最好不要调用,不是不能调用,所以构造函数跟虚构函数里面都是可以调用虚函数的,并且编译器不会报错。但是在基类中声明纯虚函数并且在基类的析构函数中调用,编译器会报错。
对于底层:
当在构造基类部分时,派生类还没被完全创建。即当A::A()执行时,B类对象还没被完全创建,此时它被当成一个A对象,而不是B对象,因此Function()绑定的是A的Function()。
基类部分在派生类部分之前被构造,当基类执行构造函数时,派生类中的数据成员还未被初始化。如果在基类构造函数中调用虚函数被解析成调用派生类的虚函数,而派生类的虚函数中又访问到未初始化的派生类数据,这是危险的,将会导致程序出现未知行为及bug。
多重继承和多继承
C++多态虚函数表详解(多重继承、多继承情况)_一个类有几个虚函数表-CSDN博客
- 多重继承
- 多继承
内存分配
内存区域 | 描述 | 分配和释放方式 | 特性 |
---|---|---|---|
堆 | 使用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的内存管理函数的区别:
- new分配内存空间无需指定分配内存大小,malloc需要。
- new返回类型指针,类型安全,malloc返回void*,再强制转换成所需要的类型。
- new是从自由存储区获得内存,malloc从堆中获取内存。
- 对于类对象,new会调用构造函数和析构函数,malloc不会。
不能混用C和C++的内存管理函数,因为它们的处理方式和内存管理机制不同。
内存布局
在C++中类对象的内存布局是如何分布的呢?
在这里,影响对象大小的有哪些因素呢?
成员变量的类型与数量、虚函数表的指针(_vftptr)
、虚基类表指针(_vbtptr)
–>产生虚函数表、单一继承、多重继承、重复继承、虚拟继承
struct的内存对齐规则
为什么要字节对齐?
需要字节对齐的根本原因在于CPU访问数据的效率问题。假如没有字节对齐,那么一个double类型的变量会存储在4-11上(正常是0-7)这样计算机取这个数据的会会取两次,降低效率。而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误。
类型转换
类型转换运算符 | 中文名 | 描述 | 示例 |
---|---|---|---|
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_cast
、reinterpret_cast
和dynamic_cast
时需要特别小心,因为它们可能会破坏类型安全或引入其他问题。在大多数情况下,使用static_cast
进行类型转换是更安全的选择。
static_cast和dynamic_cast的异同点?
答:二者都会做类型安全检查,只是static_cast在编译期进行类型检查,dynamic_cast在运行期进行类型检查。后者需要父类具备虚函数,而前者不需要。
智能指针
智能指针主要解决一个内存泄露的问题,它可以自动地释放内存空间。因为它本身是一个类,当函数结束的时候会调用析构函数,并由析构函数释放内存空间。
智能指针类型 | 名称 | 描述 |
---|---|---|
shared_ptr | 共享指针 | 多个shared_ptr可以指向相同的对象,采用引用计数机制。当最后一个引用销毁时,释放内存空间。 |
unique_ptr | 独占指针 | 保证同一时间段内只有一个unique_ptr能指向该对象,可通过move操作来传递unique_ptr。 |
weak_ptr | 弱指针 | 用来解决shared_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
- 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成员函数,但优先调用非const函数(如果有重载)。 |
Static
- static的作用?static变量什么时候初始化?
修饰对象 | 作用范围 | 描述 |
---|---|---|
文件作用域的变量 | 本文件 | static修饰的变量和函数仅在本文件可见,其他文件无法访问和使用,有助于避免重定义问题。 |
函数作用域的变量 | 函数内部 | 作为局部静态变量时,该变量在函数调用期间保持其值,只进行一次初始化,不会因函数调用而重置,但仅在该函数内部可见。 |
类的静态数据成员 | 所有类对象 | static修饰的静态数据成员是所有类对象共享的,而非每个类对象独有的。这些静态数据成员在类的声明中不占用内存,必须在.cpp文件中定义以分配内存。 |
类的静态成员函数 | 所有类对象 | static修饰的静态成员函数->静态数据成员和函数->x非静态数据成员和函数。 它们属于类本身而非类的某个特定对象。 |
初始化过程 | 文件域的静态变量和类的静态成员变量在main函数执行之前的静态初始化过程中分配内存并初始化;局部静态变量在第一次使用时分配内存并初始化。 |
说一下static关键字的作用?
全局静态变量:位于静态存储区,程序运行期间一直存在,对外部文件不可见。
局部静态变量:位于静态存储区,在局部作用域可以访问,离开局部作用域之后static变量仍存在,但无法访问。
静态函数:即在函数定义前加static,函数默认情况下为extern,即可导出的。加了static就不能为外部类访问。注意不要在头文件声明static函数,因为static只对本文件有效。
类的静态成员:可以实现多个不同的类实例之间的数据共享,且不破坏隐藏规则,不需要类名就可以访问。类的静态存储变量是可以修改的。可以通过<对象名>::<静态成员>进行访问。
类的静态函数:不能调用非静态成员,只可以通过对象名调用<对象名>::<静态成员函数>
static 不需要初始化,默认为0值。
单例模式
单例模式定义
单例模式(Singleton Pattern,也称为单件模式),使用最广泛的设计模式之一。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
定义一个单例类:
- 私有化它的构造函数,以防止外界创建单例类的对象;
- 使用类的私有静态指针变量指向类的唯一实例;
- 使用一个公有的静态方法获取该实例。
1 | public class Singleton |
单例模式 静态类的区别
1)首先单例模式会提供给你一个全局唯一的对象,静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用;
2)单例模式的灵活性更高,方法可以被override,因为静态类都是静态方法,所以不能被override;
3)如果是一个非常重的对象,单例模式可以懒加载,静态类就无法做到;
其他关键字
关键字 | ||
---|---|---|
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()函数可以将一个左值转换为右值引用。(可以通过两个很长的字符串的直接赋值和移动赋值来测试一下性能的差距)。
什么是将亡值,什么是纯右值。
所谓纯右值就是临时变量或者字面值,将亡值是C++11新定义的将要被“移动”的变量,比如move返回的变量。
深拷贝和浅拷贝
- 1为什么要自己定义拷贝构造函数?什么是深拷贝和浅拷贝?
(1)拷贝构造函数的作用就是定义了当我们用同类型的另外一个对象初始化本对象的时候做了什么,在某些情况下,如果我们不自己定义拷贝构造函数,使用默认的拷贝构造函数,就会出错。比如一个类里面有一个指针,如果使用默认的拷贝构造函数,会将指针拷贝过去,即两个指针指向同个对象,那么其中一个类对象析构之后,这个指针也会被delete掉,那么另一个类里面的指针就会变成野指针(悬浮指针);
(2)这也正是深拷贝和浅拷贝的区别,浅拷贝只是简单直接地复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。 但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
- 什么是移动构造函数,和拷贝构造函数的区别?
就是拷贝传递内存后就销毁他
C++11 移动构造函数_项脊轩-CSDN博客_c++ 移动构造
答:移动构造函数需要传递的参数是一个右值引用,移动构造函数不分配新内存,而是接管传递而来对象的内存,并在移动之后把源对象销毁;拷贝构造函数需要传递一个左值引用,可能会造成重新分配内存,性能更低。
内联函数和宏(inline define)
内联函数有什么作用?存不存在什么缺点?
(1)作用是使编译器在函数调用点上展开函数,可以避免函数调用的开销;
(2)内联函数的缺点是可能造成代码膨胀,尤其是递归的函数,会造成大量内存开销,exe太大,占用CPU资源。此外,内联函数不方便调试,每次修改会重新编译头文件,增加编译时间。
内联函数和宏有什么区别,有了宏为什么还需要内联函数?
(1)define宏命令是在预处理阶段对命令进行替换,inline是在编译阶段在函数调用点处直接展开函数,节省了函数调用的开销;
(2)define的话是不会对参数的类型进行检查的,因此会出现类型安全的问题,比如定义一个max命令,但是传递的时候可能会传递一个整数和一个字符串,就会出错,但是内联函数在编译阶段会进行类型检查;
(3)使用宏的时候可能要添加很多括号,比较容易出错。
杂项
- 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)
- 什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?你通常采用哪些方法来避免和减少这类错误?
用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。
1). 使用的时候要记得指针的长度.
2). malloc的时候得确定在那里free.
3). 对指针赋值的时候应该注意被赋值指针需要不需要释放.
4). 动态分配内存的指针最好不要再次赋值.
5). 在C++中应该优先考虑使用智能指针.