✨️ 复制成功,转载请标注本文地址

类相关面试题

4402 字
22 分钟
类相关面试题

什么是默认构造#

默认构造就是不带参数的构造函数,如果我们不实现任何构造函数,系统会为我们生成一个默认的构造函数

比如下面

#include <thread>
class JoiningThread {
public:
int GetIndex() const { return _i; }
private:
std::thread _t;
int _i;
};

所以我们可以直接使用默认构造函数构造一个对象,并且打印成员_i

//测试默认合成
JoiningThread jt;
std::cout << "member _i is " << jt.GetIndex() << std::endl;

输出

Terminal window
member _i is -1284874240

可以看到默认构造函数并不会帮我们初始化类成员变量。

什么是有参构造#

有参构造就是传递参数的构造函数,可以根据参数构造对象

#include <thread>
class JoiningThread {
public:
JoiningThread(int i) : _i{i} {}
int GetIndex() const { return _i; }
private:
std::thread _t;
int _i;
};

我们可以通过如下方式构造

JoiningThread jt(1);
std::cout << "member _i is " << jt.GetIndex() << std::endl;

当我们执行程序,会输出

Terminal window
member _i is 1

但如果这样构造会产生问题

JoiningThread jt;
std::cout << "member _i is " << jt.GetIndex() << std::endl;

注意

如果我们实现了参数构造而不实现无参构造,系统将不会为我们实现默认构造,导致无法使用默认构造生成对象。

所以稳妥一点,我们基本都会实现无参构造

#include <thread>
class JoiningThread {
public:
JoiningThread() :_i(0){}
JoiningThread(int i) : _i{i} {}
int GetIndex() const { return _i; }
private:
std::thread _t;
int _i;
};

拷贝构造函数是什么#

回答要点:

  • 定义:拷贝构造函数用于创建一个对象,该对象是通过复制另一个同类型对象来初始化的。
  • 调用时机:
    • 使用现有对象初始化新对象。
    • 按值传递对象作为函数参数。
    • 按值返回对象。
  • 默认拷贝构造函数:成员逐个拷贝。

示例:

class MyClass {
public:
MyClass(const MyClass& other) { // 拷贝构造函数
// 复制代码
}
};

是否会默认生成拷贝构造#

在 C++ 中,如果你没有为一个类显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。这个默认拷贝构造函数会按成员的逐个拷贝(member-wise copy)方式来复制对象的每个成员变量。

默认拷贝构造函数的行为#

  1. 逐个拷贝:默认拷贝构造函数会逐个拷贝所有的非静态成员变量。
  2. 指针成员:如果类中有指针成员,默认拷贝构造函数只会拷贝指针的值(地址),而不会拷贝指针所指向的对象。这可能会导致多个对象指向同一块内存,进而引发问题(如双重释放、内存泄漏等)。
  3. const** 和**引用成员:如果类中有 const 成员或引用成员,编译器不会生成默认的拷贝构造函数,因为这些成员不能被复制。
  4. 类中包含不可拷贝对象时,无法合成默认拷贝构造函数

拷贝构造是否必须实现#

当一个类A中有成员变量是另一个类类型B的时候,有时候拷贝构造会失效。

比如一个类JoiningThread中有成员变量std::threadstd::thread没有构造函数,所以A类的拷贝构造无法合成,需要显示编写。

比如我们这样调用

JoiningThread jt(1);
JoiningThread jt2(jt);
std::cout << "member _i is " << jt.GetIndex() << std::endl;

上面代码报错

error: use of deleted function 'std::thread::thread(const std::thread&)'

所以我们要显示实现拷贝构造,指定一个拷贝规则

JoiningThread(const JoiningThread & other): _i(other._i){}

什么是浅拷贝和深拷贝#

类在拷贝构造或者拷贝赋值的时候,将被拷贝的类中的成员值拷贝到目的类,如果被拷贝的类中包含指针成员,只是简单的拷贝指针的值。

同样析构也要显示编写,等待线程完成。

除此之外我们可以自己实现拷贝构造,进而实现浅拷贝和深拷贝的不同效果

浅拷贝是指对象复制时,仅复制指针的值(地址),而不复制指针所指向的实际数据。结果是两个对象的指针成员指向同一块堆内存

深拷贝是指对象复制时,为指针成员重新分配内存,并将原对象指向的数据内容逐字节复制到新分配的内存中。结果是两个对象的指针成员指向各自独立的两块内存,数据内容相同但互不影响。

构造顺序和析构顺序#

类A中包含成员变量是类B的类型,如果是先调用A的构造还是B的构造呢?

如果析构的时候是A先析构还是B先析构呢?

class InnerB {
public:
InnerB() {
std::cout << "InnerB()" << std::endl;
}
~InnerB(){
std::cout << "~InnerB()" << std::endl;
}
};
class WrapperC {
public:
WrapperC(){
std::cout << "WrapperC()" << std::endl;
}
~WrapperC(){
std::cout << "~WrapperC()" << std::endl;
}
InnerB _inner;
};

执行结果,先调用B的构造,在调用C的构造。

析构时,先析构C再析构B

InnerB()
WrapperC()
~WrapperC()
~InnerB()

类默认构造是否必须实现#

如果类中有继承关系或者其他类型的成员,默认构造函数是很有必要实现的。

系统提供的合成的默认构造函数不会对成员做初始化操作。

比如我们之后要学到的继承

class DerivedA: public BaseA {
public:
DerivedA(std::string name,std::string num) :
BaseA(name), _num(num) {
std::cout << "DerivedA()" << std::endl;
}
private:
std::string _num;
};

调用

DerivedA a("zack","1001");

this 指针的特性和用途#

  1. 指向当前对象
    • this 指针是一个隐式参数,指向调用成员函数的对象。通过 this,你可以访问当前对象的属性和方法。
  2. 区分成员变量和参数
    • 在构造函数或成员函数中,参数名和成员变量可能同名。使用
this

可以明确指代成员变量。例如:

class MyClass {
private:
int value;
public:
MyClass(int value) {
this->value = value; // 使用 this 指针区分成员变量和参数
}
};
  1. 返回当前对象
this

可以用于返回当前对象的引用,以支持链式调用。例如:

class MyClass {
private:
int value;
public:
MyClass& setValue(int value) {
this->value = value;
return *this; // 返回当前对象的引用
}
};
MyClass obj;
obj.setValue(10).setValue(20); // 链式调用
  1. 在 const 成员函数中的使用
    • const 成员函数中,this 的类型为 const MyClass*,这意味着你不能通过 this 修改成员变量。这有助于确保对象的状态不被改变。
  2. 在静态成员函数中的不可用性
    • 静态成员函数没有 this 指针,因为它们不属于任何特定对象,而是属于类本身。因此,静态成员函数不能访问非静态成员变量和成员函数。

示例代码

以下是一个简单的示例,展示了 this 指针的用法:

#include <iostream>
class MyClass {
private:
int value;
public:
// 构造函数
MyClass(int value) {
this->value = value; // 使用 this 指针区分成员变量和参数
}
// 成员函数
MyClass& setValue(int value) {
this->value = value; // 使用 this 指针
return *this; // 返回当前对象的引用
}
// 输出值
void printValue() const {
std::cout << "Value: " << this->value << std::endl; // 使用 this 指针
}
};
int main() {
MyClass obj(10);
obj.printValue(); // 输出: Value: 10
obj.setValue(20).printValue(); // 链式调用,输出: Value: 20
return 0;
}

delete和default#

C++11用法:

delete可以删除指定的构造函数。

default可以指定某个构造函数为系统默认合成。

class DefaultClass {
public:
DefaultClass() = default;
~DefaultClass() = default;
DefaultClass(const DefaultClass &) = delete;
DefaultClass &operator=(const DefaultClass &) = delete;
friend std::ostream& operator << (std::ostream &out, const DefaultClass &defaultClass);
private:
int _num ;
};

主函数中调用

DefaultClass b;
std::cout << b << std::endl;

输出num是一个随机数

Terminal window
DefaultClass num is 331

什么是移动构造函数?与拷贝构造函数有何不同?#

回答要点:

  • 定义:移动构造函数用于通过“移动”资源来初始化对象,而不是复制资源。
  • 语法:使用右值引用作为参数 (MyClass(MyClass&& other)).
  • 优势:
    • 性能更高,避免不必要的深拷贝。
    • 适用于临时对象。
  • 区别:
    • 拷贝构造函数复制资源,移动构造函数转移资源所有权。

示例:

class MyClass {
public:
MyClass(MyClass&& other) noexcept { // 移动构造函数
// 移动资源
}
};

默认构造函数和用户定义的构造函数有什么区别?#

回答要点:

  • 默认构造函数:
    • 无参数的构造函数。
    • 如果没有用户定义的构造函数,编译器会自动生成一个默认构造函数。
  • 用户定义的构造函数:
    • 开发者自定义的构造函数,可以有参数。
    • 一旦定义了任何构造函数,编译器不会再生成默认构造函数

示例:

class MyClass {
public:
MyClass() { // 默认构造函数
// 初始化代码
}
MyClass(int x) { // 有参数的构造函数
// 初始化代码
}
};

什么是初始化列表?为什么在构造函数中使用它?#

回答要点:

  • 定义:初始化列表是在构造函数的参数列表之后,函数体之前,用于初始化成员变量的语法。
  • 优点:
    • 提高性能,特别是对于常量成员或引用成员。
    • 必须用于初始化常量成员、引用成员以及基类。
    • 避免对象先默认构造再赋值,减少不必要的操作。

示例:

class MyClass {
string x;
string y;
public:
MyClass(string a, string b):x(a),y(b) { // 初始化列表
// 其他初始化代码
}
};

什么是虚析构函数?为什么需要它?#

回答要点:

  • 定义:在基类中将析构函数声明为virtual,以确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数。
  • 用途:
    • 防止内存泄漏。
    • 确保派生类的资源被正确释放。
  • 不使用虚析构函数的风险:
    • 仅调用基类析构函数,导致派生类资源未释放。

示例:

如果BaseA的析构不写成虚析构,则主函数开辟子类对象赋值给基类指针,以后delete基类指针的时候会发现没有析构子类

class BaseA{
public:
BaseA(std::string name):_name(name){
std::cout << "BaseA()" << std::endl;
}
~BaseA(){
std::cout << "~BaseA()" << std::endl;
}
private:
std::string _name;
};
class DerivedA: public BaseA {
public:
DerivedA(std::string name,std::string num) :
BaseA(name), _num(num) {
std::cout << "DerivedA()" << std::endl;
}
~DerivedA(){
std::cout << "~DerivedA()" << std::endl;
}
private:
std::string _num;
};

主函数回收内存

BaseA* base = new DerivedA("zack","1002");
delete base;

会看到只调用了基类BaseA的析构函数。

当BaseA的析构改为虚析构的时候,才会回收子类DerivedA

class BaseA{
public:
BaseA(std::string name):_name(name){
std::cout << "BaseA()" << std::endl;
}
virtual ~BaseA(){
std::cout << "~BaseA()" << std::endl;
}
private:
std::string _name;
};

什么是委托构造函数?它是如何工作的?(C++11引入的特性)#

回答要点:

  • 定义:一个构造函数可以调用同一类中的另一个构造函数,从而委托初始化任务。
  • 优点:
    • 避免代码重复,提升代码可维护性。
  • 语法:使用构造函数初始化列表中的类名和参数。

示例:

class MyClass {
int x;
int y;
public:
MyClass() : MyClass(0, 0) { } // 委托构造函数
MyClass(int a, int b) : x(a), y(b) { }
};

什么是析构函数的顺序?#

回答要点:

  • 成员变量的析构顺序:按照声明的逆序析构。
  • 继承关系的析构顺序:先析构派生类的成员和资源,再析构基类。
  • 全局/静态对象:按照创建的逆序析构。

示例说明:

class Base {
public:
~Base() { cout << "Base析构\n"; }
};
class Derived : public Base {
public:
~Derived() { cout << "Derived析构\n"; }
};
int main() {
Derived obj;
// 当obj被销毁时,首先调用Derived的析构函数,然后调用Base的析构函数。
}

输出:

Derived析构
Base析构

如何防止对象被复制?#

回答要点:

  • C++11及以上:使用delete关键字显式删除拷贝构造函数和拷贝赋值运算符。
  • C++11之前:将拷贝构造函数和拷贝赋值运算符声明为私有且不实现。

示例(C++11及以上):

class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // 禁止拷贝构造
NonCopyable& operator=(const NonCopyable&) = delete; // 禁止拷贝赋值
};

构造函数中抛出异常会发生什么?#

回答要点:

  • 对象未完全构造:如果构造函数中抛出异常,析构函数不会被调用,因为对象尚未完全构造。
  • 资源泄漏风险:如果在构造函数中分配了资源,需使用RAII(资源获取即初始化)类或智能指针来确保资源被正确释放。
  • 异常安全:确保在构造函数抛出异常时,任何已经初始化的成员都会被正确析构。

示例:

class MyClass {
std::vector<int> data;
public:
MyClass() {
data.reserve(100);
if (/* some condition */) {
throw std::runtime_error("构造函数异常");
}
}
};

解释RAII及其与构造函数、析构函数的关系#

回答要点:

  • RAII(资源获取即初始化):
    • 编程范式,通过对象的生命周期管理资源。
    • 资源在对象构造时获取,在对象析构时释放。
  • 关系:
    • 构造函数负责获取资源。
    • 析构函数负责释放资源。
  • 优点:
    • 自动管理资源,防止内存泄漏。
    • 异常安全,确保资源在异常发生时被释放。

示例:

class FileHandler {
FILE* file;
public:
FileHandler(const char* filename) {
file = fopen(filename, "r");
if (!file) throw std::runtime_error("打开文件失败");
}
~FileHandler() {
if (file) fclose(file);
}
// 禁止拷贝和移动
FileHandler(const FileHandler&) = delete;
FileHandler& operator=(const FileHandler&) = delete;
};

解释什么是赋值运算符重载?与拷贝构造函数有何不同?#

回答要点:

  • 赋值运算符重载:通过重载operator=,定义对象之间的赋值行为。
  • 区别与拷贝构造函数:
    • 拷贝构造函数用于初始化新对象。
    • 赋值运算符用于将一个已存在的对象赋值给另一个已存在的对象。
  • 常规实现:
    • 检查自赋值。
    • 释放已有资源。
    • 复制资源。
    • 返回*this

示例:

class MyClass {
int* data;
public:
MyClass& operator=(const MyClass& other) { // 赋值运算符重载
if (this == &other) return *this; // 自赋值检查
delete data; // 释放已有资源
data = new int(*other.data); // 复制资源
return *this;
}
};

解释静态成员变量在构造和析构中的处理#

回答要点:

  • 静态成员变量:
    • 属于类本身,而非任何对象实例。
    • 在程序开始时初始化,在程序结束时析构。
  • 构造顺序:
    • 单例模式中,静态成员在第一次使用时构造。
  • 析构顺序:
    • 按逆序构造顺序析构,确保依赖关系被正确处理。

示例:

class MyClass {
public:
static MyClass instance;
MyClass() { cout << "构造\n"; }
~MyClass() { cout << "析构\n"; }
};
//放在cpp中
MyClass MyClass::instance; // 静态成员变量定义

输出:

构造
析构

虚函数原理#

核心机制:虚函数表(vtable)+ 虚表指针(vptr)

C++ 通过虚函数表(Virtual Table)和虚表指针(Virtual Pointer)实现运行时多态。

虚继承与菱形继承问题#

#include <iostream>
#include <string>
// 基类 Device
class Device {
public:
std::string brand;
Device(const std::string& brand_) : brand(brand_) {}
void showBrand() const {
std::cout << "Brand: " << brand << std::endl;
}
};
// 派生类 Laptop,虚继承 Device
class Laptop : virtual public Device {
public:
Laptop(const std::string& brand_) : Device(brand_) {}
};
// 派生类 Tablet,虚继承 Device
class Tablet : virtual public Device {
public:
Tablet(const std::string& brand_) : Device(brand_) {}
};
// 派生类 Convertible
class Convertible : public Laptop, public Tablet {
public:
Convertible(const std::string& brand_) : Device(brand_), Laptop(brand_), Tablet(brand_) {}
};
int main() {
Convertible c("TechBrand");
c.showBrand();
return 0;
}

输出:

Brand: TechBrand

解析:

  • 在无虚继承的情况下,Convertible 类将拥有两份 Device 的成员变量,这会导致二义性问题。
  • 通过使用虚继承(virtual public),确保 Convertible 类只有一份 Device 的成员。
  • Convertible 的构造函数中,需要明确调用基类 Device 的构造函数,避免二义性。
  • main 函数中,创建一个 Convertible 对象,并调用 showBrand() 函数,正确显示品牌名称。

注意事项:

  • 菱形继承(多重继承导致的重复基类)可以通过虚继承来解决,确保共享同一份基类成员。
  • 虚继承会增加一定的开销,需根据具体需求权衡使用。

协变返回类型#

概念定义

  • 在 C++ 中,协变返回类型(Covariant Return Types)是一个与函数重写(Override)相关的概念。当一个派生类重写基类的虚函数时,协变返回类型允许派生类的函数返回一个从基类函数返回类型派生而来的类型。
#include <iostream>
// 基类
class Base {
public:
virtual Base* clone() const {
std::cout << "Base cloned." << std::endl;
return new Base(*this);
}
virtual ~Base() {}
};
// 派生类
class Derived : public Base {
public:
Derived* clone() const override { // 协变返回类型
std::cout << "Derived cloned." << std::endl;
return new Derived(*this);
}
};
int main() {
Base* b = new Base();
Base* d = new Derived();
Base* bClone = b->clone(); // 输出: Base cloned.
Base* dClone = d->clone(); // 输出: Derived cloned.
delete b;
delete d;
delete bClone;
delete dClone;
return 0;
}

输出:

Base cloned.
Derived cloned.

解析:

  • 基类 Base 定义了一个虚函数 clone(),返回 Base* 类型的指针。
  • 派生类 Derived 重写了 clone() 函数,返回类型为 Derived*,这是一种协变返回类型。
main

函数中,通过基类指针调用

clone()

函数:

- 对于 `Base` 对象,调用 `Base::clone()`。
- 对于 `Derived` 对象,由于虚函数机制,调用 `Derived::clone()`。
  • 协变返回类型允许派生类的重写函数返回更具体的类型,增强类型安全性和代码可读性。

注意事项:

  • 协变返回类型必须满足派生类返回类型是基类返回类型的派生类。
  • 编译器会检查协变返回类型的正确性,确保类型安全。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

类相关面试题
https://godze.online/posts/c-plus-plus/类相关免面试题/
作者
Vafail
发布于
2026-05-20
许可协议
CC BY-NC-SA 4.0

评论区

公告
欢迎来到我的博客!
分类
标签
最新动态
站点统计
文章
61
分类
6
标签
8
总字数
136,467
运行时长
0
最后活动
0 天前
站点信息
构建平台
Vercel
博客版本
Firefly v6.15.3
文章许可
CC BY-NC-SA 4.0