Skip to content

面向对象

类和结构体的区别?

唯一区别是,类中成员变量默认为私有,而结构体中则为公有

其他细节区别:

  • 名字和关键字不同
  • 默认的继承方式不同

成员被 public、protected 和 private 继承的情况

public 成员 protected 成员 private 成员
public 继承 public protected private
protected 继承 protected protected private
private 继承 private private private

空类包含哪些函数?

  • 构造函数
  • 拷贝构造函数
  • 拷贝赋值运算符
  • 析构函数
  • 取址运算符
  • 移动构造函数
  • 移动赋值运算符

sizeof 类的结果?

  1. 空类: 空类的大小为 1

  2. 虚函数: 不论有多少个虚函数,sizeof 的结果都只和虚表指针的个数相关

  3. 成员函数: 除了虚函数表外,成员函数(包括构造函数和析构函数)不影响 sizeof 的结果,因为它们不存储在对象实例中

  4. 静态数据成员: 静态数据成员不影响 sizeof 的结果

  5. 内存对齐: 编译器为了满足特定的对齐需求,可能会在数据成员之间或类的末尾添加额外的填充字节

auto 能用在类成员变量里面吗?

不能。

C++ 标准是这么规定的,auto 只不能用于类的非静态成员声明

访问类中静态函数的方式?

在 C++ 中,类的静态成员函数与静态成员变量一样,属于类本身,而不是类的任何特定对象实例。因此,静态成员函数可以在没有创建类的对象的情况下被调用。

访问静态成员函数的常见方式如下:

  1. 通过类名: 使用类名和范围解析操作符 :: 来访问静态成员函数。

    class MyClass {
    public:
        static void staticFunction() {
            std::cout << "Static member function called!" << std::endl;
        }
    };
    
    int main() {
        MyClass::staticFunction();
        return 0;
    }
    

  2. 通过对象: 尽管静态成员函数与类的特定实例无关,但仍然可以使用类的对象来调用它(尽管这不是推荐的方式,因为它可能导致混淆)。

    MyClass obj;
    obj.staticFunction();  // 这是合法的,但可能会引起混淆
    

  3. 通过指针或引用: 与使用对象直接调用的方式类似,你也可以使用指向对象的指针或引用来调用静态成员函数,但这也是不推荐的。

    MyClass *ptr = &obj;
    ptr->staticFunction();  // 这是合法的,但可能会引起混淆
    

注意:静态成员函数只能访问静态成员变量或其他静态成员函数;它们不能访问类的非静态成员,因为非静态成员必须在类的对象上下文中访问,而静态成员函数没有这样的上下文。

静态成员函数可以被 const 修饰吗?

在C++中,静态成员函数不能被声明为const

const成员函数的意义在于它不能修改调用它的对象的状态。但静态成员函数不与特定的对象实例相关,因此没有"当前对象"的状态可以被修改。因此,为静态成员函数添加const限定符没有意义,并且编译器会产生错误。

方法重载和重写的区别?

在 C++ 中,重载(Overloading)和重写(Overriding)是两个不同的概念,它们分别与函数的多态性和类的继承相关。

  1. 重载(Overloading)

    • 定义:在同一作用域内,如果有两个或多个函数拥有相同的名称,但参数列表(参数的类型、顺序或数量)不同,我们称这些函数被重载了。
    • 目的:提供一种方式,允许多个函数使用相同的名称,但通过不同的参数列表进行区分。
    • 适用于:普通函数和成员函数都可以重载。
    • 例子
      class Demo {
      public:
          void func(int a) { }
          void func(double a) { }  // 重载了上面的 func 函数
      };
      
  2. 重写(Overriding)

    • 定义:当派生类含有与基类中的虚函数同名、同参数列表的成员函数时,我们称派生类的函数重写了基类的函数。
    • 目的:提供一种机制,允许派生类改变基类中虚函数的实现,以实现多态。
    • 适用于:只适用于类的成员函数,并且基类的成员函数必须被声明为 virtual
    • 例子
      class Base {
      public:
          virtual void func() const {
              // 基类中的实现
          }
      };
      
      class Derived : public Base {
      public:
          void func() const override {  // 重写了基类的 func 函数
              // 派生类中的新实现
          }
      };
      

虚函数是怎么实现的?

  1. 虚函数表 (vtable):
    当类中声明了至少一个虚函数时,编译器会为这个类生成一个虚函数表(vtable)。虚函数表是一个函数指针数组,其中每个指针指向该类中的一个虚函数。
    一般继承时,派生类的虚函数表中先将基类虚函数放在前,再放自己的虚函数指针。
    如果派生类覆盖了基类的虚函数,虚函数将被放到了虚表中原来基类虚函数的位置。

  2. 虚函数表指针 (vptr):
    当一个类对象被实例化时,系统会在该对象的内存中为其增加一个虚函数表指针(vptr)。这个 vptr 指向对应类的虚函数表。
    vptr 在对象的内存的头部,紧接着是对象按照声明顺序排列的成员变量。

  3. 动态调用:
    当通过基类的指针或引用调用虚函数时,系统首先会访问该对象的 vptr 以得到 vtable 的地址,然后从 vtable 中找到对应虚函数的地址并进行调用。由于不同的对象类型会有不同的 vptr 值,所以它们可能指向不同的虚函数表,从而达到多态的效果。

内联函数可以是虚函数吗?

首先,inline 是给编译器的建议,最终由编译器决定是否内联,并不是写了 inline 就一定会内联。
内联是在编译期,把函数调用处的函数直接替换为函数体的内容。而虚函数,是在运行期,确定实际调用哪个版本的函数,从而实现多态。
所以,多态和内联是肯定矛盾的,只能表现出一个。
但是,虚函数在调用处能明确知道其是哪个类的成员时,可以内联,当然,此时就不表现多态性了。

虚函数表在内存中的什么位置

虚函数表一般放在只读数据段(.rodata),也就是常量区。
但是需要注意的是,C++ 标准中并没有提到虚函数表的位置,所以在不同的平台和编译器上,虚函数表的位置可能不同。

overridefinal 关键字是什么?

在 C++11 及其后续版本中,overridefinal 是两个用于虚函数的关键字,它们提供了额外的编译时检查以确保虚函数的正确性和派生类的行为。

  1. override

    • 用途:当你在派生类中重写一个基类中的虚函数时,使用 override 指定符可以告诉编译器你的意图是重写一个虚函数。
    • 好处:如果基类中没有与之匹配的虚函数,或者函数签名不完全匹配,编译器会生成一个错误。这样可以在编译时捕获到意外的函数重载或其他错误。
    • 示例
      class Base {
      public:
          virtual void foo();
      };
      
      class Derived : public Base {
      public:
          void foo() override;  // 正确:重写了基类的 foo 函数
          // void bar() override;  // 错误:基类中没有虚函数 bar
      }
      
  2. final

    • 用途

      1. 当用于虚函数时,它表示该函数在派生类中不能再被重写。
      2. 当用于类时,它表示该类不能被继承。
    • 好处:它提供了一种机制,确保派生类不会意外地改变基类中某个特定虚函数的行为,或者确保某个类不会被进一步派生。

    • 示例
      class Base {
      public:
          virtual void foo() final;  // 这个函数不能在派生类中被重写
      };
      
      class Uninheritable final {  // 这个类不能被继承
          // ...
      };
      
      class Derived : public Base {
          // void foo();  // 错误:foo 被声明为 final,不能被重写
      };
      

这两个关键字都增强了类型安全性,并帮助开发者更准确地表达他们的设计意图。

虚函数能不能使用模板?

不能。

模板函数在编译时为每种类型生成一个具体的实例。虚函数的工作方式是通过虚函数表来实现运行时多态,而不是在编译时。

什么情况下要使用多态?

  1. 抽象接口:当你希望定义一个接口,该接口可以被多种不同的类实现时,可以使用多态。例如,你可以定义一个抽象的 Shape 类,并有多个子类(如 CircleRectangle 等)实现这个接口,每个子类都有自己的 drawarea 方法。

  2. 容器中存储不同类型的对象:当你有一个容器(如 std::vector)并且希望它存储多种不同类型的对象,但你希望通过一个统一的接口与这些对象互动时,多态很有用。例如,你可以有一个 std::vector<Shape*>,其中存储了多种形状对象的指针,然后遍历它们并调用每个对象的 draw 方法。

  3. 替代大量的条件语句:如果没有多态,你可能会发现自己使用大量的条件语句来检查对象的具体类型,然后执行相应的操作。通过使用多态,你可以简化代码,减少条件语句,并使代码更易于维护。

  4. 提供扩展性:多态允许你向程序中添加新的子类,而不需要修改使用这些对象的代码。例如,如果你后来添加了一个新的 Triangle 形状类,你不需要修改与形状互动的任何代码,只需确保 Triangle 正确地实现了 Shape 接口。

  5. 实现回调和事件驱动编程:多态允许你定义一组通用的回调或事件处理接口,并允许不同的对象以他们自己的方式来响应这些事件。

  6. 工厂模式和其他设计模式:多态在许多设计模式中都有应用,例如工厂模式中,你可能有一个创建对象的方法,该方法返回一个指向基类的指针,但实际上创建的对象是某个派生类的实例。