[baka也能懂C++]关于类必须要知道的六个默认成员函数

                                                                前言

        泥嚎,这里是baka也能懂C++系列,简称b懂Cpp。本篇介绍了六个默认成员函数,并基于前四个函数以及上篇内容实现了日期类,顺带介绍了一下const成员函数。那么咱们开始吧!

目录

1.类的默认成员函数

2.构造函数

        构造函数的特点:

3.析构函数

        析构函数的特点:

4.拷贝构造函数

        拷贝构造函数的特点:

5.赋值/运算符重载

        5.1运算符重载

        5.2赋值运算符重载

        5.3日期类的实现

5.3.1构造、析构、拷贝构造函数的实现

5.3.2赋值/运算符重载

        5.4const成员函数

6.取地址运算符重载

1.类的默认成员函数

        用户没有显式实现,编译器会自动生成的成员函数成为默认成员函数。一个类,在没有写的情况下,编译器会默认生成以下6个成员函数,需要注意的是,最重要的是前四个函数,最后两个取地址重载不重要,稍微了解一下即可。

        而在C++11以后还会增加两个默认成员函数——移动构造和移动赋值,但是还在初学阶段,以后在进行讲解。

        默认成员函数很重要,也比较复杂,baka从两个方面进行学习:

第一:不写时,编译器默认生成的函数行为是什么,是否满足需求

第二:编译器默认生成的函数不满足需求时,需要自己实现,又如何实现?

        接下来就是逐个对默认成员函数进行讲解。

2.构造函数

        构造函数是特殊的成员函数,虽然说名字是叫做构造,但是构造函数的主要任务不是开一个空间,而是对实例化的成员进行初始化

        本质是为了替代baka以前实现数据结构时(比如实现栈)写的Init函数的功能,构造函数会自动调用,完美替代了Init。

        构造函数的特点:

  • 函数名与类名相同
  • 无返回值(不需要给返回值,也不需要写void,C++规定即是如此)
//以日期类为例
class Date
{
public:
	
	//构造函数
	Date(int year , int month , int day );

	
private:
	int _year;
	int _month;
	int _day;

};

Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
  • 对象实例化的时候会自动调用对应的构造函数
//修改构造函数,方便看到现象
Date::Date()
{
	cout << "Date()" << endl;
}

        运行一下以下代码来看看现象。

int main()
{
	Date d1;

	return 0;
}

        结果如下,证明构造函数是能自动调用的。

  • 构造函数可以重载   

//接下来运行这段代码,
int main()
{
    //当构造函数没有参数时,初始化不需要在对象的后面加上()
    //否则就变成了Date d1();这种形式和函数的声明产生歧义
	Date d1;

    //调用的方式不只有d2.Date(2025,5,4),还有在初始化的时候给参数
	Date d2(2025, 5, 4);
	return 0;
}

        从调试界面可以看到两个对象调用的构造函数是不一样的。

  • 如果类中没有显式定义的构造函数,C++编译器会自动生成一个无参的默认构造函数,一旦写出显示定义,编译器将不再生成。

        当注释掉上面两个构造函数,再次运行,可以看见VS生成的默认构造函数没有对内置成员变量进行初始化。

  • 6.默认构造函数有三种:无参构造函数、全缺省构造函数、不写构造时编译器默认生成的构造函数。但三者只能存其一,前两者虽然能够函数重载,但是会产生歧义。可以认为不传参就可以调用的构造叫默认构造

  • 不写,编译器默认生成的构造函数,对内置类型成员变量的初始化没有要求,也就是说,内置类型成员变量的初始化是不确定的,取决于编译器对于自定义类型成员变量,要求调用这个成员变量的默认构造函数,如果这个成员变量没有默认构造函数,那么就会报错,而要初始化自定义类型成员变量,需要用到初始化列表才能解决(下节再讲)

        在上面也可以看到,VS对默认构造函数的处理是不处理。

3.析构函数

        析构函数与构造函数的功能相反,析构函数不是完成对对象本身的销毁,因为当局部变量在栈帧时,出栈帧变量就自动销毁了,就不需要去管。

        C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比之前实现数据结构的destroy功能,也就是释放在堆上创建的变量。

        而像日期类没有destroy,也就是没有资源需要释放,所以可以说日期类不需要析构函数

        析构函数的特点:

  • 析构函数函数名是在类名前加上一个~
  • 无参无返回值(与构造函数类似,也不需要加返回值)
~Date()
{
	cout << "~Date();" << endl;
}
  • 一个类只有一个析构函数若未显示定义,系统会自动生成默认的析构函数
  • 对象的生命周期结束时,系统会自动调用析构函数

        对构造函数和析构函数有以下修改,

//无参构造函数
Date()
{
	cout << "Date();" << endl;
}
//析构函数
~Date()
{
	cout << "~Date();" << endl;
}

        运行下面代码,

int main()
{
	Date d1;

	return 0;
}

        得到以下输出,

  • 跟构造函数类似,不写,编译器自动生成的析构函数对内置类型成员的处理不确定自定义成员类型调用它自己的析构函数
  • 需要注意的是,尽管写了显式的析构函数,对于自定义成员变量还是调用它自己的析构函数,也就是说自定义类型成员无论什么时候都会自动调用析构函数
//写一个baka类
class Baka
{
public:
	Baka()
	{
		cout<< "Baka();" << endl;
	}
	~Baka()
	{
		cout << "~Baka();" << endl;
	}

	int cirno;
};

        将Baka放在日期类中

        再次运行上述代码,得到

  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器默认的析构函数,如Date;如果默认生成的析构就可以用,也就不需要显式写析构(如一个类中包含了一个自定义类型成员,这个自定义类型有写析构函数,那么这个类会自动调用那个成员的析构函数,所以不需要写);是有资源申请的时候,一定要自己写析构,否则会产生内存泄漏
  • 一个局部域的多个对象,C++规定后定义的先析构(就像是堆,后进先出)

        修改构造函数和析构函数

//无参构造函数
Date()
{
	cout <<this << "->" << "Date();" << endl;
}
//析构函数
~Date()
{
	cout <<this<<"->" << "~Date();" << endl;
}

        增加对象的数量运行代码

int main()
{
	Date d1;
	Date d2;
	Date d3;

	return 0;
}

        如下,可见后定义的d3最先调用析构函数,先调用的d1最后调用析构函数。

4.拷贝构造函数

        如果一个构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认值,则此构造函数为拷贝构造函,也就是说拷贝构造函数是一个特殊的构造函数

        拷贝构造函数的特点:

  • 拷贝构造函数是构造函数的一个重载
  • 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器会直接报错,因为语法上会产生无穷递归调用(这和下面的第三点有关)。拷贝构造函数也可以有多个参数,但是第一个参数必须为类类型变量的引用,后面的参数必须有缺省值。
//以日期类为例
//构造函数
Date(int year, int month, int day);
//拷贝构造函数
Date(const Date& d);
  • C++规定,自定义类型对象进行拷贝行为必须要调用拷贝构造,所以这里的自定义类型传值传参和传值返回都会调用拷贝构造完成

        以下面代码为例,        

        首先需要传值给Date类型的形参,此时会调用拷贝构造来进行传值;返回的时候传值,因为形参在出了作用域之后生命周期结束,所以需要创建一个临时变量来返回,这时又调用一次拷贝构造来给临时变量赋值

        如果拷贝构造函数第一个传的是类的类型变量,如下

Date(Date d);

        那么会发生什么呢?当baka调用拷贝构造函数的时候,需要传值,但是传值需要调用拷贝构造函数呱,又需要传值,又调用拷贝构造函数......baka好像鬼打墙了。自此,拷贝构造函数传的只能是自定义类型的引用。

  • 若为显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对于自定义类型成员变量会调用它的拷贝构造。

        值拷贝产生的结果是值相同,也就是说,如果有指针变量,拷贝的时候拷贝的是指针变量里面的地址,而并非是指针变量地址对应的值。

        所以:

对于有空间开辟的类,值拷贝满足不了需求,需要自己写一个深拷贝的拷贝构造函数。

对于没有空间开辟的类,像是Date没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,就不需要显式实现拷贝构造

对于含有自定义类型的类,如果自定义类型有定义拷贝构造,那么编译器生成的拷贝构造会调用自定义类型的拷贝构造,也不需要实现显式的拷贝构造

  小技巧:如果一个类显式实现了析构函数,并且有资源的释放,那么这个类就是需要写显式的拷贝构造函数的,否则就不需要。

  • 传值返回会产生一个临时变量调用拷贝构造;而传值引用返回,返回的是返回对象的别名,没有产生拷贝。

        但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,使用引用返回的时候baka就找不到了,这时的引用相当于一个野引用,类似野指针。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。

        顺带一提,调用拷贝构造函数给初始化的变量赋值的写法如下

//创建一个Date变量,并调用构造函数
Date d1(2025,5,5);
//再创建一个Date变量,调用拷贝构造函数初始化
Date d2(d1);

5.赋值/运算符重载

        5.1运算符重载

        C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。

        运算符重载是具有特殊名字的函数,由operator和后面要定义的运算符共同组成。和其他函数一样,它也具有返回类型和参数列表以及函数体。

        以日期类为例,

//一个+=运算符重载声明
Date& operator+=(int day);

        而这个运算符重载函数代表的意义是:实现日期+天数返回加后的一个日期的引用,

在写这个函数的时候,有两种调用方法:

d.operator+=(2);
d += 2;

        第二种写法符合人类习惯,使用的比较频繁

  • 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。

        但是,+=是二元运算符,缺只传了一个参数,为什么?

        答案是——this指针。如果一个重载运算符函数是成员函数,则它的第一个运算符默认传给隐式的this指针,因此运算符重载作为成员函数时,参数会比运算对象少一个。

        在写运算符重载的时候需要注意:

  1. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
  2. 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
  3. 有五个运算符不能重载: .*    ::    sizeof    ?:    .    。
  4. 重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如int operator+(int x,int y)
  5. ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-(Date d)就有意义,但是重载operator+(Date d)就没有意义

        重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,那么怎么来区分两个++呢?C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。如下,

//前置--
Date& operator--();

//后置--
Date operator--(int);

        重载<<和>>时,在想,这是一个二元运算符,那么应该传什么参呢?

        先来查询一下cout和cin

       

        可以看见,cout和cin分别是ostream类和istream类的对象,那么传参应该就是对象类型的引用了,而且为了做到能够连续输入或者输出,需要返回引用,所以有以下声明(在类内),和实现(类外)

ostream& operator<<(ostream& out);
istream& operator>>(istream& in);

ostream& Date::operator<<(ostream& out)
{
	out << _year << "/" << _month << "/" << _day << endl;
	return out;
}

istream& Date::operator>>(istream& in)
{
	in >> _year >> _month >> _day;
	return in;
}

        那么实际使用起来是怎样的呢

        诶~报错了,我为什么?噢,前面有说过,第一个运算对象传给第一个参数,第二个传给第二个参数。原来的this指针在作祟,导致两个参数传反了!让baka来调整一下。

        现在没有报错了,但是这有违常理,谁家baka会这样写啊!那么有什么解决方案吗?写在全局的话,虽然可以改变顺序,但是需要公开类的成员呱,baka不想公开,不然谁都能访问了口牙。

        这时候就要剧透一点下一期的内容了——友元函数!

        在类内进行友元函数声明,函数就是类的朋友了!看看朋友的成员不过分吧,用下朋友的成员也不过分吧,朋友的东西就是baka的东西!

friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);

        来运行一下修改后的代码吧!

        此时cin和cout就符合正常的使用习惯了。

        5.2赋值运算符重载

        赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另⼀个要创建的对象。

Date d1(2025,5,5);
Date d2(2022, 2, 2);
//调用赋值重载函数,两个变量均初始化
d1 = d2;
//调用拷贝构造函数,d3未初始化
Date d3(d2);

        需要注意的是:

  • 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝。有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。如下声明所示,
Date& operator=(const Date& d);
  • 没有显式实现时,编译器会自动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝,对自定义类型成员变量会调用他的赋值重载函数。

        也就是说和拷贝构造函数类似,

对于有空间开辟的类,值拷贝满足不了需求,需要自己写一个深拷贝的赋值运算符重载函数。

对于没有空间开辟的类,像是Date没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,就不需要显式实现赋值运算符重载函数。

对于含有自定义类型的类,如果自定义类型有定义赋值运算符重载函数,那么编译器生成的赋值运算符重载函数会调用自定义类型的值运算符重载函数,也不需要实现显式的值运算符重载函数。

        5.3日期类的实现

        学习了上述四种默认成员函数,baka就能够逐步实现日期类了!

        在现实用,使用日历的时候,baka可以看今天的日期,可以看几天前或是几天后的日期,比较日期的大小,可以知道哪个日期更加靠前。计算食品保质期还剩多少就相当于计算日期的差值。总而言之baka要大概实现上述功能。

        先来准备工作,定义一个日期类,

class Date
{
//将会在下方放置函数声明
public:

private:
	int _year;
	int _month;
	int _day;
};

        baka不知道哪年哪月有多少天,于是准备了一个函数放在public作用域内(因为在域内默认为inline,而这个函数又要频繁调用)

int GetMonthDay(int year, int month)
{
    //创建一个整型数组,下标为1-12的位置对应1-12月的天数
	static int Month[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
    //先判断是否为2月,不是二月的话就不用判断后面的判断闰年的条件了
	if (month == 2 && ((year % 400 == 0) || (year % 100 != 0 && year % 4 == 0)))
	{
		return 29;
	}
    //返回天数
	return Month[month];
}

        baka希望年月日能直接打印在屏幕上,于是同上,

void Print()
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

        如果有人故意输入了一个错误的日期呢?于是baka同上加了一个判定函数

bool CheckDate()
{
    //确保月在1-12
	if (_month < 1 && _month>12)
	{
		return false;
	}
    //在上述基础上保证日在当月范围内
	if (_day<0||_day > GetMonthDay(_year, _month))
	{
		return false;
	}
	return true;
}

注:以上三个函数都是定义在public作用域内,之后的函数均定义在全局上、声明在public作用域内,定义与声明分离。

5.3.1构造、析构、拷贝构造函数的实现

        构造函数的作用是给自定义变量赋初值,为了确保不传参时的对象能够初始化,给出了全缺省的构造函数,

//全缺省默认构造函数的声明
Date(int year=1999, int month=9, int day=9);

Date::Date(int year, int month, int day)
{
    //赋值
	_year = year;
	_month = month;
	_day = day;
}

          在日期类中,全都是值拷贝,析构和拷贝构造实际上可以不写,但baka就写

//拷贝构造函数声明
Date(const Date& d);
//析构函数声明
~Date();

Date::Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

Date::~Date()
{
	_year = 0;
	_month = 0;
	_day = 0;
}

  5.3.2赋值/运算符重载

        要写运算符重载函数,就要知道什么运算符对对象而言有意义。就比如给对象赋值的=,计算几天前/后+-,比较日期大小的>==<……

  • 赋值运算符重载函数,实现对变量的赋值
//声明
Date& operator=(const Date& d);
//返回this指针保证连续赋值
//如d1=d2=d3,先执行d2=d3,返回d2的值和d1执行d1=d2
Date& Date::operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}

          对于日期来说,有几天前和几天后的概念

  • 重载+/-/+=/-=/--/++,实现日期-天数
//日期+=天数声明
Date& operator+=(int day);

Date& Date::operator+=(int day)
{
    //当天数为负时,进行处理
	if (day < 0)
	{
        //-=会在后面实现
		return *this -= (-day);
	}
    //天数加在日期的日上
	_day += day;
    //当日>当月的天数时,月进位,直到日<当月的天数
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month += 1;
        //当月满的时候年进位
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}
//日期-=天数声明
Date& operator-=(int day);

Date& Date::operator-=(int day)
{
    //当天数为负的时候,交给+=来处理
	if (day < 0)
	{
		return *this += (-day);
	}
    //日减去天数
	_day -= day;
    //当日小于0时,需要借月的天数,直到预知到日为正
	while (_day < 0)
	{
        //月借完了向年借月
		if (_month == 1)
		{
			_month = 13;
			_year--;
		}
		_month--;
        //日加上月的天数
		_day += GetMonthDay(_year, _month);
		
	}
	return *this;
}

        有了+=和-=之后就可以利用他们来完成+/-/++/--的重载了

//日期+天数声明
Date operator+(int day) const;
//日期-天数声明
Date operator-(int day) const;

//因为加不会改变变量所以返回Date
Date Date::operator+(int day) const
{
    //创建一个临时的d
	Date d(*this);
    //利用+=对d改变
	return d += day;
}

//-同理
Date Date::operator-(int day) const
{
	Date tmp(*this);
	return tmp -= day;
}

        前置++和后置++要注意返回值返回的是加前还是加后

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

Date& Date::operator++()
{
    //前置++,返回改变后的对象
	return *this += 1;
}

Date Date::operator++(int)
{    
    //后置++,先保留+前的变量,改变对象,然后返回加前的变量
	Date tmp(*this);
	(*this) += 1;
	return tmp;
}

        前置--和后置--同理,

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

Date& Date::operator--()
{
	return *this -= 1;
}

Date Date::operator--(int)
{
	Date tmp(*this);
	(*this) -= 1;
	return tmp;
}
  • 重载>/</==/>=/<=/!=,实现日期和日期比较大小

        接下来就是对日期的比较了,当有了>和==,基本上就能组合出剩下的重载

//重载>
bool operator>(const Date& d) const;

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;
		}
	}
    //上述情况走完,剩下的情况只有false
	return false;
}
//重载==
bool operator==(const Date& d) const;

bool Date::operator==(const Date& d) const
{
	return _year == d._year && _month == d._month && _day == d._day;
}

        接下来就是借用环节,

//重载>=
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);
}
  • 实现日期-日期,再次重载-
//日期-日期
int operator-(const Date& d) const;

int Date::operator-(const Date& d) const
{
    //flag为符号的正负,默认为正
	int flag = 1;
    //假设*this为大日期d为小日期
	Date max = *this;
	Date min = d;
    //进行矫正,如果假设不对则对调,同时flag标记为负
	if (min>max)
	{
		max = d;
		min = *this;
		flag = -1;
	}
    //计数为0
	int count = 0;
    //小的日期进行++,直到加到与大的日期相等为止
    //count跟着++,结束时的值就是两个日期的差值
	while (min != max)
	{
		++min;
		++count;
	}
    //返回带符号的差值
	return count*flag;
}

        以上就差不多实现了日期类的基本功能。

        5.4const成员函数

        在上面的实现的日期类的函数中,发现有的函数后面加了一个const,这是什么意思呢?const修饰一个变量是为了确保变量的值不能改变,那么const修饰的究竟是谁呢,baka怎么看不到?对了,有一个东西是隐式的,那就是——this指针。

        因为this指针是隐式的,所以不能直接修饰const,于是C++提出了一个解决办法,在函数成员参数列表后面加上一个const表示修饰this指针。就相当于this指针变成了:

                                                const Date* const this 

6.取地址运算符重载

        取地址运算符重载分为普通取地址运算符重载const取地址运算符重载,⼀般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。如运行下列代码

int main()
{
	Date d1(2025,5,5);
	cout << &d1 << endl;

	return 0;
}

          在调试界面可以看到,d1取的地址和它在内存块中的地址是相同的。

     但是,如果不想让取到对象的地址呢?这时候就能自己写一份取地址运算符重载,并且胡乱返回一个地址。如下,

Date* operator&()
{
	 return nullptr;
}
const Date* operator&()const
{
	 return nullptr;
}

          这时候进行取地址,得到的就是空指针啦。      

                                                               结语

        在本文中,我们系统地探讨了C++中类和对象的四个个关键特殊及两个不太重要的成员函数.这些函数是面向对象编程的基石,理解它们能帮助开发者高效管理资源、避免常见错误。通过亲手实现一个日期类,将这些理论知识转化为实践,展现了类的功能性。这一过程不仅巩固了C++基础,还为后续学习更高级主题(如模板、智能指针)奠定了坚实基础。

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值