C++类与对象(中篇):构造函数、析构函数、拷贝函数(深拷贝、浅拷贝)、赋值重载、取地址重载&&const取地址重载

目录

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

 2.构造函数

3.析构函数

4.拷贝构造函数

特征:

浅拷贝:

深拷贝:

5.赋值运算符重载

1.赋值运算符重载

2.date类基本运算符重载

1.const成员函数

3.流插入运算符重载

初识友元函数

4.流提取运算符重载

6.取地址操作符重载&&const取地址操作符重载


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

如果一个类中什么成员都没有,简称为空类。

class A{};

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

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

 2.构造函数

每次类实例化出一个对象,肯定要对对象初始化,既然每次都要手动初始化,那能不能干脆直接让编译器自动调用初始化函数呢?这里构造函数其实就是这个用途。

注意:构造函数虽然叫构造,但其实是用来初始化的,可以理解成构造一个对象时自动调用的函数。

构造函数是一个特殊的成员函数,他的函数名与类名相同,是类实例化出对象时编译器自动调用的函数,并且一个对象的整个生命周期都只调用一次构造函数,这一次就是在创建对象时调用的。

其特征如下:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
    #include<iostream>
    
    using namespace std;
    
    class date
    {
    public:
    	date(int year = 0, int month = 0, int day = 0)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
        //由于没有传参,a被默认初始化成0,0,0
    	date a;
    
        //b被初始化成2,2,2
        date b(2,2,2);
    
        //报错,前面说过对象在整个生命周期中只能调用一次构造函数,
        //并且这次由编译器来调用,不可自己手动调用
    	a.date(1, 1, 1);
    	return 0;
    }
  5. 如果类中没有显式定义构造函数,则编译器会自动生成一个无参的构造函数。如果类中显式定义了构造函数,则编译器不会生成。

既然类中没有定义构造函数,编译器会自动生成一个,那这个自动生成的构造函数会干啥?有什么用?

答:有的编译器生成的构造函数会把成员变量初始化成0,有的编译器不做初始化,所以这里默认所以编译器生成的构造函数都不初始化。

那如果类中的一个成员变量也是一个类,成员类会不会调用它自己的构造函数?

看下面这段代码运行会有什么结果

#include<iostream>
using namespace std;

class Time
{
public:
	Time()
	{
		cout << "Time" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{

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

int main()
{
	Date a;
	return 0;
}

下面这段呢?

#include<iostream>
using namespace std;

class Time
{
public:
	Time()
	{
		cout << "Time" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date()
	{
		cout << "Date" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

int main()
{
	Date a;
	return 0;
}

 

得出结论, 不论外部类的构造函数是自己写的还是编译器生成的,都会调用成员类的构造函数。

这里补充一点:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时 可以给默认值。

class Date
{
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

int main()
{
    //由于Date类的成员变量在声明时给了初始值
    //所以这里a初始化成了0,0,0。注意这里不是构造函数初始化的.
    //相当于a是0,0,0然后再调用构造函数,
    //但是编译器生成的构造函数对内置类型啥都不做,所以a依然是0,0,0
	Date a;

    //报错,这里不可手动初始化成1,1,1 因为这里调用的是构造函数,
    //而编译器生成的构造函数没有参数,不可传参初始化
    Date b(1,1,1);

	return 0;
}

这里再补充一个概念:无参的构造函数和全缺省的构造函数都称为默认构造函数,而默认构造函数至多只能有一个,也可以不要。

因为编译器自动生成的构造函数是无参的构造函数,所以编译器自动生成的构造函数也是默认构造函数。

class Date
{
public:
	Date(int year = 5, int month = 5, int day = 5)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

int main()
{
	Date a;//报错,类Date包含多个默认构造函数

	return 0;
}

3.析构函数

析构函数和构造函数类似,构造函数是在对象创建时调用的,析构函数则是在对象销毁时调用的。

特性:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:构造函数可以重载,而析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
    class Date
    {
    public:
    	Date()
    	{
    		cout << "Date" << endl;
    	}
    	~Date()
    	{
    		cout << "~Date" << endl;
    	}
    private:
    	int _year = 0;
    	int _month = 0;
    	int _day = 0;
    };
    
    int main()
    {
    	Date a;
    
    	return 0;
    }

  5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
    class Time
    {
    public:
    	Time()
    	{
    		cout << "Time" << endl;
    	}
    	~Time()
    	{
    		cout << "~Time" << endl;
    	}
    private:
    	int _hour;
    	int _minute;
    	int _second;
    };
    
    class Date
    {
    public:
    	Date()
    	{
    		cout << "Date" << endl;
    	}
    	
    private:
    	int _year = 0;
    	int _month = 0;
    	int _day = 0;
    
    	Time _t;
    };
    
    int main()
    {
    	Date a;
    
    	return 0;
    }

     注意:如果类中有成员变量也是类的话,不论是编译器自动生成的析构函数还是自己定义的析构函数,函数内部都会调用类成员变量的析构函数。而且析构函数可以手动调用。

    class Time
    {
    public:
    	Time()
    	{
    		cout << "Time" << endl;
    	}
    	~Time()
    	{
    		cout << "~Time" << endl;
    	}
    private:
    	int _hour;
    	int _minute;
    	int _second;
    };
    
    class Date
    {
    public:
    	Date()
    	{
    		cout << "Date" << endl;
    	}
    	~Date()
    	{
    		cout << "~Date" << endl;
    	}
    private:
    	int _year = 0;
    	int _month = 0;
    	int _day = 0;
    
    	Time _t;
    };
    
    int main()
    {
    	Date a;
    
    	return 0;
    }

    大家感兴趣的话,可以自己研究一下嵌套类的构造函数和析构函数 的调用顺序。

4.拷贝构造函数

拷贝构造函数是构造函数的一种,也是用来初始化的,拷贝构造一般用于用对象给对象初始化。比如说想创建一个对象B,想让B和已存在的对象A一摸一样,就可以用A来初始化B。

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

特征:

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

class Date
{
public:
	Date(int year = 2023,int month = 5,int day = 3)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& a)//这个为拷贝构造函数,是构造函数的一种重载形式
	{
		_year = a._year;
		_month = a._month;
		_day = a._day;
	}

	~Date()
	{
		cout << "~Date" << endl;
	}
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;

	Time _t;
};

int main()
{
	Date a(2020,1,1);
    Date b(a);
	return 0;
}

2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

思考一下如果上面的拷贝构造函数Date(Date& a)换成Date(Date a)会怎么样?

 所以拷贝构造函数的参数最好用引用,引用就不存在初始化对象的问题了,连构造函数都不会调用,自然也就不会陷入死循环了。

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

class stack
{
public:
	stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		_top = 0;
		_capacity = capacity;
	}

	~stack()
	{
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}

    void push(int x)
	{
		_a[_top] = x;
		_top++;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
	stack a;
	a.push(1);
	
	stack b(a);

	return 0;
}

这里我没有写拷贝构造函数,如果用a来初始化b,那会调用编译器自动生成的拷贝构造函数 ,会发生什么?

可以看见a和b的内容一摸一样了,甚至连指针_a的内容也一样.这是浅拷贝,这里讲一下深拷贝浅拷贝的区别:

浅拷贝:

浅拷贝就是把变量的内容直接拷贝过去,上面就是浅拷贝。

深拷贝:

深拷贝则是把指针指向的地址的内容也拷贝。举个例子:上面的拷贝构造如果用深拷贝,那么a的_a指针和b的_a指针的内容肯定不同,他们指向两块大小相同位置不同的空间,_a里存放的是这块空间的地址,所有两个_a的内容肯定不同,但是_a所指向的这两块空间的内容相同,这就是深拷贝.

综上所述,栈的类型不能用浅拷贝,也就是说栈的拷贝构造不能由编译器来实现,必须手动实现。

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

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象

5.赋值运算符重载

1.赋值运算符重载

类的第四个默认成员函数是赋值运算符重载函数,要讲这个先得讲一下运算符重载是什么,我们知道定义一个相同的函数名,但是变量、返回值、函数内容都可能不同的函数。这个过程叫函数重载,那类比一下运算符重载则是定义一个相同的运算符,但是参数、返回值、内容可能不同。赋值运算符的意思就是这个相同的运算符是赋值运算符。关键字operator运算符()。运算符重载也是一个函数,函数名是operator加上需要重载的运算符,参数是运算符的操作数。

//这是赋值运算符重载
//这个函数有两个参数,一个是this指针,一个是a;
date& operator=(date& a)
{
    _year = a._year;
	_month = a._month;
	_day = a._day;
	return *this;
}

int main()
{
    date d(2020,1,1);
    date b;

    //这里就调用了赋值运算符重载函数,operator=,
    //其中b的地址作为隐藏传参传给this指针,d被a引用
    b = d;
    return 0;
}

 赋值运算符重载作为类的六大默认函数成员之一,如果没有显式定义,编译器也会默认生成一个。这个默认生成的和拷贝构造默认生成的都是浅拷贝,所以如果类中有涉及到动态内存的话,一定要自己写一个赋值运算符重载函数。

注意这种情况

int main()
{
    date a(2020,1,1);
    date b = a;
    return 0;
}

这里b其实调用的是拷贝构造,并不是赋值运算符重载函数,date b = a,就等于date b(a)。

 拷贝构造是在初始化的时候调用,赋值运算符重载函数是在除初始化以外的赋值的时候调用,注意这两个函数的区别。

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .*   ::   sizeof   ?:   . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

2.date类基本运算符重载

1.const成员函数

如果我想比较两个日期是否相等,如果直接写a==b那肯定是错的。但是我就想这么写,有没有实现的方法?可以写一个==运算符重载函数。

bool date::operator==(date& a)
{
	if (_year == a._year && _month == a._month && _day == a._day)
		return true;
	return false;
}

这样就能直接使用==来判断了,但是有个问题,如果有一个对象a被const修饰了,还能这样写吗?

const date a(2020,1,1);
const date b(2019,1,1);

a == b

 答案是不能,因为函数内部this接收了a的地址,而this指针的类型是date* const this,也就是说可以同过this来改变a的值,b也同理,限权被放大了,所以编译器会报错,接收b的参数date& a我们可以直接在前面加个const,但是this指针是隐式接收的,我们怎么给他前面加个const?

bool date::operator==(const date& a)const
{
	if (_year == a._year && _month == a._month && _day == a._day)
		return true;
	return false;
}

 答案是在右括号外加个const,这样就相当于给*this加了个const了。const成员函数实际上这个const就是用来修饰*this的

为了避免上面的限权放大导致报错的情况,以后只要不涉及到改变对象的内容,都最好给形参加上const,

日期类除了==,还有<、 <= 、> 、>= 、!= 、+ 、+= 、- 、-= 、前置++ 、后置++ 、前置-- 、后置-- 、以及两个日期相减求相差的天数。就不一一讲了,直接把代码搬上

date.h头文件:

#include<iostream>

using namespace std;

class date
{
public:
	date(int year = 2023, int month = 4, int day = 26);
	date(const date& a);

	bool operator==(const date& a)const;
	bool operator<(const date& a)const;
	bool operator<=(const date& a)const;
	bool operator>(const date& a)const;
	bool operator>=(const date& a)const;
	bool operator!=(const date& a)const;


	date& operator=(const date& a);
	date operator+(int day)const;
	date& operator+=(int day);
	date& operator++();//前置++
	date operator++(int);//后置++
	date operator-(int day)const;
	date& operator-=(int day);
	int operator-(const date& a);

//private:
	int GetMonthDay(int year, int month);
private:
	int _year;
	int _month;
	int _day;
};

inline int MIN(int a,int b)
{
	if (a < b)
		return a;
	return b;
}

date.cpp文件 

#include"date.h"

date::date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

date::date(const date& a)
{
	_year = a._year;
	_month = a._month;
	_day = a._day;
}

date& date::operator=(const date& a)
{
	_year = a._year;
	_month = a._month;
	_day = a._day;
	return *this;
}

bool date::operator==(const date& a)const
{
	if (_year == a._year && _month == a._month && _day == a._day)
		return true;
	return false;
}

bool date:: operator<(const date& a)const
{
	if (_year < a._year)
		return true;
	else if (_year == a._year && _month < a._month)
		return true;
	else if (_year == a._year && _month == a._month && _day < a._day)
		return true;
	return false;
}

bool date::operator<=(const date& a)const
{
	return *this < a || *this == a;
}

bool date::operator>(const date& a)const
{
	return !(*this <= a);
}

bool date::operator>=(const date& a)const
{
	return !(*this < a);
}

bool date::operator!=(const date& a)const
{
	return !(*this == a);
}

int date::GetMonthDay(int year, int month)
{
	static int monthDayArr[13] = { 0,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;
	return monthDayArr[month];
}


date date::operator+(int day)const
{
	date ret(*this);
	ret += day;
	return ret;
}

date& date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= -day;
	}
	_day += day;
	int a = GetMonthDay(_year, _month);
	while (_day > a)
	{
		_day -= a;
		++_month;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
		a = GetMonthDay(_year, _month);
	}
	return *this;
}

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

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

date date::operator-(int day)const
{
	date ret(*this);
	ret -= day;
	return ret;
}

date& date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}
	day -= _day;
	while (day >= 0)
	{
		--_month;
		if (_month == 0)
		{
			--_year;
			_month = 12;
		}
		_day = GetMonthDay(_year, _month);
		day -= _day;
	}
	_day = -day;
	return *this;
}

int date::operator-(const date& d)
{
	int a = MIN(_year, d._year);

	int day1 = 0, day2 = 0;

	int i = a;
	while (i <= _year)
	{
		if (i < _year)
		{
			day1 += (337 + GetMonthDay(i, 2));
		}
		else
		{
			int j = 1;
			while (j < _month)
			{
				day1 += GetMonthDay(i, j);
				j++;
			}
			day1 += _day;
		}
		i++;
	}

	i = a;
	while (i <= d._year)
	{
		if (i < d._year)
		{
			day2 += (337 + GetMonthDay(i, 2));
		}
		else
		{
			int j = 1;
			while (j < d._month)
			{
				day2 += GetMonthDay(i, j);
				j++;
			}
			day2 += d._day;
		}
		i++;
	}

	return day1 - day2;
}

这里面有一点注意:由于前置++和后置++的运算符重载函数的函数名都是operator++,为了区分他们俩,前置++的重载函数没有显式参数,后置++的重载函数的参数需要 加个int,这个int没有实际用途,仅仅用来区分前置++和后置++。

3.流插入运算符重载

初识友元函数

如果想打印date类,能不能用cout来打印呢?答案是可以,只需要将<<操作符重载就行了,

 其实cout是一个ostream类的对象,所以可以把cout传参,


//out不能加const,流插入本质上就是将out的内容改变。
ostream& operator<<(ostream& out, date& a)
{
	cout << a._year << "年" << a._month << "月" << a._day << "日" << endl;
	return out;
}

注意: 流插入重载不能写在类的内部,因为写在类的内部会有隐式参数this指针,而this指针默认传运算符左侧的操作数的地址,流插入的左侧只能是cout,所以会发生错误,不写在类内部就没有this指针了,但是这样会引出一个问题,就是函数没有在类域中不能访问类的成员变量,为了让类外部的函数能访问类的成员变量,引入了友元函数的概念,友元函数就是在函数声明的前面加个friend

	friend ostream& operator<<(ostream& out, date& a);

友元函数放在类的内部,其相关的类外部的函数就能访问类的成员变量了。

 友元函数没有公有私有之分,因为它不能被调用,所以不用区分公有私有。

class date
{
	friend ostream& operator<<(ostream& out, date& a);
    //其他函数就省略了,前面有
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, date& a)
{
	cout << a._year << "年" << a._month << "月" << a._day << "日" << endl;
	return out;
}

 

 这里扩展一下:大家知道cout<<后面跟内置类型能够自动识别类型并打印,自动识别是怎么做到的?

在cplusplus.com上可以看operator<<重载函数有这么多,所以大家应该可以猜到了,所谓的自动识别类型实际上就是把所有的内置类型都给重载了一遍,不同的类型调用不同的函数,就做到了所谓的自动识别。

4.流提取运算符重载

和流插入同理,需要写在类外部,但是要在类内部写个友元函数。

class date
{
    friend istream& operator>>(istream& in, date& a);
    //其他函数就省略了,前面有
private:
	int _year;
	int _month;
	int _day;
};
istream& operator>>(istream& in, date& a)
{
	in >> a._year >> a._month >> a._day;
	return in;
}

 

6.取地址操作符重载&&const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{ 
public :
 Date* operator&()
 {
 return this ;
 }
 
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值