作为一员C#人柱力,最近面了四五家游戏厂吧,几乎没人问你C#的知识,都在逮着C++薅,C#人都被薅秃了喵,于是总结了一下,写下这篇面试经验之谈,希望能给后来人一丢丢帮助吧,当然内容都是从各个垃圾桶里翻出来的,毕竟我最爱在垃圾桶里找吃的了。
1.#pragma once和#ifndefXXX #defineXXX #endif
2.C++传递方式 1.值传递:实参复制一份副本给形参,会带来额外的内存开销和时间开销
3.C++内存分区
4.const
5.malloc,new和delete-内存碎片
6.struct和class的区别
7.this
8.typedef和define
9.避免外部实例化当前类的对象
10.常用的智能指针和内存转换
11.封装继承和多态
12.构造函数和析构函数
13.静态static
14.空类最优化
15.菱形继承及其解决
16.判定一个实例是否属于一个类
17.深拷贝和浅拷贝
18.虚函数和纯虚函数
19.重写override和重载overload
20.lambda表达式
21.C++的右值引用
22.迭代器的cbegin和begin的区别
23.删除元素迭代器对不同类型的STL的影响
24.各种常用算法的效率
25.多线程确保线程安全的方式
26.死锁和活锁 死锁:多个线程无限期等待一个线程释放一个资源,但是该线程无限期锁死目标资源的问题,可以使用超时等待,锁顺序限定等方式解决
27.一个好的接口
28.C++代码转可执行文件的过程
29.大端,小端,网络字节序和主机字节序
30.指针和引用
1.#pragma once和#ifndefXXX #defineXXX #endif
#pragma once和#ifndefXXX #defineXXX #endif都是C++为了防止头文件被重复包含的预处理指令,用于提高代码的可移植性。
他们的区别是:
1.#pragma once比#ifndef#endif简洁易懂和快速,标志了#pragma once的头文件注定只会被包含一次,缺点是有些编译器无法识别。
2.#ifndefXXX #defineXXX #endif拥有更高的可移植性,标识符唯一,依赖编译器的条件编译功能,属于C系的标准预处理指令,能够被所有C系编译器识别。
2.C++传递方式
1.值传递:实参复制一份副本给形参,会带来额外的内存开销和时间开销
2.引用传递:形参指向实参的直接地址,实参能够同步对形参的操作,但是形参不能指向其他对象。
3.指针传递:形参就是实参本身,实参能够同步对形参的操作,同时形参也能够更改指向的对象,缺点是:容易产生野指针,悬挂指针。
野指针:未初始化或者已经被delete或free的指针,当其他指针指向野指针时报异常。
悬挂指针:已经被delete或free,但是delete或free后没有将指针指向nullptr的指针,通常发生在数组中,当其他指针指向悬挂指针时,报异常
4.常量引用传递:Const传递,在类型左定值,右定向,将一个只读引用传递给形参,不需要复制,更高效,且更安全。
3.C++内存分区
内核空间:存储操作系统的信息,例如进程表。
栈区:存储局部变量,函数参数,返回地址这些,自动申请释放。
共享区:多个进程共享其空间,也可以进行进程间的通信。
堆区:动态内存,由malloc,new这些申请,需要手动调用,free,delete释放空间。
全局区/静态区:储存全局变量和静态变量。
常量区:存储常量,只能读,不能修改。
代码区:存储代码,只能执行,不能修改。
堆栈的区别在于:
栈分配的空间是线性的,申请释放快,可自动释放,从高地址指向低地址。
对分配的空间采用链表存储,申请释放慢,要手动释放,从低地址指向高地址。
4.const
const int* a; int* const a;int const* a;
const int* a和int const* a;固定的是指针指向的值
int* const a固定的是指针本身,但是不固定指针的指向的值。
5.malloc,new和delete-内存碎片
malloc,new和delete都是C系进行内存管理的操作符
但是:
malloc来自C,仅用于动态内存分配,无法重载,与类本身无关联。
new和delete来自C++,用于动态内存分配和释放,与类的构造和析构相关联,且安全允许重载。
内存碎片:使用new和malloc就会产生内存碎片,尤其是全局的new和malloc,因此要减少内存碎片的手段之一就是采用内存池,内存整理和内存分配等功能,通过收集内存碎片使其积累变大让计算机进行内存分配时可记忆,第二种方式是使用智能指针和jemalloc,tcmalloc等优化的内存分配器。
6.struct和class的区别
结构体的成员,继承默认公开,类的成员,继承默认私有
7.this
this指针存储在编译器的寄存器中,不占用堆栈空间,指向当前对象,但是只有非静态成员有this指针。
8.typedef和define
#define是预处理指令,用于定义宏,预处理完成文本替换
typedef是关键字,用于为现有的数据类型定义一个别名
9.避免外部实例化当前类的对象
1.构造函数私有化
2.类抽象化
3.对不希望被实例化的成员使用delete关键字,使得成员不能被调用。
10.常用的智能指针和内存转换
四种智能指针:
第一种auto_ptr 自动智能指针,通过转移资源所以权来解决复制问题,
第二种unique_ptr 独占性智能指针,通过将构造函数,拷贝构造,拷贝赋值函数私有化的方式,解决复制问题,可以通过move进行所有权转移和赋值。
第三种 shared_ptr 共享指针,引用计数和原址操作确保线程安全,可以随便转移和赋值。
第四种 weak_ptr 弱指针,弱引用解决第三种智能指针的循环引用问题。可以通过retset进行所有权转移。
11.封装继承和多态
封装:将属性和方法通过public,private,protect等访问控制符限定住其访问域,实现保护的效果,其中:protect表示派生类可调用,private表示私有。
继承:基类派生子类,子类实现自己功能。
虚继承:如果继承时:加上一个virtual,那就是虚继承,虚继承的类不会copy父类的内容,而是和父类共用一个内容。
多态:同一个函数,不同对象调用能够产生不同结果,依赖于虚函数。本质上是每个虚函数将被分配一个虚函数表,当指针指向哪个类的虚函数时,哪个类的虚函数将被调用。
静态多态:借用重载和模版实现的多态,在编译阶段完成确定,优点是高效,缺点是难调试
动态多态:借用虚函数实现的多态,在运行阶段完成确定,优点是代码可复用,可移植性强,缺点是运行阶段才完成,降低效率。
12.构造函数和析构函数
构造函数:当new时从父级向子级开始调用,不允许虚化,不允许继承。
析构函数:对象生命周期结束前调用,从子级向父级开始调用,允许虚化(虚化后子类还是会调用父类的析构函数),且唯一,同时当父类实例指向子类实例时:若析构函数非虚,那么将不会调用对应子类的析构函数
13.静态static
标志位静态的成员在类实例化前完成实现,需要在类外完成初始化,这些成员变量在超出代码块后不会被自动回收,归属于所有对象。其主要用法有:
静态函数局部变量:函数被多次调用也仅进行一次初始化
静态类函数:只能访问静态的成员
静态全局成员:限定全局成员的作用域为源文件而不是整个项目
静态断言:static_assert(条件,No结果):判定条件是否为真。
14.空类最优化
C++中所有空类都会占用1byte内存,最优化方法是:将所有空类的派生类合并到一块
每个空类都有的默认函数为:
1.构造和析构函数
2.拷贝构造和赋值操作符
3.移动构造和赋值操作符。
15.菱形继承及其解决
菱形继承:如果一个类多次继承同一个类就会出现菱形继承的问题,即:同一个父类在子类中多次被继承。
解决方法:
1.虚继承:通常:菱形继承的原因是BC继承于A,而D继承自BC,因此在BC继承A的时候,将其该成虚继承就行。这样:BC继承A时:将直接继承A的变量,而不是copy一份出来。
2.纯虚函数
16.判定一个实例是否属于一个类
1.使用typeid获取实例信息
2.调用目标类的虚函数
3.使用dynamic_cast运算符将目标类指向实例,若能:则是,否则不是。
17.深拷贝和浅拷贝
浅拷贝:只复制对象的所有值类型,而址类型和原对象指向同一块地址。
深拷贝:既复制对象的值类型,又为址类型开辟一块全新的区域给新对象使用。
18.虚函数和纯虚函数
虚函数和纯虚函数的关键字都是virtual
其中:区别在于:
虚函数的父类有实现,且子类可选择性实现,但是虚函数需要额外内存用于存储虚函数表指针。
纯虚函数父类仅声明,非抽象子类必须实现,使类抽象化,且不需要额外的虚函数表指针。
19.重写override和重载overload
重写是同一函数名拥有相同参数和返回值的实现,其具有覆盖原函数的特点。
重载是同一函数名不同参数和返回值的实现,其特点是多个同名函数共存,能够反应多种需求。
20.lambda表达式
基本格式:
使用格式:[捕获参数](参数){内容};
定义格式:function<返回值(参数)> name;
因此:
lambda表达式接收外部传入参数的方式是:
值传递,引用传递,指针传递和捕获参数
21.C++的右值引用
右值引用的意义是移动语义和完美转发,移动语义是指通过右值引用将对象的资源所有权移动到另一个对象,完美转发是指右值引用能够将函数参数类型完全保留转发给另一个函数。
22.迭代器的cbegin和begin的区别
begin返回的迭代器的值可以改变的首元素,cbegin返回的是被const修饰的不可改变值的首元素。
23.删除元素迭代器对不同类型的STL的影响
vector:指定位置往后迭代器全失效,往后所有元素前移一位
list:指定位置失效,其他不影响
map,set:指定元素及其相关元素的迭代器失效
24.各种常用算法的效率
①排序算法
算法名 | 算法平均时间复杂度 | 算法空间复杂度 | 稳定性 |
直接插入排序 | O(n*n) | O(1) | 稳定 |
希尔排序 | O(n*n)(难说) | O(1) | 不稳定 |
简单选择排序 | O(n*n) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(1) | 不稳定 |
冒泡排序 | O(n*n) | O(1) | 稳定 |
快速排序(常考) | O(nlogn) | O(logn) | 不稳定 |
归并排序 | O(nlogn) | O(n) | 稳定 |
基数排序 | O(d*(n+r)) | O(n+r) | 稳定 |
②查找算法:查找算法的最差时间复杂度几乎都是O(n)
算法名 | 平均时间复杂度 | 空间复杂度 | 是否需要有序 |
顺序查找 | O(n) | O(1) | X |
二分查找 | O(logn) | O(1) | Y |
插值查找 | O(log(logn)) | O(1) | Y |
斐波那契查找 | O(logn) | O(n) | Y |
分块查找 | O(logn)-O(n) | O(1)-O(n) | 块间有序 |
二叉排序树(BST) | O(H)或O(logn) | O(1) | |
平衡二叉排序树(BBST) | O(logn) | O(1) | |
B-树 | |||
B+树 | |||
红黑树 | |||
哈希查找 | O(1) | O(n) | 处理冲突的方法有: 1.开放地址法 2.再哈希法 3.链地址法 4.公共溢出区法 |
③图算法:常用的有BFS,DFS,迪杰斯特拉算法,普里姆算法,弗洛伊德算法和克鲁斯卡尔算法
④树算法:
先序遍历:DLR
中序遍历:LDR
后续遍历:LRD
解释:D:当前点 L:左节点, R:右节点
25.多线程确保线程安全的方式
1.给线程加锁,任何条件变量使用都加限制
2.原子操作,确保线程安全
3.线程局部存储,每个线程的内存独立
4.避免死锁和活锁
26.死锁和活锁
死锁:多个线程无限期等待一个线程释放一个资源,但是该线程无限期锁死目标资源的问题,可以使用超时等待,锁顺序限定等方式解决
活锁:多个线程对一个资源反复争夺的过程,方法同上。
27.一个好的接口
好的接口的基本性质:稳定,兼容,可拓展,复用性强,易读写,安全
ADD:有一个有效的说明文档
28.C++代码转可执行文件的过程
预编译–头文件展开,替换宏
编译-----将代码转化成汇编语言
汇编-----将汇编转化成二进制文件
链接-----链接二进制文件
动态链接:链接时库不加入可执行文件,有库依赖
静态链接:链接时库加入可执行文件,无库依赖
29.大端,小端,网络字节序和主机字节序
大端是指高位字节被存储到低地址,而低位字节被存储到高地址。
小端是指高位字节被存储到高地址,而低位字节被存储到低地址。
网络字节序:要求数据发送时全是大端
主机字节序:有大端的,也有小端的
30.指针和引用
指针本质还是一个内存分配在堆上的变量,允许null状态的存在,指向的对象可以在堆,栈和其他区域,且指定的对象可改变,还能指定其常量性
引用本质是对象的一个别名,是一个参数或类的常量成员,只能引用已存在且非空的对象,不能改变指定的对象。