与C++特性相关的面试题

1. static在c,c++中有什么不同点
2. 堆和栈的区别
3. 纯虚函数
4. 指针和引用的区别
5. 如果构造函数出错,如何处理?
6. 对设计模式是否熟悉,用过哪些?
7. c++如何使用c中的函数,为什么?
整理:
1.
静态数据成员/成员函数,C++特有
2.
3.
在面向对象的C++语言中,虚函数(virtual function)是一个非常重要的概念。因为它充分体现了面向对象思想中的继承和多态性这两大特性,在C++语言里应用极广。有人甚至称虚函数是C++语言的精髓。
定义一个函数为虚函数,不代表函数为不被实现的函数 
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数 
定义一个函数为纯虚函数,才代表函数没有被实现 
定义他是为了实现一个接口,起到一个规范的作用,规范继承这个 
类的程序员必须实现这个函数。
有纯虚函数的类是不可能生成类对象的,如果没有纯虚函数则可以。比如: 
class CA 

public: 
virtual void fun() = 0; // 说明fun函数为纯虚函数 
virtual void fun1(); 
}; 

class CB 

public: 
virtual void fun(); 
virtual void fun1(); 
}; 

// CA,CB类的实现 
... 

void main() 

CA a; // 不允许,因为类CA中有纯虚函数 
CB b; // 可以,因为类CB中没有纯虚函数 
... 
}

虚函数在多态中使用: 
多态一般就是通过指向基类的指针来实现的。 
有一点必须明白,就是 用父类的指针在运行时刻来调用子类。父类指针通过虚函数来决定运行时刻到底是谁而指向谁的函数。
有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。 定义纯虚函数就是为了让基类不可实例化。
-------------------------------------------
纯虚函数
一、引入原因:
  1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
  2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
  为 了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
  二、纯虚函数实质:
  类中含有纯虚函数则它的VTABLE表不完全,有一个空位,所以,不能生成对象(编译器绝对不允许有调用一个不存在函数的可能)。在它的派生类中,除非重载这个函数,否则,此派生类的VTABLE表亦不完整,亦不能生成对象,即它也成为一个纯虚基类。
  三、 虚函数与构造、析构函数:
  1、构造函数本身不能是虚拟函数;并且虚机制在构造函数中不起作用(在构造函数中的虚拟函数只会调用它的本地版本)。
  想一想,在基类构造函数中使用虚机制,则可能会调用到子类,此时子类尚未生成,有何后果!?。
  2、析构函数本身常常要求是虚拟函数;但虚机制在析构函数中不起作用。
  若类中使用了虚拟函数,析构函数一定要是虚拟函数,比如使用虚拟机制调用delete,没有虚拟的析构函数,怎能保证delete的是你希望delete的对象。
  虚机制也不能在析构函数中生效,因为可能会引起调用已经被delete掉的类的虚拟函数的问题。
  四、对象切片:
  向上映射(子类被映射到父类)的时候,会发生子类的VTABLE 完全变成父类的VTABLE的情况。这就是对象切片。
  原因:向上映射的时候,接口会变窄,而编译器绝对不允许有调用一个不存在函数的可能,所以,子类中新派生的虚拟函数的入口在VTABLE中会被强行“切”掉,从而出现上述情况。
  五、虚拟函数使用的缺点
  虚函数最主要的缺点是执行效率较低,看一看虚拟函数引发的多态性的实现过程,就能体会到其中的原因。
----------------------------------------------
4.
指针与引用看上去完全不同(指针用操作符“*”和“->”,引用使用操作符“. ”),但是它们似乎有相同的功能。指针与引用都是让你间接引用其他对象。你如何决定在什么时候使用指针,在什么时候使用引用呢? 
首先,要认识到在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。 

“但是,请等一下”,你怀疑地问,“这样的代码会产生什么样的后果?” 

char *pc = 0; // 设置指针为空值 

char& rc = *pc; // 让引用指向空值 

这是非常有害的,毫无疑问。结果将是不确定的(编译器能产生一些输出,导致任何事情都有可能发生)。应该躲开写出这样代码的人,除非他们同意改正错误。如果你担心这样的代码会出现在你的软件里,那么你最好完全避免使用引用,要不然就去让更优秀的程序员去做。我们以后将忽略一个引用指向空值的可能性。 

因为引用肯定会指向一个对象,在C++里,引用应被初始化。 

string& rs; // 错误,引用必须被初始化 

string s("xyzzy"); 

string& rs = s; // 正确,rs指向s 

指针没有这样的限制。 

string *ps; // 未初始化的指针 

// 合法但危险 

不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。 

void printDouble(const double& rd) 



cout << rd; // 不需要测试rd,它 

} // 肯定指向一个double值 

相反,指针则应该总是被测试,防止其为空: 

void printDouble(const double *pd) 



if (pd) { // 检查是否为NULL 

cout << *pd; 





指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变。 

string s1("Nancy"); 

string s2("Clancy"); 

string& rs = s1; // rs 引用 s1 

string *ps = &s1; // ps 指向 s1 

rs = s2; // rs 仍旧引用s1, 

// 但是 s1的值现在是 

// "Clancy" 

ps = &s2; // ps 现在指向 s2; 

// s1 没有改变 

总的来说,在以下情况下你应该使用指针,一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空),二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。 

还有一种情况,就是 当你重载某个操作符时,你应该使用引用。最普通的例子是操作符[]。这个操作符典型的用法是返回一个目标对象,其能被赋值。 

vector<int> v(10); // 建立整形向量(vector),大小为10; 

// 向量是一个在标准C库中的一个模板(见条款M35) 

v[5] = 10; // 这个被赋值的目标对象就是操作符[]返回的值 

如果操作符[]返回一个指针,那么后一个语句就得这样写: 

*v[5] = 10; 

但是这样会使得v看上去象是一个向量指针。因此你会选择让操作符返回一个引用。(这有一个有趣的例外,参见条款M30) 

当你知道你必须指向一个对象并且不想改变其指向时,或者在重载操作符并为防止不必要的语义误解时,你不应该使用指针。而在除此之外的其他情况下,则应使用指针。
5.
使用异常捕捉,具体问题具体分析。
6.
设计模式基本思想
好的系统设计追求如下特性:
可扩展性( Extensibility ):新的功能或特性很容易加入到系统中来;
灵活性( Flexibility ):可以允许代码修改平稳发生,对一处的修改不会波及到很多其他模块;
可插入性( Pluggability ):可以很容易地将一个类或组件抽出去,同时将另一个有相同接口的类 / 接口加入进来。
具有如上特性的系统才有真正的可维护性和可复用性。而可维护性和可复用性对一个持续接入新需求,现有功能逐步完善,新功能不断丰富,版本不会终止的大型软件产品来说至关重要。
传统的复用包括:代码的 copy 复用,算法的复用,数据结构的复用。
在面向对象领域,数据的抽象化、封装、继承和多态性是几项最重要的语言特性,这些特性使得一个系统可以在更高的层次上提供可复用性。数据的抽象化和继承关系使得概念和定义可以复用;多态性使得实现和应用可以复用;而抽象化和封装可以保持和促进系统的可维护性。这样,复用的焦点不再集中在函数和算法等具体实现细节上,而是集中在最重要的宏观的业务逻辑的抽象层次上。复用焦点的倒转不是因为实现细节的复用不再重要,而是因为这些细节上的复用往往已经做的很好(例如,很容易找到并应用成熟的数据结构类库等),而真正冲击系统的是其要实现业务的千变万化。
本质上说,如果说一个软件的需求是永不变更或发展的,该软件也就不需要任何设计,怎么编码实现都行,只要需求满足,性能达标。但事实上,软件的本性就是不断增强,不断拓展的,不断变化的。我们可以控制指尖流淌出的每行代码,但控制不了奉为上帝的用户的需求。编码结束,测试全部通过,用户在试用过程中才发现原来的需求有问题,需要变更或提出新需求,怎么办?向用户抗议:需求总在变,没法做!?平抑心中的抱怨,加班加点大量的修改代码,疯狂的测试,依然是时间紧迫,心中没底?抑或了然于胸:这个变更或小需求合理,系统很方便纳入;于是坦然地和用户协商下一个交付时间点?
要使系统最大程度的适应需求的变更或新增,就必须在其要实现的宏观业务逻辑的抽象复用上下功夫。而设计模式就是综合运用面向对象技术和特性来提高业务逻辑可复用性的常用方法和经验的提取和汇总。
掌握 23 种设计模式的关键是理解它们的共通目的:使所设计的软件系统在一般或特定(系统将来在特定点上扩展的可能性大)场景下,尽可能的对扩展开放,对修改关闭。即面对新需求或需求变更时,容易开发独立于既有代码的新代码接入到现有系统或对现有代码做可控的少量修改,而不是在现有代码基础上做大量的增、删、改。为了这一目的,  23 种设计模式贯穿了面向对象编程的基本原则:
面向接口或抽象编程,而不是面向实现编程。
一个对象应当对其他对象有尽可能少的了解,即松耦合。
纯粹的功能复用时,尽量不要使用继承,而是使用组合。使已有的对象成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。
OO设计模式和设计原则
1.1 设计正在“腐烂”的征兆(Symptoms of Rotting Design)
    有四个主要的征兆告诉我们该软件设计正在“腐烂”中。它们并不是互相独立的,而是互相关联,它们是过于僵硬、过于脆弱、不可重用性和粘滞性过高。
    1. 过于僵硬 Rigidity 致使软件难以更改,每一个改动都会造成一连串的互相依靠的模块的改动,项目经理不敢改动,因为他永远也不知道一个改动何时才能完成。
    2. 过于脆弱 Fragility 致使当软件改动时,系统会在许多地方出错。并且错误经常会发生在概念上与改动的地方没有联系的模块中。这样的软件无法维护,每一次维护都使软件变得更加难以维护。(恶性循环)
    3. 不可重用性immobility 致使我们不能重用在其它项目中、或本项目中其它位置中的软件。工程师发现将他想重用的部分分离出来的工作量和风险太大,足以抵消他重用的积极性,因此软件用重写代替了重用。
    4. 粘滞性过高 viscosity有两种形式:设计的viscosity和环境的viscosity.当需要进行改动时,工程师通常发现有不止一个方法可以达到目的。但是这些方法中,一些会保留原有的设计不变,而另外一些则不会(也就是说,这些人是hacks)。一个设计如果使工程师作错比作对容易得多,那么这个设计的viscosity 就会很高。
    环境的viscosity高是指开发环境速度很慢且效率很低。
     2 面向对象的类设计原则
     2.1 开放关闭原则The Open Closed Principle (OCP)
    A module should be open for extension but closed for modification.一个模块应该只在扩展的时候被打开(暴露模块内部),在修改的时候是关闭的(模块是黑盒子)。
    在所有的面向对象设计原则中,这一条最重要。该原则是说:我们应该能够不用修改模块的源代码,就能更改模块的行为。
    2.1.1 动态多态性(Dynamic Polymorphism)
    2.1.2 静态多态性(Static Polymorphism)
    另外一种使用OCP的技术就是使用模板或范型,如Listing 2-3.LogOn函数不用修改代码就可以扩展出多种类型的modem. 2.1.3 OCP的体系结构目标(Architectural Goals of the OCP)
    通过遵照OCP应用这些技术,我们能创建不用更改内部代码就可以被扩展的模块。这就是说,在将来我们给模块增添新功能是,只要增加新的代码,而不用更改原先的代码。 第 3 页,共 17 页使软件完全符合OCP可能是很难的,但即使只是部分符合OCP,整个软件的结构性能也会有很大的提高。我们应该记住,让变化不要波及已经正常工作的代码总是好的。
    2.2 Liskov 替换原则The Liskov Substitution Principle(LSP)
    Subclasses should be substitutable for their base classes.子类应该可以替换其基类。
    如下图2-14所示。Derived类应该能替换其Base类。也就是说,Base基类的一个用户User如果被传递给一个Devrived类而不是Base类作为参数,也能正常的工作。
    2.3 依赖性倒置原则The Dependency Inversion Principle (DIP)1
     Depend upon Abstractions. Do not depend upon concretions.依赖抽象,不要依赖具体。
    如果说OCP声明了OO体系结构的目的,DIP则阐述了其主要机制。依赖性倒置的策略就是要依赖接口、或抽象函数、或抽象类,而不是依赖于具体的函数和类。这条原则就是支持组件设计、COM、CORBA、EJB等等的背后力量。
    2.3.1 依赖抽象Depending upon Abstractions.
    实现该原则十分简单。设计中的每一个依赖都应该是接口、抽象类,不要依赖任何一个具体类。
    显然这样的限制比较严峻,但是我们应该尽可能的遵守这条原则。原因很简单,具体的模块变化太多,抽象的则变化少得多。而且,抽象是“铰链”点,在这些位置,设计可以弯曲或者扩展,而不用进行更改(OCP)。
    2.4 接口隔离原则The Interface Segregation Principle (ISP)
    ‘Many client specific interfaces are better than one general purpose interface多个和客户相关的接口要好于一个通用接口。
    ISP是另一条在底层支持组件如COM技术的原则。没有它,组件和类的易用性和重用性都会大打折扣。该原则的实质很简单:如果一个类有几个使用者,与其让这个类载入所有使用者需要使用的所有方法,还不如为每一个使用者创建一个特定的接口,并让该类分别实现这些接口。
    3 包体系结构的原则Principles of Package Architecture
    类是必不可少的,但对于组织一个设计来说还不够,粒度更大的包有助于此。但是我们应该怎样协调类和包之间的从属关系?下面的三条原则都属于包聚合原则,能对我们有所帮助。
    3.1 包聚合原则
    3.1.1 发布重用等价原则The Release Reuse Equivalency Principle (REP)1
    重用的粒度就是发布的粒度。The granule of reuse is the granule of release.一个可重用的元件(组件、一个类、一组类等),只有在它们被某种发布(Release)系统管理以后,才能被重用。用户不愿意使用那些每次改动以后都要被强迫升级的元件。因此,即使开发者发布了可重用元件的新版本,他也必须支持和维护旧版本,这样才有时间让用户熟悉新版本。
    因此,将什么类放在一个包中的判断标准之一就是重用,并且因为包是发布的最小单元,它们同样也是重用的最小单元。体系结构师应该将可重用的类都放在包中。
    3.1.2 共同封闭原则The Common Closure Principle (CCP)2
   一起变化的类放在一起。Classes that change together, belong together.一个大的开发项目通常分割成很多网状互联的包。管理、测试和发布这些包的工作可不是微不足道的工作。在任何一个发布的版本中,如果改动的包数量越多,重建、测试和部署也就会越多。因此我们应该尽量减少在产品的发布周期中被改动的包的数量,这就要求我们将一起变化的类放在一起(同一个包)。
    3.1.3 共同重用原则The Common Reuse Principle (CRP)3
    不一起重用的类不应该放在一起。Classes that aren‘t reused together should not be grouped together.对一个包的依赖就是对包里面所有东西的依赖。当一个包改变时,这个包的所有使用者都必须验证是否还能正常运行,即使它们所用到的没有任何改变也不行。
    比如我们就经常遇到操作系统需要升级。当开发商发布一个新版本以后,我们的升级是迟早的问题,因为开发商将会不支持旧版本,即使我们对新版本没有任何兴趣,我们也得升级。
    如果把不一起使用的类放在一起,同样的事情我们也会遇到。一个和我们无关的类的改变也产生包的一个新版本,我们被强迫升级和验证这个包是否影响正常的运行。
    3.1.4 包聚合原则之间的张力Tension between the Package Cohesion Principles
    这三条原则实际上是互斥的。它们不能被同时满足,因为每一条原则都只针对某一方面,只对某一部分人有好处。REP和CRP都想重用元件的人有好处,CCP对维护人员有好处。CCP使得包有尽可能大的趋势(毕竟,如果所有的类都属于一个包,那么将只会有一个包变化);CRP尽量使得包更小。
    幸运的是,包并不是一成不变的。实际上,在开发过程中,包的转义和增删都是很正常的。在项目开发的早期,软件建筑师建立包的结构体系,此时CCP占主导地位,维护只是辅助。在体系结构稳定以后,软件建筑师会对包结构进行重构,此时尽可能的运用REP和CRP,从而最大的方便重用元件的人员。
    3.2 包耦合原则The Package Coupling Principles.
   下面三条原则主要关心包之间的关系。
    3.2.1 无依赖回路原则The Acyclic Dependencies Principle (ADP)1
    包与包之间的依赖不能形成回路。The dependencies between packages must not form cycles.因为包是发布的粒度。人们倾向于节省人力资源,所以工程师们通常只编写一个包而不是十几个包。这种倾向由于包聚合原则被放大,后来人们就将相关的类组成一组。
    因此,工程师发现他们只会改动较少的几个包,一旦这些改动完成,他们就可以发布他们改动的包。但是在发布前,他们必须进行测试。为了测试,他们必须编译和连编他们的包所依赖的所有的包。
    3.2.2 依赖稳定原则(Stable Dependencies Principle,SDP)
    朝稳定的方向依赖Depend in the direction of stability.虽然这条原则看起来很明显,但是关于这方面还是有很多需要说明的地方,稳定性并不一定为大家所了解。
    稳定性是什么?站在一个硬币上,这稳定吗?很可能你说不。然而,除非被打扰,硬币将保持那个位置很长时间。硬币没有变化,但是很难认为它是稳定的。稳定性与需要改动所要做的工作量相关。硬币不稳定是因为只需要很小的工作量就能把它翻过来。换个角度,桌子就要稳定得多。
    对于软件这说明什么?一个软件包很难被改动受很多因素影响:代码大小、复杂度、透明度等等。这些我们先不说,可以肯定的一点是,如果有很多其它的包依赖于一个软件包,那么该软件包就难以改动。一个包如果被许多其它包依赖,那么该包是很稳定的,因为这个包的任何一个改动都可能需要改动很多其它的包。
    3.2.3 稳定抽象原则( Stable Abstractions Principle ,SAP)
    稳定的包应该是抽象包。Stable packages should be abstract packages.我们可以想象应用程序的包结构应该是一个互相联系的包的集合,其中不稳定的包在顶端,稳定的包在底部,所有的依赖方向朝下。那些顶端的包是不稳定而且灵活的,但那些底部的包就很难改动。这就导致一个两难局面:我们想要将包设计为难以改动的吗?
    明显地,难以改动的包越多,我们整个软件设计的灵活性就越差。但是好像有一点希望解决这个问题,位于依赖网络最底部的高稳定性的包的确难以改动,但是如果遵从OCP,这样的包并不难以扩展。
7.
假设某个C函数的声明如下:
void foo(int x, int y);
该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C++程序不能直接调用C函数。C++提供了一个C连接交换指定符号extern“C”来解决这个问题。例如:
extern “C”
{
   void foo(int x, int y);
   … // 其它函数
}
或者写成
extern “C”
{
   #include “myheader.h”
   … // 其它C头文件
}
这就告诉C++编译译器,函数foo是个C连接,应该到库中找名字_foo而不是找_foo_int_int。C++编译器开发商已经对C标准库的头文件作了extern“C”处理,所以我们可以用#include 直接引用这些头文件。
数据结构:
8. AVL 平衡二叉树
操作系统:
9. 进程和线程的区别
10. 进程间通信的方法,两个进程,socket通信,一个进程将一个指针发送过去,另一个进程是否可用linux
11. /proc下的文件是干什么用的?
12. 可执行程序的结构是什么样的?bss中有些什么?
13. Linux下定时程序用过没,怎么使用?
14. Linux下如何调试程序?程序core dump后怎么办?
回答:
9.
进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。进程和线程的区别在于: 

简而言之,一个程序至少有一个进程,一个进程至少有一个线程. 
线程的划分尺度小于进程,使得多线程程序的并发性高。 
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。 
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。 

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位. 
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源. 
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
10.
信号:信号处理器   处理信号集中的信号
信号量:P、V操作   同步互斥
消息队列:进程间传递消息,可被多个进程共享(IPC实现基础)
共享内存
11.
/proc动态文件系统是一种特殊的由程序创建的文件系统,内核利用它向外界输出信息。/proc下每个文件都被绑定一个内核函数,这个函数在此文件被读取时,动态生成文件的内容。
12.
可执行程序结构一般分为三个段:
.text:存放程序二进制代码
.data:存放全局的已初始化变量
.bss:存放全局的未初始化变量(Block Started by Symbol)
13.
linux系统下让程序定时自动执行:crontab
格式 :  minute hour day month year command
# Mail the system logs at 4:30pm every June 15th. 
30 16 15 06 * for x in /var/log/*; do cat ${x} | mail postmaster; done
# Inform the administrator, at midnight, of the changing seasons. 
00 00 20 04 * echo 'Woohoo, spring is here!' 
00 00 20 06 * echo 'Yeah, summer has arrived, time to hit the beach!' 
00 00 20 10 * echo 'Fall has arrived.  Get those jackets out.  :-(' 
00 00 20 12 * echo 'Time for 5 months of misery.  ;-('
注意该指令会输出到一个标准出口 (亦即. 一个终端机 ),像是上面使用 "echo" 的例子会将输出寄 
给 "root" 帐号。如果您想要避免它,只要像下面将输出导引到一个空的设备 :  
  00 06 * * * echo 'I bug the system administrator daily at 6:00am!' >/dev/null 
crontab -e 重新编辑定时执行程序
    14.
gdb调试

你可能感兴趣的:(设计模式,数据结构,C++,String,dependencies,编译器)