CPP面试题(1)

什么是面向对象

1. 面向过程思想

完成一个需求的步骤:首先是搞清楚要做什么,然后再分析怎么做,最后再通过代码体现。一步一步去实现,而具体的每一步都需要我们去实现和操作。这些步骤相互调用和协作,从而完成需求。在上面的每一个具体步骤中我们都是参与者,并且需要面对具体的每一个步骤和过程,这就是面向过程最直接的体现。 面向过程编程,其实就是面向着具体的每一个步骤和过程,把每一个步骤和过程完成,然后由这些功能函数相互调用,完成需求;

2. 面向对象思想

面向对象的思想是尽可能模拟人类的思维方式,使得软件的开发方法与过程尽可能接近人类认识世界、解决现实问题的方法和过程,把客观世界中的实体抽象为问题域中的对象。面向对象以对象为核心,该思想认为程序由一系列对象组成。 面向对象思想的特点: - 是一种更符合人类思维习惯的思想 - 可以将复杂的问题简单化 - 将我们从执行者变成了指挥者;

3. 面向对象的三大特征:封装、继承、多态

  1. 封装:将事物属性和行为封装到一起,也就是 C++ 中的类,便于管理,提高代码的复用性。事物的属性和行为分别对应类中的成员变量和成员方法;
  2. 继承:继承使类与类之间产生关系,能够提高代码的复用性以及可维护性;
  3. 多态:多态意味着调用成员函数时,会根据调用方法的对象的类型来执行不同的函数;

多态

静态多态、动态多态、多态的实现原理、虚函数、虚函数表 标准回答 在现实生活中,多态是同一个事物在不同场景下的多种形态。在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为,与之相对应的编译时绑定函数称为静态绑定。所以多态分为静态多态和动态多态。

  1. 静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错。静态多态有函数重载、运算符重载、泛型编程等;
  2. 动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数;

加分回答

  1. 动态多态行为的表现效果为:同样的调用语句在实际运行时有多种不同的表现形态;
  2. 实现动态多态的条件:
    1. 要有继承关系;
    2. 要有虚函数重写(被 virtual 声明的函数叫虚函数);
    3. 要有父类指针(父类引用)指向子类对象;
  3. 动态多态的实现原理 当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类虚函数指针的数据结构, 虚函数表是由编译器自动生成与维护的。virtual 成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr 指针)。在多态调用时, vptr 指针就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址。

重载和重写

重载

  1. 重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同(参数列表不同)。调用的时候根据函数的参数来区别不同的函数,函数重载跟返回值无关;
  2. 重载的规则 - 函数名相同 - 必须具有不同的参数列表 - 可以有不同的访问修饰符;
  3. 重载用来实现静态多态(函数名相同,功能不一样);
  4. 重载是多个函数或者同一个类中方法之间的关系,是平行关系。

重写

  1. 重写(也叫覆盖)是指在派生类中重新对基类中的虚函数重新实现。即函数名和参数都一样,只是函数的实现体不一样;
  2. 重写的规则:
    1. 方法声明必须完全与父类中被重写的方法相同;
    2. 访问修饰符的权限要大于或者等于父类中被重写的方法的访问修饰符;
    3. 类重写的方法可以加virtual,也可以不加;
  3. 重写用来实现动态多态(根据调用方法的对象的类型来执行不同的函数);
  4. 重写是父类和子类之间的关系,是垂直关系;

加分回答

  1. C++中重载的实现 采用命名倾轧(name mangling)技术,编译时会将同名的函数或方法根据某种规则生成不同的函数或方法名(因为函数或方法的特征标不一样)。
  2. C++中重写的实现 C++中重写可以用来实现动态多态,父类中需要重写的方法要加上 virtual 关键字。 虚函数实现的原理是采用虚函数表,多态中每个对象内存中都有一个指针,被称为虚函数指针,这个指针指向虚函数表,表中记录的是该类的所有虚函数的入口地址,所以对象能够根据它自身的类型调用不同的函数。

什么情况会调用拷贝构造,什么时候会调用赋值操作

  1. 拷贝构造函数的调用时机:
    1. 用一个对象初始化另外一个对象;
    2. 对象以值传递的方式传递给函数参数;
    3. 函数局部对象以值传递的方式从函数返回;
  2. 赋值操作的调用时机:
    1. 将一个对象赋值给另外一个对象;

继承

在C++中,当创建一个派生类(子类)的对象时,构造函数的执行顺序和销毁对象时析构函数的执行顺序有特定的规则,这些规则确保了对象的正确初始化和清理。以下是子类和父类构造函数及析构函数执行的特点:

构造函数执行特点

  1. 从基类到派生类:当创建一个派生类的对象时,首先调用基类的构造函数,然后是派生类的构造函数。这样做是为了确保基类的部分首先被初始化,因为派生类通常会依赖于基类中的数据成员和成员函数。

  2. 按照继承顺序:如果有多个基类(多继承情况),则基类的构造函数按照继承列表中出现的顺序依次调用。

  3. 初始化列表:派生类构造函数的初始化列表中可以特别指定调用基类构造函数的参数,如果没有指定,则使用基类的默认构造函数。

  4. 派生类构造函数的初始化:派生类的构造函数体在基类构造函数执行完毕后执行,可以包含对派生类特有数据成员的初始化。

析构函数执行特点

  1. 从派生类到基类:当销毁一个派生类的对象时,首先调用派生类的析构函数,然后是基类的析构函数。这样做是为了确保派生类的部分首先被清理,然后是基类的部分,这样可以保证资源的正确释放顺序。

  2. 按照继承的逆序:如果有多个基类,则析构函数按照继承列表中出现的逆序依次调用。

  3. 资源管理:析构函数通常用于释放对象在生命周期中分配的资源,如内存、文件句柄等。正确的析构顺序确保了资源被正确释放,避免了资源泄露。

  4. 虚拟析构函数:如果基类中的析构函数不是虚拟的(没有使用virtual关键字),那么在派生类中重写析构函数时,基类的析构函数不会自动变为虚拟的。这可能导致在通过基类指针删除派生类对象时,派生类的析构函数不被调用,从而可能导致资源泄露。因此,建议将基类的析构函数声明为虚拟析构函数。

通过遵循这些规则,C++确保了对象的正确构造和销毁,从而维护了程序的稳定性和健壮性。正确理解和使用这些构造和析构的特点,对于编写高效、安全的面向对象程序至关重要。

虚函数的实现原理

  1. 虚函数的作用:C++ 中的虚函数的作用主要是实现了动态多态的机制。动态多态,简单的说就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
  2. 虚函数实现原理 编译器处理虚函数时,给每个对象添加一个隐藏的成员。隐藏的成员是一个指针类型的数据,指向的是函数地址数组,这个数组被称为虚函数表(virtual function table,vtbl)。虚函数表中存储的是类中的虚函数的地址。如果派生类重写了基类中的虚函数,则派生类对象的虚函数表中保存的是派生类的虚函数地址,如果派生类没有重写基类中的虚函数,则派生类对象的虚函数表中保存的是父类的虚函数地址。
  3. 加分回答:
    1. 使用虚函数时,对于内存和执行速度方面会有一定的成本:
      1. 每个对象都会变大,变大的量为存储虚函数表指针;
      2. 对于每个类,编译器都会创建一个虚函数表;
      3. 对于每次调用虚函数,都需要额外执行一个操作,就是到表中查找虚函数地址;

什么是纯虚函数,有什么作用

  1. 纯虚函数是一种特殊的虚函数,它的格式是:虚函数不给出具体的实现,也就是后面没有大括号实现体,而在后面加上 “=0” class 类名 { virtual 返回值类型 函数名(参数列表) = 0; } ;
  2. 作用:很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。例如猫类和狗类的基类是动物类,动物类中有一个吃饭的函数 eat(),那这个 eat() 函数可以是纯虚函数,因为并不能够确定动物吃的东西是什么,具体吃的内容由不同的派生类去实现;
  3. 特点:如果一个类中有纯虚函数,那么这个类也被称为抽象类。这种类不能实例化对象,也就是不能创建该类的对象。除非在派生类中完全实现基类中所有的纯虚函数,否则派生类也是抽象类,不能实例化对象。

虚函数和纯虚函数的区别

  1. 格式:虚函数的定义格式为:virtual 返回值类型 函数名(参数列表) {} 纯虚函数的定义格式为:virtual 返回值类型 函数名(参数列表) = 0;;
  2. 特点:虚函数可以有具体的实现,纯虚函数没有具体的实现。 对于虚函数来说,父类和子类都有各自的版本,由多态方式调用的时候动态绑定。 有纯虚函数的类称为抽象类,有纯虚函数的类不能实例化,派生类必须实现纯虚函数才可以实例化,否则也是抽象类;
  3. 作用:虚函数是 C++ 中用于实现动态多态的机制。 很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。

内联函数和函数的区别,内联函数的作用

区别

  1. 内联函数比普通函数多了关键字 inline;
  2. 内联函数避免了函数调用的开销;普通函数有调用的开销;
  3. 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址;
  4. 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句,如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行;普通函数没有这个要求;

内联函数的作用

因为函数调用时候需要创建时间、参数传入传递等操作,造成了时间和空间的额外开销。通过编译器预处理,在调用内联函数的地方将内联函数内的语句复制到调用函数的地方,也就是直接展开代码执行,从而提高了效率,减少了一些不必要的开销。同时内联函数还能解决宏定义的问题。

虚函数可以是内联函数吗

  1. 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联;
  2. 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联;
  3. inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生;