C/C++经典面试题目

1、关于动态申请内存

答:内存分配方式三种:

(1)从静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。全局变量,static变量。

(2)在栈上创建:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。

  栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3)用malloc或new申请内存之后,应该立即检查指针值是否为NULL.防止使用指针值为NULL的内存,不要忘记为数组和动态内存赋初值。

   动态内存的申请与释放必须配对,防止内存泄漏。用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。从堆上分配,亦称动态内存分配。

程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活。

如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,判断指针是否为NULL,如果是则马上用return语句终止本函数,

 

2、C++指针

答案:指针是一个变量,专门存放内存地址,特点是能访问所指向的内存指针本身占据了4个字节的长度。

函数指针是指向一个函数入口的指针,一个函数指针只能指向一种类型的函数,即具有相同的返回值和相同的参数的函数。

野指针是很危险的,“野指针”的成因主要有两种:

(1)指针变量没有被初始化。指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

(2)指针p被free或者delete之后,没有置为NULL

 

3、引用和指针有什么区别?

答:引用必须初始化,指针则不必;引用初始化以后不能改变,指针可以改变其指向的对象;

不存在指向空值的引用,但存在指向空值的指针;

引用是某个对象的别名,主要用来描述函数和参数和返回值。而指针与一般的变量是一样的,会在内存中开辟一块内存。

如果函数的参数或返回值是类的对象的话,采用引用可以提高程序的效率。

 

4、C++中的Const用法

答:

const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。

const修饰类的成员变量,表示成员常量,不能被修改,同时它只能在初始化列表中赋值。static const 的成员需在声明的地方直接初始。

const修饰类的成员函数,则该成员函数不能修改类中任何非const成员。一般写在函数的最后来修饰。

 

5、const常量与define宏定义的区别

答:(1) 编译器处理方式不同。define宏是在预处理阶段展开,生命周期止于编译期。const常量是编译运行阶段使用,const常量存在于程序的数据段.

    (2)类型和安全检查不同。define宏没有类型,不做任何类型检查,仅仅是展开。const常量有具体的类型,在编译阶段会执行类型检查。

    (3)存储方式不同。define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。const常量会在内存中分配(可以是堆中也可以是栈中)。

 

6、解释堆和栈的区别

答:1、栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域,栈的大小是2M。如果申请的空间超过栈的剩余空间时,将提示overflow。

栈由系统自动分配,速度较快。但程序员是无法控制的。函数调用时,第一个进栈的是主函数中后的下一条指令的地址,然后是函数的各个参数。

在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。

 

堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,

需要程序员自己申请,并指明大小,堆是向高地址扩展的数据结构,是不连续的内存区域。而链表的遍历方向是由低地址向高地址。

堆的大小受限于计算机系统中有效的虚拟内存。堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

 

7、C++的空类,默认产生哪些类成员函数?

答:class Empty

{

 public:

Empty();                                   //缺省构造函数

Empty(const Empty& );           //拷贝构造函数

~Empty();                                //析构函数

Empty& operator(const Empty& )//赋值运算符

Empty& operator&();                 //取址运算符

const Empty* operator&() const; // 取址运算符 const

}

 

8、C++函数中值的传递方式有哪几种?

答:函数的三种传递方式为:值传递、指针传递和引用传递。

 

9、将“引用”作为函数参数有哪些特点

答:(1)传递引用给函数与传递指针的效果是一样的,这时,被调函数的形参就成为原来主调函数的实参变量或者

对象的一个别名来使用,所以在被调函数中形参的操作就是对相应的目标对象的操作

     (2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作,当参数数据较大时,引用

传递参数的效率和所占空间都好

 

10、简单叙述面向对象的三个基本特征

答:封装性:把客观事物封装成抽象的类,对自身的数据和方法进行(public,private, protected)

     继承性: 继承概念的实现方式有三类:实现继承、接口继承和可视继承。

                实现继承是指使用基类的属性和方法而无需额外编码的能力;

                接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;

                可视继承是指子窗体(类)使用基窗体(类)的外观和实现代码的能力。

                抽象类仅定义将由子类创建的一般属性和方法,创建抽象类时,请使用关键字 Interface 而不是 Class

     多态性: 多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,

父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。允许将子类类型的指针赋值给父类类型的指针。

实现多态,有二种方式,覆盖(子类重新定义父类的虚函数),重载(允许存在多个同名函数,参数个数,类型不同)

 

11、类成员函数的overload, override 和 隐藏的区别

答:
(1)成员函数被重载的特征:相同的类范围,函数名字相同,参数不同,virtual 关键字可有可无。

(2)覆盖指派生类的函数覆盖基类函数,特征是分别位于基类和派生类,函数名字相同,参数相同,基类函数必须有virtual关键字

(3)隐藏是指派生类的函数屏蔽了与其同名的基类函数。1:派生类的函数与基类的函数同名,但是参数不同,不论有无virtual关键字,基类的函数将被隐藏

                                                   2:派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏

 

12、memset ,memcpy 和strcpy 的根本区别?

答:memset用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化为' '或'';它对较大的结构体或数组进行清零操作的一种最快方法。

char temp[30];     memset(temp,'\0',sizeof(temp));

char temp[30]只是分配了一定的内存空间给该字符数组,但并未初始化该内存空间,即数组。所以,需要使用memset()来进行初始化。

memcpy用来做内存拷贝,你可以拿它拷贝任何数据类型的对象,可以指定拷贝的数据长度;

strcpy就只能拷贝字符串了,它遇到'\0'就结束拷贝

 

13、请说出static关键字尽可能多的作用

答:(1)函数体内作用范围为该函数体,该变量内存只被分配一次,具有记忆能力

     (2)在模块内的static全局变量可以被模块内所有函数访问,但不能被模块外其它函数访问;

     (3)在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;

     (4)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;

     (5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

 

14、在C++程序中调用C编译后的函数,为什么要加extern C的声明?

答:因为C++支持函数重载,而C不支持函数重载,函数被C++编译后在库中的名字与C语言的不同。假设某个函数的原型为:void foo(int x, int y);

该函数被C编译器编译后在库中的名字为_foo,而C++编译器则产生像_foo_int_int之类的名字。 C++提供extern C来解决名字匹配问题

 

15、C++中哪些函数不能被声明为虚函数?

答:普通函数(非成员函数),构造函数,内联成员函数、静态成员函数、友元函数

(1)虚函数用于基类和派生类,普通函数所以不能

(2)构造函数不能是因为虚函数采用的是虚调用的方法,允许在只知道部分信息的情况的工作机制,特别允许调用只知道接口而不知道对象的准确类型的方法,

但是调用构造函数即使要创建一个对象,那势必要知道对象的准确类型。

(3)内联成员函数的实质是在调用的地方直接将代码扩展开

(4)继承时,静态成员函数是不能被继承的,它只属于一个类,因为也不存在动态联编等

(5)友元函数不是类的成员函数,因此也不能被继承

 

如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。 

 

16、在main函数执行之前,还会执行什么代码和工作

答:运行全局构造器,全局对象的构造函数会在main函数之前执行

      初始化static静态和global全局变量,即数据段的内容

      将main函数的参数传递给main函数

 

17、已知String类定义如下,尝试写出类的成员函数实现

class{

       public:

                    String(const char*str = NULL);             //通用构造函数

                    String(const String& another);             //拷贝构造函数

                    ~String();                                 //析构函数

                    String& operator = = (const String& rhs);  //赋值函数

       private:

                    char* m_data;                              //用于保存字符串

};

 

答:

String::String(const char*str)

{

     if(str == NULL)

     {

            m_data = new char[1];

            m_data[0] = '\0';

     }

     else

     {

            m_data = new char[strlen(str)+1];

            strcpy(m_data, str);        

     }

}   

String::String(const String& another)

{

     m_data = new char[strlen(another.m_data)+1];

     strcpy(m_data, another.m_data); 

}                          

String::String& operator = = (const String& rhs)

{

    if(this == &rhs)

    return &this;

    delete[]  m_data;

    m_data = new char(strlen(rhs.m_data)+1);   //删除原来的数据,新开一块内存

   strcpy(m_data, rhs.m_data); 

     return *this;

}

~String()

{

    delete[]  m_data;

}

 

18、运算符重载的三种方式和不允许重载的5个运算符

答:运算符重载意义是为了对用户自定义数据的操作和内定义的数据类型的操作形式一致

(1)普通函数,友元函数,类成员函数

(2).*(成员指针访问运算符)   

      ::(域运算符) 

      sizeof 长度运算符   

    ?:条件运算符   

     .(成员访问运算符)

 

19、动态连接库的两种方式?

答:调用一个DLL中的函数有两种方法:

     1.载入时动态链接(load-time dynamic linking),模块非常明确调用某个导出函数,使得他们就像本地函数一样。

这需要链接时链接那些函数所在DLL的导入库,导入库向系统提供了载入DLL时所需的信息及DLL函数定位。

     2.运行时动态链接(run-time dynamic linking),运行时可以通过LoadLibrary或LoadLibraryEx函数载入DLL。

DLL载入后,模块可以通过调用GetProcAddress获取DLL函数的出口地址,然后就可以通过返回的函数指针调用DLL函数了。如此即可避免导入库文件了。

 

20、windows平台下网络编程有哪几种网络编程模型?

答:有阻塞,select,异步选择,事件选择,重叠模型,完成端口模型。

      除了阻塞模型外,其他都是非阻塞模型,其中效率最高的是完成端口模型,尤其在windows下服务器最合适了。

      做客户端一般用事件模型了,select在window和类unix都可以使用。

 

21、C++四种强制类型转换

(1)const_cast

       它可以使一个本来不是const类型的数据转换成const类型的,或者把const属性去掉。

       struct SA{  int k};  const SA ra;   

       ra.k = 10;    //直接修改const类型,编译错误   

       SA& rb =  const_cast<SA&>(ra);   rb.k = 10; //可以修改

 

 (2)static_cast

      主要用于基本类型之间和具有继承关系的类型之间的转换。用于指针类型的转换没有太大的意义

      static_cast是无条件和静态类型转换,可用于基类和子类的转换,基本类型转换,把任何类型的表达式转换成void类型,

      static_cast不能进行无关类型(如非基类和子类)指针之间的转换。

      int a;     double d = static_cast<double>(a);   //基本类型转换

      int &pn = &a;     void *p = static_cast<void*>(pn);   //任意类型转换为void

 

 (3)dynamic_cast

      你可以用它把一个指向基类的指针或引用对象转换成继承类的对象动态类型转换,运行时类型安全检查(转换失败返回NULL)

如果成功的话返回的是指向类的指针或引用,基类必须有虚函数,保持多态特性才能用dynamic_cast,只能在继承类对象的指针之间或引用之间进行类型转换

      class BaseClass{public:  int m_iNum;  virtual void foo(){};};

      class DerivedClass:BaseClass{public: char* szName[100];  void bar(){};};

      BaseClass* pb = new DerivedClass();

     DerivedClass *p2 = dynamic_cast<DerivedClass *>(pb);

      BaseClass* pParent = dynamic_cast<BaseClass*>(p2);     //子类->父类,动态类型转换,正确

 

   (4)reinterpret_cast

      转换的类型必须是一个指针、引用、算术类型、函数指针或者成员指针。主要是将一个类型的指针,转换为另一个类型的指针

      不同类型的指针类型转换用reinterpreter_cast,最普通的用途就是在函数指针类型之间进行转换

      int DoSomething(){return 0;};

      typedef void(*FuncPtr)(){};

      FuncPtr funcPtrArray[10];

      funcPtrArray[0] = reinterpreter_cast<FuncPtr>(&DoSomething);

 

    与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了,容易被看漏,所以一旦转换过程出现问题,追踪

起来也更加困难

 

22、如何判断一段程序是由C 编译程序还是由C++ 编译程序编译的?

答:

         C++ 编译时定义了 __cplusplus

         C 编译时定义了 _STDC_

 

23、关键字volatile有什么含意?并给出三个不同的例子

答:一个定义为volatile的变量是说这变量可能会被意想不到地改变,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值。而不是使用保存在寄存器里的备份。

     下面是volatile变量的几个例子:

      1) 并行设备的硬件寄存器(如:状态寄存器)

      2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

      3) 多线程应用中被几个任务共享的变量

 深究:一个参数既可以是const还可以是volatile,一个例子是只读的状态寄存器,它是volatile因为它可能被意想不到地改变,

是const因为程序不应该试图去修改它。一个指针可以是volatile,一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

 

24、C++多态性的实现原理

1. 每一个类都有虚表
2. 虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
3. 派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

这就是C++中的多态性。当C++编译器在编译的时候,发现animal类的breathe()函数是虚函数,这个时候C++就会采用迟绑定(late binding)技术。也就是编译时并不确定具体调用的函数,而是在运行时,依据对象的类型(在程序中,我们传递的fish类对象的地址)来确认调用的是哪一个函数,这种能力就叫做C++的多态性。我们没有在breathe()函数前加virtual关键字时,C++编译器在编译时就确定了哪个函数被调用,这叫做早期绑定(early binding)。

4. C++类的内存布局:虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。

 

单一继承下,没有虚函数覆盖

1)虚函数按照其声明顺序放于表中。

 

2)父类的虚函数在子类的虚函数前面。

单一继承下,有虚函数覆盖

 

1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。

 

2)没有被覆盖的函数依旧。

 

 

多重继承下,没有虚函数覆盖

 

1)  每个父类都有自己的虚表。

 

2)  子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

 

 

使用虚函数需要注意的方面:(主要从安全性方面考虑)

1:任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法。

2:如果父类的虚函数是privateprotected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们可以使用访问虚函数表的方式来访问这些non-public的虚函数

 

 虚函数虚指针再探讨:

对于一个具体类型来说,它的行为是确定的,举一个简单例子,父类是几何形状,它有一个虚方法是计算自己的面积。对于具体类型例如矩形,正方形,圆形等,他的计算面积的方法是确定的。即对于同一个类型的所有实例,它们的虚函数表的内容相同,在编译时可以确定。因此,如果把虚函数表的全部内容附着在对象实例上,这样的对象模型显然是浪费内存的。因此,在对象实例起始处,放的是一个指针,指向其虚函数表,因此每个对象的虚函数表在实例中仅占一个指针(4 bytes)空间。如果一个类型有多个实例,它们指向的是同一份虚函数表(典型情况是位于进程空间的 .rdata section)。虚函数表指针的初始化由编译器生成的各种构造函数负责。如果一个函数不包含虚函数,则对象实例中不包含虚函数表指针。

25、开发者都应该使用的10个C++11特性

   参考以下博客:http://my.oschina.net/xlplbo/blog/343242

 

26、C语言中基本数据类型的隐式转换

     隐式类型转换: 所有的char型和short型数据在运算前都必须先转换为int型,所有的float型数据都必须转换为double型。

     显示类型转换:(类型名)表达式

 

27、Union数据结构

      共用体变量存储空间的长度不是该变量所有成员项的空间长度的总和,而是把长度最大的成员项的存储空间作为共用体变量的存储空间。

      下面介绍一个例子,加深对Union数据结构的理解:

       

利用union判断系统的大小端序的方法如下

       

 

28、register的特点

     register只是寄存器变量,表示这个数据要保存在寄存器中,用寄存器变量是为了提高程序运行速度,因为寄存器是取值和修改最快的,

所以把那些需要多次使用的变量保存在寄存器中是一种提高效率的做法,和算法没关。

 

29、操作符重载问题

      1、C++要求赋值=,下标[],调用(), 和成员指向-> 操作符必须被定义为类成员操作符。任何把这些操作符定义为名字空间成员的定义都会被标记为编译时刻错误。
      2、改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用,通常应该是成员函数。

      3、具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。

 

30、内存对齐问题

       有三个比较重要的原则:

       1、结构体变量的首地址是其最长基本类型的整数倍;

       2、结构体每个成员相对于结构体的首地址的偏移量都是成员大小的整数倍;

       3、结构体的总大小为结构体最宽基本类型成员大小的整数倍。

 

31、构造函数初始化时必须采用初始化列表的情况:   
      1、需要初始化的数据成员是对象(继承时调用基类构造函数)   
      2、需要初始化const修饰的类成员   
      3、需要初始化引用成员数据

 

32、存在继承关系的类型之间的转换规则

      1、从派生类向基类的类型转换只对指针或引用类型有效;

      2、基类向派生类不存在隐式类型转换;

      3、和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行。

 

33、虚继承的作用

      如果一个类是从多个基类直接继承而来的,那么有可能这些基类本身又共享了另一个基类。在这种情况下,中间类可以选择使用虚继承,从而声明愿意

与层次中虚继承同一个基类的其他类共享虚基类。用这种方法,后代派生类中将只有一个共享虚基类的副本。

 

posted @ 2016-04-13 16:06  cxm_hy  阅读(495)  评论(0编辑  收藏  举报