这是我的第一篇博客,一起参与掘金新人创作活动,开启写作之路。
1. 重写和重载的区别
重载
- 是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数
- 根据参数列表确定调用哪个函数,重载不关心函数返回类型。
#include<bits/stdc++.h>
using namespace std;
class A
{
void fun() {};
void fun(int i) {};
void fun(int i, int j) {};
};
重写(覆写)
是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
#include<bits/stdc++.h>
using namespace std;
class A
{
public:
virtual void fun()
{
cout << "A";
}
};
class B :public A
{
public:
virtual void fun()
{
cout << "B";
}
};
int main(void)
{
A* a = new B();
a->fun();//输出B
}
重载和重写的区别
- 范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。
- 参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。
- virtual的区别:重写的基类函数必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。
参考来源:C++中重载和重写的区别_菜鸡工坊-CSDN博客_c++中的重载和重写的区别
2. C++内存模型
C++内存模型(内存布局)
内存区域
C++内存分为5个区域:
- 堆 heap :
由new分配的内存块,其释放编译器不去管,由我们程序自己控制(一个new对应一个delete)。如果程序员没有释放掉,在程序结束时OS会自动回收。涉及的问题:“缓冲区溢出”、“内存泄露”
- 栈 stack :
是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。 存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了。
- 全局/静态存储区 (.bss段和.data段) :
全局和静态变量被分配到同一块内存中。在C语言中,未初始化的放在.bss段中,初始化的放在.data段中;在C++里则不区分了。
- 常量存储区 (.rodata段) :
存放常量,不允许修改(通过非正当手段也可以修改)
- 代码区 (.text段) :
存放代码(如函数),不允许修改(类似常量存储区),但可以执行(不同于常量存储区)
总结
根据C++对象生命周期不同,C++的内存模型有三种不同的内存区域:
- 自由存储区,动态区、静态区局部非静态变量的存储区域(栈)
- 动态区:用operator new,malloc分配的内存(堆)
- 静态区:全局变量、静态变量、字符串常量存在位置 参考资料:
- C++内存模型 - MrYun - 博客园 (cnblogs.com)
- C++内存模型_yj_android_develop的博客-CSDN博客
3. 类的访问修饰符,子类是否持有父类的私有成员,子类能否访问父类的私有成员?
1.子类可以间接访问父类私有成员
- 父类的私有成员函数和私有成员变量一样,只有该类内部的其他成员函数可以调用,对外是封蔽的。子类继承了父类的共有函数,且父类的公有函数调用了其内部的私有函数,此时子类调用父类的公有函数便能访问父类的私有成员函数了。
#include <iostream>
using namespace std;
class A
{
public:
void outpulic(); //基类的公有函数
private:
void outprivate(); //基类私有函数
};
void A::outpulic() //基类的公有函数调用了类自身的私有函数
{
outprivate();
}
void A::outprivate() //基类的私有成员函数定义,输出函数名
{
cout<<"outprivate"<<endl;
}
class B:public A
{
};
int main()
{
B b;
b.outpulic();
return 0;
}
运行结果:
outprivate
2.私有成员如何继承?
- 从物理结构上来说,子类确实包含了父类的私有成员,但是我们不能通过正常的渠道访问到他们。
#include<iostream>
using namespace std;
class A
{
private:
int a;
void funa(){cout<<"A"<<endl;}
};
class B:public A
{
public:
int b;
void funb(){cout<<"B"<<endl;}
};
int main()
{
A a;
B b;
cout<<sizeof(a)<<endl<<sizeof(b)<<endl;
}
运行结果:
4
8
对于私有成员的访问,我们可以通过内联汇编获取该函数的入口地址,然后就能顺利访问了。 参考资料:
4. C语言和C++的区别与联系
差异1:
- C语言面向过程,C++面向对象 面向过程:面向过程编程就是分析出解决问题的步骤,然后把这些步骤一步一步的实现,使用的时候一个一个的依次调用就可以了。
- 考虑的是实际地实现.
- 一般从上往下步步求精.
- 最重要的是模块化的思想方法.当程序规模不是很大时,面向过程的方法还会体现出一种优势,因为程序的流程很清楚,按着模块与函数的方法可以很好的组织. 面向对象:面向对象编程就是把问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。
- 封装:把客观事物抽象成类,每个类对自己的数据和方法进行访问权限保护
- 继承:可使用现有类的所有功能,在无需重新编写原来的类的情况下对这些功能进行扩展。
- 多态:可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,面向对象的核心,多态的目的则是为了接口重用。
面向过程就是自顶向下的编程(步骤划分)、面向对象就是高度实物抽象化(功能划分)
差异2:
语法上
- c++具有重载,继承,多态三种特性。
- c++增加了许多类型安全功能(如强制类型转换)
- 类型安全代码指访问被授权可以访问的内存位置。
- 例如,类型安全代码不能从其他对象的私有字段读取值。它只从定义完善的允许方式访问类型才能读取。 c++中:
- (1)操作符new返回的指针类型严格与对象匹配,而不是void*;
- (2)C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
- (3)引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换;
- (4)一些#define宏可被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全;
- (5) C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比 static_cast涉及更多具体的类型检查。
基于上述的例子,二者的优劣总结如下:
- 面向过程语言
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
- 面向对象语言:
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低
参考资料:
5. C++重载、重写和重定义的区别
- 重载:函数名相同,函数的参数个数、参数类型或参数顺序三者中必须至少有一种不同。函数返回值的类型可以相同,也可以不相同。发生在一个类内部,不能跨作用域。
- 重写:也叫做覆盖,一般发生在子类和父类继承关系之间。子类重新定义父类中有相同名称和参数的虚函数。(override)
- 重定义:也叫做隐藏,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,指派生类的函数屏蔽了与其同名的基类函数。可以理解成发生在继承中的重载。
如果一个派生类,存在重定义的函数,那么,这个类将会隐藏其父类的方法,除非你在调用的时候,强制转换为父类类型,才能调用到父类方法。否则试图对子类和父类做类似重载的调用是不能成功的。
重写需要注意:
- 被重写的函数不能是static的。必须是virtual的
- 重写函数必须有相同的类型,名称和参数列表
- 重写函数的访问修饰符可以不同。
重定义规则如下:
- 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏(如果相同有Virtual就是重写覆盖了)。
#include<iostream>
using namespace std;
class Animal
{
public:
void func1(int tmp)
{
cout << "I'm an animal -" << tmp << endl;
}
void func1(const char *s)//函数的重载
{
cout << "I'm an animal func1 -" << s << endl;
}
virtual void func2(int tmp)
{
cout << "I'm virtual animal func2 -" << tmp << endl;
}
void func3(int tmp)
{
cout << "I'm an animal func3 -" << tmp << endl;
}
};
class Fish :public Animal
{
public:
void func1()//函数的重定义 会隐藏父类同名方法
{
cout << "I'm a fish func1" << endl;
}
void func2(int tmp) //函数的重写, 覆盖父类的方法 override
{
cout << "I'm a fish func2 -" << tmp << endl;
}
void func3(int tmp) { //函数的重定义 会隐藏父类同名方法
cout << "I'm a fish func3 -" << tmp << endl;
}
};
int main()
{
Fish fi;
Animal an;
fi.func1();
// 由于是重定义 父类的方法已经被隐藏
// 需要显示声明,重载不能跨作用域
fi.Animal::func1(1);
dynamic_cast<Animal *>(&fi)->func1(11); // 强转之后即可调用到父类被隐藏的方法
dynamic_cast<Animal *>(&fi)->func1("hello world"); // 强转之后即可调用到父类被隐藏的方法
fi.func2(2); // 调用子类
dynamic_cast<Animal *>(&fi)->func2(22); // 调用"子类方法"(因为是虚函数,会被子类覆盖)
dynamic_cast<Animal *>(&fi)->func3(222); // 调用父类
fi.func3(2222); // 调用子类
cout << endl << " ************ " << endl;
an.func1(1);
an.func1("I'm an animal");
an.func2(1);
system("pause");
return 0;
}
输出结果:
I'm a fish func1
I'm an animal -1
I'm an animal -11
I'm an animal func1 -hello world
I'm a fish func2 -2
I'm a fish func2 -22
I'm an animal func3 -222
I'm a fish func3 -2222
************
I'm an animal -1
I'm an animal func1 -I'm an animal
I'm virtual animal func2 -1
参考来源:C++_重载、重写和重定义的区别_fzzjoy的专栏-CSDN博客_c++重载和重写的区别
6. 怎么判断机器是32位还是64位
- 首先得明白32位系统和64位系统的区别。
32位和64位系统的主要差别在于CPU一次处理数据的能力是32位还是64位。所以从这里来看64位系统的执行效率是比32位更高的。而对于C++程序员来说它对我们的影响除了X86和X64架构平台影响之外,还有存储时地址的影响。32位的寻址范围是2^32 = 4G,也就是说32位系统最高支持4G内存,而64位系统的寻址范围是2^64 = 4G*4G,它所支持的内存对于现在我技术来时是个天文数字。
方法一: 用指针的方式,因为32位系统的寻址空间只有32位大小,所以32位系统的指针所指向的地址应该是32位,如果用sizeof求任意类型的的指针的大小的话应该得到4字节。同里如果是64位系统的话则是8字节。
cout << "sizeof(int*):" << sizeof(int*) << endl;
// 32 位系统则是4字节, 64位系统则是8字节
方法二: 二级指针
//简单小例子判断是64位系统还是32位
char *test = nullptr;
char *start = (char *)&test;
char *end = (char *)(&test + 1);
//..
if (4 == (end - start))
cout << "32位" << endl;
if (8 == (end - start))
cout << "64位" << endl;
参考资料:
- 如何用C++代码检测你用的编程环境是64系统还是32位系统?_3D Matrix-CSDN博客
- 如何用C++代码稳定判断64位还是32位系统(不使用任何宏定义,或者API)_MorningStar的博客-CSDN博客
7. 虚析构函数(为什么我们不提虚构造函数呢?)
virtual ~类名(){}
- 只有析构函数可以被声明为虚函数,构造函数不能声明为虚函数!!!\
- 虚函数是根据不同类型的对象产生不同的动作,构造函数是在对象生成之前调用的,如果对象还没有产生,那么虚构造函数也就没什么意义
- 如果一个类的析构函数是虚函数,那么由它派生而来的所有派生类的析构函数,不管是否用virtual进行说明,也都是虚析构函数。保证使用时基类类型的指针能够调用适当的析构函数针对不同的对象进行清理工作 参考资料:谈谈你对虚函数的理解_浮生流年的博客-CSDN博客_如何理解虚函数
8. 多态是什么
- 多态是什么?
多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作\ - 多态的优点\
- 消除类型之间的耦合关系
- 可替换性
- 可扩充性
- 接口性
- 灵活性
- 简化性
多态存在的三个必要条件
1.继承
2.重写
3.父类引用指向子类对象
参考资料:什么是多态?_LeeMxuan_的博客-CSDN博客_jn是什么含义
9. 怎么判断大端小端
什么是大端序,小端序?
其实就是字节的存储顺序,如果数据都是单字节的,那怎么存储无所谓了,但是对于多字节数据,比如int,double等,就要考虑存储的顺序了。
举个例子: 一个32位 int 型变量 0x11223344 占用四个字节,11 占用一个,22占用一个,33占用一个,44占用一个;存储的地址为 0x100 0x101 0x102 0x103;那么问题是,11 占用的是哪个字节呢? 11 占用 0x100这个字节还是,0x103这个字节呢?这时便有了两种方式排序:
1.大端序:
大端序即数字的高位占用低地址,低位占用高地址,这种也是最符合直觉的
2. 小端序:
小端序即数字的低位占用低地址,高位占用高地址
判断方法1:
实现思想:
- 定义一个 32 位的 int 型变量,0x11223344
- 将这个 int 型变量的低地址开始的 8 位存储的值取出来,取出来的方法就是利用强制类型转换
- 如果这个值是 0x11 那么说明低地址存储了值的高位,所以为大端序
- 如果这个值是 0x44 那么说明低地址存储了值的低位,所以为小端序
#include "stdio.h"
#include "stdlib.h"
// 判断大端还是小端??
// 如果是大端序函数返回 1
// 如果是小端序函数返回 0
int Judge_BS(int n) {
// 如果是大端序,数字 n 的低位存储在高地址中
// 即 44 存储在高地址中,11 存储在低地址中
// 地址: 0x100 0x101 0x102 0x103
// 数字: 11 22 33 44
// 如果是小端序,数字 n 的低位存储在低地址中
// 即 11 存储在高地址中,44 存储再低地址中
// 地址: 0x100 0x101 0x102 0x103
// 数字: 44 33 22 11
// 所以我们可以将32位数字 n 的 低 8 位取出来
// 如果低 8 位是 11 则为大端序
// 如果低 8 位是 44 则为小端序
//此处的地址存储的是低地址
char* p = &n;
printf("%x\n", *p);
printf("%x\n", *(p + 1));
printf("%x\n", *(p + 2));
printf("%x\n", *(p + 3));
// 用 p 获得 32 的低地址,每 8 位打印一次数字,打印结果为
// 44 33 22 11
// 低地址对应着低位,是小端序
char t = *p;
if (t == 11) {
return 1;
}
return 0;
}
int main() {
int n = 0x11223344;
if (Judge_BS(n)) {
printf("是大端序!");
} else {
printf("是小端序!");
}
system("pause");
return 0;
}
判断方法2:
- 实现思想:思想基本与第一种方法相同,区别在于这次使用了联合体,利用联合体的共用内存的特点实现。
#include <stdio.h>
#include <stdlib.h>
int main() {
union Un {
int a;
char b;
} Un;
Un.a = 0x11223344;
if (Un.b == 0x11) {
printf("大端\n");
} else {
printf("小端\n");
}
system("pause");
return 0;
}
参考资料:判断 机器是大端还是小端(两种方法)_z7436-CSDN博客_你的机器是大端方式还是小端方式
10.构造函数和析构函数的调用顺序(包含多重继承、类中包含对象成员)
构造函数顺序:
首先调用基类的构造函数(先调用哪个基类与派生类继承基类的顺序一致)
(若为菱形结构,则基虚类构造函数只被调用一次)
其次调用类的对象成员的构造函数(先调用哪个对象成员的构造函数与对象成员的定义顺序一致)
最后调用派生类的构造函数
析构函数的调用顺序与构造函数相反
参考资料:谈谈你对虚函数的理解_浮生流年的博客-CSDN博客_如何理解虚函数
11.纯虚函数与抽象类
包含纯虚函数的类被称为抽象类
class <类名>
{
virtual <类名> <函数名> (<参数表>) = 0;
}
纯虚函数的作用是在基类中为派生类保留一个函数的名字,以便派生类根据需要对它进行定义。
参考资料:谈谈你对虚函数的理解_浮生流年的博客-CSDN博客_如何理解虚函数
12.为什么要引用抽象基类和纯虚函数
<1>为了方便使用多态性
<2>在很多情况下,基类本身生成对象是不合理的。例如:动物作为一个基类可以派生出老虎、狮子等子类,但动物本身生成对象明显不合常理。抽象基类不能够被实例化,它定义的纯虚函数相当于接口,能把派生类的共同行为提取出来。
参考资料:谈谈你对虚函数的理解_浮生流年的博客-CSDN博客_如何理解虚函数
13.虚函数与纯虚函数的区别
<1>虚函数是实现的,哪怕是空实现;纯虚函数只是一个接口,是函数声明,需要子类去实现
<2>虚函数在子类中也可以不修改;但纯虚函数必须在子类中实现
<3>虚函数的类用于“实作继承”,也就是说继承接口的同时也继承了父类的实现,当然也可以完成自己的实现;纯虚函数的类用于“介面继承”,即纯虚函数关注的是接口统一性,实现由子类完成
<4>带纯虚函数的类叫虚基类(抽象类),这种类不能直接实例化对象,只有被继承,并实现其纯虚函数后,才能使用
虚函数与纯虚函数 在他们的子类中都可以被重写。它们的区别是:
(1)纯虚函数只有定义,没有实现;而虚函数既有定义,也有实现的代码。
纯虚函数一般没有代码实现部分,如\
virtual void print() = 0;
而一般虚函数必须要有代码的实现部分,否则会出现函数未定义的错误。
(2)包含纯虚函数的类不能定义其对象,而包含虚函数的则可以。
virtual void print()
{ printf("This is virtual function\n"); }
(1)类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样的话,这样编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
(2)虚函数在子类里面也可以不重载的;但纯虚函数必须在子类去实现,这就像Java的接口一样。通常把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为很难预料到父类里面的这个函数不在子类里面不去修改它的实现。
(3)虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。当然大家也可以完成自己的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
(4)带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。这样的类也叫抽象类。
参考资料:
14.运算符重载/函数重载对比虚函数和纯虚函数的优缺点
<1>运算符重载和函数重载
静态关联:关联工作在编译、链接阶段完成,在此期间,系统就可以根据函数的参数类型和参数个数决定调用哪个同名函数。
静态关联的优缺点:程序执行效率高,但对程序员水平要求较高
<2>虚函数和纯虚函数
动态关联:关联工作在程序运行阶段完成。
动态关联的优缺点:提供更好的编程灵活性、问题抽象性和程序的易维护性,但是函数调用速度慢
参考资料:谈谈你对虚函数的理解_浮生流年的博客-CSDN博客_如何理解虚函数
15.降低延迟和抖动的方案
网络中的延迟是指信息从发送到接收经过的延迟时间,一般由传输延迟及处理延迟组成;而抖动是指最大延迟与最小延迟的时间差,如最大延迟是20毫秒,最小延迟为5毫秒,那么网络抖动就是15毫秒,它主要标识一个网络的稳定性。
一、网络抖动:
- 定义:抖动是QOS里面常用的一个概念,其意思是指分组延迟的变化程度。
- 产生原因:如果网络发生拥塞,排队延迟将影响端到端的延迟,并导致通过同一连接传输的分组延迟各不相同,而抖动,就是用来描述这样一延迟变化的程度。 二、网络延迟:
- 定义:网络延迟是在传输介质中传输所用的时间,即从报文开始进入网络到它开始离开网络之间的时间。
- 产生原因:网络延迟是指各式各样的数据在网络介质中通过网络协议(如TCP/IP)进行传输,如果信息量过大不加以限制,超额的网络流量就会导致设备反应缓慢,造成网络延迟。
一、解决网络延迟的方法:
- 提升WAN性能,通过选择较短和更有效率的路由路径、部署低延迟的交换机和路由设备、主动避免网络设备停机时间,WAN运营商也可以对降低延迟作出贡献。
- 增加WAN带宽能提高应用程序的性能,在实践中,运用能够更有效利用现有WAN带宽的各种技术同样可以提升WAN应用程序的性能。 二、解决网络抖动的方法:
- 数据包接收端的抖动缓存指针队列的入队线程接收数据包,对接收到的数据包进行排序后将接收到的数据包插入抖动缓存指针队列的相应位置
- 抖动缓存指针队列的出队线程定时器以一定时间间隔触发出队线程,出队线程判断抖动缓存指针队列队头的数据包是否应该在当前触发时刻出队,如果是,则将该数据包出队。 参考资料:网络中的抖动和延迟是什么?怎么产生的?_百度知道 (baidu.com)
16.NULL,0,nullptr的区别分析
在编写C程序的时候只看到过NULL,而在C++的编程中,我们可以看到NULL和nullptr两种关键字,其实nullptr是C++11版本中新加入的,它的出现是为了解决NULL表示空指针在C++中具有二义性的问题,为了弄明白这个问题,我查找了一些资料,总结如下。
一、C程序中的NULL
在C语言中,NULL通常被定义为:#define NULL ((void *)0)
所以说NULL实际上是一个空指针,如果在C语言中写入以下代码,编译是没有问题的,因为在C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。
int *pi = NULL;
char *pc = NULL;
二、C++程序中的NULL
但是问题来了,以上代码如果使用C++编译器来编译则是会出错的,因为C++是强类型语言,void*是不能隐式转换成其他类型的指针的,所以实际上编译器提供的头文件做了相应的处理:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
可见,在C++中,NULL实际上是0,而不是(void*)0,因为C++中不能把void*类型的指针隐式转换成其他类型的指针,所以为了结果空指针的表示问题,C++引入了0来表示空指针,这样就有了上述代码中的NULL宏定义。
但是实际上,用NULL代替0表示空指针在函数重载时会出现问题,程序执行的结果会与我们的想法不同,举例如下:
#include <iostream>
using namespace std;
void func(void* i)
{
cout << "func1" << endl;
}
void func(int i)
{
cout << "func2" << endl;
}
void main(int argc,char* argv[])
{
func(NULL);
func(nullptr);
getchar();
}
在这段代码中,我们对函数func进行可重载,参数分别是void*类型和int类型,但是运行结果却与我们使用NULL的初衷是相违背的,因为我们本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数版本(因为NULL定义为0),所以是有问题的,这就是用NULL代替空指针在C++程序中的二义性。
三、C++中的nullptr
为解决NULL代指空指针存在的二义性问题,在C++11版本(2011年发布)中特意引入了nullptr这一新的关键字来代指空指针,从上面的例子中我们可以看到,使用nullptr作为实参,确实选择了正确的以void*作为形参的函数版本。
总结:
- NULL在C++中就是0,这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以之前C++中用0来代表空指针,但是在重载整形的情况下,会出现上述的问题。
- 所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,因此,建议以后还是都用nullptr替代NULL吧,而NULL就当做0使用。 参考资料:
- 史上最明白的 NULL、0、nullptr 区别分析(老师讲N篇都没讲明白的东东),今天终于明白了,如果和我一样以前不明白的可以好好的看看... - porter_代码工作者 - 博客园 (cnblogs.com)
- C++中NULL和nullptr的区别_天涯明月刀的博客-CSDN博客_nullptr和null区别
17.哈希表插入时间复杂度
哈希表插入的时间复杂度与冲突次数有关,O(冲突次数/n),最好的情况冲突次数为0,直接插入,时间复杂度为O(1)。最坏情况是所有值对应同一个键值,这是冲突次数最多,为0+1+2+3+4+…+(n-1)=n*(n-1)/2,平均比较次数为(n-1)/2,时间复杂度为O(n)
参考资料:以下数据结构说法,错误的是___?_阿里巴巴笔试题_牛客网 (nowcoder.com)
18. 什么情况下必须使用C++的初始化列表
理论而言:
-
初始化 != 赋值. a. 初始化代表为变量分配内存. 变量在其定义处被编译器初始化(编译时). 在函数中, 函数参数初始化发生在函数调用时(运行时).
b. 赋值代表"擦除对象当前值, 赋予新值". 它不承担为对象分配内存的义务. -
C++中, 类成员的初始化于初始化列表中完成, 先于构造函数体执行. 即成员真正的初始化发生在初始化列表中, 而不是构造函数体中.
再给说明。
- 如果类中有一个成员是一个引用, 由于引用必须给予初始值, 因此, 引用必须使用初始化列表.
- 同理, const属性必须给予初始值, 必须使用初始化列表.
- 继承类中调用基类初始化构造函数, 实际上就是先构造基类对象, 必须使用初始化列表.
别的不再说明, 什么时候必须使用初始化列表是很明显的; 另外, 简单的说, 任何时候都鼓励使用初始化列表, 一些别的事情(比如在构造函数中分配资源之类的, 请参考RAII, Resource Acquizition Is Initialization)可以放在构造函数体内完成.
参考资料:什么情况下必须使用C++的初始化列表_WingC的博客-CSDN博客_c++什么时候用初始化列表
19.智能指针的引用计数放在哪的
堆上,不是堆上的话一个可能就是不同对象进行check,多次判零,多次释放,内存泄漏
参考资料:c++智能指针的引用计数是存放在栈上还是堆上呢?_技术交流_牛客网 (nowcoder.com)
20.红黑树
线性查找 —性能低—>二分查找— 二查叉树会出现退化成链表的问题—>出现AVL平衡二叉树—数据变化有频繁更新节点问题—>出现红黑树 什么是红黑树? 红黑树(Red Black Tree)是一颗自平衡(self-balancing)的二叉排序树(BST),树上的每一个结点都遵循下面的规则(特别提醒,这里的自平衡和平衡二叉树AVL的高度平衡有别):
- 每一个结点都有一个颜色,要么为红色,要么为黑色;
- 树的根结点为黑色;
- 树中不存在两个相邻的红色结点(即红色结点的父结点和孩子结点均不能是红色);
- 从任意一个结点(包括根结点)到其任何后代 NULL 结点(默认是黑色的)的每条路径都具有相同数量的黑色结点。 参考资料:什么是红黑树,一篇文章解决所有疑惑~~ - 知乎 (zhihu.com)
21.智能指针
C++11 引入了 3 个智能指针类型:
std::unique_ptr<T>:独占资源所有权的指针。std::shared_ptr<T>:共享资源所有权的指针。std::weak_ptr<T>:共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。 std::auto_ptr已被废弃。
std::unique_ptr
简单说,当我们独占资源的所有权的时候,可以使用 std::unique_ptr 对资源进行管理——离开 unique_ptr 对象的作用域时,会自动释放资源。这是很基本的 RAII 思想。
std::unique_ptr 的使用比较简单,也是用得比较多的智能指针。这里直接看例子。
std::unique_ptr
简单说,当我们独占资源的所有权的时候,可以使用 std::unique_ptr 对资源进行管理——离开 unique_ptr 对象的作用域时,会自动释放资源。这是很基本的 RAII 思想。
std::unique_ptr 的使用比较简单,也是用得比较多的智能指针。这里直接看例子。
- 使用裸指针时,要记得释放内存。
{
int* p = new int(100);
// ...
delete p; // 要记得释放内存
}
- 使用 std::unique_ptr 自动管理内存。
{
std::unique_ptr<int> uptr = std::make_unique<int>(200);
//...
// 离开 uptr 的作用域的时候自动释放内存
}
- std::unique_ptr 是 move-only 的。
{
std::unique_ptr<int> uptr = std::make_unique<int>(200);
std::unique_ptr<int> uptr1 = uptr; // 编译错误,std::unique_ptr<T> 是 move-only 的
std::unique_ptr<int> uptr2 = std::move(uptr);
assert(uptr == nullptr);
}
std::shared_ptr
std::shared_ptr 其实就是对资源做引用计数——当引用计数为 0 的时候,自动释放资源。
std::shared_ptr 的实现原理
一个 shared_ptr 对象的内存开销要比裸指针和无自定义 deleter 的 unique_ptr 对象略大。
shared_ptr 需要维护的信息有两部分:
- 指向共享资源的指针。
- 引用计数等共享资源的控制信息——实现上是维护一个指向控制信息的指针。
std::weak_ptr
std::weak_ptr 要与 std::shared_ptr 一起使用。 一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期:
- 如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。
- 当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。
当 shared_ptr 析构并释放共享资源的时候,只要 weak_ptr 对象还存在,控制块就会保留,weak_ptr 可以通过控制块观察到对象是否存活。
enable_shared_from_this
一个类的成员函数如何获得指向自身(this)的 shared_ptr 成员函数获取 this 的 shared_ptr 的正确的做法是继承 std::enable_shared_from_this。
小结
智能指针,本质上是对资源所有权和生命周期管理的抽象:
- 当资源是被独占时,使用 std::unique_ptr 对资源进行管理。
- 当资源会被共享时,使用 std::shared_ptr 对资源进行管理。
- 使用 std::weak_ptr 作为 std::shared_ptr 管理对象的观察者。
- 通过继承 std::enable_shared_from_this 来获取 this 的 std::shared_ptr 对象。
参考资料: