【C++】类与对象(中)

发布于:2024-06-25 ⋅ 阅读:(153) ⋅ 点赞:(0)

类的6个默认成员函数

class A{};

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员
函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

一、构造函数 

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

class Date {
public:

	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

  我们没有调用Init,打印出的数要么是随机值,要么程序崩溃

1.1 特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任
并不是开空间创建对象(对象是开空间的),而是初始化对象。类似Init函数的功能

特征如下

1. 函数名与类名相同。
2. 无返回值。--不需要写void
3. 对象实例化时编译器自动调用对应的构造函数。

4. 构造函数可以重载。--可以写多个构造函数,可以函数重载 

我们在上面的代码中,public下添加此代码。1.函数名和类名相同,2.无返回值

Date()
{
	_year = 1;
	_month = 1;
	_day = 1;
}

调试一下 ,发现构造函数直接自动调用。 3.自动调用对应的构造函数。

帮我们初始化了 

4.多个构造函数发生函数重载

	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
//main函数	
Date d2(2024,5,29);

 我们没写构造函数,有没有构造函数? -->有

C++分为:内置类型/基本 int/char/double.../指针
                  自定义类型    class/struct...
 编译器自动生成构造函数,对于内置类型成员变量,没有规定要不要处理(有些编译器会处理)
                                            对于自定义类型成员变量才会调用他的无参构造

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数

内置类型成员变量】 

class Date {
public:
	void Print()
	{
		cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}


没有进行初始化,而是生成了随机值。

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

class Date {
public:

	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	//给缺省值
	int _year = 1;
	int _month = 2;
	int _day ;
};
	Date d1;
	d1.Print();

 

 【自定义类型成员变量

 【无参

class A {
public:
	A()
	{
		_a = 0;
		cout << "A()" << endl;
	}
private:
	int _a;
};
class Date {
public:
	void Print()
	{
		cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	A aa;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

  【有参

     在构造函数添加了参数

	A(int a = 1)
	{
		_a = 0;
		cout << "A()" << endl;
	}

 会发生报错

二、析构函数

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

2.1 特性

析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~
2. 无参数无返回值类型。--不需要写void
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数

5.跟构造函数类似

      a、内置类型不做处理

      b、自定义类型去调用他的析构

在public中添加

	~Date()
	{
		//销毁
	}

日期类不需要析构函数,但是要演示一下,方便大家理解 

三、拷贝构造函数

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎,那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

【定义】

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

class Date {
public:
	Date(int year, int month, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    //调用拷贝构造
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year = 1;
	int _month = 2;
	int _day ;
};
int main()
{
	Date d1(2024, 6, 17);
	d1.Print();
    //拷贝构造
	Date d2(d1);//Date d2 = d1;
	d2.Print();
	return 0;
}

3.1 特性

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

在Date类中 

	//Date(const Date d)错误写法,会引发无穷递归,为什么
    Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

 先了解底层在func1中,因为自定义类型的拷贝,都会调用拷贝构造

若拷贝构造函数没有引用,则会一直传给Date d 然后底层继续call,而拷贝构造还是 Date(const Date d),然后继续无线循环下去

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

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

我们把构造函数屏蔽,依旧把d1的值赋给d2

//	Date(Date& d)
//	{
//		cout << "Date(Date& d)" << endl;
//		_year = d._year;
//		_month = d._month;
//		_day = d._day;
//	}

但是这只是浅拷贝,只是把d1的字节拷贝到d2了,类似于memcpy,有局限,不能拷贝空间,假如拷贝一个有空间的内置变量,程序结束时,析构两次导致程序崩溃,等还有诸多问题。

4.深拷贝-->不是默认拷贝构造

开辟的空间是独立的

例子 

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 5)
	{
		_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(const Stack& st)
	{
		_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_array, st._array, sizeof(DataType) * st._size);
		_size = st._size;
		_capacity = st._capacity;
	}
	bool Empty()
	{
		return _size == 0;
	}
	DataType Top()
	{
		return _array[_size - 1];
	}
	void Pop()
	{
		_size--;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);

	Stack st2 = st1;
	st2.Push(2);
	st2.Push(2);

	while (!st2.Empty())
	{
		cout << st2.Top() <<" ";
		st2.Pop();
	}
	cout << endl;
	while (!st1.Empty())
	{
		cout << st1.Top() << " ";
		st1.Pop();
	}
	cout << endl;
	return 0;
}

四、赋值运算符重载

4.1运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型  operator操作符(参数列表)

class Date {
public:
	Date(int year = 2024, int month = 6, int day = 19)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
    //重载成员函数一般写在类中-----因为this是隐含,所以比较相等需要this和d
    //返回值 operator 操作符
	bool operator==(const Date& d)
	{
		return _year == d._year &&
			_month == d._month &&
			_day == d._year;
	}
private:
	//给缺省值
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d(2024, 6, 19);
	Date d2 = d;
	//显示调用
	d.operator==(d2);
	//转换调用 等价上面 d2与顺序不能变
	d2 == d;
	return 0;
}

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
  • 藏的this
  • .*    ::    sizeof    ?:    .  注意以上5个运算符不能重载。这个经常在笔试选择题中出
  • 现。
class OB
{
public:
	void func(){}
};
typedef void(OB::* PtrFunc)();
int main()
{
	//普通函数函数名就是地址
	//成员函数规定-->要加&才能取到函数指针
	PtrFunc fp = &OB::func;
	OB temp;
	(temp.*fp)();//调用成员函数函数指针
	return 0;
}

 4.2 赋值运算符重载

类中添加此代码

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

我们发现main函数中

d1 = d2没有编译错误

d1 = d2 = d3就发生编译错误

也就是说 d2.operator=(d4)需要返回一个同类型的右操作数好赋值给d1

需要改成

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

 为了提高效率需要引用返回

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

为什么?
当main函数是这个时候,传值返回与引用返回的区别

Date d1(2024,6,19);
Date d2(d1);
Date d3 = d2;
Date d4(2024, 6, 20);
d1 = d2;
d1 = d2 = d4;

传值返回 

引用返回

自己跟自己赋值会白白调用

所以再次修改

	Date operator=(const Date& d)
	{
		if (this != &d)//取地址
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注
意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值
。跟拷贝构造类似 Date或者MyQueue默认生成的拷贝就可以。

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

4.3 传值返回和引用返回有什么区别

class Date {
public:
	Date(int year = 2024, int month = 6, int day = 19)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;

		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
	~Date()
	{
		cout << "~Date()" << endl;
		_year = -1;
		_month = -1;
		_day = -1;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date func()
{
	Date d(2024,6,19);
	return d;
}
int main()
{
	func();
	return 0;
}

传值返回

引用返回 

 想接收2024,6,19,为什么返回的是随机值,因为析构了--------->return d 返回的是d的别名,d发生析构,d的别名也发生了析构,既然析构了应该返回的是-1,-1,-1,为什么会出现随机值?func1函数栈帧销毁后Print重新在此空间中,重新建立,从而占用了原先内置类型空间地址,进而生成了随机数

总结:返回对象是一个局部对象或临时对象,出了当前函数作用域,就析构销毁了,那么不能用引用返回,用引用返回存在风险,因为引用对象在func1已经销毁

什么情况可以使用引用?

出了作用域,返回对象还在没有析构,那就可以用引用返回,减少拷贝(提高效率)

例如:

Date& func1()
{
	static Date d(2024,6,19);
	return d;
}
int main()
{
    Date& ref = func1();
	ref.Print();
	return 0;
}

4.4 前置++和后置++重载

多个同一运算符重载可以构成函数重载

前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载

C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
自动传递。

后置++做了特殊处理,最好不要把规定(++(int))打破,以防造成混淆。

	// 前置++ ++d1
	Date& operator++();
	// 后置++ d1++
	Date operator++(int);
	// 前置-- --d1
	Date& operator--();
	// 后置-- d1--
	Date operator--(int);
//++d1
Date& Date::operator++()
{
	*this += 1;
	return *this;
}
//d1++
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}

4.5 日期计算器

.h文件class Date中声明

	//日期加天数
    Date& operator+=(int day);
	Date operator+(int day);
	//日期-天数
	Date& operator-=(int day);
	Date operator-(int day);
    //日期相减求日期差
    int operator-(const Date& d);

直接定义类里面,他默认是inline

频繁调用定义类里面

四年一闰,百年不闰,四百年又闰

	int GetMonthDay(int year,int month)
	{
		assert(month > 0 && month < 13);
		static int monthDayArray[13] = { -1,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;
		}
		else
		{
			return monthDayArray[month];
		}
	}

.cpp   

日期加天数 

//引用返回减少拷贝构造
Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}
	return *this;
}
//因为tmp临时变量要销毁所以要发生析构,不能引用返回
Date Date::operator+(int day)
{
	Date tmp = *this;
	tmp += day;
	return tmp;
}

日期-天数 

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this -= -day;
	}
	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			_year--;
		}
		// 借上个月的天数
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}
Date Date::operator-(int day)
{
	Date tmp = *this;
	tmp -= day;
	return tmp;
}

 日期相减求日期差

int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}
	return n * flag;
}

4.6 流插入和流提取运算符重载

在C++中,我们输出和输入一个数据通常是通过cout、cin 他们包含在头文件iostream中其实就是全局对象,所以输入、输出其实就是调用两个运算符重载函数。

  

已知流插入与流提取可以输出内置类型,因为头文件已经包含

如果我们想输出自定义类型,如何操作

4.6.1 流插入

运算符重载中,参数顺序和操作数顺序是一致的

 .h文件Date类中声明

//流插入
void operator<<(ostream& out);
//参数顺序    Date     ostream
//默认this参数类型              

 .cpp

//流插入
void Date::operator<<(ostream& out)
{
	cout << _year << "年" << _month << "月" << _day << "天";
}

 main函数

cout << d1;
//操作数顺序 ostream Date

 上面对不对?不对,为什么?参数顺序和操作数顺序不一致

d1.operator<<(cout);
操作数顺序 Date  ostream 
d1 << cout;

使用d<<cout不符合我们使用习惯,如何修改

operator<<从成员函数拿出来,成全局函数

为了能打印多个参数把void改成ostream&

.h

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

为了访问成员函数,我们在类中进行友元函数

.cpp

ostream& operator<<(ostream& out, const Date& d)
{
	cout << d._year << "年" << d._month << "月" << d._day << "天";
	return out;
}
4.6.2 流提取

 和上面大差不差

 .h 

istream& operator>>(istream& in, Date& d);

把const去掉因为提取出来的值要放到 Date当中

 .cpp 

istream& operator>>(istream& in, Date& d)
{
	in >> d._year>>d._month >> d._day;
	return in;
}

五、取地址及const取地址操作符重载

.h Date类中

	Date* operator&();
	const Date* operator&() const;

.cpp 

Date* Date::operator&()
{
	return this;
}
//后面的const实际修饰的是形参 this-----> 这种要注意权限问题
const Date* Date::operator&() const
{
	return this;
}

如果不定义声明和实现也可以


网站公告

今日签到

点亮在社区的每一天
去签到