类的默认成员函数

类的六个默认成员函数

每当一个新的类被创建时,它就会自动生成6个默认的成员函数,分别为:构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、普通对象取地址运算符重载、const对象取地址运算符重载。
下面分别对这六个默认成员函数进行分析。

1. 构造函数

1.1 概念

以前,我们创建一个类对象的时候,若想要给它进行初始化赋值,必须通过调用一个自定义的成员函数Init来实现,这多少有些麻烦,而构造函数的存在,就是为了避免这种麻烦的。

class Date
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    d1.Init(2024,1,4);
    return 0;
}

构造函数:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

以后我们就可以这么写:

class Date
{
public:
	Date(int year, int month, int day)
	{
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
	int _month;
    int _day;
};
int main()
{
	Date d1(2024,1,4);
	return 0;
}
1.2 构造函数的特性
  1. 函数名与类名相同。(参考上面的代码)

  2. 无返回值。(可以传参,实参在创建对象的时给)

  3. 对象实例化时,编译器自动调用对应的构造函数。

  4. 构造函数可以重载。

    class Date
    {
    public:
        // 1.无参构造函数
        Date()
        {}
        // 2.带参构造函数
        Date(int year, int month, int day)
        {
            _year = year;
            _month = month;
            _day = day;
        }
    private:
        int _year;
        int _month;
        int _day;
    };
    void TestDate()
    {
        Date d1; 				// 调用无参构造函数
        Date d2(2015, 1, 1); 	// 调用带参的构造函数
        Date d3(); // 错误!编译器无法区分d3是对象还是函数名!
    }
    

    注意:调用无参构造函数时,类对象后面不要加括号,否则就变成函数的声明了!

  5. C++编译器会自动生成一个无参的默认构造函数。
    如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成 (就是我们不写它就自己生成,我们写它就不自己生成)

    自己生成的默认构造函数,有两个功能:

    1. 对内置类型不做处理
      如int、char等等
    2. 对自定义类型,会调用它们自己的默认构造函数
      如我们写的一些类等等

    有些编译器会把内置类型也做处理,但这不是C++的性质,是编译器自己干的,别的编译器就不一定了。

  6. 默认构造函数只能有一个。
    无参的构造函数和全缺省的构造函数都称为默认构造函数,并且==默认构造函数只能有一个==。注意:无参构造函数全缺省构造函数、我们没写从而编译器默认生成的构造函数,这三者都可以认为是默认构造函数。(不传参的都是默认构造函数。)

    一个无参构造函数和全缺省的构造函数,是能构成函数重载,但是编译器无法区分,所以不能把它们一起写。

  7. 可以给内置类型成员加缺省值。
    一开始默认构造函数对内置类型不做处理,然后c++11的补丁,可以给内置类型成员加缺省值(不是初始化!!)缺省值是给初始化列表使用的。

    class Date
    {
        private:
        // 基本类型(内置类型)
        int _year = 1970;
        int _month = 1;
        int _day = 1;
    
        // 自定义类型
        Time _t;
    };
    

    比如上面的基本类型,都给了缺省值!(注意,不是对成员变量进行初始化!只有实例化之后才叫成员变量的初始化)

1.3 注意点
  1. 一般情况下,构造函数都需要我们自己写,尤其是有内置类型成员,更不能让编译器自己生成了。

​ a. 除非内置类型成员都有缺省值,且给的值都符合我们的要求。且自定义类型有我们写了的默认构造函数,就可以考虑让编译器自己生成。

​ b. 全部是自定义类型成员时,且它们都有我们写了的默认构造函数(我们写的不传参的构造函数),就可以考虑让编译器自己生成。

上面只是常规情况,以后复杂的情况要更复杂的处理或者随机应变。

2. 析构函数

2.1 概念

析构函数:与构造函数功能相反,析构函数不是完成对象本身的销毁,局部对象的销毁工作是由编译器完成的,而析构函数是完成对象中资源的清理工作,它是对象在销毁时自动调用的。(它不是销毁对象本身,而是销毁对象的一系列操作过程中所开辟的空间,这些空间无法由编译器自动销毁,需要析构函数来执行销毁的工作)

2.2 特性
  1. 析构函数名是在类名前面加上~

  2. 无参数、无返回值类型。

  3. 一个类只能有一个析构函数。若未显式定义(我们没有手动定义),编译器会自动生成默认析构函数。

  4. 对象生命周期结束时,C++编译器会自动调用析构函数。

    typedef int DataType;
    class Stack
    {
    public:
        Stack(size_t capacity = 4)	//构造函数
        {
            _array = (DataType*)malloc(sizeof(DataType) * capacity);
            if (NULL == _array)
            {
                perror("malloc申请空间失败!!!");
                return;
            }
            _capacity = capacity;
            _size = 0;
        }
        void Push(DataType data)
        {
            // CheckCapacity();
            _array[_size] = data;
            _size++;
        }
    
        // 其他...
    
        ~Stack()	// 析构函数
        {
            if (_array)
            {
                free(_array);
                _array = NULL;
                _capacity = 0;
                _size = 0;
            }
        }
    private:
        DataType* _array;
        int _capacity;
        int _size;
    };
    void TestStack()
    {
        Stack s;
        s.Push(1);
        s.Push(2);
    }
    
  5. C++编译器会自动生成默认析构函数。
    和默认构造函数一样,若我们没有手动定义析构函数,编译器会自动生成一个默认析构函数。
    默认析构函数有以下两种功能:

    1. 内置类型成员变量不做处理。
      如int、char等
    2. 自定义类型会调用它自己的析构函数。
      如我们定义的类
  6. 什么时候可以不写析构函数?
    当类中没有申请资源时,比如Date类,可以直接用编译器默认生成的默认析构函数。
    当类中有申请资源时,比如Stack类,一定要自己写析构函数,否则会造成资源泄露。

2.3 注意点
  1. 析构函数和构造函数类似,但是不能重载。
  2. 一般情况下,有动态申请资源,就需要我们自己写析构函数释放资源;没有动态申请资源时,一般可以不用写。
  3. 需要释放资源的成员都是自定义类型,如果某个自定义类型的析构函数也符合第2点,那么这个自定义类型也可以不手动写析构函数。
  4. 上面的特性只是常规情况,若有复杂情况也是正常的,具体的处理方法需要针对具体的情况而随机应变。

3. 拷贝构造函数

3.1 概念

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

3.2 特性
  1. 拷贝构造函数是构造函数的一个重载形式。

  2. 拷贝构造函数的参数只有一个,且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
    C++规定,传参、赋值时,内置类型直接拷贝,自定义类型必须调用它的拷贝构造函数来完成拷贝。
    在这里插入图片描述

    上图中,d1作为参数,因为是传值的方式,const Date date中的date是一个临时变量,d1会赋值给date(传参时若传值,形参是一个临时变量,实参把自己的值拷贝给形参)。而d1赋值给date,又会调用拷贝构造函数,变成Date date(d1),后面就会无限循环下去。
    如果d1传参是传引用的话,就不需要创建一个临时变量并把值拷贝给它了,也就不会再调用拷贝构造函数,不会无限循环了。

  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。(仅仅是把值拷贝过来,申请的空间资源不会拷贝过来)
    默认拷贝构造函数的两个功能:

    1. 内置类型成员完成值拷贝/浅拷贝
    2. 自定义类型成员调用它自己的拷贝构造函数

    浅拷贝的问题:

    1. 如果有个数组,浅拷贝仅仅是创建两个指针,但是都指向同一块空间,可能会析构两次。
    2. 在上面这种情况下, 修改一个可能会影响另一个。

    因此,有申请资源的类一般都要深拷贝。

  4. 拷贝构造函数典型的调用场景:

    • 使用已存在对象创建新的对象
    • 函数参数类型为类类型对象
    • 函数返回值类型为类类型对象
  5. 示例:

    class Date
    {
    public:
        Date(int year, int month, int day) // 构造
        {
            cout << "Date(int,int,int):" << this << endl;
        }
        
        Date(const Date& d)	// 拷贝构造
        {
            cout << "Date(const Date& d):" << this << endl;
        }
        
        ~Date()
        {
            cout << "~Date():" << this << endl;
        }
    private:
        int _year;
        int _month;
        int _day;
    };
    
    int main()
    {
        Date d1(2024,1,4);
        Date d2(d1);
        return 0;
    }
    

4. 赋值运算符重载

4.1 运算符重载概念

要说赋值运算符重载,我们得先知道什么是运算符重载,其实它和之前谈到的函数重载有很多相似之处。

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

函数的名字为:关键字operator + 需要重载的运算符符号

函数原型:返回类型 operator 操作符(参数列表)

注意点:

  1. 不能通过连接其他符号来创建新的操作符,比如:operator@。只能重载平时常见的操作符:+ - * / % = += -= *= /= %= ++(前置或后置) --(前置或后置) 等等。

  2. 重载操作符(重载函数的参数)必须有一个类类型参数。

  3. 用于内置类型的运算符,其含义不能改变,比如:内置类型整型的+,不能改变其含义。

  4. 运算符重载作为类的成员函数时,其形参看起来要比操作数数量少1个,因为类成员函数的第一个参数一定是隐藏的this指针。

    class Date
    {
    public:
        Date(int year, int month, int day)
        {
            _year = year;
            _month = month;
            _day = day;
        }
        
        bool operator<(const Date& x1, const Date& x2)
        {
            // ...
        }
        
    private:
        int _year;
        int _month;
        int _day;
    };
    
    int main()
    {
        Date d1(2024,1,6);
        Date d2(2024,1,7);
        if(d1 < d2)
            cout << "d1 < d2" << endl;
        else
    		cout << "d1 >= d2" << endl;
        return 0;
    }
    

    上面的代码中运算符<的重载即是错误的写法!
    该函数应该这么写:

    // ...
    	bool operator<(const Date& x)
        {
            // ...
        }
    // ...
    

    分析:代码中的d1 < d2,被编译器转换成了这样:operator<(d1, d2),也就是直接去调用重载函数了。而因为这个运算符重载函数是类成员函数,所以编译器把运算符左边的那个对象的地址变成this指针,&d1就是隐藏的this指针,右边的对象就是我们写的x。所以当一个运算符重载需要两个操作数时,我们编写运算符重载函数的时候只需写一个形参即可,另一个是隐藏的this指针。如果我们多写了,那么编译器会报错,因为形参数量和实参数量不匹配!

  5. 以下5个运算符不能重载,笔试选择题中经常出现:.* :: sizeof ?: .

  6. 当代码中遇到运算符时,内置类型就转换成对应指令,自定义类型就转换成调用运算符重载函数。

  7. 后置++--与它们的前置构成重载,那么怎么区分前置和后置呢?

    Date& operator++(); // 前置++
    Date& operator++(int); // 后置++
    

    编译器为了区分前置和后置,默认正常写的是前置(++--),而对于后置,编译器会自动传递一个整型为参数,来与前置构成重载。所以我们写函数的时候,在括号里写上一个int即可。

  8. 流插入(cin >> d1)运算符重载作为成员函数,d1在运算符的右边,而this指针从来都只是运算符左边那个,也就是说&d1不是this指针,&cin变成this指针了,这样是不行的,cin并不是类的类型,不能与默认this指针匹配,因此我们不能把流插入运算符重载写成类的成员函数,必须要写在类外面。然后通过友元函数来访问类的内部。

4.2 赋值运算符重载
  1. 概念
    赋值运算符重载:已经存在的两个对象之间的复制拷贝(是6个默认成员函数之一)。

与拷贝构造区分:用一个已经存在的对象初始化另一个对象。
2. 有返回值,返回*this,且返回要用引用
3. 要检查是否自己给自己赋值
4. 赋值运算符重载只能重载成类的成员函数,不能在全局域中重载
因为:赋值运算符重载是默认成员函数,当我们不写的时候会默认生成,如果在全局中重载赋值运算符,那么它就会与类中默认生成的产生冲突。
5. 当用户没有显式实现时,编译器会自动生成一个默认运算符重载函数
默认生成的赋值运算符重载有两种功能:

1. 对于内置类型,直接赋值(浅拷贝)。
2. 对于自定义类型,会调用它的赋值运算符重载函数。

5. 取地址操作符重载和const取地址操作符重载

对于这两个默认成员函数,我们一般不需要自己手动定义,使用编译器默认生成的即可。
只有在一些特殊的情况下才需要我们重新定义,比如想让别人获取到指定的内容。

Date* operator&()				// 普通对象
{
    return this;
}
const Date* operator&() const	// const对象
{
    return this;
}

当我们不想让别人取到this指针的地址时,就可以自己手动定义。

6. const成员

我们之前已经谈及const成员函数,就是在成员函数的后面加上一个const,其修饰的是隐藏的this指针,使Date* const this变成const Date* const this,从原来的不能改变this指向,只能改变this指向的内容既不能改变this指向,也不能改变this指向的内容

在下方实现的日期类中就有很多个const成员函数。

const我们并不陌生,不过需要注意的是:const修饰会使得变量的权限变小,对于const修饰的变量,我们不能将其权限变大。
比如:

  1. const对象不能调用非const的成员函数
    非const的成员函数可能会用到一些const对象不允许的范围,传递this指针时权限被放大
  2. 非const对象可以调用const的成员函数
    传递this指针时权限被缩小
  3. const的成员函数不可以调用其他的非const的成员函数
    传递this指针时权限被放大
  4. 非const的成员函数可以调用其他的const的成员函数
    传递this指针时权限被缩小

不过也不是所有的成员都可以加上const,对于那些我们想要修改this指向的内容的成员,是不能加const的。

7. 实现日期类

1. Date.h
#include<iostream>

using namespace std;

class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	// 全缺省构造
	Date(int year = 2024, 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;
	}

	// 赋值运算符重载,对于日期类可以不写

	// 读取日期
	//void Getdate()
	//{
	//	cout << _year << ' ' << _month <<
	//		' ' << _day << endl;
	//}

	// 析构
	~Date(){}	

	// Date的运算符重载
	static int GetMonthDays(int year, int month); 		// 获取该月天数
	bool operator<(const Date& x) const;				// 日期小于
	bool operator==(const Date& x) const;				// 日期等于
	bool operator>(const Date& x) const;				// 日期大于
	bool operator<=(const Date& x) const;				// 日期小于等于
	bool operator>=(const Date& x) const;				// 日期大于等于
	bool operator!=(const Date& x) const;				// 日期不等于
	Date operator+(int day) const;						// 加天数
	Date& operator+=(int day);							// 加等于天数
	Date& operator++();									// 加一天(前置)
	Date operator++(int);								// 加一天(后置)
	Date& operator-=(int day);							// 减等于天数
	Date operator-(int day) const;						// 减天数
	Date& operator--();									// 减一天(前置)
	Date operator--(int);								// 减一天(后置)
	int operator-(const Date& d) const;					// 日期相减
	
private:
	int _year;
	int _month;
	int _day;

};

ostream& operator<<(ostream& out, const Date& d);	// 流插入
istream& operator>>(istream& in, Date& d);			// 流提取
2.Date.cpp
#include"Date.h"

// 获取该月天数
int Date::GetMonthDays(int year, int month)
{
	char Days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	int ret = 0;
	if (((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) && month == 2)
		ret++;
	ret += Days[month];
	return ret;
}

// 日期小于
bool Date::operator<(const Date& x) const
{
	// 记住,要比的是 *this < x
	if (_year > x._year)
		return false;
	else if (_year == x._year && _month > x._month)
		return false;
	else if (_year == x._year && _month == x._month && _day >= x._day)
		return false;
	else
		return true;
}

// 日期等于
bool Date::operator==(const Date& x) const
{
	if (_year == x._year && _month == x._month && _day == x._day)
		return true;
	else
		return false;
}

// 日期大于
bool Date::operator>(const Date& x) const
{
	return !(*this < x || *this == x);
}

// 日期小于等于
bool Date::operator<=(const Date& x) const
{
	return !(*this > x);
}

// 日期大于等于
bool Date::operator>=(const Date& x) const
{
	return !(*this < x);
}

// 日期不等于
bool Date::operator!=(const Date& x) const
{
	return !(*this == x);
}

// 加等于天数
Date& Date::operator+=(int day)
{
	_day += day;
	int days_tmonth = GetMonthDays(_year, _month);
	while (_day > days_tmonth)
	{
		_day -= days_tmonth;
		_month++;
		if (_month > 12)
		{
			_year++;
			_month = 1;
		}
		days_tmonth = GetMonthDays(_year, _month);
	}
	return *this;
}

// 加天数
Date Date::operator+(int day) const
{
	Date d(*this);
	d += day;
	return d;
}

// 前置++
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

// 后置++
Date Date::operator++(int)
{
	Date d = *this;
	*this += 1;
	return d;
}

// 减天数
Date Date::operator-(int day) const
{
	Date d = *this;
	d -= day;
	return d;
}

// 减等天数
Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day < 1)
	{
		_month--;
		if (_month < 1)
		{
			_year--;
			_month = 12;
		}
		_day += GetMonthDays(_year, _month);
	}

	return *this;
}

// 减一天(前置)
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

// 减一天(后置)
Date Date::operator--(int)
{
	Date d = *this;
	*this -= 1;
	return d;
}

// 日期相减
int Date::operator-(const Date& d) const
{
	int count = 0;
	Date large = *this;
	Date small = d;
	int sign = 1;
	if (*this == d)
		return 0;
	if (*this < d)
	{
		large = d;
		small = *this;
		sign = -1;
	}

	while (small != large)
	{
		++small;
		count++;
	}
	return count * sign;
}

// 流插入
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

// 流提取
istream& operator>>(istream& in, Date& d)
{
	int year, month, day;
	in >> year >> month >> day;

	// 检查输入的日期是否规范
	while (month < 1 || month > 12 || day < 1 || day > Date::GetMonthDays(year, month))
	{
		cout << "日期不规范!重新输入" << endl;
		in >> year >> month >> day;
	}

	d._year = year;
	d._month = month;
	d._day = day;
	return in;
}
3. Test.cpp
#include"Date.h"

int main()
{
	//Date d1(2025,2,2);
	//Date d2;
	//d2 = d1;

	//Date d1(2024, 1, 8);
	//Date d2 = (d1 + 2);
	//Date d3;
	//d3 += 2;
	//cout << (d1 < d2) << endl;

	Date d1(2024, 12, 23);
	Date d2(2024, 1, 8);
	//cout << (d1 != d2) << endl;
	//d2 = d1 + 1;
	//d1 += 1;
	//d2 = d1 - 1;
	//d1 -= 1;
	//d2 = d1++;
	//d2 = d1--;
	//d2 = --d1;
	//d2 += 52;
	//d2.Getdate();
	//cout << (d1 - d2) << endl;
	cout << d1;
	cin >> d2;
	cout << d2;

	return 0;
}
  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值