C++常见面试题-2.C++类相关
C++ 面试问题整理 - C++类相关
二、C++ 类相关
2.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;
-
-
什么是面向对象(OOP)?面向对象的意义是什么?
- 面向对象(OOP):一种编程范式,将现实世界中的实体抽象为程序中的对象,每个对象包含数据(属性)和行为(方法)。
- 面向对象的意义:
- 代码复用:通过继承和组合实现代码复用;
- 封装性:隐藏内部实现细节,提高安全性;
- 可维护性:模块化设计,便于代码维护和扩展;
- 灵活性:通过多态实现运行时的动态行为;
- 更好的模型化:更接近现实世界的问题建模方式。
-
封装、继承、多态的详细解释(包括继承方式的区别:公有继承、私有继承、保护继承)?
-
封装:见问题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)。每个包含虚函数的类都有一个虚函数表,每个对象内部都有一个指向虚函数表的指针。
-
-
类的访问权限(public、protected、private)的区别?派生类对基类成员的访问规则?
- 访问权限:
- public:可以被任何代码访问(类内部、派生类、外部代码)。
- protected:可以被类内部和派生类访问,但不能被外部代码访问。
- private:只能被类内部访问,不能被派生类和外部代码访问。
- 派生类对基类成员的访问规则:
- 公有继承:基类的public成员在派生类中为public,protected成员在派生类中为protected,private成员不可访问。
- 保护继承:基类的public和protected成员在派生类中为protected,private成员不可访问。
- 私有继承:基类的public和protected成员在派生类中为private,private成员不可访问。
- 注意:无论哪种继承方式,派生类都不能直接访问基类的私有成员,但可以通过基类的public或protected方法间接访问。
- 访问权限:
-
什么是组合和聚合?与继承相比,它们的优缺点(如 “组合优于继承” 的设计原则)?
-
组合(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 构造函数与析构函数
-
构造函数和析构函数的作用?它们的调用时机和顺序(如派生类与基类的构造 / 析构顺序)?
-
构造函数(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; }
-
-
拷贝构造函数的作用?何时会被调用(如对象赋值、作为函数参数传递、函数返回对象)?
-
拷贝构造函数(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(); // 可能调用拷贝构造函数(取决于编译器优化) }
-
-
浅拷贝和深拷贝的区别?如何实现深拷贝?
-
浅拷贝(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;} };
-
-
赋值运算符重载与拷贝构造函数的区别?
- 拷贝构造函数:
- 用于创建一个新对象,使其成为现有对象的副本;
- 语法:
Class(const Class& other);
; - 调用时机:用一个对象初始化另一个新对象时。
- 赋值运算符重载:
- 用于将一个对象的内容赋值给另一个已存在的对象;
- 语法:
Class& operator=(const Class& other);
; - 调用时机:两个已存在的对象之间进行赋值操作时。
- 主要区别:
- 拷贝构造函数创建新对象,赋值运算符操作已存在的对象;
- 赋值运算符需要处理自我赋值的情况,拷贝构造函数不需要;
- 赋值运算符需要释放目标对象原有的资源,拷贝构造函数不需要。
- 拷贝构造函数:
-
C++ 空类默认有哪些成员函数?
- C++空类(没有显式定义任何成员的类)在编译时会默认生成以下6个成员函数:
- 默认构造函数(Default Constructor):
Class();
- 默认析构函数(Default Destructor):
~Class();
- 默认拷贝构造函数(Default Copy Constructor):
Class(const Class&);
- 默认拷贝赋值运算符(Default Copy Assignment Operator):
Class& operator=(const Class&);
- 默认移动构造函数(Default Move Constructor,C++11):
Class(Class&&);
- 默认移动赋值运算符(Default Move Assignment Operator,C++11):
Class& operator=(Class&&);
- 默认构造函数(Default Constructor):
- 注意:如果显式定义了任何一个构造函数,编译器就不会生成默认构造函数。同样,如果显式定义了拷贝构造函数、拷贝赋值运算符、析构函数中的任意一个,编译器可能不会生成移动构造函数和移动赋值运算符(C++11规则)。
- C++空类(没有显式定义任何成员的类)在编译时会默认生成以下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 静态成员与友元
-
类的静态成员(静态变量、静态函数)的特点?静态函数能否访问非静态成员?
-
静态变量(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指针来访问。
-
-
友元函数和友元类的作用?为何友元会破坏封装性但仍被使用?
-
友元函数(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 虚函数与多态
-
虚函数的作用?其实现原理(虚函数表、虚指针)?
-
虚函数(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):每个包含虚函数的对象内部都有一个指向其所属类虚函数表的指针。
- 多态实现过程:
- 编译器在编译时为每个包含虚函数的类生成一个虚函数表;
- 每个对象在创建时,其内部的虚指针被初始化为指向该类的虚函数表;
- 当通过基类指针或引用调用虚函数时,程序会通过虚指针找到虚函数表,然后根据虚函数表中的地址调用相应的函数(派生类的重写版本)。
-
-
纯虚函数和抽象类的定义?抽象类能否实例化?
-
纯虚函数(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; }
-
-
虚析构函数的作用?为何基类析构函数通常需要声明为虚函数?
-
虚析构函数(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 }
-
-
什么是多态?编译时多态(重载)和运行时多态(重写)的区别?
-
多态(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)
-
区别:
- 确定时间:编译时多态在编译期确定,运行时多态在运行期确定;
- 实现方式:编译时多态通过函数重载实现,运行时多态通过虚函数重写实现;
- 灵活性:运行时多态更灵活,可以处理运行时才确定的对象类型;
- 性能:编译时多态性能更好,因为不需要运行时查找虚函数表。
-
-
派生类重写基类虚函数的规则(函数名、参数列表、返回值必须相同,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 {} };
-
-
静态绑定和动态绑定的介绍(包括静态类型、动态类型)?
-
静态类型(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; }
-
-
为什么不把所有函数设为虚函数?
- 性能开销:虚函数需要通过虚函数表和虚指针进行调用,比普通函数调用多了一次间接查找,有额外的性能开销;
- 内存开销:每个包含虚函数的对象都需要一个虚指针,增加了对象的内存大小;
- 不需要多态:很多函数不需要被派生类重写,设为虚函数没有意义;
- 构造函数不能是虚函数:构造函数在对象创建时调用,而虚指针在构造函数执行过程中才被初始化;
- 静态函数不能是虚函数:静态函数属于类而不是对象,没有this指针。
-
构造函数、析构函数、虚函数能否是内联函数?
-
构造函数:可以是内联函数。内联构造函数可以减少函数调用开销,尤其是在频繁创建小对象的场景。
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; }
-
-
虚函数声明为 inline 会有什么问题?
- 虚函数声明为inline可能会导致以下问题:
- 内联失效:当通过基类指针或引用调用虚函数时,由于需要动态绑定,编译器无法在编译时确定要调用的具体函数,因此内联优化会失效;
- 代码膨胀:即使内联有效,虚函数的实现在多个翻译单元中展开也可能导致代码膨胀;
- 误导性:声明为inline可能会让开发者误认为函数总是内联的,但实际上在多态调用时内联会失效;
- 调试困难:内联函数在调试时可能难以设置断点。
- 注意:虚函数声明为inline本身不是错误,但需要了解其局限性。在不需要多态调用的场景下,内联虚函数可以提高性能;在需要多态调用的场景下,内联优化会被忽略。
- 虚函数声明为inline可能会导致以下问题:
2.5 继承与其他
-
菱形继承(多继承时基类被多次继承)的问题?如何用虚继承解决?
-
菱形继承(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只存在一个副本 }
-
-
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(); // 会崩溃,因为访问了成员变量 }
-
-
类对象的大小受哪些因素影响?
-
类对象的大小主要受以下因素影响:
- 成员变量:对象中包含的所有非静态成员变量的大小之和(考虑内存对齐);
- 虚函数:如果类包含虚函数,对象中会有一个指向虚函数表的指针(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位系统)
-
-
匿名对象的特点和使用场景?
-
匿名对象(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的引用 }
-