C++的内存分配机制
C++的内存有5个储存区:
- 栈,分配给函数局部变量的储存单元,函数结束后会自动释放,但递归是会极大占用栈的资源,容易导致栈的溢出。栈的分配效率较高,内存的生长方向是向下的。
- 堆,由new或者malloc创建,若用户不手动释放此内存,会在程序结束时自动释放。内存的生长方向是向上的。
- 全局/静态储存区, 储存全局变量与静态变量的区域,此区域的内存在编译时分配。
- 常量储存区,用于储存常量,常量内容不能更改。
new与malloc的区别
- 由new分配的内存本质上是分配在自由储存区的,而malloc则是在栈上进行分配的。
- 返回结果不同,new成功分配内存后会返回对象类型的指针,而malloc返回的是void*,需要进行一次额外的强制转换。
- 若new分配内存失败会返回bac_alloc异常,而malloc分配失败会返回NULL。
- 使用new分配时不需要指定内存的大小,而使用malloc需要指定分配内存的大小。
- 使用new来为对象分配内存时会执行构造函数,同理使用delete会执行析构函数,而malloc与free则不会。
- new与delete可以被重载,而malloc与free则不可以。
- 使用malloc分配的内存如果使用时发现内存不足时可以使用realloc进行内存的扩充,而new则没有类似直观的用法。
什么是野指针与野指针的来历
野指针并不是空指针,空指针是指不指向任何实际内容的指针,而野指针是指不知道指向何处的指针,形成原因主要有以下几个方面:
- 指针没有被初始化,指针指向的内容时随机的,指针变量应该在创建的同时进行初始化。
- 在删除由new创建的指针后,没有为指针赋NULL值。
- 指针操作超过了变量的作用范围,第一种情况是指向了不存在的内存,或者时指向临时变量的引用,当临时变量失效时指针指向了一块无法使用的内存。
const与static的区别
- const定义的常量会在超出作用域之后释放。static定义的静态常量在函数执行之后不会释放其储存空间。
- const只能通过构造函数初始化列表进行初始化。static需要在类的外部进行初始化过程。如果想要建立在每个类中都恒定的常量,需要使用const static。
- const成员的作用域为整个对象,而static的作用域为整个类。
- const成员函数的主要目的时防止成员函数修改对象内容,因此const成员函数不能修改成员变量的值,但可以访问成员变量,只有const成员函数才可以访问成员变量。static成员函数主要目的是作为类作用域的全局函数,不能访问类的非静态成员变量,且类的静态成员没有this指针,这导致static成员函数不能声明为virtual。
结构体与联合体的区别
- union联合体是一种自定义数据类型,一个不同种类的变量共占一段内存。联合体只能存构成其数据类型的一种。在多种数据只取其一使用的时候可以使用联合体。
- struct结构体是一种自定义数据类型,每个数据类型占有一块单独的内存。
什么是智能指针,有哪些用途
在C++编码工程中,很多操作如new,需要在堆上分配内存,我们知道,在new一个新的对象时,会执行对象的构造函数,同时在对象超出使用范围时会自动执行析构函数,然而,当new的对象是一个常规指针呢,值得注意的是只有对象指针才有析构函数,因此需要智能指针来减少这种情况发生。因此智能指针的设计思想为:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
C++提供的智能指针
- auto_ptr
- unique_ptr
- shared_ptr
- weak_ptr
auto_ptr.在C++中auto_ptr是最先使用的智能指针之一,但是由于auto_ptr指针的一些弊端,此指针已经被弃用,如下语句:
auto_ptr<string> ps(new string("abc"));
auto_ptr<string> pv;
pv = ps;
cout<<ps;
上述语句乍一看没有问题,编译也可以通过,但其实是存在问题的,在将ps赋值给pv时,指针的所有权也发生了转让,指针ps指向的为一段空的内存,因此程序报错,防止这种情况发生的方式有两种:
- 建立指针的所有权时提高标准,在编译阶段诊断出所有权转让发生的问题,unique_ptr即采用此方法。
- 创建智能程度更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。当赋值时计数+1,当超出作用域时计数-1,当0时才进行delete操作。shared_ptr采用的就是此方法。
因此,在程序需要多个指向同一对象的指针时可以使用shared_ptr,反之使用unique_ptr.
mutable关键字的作用是什么
当需要在const方法中修改对象的数据成员时,可以在数据成员前使用mutable关键字,防止出现编译出错。使用此方法可以确保程序编译通过。
拷贝构造函数与赋值运算符的重载
拷贝分为深拷贝与浅拷贝两种,在某些情况下,类内成员需要动态开辟内存,浅拷贝会使两个不同对象的同一成员指向的内存段相同,因此在对任意一个进行析构时会使另一个对象内存指向未知位置,造成野指针现象,深拷贝则不会有此种现象发生。
基本的拷贝构造函数如下:
class A{
public:
A(int a) : a_(a){}
~A(){}
A(const A &a){
a_ = a.a_;
}
int a_;
};
即便我们不自己重写拷贝构造函数,系统也会存在默认的拷贝构造函数,但此拷贝大多数时浅拷贝,使用时风险较大。使用拷贝构造函数的情况如下:
- 一个对象以值传递的形式传递至函数体
- 一个对象以值传递的形式从函数返回
- 一个对象根据另一个对象进行初始化
赋值运算符的重载也会用于对象的拷贝当中:
class A{
public:
A(int a) : a_(a){}
~A(){}
int a_;
A &operator =(const A& a){
a_ = a.a_;
return *this;
}
};
上述=的重载中函数参数为引用是为了在赋值是减少一次对象的拷贝过程,提高程序的运行效率,而返回引用则是为了可以进行连续的赋值操作。同时此种赋值运算符也存在深浅拷贝。
C++中explicit关键字的作用
禁制将构造函数作为转换函数,即禁止构造函数自动进行隐式类型转换。
例如类A的构造函数仅有一个参数num,在构建类A时可以使用A a = 10.使用explicit关键字可以防止这种转换的发生。
虚继承的作用是什么
在多继承中,子类可能同时拥有多个父类,如果这些父类还有相同的父类,那么子类中就会有多份祖先类。例如,类B和类C都继承与类A,如果类D派生于B与C,那么类D中就会有两份A。为了防止在多继承中子类存在重复父类的情况,可以在父类继承时使用虚函数,即在类B和类C继承类A时使用virtual关键字,例如
class B: virtual public A
class C: virtual public A
进程与线程的区别,进程之间如何通信?线程之间如何通信?
进程
狭义上讲,进程就是一段程序的执行过程,主要有几个特点
- 进程是一个实体,每个进程都有自己的储存空间,一般情况下包括文本区域,数据区域,堆栈区域。文本区域储存处理器执行的代码,数据区域储存变量和进程执行期间使用的动态分配的内存,堆栈区域储存活动过程中调用的指令和本地变量。
- 进程是一个“执行中的程序”。只有处理器赋予程序声明时才称之为进程。
- 进程有三个状态,就绪,运行,阻塞。就绪状态是指程序获取了除cpu外的所有资源,只要处理器分配资源就可以开始执行。运行状态就是进程正在执行,阻塞状态就是进程阻塞,等待阻塞条件满足时才继续执行。
线程
通常在一个进程中可以包括多个线程,且至少有一个线程。线程可以利用进程拥有的资源,在引入进程的系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。在很多程序中,都使用了多线程的技术,可以使系统同时完成多个任务。
线程与进程的区别
- 一个程序至少有一个进程,一个进程至少有一个线程
- 线程的划分尺度小于进程,使得多线程的程序并发性高
- 进程在执行过程中拥有独立的内存单元,而多个线程共享一个内存单元,极大的提高了程序的运行效率
- 线程不能够独立执行,必须依赖进程才可以执行,而进程可以独立执行
- 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行,但是操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。
进程间的通信方式
- 管道
- 信号,Linux系统下通过kill发出
- 消息队列
- 共享内存
- 不同机器之间可以使用socket套接字来实现通信
线程间通信方式
- 使用全局变量
- 线程的消息队列
- 事件
线程间同步方式
- 互斥量
- 临界区
- 事件
C++指针与引用的区别
- 引用被创建时必须直接进行初始化,指针可以在任意时刻初始化
- 一旦一个引用被初始化为指向一个对象,就不能指向其他对象,而指针可以改变指向的对象
- 不能有NULL引用
- 指针是一个实体,而引用仅是一个变量的别名
- 程序会向指针变量分配内存,而引用不会分配内存
C++网络编程的基本步骤
服务端
- 调用socket函数创建套接字
- 调用bind函数调用ip地址以及端口号
- 调用listen函数转为可接受请求状态
- 调用accept函数受理连接请求,在未连接到请求之前accept函数不会返回,accept函数会返回一个用于发送接收数据的套接字。
客户端
- 调用socket函数创建套接字
- 调用connect函数向服务器发送连接请求(需要在服务端listen之后,即服务端确定可以接受请求之后才能进行连接,否则函数调用会出现错误)
套接字的基本类别
- 面向连接的套接字,主要特点包括:数据在传输过程中不会消失,数据按序传递,传输过程中不存在数据边界问题。tcp就属于面向连接的协议。
- 面向消息的套接字,主要特点包括:强调快速传输而非顺序传输,传输的数据可能丢失也可能损坏,传输的数据有数据边界,限制每次传输的数据大小。
网络通信的四个层次与作用
网络通信有四个层次框架,当应用需要从网络传递与接受数据时这四个层次会分别对数据进行解析,完成数据的传递与接收过程:
当http或者其他应用发起一个请求时,传输层与网络层会对数据内容进行加工,最终到达链路层形成以太数据包,此数据包通过物理地址传递给服务端,服务端经过网络层与传输层对以太数据包数据进行解包,成为应用层可以使用的数据格式。
链路层
链路层时陆离链接领域标准化的结果,链路层定义了各种网络标准,网络之间的数据就通过这些网络进行数据的传输。
网络层
链路层可以将数据发送出去,但是发送的路径,发送给谁就要由网络层进行决定。网络层为了解决这些问题制定了三个协议,分别是ip协议,ARP协议,路由协议。
IP协议用处最为广泛,我们判断两台主机是否处与同一网络使用的就是IPV4地址簇。同时ip地址还分为两个部分,分别为网络地址与主机地址。
ARP协议。ARP协议是根据ip地址获取MAC地址的一个网络层协议。
路由协议。若两个主机不再一个网段内,路由协议会将数据内容发送给本子网的网管进行路由。
传输层
传输层以网络层提供的传输路径信息为基础进行数据的传输,首先诞生的是UDP协议,UDP协议较为简单,客户端与服务端之间没有确认机制,数据包一旦发出,无法知道对方是否收到,因此可靠性较差,为了解决这个问题,TCP协议诞生了。
TCP协议是一种面向连接的,可靠的,基于字节流的通信协议。简单来说TCP就是有确认机制的UDP协议,每发出一个数据包都要求确认,如果有一个数据包丢失,收不到确认,发送方就必须重发这个数据包。
应用层
应用层是程序员们最为关注的一层,使用人员通过应用层来操控数据的发送与接收,而以上三层的具体实现则无需考虑。
TCP/IP协议的三次握手
TCP不仅在连接过程中会进行三次通话建立连接,在实际通信中也是如此,这种过程又成为三次握手
首先,请求连接的主机A向主机B传递如下信息:
[SYN] SEQ: 1000, ACK:-
此消息中SEQ为1000,ACK为空,而SEQ的含义如下:
“现传递的数据包序号为1000,如果接收无误,请通知我向您传递1001号数据包”
这是首次请求连接时使用的消息,又称为SYN,表示收发数据前传输的同步消息。接下来主机B向A传递如下消息:
[SYN+ACK] SEQ: 2000,ACK:1001
此时SEQ为2000,ACK为1001,而SEQ为2000的含义如下:
“现传递的数据包序号为2000,如果接收无误,请通知我向您传递2001号数据包”
此条数据对主机A首次传输的数据包的确认消息(ACK 1001)和为主机B传输数据做准备的同步消息(SEQ 2000)捆绑发送,因此,此种类型的消息又称为SYN+ACK.
最后主机A向主机B传输的消息为:
[ACK] SEQ:1001, ACK:2001
因为TCP连接过程中发送数据包时需要分配序号。因此此时的SEQ值为1001,1000+1.此时传递的消息如下:
“已经正确收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包”。
上述三个步骤完成,主机A与主机B就确认了彼此均就绪
C++ Const与define的区别
- const定义的变量,带有类型而define定义的数据没有类型
- define是在程序的预编译阶段起的作用,而const是在程序的编译,运行阶段起的作用
- define只是简单的进行字符串替换,并没有进行类型检查,容易造成各种边界错误,而const提供了类型检查。
- const常量是可以进行调试的,而define是无法进行调试的,在预编译阶段就已经替换了
C/C++程序编译过程
C/C++程序编译主要有四个步骤,分别为:
- 编译预处理阶段
- 编译,优化阶段
- 汇编过程
- 链接过程
下面分别大致介绍一下这几个过程:
预处理阶段
预处阶段主要处理程序中的宏定义指令,条件编译指令,头文件包含指令等一系列预处理指令。
编译阶段
将预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。
汇编阶段
将编译完成的汇编代码文件翻译成机器指令,并生成可重新定位目标程序的.o文件,该文件为二进制文件,字节编码是机械指令。
链接阶段
由汇编生成的文件并不能直接执行,当一个源文件中引用了另一个源文件的内容时,就需要链接程序的处理方法来解决此种问题。程序链接的方式有两种,分别是静态链接与动态链接。下面会详细介绍两者的区别。
静态库与动态库的含义,两者区别是什么?
首先理解什么是库,库是现成的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库。
本质上来说库是一种可执行的二进制形式,可以被操作系统载入内存执行,库有两种:静态库(.a, .lib)和动态库(.so, .dll).
所谓静态与动态,是指程序链接阶段时的处理方式。
静态库
之所以称之为静态库,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式成为静态链接。其实静态库可以看作是一组目标文件(.o/.obj)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:
- 静态库的函数库的链接是放在编译时期完成的
- 程序在运行时与函数库再无瓜葛,移植方便
- 浪费资源与空间,因为所有相关的目标文件会牵扯到函数库被链接合成一个可执行文件
动态库
在大多是情况下静态库已经可以胜任很多工作了,但是为什么还需要动态库呢?
- 静态库造成的空间浪费现象较大,多个不同程序使用同一静态库会造成相同内容的大量拷贝。
- 静态库对程序的更新,部署和发布会带来麻烦。如果静态库lib更新了,会使所有使用他的应用程序都需要重新编译。
动态库则没有这些问题出现,动态库在程序编译时并不会连接到目标代码中,而是在程序运行时才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要一份该共享库的实例即可。动态库的一些特点如下:
- 动态库把一些库函数的链接载入推迟到程序运行时的时期。
- 实现进程之间的资源共享
- 使程序的升级变得简单
- 程序的移植较为困难,需要对不同的动态链接库分别操作。
C++多态是如何实现的
C++的多态分为静态多态与动态多态,静态多态在程序编译时期确定实现,而动态多态在程序运行时期决定。
静态多态分为两种,分别为重载、模板
动态多态主要通过虚函数来实现,父类的指针指向派生类对象,执行派生类函数。虚函数通过虚函数表与虚函数指针实现。
static关键字的作用
- 在函数体内,一个被声明为静态的变量在这个函数的执行过程中保持不变
- 在模块内,函数体外,一个静态变量可以被模块内的所有函数访问,但不可以被模块外的函数访问
- 在模块内,一个被声明为静态的函数仅可以由模块内的函数调用,不可以被模块外的函数调用