C++

文章目录

1.深拷贝和浅拷贝

1.在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的;但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象,所以,此时,必须采用深拷贝。
2.深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
浅拷贝的例子:

#include <iostream>  
using namespace std;
 
class Student
{
private:
	int num;
	char *name;
public:
	Student();
	~Student();
};
 
Student::Student()
{
	name = new char(20);
	cout << "Student" << endl;
 
}
Student::~Student()
{
	cout << "~Student " << (int)name << endl;
	delete name;
	name = NULL;
}
 
int main()
{
	{// 花括号让s1和s2变成局部对象,方便测试
		Student s1;
		Student s2(s1);// 复制对象
	}
	system("pause");
	return 0;
}

系统奔溃
执行结果:调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同。name指针被分配一次内存,但是程序结束时该内存却被释放了两次,会造成指针悬挂,导致崩溃!这是由于编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝!即对指针name拷贝后会出现两个指针指向同一个内存空间。
在这里插入图片描述
所以,在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。

#include <iostream>  
using namespace std;
 
class Student
{
private:
	int num;
	char *name;
public:
	Student();
	~Student();
	Student(const Student &s);//拷贝构造函数,const防止对象被改变
};
 
Student::Student()
{
	name = new char(20);
	cout << "Student" << endl;
 
}
Student::~Student()
{
	cout << "~Student " << (int)name << endl;
	delete name;
	name = NULL;
}
Student::Student(const Student &s)
{
	name = new char(20);
	memcpy(name, s.name, strlen(s.name));
	cout << "copy Student" << endl;
}
 
int main()
{
	{// 花括号让s1和s2变成局部对象,方便测试
		Student s1;
		Student s2(s1);// 复制对象
	}
	system("pause");
	return 0;
}

在这里插入图片描述
执行结果:调用一次构造函数,一次自定义拷贝构造函数,两次析构函数。两个对象的指针成员所指内存不同。
总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题。
使用智能指针的原因至少有以下三点:
  1)智能指针能够帮助我们处理资源泄露问题;
  2)它也能够帮我们处理空悬指针的问题;
  3)它还能够帮我们处理比较隐晦的由异常造成的资源泄露。

2.delete 与 delete[] 区别

1、针对简单类型 使用 new 分配后的不管是数组还是非数组形式内存空间用两种方式均可 如:

int *a = new int[10];
delete a;
delete [] a;
此种情况中的释放效果相同,原因在于:分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理,在析构时,系统并不会调用析构函数, 它直接通过指针可以获取实际分配的内存空间,哪怕是一个数组内存空间(在分配过程中 系统会记录分配内存的大小等信息,此信息保存在结构体_CrtMemBlockHeader中, 具体情况可参看VC安装目录下CRT\SRC\DBGDEL.cpp)

2、针对类Class,两种方式体现出具体差异

当你通过下列方式分配一个类对象数组:

class A
{
    private:
        char *m_cBuffer;
        int m_nLen;
    public:
        A(){ m_cBuffer = new char[m_nLen]; }
        ~A() { delete [] m_cBuffer; }
};
A *a = new A[10];
// 仅释放了a指针指向的全部内存空间 但是只调用了a[0]对象的析构函数 剩下的从a[1]到a[9]这9个用户自行分配的m_cBuffer对应内存空间将不能释放 从而造成内存泄漏
delete a;
// 调用使用类对象的析构函数释放用户自己分配内存空间并且   释放了a指针指向的全部内存空间
delete [] a;

所以总结下就是,如果ptr代表一个用new申请的内存返回的内存空间地址,即所谓的指针,那么:
delete ptr – 代表用来释放内存,且只用来释放ptr指向的内存。
delete[] rg – 用来释放rg指向的内存,!!还逐一调用数组中每个对象的 destructor!!
对于像 int/char/long/int*/struct 等等简单数据类型,由于对象没有 destructor,所以用 delete 和 delete [] 是一样的!但是如果是C++ 对象数组就不同了!

3.new和malloc区别

其实在使用的大部分场景下,两者是可以通用的,但是我们还是要知道他两的区别。
1、malloc与free是c++/c语言的标准函数,new/delete是C++的运算符。2、他们都可用于申请动态内存和释放内存。new/delete比malloc/free更加智能,其实底层也是执行的malloc/free。为啥说new/delete更加的智能?因为new和delete在对象创建的时候自动执行构造函数,对象消亡之前会自动执行析构函数。既然new/delete的功能完全覆盖了malloc和free,为什么C++中不把malloc/free淘汰出局呢?因为c++程序经常要调用c函数,而c程序智能用malloc/free管理动态内存。
3、new返回指定类型的指针,并且可以自动计算出所需要的大小
*int p; p = new int; //返回类型为int类型,大小为sizeof(int);
int *pa; pa = new int[50];//返回类型为int ,大小为sizeof(int) * 100;
malloc必须用户指定大小,并且默然返回类型为void
,必须强行转换为实际类型的指针。

4.const的用法及和define的差别

用法:

/*const是一个常量关键字,主要是为了防止所修饰对象被修改。
我们在定义一个变量时,如果想要防止这个变量被修改,可以
用const来修饰这个变量。也就是说,被const修饰过的变量或者
函数,不能对其进行修改,否则,编译器就会报错*/
/*用与修饰全局变量*/
#define PI1 3.14
const double PI2 = 3.14;
/*两种方式都可以用来定义全局变量。不过,第二种要比

第一种方式要好,使用宏定义的变量,其信息一般以表格

的形式储存在系统中,当我们在调试程序时,就有可能使

得这个宏定义的变量反复出现在符号表中。而const修饰

的变量会一直出现在符号表,使得我们调试方便许多。*/
/*用于修饰指针*/
const int *p1 = 100;   //常量整形指针
int *const p2 = 200;   //整形常量指针
/*常量整形指针,不能通过这个指针修改它所指向的变量,
指针本身是可变的。
整形变量指针,指针不可修改,但是指向的变量可以修改*/
/*用于表示函数的输入,输出*/
char *strcpy(char *buf,const char *str)   //将str拷贝给buf
/*由于buf是需要修改的,所以不用const修饰,而str是传递数据进来
的,并不希望进行改变,所以可以用const修饰*/
/*修饰类成员函数*/
class student {
public:
	student(int age) :_age(age) {}
	void getAge()const {      //不希望通过函数来改变类的私有变量
		_age = 100;       //错误,编译器会报警
		cout << _age << endl;
	}
private:
	int _age;
};
/*如果我们在定义一个类的成员函数时,并不希望这个函数能够改变类的

私有变量,这时我们可以使用const将这个成员函数定义为常量函数,这样,

这个函数就不能修改私有变量了。*/

两者区别
(1)就起作用的阶段而言: #define是在编译的预处理阶段起作用,而const是在 编译、运行的时候起作用
(2)就起作用的方式而言: #define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。
(3)就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份const定义的只读变量在程序运行过程中只有一份备份
(4)从代码调试的方便程度而言: const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经替换掉了。
const优点
(1)const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
(2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
(3)const可节省空间,避免不必要的内存分配,提高效率

5.编译和链接的区别

编译:将预处理生成的文件,经过词法分析、语法分析、语义分析以及优化后编译成若干个目标模块。可以理解为将高级语言翻译为计算机可以理解的二进制代码,即机器语言,编译是将文件编译成中间代码(如在Windows下面为.obj文件)。
链接:由链接程序将编译后形成的一组目标模块以及它们所需要的库函数链接在一起,形成一个完整的载入模型。链接主要解决模块间的相互引用问题。也就是那些目标文件之间相互链接自己所需要的函数和全局变量,而函数可能来源于其他目标文件或库文件。大量的Object File合成执行文件,这个动作称为链接。

6.堆和栈的效率比较

1)申请方式:
栈:系统自动分配
堆:程序员手动申请
2) 申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
3)申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
4)申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
堆是一种常用的树形结构,是一种特殊的完全二叉树,当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆。

7.struct和class的区别

  1. 默认权限(struct-public, class-private)
  2. 是否可用于声明模板(struct不可以, class可以)

8.虚函数和纯虚函数

1.虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。
2.虚函数在基类中定义,可以被直接使用,也可以被子类重载以后,以多态的形式调用,在子类中可以重写virtual void funtion1()
3.纯虚函数是在基类中声明的虚函数,它在基类中只有声明没有定义,要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” :virtual void funtion1()=0。定义纯虚函数就是为了让基类不可实例化(动物这个类不可实例化,是个统称没有意义),因为实例化这样的抽象数据结构本身并没有意义或者给出实现也没有意义。

9.sizeof()和strlen()比较

1、sizeof():返回所占总空间的字节数,strlen():返回字符数组或字符串所占的字节数,sizeof针对整型字符型数组和整型或字符型指针,strlen针对字符数组和字符指针。
2、size是运算符,其值在编译时即计算好了,strlen(…)是函数,要在运行时才能计算。

10.堆区和栈区的区别

  • 管理方式不同:堆由程序员管理,栈是由操作系统自动管理
  • 碎片问题:堆频繁的new/malloc会造成大量的内存碎片,栈先入后出的结构,进出一一对应,不会产生内存碎片
  • 生长方向:堆向上,向高地址方向增长;栈向下,向低地址方向生长
  • 分配方式:堆是动态分配,没有静态分配;栈中有静态分配也有动态分配,静态分配是由编译器完成,动态分配由alloca函数分配,编译器自动释放,无需程序员实现
  • 空间大小:堆是不连续的内存空间(用链表来存储空闲内存地址),空间大;栈和数据结构中的栈一样,是一块连续的内存空间,空间小。

11.list和vector的区别,map和set的区别

集合的三种类型:list(列表)、set(集)、map(映射)
https://www.cnblogs.com/shijingjing07/p/5587719.html
1.vector数据结构
vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。
因此能高效的进行随机存取,时间复杂度为o(1);
但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。
另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。
2.list数据结构
list是由双向链表实现的,因此内存空间是不连续的。
只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);但由于链表的特点,能高效地进行插入和删除

vector拥有一段连续的内存空间,能很好的支持随机存取,
因此vector::iterator支持“+”,“+=”,“<”等操作符。

list的内存空间可以是不连续,它不支持随机访问,
因此list::iterator则不支持“+”、“+=”、“<”等

vector::iterator和list::iterator都重载了“++”运算符。
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;如果需要大量的插入和删除,而不关心随机存取,则应使用list
3.set是一种关联式容器,其特性如下:
set以RBTree作为底层容器
所得元素的只有key没有value,value就是key
不允许出现键值重复
所有的元素都会被自动排序
不能通过迭代器来改变set的值,因为set的值就是键
4.map和set一样是关联式容器,它们的底层容器都是红黑树,区别就在于map的值不作为键,键和值是分开的。它的特性如下:
map以RBTree作为底层容器
所有元素都是键+值存在
不允许键重复
所有元素是通过键进行自动排序的
map的键是不能修改的,但是其键对应的值是可以修改的

12.TCP与UDP区别总结

1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保 证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。
7、TCP的缺点: 慢,效率低,占用系统资源高。

13.基于TCP和UDP的常用协议

基于TCP:HTTP、HTTPS、FTP、TELNET、SMTP(简单邮件传输协议)。
基于UDP:TFTP、DNS、DHCP、TFTP、SNMP(简单网络管理协议)、RIP。
在这里插入图片描述

14.C++内存泄漏及解决办法

C++没有垃圾回收机制,我们需要关注那些类型的内存泄漏?
堆内存泄漏。在内存中程序员手动分配的一块内存,malloc\new。完成相关操作后,没有调用相对应的free\delete释放掉内存,这时这块内存就会常驻内存,造成堆内存泄漏
系统资源泄漏。分配给程序使用的资源没有使用相应函数释放,如bitmap\handle\socket.
堆内存泄漏比较常见:
1.在类的构造函数和析构函数中没有匹配的调用new和delete函数;
2. 在释放对象数组时在delete中没有使用方括号;
3. 缺少拷贝构造函数;
怎么有效解决内存泄漏问题?
智能指针。因为智能指针可以自动删除分配的内存。智能指针和普通指针类似,只是不需要手动释放指针,而是通过智能指针自己管理内存的释放。
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

15.stack和queue的使用+map+set讲解

栈:栈(stack)又名堆栈,它是限定在表的一端进行插入和删除操作的线性表(后进先出)。这一端被称为栈顶,相对地,把另一端称为栈底。不含元素的空表称为空栈。
用法:

a.push();   //向栈内压入一个成员
a.pop();   //从栈顶弹出一个成员
a.empty();   //为空返回true,否则返回false
a.top();   //返回栈顶,但不删除成员
a.size();   //返回栈的大小

队列:和栈相反,队列(queue)是一种先进先出的线性表。它只允许在表的前端进行删除操作,而在表的后端进行插入操作。允许插入的一端称为队尾(rear),允许删除的一端称为队头(front)。
用法:

q.push();  //在队尾插入数据  
q.pop();  //在队首删除数据   
q.empty();  //为空返回true,否则返回false
q.front(); //返回队首元素   
q.back();  //返回队尾的元素 
q.size();  //返回队列的大小

map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据处理能力,由于这个特性,它在我们处理一对一数据的时候,在编程上提供快速通道
set中不会包含重复的元素。set的含义是集合,是一个有序的容器,支持插入、删除、查找等操作,效率非常高。
上11有list和vector的区别
map种类有map、multimap、unorder_map、unorder_multimap区别
map内部实现了一个红黑树(故有序,红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作,其时间复杂度都是O(logn),运行效率高,map是保存的是有序键值对。但因为map内部实现了红黑树,虽然提高了,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间,空间占用率高
multimap容器保存的也是有序的键值对,但是可以保存重复的元素,即multimap中会出现具有相同键值的元素序列,底层也是红黑树。
unordered_map内部实现了一个哈希表,元素是无序的。查找速度非常的快,但哈希表的建立比较耗费时间
unordered_multimap元素无序,并且可以有多个key值相同

16.为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?

将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

17.C++中的存储分区

内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
1.,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
2.,就是那些由malloc等分配的内存块,用free来结束自己的生命。
3.自由存储区,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
4.全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
5.常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)。

18.new和malloc的区别

1.malloc是函数,new是关键字;
2.malloc不能赋初值,new可以,如int *p = new int(2)代表分配一个int型的内存空间,并赋初值2.如果new int ()代表赋初值0,new int[10]代表分配10个int;
3.malloc返回的指针是void 类型,而new返回的指针是它分配空间的类型;
4.malloc和free在C程序中使用,而C++程序中使用new和delete,删除数组delete[]p,指针释放后,要将指针置空。

19.野指针

野指针不是空指针,是一个指向垃圾内存的指针。
野指针形成原因
1.指针变量没有被初始化
任何指针变量被刚创建时不会被自动初始化为NULL指针,它的缺省值是随机的。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如:

char* p = NULL;char* str = (char*)malloc(1024);

2.指针被free或者delete之后,没有设置为NULL,让人误以为这是一个合法指针
free和delete只是把指针所指向的内存给释放掉,但并没有把指针本身给清理掉。这时候的指针依然指向原来的位置,只不过这个位置的内存数据已经被毁尸灭迹,此时的这个指针指向的内存就是一个垃圾内存。但是此时的指针由于并不是一个NULL指针(在没有置为NULL的前提下),在做如下指针校验的时候if(p != NULL)会逃过校验,此时的p不是一个NULL指针,也不指向一个合法的内存块,造成会面程序中指针访问的失败。
3.指针操作超越了变量的作用范围
由于C/C++中指针有++操作,因而在执行该操作的时候,稍有不慎,就容易指针访问越界,访问了一个不该访问的内存,结果程序崩溃
另一种情况是指针指向一个临时变量的引用,当该变量被释放时,此时的指针就变成了一个野指针。

20.指针和引用的区别

空初大小多变const
空:指针有自己的一块空间,而引用只是一个别名;
初:指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
大小:使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
多:指针可以有多级指针(p),而引用至多一级;
变:指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
可以有const指针,但是没有const引用;

21.内敛函数的引用inline

内联函数inline:引入内联函数的目的是为了解决程序中函数调用的效率问题,这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的i节省。所以内联函数一般都是1-5行的小函数。在使用内联函数时要留神:
1.在内联函数内不允许使用循环语句和开关语句;
2.内联函数的定义必须出现在内联函数第一次调用之前;
3.类结构中所在的类说明内部定义的函数是内联函数。

22.static用法

1.把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。程序的整个周期。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。默认初始化为0。作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
2.把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。只能作用于本cpp文件。
3.static用于类中时,可通过类名直接调用,静态成员函数只能访问静态成员变量。

23.SSL(Secure Socket Layer,安全套接字层协议)

SSL(Secure Socket Layer,安全套接字层协议)是由 Netscape设计的一种开放协议;它指定了一种在应用程序协议(例如http、telnet、 NNTP、 FTP)和 TCP/IP之间提供数据安全性分层的机制。它为TCP/IP连接提供数据加密、服务器认证、消息完整性以及可选的客户机认证。
SSL的主要目的是在两个通信应用程序之间提供私密信和可靠性。这个过程通过3个元素来完成:握手协议、记录协议和警告协议。
此外还有socks协议,SSH协议。

24.const的用法

1.const修饰成员函数
在类中将成员函数修饰为const不能修改对象的数据成员而且不能调用非const函数。——int get() const{}

2.const修饰函数返回值其实用的并不是很多,它的含义和const修饰普通变量以及指针的含义基本相同。

 a.const int fun1() //这个其实无意义,因为参数返回本身就是赋值。   
 b. const int * fun2() //调用时const int *pValue = fun2();
 //我们可以把fun2()看作成一个变量,即指针内容不可变。    
 c.int* const fun3()   //调用时int * const pValue = fun2();
                          //我们可以把fun2()看作成一个变量,即指针本身不可变。

3.const修饰函数参数防止传入的参数代表的内容在函数体内被改变,但仅对指针和引用有意义。因为如果是按值传递,传给参数的仅仅是实参的副本,即使在函数体内改变了形参,实参也不会得到影响

  • const修饰的函数参数是指针时:

代表在函数体内不能修改该指针所指的内容,起到保护作用,在字符串复制的函数中保证不修改源字符串的情况下,实现字符串的复制。

void fun(const char * src, char * des){  //保护源字符串不被修改,若修改src则编译出错。
	strcpy(des,src);
}
void main(){
	char a[10]="china";
	char b[20];
	fun(a,b);
	cout<<b<<endl;
}
  • const修饰的函数参数为引用时:
    void h(A a){}与void h(const A & a){}的区别
    void h(A a){}传递进来的参数a是实参对象的副本,要调用构造函数来构造这个副本,而且函数结束后要调用析构函数来释放这个副本,在空间和时间上都造成了浪费,所以函数参数为类对象的情况,推荐用引用。但按引用传递,造成了安全隐患,通过函数参数的引用可以修改实参的内部数据成员,所以用const来保护实参void h(const A & a){}。

25.为什么构造函数不能为虚函数,而析构函数可以为虚函数

  • 为什么构造函数不能为虚函数?
    虚函数的调用需要虚函数表指针,而该指针存放在对象的内容空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数——构造函数了。

  • 为什么析构函数可以为虚函数,如果不设为虚函数可能会存在什么问题?
    首先析构函数可以为虚函数,而且当要使用基类指针或引用调用子类时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。
    举例说明:
    子类B继承自基类A;A *p = new B; delete p;
    1) 此时,如果类A的析构函数不是虚函数,那么delete p;将会仅仅调用A的析构函数,只释放了B对象中的A部分,而派生出的新的部分未释放掉。
    2) 如果类A的析构函数是虚函数,delete p; 将会先调用B的析构函数,再调用A的析构函数,释放B对象的所有空间。
    补充: B *p = new B; delete p;时也是先调用B的析构函数,再调用A的析构函数。

26.CPU调度算法

1)批处理系统中的调度算法:
需要考虑的因素:
1. 吞吐量
2. cpu利用率
3. 周转时间
4. 公平性

具体的算法:
1.先来先服务: FCFS:
优点:实现简单
缺点:可能造成周转时间长
2.最短作业优先 SJF(非抢占式)
优点:平均周转时间最短
缺点:不公平,短任务多时,长任务一直得不到执行,产生starvation。
3. 最短剩余时间优先 SRTN :Shortest Remainning Time Next
SJF的抢占式版本
4. 最短响应比优先 HRRN:highest response ratio next
是一个综合考虑的算法,调度时,先计算每个进程的响应比,之后总是选择响应比高的执行:响应比R = 1+(等待时间/处理时间)
2)交互式系统中的调度算法
考虑主要因素:
响应时间
公平性
1.RR-round Robin (时间片轮转):
目标:为改善短任务的平均响应时间
主要思想:
1.周期性切换
2.每个进程分配一段时间片
3.利用时钟中断进行进程切换
如何选择合适长度的时间片:
-时间片太长:
1.如何每个进程的处理时间都小与时间片的长度,就会退化为FCFS
2.延长短进程的响应时间
-时间片太短:
1.频繁的cpu切换浪费cpu
优点:
1.公平
2.有利于交互式计算,响应时间短
缺点
1.由于进程切换,RR算法开销较大
2.对于各个进程处理时间大致相同的情况不利,造成更大的平均处理时间。
2.mutilevel feedback (多级反馈队列)
unix BSD5.3 版本使用的调度算法
是一个综合的调度算法
基本思想:
设置多个就绪,第一级队列优先级最高
优先级高,时间片越短,优先级越低,时间片越长。
第一级队列为空时,开始调度第二级队列,以此类推
各级队列使用RR算法调度
当一个新创建进程就绪后,进入第一级队列
进程用完时间片,放弃cup,就进入下一级队列
由于阻塞而放弃cpu的进程,进入相应的等待队列,一旦等待的事件发生,就回到原来的就绪队列
多级反馈队列对i/o型进程更友好

27.Struct与Union以及内存对齐问题

https://blog.csdn.net/weixin_41533956/article/details/101319903
主要区别

  • struct和union都是由多个不同的数据类型成员组成, 但在任何同一时刻, union中只存放了一个被选中的成员, 而struct的所有成员都存在。

  • 在struct中,各成员都占有自己的内存空间,它们是同时存在的。一个struct变量的总长度等于所有成员长度之和。

  • 在Union中,所有成员不能同时占用它的内存空间,它们不能同时存在。Union变量的长度等于最长的成员的长度。

  • 对于union的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于struct的不同成员赋值是互不影响的。

结构体内存对齐三大原则

  • 对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍

  • 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍

  • 如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型。
    在这里插入图片描述
    以上面结构体A为例,第一个成员a是char类型,占用1个字节空间,偏移量为0,第二个成员b是int类型,占用4个字节空间,按照规则1,b的偏移量必须是int类型的整数倍,所以编译器会在a变量后面插入3字节缓冲区,保证此时b的偏移量(4字节)是b类型的整数倍(当前恰好是1倍),第3个成员c为short类型,此时c的偏移量正好是4+4=8个字节,已经是short类型的整数倍,故b与c之间不用填充缓冲字节。但这时,结构体A的大小为8+2=10个字节,按照规则2,结构体A大小必须是其最大成员类型int的整数倍,所以在10个字节的基础上再填充2个字节,保证最后结构体大小为12,以符合规则2.
    在这里插入图片描述
    在这里插入图片描述

28.操作系统死锁

https://blog.csdn.net/weixin_41969690/article/details/107617634
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不可抢占条件:进程已获得的资源,在末使用完之前,不能强行剥夺,只能在进程使用完时由自己释放。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

29.输入一个url后的过程;浏览器从发送请求的全部过程

域名解析
发起TCP的3次握手
建立TCP连接后发起http请求
服务器响应http请求
浏览器解析html代码,并请求html代码中的资源(如js,css,图片等)
断开TCP连接
浏览器对页面进行渲染呈现给用户

30.数据特别大的时候,快速排序用递归会有什么问题?怎么解决?

会出现栈溢出(递归次数太多,大于栈的最大空间),可以使用堆排序,自己new空间。

31.TCP三次握手中SYN,ACK,Seq含义

TCP(Transmission Control Protocol)传输控制协议TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接:位码即tcp标志位,有6种标示:SYN(synchronous建立联机) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)Sequence number(顺序号码) Acknowledge number(确认号码)
第一次握手:主机A发送位码为syn=1,随机产生seq number=1234567的数据包到服务器,主机B由SYN=1知道,A要求建立联机;
第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq+1),syn=1,ack=1,随机产生seq=7654321的包;
第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。

32.TCP断开链接四次挥手

假设Client端发起中断连接请求,即发送FIN报文;
Server端接到FIN报文后,意思是说"Client端没有数据要发送了",但是如果Server端还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以Server端先发送ACK,告诉Client端"请求已经收到,但是Server端还没准备好,请继续等待消息";
这个时候Client端进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,告诉Client端"数据发送完成,准备好关闭连接";
Client端收到FIN报文后知道可以断开连接,所以再次坚定关闭发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。Server端收到ACK后断开连接。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,则自身关闭连接。
在这里插入图片描述
TCP建立连接时之所以只需要"三次握手",是因为在第二次"握手"过程中,服务器端发送给客户端的TCP报文是以SYN与ACK作为标志位的。SYN是请求连接标志,表示服务器端同意建立连接;ACK是确认报文,表示告诉客户端,服务器端收到了它的请求报文。
TCP释放连接时之所以需要“四次挥手”,是因为FIN释放连接报文与ACK确认接收报文是分别由第二次和第三次"挥手"传输的。
释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文。

33.OSI七层和TCP四层协议,OSI七层模型/五层模型,每一层有哪些协议,http,tcp,ip位于哪一层

在这里插入图片描述

34.为什么TCP是可靠的,TCP,UDP的区别

UDP协议和TCP协议都是传输层协议。
TCP(Transmission Control Protocol,传输控制协议)提供的是面向连接,可靠的字节流服务。即客户和服务器交换数据前,必须现在双方之间建立一个TCP连接,之后才能传输数据。并且提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。
可靠传输:
序列号 确认应答 超时重传 拥塞控制

确认应答机制&序列号
TCP将每个字节的数据都进行了编号,即为序列号。
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;;下一次你从哪里开始发。
超时重传&序列号
主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B; 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
主机A未收到B发来的确认应答,也可能是因为ACK丢失了,因此主机B会收到很多重复数据.。那么TCP协议需要能够识别出那些包是重复的包,,并且把重复的丢弃掉.,这时候我们可以利用序列号, 就可以很容易做到去重的效果。
拥塞控制
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口。拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。
提高传输效率:滑动窗口、流量控制、延迟应答、捎带应答
协议
HTTP
HTTPS
SSH
Telnet
FTP
SMTP
UDP(User Data Protocol,用户数据报协议)是一个简单的面向数据报的运输层协议。它不提供可靠性,只是把应用程序传给IP层的数据报发送出去,但是不能保证它们能到达目的地。由于UDP在传输数据报前不用再客户和服务器之间建立一个连接,且没有超时重发等机制,所以传输速度很快。
协议:
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议

35.http与https协议的区别:

http是超文本传输协议,信息是明文传输。https则是具有安全性的ssl加密传输协议。
http与https使用的是完全不同的连接方式,用的端口号也不一样,http使用的端口为80,https为443。
http连接很简单,是无状态的,https协议是由ssl+http构建的可进行加密传输、身份认证的网络协议,比http更加安全。
加密的过程:分私钥和公钥的是非对称的加密(HTTPS 在内容传输的加密上使用的是对称加密,非对称加密只作用在证书验证阶段)
在这里插入图片描述

36. 进程和线程的关系如下:(一个进程至少包括一个线程)

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位(分配车位)。
线程是进程的一个实体(洗车工,美容师,检修师),是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

  • 进程和线程的区别
    1.一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
    2.进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)
    3.进程是资源分配的最小单位,线程是CPU调度的最小单位
    4.系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,进程操作时操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。
    5.通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预
    6.进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
    7.进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉
    8.进程适应于多核、多机分布;线程适用于多核
  • 什么时候情况下要用多线程

对于处理时间短的服务或者启动频率高的要用单线程,相反用多线程!
不论什么时候只要能用单线程就不用多线程,只有在需要响应时间要求比较高的情况下用多线程。
某此操作允许并发而且该操作有可能阻塞时, 用多线程. 例如SOCKET, 磁盘操作。

37.进程间的通讯方式

管道,信号(用于接受某种事件发生),信号量(来控制多个进程对共享资源的访问),消息队列(有权限的进程可以进行读写),共享内存,套接字SOCKET(socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信)。

38.线程间的通讯方式

1、临界区:
通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
2、互斥量 Synchronized/Lock:
互斥对象和临界区对象非常相似,只是互斥量允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。
3、信号量 Semphare:
为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
即当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。
4、事件(信号),Wait/Notify
事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。

39.数据库

SQL数据库面试题以及答案(50例题):https://blog.csdn.net/hundan_520520/article/details/54881208?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param
MySQL数据库面试题(2020最新版):https://blog.csdn.net/ThinkWon/article/details/104778621?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

40.http状态码

状态代码可以指明具体请求是否已成功,还可以揭示请求失败的确切原因。
HTTP 400 – 请求无效
HTTP 403 – 禁止访问
HTTP 404- 无法找到文件
HTTP 405 – 资源被禁止
HTTP 500 – 内部服务器错误

41.并发和并行

解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:并行是在多台处理器上同时处理多个任务。如 hadoop 分布式集群,并发是在一台处理器上“同时”处理多个任务。

42.数据库事务

一个逻辑工作单元要成为事务,必须满足ACID特性(原子性、一致性、隔离性和持久性)。
① Atomic(原子性)
事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。
实例:银行转账工作——从一个账号扣款并使另一个账号增款,这两个操作要么都执行,要么都不执行。
② Consistency(一致性):
事物完成时,和事务开始之前,数据应处于一致状态,保证数据的无损,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
实例:假设用户A和用户B两者的钱加起来一共是1000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是1000。
③ Isolation(隔离性):
对数据进行修改的多个事务是彼此隔离的。这表明事务必须是独立的,不应该以任何方式依赖于于或影响其他事务。
实例:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,每个事务都感觉不到有其他事务在并发地执行。
④ Durability(持久性):
一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作,修改一直保持。
实例:在提交事务方法后,提示用户事务操作完成,就可以认定事务以及正确提交,即使这时数据库出现了问题,也必须要将事务完全执行完成,否则就会造成提示事务处理完毕,但数据库因为故障而没有执行事务的重大错误。

43.二叉搜索树

二叉搜索树又叫二叉查找树、二叉排序树,二叉搜索树有如下几个特点:

  • 节点的左子树小于节点本身
  • 节点的右子树大于节点本身
  • 左右子树同样为二叉搜索树
    在这里插入图片描述
    二叉排序树的优点:
    二叉排序树是一种比较有用的折衷方案。
    数组的搜索比较方便,可以直接用下标,但删除或者插入某些元素就比较麻烦。
    链表与之相反,删除和插入元素很快,但查找很慢。
    二叉排序树就既有链表的好处,也有数组的好处。 在处理大批量的动态的数据是比较有用。

二叉搜索树的查找就是我们平常说的二分查找,这种二叉搜索树好像查找效率很高,但同样它也有缺陷,如下面这样的二叉搜索树。
在这里插入图片描述
像这样的二叉查找树,如果我们要找值为 50 的节点,基本上和单链表查询没多大区别了,性能将大打折扣,这时候需要均衡二叉树,均衡二叉树就是在二叉搜索树的基础上添加了自动维持平衡的性质。 二叉树有可能出现worst-case,如果输入序列已经排序,则时间复杂度为O(N),平衡二叉树/红黑树就是为了将查找的时间复杂度保证在O(logN)范围内。
总结平衡二叉树特点:
(1)非叶子节点最多拥有两个子节点;
(2)非叶子节值大于左边子节点、小于右边子节点
(3)树的左右两边的层级数相差不会大于1;
(4)没有值相等重复的节点;

红黑树具体有哪些规则特点呢?具体如下:

  1. 节点分为红色或者黑色。
  2. 根节点必为黑色。
  3. 叶子节点都为黑色,且为 null。
  4. 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点)。
  5. 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点。
  6. 新加入到红黑树的节点为红色节点。

44.左值和右值

左值(lvalue):一个标识非临时性对象的表达式。通常来说,可以将程序中所有带名字的变量看做左值。
右值(rvalue):相对的,右值标识是临时性对象的表达式,这类对象没有指定的变量名,都是临时计算生成的。

vector<string> arr(3);
const int x = 2;
int y;
int z = x + y;
string str = "foo";
vector<string> *ptr = &arr;

在上述代码中,arr, str, y, z等都是左值,x也是一个左值,且他不是一个可修改的左值;而类似于2, x+y这类临时(没有专属变量名)的值则是右值。
左值引用:左值引用的声明是通过在某个类型后放置一个符号&来进行的。前文代码中的int & y = x;便是一个左值引用。需要注意的是,在定义左值引用时,=右边的要求是一个可修改的左值。
左值引用的用途:
(1)作为复杂名称变量的别名
(2)用于rangeFor循环
通过rangeFor循环使一个vector对象所有值都增加1,下面的rangeFor循环是做不到的

for (auto x : arr)   // x仅相当于每个元素的拷贝
    ++x;

可以通过使用引用达到这一目的

for (auto & x : arr)
    ++x;

(3)避免复制大的对象

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值