文章目录
- 1.类的6个默认成员函数
- 2.构造函数
- 3.析构函数
- 4. 拷贝构造函数
- 5.赋值运算符和运算符重载
- 6.日期类实现
- 7.const成员
- 8.重载流插入<< ,流提取>>
- 1.流插入
- 2.流提取
- 9.取地址及const取地址操作符重载
1.类的6个默认成员函数
空类:也就是什么成员都没有的类。
但事实上,空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
2.构造函数
概念:
构造函数:创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次
#include <iostream>
using namespace std;
class Date
{
public:
// 以下构造函数只能存在一个!!!
// 无参默认构造函数
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
// 全缺省默认构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 编译器生成的默认构造函数
// C++11补丁新增内置类型成员变量(int char double 指针....)在类中声明时可以给默认值,此处仍是声明不是定义!!!
private:
int _year = 0;
int _month = 0;
int _day = 0;
};
// 以下测试函数能通过编译吗?
void Test()
{
Date d1;
}
int main()
{
Test();
return 0;
}
特性:
- 函数名与类名相同,无返回值类型
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载。
- 构造函数的并不是开辟空间创建对象,而是初始化对象
- 如果类中没有显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,而用户显式定义编译器将不再生成
- 不实现构造函数的情况下,编译器会生成默认的构造函数,此默认构造函数对内置类型不起作用,只对自定义类型初始化
- C++11中针对内置类型成员不初始化的缺陷,又打了补丁:内置类型成员变量在类中声明时可以给默认值。
3.析构函数
概念:
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器在栈区上完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作,一般清理的是堆上malloc开辟出来的空间
- 函数名是在类名前加上字符 ~,无参数,无返回值类型
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数,析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类需要动态开辟空间
#include <iostream>
using namespace std;
class stack
{
public:
void Push(const int& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 半缺省构造函数
stack(int capacity = 10)
{
cout << "构造" << endl;
_array = (int*)malloc(10 * sizeof(int));
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = 0;
_capacity = capacity;
}
// 析构函数
~stack()
{
cout << "析构" << endl;
free(_array);
_array = NULL;
_size = 0;
_capacity = 0;
}
// 拷贝构造函数:深拷贝
stack(const stack& st)
{
cout << "拷贝构造" << endl;
_array = (int*)malloc(st._capacity * sizeof(int));
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = st._size;
_capacity = st._capacity;
}
private:
int* _array;
int _size;
int _capacity;
};
int main()
{
stack st1;
stack st2(st1);
return 0;
}
4. 拷贝构造函数
概念:
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰,防止原本对象被修改),在创建对象时,可以创建一个与已存在对象一某一样的新对象
特性:
-
函数名是类名,无返回值类型,参数只有一个且必须是类对象的引用,使用传值方式编译器会报错,因为会引发无穷递归调用
-
拷贝构造函数是构造函数的一个重载形式
-
若未显式定义拷贝构造函数,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数根据对象按内存大小存储,按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
-
类中如果没有涉及资源申请时,拷贝构造函数写不写都可以,一旦涉及到资源申请时,则拷贝构造函数一定要写成深拷贝的,否则默认拷贝构造函数为浅拷贝
// 拷贝构造:浅拷贝
#include <iostream>
using namespace std;
class Date
{
public:
~Date()
{
cout << "~Data()" << endl;
}
Date(int year = 2024, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
cout << "Data()" << endl;
}
/*
拷贝构造函数:浅拷贝
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Data(Date& d)" << endl;
}
*/
private:
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
// c++规定自定义类型都会调用拷贝构造,传引用传参就不会调用拷贝构造了,否则会无限递归
Date d1(2024,1,28);
Date d2(d1);
return 0;
}
#include <iostream>
using namespace std;
class stack
{
public:
void Push(const int& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 半缺省构造函数
stack(int capacity = 10)
{
_array = (int*)malloc(10 * sizeof(int));
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = 0;
_capacity = capacity;
cout << "构造" << endl;
}
// 析构函数
~stack()
{
free(_array);
_array = NULL;
_size = 0;
_capacity = 0;
}
// 拷贝构造函数:深拷贝
stack(const stack& st)
{
_array = (int*)malloc(st._capacity * sizeof(int));
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = st._size;
_capacity = st._capacity;
}
private:
int* _array;
int _size;
int _capacity;
};
int main()
{
stack st1;
stack st2(st1);
return 0;
}
5.赋值运算符和运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针
.* :: sizeof ?: .
注意以上5个运算符不能重载
赋值运算符重载
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :符合连续赋值的含义
赋值运算符只能重载成类的成员函数不能重载成全局函数?
- 赋值运算符的左操作数是被赋值的对象本身,而右操作数是要赋给该对象的值。因此,赋值运算符的操作涉及到两个对象:被赋值对象和赋值对象。如果定义为全局函数,就需要两个参数,而如果定义为成员函数,就只需要一个参数,因为左操作数可以通过this指针访问。
- 如果定义为全局函数,就可能出现二义性,因为编译器会为每个类隐式地定义一个赋值运算符。如果再定义一个全局的赋值运算符,就会导致编译器无法确定调用哪个函数。
- 如果定义为全局函数,就可能破坏类的封装性,因为全局函数无法访问类的私有成员或静态成员。如果要访问这些成员,就需要将全局函数声明为类的友元函数,这样就会增加类的复杂性和耦合性。
- 如果定义为全局函数,就可能导致语义不清,因为赋值运算符通常是类的内在行为,与类的具体实现密切相关。如果将赋值运算符定义为全局函数,就会使得类的实现细节暴露给外部,降低了类的抽象性和可维护性。
为什么运算符重载可以重载成全局函数呢?
- 全局函数可以支持左操作数不是类对象的情况,例如可以将数字和向量对象相乘,而不仅仅是向量对象和数字相乘。
- 全局函数可以避免重载运算符时的二义性,例如如果重载赋值运算符为全局函数,就不会与编译器隐式定义的赋值运算符冲突。
- 全局函数可以提高运算符的灵活性和通用性,例如可以重载输入/输出运算符,使得可以用 cout 和 cin 来输出和输入类对象。
- 全局函数可以保持运算符的对称性,例如如果重载加法运算符为全局函数,就可以同时支持 a+b 和 b+a 的形式,而不需要为每种情况都定义一个成员函数。
6.日期类实现
Date.h:
#pragma once
#include<assert.h>
#include<iostream>
using namespace std;
// 日期类实现
class Date
{
public:
Date(int year = 2024, int month = 1, int day = 1);
~Date();
Date(const Date& d); // 同类型对象进行初始化
// 获取某年某月的天数
int GetMonthDay(int year, int month) const // 直接在类中定义相当于inline内联展开(此函数我们后续需要频繁调用)
{
assert(month > 0 && month < 13);
static int monthday[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };// 数组放到静态区,增加程序效率
// 1.四年一润百年不润 2.四百年一润
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
{
return 29;
}
return monthday[month];
}
/*
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,
实际修饰该成员函数隐含的this指针指向的内容,表明在该成员函数中不能对类的任何成员进行修改
*/
void Print() const
{
cout << _year << '-' << _month << '-' << _day << endl;
}
/*
1. 运算符重载成全局的就需要成员变量是公有的,问题来了,封装性如何保证?友元解决或者干脆重载成类的成员函数
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
*/
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=(const Date& d);
Date operator+(int day) const;
Date& operator+=(int day);
Date operator-(int day) const;
Date& operator-=(int day);
// 前置++运算符重载
Date& operator++();
// 后置++运算符重载
Date operator++(int);
// 前置--运算符重载
Date& operator--();
// 后置--运算符重载
Date operator--(int);
// 流插入 << 重载:返回值目的为了支持连续性
friend ostream& operator<<(ostream& out, const Date& d);
// 流提取 >> 重载:返回值目的为了支持连续性
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
Date.cpp:
#include"Date.h"
// 声明和定义分离
// 构造函数
Date::Date(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
// 深度拷贝构造函数
Date::Date(const Date& d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
// 析构函数
Date::~Date()
{
this->_year = 2024;
this->_month = 1;
this->_day = 1;
}
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)
{
if (_day > d._day)
{
return true;
}
}
}
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=(const Date& d)
{
if (this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
Date Date::operator+(int day) const
{
Date tmp = *this;// 赋值重载拷贝防止原来的日期被改变
tmp._day = tmp._day + day;
int monthday = Date::GetMonthDay(tmp._year, tmp._month);
if (tmp._day <= monthday)
{
return tmp;
}
while (tmp._day > monthday)
{
monthday = Date::GetMonthDay(tmp._year, tmp._month);
tmp._day = tmp._day - monthday;
if (tmp._month < 12)
{
tmp._month++;
}
else
{
tmp._month = 1;
tmp._year++;
}
}
return tmp;
}
Date& Date::operator+=(int day)
{
this->_day = this->_day + day;
int monthday = Date::GetMonthDay(this->_year, this->_month);
if (this->_day <= monthday)
{
return *this;
}
while (this->_day > monthday)
{
monthday = Date::GetMonthDay(this->_year, this->_month);
this->_day = this->_day - monthday;
if (this->_month < 12)
{
this->_month++;
}
else
{
this->_month = 1;
this->_year++;
}
}
return *this;
}
Date Date::operator-(int day) const
{
Date tmp = *this;// 赋值重载拷贝防止原来的日期被改变
tmp._day = tmp._day - day;
int monthday = Date::GetMonthDay(tmp._year, tmp._month);
if (tmp._day > 0)
{
return tmp;
}
while (tmp._day <= 0)
{
if (tmp._month == 1)
{
tmp._month = 12;
tmp._year--;
}
else
{
tmp._month--;
}
monthday = Date::GetMonthDay(tmp._year, tmp._month);
tmp._day = tmp._day + monthday;
}
return tmp;
}
Date& Date::operator-=(int day)
{
this->_day = this->_day - day;
int monthday = Date::GetMonthDay(this->_year, this->_month);
if (this->_day > 0)
{
return *this;
}
while (this->_day <= 0)
{
if (this->_month == 1)
{
this->_month = 12;
this->_year--;
}
else
{
this->_month--;
}
monthday = Date::GetMonthDay(this->_year, this->_month);
this->_day = this->_day + monthday;
}
return *this;
}
// 前置++
Date& Date::operator++()
{
this->_day++;
int monthday = Date::GetMonthDay(this->_year, this->_month);
if (this->_day > monthday)
{
this->_month++;
this->_day = this->_day - monthday;
if (this->_month > 12)
{
this->_month = 1;
this->_year++;
}
}
// 返回的是对象++后的全新的对象
return *this;
}
// 后置++
Date Date::operator++(int)// 注意为何返回值区分了对象本身和对象的引用,在后置++中我们返回的是tmp这个局部变量因此只能返回局部对象的拷贝而不能返回引用
{
Date tmp = *this;
this->_day++;
int monthday = Date::GetMonthDay(this->_year, this->_month);
if (this->_day > monthday)
{
this->_month++;
this->_day = this->_day - monthday;
if (this->_month > 12)
{
this->_month = 1;
this->_year++;
}
}
//返回的是对象++之前的对象的拷贝
return tmp;
}
// 前置--
Date& Date::operator--()
{
this->_day--;
if (this->_day > 0)
{
return *this;
}
else
{
this->_month--;
if (this->_month == 0)
{
this->_month = 12;
this->_year--;
}
int monthday = Date::GetMonthDay(this->_year, this->_month);
this->_day = this->_day + monthday;
return *this;
}
}
// 后置--
Date Date::operator--(int)
{
Date tmp = *this;
this->_day--;
if (this->_day > 0)
{
return tmp;
}
else
{
this->_month--;
if (this->_month == 0)
{
this->_month = 12;
this->_year--;
}
int monthday = Date::GetMonthDay(this->_year, this->_month);
this->_day = this->_day + monthday;
return tmp;
}
}
// 友元函数
//
// 流插入 << 重载
ostream& operator<<(ostream& out, const Date& d)
{
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
// 流提取 >> 重载
istream& operator>>(istream& in, Date& d)
{
cout << "请依次输入年月日:" << endl;
in >> d._year >> d._month >> d._day;
}
7.const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类的成员函数,实际修饰该成员函数隐含的this指针指向的内容,表明在该成员函数中不能对类的任何成员进行修改
读函数:建议加const
写函数:谨慎加const
// 读函数
void Print() const
{
cout << _year << '-' << _month << '-' << _day << endl;
}
// 写函数
// 构造函数
Date::Date(int year, int month, int day) const //错误const加入
{
this->_year = year;
this->_month = month;
this->_day = day;
}
- [const对象不能调用非const成员函数,因为这样会破坏const对象的不可变性,或者导致类型不匹配的错误。如果非要调用非const成员函数,就需要使用const_cast来强制去除const限定]。
- [非const对象可以调用const成员函数,因为这样不会改变非const对象的状态,或者导致类型不匹配的错误。非const对象可以隐式转换为const对象,从而调用const成员函数]。
- [const成员函数不能调用其它的非const成员函数,因为这样会破坏const成员函数的不可变性,或者导致类型不匹配的错误。如果非要调用非const成员函数,就需要对this指针使用const_cast来强制去除const限定]。
- [非const成员函数可以调用其它的const成员函数,因为这样不会改变非const成员函数的状态,或者导致类型不匹配的错误。非const成员函数可以隐式转换为const成员函数,从而调用const成员函数]。
总之,const的调用满足权限可以缩小但不可以放大,也就是说非const修饰的对象或函数可以调用const修饰的对象或函数,但const修饰的对象或函数不可以调用非const修饰的对象或函数。
8.重载流插入<< ,流提取>>
1.流插入
这样写ok么?
// 日期类实现
class Date
{
public:
Date(int year = 2024, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//流插入<<重载成为成员函数
void operator<<(ostream& out)
{
cout << this->_year << "年" << this->_month << "月" << this->_day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,2,1);
//错误运行;
cout<<d1;//cout作为左操作数,d1作为右操作数,d1日期类对象插入到控制台cout中去符合思维,但实际报错
//正确运行:
d1<<cout;//d1作为左操作数,cout作为右操作数,重载成功但是发现逻辑是控制台cout插入到日期类d1中
return 0;
}
本质原因是什么呢?
解释:
<<
作为成员函数重载,this指针占据了第一个参数,意味着日期类(Date)对象必须是左操作数,因此我们要设法让cout作为第一个参数,因此为了实现这个操作符重载,我们不能将它写为成员函数,应当写为全局函数,但是当我们将函数写成全局函数后面临它无法访问私有,因此此处我们要么私有公开为公有,要么使用友元,并且注意全局函数不能在.h头文件中定义,否则链接时候会发生重定义。
第一种方式:
// 流输出 << 重载为全局函数,并且将类对象成员私有公开
void operator<<(ostream& out,const Date& d)
{
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
第二种方式:
设置Get,Set函数获取私有成员的值(不想破坏封装,可以替换掉友元)
第三种方式:
友元函数
friend void operator<<(ostream& out, const Date& d);
问题来了:我们实现的流插入如何支持连续插入呢?cout<<......<<......<<.......<<endl;
与赋值类似,不过结合性顺序相反:
cout<<d1<<d2<<;
中 cout<<d1
为一次函数调用,并且带有一个返回值作为左操作数再次进行流插入。
流插入:
friend ostream& operator<<(ostream& out, const Date& d);
ostream& operator<<(ostream& out, const Date& d)
{
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
为什么C++要支持流插入和自定义重载流插入呢?因为C语言中的
printf
函数无法支持自定义类型直接通过printf
输出,而流插入就很好的解决了所有对象的打印问题,无论是内置类型还是自定义类型。
2.流提取
流提取:
friend istream& operator>>(istream& in, Date& d);
istream& operator>>(istream& in, Date& d)
{
cout << "请依次输入年月日:" << endl;
in >> d._year >> d._month >> d._day;
}
9.取地址及const取地址操作符重载
这两个运算符一般不需要重载,直接取得对象的地址即可,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,一般不需要自己重写