1. 内存泄漏 & 内存溢出
参考:https://blog.csdn.net/buutterfly/article/details/6617375
memory leak, out of memory
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出.
以发生的方式来分类,内存泄漏可以分为4类:
1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
2.extern C的作用
参考:
https://baike.baidu.com/item/extern%20%22C%22/15267013
extern C的主要作用是为了能够正确实现C++代码调用其他C语言代码。加上extern C后,会指示编译器这部分代码按C语言进行编译,而不是C++。
实现C++与C及其他语言的混合编程。
由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中(称为:name mangling,例如函数 void foo(int x,int y)编译后的名字可能为:_foo_int_int,包含了函数名,函数参数数量以及类型信息),而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名(例如:void foo(int x,int y) 被C编译器编译后再符号库中的名字为_foo)。
name mangling:https://cloud.tencent.com/developer/article/1005044
3.malloc和new,free和delete的区别
参考:https://zhuanlan.zhihu.com/p/72333166
1. malloc和free是库函数,而new和delete是C++操作符;
2. new自己计算需要的空间大小,例如:int* a = new int(2),malloc要指定大小,例如:int* a = malloc(sizeof(int));
3.new在动态分配内存的时候可以初始化对象,调用其构造函数,delete在释放内存时调用对象的析构函数。而malloc只分配一段给定大小的内存,并返回该内存首地址指针,如果失败则返回NULL;
4. new是C++操作符,是关键字,而operator new是C++库函数;
5. operator new/operator delete可以重载,而malloc不行;
6. new可以调用malloc来实现,但是malloc不能调用new来实现;
7.对于数组,C++定义了 new[] 用于进行动态数据分配,用 delete[] 进行数组销毁。new[] 会一次分配内存,然后多次调用对象的构造函数;delete[] 会先调用多次对象的析构函数,然后一次性释放内存空间;
例如:
char* p = new char[100]; char* p = malloc((char)*100;
8. malloc能够直观地重新分配内存
使用malloc分配内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针指向的内存是否有足够的连续空间。如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果不够,先按照新制定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。
new没有这样直观的配套设施来扩充内存。
4. 指针和引用的区别
引用,与指针类似,存储着一个指向内存中某对象的地址。可以认为引用是变量的一个别名。
- 与指针不同的是,引用被初始化后不能再指向另一个对象,或设置为null;
- 指针可以指向另外一个指针,但是引用只能指向对象;
- 指针可以存储在数组里,但是引用不能。
- 引用保存对象的地址,且行为类似于一个对象。
引用的规则:
引用被创建时必须被初始化,指针则可以在任何时候被初始化
不能有null引用,引用必须与合法的存储单元关联(因此如果函数参数为引用,则不用检查该参数是否为nullptr)
一旦引用被初始化,就不能改变引用的关系;指针则可以随时改变所指对象;
引用主要作用:传递函数的参数、返回值
C++中函数参数和返回值的传递方式有三种:值传递、指针传递、引用传递。
指针传递 & 引用传递
指针传递:
对于如下的代码:
void func2(int* p) { cout << "address of p0 in func: " << &p << endl; cout << "address p in func:" << p << endl; cout << "value p in func:" << *p << endl; cout << endl; p = new int(33); cout << "address of p0 in func2: " << &p << endl; cout << "address p in func2:" << p << endl; cout << "value p in func2:" << *p << endl; return; }
int main(int argc, char* argv[]) { int x = 44; int* p = &x; cout << "address of p0: " << &p << endl; cout << "address of p: " << p << endl; cout << "value of p: " << *p << endl; cout << endl; func2(p); cout << endl; cout << "address p0 after func:" << &p << endl; cout << "address p after func:" << p << endl; cout << "value p after func:" << *p << endl; }
上述代码的打印结果为:
在 void func2(int* p)中,参数为一个指针,可以看出在函数内外,p的值是相同的,都是0099FED8,且指向的值也相同,都是44,但是对p取地址(&p)发现两者的值并不相同,一个是0099FEC8,另一个是0099FEC0,可以通过如下图解释,指针传递相当于值传递,只是传递了一个地址拷贝,但是函数内部的指针是一个临时变量,只是其指向的地址与外部相同,通过该地址可以操作指向的值,即例子中的44。
但是如果在函数内部,修改了这个指针指向的位置,并不会影响函数外的指针,例子中,在func2中,p = new int(33);通过new的形式将该指针指向的地址进行了改变,但是打印结果表明并没有改变函数外指针指向的地址。
可以通过如下图解释,红色表示通过p = new int(33);后函数内部指针的修改,函数内部局部变量指针指向的地址发生了改变。
引用传递:
如下代码:
void func(int& p) { cout << "ref p address:" << &p << endl; cout << "ref p val:" << p << endl; } //调用处 //引用传递 cout << "ref test" << endl; int& refP = x; cout << "ref p address:" << &refP << endl; cout << "ref p val:" << refP << endl; func(refP);
打印结果如下:
参考:https://blog.csdn.net/luoshenfu001/article/details/8601494
在C++中,指针和引用经常用于函数的参数传递,然而,指针传递参数和引用传递参数是有本质上的不同的:
指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址(指针传递参数时,指针中存放的也是实参的地址,但是在被调函数内部指针存放的内容可以被改变,即可能改变指向的实参,所以并不安全,而引用则不同,它引用的对象的地址一旦赋予,则不能改变)。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。 即指针传递只是传了一个地址copy, 在函数内部改变形参所指向的地址,不能改变原实参指向的地址,仅可以通过修改形参地址的内容,来达到修改实参内容的目的(原C语言中的通过指针来互换值小函数例子),所以如果想通过被调函数来修改原实参的地址或给重新分配一个对象都是不能完成的,只能使用双指针或指针引用(下面会进行详解)。
为了进一步加深大家对指针和引用的区别,下面我从编译的角度来阐述它们之间的区别:
程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
最后,总结一下指针和引用的相同点和不同点:
★相同点:
1.都是地址的概念;
2.指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。
★不同点:
1.指针是一个实体,而引用仅是个别名;
2.引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
3.引用没有const,指针有const,const的指针不可变;(具体指没有int& const a这种形式,而const int& a是有 的, 前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)
4.引用不能为空,指针可以为空;
5.“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
6.指针和引用的自增(++)运算意义不一样;
7.引用是类型安全的,而指针不是 (引用比指针多了类型检查
指针的指针&引用的指针
todo
5.常用的容器
参考:https://zhuanlan.zhihu.com/p/163305376
vector
可以当做数组使用,区别在于空间运用特灵活,数组是静态空间,vector是动态空间,随着使用动态增长大小。
查找复杂度O(1),插入和删除复杂度O(n),尾部插入删除O(1)
主要用于存储数据,查找记录,修改少。
list
链表,底层实现为双向链表,查找O(n),插入删除O(1)
主要用于插入删除较多情况。
stack
栈,先进后出,只能操作栈顶,在一端操作数据,不能操作中间数据。
queue/priority_queue
队列,FIFO,先进先出,在两端操作数据,只能操作头尾数据。
set/unordered_set
集合,一般用于记录某数是否出现过,一个集合中一个数只能出现一次,出现多次需要使用multiset
如果在set中存储自定义对象,需要重载operator== 以通过自己写的函数来判断对象是否相等。
注意:
operator= ,重载赋值操作符,即用=等号实现对象的赋值
operator==,该操作符用于判断==左右两侧的对象是否相等 ,重载后,可用于find函数的查找,对象放到set时判断是否相等
https://www.cplusplus.com/doc/tutorial/operators/
unordered_set:哈希集合,C++11标准,带hash特性,因此查找时间复杂度为:O(1)
map/unordered_map
map由key value组成,并根据key内部进行了排序。一个key只能对于一个value,如果需要对应多个值则使用multimap
哈希表,查找速度较快,O(1)
https://zhuanlan.zhihu.com/p/48066839
map:
基于红黑树实现,红黑树是自平衡二叉树,保障了良好的最坏情况运行时间,它可以做到在O(logn)时间内完成查找、插入、删除,在对单词时间敏感的场景下建议使用map作为容器。
红黑树是一种二叉查找树,二叉查找树的一个重要形式是有序,且中序遍历时取出的元素是有序的,在对一些需要用到有序性的场景中,使用map。
unordered_map:
基于hash_table实现,一般是由一个大vector,vector元素节点可挂接链表来解决冲突来实现。hash_table最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。
值得注意的是,在使用unordered_map设置合适的hash方法,可以获得良好的性能。
map在增删查三项上均弱于unordered_map,内存使用map略少,但不明显。
在有序性或者对单词查询有时间要求的应用场景下,使用map;其他情况使用unordered_map。
string
字符串
容器相关内容总结如下:
常用数据结构,总结如下:
6. volatile关键字的作用
参考:https://zhuanlan.zhihu.com/p/62060524
volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,例如操作系统、硬件或其他线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不能再进行优化,从而可以提供对特殊地址的稳定访问。
语法:
volatile 数据类型 变量名
当要求使用volatile声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它签名的指令刚刚从该处读取过数据。
volatile int i=10; int a = i; int b = i;
volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。
使用:
1) 中断服务程序中修改的供其它程序检测的变量需要加volatile;
2) 多任务环境下各任务间共享的标志应该加volatile;
3) 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
7. 强制类型转换及使用场景
参考:https://www.cnblogs.com/chenyangchun/p/6795923.html
C风格:
- (type_id)expression
- type_id(expression)
C++
- 支持C风格的强转
- static_cast
- dynamic_cast
- const_cast
- reinterpret_cast
《Effective C++》中将c语言强制类型转换称为旧式转型,c++强制类型转换称为新式转型。
static_cast:
static_cast相当于传统的C语言里的强制转换,该运算符把expression转换为new_type类型,用来强迫隐式转换,例如non-const对象转为const对象,编译时检查,用于非多态的转换,可以转换指针及其他,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
dynamic_cast
格式:
dynamic_cast<type*>(e) dynamic_cast<type&>(e) dynamic_cast<type&&>(e)
const_cast
const_cast用于修改类型的const属性,或volatile属性。
该运算符用于修改类型的const(唯一有能力的C++类型转型操作符),或volatile属性。除了const和volatile修饰外,new_type和expression的类型是一样的。
1.常量指针被转化为非常量的指针,并且仍然指向原来的对象;
2.常量引用被转换为非常量引用,并且仍然指向原来的对象;
3.const_cast一般用于修改底指针(??),如const char *p形式。
const int g = 20; int *h = const_cast<int*>(&g);//去掉const常量const属性 const int g = 20; int &h = const_cast<int &>(g);//去掉const引用const属性 const char *g = "hello"; char *h = const_cast<char *>(g);//去掉const指针const属性
reinterpret_cast
new_type必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针,(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。
reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编辑器,这也就表示它不可移植。
IBM的C++指南、C++之父Bjarne Stroustrup的FAQ网页和MSDN的Visual C++也都指出:错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。
c++强制转换注意事项
- 新式转换较旧式转换更受欢迎。原因有二,一是新式转型较易辨别,能简化“找出类型系统在哪个地方被破坏”的过程;二是各转型动作的目标愈窄化,编译器愈能诊断出错误的运用。
- 尽量少使用转型操作,尤其是dynamic_cast,耗时较高,会导致性能的下降,尽量使用其他方法替代。
8.C++11新特性
看《深入理解C++11 C++11新特性解析与应用》
C++11是C++程序设计语言标准的一个新的版本,在2011年由ISO批准并发布。C++11新标准从而替代了原来的C++98,C++03,。C++11标准是对C++的一次巨大的改进和扩充,增加许多新功能,例如auto、deltype、nullptr等关键字,增加范围for循环,lambda表达式等。
auto关键字
在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型,类似C#中的var关键字
auto的自动推断发生在编译器,因此不会造成程序运行时效率的降低,必须在定义时初始化,因为需要自动推导
使用auto代替较长的变量类型,例如用于for循环中,例如容器的iterator等
注意:在for循环中,可以使用auto&以引用的方式迭代容器内容,这样减少对象的创建与销毁,提高效率;
void f(const vector& v) { for (auto x : v) cout << x << ‘n’; for (auto& x : v) ++x; // 使用引用,方便我们修改容器中的数据 }
decltype关键字
参考:https://www.cnblogs.com/QG-whz/p/4952980.html
decltype与auto关键字一样,用于进行编译时类型推导,不过它与auto还是有一些区别的。decltype的类型推导并不是像auto一样是从变量声明的初始化表达式获得变量的类型,而是总是以一个普通表达式作为参数,返回该表达式的类型,而且decltype并不会对表达式进行求值。
推导出表达式类型
int i = 4; decltype(i) a; //推导结果为int。a的类型为int。
与using/typedef合用,用于定义类型。
using size_t = decltype(sizeof(0));//sizeof(a)的返回值为size_t类型 using ptrdiff_t = decltype((int*)0 - (int*)0); using nullptr_t = decltype(nullptr); vector<int >vec; typedef decltype(vec.begin()) vectype; for (vectype i = vec.begin; i != vec.end(); i++) { //... }
这样和auto一样,也提高了代码的可读性。
重用匿名类型
在C++中,我们有时候会遇上一些匿名类型,如:
struct { int d ; doubel b; }anon_s;
而借助decltype,我们可以重新使用这个匿名的结构体:
decltype(anon_s) as ;//定义了一个上面匿名的结构体
泛型编程中结合auto,用于追踪函数的返回值类型
这也是decltype最大的用途了。
template <typename _Tx, typename _Ty> auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty) { return x*y; }
nullptr 字面值
nullptr是C++11中专门用来表示空指针的,代替原有的NULL。
NULL is a normal macro that expands in 0 which has int type.
1.NULL可以与其他int类型进行比较
2.如果有两个重载函数,一个接受指针,一个接受int,如果传入NULL则会调用接受int的函数,导致歧义;
nullptr has its own type nullptr_t. 而nullptr则有自己的类型:nullptr_t,nullptr不能被视为任何数值类型。
This is why we cannot use NULL(and more so 0) in modern C++.
constexpr关键字
The constexpr
specifier declares that it is possible to evaluate the value of the function or variable at compile time. Such variables and functions can then be used where only compile time constant expressions are allowed (provided that appropriate function arguments are given). A constexpr specifier used in an object declaration or non-static member function (until C++14) implies const. A constexpr specifier used in a function or static member variable (since C++17) declaration implies inline. If any declaration of a function or function template has a constexpr
specifier, then every declaration must contain that specifier.
constexpr说明符用于表明函数或变量的值可以在编译期计算。这样的变量和函数
常量表达式的基本理念是:将特定的计算放到编译期(compile time)完成,而不是运行期,这样可以提高程序运行速度:如果在编译期完成,则该计算只需要完成一次;如果在运行期,则每次运行都要计算一次。
constexpr 与 const对比:
如果将一个对象/函数声明为constexpr,则该对象/函数同样是const的,可对constexpr传入const参数;但反过来并不成立,声明一个对象/函数为const并不代表该对象/成员是constexpr。
参考:https://www.zhihu.com/question/35614219
语义上:
constexpr:告诉编译器我可以是编译期间可知的,尽情的优化我吧。
const:告诉程序员没人动得了我,放心的把我传出去;或者放心的把变量交给我,我啥也不动就瞅瞅。
语法上:
constexpr是一种比const 更严格的束缚, 它修饰的表达式本身在编译期间可知, 并且编译器会尽可能的 evaluate at compile time. 在constexpr 出现之前, 可以在编译期初始化的const都是implicit constexpr. 直到c++ 11, constexpr才从const中细分出来成为一个关键字, 而 const从1983年 c++ 刚改名的时候就存在了... 如果你初学c++, 应当尽可能的, 合理的使用constexpr来帮助编译器优化代码.
在 C++11 以后,建议凡是「常量」语义的场景都使用 constexpr,只对「只读」语义使用 const。
range-based for loop
https://en.cppreference.com/w/cpp/language/range-for
Executes a for loop over a range.
Used as a more readable equivalent to the traditional for loop operating over a range of values, such as all elements in a container.
与传统的for循环等价,但是代码的可读性更高,用于在一定范围内的值进行操作,例如容器。
格式:
for(element _declaration : array)
statement;
1.在for-loop中使用auto关键字,让C++自动推断容器中元素的类型,让代码更简洁。
2.for-loop与引用:
在大多数的for循环中,并不会改变容器中的值,这种情况下使用引用以提高效率:
上面第一种方式,每个元素的处理都会通过拷贝构造函数产生一个临时变量,浪费;而第二种通过引用访问元素,不会产生元素的复制/赋值,效率较高。
如果只是想遍历元素,声明为const引用:
使用:
固定长度的数组
vector list tree等
不能用于指针指向的数组,因为无法知道数组的大小,
lambda表达式
看自己博客
智能指针
看另外的一篇:https://www.cnblogs.com/zyk1113/p/13474149.html
多线程库
<thread>等
右值引用
9.C++三大特性
面向对象三大特征:封装、继承、多态
看:https://blog.csdn.net/michael019/article/details/48844945
10.C++多态的实现原理
《深入探索C++对象模型》
在C++中,多态polymorphism表示“以一个public base class的指针(或引用 reference),寻址出一个derived class object”。
https://www.cnblogs.com/cxq0017/p/6074247.html
https://zhuanlan.zhihu.com/p/104605966
多态解释:https://www.tutorialspoint.com/cplusplus/cpp_polymorphism.htm#:~:text=C%2B%2B%20polymorphism%20means%20that%20a%20call%20to%20a,has%20been%20derived%20by%20other%20two%20classes%20%E2%88%92
The word polymorphism means having many forms. Typically, polymorphism occurs when there is a hierarchy of classes and they are related by inheritance.
多态这个单词的含义是有多重形式。典型地讲,多态发生在类的继承关系、且通过继承关联起来的类中。
C++ polymorphism means that a call to a member function will cause a different function to be executed epending on the type of object that invokes the function.
C++多态指的是调用成员函数会根据调用函数的对象的类型的不同而执行不同的函数。
C++多态用一句话概括为:在基类的函数前加上virtual关键字,在派生类中重写override该函数,运行时将根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数;
1. 用virtual关键字声明的函数叫虚函数 virtual function,虚函数肯定是类的成员函数;
2.存在虚函数的类都有一个一维的虚函数表叫做虚表 virtual table,类的对象有一个指向虚表开始的虚指针;虚表和类是对应的,虚表指针和对象是对应的,(即:定义一个类,在运行时不管创建了多少对象,这个类的虚表只有一个;但是每个对象都有一个指向该类虚表的指针)
3.多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性;
4.多态用虚函数来实现,结合动态绑定;
5.纯虚函数是虚函数加上 =0;(即:在声明该函数时,在最后加上=0,例如: virtual void fun() =0; 即声明为纯虚函数,声明为纯虚函数的类不能直接创建,类中如果有纯虚函数,则成为抽象类)
6.抽象类是指包含至少一个纯虚函数的类;
虚表的创建和销毁时机:
调用类的构造函数、析构函数时,进行虚表的创建/销毁、虚表指针的初始化/销毁。
《深入探索C++对象模型》:vptr虚表指针的设定(setting)和重置(resetting)都由每一个class的构造函数、析构函数和copy assignment运算符自动完成,每一个class所关联的type_info_ojbect(用以支持runtime type identification,RTTI)也经由virtual table 被指出来,通常放在表格的第一个slot处。
《深入探索C++对象模型》中对多态的说明如下:
C++以下列方法支持多态:
1.经由一组隐含的转化操作,例如把一个derived class指针转化为一个指向其 public base type的指针:
shape *ps = new circle();
2.经由virtual function机制:
ps->rotate();
3.经由dynamic_cast和typeid运算符:
if(circle *pc = dynamic_cast<circle*>(ps)) ...
多态的主要用途是经由一个共同的及饿哦库来影响类型的封装,这个借口通常被定义在一个抽象的base class中,例如Library_materials class就为Book、Video、Puppet等subtype定义了一个接口。这个共享接口是以 virtual function 机制引发的,它可以在执行期根据object的真正类型解析出到底是哪一个函数实体被调用,经由这样的操作:
Library_material->check_out();
我们的代码可以避免由于“借助某一特定library的materials”而导致变动无常,这不止使得“当类型有所增加、修改、或删减时,我们的代码不需要改变”,而且也使一个新的Library_materials subtype的供应者不需要重新洗出“对继承体系中的所有类型都共通”的行为和操作。
11.什么是虚函数 & 什么是纯虚函数
https://blog.csdn.net/hackbuteer1/article/details/7558868
https://www.zhihu.com/topic/20029040/top-answers
https://docs.microsoft.com/en-us/cpp/cpp/virtual-functions?view=msvc-160
https://www.geeksforgeeks.org/virtual-function-cpp/
12. 虚表指针的大小 & 虚函数表的存放内容
参考:https://blog.twofei.com/496/ C++虚函数(表)实现机制
虚表指针大小:在32位机器上是32位,4个byte,
《深入探索C++对象模型》中:
存放的内容:
当一个类继承一个含有virtual function的父类后,其virtual table发生的变化有如下三种可能:
14. 一个空类会生成哪些函数
《Effective C++》条款5:了解C++默默编写并调用哪些函数
当你声明一个空类,编译器会为它声明一个copy构造函数、一个copy assignment操作符(赋值操作符=)、和一个析构函数;如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数,所有这些函数都是public且inline的。
例如,你写下如下代码:
class Empty{};
等价于你写下如下代码:
class Empty { public: Empty(){} //deafult构造函数 Empty(const Empty& rhs){} //copy构造函数 ~Empty(){} //析构函数 Empty& operator=(const Empty& rhs){} //copy assignment操作符 };
如果你不想要一个类有某个函数,例如拷贝构造函数等,在C++11中可以显示指定:delete
15. 构造函数可以是虚函数吗? & 析构函数可以是虚函数嘛
参考:https://blog.csdn.net/qq_28584889/article/details/88749862
构造函数不可以是虚函数,析构函数可以是虚函数
构造函数:
1. 从vptr角度解释
虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的vptr指针指向,该指针存放在对象的内部空间中,需要调用构造函数完成初始化。如果构造函数是虚函数,那么调用构造函数就需要去找vptr,但此时vptr还没有初始化,所有构造函数不能是虚函数。
2. 从多态角度解释
虚函数主要是实现多态,在运行时才可以明确调用对象,根据传入的对象类型来调用函数,例如通过父类的指针或引用来调用它的时候可以调用子类override的成员函数(如果该指针、引用指向的是子类对象)。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针、引用去调用。那么使用虚函数也么有实际意义。
在调用构造函数时还不能确定对象的真实类型(由于子类会调用父类的构造函数),并且构造函数的作用是提供初始化,在对象的声明周期中只运行一次,不是对象的多态行为,没有必要成为虚函数。
析构函数
析构函数可以是虚函数,且父类的虚构函数必须是虚函数,因为在子类调用析构函数时,如果其析构函数不是虚函数,则只会调用自己的析构函数,不会调用其他继承相关类的析构函数,这样会导致父类持有的一些资源没有机会被释放,导致内存泄漏。
此时vtable已经初始化了,可以把析构函数放在虚函数表中调用。
例如:
SubClass* pObj = new SubClass(); delete pObj;
使用类自己的指针构造对象,在delete释放指针的时候都会调用自己的析构函数,不会产生问题(不管这个类的析构函数是不是virtual的)
如果使用父类的指针指向子类对象:
BaseClass* pObj = new SubClass(); delete pObj;
也就是多态中的写法,则父类、子类的析构函数必须是virtual的,不然delete pObj时只会调用BaseClass自己的析构函数,而不会调用子类的,这样如果子类持有了资源,就没有时机被释放,从而导致内存泄漏。
若析构函数是虚函数(即加上virtual关键词),delete时基类和子类都会被释放;
若析构函数不是虚函数(即不加virtual关键词),delete时只释放基类,不释放子类;
(1)基类的析构函数不是虚函数的话,删除指针时,只有基类的内存被释放,派生类的没有。这样就内存泄漏了。
(2)析构函数不是虚函数的话,直接按指针类型调用该类型的析构函数代码,因为指针类型是基类,所以直接调用基类析构函数代码。
(3)养成习惯:用于多态的基类的析构一定是virtual,有的基类并不是用于多态的可以不是虚析构函数,例如继承一个NonCopy父类,让子类不能进行拷贝,这样的情况不需要析构函数为virtual。
16.左值和右值
https://www.cnblogs.com/catch/p/3500678.html
https://blog.csdn.net/dbzhang800/article/details/6663353
https://www.zhihu.com/question/26203703
英文:
https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c#:~:text=In%20C%2B%2B%20an%20lvalue%20is%20something%20that%20points,a%20longer%20life%20since%20they%20exist%20as%20variables.
左值是可以取地址的变量,左值与右值的区别在于能否获取内存地址 。通常,右值时临时性的且声明周期较短,而左值的声明周期更长,因为它以变量的形式存在。临时变量(右值)生命周期:临时对象应该在完整表达式结束时销毁;常量左值引用会延长临时变量的生命。
在赋值操作符=左侧必须是左值;&操作符后也需要左值;
左值转右值:
int x = 1; int y = 3; int z = x + y; // ok
在上面的代码中,x y都是左值,但是+操作符需要+两侧都是右值,这里x y发生了隐式转换,从左值转换为右值;类似的操作符还有:加法、减法、除法等。
右值转左值:
不能转换
左值引用 lvalue reference
17.什么是智能指针,有几种、作用、实现原理
自己的博客:https://www.cnblogs.com/zyk1113/p/13474149.html
18. unique_ptr中std::move()的作用
unique_ptr不共享它的指针,无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库STL算法,只能移动unique_ptr,这意味着,内存资源所有权将转移到另一unique_ptr,并且原来的unique_ptr不再拥有该资源。
参考:https://www.cnblogs.com/DswCnblog/p/5628195.html
19.如何避免循环依赖
参考:https://tlanyan.me/cpp-forward-declaration/
如果存在两个类A、B,A中的成员变量使用B,B中的成员变量使用A,则会产生循环依赖,编译时可能产生错误。例如:
class A { public: B b; }; class B { public: A a; };
前置声明解决该问题需要与指针配合,具体如下:
- 至少将某类的变量类型转换成指针,例如A中将B转成
B*
; - 类A中对B使用前置声明;
- 类A的定义文件中移除对类B文件的包含(做了包含保护则可忽略)。
使用前置声明后,以下是一种可行的解决形式(两个类均使用了前置声明):
// file: A.hpp //3. 移除对B的包含(使用了#pragma once或者#ifndef B_HPP等保护措施则无必要) // 2. 前置声明类B class B; class A { int id; // 1. 成员变量转换成指针 B* b; }; // file: B.hpp // 3. 移除对A的包含(有包含保护则非必要) // 2. 前置声明类A class B { ... // 1. 成员变量转换成指针 A* a; };
20.static关键字的使用
参考:
https://www.cnblogs.com/songdanzju/p/7422380.html
https://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777441.html
1. 隐藏:
所有未加static前缀的全局变量和函数都具有全局可见性,其他的源文件能访问到。如果加了static前缀,就会对其他源文件隐藏(其他源文件没法使用这个变量/函数)。这样就可以在不同的文件中定义同名函数和同名变量,而不用担心命名冲突。
2.保持变量内容的持久-static变量中的记忆功能和全局生存期 (https://blog.csdn.net/yangle4695/article/details/52153143)
存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一一次初始化。共两种变量存储在静态存储区:
全局变量
static变量
和全局变量相比,static变量可以控制变量的可见范围,如果static局部变量定义在函数内,则它的生存期为整个源程序,但是作用域与自动变量相同。
3. static的第三个作用是默认初始化
与全局变量相同,在静态数据区,内存中所有的字节默认值都是0x00,某些时刻可以用这一特点减少工作量,例如定义变量时不用初始化为零。
==》static的最主要功能为隐藏,其次因此static变量存放在静态存储区,所以具备持久性和默认值0.
4. C++中类成员声明static
类的静态成员函数属于整个类而不是类的对象,所以它没有this指针,这就导致了它仅能访问类的静态数据和静态成员函数。
不能将静态函数定义为虚函数。
静态数据成员初始化与一般成员初始化不同:初始化要在类体外部进行,而且前面不加static(即在声明时加static,在.cpp中初始化时前面不用加static)
21. const关键字使用
参考:https://blog.csdn.net/J_avaSmallWhite/article/details/111189714
22. define 与 const的区别
参考:https://blog.csdn.net/yingyujianmo/article/details/51206460
1.编译器处理方式
define-在预处理阶段进行替换,即将代码中定义的所有宏替换为其实际内容,注意定义时括号()的使用
const-在编译时确定其值
2.类型检查
define-无类型,不进行类型安全检查,可能会产生错误
const-有数据类型,编译时会进行类型检查
3.内存空间
define-不分配内存,代码中出现几次就进行几次替换,在内存中会存在多个拷贝,占用内存较大
const-在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝
4.效率
在编译时,编译器通常不为const常量分配存储空间,而是将它保存在符号表中,这使得它成为一个编译期间常量,没有了存储与读内存的操作,效率较高。
define可以定义与函数效果类似的宏定义,例如:#define add(x, y) (x + y)
5.作用范围
宏定义的作用范围仅限于当前文件
const对象只在文件内有效,如果想在其他文件间共享const对象,必须在变量定义前添加extern关键字(在声明和定义时都要加)。
23.面向对象设计的原则
参考:https://blog.csdn.net/qq_34760445/article/details/82931002
1.开闭原则 the Open Close Principle,OCP
对扩展开放,对修改关闭
在面向对象设计中,设计类或其他程序单位时:对扩展开放(open),对修改关闭(closed)
2. 里氏替换原则 Liskov Substitution Principle, LSP
所有引用基类的地方必须能够透明地使用其派生类的对象,即所有父类能出现的地方,其子类都能出现。
3. 迪米特原则-最少知道原则,Law of Demeter, LoD
又称为最少知道原则(Least Knowledge Principle)
a).一个软件实体应当尽可能地少与其他实体发生相互作用。(高内聚?)
b).每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
4. 单一职责原则(Single Responsibility Principle, SRP)
不要让一个类存在多个改变的理由
即:如果一个类需要改变,则改变它的理由永远只有一个,如果存在多个改变它的理由,就需要重新设计该类。
单一职责原则的核心含义:只能让一个类/接口/方法有且仅有一个职责。
5. 接口分隔原则(Interface Segregation Principle, ISP)
不要强迫用户去依赖那些他们不使用的接口。
使用多个专门的接口比使用单一的总接口要好。它包含2层含义:
a). 接口设计原则:接口设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口。
b). 接口的依赖(继承)原则:如果一个接口a继承另一个接口b,则接口a相当于继承了接口b的方法,那么继承了接口b后的接口a也应该遵循上述原则:不应该包含用户不使用的方法。反之,则说明接口a被b给污染了。应该重新设计它们的关系。
总之:接口分隔原则指导我们:
一个类对另一个类的依赖应该建立在最小接口上
建立单一接口,不要建立庞大臃肿的接口
尽量细化接口,接口中的方法尽量少
6. 依赖倒置原则(Dependency Inversion Principle, DIP)
a. 高层模块不应该依赖于底层模块,二者都应该依赖于抽象
b. 抽象不应该依赖于细节,细节应该依赖于抽象
c. 针对接口编程,不要针对实现编程
7. 组合/聚合复用原则(Composite / Aggregate Reuse Principle, CARP)
尽量使用组合、聚合,尽量不要使用类继承
在一个新对象中使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的,也就是说尽量使用合成和聚合,而不是直接通过类的继承达到复用的目的。
组合、聚合:类之间的关系,
聚合:整体和部分的关系,且部分可以离开整体而存在,UML图如下:
组合:整体与部分的关系,且整体与部分是共生命周期的,部分不能离开整体而存在,UML图如下:
=========================================数据结构部分==============================
1. vector扩容原理
参考:https://blog.csdn.net/yangshiziping/article/details/52550291
扩容原理概述
- 新增元素:Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素;
- 对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 ;
- 初始时刻vector的capacity为0,塞入第一个元素后capacity增加为1;
- 不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。
2. vector list区别
list:
由双向链表实现,内存空间不连续。
优点:插入、删除效率较高,只需要在插入的前后两个节点更改指针即可,不用移动已有的数据;
缺点:list查询效率较低,时间复杂度为O(n)
vector:
拥有一段连续的内存,且起始地址不变,与数组类似。
优点:方便随机访问,时间复杂度为O(1)
缺点:由于内存是连续的,在插入、删除时可能会造成内存块的拷贝(例如插入后的size大于capacity时,需要对内存进行扩容;如果在中间位置插入,则该位置及后面的元素都要向后移动一个位置),时间复杂度为O(n)
3.map底层实现
map底层通过红黑树实现((Red-Black Tree),是一种高效的平衡检索二叉树。红黑树的统计性能要好于一般的平衡二叉树,所以被STL选为了关联容器的内部结构。
map是基于红黑树实现。红黑树作为一种自平衡二叉树,保障了良好的最坏情况运行时间,即它可以做到在O(log n)时间内完成查找,插入和删除,在对单次时间敏感的场景下比较建议使用map做为容器。比如实时应用,可以保证最坏情况的运行时间也在预期之内。
为什么map和set的插入、删除效率要比其他序列容器高?
对于关联容器而言,不需要内存拷贝和内存移动。map和set容器内的所有元素都是以节点方式存储的,其节点和链表类似,指向父节点、子节点。在插入时候只需要稍作变换,把节点的指针指向新的节点就可以了;删除类似,这里涉及了指针的转换,而没有涉及内存的移动。
参考:
https://blog.csdn.net/lym940928/article/details/88377649
https://blog.csdn.net/villasy1990/article/details/8270004
4. map unordered_map区别
map-红黑树实现,红黑树的内存占用比哈希表高,红黑树存储是有序的,则map中key的顺序也是有序的,存取的时间复杂度为O(logn)
unordered_map:哈希表实现,key的顺序是无序的,存取的时间复杂度为O(1)
unordered_map是基于hash_table实现,一般是由一个大vector,vector元素节点可挂接链表来解决冲突来实现。hash_table最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。
map的插入、删除效率较高,而unordered_map的查询效率较高?
参考:https://zhuanlan.zhihu.com/p/48066839
5.如何计算循环链表的长度
使用双指针,即快慢指针,来判断一个链表是否存在环,确定环的位置,以及循环链表中环的长度。
力扣题目:
参考:https://zhuanlan.zhihu.com/p/95747836
思路:
类似于龟兔赛跑,两个链表上的指针从同一节点出发,其中一个指针前进速度是另一个指针的两倍。利用快慢指针可以用来解决某些算法问题,比如
- 计算链表的中点:快慢指针从头节点出发,每轮迭代中,快指针向前移动两个节点,慢指针向前移动一个节点,最终当快指针到达终点的时候,慢指针刚好在中间的节点。
- 判断链表是否有环:如果链表中存在环,则在链表上不断前进的指针会一直在环里绕圈子,且不能知道链表是否有环。使用快慢指针,当链表中存在环时,两个指针最终会在环中相遇。
- 判断链表中环的起点:当我们判断出链表中存在环,并且知道了两个指针相遇的节点,我们可以让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。
- 求链表中环的长度:只要相遇后一个不动,另一个前进直到相遇算一下走了多少步就好了
- 求链表倒数第k个元素:先让其中一个指针向前走k步,接着两个指针以同样的速度一起向前进,直到前面的指针走到尽头了,则后面的指针即为倒数第k个元素。(严格来说应该叫先后指针而非快慢指针)
6. 二叉搜索树、平衡二叉树,红黑树的区别
参考:https://blog.csdn.net/qq_25940921/article/details/82183093
二叉搜索树:
又称二叉查找树、二叉排序树,为空树,或具有如下性质的二叉树:
a.若某节点的左子树不为空,则左子树上的所有节点的值小于它的根节点的值;
b.若某节点的右子树不为空,则右子树上的所有节点的值都大于它的根节点的值;
c.任意节点的左子树、右子树也分别为二叉查找树;
d.没有键值相等的节点;
二叉查找树按照中序遍历后,输出结果为有序的
二叉查找树的插入、查找的时间复杂度为:O(nlogn),最坏情况下可能为O(n),因为二叉查找树可能由于左右子树高度相差太大而退化为一个链表。
平衡二叉树-AVL树
为空树,或左右两个子树的高度相差不超过1,并且左右子树都是一个平衡二叉树。
普通的二叉查找树容易失去平衡,极端情况下,二叉查找树会退化为线性链表,导致插入和查找的时间复杂度为O(n).
在构建平衡二叉树过程中,如果新插入的结点破坏了平衡性,则需要通过旋转去改变树结构,以保持树的平衡性。(左旋、右旋)
缺点:
不管是插入、删除,只要不满足AVL性质,就需要旋转来保持平衡性,而旋转是很耗时的,因此AVL树适合于插入、删除次数比较少,查找多的情况。
红黑树:
参考:https://zhuanlan.zhihu.com/p/79980618
https://www.zhihu.com/question/19856999
Red Black Tree,红黑树的平衡要求不如AVL那么严格,相对于AVL树,牺牲了部分平衡性,以换取删除、插入操作时少量的旋转次数,整体来说性能优于AVL树。
增删改查:都是O(logn)
性质:
a.结点为红色或黑色
b.根节点为黑色
c.每个叶子节点都是黑色(叶子节点都是NULL节点,空)
d.每个红色节点的两个子节点都是黑色的,从每个叶子到根的所有路径上不能有两个连续的红色节点。
e.从任一节点到其每个叶子的所有路径中都包含相同数目的黑色节点;
红黑树的两个操作
recolor-重新标记黑色或红色
rotation-旋转,保持树的平衡性
参考:https://segmentfault.com/a/1190000012728513?utm_source=sf-related
7. STL的sort实现
参考:https://stackoverflow.com/questions/1717773/which-sorting-algorithm-is-used-by-stls-listsort
C++标准并没有规定特定的算法,只规定了使用的算法是稳定的,时间复杂度为nlogn。也就是说,可以使用归并排序(merge-sort),或链表版本的快速排序(quick sort)(与普通观点不同的是,快速排序不一定是不稳定的,即使数组的最常见快排是稳定的)。
在这一假设下,对于大多数当前标准库,std::sort使用的是intro-sort(introspective sort,内省式排序),该算法是一个可以追踪递归深度的快速排序算法,且如果递归调用深度过深则会切换为堆排序(堆排序通常较慢,但是能保证为O(nlogn))。Introsort发明于1990's晚期。较老的标准库通常使用快速排序。
stable_sort存在的原因是:对类似于数组的容器进行排序时,大多数最快的排序算法是不稳定的,因此标准库中同时提供了std::sort(快,但是不稳定) 和 std::stable_sort(稳定,但是通常速度较sort 慢)。
sort实现代码说明:
参考:
https://www.cnblogs.com/fengcc/p/5256337.html
https://www.cnblogs.com/AlvinZH/p/8682992.html
STL的sort并不只是普通的快速排序,除了对普通快速排序的优化外,还结合了插入排序和堆排序。根据不同的数量级以及不同的情况,能自动选择合适的排序方法。
8. 哈希表、哈希函数,如何解决冲突
参考:https://blog.csdn.net/u011240877/article/details/52940469
https://baike.baidu.com/item/%E5%93%88%E5%B8%8C%E8%A1%A8
=========================================操作系统部分==============================
1.互斥锁、自旋锁、读写锁、悲观锁、乐观锁
参考:https://blog.csdn.net/qq_34827674/article/details/108608566
总结如下:
2.进程、线程区别
3.进程状态转换
参考:https://blog.csdn.net/qq_34827674/category_10149766.html
进程状态转换图如下所示:
进程状态转换说明如下:
如果有大量处于阻塞状态的进程,进程可能会占用着物理内存空间,显然不是我们所希望的,毕竟物理内存空间是有限的,被阻塞状态的进程占用着物理内存就一种浪费物理内存的行为。
所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。
那么,就需要一个新的状态,来描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态。这跟阻塞状态是不一样,阻塞状态是等待某个事件的返回。
另外,挂起状态可以分为两种:
-
- 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
- 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行;
这两种挂起状态加上前面的五种状态,就变成了七种状态变迁,见如下图:
4.C/C++程序编译过程
参考:https://www.cnblogs.com/mickole/articles/3659112.html
5.静态链接 & 动态链接
参考:
https://www.cnblogs.com/cyyljw/p/10949660.html
https://cloud.tencent.com/developer/article/1531843
https://blog.csdn.net/smilesundream/article/details/74998316
总结如下:
6.进程间通信方式
7.什么是协程,什么情况下使用协程