c++ 和c11
内存分配
内存
| 内存区域 | 描述 | 分配和释放方式 | 特性 |
|---|---|---|---|
| 堆 | 使用malloc、free动态分配和释放空间,能分配较大的内存 | malloc/free | 1. 需要手动申请和手动释放 2. 能分配的内存较大 3. 分配和释放内存可能产生内存碎片 4. 分配效率较低 5. 地址从低向上 |
| 栈 | 为函数的局部变量分配内存,能分配较小的内存 | 由操作系统自动分配和释放 | 1. 自动申请和自动释放 2. 能分配的内存较小 3. 不会产生内存碎片 4. 分配效率高 5. 地址由高向下 |
| 全局/静态存储区(.bss 和 .data段) | 用于存储全局变量和静态变量 | 编译器分配和释放 | 存储全局和静态变量的生命周期(未初始化在.bss段,初始化的在.data段) |
| 常量存储区(.data段) | 专门用来存放常量 | 编译器分配和释放 | 存储常量,不可修改 |
| 自由存储区 | 通过new和delete分配和释放空间的内存,具体实现可能是堆或者内存池 | new/delete | 1. 是C++的术语,抽象概念 2. 可以通过重载操作符使用其他内存实现 3. 对于自定义对象,能调用构造函数和析构函数 |
| 代码区(.text段) | 存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里. |
关于堆和栈的内存区别:
| 区别点 | 堆 | 栈 |
|---|---|---|
| 内存申请和释放 | 手动申请和释放 | 自动申请和释放 |
| 内存大小 | 较大(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)–>产生虚函数表、单一继承、多重继承、重复继承、虚拟继承
- 问:类的内存布局是什么样的?考虑有虚函数、多继承、虚继承几种情况。(提问概率:★★★★★)
简单总结一下就是类只有成员变量占用内存(静态成员不占类内部内存,函数不占内存)。如果有虚函数,每个类对象都会有一个虚函数指针Vptr(占用一个指针大小的内存),vptr指向一个虚函数表,表里面记录了各项标记virtual的函数,子类如果覆盖父类虚函数,对应虚表位置的虚函数会被子类的替换。如果是虚继承,还会有虚基类表记录当前对象相对虚基类的偏移,以及一个虚基类指针指向这个虚基类表。
虚表在编译完成时大小与布局就被决定了,加载时其内存位置也就被确定了。
- 1.单继承一个或多个类只有一个虚表一个虚指针
- 2.普通多继承会有基类的个数个虚表,基类的个数个虚指针。派生类自己独有的虚函数可能会放在第一个虚表的最后面
- 3.单个虚继承会有两个虚表(看情况)以及一个虚基类表,两个虚指针(这个可能与我们想象中的不一样,一个指向自己独有的虚函数的虚表,一个指向覆盖基类虚函数的虚表)以及一个虚基类指针与虚基类表 注意:如果派生类自己的虚函数与基类完全相同,可能只有一个虚表,一个虚指针
- 4.菱形多虚继承会有基类的个数个虚指针以及虚表(看情况,第3条有提到),有几个虚继承就有几个虚基类指针以及虚基类表
- C++的
sizeof影响因素
- 非静态成员变量:直接贡献大小。
- 内存对齐:编译器填充提升访问速度(如
#pragma pack)。 - 虚函数表指针:存在虚函数时增加一个指针大小(64位下8字节)。
- 继承关系:基类成员+派生类成员,可能受空基类优化影响。
**空对象sizeof**:
默认为1字节(占位),若继承空基类可能优化为0(Empty Base Optimization)。
虚函数表为什么不放在全局区
- 原因:
- 虚函数表(vtable)是与类相关的,而不是与程序全局相关的。
- 每个类可能有多个虚函数表(如多继承)。
- 虚函数表需要与类的定义紧密关联,放在全局区会增加管理复杂度。
- 存储位置:
- 虚函数表通常存放在只读数据段(.rodata)。
const 变量的存储位置
- 常量区(.rodata):
- 全局
const变量。 - 字符串常量。
- 全局
- 栈区:
- 局部
const变量。
- 局部
内存对齐
为什么要字节对齐?
需要字节对齐的根本原因在于CPU访问数据的效率问题。假如没有字节对齐,那么一个double类型的变量会存储在4-11上(正常是0-7)这样计算机取这个数据的会会取两次,降低效率。而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误。
自定义类型的内存对齐规则
- 规则:
- 对齐值为成员中最大对齐值的整数倍。
- 每个成员的偏移量必须是其对齐值的整数倍。
1 | struct Example { |
什么时候不希望进行内存对齐
- 节省内存:在某些嵌入式系统中,内存资源有限,可能需要手动调整对齐方式。
- 兼容性:与某些硬件或协议交互时,可能需要特定的内存布局。
- 问:memory alignmentand padding, 内存对齐的原理与意义(提问概率:★★★★)
结构体以及类成员对齐,意义就是减少cpu读取的次数,提高效率。比如一个int变量长度为4个字节,cpu一次读4个字节,当然是一次读取比较好。但是如果前面有一个char,地址为0-1。那么这个int的地址就为1-4。导致cpu,分两次读取int值。
具体的对齐规则,要说的非常准确可能比较麻烦,简单来讲就是,每个变量看后面的变量,如果后面的变量大,就和后面的大小对齐并补充字节。最后一个变量,按照成员内最大的对齐值,对齐并补充字节
内存泄漏
- 问:内存泄露是什么意思?如何检测与避免内存泄漏?(提问概率:★★★★)
指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费
常量
Const
- const的作用?指针常量和常量指针?const修饰的函数能否重载?
| 描述 | 细节 | |
|---|---|---|
| const修饰符 | 用来定义常量,表示不可变性。 | |
| 常量 | 被const修饰的成员函数不能修改类中的数据成员。 | 具有不可变性 |
| 指针常量 | 指针本身不可修改,但指向的对象可以修改。 | 指针本身的常量性 |
| 常量指针 | 指针指向的对象不可修改,但指针本身可以修改。 | 指针指向对象的常量性。 |
| const成员函数 | 不能改变类内的数据成员,也无法调用非const的成员函数。 | |
| const类对象函数调用const函数 | 只能调用const成员函数。 | |
| 非const类对象函数调用const函数 | 可以调用const和非const成员函数,但优先调用非const函数(如果有重载)。 |
- 了解const么?哪些时候用到const?与宏定义有什么差异?(提问概率:★★★★)
简单理解,const的目的就是定义一个“不会被修改的常量”,可以修饰变量、引用、指针,可以用于函数参数、成员函数修饰、成员变量,修饰成员函数本质上就是修饰“this”指针,所以不能修改函数内部的成员变量。
相比宏定义,const在编译期也会起作用(宏定义只是预编译期),会做一些类型检查,方便调试。而且,const不需要在每个用到的地方都申请一块内存空间,要更节省内存。
Static
- static的作用?static变量什么时候初始化?
| 修饰对象 | 作用范围 | 描述 |
|---|---|---|
| 文件作用域的变量 | 本文件 | static修饰的变量和函数仅在本文件可见,其他文件无法访问和使用,有助于避免重定义问题。 |
| 函数作用域的变量 | 函数内部 | 作为局部静态变量时,该变量在函数调用期间保持其值,只进行一次初始化,不会因函数调用而重置,但仅在该函数内部可见。 |
| 类的静态数据成员 | 所有类对象 | static修饰的静态数据成员是所有类对象共享的,而非每个类对象独有的。这些静态数据成员在类的声明中不占用内存,必须在.cpp文件中定义以分配内存。 |
| 类的静态成员函数 | 所有类对象 | static修饰的静态成员函数->静态数据成员和函数->x非静态数据成员和函数。 它们属于类本身而非类的某个特定对象。 |
| 初始化过程 | 文件域的静态变量和类的静态成员变量在main函数执行之前的静态初始化过程中分配内存并初始化;局部静态变量在第一次使用时分配内存并初始化。 |
说一下static关键字的作用?
全局静态变量:位于静态存储区,程序运行期间一直存在,对外部文件不可见。
局部静态变量:位于静态存储区,在局部作用域可以访问,离开局部作用域之后static变量仍存在,但无法访问。
静态函数:即在函数定义前加static,函数默认情况下为extern,即可导出的。加了static就不能为外部类访问。注意不要在头文件声明static函数,因为static只对本文件有效。
类的静态成员:可以实现多个不同的类实例之间的数据共享,且不破坏隐藏规则,不需要类名就可以访问。类的静态存储变量是可以修改的。可以通过<对象名>::<静态成员>进行访问。
类的静态函数:不能调用非静态成员,只可以通过对象名调用<对象名>::<静态成员函数>
static 不需要初始化,默认为0值。
extern与static
- 问:extern与static(提问概率:★★★)
extern 声明一个变量定义在其他文件,这样当前文件就可以使用这个变量,否则会编译失败,如果两个全局变量名称一样会出现链接失败。extern c的作用更重要,因为c++的编译方式与c是不同的,比如函数重载利用mangling的优化。
static变量,很多编译器优化后的效果就是声明一个全局变量,然后判断是否初始化,是的话之后就不需要再初始化了,但是不绝对,win7的全局变量与static的位置就有差异。static成员函数其实在编译后与class完全没有关系。static成员其实也没关系,但是private的需要通过类去调用。static全局变量需要注意,他只能在当前编译单元也就是.cpp内使用(内链接)。全局函数变量是外链接,可以跨单元调用。
static相关注意与理解:静态内存是在main前分配,在main后释放。当存在多个复杂的static变量时,你就不知道哪个先分配了,也控制不了。另外,关于static具体的存储位置,一般是我们常说的静态存储区(bss,数据区等),更贴切的说他是一个可执行文件里面的区域,到操作系统层面可能是另一种叫法,对不同的编译器、C++版本、操作系统可能都有所差异。我们一个程序编译链接好后会把一些静态数据写到exe、dll里面,注意这时候exe并没有放入到内存,所以,其实所谓的编译后内存位置就确定了只不过是一种“理解方式”,真正的静态区(全局变量、静态变量、常量)也是在程序运行后操作系统将这些数据装入内存后的一个位置,这个位置相对exe来说可以理解为静态的,然后当我们运行exe动态申请内存时就是我们 常说的堆区(也可以叫动态区、C++叫自由存储区等)
单例模式
单例模式(Singleton Pattern,也称为单件模式),使用最广泛的设计模式之一。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
定义一个单例类:
- 私有化它的构造函数,以防止外界创建单例类的对象;
- 使用类的私有静态指针变量指向类的唯一实例;
- 使用一个公有的静态方法获取该实例。
1 | public class Singleton |
单例模式 静态类的区别
1)首先单例模式会提供给你一个全局唯一的对象,静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用;
2)单例模式的灵活性更高,方法可以被override,因为静态类都是静态方法,所以不能被override;
3)如果是一个非常重的对象,单例模式可以懒加载,静态类就无法做到;
强制类型转换
四个转换类型
static_cast (隐式类型转换)
实现隐式类型转换,支持基本数据类型、枚举、结构体、类之间的转换,以及类层次间的向上和向下转换(向下转换不安全)。
1 | double d = 3.14; |
用于各种隐式转换,比如void* 转ptr*,例如:
1 | double a = 1.0f; |
const_cast (常量转换)
转换const属性,可以添加或移除const属性,仅适用于指针或引用,只能改变对象的底层const。
1 | const int a = 10; |
用来移除变量的const或volatile限定符。
1 | const int constant=21; |
引申:volatile -> 跟编译器优化有关,告诉编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的备份。
dynamic_cast (动态类型转换)
动态类型转换,用于将基类安全地转换为派生类(或向上转换),失败时返回NULL或抛出异常。需要基类有虚函数。
安全的向下进行类型转换。只能用于含有虚函数的类,只能转指针或引用。
1 | class Base { |
- 问:dynamic_cast是怎么实现的(提问概率:★★★★)
dynamic_cast属于RTTI,运行时类型识别的一个内容,他是c++realise1.0的主要扩充功能之一。主要内容是typeid与typeinfo的实现,基本思路就是在有虚函数的类的虚表的头部位置存放RTTI的相关信息。在VC里面可以看到是一个叫做RTTI Complete Object Locator的结构体里面存放相关的信息。在强转的时候,会读取里面对应的类的信息进而判断是否能转换成功。
reinterpret_cast (重新解释转换)
重新解释二进制数据,可以转换任何类型到任何类型,不保证类型安全。
1 | int a = 10; |
允许将任何指针转换为任何其他指针类型,并不安全。
强制转换的异同点
类型安全
请注意,使用const_cast、reinterpret_cast和dynamic_cast时需要特别小心,因为它们可能会破坏类型安全或引入其他问题。在大多数情况下,使用static_cast进行类型转换是更安全的选择。
static_cast和dynamic_cast的异同点?
答:二者都会做类型安全检查,只是static_cast在编译期进行类型检查,dynamic_cast在运行期进行类型检查。后者需要父类具备虚函数,而前者不需要。
static_cast 和 reinterpret_cast 的区别
- static_cast
用途:用于相关类型之间的转换(如基类与派生类指针、数值类型转换等)。
void* 转换:
保留原始地址信息,转换是安全的、可预测的。
示例:
1 | int* a = new int(); |
- reinterpret_cast
用途:用于无关类型之间的低级别重新解释(如整数与指针互转、模糊类型转换)。
void* 转换:
转换后的 void* 地址可能不保留原始语义(尽管实际实现通常保留地址)。
示例:
1 | int* a = new int(); |
- 总结
static_cast:安全、可预测,适合 void* 互转。
reinterpret_cast:仅保证恢复后的指针值相同,中间步骤可能不可移植。
使用建议:
对于 void* 的转换,优先使用 static_cast。
对于模糊类型的转换,使用 reinterpret_cast。
智能指针
三个智能指针
智能指针主要解决一个内存泄露的问题,它可以自动地释放内存空间。因为它本身是一个类,当函数结束的时候会调用析构函数,并由析构函数释放内存空间。
| 智能指针类型 | 名称 | 描述 |
|---|---|---|
| shared_ptr | 共享指针 | 多个shared_ptr可以指向相同的对象,采用引用计数机制。当最后一个引用销毁时,释放内存空间。 |
| unique_ptr | 独占指针 | 保证同一时间段内只有一个unique_ptr能指向该对象,可通过move操作来传递unique_ptr。 |
| weak_ptr | 弱指针 | 用来解决shared_ptr相互引用时的死锁问题,是对对象的一种弱引用,不会增加对象的引用计数。 |
shared_ptr
- 初始化
1 | std::shared ptr<T> sptr= std::make shared<T>(...);// 初始化方式1 |
- 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)
- 什么是shared_ptr的循环引用问题,如何解决?
A⇌B (shared_ptr</T/>)
1 | struct ListNodeint { |
一个最简单的情况是,某对象存在一个shared_ptr类型的指针ptr,A的ptr指向B,B的ptr指向A。两个智能指针对象指向A,B,再加上他们的ptr分别指向B,A,所以引用计数均为2,造成了循环引用,谁也不会被释放。一般有三种解决方法:
当剩下最后一个引用时,需要手动打破循环引用释放对象;
当A的生存周期超过B的生存周期,B改为一个普通指针指向A;
将共享指针改为弱指针weak_ptr
一般采用第三者办法,原理是弱指针的指针_prev和_next不会增加node1和node2的引用计数。
1 | struct ListNodeint { |
彻底搞懂之C++智能指针 - sunsky303 - 博客园
weak_ptr
- weak_ptr是为了解决shared_ptr的循环引用问题,那为什么不用raw ptr来解决这个问题?
答:一个weak_ptr绑定到shared_ptr之后不会增加引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使weak_ptr指向对象,也还是会释放;raw指针,当对象销毁之后会变成悬浮指针。
unique_ptr
- 如何实现独占式指针?
由于指针或引用在离开作用域是不会调用析构函数的,但对象在离开作用域会调用析构函数。unique_ptr本质是一个类,将复制构造函数和赋值构造函数声明为delete就可以实现独占式,只允许移动构造和移动赋值。
1 | std::unique_ptr<T> uptr = std::make_unique<T>(...); // 方式1 |
reference and pointer
说一下C++指针和引用的区别?
指针有自己的内存地址,占四个字节(32位系统) ,而引用只是一个别名,没有专门的内存地址。
指针可以被初始化为指向nullptr,而引用必须指向一个已有的对象。
作为参数传递是,指针需要解引用(*),而直接修改引用会改变原对象。
指针可以多级,而引用最多一级。
如果返回动态内存分配对象,必须用指针,否则可能引起内存泄漏。
- 问:reference和pointer的区别?哪些情况使用pointer?(提问概率:★★)
1.指针可以为空,而引用强烈建议不要指向空值,否则可能会出现下面的情况。
2.指针可以不初始化,引用必须初始化。这意味着引用不需要检测合法性(是否为空)
3.指针可以随时更改指向的目标,而引用初始化后就不可以再指向任何其他对象
根据上面的情况我们知道大概知道哪些时候需要使用指针了。不过还有一种情况,在重载如[]符号的时候,建议返回引用,这样便于我们书写习惯也方便理解。因为平时我们都是这样使用, a[10] = 10;而不是*a[10] = 10;
RALL
- 问:RAII是什么?有什么意义?应用场景?(提问概率:★★★★)
RAII 是 resource acquisition is initialization 的缩写,意为“资源获取即初始化”。其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。常见的例子就是智能指针,通过声明一个包含资源对象指针的类,在这个类执行析构的时候释放指针指向的对象。
左值右值 构造函数
左值右值定义
- 代码来说:
右值引用是C++11引入的,与之对应C++98中的引用统称为左引用。左引用的一个最大问题就是,它不能对不能取地址的量(比如字面量常量)取引用。比如int &a = 1;就不可以。
为此专门定义了左值和右值,能取地址的都是左值,反之是右值。
通过右值引用,可以增长变量的生命周期,避免分配新的内存空间.
并用&&来表示右值引用,这样就可以int &&a=1;并用&来表示左值引用。
总结:左值引用只能绑定左值;右值引用只能绑右值,但常量左值引用可以绑字面量,比如const int &b = 10;已命名的右值引用,编译器会认为是一个左值;临时对象是左值。
- 内存来说:
左值就是具有可寻址的存储单元,并且能由用户改变其值的量,比如常见的变量:一个int,float,class等。左值具有持久的状态,直到离开作用域才销毁;右值表示即将销毁的临时对象,具有短暂的状态,比如字面值常量“hello”,返回非引用类型的表达式int func()等,都会生成右值;
右值引用就是必须绑定到右值的引用,可以通过&&(两个取地址符)来获得右值引用;右值引用只能绑定到即将销毁的对象,因此可以自由地移动其资源;
右值引用是为了支持移动操作而引出的一个概念,它只能绑定到一个将要销毁的对象,使用右值引用的移动操作可以避免无谓的拷贝,提高性能。使用std::move()函数可以将一个左值转换为右值引用。(可以通过两个很长的字符串的直接赋值和移动赋值来测试一下性能的差距)。
什么是将亡值,什么是纯右值。
所谓纯右值就是临时变量或者字面值,将亡值是C++11新定义的将要被“移动”的变量,比如move返回的变量。
移动语义 move
移动语义(move semantic)
某对象持有的资源或内容转移给另一个对象。为了保证移动语义,必须记得用 std::move 转化左值对象为右值,以避免调用复制构造函数.
1 | vector<int> a{1,2,3}; |
完美转发(perfect forwarding)
为了解决引用折叠问题,必须写一个任意参数的函数模板,并转发到其他函数.
为了保证完美转发,必须使用std::forward,我们希望左值转发之后还是左值,右值转发后还是右值
什么是引用折叠? forward函数的原理。
引用折叠就是,如果间接创建一个引用的引用,那么这些引用就会折叠。
深拷贝和浅拷贝
- 为什么要自己定义拷贝构造函数?什么是深拷贝和浅拷贝?
(1)拷贝构造函数的作用就是定义了当我们用同类型的另外一个对象初始化本对象的时候做了什么,在某些情况下,如果我们不自己定义拷贝构造函数,使用默认的拷贝构造函数,就会出错。比如一个类里面有一个指针,如果使用默认的拷贝构造函数,会将指针拷贝过去,即两个指针指向同个对象,那么其中一个类对象析构之后,这个指针也会被delete掉,那么另一个类里面的指针就会变成野指针(悬浮指针);
(2)这也正是深拷贝和浅拷贝的区别,浅拷贝只是简单直接地复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。 但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
移动构造函数与移动赋值
- 什么是移动构造函数和拷贝构造函数的区别?
就是拷贝传递内存后就销毁他
C++11 移动构造函数_项脊轩-CSDN博客_c++ 移动构造
答:移动构造函数需要传递的参数是一个右值引用,移动构造函数不分配新内存,而是接管传递而来对象的内存,并在移动之后把源对象销毁;拷贝构造函数需要传递一个左值引用,可能会造成重新分配内存,性能更低。
- 代码来说:
移动构造函数能直接使用临时对象已经申请的资源,它以右值引用为参数,拷贝以左值。
由于临时对象是右值,这里就需要使用一个move函数,它的作用的将左值强制转换为右值。
移动赋值是在赋值运算符重载的基础上,将对象右值引用作为形参进行拷贝或者赋值,从而避免创建新对象。
下面的例子展示了拷贝构造函数、赋值运算符重载、移动拷贝和移动赋值运算符重载,请仔细区别:
1 |
|
其他关键字
关键字
| 关键字 | ||
|---|---|---|
| extern | 它与”C”一起连用时,如: extern “C” void fun(int a, int b);则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的;当它作为一个对函数或者全局变量的外部声明,提示编译器遇到此变量或函数时,在其它模块中寻找其定义。 | |
| explicit | 标明类的构造函数是显式的,不能进行隐式转换。 | |
| constexpr | 这个关键字明确的告诉编译器应该去验证(函数或变量)在编译期是否就应该是一个常数(这样编译器就可以大胆进行优化)。 | |
| volatile | 跟编译器优化有关,告诉编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的备份。 | |
| mutable | 可变的意思,使类中被声明为const的函数可以修改类中的非静态成员 | |
| auto | 用于实现类型自动推导,让编译器来操心变量的类型,auto不能用于函数传参和推导数组类型 | |
| deltype | 用于实现类型自动推导,让编译器来操心变量的类型,deltype可以解决这个问题 |
内联函数
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++中应该优先考虑使用智能指针.
问:C++03/98有什么你不习惯或不喜欢的用法?C++11有哪些你使用到的新特性?(提问概率:★★★★★)
个问题最简单的办法就是看下一个版本的C++有哪些特性,新的特性肯定是有意义的。
如:
auto,有一些迭代器或者map嵌套类型,遍历时比较麻烦,auto写起来很方便。
vector以及其他容器的列表初始化,原来想要像数组一样初始化的话,需要一个一个来,很麻烦。
类内初始值问题,总是需要放到构造函数里面初始化,初始化列表倒是不错,但是初始化数据太多就不行了。
nullptr,C++11前的NULL一般是是这样定义的 #define NULL 0,这可能会导致一些函数参数匹配问题。而nullptr可以避免这个问题。
thread,不需要再使用其他的库来写多线程了。
智能指针shareptr,一定程度上解决内存泄露问题。
右值引用,减少拷贝开销。
lambda function,简化那些结构简单的函数代码。
当然,你要是能说出一些还没有改正或者有待考虑的问题就更好了,比如内存管理的困难(没有GC),没有反射以及一些C#,java里面有而C++没有的特性等,要能深入一点说那就更好了

