构造与析构

前言
构造函数(构造方法)和析构函数(析构方法)是类的特殊成员函数,核心职责分别是对象创建时的初始化(初始化成员属性、申请资源等)和对象销毁时的善后处理(释放堆内存、关闭文件等);拷贝构造函数是构造函数的特殊形式,负责通过已有对象复制创建新对象。三者共同保障类对象的生命周期(创建→使用→销毁)中资源的合理管理,是C++面向对象编程的基础核心知识点。


构造函数

核心概念

类的特殊成员函数,专门用于初始化类的各项数据(包括属性、堆内存、文件句柄等资源),是对象创建时的“初始化器”。

关键特点

  • 任何一个类必然有至少一个构造函数(显式定义或系统默认生成)。
  • 若类未显式定义构造函数,系统会自动添加隐藏的无参空构造函数
  • 若类显式定义了构造函数(无论有无参数),系统默认的无参空构造函数会自动取消
  • 每当通过“定义对象”或“new申请堆对象”时,构造函数会自动调用(malloc/calloc申请堆空间不会调用)。
  • 无返回值类型(无需写void),函数名必须与类名完全一致
  • 支持重载(可定义多个参数列表不同的构造函数,如无参、有参构造)。

代码示例

示例1:BMP类构造函数(头文件+源文件分离)

// bmp.hpp(头文件:类声明)
#ifndef __BMP_HPP
#define __BMP_HPP
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string>
using namespace std;

class bmp {
private:
    string PathName;  // 图片路径
    int Wide;         // 图片宽度
    int High;         // 图片高度
    FILE* fp;         // 文件句柄
    char* RGB;        // 存储RGB数据的堆内存指针
public:
    bmp(string path = "");  // 带默认参数的构造函数(兼顾无参使用)
    ~bmp();
    void readRGB();
    void showInfo();
    bool DisplayBmp();
};
#endif

// bmp.cpp(源文件:构造函数实现)
#include "bmp.hpp"
bmp::bmp(string path) {
    cout << "构造.." << endl;
    if (path.empty()) {
        cout << "文件路径为空.." << endl;
        fp = NULL;
        RGB = NULL;
        return;
    }
    PathName = path;
    // 打开文件
    fp = fopen(PathName.c_str(), "r");
    if (fp == NULL) {
        cout << "文件打开失败.." << endl;
        fp = NULL;
        RGB = NULL;
        return;
    }
    // 读取图像头部信息(省略具体解析逻辑)
    Wide = 800;
    High = 480;
    // 根据宽高分配堆内存(3字节/像素:RGB)
    RGB = new char[Wide * High * 3];
}

示例2:LCD类构造函数

#include <iostream>
#include <unistd.h>
using namespace std;

class LCD {
private:
    int fd;         // 显示器文件描述符
    char* map;      // 内存映射地址
    int Width;      // 显示器宽度
    int Height;     // 显示器高度
    int ColorDepth; // 显示器色深
public:
    LCD(/* args */);  // 无参构造函数声明
    ~LCD();
};

// 构造函数实现:初始化显示器资源
LCD::LCD(/* args */) {
    // 打开显示器设备文件
    cout << "打开显示器设备文件..." << endl;
    fd = 4;  // 模拟文件描述符(实际通过open系统调用获取)
    // 获取LCD硬件属性(实际通过fcntl/ioctl获取)
    cout << "获取LCD的硬件属性..." << endl;
    Width = 800;
    Height = 480;
    // 内存映射(实际通过mmap系统调用实现)
    cout << "内存映射..." << endl;
    map = static_cast<char*>(calloc(1, 32));
    cout << "成功构造LCD类对象..." << endl;
}

析构函数

核心概念

类的特殊成员函数,专门用于处理对象被释放时的收尾工作(释放堆内存、关闭文件、解除内存映射等),是对象销毁时的“清理器”。

关键特点

  • 任何一个类有且仅有一个析构函数(显式定义或系统默认生成)。
  • 若类未显式定义析构函数,系统会自动添加隐藏的空析构函数
  • 若类显式定义了析构函数,系统默认的空析构函数会自动取消
  • 每当对象被释放(离开作用域、delete释放堆对象)时,析构函数会自动调用(free释放堆对象不会调用)。
  • 无返回值类型,无参数,函数名与类名一致,且前面加波浪号~
  • 不支持重载(唯一且固定格式)。

代码示例

示例1:BMP类析构函数

// bmp.cpp 中实现
bmp::~bmp() {
    cout << "析构.." << endl;
    // 关闭文件(若文件已打开)
    if (fp) {
        fclose(fp);
    }
    // 释放RGB堆内存(若已分配)
    if (RGB) {
        delete[] RGB;
    }
}

示例2:LCD类析构函数

// LCD类析构函数实现:释放显示器资源
LCD::~LCD() {
    // 解除内存映射(释放堆内存,模拟munmap)
    free(map);
    cout << "释放申请的内存..." << endl;
    // 关闭LCD设备文件
    cout << "关闭LCD设备文件..." << endl;
    cout << "LCD类对象析构完成..." << endl;
}

析构函数的调用场景

  1. 栈上对象:离开定义的作用域时自动调用(如main函数中局部对象、代码块内对象)。
  2. 堆上对象:通过delete释放时自动调用(free不会调用,导致资源泄漏)。
  3. 全局对象/静态对象:程序结束时自动调用(析构顺序与构造顺序相反)。

this指针

核心作用

解决类方法中“成员变量与参数同名”的命名冲突,明确指向当前调用该方法的本类对象

关键特性

  • 是所有类方法(构造函数、析构函数、普通成员函数)的隐藏参数(无需显式声明,编译器自动传递)。
  • 本质是指向当前对象的常量指针this不可修改指向,但可修改指向的对象内容,如this->num = 10)。
  • 仅在类方法内部有效,不能在类外部访问。

代码示例(解决命名冲突)

class BMP {
private:
    string fileName;  // 成员变量与参数同名
    int width;
    int height;
    char* RGB;
public:
    // 构造函数:参数fileName与成员变量同名
    BMP(string fileName) {
        // this->fileName 明确指向成员变量,右侧为参数
        this->fileName = fileName;
    }
};

拷贝构造函数

核心概念

拷贝构造函数是特殊的构造函数,用于通过一个已存在的类对象,复制创建一个新的同类对象(新对象与原对象成员值完全一致)。

默认形式

若类未显式定义拷贝构造函数,系统会自动生成默认拷贝构造函数,行为是“浅拷贝”(逐字节复制成员变量)。

显式声明格式

类名(const 类名& 源对象引用);
// 示例:
class CAT {
public:
    CAT(const CAT& src);  // 拷贝构造函数声明
};

浅拷贝与深拷贝

浅拷贝(默认拷贝构造)

  • 核心行为:仅复制成员变量的“值”,若成员是指针/引用,仅复制地址(不复制地址指向的内存内容)。
  • 问题:新对象与原对象的指针成员指向同一片堆内存,导致:
    1. 重复释放:析构时两个对象都尝试释放同一块内存,触发内存崩溃。
    2. 数据篡改:一个对象修改指针指向的内容,会影响另一个对象。

深拷贝(自定义拷贝构造)

  • 核心行为:不仅复制成员变量的值,若成员是指针/引用,会重新申请新的堆内存,并将原内存的内容复制到新内存中。
  • 作用:解决浅拷贝的内存冲突问题,确保新对象与原对象完全独立(资源互不干扰)。

代码示例(浅拷贝vs深拷贝)

class CAT {
private:
    int Num;
    string Name;
    char* ptr;  // 指针成员(触发浅拷贝问题)
public:
    // 普通构造函数
    CAT(int num, string name) {
        Num = num;
        Name = name;
        ptr = static_cast<char*>(calloc(1, 32));  // 申请32字节堆内存
    }

    // 1. 默认浅拷贝构造函数(系统自动生成,等价于以下代码)
    CAT(const CAT& src) {
        this->Num = src.Num;
        this->Name = src.Name;
        this->ptr = src.ptr;  // 仅复制指针地址,未复制内存内容
    }

    // 2. 自定义深拷贝构造函数(解决浅拷贝问题)
    CAT(const CAT& src) {
        cout << "cpy Func ... " << endl;
        this->Num = src.Num;
        this->Name = src.Name;
        // 重新申请堆内存
        this->ptr = static_cast<char*>(calloc(1, 32));
        // 复制原内存中的内容到新内存
        memcpy(this->ptr, src.ptr, 32);
    }

    ~CAT() {
        if (ptr) free(ptr);  // 析构时释放堆内存
    }
};
浅拷贝内存问题示意图(原对象与新对象的ptr指针指向同一块堆内存,析构时两次free导致崩溃)

关键注意事项

  1. 默认拷贝构造函数始终存在,即使未显式定义,也可通过类名 新对象(原对象)类名 新对象 = 原对象创建对象。
  2. 初始化语句中的=并非赋值操作,而是拷贝构造的另一种书写方式(与赋值运算符函数无关):
    CAT tom(1, "tom");
    CAT tom1(tom);    // 拷贝构造
    CAT tom2 = tom;   // 等价于tom1,仍是拷贝构造(非赋值)
    
  3. 拷贝构造函数的参数必须是类的引用(而非指针):
    • 指针参数:无法确保指向有效对象(可能是NULL/野指针),存在内存非法访问风险。
    • 引用参数:确保传递的是已初始化的有效对象,拷贝过程更安全。
  4. 参数建议加const:防止在拷贝过程中意外修改源对象的内容。

拷贝构造与赋值操作的区别

场景 调用的函数 核心区别
A b(a);A b = a; 拷贝构造函数 新对象创建时初始化(无旧对象)
A b; b = a; 赋值运算符函数 已有对象的内容覆盖(有旧对象)

代码验证

class A {
public:
    int x;
    A(int x=0) { this->x = x; }

    // 拷贝构造函数
    A(const A& rh) {
        cout << "拷贝构造" << endl;
        this->x = rh.x;
    }

    // 赋值运算符函数(重载=)
    const A& operator=(const A& rh) {
        cout << "赋值操作" << endl;
        this->x = rh.x;
        return *this;
    }

    void show() { cout << x << endl; }
};

int main() {
    A a(1);
    A b(a);    // 输出"拷贝构造"(新对象初始化)
    A c;
    c = a;     // 输出"赋值操作"(已有对象覆盖)
    return 0;
}

空类的“隐藏成员”

空类的本质

C++中“空类”(无显式成员的类)并非真正为空,编译器会自动为其生成4个默认成员函数,确保对象能正常创建、拷贝、赋值和销毁。

// 显式的空类
class empty {};

// 编译器自动生成的等价代码
class empty {
public:
    // 1. 默认无参构造函数
    empty() {}
    // 2. 默认析构函数
    ~empty() {}
    // 3. 默认拷贝构造函数
    empty(const empty& r) { *this = r; }
    // 4. 默认赋值运算符函数
    empty& operator=(const empty& r) { return *this; }
};

默认生成的4个成员函数特性

  1. 默认无参构造/析构函数:空实现,显式定义任何构造函数(含拷贝构造)后,默认无参构造会消失。
    class empty {
    public:
        empty(int x) {}  // 显式定义带参构造
    };
    empty e1;  // 报错:默认无参构造已消失
    
  2. 默认拷贝构造函数:仅在未显式定义“单参数为类引用”的构造函数时存在。
  3. 默认赋值运算符函数:行为是浅拷贝,返回类引用(支持连续赋值,如e1 = e2 = e3)。

头文件与源文件分离(类的分文件编写)

核心原则

C++工程中,类的声明(成员属性、函数原型)放在头文件(.h/.hpp),实现(函数体)放在源文件(.cpp),便于代码复用和维护。

代码示例(Person类)

头文件:person.h(类声明)

#ifndef _PERSON_H
#define _PERSON_H
#include <string>
using namespace std;

class Person {
private:
    unsigned int ID;
    string name;
public:
    // 构造函数声明(支持重载)
    Person(unsigned int id);
    Person(unsigned int id, string n);
    // 普通成员函数声明
    void showInfo();
    void setName(string newName);
};
#endif

源文件:person.cpp(类实现)

#include "person.h"
#include <iostream>
using namespace std;

// 构造函数实现:通过::指定类作用域
Person::Person(unsigned int id) {
    ID = id;
}

Person::Person(unsigned int id, string n) {
    ID = id;
    name = n;
}

// 普通成员函数实现
void Person::showInfo() {
    cout << "ID:" << ID << endl;
    cout << "name:" << name << endl;
}

void Person::setName(string newName) {
    name = newName;
}

作用域解析符::的使用

  • 用途:明确指定函数/变量所属的类(解决作用域冲突)。
  • 格式:类名::函数名(参数列表) { 函数体 }
  • 注意:仅在类声明外部实现函数时需要,类内部实现(内联函数)无需添加。

拓展(补充核心知识点)

初始化列表(构造函数的高效初始化)

概念

构造函数的特殊语法,在函数体执行前直接初始化成员变量(尤其适用于const成员、引用成员、没有默认构造的类成员)。

格式与示例

class Student {
private:
    const int id;    // const成员(必须初始化,不能赋值)
    string& name;    // 引用成员(必须初始化)
    int age;
public:
    // 初始化列表:成员变量(初始值),用逗号分隔
    Student(int i, string& n, int a) : id(i), name(n), age(a) {
        // 函数体可补充其他逻辑(无需再初始化成员)
    }
};

优势

  • 效率更高:直接初始化成员,避免“先默认构造再赋值”的额外开销。
  • 功能更强:支持const、引用、无默认构造的类成员的初始化(函数体赋值无法实现)。

构造函数的初始化顺序

  • 初始化顺序与初始化列表的顺序无关,仅与成员变量在类中的声明顺序一致。
  • 示例:
    class Test {
    private:
        int a;
        int b;
    public:
        Test() : b(2), a(b) {}  // 看似a=2,实际a是随机值
    };
    // 声明顺序a在前、b在后,因此先初始化a(此时b未初始化,a=随机值),再初始化b=2
    

析构函数的调用顺序

  • 栈上对象:先构造后析构(与创建顺序相反,类似栈的“后进先出”)。
  • 堆上对象:仅在delete时调用析构,调用顺序由delete的顺序决定。
  • 全局/静态对象:程序结束时调用,析构顺序与构造顺序相反。

explicit关键字(禁止隐式类型转换)

  • 用途:修饰构造函数,禁止通过“赋值语法”进行隐式类型转换。
  • 示例:
    class A {
    public:
        explicit A(int x) {}  // explicit修饰单参构造函数
    };
    
    A a1(1);    // 正常(直接初始化)
    A a2 = 1;   // 报错(禁止隐式转换,explicit生效)
    

拷贝构造函数的参数为什么是const引用?

  1. 引用:避免拷贝构造函数调用自身(若参数是值传递,会触发拷贝构造,导致无限递归)。
  2. const:防止在拷贝过程中修改源对象的内容(保护源对象的只读性)。

禁止拷贝构造与赋值操作(C++11)

  • 场景:某些类(如资源独占类)不允许复制对象,可通过=delete禁止默认的拷贝构造和赋值运算符函数。
  • 示例:
    class NoCopy {
    public:
        NoCopy() {}
        // 禁止拷贝构造
        NoCopy(const NoCopy&) = delete;
        // 禁止赋值操作
        NoCopy& operator=(const NoCopy&) = delete;
    };
    
    NoCopy a;
    NoCopy b(a);  // 报错:拷贝构造已被禁止
    b = a;        // 报错:赋值操作已被禁止
    

posted @ 2025-12-18 08:41  Jaklin  阅读(12)  评论(0)    收藏  举报