构造与析构
前言
构造函数(构造方法)和析构函数(析构方法)是类的特殊成员函数,核心职责分别是对象创建时的初始化(初始化成员属性、申请资源等)和对象销毁时的善后处理(释放堆内存、关闭文件等);拷贝构造函数是构造函数的特殊形式,负责通过已有对象复制创建新对象。三者共同保障类对象的生命周期(创建→使用→销毁)中资源的合理管理,是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;
}
析构函数的调用场景
- 栈上对象:离开定义的作用域时自动调用(如
main函数中局部对象、代码块内对象)。 - 堆上对象:通过
delete释放时自动调用(free不会调用,导致资源泄漏)。 - 全局对象/静态对象:程序结束时自动调用(析构顺序与构造顺序相反)。
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); // 拷贝构造函数声明
};
浅拷贝与深拷贝
浅拷贝(默认拷贝构造)
- 核心行为:仅复制成员变量的“值”,若成员是指针/引用,仅复制地址(不复制地址指向的内存内容)。
- 问题:新对象与原对象的指针成员指向同一片堆内存,导致:
- 重复释放:析构时两个对象都尝试释放同一块内存,触发内存崩溃。
- 数据篡改:一个对象修改指针指向的内容,会影响另一个对象。
深拷贝(自定义拷贝构造)
- 核心行为:不仅复制成员变量的值,若成员是指针/引用,会重新申请新的堆内存,并将原内存的内容复制到新内存中。
- 作用:解决浅拷贝的内存冲突问题,确保新对象与原对象完全独立(资源互不干扰)。
代码示例(浅拷贝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); // 析构时释放堆内存
}
};
关键注意事项
- 默认拷贝构造函数始终存在,即使未显式定义,也可通过
类名 新对象(原对象)或类名 新对象 = 原对象创建对象。 - 初始化语句中的
=并非赋值操作,而是拷贝构造的另一种书写方式(与赋值运算符函数无关):CAT tom(1, "tom"); CAT tom1(tom); // 拷贝构造 CAT tom2 = tom; // 等价于tom1,仍是拷贝构造(非赋值) - 拷贝构造函数的参数必须是类的引用(而非指针):
- 指针参数:无法确保指向有效对象(可能是NULL/野指针),存在内存非法访问风险。
- 引用参数:确保传递的是已初始化的有效对象,拷贝过程更安全。
- 参数建议加
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个成员函数特性
- 默认无参构造/析构函数:空实现,显式定义任何构造函数(含拷贝构造)后,默认无参构造会消失。
class empty { public: empty(int x) {} // 显式定义带参构造 }; empty e1; // 报错:默认无参构造已消失 - 默认拷贝构造函数:仅在未显式定义“单参数为类引用”的构造函数时存在。
- 默认赋值运算符函数:行为是浅拷贝,返回类引用(支持连续赋值,如
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引用?
- 引用:避免拷贝构造函数调用自身(若参数是值传递,会触发拷贝构造,导致无限递归)。
- const:防止在拷贝过程中修改源对象的内容(保护源对象的只读性)。
禁止拷贝构造与赋值操作(C++11)
- 场景:某些类(如资源独占类)不允许复制对象,可通过
=delete禁止默认的拷贝构造和赋值运算符函数。 - 示例:
class NoCopy { public: NoCopy() {} // 禁止拷贝构造 NoCopy(const NoCopy&) = delete; // 禁止赋值操作 NoCopy& operator=(const NoCopy&) = delete; }; NoCopy a; NoCopy b(a); // 报错:拷贝构造已被禁止 b = a; // 报错:赋值操作已被禁止

浙公网安备 33010602011771号