赞
踩
数据结构 | 底层实现 |
---|---|
vector | 动态数组 |
list | 双向链表 |
map 、set、multiset、multimap | 红黑树 |
unordered_map、unordered_set | 哈希表 |
STL中的sort并非只是普通的快速排序,除了对普通的快速排序进行优化,它还结合了插入排序和堆排序。根据不同的数量级别以及不同情况,能自动选用合适的排序方法。当数据量较大时采用快速排序,分段递归。一旦分段后的数据量小于某个阀值,为避免递归调用带来过大的额外负荷,便会改用插入排序。而如果递归层次过深,有出现最坏情况的倾向,还会改用堆排序。
不加限定词,默认是private继承
区分覆盖和隐藏:
覆盖指的是子类覆盖父类函数(被覆盖),特征是:
1.分别位于子类和父类中
2.函数名字与参数都相同
3.父类的函数是虚函数(virtual)
隐藏指的是子类隐藏了父类的函数(还存在),具有以下特征:
1.子类的函数与父类的名称相同,但是参数不同,父类函数被隐藏
2.子类函数与父类函数的名称相同,参数也相同,但是父类函数没有virtual,父类函数被隐藏
判别流程如下图:
构造函数不能是虚函数,析构函数可以是虚函数且推荐最好设置为虚函数。
不可以。假设有一个vector,析构第一个元素抛出异常,析构第二个元素又抛出异常,两个同时作用的异常会导致程序提前结束或者导致不明确的行为。
C++使用虚函数机制来实现多态,虚函数是通过一张虚函数表来实现的,C语言可以利用函数指针来创建虚函数表,从而实现多态。
补充:
dynamic_cast < type-id > ( expression)
该运算符把expression转换成type-id类型的对象。Type-id可以是类的指针、类的引用或者void*。如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。将一个基类对象指针(或引用)cast到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理, 即会作出一定的判断。dynamic_cast在将父类cast到子类时,父类必须要有虚函数,否则编译器会报错。
单一对象和数组的内存布局不一样,数组需要记录数组大小。delete数组一部分会导致未定义行为。(VS2019编译通过,运行未报错,但编辑器会提示用法出错)
常见优化比如:
RVO:函数返回的对象如果是新构造的值类型就直接通过一个引用作为参数来构造,进而避免创建一个临时的“temp”对象。
NRVO:相比RVO进一步优化。对于RVO,如果函数在返回前创建了一个临时变量,这个临时变量还是会被构造的,参考下面代码
Point3d Factory() { Point3d po(1,2, 3); return po; } //RVO优化后 void Factory(Point3d &_result) { Point3d po(1,2,3); _result.Point3d::Point3d(po); return; } //NRVO优化后 void Factory(Point3d &_result) { _result.Point3d::Point3d(1, 2, 3); return; }
NRVO则直接跳过临时对象的构造。
预处理——编译——汇编——链接。预处理器先处理各种宏定义,然后交给编译器;编译器编译成.s为后缀的汇编代码;汇编代码再通过汇编器形成.o为后缀的机器码(二进制);最后通过链接器将一个个目标文件(库文件)链接成一个完整的可执行程序(或者静态库、动态库)。
参考 单例模式VS静态方法
参考 工厂设计模式有什么用?
简单总结一下就是类只有成员变量占用内存(静态成员不占类内部内存,函数不占内存)。如果有虚函数,每个类对象都会有一个虚函数指针Vptr(占用一个指针大小的内存),vptr指向一个虚函数表,表里面记录了各项标记virtual的函数,子类如果覆盖父类虚函数,对应虚表位置的虚函数会被子类的替换(虚表在运行时其位置与大小就被决定了,一个类只有一个虚表),详细参考C++继承内存对象模型
把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。static局部变量只被初始化一次,下一次依据上一次结果值。对于函数来讲,static的作用仅限于隐藏(改变了作用域)。
使用内联函数可以避免函数调用的开销,缺点是直接导致可执行程序变大。三种情况编译器会拒绝inline:虚函数、带有循环或者递归的调用、通过函数指针调用inline函数。
C/C++中内存分5大区:栈,堆,全局/静态存储区,常量存储区,代码区。
- 栈(stack):指那些由编译器在需要的时候分配,不需要时⾃动清除的变量所在的存储区,效率高,分配的内存空间有限,形参和局部变量分配在栈区,栈是向地地址生长的数据结构,是一块连续的内存
- 堆(heap):由程序员控制内存的分配和释放的存储区,是向高地址生长的数据结构,是不连续的存储空间,堆的分配(malloc)和释放(free)有程序员控制,容易造成二次删除和内存泄漏
- 静态存储区(static):存放全局变量和静态变量的存储区,初始化的变量放在初始化区,未初始化的变量放在未初始化区。在程序结束后释放这块空间
- 常量存储区(const):存放常量字符串的存储区,只能读不能写,const修饰的局部变量存储在常量区(取决于编译器),const修饰的局部变量在栈区
- 程序代码区:存放源程序二进制代码
以上内容整理自网络,仅供参考,接下来是《游戏引擎架构》P115更靠谱的总结,本问题和“进程的数据段”、“可执行文件的数据段”都是一样的:
总结起来就是 “堆、栈、静态、常量、代码”:
内存对齐的主要作用是:
当使用一个容器的insert或者erase函数通过迭代器插入或删除元素"可能"会导致迭代器失效,因此我们为了避免危险,应该获取insert或者erase返回的迭代器,以便用重新获取的新的有效的迭代器进行正确的操作
①
第一种写法效率更高,主要是Cache机制导致的,参考C/C++遍历二维数组,列优先(column-major)比行优先(row-major)慢,why?
参考这篇博客整理一下《C++ Primer》和《Effective C++》
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。
参考extern “C”的作用详解
反射的概念:反射是.NET中的重要机制,通过反射,可以在运行时获得程序或程序集中每一个类型(包括类、结构、委托、接口和枚举等)的成员和成员的信息。有了反射,即可对每一个类型了如指掌。另外我还可以直接创建对象,即使这个对象的类型在编译时还不知道。
在C#中,委托(delegate)是一种引用类型,在其他语言中,与委托最接近的是函数指针,但委托不仅存储对方法入口点的引用,还存储对用于调用方法的对象实例的引用。简单的讲委托(delegate)是一种类型安全的函数指针。C/C++函数指针是通过寻找函数的入口来调用一个函数,C#委托是把函数名当做一个参数传入一个委托对象当中,委托是类型,函数指针是指针。
闭包是指有权访问另一个函数作用域中的变量的函数。注意,闭包这个词本身指的是一种函数。而创建这种特殊函数的一种常见方式是在一个函数中创建另一个函数。
union U
{
char s[9];
int n;
double d;
};
以上联合体所占空间大小为16字节。
参考关于C语言中联合体union占用内存的情况
在进行动态内存分配时,例如malloc()函数或者其他高级语言中的new关键字,操作系统会在硬盘中创建或申请一段虚拟内存空间,并更新到页表(分配一个PTE,使该PTE指向硬盘上这个新创建的虚拟页)。
参考 虚拟内存的那些事儿
i++ 与 ++i 的主要区别有两个:
i++ 返回原来的值,++i 返回加1后的值。
i++ 不能作为左值,而++i 可以。
int i = 5;
int *p1 = &(++i); //正确,++i可通过取地址&运算符获得内存地址
int *p2 = &(i++); //错误,i++不可通过取地址&运算符获得内存地址
++i = 1; //正确,++i可作为左值
i++ = 5; //错误,i++不可作为左值
前缀形式:
int& int::operator++() //这里返回的是一个引用形式,就是说函数返回值也可以作为一个左值使用。
{//函数本身无参,意味着是在自身空间内增加1的
*this += 1; // 增加
return *this; // 取回值
}
后缀形式:
const int int::operator++(int) //函数返回值是一个非左值型的,与前缀形式的差别所在。
{//函数带参,说明有另外的空间开辟
int oldValue = *this; // 取回值
++(*this); // 增加
return oldValue; // 返回被取回的值
}
//如上所示,i++ 最后返回的是一个临时变量,而临时变量是右值。
类型 | 32位操作系统 | 64位操作系统 |
---|---|---|
char | 1 | 1 |
int | 4 | 4 |
long | 4 | 4 |
float | 4 | 4 |
double | 8 | 8 |
char* | 4 | 8 |
不可以。参考《Effective C++》第9条款。
野指针:野指针就是没有被初始化过的指针。
悬空指针:悬空指针是指针最初指向的内存已经被释放了的一种指针。
重载的必要性:C++标准库提供的new和delete操作符,是一个通用实现,未针对具体对象做具体分析
存在分配器速度慢、小型对象空间浪费严重等问题,不适用于对效率和内存有限制的应用场景
重载的好处:
参数要求:参数个数可以任意,只需保证第一个参数为size_t类型,饭后void *类型即可,其他操作符重载时参数需保持严格一致
用2是出于对空间和时间的权衡。简单来说, 空间分配的多,平摊时间复杂度低,但浪费空间也多。但vector扩容不一定是2倍,用2倍导致之前的空间无法使用,cache不友好,但平摊时间复杂度低。参考
C++ STL 中 vector 内存用尽后, 为什么每次是 2 倍的增长, 而不是 3 倍或其他值?
栈内存由编译器分配和释放,堆内存由程序员手动申请和释放。
函数里的变量属于栈内存。
简单来说就是编译器只有遇到模板调用的时候才会使用模板参数去生成模板类或者模板函数的定义,这就要求此时编译器可以访问到模板的定义。如果模板声明在A.h而实现在A.cpp,那么在B.cpp使用一个具体类型定义模板类或者模板函数的时候,此时编译器正在编译b单元,无法访问到A单元中的实现。 参考 模板 第35.12问答。
当模板的声明头文件中包含全局变量时,为了解决此头文件被多次包含后导致的重定义问题,需要把模板声明和定义分开写。
通过在cpp中显示实例化模板类或模板函数并在h文件中添加实例化的模板声明,可以实现模板的声明和定义分离。参考 一种将C++函数模板定义和声明分开的方法
模板特化可以只让特定类型使用函数模板。
通过全特化一个模板,可以对一个特定参数集合自定义当前模板,类模板和函数模板都可以全特化。(这样就使得只有这个参数集合类型才能使用这个模板了)
类似于全特化,偏特化也是为了给自定义一个参数集合的模板,但偏特化后的模板需要进一步的实例化才能形成确定的签名。
值得注意的是函数模板不允许偏特化,多数情况下函数模板重载就可以完成函数偏特化的需要。
参考 C++模板的偏特化与全特化
委托可绑定到任何的c++可调用对象, 全局函数(静态成员函数), 类的非静态成员函数,Lambda 以及 按Name 绑定到UFunction.尔后可在恰当的时机调用它。
由于不同的可调用对象的调用方式差别巨大,所以我们需要把这些可调用对象统一成一个东西, 即Delegate, 不同的Delegate之间仅仅是所代表的可调用对象的参数和返回值类型不同。而具体的调用方式, 则根据你绑定的可调用对象的形式而定。
(个人理解:就是实现了一个类,把全局函数,类的非静态成员函数,lambda等的函数指针调用统一了起来。这些不同函数的委托实现原理不一样,比如全局函数只需要使用简单的函数指针,但类的非静态成员函数则需要结合成员函数指针这个特性去实现)
参考 UE4-深入委托Delegate实现原理
参考 UE 反射实现分析:C++ 特性
参考 UE 反射实现分析:基础概念
扫描solution中的模块和插件
判断哪些模块需要重写编译
调用UHT去解析C++头文件
根据.Build.cs 和 .Target.cs创建编译器和链接器选项
执行各平台特定的编译器
解析包含UClass的C++头文件
为UClass和UFunction生成胶水代码
在Intermediates文件夹中存储生成的文件
参考 Build flow of the Unreal Engine4 project
UCLASS/USTRUCT/UFUNCTION/UPROPERTY 等可以在 () 中添加很多的标记值以及 meta 参数,用于指导 UHT 来生成对应的反射代码。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。