继承和多态

comeweijjd枫长LH...大约 36 分钟

1. 什么是虚函数?

虚函数(Virtual Function)是一种特殊的成员函数,主要用于实现多态(Polymorphism)。虚函数允许基类的指针或引用调用派生类的成员函数,从而实现了对函数的动态绑定。这种绑定方式使得程序在运行时根据对象的实际类型来选择调用哪个函数,提高了代码的可扩展性和维护性。

虚函数的定义和使用方法如下:

声明虚函数:在基类中声明虚函数时,在成员函数声明之前加上关键字virtual。例如:

class Base {
public:
    virtual void display() {
        cout << "Base class display function." << endl;
    }
};

重写虚函数:派生类可以重写(Override)基类的虚函数,实现自己的版本。派生类中重写虚函数时,函数的签名(即返回类型、函数名和参数列表)必须与基类中的虚函数完全相同。例如:

class Derived : public Base {
public:
    void display() override {
        cout << "Derived class display function." << endl;
    }
};

使用虚函数:当基类的指针或引用指向派生类对象时,调用虚函数将执行派生类中覆盖的版本。例如:

int main() {
    Base* basePtr; // 基类指针
    Derived derivedObj; // 派生类对象

    basePtr = &derivedObj; // 基类指针指向派生类对象
    basePtr->display(); // 调用派生类的 display 函数

    return 0;
}

注意事项

  • 析构函数(Destructor)也应该声明为虚函数,这样可以确保在删除基类指针指向的派生类对象时,派生类的析构函数能够被正确调用,避免内存泄漏等问题。
  • 如果基类的虚函数没有在派生类中覆盖,则在调用虚函数时会执行基类中的实现。
  • 虚函数可以有默认实现,但也可以声明为纯虚函数(Pure Virtual Function)。纯虚函数在基类中没有实现,派生类必须覆盖这些函数。声明纯虚函数的方法是在函数声明后添加= 0。如果一个类包含至少一个纯虚函数,那么这个类就是抽象类(Abstract Class),不能直接实例化。例如:
class AbstractBase {
public:
    virtual void pure_virtual_func() = 0;
};

总结

带有虚函数(前面加入virtual)的类,编译器会为其分配一个虚函数表,里面记录的是虚函数的函数的地址。当该类被继承时,子类如果重写了虚函数就在子类的虚函数表中将父类的虚函数覆盖,否则继承父类的虚函数地址。

实例化之后,内存分配给对象虚函数指针+成员变量,虚函数指针指向虚函数表,这样程序运行的时候,通过虚函数指针找到虚函数表就是根据对象的类型来指向的,也就是实现了运行时多态

2. 虚函数的原理

虚函数的实现原理主要依赖于虚函数表(Virtual Function Table,简称Vtable)和虚函数指针。

  • 虚函数表(Vtable): 当一个类中声明了虚函数时,编译器会为这个类生成一个虚函数表。虚函数表是一个存储类成员函数指针的数组。每个包含虚函数的类都有自己的虚函数表,其中包含了类的所有虚函数的地址。当派生类覆盖(Override)了基类的虚函数时,派生类的虚函数表中会使用派生类的函数地址替换基类的函数地址。

  • 虚函数指针: 每个包含虚函数的类的对象都有一个隐藏的成员变量,即虚函数指针(Virtual Function Pointer),通常称为vptr。对象的vptr指向其类的虚函数表。当我们通过基类指针或引用调用虚函数时,编译器会自动根据vptr找到相应的虚函数表,然后根据虚函数在虚函数表中的索引找到并调用相应的函数。这个过程发生在运行时,因此实现了多态。

3. 什么是析构函数,析构函数的作用是什么?

C++中的析构函数(Destructor)是一种特殊的成员函数,用于在一个对象的生命周期结束时(如对象超出作用域或使用delete关键字释放动态分配的对象时)对该对象进行清理。析构函数的主要作用是回收对象所占用的资源,例如关闭文件、释放内存等。

析构函数的特点:

  1. 析构函数的名称与类名相同,但前面加上一个波浪符(~)表示,例如:~ClassName()
  2. 析构函数没有返回类型,也没有参数。
  3. 一个类只能有一个析构函数,不能被重载。
  4. 析构函数会自动调用基类和成员对象的析构函数。
  5. 如果用户没有显式地定义析构函数,编译器会自动生成一个默认析构函数。默认析构函数执行简单的清理工作,如调用基类和成员对象的析构函数。但它不会释放对象动态分配的内存,所以如果类中有动态分配的资源,需要自己定义析构函数。

下面是一个简单的析构函数示例:

class MyClass {
public:
    // 构造函数
    MyClass() {
        cout << "Constructor called." << endl;
    }

    // 析构函数
    ~MyClass() {
        cout << "Destructor called." << endl;
    }
};

int main() {
    MyClass obj; // 创建对象
    return 0; // 当 obj 超出作用域时,析构函数会被自动调用
}

输出结果:

Constructor called.
Destructor called.

当涉及到继承时,析构函数应该被声明为虚函数(virtual)。这是因为如果基类指针指向派生类对象时,通过基类指针删除派生类对象将只调用基类的析构函数,而不是派生类的析构函数。声明析构函数为虚函数可以确保派生类的析构函数被正确调用,避免资源泄漏。

总结

析构函数是一个成员函数,在对象超出范围或通过调用 delete 显式销毁对象时,会自动调用析构函数。 析构函数具有与类相同的名称,前面是波形符 (~)。析构函数⽤于撤销对象的⼀些特殊任务处理,可以是释放对象分配的内存空间。

4. 虚函数与纯虚函数的区别?

主要区别在于定义方式、功能和使用场景。

定义方式

虚函数(Virtual Function)是在基类中声明的带有virtual关键字的成员函数。虚函数在基类中可以提供一个默认的实现,也可以在派生类中被覆盖(Override)。

class Base {
public:
    virtual void display() {
        cout << "Base class display function." << endl;
    }
};

纯虚函数(Pure Virtual Function)是在虚函数的基础上,同时在声明后面添加= 0表示这个函数没有默认实现。派生类必须覆盖纯虚函数,提供具体的实现。

class AbstractBase {
public:
    virtual void pure_virtual_func() = 0;
};

功能

虚函数用于实现多态,允许基类的指针或引用调用派生类的成员函数。虚函数在基类中可以有默认实现,如果派生类没有覆盖该虚函数,将使用基类中的实现。纯虚函数主要用于定义接口,强制派生类实现某些函数。纯虚函数在基类中没有实现,派生类必须覆盖并实现这些纯虚函数。

使用场景

虚函数适用于基类和派生类之间需要共享相同的方法名,但实现可能不同的情况。这种情况下,基类可以提供一个默认实现,而派生类可以根据需要覆盖基类的实现。纯虚函数适用于需要为派生类定义一个通用接口的情况。这种情况下,基类定义了一个或多个纯虚函数,强制派生类遵循相同的接口规范。包含纯虚函数的类称为抽象类(Abstract Class),不能直接实例化。

总结

概念上:定义为虚函数不代表其本身没有实现,而是代表运行基类指针调用派生类函数;而纯虚函数代表这个函数没有被实现(无法提供一个默认的实现),仅仅是一个接口,规范这个类的程序员必须实现这个函数;纯虚函数定义的类叫做抽象类,不能被实例化。

在表现方式上,纯虚函数是在虚函数的基础上加入了=0

5. 构造函数能否为虚函数?

构造函数不建议是虚函数。在C++中,虚函数的主要目的是实现多态,允许派生类覆盖或重写基类中的虚函数。虚函数在运行时通过虚函数表(vtable)来实现动态分派。

然而,构造函数负责初始化类的对象,它是在对象创建时自动调用的。当我们创建一个派生类对象时,首先调用基类的构造函数,然后依次调用派生类的构造函数。在这个过程中,虚函数表并没有完全建立,因此无法实现动态分派。此外,构造函数没有返回类型,不能被继承,这意味着派生类不能覆盖基类的构造函数。所以,构造函数不能是虚函数。

6. 析构函数为什么一定是虚函数?

实际上,并非所有的析构函数都必须是虚函数。但在涉及到继承关系的类层次结构中,如果存在基类指针指向派生类对象的情况,那么将基类的析构函数声明为虚函数是非常重要的。这是因为:

  • 多态:析构函数声明为虚函数可以确保在通过基类指针删除派生类对象时,可以正确地调用派生类的析构函数。这样可以避免内存泄漏和资源泄漏。

例如:

class Base {
public:
    virtual ~Base() {
        std::cout << "Base destructor" << std::endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor" << std::endl;
    }
};

int main() {
    Base* base_ptr = new Derived();
    delete base_ptr; // 首先调用Derived的析构函数,然后调用Base的析构函数
    return 0;
}
  • 资源清理:虚析构函数确保了在析构对象时,可以逐层调用派生类和基类的析构函数,从而正确地清理对象占用的资源。

如果基类的析构函数不是虚函数,在删除基类指针指向的派生类对象时,只有基类的析构函数会被调用,而派生类的析构函数不会被调用。这可能导致派生类对象中的资源未被正确释放,从而引起内存泄漏等问题。

综上所述,在面向对象编程中,当涉及到继承和多态时,为了确保资源正确释放,通常建议将基类的析构函数声明为虚函数。然而,对于不涉及继承关系的类,析构函数不需要是虚函数。

7. C++中有多少种构造函数?

在C++中,构造函数的种类取决于它们的参数和调用方式。以下是常见的几种构造函数:

  • 默认构造函数(Default constructor):不带任何参数(或所有参数具有默认值)的构造函数。如果程序员没有为类定义任何构造函数,编译器将自动生成一个默认构造函数。
  • 带参数构造函数(Parameterized constructor):带有一个或多个参数的构造函数。这种构造函数允许在创建对象时初始化对象的成员。
  • 拷贝构造函数(Copy constructor):接受相同类类型对象的引用作为参数的构造函数。它用于初始化一个对象为另一个对象的副本。如果程序员没有定义拷贝构造函数,编译器将自动生成一个。
  • 移动构造函数(Move constructor):接受相同类类型对象的右值引用作为参数的构造函数。它用于在不创建对象副本的情况下,将资源从一个对象移动到另一个对象。这在C++11及更高版本中引入,以优化性能。如果程序员没有定义移动构造函数,编译器可能会自动生成一个。

8. 虚函数可以声明为inline吗?

虚函数可以声明为inline,但是将虚函数声明为inline的实际效果有限。inline函数是编译器在编译时将函数体直接插入到调用处的函数,目的是减少函数调用的开销。然而,虚函数的主要目的是实现运行时多态,它们在运行时通过虚函数表(vtable)来实现动态分派。这意味着编译器在编译时无法知道确切的虚函数实现,因此很难将虚函数内联。

在某些情况下,编译器可以内联虚函数。例如,如果在编译时可以确定调用的确切实现,那么编译器可以内联这些调用。这通常发生在以下情况:

  • 当通过对象而不是指针或引用调用虚函数时。
  • 当编译器可以通过分析或优化确定实际类型时。

总之,虚函数可以声明为inline,但实际内联的可能性有限。对于需要多态性的场景,通常不建议将虚函数声明为inline

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

在C++中,空类(没有定义任何成员变量和成员函数的类)会自动获得一些隐式成员函数。编译器会为这个类生成以下特殊成员函数:

  • 默认构造函数:如果没有提供其他构造函数,编译器将生成一个默认构造函数。
  • 析构函数:用于在对象生命周期结束时清理资源。
  • 拷贝构造函数:用于创建一个对象的副本。
  • 拷贝赋值运算符:用于将一个对象的值赋给另一个对象。
  • 移动构造函数(C++11引入):用于从一个对象转移资源到另一个对象,而无需创建副本。
  • 移动赋值运算符(C++11引入):用于将一个对象的资源移动到另一个对象。

这些成员函数在需要时才会被生成,例如当我们创建对象或执行相应的操作时。

10. 空类的大小是多少?为什么?

空类的大小通常为1字节。这是因为C++标准要求每个对象都有一个独特的地址,这样我们就可以创建并操作空类的对象。尽管空类没有数据成员,但为了确保其对象具有独立的内存地址,编译器会分配至少1字节的空间。不同编译器可能会有不同的实现方式,但通常情况下,空类的大小为1字节。这样可以确保在创建空类的多个对象时,它们具有不同的内存地址。

当使用空类作为基类时,派生类的大小可能会受到空基类优化(Empty Base Optimization,EBO)的影响。编译器可以选择不为派生类分配额外的空间来存储空基类。这种优化有助于减少派生类的内存占用。

11. C++中public/private/protect区别,详细介绍一下?

在C++中,publicprivateprotected是访问修饰符,用于控制类成员的访问级别。这些修饰符提供了封装和数据隐藏机制,有助于保持代码的模块化和可维护性。以下是关于这三个访问修饰符的详细介绍:

  • public(公有)public访问修饰符允许类成员在类的外部被访问。这意味着,如果一个成员被声明为public,那么它可以在类的任何地方被访问,包括类的实例、派生类以及非成员函数。通常,类的公有成员用于定义类的公共接口,允许外部代码与类进行交互。

  • private(私有)private访问修饰符限制类成员仅在类的内部被访问。这意味着,如果一个成员被声明为private,那么它不能在类的外部或派生类中被访问。通常,类的私有成员用于存储类的内部状态和实现细节。这些成员不应该被外部代码直接访问,以保持封装性和数据隐藏。

  • protected(受保护)protected访问修饰符介于publicprivate之间。如果一个成员被声明为protected,那么它可以在类的内部以及派生类中被访问,但不能在其他非成员函数中访问。protected成员通常用于存储在派生类中需要访问或修改的数据或实现细节。

总结一下:

  • public成员:可以在类的内部、派生类以及类的外部访问。
  • private成员:只能在类的内部访问,不能在派生类或类的外部访问。
  • protected成员:可以在类的内部以及派生类中访问,但不能在类的外部访问。

12. 为什么拷贝构造函数必须传引用不能传值?

拷贝构造函数必须接受对象的引用而非对象本身,原因有两点:

  • 无限递归问题: 如果拷贝构造函数接受一个对象作为参数(按值传递),则在拷贝构造函数被调用时,它需要创建一个新的对象。创建新对象时,会再次调用拷贝构造函数,这将导致无限递归调用。最终,程序会因栈溢出而崩溃。
  • 效率问题: 按值传递对象时,需要调用拷贝构造函数创建临时对象。这会导致额外的开销,特别是在处理大型对象时。而按引用传递对象可以避免额外的拷贝操作,从而提高程序效率。

为了避免无限递归调用和提高程序效率,拷贝构造函数应该接受对象的引用作为参数,而非对象本身。在C++中,拷贝构造函数通常接受一个常量引用作为参数,这样既可以避免额外的拷贝操作,又可以确保不会修改原始对象。例如:

class MyClass {
public:
    MyClass(const MyClass& other) {
        // 拷贝构造函数的实现
    }
};

13. 在成员函数中调用delete this会出现什么问题,对象还可以使用吗?

在成员函数中调用delete this会导致当前对象被释放,其内存被归还给操作系统或内存管理子系统。在调用delete this之后,当前对象变得未定义,这意味着再次访问或操作该对象将导致未定义行为。是使用delete this可能引发的问题:

  • 未定义行为:在调用delete this之后继续访问或操作该对象可能导致未定义行为。这可能导致程序崩溃、数据损坏或其他不可预测的结果。
  • 内存泄漏:如果对象拥有其他动态分配的资源,直接调用delete this可能导致内存泄漏,因为析构函数可能无法正确清理这些资源。
  • 重复删除:如果在其他地方再次尝试删除该对象,将导致重复删除。这可能导致程序崩溃或其他未定义行为。

在成员函数中调用delete this是危险且容易出错的,应该避免这种做法。在调用delete this之后,对象不再可用,访问或操作它可能导致未定义行为。正确的做法是确保在调用delete之前完成对对象的所有操作,并确保不会再次访问或操作该对象。此外,在合适的位置调用delete而不是在成员函数中,可以帮助避免潜在的问题。

14. 静态函数能定义为虚函数吗?

静态函数不能定义为虚函数。原因在于静态函数和虚函数的特性是互斥的。

  • 静态函数(static member function)是属于类的,而不是属于类的某个对象的。它可以在没有创建类对象的情况下被调用。静态成员函数没有this指针,因为它们不与任何特定的类对象关联。所以,静态函数不能访问类的非静态成员。
  • 虚函数(virtual function)的目的是实现多态。虚函数允许派生类覆盖或重写基类中的虚函数。多态是通过虚函数表(vtable)实现的,每个具有虚函数的对象都有一个虚函数表指针。当我们通过基类指针或引用调用虚函数时,编译器会根据虚函数表动态地确定要调用的实际函数实现。

因为静态函数不与任何类对象关联,所以它们不具备this指针和虚函数表指针。这意味着它们无法实现动态分派,也就不能实现虚函数的多态特性。所以,静态函数不能定义为虚函数。如果试图将静态函数定义为虚函数,编译器会报错,指出这是不允许的行为。

15. 什么是类的继承,从访问方式谈谈?

类的继承是面向对象编程(OOP)中的一个重要概念,它允许一个类(派生类)继承另一个类(基类)的属性和方法。继承可以实现代码的重用,同时提供一种组织和建立类之间关系的方式。继承表示一种 "is-a"(是一个)关系,意味着派生类是基类的特化。

从访问方式的角度来看,继承涉及到以下三种访问修饰符:publicprivateprotected。这些修饰符确定了基类成员在派生类中的可访问性。

  • public 继承: 当使用public继承时,基类的public成员在派生类中保持为public,基类的protected成员在派生类中保持为protected,基类的private成员在派生类中不可访问。这是继承的最常用形式,保持了基类的访问级别。
  • private 继承: 当使用private继承时,基类的publicprotected成员在派生类中变为private。基类的private成员在派生类中仍然不可访问。这种继承形式在实现细节上有所不同,因为派生类以一种更受限的方式继承了基类的成员。它通常用于实现 "is-implemented-in-terms-of"(基于实现)关系。
  • protected 继承: 当使用protected继承时,基类的publicprotected成员在派生类中变为protected,基类的private成员在派生类中不可访问。这种继承形式的用途较为有限,但它可以在某些特定情况下使用,例如当你希望派生类能访问基类的某些成员,但同时限制这些成员在派生类之外的访问。

16. 什么是组合?

组合(Composition)是一种面向对象编程(OOP)设计原则,它表示一个类可以包含另一个类的对象作为其成员变量。组合可以用来表示一种“has-a”关系(拥有关系)或者“part-of”关系(部分关系)。

组合的主要优点是它提供了代码的模块化、复用性和可维护性。通过将不同的功能和特性分解成独立的类,可以将这些类组合成更复杂的类。在这种情况下,组合的类不需要知道其成员对象的内部实现细节,只需要知道如何与它们交互。

17. 组合与继承优缺点?

组合和继承都是面向对象编程(OOP)中的重要概念,它们用于表示类与类之间的关系。组合是一种松耦合概念,继承是一种紧耦合概念,从设计模式的角度来说,组合会更好一些。

组合(Composition)

优点

  • 模块化:组合提供了一种将不同功能和特性分解成独立类的方法,使得代码更加模块化。
  • 复用性:组合可以使类中的功能在多个类之间复用,而不需要进行继承。
  • 松耦合:组合有助于减少类之间的耦合,使代码更加灵活和易于修改。
  • 易于维护:组合使得类更关注于它们的核心功能,从而降低了代码的复杂性,提高了可维护性。

缺点

  • 增加类数量:组合可能导致大量的小型类,从而增加了代码的复杂性。
  • 嵌套关系:组合可能引入深层次的嵌套关系,使得代码难以理解和维护。

继承(Inheritance)

优点

  • 代码复用:继承可以实现代码的重用,派生类可以继承基类的属性和方法,从而减少代码重复。 2. 易于扩展:继承提供了一种通过创建新的派生类来扩展基类功能的方法,这使得代码更易于扩展。
  • 多态:继承可以实现多态,这意味着可以通过基类指针或引用来操作派生类对象。这提高了代码的灵活性和可扩展性。
  • 统一接口:继承允许在派生类中统一基类的接口,使得代码更具一致性。

缺点:

  • 紧耦合:继承可能导致类之间的紧耦合,这使得代码更加脆弱,容易出现问题。当基类发生更改时,可能影响到所有派生类。
  • 繁复的继承层次:过度使用继承可能导致繁复的继承层次,这使得代码难以理解和维护。
  • 隐藏实现细节:当使用继承时,基类的实现细节可能被隐藏,这可能导致误解和错误。
  • 不恰当的继承关系:继承有时候会导致不合适的"is-a"关系,这可能导致类之间的关系变得模糊和不清晰。

18 . 用C++设计一个不能被继承的类?

在C++中,要设计一个不能被继承的类,可以使用关键字final。将final关键字添加到类定义中,可以阻止其他类继承这个类。如果尝试继承带有final关键字的类,编译器将报错。

以下是一个示例:

class NonInheritable final {
public:
    // 类的成员和方法定义
};

如果尝试继承NonInheritable类,编译器将报错:

class Derived : public NonInheritable { // 编译错误
public:
    // 类的成员和方法定义
};

final关键字添加到类定义中,可以确保类不能被继承,从而实现类的封闭性。

19. 面对对象三大特性?

面向对象编程(Object-Oriented Programming,简称 OOP)是一种编程范式,其核心思想是将现实世界中的事物抽象为程序中的对象。面向对象编程的三大特性是封装、继承和多态。下面将详细介绍这三个特性:

  • 继承(Inheritance): 继承是指一个类(子类)可以继承另一个类(父类)的属性和方法。子类除了拥有父类的特性外,还可以具有自己特有的属性和方法。这种方式可以实现代码的重用和模块化,减少重复代码的编写。继承有助于实现类之间的层次关系,并有助于理解和维护代码结构。

  • 封装(Encapsulation): 封装是指将数据和操作数据的方法包装在一起,形成一个独立的“对象”。这样的设计有助于降低系统的复杂性,提高可维护性。封装的主要目的是增强安全性,保护数据以免被外部代码随意访问和修改。为此,通常将对象的属性设为私有(private)或受保护(protected),并通过公共(public)方法(如 getter 和 setter 方法)来访问和修改这些属性。

  • 多态(Polymorphism): 多态是指允许不同类的对象对同一消息作出响应,即同一方法名可以在不同类中具有不同的实现。多态实现的主要方式有两种:接口实现和重写(覆盖)。

20. 类如何实现只能静态分配和只能动态分配?

在 C++ 中,可以通过控制类的构造函数、拷贝构造函数、赋值运算符和析构函数的可见性和可用性来限制类对象的分配方式。下面分别介绍如何实现只能静态分配和只能动态分配:

只能静态分配: 要实现只能静态分配,需要禁止类对象的动态分配。这可以通过将 operator newoperator delete 声明为私有(private)来实现。这样,类的外部代码将无法使用 newdelete 操作符来分配和释放类对象。示例代码如下:

class StaticOnly {
private:
    // 禁止使用 new 和 delete 操作符
    static void* operator new(size_t) = delete;
    static void operator delete(void*) = delete;

public:
    // 构造函数、拷贝构造函数、赋值运算符和析构函数
    StaticOnly() {}
    StaticOnly(const StaticOnly&) {}
    StaticOnly& operator=(const StaticOnly&) { return *this; }
    ~StaticOnly() {}
};

只能动态分配: 要实现只能动态分配,需要禁止类对象的静态分配和栈分配。这可以通过将构造函数、拷贝构造函数、赋值运算符和析构函数声明为私有(private)来实现。此外,可以提供一个公共(public)的静态工厂方法来动态分配和返回类对象。示例代码如下:

class DynamicOnly {
private:
    // 构造函数、拷贝构造函数、赋值运算符和析构函数
    DynamicOnly() {}
    DynamicOnly(const DynamicOnly&) {}
    DynamicOnly& operator=(const DynamicOnly&) { return *this; }
    ~DynamicOnly() {}

public:
    // 静态工厂方法,用于动态分配和返回类对象
    static DynamicOnly* createInstance() {
        return new DynamicOnly();
    }

    // 用于释放动态分配的类对象
    static void destroyInstance(DynamicOnly* instance) {
        delete instance;
    }
};

这样,类的外部代码只能通过调用 createInstance() 方法来动态分配类对象,并通过调用 destroyInstance() 方法来释放动态分配的类对象。静态分配和栈分配将被禁止。

22. 抽象基类为什么不能创建对象?

在 C++ 中,抽象基类(Abstract Base Class)是一种包含至少一个纯虚函数(Pure Virtual Function)的类。纯虚函数是用关键字 virtual 声明的,并被初始化为 0 的成员函数。抽象基类不能创建对象,原因如下:

  • 不完整性:抽象基类中的纯虚函数没有实现,这意味着抽象基类是一个不完整的类。创建一个不完整的类对象是没有意义的,因为它无法完成预期的操作。
  • 设计目的:抽象基类的主要目的是为派生类提供一个共同的接口和基础结构。抽象基类通常表示一个抽象概念,它封装了一组共享的行为和属性,这些行为和属性可以在派生类中具体实现。因此,抽象基类的设计初衷就是让其他类继承和实现它,而不是直接创建对象。
  • 防止误用:禁止创建抽象基类对象有助于确保开发者遵循面向对象设计原则,不会误用抽象基类。如果允许创建抽象基类对象,那么在调用纯虚函数时,将无法找到对应的实现,从而导致未定义行为。禁止创建抽象基类对象可以确保这种情况不会发生。

因此,在 C++ 中,抽象基类不能创建对象。要使用抽象基类,需要创建一个继承自抽象基类的派生类,并为派生类中的纯虚函数提供实现。然后,可以创建派生类的对象,或者通过指向派生类对象的基类指针或引用来使用这些对象。

23. 类什么时候会析构?

在 C++ 中,类对象的析构函数会在以下几种情况下被调用:

局部对象离开作用域:当一个类对象是在某个作用域(如函数内部)创建的局部对象时,一旦该作用域结束,局部对象会离开作用域并被销毁。此时,对象的析构函数会被自动调用。

void someFunction() {
    MyClass obj; // 局部对象
    // ... 执行其他操作
} // obj 离开作用域,析构函数被调用

动态分配的对象被删除:当一个类对象是通过 new 操作符动态分配的堆内存空间时,需要使用 delete 操作符来释放该内存空间。在调用 delete 时,对象的析构函数会被调用。

MyClass* obj = new MyClass(); // 动态分配对象
// ... 执行其他操作
delete obj; // 调用析构函数并释放内存

全局对象和静态对象的程序结束:全局对象和静态对象的生命周期与程序的生命周期相同。当程序结束时,这些对象会被销毁,此时它们的析构函数会被调用。需要注意的是,全局对象和静态对象的析构顺序可能是不确定的,这取决于编译器和链接器。

MyClass globalObj; // 全局对象

void someFunction() {
    static MyClass staticObj; // 静态对象
    // ... 执行其他操作
} // 静态对象的析构函数在程序结束时调用

int main() {
    someFunction();
    // ... 执行其他操作
    return 0;
} // 全局对象和静态对象在程序结束时销毁,析构函数被调用

容器中的对象被移除:当一个类对象是某个容器(如 std::vectorstd::list 等)中的元素时,如果该元素被从容器中移除或者容器本身被销毁,对象的析构函数会被调用。

#include <vector>

int main() {
    std::vector<MyClass> objVector;
    objVector.emplace_back(); // 向容器中添加对象

    // ... 执行其他操作

    objVector.clear(); // 移除容器中的所有对象,对象的析构函数被调用
    return 0;
} // 容器在作用域结束时销毁,包含的对象的析构函数被调用

类成员对象的析构:当一个类对象包含其他类对象作为其成员时,包含的类对象会在外层类对象析构时一同被销毁。在这种情况下,成员对象的析构函数会在外层类对象的析构函数执行之前被调用。需要注意的是,类成员对象的析构顺序与它们在类中的声明顺序相反。

class MemberClass {
public:
    MemberClass() {
        std::cout << "MemberClass constructor called." << std::endl;
    }

    ~MemberClass() {
        std::cout << "MemberClass destructor called." << std::endl;
    }
};

class MyClass {
private:
    MemberClass member1;
    MemberClass member2;

public:
    MyClass() {
        std::cout << "MyClass constructor called." << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destructor called." << std::endl;
    }
};

int main() {
    MyClass obj;
    // ... 执行其他操作
    return 0;
} // obj 离开作用域,MyClass 的析构函数被调用,然后是 member2 和 member1 的析构函数

24. 设计一个类计算子类的个数?

可以利用构造函数和析构函数,以及一个静态成员变量来记录子类对象的个数。以下是一个示例:

#include <iostream>

class Base {
protected:
    // 静态成员变量,用于记录子类对象的个数
    static int count;

public:
    // 构造函数
    Base() {
        // 当基类的构造函数被调用时,说明一个子类对象正在被创建
        ++count;
    }

    // 析构函数
    virtual ~Base() {
        // 当基类的析构函数被调用时,说明一个子类对象正在被销毁
        --count;
    }

    // 获取子类对象的个数
    static int getCount() {
        return count;
    }
};

// 初始化静态成员变量
int Base::count = 0;

// 子类
class Derived1 : public Base {
    // ... 其他成员
};

class Derived2 : public Base {
    // ... 其他成员
};

int main() {
    Derived1 d1;
    Derived2 d2, d3;

    std::cout << "子类对象个数: " << Base::getCount() << std::endl; // 输出:子类对象个数: 3

    return 0;
}

创建了一个基类 Base,该类具有一个静态成员变量 count,用于记录子类对象的个数。基类的构造函数和析构函数负责更新 count 变量。每当创建一个子类对象时,基类的构造函数会被调用,从而使 count 增加;每当销毁一个子类对象时,基类的析构函数会被调用,从而使 count 减少。

25. C++如何阻止一个类被实例化?一般在什么时候将构造函数声明为private?

在 C++ 中,可以通过将构造函数声明为 private 来阻止类被实例化。如果类的构造函数(包括拷贝构造函数)是私有的,那么该类的对象无法在类的外部创建。通常,在以下情况下,我们会将构造函数声明为 private

单例模式:单例模式是一种设计模式,用于确保一个类只有一个实例。为了实现单例模式,构造函数(包括拷贝构造函数)和赋值运算符需要声明为 private,以防止外部代码创建类的多个实例。在这种情况下,类通常会提供一个公共的静态成员函数(如 getInstance()),用于获取类的唯一实例。

class Singleton {
private:
    // 私有构造函数
    Singleton() {}

    // 禁止拷贝构造
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance;

public:
    // 获取唯一实例的静态成员函数
    static Singleton* getInstance() {
        if (!instance) {
            instance = new Singleton();
        }
        return instance;
    }
};

// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;

工具类和静态成员函数类:有时可能需要创建一个工具类,该类只包含静态成员函数,而不需要创建对象。在这种情况下,可以将构造函数声明为 private,以防止类被实例化。这样可以确保类只用于调用静态成员函数,而不需要创建对象。

class Utility {
private:
    // 私有构造函数
    Utility() {}

public:
    // 静态成员函数
    static int add(int a, int b) {
        return a + b;
    }

    static int subtract(int a, int b) {
        return a - b;
    }
};

int main() {
    int sum = Utility::add(3, 4); // 使用静态成员函数,而不需要创建 Utility 对象
    return 0;
}

阻止派生类实例化:在某些情况下,希望一个类只能作为基类,而不能被实例化。这种情况下,可以将构造函数声明为 privateprotected。但这种方法并不是最佳实践,因为这样会使得派生类无法访问基类的构造函数。为了达到这个目的,通常会将基类的一个或多个成员函数声明为纯虚函数,从而使基类成为抽象基类。这样就能阻止基类被实例化,同时不影响派生类的实例化。

26. 如何禁止自动生成拷贝构造函数?

在 C++11 及以后的版本中,可以通过在拷贝构造函数声明后使用 = delete 关键字来禁止自动生成拷贝构造函数。这将显式地删除拷贝构造函数,使得在尝试拷贝构造对象时会产生编译错误。同样的方法也可以应用于拷贝赋值运算符。

以下是一个示例:

class NonCopyable {
public:
    NonCopyable() {}

    // 禁止自动生成拷贝构造函数
    NonCopyable(const NonCopyable&) = delete;

    // 禁止自动生成拷贝赋值运算符
    NonCopyable& operator=(const NonCopyable&) = delete;
};

int main() {
    NonCopyable obj1;
    // NonCopyable obj2(obj1); // 编译错误,拷贝构造函数已删除
    // NonCopyable obj3 = obj1; // 编译错误,拷贝构造函数已删除
    // obj1 = obj3; // 编译错误,拷贝赋值运算符已删除

    return 0;
}

在这个示例中,我们创建了一个名为 NonCopyable 的类,并显式地删除了拷贝构造函数和拷贝赋值运算符。这样,当尝试使用拷贝构造函数或拷贝赋值运算符创建或赋值一个 NonCopyable 类的对象时,编译器会产生错误,从而阻止自动生成这些函数。

main() 函数中,创建了一个 NonCopyable 类的对象 obj1。接下来,尝试使用拷贝构造函数创建 obj2 或赋值给 obj3 都会导致编译错误,因为拷贝构造函数已被删除。同样,尝试使用拷贝赋值运算符将 obj3 赋值给 obj1 也会导致编译错误,因为拷贝赋值运算符已被删除。

27. 谈谈你对拷贝构造函数和赋值运算符的认识?

拷贝构造函数和赋值运算符是 C++ 中用于控制对象复制行为的两个重要成员函数。它们在不同的情况下起作用,但都涉及到对象的复制。让我们分别了解它们。

  • 拷贝构造函数: 拷贝构造函数是一种特殊的构造函数,用于根据已有对象创建一个新对象的副本。拷贝构造函数的参数是对同类型对象的引用。当以下情况发生时,拷贝构造函数会被调用:

  • 用一个已有对象初始化新对象:MyClass newObj(oldObj);

  • 将对象作为参数传递给函数,且传递方式为值传递(而非引用或指针传递)。

  • 函数返回对象,且返回方式为值返回(而非引用或指针返回)。

如果没有为类显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。默认拷贝构造函数执行的是逐个拷贝类的成员变量(浅拷贝)。这在大多数情况下是合适的,但如果类包含指向动态分配内存或资源的指针时,可能需要自定义拷贝构造函数以实现深拷贝。通过实现深拷贝,可以确保新对象拥有自己独立的资源,避免潜在的内存泄漏或资源竞争问题。

  • 赋值运算符: 赋值运算符(operator=)是一种特殊的成员函数,用于将一个对象的值赋给另一个已存在的对象。赋值运算符的参数是对同类型对象的引用,返回值通常是对当前对象的引用。赋值运算符在以下情况下被调用:

  • 将一个对象赋值给另一个对象:obj1 = obj2;

与拷贝构造函数一样,如果没有为类显式定义赋值运算符,编译器会自动生成一个默认的赋值运算符。默认赋值运算符执行的是逐个赋值类的成员变量(浅拷贝)。

你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.14.8