当前位置: 首页 > web >正文

C++常见面试题-2.C++类相关

C++ 面试问题整理 - C++类相关

二、C++ 类相关

2.1 类的基本概念与特性

  1. 类的三大特性(封装、继承、多态)分别指什么?

    • 封装(Encapsulation):将数据(成员变量)和操作(成员函数)封装在一个类中,隐藏内部实现细节,只提供公共接口与外部交互。

      class Person {
      private:std::string name;  // 私有成员,隐藏实现细节int age;
      public:// 公共接口void setName(const std::string& n) { name = n; }std::string getName() const { return name; }
      };
      
    • 继承(Inheritance):派生类继承基类的属性和方法,实现代码复用和扩展。

      class Student : public Person {  // Student继承自Person
      private:std::string studentId;
      public:void setStudentId(const std::string& id) { studentId = id; }std::string getStudentId() const { return studentId; }
      };
      
    • 多态(Polymorphism):通过虚函数实现,允许基类指针或引用指向派生类对象,并调用派生类的方法。

      class Animal {
      public:virtual void speak() const { std::cout << "Some sound" << std::endl; }
      };class Dog : public Animal {
      public:void speak() const override { std::cout << "Woof!" << std::endl; }
      };Animal* animal = new Dog();
      animal->speak();  // 输出"Woof!",体现多态
      delete animal;
      
  2. 什么是面向对象(OOP)?面向对象的意义是什么?

    • 面向对象(OOP):一种编程范式,将现实世界中的实体抽象为程序中的对象,每个对象包含数据(属性)和行为(方法)。
    • 面向对象的意义
      • 代码复用:通过继承和组合实现代码复用;
      • 封装性:隐藏内部实现细节,提高安全性;
      • 可维护性:模块化设计,便于代码维护和扩展;
      • 灵活性:通过多态实现运行时的动态行为;
      • 更好的模型化:更接近现实世界的问题建模方式。
  3. 封装、继承、多态的详细解释(包括继承方式的区别:公有继承、私有继承、保护继承)?

    • 封装:见问题1。

    • 继承

      • 公有继承(public):基类的public成员在派生类中为public,protected成员在派生类中为protected,private成员不可访问。
      • 保护继承(protected):基类的public和protected成员在派生类中为protected,private成员不可访问。
      • 私有继承(private):基类的public和protected成员在派生类中为private,private成员不可访问。
      class Base {
      public: int publicVar;    // 公有成员
      protected: int protectedVar;  // 保护成员
      private: int privateVar;  // 私有成员
      };class PublicDerived : public Base {// publicVar -> public// protectedVar -> protected// privateVar -> 不可访问
      };class ProtectedDerived : protected Base {// publicVar -> protected// protectedVar -> protected// privateVar -> 不可访问
      };class PrivateDerived : private Base {// publicVar -> private// protectedVar -> private// privateVar -> 不可访问
      };
      
    • 多态:见问题1。多态的实现依赖于虚函数表(vtable)和虚指针(vptr)。每个包含虚函数的类都有一个虚函数表,每个对象内部都有一个指向虚函数表的指针。

  4. 类的访问权限(public、protected、private)的区别?派生类对基类成员的访问规则?

    • 访问权限
      • public:可以被任何代码访问(类内部、派生类、外部代码)。
      • protected:可以被类内部和派生类访问,但不能被外部代码访问。
      • private:只能被类内部访问,不能被派生类和外部代码访问。
    • 派生类对基类成员的访问规则
      • 公有继承:基类的public成员在派生类中为public,protected成员在派生类中为protected,private成员不可访问。
      • 保护继承:基类的public和protected成员在派生类中为protected,private成员不可访问。
      • 私有继承:基类的public和protected成员在派生类中为private,private成员不可访问。
    • 注意:无论哪种继承方式,派生类都不能直接访问基类的私有成员,但可以通过基类的public或protected方法间接访问。
  5. 什么是组合和聚合?与继承相比,它们的优缺点(如 “组合优于继承” 的设计原则)?

    • 组合(Composition):表示"部分-整体"关系,其中部分对象的生命周期依赖于整体对象。整体对象创建时创建部分对象,整体对象销毁时销毁部分对象。

      class Engine {  // 引擎类
      public:void start() { /* 启动引擎 */ }
      };class Car {  // 汽车类
      private:Engine engine;  // 组合关系,Car包含Engine
      public:void drive() {engine.start();  // 使用引擎/* 行驶逻辑 */}
      };
      
    • 聚合(Aggregation):也表示"部分-整体"关系,但部分对象的生命周期不依赖于整体对象。整体对象和部分对象可以独立存在。

      class Wheel {  // 车轮类
      public:void rotate() { /* 旋转 */ }
      };class Car {
      private:std::vector<Wheel*> wheels;  // 聚合关系,Car使用Wheel但不拥有
      public:void addWheel(Wheel* wheel) {wheels.push_back(wheel);}// Car销毁时,wheels指向的对象可能仍然存在
      };
      
    • 与继承相比的优缺点

      • 组合/聚合的优点
        • 更低的耦合度:整体对象和部分对象之间是松耦合的;
        • 更好的灵活性:可以在运行时动态更换部分对象;
        • 避免了继承带来的菱形继承等问题;
        • 符合"组合优于继承"的设计原则。
      • 组合/聚合的缺点
        • 不能直接使用部分对象的方法,需要通过委托来实现;
        • 实现多态可能需要额外的接口设计。
      • 继承的优点
        • 可以直接使用基类的方法,代码更简洁;
        • 易于实现多态。
      • 继承的缺点
        • 高耦合度:派生类依赖于基类的实现;
        • 灵活性差:运行时不能改变继承关系;
        • 可能导致类层次过深,难以维护;
        • 容易导致菱形继承等问题。

2.2 构造函数与析构函数

  1. 构造函数和析构函数的作用?它们的调用时机和顺序(如派生类与基类的构造 / 析构顺序)?

    • 构造函数(Constructor)

      • 作用:创建对象时初始化对象,分配资源。
      • 调用时机:创建对象时自动调用。
      • 特点:与类名相同,没有返回类型,可以重载,不能手动调用。
    • 析构函数(Destructor)

      • 作用:销毁对象时清理资源,释放内存。
      • 调用时机:对象销毁时自动调用。
      • 特点:名称为波浪号(~)加类名,没有返回类型,不能重载,只有一个析构函数。
    • 派生类与基类的构造/析构顺序

      • 构造顺序:先调用基类的构造函数,再调用派生类的构造函数。
      • 析构顺序:先调用派生类的析构函数,再调用基类的析构函数。
      class Base {
      public:Base() { std::cout << "Base constructor" << std::endl; }~Base() { std::cout << "Base destructor" << std::endl; }
      };class Derived : public Base {
      public:Derived() { std::cout << "Derived constructor" << std::endl; }~Derived() { std::cout << "Derived destructor" << std::endl; }
      };int main() {Derived d;// 输出顺序:// Base constructor// Derived constructor// Derived destructor// Base destructorreturn 0;
      }
      
  2. 拷贝构造函数的作用?何时会被调用(如对象赋值、作为函数参数传递、函数返回对象)?

    • 拷贝构造函数(Copy Constructor)

      • 作用:创建一个新对象,使其成为现有对象的副本。
      • 声明形式Class(const Class& other);
      • 默认拷贝构造函数:如果没有显式定义,编译器会生成默认的拷贝构造函数,执行浅拷贝。
    • 调用时机

      • 用一个对象初始化另一个新对象;
      • 对象作为函数参数,以值传递的方式传递;
      • 函数返回一个对象(在某些编译器优化前);
      • 初始化容器中的元素(如vector的push_back)。
      class MyClass {
      public:int value;// 自定义拷贝构造函数MyClass(const MyClass& other) {value = other.value;std::cout << "Copy constructor called" << std::endl;}// 普通构造函数MyClass(int v) : value(v) {}
      };// 测试拷贝构造函数调用
      void test() {MyClass a(10);MyClass b = a;  // 调用拷贝构造函数MyClass c(a);   // 调用拷贝构造函数// 函数参数传递(值传递)func(a);  // 调用拷贝构造函数// 函数返回对象MyClass d = createObject();  // 可能调用拷贝构造函数(取决于编译器优化)
      }
      
  3. 浅拷贝和深拷贝的区别?如何实现深拷贝?

    • 浅拷贝(Shallow Copy)

      • 仅复制对象的成员变量值,不复制指针指向的堆内存。
      • 多个对象可能共享同一块堆内存,容易导致重复释放或悬垂指针问题。
      • 默认拷贝构造函数和默认赋值运算符执行浅拷贝。
    • 深拷贝(Deep Copy)

      • 不仅复制对象的成员变量值,还复制指针指向的堆内存。
      • 每个对象拥有独立的堆内存,避免了共享内存带来的问题。
      • 需要显式定义拷贝构造函数和赋值运算符来实现深拷贝。
    • 实现深拷贝

      class MyString {
      private:char* data;public:// 普通构造函数MyString(const char* str) {if (str) {data = new char[strlen(str) + 1];strcpy(data, str);} else {data = new char[1];data[0] = '\0';}}// 拷贝构造函数(深拷贝)MyString(const MyString& other) {data = new char[strlen(other.data) + 1];strcpy(data, other.data);}// 赋值运算符(深拷贝)MyString& operator=(const MyString& other) {if (this != &other) {  // 自我赋值检查delete[] data;  // 释放原有内存data = new char[strlen(other.data) + 1];  // 分配新内存strcpy(data, other.data);  // 复制数据}return *this;}// 析构函数~MyString() {delete[] data;}
      };
      
  4. 赋值运算符重载与拷贝构造函数的区别?

    • 拷贝构造函数
      • 用于创建一个新对象,使其成为现有对象的副本;
      • 语法:Class(const Class& other);
      • 调用时机:用一个对象初始化另一个新对象时。
    • 赋值运算符重载
      • 用于将一个对象的内容赋值给另一个已存在的对象;
      • 语法:Class& operator=(const Class& other);
      • 调用时机:两个已存在的对象之间进行赋值操作时。
    • 主要区别
      • 拷贝构造函数创建新对象,赋值运算符操作已存在的对象;
      • 赋值运算符需要处理自我赋值的情况,拷贝构造函数不需要;
      • 赋值运算符需要释放目标对象原有的资源,拷贝构造函数不需要。
  5. C++ 空类默认有哪些成员函数?

    • C++空类(没有显式定义任何成员的类)在编译时会默认生成以下6个成员函数:
      1. 默认构造函数(Default Constructor)Class();
      2. 默认析构函数(Default Destructor)~Class();
      3. 默认拷贝构造函数(Default Copy Constructor)Class(const Class&);
      4. 默认拷贝赋值运算符(Default Copy Assignment Operator)Class& operator=(const Class&);
      5. 默认移动构造函数(Default Move Constructor,C++11)Class(Class&&);
      6. 默认移动赋值运算符(Default Move Assignment Operator,C++11)Class& operator=(Class&&);
    • 注意:如果显式定义了任何一个构造函数,编译器就不会生成默认构造函数。同样,如果显式定义了拷贝构造函数、拷贝赋值运算符、析构函数中的任意一个,编译器可能不会生成移动构造函数和移动赋值运算符(C++11规则)。
  6. C++ 中的五种构造函数(默认构造函数、普通构造函数、拷贝构造函数、转换构造函数、移动构造函数)分别是什么?

    • 默认构造函数:没有参数的构造函数,或所有参数都有默认值的构造函数。如果没有显式定义,编译器会生成默认构造函数。

      class Person {
      public:Person() { /* 默认构造函数 */ }
      };
      
    • 普通构造函数:带有参数的构造函数,用于根据参数初始化对象。

      class Person {
      public:Person(const std::string& name, int age) {this->name = name;this->age = age;}
      private:std::string name;int age;
      };
      
    • 拷贝构造函数:见问题2。

    • 转换构造函数:只有一个参数的构造函数(除拷贝构造函数外),用于将其他类型转换为类类型。可以通过explicit关键字防止隐式转换。

      class MyString {
      public:// 转换构造函数(允许从const char*转换为MyString)MyString(const char* str) { /* 实现 */ }// 防止隐式转换explicit MyString(int size) { /* 实现 */ }
      };void test() {MyString s1 = "hello";  // 合法:隐式转换// MyString s2 = 10;     // 非法:explicit关键字禁止隐式转换MyString s3(10);        // 合法:显式调用
      }
      
    • 移动构造函数(C++11):用于从一个将亡值(rvalue)创建新对象,避免不必要的拷贝操作。

      class MyString {
      private:char* data;
      public:// 移动构造函数MyString(MyString&& other) noexcept {data = other.data;  // 直接接管资源other.data = nullptr;  // 避免原对象析构时释放资源}
      };
      

2.3 静态成员与友元

  1. 类的静态成员(静态变量、静态函数)的特点?静态函数能否访问非静态成员?

    • 静态变量(Static Member Variable)

      • 属于类而不是对象,所有对象共享同一个静态变量;
      • 必须在类外进行定义和初始化(C++17后可以在类内初始化inline静态成员);
      • 生命周期贯穿整个程序运行期间;
      • 可以通过类名直接访问,也可以通过对象访问。
      class Counter {
      public:static int count;  // 声明静态成员变量Counter() { count++; }~Counter() { count--; }
      };int Counter::count = 0;  // 定义并初始化静态成员变量void test() {Counter c1, c2;std::cout << Counter::count;  // 输出2std::cout << c1.count;        // 也输出2
      }
      
    • 静态函数(Static Member Function)

      • 属于类而不是对象,不依赖于对象实例;
      • 不能访问非静态成员变量和非静态成员函数(因为没有this指针);
      • 可以访问静态成员变量和静态成员函数;
      • 可以通过类名直接调用,也可以通过对象调用。
      class Math {
      public:static int add(int a, int b) {  // 静态成员函数return a + b;}
      };void test() {int sum = Math::add(5, 3);  // 直接通过类名调用
      }
      
    • 静态函数不能访问非静态成员的原因:静态函数没有this指针,而非静态成员是与对象实例相关联的,需要通过this指针来访问。

  2. 友元函数和友元类的作用?为何友元会破坏封装性但仍被使用?

    • 友元函数(Friend Function)

      • 不是类的成员函数,但可以访问类的所有成员(包括私有成员);
      • 在类中通过friend关键字声明;
      • 通常用于运算符重载或需要访问类内部实现的函数。
      class MyClass {
      private:int value;
      public:MyClass(int v) : value(v) {}// 声明友元函数friend int getValue(const MyClass& obj);
      };// 定义友元函数
      int getValue(const MyClass& obj) {return obj.value;  // 可以访问私有成员
      }
      
    • 友元类(Friend Class)

      • 一个类被另一个类声明为友元后,可以访问该类的所有成员;
      • 在类中通过friend class关键字声明;
      • 常用于两个关系密切的类之间,如容器类和迭代器类。
      class A {
      private:int secret;
      public:friend class B;  // 声明B为A的友元类
      };class B {
      public:void accessA(A& a) {a.secret = 10;  // B可以访问A的私有成员}
      };
      
    • 友元破坏封装性但仍被使用的原因:

      • 性能考虑:某些操作如果作为成员函数实现会导致性能下降;
      • 设计需要:在某些设计模式或特定场景下,需要两个类之间紧密协作;
      • 兼容性:为了兼容C语言或旧代码;
      • 简化代码:在不影响整体设计的前提下,友元可以简化代码实现。
      • 注意:友元关系是单向的、非传递的,应谨慎使用,避免过度使用导致封装性被严重破坏。

2.4 虚函数与多态

  1. 虚函数的作用?其实现原理(虚函数表、虚指针)?

    • 虚函数(Virtual Function)

      • 作用:允许派生类重写基类的方法,实现运行时多态。
      • 声明方式:在基类的成员函数声明前加上virtual关键字。
      class Animal {
      public:virtual void speak() const { std::cout << "Some sound" << std::endl; }virtual ~Animal() {}
      };class Dog : public Animal {
      public:void speak() const override { std::cout << "Woof!" << std::endl; }
      };
      
    • 实现原理(虚函数表、虚指针)

      • 虚函数表(Virtual Table,vtable):每个包含虚函数的类都有一个虚函数表,存储该类所有虚函数的地址。
      • 虚指针(Virtual Pointer,vptr):每个包含虚函数的对象内部都有一个指向其所属类虚函数表的指针。
      • 多态实现过程
        1. 编译器在编译时为每个包含虚函数的类生成一个虚函数表;
        2. 每个对象在创建时,其内部的虚指针被初始化为指向该类的虚函数表;
        3. 当通过基类指针或引用调用虚函数时,程序会通过虚指针找到虚函数表,然后根据虚函数表中的地址调用相应的函数(派生类的重写版本)。
  2. 纯虚函数和抽象类的定义?抽象类能否实例化?

    • 纯虚函数(Pure Virtual Function)

      • 没有实现的虚函数,在声明时初始化为0;
      • 语法:virtual 返回类型 函数名(参数列表) = 0;
      • 派生类必须实现纯虚函数,否则派生类也是抽象类。
    • 抽象类(Abstract Class)

      • 包含至少一个纯虚函数的类;
      • 用于定义接口,不能实例化;
      • 可以定义其他非纯虚函数和成员变量。
    • 抽象类不能实例化的原因:抽象类中包含未实现的纯虚函数,无法创建一个完整的对象。

      class Shape {
      public:virtual double area() const = 0;  // 纯虚函数virtual ~Shape() {}
      };class Circle : public Shape {
      private:double radius;
      public:Circle(double r) : radius(r) {}double area() const override {  // 必须实现纯虚函数return 3.14159 * radius * radius;}
      };void test() {// Shape s;  // 错误:抽象类不能实例化Shape* shape = new Circle(5);  // 正确:基类指针指向派生类对象std::cout << shape->area();    // 多态调用delete shape;
      }
      
  3. 虚析构函数的作用?为何基类析构函数通常需要声明为虚函数?

    • 虚析构函数(Virtual Destructor)

      • 声明为virtual的析构函数;
      • 作用:确保通过基类指针或引用删除派生类对象时,能够正确调用派生类的析构函数,避免资源泄漏。
    • 基类析构函数通常需要声明为虚函数的原因:

      • 如果基类析构函数不是虚函数,当通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,导致派生类中分配的资源无法释放,造成资源泄漏。
      class Base {
      public:Base() { std::cout << "Base constructor" << std::endl; }// 非虚析构函数~Base() { std::cout << "Base destructor" << std::endl; }
      };class Derived : public Base {
      private:int* data;
      public:Derived() {data = new int[10];std::cout << "Derived constructor" << std::endl;}~Derived() {delete[] data;std::cout << "Derived destructor" << std::endl;}
      };void test() {Base* base = new Derived();delete base;  // 只调用Base的析构函数,导致内存泄漏// 输出:// Base constructor// Derived constructor// Base destructor
      }// 修复:将基类析构函数声明为虚函数
      class Base {
      public:virtual ~Base() { std::cout << "Base destructor" << std::endl; }
      };void testFixed() {Base* base = new Derived();delete base;  // 先调用Derived的析构函数,再调用Base的析构函数// 输出:// Base constructor// Derived constructor// Derived destructor// Base destructor
      }
      
  4. 什么是多态?编译时多态(重载)和运行时多态(重写)的区别?

    • 多态(Polymorphism):同一个接口可以有不同的实现方式,使得不同的对象可以以统一的方式被处理。

    • 编译时多态(静态多态,Overloading)

      • 在编译阶段确定调用哪个函数;
      • 通过函数重载和运算符重载实现;
      • 依赖于编译器的名称修饰和静态绑定。
      // 函数重载示例
      void print(int i) { std::cout << "Integer: " << i << std::endl; }
      void print(double d) { std::cout << "Double: " << d << std::endl; }void test() {print(5);     // 调用print(int)print(3.14);  // 调用print(double)
      }
      
    • 运行时多态(动态多态,Overriding)

      • 在运行阶段确定调用哪个函数;
      • 通过虚函数和继承实现;
      • 依赖于虚函数表和动态绑定。
      // 运行时多态示例(见问题1)
      
    • 区别

      • 确定时间:编译时多态在编译期确定,运行时多态在运行期确定;
      • 实现方式:编译时多态通过函数重载实现,运行时多态通过虚函数重写实现;
      • 灵活性:运行时多态更灵活,可以处理运行时才确定的对象类型;
      • 性能:编译时多态性能更好,因为不需要运行时查找虚函数表。
  5. 派生类重写基类虚函数的规则(函数名、参数列表、返回值必须相同,override 关键字的作用)?

    • 重写(Override)规则

      • 函数名必须相同:派生类重写的函数名必须与基类的虚函数名完全相同;
      • 参数列表必须相同:派生类重写的函数参数列表(类型、数量、顺序)必须与基类的虚函数完全相同;
      • 返回值类型兼容:派生类重写的函数返回值类型必须与基类的虚函数返回值类型相同,或者是其派生类(协变返回类型,仅适用于指针或引用);
      • 访问权限:派生类重写的函数访问权限可以比基类更宽松,但不能更严格;
      • virtual关键字:派生类重写的函数可以省略virtual关键字,但为了代码清晰,建议保留。
    • override关键字(C++11)

      • 显式标记派生类中的函数是用来重写基类的虚函数;
      • 编译器会检查是否真的重写了基类的虚函数,如果没有则报错;
      • 提高代码可读性,防止意外的函数重载。
      class Base {
      public:virtual void func(int x) {}virtual Base* clone() const { return new Base(*this); }
      };class Derived : public Base {
      public:// 正确的重写void func(int x) override {}// 协变返回类型(返回派生类指针)Derived* clone() const override { return new Derived(*this); }// 错误:参数列表不同,不是重写(编译器会报错)// void func(double x) override {}
      };
      
  6. 静态绑定和动态绑定的介绍(包括静态类型、动态类型)?

    • 静态类型(Static Type):变量声明时的类型,在编译时确定,不能改变。

    • 动态类型(Dynamic Type):指针或引用实际指向的对象的类型,在运行时确定,可以改变。

    • 静态绑定(Static Binding)

      • 函数调用在编译时确定;
      • 用于非虚函数、静态函数、构造函数、析构函数(除非是虚析构函数);
      • 基于变量的静态类型进行绑定。
    • 动态绑定(Dynamic Binding)

      • 函数调用在运行时确定;
      • 仅用于虚函数;
      • 基于变量的动态类型进行绑定。
      class Animal { public: void eat() { std::cout << "Animal eats" << std::endl; } };
      class Dog : public Animal { public: void eat() { std::cout << "Dog eats bones" << std::endl; } };void test() {Animal* animal = new Dog();  // 静态类型:Animal*,动态类型:Dog*animal->eat();  // 静态绑定,调用Animal::eat(),输出"Animal eats"// 如果eat是虚函数// animal->eat();  // 动态绑定,调用Dog::eat(),输出"Dog eats bones"delete animal;
      }
      
  7. 为什么不把所有函数设为虚函数?

    • 性能开销:虚函数需要通过虚函数表和虚指针进行调用,比普通函数调用多了一次间接查找,有额外的性能开销;
    • 内存开销:每个包含虚函数的对象都需要一个虚指针,增加了对象的内存大小;
    • 不需要多态:很多函数不需要被派生类重写,设为虚函数没有意义;
    • 构造函数不能是虚函数:构造函数在对象创建时调用,而虚指针在构造函数执行过程中才被初始化;
    • 静态函数不能是虚函数:静态函数属于类而不是对象,没有this指针。
  8. 构造函数、析构函数、虚函数能否是内联函数?

    • 构造函数:可以是内联函数。内联构造函数可以减少函数调用开销,尤其是在频繁创建小对象的场景。

      class Point {
      private:int x, y;
      public:inline Point(int x_, int y_) : x(x_), y(y_) {}  // 内联构造函数
      };
      
    • 析构函数:可以是内联函数。与构造函数类似,内联析构函数可以减少函数调用开销。虚析构函数也可以是内联的,但只有在非多态调用时才能内联展开。

      class MyClass {
      public:inline ~MyClass() { /* 清理资源 */ }  // 内联析构函数
      };
      
    • 虚函数:可以声明为内联函数,但只有在非多态调用时才能内联展开。当通过基类指针或引用调用虚函数时,由于需要动态绑定,内联优化会被忽略。

      class Base {
      public:inline virtual void func() { std::cout << "Base func" << std::endl; }
      };class Derived : public Base {
      public:inline void func() override { std::cout << "Derived func" << std::endl; }
      };void test() {Derived d;d.func();  // 可能内联展开,调用Derived::func()Base* base = new Derived();base->func();  // 动态绑定,不会内联展开delete base;
      }
      
  9. 虚函数声明为 inline 会有什么问题?

    • 虚函数声明为inline可能会导致以下问题:
      • 内联失效:当通过基类指针或引用调用虚函数时,由于需要动态绑定,编译器无法在编译时确定要调用的具体函数,因此内联优化会失效;
      • 代码膨胀:即使内联有效,虚函数的实现在多个翻译单元中展开也可能导致代码膨胀;
      • 误导性:声明为inline可能会让开发者误认为函数总是内联的,但实际上在多态调用时内联会失效;
      • 调试困难:内联函数在调试时可能难以设置断点。
    • 注意:虚函数声明为inline本身不是错误,但需要了解其局限性。在不需要多态调用的场景下,内联虚函数可以提高性能;在需要多态调用的场景下,内联优化会被忽略。

2.5 继承与其他

  1. 菱形继承(多继承时基类被多次继承)的问题?如何用虚继承解决?

    • 菱形继承(Diamond Inheritance)

      • 指的是一个派生类同时继承自两个基类,而这两个基类又共同继承自一个公共基类的情况,形成菱形结构。
      • 问题:派生类会包含公共基类的两个副本,导致二义性和数据冗余。
      class Base { public: int value; };
      class Derived1 : public Base { /* ... */ };
      class Derived2 : public Base { /* ... */ };
      class Final : public Derived1, public Derived2 { /* ... */ };void test() {Final f;// f.value = 10;  // 错误:二义性,不知道是Derived1::value还是Derived2::valuef.Derived1::value = 10;  // 必须显式指定作用域f.Derived2::value = 20;
      }
      
    • 虚继承(Virtual Inheritance)

      • 解决菱形继承问题的方法,通过在继承声明中添加virtual关键字,使得公共基类在最终派生类中只保留一个副本。
      • 虚继承会增加额外的存储开销和访问开销,但解决了二义性和数据冗余问题。
      class Base { public: int value; };
      class Derived1 : virtual public Base { /* ... */ };
      class Derived2 : virtual public Base { /* ... */ };
      class Final : public Derived1, public Derived2 { /* ... */ };void test() {Final f;f.value = 10;  // 正确:没有二义性,Base只存在一个副本
      }
      
  2. this 指针的作用?this 指针能否为 nullptr?

    • this指针

      • 是一个隐含的指针,指向当前对象的地址;
      • 在非静态成员函数内部使用,可以访问当前对象的成员;
      • 不是对象的一部分,不占用对象的内存空间;
      • 类型为Class* const(在非const成员函数中)或const Class* const(在const成员函数中)。
      class Person {
      private:std::string name;
      public:void setName(const std::string& name) {this->name = name;  // this指针指向当前对象,区分成员变量和参数}Person* getThis() {return this;  // 返回当前对象的指针}
      };
      
    • this指针能否为nullptr

      • 在正常情况下,this指针不会为nullptr,因为成员函数只能通过对象调用,而对象必须有有效的地址;
      • 如果通过空指针调用成员函数,且该函数访问了成员变量或调用了虚函数,会导致未定义行为(通常是程序崩溃);
      • 如果成员函数没有访问任何成员变量,也没有调用虚函数,通过空指针调用该函数可能不会立即崩溃,但这是一种错误的用法,应避免。
      class Test {
      public:void func1() { std::cout << "Hello" << std::endl; }void func2() { std::cout << value << std::endl; }int value;
      };void test() {Test* p = nullptr;p->func1();  // 可能不会崩溃,但行为未定义p->func2();  // 会崩溃,因为访问了成员变量
      }
      
  3. 类对象的大小受哪些因素影响?

    • 类对象的大小主要受以下因素影响:

      • 成员变量:对象中包含的所有非静态成员变量的大小之和(考虑内存对齐);
      • 虚函数:如果类包含虚函数,对象中会有一个指向虚函数表的指针(vptr),增加4或8字节(取决于系统位数);
      • 虚继承:如果类虚继承自其他类,对象中会有虚基类指针(vbase ptr),增加额外的指针大小;
      • 内存对齐:编译器为了优化访问速度,会对成员变量进行内存对齐,可能导致对象大小大于成员变量大小之和;
      • 空类:C++中空类的大小为1字节,用于区分不同的对象实例。
      // 示例:不同类对象的大小
      class Empty { };
      // sizeof(Empty) = 1class Simple { int x; char c; };
      // sizeof(Simple) = 8(假设int为4字节,char为1字节,内存对齐到4字节)class WithVirtual { virtual void func(); int x; };
      // sizeof(WithVirtual) = 12(虚指针8字节 + int4字节,假设64位系统)
      
  4. 匿名对象的特点和使用场景?

    • 匿名对象(Anonymous Object)

      • 没有名称的临时对象,生命周期仅限于创建它的表达式;
      • 语法:ClassName(参数列表)
      • 特点:创建后立即使用,使用完后立即销毁。
    • 使用场景

      • 临时计算:需要一个临时对象进行计算,但不需要保留结果;
      • 函数参数:直接作为函数参数传递,避免创建命名对象;
      • 函数返回值:函数返回一个临时对象;
      • 链式调用:用于实现链式调用(如流操作符<<)。
      class Calculator {
      public:int add(int a, int b) { return a + b; }void printResult(int result) { std::cout << "Result: " << result << std::endl; }
      };void test() {// 临时计算int sum = Calculator().add(5, 3);// 作为函数参数Calculator c;c.printResult(Calculator().add(10, 20));// 链式调用std::cout << "Hello" << std::endl;  // std::cout << "Hello"返回std::cout的引用
      }
      
http://www.xdnf.cn/news/18328.html

相关文章:

  • EPM240T100I5N Altera FPGA MAX II CPLD
  • 深度学习-167-MCP技术之工具函数的设计及注册到MCP服务器的两种方式
  • TensorFlow 面试题及详细答案 120道(11-20)-- 操作与数据处理
  • 【Linux】文件系统
  • 前端面试核心技术30问
  • 《C++进阶之STL》【二叉搜索树】
  • 神经网络中的那些关键设计:从输入输出到参数更新
  • Python 函数进阶:深入理解参数、装饰器与函数式编程
  • Java 大视界 -- Java 大数据在智能物流无人配送车路径规划与协同调度中的应用
  • 暴雨中的“天眼”:天通哨兵PS02卫星图传系统筑牢防汛安全网
  • 前端面试题1
  • 边缘智能体:Go编译在医疗IoT设备端运行轻量AI模型(上)
  • Springboot使用Selenium+ChormeDriver在服务器(Linux)端将网页保存为图片或PDF
  • Rust学习笔记(七)|错误处理
  • 0819 使用IP多路复用实现TCP并发服务器
  • 反向代理实现服务器联网
  • Auto-CoT:大型语言模型的自动化思维链提示技术
  • 微服务-08.微服务拆分-拆分商品服务
  • 深度学习环境搭建Windows+ TensorFlow 2.6.0 GPU 版
  • 亚矩阵云手机智能定位:助力Snapchat矩阵账号的本地化内容运营穿透技术
  • Apache IoTDB(4):深度解析时序数据库 IoTDB 在Kubernetes 集群中的部署与实践指南
  • 连接远程服务器上的 jupyter notebook,解放本地电脑
  • VSCode 从安装到精通:下载安装与快捷键全指南
  • 11.第11章 开发环境优化
  • 【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day7
  • Nacos-6--Naco的QUIC协议实现高可用的工作原理
  • 2025年- H98-Lc206--51.N皇后(回溯)--Java版
  • ARM架构下的cache transient allocation hint以及SMMUv2的TRANSIENTCFG配置详解
  • EasyExcel篇
  • OVS:ovn为什么默认选择Geneve作为二层隧道网络协议?