首页 > 代码库 > 面向对象 C++ 面试

面向对象 C++ 面试

面向对象:抽象、封装,继承,多态

抽象:是对具体问题进行概括,抽象出一类对象的公共性质并加以描述的过程。

封装:将抽象得到的数据和行为相结合,形成一个有机的整体,也就是将数据和操作数据的函数代码进行有机的结合,形成类。隐藏实现细节,使代码模块化     (代码重用)

继承:子类继承所有基类的成员,并扩展已存在的类             (代码重用)

    继承类的类对象执行顺序

     在多继承方式下,派生类的构造函数和析构函数调用顺序是怎么样的?

     1.若类有直接或间接虚基类,先执行虚基类的构造函数;

     2.如果该类有其他基类,按继承声明列表中出现的次序,分别执行他们的 

       构造函数,但构造过程中不再执他们的虚基类的构造函数。

     3.按类定义中出现的先后顺序,对派生类新增的成员对象初始化。对于类 

       类型的成员对象,若出现在构造函数初始化列表中,以其指定的参数执

       行构造函数,或默认构造函数;对于基本类型的成员对象,若出现在初

       始化列表中,以其指定值为其赋初值,否则什么也不做。

     4.执行构造函数函数体

      is-a:继承

      has-a:组合  类对象

虚继承:多重继承特有概念

class B1:public virtual B

class B2:public B

    class A:public B1,public B2

      继承方式         |public    |protected   |private

 基类成员权限  public |public    |protected   |private(子类的私有)

    ----------------------------------------------

              protected |protected |protected   |private

    ----------------------------------------------

                private |不可访问的|不可访问的  |不可访问的

多态:一个接口,多种方法(不同对象接受同一消息的不同反应)

      多态类型指具有虚函数的类类型。

     重载多态,强制多态,包含多态,参数多态(接口重用)

     运行多态满足的3个条件:

     1.基类与派生类---复制兼容规则;

     2.同名同参的函数加virtural---覆盖;

     3.通过基类指针或引用访问基类

虚函数:

允许被子类重新定义成员函数,即覆盖或重写,父类指针根据赋值给他的不同子类指针动态调用子     类的该函数虚函数都有虚表,类中每一个对象都有一个虚指针指向虚表,其中含有虚函数入口地址

多态类中的虚函数表是Compile-Time,还是Run-Time时建立的?

虚拟函数表是在Compile-Time就建立了,各个虚拟函数这时被组织成了一个虚拟函数的入口地址的数组.而对象的隐藏成员--虚拟函数表指针是在Run-Time--也就是构造函数被调用时进行初始化的,这是实现多态的关键. 

    1.只有成员函数为虚函数。

2.不能为虚函数:静态成员函数,内联函数(静态处理),构造函数(创建是 

  不可以实现多态化,需一一创建),友元函数,全局函数

    3.析构函数可以且通常为虚函数(用指针引用时动态绑定,实现运行时多态)

      即:若基类指针调用对象的析构函数(delete),就需要让基类的析构函

       数为虚函数否则产生不确定后果

4.构造函数不可以声明为虚函数,虚函数采用虚调用方式,其允许调用只知

      接口而不知其准确对象类型的函数,但创建对象时,必须知道对象的准确

      数据类型,所以不可以声明为虚函数。

5.基类为virtual,子类不声明也是虚函数

纯虚函数:eg:shape 类中的Draw函数是纯虚函数 virtual void Draw=0,则shape类不可以直接实例化对象,shape s  (X)

   基类的指针可以指向派生类对象,调用基类虚函数时,实际执行的操作由派生类决定,继承基类接口,不必继承虚函数的实现

C++语言采用重载机制.

1.在C++ 程序中,可以将EatBeef,Eat类型的参数加以区别。 

2.一个理由是:类的构造函数需要重载机制。因为C++ 规定构造函数与类同名(请参见第 9 章),构造函数只能有一个名字。如果想用几种不同的方法创建对象该怎么办?别无选择,只能用重载机制来实现。所以类可以有多个同名的构造函数。


重载:在同一作用域内,函数名相同,参数列表不同的两个或者两个以上函数,称为重载。

覆盖:在不同的作用于中,子类中的函数和父类中的函数同名同参,并且父类中的函数有

      virtual关键字修饰。

隐藏:不同作用域,子类中的函数和父类中的函数同名,参数列表相同(则有virtual即为覆盖)或不同(不用加virtual)

(1 )如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。 

(2 )如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。 

**************

//用派生类对象初始化基类指针:Derived  a; Base*p =&a

// 对于同名同参有virtual的函数(覆盖),p->f(),输出派生类的

//对于同名同参(无virtual)或同名不同参(有无virtual均可)的函数(隐藏),p->f(),输出基类的

//用派生类对象初始化派生类指针:Derived  a; Derived *p =&a

//对于同名同参有virtual的函数(覆盖),p->f(),输出派生类的

//对于同名同参(无virtual)|同名不同参(有无virtual均可)函数(隐藏),p->f(),输出派生类的

构造函数:

创建类对象并对其初始化(传参,按声明顺序构造对象(申请空间),执行函数体(赋值)),类对象大小=各成员大小之和;

函数名为类名;

无返回值,

定义时系统自动调用;

参数不定,

有默认构造函数,带参产生不同结果,不唯一。

创建类对象时无参不用括号

不可以声明为虚函数,虚函数采用虚调用方式,其允许调用只知接口而不知其准确对象类型的函数,但创建对象时,必须知道对象的准确数据类型,所以不可以声明为虚函数

派生类构造函数:

  调用基类构造,按其继承声明顺序

  对派生类新增成员对象初始化,按类中声明的顺序。

  执行派生类构造函数体的内容

析构函数

   唯一,有默认,无需参数则不可重载,可以是内联函数

   可以为虚函数---Base*p;child c;p=&c;撤销p时调用基类析构,子类析构未调用,造成内存泄露;若基类虚构为虚,先调用子类析构,再调基类析构拷贝、赋值构造函数:

Point a(4,5);

Point b=a; //1 用类对象初始化类对象;调用拷贝构造函

fn1(b);    //2,类对象作为实参,调用拷贝构造函数,再执行函数体 4

b=fn2(a);  //3,返回值为类类型,调用拷贝构造函数(对实参),再执行函数

             体(返回值)

总结const的用法

1、C++中:const修饰的标识符为常量;C中修饰的标识符为只读变量

2、Const修饰的函数为常成员函数。

3、若将一个对象声明为常对象,则该对象只可调用它的常成员函数。

4、在常成员函数调用期间,目的对象均视为常对象,目的对象的数据成员不可更新。

5常引用:常引用所引用的对象不可以更新

  可用于对重载函数的区别void print();void print() const

6、const int *p; i=9; p=&I;不可变指向内容,*p=6(error)

但可改变指针本身 p=&j     Int * const p 可改变指向的内容,

*:指针运算符

   出现在声明语句中,变量之前,表声明的是指针 int *p

   出现在执行或声明语句的初始化表达式中,表示访问指针所指对象的内容*p

&:取地址运算符 

   在被声明变量左边,表示声明的是引用。int &ref 

   在等号右边表取对象的地址                  int *p=&i

指针,引用

   引用必须初始化,指针可以不用;

   初始化后知不可以改变,指针可以修改指针所指向的对象;

   不存在指向空的引用;

   其占内存空间长度是对象的大小,指针是4;

   传引用,无需新开辟空间,引用是直接访问,而指针是间接访问。

   case:

   1、对于数据参数传递,减少大对象的参数传递开销两个用途来讲,引用更好

      用,简洁,安全。

   2、使用指针:若指针所指对象,需用分支语句加以确定,或需改变其指向;

              用空指针的时候;

              使用函数指针时;

              new动态创建的对象、数组,用指针存储地址;

              以数组传递大批量数据用指针类型接收;

 

       一般:指针的值只能赋值于相同变量的指针。

       void 不可以声明变量

       void 类型指针(一般只用于指针所指的数据类型不明时)可存储任何类

       型对象的地址。但必须使用显式类型化,eg:

              void *p;

              p=&i;

              int *pp=static_cast<int *>(p);

         

作用域:一个标识符在程序正文中的有效区域。

1:函数原型作用域;

2:局部作用域;

3:类作用域;x:X....x::X(访问类的静态成员);ptr->m,ptr:指向X类的一个对象的指针

4:命名空间作用域:全局(默认)和匿名(显式声明) 命名空间

可见性:从标识符有效范围:程序运行到某点时可引用到的标识符,就是可见标识符规则:

标识符声明在前,引用在后;

同一作用域,不可有同名标识符;

未互相包含且处于不同作用域的同名标识符互不影响;

互相包含且处于不同作用域的同名标识符,外层在内层不可见;

Static

==把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。static局部变量只被初始化一次,下一次依据上一次结果值;

==把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。

static全局变量只初使化一次,防止在其他文件单元中被引用;

==修饰类的成员变量,在内存中只有一份拷贝,为所有对象共享。

==static函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件 ,static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝

==Static修饰类的成员函数,此函数不接受this指针,只能访问类的static成员变量。

类的静态成员和非静态成员有何区别?

(1)类的数据方面。类的静态数据成员为所有类的所有对象共享,内存中只有一份。 

     而非静态数据成员每个类对象都拥有自己的一份。

(2)类的成员函数。类的静态成员函数没有默认的this指针,不能在类中对成员进行  

     直接访问。而类的成员函数有一个默认的this指针。

(3)调用方式。静态成员用类名加上作用域调用||类对象调用,而非静态成员必须要  

    用类对象调用。 c.showCount;

                   Point ::showCount ; 

在类定义后还需再类外加以定义。

static int count ;

int Point ::count=0;  类名::标识符。

若类的静态常量是int 或enum,直接在类定义中指定常量值

运算符:+-*/%++--   &|~^<<>> !&&|| ><==  = [] () -> , new delete ->*  

1.不能重载的运算符 "." ".*" "::" "?:" 4个

2. 不能造新的运算符

3. 不会改变三特性(目数,优先级,结合方向)

4*.功能一致(相当) 

5. 至少有一个操作数是自定义类型

6. "=" "->" "[]" "()" 只能重装为成员函数

Point operator ++(int )

Point &operator ++( )

friend ostream &operator<<(ostream &out,const Point &p);

在C++ 语言中,可以用关键字operator加上运算符来表示函数,运算符重载。

例如两个复数相加函数: 

Complex Add(const Complex &a, const Complex &b); 

可以用运算符重载来表示: 

Complex operator +(const Complex &a, const Complex &b); 

运算符与普通函数在调用时的不同之处是:对于普通函数,参数出现在圆括号内;

而对于运算符,参数出现在其左、右侧。例如 

  Complex a, b, c; 

  … 

  c = Add(a, b); //  用普通函数 

  c = a + b;   //  用运算符 + 

如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。 

如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有个右侧参数,因为对象自己成了左侧参数。 

从语法上讲,运算符既可以定义为全局函数,也可以定义为成员函数。


所有的一元运算符  建议重载为成员函数 

= () [] ->  只能重载为成员函数 

+= -= /= *= &= |= ~= %= >>= <<=  建议重载为成员函数 

所有其它运算符  建议重载为全局函数 

表8-4-1 运算符的重载规则 


由于C++ 语言支持函数重载,才能将运算符当成函数来用,C 语言就不行。我们要以平常心来对待运算符重载: 

(1 )不要过分担心自己不会用,它的本质仍然是程序员们熟悉的函数。 

(2 )不要过分热心地使用,如果它不能使代码变得更加易读易写,那就别用,否则会自找麻烦

C++中为什么用模板类?

(1)可用来创建动态增长和减小的数据结构 

(2)它是类型无关的,因此具有很高的可复用性。 

(3)它在编译时而不是运行时检查数据类型,保证了类型安全 

(4)它是平台无关的,可移植性 

(5)可用于基本数据类型

函数模板与类模板有什么区别?

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

const #define 均可以定义常量 区别:

   const只读变量有数据类型,编译器可进行安全检查;有集成化的调试工

     具可对const常量进行调试
   
define只是字符替换

内联函数、宏的区别

  内联可以加快程序运行速度,编译时直接嵌入目标代码(调用函数时不是 

  跳转);要做参数类型检查,安全。(短程序不断被调,函数无while,     

  for,switch等)

  只是简单替换

指针和句柄

  句柄:32b的整数,是windows在内存中维护的对象内存物理地址列表的整数索引。对象驻留在内存,但是windows经常会将空闲对象释放来满足和应用对内存的需求,下一次用的对象物理地址改变,所以用句柄将对象新地址保存。知道句柄地址便间接知道对象位置。

  指针:标记某物理内存地址

描述内存分配方式以及他们的区别?

内存分成5个区,他们分别是堆、栈、代码区、全局/静态存储区和常量存储区。 

栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。 

堆,就是那些由new/malloc分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete/free。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。 

代码区:存放函数代码,程序代码。我们调用一个函数,函数指针指向的就是这片内存区。

全局/静态存储区,全局变量和静态变量被分配到同一块内存中。

常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量。

 

内存泄露一般是如何产生的?如何避免?

动态开辟的内存没有释放,造成内存泄露。为了防止泄露可以定义智能指针或者自己写一个类,将申请和释放封装在一个类中。

堆栈溢出一般是由什么原因导致的?

没有回收垃圾资源;

层次太深的递归调用。

extern "C"

是连接申明(linkage declaration),被extern "C"修饰的变量和函数是按照C语言方式编译和连接的,作为一种面向对象的语言,C++支持函数重载(同名不同参),函数被C++编译后在符号库中的名字与C语言的不同。

 

 

 

 

 

 

 

 

 

 

 


本文出自 “sunnyting” 博客,转载请与作者联系!

面向对象 C++ 面试