C++ 面试必问:构造函数和析构函数的那些你不知道的事!

哈喽大家好,我是小康!

前言:构造函数和析构函数,真的有那么神秘?

初学 C++ 的朋友们可能都经历过这样的困惑:
“构造函数到底有什么用?析构函数又是什么操作?”
“为什么对象一创建就会调用构造函数,销毁时又要调用析构函数?”
“手写一个构造函数太简单,但大多数时候我觉得好像没啥用啊!”

别急!这篇文章就是为了解开你心中的迷雾,用 最简单的大白话,把构造函数和析构函数的秘密说透,顺便告诉你它们的 隐藏用法和坑。看完这篇文章,保你不再害怕面试官的灵魂拷问:“说说 C++ 的构造函数和析构函数的作用。”

微信搜索 【跟着小康学编程】,关注我,定期分享计算机编程硬核技术文章。

一、什么是构造函数和析构函数?

构造函数:对象诞生的“接生员”

构造函数(Constructor)是 C++ 对象创建时由 编译器自动调用 的一个特殊函数。它的主要职责是:初始化对象,让对象一出生就拥有良好的“初始状态”。

它的特点:

  • 名字和类名完全一样。
  • 没有返回值(哪怕是 void 都不行)。
  • 可以被重载(也就是说,一个类可以有多个构造函数,但参数不同)。

举个栗子🌰:

假如你在现实生活中领养了一只宠物,你肯定希望它出生后就有名字、有颜色、有健康值,这就是构造函数的作用。

来看看代码怎么写?

#include <iostream>
using namespace std;

class Pet {
public:
    string name;
    string color;
    int health;

    // 构造函数
    Pet(string n, string c, int h) : name(n), color(c), health(h) {
        cout << "宠物 " << name << " 出生啦!" << endl;
    }
};

int main() {
    Pet myPet("小白", "白色", 100);
    cout << "宠物信息:" << myPet.name << ", " << myPet.color << ", 健康值:" << myPet.health << endl;
    return 0;
}

运行结果:

宠物 小白 出生啦!
宠物信息:小白, 白色, 健康值:100

析构函数:对象“退场”的清道夫

析构函数(Destructor)则是对象销毁时 编译器自动调用 的特殊函数。它的主要职责是:清理资源,比如释放内存、关闭文件、归还连接池资源等等。

它的特点:

  • 名字是类名前面加个波浪号 ~
  • 没有参数,也没有返回值。
  • 一个类只能有一个析构函数,不能重载。

还是宠物的例子,当小白 寿终正寝时,我们就需要一个析构函数来负责清理它占用的资源。

class Pet {
public:
    string name;

    Pet(string n) : name(n) {
        cout << "宠物 " << name << " 出生啦!" << endl;
    }

    ~Pet() {
        cout << "宠物 " << name << " 离开了这个世界……" << endl;
    }
};

int main() {
    Pet myPet("小白");
    return 0;
}

运行结果:

宠物 小白 出生啦!
宠物 小白 离开了这个世界……

二、构造函数和析构函数,咋就这么重要?

1. 没有构造函数,麻烦事多了去了

想象一下,如果没有构造函数,你的对象初始化全靠手写:

Pet p;
p.name = "小白";
p.color = "白色";
p.health = 100;

这样写不仅繁琐,还容易漏掉某些属性,导致对象的状态不完整。

有了构造函数后

Pet p("小白", "白色", 100); // 看起来多清晰啊!

是不是一键搞定?构造函数让对象从“出生”就带着完整状态!

2. 没有析构函数,资源管理会出问题

在 C++ 中,我们经常会用到动态内存分配(比如 new),如果你忘记释放内存,就会发生内存泄漏。

有了析构函数后:

#include <iostream>
using namespace std;

class Pet {
private:
    int* healthData; // 动态分配的资源
public:
    Pet() {
        healthData = new int[100];
        cout << "宠物出生啦!分配了健康数据内存。" << endl;
    }

    ~Pet() {
        delete[] healthData;
        cout << "宠物离开啦!释放了健康数据内存。" << endl;
    }
};

int main() {
    Pet myPet; // 构造函数分配内存
    return 0;  // 析构函数释放内存
}

运行结果

宠物出生啦!分配了健康数据内存。
宠物离开啦!释放了健康数据内存。

总结: 析构函数是对象的“清洁工”,帮你收拾资源。它的作用是保证对象销毁时释放动态分配的资源,避免内存泄漏。

三、那些你必须知道的构造与析构细节

1. 构造函数的默认实现

如果你没有写构造函数,编译器会默认生成一个“无参构造函数”。这个默认构造函数啥都不干,只是“凑个数”,确保你的对象可以被创建。

但是,一旦你自己写了一个带参数的构造函数,编译器就不会再帮你生成默认的无参构造函数了。如果你还想要无参构造函数,就需要自己写。

例子:

#include <iostream>
using namespace std;

class Pet {
public:
    string name;

    // 带参数的构造函数
    Pet(string n) : name(n) {
        cout << "宠物 " << name << " 出生了!" << endl;
    }

    // 手动实现无参构造函数
    Pet() {
        cout << "无名宠物出生了!" << endl;
    }
};

int main() {
    Pet p1("小白");  // 调用带参数的构造函数
    Pet p2;          // 调用无参构造函数,如果类 Pet 中没有实现无参构造函数,这行会报错!
    return 0;
}

2. 析构函数不能传参

析构函数是对象生命结束时自动调用的,没法让你手动传参数。所以 C++ 规定析构函数不能有参数,也没有返回值

例子:

#include <iostream>
using namespace std;

class Pet {
public:
    ~Pet() {
        cout << "宠物离开啦!" << endl;
    }
};

int main() {
    Pet myPet;  // 构造函数调用
    return 0;   // 析构函数自动调用,无需传参
}

3. 构造函数和析构函数的调用顺序

  • 构造函数调用顺序:从基类到派生类(“从老祖宗到小辈”)。
  • 析构函数调用顺序:从派生类到基类(“从小辈到老祖宗”)。

例子:

#include <iostream>
using namespace std;

class Animal {
public:
    Animal() {
        cout << "动物出生了!" << endl;
    }
    ~Animal() {
        cout << "动物离开了!" << endl;
    }
};

class Pet : public Animal {
public:
    Pet() {
        cout << "宠物出生了!" << endl;
    }
    ~Pet() {
        cout << "宠物离开了!" << endl;
    }
};

int main() {
    Pet myPet;  // 创建对象,构造函数按顺序调用
    return 0;   // 对象销毁,析构函数按逆序调用
}

输出:

动物出生了!
宠物出生了!
宠物离开了!
动物离开了!

4. 为什么要用初始化列表

在 C++ 中,如果你的类有 const 或引用类型的成员变量,这些成员变量必须在对象创建时初始化,而不能在构造函数内部赋值。这时候,初始化列表就派上用场了

例子:

#include <iostream>
using namespace std;

class Pet {
private:
    const int id;   // 宠物的编号,常量
    int& health;    // 宠物的健康值,引用
public:
    // 使用初始化列表初始化 const 和引用
    Pet(int i, int& h) : id(i), health(h) {
        cout << "宠物编号 " << id << " 出生啦,健康值:" << health << endl;
    }
};

int main() {
    int health = 100;
    Pet myPet(1, health);  // 初始化编号和健康值
    return 0;
}

输出:

宠物编号 1 出生啦,健康值:100

总结: 初始化列表不仅能简化代码,还能提高效率,尤其是对于 const 和引用变量来说,是唯一的初始化方式。

5. 析构函数与动态内存管理

如果类中使用了动态内存分配,析构函数必须负责释放这些资源。否则,程序会发生 内存泄漏

什么是内存泄漏? 内存泄漏就是你申请了内存但忘记释放,导致这些内存永远占着不用,最后程序越来越卡甚至崩溃。

例子:

#include <iostream>
using namespace std;

class Pet {
private:
    int* healthHistory; // 动态分配的健康记录
public:
    Pet() {
        healthHistory = new int[100];  // 分配内存
        cout << "宠物出生啦!分配了健康记录的内存!" << endl;
    }

    ~Pet() {
        delete[] healthHistory;  // 释放内存
        cout << "宠物离开啦!释放了健康记录的内存!" << endl;
    }
};

int main() {
    Pet myPet;  // 创建宠物对象
    return 0;   // 自动调用析构函数,释放资源
}

输出:

宠物出生啦!分配了健康记录的内存!
宠物离开啦!释放了健康记录的内存!

总结:构造与析构的细节要牢记

  • 默认构造函数会被覆盖,记得显式声明无参构造函数。
  • 析构函数不能传参,自动调用无需干预。
  • 构造函数从基类到派生类,析构函数反过来。
  • 初始化列表是 const 和引用成员变量的好帮手。
  • 动态分配内存一定要在析构函数里释放,别让内存泄漏成为你的“宠物负担”!

掌握这些细节,你会发现构造和析构的世界不再神秘!

微信搜索 【跟着小康学编程】,关注我,定期分享计算机编程硬核技术文章。

四、常见问题与注意事项

学习 C++ 的构造函数和析构函数,难免会遇到一些让人挠头的问题。这里用几条通俗易懂的解释+例子,手把手带你避开坑。读完,你就能轻松应对那些常见的“坑爹场景”了!

1. 构造函数能不能调用析构函数?

答案:绝对不能!

为什么?

构造函数是让对象“出生”,析构函数是让对象“离开”。你能想象一个小孩刚出生,立刻安排“谢幕”吗?这种操作会让程序直接崩溃!

错误例子:

#include <iostream>
using namespace std;

class Pet {
public:
    Pet() {
        cout << "宠物出生啦!" << endl;
        this->~Pet(); // 这里调用了析构函数,错误!
    }

    ~Pet() {
        cout << "宠物离开啦!" << endl;
    }
};

int main() {
    Pet myPet; // 构造函数和析构函数冲突
    return 0;
}

运行结果:

宠物出生啦!
宠物离开啦!
宠物离开啦!(程序崩溃)

记住:构造函数负责“出生”,析构函数负责“离开”,两者不要乱插队,默认的顺序最好别动!

2. 析构函数能不能抛出异常?

答案:万万不能!

为什么?

析构函数是对象“谢幕”的时候自动执行的。如果这个阶段抛出异常,程序就可能炸掉,尤其是在异常处理过程中,再抛出一个异常,系统直接“懵了”。

错误例子:

#include <iostream>
#include <stdexcept>
using namespace std;

class Pet {
public:
    ~Pet() {
        throw runtime_error("析构函数里出错了!"); // 不要这样写!
    }
};

int main() {
    try {
        Pet myPet; // 创建对象
    } catch (const exception& e) {
        cout << "捕获到异常:" << e.what() << endl;
    }
    return 0;
}

结果:程序行为不可预测,甚至崩溃!

正确做法: 析构函数内部捕获异常,自行处理:

#include <iostream>
using namespace std;

class Pet {
public:
    ~Pet() {
        try {
            throw runtime_error("错误发生!");
        } catch (...) {
            cout << "析构函数内部处理了异常。" << endl;
        }
    }
};

int main() {
    Pet myPet; // 析构函数会捕获异常
    return 0;
}

运行结果:

析构函数内部处理了异常。

记住:析构函数要低调点,能自己处理的事情就别给外界添麻烦。

3. 对象数组初始化需要注意什么?

当你用对象数组时,编译器会默认调用无参构造函数来初始化每个数组元素。如果你的类只有带参数的构造函数,编译器会很迷惑:“我该用什么参数来初始化这些对象?”

如果你没写无参构造函数,编译器就只能罢工,报个错给你看。

有无参构造函数时,一切正常

例子:

#include <iostream>
using namespace std;

class Pet {
public:
    Pet() {
        cout << "无名宠物出生了!" << endl;
    }
};

int main() {
    Pet pets[3]; // 每个数组元素调用无参构造函数
    return 0;
}

运行结果:

无名宠物出生了!
无名宠物出生了!
无名宠物出生了!

编译器轻松搞定,每个数组元素用无参构造函数初始化。

只有带参数构造函数时,编译器发愁

例子:

#include <iostream>
using namespace std;

class Pet {
public:
    Pet(string name) {
        cout << "宠物 " << name << " 出生了!" << endl;
    }
};

int main() {
    Pet pets[3]; // 错误!没有无参构造函数
    return 0;
}

错误提示:

error: no matching function for call to ‘Pet::Pet()

问题: 编译器默认需要无参构造函数来初始化数组,但你的类里没有,怎么办?

解决办法:显式初始化数组

你可以显式告诉编译器每个数组元素该怎么初始化:

例子:

#include <iostream>
using namespace std;

class Pet {
public:
    Pet(string name) {
        cout << "宠物 " << name << " 出生了!" << endl;
    }
};

int main() {
    Pet pets[3] = {Pet("小白"), Pet("小黑"), Pet("小黄")}; // 显式初始化
    return 0;
}

运行结果:

宠物 小白 出生了!
宠物 小黑 出生了!
宠物 小黄 出生了!

小结一下:

  1. 对象数组初始化时,编译器默认调用无参构造函数。
  2. 如果类中没有无参构造函数,编译器会报错,无法完成数组初始化。
  3. 遇到这种情况,可以用显式初始化方式,为数组的每个元素指定参数。

总之:编译器不会帮你猜参数,得你自己明确告诉它!

4. 析构函数为什么不能重载?

析构函数的名字是固定的 ~ClassName(),没有参数,也没有返回值。这是因为析构函数是系统自动调用的,不需要你手动去管。所以,重载完全没有意义。

记住:析构函数干的就是单纯的“善后工作”,设计的初衷就是不需要外界干预,所以不用也不能重载。

5. 为什么构造函数不能是虚函数?

简单理解: 构造函数负责初始化对象,而虚函数是为了实现多态。问题是,多态依赖于对象的类型信息(即虚函数表指针),但对象在构造阶段还没完全初始化完成 ,类型信息也就不完整,所以谈不上多态。

再打个比方: 构造函数好比“新生儿的出生登记表”,它负责给对象安上属性和身份。如果你连“新生儿是谁”都没确认,就要搞“成人身份证”(虚函数表)去干复杂的多态事情,这显然是不可能的。

总之: 构造函数的职责是“出生”,虚函数的职责是“多态”,两者分工明确,互不干涉。构造函数的目标是让对象能正确初始化,等初始化完成后再谈多态问题。

6. 什么情况下构造函数和析构函数必须手动写?

类中有动态内存分配时

动态分配的资源要在析构函数中释放,否则就会内存泄漏。

例子:

#include <iostream>
using namespace std;

class Pet {
private:
    int* healthHistory; // 动态分配资源
public:
    Pet() {
        healthHistory = new int[100]; // 分配内存
        cout << "宠物出生啦!分配了健康记录。" << endl;
    }

    ~Pet() {
        delete[] healthHistory; // 释放内存
        cout << "宠物离开啦!健康记录被释放了。" << endl;
    }
};

int main() {
    Pet myPet; // 构造分配,析构释放
    return 0;
}

输出:

宠物出生啦!分配了健康记录。
宠物离开啦!健康记录被释放了。
类中有 const 或 引用 成员时

这些成员变量必须在对象创建时初始化,必须用构造函数的初始化列表。

例子:

#include <iostream>
using namespace std;

class Pet {
private:
    const int id;   // 宠物的编号,常量
    int& health;    // 宠物的健康值,引用
public:
    Pet(int i, int& h) : id(i), health(h) {
        cout << "宠物编号 " << id << " 出生啦,健康值:" << health << endl;
    }
};

int main() {
    int health = 100;
    Pet myPet(1, health);
    return 0;
}

输出:

宠物编号 1 出生啦,健康值:100

总结

  • 构造函数不能乱调析构函数,职责分清楚。
  • 析构函数不要抛异常,自己处理自己的问题。
  • 对象数组初始化时,必须注意构造函数的类型。
  • 析构函数不能重载,构造函数不能是虚函数,各干各的活。
  • 动态内存分配要记得用析构函数释放,不然程序哭给你看!
  • const 和引用成员变量,用初始化列表才能搞定。

读到这里,你是不是觉得构造函数和析构函数其实没那么难?只要注意上面的这些坑,写起来就稳了!

五、总结:构造函数和析构函数,这些点你要记住!

1.构造函数是干啥的?
对象一出生,构造函数帮它“准备好一切”,比如初始化属性,让它能正常用。

2.析构函数是干啥的?
对象“退场”时,析构函数帮忙“收拾残局”,比如释放内存、关闭文件,不留麻烦。

3.它俩的调用顺序?

  • 构造: 先基类,再派生类。
  • 析构: 先派生类,再基类。

4.动态内存要小心!
如果类里面用了new分配内存,记得用析构函数里的delete释放,不然会出现“内存泄漏”。

一句话记住: 构造函数管“出生”,析构函数管“善后”,俩人搭配,才能保证对象“活得好,走得清”。

如果你觉得这篇文章简单好懂,别忘了点赞、收藏、关注 或者留言聊聊你的想法!下一篇,我们聊聊 C++ 的 “拷贝构造函数” 和它带来的那些“深浅问题”,准备好再升级一波你的 C++ 技能吧!

也欢迎大家来关注我公众号 「跟着小康学编程」,这里会持续分享计算机编程硬核技术文章!

怎么关注我的公众号?

微信搜索 【跟着小康学编程】,关注我,定期分享计算机编程硬核技术文章。

另外,小康还建了一个技术交流群,专门聊技术、答疑解惑。如果你在读文章时碰到不懂的地方,随时欢迎来群里提问!我会尽力帮大家解答,群里还有不少技术大佬在线支援,咱们一起学习进步,互相成长!

想找我?加我微信即可,微信号:jkfwdkf ,备注 「加群

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值