C++(下):类和对象——默认成员函数

上篇主要呢,主要是给类和对象开了个小头。这篇主讲的就是类的六大默认成员函数。

(1)构造函数:

关于构造函数,其实就是对一个类的值,进行初始化。

在之前,对日期类进行初始化的时候,都得调用InitDate。这样会很麻烦,因为有时候,可能自己会忘记初始化。

所以,那能否在对象创建时,就将信息设置进去呢? 所以C++引入了这个默认成员函数

①特性:

构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象

其特征如下:

1. 函数名类名相同。2. 无返回值。3. 对象实例化时编译器自动调用对应的构造函数。4. 构造函数可以重载

 

 

 

 

	//1无参形式
	Date()
	{

	}

	//2.传参形式
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//3.全缺省参数
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}


    Date d1;//无参形式

	Date d2(2022,11,13);  //有参数

	Date d3; //全缺省

注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明

 在之前也就说过,如果用户自己不去定义构造函数,那么编译器也就会自己帮你,生成一个无参的构造函数。一旦用户设定,也就不存在了。(特性)

可以看出,编译器在生成默认构造函数的时候,很偷懒,干脆只赋值而不给确定值。

我们在谈到默认构造函数的时候,如果只谈编译器,在我们没有定义构造函数下,是默认构造,是不准确的。

所以准确来说,默认构造函数有三类

1.编译器自己产生的。

2.我们写的无参数。

3.我们写的全缺省的。

因此,很多人就会不明白,这编译器默认生成的构造函数貌似没用,没有做实质性的事。d对象调用了构造函数(_year,day……)生成的依旧是随机值。那这是否,对其他类型也是一样的?

探讨C++类型:内置类型 、 自定义类型:

什么是内置类型呢? 比如int\double\short这类。

自定义类型,诸如:union struct class.

我们来看看,编译器自动申请的默认构造函数,是否对自定义类型会起作用?

class Test
{
public:
	Test(int a=1)
	{
		_a = a;
		cout << "Test构造" << endl;
	}

	void Show()
	{
		cout << _a << endl;
	}

private:
	int _a;
};

我们把创建好的自定义类型,放入到日期类中:

 很容易看出,在构造自定义类的时候,会去调用自定义类的默认构造函数。

所以我们得出一个结论:
 内置类型——不做处理(初始化)        自定义类型——调用他自己的默认构造函数.


(2)析构函数:

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数同构造函数,就大相径庭。一个是构造,一个是在对象销毁的时候,自动清理

特性:

1.析构函数在函数名后面+~.

2.无参数、无返回值

3.和构造函数一样,只有一个,系统会自动生成。

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

在我们实际中啊,尤其是用到需要被手动释放的空间,如malloc开辟的,有时候会忘掉。

所以析构函数就是为了解决这样的问题。不再需要用户进行手动释放。

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

	void Stackpush(int x)
	{

	}

	void StackDestory()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;

	}

private:
	int* _a;
	int _capacity;
	int _size;
};
int main()
{
	Stack st1;
	st1.Stackpush(1);
	st1.Stackpush(1);
	st1.Stackpush(1);
	st1.Stackpush(1);

	st1.StackDestory();
return 0;
}

此时我们写了 一个简易的栈。每次创建栈的时候,都要进行手动的销毁。

此时走到了程序结束的最后:

这里会自动调用析构,把类销毁掉。

注:那如果多个st需要析构呢?析构的顺序又是如何?

不仅仅是析构:构造和析构都按照栈入栈和出栈的方式:先进后出


(3) 拷贝构造:

拷贝构造顾名思义,也就是用一个已有的对象,去拷贝出另外一个对象。

特性:

1.拷贝构造是,构造函数的一种重载

2.拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

3.我们不写,编译器也会默认生成。

	//构造函数
	Date(int year = 0, 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;
	}

为什么需要传引用?

拷贝d2;

可以看出,当我们不写的时候,编译器也会自动生成拷贝构造。

 所以,在任何情况下都不用写了吗?

答案肯定不是:

我们先拿出简易的栈:

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

	//Stack(const Date& d)
	//{

	//}

	void Stackpush(int x)
	{

	}


	~Stack()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}


private:
	int* _a;
	int _capacity;
	int _size;
};

 用st1  去拷贝构造 st2;

 程序会直接崩溃: 

这里就牵涉到深、浅拷贝的问题.

 所以,默认拷贝函数,实质上是按照浅拷贝的方式,仅仅是赋值。那么,针对像Date类,就已经够用了。至于像栈、队列这系列数据结构,需要的就是深拷贝。


(4)赋值运算赋重载:

在正式讲 赋值运算符重载之前,还得先铺垫铺垫,什么是运算符重载.

运算赋重载:

为什么引入这个概念?

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。

特性:

1.函数名字为:关键字operator后面接需要重载的运算符符号

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

注意事项:

1.不能来创建新的操作符:比如operator@

2.重载操作符必须有一个类类型或者枚举类型的操作数

3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义

4.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的 操作符有一个默认的形参this,限定为第一个形参.

5.".*" 、"::" 、"sizeof"、"?:" 、"." 注意以上5个运算符不能重载

这是重载两个数相加

class A
{
public:
	A(int a = 0)
	{
		_a = a;
	}
    //int operator(A* a1,A&b)
	int operator+(A& b)
	{
		return _a + b._a;
	}

private:
	int _a;
};


int main()
{
	A a1(1);
	A a2(2);
    //运算赋重载
	cout << a1 + a2 << endl;
    return 0;
}

 当然也可以重载相等:

class A
{
public:
	A(int a = 0)
	{
		_a = a;
	}

	bool operator==(const A& c)
	{
		return _a == c._a;
	}
private:
	int _a;
};


int main()
{
	A a1(3);
	A a2(3);
	cout << (a1 ==a2) << endl;

	A a3(4);
	cout << (a1 == a3 )<< endl;
    return 0;
}

当然运算符重载也可以写在全局。

方法一:

方法二:

 

 但我们对于友元态度是谨慎,不到万不得已是不用的。

花了这么多大篇幅讲,运算赋的重载。那么想必也对这个方式有点理解了。

赋值运算符重载的特性: 

1. 参数类型

2. 返回值

3. 检测是否自己给自己赋值

4. 返回*this

5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。

下面是没有自己写赋值运算赋重载的情况:

区分拷贝构造、和赋值运算符重载:

 从概念上:

拷贝构造,是拿一个已经初始化的对象,对另外一个进行初始化。

赋值运算赋重载,是拿同类去初始化。(已经是两个生成的类)。


(5)取地址重载

关于取地址重载是很鸡肋的默认成员函数,实际中也很少用,也就不再多说。

(6)const修饰的成员函数:

将const修饰的类成员函数称之为const成员函数

const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。

    //void Display(const Date* this)
	void Display() const  //修饰this  
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

权限的放大与缩小:

我们来看以下代码:
 

class Date
{
public:
	//构造函数
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}


	//void Display(const Date* this)
	void Display() const  //修饰this  
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	void Display()  
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

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

int main()
{
	Date d1(2020, 11, 16);
	d1.Display();

	const Date d2(2013, 5, 21);
	d2.Display();
return 0;
}

按习惯,不同类的Date会去调用不同的函数 

   

 

如果把const修饰的函数屏蔽掉,编译器不能跑过。

 但,如果只屏蔽没有由const修饰的函数,则可以跑过。

为什么?

 下面四个问题,帮助更好的理解.

1. const对象可以调用非const成员函数吗? d2->非const函数   ×

2. 非const对象可以调用const成员函数吗?  d1->const函数      √

3. const成员函数内可以调用其它的非const成员函数吗?  ×

4. 非const成员函数内可以调用其它的const成员函数吗?   √


自定义运算赋重载的输入输出:

为什么cin、cout能够自动识别类型?

class A
{
public:
	A(int a = 0;)
	{
		_a = a;
	}

	void operator<<(ostream& out) 
	{
		out << _a << endl;
	}

	/*operator>>();*/

private:
	int _a;
};


int main()
{
	A a(1);
	cout << a;

此时我们发现编译不过?

所以我们交换一次。 

 就能行得通了。   但是这样反而难以理解输出。不能很好体现,运算赋重载的可读性。

class A
{
public:
	A(int a = 0)
	{
		_a = a;
	}
     
    //友元函数
	friend void operator<<(A* a, ostream& out);
	//cout 和 a 争抢this指针
	//这里的操作对象是a 
	//所以应当把a 写到前面 out
	//这样不是不可以只是降低了可读性
	//void operator<<(ostream& out) 
	//{
	//	//void operator(A* this,osteam& out)
	//	// cout<<a
	//	out << _a << endl;
	//}

	/*operator>>();*/

private:
	int _a;
};

//所以我们干脆 定义全局
//全局 函数不能直接访问的类的成员 所以需要友元
void operator<<(ostream& out,const A& a)
{
	out << a._a << endl;
}

 当然如果想要连续输入,那么运算赋重载就需要返回值。

ostream& operator<<(ostream& out,const A& a)
{
	out << a._a << endl;
	//支持连续赋值
    //out链接支持 下一个输出的对象
	return out;
}

那么关于输入也是同理:

istream& operator>>(istream& in, A& b)
{
	in >> b._a;
	//支持连续赋值
	return in;
}

 


这就是本篇罗列的一些默认成员函数。

当然还有一些这些基础上的补充。也就留到下篇啦

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值