29道C++ 面向对象高频题整理(附答案背诵版)

1、什么是类?

在C++中,类是一种用户定义的数据类型,它可以包含数据成员和函数成员。数据成员用于存储与类相关的状态,而函数成员可以定义对这些数据进行操作的方法。可以把类想象为一个蓝图,根据这个蓝图可以创建对象,这些对象在内存中是类的实例。

比如说,我们可以定义一个Car类来表示汽车。这个类可以有数据成员如brandcolormaxSpeed来存储汽车的品牌、颜色和最高速度等属性。同时,Car类可能有函数成员如accelerate()brake()来定义汽车加速和刹车的操作。

在现实生活中,每辆汽车都是根据汽车制造商设计的蓝图制造出来的,蓝图定义了汽车的特性和功能,类似地,在编程中,我们根据类创建对象来表示现实世界中的各种事物和概念。

2、面向对象的程序设计思想是什么?

面向对象程序设计(OOP)是一种编程范式,它使用“对象”来设计软件。在OOP中,对象是类的实例,类包含数据(属性)和可以对数据执行操作的方法(行为)。面向对象的核心概念包括封装、继承和多态性。

  1. 封装:是指将数据(属性)和操作数据的代码(方法)打包在一起,形成一个独立的对象。这样可以隐藏对象的内部细节,只暴露必要的操作接口。比如,一个汽车对象封装了引擎、变速器等细节,只提供加速和刹车等接口。

  2. 继承:允许新的类(子类)继承现有类(父类)的属性和方法。继承可以复用代码,并且可以创建层次结构。例如,可以有一个基本的车辆类,然后有子类如汽车、摩托车等,它们继承基本类的共同特性。

  3. 多态性:指的是不同类的对象可以通过同一接口调用,具有不同的行为。例如,如果有一个函数接受车辆类的对象,那么任何车辆的子类对象,如汽车或摩托车,都可以使用该函数,但具体的行为会根据对象的实际类型而有所不同。

OOP的思想是通过模仿现实世界来组织和设计代码,使得代码更加模块化、易于理解和维护。通过把现实世界的实体映射成程序中的类和对象,开发者可以在更高的层次上思考问题,这样可以更容易地解决复杂的软件问题。

3、面向对象的三大特征是哪些?

面向对象编程(OOP)的三大特征是封装、继承和多态。它们是OOP中最核心的概念,每个特征都解决了软件开发中的一些常见问题。

  1. 封装:封装是隐藏对象内部复杂性的过程,同时暴露出必要的功能。这可以防止外部代码直接访问对象内部的状态,减少了外部干扰和错误使用的可能性。在C++中,通常通过访问修饰符(private、protected、public)来实现封装。

    应用场景示例:银行账户类(BankAccount)可能包含私有数据成员来存储账户余额,并提供公共方法来进行存款和取款,而不允许直接修改账户余额。

  2. 继承:继承允许新创建的类(称为子类)继承父类的属性和方法。继承可以实现代码复用,并且可以形成一个类的层次结构。

    应用场景示例:可以有一个通用的Vehicle类,它包含所有交通工具的共通特征,然后可以有子类如CarTruckMotorcycle,它们继承Vehicle类并添加特定于它们的属性和方法。

  3. 多态:多态性意味着可以通过基类的指针或引用来调用派生类的方法。这使得程序可以在不知道对象确切类型的情况下对对象进行操作,从而使程序可以在运行时动态决定对象的行为。

    应用场景示例:可以定义一个Shape基类,并且有多个派生类如CircleRectangleTriangle。每个派生类都有一个draw()方法的实现。如果有一个Shape类型的数组,程序可以遍历这个数组,并调用每个形状的draw()方法,具体调用哪一个实现,取决于数组元素的实际类型。

这三个特性共同支撑起面向对象编程的基础结构,使得OOP成为了一个强大和灵活的编程范式。

4、C++中struct和class有什么区别?

在C++中,struct(结构体)和class(类)在语法上非常相似,但它们有一个主要的默认访问权限和默认继承类型的区别:

  1. 默认访问权限:在class中,默认的成员访问权限是私有的(private),而在struct中,默认的是公共的(public)。这意味着除非你明确指定,否则class的成员和继承类型都是私有的,而struct的成员和继承类型默认是公开的。

  2. 默认继承类型:当从structclass继承时,如果没有显式指定继承类型(public、protected或private),struct会默认采用public继承,而class会默认采用private继承。

除了这些默认行为的差异,structclass在C++中是几乎相同的,它们都可以包含数据成员、成员函数、构造函数、析构函数、成员函数重载、运算符重载等。

在实际使用中,struct通常用于包含数据的简单的聚合类型,而class通常用于需要封装和复杂行为的对象。但这更多是编程风格和传统的选择,而不是强制的规则。

例如,如果你有一个只包含数据的点结构,你可能会选择使用struct

struct Point {
    int x;
    int y;
};

如果你有一个更复杂的数据结构,可能需要封装和方法来操作数据,你可能会选择使用class

class Car {
private:
    int speed;
    int gear;
public:
    void accelerate(int increment);
    void decelerate(int decrement);
    // 更多的成员函数和构造函数
};

在现代C++编程中,选择struct还是class更多是基于你想要表达的意图,而不是它们的技术区别。

5、动态多态有什么作用?有哪些必要条件?

动态多态是面向对象编程中的一个核心特性,它允许在运行时通过指向基类的指针或引用来调用派生类的方法,使得相同的操作可以作用于不同类型的对象上,从而表现出不同的行为。

动态多态的作用非常广泛,它允许程序代码更加通用和灵活。例如,你可以设计一个函数,它接受一个基类的引用,然后在运行时,这个函数可以用不同派生类的对象来调用,而且不需要修改函数本身的代码。这种能力使得代码重用更加容易,可以构建更加抽象和动态的系统。

动态多态的实现有几个必要条件:

  1. 继承:必须有两个类,一个基类和一个从基类派生出来的子类。

  2. 基类中的虚函数:在基类中必须有至少一个函数被声明为虚函数(使用virtual关键字)。派生类通常会重写(override)这个虚函数来提供特定的功能。

  3. 基类的指针或引用:需要通过基类的指针或引用来调用虚函数,这样C++运行时才能利用虚函数表(v-table)来动态决定调用哪个函数。

  4. 动态绑定:当通过基类的指针或引用调用虚函数时,发生的是动态绑定,这意味着直到程序运行时,才决定调用对象的哪个方法。

举个例子,假设有一个基类Shape和两个派生类CircleSquare。基类中有一个虚函数draw()。那么你可以通过Shape的指针或引用来调用draw(),在运行时,如果指向的是Circle对象,则调用的是Circledraw()实现,如果是Square对象,则调用Squaredraw()实现。

这使得程序能够对不同类型的对象进行操作,而无需知道对象的确切类型,从而增加了程序的灵活性和可扩展性。

6、C++中类成员的访问权限

在C++中,类成员的访问权限是通过访问修饰符来控制的,主要有三种:publicprotectedprivate

  1. Public(公共):

    • public成员在任何地方都可以访问。
    • 如果一个类的成员被声明为public,那么这个成员可以在类的内部被访问,类的对象可以直接访问它,继承该类的子类也可以访问。
  2. Protected(受保护):

    • protected成员在类内部和派生类中可以访问,但是不能通过类的对象直接访问。
    • 这意味着如果一个成员声明为protected,那么它对于任何从该类派生的类都是可访问的,但是不可以通过对象来直接访问。
  3. Private(私有):

    • private成员只能在类内部被访问。
    • 这是最严格的访问级别,如果成员被声明为private,那么它只能被类的成员函数、友元函数访问,即使是子类也无法访问私有成员。

下面是一个简单的类定义,展示了如何使用这些访问修饰符:

class MyClass {
public:    // 公共成员
    int publicVariable;

    void publicFunction() {
        // ...
    }

protected: // 受保护成员
    int protectedVariable;

    void protectedFunction() {
        // ...
    }

private:   // 私有成员
    int privateVariable;

    void privateFunction() {
        // ...
    }
};

访问权限是面向对象设计的一个重要方面,它帮助我们实现封装。封装不仅仅是将数据和行为包装在一起,还包括对数据的保护,确保只有通过类提供的接口才能访问和修改数据,防止了外部的非法访问,降低了代码的复杂性,并使得维护和扩展更加容易。

7、多态的实现有哪几种?

在C++中,多态主要通过以下两种方式实现:

  1. 编译时多态(静态多态)

    • 这种多态在编译时发生,主要通过函数重载和运算符重载实现。
    • 函数重载是在同一作用域内有多个同名函数,但它们的参数类型或数量不同,编译器根据函数调用时传入的参数类型和数量来决定调用哪个函数。
    • 运算符重载是一种特殊的函数重载,它允许为类定义新的操作符函数,使得可以使用传统操作符来操作对象。
  2. 运行时多态(动态多态)

    • 这种多态在程序运行时发生,主要通过虚函数实现。
    • 虚函数:当一个函数在基类中被声明为虚函数时,它可以在任何派生类中被重写。通过基类的指针或引用调用虚函数时,会根据对象的实际类型来调用相应的函数,即使是在基类类型的引用或指针下也是如此。
    • 纯虚函数和抽象类:当在类中声明一个虚函数但不提供实现,只提供其声明的时候,这个函数就是纯虚函数(使用= 0语法),包含纯虚函数的类称为抽象类。抽象类不能被实例化,只能被继承,并且派生类必须提供纯虚函数的实现。

动态多态是通过虚函数表(也称为V-Table)来实现的,这是一种在运行时用来解析函数调用的机制。当类中包含虚函数时,每个对象会包含一个指向虚函数表的指针,虚函数表中存储了对应于该对象实际类型的函数地址。这样,当调用虚函数时,程序能够动态地决定应该调用哪个函数实现。

这两种多态的方式都允许同一接口使用不同的实现,使得程序可以在不完全知道对象类型的情况下,对对象进行操作。静态多态的优点是效率高,因为函数调用在编译时就已经解析了;而动态多态的优点是灵活性高,可以在运行时决定调用哪个函数。

8、动态绑定是如何实现的?

在C++中,动态绑定是通过虚函数来实现的。虚函数允许在派生类中重写基类的行为。在基类中声明虚函数时,使用关键字virtual,这样在派生类中就可以覆盖这个函数以实现不同的行为。

当我们使用基类的指针或引用来调用一个虚函数时,C++运行时会根据对象的实际类型来决定应该调用哪个函数,这个过程是在运行时发生的,因此被称为“动态绑定”。

举个例子,假设我们有一个Animal基类和两个派生类DogCatAnimal类中有一个虚函数makeSound()DogCat类分别覆盖了这个函数,提供了各自的实现。

class Animal {
public:
    virtual void makeSound() {
        std::cout << "Some generic animal sound\n";
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof!\n";
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Meow!\n";
    }
};

当我们这样调用时:

Animal* myAnimal = new Dog();
myAnimal->makeSound(); // 输出 "Woof!"

即使myAnimal是一个Animal类型的指针,它也会调用Dog类中的makeSound()函数,因为myAnimal实际指向的是一个Dog对象。这就是动态绑定的工作原理。如果将myAnimal指向Cat类的对象,那么调用myAnimal->makeSound()将输出"Meow!"。这种机制使得我们可以写出更加灵活和可扩展的代码。

9、动态多态有什么作用?有哪些必要条件?

动态多态在C++中主要用于允许在运行时选择使用哪个函数,即使我们在编写代码时不知道确切的对象类型。它使得程序可以更加灵活,可以编写出既通用又可扩展的代码。通过动态多态,同一个接口可以对应多种不同的实现,这有助于减少代码冗余和增强代码的可维护性。

动态多态的实现有以下必要条件:

  1. 继承:必须有一个基类和一个或多个派生类。
  2. 虚函数:在基类中必须有虚函数,派生类中可以重写这些虚函数。
  3. 指针或引用:使用基类类型的指针或引用来操作派生类的对象。

应用场景的例子:考虑一个图形编辑器,我们可以定义一个Shape基类,并且有多个派生类如CircleRectangle等。Shape类中有一个虚函数draw(),每个派生类都有自己的实现。

class Shape {
public:
    virtual void draw() const = 0; // 纯虚函数,使得Shape成为抽象类
};

class Circle : public Shape {
public:
    void draw() const override {
        // 绘制圆形的代码
    }
};

class Rectangle : public Shape {
public:
    void draw() const override {
        // 绘制矩形的代码
    }
};

在图形编辑器中,我们可能有一个Shape类型的列表,其中包含了各种形状的对象。在运行时,我们可以遍历这个列表,调用每个形状的draw()函数来绘制它们。这样,无论列表中有什么类型的形状,都会调用正确的绘制函数,这就是动态多态的作用。

10、纯虚函数有什么作用?如何实现?

纯虚函数在C++中用于创建抽象类,这种类不能直接实例化,而是用来定义派生类应遵循的接口。当类中至少有一个纯虚函数时,这个类就成为了抽象类。纯虚函数定义了一个接口,派生类需要覆盖这个接口提供具体的实现。

纯虚函数的作用主要有两个:

  1. 定义接口规范:它规定了派生类必须实现的函数,确保所有派生类都遵循同一接口规范。
  2. 阻止基类实例化:它使得不能创建基类的对象,只能创建派生类的对象,这样可以确保客户代码不会错误地使用不完整的基类对象。

纯虚函数的声明在C++中是在函数声明末尾加上= 0。这里的= 0并不表示函数返回值为0,而是C++语法中表示函数为纯虚函数的特殊标记。

下面是一个纯虚函数的例子:

class Base {
public:
    virtual void doSomething() = 0; // 纯虚函数
};

class Derived : public Base {
public:
    void doSomething() override {
        // 提供具体的实现
    }
};

在这个例子中,Base是一个抽象类,因为它有一个纯虚函数doSomething()Derived类继承自Base并提供了doSomething()的具体实现。这样,不能直接创建Base类的对象,但可以创建Derived类的对象。

在设计模式中,纯虚函数经常用来定义接口或者抽象基类,以便不同的派生类可以提供多样化的实现,这是实现多态的关键部分。

11、虚函数表是针对类的还是针对对象的?同一个类的两个对象的虚函数表是怎么维护的?

答:虚函数表,或者称为vtable,是针对类的。虚函数表是一个存储类中所有虚函数地址的数组。当我们定义一个类,并在其中声明了虚函数时,编译器就会为这个类生成一个虚函数表。

每一个对象(或者说是实例),只要它的类有虚函数,那么它就会有一个指向这个类的虚函数表的指针。这意味着,同一个类的各个对象,它们的虚函数表指针都指向同一个虚函数表。所以,虽然每个对象都有自己的虚函数表指针,但是同一个类的所有对象共享同一个虚函数表。

举个例子,假设我们有一个基类Animal,它有一个虚函数makeSound()。那么,Animal就有一个虚函数表,其中包含了makeSound()的地址。然后我们创建了两个Animal对象,catdog。这两个对象都有一个指针指向Animal的虚函数表,即使是两个不同的对象,但是它们的虚函数表是相同的。

然后,如果我们有一个子类Cat继承自Animal,并且重写了makeSound()函数。那么,Cat也会有一个虚函数表,其中makeSound()的地址被替换为Cat类中的makeSound()函数的地址。当我们创建一个Cat对象kitty时,kitty的虚函数表指针就会指向Cat的虚函数表。

12、为什么基类的构造函数不能定义为虚函数?

在C++中,基类的构造函数不能被定义为虚函数,原因有两个:

  1. 构造函数的目的是初始化对象。当我们创建一个对象时,构造函数被调用来初始化对象的数据成员。在这个阶段,对象才刚刚开始被构建,还没有完全形成,因此它还不具备执行虚函数调用的条件(即,动态绑定)。因为执行虚函数调用需要通过对象的虚函数表指针,而这个指针在构造函数执行完毕后才会被设置。

  2. 虚函数通常在有继承关系的类中使用,用于实现多态。在子类对象的构造过程中,首先会调用基类的构造函数,然后才是子类的构造函数。如果基类的构造函数被定义为虚函数,那么在执行基类的构造函数时,由于子类的部分还没有被构造,所以无法正确地执行子类构造函数中对虚函数的重写。这就破坏了虚函数的目的,即允许子类重写基类的行为。

因此,基于以上原因,C++不允许构造函数为虚函数。但是,析构函数可以(并且通常应该)被声明为虚函数,以确保当删除一个指向派生类对象的基类指针时,派生类的析构函数能被正确调用,避免资源泄露。

13、为什么基类的析构函数需要定义为虚函数?

在C++中,基类的析构函数应该被定义为虚函数,主要是为了能正确地释放动态分配的资源,避免内存泄漏。

当我们使用基类指针指向派生类对象,并使用delete删除这个指针时,如果基类的析构函数不是虚函数,那么只有基类的析构函数会被调用。这样,派生类的析构函数就没有机会被调用,导致派生类中的资源没有被正确释放,造成内存泄漏。

而如果我们将基类的析构函数定义为虚函数,那么在删除基类指针时,就会根据这个指针实际指向的对象类型,调用相应的析构函数,先调用派生类的析构函数,然后再调用基类的析构函数。这样就能确保所有的资源都被正确释放,避免内存泄漏。

举个例子,假设我们有一个基类Animal和一个派生类CatCat类在堆上分配了一些资源。如果我们用一个Animal指针指向一个Cat对象,然后用delete删除这个指针,如果Animal的析构函数不是虚函数,那么只有Animal的析构函数会被调用,Cat的析构函数不会被调用,Cat在堆上分配的资源就没有被释放,造成内存泄漏。而如果Animal的析构函数是虚函数,那么就会先调用Cat的析构函数,释放Cat的资源,然后再调用Animal的析构函数,这样就避免了内存泄漏。

14、构造函数和析构函数能抛出异常吗?

在C++中,构造函数和析构函数都可以抛出异常,但这并不是一个被推荐的做法,原因如下:

构造函数抛出异常:

如果在构造函数中抛出异常,那么对象的构造过程就会被中断。这就意味着对象可能处于一个部分初始化的状态,其成员可能没有被正确初始化。如果你试图在后续的代码中使用这个对象,可能会出现未定义的行为。

举个例子,你有一个DatabaseConnection类,其构造函数试图连接到数据库。如果连接失败,构造函数就抛出一个异常。这个时候,如果你在后续的代码中试图使用这个DatabaseConnection对象,就可能出现问题,因为它并没有正确地初始化。

析构函数抛出异常:

如果在析构函数中抛出异常,情况就更复杂了。析构函数通常在对象生命周期结束时被调用,或者在释放动态分配的内存时被调用。如果在这个过程中析构函数抛出了异常,而你又没有正确地捕获这个异常,那么程序就可能会中断,并可能导致资源泄露。

更糟糕的是,如果析构函数是在处理另一个异常时被调用,并在这个过程中又抛出了一个新的异常,那么C++会立即调用std::terminate,程序会立即终止。

因此,虽然构造函数和析构函数都可以抛出异常,但是在大多数情况下,我们应该尽量避免在这两个函数中抛出异常,或者至少确保这些异常被正确地捕获和处理,以避免未定义的行为

15、如何让一个类不能实例化?

在C++中,如果你希望一个类不能被实例化,也就是不能创建该类的对象,你可以通过以下两种方式来实现:

  1. 声明类的构造函数为protected或private: 如果一个类的构造函数被声明为protected或private,那么在类的外部就不能直接调用这个构造函数来创建类的对象。只有类本身和它的友元函数或类可以访问它的私有或保护成员。
class NonInstantiable1 {
private:
    NonInstantiable1() {} // private constructor
};
  1. 将类声明为抽象基类(Abstract Base Class, ABC): 如果一个类至少有一个纯虚函数,那么这个类就是抽象基类,无法被实例化。纯虚函数是在基类中声明但不定义的虚函数,它在基类中的声明形式如下:virtual void func() = 0;。纯虚函数使得派生类必须提供自己的实现,否则派生类也将成为抽象基类。
class NonInstantiable2 {
public:
    virtual void func() = 0; // pure virtual function
};

上述两种方式都可以让一个类不能直接实例化,但是可以作为基类被继承。在派生类中,你可以提供构造函数的实现或者实现基类中的纯虚函数,使得派生类可以被实例化。

由于内容太多,更多内容以链接形势给大家,点击进去就是答案了

16. 如果类A是一个空类,那么sizeof(A)的值为多少?

17. 覆盖和重载之间有什么区别?

18. 拷贝构造函数和赋值运算符重载之间有什么区别?

19. 对虚函数和多态的理解

20. 请你来说一下C++中struct和class的区别

21. 说说强制类型转换运算符

22. 简述类成员函数的重写、重载和隐藏的区别

23. 类型转换分为哪几种?各自有什么样的特点?

24. RTTI是什么?其原理是什么?

25. 说一说c++中四种cast转换

26. C++的空类有哪些成员函数

27. 模板函数和模板类的特例化

28. 为什么析构函数一般写成虚函数

posted @ 2023-12-18 19:03  帅地  阅读(120)  评论(0编辑  收藏  举报