【C++】宇宙最强知识点总结

文章目录


1、C和C++的区别

(面向过程/面向对象、动态管理内存的方式不同、引用、函数重载)

  • C是面向过程的语言,通过一个过程对输入进行处理得到输出;C++是面向对象的语言,主要特征是“封装、继承和多态”。
  1. 封装隐藏了实现细节,使得代码模块化;
  2. 继承是子类继承父类的变量和函数实现了代码重用;
  3. 多态则是“一个接口,多种实现”。
  • 动态管理内存的方法不一样,C是使用malloc/free,而 C++ 除此之外还有new/delete关键字。
  • C++有引用,C不存在引用
  • C++支持函数重载,C不支持函数重载

2、C++中指针和引用的区别

  • 【存储的内容】指针是一个变量,存储的是另一个变量的地址,而引用则是原变量的别名,跟原变量实质上是同一个东西
  • 引用只有一级,而指针可以有多级
  • 【为空】指针可以为空,引用不能为空且在定义时必须初始化
  • 【引用为指针常量】指针在初始化后可以改变指向,而引用在初始化之后不可以改变指向
  • 【sizeof】sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
  • 【函数参数】当把指针作为函数参数进行传递时,实际上也是将实参的拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个形参的指向并不会影响实参,而引用却可以。

3、结构体struct和共同体union(联合)的区别

  • 结构体:不同类型的变量组合成一个整体
  • 共同体:不同类型的变量共同占用一段内存
  • 【同时存在】结构体中的每个成员都有自己独立的地址它们是同时存在的;共同体中的所有成员占用同一段内存,它们不能同时存在
  • 【sizeof】sizeof(struct)是内存对齐后所有成员变量的长度的总和,sizeof(union)是内存对齐后最长成员变量的长度

3.1 结构体为什么要内存对齐呢?

  • 计算机中内存是以字节为单位划分的,CPU通过地址总线来访问内存,CPU一个时钟周期内能处理多少字节的数据,就命令地址总线读取几个字节的数据。简单的说内存对齐能够提高 cpu 读取数据的速度,减少 cpu 访问数据的出错性

32位CPU为例,寻址步长为4,程序中如果一个int变量的地址为8,那么一次寻址就可以拿到该变量的值,如果int变量的地址为10,那么需要先寻址地址为8的地址拿到数据的一部分,再寻址12的地址拿到另一部分,然后再进行拼接。

4、宏定义(#define)

  • 【处理阶段】宏定义在编译预处理过程中完成替换,相当于直接插入了代码,可能有多个拷贝;
  • 【定义函数】宏定义可以定义简单的函数
  • 【函数返回值】没有返回值
  • 【函数参数】宏定义参数没有类型,不进行参数类型检查
  • 【常量类型】宏定义的常量没有类型,所给出的是一个立即数
  • 【常量不可指针指向】宏定义的常量是不可以用指针去指向
  • 【分号】宏定义不是语句,不需要加分号
  • 【定义】宏定义主要用于定义常量及书写复杂的内容

4.1 宏定义和函数有何区别?

  • 【处理阶段】函数调用是在运行时需要跳转到被调函数。
  • 【函数返回值】函数调用具有返回值。
  • 【函数参数】函数参数具有类型,需要进行参数类型检查。

4.2 宏定义和const的区别

  • 【处理阶段】const所定义的变量在编译时确定其值,只有一个拷贝。
  • 【常量类型】const定义的常量有类型
  • 【常量指针指向】const定义的常量可以用指针去指向该常量的地址
  • 【定义函数】const不可以定义函数

4.3 宏定义和类型别名(typedef)的区别

  • 【处理阶段】typedef是编译期进行处理。
  • 【分号】typedef是语句,需要加分号。
  • 【函数参数】typedef会进行参数类型检查
  • 【定义指针】typedef主要用于定义类型别名。宏定义和typedef定义多个指针有很大区别。(宏定义定义的第一个为指针类型,第二个为普通类型。typedef定义的都为指针类型
#define INT1 int *
typedef int * INT2;
INT1 a1, b1;
INT2 a2, b2;
b1 = &m;         //报错
b2 = &n;         // OK

4.4 宏定义和内联函数的区别

主要区别

  • 【处理阶段】内联函数在编译时直接将函数代码嵌入到目标代码中,省去了函数调用时开销,提高了执行效率
  • 【函数参数】内联函数要进行参数类型检查
  • 【函数返回值】内联函数具有返回值,并且可以实现函数重载

内联函数适用场景

  • 使用宏定义的地方都可以使用inline函数。
  • 内联函数可以作为类成员接口函数来读写类的私有成员或者保护成员。

4.4.1 为什么不能把所有的函数写成内联函数

  • 如果内联函数的代码比较长,尽量就不使用内联函数。因为每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  • 如果内联函数体内出现循环,那么执行函数体内代码的时间要比函数调用的时间开销大。
  • 1.在内联函数内不允许用循环语句和开关语句。内联函数只适合于只有1~5行的小函数。
  • 2.递归函数不能定义为内联函数
  • 3.内联函数只能先定义后使用,否则编译系统也会把它认为是普通函数。
  • 4.对内联函数不能进行异常的接口声明。

5、重载overload,覆盖(重写)override,隐藏(hide)的区别

重载

将语义相近的几个函数用同一个名字表示,主要特点是函数名相同,但是参数的个数或类型或顺序不同,这就是函数重载,函数的返回值不可以作为函数重载的条件

重写:

指的是在派生类中覆盖基类中的同名函数,要求基类函数必须是虚函数,且与基类的虚函数有相同的参数个数、参数类型、返回值类型

virtual void fun(int);
void fun(int);
1.如果一个成员函数后面跟了一个override关键字,说明这个函数将重写这个函数
2.继承父类但是必须重写父类的一个函数的时候使用这个声明,否则报错。
3.后面加override算是一种声明,此函数要重写同名函数,所以如果将函数的名字写错了比如写成f00则会报错,这样也可以提醒代码阅读者这是一个重写的函数。

class A{
    virtual void foo();
}

class B :public A{
    void foo() override; 
}

隐藏

  • 被隐藏的可以通过作用域访问,重写不能通过作用域访问。指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数

  • 若两个函数参数相同,但是基类函数不是虚函数,则会被隐藏。和重写的区别在于基类函数是否是虚函数

void fun(int);
void fun(int);
  • 若两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏,两个函数在同一个类中。和重载的区别在于两个函数不在同一个类中
(virtual) void fun(int);
void fun(float);
C++有两种调用被隐藏的函数:
1.using关键字
2.用域操作符::,可以调用基类中被隐藏的所有成员函数和变量
class Father {
public:
	virtual void test1() { cout << "father test1!" << endl; }
	virtual void test2() { cout << "father test2!" << endl; }
};
class Child : public Father{
public:
	virtual void test1() { cout << "child test1!" << endl; }
	virtual void test2(int i) {cout << "child test2!" << endl;}
	void test3() { cout << "child test3!" << endl; }
};
int main(int argc, char* argv[]) {
	Father* f = new Child();
	f->test1();
	f->test2();
	//f->test2(1); //父类指针不能调用子类非重写函数
	//f->test3();
	//f->Child::test3();

	cout << "-------------------------"<< endl;

	Child c;
	c.test1();
	//c.test2(); //不能访问基类的同名函数,被子类隐藏
	c.Father::test2(); //加作用域访问

	system("pause");
	return 0;
}

6、new、delete、malloc、free之间的关系

6.1 既然有了malloc/free,C++中为什么还需要new/delete呢?

相同点

  • 都可用于内存的动态申请和释放

不同点

  • new/delete是C++ 运算符,malloc/free是C/C++ 标准库函数
  • 【构造和析构】malloc仅仅是分配内存,free仅仅是回收内存,不具备调用对象构造函数和析构函数功能,故用malloc分配空间存储类的对象存在风险;而new和delete还会调用对象构造函数和析构函数。虽然new中封装了malloc,直接free不会报错,但是这只是回收内存,而不会调用对象的析构函数
  • 【内存空间计算】new自动计算要分配的空间大小,malloc需要手工计算,而且malloc对开辟的空间大小严格指定,而new只需要对象名
  • 【返回指针类型】malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。

6.2 delete和delete[]的区别

  • delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数, delete[]时,数组中的元素是按逆序来进行析构;
  • 用new分配的内存用delete释放,用new[]分配的内存用delete[]释放

7、函数指针?

  • 函数指针指向的是特殊的数据类型,函数的数据类型是由其返回的数据类型和其参数列表共同决定的。

为什么有函数指针

  • 我们希望在同一个函数中通过使用相同的形参在不同的时间产生不同的效果。
  • 函数指针允许将函数作为变量传递给其他函数

一个函数名就是一个指针,它指向该函数的地址。函数地址是该函数的进入点,也就是调用该函数的地址,而函数的调用可以通过函数名,也可以通过指向该函数的指针来调用。
两种方法赋值:

指针名 = 函数名; 指针名 = &函数名

//声明
double (*pf) (int a, int b); //函数指针:pf是一个指向函数的指针
double* pf (int a, int b); //指针函数:pf()是返回值为指针类型的函数

//调用
void func(int a, double (*pf) (int a, int b); // 函数原型
func(50, func1); //调用。func1函数名,函数的入口地址
int* p[n]; //指针数组:数组中元素为n个指针
int (*p)[n]; //数组指针:p是指向n个int类型的指针变量

8、虚函数、纯虚函数、抽象类、虚继承

  • 虚函数:利用父类指针访问子类中的虚函数,这种情况下采用的是动态绑定技术
  • 纯虚函数:纯虚函数是在基类中声明的虚函数,它在基类中没有定义但要求任何子类都要定义纯虚函数不能实例化对象
  • 抽象类: 称带有纯虚函数的类为抽象类。抽象类只能作为基类来使用,其抽象类中的纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类

纯虚函数的引入,是出于两个目的:

  • 1、为了安全,提醒子类去做应做的重写
  • 2、为了编码的效率开闭原则:对拓展进行开放,对修改进行关闭

8.1 虚函数是怎么实现的

当类中存在虚函数时,内存中保存的是一个虚函数表指针,这个虚函数表指针指向了一个虚函数表(vftable),而这个虚函数表中存放的是类中虚函数的地址。
在出现继承的情况下,虚函数表指针也被继承了,所以在子类中依然存在一个虚函数表指针指向一个虚函数表,在虚函数表中存放的先是来自父类的虚函数地址,而后是子类中同名的虚函数覆盖了父类的虚函数地址。

8.2 子类继承父类,父类构造函数为什么一般不定义为虚函数?

(1.实际类型确定不了 2.虚函数表指针存放在内存空间中)

  • 在构造对象时必须先确定对象的实际类型,但是在调用构造函数时,在运行期间确定实际类型确定不了,这是因为虚函数行为是在运行期间确定实际类型的,也就是编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,故无法构造对象
  • 在构造对象时,首先会调用构造函数,若构造函数是虚函数,会调用虚函数表指针,而虚函数表指针存放在内存空间中,由于对象还未创建,故没有内存空间,就不能调用虚函数表指针,也就不能构造对象

8.3 子类继承父类,父类析构函数一般写成虚函数的原因(有继承关系就需要) ?

在类的继承中,如果有父类指针指向子类对象,那么父类指针释放时,如果父类析构函数不定义成虚析构,只会调用父类的析构函数而不调用子类析构函数,这样就会造成子类对象析构不完全,造成内存泄漏

1.一般规律是,只要类中的任何一个函数是虚函数,那么析构函数也应该是虚函数。(就算基类中没有虚函数,析构函数也应该设为虚析构)
2.在多态当中,一定要将基类的析构函数设置为虚函数并将其实现,只有这样,才能够达到按对象构造的逆序来析构对象;

子类析构时,要调用父类的析构函数吗?

析构函数调用的次序是先派生类后基类的。和构造函数的执行顺序相反。并且析构函数要是虚的

8.4 虚继承

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费且毫无意义。
  • 虚继承:为了解决多继承时的命名冲突和冗余数据问题,使得在派生类中只保留一份间接基类数据
  • 从父类中拿到的只是一个虚基类指针

9、静态绑定和动态绑定

静态绑定和动态绑定

  • 静态绑定:绑定的是对象的静态类型,静态类型是对象在声明时采用的类型,在编译时确定
  • 动态绑定:绑定的是对象的动态类型,动态类型是指一个指针或引用所指对象的类型,是在运行期确定的。只有虚函数才使用的是动态绑定,其他的全部是静态绑定。

静态绑定和动态绑定的区别:

  1. 静态绑定发生在编译期,动态绑定发生在运行期
  2. 对象的动态类型可以更改,但是静态类型无法更改;
  3. 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;

9.1、引用是否能实现动态绑定,为什么引用可以实现

可以。因为引用或指针既可以指向基类对象也可以指向派生类对象,这一特点是动态绑定的关键。用引用或指针调用的虚函数在运行时确定,被调用的虚函数所在对象类型是引用或指针所指对象的实际类型所决定的。

10、堆和栈的区别

  • 栈stack:存放函数的参数值、局部变量,由编译器自动分配释放
  • 堆heap:是由new分配的内存块,程序员手动利用delete释放,如果没有,程序结束后,操作系统自动回收
  • 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方式是向下的,是向着内存地址减小的方向增长。

堆快一点还是栈快一点?

栈快一点

  • 因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
  • 堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适的内存空间。并且获取堆内存的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址来访问内容,因此堆比较慢。

11、const和static的作用

11.1 static的作用

(1.隐藏 2.持久性)

  1. 隐藏:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。(static函数,static变量均可)
  2. 保持变量内容的持久性
  3. static是默认初始化为0(static变量)
  • 函数体内: static 修饰的局部变量作用范围为该函数体,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
  • 模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内
  • 类中修饰成员变量:表示该变量属于整个类所有,对类的所有对象只有一份拷贝。static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;
  • 类中修饰成员函数:表示该函数属于整个类所有,没有this指针,只能访问类中的静态成员变量

static成员函数不能被virtual修饰

  • 1.static成员函数不属于任何对象或实例,所以声明为虚函数没有任何实际意义;
  • 2.而且静态成员函数没有this指针,而虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能被virtual修饰;虚函数的调用关系,this->vptr->ctable->virtual function

11.2 const的作用

  1. C++中的const的是将对象和函数声明为只读状态的一个关键字。
  2. const可以与基本内置类型使用,此时const定义的变量必须初始化且之后就不能改变,编译器会将用到该变量的地方都替换成变量的值。
  3. const可以修饰自定义对象,常对象中的成员变量不可以被修改,且只能调用常成员函数。(常对象只能调用常函数)
  4. const可以与引用使用,可以将常量引用绑定在临时量上。因此,我们可以将函数的返回值声明成常量引用:
  5. const可以与指针使用,有指针常量和常量指针两种
  6. const可以修饰函数的参数,通常传递常量引用时使用。const形参可以接收const和非const类型的实参
  7. const可以修饰函数的返回值,通常在返回指针和引用时使用。
  8. const可以修饰成员函数,可以修改指针常量this的类型,使常对象可以调用常成员函数。但不可以改变函数中的值,可以用mutable关键字,声明的变量可以在const成员函数中被修改
  9. const可以修饰成员变量,不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;

static和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象

12 形参与实参的区别?

  • 形参变量只有在被调用时才分配内存单元,在函数调用结束时, 即刻释放所分配的内存单元。
  • 当形参不是引用和指针类型时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份。形参的值发生改变时,并不会影响实参。

12.1 值传递、指针传递、引用传递的区别和效率

  • 值传递:函数形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象或是大的结构体对象,将消耗一定的时间和空间。
  • 指针传递:函数形参也是向函数所属的栈拷贝数据的过程,但拷贝的数据是一个指针变量大小的地址
  • 引用传递:只是为该数据所在的地址起了一个别名。

13 全局变量和局部变量有什么区别?

  • 生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在函数内部存在,退出就不存在;
  • 使用方式不同:通过声明后全局变量在程序的各个部分都可以用到局部变量分配在堆栈区,只能在局部使用
  • 操作系统和编译器通过内存分配的位置可以区分两者:全局变量分配在全局数据段并且在程序开始运行时被加载。局部变量则分配在堆栈区

14 全局变量和static变量的区别

  • 非静态全局变量:作用域是整个源程序,当一个源程序由多个源文件组成时,非静态全局变量在各个源文件中都是有效的
  • 静态全局变量:则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它,因此可以避免在其他源文件中引起错误。

15、STL中的vector的实现,是怎么扩容的?

  • vector就是一个动态增长的数组,里面有一个指针指向一块连续的空间,当空间装不下的时候,并不是在原有的空间上持续新的空间(因为无法保证原空间的后面还有可供配置的空间),而是会申请一块更大的空间,将原来的数据拷贝过去,并释放原来的旧空间。对于普通数组而言则是配置了静态空间,不能改变其空间大小。在VS下是1.5倍扩容,在GCC下是2倍扩容

15.1 resize、reserve、reverse(algorithm)的区别

resize 重新指定容器的长度(大小)

v1.resize(15);
//重新指定容器的长度为15,若容器变长,则以默认值0填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除。

v1.resize(20,10);  //20个10 
//重新指定容器的长度为20,若容器变长,则以10值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除

vector预留空间(reserve 预定)

  • 1.减少vector在动态扩容时的扩展次数,如果数据量较大,可以一开始利用reserve预留空间
  • 2.vector预留的空间不可初始化,容器类元素不可访问
v.reserve(10000);   //容器预留10000个元素长度(是空地,不是0初始化)

reverse(反转)

//将容器内元素进行反转
reverse(v.begin(), v.end());
// beg 开始迭代器 ,end 结束迭代器

15.1 push_back()和emplace_back()的区别

emplace_back()函数是 C++ 11 新增加的,其功能和 push_back() 相同,都是在 vector 容器的尾部添加一个元素。

emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同

  • push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中
  • emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程
  • emplace_back() 的执行效率比 push_back() 高

15.3 vector和list的区别

  • 1.vector拥有一段连续的内存空间,因此支持随机存取,但是插入和删除的效率不高,会造成内存块的拷贝,时间复杂度为o(n)。
  • 2.list拥有一段不连续的内存空间,因此不支持随机存取,由于通过指针访问数据,时间复杂度为o(n)。由于链表的特点,能高效地进行插入和删除。

16、STL中unordered_map和map的区别

  • map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是O(logN)
  • 而unordered_map不会根据键值进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据键值的hash值判断元素是否相同,即unoredered_map内部是无序的

17、深拷贝和浅拷贝的区别

浅拷贝

浅拷贝只是拷贝一个指针,并没有新开辟新的内存空间,拷贝的指针和原来的指针指向同一块内存空间,如果原来的指针所指向的内存空间被释放了,那么再释放浅拷贝的指针所指向的内存空间就会报错。

深拷贝

深拷贝不仅拷贝值,还开辟出一块新的内存空间用来存放拷贝值,即使原先的对象被析构掉,释放了内存空间,也不会影响到深拷贝得到的值。所以在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己来实现深拷贝。

18、什么情况下会调用拷贝构造函数(三种情况)

系统自动生成的构造函数:普通构造函数拷贝构造函数 (在没有定义对应的构造函数的时候)

  • 用类的一个对象去初始化另一个对象的时候
  • 当函数的参数是类的对象时,值传递的时候,如果是引用传递则不会调用
  • 当函数的返回值是类的对象的时候,值返回

19、C++的四种强制转换

1.static_cast

  • 用来进行“比较自然”和低风险的转换,比如整形和浮点型、字符型之间互相转换。
  • 不能用于在不同类型的指针之间互相转换,也不能用于不同类型的引用之间的转换,也不能用于整型和指针之间的互相转换,

2.reinterpret_cast(重新解释)

  • 用来进行各种不同类型的指针之间的转换、不同类型的引用之间转换、以及指针和能容纳得下指针的整数类型之间的转换。转换的时候,执行的是逐位拷贝的操作。

3.const_cast

  • 用来进行去除const属性的转换。将const引用转换成同类型的非const引用,将const指针转换为同类型的非const指针

4.dynamic_cast

  • 专门用于多态基类的指针或引用,强制转换为派生类的指针或引用,而且能够检查转换的安全性。对于不安全的指针的转换,转换结果返回空(nullptr)。
  • 不能用于非多态基类的指针或引用,强制转换为派生类的指针或引用。

异常处理

通常的做法是:在预计会发生异常的地方,加入相应的代码,但是这种做法并不总是适用的。

//对文件A进行了相关的操作
fun(arg ,);//可能发生异常
…….trycatch处理异常

如果一个函数在执行的过程中,抛出的异常在本函数内就被catch块捕获并处理了,那么该异常就不会抛给这个函数的调用者(也称“上一层的函数”);如果异常在本函数中没被处理,就会被抛给上一层的函数。

19.1 继承机制中对象之间如何转换?指针和引用之间如何转换?

向上类型转换

派生类指针或引用转换为基类的指针或引用,向上类型转换会自动进行,而且向上类型转换是安全的

向下类型转换

基类指针或引用转换为派生类指针或引用,向下类型转换不会自动进行,而且向下类型转换不是安全的,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。RTTI技术,用dynamic_cast进行向下类型转换。

19.2 static_cast比C语言中的转换强在哪里?

  • 更加安全
  • 直接明显,能够一眼看出是什么类型转换为什么类型,容易debug;

20、调试程序的方法

  • windows下直接使用vs的debug功能
  • linux下直接使用gdb,我们可以在其过程中给程序添加断点,监视等辅助手段,监控其行为是否与我们设计相符

21、extern“C”作用

extern "C"的主要作用指示编译器这部分是按照C语言的进行编译,而不是C++ 的。就是为了能够正确实现C++ 代码中调用其他C语言代码

哪些情况下使用extern “C”:

  • C++代码中调用其他C语言代码;
  • 在C++中的头文件中使用;
  • 在多个人协同开发时,可能有人擅长C语言,而有人擅长C++;

22、map、set是怎么实现的

  • map/set的底层都是以红黑树的结构来实现的,他们可以完成高效的插入删除,其中插入删除的时间复杂度都在对数阶O(logN)

红黑树是怎么能够同时实现这两种容器?

  • 实现map的红黑树的节点数据类型是键值+实值,而实现set的节点数据类型只有实值 (set的key和value其实是一样的)

为什么使用红黑树?

  • 因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低。

23、引用作为函数参数以及返回值的好处

  • 函数内部可以对参数进行修改
  • 与传值比较)当函数参数传递的数据量比较大时,用引用比直接传值效率更高,因为引用没有了传值和生成副本的时间和空间消耗,能提高函数调用和运行的效率。
  • 与传指针比较)指针作为函数的参数虽然也能达到和引用的一样效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"指针变量名"的形式进行运算,这样很容易产生错误且程序可读性变差;

23.1、引用作为函数返回值的限制

  • 不能返回局部变量的引用。因为函数结束以后局部变量就会被销毁(可以返回局部变量的拷贝
  • 不能返回函数内new分配的引用。因为如果被函数返回的引用只是作为一 个临时变量来使用(也就是作为右值来使用),而没有被赋予一个实际的变量,那么这个引用所指向的由new分配的空间就无法释放,就会造成内存泄漏
  • 可以返回类成员变量的引用,但是最好是const。因为如果其他对象可以获得该变量的非const的引用,就会导致类成员的变量被修改

24、空指针、野指针、悬空指针、智能指针

  • 空指针是指指向地址为nullptr的指针变量
  • 悬空指针是指最初指向的内存已经被释放了的一种指针变量。
  • 野指针是指因为没有初始化等原因指向一处随机或者无效的地址的指针变量,对于指针初始化通常是赋值为 nullptr
  • 智能指针:C++智能指针的本质就是避免悬空指针的产生

24.1 野指针、悬空指针产生原因:

  • 悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。
  • 野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。

25、什么是内存泄漏?

内存泄漏动态分配内存空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。程序员失去了对该内存的控制

25.1 C++中内存泄漏的几种情况

  • new/delete)类的构造函数和析构函数中new和delete没有配套
  • 析构函数定义为虚函数)没有将基类的析构函数定义为虚函数,当父类指针指向子类对象时,那么子类的析构函数将不会被调用,子类的数据没有正确释放,造成内存泄露

25.2 避免内存泄露的几种方式

  • 计数法:使用new或者malloc时,让计数+1,delete或free时,让计数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露(类似于智能指针的引用计数
  • 一定要将基类的析构函数声明为虚函数
  • new一个对象数组,在释放时一定要用delete []

26、栈溢出的原因以及解决方法

栈的大小通常是1M-2M,所以栈溢出包含两种情况:

  • 一是分配的的大小超过栈的最大值,例如:1.函数调用层次过深,每调用一次,函数的参数、局部变量等信息就被压入一次栈,就会导致超过栈的最大值 2.局部变量体积太大。
  • 二是分配的大小没有超过最大值,但是接收的buffer(缓冲区)比原buffer小

解决办法:

  • 1 增加分配的栈内存的大小;如果是不超过栈大小但是分配值小的,就增大分配的大小
  • 2 使用堆内存;使用动态分配内存
  • 3 局部变量变成全局变量:直接在定义前边加个static,直接变成静态变量

27、友元函数和友元类

  • 作用:友元提供了不同类的成员函数之间类的成员函数和一般函数之间进行数据共享的机制。通过友元,另一个类中的成员函数可以访问该类中的私有成员和保护成员。
  • 优缺点:友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

全局函数做友元(友元函数)

  • 友元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员。但是需要在类的定义中声明所有可以访问它的友元函数
    1. 全局函数类内实现(建议)——直接在类内声明友元(friend)即可,对象不可调用全局函数
    1. 全局函数类内声明类外实现——需要提前让编译器知道全局函数的存在

类做友元(友元类)

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。

其他类的成员函数做友元

27.1 使用友元类时注意

  • 友元关系不能被继承
  • 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
  • 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

28、类初始化定义函数,静态成员变量初始化

  • 赋值初始化:通过在函数体内进行赋值初始化;
  • 列表初始化:在冒号后使用初始化列表进行初始化。

这两种方式的主要区别在于:

  • 赋值初始化:是在函数体内进行初始化,是在所有的数据成员变量被分配内存空间后才进行的。
  • 列表初始化:是给数据成员变量分配内存空间时就进行初始化,也就是说此时函数体还未执行,只要冒号后面有此数据成员变量的括号赋值表达式,那么在分配了内存空间后在进入该函数体之前进行初始化。
  • 赋值初始化是在构造函数当中做赋值的操作,而C++的赋值操作是会产生临时对象的,临时对象的出现会导致程序的运行效率降低;
  • 列表初始化是做纯粹的初始化操作

28.1 声明、定义、初始化

  • 声明:是告诉编译器变量的类型和名字,不会为变量分配空间
  • 定义:需要分配空间,同一个变量可以被声明多次,但是只能被定义一次
  • 初始化:为该成员变量赋初值。

28.2 静态成员变量初始化

  • 静态成员变量不能在类内初始化,一般在类外和main()函数之前初始化,缺省时初始化为0。因为静态成员变量整个类所有,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。
  • 静态成员变量存储在静态存储区,其定义过程是在编译时完成的。
class A{
public:
  static int a;
}
int A::a;  //类外定义,没有初始化(默认初始化为0)
int A::a=0;//类外定义,并进行初始化。

28.3 成员初始化列表的概念,为什么用它会快一些?

  • 如果在构造函数内进行赋值初始化操作,对于内置数据类型几乎没什么影响,但如果有些成员变量是属于某个类,那么在进入构造函数之前,会先调用该类默认构造函数,进入构造函数后所做的事其实是一次赋值操作(临时对象产生)。故其赋值初始化是调用一次默认构造加一次赋值操作
  • 而由于构造函数的初始化列表操作发生在进入构造函数之前,故初始化列表只是做一次赋值操作

28.4 成员初始化列表会在什么时候用到?它的调用过程是什么?

  • 当初始化一个引用成员变量时;
  • 初始化一个const成员变量时;
  • 当调用一个基类的构造函数,而构造函数拥有一组参数时;
  • 当调用一个成员类的构造函数,而构造函数拥有一组参数;

调用过程

编译器会逐个操作初始化列表,以在类中的成员变量声明顺序,而不是初始化列表中的排列顺序,来进行构造函数中的初始化操作。

29、智能指针

智能指针的作用:是管理一个指针,存在以下情况:由于申请的内存空间在函数结束时忘记释放,造成内存泄漏。而智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放。

auto_ptr

  • 采用所有权模式,独占式拥有概念。(c++ 98的方案,c++ 11已经抛弃)
  • 例如:p1指向一块堆区的内存,把p1赋值给p2,则p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。故auto_ptr存在潜在的内存泄漏问题

unique_ptr(替换auto_ptr)

  • 采用所有权模式。unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免内存泄露问题特别有用。
  • 如果源unique_ptr是个临时右值,编译器允许把一个临时智能指针赋给一个持久的;如果源unique_ptr是持久智能指针,编译器将禁止这么做。但是C++有一个标准库函数std::move(),能够将一个持久的unique_ptr赋给另一个unique_ptr

shared_ptr

  • shared_ptr实现共享式拥有概念。就是多个智能指针可以同时指向一个对象,它是在使用引用计数的基础上提供了可以共享所有权的智能指针。该对象和其相关资源会在“最后一个引用被销毁”时候释放。
  • shared_ptr 是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的)。

weak_ptr

  • weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象,而对该对象的内存管理的是那个强引用的shared_ptr,它只是提供对对象内存管理的一个访问手段它的构造和析构不会引起引用计数的增加或减少
  • weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用, 那么这两个指针的引用计数永远不可能下降为0,对象和资源永远不可能释放。而weak_ptr是对对象的一种弱引用,不会增加对象的引用计数。
  • weak_ptr和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给weak_ptr,而weak_ptr可以通过调用lock函数来获得shared_ptr。

注意通过weak_ptr不能直接访问对象的成员函数需要转换为shared_ptr

30 C++中struct和class的区别

相同点

  • 两者都拥有成员函数、公有和私有部分。任何可以使用class完成的工作,同样可以使用struct完成

不同点

  • class默认是私有的,struct默认是公有的
  • 在继承上,class默认是私有继承,而struct默认是公有继承

30.1 C++ 和 C 的struct区别

  • C 中struct是用户自定义数据类型(UDT); C++ 中struct是抽象数据类型(ADT)
  • C 中struct是没有权限的设置的,只是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数;而 C++ 成员可以是函数,能实现继承和多态

31 数组名和指针(这里为指向数组首元素的指针)区别?

相同点

  • 二者均可通过增减偏移量来访问数组中的元素。
int arr[10];
int* p;
*(arr+1) <=> arr[1]
*(p+1)   <=> p[1]

不同点

arr++; //数组名没有自增自减
p++;
  • 数组名不是真正意义上的指针,没有自增、自减等操作。当数组名作为函数参数传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof数组名不能再得到原数组容量的大小。
  • sizeof数组名可以计算出数组的容量大小;sizeof指针得到的是一个指针变量的大小,而不是指针所指向的数组容量大小。

32 迭代器:++ it、it ++哪个好,为什么

  • 前置返回一个引用,后置返回一个对象
  • 前置不会产生临时对象,后置必须产生临时对象,临时对象会导致程序运行效率降低
  • 优先级:前置(++ ,–)> 赋值(=)> 后置(++ ,–)
a=i++,先a=i=4,再i=i+1=5,故a=4,i=5
a=++i,先i=i+1=5,再a=i=5,故a=5,i=5
++i比i++效率高
// ++i实现代码为:(前置)
int& operator++() {
  *this += 1;
  return *this;
} 

//i++实现代码为: (后置)              
int operator++(int) {
    int temp = *this;                   
   ++*this;                       
   return temp;                  
}

33 C/C++程序编译流程(预处理->编译->汇编->链接)

33.1 预处理

图片名称

1.对编译预处理指令(以#开头的指令)进行处理

  • 展开所有的宏定义
  • 处理所有的条件编译指令,如:“#if”、“#ifdef”、“#elif”、“#else”、“#endif”等。这些伪指令的引入,使得程序员可以通过定义不同的宏定义,来决定编译程序对哪部分代码进行处理。
  • 处理“#include”编译预处理指令,将被包含的文件插入到该编译预处理指令的位置。
    (注意:这个过程可能是递归进行的,也就是说被包含的文件可能还包含其他文件)

2.删除所有的注释

3.添加行号和文件名标识

  • 用于编译器产生行号信息,以方便调试和编译过程中的错误或警告的定位

4.保留所有的#pragma once预编译指令,是为了防止有文件被重复引用

33.2 编译

将编译预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码.s文件。

33.2.1 动态编译与静态编译

  • 静态编译:编译器在编译可执行文件时,需要把用到的对应的动态链接库中的部分提取出来,连接到可执行文件中,使可执行文件在运行时不依赖于动态链接库;
  • 动态编译:编译器在编译可执行文件时需要附带一个动态链接库,在执行可执行文件,就需要调用其对应的动态链接库的命令。
    • 优点是一方面是缩小了可执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。
    • 缺点一是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的动态链接库;是如果其他计算机上没有安装对应的动态链接库,那么用了动态编译的可执行文件就不能运行。

33.3 汇编

将编译完的汇编代码.s文件翻译成机器指令,并生成目标程序的.o文件,该文件为二进制文件,字节编码是机器指令。每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的词法分析、语法分析、语义分析,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译即可。

33.4 链接

通过链接器将不同的源文件产生的目标文件.o进行链接,从而形成一个可以执行的程序。

33.4.1 静态链接和动态链接

  • 动态链接使用动态链接库。
  • 静态链接使用静态链接库。

区别

  • 静态链接对函数库的链接是放在编译期完成的;动态链接对动态库是在程序运行时才被载入。
  • 静态库移植方便,但对程序的更新、部署会带来麻烦;而动态库是在程序运行时才被载入,只需要更新动态库即可,增量更新
  • 静态链接浪费了空间和资源,因为静态链接是将所有相关的目标文件与函数库都会被链接合成一个可执行文件中;而动态链接只需要有一份该共享库的实例,规避了空间浪费的问题。

请添加图片描述
请添加图片描述请添加图片描述

34 C++的内存分区?

C++中的内存分区,分别是堆、栈、全局/静态存储区、常量存储区和代码区。

图片名称
  • :指那些由编译器在需要的时候分配,不需要时手动清除的变量所在的存储区,效率高,分配的内存空间有限,形参和局部变量分配在栈区,栈是向低地址生长的数据结构,是一块连续的内存
  • :就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收
  • 全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量和静态变量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区,在该区定义的变量若没有初始化,则会被自动初始化,例如int型变量自动初始为0
  • 常量存储区:这是一块比较特殊的存储区,这里面存放的是字符串常量、const修饰的全局变量存储在常量区,const修饰的局部变量在栈区,不允许修改
  • 代码区:存放函数体的二进制代码

35 C++中类的成员变量和成员函数内存分布情况?

首地址、静态成员函数

  • 一个类对象的地址就是类所包含的内存空间的首地址,这个首地址也就对应具体某一个成员变量的地址。在定义类对象的同时这些成员变量也就被定义了。
  • 静态成员函数与一般成员函数的唯一区别就是没有this指针,因此不能访问非静态成员变量,因为非静态成员变量属于类的对象上。
  • 所有函数都存放在代码区,静态函数也不例外。有人一看到 static 这个单词就主观的认为是存放在全局存储区,那是不对的。
  • 一般成员函数(非静态成员函数)也只有一份函数实例。
  • C语言的结构体只有成员变量

36 this指针

  • this指针是类的指针,指向对象的首地址。this指针存储位置会因编译器不同而不同。可能是栈,也可能是寄存器,甚至全局变量。
  • this指针只能在非静态成员函数中才有定义,在全局函数、静态成员函数中都不能用this指针。
  • this指针在普通成员函数开始前构造,在普通成员函数结束后清除

37 类对象的大小受哪些因素影响?

  • 类的非静态成员变量的数据类型大小之和
  • 为了内存对齐加入的填充。
  • 如果有虚函数的话,需要加入指向虚函数表的虚函数表指针
  • 如果该类是某类的派生类,需要加入继承下来的基类中的非静态成员变量

空类(无非静态成员变量)的对象的size为1。

38 C++ 11有哪些新特性?

  • Lambda 表达式(匿名函数)
  • 引入了 auto 和 decltype 这两个关键字实现了类型推导
  • nullptr替代 NULL
  • 基于范围的 for 循环for (auto& i : res){}
  • 类和结构体的中初始化列表
  • std::forward_list(单向链表)
  • 右值引用、移动语义(std::move)、完美转发(std::forward)

38.1 Lambda 表达式(匿名函数)

  • 利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象
[capture list] (params list) mutable exception-> return type { function body }
  • capture list:捕获外部变量列表
  • params list:形参列表
  • mutable指示符:用来说用是否可以修改捕获的变量
  • exception:异常设定
  • return type:返回类型
  • function body:函数体

38.2 auto、decltype和decltype(auto)的用法

auto

原理:auto被称作类型说明符,主要是让编译器通过初始值来进行类型推演。auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。有点类似于模板类型推导

  • auto作为函数返回值时,只能用于定义函数,不能用于声明函数
  • auto仅仅是一个占位符,它并不是一个真正的类型
  • auto 定义的变量必须有初始值
  • auto与复合类型的搭配 ①数组名会被转换成首元素的指针,②引用会被去掉,③指针会被保留,④顶层const会被去掉,底层const会被保留
auto value4 = "QAQ", value5 = &value1;
//在编译时出现了报错,原因是因为 value4 推断出的类型是字符串型,而 value5 
//推断出来的类型是指针类型,一条语句在声明多个变量的时候,只能有一个基本数据类型

decltype

原理与auto的区别在于编译器只是分析表达式并得到它的类型,而不会实际去计算表达式的值,并不是用表达式的值去初始化变量。decltype的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低

decltype可以作用于变量、表达式及函数名

  • ①作用于变量直接得到变量的类型(包括顶层const和引用会被保留,不负责把数组名会被转换成首元素的指针);
  • ②作用于表达式,结果是左值的表达式得到该类型的引用,结果是右值的表达式得到该类型;
  • ③作用于函数名会得到函数类型,不会自动转换成指针。

另外decltype ((variable)) (注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身是一个引用的时候才是一个引用。

38.3 为什么用nullptr替代 NULL

  • 在C语言中,NULL被定义为指针(void*)0,而在C++语言中,NULL则被定义为整数0
  • nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成函数参数传递错误。

38.4 C++左值引用和右值引用?

  • 左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象
  • 一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。 右值虽然无法获取地址,但是右值的右值引用可以获取地址,该地址表示临时对象的存储地址。
  • 左值引用和右值引用都必须立即进行初始化
  • 右值引用通常不能绑定到任何的左值,要想绑定一个左值,通常需要std::move()将左值强制转换为右值。
  • 非const左值引用:只能绑定到非const左值。不能绑定到const左值、非const右值和const右值;
  • const左值引用:可以绑定到const左值、非const左值、const右值、非const右值。(不过const左值所绑定的右值只能是只读的
  • 非const右值引用:只能绑定到非const右值;(不能将一个右值引用绑定到任何左值上
  • const右值引用:可绑定到const右值和非const右值。
template<typename T>
void testForward(T&& v) {
	print(v);
	print(std::forward<T>(v));
	print(std::move(v));
}
int x = 1;
testForward(x); //特例:将一个右值引用绑定到左值上
//关于多引用的“折叠”:上面得出函数的实参为X& && - X的左值引用的右值引用
//通过“折叠”规则,会得出函数的实参为X&:
X& &   -> X&
X&& &  -> X&
X& &&  -> X&
X&& && -> X&&

模板类型参数T的推断:若传给函数一个X类型的左值,且函数的参数是模板类型参数T的右值引用T&&时,T会被推断为X&,而不是X;

38.5 C++ RAII机制

  • RAII是Resource Acquisition Is Initialization(wiki上面翻译成 “资源获取就是初始化”)的简称,是C++ 语言的一种管理资源、避免泄漏的惯用法。利用的就是C++ 构造的对象最终会被销毁的原则

如何使用RAII?

  • 由于系统的资源不具备自动释放的功能,而C++的类具有自动调用析构函数的功能。如果能把资源用类进行封装,对资源的操作都封装在类的内部,那么就可以用析构函数来释放资源

38.6 C++11的移动语义和完美转发

移动构造

  • 我们可以定义一个右值引用将一个右值(将亡值)的内存资源获取。特别是在拷贝构建一个对象时候,如果传入参数是一个右值,那么我们就可以直接引用这个右值,无需开辟资源深拷贝。这种做法称之为移动构造

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kx1RoVWW-1663917791488)(https://note.youdao.com/yws/api/personal/file/CE8AA0FBCF864959B50168619645417B?method=getImage&version=19771&cstk=mbTRW8G-)]

移动语义(std::move)

  • 功能就是将一个左值强制转化为右值,通过右值引用获取该值,实现移动语义。
    MyString s1("Hello World!!!");//左值s1
	MyString&& s2 = std::move(s1);//右值引用s2
	s1 = MyString("你好,世界!!!");//s1仍然是左值

完美转发(std::forward)

  • 完美转发函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。 这样做是为了保留在其他函数中针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。

std::forward被称为完美转发:它的作用是保持原来的值属性不变。通俗的讲就是,如果原来的值是左值,经std::forward处理后该值还是左值;如果原来的值是右值,经std::forward处理后它还是右值。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
int Func(int&& a)
{
	a = 20;//丢失了右值属性的右值引用
	return a;
}
int main()
{
	cout<<Func(10)<<endl;//纯右值
	system("pause");
}
//函数参数是右值,在调用右值引用的函数时,函数参数会丢失其右值属性这并不完美,
//我们需要函数参数在传参之后任然保持其原来的属性才完美
#include <iostream>
#include <vector>
#include <string>

using namespace std;

int main()
{
    string A("abc");
    string&& Rval = std::move(A);
    string B(Rval);    // this is a copy , not move.
    cout << A << endl;   // output "abc"
    string C(std::forward<string>(Rval));  // move.
    cout << A << endl;       /* output "" */
    return 0;
}

39 什么是STL?

  • C++ STL从广义来讲包括了三类:容器、算法、迭代器
  • 算法:包括遍历、查找、排序等常用算法,以及不同容器特定的算法。
  • 容器:就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。
  • 迭代器:就是在不暴露容器内部结构的情况下对容器的遍历。

40 STL迭代器如何实现

  • 原理:迭代器内部保存了一个与容器相关联的指针,然后重载各种运算符来遍历。这和C++中的智能指针很像,智能指针也是将一个指针进行封装,然后通过引用计数或是其他方法完成自动释放内存的功能。
  • 作用:就是在不暴露容器内部结构的情况下对容器的遍历。其次最重要的一个作用就是扮演了容器和算法之间的胶合剂

41 STL中hash_table的实现?

  • hash_set、hash_map、hash_multiset、hash_multimap四个关联容器都是以hashtable为底层来实现的
  • 原理:哈希表底层实现是通过开链法(开链法解决哈希冲突)来实现的,哈希表内的元素称为桶(bucket),而由桶所链接的元素称为节点(node),其中存入桶元素的容器是序列式容器——vector容器。选择vector为存放桶元素的基础容器,主要是因为vector容器具有动态扩容能力,无需人工干预。
  • 当插入键值对时,并不是直接插入该数组中,而是通过对键值进行哈希运算得到哈希值然后和数组容量取模,得到在数组中的位置后再插入。

这样做的好处是在查找、插入、删除等操作可以做到O(1),最坏的情况是O(n),当然这种是最极端的情况,极少遇到。

实现哈希表我们可以采用两种方法:

  • 1、数组+链表
  • 2、数组+二叉树
图片名称

  它通过把关键码(key)值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。给定表M,存在散列函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,映射函数f(key)为哈希函数(散列函数)。

41.1 哈希冲突

对不同的键值可能得到同一哈希地址,即k1≠k2,而f(k1)=f(k2),这种现象称为哈希冲突

哈希碰撞还取决于负载因子。负载因子是存储的键值对数目与数组容量的比值,负载因子决定了哈希表什么时候扩容,如果负载因子的值太大,说明存储的键值对接近容量,增加碰撞的风险;如果值太小,则浪费空间

比如数组容量100,当前存贮了90个键值对,负载因子为0.9。

主要有四类处理冲突的方法:

  • 开链法(常用)
  • 开放定址法(常用)
  • 公共溢出区(不常用)
  • 再Hash法(不常用)

1 开链法

主要思想是基于数组和链表的组合来解决冲突,桶(Bucket)中不直接存储键值对,每个桶都链接一个链表,当发生冲突时,将冲突的键值对插入链表中。

  • 优点在于方法简单,非同义词之间也不会产生聚集现象(相比于开放定址法),并且其空间结构是动态申请的,所以比较适合无法确定表长的情况
  • 缺点是链表指针需要额外的空间,遇到碰撞拒绝服务时会退化为单链表。

2 开放定址法

主要思想是发生冲突时,直接去寻找下一个空的地址,只要底层的表足够大,就总能找到空的地址。这个寻找下一个地址的行为,叫做探测

3 公共溢出区

主要思想是建立一个独立的公共区,把冲突的键值对都放在其中

4 再Hash法

主要思想是有冲突时,换另外一个Hash函数来算Hash值

42 deque和vector的区别

  • deque原理:deque内部有个中控器,中控器维护的是每个缓冲区的地址,缓冲区中存放真实数据,使得使用deque时像一片连续的内存空间。deque没有容量的概念,因为它是以分段连续空间组合而成,可以随时增加一段新的空间并链接起来
  • vector是尾部单向开口的连续线性空间,而deque则是双向开口的连续线性空间。虽然vector也可以在头部进行元素操作,但是对头部的操作 效率十分低下(主要是涉及到整体的移动),但vector访问容器内元素的速度会比deque快。

43 介绍一下几种典型的锁

读写锁

  • 多个读者可以同时进行读
  • 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
  • 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

互斥锁

  • 一次只能一个线程拥有互斥锁,其他线程只有等待

自旋锁

  • 如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。

44、#include<file.h> #include “file.h” 的区别

  • 前者是从标准库路径寻找
  • 后者是从当前工作路径
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇光_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值