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.如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法.

c++面试进阶_第1张图片

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 
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常量

c++面试进阶_第2张图片2.static

主要有2种用法:

<1>限定作用域,比如

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

  • 修饰函数-静态函数

  • 修饰成员变量

  • 修饰成员函数

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

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

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

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

test.c

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

main.c

#include 
 
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
 
 
static void printf_static_func()
{
    printf("printf_static_func() be called\n");
}
 
void test()    //要在其他文件使用只能再加一层函数调用
{
    printf_static_func();
}

test2.c

#include
 
 
static void printf_static_func()
{
    printf("printf_static_func() be called\n");
}
 
static void test()    //在其他文件中不可见,加了static,不会引起重定义错误
{
    printf_static_func();
}

mian.c

#include 
 
extern void test();
int main()
{
	test();
	return 0;
}

使用原则

  • (1)某一个函数不想被其他模块所引用,则使用static进行修饰;

  • (2)不同的文件可能函数命名有相同,此时使用static进行修饰可以解决重名的问题。

(3)修饰局部变量

在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。

作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。局部静态变量和普通局部变量的作用域是一模一样的,即都只能在定义它的函数或语句块中使用,在不同的作用域中的同名static变量的内存地址也不一样

生命周期:但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,该函数再次被调用,其值和前次调用退出时一样。

局部变量是定义在栈上,函数退出后变量就被销毁。

局部静态变量是定义在静态区,其在程序开始运行时就已经在内存里面。在整个程序结束时才会被销毁

#include 
 
void printf_call_count()
{
	int call_count = 0;     //与静态变量不在一块区域
	static int s_call_count = 0;//静态变量只初始化一次
	printf("origin s_call_count = %d\n", s_call_count);
	++s_call_count;
	printf("++s_call_count = %d\n", s_call_count);
	printf("1 s_call_count = %p,call_count = %p\n", &s_call_count, &call_count);
 
	{
		static int s_call_count = 0;    //与作用外的静态变量内存地址不一样
		++s_call_count;
		printf("2 s_call_count = %p\n\n", &s_call_count);
	}
 
}
int main()
{
	printf_call_count();  // 每次调用结果都不一样
	printf_call_count();
	printf_call_count();
	return 0;
}

c++面试进阶_第3张图片
(4)修饰成员变量-静态成员变量
一个类的成员为 static 时,即为静态成员,从属于于类,这个类无论有多少个对象被创建,这些对象共享这个 static 成员,

静态数据成员一旦被定义一直存在于程序的整个生命周期中

静态成员变量初始化::

不是由类的构造函数初始化,一般不能在类的内部初始化静态成员变量,只是在类的内部声明,在类的外部定义和初始化,格式如下:

格式:数据类型 类名:: 静态数据成员名 = 值

访问静态成员变量格式:

类名>::静态成员名

如果创建了对象,也可以用对象来访问:

 对象名.静态成员名

 对象指针->静态成员名
#include 
#include 
 
using namespace std;
 
class  demo {
public:
	demo() {}
 
	~demo() {
		cout << "~析构" << endl;
	}
 
public:
	int a;
	static  int b;
};
//初始化静态成员变量
int demo::b = 10;
 
 
 
int main(void) {
	demo demo1;
	demo demo2;
 
	//访问静态成员
	printf("demo::b = %d\n", demo::b);  //通过类访问
	printf("demo1.b = %d\n", demo1.b);  //通过对象访问
	printf("demo2.b = %d\n", demo2.b);
 
	demo::b = 100;   //通过类赋值
 
	printf("demo::b = %d\n", demo::b);
	printf("demo1.b = %d\n", demo1.b);
	printf("demo2.b = %d\n", demo2.b);
 
	return 0;
}

c++面试进阶_第4张图片
静态数据成员与普通数据成员的区别

(1)静态数据成员的类型可以是它所属的类类型,而非静态数据成员,只能声明成它所属类的指针或引用

class Bar
{
public:
     ...........
private:
    static Bar mem1; //正确
    Bar *mem2;       //正确
    Bar mem3;        //错误
};

(2)可以使用静态成员变量作为默认实参,而普通成员变量不可以,因为它的值属于对象的一部分,这么做无法真正提供一个对象以便从中获取成员的值

(3)静态成员变量使用前必须进行初始化,而普通成员变量如果不初始化,会被默认初始化

(4)对于类中的static成员变量只有在静态区中的一块内存,在编译时确定,不随对象开辟新的空间;而普通成员会随对象开辟新的空间

(5)修饰成员函数
静态成员函数没有this指针,不能声明成const,不能在static函数体内使用this指针

成员函数不用通过作用域运算符就能直接使用静态成员

可以在类的内部定义静态函数,也可以在类的外部,但static关键字只能出现在类内部的声明语句中,只出现一次

在类外调用静态成员函数用 “类名 :: ”作限定词,或通过对象调用

静态成员函数不能调用非静态成员函数或变量,但非静态成员函数可以调用静态成员函数或变量,因为静态成员从属于类,非静态成员从属于对象,静态成员在编译时就确定,而这时对象还没有被创建,所以对非静态成员并没有确定的对象来访问,而在调用对象的非静态成员函数时,静态成员已经确定,所以能正确调用

静态成员函数的应用
单例模式

//定义一个指向当前类对象的static指针变量
//定义获取单例的接口函数
#include 
#include 
 
using namespace std;
 
class  demo {
public:
	demo() {}
 
	~demo() {
		cout << "~析构" << endl;
	}
 
	/*单例模式*/
	static demo * getInstance() {
		if (single == NULL) {
			single = new demo();
		}
 
		return single;
	}
 
	//静态成员函数
	static void set_b(int _b) {
		b = _b;
	}
 
	static int get_b() {
		return b;
	}
 
public:
	int a;
 
private:
	static  int b;
	static  demo * single; //指向类的静态指针
 
	//static demo single;  虽然我们这样定义数据成员,但是在C++中没有空对象的概念,
	                    // 所以一般使用指针,来判断是否为空
};
 
int demo::b = 10;
demo * demo::single = NULL;
 
void test() {
	demo *d2 = demo::getInstance();
	cout << "d2->get_b()的值是:" << d2->get_b() << endl;
	cout << "d2->a 的值是:" << d2->a << endl;
	printf("d2 的指针是: %p\n", d2);
	d2->set_b(8888);
	d2->a = 8888;
}
 
int main(void) {
 
	//demo::b = 100;
	//没创建对象前,如何修改私有static 成员变量的值
	//使用static函数
	demo::set_b(100);
 
	demo demo1;
	demo1.set_b(1000);
	cout << "demo.get_b()的值是:" << demo::get_b() << endl;
 
	//测试单例模式
	demo * d1 = demo::getInstance();
	d1->set_b(6666);
	d1->a = 6666;
	cout << "d1->get_b()的值是:" << d1->get_b() << endl;
	cout << "d1->a 的值是:" << d1->a << endl;
	printf("d1 的指针是: %p\n", d1);
 
	test();
	cout << "after test(), d1->get_b()的值是:" << d1->get_b() << endl;
	cout << "after test(), d1->a 的值是:" << d1->a << endl;
 
	return 0;
}

c++面试进阶_第5张图片
这里对于single的定义不能声明非static成员变量,因为这样的话,就不能在getInstance()中使用single变量了,所以该类对象指针和创建实例的函数必须都为static,才方便外部使用类来调用来创建单一的实例

3.extern

extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义

(1)修饰变量
比如说:

ga.c中定义如下:

int g_a = 10;       // 定义一个全局变量

main.c中定义如下:

#include
 
extern int g_a;   // 要在这个文件中使用别的文件中定义的变量就要加extern在本文件中定义,否则会报错
{
    printf("使用ga.c的变量:g_a = %d\n", g_a);
    return 0;
}

需要注意的点

(1)变量前有extern不一定就是声明,如果变量有被初始化则是定义,没有被初始化则为声明

extern int a =0 ;//定义一个全局变量g_a 并给初值。
int a =0; //定义一个全局变量g_a,并给初值。

(2)而变量前无extern就只能是定义。

extern int g_a; //声明一个全局变量g_a
int g_a; //定义一个全局变量g_a

(3)变量可以多次声明,但只能定义在一个地方。

注:定义要为变量分配内存空间;而声明不需要为变量分配内存空间。

正确的用法

《1》int a = 0; //定义的时候赋予初始值,并在.c文件定义全局变量,不要在.h文件定义

《2》其他模块引用时

extern int a; // 声明即可。

(2)修饰函数
定义函数要有函数体,声明函数没有函数体并以分号结尾。

函数同样可以多次声明,但只能在一个地方定义。

当前模块使用外部模块的函数,建议使用extern进行声明。

test.c

#include
void test()
{
     printf("我是test.c的test()\n");
}

main.c

#include
 
extern void test();
int main()
{
    test();
    return 0;
}

注意:因为声明函数没有函数体(还有以分号结尾),所以在声明函数的时候可以将extern省略掉。但一般是在头文件(.h文件)声明的时候才省略掉extern,如果是在其他c文件声明则建议加上extern,增强代码可读性。

(3)extern与头文件的联系
原则:
(1)不要在头文件(.h文件)定义全局变量; 全局变量定义在.c文件,在.h文件只是声明即可。因为全局变量可以多次声明,但只能定义一次,如果在头文件中定义全局变量,当头文件被多个文件包含时,就会因为重定义报错

  // test.h
 
extern int g_a;
 
  // test.c
 
int g_a = 10;

(2)在.h声明函数时可以不使用extern进行修饰,且也不要在头文件定义函数,函数定义放在.c文件。

4.volatile

(1)为什么用volatile?
C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

volatile int i=10;
int a = i;
...
// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。注意,在 VC 6 中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无 volatile 关键字,对程序最终代码的影响,输入下面的代码:

实例:

#include  
void main() 
{ 
    int i = 10; 
    int a = i; 
    printf("i = %d", a); 
    // 下面汇编语句的作用就是改变内存中 i 的值,但是又不让编译器知道
 
    __asm { 
        mov dword ptr [ebp-4], 20h 
    } 
    int b = i; 
    printf("i = %d", b); 
}

然后,在 Debug 版本模式运行程序,输出结果如下:

i = 10
i = 32

然后,在 Release 版本模式运行程序,输出结果如下:

i = 10
i = 32

输出的结果明显表明,Release 模式下,编译器对代码进行了优化,第二次没有输出正确的 i 值。下面,我们把 i 的声明加上 volatile 关键字,看看有什么变化:

实例

#include  
void main() 
{ 
    volatile int i = 10; 
    int a = i; 
    printf("i = %d", a); 
    __asm { 
        mov dword ptr [ebp-4], 20h
    } 
    int b = i; 
    printf("i = %d", b); 
}

分别在 Debug 和 Release 版本运行程序,输出都是:

i = 10
i = 32

这说明这个 volatile 关键字发挥了它的作用。其实不只是内嵌汇编操纵栈"这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。一般说来,volatile用在如下的几个地方:

    1. 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
    1. 多任务环境下各任务间共享的标志应该加 volatile;
    1. 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;

(2)volatile 指针
和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念:

修饰由指针指向的对象、数据是 const 或 volatile 的:

const char* cpch;
volatile char* vpch;

注意:对于 VC,这个特性实现在 VC 8 之后才是安全的。

指针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:

char* const pchc;
char* volatile pchv;

注意

  • (1) 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
  • (2) 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
  • (3) C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。

(3)多线程下的volatile
有些变量是用 volatile 关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile 声明,该关键字的作用是防止优化编译器把变量从内存装入 CPU 寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile 的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值,如下:

volatile  BOOL  bStop  =  FALSE;

(1) 在一个线程中:

while(  !bStop  )  {  ...  }  
bStop  =  FALSE;  
return;    

(2) 在另外一个线程中,要终止上面的线程循环:

bStop  =  TRUE;  
while(  bStop  );  //等待上面的线程终止,如果bStop不使用volatile申明,那么这个循环将是一个死循环,
//因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,加上volatile,
//程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。

这个关键字是用来设定某个对象的存储位置在内存中,而不是寄存器中。因为一般的对象编译器可能会将其的拷贝放在寄存器中用以加快指令的执行速度,例如下段代码中:

...  
int  nMyCounter  =  0;  
for(;  nMyCounter<100;nMyCounter++)  
{  
...  
}  
...

在此段代码中,nMyCounter 的拷贝可能存放到某个寄存器中(循环中,对 nMyCounter 的测试及操作总是对此寄存器中的值进行),但是另外又有段代码执行了这样的操作:nMyCounter -= 1; 这个操作中,对 nMyCounter 的改变是对内存中的 nMyCounter 进行操作,于是出现了这样一个现象:nMyCounter 的改变不同步。

12.声明和定义的区别

1 声明/定义次数
变量/函数可以声明多次,变量/函数的定义只能一次。

2 分配内存
声明不会分配内存,定义会分配内存。

3 做了什么
声明是告诉编译器变量或函数的类型和名称等,定义是告诉编译器变量的值,函数具体干什么。

13.指针函数和函数指针

指针函数
指针函数是指带指针的函数,即本质是一个函数,函数返回类型是某一类型的指针。

//类型标识符 *函数名(参数表)
int *f(x,y);

首先它是一个函数,只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有函数返回值,而且,在主调函数中,函数返回值必须赋给同类型的指针变量。
表示:

float *fun();
float *p;
p = fun(a);

当一个函数声明其返回值为一个指针时,实际上就是返回一个地址给调用函数,以用于需要指针或地址的表达式中。
格式:
类型说明符 * 函数名(参数)
当然了,由于返回的是一个地址,所以类型说明符一般都是int。

函数返回的是一个地址值,经常使用在返回数组的某一元素地址上。

函数指针:
是指向函数的指针变量,即本质是一个指针变量。

    int (*f) (int x); /*声明一个函数指针 */

   f=func; /* 将func函数的首地址赋给指针f */

指向函数的指针包含了函数的地址的入口地址,可以通过它来调用函数。
声明格式如下
类型说明符 (*函数名) (参数)

其实这里不能称为函数名,应该叫做指针的变量名。这个特殊的指针指向一个返回整型值的函数。指针的声明笔削和它指向函数的声明保持一致。
指针名和指针运算符外面的括号改变了默认的运算符优先级。如果没有圆括号,就变成了一个返回整型指针的函数的原型声明。
例如:

void (*fptr)();

把函数的地址赋值给函数指针,可以采用下面两种形式:

fptr=&Function;
fptr=Function;

取地址运算符&不是必需的,因为单单一个函数标识符就标号表示了它的地址,如果是函数调用,还必须包含一个圆括号括起来的参数表。
可以采用如下两种方式来通过指针调用函数:

x=(*fptr)();
x=fptr();

第二种格式看上去和函数调用无异。但是有些程序员倾向于使用第一种格式,因为它明确指出是通过指针而非函数名来调用函数的。

14.strcpy

这有啥好问的

15.常量指针和指针常量

指针常量:顾名思义它就是一个常量,但是是指针修饰的。
格式为:

int * const p //指针常量

在这个例子下定义以下代码:

int a,b;
int * const p=&a //指针常量
//那么分为一下两种操作
*p=9;//操作成功
p=&b;//操作错误


因为声明了指针常量,说明指针变量不允许修改。如同次指针指向一个地址该地址不能被修改,但是该地址里的内容可以被修改

常量指针:如果在定义指针变量的时候,数据类型前用const修饰,被定义的指针变量就是指向常量的指针变量,指向常量的指针变量称为常量指针,格式如下

const int *p = &a; //常量指针

在这个例子下定义以下代码:

int a,b;
 const int *p=&a //常量指针
//那么分为一下两种操作
*p=9;//操作错误
p=&b;//操作成功

因为常量指针本质是指针,并且这个指针是一个指向常量的指针,指针指向的变量的值不可通过该指针修改,但是指针指向的值可以改变。

附加题
指向常量的指针常量该怎么写?
答案:

const int * const b = &a;//指向常量的指针常量

16.深拷贝与浅拷贝

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
示意图:

c++面试进阶_第6张图片
赋值和浅拷贝的区别
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

17.C++左值和右值

右值引用可以从字面意思上理解,指的是以引用传递(而非值传递)的方式使用 C++ 右值。

在 C++ 或者 C 语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。
值得一提的是,左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、“right value” 的缩写,其实不然。lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 “read value”,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)

通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法。

    1. 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。举个例子:
int a = 5;
5 = a; //错误,5 不能为左值

其中,变量 a 就是一个左值,而字面量 5 就是一个右值。值得一提的是,C++ 中的左值也可以当做右值使用,例如:

int b = 10; // b 是一个左值
a = b; // a、b 都是左值,只不过将 b 可以当做右值使用
    1. 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
      以上面定义的变量 a、b 为例,a 和 b 是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量 5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值。

注意,以上 2 种判定方法只适用于大部分场景。由于本节主要讲解右值引用,因此这里适可而止,不再对 C++ 左值和右值做深度剖析,感兴趣的读者可自行研究

C++右值引用
前面提到,其实 C++98/03 标准中就有引用,使用 “&” 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子:

int num = 10;
int &b = num; //正确
int &c = 10; //错误

如上所示,编译器允许我们为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。因此,C++98/03 标准中的引用又称为左值引用。

注意,虽然 C++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。也就是说,常量左值引用既可以操作左值,也可以操作右值,例如:

int num = 10;
const int &b = num;
const int &c = 10;

我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的。

为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。
话说,C++标准委员会在选定右值引用符号时,既希望能选用现有 C++ 内部已有的符号,还不能与 C++ 98 /03 标准产生冲突,最终选定了 2 个 ‘&’ 表示右值引用。

需要注意的,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

int num = 10;
//int && a = num;  //右值引用不能初始化为左值
int && a = 10;

和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:

int && a = 10;
a = 100;
cout << a << endl;

程序输出结果为 100。

另外值得一提的是,C++ 语法上是支持定义常量右值引用的,例如:

const int&& a = 10;//编译器不会报错

但这种定义出来的右值引用并无实际用处。一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。

学到这里,一些读者可能无法记清楚左值引用和右值引用各自可以引用左值还是右值,这里给大家一张表格,方便大家记忆:

c++面试进阶_第7张图片
表中,Y 表示支持,N 表示不支持。

18.脚本语言

(英語:Scripting language)是为了缩短传统的「编写、编译、链接、运行」(edit-compile-link-run)过程而创建的计算机编程语言。 早期的脚本语言经常被称为批处理语言或工作控制语言。 一个脚本通常是解释运行而非编译。
特点
1、脚本语言(JavaScript,VBscript等)介于HTML和C,C++,Java,C#等编程语言之间。 HTML通常用于格式化和链接文本。而编程语言通常用于向机器发出一系列复杂的指令。
2、脚本语言与编程语言也有很多相似地方,其函数与编程语言比较相像一些,其也涉及到变量。与编程语言之间最大的区别是编程语言的语法和规则更为严格和复杂一些.
3、与程序代码的关系:脚本也是一种语言,其同样由程序代码组成。
4、脚本语言是一种解释性的语言,例如Python、vbscript,javascript,installshield script,ActionScript等等,它不象c\c++等可以编译成二进制代码,以可执行文件的形式存在,脚本语言不需要编译,可以直接用,由解释器来负责解释。
5、脚本语言一般都是以文本形式存在,类似于一种命令。
举个例子说:如果建立了一个程序,叫aaa.exe,可以打开.aa为扩展名的文件,为.aa文件的编写指定了一套规则(语法),当别人编写了.aa文件后,自己的程序用这种规则来理解编写人的意图,并作出回应,那么,这一套规则就是脚本语言。 [5]
6、相对于编译型计算机编程语言:用脚本语言开发的程序在执行时,由其所对应的解释器(或称虚拟机)解释执行。系统程序设计语言是被预先编译成机器语言而执行的。脚本语言的主要特征是:程序代码即是脚本程序,亦是最终可执行文件。脚本语言可分为独立型和嵌入型,独立型脚本语言在其执行时完全依赖于解释器,而嵌入型脚本语言通常在编程语言中(如C,C++,VB,Java等)被嵌入使用。
7、和系统程序设计语言相比:不同是脚本语言是被解释而系统程序设计语言是被编译。被解释的语言由于没有编译时间而提供快速的转换,通过允许用户运行时编写应用程序,而不需要耗时的编译/打包过程。解释器使应用程序更加灵活,脚本语言的代码能够被实时生成和执行。脚本语言通常都有简单、易学、易用的特性,目的就是希望能让程序设计师快速完成程序的编写工作。

优点
快速开发:脚本语言极大地简化了“开发、部署、测试和调试”的周期过程。
容易部署:大多数脚本语言都能够随时部署,而不需要耗时的编译/打包过程。
同已有技术的集成:脚本语言被Java或者COM这样的组件技术所包围,因此能够有效地利用代码。
易学易用:很多脚本语言的技术要求通常要低一些,因此能够更容易地找到大量合适的技术人员。
动态代码:脚本语言的代码能够被实时生成和执行,这是一项高级特性,在某些应用程序里(例如JavaScript里的动态类型)是很有用也是必需的。
缺点
脚本语言不够全面:它们会要求一门“真正的”编程语言的存在,必须找一个数据库驱动程序将其内置进脚本语言里。
脚本语言并不是软件工程和构建代码结构的最佳选择,例如面向对象和基于组件的开发。
脚本语言通常不是“通用”语言,但是能够根据专门的应用来调整,例如:PHP。

19.类对象的大小?内存对齐?成员函数是否占空间?

类所占内存的大小主要是由成员变量(静态变量除外)决定的,成员函数(虚函数除外)是不计算在内的。

成员函数的存储还是以一般函数的模式进行存储。a.fun()是通过fun(a.this)来调用的。所谓成员函数只是在名义上是类里的。其实成员函数的大小不在类的对象里面,同一个类的多个对象共享函数代码。

类中的成员函数相当于C语言中一个普通函数,按照一个普通函数的方式存储在内存中,做到使得两者相联系的功臣是:this指针,连接对象与其成员函数的唯一桥梁。

在编译期中,成员函数其实被编译成了与对象无关的普通函数,但是成员函数需要知道它对应的对象是谁,因为成员函数中一般涉及到访问其对象的数据成员。这个时候this指针就会作为隐藏的第一个参数传入成员函数。this指针的地址就是对象的首地址,知道了首地址之后,成员函数便知道了其对象所在位置,就可以很方便的访问其数据成员了。

(一)

class CBase 
{ 
}; 
sizeof(CBase)=1

为什么空的类占用内存空间是1呢?
c++要求每个实例在内存中都有独一无二的地址。//注意这句话!!!!!!!!!!
空类也会被实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的sizeof为1。
(二)

class CBase 
{ 
int a; 
char p; 
}; 
sizeof(CBase)=8;

记得对齐的问题。int 占4字节//注意这点和struct的对齐原则很像!!!!!
char占一字节,补齐3字节
(三)

class CBase 
{ 
public: 
CBase(void); 
virtual ~CBase(void); 
private: 
int   a; 
char *p; 
}; 

再运行:sizeof(CBase)=12

C++ 类中有虚函数的时候有一个指向虚函数的指针(vptr),在32位系统分配指针大小为4字节。类继承自多个基类的时候可能有多个虚函数表指针,可能会占据多个内存空间。
(四)

class CChild : public CBase 
{ 
public: 
CChild(void); 
~CChild(void); 
 
virtual void test();
private: 
int b; 
}; 

输出:sizeof(CChild)=16;
可见子类的大小是本身成员变量的大小加上父类的大小。//其中有一部分是虚拟函数表的原因,一定要知道

父类子类共享一个虚函数指针

(五)

#include
class a {};
class b{};
class c:public a
{
    virtual void fun()=0;
};
class d:public b,public c{};
int main()
{
    cout<<"sizeof(a)"<<sizeof(a)<<endl;
    cout<<"sizeof(b)"<<sizeof(b)<<endl;
    cout<<"sizeof(c)"<<sizeof(c)<<endl;
    cout<<"sizeof(d)"<<sizeof(d)<<endl;
    return 0;
}

程序执行的输出结果为:

sizeof(a) =1

sizeof(b)=1

sizeof©=4

sizeof(d)=8

前三种情况比较常见,注意第四种情况。类d的大小更让初学者疑惑吧,类d是由类b,c派生迩来的,为什么是8 呢?这是因为为了提高实例在内存中的存取效率.类的大小往往被调整到系统的整数倍.并采取就近的法则,里哪个最近的倍数,就是该类的大小,所以类d的大小为8个字节.

总结

空的类是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。

(一)类内部的成员变量:

  • 普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。
  • static修饰的静态变量:不占用内容,原因是编译器将其放在全局变量区。

(二)类内部的成员函数:

  • 普通函数:不占用内存。
  • 虚函数:要占用4个以上字节,用来指定虚函数的虚拟函数表的入口地址。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系的。

20.C/C++与python的本质区别

一、通俗理解什么是编程语言
​ 首先要搞清楚"编程语言"这个概念.

​ 小时候,我们说的是"汉语",有需求了会跟父母提出,父母就会满足我们的需求,我们使用"汉语"来控制父母,让父母来做我们喜欢的事.

​ 同样,我们也可以通过’'汉语"来控制计算机,让计算机为我们做事情,这样的语言就叫做编程语言(Programming Language)

二、编程语言的种类
·对数据类型的要求

强类型语言----强调数据类型

弱类型语言----忽略数据类型

按计算机语言分类

低级语言、高级语言、专用语言、脚本语言

1.低级语言:典型的语言为汇编语言----汇编语言必须经过汇编,生成文件,然后执行
2.高级语言:C+±—高级语言源程序可以用解释、编译两种方式执行。
3.专用语言:CAD系统中的绘图语言和DBMS的数据库查询语言
4.脚本语言:是为了缩短传统的编写-编译-链接-运行过程而创建的计算机编程语言。需要有相应的脚本引擎来解释执行。一个脚本通常是解释运行而非编译。脚本语言简单、易学、易用。使程序员快速的完成程序的编写工作。

三、计算机执行方式
·计算机按执行方式可分为三种

编译型语言、解释型语言、混合型语言

1.解释型语言

·由解释器根据输入的数据当场执行而不生成任何的目标程序,
·如在终端上打一条命令或语句,解释程序就立即将此语句解释成一条或几条指令并提交硬件立即执行且将执行结果反映到终端,从终端把命令打入后,就能立即得到计算结果。这的确是很方便的,很适合于一些小型机的计算问题。但解释程序执行速度很慢,
·如果源程序中出现循环,则解释程序也重复地解释并提交执行这一组语句,这就造成很大浪费。 

2.编译型语言

·先将源代码编译成目标语言之后通过连接程序连接到生成的目标程序进行执行
·编译程序工作时,先分析,后综合,从而得到目标程序。所谓分析,是指词法分析和语法分析;所谓综合是指代码优化,存储分配和代码生成。为了完成这些分析综合任务,编译程序采用对源程序进行多次扫描的办法,每次扫描集中完成一项或几项任务,也有一项任务分散到几次扫描去完成的。

3.混合型语言

典型的语言Java,Java很特殊,Java程序也需要编译,但是没有直接编译称为机器语言,而是编译称为字节码,然后在Java虚拟机上用解释方式执行字节码。

四、C++和python的一些区别

1.通过上面的分类,我们可以将**C++和python都归类为强类型语言。**python变量无需声明并不意味着就是弱类型,弱类型是指能够进行隐式转换,python是不能这么转换的,每个实例类型是固定的,转换实例类实际上是重新创建一个内存空间。

2.C++为编译型语言;python为解释型的脚本语言。

3.C++效率高,编程难;python效率低,编程简单。同样的功能,或许python可以很快的写出代码,但运行所需的时间需要成倍于C++。

21.C++异常机制

异常处理
增强错误恢复能力是提高代码健壮性的最有力的途径之一,C语言中采用的错误处理方法被认为是紧耦合的,函数的使用者必须在非常靠近函数调用的地方编写错误处理代码,这样会使得其变得笨拙和难以使用。C++中引入了异常处理机制,这是C++的主要特征之一,是考虑问题和处理错误的一种更好的方式。使用错误处理可以带来一些优点,如下:

  • 错误处理代码的编写不再冗长乏味,并且不再和正常的代码混合在一起,程序员只需要编写希望产生的代码,然后在后面某个单独的区段里编写处理错误的嗲吗。多次调用同一个函数,则只需要某个地方编写一次错误处理代码。
  • 错误不能被忽略,如果一个函数必须向调用者发送一次错误信息。它将抛出一个描述这个错误的对象。

传统的错误处理和异常处理
在讨论异常处理之前,我们先谈谈C语言中的传统错误处理方法,这里列举了如下三种:

  • 在函数中返回错误,函数会设置一个全局的错误状态标志。
  • 使用信号来做信号处理系统,在函数中raise信号,通过signal来设置信号处理函数,这种方式耦合度非常高,而且不同的库产生的信号值可能会发生冲突
  • 使用标准C库中的非局部跳转函数 setjmp和longjmp
    这里使用setjmp和longjmp来演示下如何进行错误处理:
#include 
#include 
jmp_buf static_buf; //用来存放处理器上下文,用于跳转

void do_jmp()
{
    //do something,simetime occurs a little error
    //调用longjmp后,会载入static_buf的处理器信息,然后第二个参数作为返回点的setjmp这个函数的返回值
    longjmp(static_buf,10);//10是错误码,根据这个错误码来进行相应的处理
}

int main()
{
    int ret = 0;
    //将处理器信息保存到static_buf中,并返回0,相当于在这里做了一个标记,后面可以跳转过来
    if((ret = setjmp(static_buf)) == 0) {
        //要执行的代码
        do_jmp();
    } else {    //出现了错误
        if (ret == 10)
            std::cout << "a little error" << std::endl;
    }
}

错误处理方式看起来耦合度不是很高,正常代码和错误处理的代码分离了,处理处理的代码都汇聚在一起了。但是基于这种局部跳转的方式来处理代码,在C++中却存在很严重的问题,那就是对象不能被析构,局部跳转后不会主动去调用已经实例化对象的析构函数。这将导致内存泄露的问题。下面这个例子充分显示了这点

#include 
#include 

using namespace std;

class base {
    public:
        base() {
            cout << "base construct func call" << endl;
        }
        ~base() {
            cout << "~base destruct func call" << endl;
        }
};

jmp_buf static_buf;

void test_base() {
    base b;
    //do something
    longjmp(static_buf,47);//进行了跳转,跳转后会发现b无法析构了
}

int main() {
    if(setjmp(static_buf) == 0) {
        cout << "deal with some thing" << endl;
        test_base();
    } else {
        cout << "catch a error" << endl;
    }
}

在上面这段代码中,只有base类的构造函数会被调用,当longjmp发生了跳转后,b这个实例将不会被析构掉,但是执行流已经无法回到这里,b这个实例将不会被析构。这就是局部跳转用在C++中来处理错误的时候带来的一些问题,在C++中异常则不会有这些问题的存在。那么接下来看看如何定义一个异常,以及如何抛出一个异常和捕获异常吧.

异常的抛出

class MyError {
    const char* const data;
public:
    MyError(const char* const msg = 0):data(msg)
    {
        //idle
    }
};

void do_error() {
    throw MyError("something bad happend");
}

int main()
{
    do_error();
}

上面的例子中,通过throw抛出了一个异常类的实例,这个异常类,可以是任何一个自定义的类,通过实例化传入的参数可以表明发生的错误信息。其实异常就是一个带有异常信息的类而已。异常被抛出后,需要被捕获,从而可以从错误中进行恢复,那么接下来看看如何去捕获一个异常吧。在上面这个例子中使用抛出异常的方式来进行错误处理相比与之前使用局部跳转的实现来说,最大的不同之处就是异常抛出的代码块中,对象会被析构,称之为堆栈反解.

异常的捕获

C++中通过catch关键字来捕获异常,捕获异常后可以对异常进行处理,这个处理的语句块称为异常处理器。下面是一个简单的捕获异常的例子:

    try{
        //do something
        throw string("this is exception");
    } catch(const string& e) {
        cout << "catch a exception " << e << endl;
    }

catch有点像函数,可以有一个参数,throw抛出的异常对象,将会作为参数传递给匹配到到catch,然后进入异常处理器,上面的代码仅仅是展示了抛出一种异常的情况,加入try语句块中有可能会抛出多种异常的,那么该如何处理呢,这里是可以接多个catch语句块的,这将导致引入另外一个问题,那就是如何进行匹配。

异常的匹配
异常的匹配我认为是符合函数参数匹配的原则的,但是又有些不同,函数匹配的时候存在类型转换,但是异常则不然,在匹配过程中不会做类型的转换,下面的例子说明了这个事实:

#include 

using namespace std;
int main()
{
    try{

        throw 'a';
    }catch(int a) {
        cout << "int" << endl;
    }catch(char c) {
        cout << "char" << endl;
    }
}

上面的代码的输出结果是char,因为抛出的异常类型就是char,所以就匹配到了第二个异常处理器。可以发现在匹配过程中没有发生类型的转换。将char转换为int。尽管异常处理器不做类型转换,但是基类可以匹配到派生类这个在函数和异常匹配中都是有效的,但是需要注意catch的形参需要是引用类型或者是指针类型,否则会导致切割派生类这个问题。

//基类
class Base{
    public:
        Base(string msg):m_msg(msg)
        {
        }
        virtual void what(){
            cout << m_msg << endl;
        }
    protected:
        string m_msg;
};
//派生类,重新实现了虚函数
class CBase : public Base
{
    public:
        CBase(string msg):Base(msg)
        {

        }
        void what()
        {
           cout << "CBase:" << m_msg << endl;
        }
        void test()
        {
            cout << "I am a CBase" << endl;
        }
};


int main()
{
    try {
        //do some thing
    //抛出派生类对象
        throw CBase("I am a CBase exception");

    }catch(Base& e) {  //使用基类可以接收
        e.what();
    }
}

上面的这段代码可以正常的工作,实际上我们日常编写自己的异常处理函数的时候也是通过继承标准异常来实现字节的自定义异常的,但是如果将Base&换成Base的话,将会导致对象被切割,例如下面这段代码将会编译出错,因为CBase被切割了,导致CBase中的test函数无法被调用。

    try {
        //do some thing
        throw CBase("I am a CBase exception");

    }catch(Base e) {
        e.test();
    }

到此为此,异常的匹配算是说清楚了,总结一下,异常匹配的时候基本上遵循下面几条规则:
异常匹配除了必须要是严格的类型匹配外,还支持下面几个类型转换.

  • 允许非常量到常量的类型转换,也就是说可以抛出一个非常量类型,然后使用catch捕捉对应的常量类型版本
  • 允许从派生类到基类的类型转换
  • 允许数组被转换为数组指针,允许函数被转换为函数指针

假想一种情况,当我要实现一代代码的时候,希望无论抛出什么类型的异常我都可以捕捉到,目前来说我们只能写上一大堆的catch语句捕获所有可能在代码中出现的异常来解决这个问题,很显然这样处理起来太过繁琐,幸好C++提供了一种可以捕捉任何异常的机制,可以使用下列代码中的语法。

   catch(...) {
    //异常处理器,这里可以捕捉任何异常,带来的问题就是无法或者异常信息
   }

如果你要实现一个函数库,你捕捉了你的函数库中的一些异常,但是你只是记录日志,并不去处理这些异常,处理异常的事情会交给上层调用的代码来处理.对于这样的一个场景C++也提供了支持.

    try{
        throw Exception("I am a exception");    
    }catch(...) {
        //log the exception
        throw;
    }

通过在catch语句块中加入一个throw,就可以把当前捕获到的异常重新抛出.在异常抛出的那一节中,我在代码中抛出了一个异常,但是我没有使用任何catch语句来捕获我抛出的这个异常,执行上面的程序会出现下面的结果.

terminate called after throwing an instance of 'MyError'
Aborted (core dumped)

为什么会出现这样的结果呢?,当我们抛出一个异常的时候,异常会随着函数调用关系,一级一级向上抛出,直到被捕获才会停止,如果最终没有被捕获将会导致调用terminate函数,上面的输出就是自动调用terminate函数导致的,为了保证更大的灵活性,C++提供了set_terminate函数可以用来设置自己的terminate函数.设置完成后,抛出的异常如果没有被捕获就会被自定义的terminate函数进行处理.下面是一个使用的例子:

#include 
#include 
#include 
using namespace std;

class MyError {
    const char* const data;
public:
    MyError(const char* const msg = 0):data(msg)
    {
        //idle
    }
};

void do_error() {
    throw MyError("something bad happend");
}
//自定义的terminate函数,函数原型需要一致
void terminator()
{
    cout << "I'll be back" << endl;
    exit(0);
}

int main()
{
    //设置自定义的terminate,返回的是原有的terminate函数指针
    void (*old_terminate)() = set_terminate(terminator);
    do_error();
}
上面的代码会输出I'll be back

到此为此关于异常匹配的我所知道的知识点都已经介绍完毕了,那么接着可以看看下一个话题,异常中的资源清理.

异常中的资源清理
在谈到局部跳转的时候,说到局部调转不会调用对象的析构函数,会导致内存泄露的问题,C++中的异常则不会有这个问题,C++中通过堆栈反解将已经定义的对象进行析构,但是有一个例外就是构造函数中如果出现了异常,那么这会导致已经分配的资源无法回收,下面是一个构造函数抛出异常的例子:

#include 
#include 
using namespace std;

class base
{
    public:
        base()
        {
            cout << "I start to construct" << endl;
            if (count == 3) //构造第四个的时候抛出异常
                throw string("I am a error");
            count++;
        }

        ~base()
        {
            cout << "I will destruct " << endl;
        }
    private:
        static int count;
};

int base::count = 0;

int main()
{
        try{

            base test[5];

        } catch(...){

            cout << "catch some error" << endl;

        }
}
上面的代码输出结果是:
I start to construct
I start to construct
I start to construct
I start to construct
I will destruct 
I will destruct 
I will destruct 
catch some error

在上面的代码中构造函数发生了异常,导致对应的析构函数没有执行,因此实际编程过程中应该避免在构造函数中抛出异常,如果没有办法避免,那么一定要在构造函数中对其进行捕获进行处理.最后介绍一个知识点就是函数try语句块,如果main函数可能会抛出异常该怎么捕获?,如果构造函数中的初始化列表可能会抛出异常该怎么捕获?下面的两个例子说明了函数try语句块的用法:

#include 

using namespace std;

int main() try {
    throw "main";
} catch(const char* msg) {
    cout << msg << endl;
    return 1;
}
main函数语句块,可以捕获main函数中抛出的异常.
class Base
{
    public:
        Base(int data,string str)try:m_int(data),m_string(str)//对初始化列表中可能会出现的异常也会进行捕捉
       {
            // some initialize opt
       }catch(const char* msg) {

            cout << "catch a exception" << msg << endl;
       }

    private:
        int m_int;
        string m_string;
};

int main()
{
    Base base(1,"zhangyifei");
}

上面说了很多都是关于异常的使用,如何定义自己的异常,编写异常是否应该遵循一定的标准,在哪里使用异常,异常是否安全等等一系列的问题,下面会一一讨论的.

标准异常
C++标准库给我们提供了一系列的标准异常,这些标准异常都是从exception类派生而来,主要分为两大派生类,一类是logic_error,另一类则是runtime_error这两个类在stdexcept头文件中,前者主要是描述程序中出现的逻辑错误,例如传递了无效的参数,后者指的是那些无法预料的事件所造成的错误,例如硬件故障或内存耗尽等,这两者都提供了一个参数类型为std::string的构造函数,这样就可以将异常信息保存起来,然后通过what成员函数得到异常信息.

#include 
#include 
#include 
using namespace std;

class MyError:public runtime_error {
public:
    MyError(const string& msg = "") : runtime_error(msg) {}

};

//runtime_error logic_error 两个都是继承自标准异常,带有string构造函数
//
int main()
{
    try {
        throw MyError("my message");   
    }   catch(MyError& x) {
        cout << x.what() << endl;    
    }
}

异常规格说明
假设一个项目中使用了一些第三方的库,那么第三方库中的一些函数可能会抛出异常,但是我们不清楚,那么C++提供了一个语法,将一个函数可能会抛出的异常列出来,这样我们在编写代码的时候参考函数的异常说明即可,但是C++11中这中异常规格说明的方案已经被取消了,所以我不打算过多介绍,通过一个例子看看其基本用法即可,重点看看C++11中提供的异常说明方案:

#include 
#include 
#include 
#include 
using namespace std;

class Up{};
class Fit{};
void g();
//异常规格说明,f函数只能抛出Up 和Fit类型的异常
void f(int i)throw(Up,Fit) {
    switch(i) {
        case 1: throw Up();
        case 2: throw Fit();    
    }
    g();
}

void g() {throw 47;}

void my_ternminate() {
    cout << "I am a ternminate" << endl;
    exit(0);
}

void my_unexpected() {
    cout << "unexpected exception thrown" << endl;
 //   throw Up();
    throw 8;
    //如果在unexpected中继续抛出异常,抛出的是规格说明中的 则会被捕捉程序继续执行
    //如果抛出的异常不在异常规格说明中分两种情况
    //1.异常规格说明中有bad_exception ,那么会导致抛出一个bad_exception
    //2.异常规格说明中没有bad_exception 那么会导致程序调用ternminate函数
   // exit(0);
}

int main() {
 set_terminate(my_ternminate);
 set_unexpected(my_unexpected);
 for(int i = 1;i <=3;i++)
 {
     //当抛出的异常,并不是异常规格说明中的异常时
     //会导致最终调用系统的unexpected函数,通过set_unexpected可以
     //用来设置自己的unexpected汗函数
    try {
        f(i);    
    }catch(Up) {
        cout << "Up caught" << endl;    
    }catch(Fit) {
        cout << "Fit caught" << endl;    
    }catch(bad_exception) {
        cout << "bad exception" << endl;    
    }
 }
}

上面的代码说明了异常规格说明的基本语法,以及unexpected函数的作用,以及如何自定义自己的unexpected函数,还讨论了在unexpected函数中继续抛出异常的情况下,该如何处理抛出的异常.C++11中取消了这种异常规格说明.引入了一个noexcept函数,用于表明这个函数是否会抛出异常

void recoup(int) noexecpt(true);  //recoup不会抛出异常
void recoup(int) noexecpt(false); //recoup可能会抛出异常

此外还提供了noexecpt用来检测一个函数是否不抛出异常.

异常安全
异常安全我觉得是一个挺复杂的点,不光光需要实现函数的功能,还要保存函数不会在抛出异常的情况下,出现不一致的状态.这里举一个例子,大家在实现堆栈的时候经常看到书中的例子都是定义了一个top函数用来获得栈顶元素,还有一个返回值是void的pop函数仅仅只是把栈顶元素弹出,那么为什么没有一个pop函数可以
即弹出栈顶元素,并且还可以获得栈顶元素呢?

template<typename T> T stack<T>::pop()
{
    if(count == 0)
        throw logic_error("stack underflow");
    else
        return data[--count];
}

如果函数在最后一行抛出了一个异常,那么这导致了函数没有将退栈的元素返回,但是count已经减1了,所以函数希望得到的栈顶元素丢失了.本质原因是因为这个函数试图一次做两件事,1.返回值,2.改变堆栈的状态.最好将这两个独立的动作放到两个独立的函数中,遵守内聚设计的原则,每一个函数只做一件事.我们
再来讨论另外一个异常安全的问题,就是很常见的赋值操作符的写法,如何保证赋值操作是异常安全的.

class Bitmap {...};
class Widget {
    ...
private:
    Bitmap *pb;

};
Widget& Widget::operator=(const Widget& rhs)
{
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

上面的代码不具备自我赋值安全性,倘若rhs就是对象本身,那么将会导致*rhs.pb指向一个被删除了的对象.那么就绪改进下.加入证同性测试.

Widget& Widget::operator=(const Widget& rhs)
{
    If(this == rhs) return *this; //证同性测试
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

但是现在上面的代码依旧不符合异常安全性,因为如果delete pb执行完成后在执行new Bitmap的时候出现了异常,则会导致最终指向一块被删除的内存.现在只要稍微改变一下,就可以让上面的代码具备异常安全性.

Widget& Widget::operator=(const Widget& rhs)
{
    If(this == rhs) return *this; //证同性测试
    Bitmap *pOrig = pb;
    pb = new Bitmap(*rhs.pb); //现在这里即使发生了异常,也不会影响this指向的对象
    delete pOrig;
    return *this;   
}

这个例子看起来还是比较简单的,但是用处还是很大的,对于赋值操作符来说,很多情况都是需要重载的.

21.python中的GC机制

和java一样 python也有垃圾自动回收机制,但实现方法与java并不相同
python中以引用计数为主,零代为辅

1 引用计数机制
python里每一个东西都是对象,它们的核心就是一个结构体:PyObject

typedef struct_object {
    int ob_refcnt;
    struct_typeobject *ob_type;
} PyObject;

PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少
如果一个对象的引用数量降为0,会立即释放占用的内存

引用计数机制的优点:

  • 简单
  • 实时性 即一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。

引用计数机制的缺点:

  • 维护引用计数消耗资源,在对象内部保留一小部分空间用以存储引用数,同时在操作大量数据时不得不耗费精力处理每个对象的引用数
  • 引用计数不能处理环形数据结构–也就是含有循环引用的数据结构。

第一个缺点在当前硬件条件下并不明显,但第二个在某些情况下却是致命危险,可能引起内存泄露,所以python又引入了标记清除和分代回收

2标记-清除
标记清除就是用来解决循环引用的问题的只有容器对象才会出现引用循环,比如列表、字典、类、元组。 首先,为了追踪容器对象,需要每个容器对象维护两个额外的指针, 用来将容器对象组成一个链表,指针分别指向前后两个容器对象,方便插入和删除操作。试想一下,现在有两种情况:
A:

a=[1,3]
b=[2,4]
a.append(b)
b.append(a)
del a
del b

B:

a=[1,3]
b=[2,4]
a.append(b)
b.append(a)
del a

Okay,现在开始说正题。在标记-清除算法中,有两个集中营,一个是root链表(root object),另外一个是unreachable链表。

     对于情景A,原来再未执行DEL语句的时候,a,b的引用计数都为2(init+append=2),但是在DEL执行完以后,a,b引用次数互相减1。a,b陷入循环引用的圈子中,然后标记-清除算法开始出来做事,找到其中一端a,开始拆这个a,b的引用环(我们从A出发,因为它有一个对B的引用,则将B的引用计数减1;然后顺着引用达到B,因为B有一个对A的引用,同样将A的引用减1,这样,就完成了循环引用对象间环摘除。),去掉以后发现,a,b循环引用变为了0,所以a,b就被处理到unreachable链表中直接被做掉。

    对于情景B,简单一看那b取环后引用计数还为1,但是a取环,就为0了。这个时候a已经进入unreachable链表中,已经被判为死刑了,但是这个时候,root链表中有b。如果a被做掉,那世界上还有什么正义... ,在root链表中的b会被进行引用检测引用了a,如果a被做掉了,那么b就...凉凉,一审完事,二审a无罪,所以被拉到了root链表中。

Q&A: 为什么要搞这两个链表

之所以要剖成两个链表,是基于这样的一种考虑:现在的unreachable可能存在被root链表中的对象,直接或间接引用的对象,这些对象是不能被回收的,一旦在标记的过程中,发现这样的对象,就将其从unreachable链表中移到root链表中;当完成标记后,unreachable链表中剩下的所有对象就是名副其实的垃圾对象了,接下来的垃圾回收只需限制在unreachable链表中即可。

3 分代回收
也叫零代回收(Generation Zero)或隔代回收,基本思想是 大部分对象生命期很短,对年轻代和老年代使用不同的算法可以提高效率。 新创建出来的对象放在零代链表上,经过一段时间后gc检测零代链表中是否有循环引用,有则引用计数减1,当引用计数为0时释放内存,大于0且没有循环引用之后会将对象放入一代链表,再经过一段时间后检查一代放入三代。这就是新生代和老年代


了解分类回收,首先要了解一下,GC的阈值,所谓阈值就是一个临界点的值。随着你的程序运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,创建==释放数量应该是这样子。但是如果存在循环引用的话,肯定是创建>释放数量,当创建数与释放数量的差值达到规定的阈值的时候,当当当当~分代回收机制就登场啦。

垃圾回收=垃圾检测+释放。

分代回收思想将对象分为三代(generation 0,1,2),0代表幼年对象,1代表青年对象,2代表老年对象。根据弱代假说(越年轻的对象越容易死掉,老的对象通常会存活更久。) 新生的对象被放入0代,如果该对象在第0代的一次gc垃圾回收中活了下来,那么它就被放到第1代里面(它就升级了)。如果第1代里面的对象在第1代的一次gc垃圾回收中活了下来,它就被放到第2代里面。gc.set_threshold(threshold0[,threshold1[,threshold2]])设置gc每一代垃圾回收所触发的阈值。从上一次第0代gc后,如果分配对象的个数减去释放对象的个数大于threshold0,那么就会对第0代中的对象进行gc垃圾回收检查。 从上一次第1代gc后,如过第0代被gc垃圾回收的次数大于threshold1,那么就会对第1代中的对象进行gc垃圾回收检查。同样,从上一次第2代gc后,如过第1代被gc垃圾回收的次数大于threshold2,那么就会对第2代中的对象进行gc垃圾回收检查。

22.struct与class的区别

一.首先看一下C中struct
1.struct的定义

struct A
{
	int a;
	int b;
	//成员列表
};

注意:因为struct是一种数据类型,那么就肯定不能定义函数,所以在面向c的过程中,struct不能包含任何函数。否则编译器会报错

面向过程的编程认为,数据和数据操作是分开的。然而当struct进入面向对象的c++时,其特性也有了新发展,就拿上面的错误函数来说,在c++中就能运行,因为在c++中认为数据和数据对象是一个整体,不应该分开,这就是struct在c和c++两个时代的差别。

在C++中struct得到了很大的扩充:

1.struct可以包括成员函数

2.struct可以实现继承

3.struct可以实现多态

二.strcut和class的区别
1.默认的继承访问权。class默认的是private,strcut默认的是public。

struct A
{
	int a;
};
 
struct B: A
{
	int b;
};

例如上边的代码,strcut B就是公有继承(public)的struct A。如果将strcut变为 class 那么将会是私有继承(private)这里就不做展示了。所以我们在写类的时候都会显示的写出是公有继承还是私有继承 。
当然,到底默认是public继承还是private继承,取决于子类而不是基类。我的意思是,struct可以继承class,同样class也可以继承struct,那么默认的继承访问权限是看子类到底是用的struct还是class。如下:

struct A
{
	int a;
};
 
struct B: A   //共有继承
{
	int b;
};
 
class C: A    //私有继承
{
	int c
};

2.默认访问权限:struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。

struct A
{
	int a;
};
int main()
{
	A n;
	n.a = 10;
	return 0;
}
 
//可以在类外访问成员变量,所以struct默认是共有的
 
class B
{
	int b;
};
 
int main()
{
 
	B n1;
	n1.b = 10;
	return 0;
}
 
//在内外无法访问私有变量

请看编译结果:(运行环境vs2013)
在这里插入图片描述

3.“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。

从上面的区别,我们可以看出,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。

4.class和struct在使用大括号{ }上的区别

关于使用大括号初始化

  • 1.)class和struct如果定义了构造函数的话,都不能用大括号进行初始化
  • 2.)如果没有定义构造函数,struct可以用大括号初始化。
  • 3.)如果没有定义构造函数,且所有成员变量全是public的话,class可以用大括号初始化。

虽然感觉struct是多余的,但考虑到“对c兼容”就将struct保留了下来,并做了一些扩展使其更适合面向对象,所以c++中的struct再也不是c中的那个了。
两者最大的区别就在于思想上,c语言编程单位是函数,语句是程序的基本单元。而C++语言的编程单位是类。从c到c++的设计有过程设计为中心向以数据组织为中心转移。

23.面向对象与面向过程的区别

面向过程(pop)和面向对象(oop)是什么
pop(Process-oriented programming)的缩写,“面向过程”是一种是事件为中心的编程思想。就是分析出解决问题所需的步骤,然后用函数把这写步骤实现,并按顺序调用。
oop(Object Oriented Programming)的缩写面向对象:用线性的思维。与面向过程相辅相成。在软件开发过程中,宏观上,用面向对象来把握事物间复杂的关系,分析系统。微观上,仍然使用面向过程。”面向对象“是以“对象”为中心的编程思想。
举例说明
简单的举个例子:汽车发动、汽车到站。汽车启动是一个事件,汽车到站是另一个事件,面向过程编程(pop)的过程中我们关心的是事件,而不是汽车本身。针对上述两个事件,形成两个函数,之后依次调用。
对于面向对象(oop)来说,我们关心的是汽车这类对象,两个事件只是这类对象所具有的行为。而且对于这两个行为的顺序没有强制要求。
总结: 面向过程的思维方式是分析综合,面向对象的思维方式是构造。

可拓展性对比

简单来说:用面向过程(pop)的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。所谓盖浇饭,就是在米饭上面浇上一份盖菜,你喜欢什么菜,你就浇上什么菜。我觉得这个比喻还是比较贴切的。蛋炒饭制作的细节,我不太清楚,因为我没当过厨师,也不会做饭,但最后的一道工序肯定是把米饭和鸡蛋混在一起炒匀。盖浇饭呢,则是把米饭和盖菜分别做好,你如果要一份红烧肉盖饭呢,就给你浇一份红烧肉;如果要一份青椒土豆盖浇饭,就给浇一份青椒土豆丝。

蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是全部倒掉,重新做一份青菜炒饭了。盖浇饭就没这么多麻烦,你只需要把上面的盖菜拨掉,更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。到底是蛋炒饭好还是盖浇饭好呢?其实这类问题都很难回答,非要比个上下高低的话,就必须设定一个场景,否则只能说是各有所长。那么从饭馆角度来讲的话,做盖浇饭显然比蛋炒饭更有优势,他可以组合出来任意多的组合,而且不会浪费。

盖浇饭的好处就是“菜”“饭”分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是“可维护性”比较好,“饭” 和“菜”的耦合度比较低。蛋炒饭将“蛋”“饭”搅和在一起,想换“蛋”“饭”中任何一种都很困难,耦合度很高,以至于“可维护性”比较差。软件工程追求的目标之一就是可维护性,可维护性主要表现在3个方面:可理解性、可测试性和可修改性。面向对象的好处之一就是显著的改善了软件系统的可维护性。

面向过程(POP)和面向对象(OOP)是不是就是指编码的两种方式呢?不是!你拿到了一个用户需求,比如有人要找你编个软件,你是不是需要经过需求分析,然后进行总体/详细设计,最后编码,才能最终写出软件,交付给用户。这个过程是符合人类基本行为方式的:先想做什么,再想如何去做,最后才是做事情。有的同学说:“我没按照你说的步骤做啊,我是直接编码的”。其实,你一定会经历了这三个阶段,只不过你潜意识里没有分得那么清楚。对于拿到需求就编码的人,可能编着编着,又得倒回去重新琢磨,还是免不了这些过程,

以OO为例,对应于软件开发的过程,OO衍生出3个概念:OOA、OOD和OOP。采用面向对象进行分析的方式称为OOA,采用面向对象进行设计的方式称为OOD,采用面向对象进行编码的方式称为OOP。面向过程(OP)和面向对象(OO)本质的区别在于分析方式的不同,最终导致了编码方式的不同。

面向过程总结

面向过程是一种自顶向下的编程。
面向过程优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展

面向对象总结

面向对象是将事物高度抽象化。面向对象必须先建立抽象模型,之后直接使用模型就行了。
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低

24.C++重载与重写

重载
是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

#include

using namespace std;

class A
{
	void fun() {};
	void fun(int i) {};
	void fun(int i, int j) {};
};

重写(覆写)

是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

#include

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
}

重载和重写的区别

(1)范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。

(2)参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。

(3)virtual的区别:重写的基类函数必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。

25.编译、翻译、解释运行区别

翻译程序是指这样一个程序,它把一种语言所写的源程序翻译成与之等价的另一种语言的目标程序。

编译程序是一种翻译程序,它把高级语言所写的源程序翻译成等价的机器语言或汇编语言的目标程序。

解释程序也是一种翻译程序,它将源程序作为输入并执行它,边解释边执行。它与编译程序的主要区别在于在解释程序执行的过程中不产生目标程序,而是按照源语言的定义解释执行源程序本身。

编译过程为:

  • 1.词法分析

  • 2.语法分析

  • 3.语义分析及中间代码生成

  • 4.代码优化

  • 5.目标代码生成

26.构造函数与成员函数的区别?

构造函数是一种特殊的方法,主要用来在创建对象时初始化对象即为对象成员变量赋初始值。总与new运算符一起使用在创建对象的语句中。特别的,一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们,即构造函数的重载。

构造函数与其他方法的区别

1.构造函数的命名必须和类名完全相同;而一般方法则不能和类名相同.

2.构造函数的功能主要用于在类的对象创建时定义初始化的状态.它没有返回值,也不能用void来修饰.这就保证了它不仅什么也不用自动返回,而且根本不能有任何选择.而其他方法都有返回值.即使是void返回值,尽管方法体本身不会自动返回什么,但仍然可以让它返回一些东西,而这些东西可能是不安全的.

3.构造函数不能被直接调用,必须通过new运算符在创建对象时才会自动调用,一般方法在程序执行到它的时候被调用.

4.当定义一个类定义的时候,通常情况下都会显示该类的构造函数,并在函数中指定初始化的工作也可省略不去Java编译器会提供一个默认的构造函数.此默认构造函数是不带参数的.而一般方法不存在这一特点

构造函数有以下特点

1.构造函数的名字必须与类名相同;

2.构造函数可以有任意类型的参数,但不能具有返回类型;

3.定义对象时,编译系统会自动地调用构造函数;

4.构造函数是特殊的成员函数,函数体可以在类体内,也可写在类体外;

5.构造函数被声明为公有函数,但它不能像其他成员函数那样被显式调用,它是在定义对象的同时被调用的。

你可能感兴趣的:(c++,面试,c语言)