【c++面向对象编程】第3篇:类与对象(二):构造函数与析构函数

目录

一、一个让人头疼的问题

二、构造函数:对象出生时的"第一声啼哭"

[1. 最基本的构造函数](#1. 最基本的构造函数)

[2. 带参数的构造函数(重载)](#2. 带参数的构造函数(重载))

[3. 初始化列表:更高效的初始化方式](#3. 初始化列表:更高效的初始化方式)

三、默认构造函数:那个"看不见"的函数

四、析构函数:对象临终前的"遗言"

五、完整的例子:动态字符串数组类

六、三个容易踩的坑

[1. 构造函数里抛出异常](#1. 构造函数里抛出异常)

[2. 析构函数里再抛异常](#2. 析构函数里再抛异常)

[3. 忘记写析构函数导致内存泄露](#3. 忘记写析构函数导致内存泄露)

七、这一篇的收获


一、一个让人头疼的问题

继续用上一讲的Book类:

cpp

复制代码
Book b;
b.setPrice(128.5);  // 必须记得手动初始化
b.print();          // 如果忘了setPrice,price是未定义的垃圾值

C的结构体好歹可以用{0}初始化,但C++的类不行------因为类可能有复杂的内部逻辑,简单的内存置零可能破坏状态。

更麻烦的是,如果你的类需要分配动态内存 (比如用new申请了一块空间),用完必须手动delete。一旦忘记,内存就泄露了。

构造函数和析构函数就是用来解决这些问题的。


二、构造函数:对象出生时的"第一声啼哭"

构造函数是一种特殊的成员函数

  • 名字和类名完全相同

  • 没有返回值 (连void都不写)

  • 在创建对象时自动调用

  • 可以重载(多个构造函数,参数不同)

1. 最基本的构造函数

cpp

复制代码
class Book {
private:
    string title;
    string author;
    double price;
    
public:
    // 构造函数——名字和类一样,没返回值
    Book() {
        title = "未知";
        author = "未知";
        price = 0.0;
        cout << "构造函数被调用了" << endl;
    }
    
    void print() {
        cout << title << ", " << author << ", " << price << endl;
    }
};

int main() {
    Book b;  // 自动调用构造函数,输出"构造函数被调用了"
    b.print();  // 输出:未知, 未知, 0
}

注意最后一行的Book b;------我们没有写任何"初始化"的代码,但对象已经有了合理的默认值。

2. 带参数的构造函数(重载)

一个类可以有多个构造函数,只要参数不同:

cpp

复制代码
class Book {
private:
    string title;
    string author;
    double price;
    
public:
    // 默认构造函数
    Book() {
        title = "未知";
        author = "未知";
        price = 0.0;
    }
    
    // 带三个参数的构造函数
    Book(string t, string a, double p) {
        title = t;
        author = a;
        price = p;
    }
    
    // 只传书名的构造函数
    Book(string t) {
        title = t;
        author = "佚名";
        price = 0.0;
    }
    
    void print() {
        cout << "《" << title << "》 " << author << " ¥" << price << endl;
    }
};

int main() {
    Book b1;                     // 调用默认构造函数
    Book b2("C++ Primer", "Lippman", 128.5);  // 调用3参数构造
    Book b3("三体");              // 调用1参数构造
    
    b1.print();  // 《未知》 未知 ¥0
    b2.print();  // 《C++ Primer》 Lippman ¥128.5
    b3.print();  // 《三体》 佚名 ¥0
}

3. 初始化列表:更高效的初始化方式

上面的构造函数在函数体里赋值,但还有更正统的写法------初始化列表

cpp

复制代码
Book(string t, string a, double p) : title(t), author(a), price(p) {
    // 函数体可以为空,或者写其他逻辑
}

:后面的title(t)意思是"用参数t来初始化title成员变量"。

为什么要用初始化列表?

  • 对于stringvector等有构造函数的类型,初始化列表直接构造一次;在函数体里赋值会先默认构造再赋值,多一步开销

  • const成员变量和引用类型必须用初始化列表

  • 成员变量的初始化顺序按它们在类中声明的顺序,不是按初始化列表的顺序

cpp

复制代码
class Demo {
private:
    const int id;        // const成员必须用初始化列表
    int& ref;            // 引用必须用初始化列表
    string name;
    
public:
    Demo(int i, int& r, string n) : id(i), ref(r), name(n) {
        // 这里id和ref已经初始化好了
    }
};

三、默认构造函数:那个"看不见"的函数

如果你写一个类,没有定义任何构造函数 ,编译器会悄悄给你生成一个默认构造函数(什么也不做)。

cpp

复制代码
class Simple {
    int x;
    string s;
    // 编译器会生成一个 Simple() {}
};

Simple obj;  // 可以编译,但x是垃圾值,s是空字符串

但有一个陷阱:一旦你自己写了任何一个构造函数,编译器就不再生成默认构造函数。

cpp

复制代码
class Book {
public:
    Book(string t) { title = t; }  // 写了构造函数
private:
    string title;
};

Book b1("C++");  // ✅ 没问题
Book b2;         // ❌ 编译错误!没有默认构造函数

如果你既想要自己的构造函数,又想要无参的版本,必须显式写出来

cpp

复制代码
Book() {}  // 或者 = default (C++11)

C++11提供了更简洁的写法:

cpp

复制代码
Book() = default;  // 让编译器生成默认版本

四、析构函数:对象临终前的"遗言"

析构函数是构造函数的"镜像":

  • 名字是~类名

  • 没有参数,不能重载(一个类只有一个析构函数)

  • 没有返回值

  • 对象销毁时自动调用

cpp

复制代码
class Book {
private:
    string* pComment;  // 指向动态分配的评论
    
public:
    Book(string comment) {
        pComment = new string(comment);
        cout << "构造:分配内存" << endl;
    }
    
    ~Book() {
        delete pComment;  // 释放内存
        cout << "析构:释放内存" << endl;
    }
};

int main() {
    Book b("很好看的一本书");
    // 函数结束时,b被销毁,析构函数自动调用
}
// 输出:
// 构造:分配内存
// 析构:释放内存

什么时候调用析构函数?

对象类型 析构时机
局部对象(栈上) 离开作用域时(如函数结束)
静态局部对象 程序结束时
全局对象 程序结束时
new创建的对象 显式调用delete

五、完整的例子:动态字符串数组类

把构造和析构结合起来,写一个管理动态数组的类StringArray

cpp

复制代码
#include <iostream>
#include <string>
using namespace std;

class StringArray {
private:
    string* data;   // 指向动态数组的指针
    int size;       // 数组大小
    
public:
    // 构造函数:分配内存并初始化
    StringArray(int n) : size(n) {
        data = new string[n];
        cout << "分配了" << n << "个字符串的空间" << endl;
    }
    
    // 析构函数:释放内存
    ~StringArray() {
        delete[] data;
        cout << "释放了" << size << "个字符串的空间" << endl;
    }
    
    // 设置元素
    void set(int index, const string& s) {
        if (index >= 0 && index < size) {
            data[index] = s;
        }
    }
    
    // 获取元素
    string get(int index) {
        if (index >= 0 && index < size) {
            return data[index];
        }
        return "";
    }
    
    // 打印所有
    void print() {
        for (int i = 0; i < size; i++) {
            cout << "[" << i << "] = " << data[i] << endl;
        }
    }
};

int main() {
    StringArray arr(3);   // 构造:分配内存
    arr.set(0, "Hello");
    arr.set(1, "World");
    arr.set(2, "C++");
    arr.print();
    // 函数结束,arr被销毁 → 析构函数自动调用,释放内存
    return 0;
}

运行结果:

text

复制代码
分配了3个字符串的空间
[0] = Hello
[1] = World
[2] = C++
释放了3个字符串的空间

注意:我们没有手动调用任何"释放"函数,析构函数自动帮我们做了。


六、三个容易踩的坑

1. 构造函数里抛出异常

如果在构造函数里抛异常,对象被认为"没有构造完成",析构函数不会被调用。这意味着构造函数里已经分配的资源需要自己处理。

cpp

复制代码
Book() {
    p = new int[100];
    throw "error";  // 如果这里抛异常,~Book()不会被调用,内存泄露
}

解决方案:用智能指针(后面的章节会讲),或者在构造函数里用try-catch。

2. 析构函数里再抛异常

极其危险 !如果析构函数在栈展开期间(处理另一个异常时)又抛出异常,程序会直接调用terminate()崩溃。

cpp

复制代码
~Book() {
    delete p;
    throw "析构异常";  // ❌ 千万别这么写
}

规则:析构函数应该吞掉所有异常,只做清理,不抛出任何东西

3. 忘记写析构函数导致内存泄露

如果你的类里有指针成员,并且你在构造函数里用new分配了内存,必须写析构函数用delete释放,否则内存就泄露了。

cpp

复制代码
class Leaky {
    int* p;
public:
    Leaky() { p = new int[1000]; }
    // 没有析构函数 → 内存泄露!
};

七、这一篇的收获

你现在应该理解:

  • 构造函数在对象创建时自动调用,适合做初始化

  • 析构函数在对象销毁时自动调用,适合做清理

  • 构造函数可以重载(多个版本)

  • 初始化的优先级:初始化列表 > 构造函数体内的赋值

  • 如果你管理了动态资源(new),必须提供析构函数去释放

💡 小作业:写一个IntArray类,构造函数接收大小n并分配n个int的空间,析构函数释放空间。提供set()get()方法。在main中创建对象,观察构造和析构的调用时机。


下一篇预告:第4篇《类与对象(三):拷贝构造函数与深浅拷贝问题》------当你把一个对象赋值给另一个对象时,背后发生了什么?默认的拷贝行为为什么可能导致程序崩溃?

相关推荐
小年糕是糕手1 小时前
【C++】vector 不踩坑指南:用法、底层实现与迭代器失效解析
c++·算法
不会写DN1 小时前
PyScript-GitHubRepo:构建高性能GitHub仓库批量下载工具的技术实践
开发语言·前端·python
SilentSamsara2 小时前
生成器完全指南:`yield` 与惰性求值的工程价值
linux·开发语言·python·算法·机器学习·青少年编程
玛卡巴卡ldf2 小时前
【LeetCode 手撕算法】(二分查找)搜索插入位置、搜索二维矩阵、查找数组相同的所有位置、搜索旋转排序数组、旋转升序数组的最小值
数据结构·算法·leetcode
谷雨不太卷9 小时前
进程的状态码
java·前端·算法
jieyucx9 小时前
Go语言深度解剖:Map扩容机制全解析(增量扩容+等量扩容+渐进式迁移)
开发语言·后端·golang·map·扩容策略
顾温9 小时前
default——C#/C++
java·c++·c#
凉茶钱10 小时前
【c语言】动态内存管理:malloc,calloc,realloc,柔性数组
c语言·c++·vscode·柔性数组
脏脏a10 小时前
【C++模版】泛型编程:代码复用的终极利器
开发语言·c++·c++模版