c++面试进阶

1.STL三种容器:list、vector、deque的区别:

在写C++程序的时候会发现STL是一个不错的东西,减少了代码量,使代码的复用率大大提高,减轻了程序猿的负担。还有一个就是容器,你会发现要是自己写一个链表、队列,或者是数组的时候,既要花时间还要操心怎么去维护,里面的指针啊,内存够不够用啊,长度问题,有没有可能溢出啊等等一系列的问题等着我们去解决,还是比较头疼的。所以容器的出现解决了这一个问题,它将这些数据结构都封装成了一个类,只需要加上头文件,我们就可以轻松的应用,不用那么复杂,就连指针也被封装成了迭代器,用起来更方便,更人性化,方便了我们的编程,对于程序员来说还是一大福音!!

C++中的容器类包括“顺序存储结构”和“关联存储结构”,前者包括vector,list,deque等;后者包括set,map,multiset,multimap等。若需要存储的元素数在编译器间就可以确定,可以使用数组来存储,否则,就需要用到容器类了。

1、vector
连续存储结构,每个元素在内存上是连续的;支持 高效的随机访问和在尾端插入/删除操作,但其他位置的插入/删除操作效率低下; 相当于一个数组,但是与数组的区别为:内存空间的扩展。vector支持不指定vector大小的存储,但是数组的扩展需要程序员自己写。
vector的内存分配实现原理:
STL内部实现时,首先分配一个非常大的内存空间预备进行存储,即capacity()函数返回的大小,当超过此分配的空间时再整体重新放分配一块内存存储( VS6.0是两倍,VS2005是1.5倍),所以 这给人以vector可以不指定vector即一个连续内存的大小的感觉。通常此默认的内存分配能完成大部分情况下的存储。
扩充空间(不论多大)都应该这样做:
(1)配置一块新空间
(2)将旧元素一一搬往新址
(3)把原来的空间释放还给系统
注:vector 的数据安排以及操作方式,与array 非常相似。两者的唯一差别在于空间的利用的灵活性。Array 的扩充空间要程序员自己来写。
vector类定义了好几种构造函数,用来定义和初始化vector对象:
vector v1; vector保存类型为T的对象。默认构造函数v1为空。
vector v2(v1); v2是v1的一个副本。
vector v3(n, i); v3包含n个值为i的元素。
vector v4(n); v4含有值初始化的元素的n个副本。
2、deque
连续存储结构,即其每个元素在内存上也是连续的,类似于vector,不同之处在于, deque提供了两级数组结构, 第一级完全类似于vector,代表实际容器;另一级维护容器的首位地址。这样,deque除了具有vector的所有功能外, 还支持高效的首/尾端插入/删除操作。
deque 双端队列 double-end queue
deque是在功能上合并了vector和list。
优点:(1) 随机访问方便,即支持[ ]操作符和vector.at()
(2) 在内部方便的进行插入和删除操作
(3) 可在两端进行push、pop
缺点:占用内存多
使用区别:
(1)如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
(2)如果你需要大量的插入和删除,而不关心随机存取,则应使用list
(3)如果你需要随机存取,而且关心两端数据的插入和删除,则应使用deque
3、list
非连续存储结构,具有双链表结构,每个元素维护一对前向和后向指针,因此支持前向/后向遍历。 支持高效的随机插入/删除操作,但随机访问效率低下,且由于需要额外维护指针 ,开销也比较大。每一个结点都包括一个信息快Info、一个前驱指针Pre、一个后驱指针Post。可以不分配必须的内存大小方便的进行添加和删除操作。使用的是非连续的内存空间进行存储。
优点:(1) 不使用连续内存完成动态操作。
(2) 在内部方便的进行插入和删除操作
(3) 可在两端进行push、pop
缺点:(1) 不能进行内部的随机访问,即不支持[ ]操作符和vector.at()
(2) 相对于verctor占用内存多
使用区别:
(1)如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
(2)如果你需要大量的插入和删除,而不关心随机存取,则应使用list
(3)如果你需要随机存取,而且关心两端数据的插入和删除,则应使用deque
4、vector VS. list VS. deque
a、若需要随机访问操作,则选择vector;
b、若已经知道需要存储元素的数目,则选择vector;
c、若需要随机插入/删除(不仅仅在两端),则选择list
d、只有需要在首端进行插入/删除操作的时候,还要兼顾随机访问效率,才选择deque,否则都选择vector。
e、若既需要随机插入/删除,又需要随机访问,则需要在vector与list间做个折中-deque。
f、当要存储的是大型负责类对象时,list要优于vector;当然这时候也可以用vector来存储指向对象的指针,
同样会取得较高的效率,但是指针的维护非常容易出错,因此不推荐使用。

问题一:list和vector的区别
(1)vector为存储的对象分配一块连续的地址空间 ,随机访问效率很高。但是 插入和删除需要移动大量的数据,效率较低。尤其当vector中存储
的对象较大,或者构造函数复杂,则在对现有的元素进行拷贝的时候会执行拷贝构造函数。
(2)list中的对象是离散的,随机访问需要遍历整个链表, 访问效率比vector低。但是在list中插入元素,尤其在首尾 插入,效率很高,只需要改变元素的指针。
(3)vector是单向的,而list是双向的;

(4)向量中的iterator在使用后就释放了,但是链表list不同,它的迭代器在使用后还可以继续用;链表特有的;

使用原则
(1)如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;
(2)如果需要大量高效的删除插入,而不在乎存取时间,则使用list;
(3)如果需要搞笑的随机存取,还要大量的首尾的插入删除则建议使用deque,它是list和vector的折中;
问题二:常量容器const
const vector vec(10);//这个容器里 capacity和size和值都是不能改变的, const修饰的是vector;
迭代器:const vector::const_iterrator ite; //常量迭代器;
注:const vector vec(10) —— 与const int a[10]是一回事,意思是vec只有10个元素,不能增加了,里面的元素也是不能变化的

2.指针和引用的区别:

指针和引用主要有以下区别

  • 引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。
  • 引用初始化后不能被改变,指针可以改变所指的对象。
  • 不存在指向空值的引用,但是存在指向空值的指针。

注意:引用作为函数参数时,会引发一定的问题,因为让引用作参数,目的就是想改变这个引用所指向地址的内容,而函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用,因此可能引发错误。所以使用时一定要小心谨慎。

从概念上讲。指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。

而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。

在C++中,指针和引用经常用于函数的参数传递,然而,指针传递参数和引用传递参数是有本质上的不同的:

  • 指针传递参数本质上是 值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的 实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
  • 而在引用传递过程中, 被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址被调函数对形参的任何操作都被处理成间 接寻址即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
  • 引用传递和指针传递是 不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量而对于指针 传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的 指针,或者指针引用。

为了进一步加深大家对指针和引用的区别,下面我从编译的角度来阐述它们之间的区别

程序在编译时分别将指 针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为 引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

最后,总结一下指针和引用的相同点和不同点:

★相同点

  • 都是地址的概念

指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。

★不同点

  • 指针是一个实体,而引用仅是个别名;

  • 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;

  • 引用没有const,指针有const,const的指针不可变;

  • 引用不能为空,指针可以为空;

  • “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;

  • 指针和引用的自增(++)运算意义不一样;

  • 引用是类型安全的,而指针不是 (引用比指针多了类型检查)


引用初始化

C++类中引用成员和常量成员的初始化(初始化列表)
如果一个类是这样定义的:

Class A
{
   
     public:
          A(int pram1, int pram2, int pram3);
     privite:
          int a;
          int &b;
          const int c; 
}

假如在构造函数中对三个私有变量进行赋值则通常会这样写:

A::A(int pram1, int pram2, int pram3)
{
   
     a=pram1;
     b=pram2;
     c=pram3;
}

但是,这样是编译不过的。因为常量和引用初始化必须赋值。所以上面的构造函数的写法只是简单的赋值,并不是初始化。

正确写法应该是:

A::A(int pram1, int pram2, int pram3):b(pram2),c(pram3)
{
   
     a=pram1;
}

采用初始化列表实现了对常量和引用的初始化。采用括号赋值的方法,括号赋值只能用在变量的初始化而不能用在定义之后的赋值。

凡是有引用类型的成员变量或者常量类型的变量的类,不能有缺省构造函数。默认构造函数没有对引用成员提供默认的初始化机制,也因此造成引用未初始化的编译错误。并且必须使用初始化列表进行初始化const对象、引用对象。

3.strlen和sizeof 区别

一、sizeof 运算符:计算所占的字节大小
sizeof()是运算符,其值在编译时 就已经计算好了,参数可以是数组、指针、类型、对象、函数等。

它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。

由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。实际上,用sizeof来返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。

具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:

数组——编译时分配的数组空间大小;
指针——存储该指针所用的空间大小(在32位机器上是4,64位机器上是8);
类型——该类型所占的空间大小;
对象——对象的实际占用空间大小;
函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。
二、strlen函数: 字符串的具体长度即字符个数
通过查看 strlen文档
我们知道strlen(…)是函数,要在运行时 才能计算。参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化成指针了。

它的功能是:返回字符串的长度。
该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符’\0’停止。返回的长度大小不包括‘\0’。

总结一下二者的区别
二者的区别主要是以下四点:

  • sizeof()是运算符,strlen()是库函数
  • sizeof()在编译时计算好了,strlen()在运行时计算
  • sizeof()计算出对象使用的最大字节数,strlen()计算字符串的实际长度
  • sizeof()的参数类型多样化(数组,指针,对象,函数都可以),strlen()的参数必须是字符型指针(传入数组时自动退化为指针)

4.进程和线程的区别

1.进程的概述

① 进程和线程

进程(Process)是资源分配的基本单位,线程(Thread)是CPU调度的基本单位。

  • 线程将进程的资源分和CPU调度分离开来。 以前进程既是资源分配又是CPU调度的基本单位,后来为了更好的利用高性能的CPU,将资源分配和CPU调度分开。因此,出现了线程。
  • 进程和线程的联系: 一个线程只能属于一个进程,一个进程可以拥有多个线程。线程之间共享进程资源。
  • 进程和线程的实例: 打开一个QQ,向朋友A发文字消息是一个线程,向朋友B发语音是一个线程,查看QQ空间是一个线程。QQ软件的运行是一个进程,软件中支持不同的操作,需要由线程去完成这些不同的任务。

② 进程和线程的区别

广义上的区别

  • 资源: 进程是资源分配的基本单位,线程不拥有资源,但可以共享进程资源。
  • 调度: 线程是CPU调度的基本单位,同一进程中的线程切换,不会引起进 程切换;不同进程中的线程切换,会引起进程切换。
  • 系统开销: 进程的创建和销毁时,系统都要单独为它分配和回收资源,开销远大于线程的创建和销毁;进程的上下文切换需要保存更多的信息,线程(同一进程中)的上下文切换系统开销更小。
  • 通信方式: 进程拥有各自独立的地址空间,进程间的通信需要依靠IPC;线程共享进程资源,线程间可以通过访问共享数据进行通信。
    进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
    IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

Linux系统中进程和线程的区别

  • 在Linux系统中,内核调度的单元是struct task_struct,每个进程对应一个task_struct。
  • 2.6以前的内核中没有线程的概念,内核将线程视为轻量级进程(LWP),并为每一个线程分配一个task_struct。
  • 2.6以后的内核中出现了线程组的概念,同一个进程中的线程放入一个线程组中;内核仍然视线程为轻量级进程,每个task_struct对应一个进程或者线程组中的一个线程。
  • 如果线程完全在内核态中实现(内核线程,KLT),内核调度的单元是线程。此时,进程与线程的区别非常微妙。
  • 如果线程完全在用户态实现(用户线程,ULT),内核调度的单元是进程,内核对用户线程一无所知。内核只负责分配CPU给进程,进程得到CPU会后再分配给内部的线程

③ 进程的组成

进程由程序代码、数据、进程控制块(Process Control Block, PCB)三个部分组成,即进程映像(Process Image)。

关于PCB :

  • PCB描述进程的基本信息和运行状态,所谓的创建撤销进程,都是指对 PCB 的操作。
  • PCB是一个数据结构,它常驻内存,其中的进程ID(PID)唯一标识一个进程。
  • 标识符:自身ID(PID)、父进程ID(PPID)、用户ID(UID)
  • 处理机状态:主要由处理机的各种寄存器中的内容组成,包括通用寄存器,程序计数器(PC),存放下一条要访问的指令地址;程序状态字(PSW),包含条件码、执行方式、中断屏蔽标志等状态信息;用户栈指针,存放过程和系统调用的参数及调用地址。
  • 进程调度信息 :包括进程状态,指明进程的当前状态;进程优先级;进程调度所需的其它信息,如进程已等待CPU的时间总和、进程已执行的时间总和等;事件,由执行状态转变为阻塞状态所等待发生的事件,即阻塞原因。
  • 进程控制信息:包括程序和数据的地址,是指进程的程序和数据所在的内存或外存地址;进程同步和通信机制,指实现进程同步和进程通信时必需的机制,如消息队列指针、信号量等;资源清单,进程所需的全部资源及已经分配到该进程的资源的清单;链接指针。

PCB的组织方式: 链接和索引

  • 链接:运行态、就绪态、阻塞态分别维护一个链表,每种状态的PCB通过链表连接。其中就绪态的链表只有一个PCB,因为同一时刻只有一个进程处于就绪态。
  • 索引: 运行态、就绪态、阻塞分别维护一个PCB表,该表中的每个entry指向一个PCB。
    每个进程都有自己的PID,进程依靠进程树进行组织。其中根进程的PID = 1,父进程的撤销会撤销全部的子进程。

5.抽象类和接口的区别

抽象类:抽象类是特殊的类,只是不能被实例化(将定义了一个或多个纯虚函数的类称为抽象类);除此以外,具有类的其他特性;重要的是抽象类可以包括抽象方法,这是普通类所不能的,但同时也能包括普通的方法。抽象方法只能声明于抽象类中,且不包含任何实现,派生类必须覆盖它们。另外,抽象类可以派生自一个抽象类,可以覆盖基类的抽象方法也可以不覆盖,如果不覆盖,则其派生类必须覆盖它们。虽然不能定义抽象类的实例,但是可以定义它的指针,并且指向抽象类的指针实际上在赋值时是指向其继承类的实例化对象的,这样通过统一的使用该指针可以很好的封装不同子类的实现过程,这在模式设计的过程中得到了极大的应用!

接口:接口是一个概念。它在C++中用抽象类来实现,在C#和Java中用interface来实现。

接口是引用类型的,类似于类,和抽象类的相似之处有三点

  • 1、不能实例化;
  • 2、包含未实现的方法声明;
  • 3、派生类必须实现未实现的方法,抽象类是抽象方法,接口则是所有成员(不仅是方法包括其他成员);

另外,接口有如下特性
接口除了可以包含方法之外,还可以包含属性、索引器、事件,而且这些成员都被定义为公有的。除此之外,不能包含任何其他的成员,例如:常量、域、构造函数、析构函数、静态成员。一个类可以直接继承多个接口,但只能直接继承一个类(包括抽象类)。

抽象类和接口的区别
1.接口和抽象类的概念不一样。接口是对动作的抽象,抽象类是对根源的抽象。抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什 么。比如,男人,女 人,这两个类(如果是类的话……),他们的抽象类是人。说明,他们都是人人可以吃东西,狗也可以吃东西, 你可以把“吃 东西”定义成一个接口,然后让这些类去实 现它.所以,在高级语言上,一个类只能继承一个类(抽象类)(正如人不可能同时 是生物和非生物),但 是可以实现多个接口(吃饭接口、走路接口)。

2.抽象类在定义类型方法的时候,可以给出方法的实现部分,也可以不给出;而对于接口来说,其中所定义的方法都不能给出实现部分。
3.继承类对于两者所涉及方法的实现是不同的。继承类对于抽象类所定义的抽象方法,可以不用重写,也就是说,可以延用抽象类的方法;而对于接口类所定义的方法或者属性来说,在继承类中必须要给出相应的方法和属性实现。
4.接口可以用于支持回调,而继承并不具备这个特点.
5.抽象类不能被密封,一个类一次可以实现若干个接口,但是只能扩展一个(抽象类)父类 ;。
6.抽象类实现的具体方法默认为虚的,但实现接口的类中的接口方法却默认为非虚的,当然您也可以声明为虚的.
7.(接口)与非抽象类类似,抽象类也必须为在该类的基类列表中列出的接口的所有成员提供它自己的实现。但是,允许抽象类将接口方法映射到抽象方法上。
8.抽象类实现了oop中的一个原则,把可变的与不可变的分离。抽象类和接口就是定义为不可变的,而把可变的座位子类去实现。
9.好的接口定义应该是具有专一功能性的,而不是多功能的,否则造成接口污染。(如果一个类只是为实现了这个接口的中一个功能,而但是却不得不去实现接口中的其他方法,就叫接口污染。 )
10.尽量避免使用继承来实现组建功能,而是使用黑箱复用,即对象组合。因为继承的层次增多,造成最直接的后果就是当你调用这个类群中某一 类,就必须把他们全部加载到栈中!后果可想而知.(结合堆栈原理理解)。同时,有心的朋友可以留意到微软在构建一个类时,很多时候用 到了对象组合的方法。比如asp.net中,Page类,有Server Request等属性,但其实他们都是某个类的对象。使用Page类的这个对象来调 用另外的类的方法和属性,这个是非常基本的一个设计原则。
11.如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法.

在这里插入图片描述

6.c++总的map和set有什么区别,如何实现的

map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。

map和set区别在于

(1)map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;

Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。

(2)set的迭代器是const的,不允许修改元素的值;

map允许修改value,但不允许修改key。

其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。

(3)map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。

7.C++内存管理

参看大佬博客

8.C++野指针

1.野指针与垂悬指针的区别
野指针:访问一个已销毁或者访问受限的内存区域的指针,野指针不能判断是否为NULL来避免
垂悬指针:指针正常初始化,曾指向一个对象,该对象被销毁了,但是指针未置空,那么就成了悬空指针。

2.概念
指针指向了一块随机的空间,不受程序控制。

3.野指针产生的原因

  • 1).指针定义时未被初始化:指针在被定义的时候,如果程序不对其进行初始化的话,它会随机指向一个区域,因为任意指针变量(出了static修饰的指针)它的默认值都是随机的
  • 2).指针被释放时没有置空:我们在用malloc()开辟空间的时候,要检查返回值是否为空,如果为空,则开辟失败;如果不为空,则指针指向的是开辟的内存空间的首地址。指针指向的内存空间在用free()和delete释放后,如果程序员没有对其进行置空或者其他赋值操作的话,就会成为一个野指针
  • 3).指针操作超越变量作用域:不要返回指向栈内存的指针或者引用,因为栈内存在函数结束的时候会被释放。

4.野指针的危害
问题:指针指向的内容已经无效了,而指针没有被置空,解引用一个非空的无效指针是一个未被定义的行为,也就是说不一定导致错误,野指针被定位到是哪里出现问题,在哪里指针就失效了,不好查找错误的原因。

5.规避方法
1.初始化指针的时候将其置为nullptr,之后对其操作。
2.释放指针的时候将其置为nullptr。

9.C++内存泄漏与智能指针

1.首先说到c++内存泄漏时要知道它的含义?

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

2.内存泄漏的后果?

最难捉摸也最难检测到的错误之一是内存泄漏,即未能正确释放以前分配的内存的 bug。 只发生一次的小的内存泄漏可能不会被注意,但泄漏大量内存的程序或泄漏日益增多的程序可能会表现出各种征兆:从性能不良(并且逐渐降低)到内存完全用尽。 更糟的是,泄漏的程序可能会用掉太多内存,以致另一个程序失败,而使用户无从查找问题的真正根源。 此外,即使无害的内存泄漏也可能是其他问题的征兆。

3.对于C和C++这种没有垃圾回收机制的语言来讲,我们主要关注两种类型的内存泄漏

  • (1)堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.

  • (2)系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

4.使用C/C++语言开发的软件在运行时,出现内存泄漏。可以使用以下两种方式,进行检查排除:

  • ⑴ 使用工具软件BoundsChecker,BoundsChecker是一个运行时错误检测工具,它主要定位程序运行时期发生的各种错误。

  • ⑵ 调试运行DEBUG版程序,运用以下技术:CRT(C run-time libraries)、运行时函数调用堆栈、内存泄漏时提示的内存分配序号(集成开发环境OUTPUT窗口),综合分析内存泄漏的原因,排除内存泄漏。

5.解决内存泄漏最有效的办法就是使用智能指针(Smart Pointer)。使用智能指针就不用担心这个问题了,因为智能指针可以自动删除分配的内存。智能指针和普通指针类似,只是不需要手动释放指针,而是通过智能指针自己管理内存的释放,这样就不用担心内存泄漏的问题了。

在此借鉴了“LemonLee‘s Blog”和“程序媛想事儿(Alexia)”等人的内容。

c++提供了auto_ptr、unique_ptr、shared_ptr和weak_ptr这几种智能指针(auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。)在此我们只介绍后三个智能指针:

  • (1)shared_ptr共享的智能指针
    shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。
    注意事项:

    • 1.不要用一个原始指针初始化多个shared_ptr。
    • 2.不要再函数实参中创建shared_ptr,在调用函数之前先定义以及初始化它。
    • 3.不要将this指针作为shared_ptr返回出来。
    • 4.要避免循环引用。
  • (2)unique_ptr独占的智能指针

    • <1>Unique_ptr是一个独占的智能指针,他不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另外一个 unique_ptr。

    • <2>unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再 拥有原来指针的所有权了。

    • <3>如果希望只有一个智能指针管理资源或管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

  • (3)weak_ptr弱引用的智能指针
    弱引用的智能指针weak_ptr是用来监视shared_ptr的,不会使引用计数加一,它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命 周期,更像是shared_ptr的一个助手。 weak_ptr没有重载运算符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中关连的资源是否存在。 weak_ptr还可以用来返回this指针和解决循环引用的问题。

10.多态实现

1). inliine函数可以实虚函数码?

不可以,因为inline函数没有地址,无法将他存放到虚函数表中。

2). 静态成员可以是虚函数吗?

不能,因为静态成员函数中没有this指针,使用::成员函数的嗲用用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

3). 构造函数可以是虚函数吗?

不可以,因为对象中的虚函数指针是在对象构造的时候初始化的。

4). 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

可以,最好将析构函数设置为虚函数最好是将父类的析构函数设置为虚函数 ,因为这样可以避免内存泄漏的问题。如果一个父类的指针指向了子类的的对象,并且父类的虚函数没有设置成虚函数,那么子类对象中的虚函数就没有实现多态,他只会调用父类的析构函数,不会调用子类的析构函数,但是他创建对象的时候调用了子类的构造函数,所以说就用子类的构造函数就应该该取调用他的析构函数,这样才能保证所有的必须释放的资源都是放了,才可以保证不会有内存泄漏。如果是多态的,就会先去调用子类的析构函数,然后再取调用父类的析构函数,这样子类和父类的资源就都可以释放。

5). 对象访问普通函数快还是虚函数快?

如果是普通对象,是一样快的,如果是指针对象或者是引用对象,调用普通函数更快一些,因为构成了多态,运行时调用虚函数要先到虚函数表中去查找。这样然后才拿到函数的地址,这样就不如直接可以拿到函数地址的普通函数快。

6). 虚函数表时再什么阶段生成的?他存放在哪里?

虚函数时再编译阶段生成的,他一般存放再代码段,也就是常量区。

7). 执行下面这段代码的结果

#include <iostream>
using namespace std;

class Base
{
   
public:
	virtual void x()
	{
   
		cout << "Base::x" << endl;
	}

	void y()
	{
   
		x();
		cout << "Base::y" << endl;
	}
};

class Derive : public Base
{
   
public:
	virtual void x()
	{
   
		cout << "Derive::x" << endl;
	}

	void y()
	{
   
		cout << "Derive::y" << endl;
	}
};

int main()
{
   
	Base* p = new Derive;
	p->y();
	return 0;
}

解析:很显然Derive继承了Base,并且实现了多态,但是只有x()是虚函数重写,y()只在子类中声明了虚函数,没有在父类中声名所以不能y()不是虚函数重写,而是对父类中的y()重定义,所以在p调用y()的时候直接调用Base中的y(),在Base的y()中调用了x(),由于x()在子类中构成了虚函数重写,所以调用子类中的x(),答案也就不晓而知了。

8). 是否可以将类中的所有成员函数都声明称为虚函数,为什么?

虚函数是在程序运行的时候通过寻址操作才能确定真正要调用的的函数,而普通的成员函数在编译的时候就已经确定了要调用的函数。这个两者的区别,从效率上来说,虚函数的效率要低于普通成员函数,因为虚函数要先通过对象中的虚标指针拿到虚函数表的地址,然后再从虚函数表中找到对应的函数地址,最后根据函数地址去调用,而普通成员函数直接就可以拿到地址进行调用,所以没必要将所有的成员函数声明成虚函数。

9). 虚函数表指针被编译器初始化的过程怎么理解的?

当类中声明了虚函数是,编译器会在类中生成一个虚函数表VS中存放在代码段,虚函数表实际上就是一个存放虚函数指针的指针数组,是由编译器自动生成并维护的。虚表是属于类的,不属于某个具体的对象,一个类中只需要有一个虚表即可。同一个类中的所有对象使用同一个虚表,为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在每个对象的头添加了一个指针,用来指向虚表,并且这个指针的值会自动被设置成指向类的虚表,每一个virtaul函数的函数指针存放在虚表中,如果是单继承,先将父类的虚表添加到子类的虚表中,然后子类再添加自己新增的虚函数指针,但是在VS编译器中我们通常看不到新添加的虚函数指针,是编译器故意将他们隐藏起来,如果是多继承,在子类中新添加的虚函数指针会存放在第一个继承父类的虚函数表中。

10). 多态的分类?

静态绑定的多态的是通过函数的重载来实现的。动态绑定的多态是通过虚函数实现的。

11). 为什么要引入抽象类和纯虚函数?

为了方便使用多态特性,在很多情况下由基类生成对象是很不合理的,纯虚函数在基类中是没有定义的,要求在子类必须加以实现,这种包含了纯虚函数的基类被称为抽象类,不能被实例化,如果子类没有实现纯虚函数,那么它他也是一个抽象类。

12). 虚函数和纯虚函数有什么区别?

从基类的角度出发,如果一个类中声明了虚函数,这个函数是要在类中实现的,它的作用是为了能让这个函数在他的子类中能被重写,实现动态多态。纯虚函数,只是一个接口,一个函数声明,并没有在声明他的类中实现。对于子类来说它可以不重写基类中的虚函数,但是他必须要将基类中的纯虚函数实现。虚函数既继承接口的同时也继承了基类的实现,纯虚函数关注的是接口的统一性,实现完全由子类来完成。

13). 什么是多态?他有什么作用?

多态就是一个接口多种实现,多态是面向对象的三大特性之一。多态分为静态多态和动态多态。静态多态包含函数重载和泛型编程,进程多态是程序调用函数,编译器决定使用哪个可执行的代码块。静态多态是由继承机制以及虚函实现的,通过指向派生类的基类指针或者引用,访问派生类中同名重写成员函数。堕胎的作用就是把不同子类对象都当作父类来看,可以屏蔽不同子类之间的差异,从而写出通用的代码,做出通用的编程,以适应需求的不断变化。

11.C++几个关键字总结——const、static、extern、volatile

const

const 基本原理 : 被修饰的对象的值不可以被修改

const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点。

(1)const修饰基本数据类型
表示常量,必须进行初始化,有以下两种初始化的方式:

编译时初始化: 编译器在编译时会把所有用到j的地方都替换成对应常数,如const int a=42;,即这种情况下,编译器是不为常量a分配内存的

运行时初始化:初始值不是常量表达式, 如const int i=get_size();

(2)const修饰引用
表示对常量的引用,不能通过此引用修改它所指向的对象,只是限制了这个操作,并未限制它指向的对象(可以是非const的)

可以将用一个非常量对象来初始化一个指向常量的引用(类型能自动转换即可);不可以用一个常量对象初始化一个指向非常量的引用

(3)const修饰指针
区分以下几种形式:

int a = 0;
int *const b=a;     //b是指向非常量int的常量指针,指针本身不可变,即该指针的指向(存储的地址)不可变,但可以通过它修改它所指向的对象
 
const int c = 0;
const int *d = c;  //c是指向const int的非常量指针,也就是该指针所指的对象不可变,即不能通过指针去修改它所指向的对象
                   //但可以修改它自己的存放的地址(指向)
 
 
const int* const e = c;   //e是指向const int的常量指针,指针的指向和指针所指的对象都不可变

对于指向常量的非常量指针,也只是限制了通过此指针去修改该它所指的对象这样的操作,并未限制它所指象的对象(可以是非const的)

(4)const修饰常对象
常对象是指对象常量,定义格式如下:
class A; const A a;
A const a;

定义常对象时,同样要进行初始化,并且该对象不能再被更新,修饰符const可以放在类名后面,也可以放在类名前面。

const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;

(5)const修饰函数形参
传递过来的参数在函数内不可以改变

(6)const修饰函数返回值
待补充

(7)const修饰成员函数
放在函数声明的最后面即形参列表的括号后面,如下:

class ClassName {
    
public:     
int Fun() const;   
}

表示该函数不可以修改该类的成员变量的值,并且不可以调用类中非成员函数,但非const成员函数可以调用const成员函数;
(8)在多个文件共同使用一个const常量

在这里插入图片描述2.static

主要有2种用法:

<1>限定作用域,比如

  • 修饰全局变量-全局静态变量

  • 修饰函数-静态函数

  • 修饰成员变量

  • 修饰成员函数

<2>保持变量内容持久化

修饰局部变量-局部静态变量

(1)修饰全局变量
在全局变量前加上关键字static,全局变量就定义成一个全局静态变量(限定作用域)。

全局静态变量作用域被限定,只在定义它的文件之内可见,准确地说是从定义之处开始,到文件结尾。

test.c

#include <stdio.h>
 
static int s_a = 20; // 如果不想该变量被其它文件访问,则需要加上static
 
int get_a(void)
{
   
	return s_a;
}

main.c

#include <stdio.h>
 
extern int s_a;   
extern int get_a(void);
int main()
{
   
	//printf("s_a = %d\n", s_a);   无法打印s_a,因为它是static的
	printf("s_a = %d\n", get_a()); //我们可以使用函数接口来访问s_a
	return 0;
}

(2)修饰函数
在函数返回类型前加关键字static,函数就定义成静态函数。静态函数只是在定义他的文件当中可见,不能被其他文件所用。

test.c

#include<stdio.h>
 
 
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值