跳到主要内容C++ 类的默认成员函数详解及日期类实现 | 极客日志C++
C++ 类的默认成员函数详解及日期类实现
综述由AI生成C++ 类的默认成员函数包括拷贝构造函数、赋值运算符重载、取地址运算符重载及 const 成员函数。详细讲解了这些函数的定义、特点及使用场景,重点区分了浅拷贝与深拷贝的区别,并通过完整的日期类(Date)代码示例演示了运算符重载的具体实现,涵盖比较、加减运算及流操作符重载等知识点。
不羁23 浏览 4.拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
拷贝构造的特点:
- 拷贝构造函数是构造函数的一个重载(无返回值)。
- 拷贝构造函数的第一个参数必须是当前类类型对象的引用(最好加上 const,防止权限放大的问题),使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
- C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造函数。
- 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝 (一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
- 像 Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。
像 Stack 这样的类,虽然也都是内置类型,但是_a 指向了资源(动态分配的空间),编译器自动生成的拷贝构造完成的值拷贝/浅拷贝会有两个缺点:(1) 一个对象的修改,会影响另一个对象(比如给 Stack st2 拷贝 Stack st1 的内容,如果对 st1 出栈/入栈操作,会影响 st2);(2) 析构时,释放两次空间(st2 释放_a 指向的空间,st1 会再次释放)。所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的拷贝构造会调用 Stack 的拷贝构造,也不需要我们显示实现 MyQueue 的拷贝构造。
这里还有一个小技巧,如果一个类显示实现了析构并释放资源(说明有资源需要显式释放),那么它就需要显式写拷贝构造,否则就不需要。
- 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名 (引用),没有产生拷贝。
但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; }
Date(Date* d) { _year = d->_year; _month = d->_month; _day = d->_day; }
void { cout << _year << << _month << << _day << endl; }
:
_year;
_month;
_day;
};
{ cout << &d << endl; d.(); }
{
;
tmp.();
tmp;
}
{
;
(d1); cout << &d1 << endl;
; d(); d();
; d();
Date ret = (); ret.();
;
}
Print
()
"-"
"-"
private
int
int
int
void Func1(Date d)
Print
Date& Func2()
Date tmp(2024, 7, 5)
Print
return
int main()
Date d1(2024, 7, 5)
Func1
Date d2(&d1)
1.
Print
2.
Print
Date d3(d1)
2.
Print
Func2
Print
return
0
Stack st4(st3);
Stack st5 = st3;
#include <iostream>
using namespace std;
typedef int STDataType;
class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType)* n);
if (nullptr == _a) {
perror("malloc 申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
Stack(const Stack& st) {
_a = (STDataType*)malloc(sizeof(STDataType)* st._capacity);
if (nullptr == _a) {
perror("malloc 申请空间失败!!!");
return;
}
memcpy(_a, st._a, sizeof(STDataType)* st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(STDataType x) {
if (_top == _capacity) {
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack() {
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue {
public:
private:
Stack pushst;
Stack popst;
};
int main() {
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2 = st1;
MyQueue mq1;
MyQueue mq2 = mq1;
return 0;
}
5.赋值运算符重载
5.1 运算符重载
• 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
• 运算符重载是具有特殊名字的函数,他的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
• 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
• 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的 this 指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
• 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
• 不能通过连接语法中没有的符号来创建新的操作符:比如 operator@。
注意以上 5 个运算符不能重载。(选择题里面常考,大家要记一下)
void func() { cout << "func()" << endl; }
class A {
public:
void func2() { cout << "A::func()" << endl; }
};
int main() {
void (*pf1)() = func;
void (A::*pf2)() = &A::func2;
return 0;
}
• 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x, int y)。
• 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如 Date 类重载 operator- 就有意义,但是重载 operator* 就没有意义。
• 重载 ++ 运算符时,有前置 ++ 和后置 ++,运算符重载函数名都是 operator++,无法很好的区分。C++规定,后置 ++ 重载时,增加一个 int 形参,跟前置 ++ 构成函数重载,方便区分。
• 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this 指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把 ostream/istream 放到第一个形参位置就可以了,第二个形参位置当类类型对象。
#include <iostream>
using namespace std;
int operator+(int x, int y) {
return x - y;
}
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
void Print() { cout << _year << "-" << _month << "-" << _day << endl; }
};
bool operator==(const Date& d1, const Date& d2) {
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
int main() {
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
d1 == d2;
return 0;
}
重载函数作用域在全局时无法访问私有成员变量
有几种方法可以解决:
1、成员放公有
这个方法得不偿失
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
void Print() { cout << _year << "-" << _month << "-" << _day << endl; }
bool operator==(const Date& d) {
return _year == d._year && _month == d._month && _day == d._day;
}
Date& operator++() {
cout << "前置++" << endl;
return *this;
}
Date operator++(int) {
Date tmp;
cout << "后置++" << endl;
return tmp;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2026, 3, 6);
Date d2(2024, 3, 6);
d1.operator==(d2);
d1 == d2;
return 0;
}
5.2 赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
赋值运算符重载的特点:
- 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引用,否则会传值传参会有拷贝
- 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
- 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝 (一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
- 像 Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。
像 Stack 这样的类,虽然也都是内置类型,但是_a 指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝 (对指向的资源也进行拷贝)。
像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的赋值运算符重载会调用 Stack 的赋值运算符重载,也不需要我们显示实现 MyQueue 的赋值运算符重载。
这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
Date(const Date& d) {
cout << " Date(const Date& d)" << endl;
_year = d._year; _month = d._month; _day = d._day;
}
Date& operator=(const Date& d) {
if (this != &d) {
_year = d._year; _month = d._month; _day = d._day;
}
return *this;
}
void Print() { cout << _year << "-" << _month << "-" << _day << endl; }
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2024, 7, 5);
Date d2(d1);
Date d3(2024, 7, 6);
d1 = d3 = d5;
Date d4 = d1;
return 0;
}
6.取地址运算符重载
6.1 const 成员函数
- 将 const 修饰的成员函数称之为 const 成员函数,const 修饰成员函数放到成员函数参数列表的后面。const 不能修饰全局函数(可以理解为没有 this 指针
2.1 const 修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对类的任何成员进行修改。
2.2 const 修饰 Date 类的 Print 成员函数,Print 隐含的 this 指针由
Date* const this 变为 const Date* const this
总结:不修改成员变量的成员函数都应该加上 const,这样普通对象和 const 对象都可以调用这个 const 成员函数
#include <iostream>
using namespace std;
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;
};
int main() {
Date d1(2024, 7, 5);
d1.Print();
const Date d2(2024, 8, 5);
d2.Print();
return 0;
}
6.2 取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和 const 取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。
除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。
class Date {
public:
Date* operator&() {
return this;
}
const Date* operator&() const {
return this;
}
private:
int _year;
int _month;
int _day;
};
int main() {
const Date d1(2025, 9, 1);
const Date* p1 = &d1;
Date d2(2025, 9, 8);
Date* p2 = &d2;
cout << p1 << " " << p2 << endl;
return 0;
}
7. 日期类实现
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;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) const {
Date tmp = *this;
tmp += day;
return tmp;
}
Date& Date::operator+=(int day) {
*this = *this + day;
return *this;
}
Date Date::operator+(int day) {
if (day < 0) {
return *this -= -day;
}
Date tmp(*this);
tmp._day += day;
while (tmp._day > GetMonthDay(tmp._year, tmp._month)) {
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp.month;
if (tmp._month == 13) {
++tmp._year;
tmp._month = 1;
}
}
return tmp;
}
d1-d2(日期相减)
- 最简单直接办法:二者差的年数*365+跨越闰年数+(二者距自己当前年 1 月 1 号差的天数相减)
- 小的日期不断++,直到和大的日期相等,++了多少次,他们就相差多少天
#pragma once
#include <iostream>
using namespace std;
#include <assert.h>
class Date {
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1);
void Print() const;
int GetMonthDay(int year, int month) const {
assert(month > 0 && month < 13);
static int monthDayArray[13] = {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
return 29;
} else {
return monthDayArray[month];
}
}
bool CheckDate();
bool operator<(const Date& d) const;
bool operator<=(const Date& d) const;
bool operator>(const Date& d) const;
bool operator>=(const Date& d) const;
bool operator==(const Date& d) const;
bool operator!=(const Date& d) const;
Date& operator+=(int day);
Date operator+(int day) const;
Date& operator-=(int day);
Date operator-(int day) const;
int operator-(const Date& d) const;
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
#include "Date.h"
bool Date::CheckDate() {
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month)) {
return false;
} else {
return true;
}
}
Date::Date(int year, int month, int day) {
_year = year; _month = month; _day = day;
if (!CheckDate()) {
cout << "日期非法" << endl;
}
}
void Date::Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
bool Date::operator==(const Date& d) const {
return _year == d._year && _month == d._month && _day == d._day;
}
bool Date::operator<(const Date& d) const {
if (_year < d._year) {
return true;
} else if (_year == d._year) {
if (_month < d._month) {
return true;
} else if (_month == d._month) {
return _day < d._day;
}
}
return false;
}
bool Date::operator<=(const Date& d) const {
return *this < d || *this == d;
}
bool Date::operator>(const Date& d) const {
return !(*this <= d);
}
bool Date::operator>=(const Date& d) const {
return !(*this < d);
}
bool Date::operator!=(const Date& d) const {
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;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) const {
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) {
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) const {
Date tmp = *this;
tmp -= day;
return tmp;
}
Date& Date::operator++() {
*this += 1;
return *this;
}
Date Date::operator++(int) {
Date tmp(*this);
*this += 1;
return tmp;
}
Date& Date::operator--() {
*this -= 1;
return *this;
}
Date Date::operator--(int) {
Date tmp(*this);
*this -= 1;
return tmp;
}
int Date::operator-(const Date& d) const {
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d) {
max = d;
min = *this;
flag = -1;
}
int day = 0;
while (min != max) {
++min;
++day;
}
return day * 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 << "日期非法,请重新输入:" << endl;
}
}
return in;
}
#include "Date.h"
void TestDate1() {
Date d1(2025, 9, 8);
Date d2 = d1 + 2000;
d1.Print(); d2.Print();
Date d3(2024, 4, 14);
Date d4 = d3 - 5000;
d3.Print(); d4.Print();
Date d5(2024, 4, 14);
d5 += -5000;
d5.Print();
}
void TestDate2() {
Date d1(2025, 9, 8);
Date d2 = ++d1;
d1.Print(); d2.Print();
Date d3 = d1++;
d1.Print(); d3.Print();
Date d5 = --d1;
Date d4 = d1--;
}
void TestDate3() {
Date d1(2026, 3, 4);
Date d2(2026, 7, 15);
int n = d1 - d2;
cout << n << endl;
}
void TestDate4() {
Date d1(2026, 3, 4);
Date d2 = d1 + 3000;
cout << d1; cout << d2;
cin >> d1 >> d2;
cout << d1 << d2;
}
void TestDate5() {
const Date d1(2024, 4, 14);
d1.Print();
Date d2(2024, 4, 25);
d2.Print(); d2 += 100;
d1 < d2; d2 < d1;
}
int main() {
TestDate4();
return 0;
}
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online