C++面试题目解析01

1,new  malloc的实现区别,

【{      0.       属性

 

      new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。

 

      1.       参数

 

      使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

 

      2.       返回类型

 

      new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

 

      3.       分配失败

 

      new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

 

      4.      自定义类型

 

      new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。

 

      malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

 

      5.      重载

 

      C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。

 

      6.       内存区域

 

      new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。}】

 

2,解释struct的字节对齐,估计是昨天的试卷这个错误比较严重,,为什么要对齐,

 

     如何实现两个结构体的比较相等,可以用compar()按字节比较吗

【什么是字节对齐

 

  现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

 

字节对齐的原因和作用

 

  各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那 么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数 据。显然在读取效率上下降很多。

】【

 

3,指针和引用的区别

。【1.指针和引用的定义和性质区别:

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来

的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:

int a=1;int *p=&a;

int a=1;int& b=a;

上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。

而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单

元。

(2)引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。

(3)可以有const指针,但是没有const引用;

(4)指针可以有多级,但是引用只能是一级(int **p;合法 而 int&& a是不合法的)

(5)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(6)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

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

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

(9)如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;

2.指针和引用作为函数参数进行传递时的区别。

(1)指针作为参数进行传递:

使用指针传递参数,可以实现对实参进行改变的目的,是因为传递过来的是实参的地址将指针作为参数进行传递时,事实上也是值传递,只不过传递的是地址。当把指针作

为参数进行传递时,也是将实参的一个拷贝传递给形参,(2)将引用作为函数的参数进行传递。

在讲引用作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对

形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。以在引用进行参数传递时,事实上传递的是实参本身而不是拷贝副本。所以在上述要达到同时修改指针的

目的的话,就得使用引用了。】

 

4,什么技术可以代替宏定义,

【《      常量定义

#define NUM 100

      《EffectiveC++》的第一个条款,讨论的就是这个宏。由于宏是预编译程序来处理,所以NUM这个名字不会加入到符号表中,如果出现编译错误时,提示信息中就不会出现NUM,而是100,为排除错误增加了额外的障碍。

 

      替代方案就是使用const来定义常量,或者使用枚举enum。

 

      const int NUM = 100;

 

      const常量放在头文件中,也不必担心存在多个实例的问题,对于const修饰的变量,编译器一般也会对其进行优化,不会出现多重定义的问题。

 

      C语言中还有一个特殊的常量定义:NULL。其一般的定义为 #define NULL 0,指针的内容却是一个整型,这不符合常理。所以在C++11中使用nullptr代替了NULL。

 

      2.函数定义

 

      由于宏只是在代码中做字符串替代展开,所以,用宏定义的函数,实际上并没有减少代码的体积。另外,还有一些天然的缺陷,假设一个求平方的函数宏

 

[cpp] view plain copy #definesquare(x) (x*x) voidf(double d, int i) { square(d); //OK square(i++); //糟糕, (i++*i++) square(d+1); //更糟,(d+1*d+1) }

      纵然可以把参数加上括号来解决,#define square(x) ((x)*(x)),但i++被执行两次这个问题还是无法解决。

 

      C++中的替代方案,就是使用inline函数。

 

      [cpp] view plain copy

      inline int square(intvalue)

      {

      return value*value;

      }

 

      inline函数具有函数的性质,参数传递不管是传值还是传引用,都不会对参数进行重复计算;同时会对参数做类型检查,保证代码的正确性;inline函数也是在代码中做代码展开,效率上并不比宏逊色。

 

      如果整型不满足需求,还可以定义为模板:

 

      [cpp] view plain copy

      template

      inline T square(T& value)

      {

      return value*value;

      }

 

      还有一种更离谱的函数定义形式:

 

[cpp] view plain copy #defineNull_Check(p)\ if(p == NULL) return;

      这种使用反斜杠定义的函数,更得注意,如果在反斜杠后多了个空格的话,有的编译器会出现变异错误,而提示信息嘛,你可能会困扰很久的。因为反斜杠会对空格这个字符做反义,就会在代码中的引入非法字符,人眼是很难发现这种错误的。

 

      3.类型重定义

 

#defineDWORD unsigned int

      这种类型重定义完全可以使用 typedef unsigned int DWORD 来替代。

 

      4.条件编译

 

[cpp] view plain copy #ifdefSystemA testA(); #else//SystemB testB(); #endif

      这种条件编译宏,一般在不同的产品或平台使用同一套代码的情况,大量出现。定义了SystemA的时候,SystemB的代码是不编译的,也就意味着你的代码没有时刻处于编译器的监控中。可以使用template技术来解决。

 

      [cpp] view plain copy

      constint SystemA = 1;

      constint SystemB = 2;

 

      template

      void test()

      {}

      //定义不同的系统的特化版本

      template<> void test(){ //SystemA的实现 }

      template<> void test(){ //SystemB的实现 }

 

      这样,不同的系统使用自己的模板即可,别人的代码也会同时接受编译器的检查,不至于出现遗漏编译错误的情况。

 

      5.头文件包含

 

[cpp] view plain copy #ifndeftest_h #definetest_h //test.h的实现 #endif

      为了防止头文件重复包含,C++中现在也只能这么做,目前没有别的替代方案。且看看原委,Bjarne Stroustrup在《C++语言的设计与演化》一书中,提供了一个include的设计,可惜的是并没有真正实现。(Cpp, 即C语言预处理器)

 

      我曾经建议可以给C++本身增加一个include指示字,作为Cpp的#include的替代品。C++的这种include可以在下面三个方面与Cpp的#include不同:

 

      1)如果一个文件被include两次,第二个include将被忽略。这解决了一个实际问题,而目前这个问题是通过#define和#ifdef,以非常低效而笨拙的方式处理的。

 

      2)在include的正文之外定义的宏将不在include的正文内部展开。这就提供了一种能够隔离信息的机制,可以使这些信息不受宏的干扰。

 

      3)在include的正文内容定义的宏在include正文之后的正文处理中不展开。这保证了include正文内部的宏不会包含它的编译单位强加上某种顺序依赖性,并一般地防止了由宏引起的奇怪情况。

 

      对于采用预编译头文件的系统而言一般地说,对于那些要用独立部分组合软件的人们而言,这种机制都将是一个福音。请注意,无论如何这还只是一个思想而不是一个语言特征。》】

5,描述下快速排序的思想,时间复杂度是多少,什么情况下复杂度最大

【《快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

 

      从数列中挑出一个元素,称为 “基准”(pivot);

      重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

      递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。》】《在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。

 

      在最坏的情况下,待排序的序列为正序或者逆序,每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。如果递归树画出来,它就是一棵斜树。此时需要执行n‐1次递归调用,且第i次划分需要经过n‐i次关键字的比较才能找到第i个记录,也就是枢轴的位置,最终其时间复杂度为O(n2)。》

6,写下二分查找,现场和平时写程序还是有很大区别的,无形中估计是很紧张,特简单的一个程序都写的特别乱

【《《#include

usingnamespacestd;

intbinary_search(intarr[],intn,intkey)

{

          intleft = 0;  //数组的首位置,即arr[0]处

          intright = n - 1;//数组的最后一个位置,即arr[n-1],数组大小为n

 

          //循环条件一定要注意

          while(left< = right)

          {

                   intmid = left + ((right - left) >> 1);//此处的mid的计算一定要放在while循环内部,否则mid无法正确更新;并且此处用移位代替除以2可以提高效率,而且可以防止溢出。

                   if(arr[mid] > key)//数组中间的位置得数大于要查找的数,那么我们就在中间数的左区间找

                   {

                             right = mid - 1;

                   }

                   elseif(arr[mid]< key)//数组中间的位置得数小于要查找的数,那么我们就在中间数的右区间找

                   {

                             left = mid + 1;

                   }

                   else

                   {

                             returnmid;//中间数刚好是要查找的数字。

                   }

          }

 

          //执行流如果走到此处说明没有找到要查找的数字。

          return-1;

}

测试用例如下所示:

voidTest()

{

          intarr[5] = { 1, 3, 5, 9, 10 };

          intret1 = 0,ret2 = 0,ret3 = 0;

          ret1 = binary_search(arr, 5, 9);

          cout<< ret1<< endl;

          ret2 = binary_search(arr, 5, 5);

          cout<< ret2<< endl;

          ret3 = binary_search(arr, 5, 2);

          cout<< ret3<< endl;

}》》】

 

7,动态链接库的接口函数是什么《

【《》】》

 

8,云上传是怎么实现秒传的

《【原理:

   当我们上传文件的时候,软件会有短暂的延迟(提示“正在准备上传文件”),这段延迟就是先校验你的文件的MD5(详解见下文),然后再在该云盘的服务器中通过MD5查找服务器中是否有相同的文件,如果有的话,那么就是将服务器中的文件直接复制一份到你的云盘中,而不是将你的文件传到云盘。这样的话就有了“秒传”的功能。当然,如果服务器中没有相同的文件,那么必须经过一点点的上传才可以,这时候上传的时间就得看你上传文件的大小和网速了。

   如果大家没有这个经历的话,尝试一下通过别人的分享,然后直接保存到自己的网盘,这样可以很快的体验到“秒传”的快感!

【《{}MD5算法的简要叙述:

MD5以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。

MD5算法特点:

1、压缩性:任意长度的数据,算出的MD5值长度都是固定的(把一个任意长度的字节串变换成一定长的十六进制数字串)。

2、容易计算:从原数据计算出MD5值很容易。

3、抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。

4、弱抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。

5、强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。》】】》

你可能感兴趣的:(C++面试题目解析01)