面试专区 |【28道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)的值为多少?

在C++中,即使是一个空类(即一个没有任何数据成员和成员函数的类),sizeof 还是会返回一个大于0的值。这是因为,即便是空类,也需要有一种方式来识别其实例。因此,C++标准规定,空类的大小至少为1字节。

以代码为例:

class A { };
std::cout << sizeof(A) << std::endl;

这段代码将会输出 1,意味着空类 A 的大小是1字节。

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

“覆盖”和”重载”在C++面向对象编程中是两个非常重要的概念,它们的区别如下:

函数重载(Overloading):这发生在同一个类中,当你有两个或更多数量的函数拥有相同的名字但是参数列表不同(比如参数数量不同,参数类型不同,或者参数顺序不同)时,我们就称之为函数重载。例如:

class Example {
public:
    void func(int a) { /*...*/ } // 第一个版本的func函数
    void func(double a) { /*...*/ } // 第二个版本的func函数,参数是双精度浮点数
};

函数覆盖(Overriding):这发生在继承关系中,当子类有一个和父类完全相同的函数(函数名相同,参数列表相同,返回类型也相同),我们就称之为函数覆盖。子类覆盖父类的函数是为了提供不同的实现,这是多态性的一个重要体现。例如:

class Base {
public:
    virtual void func() { /*...*/ } // 父类的func函数
};

class Derived : public Base {
public:
    void func() override { /*...*/ } // 子类的func函数,覆盖了父类的func函数
};

在这个例子中,如果你有一个指向Derived类对象的Base类指针,并且通过这个指针调用func函数,将会执行Derived类的func函数,这就是多态性的体现。

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

拷贝构造函数和赋值运算符重载都用于在C++中复制对象,但是它们的用途和执行方式有所不同。

拷贝构造函数:拷贝构造函数在一个新对象创建的时候被调用,用于将一个已存在的对象的状态(也就是成员变量的值)复制到新对象。例如:

class Example {
public:
    int value;
    Example(int val) : value(val) { } // 常规构造函数
    Example(const Example& other) : value(other.value) { } // 拷贝构造函数
};

Example a(10); // 使用常规构造函数
Example b(a); // 使用拷贝构造函数,b的value值将会是10

赋值运算符重载:赋值运算符重载在一个已经存在的对象需要被赋予另一个已经存在的对象的状态时被调用。例如:

class Example {
public:
    int value;
    Example(int val) : value(val) { } // 常规构造函数
    Example& operator=(const Example& other) { // 赋值运算符重载
        if (this != &other) { // 防止自我赋值
            value = other.value;
        }
        return *this;
    }
};

Example a(10); // 使用常规构造函数
Example b(20); // 使用常规构造函数
b = a; // 使用赋值运算符重载,b的value值将会是10

注意,赋值运算符重载通常需要注意自我赋值的情况,并且应返回对象本身的引用,这样可以支持连续赋值如a = b = c。

总的来说,拷贝构造函数用于初始化新对象,而赋值运算符重载用于已存在的对象。

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

虚函数和多态是C++面向对象编程中非常重要的概念,它们之间有着密切的关联。

虚函数是在基类中声明的带有virtual关键字的成员函数。它允许在派生类中进行函数的重定义,从而实现多态性。当基类指针或引用指向派生类对象,并调用虚函数时,实际执行的是派生类中重定义的函数。这种行为称为动态绑定或后期绑定。

以下是一个示例:

class Animal {
public:
    virtual void makeSound() {
        cout << "Animal makes a sound." << endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        cout << "Dog barks." << endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        cout << "Cat meows." << endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->makeSound(); // 输出:Dog barks.
    animal2->makeSound(); // 输出:Cat meows.

    delete animal1;
    delete animal2;

    return 0;
}

在上面的例子中,Animal是基类,Dog和Cat是派生类。makeSound()函数在基类中被声明为虚函数,并在派生类中进行了重定义。当使用基类指针指向派生类对象并调用makeSound()函数时,根据实际对象的类型,将执行相应派生类中的函数。这就展现了多态性的特性。

多态性使得程序能够根据实际对象的类型来动态地调用相应的函数,提供了灵活性和可扩展性。通过使用虚函数和多态,我们可以编写更具通用性和可维护性的代码。

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

在C++中,struct和class是用于定义类的关键字,它们之间的主要区别在于默认的访问权限和继承方式。

默认的访问权限:在struct中,默认的访问权限是public,也就是说,struct中的成员变量和成员函数默认是可以被外部访问的。而在class中,默认的访问权限是private,也就是说,class中的成员变量和成员函数默认是只能在类的内部访问的。
下面是一个示例来说明这一点:

struct MyStruct {
    int publicVariable; // 默认为public
private:
    int privateVariable;
};

class MyClass {
    int privateVariable; // 默认为private
public:
    int publicVariable;
};

继承方式:在C++中,类可以通过继承来扩展其功能。对于struct来说,默认的继承方式是public继承,而对于class来说,默认的继承方式是private继承。
下面是一个示例来说明这一点:

struct BaseStruct {
    int x;
};

struct DerivedStruct : BaseStruct { // 默认为public继承
    int y;
};

class BaseClass {
    int x;
};

class DerivedClass : BaseClass { // 默认为private继承
    int y;
};

除了上述区别之外,struct和class在其他方面是相似的,它们都可以拥有成员变量和成员函数,并且都可以用于定义对象。选择使用struct还是class取决于你对类的设计意图和数据封装的需求,以及个人或团队的编程风格习惯。

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

在C++中,强制类型转换运算符(也称为类型转换操作符)用于在不同类型之间进行显式的类型转换。C++提供了四种类型的强制类型转换运算符,分别是static_cast、dynamic_cast、reinterpret_cast和const_cast。

这些强制类型转换运算符的用法如下:

static_cast:用于进行静态类型转换,可以将一种类型的值转换为另一种类型,前提是转换是合法的。例如,可以将一个整数转换为浮点数、将指针或引用进行类型转换等。静态类型转换在编译时进行检查,因此需要开发人员保证转换的安全性。

int x = 10;
double y = static_cast<double>(x); // 将整数x转换为浮点数

Base* basePtr = new Derived(); // Derived是Base的派生类
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 将基类指针转换为派生类指针

dynamic_cast:用于进行动态类型转换,主要用于处理多态情况下的类型转换。它可以在运行时检查对象的实际类型,并进行安全的向下转型(派生类到基类)或跨继承层次的类型转换。如果转换是不安全的,dynamic_cast将返回空指针(对于指针转换)或抛出std::bad_cast异常(对于引用转换)。

Base* basePtr = new Derived(); // Derived是Base的派生类
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 安全地将基类指针转换为派生类指针

Base* basePtr2 = new Base();
Derived* derivedPtr2 = dynamic_cast<Derived*>(basePtr2); // 转换失败,derivedPtr2将为nullptr

reinterpret_cast:用于进行低级别的类型转换,可以将任何指针或引用转换为其他类型的指针或引用,甚至没有关联的类型。reinterpret_cast通常用于处理底层的二进制数据表示,但它的使用需要非常谨慎,因为它会绕过类型系统的安全检查。

int x = 10;
void* voidPtr = reinterpret_cast<void*>(&x); // 将int指针转换为void指针

Derived* derivedPtr = new Derived();
Base* basePtr = reinterpret_cast<Base*>(derivedPtr); // 将派生类指针转换为基类指针

const_cast:用于去除表达式的常量性,可以将const或volatile限定符添加或删除。const_cast主要用于修改指针或引用的底层const属性,但是不能用于修改本身是常量的对象。

const int x = 10;
int* nonConstPtr = const_cast<int*>(&x); // 去除常量性,但修改非常量对象是未定义行为

const int y = 20;
int& nonConstRef = const_cast<int&>(y); // 去除常量性,但修改非常量对象是未定义行为

需要注意的是,尽管强制类型转换运算符可以用于特定的类型转换,但过度使用它们可能会导致代码难以理解和维护。因此,在使用强制类型转换运算符时,应当谨慎并确保转换的合理性和安全性。

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

在C++中,类成员函数的重写、重载和隐藏是面向对象编程中常用的概念,它们的含义和使用方式如下:

重写:在派生类中,如果基类的虚函数(virtual function)在派生类中有相同的函数签名(即函数名和参数类型),那么基类的虚函数被派生类的函数重写。这种情况下,通过基类的指针或引用调用该函数时,会执行派生类的函数,这是多态性的一种体现。

class Base {
public:
    virtual void func() { cout << "Base::func()" << endl; }
};

class Derived : public Base {
public:
    void func() override { cout << "Derived::func()" << endl; } // 重写基类的func()函数
};

重载:在同一个类中,如果有两个或多个函数名相同,但参数列表(包括参数数量和类型)不同,那么这些函数就构成了重载函数。函数的返回类型不能作为重载的依据。

class MyClass {
public:
    void func() { cout << "func()" << endl; } // func()函数的一个版本
    void func(int x) { cout << "func(int)" << endl; } // func()函数的另一个版本,形成重载
};

隐藏:在派生类中,如果存在与基类同名但不同参数列表的函数,那么基类中的同名函数在派生类中会被隐藏。这意味着你不能通过派生类的对象或指针来调用被隐藏的基类函数,除非显式地通过基类的名字调用。

class Base {
public:
    void func() { cout << "Base::func()" << endl; }
};

class Derived : public Base {
public:
    void func(int x) { cout << "Derived::func(int)" << endl; } // 隐藏了基类的func()函数
};

在这个例子中,如果你创建了一个Derived对象,并尝试调用func()函数(没有参数),编译器会报错,因为Derived::func(int)隐藏了Base::func()。

这三种概念在实际编程中应用频繁,理解它们对于编写和理解面向对象的C++代码至关重要。

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

在C++中,类型转换主要分为以下几种类型:

隐式类型转换(Implicit Type Conversion):编译器自动进行的类型转换。例如,从小的数值类型(如int)转换为大的数值类型(如double),或者从派生类转换为基类。隐式转换通常在赋值、函数调用、算术运算和逻辑运算等操作中发生。

int x = 10;
double y = x; // 隐式转换,从int转换为double

class Base {};
class Derived : public Base {};
Derived d;
Base& b = d; // 隐式转换,从Derived转换为Base

显式类型转换(Explicit Type Conversion):由程序员显式进行的类型转换,使用C++的强制类型转换运算符,如static_cast、dynamic_cast、reinterpret_cast和const_cast。

int x = 10;
double y = static_cast<double>(x); // 显式转换,使用static_cast从int转换为double

Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 显式转换,使用dynamic_cast从Base*转换为Derived*

构造函数和类型转换函数的转换:C++允许通过构造函数和类型转换函数进行类型转换。一个单参数的构造函数定义了如何从参数类型转换为类类型,而类型转换函数定义了如何从类类型转换为其他类型。

class MyComplex {
public:
    MyComplex(double real, double imag = 0) {} // 构造函数,定义了如何从double转换为MyComplex
    operator double() {} // 类型转换函数,定义了如何从MyComplex转换为double
};

double d = 1.0;
MyComplex c = d; // 通过构造函数进行转换

double d2 = static_cast<double>(c); // 通过类型转换函数进行转换

以上三种类型转换有各自的特点和适用场合。隐式类型转换简单方便,但可能导致精度丢失或意料之外的结果。显式类型转换提供了更多的控制,但需要程序员确保转换的安全性。构造函数和类型转换函数的转换则相对灵活,可以自定义转换规则,但需要更多的代码来实现。

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

RTTI,全称是”Run-Time Type Identification”,中文叫做”运行时类型信息”。它是C++的一个特性,它能让我们在程序运行的时候获取到对象的类型信息,或者说是能让我们知道一个对象是什么类型的。

RTTI的工作原理主要有两个方面:

type_info:这是一个类,当我们用typeid操作符去获取一个对象的类型信息时,就会得到这个类的一个实例。这个实例包含了类型的一些信息,比如类型的名字。
dynamic_cast:这是一个转换操作符,我们可以用它来在运行时检查一个对象是否能被安全的转换到某个类型。

举个例子吧,比如我们有个动物的基类Animal,然后有两个派生类Dog和Cat。我们有一个Animal的指针,但是我们不知道它实际指向的是Dog还是Cat。

如果我们想要调用Dog或者Cat特有的方法,我们就需要知道这个指针实际指向的是哪个类。这时候,我们就可以利用RTTI的dynamic_cast来检查这个指针能不能被转换到Dog或Cat,如果能,那就说明这个指针指向的就是那个类。

Animal* myAnimal = getAnimalSomehow(); // 这个函数返回一个Animal指针,但是我们不知道它实际指向的是Dog还是Cat。


Dog* myDog = dynamic_cast<Dog*>(myAnimal);
if (myDog != nullptr) {
    // 如果myDog不是nullptr,说明myAnimal实际指向的是Dog,那么我们就可以安全的调用Dog的方法了。
    myDog->bark();
} else {
    // 否则,myAnimal可能指向的是Cat。
    Cat* myCat = dynamic_cast<Cat*>(myAnimal);
    if (myCat != nullptr) {
        // 如果myCat不是nullptr,说明myAnimal实际指向的是Cat,那么我们就可以安全的调用Cat的方法了。
        myCat->meow();
    }
}

这样,我们就可以利用RTTI在运行时确定对象的类型,然后做出相应的操作。

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

在C++中,有四种类型的类型转换运算符,或者说是”cast”。这四种转换运算符分别是:static_cast,dynamic_cast,const_cast,reinterpret_cast。每一种都有它们自己的用途和限制。

static_cast:这是最常用的类型转换操作符。它可以在各种不同类型之间进行转换,包括基本数据类型,指针,引用等等。但是,它不能去掉const属性,也不能在没有相关继承关系的类之间进行转换。
例如,我们可以把一个double类型的数转换为int类型:

double value = 3.14;
int intValue = static_cast<int>(value); // intValue 现在是 3

dynamic_cast:这个转换操作符主要用在多态类型的对象上,也就是在有继承关系的类之间进行转换。它在运行时检查转换的安全性。如果转换不安全,比如把父类对象转换为子类类型,那么转换就会失败,返回一个nullptr。
const_cast:这个转换操作符主要用来去掉对象的const属性。但是要注意,去掉const属性并修改对象的值是未定义行为,可能会导致程序崩溃。

例如,我们可以去掉一个const int的const属性:

const int value = 10;
int* mutableValue = const_cast<int*>(&value);

reinterpret_cast:这个转换操作符可以在任何两种类型之间进行转换,包括指针,引用,基本数据类型等等。但是,它是最不安全的转换操作符,因为它会直接进行二进制的转换,不会进行任何类型的检查。
例如,我们可以把一个int类型的数转换为一个指针:

int value = 42;
void* pointer = reinterpret_cast<void*>(value);

这四种类型转换操作符都有各自的用途和限制,使用时需要谨慎,确保转换的安全性。

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

在C++中,一个空类(即没有定义任何数据成员和成员函数的类)默认会有以下几个成员函数被编译器自动生成(也就是说,即使你在代码中没有显式地定义这些函数,编译器在编译的时候也会为你的类自动添加这些函数):

默认构造函数:这是一个没有任何参数的构造函数。如果你没有为你的类定义任何构造函数,编译器就会为你的类生成一个默认构造函数。
析构函数:这是一个用来清理对象的函数。当你的对象不再被使用时(比如它离开了它的作用域,或者你用delete来删除了一个动态分配的对象),这个函数会被调用。

拷贝构造函数:这个函数用来创建一个新对象,这个新对象的状态和另一个已经存在的对象(也就是这个函数的参数)相同。

拷贝赋值运算符:这个函数用来把一个已经存在的对象的状态复制到另一个已经存在的对象。

移动构造函数(C++11及更高版本):这个函数用来创建一个新对象,这个新对象会接管另一个已经存在的对象的资源。这个”另一个对象”通常是一个即将被销毁的临时对象。

移动赋值运算符(C++11及更高版本):这个函数用来把一个已经存在的对象的资源转移到另一个已经存在的对象,然后销毁原来的对象。

要注意的是,虽然编译器会自动生成这些函数,但如果你为你的类显式地定义了任何一个构造函数,编译器就不会再为你的类生成默认构造函数。同样,如果你为你的类显式地定义了拷贝构造函数,拷贝赋值运算符,或者析构函数,编译器就不会为你的类生成移动构造函数和移动赋值运算符。

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

在C++中,模板是一种功能强大而灵活的工具,它允许我们编写通用的代码,这些代码可以处理各种不同的数据类型。然而,在某些情况下,我们可能希望对某些特定的数据类型使用特定的代码实现,而不是使用通用的模板。这就是特例化的作用。

函数模板特例化:对于函数模板,我们可以提供特定类型的特例化版本。当这个特定类型的函数被调用时,编译器会优先选择这个特例化版本的函数,而不是通用的模板函数。

// 通用模板函数
template <typename T>
void print(const T& value) {
    std::cout << value << "\n";
}

// 特例化版本,用于std::string类型
template <>
void print<std::string>(const std::string& value) {
    std::cout << "String value: " << value << "\n";
}

在这个例子中,如果我们调用print(42),编译器会选择通用模板函数;如果我们调用print(std::string(“Hello”)),编译器会选择特例化版本的函数。

类模板特例化:对于类模板,我们也可以提供特定类型的特例化版本。当我们用这个特定类型来实例化这个模板类时,编译器会使用这个特例化版本的类,而不是通用的模板类。

// 通用模板类
template <typename T>
class MyArray {
    // ...一些通用的实现...
};

// 特例化版本,用于bool类型
template <>
class MyArray<bool> {
    // ...一些针对bool类型的特殊实现...
};

在这个例子中,如果我们写MyArray myIntArray;,编译器会使用通用模板类;如果我们写MyArray myBoolArray;,编译器会使用特例化版本的类。

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

在C++中,当我们有一个指向基类的指针或引用,而它实际上指向的是一个派生类的对象时,如果我们通过这个指针或引用去删除这个对象,那么就会调用相应的析构函数来清理这个对象。

如果这个析构函数不是虚函数,那么就会调用基类的析构函数,而派生类的析构函数则不会被调用。这就可能导致资源泄漏,因为派生类的析构函数通常负责清理派生类特有的资源。

但是如果我们把基类的析构函数声明为虚函数,那么在删除对象时就会调用正确的析构函数。也就是说,如果我们通过一个指向基类的指针删除一个派生类的对象,那么就会先调用派生类的析构函数,然后再调用基类的析构函数。这样就能确保所有的资源都被正确地清理。

举个例子:

class Base {
public:
    virtual ~Base() {
        // 清理基类的资源
    }
};

class Derived : public Base {
public:
    ~Derived() {
        // 清理派生类的资源
    }
};

Base* obj = new Derived();
delete obj;  // 这会先调用Derived的析构函数,然后再调用Base的析构函数

因此,如果你的类是作为基类使用的(也就是说,你的类有可能被其他类继承),那么你应该让你的类的析构函数成为虚函数,以防止资源泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值