面试总结6--C++基础问题Part2

1、C和C++中struct有什么区别,c++中struct和class区别?

C的struct与C++的class的区别:struct只是作为一种复杂数据类型定义,不能用于面向对象编程。在纯粹的C语言中,struct不能定义成员函数,只能定义变量

C++中的struct扩充了C的struct功能struct还有构造函数和成员函数,其实它还拥有class的其他特性,例如继承、虚函数等。

C++中的struct和class的区别:对于成员访问权限以及继承方式,class中默认的是private的,而struct中则是public的。class还可以用于表示模板类型,struct则不行。

 

2、指针和引用的区别?

1、本质:指针是一个变量,存储内容是一个地址,指向内存的一个存储单元。而引用是原变量的一个别名,实质上和原变量是一个东西,是某块内存的别名。2、指针的值可以为空,且非const指针可以被重新赋值以指向另一个不同的对象。而引用的值不能为空,并且引用在定义的时候必须初始化,一旦初始化,就和原变量“绑定”,不能更改这个绑定关系。

3、对指针执行sizeof()操作得到的是指针本身的大小(32位系统为4,64位系统为8)。而对引用执行sizeof()操作得到的是所绑定的对象的所占内存大小。4、指针的自增(++)运算表示对地址的自增,自增大小要看所指向单元的类型。而引用的自增(++)运算表示对值的自增。

5、在作为函数参数进行传递时的区别:指针所以函数传输作为传递时,函数内部的指针形参是指针实参的一个副本,改变指针形参并不能改变指针实参的值,通过解引用*运算符来更改指针所指向的内存单元里的数据。而引用在作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。

 

3、处理New分配内存失败情况?

我们经常会使用new给一个对象分配内存空间,而当内存不够会出现内存不足的情况。C++提供了两种报告方式:

1、抛出bad_alloc异常来报告分配失败;

2、返回空指针,而不会抛出异常

operator new 无法满足内存需求时,它会不只一次地调用new_handler函数(如果new_handler没有退出程序的话);它会不断地调用,直到找到足够的内存为止。可以用set_new_hander 函数为 new 设置用户自己定义的异常处理函数,也可以让 malloc 享用与 new 相同的异常处理函数。


4、C++是不是类型安全的?

不是。两个不同类型的指针之间可以强制转换(用reinterpret_cast)


5、当一个类中没有声命任何成员变量与成员函数这时sizeof(A)的值是多少,如果不是零,请解释一下编译器为什么没有让它为零。

通常是1,用作占位的。为了确保不同对象有不同的地址


6为什么内联函数,构造函数,静态成员函数不能为virtual函数?

1> 内联函数

内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾,不能定义内联函数为虚函数。

2> 构造函数

构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚未形成,所以不能将构造函数定义为虚函数

3> 静态成员函数

静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别。

这个可以从两个角度去理解:

1。virtual意味着在执行时期进行绑定,所以在编译时刻需确定信息的不能为virtual

构造函数需在编译时刻,因为需构造出个对象,才能执行动作。
          静态成员函数不属于任何一个对象,编译时刻确定不存在执行的时候选择执行哪个的情形。
          内联函数,由于属于编译器的建议机制,所以其实可以virtual。

2。virtual意味着派生类可以改写其动作
        派生类的构造函数会先执行基类的构造函数而不是取代基类构造函数,也就是说基类的构造函数可以看作派生类构造函数的组成,所以并不能改写这个函数。
        静态成员函数不属于任何一个对象,所以更不能改写其动作了。

       inline和virtual不会同时起作用。带virtual的函数在不需要动态绑定调用的时候,就可以inline。


7、C++函数中那些不可以被声明为虚函数的函数?为什么C++不支持友元函数为虚函数?

常见的不不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。

因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。


8、构造函数和析构函数为什么没有返回值?

构造函数和析构函数是两个非常特殊的函数:它们没有返回值。这与返回值为void的函数显然不同,后者虽然也不返回任何值,但还可以让它做点别的事情,而构造函数和析构函数则不允许。在程序中创建和消除一个对象的行为非常特殊,就像出生和死亡,而且总是由编译器来调用这些函数以确保它们被执行。如果它们有返回值,要么编译器必须知道如何处理返回值,要么就只能由客户程序员自己来显式的调用构造函数与析构函数,这样一来,安全性就被人破坏了。另外,析构函数不带任何参数,因为析构不需任何选项。

 

9、函数模板与类模板的区别?

函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。即函数模板允许隐式调用和显式调用而类模板只能显示调用。

 

10、基类的析构函数不是虚函数,会带来什么问题?

如果基类的为虚的话,那么在释放派生类对象的时候,会首先调用派生类的析构函数,然后再调用基类的析构函数

如果基类析构函数不为虚的话,在释放派生类对象的时候就不会调用派生类的析构函数,有可能造成内存泄露

 

11、构造函数可以调用虚函数吗?语法上通过吗?语义上可以通过吗?

 调用当然是没有问题的但多态这个功能被屏蔽了。

在构造一个子类对象的时候,首先会构造它的基类,如果有多层继承关系,实际上会从最顶层的基类逐层往下构造(虚继承、多重继承这里不讨论),如果是按照上面的情形进行输出的话,那就是说在构造Base的时候,也就是在Base的构造函数中调用Fuction的时候,调用了子类A的Fuction,而实际上A还没有开始构造,这样函数的行为就是完全不可预测的,因此显然不是这样。

在调用Base的构造函数时已经出现了虚函数表指针,这个指针指向Base的虚函数表,所以在Base的构造函数中调用的虚函数其实都是Base的虚函数;而在构造A时,虚函数表指针被指向了A的虚函数表,所以此时调用的虚函数其实是A的虚函数表中的项。


12、析构函数可以抛出异常吗?为什么不能抛出异常?除了资源泄露,还有其他需考虑的因素吗?

 

1C++中析构函数的执行不应该抛出异常;

2)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。析构函数中抛出异常导致程序不明原因的崩溃是许多系统的致命内伤

解决办法:那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外。如使用

Try

 {  }

Catch

{/这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外}

 

13、c++中类型转换机制?各适用什么环境?dynamic_cast转换失败时,会出现什么情况?

1.static_cast

最常用的类型转换符,在正常状况下的类型转换,如把int转换为float,如:int ifloatf f=floati;或者f=static_cast<float>(i);

2.const_cast

用于取出const属性,把const类型的指针变为非const类型的指针,如:const int *fun(int x,int y){}int *ptr=const_cast<int*>(fun(2.3))

3.dynamic_cast(在继承体系中使用)

该操作符用于运行时检查该转换是否类型安全,但只在多态类型时合法,即该类至少具有一个虚拟方法。dynamic_caststatic_cast具有相同的基本语法,dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_caststatic_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

4.reinterpret_cast(最常用途是转换函数指针类型)

interpret是解释的意思,reinterpret即为重新解释,此标识符的意思即为数据的二进制形式重新解释,但是不改变其值。如:int i; char *ptr="hello freind!";i=reinterpret_cast<int>(ptr);这个转换方式很少使用。

dynamic_cast转换失败时对指针,返回NULL.对引用,抛出bad_cast异常


14、虚函数与纯虚函数区别?

定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。

1.  虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称为抽象类(abstract class)。

2.  虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class)只有声明而没有定义。

3.  虚函数和纯虚函数都可以在子类(sub class)中被重载,以多态的形式被调用

 

15、C++ static、const和static const 以及它们的初始化?

static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化,

const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static cosnt。

但是static const 的整型(bool,char,int,long)可以在类声明中初始化 

 

16、私有继承作用?is-a,has-a区别?

私有继承是一种实现继承,有“with a”的语义。子类利用父类中某些代码实现子类的某些功能。

   is-a继承关系,has-a组合关系。

    Public继承意味is-a,适用于base classes身上的每一件事情一定也适用于derive classes身上,因为每一个derive class对象也都是一个base class对象。

    has-a我们想实现一个Set类,而已经有一个List类可提供给你使用,我们到底用is-a(public继承)关系还是用has-a(组合)关系呢?

    答案是用has-a(组合)关系,因为list可以含重复元素而,set不可以。


17、在构造函数和析构函数中抛出异常会发生什么?什么是栈展开?

 

构造函数中可以抛出异常,构造抛异常之前必须把已经申请的资源释放掉这样,就算你的对象是new出来的,也不会造成内存泄漏。
     因为析构函数不会被调用,所以抛出异常后,你没机会释放资源。在构造函数中抛出异常导致析构这个对象。

构造函数中抛出异常时概括性总结
1 C++中通知对象构造失败的唯一方法那就是在构造函数中抛出异常;

2构造函数中抛出异常将导致对象的析构函数不被执行;

3当对象发生部分构造时,已经构造完毕的子对象将会逆序地被析构;

栈展开:抛出异常时,将暂停当前函数的执行,开始查找匹配的catch子句。首先检查throw本身是否在try块内部,如果是,检查与该try相关的catch子句,看是否可以处理该异常。如果不能处理,就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到上层的调用函数中查找,直到找到一个可以处理该异常的catch。这个过程称为栈展开(stack unwinding

18、++iterator 和iterator++效率问题?

两种方式iterator遍历的次数是相同的,但在STL中效率不同,前++返回引用,后++返回一个临时对象,因为iterator是类模板,使用 it++这种形式要返回一个无用的临时对象,而it++函数重载,所以编译器无法对其进行优化,所以每遍历一个元素,你就创建并销毁了一个无用的临时对象。
      
除了特殊需要和对内置类型外使用++it来进行元素遍历的。

 

19、string的内存怎么实现的,长度的动态变化怎么处理,为什么两倍扩展。stringa; string b = a;这时会不会有内存拷贝相关的东西?

 String对象可能是1~7char*大小,当插入string的长度大于其Capacity会重新分配一个2倍长度的内存,将现在的内容拷贝过去然后将新的内容加入到尾部,并将老的空间释放掉。

不会有拷贝相关的东西,ba指向同一地址的字符串,引用计数加1.


20map是怎么实现的?

 

21、动态链接和静态链接比较?

静态库:代码的装载速度快,执行速度也比较快,因为编译时它只会把你需要的那部分链接进去,应用程序相对比较大。但是如果多个应用程序使用的话,会被装载多次,浪费内存。动态链接可以动态的安装和卸载,更加灵活。


22孤儿进程:父进程先退出,子进程被init进程收养;僵尸进程:子进程退出,父进程并没有获取子进程的状态信息,子进程的状态描述符仍然存在系统中;守护进程:Linux的后台服务进程,比如crond等,不受终端控制,在系统启动时启动,在系统关闭时终止。


23脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。[1]悲观锁假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。悲观的缺陷是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并发访问性不好。

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。 乐观锁则认为其他用户企图改变你正在更改的对象的概率是很小的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要比悲观锁短,乐观锁可以用较大的锁粒度获得较好的并发访问性能。但是如果第二个用户恰好在第一个用户提交更改之前读取了该对象,那么当他完成了自己的更改进行提交时,数据库就会发现该对象已经变化了,这样,第二个用户不得不重新读取该对象并作出更改。这说明在乐观锁环境中,会增加并发用户读取对象的次数。


24、Windows内存管理的方法

内存管理的主要方式:块式管理页式管理,段式管理,段页式管理。

25、宏定义和内联函数有什么区别吗,分别在什么样的场景下应用啊

内联函数在编译时,将调用处进行函数替换,避免调来调去压榨出栈的时间开销,以空间换时间,还有内联函数有类型检测,宏替换没有类型检测,内联函数只是向编译器申请,若内联函数体内有循环递归等,申请会失败,系统会当初普通函数处理。

 

26、如何减少频繁分配内存(malloc或者new)造成的内存碎片?

内存池(Memory Pool)是一种内存分配方式。 通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升。

 

27、Windows程序的入口是哪里?写出Windows消息机制的流程?

入口在main()/WinMain()

<1>操作系统接收应用程序的窗口消息,将消息投递到该应用程序的消息队列中

<2>应用程序在消息循环中调用GetMessage函数从消息队列中取出一条一条的消息,取出消息后,应用程序可以对消息进行一些预处理

<3>应用程序调用DispatchMessage,将消息回传给操作系统

<4>系统利用WNDCLASS结构体的ipfoWndProc成员保存的窗口过程函数的指针调用窗口过程,对消息进行处理


28、抛出exception、传递对象到函数、和调用虚函数之间的差异?

主要有3个差异:

1、 exception对象总是会被复制,如果以by value方式捕捉,它会被复制两次。传递给函数的对象则不一定得复制。

2、 被抛出的exception只允许继承架构中的类型转换和有型指针与void* 转化,所以一个对const void* 指针而设计的catch子句,可以捕捉任何指针类型的exception。

3、 exception处理机制采用最先吻合策略,第一个匹配成功者便执行,而虚函数则采用最佳吻合策略。

4、 函数调用控制权最终会回到调用端,而抛出一个exception控制权不会回再回到抛出端。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章