c++ 和c11
强制类型转换
四个转换类型
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)
1 |
|
- 什么是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,我们希望左值转发之后还是左值,右值转发后还是右值
std::move的核心原理是:
类型转换:将左值转换为右值引用
启用移动语义:使得编译器可以选择移动构造函数/移动赋值运算符
零成本抽象:运行时没有任何开销,完全是编译时的类型操作
资源转移:允许高效地转移资源所有权,避免深拷贝

什么是引用折叠? 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++没有的特性等,要能深入一点说那就更好了

