在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
在C++中,编译器会为每个类自动生成一些成员函数,即使你没有显式地编写这些函数。这些默认成员函数帮助我们快速完成一些常见的操作。通常情况下,一个类在没有显式定义某些函数时,编译器会为其自动生成六个默认成员函数(需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可):
1.默认构造函数: 当没有显式编写构造函数时,编译器会自动生成一个默认构造函数,用来初始化对象。 2.析构函数: 在没有定义析构函数时,编译器会自动生成一个析构函数,用来在对象被销毁时释放资源。 3.拷贝构造函数: 如果没有编写拷贝构造函数,编译器会生成一个默认的拷贝构造函数,用来通过已有对象创建新的对象。 4.赋值运算符重载: 当我们没有定义赋值运算符(=)时,编译器会生成一个默认的赋值运算符,用来将一个对象的值赋给另一个对象。 5.取地址运算符重载: 允许使用 & 来获取对象的地址,编译器会为每个类自动生成取地址运算符。 6.const 取地址运算符重载: 当对象是 const 时,编译器也会生成对应的取地址运算符。
接下来我们会具体讨论这些函数,了解它们的作用以及在什么情况下我们需要自行实现这些函数。
构造函数是一个用于初始化对象的特殊成员函数。它的名字与类名相同,并且在创建对象时会被自动调用。构造函数的主要任务是确保对象在被创建时有一个明确的初始状态。可以将它理解为对象的"出生",从它开始,对象拥有了完整的、可用的状态。
构造函数的功能类似于我们在C语言中为结构体编写的初始化函数,但更为方便,因为它可以自动调用,而不需要每次手动去调用一个初始化函数。
1.函数名与类名相同:构造函数的名字必须和类名一致。 2.没有返回值:构造函数不需要返回类型,也不能有返回值。 3.自动调用:对象创建时,系统自动调用构造函数初始化对象。 4.支持重载:可以根据不同参数列表定义多个构造函数。 5.默认构造函数:
6.默认构造的多种情况:
7.初始化行为:
补充说明:
int
、char
、double
和指针等。class
或 struct
等关键字定义的类型。C++中,构造函数可以有多个类型,主要包括:
无参构造函数:用于初始化一个对象,没有需要用户提供的参数。例如:
class Date {
public:
Date() {
_year = 1;
_month = 1;
_day = 1;
}
private:
int _year;
int _month;
int _day;
};
在这个例子中,无参构造函数会将日期的年、月、日初始化为1。
带参构造函数:用于在创建对象时指定初始值。例如:
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
这样用户在创建对象时,可以通过传递参数来指定对象的初始状态:Date d(2025, 5, 10);
全缺省构造函数:带有所有默认参数的构造函数,也可以作为无参构造函数使用。例如:
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
这种方式使得在创建对象时,既可以不传递参数,也可以只传递部分参数,从而提高了代码的灵活性。
什么是初始化列表? 初始化列表是构造函数的一种特殊语法,用于在对象创建时为其成员变量赋初值。它的语法是在构造函数的参数列表之后,冒号(
:
)后面跟随成员变量的初始化代码。初始化列表让成员变量在对象创建时直接被初始化,而不是先默认初始化再赋值。
class Point {
public:
Point(int x, int y) : _x(x), _y(y) {} // 这里是初始化列表
private:
int _x;
int _y;
};
在这个例子中,Point
类有两个成员变量 _x
和 _y
。构造函数使用初始化列表将传入的参数 x
和 y
直接赋值给 _x
和 _y
。
为什么要用初始化列表?
const
)或引用(&
)类型的成员变量,它们必须在对象创建时通过初始化列表进行赋值。构造函数在以下几种情况下被调用:
Date d1;
,会调用无参构造函数。
Date d2(2025, 12, 25);
,会调用带参构造函数。
std::vector
中添加元素,容器会使用构造函数创建新对象。
析构函数是用于销毁对象的特殊成员函数。它的名字是在类名前加上波浪号~
,没有参数且没有返回值。析构函数的主要任务是释放对象在生命周期中占用的资源,例如动态分配的内存、打开的文件句柄等。
析构函数和构造函数形成了一个完整的生命周期管理机制,确保对象的创建和销毁过程一致性和安全性。
1.函数命名:析构函数的名字是在类名前加上
~
,例如,类Stack
的析构函数为~Stack()
。 2.无参且无返回值:析构函数没有参数,也不需要返回类型。 3.自动调用:当对象超出其作用域或被显式删除(使用delete
)时,析构函数会被自动调用。 4.唯一性:一个类只能有一个析构函数。如果没有显式定义,系统会自动生成一个默认析构函数。 5.编译器生成的析构行为:
6.显式定义的析构函数:
7.手动编写析构函数的必要性:
8.析构顺序:在局部作用域中,多个对象按定义的逆序进行析构(后定义的先析构)。
例如:
class Stack {
public:
Stack(int n = 4) {
_array = new int[n];
_capacity = n;
_top = 0;
}
~Stack() {
delete[] _array;
}
private:
int* _array;
size_t _capacity;
size_t _top;
};
在这个例子中,~Stack()
析构函数用于释放构造函数中动态分配的内存。如果没有这个析构函数,当对象销毁时,动态分配的内存无法释放,就会导致内存泄漏。
析构函数在以下情况下会被调用:
main()
函数中定义的局部对象在函数结束时会被自动销毁。
delete
销毁一个对象时,析构函数会被调用。
std::vector
或其他容器销毁其持有的对象时,它们也会调用相应对象的析构函数。
析构函数对于管理动态内存和其他系统资源非常重要。例如,如果类中包含指向堆内存的指针,而我们没有实现自定义的析构函数,则该指针所指向的内存不会被释放,从而导致内存泄漏。因此,任何涉及到动态内存分配的类,几乎都需要实现一个自定义的析构函数。
拷贝构造函数用于通过已有对象创建新对象。拷贝构造函数的主要目的是使新对象具有与原对象相同的状态。
比如说,你有一个日历日期对象 Date
,想要再创建一个新的 Date
,内容和原来的日期一样,这时就需要用到拷贝构造函数。
#include <iostream>
using namespace std;
class Date {
public:
// 普通构造函数,用年、月、日来创建日期对象
Date(int year, int month, int day) : _year(year), _month(month), _day(day) {}
// 拷贝构造函数,用一个已有的Date对象d来创建新的Date对象
Date(const Date& d) : _year(d._year), _month(d._month), _day(d._day) {}
// 打印日期的方法
void Print() const {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main() {
Date date1(2024, 10, 21); // 用普通构造函数创建日期对象
Date date2 = date1; // 用拷贝构造函数创建一个新的Date对象,内容和date1一样
// 打印两个日期对象
date1.Print(); // 输出:2024/10/21
date2.Print(); // 输出:2024/10/21
return 0;
}
这里,拷贝构造函数Date(const Date& d)
通过已有的对象d
来初始化新的对象d2
。
1.构造函数重载:拷贝构造函数是构造函数的一种重载。 2.参数要求:第一个参数必须是类类型对象的引用,不能用传值方式,否则会引发无限递归。可以有多个参数,但第一个必须是引用,后面的参数要有默认值。 3.调用场合:拷贝构造在传值传参和传值返回时都会被调用。 4.默认生成:如果没有显式定义,编译器会生成默认的拷贝构造,对内置类型执行浅拷贝,对自定义类型调用其拷贝构造。 5.使用场景:
6.实现建议:如果类有析构函数处理资源,通常也需要自己写拷贝构造。 7.传值返回与引用:
拷贝构造函数通常在以下几种情况下调用:
对象按值传递时:
void Func(Date d) {
d.Print();
}
Date d1(2024, 5, 12);
Func(d1); // 调用拷贝构造函数
在将对象d1
传递给函数Func
时,d1
按值传递,因此会调用拷贝构造函数。
对象按值返回时:
Date CreateDate() {
Date d(2024, 5, 12);
return d; // 返回时调用拷贝构造函数
}
在函数返回对象时,会调用拷贝构造函数。
用已有对象初始化新对象时:
Date d1(2024, 5, 12);
Date d2 = d1; // 调用拷贝构造函数
示例代码:实现深拷贝
以下是一个 Stack
类的示例,实现了深拷贝:
#include <iostream>
#include <cstring> // 用于memcpy
using namespace std;
class Stack {
public:
// 构造函数,初始化栈
Stack(int n = 4) {
_array = new int[n]; // 分配内存
_capacity = n;
_top = 0;
}
// 拷贝构造函数,实现深拷贝
Stack(const Stack& other) {
_array = new int[other._capacity]; // 分配新内存
_capacity = other._capacity;
_top = other._top;
memcpy(_array, other._array, sizeof(int) * _top); // 复制数据
}
// 析构函数,释放内存
~Stack() {
delete[] _array;
}
private:
int* _array; // 指向栈的数组
size_t _capacity; // 栈的容量
size_t _top; // 栈顶位置
};
代码要点
构造函数
Stack(int n = 4)
:
new
分配内存来存放整数数组,_capacity
用来存储栈的容量,_top
表示栈顶的位置(初始为0)。
拷贝构造函数 Stack(const Stack& other)
:
Stack
对象创建新的 Stack
对象时,这个构造函数会被调用。首先,为新对象分配一块和原对象 _capacity
大小相同的内存。然后,将原对象的 _capacity
和 _top
的值复制给新对象。使用 memcpy
函数,将原对象 _array
中的数据复制到新对象的 _array
中。这一步是深拷贝的关键,因为它确保了新对象和原对象有独立的内存空间。
析构函数 ~Stack()
:
Stack
对象,析构函数在对象生命周期结束时自动调用。
为什么要用深拷贝? 当类中包含指针成员(如动态分配的内存)时,必须使用深拷贝,否则会出现多个对象共享同一块内存的情况。这可能导致程序出错或崩溃,特别是在析构时释放内存时。如果类只包含内置类型成员(如
int
、double
),那么默认的浅拷贝就足够了。
C++支持运算符重载,使得自定义类型可以像内置类型一样使用运算符。例如,可以为自定义类重载+
、-
、=
等运算符,使这些类对象能与内置类型有类似的操作体验。运算符重载的目的是提高代码的可读性和简洁性,让代码更自然地表达程序的意图。
默认情况下,C++对对象进行赋值时,编译器会执行“浅拷贝”,即按成员逐个复制。这在类中仅包含内置类型成员时没问题,但如果类中有指针成员,浅拷贝会导致两个对象共享同一块内存资源,可能会引发内存管理问题,例如重复释放同一块内存。因此,针对有动态内存分配的类,我们需要重载赋值运算符,以实现“深拷贝”。
赋值运算符重载的实现示例
#include <cstring> // 为了使用memcpy函数
class Stack {
public:
// 构造函数,初始化栈,默认容量为4
Stack(int n = 4) {
_array = new int[n]; // 为栈分配内存
_capacity = n; // 设置栈的容量
_top = 0; // 初始化栈顶位置为0,表示栈为空
}
// 赋值运算符重载,用于将一个已有的Stack对象赋值给另一个Stack对象
Stack& operator=(const Stack& other) {
if (this != &other) { // 检查是否自赋值(避免自己给自己赋值)
delete[] _array; // 释放已有的内存资源,防止内存泄漏
_array = new int[other._capacity]; // 分配新的内存空间
_capacity = other._capacity; // 复制原对象的容量
_top = other._top; // 复制原对象的栈顶位置
memcpy(_array, other._array, sizeof(int) * _top); // 复制原对象的数据
}
return *this; // 返回当前对象的引用,以支持链式赋值
}
// 析构函数,释放栈的内存资源
~Stack() {
delete[] _array; // 释放分配的内存,防止内存泄漏
}
private:
int* _array; // 动态数组,用于存储栈的元素
size_t _capacity; // 栈的最大容量
size_t _top; // 当前栈顶位置(表示栈中的元素个数)
};
在这个示例中,重载了赋值运算符以确保在赋值时正确处理动态内存,并避免内存泄漏或重复释放的错误。自赋值检查 (if (this != &other)
) 可以避免在赋值给自己时发生内存问题。
Date.h
#pragma once
#include<iostream>
#include<stdbool.h>
#include<assert.h>
using namespace std;
class Date
{
// 友元声明,使得非成员函数可以访问私有成员
friend ostream& operator<<(ostream& out, const Date& d); // 重载输出运算符 <<
friend istream& operator>>(istream& in, Date& d); // 重载输入运算符 >>
public:
// 构造函数,提供默认参数用于初始化年、月、日
Date(int year = 2000, int month = 1, int day = 1); // 构造函数声明,默认值在这里给出
// 打印日期
void Print();
// 检查日期是否合法
bool CheckDate() {
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month)) {
return false; // 如果月份或日期超出合理范围,返回false
} else {
return true; // 日期有效,返回true
}
}
// 获取指定月份的天数
int GetMonthDay(int year, int month) {
// 定义每个月份的天数,数组下标从1开始,0元素为-1占位
static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
// 判断是否为闰年,且月份为2月,如果是,返回29天
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
return 29;
}
return monthDayArray[month]; // 否则返回对应月份的天数
}
// 比较运算符重载,比较日期大小
bool operator<(const Date& d);
bool operator>(const Date& d);
bool operator<=(const Date& d);
bool operator>=(const Date& d);
bool operator==(const Date& d);
bool operator!=(const Date& d);
// 日期增加指定天数,影响当前对象
Date& operator+=(int day);
// 日期增加指定天数,返回新的日期对象
Date operator+(int day);
// 日期减少指定天数,影响当前对象
Date& operator-=(int day);
// 日期减少指定天数,返回新的日期对象
Date operator-(int day);
// 前置自增运算符,增加一天,返回增加后的引用
Date& operator++();
// 后置自增运算符,增加一天,返回增加前的对象(使用int区分后置)
Date operator++(int);
// 前置自减运算符,减少一天,返回减少后的引用
Date& operator--();
// 后置自减运算符,减少一天,返回减少前的对象(使用int区分后置)
Date operator--(int);
// 计算两个日期之间的差距,返回天数
int operator-(const Date& d);
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
// 重载输出运算符,用于输出日期对象
ostream& operator<<(ostream& out, const Date& d);
// 重载输入运算符,用于输入日期对象
istream& operator>>(istream& in, Date& d);
Date.cpp
#define _CRT_SECURE_NO_WARNINGS
#include "Date.h"
// 构造函数 - 初始化年、月、日
Date::Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
// 检查日期是否有效,如果无效则输出提示信息
if (!CheckDate()) {
cout << "日期非法->";
cout << *this; // 输出当前日期对象
}
}
// 打印日期
void Date::Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
// 重载小于运算符 - 比较日期大小
bool Date::operator<(const Date& d) {
if (_year < d._year) {
return true; // 当前年份小于比较对象
} else if (_year == d._year && _month < d._month) {
return true; // 年相等,当前月份小于比较对象
} else if (_year == d._year && _month == d._month && _day < d._day) {
return true; // 年月相等,当前日期小于比较对象
}
return false; // 不满足以上条件,返回false
}
// 重载大于运算符 - 实现为取小于运算符的否定
bool Date::operator>(const Date& d) {
return !(*this < d);
}
// 重载小于等于运算符
bool Date::operator<=(const Date& d) {
return *this < d || *this == d; // 小于或等于则返回true
}
// 重载大于等于运算符
bool Date::operator>=(const Date& d) {
return !(*this < d); // 取小于的反面
}
// 重载等于运算符 - 检查年、月、日是否均相等
bool Date::operator==(const Date& d) {
return _year == d._year && _month == d._month && _day == d._day;
}
// 重载不等于运算符
bool Date::operator!=(const Date& d) {
return !(*this == d);
}
// 重载+=运算符 - 日期加上指定天数
Date& Date::operator+=(int day) {
if (day < 0) {
return *this -= -day; // 如果天数为负数,则调用-=
}
_day += day; // 增加天数
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month); // 减去当前月份的天数
++_month; // 进入下一个月
if (_month == 13) {
_year++; // 如果月份是13,进入下一年
_month = 1; // 月份重置为1
}
}
return *this; // 返回当前对象的引用
}
// 重载+运算符 - 返回新的日期对象
Date Date::operator+(int day) {
Date tmp(*this); // 创建副本,避免修改当前对象
tmp += day; // 使用+=实现
return tmp;
}
// 重载-=运算符 - 日期减去指定天数
Date& Date::operator-=(int day) {
if (day < 0) {
return *this += -day; // 如果天数为负数,则调用+=
}
_day -= day; // 减去天数
while (_day <= 0) {
--_month; // 进入上一个月
if (_month == 0) {
_year--; // 如果月份是0,进入上一年
_month = 12; // 月份设为12
}
_day += GetMonthDay(_year, _month); // 补足当前月的天数
}
return *this; // 返回当前对象的引用
}
// 重载-运算符 - 返回新的日期对象,日期减去指定天数
Date Date::operator-(int day) {
Date tmp(*this); // 创建副本
tmp -= day; // 使用-=实现
return tmp;
}
// 重载前置++运算符 - 日期加1天,返回自身
Date& Date::operator++() {
*this += 1;
return *this;
}
// 重载后置++运算符 - 日期加1天,返回旧值
Date Date::operator++(int) {
Date tmp(*this); // 创建副本,保存当前状态
*this += 1; // 增加1天
return tmp; // 返回旧的对象
}
// 重载前置--运算符 - 日期减1天,返回自身
Date& Date::operator--() {
*this -= 1;
return *this;
}
// 重载后置--运算符 - 日期减1天,返回旧值
Date Date::operator--(int) {
Date tmp(*this); // 创建副本,保存当前状态
*this -= 1; // 减少1天
return tmp; // 返回旧的对象
}
// 重载-运算符 - 返回两个日期之间的天数差
int Date::operator-(const Date& d) {
Date max = *this; // 假定当前日期为较大者
Date min = d;
int flag = 1; // 标志符号,表示方向
if (*this < d) {
max = d;
min = *this;
flag = -1; // 如果当前日期小于比较日期,调整符号
}
int n = 0; // 用于计数两个日期之间的天数
while (min != max) {
++min; // 将较小的日期逐步增加
++n; // 计数增加
}
return n * flag; // 返回带有方向的天数差
}
// 重载输出运算符 - 输出日期信息
ostream& operator<<(ostream& out, const Date& d) {
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
// 重载输入运算符 - 输入日期信息
istream& operator>>(istream& in, Date& d) {
while (1) {
cout << "请依次输入年月日:>";
in >> d._year >> d._month >> d._day;
if (d.CheckDate()) {
break; // 如果日期有效,退出循环
} else {
cout << "日期非法,请重新输入"; // 如果无效,要求重新输入
}
}
return in;
}
test.cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include"Date.h"
using namespace std;
int main()
{
Date d1(2024, 10, 21);
d1.Print();
//Date d2 = d1 + (-100);
//d2.Print();
//d1 += -100;
//d1.Print();
//++d1;
//d1.Print();
//d1++;
//d1.Print();
Date d2(2024, 12, 16);
//cout << d1 - d2 << endl;
cout << d2 - d1 << endl;
cout << d1;
operator<<(cout, d1);
cin >> d1>>d2;
cout << d1 << d2 << endl;
return 0;
}
const
成员函数const
成员函数用于保证函数内部不能修改对象成员变量。例如:
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}
void Print() const { // 该函数不能修改对象的状态
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
可以重载取地址运算符 &
来控制取对象地址的行为,例如不希望外部获取对象的地址:
class Date {
public:
Date* operator&() {
return nullptr; // 返回空指针,隐藏真实地址
}
const Date* operator&() const {
return nullptr;
}
private:
int _year;
int _month;
int _day;
};
通过这篇博客,我们讨论了C++类的默认成员函数、构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符的重载。理解这些概念是学习C++面向对象编程的基础,也是管理内存、资源安全的关键。
如果有任何疑问,欢迎在评论区留言交流!