C++基础特性:
变量及基本类型:
- 初始化和赋值的区别:初始化和赋值并不是一回事,初始化是对象定义后为其赋予一个初始值,但是赋值设计的操作是将变量原油的值抹去,并为其赋予新值
- 列表初始化,下边的几种初始化类型都满足要求,但是{}相关的两种初始化类型称为列表初始化,能够防止因为强制类型转换时导致的信息丢失
1 | int a = 0; |
- 建议为内置对象(不是程序员定义的)都赋初值,所有函数外定义的内置类型会自动初始化为0,函数内的内置类型如果无初始化的话,其值为未定义的类型
- 变量声明和定义的关系:
- 声明: 让一个变量的名字让其他模块或程序所知,如果想引用其他模块定义的对象,那么必须引用包含该对象声明的文件才行
- 如果仅想声明一个变量,那么必然不能初始化它,且需要在变量前添加关键字extern
1
2
3extern int i; //这是声明
externa int i = 0; //这不是声明,这是定义
/* 且若尝试在函数体内对extern标记的变量进行初始化,会报错 */ - 变量只能被定义一次,但是可以被声明多次
- 如果需要在多个文件中使用某个变量,那必须将某个变量的声明和定义分开,切其他需要使用该变量的地方都需要实现进行声明
引用
- 引用并非对象,只是一个已存在对象的别名而已,切后续不能重新切换绑定关系,所以引用必须初始化
- 引用不是对象,所以不能定义引用的引用,只能对对象定义引用
指针
指针和引用的区别:
1. 指针是对象;引用是绑定(别名),所以一般情况下指针是占内存空间的,但是引用不占(有些编译器底层用指针实现引用的话,就占)
2. 指针可以不初始化;引用必须初始化
3. 指针所指向的对象可以变化;引用不能更改绑定关系
4. 指针可以有多级,但是引用只能有一级,也就是说可以定义指针的指针,但是不能再定义引用的引用
- **int *p = val**,可以解释为p存放val的地址,亦可解释为p指向变量val
- 指针类型需要和所指向的对象类型完全匹配
- void*类型是一个特殊的存在:
- void*是一个能指向任意类型的指针
- void能做的操作有限:比较、函数如参、返回值,或者赋值给另外一个void的指针
- void*指向的对象不能直接访问,说白了,因为对象的类型不定,即只知道初始地址,但是不知道向后去多少
函数指针/指针函数:
函数指针:
顾名思义,是一个指针,指向函数在代码段中的起始位置(程序在编译之后,在代码段中都有自己的起始地址和结束地址,函数指针就是指向函数起始地址的指针)
指针函数:
顾名思义,是一个函数,返回值是一个指针
野指针/悬空指针:
- 野指针:指向不确定的指针,通常是定义后未初始化的指针;避免方法就是定义时就做初始化
- 悬空指针:指向确定,但是内存被释放了;释放对应内存后,指针也置空
1 | int *ptr_1; //野指针 |
nullptr
和NULL
的区别:NULL
:是预处理变量,是一个宏定义,等于0nullptr
:是关键字,有c++11引入的,是有类型的,可以被转换成为任意一种类型- nullptr的优势:
- 有类型
- 函数重载:如果重载函数的实参为NULL,因为NULL就是0,有可能无法明确区分到底是调用哪个,而nullptr则不会出现这种错误
const限定符:
const是用来修饰常量的,即被const修饰的变量会成为常量,这也意味着必须为const修饰的对象初始化
const修饰的对象默认情况下只在定义的文件内有效,如果需要在多个文件内都生效,则需要重复定义,但是若不想重复定义,也有方法:就在对象定义和声明的地方都加extern修饰符
1
2extern const int fileSize = 512; //tess.cpp
extern const int filezSize; //test.h指向常量的指针和引用,只是它们的自以为是,它们觉得自己指向的是常量,自觉的不去修改对象的值,但是又可能实际指向的对象是非常量,且可以通过其他途径对其值进行修改
常量指针:是一个指针,用常量来修饰,则说明这个指针是指向常量的,即指向的目标地址内的值不能变,但是指针指向的地方可以变
1
2const int *p;
int const *p; //从右至左,首先是一个指针,再加了const修饰指针常量:是一个常量,用指针来修饰,则说明这个指针是不能变的,某个指针赋值之后就不能变了,即指向的内存地址是固定的
1
int* const p; //从右至左,首先是一个常量,再是一个指针
cost成员变量:只能在类的内部进行定义、声明,在构造函数初始化列表中初始化
const成员函数:const的函数不能修改其他成员变量的值(除非嫁加mutable修饰符的成员变量),不能调用其他非const的成员函数
const参数:函数内部不能修改实参的值,一般情况下都是const指针,或引用
const返回值:如果函数的返回值为指针或者引用,那么可以用const修饰,避免发生修改
普通引用不能绑定到常量上,但是常量引用可以绑定到常量上:
1
2
3
4
5
6
7
8int &i = 0; //非法
const int &j = 0; //合法
void func(int &arg);
func(2); //error
void func(const int &arg);
func(2); //correct
cosnt和consexpr:
常量表达式修饰符constexpr
,所谓常量表达式
,就是组成该表达式的都是常量,当常量表达式编译后,值就确定了,且不能再改变
常量表达式和非常量表达式的计算时机是不同的,常量表达式
在编译阶段
,而非常量表达式
是在运行阶段
,那就意味着常量表达式只需要计算一次,能够节省程序运行的时间constexpr
用于修饰普通变量
、函数
、构造函数
const的语义二义性:
所谓const的二义性指的是,const既可以表示只读
,又可以表示常量
,通过如下的例子可以分别两者的区别:
1 | void func(const int a) { |
所以说在C++11中为了区分开const只读、常量的语义,引入了constexpr修饰符,将const的常量语义划分给了const
define和const的区别:
- define是预处理命令;const是编译阶段的修饰符
- define是宏定义,在预处理的时候编译器会直接进行代码替换,有多少就替换多少;但const是在编译的时候进行处理的
- define就是简单的语句替换,没有类型;const是有类型和对应的类型检查的
- define可以接受参数;const不能接受参数
- define可以用来防止头文件被多次引入
- define不能进行调试,但是const可以
define和typedef的区别:
- define是预处理命令,typedef是编译阶段生效的命令,用来定义别名
- define没有类型检查,只是简单的语句替换
- define没有作用域,在源文件中define的宏可以在文件内部随处使用;在头文件中定义的宏,可以在所有include的其他地方进行替换
- 处理指针时不一样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using namespace std;
typedef char* ptr;
int main() {
ptr a, b, c;
PTR d, e, f;
cout << sizeof(a) << sizeof(b) << sizeof(c) << endl;
cout << sizeof(d) << sizeof(e) << sizeof(f) << endl;
return 0;
}
/* 输出 */
// 888
// 811
static关键字
在C语言中,static
用于定义:局部静态变量
、外部静态变量
、静态函数
在C++中,static
用于定义:局部静态变量
、外部静态变量
、静态函数
,因为C++中有类的概念,所以就多了静态成员函数
、静态成员变量
个人理解:static的作用有2个主要方面:
- 限制作用域
- 限定变量或函数的生命周期
如下是一个常见的静态的东西:
静态全局变量:
静态全局变量和全局变量都存储在静态存储区
,所以它们的生命周期和整个程序一致,只能被初始化一次,But普通全局变量的作用域是整个程序,也就是别的源文件中的函数也是可以访问的;而静态全局变量的作用域就只是源文件内部,只能被源文件内部的函数使用,静态局部变量:
静态局部变量也是存放在静态存储区的,所以生命周期和程序一致,且只能被初始化一次,但是其作用于是函数内部,和正常的函数局部变量的作用域是一样的
1 |
|
静态函数:
静态函数的作用域是源文件内部,不能被其他文件访问静态成员变量
:虽然是成员变量,但是其存储位置仍然是静态存储区域,这也就是为何要求警静态成员变量必须是在外部定义&初始化,只在类内部进行声明了,有一些需要注意的点:- 静态成员变量不能在构造函数内部对齐进行初始化(只有一次,在外部)
- 类的所有成员(包括派生类)共享静态成员变量,切不能对其值做修改
- 静态成员变量可以作为成员函数的参数,其他成员变量🙅
- 静态成员变量的类型可以是所属类的类型,但是其他的成员变量只能是所属类的指针、引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18static int var = 0;
class A {
public:
int var1
static int var2; //静态成员变量
static A varA; //静态成员
A *ptrA; //OK
A &refA; //OK
A varA1; //error,不能这样搞
void func2(int i = var1) { //error
...
}
void func1(int i = var2) { //OK
...
}
}
静态成员函数
:静态成员函数不能访问非静态的成员变量和成员函数,因为静态成员函数没有this
指针静态对象:静态对象的生命周期是整个程序,但是非静态的就是随时可能终结
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using namespace std;
class ClassA {
public:
ClassA() {
cout << "Class A start" << endl;
}
~ClassA() {
cout << "Class A stop" << endl;
}
};
class ClassB {
public:
ClassB() {
cout << "Class B start" << endl;
}
~ClassB() {
cout << "Class B stop" << endl;
}
};
int main() {
static ClassA classA;
if (true) {
ClassB classB;
}
cout << "Main output" << endl;
return 0;
}
/* 输出结果 */
// Class A start
// Class B start
// Class B stop
// Main output
// Class A stop为何静态变量只能初始化一次?
因为它存放在静态存储区,多个对象共享的,所以如果在别人对这个静态变量正在使用时,有个玩意给重新初始化了,那会影响其他正在使用的对象的
this指针:
this指针是指向类对象的首地址的指针,this指针本身的存放不一定,这个要看编译器的实现
this指针在编译过程中,编译器自动为类的非静态成员函数隐式加入一个this指针的形参,作用就是在非静态成员函数中访问非静态成员变量;所以静态成员函数和全局函数都没this指针,因此这些静态成员函数只能访问静态成员变量,不能访问非静态成语变量
C和C++对于struct的区别:
其实c++中的class完全包含了struct的所有能力,struct只是一个抽象数据类型,把一些变量、函数放在一起而已;所以struct的成员默认都是public的,而class的默认都是private的
struct和union:
union的成员共享存储空间(最大成员的整数倍),所以也代表同时只能有一个union的成员是有效的,而struct的成员各有各的内存空间(整体的内存是按照内存空间的策略执行的),union成员必须是定长的,struct内部可以接受变长成员
typedef关键字
typedef关键字是用以定义一种新关键字的修饰符,通常用于简化一些冗长的类或结构体的声明,用以赋予一个更简单、更容易理解的声明
一个很通用的举例子:
1 | /* 定义一个名为Student的结构体及两个Student类型的变量stu1、stu2 */ |
new/malloc/calloc的区别和联系
new和malloc的区别
new和delete是一对,需要配对使用,是C++的运算符
malloca和free是一对,最好配对使用,是C的标准库函数
相同点:
- 都用于申请动态内存空间
不同点:
- new申请的空间有可能来自于自由存储区,但是malloc申请的空间一定是堆空间
- new可以根据类型自动计算存储空间;而malloc需要手动计算
- 对于类对象来说,malloc无法满足动态对象的要求,而new和delete会在对象创建和销毁时分别调用构造函数和析构函数,所以new分为2步:内存申请和构造
- new申请成功后返回的是对象的指针,而malloc返回的是void *,需要强转类型
- new是类型安全的,而malloc不是类型安全的,例如:
1
2int ptr = new float[2]; //编译会报错
int *ptr = (int *) malloc(size(float) * 2); //编译不会报错 - new是运算符,所以可以重载;但malloc函数不能重载
- malloc可以通过relloc来修改申请的空间大小,但new申请完空间久不能再变了
有了malloc&free,为何还需要new&delete:
malloc&free不能操作动态对象的要求,即无法自动调用构造和析构
有了new&delete,为何不删除malloc&free:
C++经常还要调用C的函数,所以涉及到的部分必须使用malloc&free进行动态内存操作
malloc和calloc的区别
相同点:
- 二者都可以用来申请动态内存
- 二者都是从堆上申请空间
- 二者申请的空间都是地址连续的内存空间
- 二者申请的空间都使用free释放
不同点:
- 参数数量不同:malloc一个参数用于指定申请的内存空间大小;calloc两个参数,分别用于指定对象个数和每个对象的大小
- 内存初始化不同:malloc申请的内存不会初始化;calloc申请的内存会自动初始化为全0
- 运行效率不同,malloc比calloc厉害(主要原因初始化操作耗时)
size_t:
size_t是一种数据类型,是无符号整数,与平台无关,取值范围为0-MAXINT(是int的2倍),不同系统中对size_t中有不同的定义:
1 | typedef unsigned size_t; |
内联函数inline:
inline
修饰的函数其实在编译时就是直接代码的替换,这个和用define
修饰的作用是一样,主要的目的就是为了避免对一些简单、常用的函数多次调用,影响执行效率;但是inline
比define
高级的地方就在于inline
能够在编译时进行类型检查,避免一些基础错误,而且inline函数可以进行调试,但define不行inline
函数可以在头文件中被定义,在其他源文件使用,inline
关键字一定要跟函数定义绑定,inline
放在函数声明中时无任何作用的
类内部除了虚函数之外的其他函数,默认都是内联函数
volatile:
volatile
关键字修饰变量的主要目的是防止编译过度优化,而产生对变量的访问错误,用volatile
修饰过的变量一定会按照正常的指令执行顺序执行,且每次访问该变量都是直接访问内存,而不是寄存器(有时编译器为了优化读取,可能病不会把修改后的结果直接写会内存,而是缓存在寄存器中,这样别的地方访问的时候就出现了不一致的问题)
extern C
关键字:
C++中有重载,所以在编译的时候函数名并不完全等于函数,而是会把修饰符、参数都加到函数名中去,但是C里边没重载,所以不会;在C++中调用C的代码时,默认按照C++的命名规则去符号表查找函数,这样很多情况下会出错,所以为了避免这种情况,所以用extern C修饰函数,这样C++编译器就会知道到底应该找哪个了名字了,不然很可能找不到
strcpy的缺陷:
strcpy
是用来进行字符串拷贝的函数,作用是将一块连续地址空间内的字符拷贝到另外一个连续地址空间内,包括结束符\0
,但并不会检查目的缓冲区的大小,所以把一个大字符串拷贝到小空间的话,会覆盖其他不属于目的变量的内存空间,导致缓冲区溢出
自由存储区
自由存储区是C++专门针对new&delete运算符的一个抽闲概念而已,而堆则是操作系统中为程序运行过程中可动态分配的内存空间的定义,所以从实际意义上讲,堆等价于动态内存空间,而自由存储区并不等价;但是由于大多数编译器中对于new的实现都是基于malloc,因此实际上绝大多数自由存储区和堆都对应相同的存储空间,所以这个自由存储区和堆是否划等号,要看new的实际实现。
此外,new是可以重载的,重载后的内存完全可以不在动态内存区域
左值/右值:
左值/右值:
左值: 执行表达式之后仍然存在的对象,仍然可以通过取地址符来获取地址
右值: 执行表达式之后已经销毁的临时对象,不能再通过取地址符&来对其地址进行访问
在使用=
赋值时,左边只能是左值,右值只能在右边,当然左值也可以在右边
函数的返回值,既可以是左值,也可以是右值
1 | // x 是左值,666 为右值 |
左值引用/右值引用:
左值引用:
顾名思义,就是对左值的引用,用&
来表示,底层是指针实现的
如下是左值引用的一些特殊限定:
- 左值引用分为:常量左值引用;非常量左值引用
- 非常量左值引用只能绑定到非常量左值上,不能绑定到常量左值和右值上
- 常量左值引用可以绑定到非常量左值,常量左值、右值上
右值引用:
顾名思义,就是对右值的引用,用&&
来表示,是C++ 11引入的新特性,主要的作用是实现转移语义和精确传递
转移语义:通过对右值绑定引用,使得临时对象不会立即释放了,临时对象内存的声明周期和对应的右值引用一致了,特别是类似于移动构造的使用下,能够避免大规模的内存拷贝
精确传递:其实也是利用了在实现参数传递的时候避免了拷贝等操作
右值引用必须绑定到右值上
可以通过std:move将一个左值强制转换为右值,实现转移引用,通过右值引用访问左值
移动构造函数
所谓移动构造函数,就是在用一个对象初始化另外一个对象的时候,不需要做内存拷贝,而是只做内存和对象关系的移动,说白了就是我把我的身体给你,我自己灵魂出窍了(升天了,要g了)
⚠️:正常情况下移动构造的实参一定是右值,但是如果非得用左值,那就用move把左值转换为右值
移动构造和拷贝构造:
移动构造入参:右值引用
拷贝构造入参:左值引用
正常情况下使用临时对象初始化一个新对象时,会先尝试使用移动构造,如果移动构造没有定义,则才会使用拷贝构造
之所以用移动构造,就是为了防止对象内部有指针变量,如果用拷贝构造,那么就需要将指针的地址拷贝一份出来,效率和代价比较大;但是用了移动构造,指针指向的空间不用内存拷贝了,直接用新对象的指针变量指向老对象的内存,然后将老对象的指针置为NULL。
move函数的实现原理:
待补充
智能指针:
使用智能指针的目的就是为了防止内存泄露,也就是申请的内存在使用完毕后没有释放
智能指针在C++11中引入(其实C++ 98中也有auto_ptr,只是比较简单,场景受限),C++ 11中的3种智能指针:
智能指针的三个重要函数:
- **get()**:返回智能指针托管的内存地址,但是一般不用,因为我们就是想着不用去直接操作内存才有的这些高级封装操作
- **release()**:取消智能指针对内存的托管
- **reset()**:充值智能指针托管的内存,参数为空,则重置为NULL
auto_ptr比unique_ptr差的地方:
- 拷贝和赋值都会改变智能指针对内存的所有权
- STL容器中使用auto_ptr存在很大风险,因为STL内元素必须支持拷贝&赋值
- auto_ptr不支持对象数组的操作
1 |
|
unique_ptr:
这种智能指针只是是不允许多个指针指向同一对象内存空间的,即独享所有权的指针,不支持拷贝构造、复制
需要注意的是unique_ptr在调用reset的时候,会赋新值
因为unique_ptr因为自身不允许多个智能指针指向同一内存的缺陷,它有可能造成悬空智能指针,比如下面的代码中,由p1先管理str,但是后边p2作为独占管理,那么此时如果用p1访问的话就会报错了
1 | auto_ptr<string> p1; |
share_ptr:
share_ptr是允许多个智能指针同时管理一个对象的内存空间的,用计数器实现,当某个智能指针析构时,会检查计数器,如果计数器等于0,则证明没有指针指向这块内存,则释放内存;而当执行智能指针赋值、拷贝构造时就会让计数器+1,通过如下的例子可以感受share_ptr的使用
1 |
|
share_ptr可以用make_shared来初始化(更快):shared_ptr<string> p1 = make_shared<string>("AAAA")
share_ptr可以交换彼此的托管,彼此的计数不变:p1.swap(p2); std:swap(p1, p2);
shared_ptr的使用缺陷场景: 多个类交叉包含彼此对象的智能指针,会造成内存泄漏,无法释放的问题,交叉持有对方智能指针对象时,引用计数为2,但是每个对象在析构时又只会释放一次,也就是计数器只会-1,所以内存不会释放
weak_ptr
为了解决上述shared_ptr的交叉持有时释放不了的情况,引入了weak_ptr来协助shared_ptr,weakptr的构造和析构不会引起计数器的增减,weakptr不支持*
和->
的重载,所以没法访问对象,如果一定要访问,那就在对象内部将weakptr转换成共享指针 lock()
1 | shared_ptr<Girl> sp_girl; |
值传递、引用传递、指针传递
函数调用时有三种参数传递的方式:
值传递:
通过值的内存拷贝,实现参数传递,其实就是把实参的值给形参拷贝了一份
引用传递:
形参就是实参的引用,所以函数内部的修改会影响实参的值
指针传递:
本质上时值的传递,只不过拷贝的是指针的值,也就是说把对实参的指针的值拷贝了一份给形参
结构体相等的判断方式:
如果在C++种想判断2个结构体是否相等,不能直接用memcmp
函数,因为结构体是内存对齐的,所以内存种免不了有垃圾数据,而memcmp
是按字节比较的,所以不能用memcmp
,只能通过重载==
运算符,对结构体内元素逐一比较
强制类型转换:
C语言中的强制类型转换:
C语言中的强制类型转换主要是为了基础数据类型之间的转换
1 | (类型)expression |
C++中的强制类型转换:
C++除了保留C中的基础数据类型的强制转换之外,还针对面向对象的类,引入了一些基类与派生类之间继承关系转化的相关操作,共有四种:static_cast
,dynamic_cast
, const_cast
, reinterpret_cast
static_cast:
1 | static_cast(new type) expression; |
常用的4种用法:
- 把空指针转换为任何类型的指针
- 把任何类型的表达式转换为void类型
- 基础数据类型转换
- 基类和派生类的上行/下行类型转换
dynamic_cast:
1 | dynamic_cast:(new type) expression; |
const_cast:
1 | const_cast:(new type) expression; |
reinterpret_cast:
1 | reinterpret(new type) expression; |
一些高级概念:
泛型编程(模版):
泛型是一种思想:让相同的函数或类能够处理不同的数据结构,一般通过定义模版实现,主要的作用就是:通用型、效率
函数模版:
使用类型声明函数模版的语法:
1 | template<typename identifier> declaration; |
类模版:
类模版的语法:
1 | template<typename T> |
类模版的使用举例:
1 |
|
注意⚠️:
- 如果基类为类模版,派生类需要指定模版中的类型,否则编译报错,因为编译器不知道分配多少内存
- 如果派生类也想灵活给出模版中参数类型,那就要求派生类也为类模版实现
变量模版:
待补充
函数重载和函数模版的区别:
相同点
:都是多态的实现方式,且不管是重载还是模版,在编译的时候都是生成多个函数代码的,目标代码不会变少,但是通过模版可以减少写代码不同点
:函数操作相似用重载;函数操作相同用模版
函数模版和类模版的区别:
- 自动类型推导:类模版无自动类型推导;函数模版有自动类型推导
- 类模版可以接受默认参数,函数模版不能使用默认参数
1
2
3
4template<class TypeName, int a>
class A {...};
template<class TypeName, int a = 1>
class B {...};
STL:
教程
STL是C++种提供的标准模版库,主要依赖模版实现,内置了很多函数模版、类模版供使用,STL一般情况下是数据和操作分离的,比如sort函数,我们可以用来对数组、链表、容器等进行排序。
STL的6大组件:
容器:
包括2个大类:
1、序列容器:向量(vector)
、列表(list)
、双端队列(deque)
2、关联容器:集合(set)
、multiset
、映射(map)
、multimap
STL的容器用于存储和管理数据,容器类自动申请和释放内存,无需new和delete操作
容器的概要及使用原则:
- 最常用的容器就是vector
- 如果程序有很多小元素且空间的额外开销很重要,不要使用list或forward_list
- 要求随机访问元素,应该使用vector或deque
- 要求中间插入或删除元素,应该使用list或forward_list
- 要求在头尾插入或删除元素,且中间不进行插入或删除,应该使用deque
- 如果只在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素(首先可以考虑在读取输入时使用vector,再调用sort函数重排容器中的元素,从而避免在中间位置添加元素。如果必须在中间位置插入元素,考虑在输入阶段使用list,输入完成将list拷贝到vector中)
- 如果实在不确定使用哪种容器,可以在程序中只使用vector和list的公共操作迭代器而非下标,避免随机访问。这样可以在必要时选择使用vector或list
迭代器:
提供了一种访问容器中元素的通用方式,迭代器通常用来遍历容器内容,但是并不需要关心容器的内部实现,一般情况下基于迭代器,用户可以自定实现一些算法,包括输入迭代器
、输出迭代器
、前向迭代器
、双向迭代器
和随机访问迭代器
算法:
包括排序
、查找
、遍历
、修改
、复制
、合并
、反转
、旋转
等常用算法,通常算法和所操作的数据是分离的
这个数据和算法分离的概念可以这么理解,就是一个sort算法,可以适应的是所有类型的容器,比如list中的对象可以是int,也可以是个很复杂的类对象,sort函数不care,照样弄你
仿函数:
是可调用对象,可以像函数一样使用,用于定制算法的行为,说白了就是让类像函数一样使用
适配器:
用于将容器或函数对象转换成其他类型,以适应不同的需求。包括容器适配器(如栈和队列的适配器)
、迭代器适配器(如反向迭代器和插入迭代器)
和函数适配器(如绑定器和取反器)
空间配置器件:
用于管理动态内存的分配和释放
STL的缺陷:
- STL非线程安全的,需要使用者自己加锁,普通情况下锁的粒度会很大
- STL极度追求效率,导致内部实现比较复杂
vector中reserve和resize的区别:
首现需要明确2个概念,capacity
和size
,其中:capacity
:vector最多能容纳的元素个数size
:现在vector有效的元素个数
Reserve:
修改当前vector的capacity,但是不做初始化
Resieze:
修改当前vector的有效元素个数,会做初始化,有可能会改变capacity
模版特化:
所谓模版特化,即模版的参数并不都是同一种类型,有全特化、部分特化两种。特化的主要原因是,一些模版在接受某些特定类型参数时可能报错,所以为了排除这种问题,定义模版时将部份或全部参数指定为特定的类型
类型萃取:
在模版内部需要针对不同类型的数据做区分处理时,需要识别参数的类型,这个操作就叫类型萃取,和特化做个对比就是知道,特化是限制模版参数输入,而类型萃取是在模版内部对类型做限制
C++ 11的新特性:
auto类型推导:
auto
关键字是在C++11种引入的自动类型推导的关键字,可以根据函数返回值
、表达式
、初始值
来自动推导出变量的类型
1 | int var1 = 100; |
⚠️注意:
auto
在执行自动类型推导时会自动脱去引用&
、顶层const
、volatile
关键字的修饰auto
修饰的变量在定义是必须初始化,这是因为auto
关键字是在编译阶段生效的,如果没有初始化,则无法知道对应变量的类型了
decltype类型推导:
其实和auto
一样,decltype
用来做类型推导的,和auto
的区别也很明显,auto
在做类型推导的同时要赋值,但是decltype
就只做类型的提取,不做赋值
lambda表达式:
又被成为匿名函数,主要的作用就是为了写程序方便,可以就地定义函数,而不需要去别的地方做函数定义、函数声明等一系列麻烦的操作,让程序员更加专心眼前的逻辑处理
1 | [capture list] (parameter list) -> return type { |
其中捕获列表:指定lambda表达时内部需要使用的局部变量,一般被称作闭包
,如下所示为一些常用的闭包:
1 | [] // 没有定义任何变量。使用未定义变量会引发错误 |
⚠️注意:lambda表达式是在捕获列表中是以值方式
对环境中的变量进行捕获的,在lambda表达式内部不能进行修改,否则编译报错。如果想强行在lambda中修改,必须在表达式中加入mutalbe
关键字修饰,而且对于同一个捕获来说,后续对lambda的重复使用将对捕获时的值叠加作用,但是对捕获外部的原始值没有影响
1 |
|
for语句
1 | for (declaration : expression){ |
右值引用
move函数
智能指针
使用/禁止对象的默认构造函数:
C++11中允许显式的说明使用/禁止编译器提供的内置函数,分别使用default和delete修饰符:
1 | class ClassA { |
constexpr:
nullptr:
C++11中为了将空指针和0区分开,引入了nullptr,是有类型的,说白了就是nullptr可以转化为任何类型的指针,因为在以前指针为空用NULL来表示,但实际上NULL是一个宏定义,它就是0,如果在重载函数场景下用NULL的话,可能造成函数调用歧义
可扩展的随机数:
C++11提供了生成伪随机数的新方法,生成随机数包含2部份:随机数引擎+随机数分布,也就是说如果想的到生成随机数的对象,必须先定义随机数引擎,并制定随机数的分布
1
2
3
4
std::uniform_int_distribution<int> distribution(0, 99); // 离散型均匀分布
std::mt19937 engine; // 随机数生成引擎
auto generator = std::bind(distribution, engine); // 将随机数生成引擎和分布绑定生成函数
int random = generator(); // 产生随机数
1 | std::uniform_int_distribution<int> distribution(0, 99); // 离散型均匀分布 |
C++的面向对象:
面向对象的三大特征:
- 继承:子类可以继承父类的特征和行为,包含成员变量和函数
- 多态:C++中主要是利用虚函数实现,即不同的继承类对象对同一调用做出不同的反应
- 封装:将某个操作封装成为具体的函数,只能通过接口方式调用,降低了耦合性
函数的重载/重写/隐藏:
重载:就是同名的函数,通过不同的参数来进行区分,而且不区分返回值
1
2
3
4
5
6class Student{
public:
int func(int arg1, int arg2);
int func(int arg1, int arg3);
void func(int arg1);
}重写:重写值得是派生类对基类中用virtual关键字修饰的函数,对其函数体进行重写的操作,但是不会修改函数的返回值和参数,利用重写可以实现多态
隐藏:派生类中隐藏基类的同名函数,不管参数列表是否相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Base {
public:
void func(int arg1, int arg2) {
return arg1 + arg2;
}
}
class Device : public Base {
void func(int arg1) {
return arg1 * 2;
}
}
int main() {
Device dev;
dev.func(1); //ok
dev.func(1, 2) //error, 因为这里派生类隐藏了基类的同名函数,如果需要访问则需要:
dev.Base::func(1, 2); //需要明确指定
}重写/重载的区别:
- 范围:重写值得是子类重写父类的函数;而重载是在某个类的内部对某个名字的函数进行不同的实现
- 参数区别:重载可以要求函数有不同的返回值、参数;但是重写要求返回值和参数都一致,只修改函数内部实现逻辑
- 关键字修饰:重写的函数必须是基类中用virtual修饰的才行
多态的总结:
- 多态就是用虚函数实现的,每个有虚函数的类都会有一个虚函数表,并为类对象中有个虚表指针指向虚表
- 虚函数是在运行时根据对象动态绑定,由对象决定,也就是说不管用基类的指针还会子类的指针,只要指向的对象是子类,那就用子类的函数,如果指向的是基类,那就用基类的函数
- 函数隐藏的情况下,基类指针指向子类对象时,其实仍然访问的是基类的函数,也就是说函数隐藏取决于指针类型,而重写取决于对象
虚函数和纯虚函数:
虚函数是派生类可以重写的,但是不一定需要重写,看需求;但是纯虚函数就要求派生类必须实现纯虚函数的实现,如果不实现编译时会报错,那派生类仍然是个抽象类,而抽象类是不能进行对象实例化、作入参、做返回值的
理解下来就是说:虚函数都是用来实现多态的,但是纯虚函数的定存在,就是为了派生类必须要做某个动作的定义,如果不定义那就直接报错了,所以正常情况下推荐将析构函数定义为纯虚函数,这样就不会忘了在派生类中定义析构了
- 构造函数不能定义为虚函数:虚函数是又了对象之后,才会有的,但是有类对象则一定要执行构造函数,这就前后矛盾了
- 推荐将析构函数定义为虚函数:能够防止不及时回收内存导致内存泄漏
⚠️注意:2个问题:
1、虚函数表什么时候生成,放在哪里?
虚函数表是在程序编译阶段生成的,虚函数表中防止的就是虚函数的起始地址,放在只读数据段
2、虚表指针什么时候生成,放在哪里?
虚表指针是在程序运行过程中对象初始化后生成的,放在类对象内存的起始位置,指向虚函数表
虚继承:
深拷贝/浅拷贝:
- 深拷贝:完全的内存复制,每个对象对应一个单独内存空间,互补影响
- 浅拷贝:只拷贝栈空间,而不管堆
所以当对象有指针变量是,推荐使用深拷贝,不然可能造成前一个对象把堆内存释放了,结果第二个对象再去访问,造成崩溃
为了能够保证深拷贝,则需要定义拷贝构造函数,即对新对象拷贝时,在构造函数中重新new内存出来
⚠️注意:类对象的深拷贝必须显式定义拷贝构造
友元
友元函数:
通过声明友元函数,可以实现普通函数、其他类成员函数对某个类的privte/protected成员的访问
友元类
通过在类中声明友元类,可以实现对其他类的private/protected成员变量的访问
友元函数的2个特性:
单向性
:A可以是B的友元,但B不一定是A的友元不可传递性
:指的是派生类不能继承父类的友元关系
如何让一个类不能被继承:
- 方法1:用final关键字
- 方法2:使用友元、虚继承和私有构造函数来实现
如何让类对象不能拷贝:
- 方法1:私有化:
- 将
拷贝构造函数
和=的重载
声明为private - 派生类私有继承
- 将
- 方法2:delete:将拷贝构造函数用
delete
修饰
如何让一个类不能实例化:
- 方法1:类中有纯虚函数,抽象类不能实例化对象
- 方法2:所有构造函数私有化
- 方法3:类的所有构造函数用
=delete
修饰
拷贝构造为何必须声明为引用:
之所以拷贝构造函数中必须声明为引用的,是为了防止拷贝构造无限循环导致内存溢出,如下是几种拷贝构造的常见触发场景:
1 | ClassName *p = new ClassName("test"); //No.1 |
实例化一个类对象的步骤:
- 申请内存:
- 初始化:
- 赋值:构造函数做的事情,也意味着构造函数执行完毕之后,一个对象的实例化就完成了
注:有虚函数的对象实例化的时候还需要给虚表指针赋值
C++类对象的初始化顺序:
- 调用基类的构造函数:
默认按照继承说明的顺序进行调用(但是虚继承优先) - 类成员的构造函数:按照声明的顺序调用
- 派生类自身的构造函数
类成员的初始化顺序:
- 类的static成员是main函数开始前就初始化了的
- 如果成员变量在初始化列表初始化,则按照成员在类中的声明顺序进行初始化
- 如果是在构造函数内部,则与实际的代码逻辑匹配
如何禁止构造函数被使用:
构造函数私有化是一个方法,但是这种方法避免不了类内部成员、友元的访问,因此在C++引入了=delete
关键字(=default
是让一个构造函数可以被调用)
1 | class A { |
所以个人理解,正常情况下不会把所有的构造函数都不可调用,是为了让用户自定义一些构造函数,而把默认构造定义为不可调用
使用列表初始化快的原因:
如果不使用列表初始化,在构造函数内部初始化成员变量的话,会先调用默认构造函数,因此反过来说如果使用列表初始化,则省去了调用默认构造函数的开销
静态绑定/动态绑定:
- 静态类型:对象在声明时的类型,编译阶段指定,不可更改
- 动态类型:对象当前所指的类型,运行时指定,可以更改
- 静态绑定:在编译阶段为对象指定类型的过程
- 动态绑定:在运行阶段为对象指定类型的过程
注: 类的成员,只有虚函数是动态绑定,其他都是静态绑定
1 |
|
类的内存占用由成员变量和指向虚函数表的指针组成,同时派生类的成员变量是会把基类的成员变量都继承的,虚函数指向虚函数表,虚函数表中放的是虚函数的入口地址
基类和派生类的同名虚函数在虚函数表中的偏移是一致的,如果没有重写虚函数,那就派生类和基类相同偏移的函数都是一致的,如果有重写,那派生类就替换相同偏移位置的虚函数入口地址,如果派生类中新定义了非同名虚函数,那就直接在虚函数表末尾加入
编译时多态/运行时多态
编译时多态:
实际指的是模版
、重载(泛型)
,编译时都会生成不同的函数,比如重载,在代码段中都是不同的函数
运行时多态:
实际指的是类的多态,其实就是虚函数,指的是用基类指针
/引用
访问派生类虚函数
两者的区别:
- 发生时期不同:
编译时多态-编译阶段
;运行时多态-运行阶段
- 实现方式不同:
编译时多态-泛型编程
;运行时多态-虚函数表
如何让成员函数不可改变成员变量的值:
将成员函数用const
修饰,即可做到让该函数无法修改成员变量的值,但是如果就是想在const
函数内部修改某个成员变量的值,也有办法:将该成员变量声明时加上mutable
关键字的修饰(被mutable修饰的变量将处于永远可以被修改的状态)
1 | class MyClass { |
限制对象内存在栈/堆:
限制类对象的内存在栈:
在类的内部私有化重载new和delete运算符(底层的原理是啥呢?)
限制类对象的内存在堆:
可以尝试将析构函数私有化,这样在函数检查时发现如果将内存放在栈里头,因为析构函数私有化,那么久没发释放内存,程序就执行不下去了
多线程编程
进程间通信的方式:
进程间通信的方式一共有4种:共享内存、管道、消息队列、文件
线程间通信的方式:共享内存
互斥量
用于防止多个线程对同一个共享变量同时操作(同时访问临界区变量
1 |
|
互斥量:
用于限制多个线程同时读取/写入,这里强调的是读和写都是一样的
共享互斥量:
std::shared_mutex
跟普通的互斥量有区别,使用场景是:允许同时进行多个读取,但读取还是只能一个,当然读取和写入还是互斥的,std::shared_mutex
配合unique_lock
和shared_lock
使用,unique_lock
用于写入时加锁,shared_lock
用于读取时加锁
对象在构造时自动对std::shared_mutex
加锁,析构时自动解锁?
未完待续。。。
信号量:
semaphore(信号量)
用来进行线程同步
信号量一般包括2类:
二元信号量(binary_semaphore)
:只有0/1,实质上类似于互斥量的作用计数信号量(counting_semaphore)
:正常情况下,用acquire来获取资源的访问权(计数器减),release用来释放资源(计数器加),当计数器为0时,线程阻塞
条件变量:
condition_variable
也是用来进行多线程同步的,当条件不满足时,线程阻塞;当满足条件时开始唤醒线程。条件变量利用全局变量共享的方式来达到线程同步的操作,主要如下两个动作:
- 一个线程因等待条件满足而阻塞(
wait
/wait_for
/wait_until
) - 一个线程完成操作,给出条件满足的信号(
notify_one
/notify_all
),从而使其他线程被唤醒
常规情况下,条件变量通常和一个互斥量std::mutex绑定
call_once:
call_once是在c++11引入的,作用是让某个操作只执行一次,即使在多线程操作下,也只执行一次
线程操作相关:
join
:阻塞主线程detach
:不阻塞主线程,主线程和子线程的运行互不影响
异步编程:
异步编程的情况下,如果主线程需要使用子线程的计算结果,那么常用的方法是:共享变量
/ 消息队列
,但操作和实现比较麻烦,所以C++11中引入了一个简单的异步接口工具std::async
std::future类模版
用来关联线程运行的函数及其返回值,提供了访问异步操作结果的机制
std::future
的三种状态:- deferred:未启动
- ready:已完成
- timeout:超时
std::future
结果的获取方式:- get:等待异步结果并返回
- wait:只等待结果,无返回值
- wait_for:超时等待返回结果
epoll/select
I/O
设计模式
设计模式的6大原则:
单一职责原则
:就一个类而言,应该仅有一个引起它变化的原因。开放封闭原则
:软件实体可以扩展,但是不可修改。即面对需求,对程序的改动可以通过增加代码来完成,但是不能改动现有的代码。里氏代换原则
:一个软件实体如果使用的是一个基类,那么一定适用于其派生类。即在软件中,把基类替换成派生类,程序的行为没有变化。依赖倒转原则
:抽象不应该依赖细节,细节应该依赖抽象。即针对接口编程,不要针对实现编程。迪米特原则
:如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某个方法的话,可以通过第三个类转发这个调用。接口隔离原则
:每个接口中不存在派生类用不到却必须实现的方法,如果不然,就要将接口拆分,使用多个隔离的接口。
设计模式三大类:
创造型模式
:单例模式
、工厂模式
、建造者模式、原型模式结构型模式
:适配器模式、桥接模式、外观模式、组合模式、装饰模式、享元模式、代理模式行为型模式
:责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式
、状态模式、策略模式、模板方法模式、访问者模式
几种常见的设计模式:
单例模式
、工厂模式
、观察者模式
单例模式
单例模式顾名思义就是只实例化一个对象,并为之提供唯一的全局访问入口
主要的应用场景: 对于文件系统的操作,对于打印机的操作等,由于这些资源都是独一份的,所以正常情况下也就只单个对象单独访问,如果多个对象同时访问,则容易出现访问冲突的问题,所以单例模式就是为了解决这些场景问题诞生的
常用的实现方式: 将默认构造函数、赋值构造函数、拷贝构造函数都声明成为私有函数,且将唯一全局访问入口声明成为静态函数,使用时直接用类名调用
懒汉模式
:就是在首次使用的时候再初始化对象,这种方式有线程安全的问题,如果同时有多个线程去访问入口,则会实例化多个对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using namespace std;
class Student {
private:
static Student *instance;
Student() {};
Student(const Student &stu) {};
Student& operator = (const Student &stu) {};
public:
static Student* getInstance() {
if (instance == nullptr) {
instance = new Student();
} // 非线程安全
return student;
}
}
Student* Student::instance instance;上述懒汉模式不是线程安全的,可以通过加锁的方式保证安全,但是加锁会影响运行速率,故而一般情况下推荐使用如下的饿汉模式
饿汉模式
:就是在类定义的时候就实例化1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using namespace std;
class Student {
private:
static Student *instance;
Student() {};
Student(const Student &stu) {};
Student& operator = (const Student &stu) {};
public:
static Student* getInstance() {
return instance;
}
}
Student* Student::student instance = new Student(); //静态成员变量的外部定义和初始化,参考static相关章节
工厂模式:
工厂模式有3种:简单工厂模式、工厂方法模式、抽象工厂模式
简单工厂模式:
主要用于创建对象,工厂根据不同的输入产生不同的类,根据不同的虚函数的到不同的结果
主要适用在不同参数创建不同类的情况,比如计算器的+ - * /,如下举例:
1 |
|
工厂方法模式:
在简单工厂模式的基础上修正了不遵守开放修正原则,把选择和判断移到了客户端,如果想添加新功能,就只需要修改客户端即可,不用修改原来的类
主要应用场景:
待续。。。
抽象工厂模式:
待续。。。
观察者模式:
是一种多个观察者和一个被观察者绑定的关系,实际上每个观察者里边都有一个被观察者的对象,而被观察者内部有所有观察者的对象,在被观察者状态发生变化的时候,就会通知所有观察者,观察者们就会做出相应的动作,说白了就是被观察者给观察者放风,学术化的讲就是当某个对象的变化需要引起其他对象的变化,且不知具体有哪些对象需要产生变化时,就可以用观察者模式
1 |
|
日常总结:
指针大小:
在64位计算机中,指针类型大小为8字节
解决哈希冲突的方法:
开放寻址法:
开放寻址的思路是,产生hash冲突的时候,给哈希的结果加上一个增量d,加了增量之后看下对应的空间有没有数据,如果没有就把数据放进去,根据增量序列产生的方式又分为3中小类:
- 线性探测:朝着一个方向逐个探测
- 再平方探测:连个方向探测
- 随机探测:随机探测
链表:
所有冲突的元素组成一个链表
溢出表:
一个哈希表分为2个部份,一部份是基表,一部分是溢出表,发生冲突的元素都放在溢出表中
再哈希:
发生冲突后,利用比的hash方法再算一次,直到不冲突
UML
UML(Unified Modeling Language)同一建模语言,是一种在面向对象开发系统中的建模语言,通常用来表示类之间的依赖
、关联
、聚合
、组合
、泛化
依赖:
用来表示A类use
了B,通常用---->
来表示,在C++中可以有如下的几种形式:
- A的成员函数返回了B的值
- A的成员函数使用B类型作为参数
- A的成员函数内部实现时使用了B
关联:
通常理解为Aknow
B,在C++中一般通过A类的成员变量是B的指针(引用/值),一般有三种关联关系:单向关联
、双向关联
、自身关联
单项关联A->B:
就是A知道B,但是B不知道A,A可以调用B的公共属性和方法,且没有生命周期的限制
1 | class ClassA { |
双向关联A<->B:
就是A和B互相知道对方,即可以互相调用对方的public方法和属性
1 | class ClassA { |
自身关联:
就是自己知道自己,这个在链表中是很常见的
1 | class ClassA { |
聚合:
聚合和组合都是用来表述整体-部分关系的
聚合是描述has a
关系,类A中有类B,当类A的生命周期结束后类B仍然存在,比如桌子和房间的关系,当房间不在了,桌子时可以单独存在的
1 | class Table { |
组合:
聚合和组合都是用来表述整体-部分关系的
聚合是描述is part of
关系,类A中有类B,当类A的生命周期结束后类B也不存在,比如鸟和翅膀的关系,当鸟不在了,翅膀也不在了
1 | class Wing { |
⚠️注意:从代码形式上看,聚合和组合是一样的,这个不同需要从语义分析的角度来区分
泛化:
泛化关系通常等价于类的继承关系,用来描述is a
关系,比如警车也是个车
1 | class Car { |