C++类与对象:默认成员函数

文章目录

  • 1.类的6个默认成员函数
  • 2.构造函数
  • 3.析构函数
  • 4. 拷贝构造函数
  • 5.赋值运算符和运算符重载
  • 6.日期类实现
  • 7.const成员
  • 8.重载流插入<< ,流提取>>
    • 1.流插入
    • 2.流提取
  • 9.取地址及const取地址操作符重载

1.类的6个默认成员函数

空类:也就是什么成员都没有的类。

但事实上,空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
C++类与对象:默认成员函数_第1张图片

2.构造函数

概念:

构造函数:创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次

#include 
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++类与对象:默认成员函数_第2张图片

特性:

  • 函数名与类名相同,无返回值类型
  • 对象实例化时编译器自动调用对应的构造函数
  • 构造函数可以重载。
  • 构造函数的并不是开辟空间创建对象,而是初始化对象
  • 如果类中没有显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,而用户显式定义编译器将不再生成
  • 不实现构造函数的情况下,编译器会生成默认的构造函数,此默认构造函数对内置类型不起作用,只对自定义类型初始化
  • C++11中针对内置类型成员不初始化的缺陷,又打了补丁:内置类型成员变量在类中声明时可以给默认值。

3.析构函数

概念:

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器在栈区上完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作,一般清理的是堆上malloc开辟出来的空间

  • 函数名是在类名前加上字符 ~,无参数,无返回值类型
  • 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数,析构函数不能重载
  • 对象生命周期结束时,C++编译系统系统自动调用析构函数
  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类需要动态开辟空间
#include 
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;
}

C++类与对象:默认成员函数_第3张图片

4. 拷贝构造函数

概念:

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰,防止原本对象被修改),在创建对象时,可以创建一个与已存在对象一某一样的新对象

特性:

  • 函数名是类名,无返回值类型,参数只有一个且必须是类对象的引用,使用传值方式编译器会报错,因为会引发无穷递归调用

  • 拷贝构造函数是构造函数的一个重载形式

  • 若未显式定义拷贝构造函数,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数根据对象按内存大小存储,按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

  • 类中如果没有涉及资源申请时,拷贝构造函数写不写都可以,一旦涉及到资源申请时,则拷贝构造函数一定要写成深拷贝的,否则默认拷贝构造函数为浅拷贝

// 拷贝构造:浅拷贝
#include 
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;
}

C++类与对象:默认成员函数_第4张图片

#include 
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;
}

C++类与对象:默认成员函数_第5张图片

5.赋值运算符和运算符重载

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似

  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针
  • .* :: sizeof ?: . 注意以上5个运算符不能重载

赋值运算符重载

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :符合连续赋值的含义

赋值运算符只能重载成类的成员函数不能重载成全局函数?

  • 赋值运算符的左操作数是被赋值的对象本身,而右操作数是要赋给该对象的值。因此,赋值运算符的操作涉及到两个对象:被赋值对象和赋值对象。如果定义为全局函数,就需要两个参数,而如果定义为成员函数,就只需要一个参数,因为左操作数可以通过this指针访问。
  • 如果定义为全局函数,就可能出现二义性,因为编译器会为每个类隐式地定义一个赋值运算符。如果再定义一个全局的赋值运算符,就会导致编译器无法确定调用哪个函数。
  • 如果定义为全局函数,就可能破坏类的封装性,因为全局函数无法访问类的私有成员或静态成员。如果要访问这些成员,就需要将全局函数声明为类的友元函数,这样就会增加类的复杂性和耦合性。
  • 如果定义为全局函数,就可能导致语义不清,因为赋值运算符通常是类的内在行为,与类的具体实现密切相关。如果将赋值运算符定义为全局函数,就会使得类的实现细节暴露给外部,降低了类的抽象性和可维护性。

为什么运算符重载可以重载成全局函数呢?

  • 全局函数可以支持左操作数不是类对象的情况,例如可以将数字和向量对象相乘,而不仅仅是向量对象和数字相乘。
  • 全局函数可以避免重载运算符时的二义性,例如如果重载赋值运算符为全局函数,就不会与编译器隐式定义的赋值运算符冲突。
  • 全局函数可以提高运算符的灵活性和通用性,例如可以重载输入/输出运算符,使得可以用 cout 和 cin 来输出和输入类对象。
  • 全局函数可以保持运算符的对称性,例如如果重载加法运算符为全局函数,就可以同时支持 a+b 和 b+a 的形式,而不需要为每种情况都定义一个成员函数。

6.日期类实现

Date.h:

#pragma once
#include
#include
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;
}
  1. [const对象不能调用非const成员函数,因为这样会破坏const对象的不可变性,或者导致类型不匹配的错误。如果非要调用非const成员函数,就需要使用const_cast来强制去除const限定]。
  2. [非const对象可以调用const成员函数,因为这样不会改变非const对象的状态,或者导致类型不匹配的错误。非const对象可以隐式转换为const对象,从而调用const成员函数]。
  3. [const成员函数不能调用其它的非const成员函数,因为这样会破坏const成员函数的不可变性,或者导致类型不匹配的错误。如果非要调用非const成员函数,就需要对this指针使用const_cast来强制去除const限定]。
  4. [非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<

本质原因是什么呢?

解释:

<< 作为成员函数重载,this指针占据了第一个参数,意味着日期类(Date)对象必须是左操作数,因此我们要设法让cout作为第一个参数,因此为了实现这个操作符重载,我们不能将它写为成员函数,应当写为全局函数但是当我们将函数写成全局函数后面临它无法访问私有,因此此处我们要么私有公开为公有,要么使用友元,并且注意全局函数不能在.h头文件中定义,否则链接时候会发生重定义

C++类与对象:默认成员函数_第6张图片

第一种方式:

// 流输出 << 重载为全局函数,并且将类对象成员私有公开
void operator<<(ostream& out,const Date& d)
{
	cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

C++类与对象:默认成员函数_第7张图片

第二种方式:

设置Get,Set函数获取私有成员的值(不想破坏封装,可以替换掉友元)

第三种方式:

友元函数

friend void operator<<(ostream& out, const Date& d);

C++类与对象:默认成员函数_第8张图片

问题来了:我们实现的流插入如何支持连续插入呢?cout<<......<<......<<.......<

与赋值类似,不过结合性顺序相反:

cout<cout<为一次函数调用,并且带有一个返回值作为左操作数再次进行流插入。

流插入:

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取地址操作符重载

这两个运算符一般不需要重载,直接取得对象的地址即可,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,一般不需要自己重写

你可能感兴趣的:(C++学习专栏,c++)