C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读

Posted 康x呀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读相关的知识,希望对你有一定的参考价值。

构造函数

什么是构造函数?

当建立一个对象时,通常最需要立即做的工作是初始化对象,如对数据成员赋初值等
构造函数就是用来在创造对象时初始化对象,为对象数据成员赋初始值
——初始化对象+申请资源

下面以创建Date对象举例说明:
创建Data对象的同时并给对象设置一个值

C++提供了构造函数(constructor)来处理对象的初始化问题构造函数是类的一种特殊成员函数,不需要人为调用,而是在建立对象时自动被执行。换言之,在建立对象时构造函数就被自动执行了,而且在上述代码中:

1.名字与类名相同,创建类类型对象时由编译器自动调用
2.在对象的声明周期只调用一次

构造函数的特点

构造函数的特性其主要特征如下:
1.名字与类名相同
2.无返回值
3.对象实例化时编译器会自动调用构造函数
4.构造函数可以重载


在上述代码中,带参构造函数和无参构造函数形成重载

如果我们在类中没有写一个构造函数,那么我们调用对象时,编译器自动生成一个默认构造函数。一旦用户在类中定义了一个构造函数,则这个默认构造函数将不会生成,也就是说首先会调用用户定义的构造函数。

在Data类中没有一个构造函数,当我们创建一个对象时,编译器会自动调用默认构造函数将a进行了初始化,只是a中的成员变量为随机值,使用无参的构造函数不需要()

类中无构造函数,调用系统默认构造函数,初始化后a的值为随机值,具体代码如下:

综上:我们建议定义出一个全缺省构造函数,因为它可以灵活应用,它既可以传参,也可以不需要传参也能够初始化对象。

析构函数

析构函数概念

用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数——析构函数,用该函数去完成一些资源清理工作——释放资源

例如构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。
那么如何去声明和定于析构函数?

析构函数特性

析构函数是特殊的成员函数

其特征如下:
1.析构函数是在在类名加 ~
2.一个类只能有一个析构函数,如果类中没有定义一个析构函数,则系统会自动生成一个默认的析构函数
3.无参数无返回值。
4.对象声明周期结束时会编译器会自动调用析构函数。

将SeqList中的变量s进行初始化:

下面程序,我们看到编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。

我们自己没有写析构函数,那么编译器会去调用系统默认的析构函数,而这个析构函数对内置类型不做处理,对自定义类型会去调用它自己的析构函数


注意:析构函数是无参数返回的函数且没有参数,一个类只有一个析构函数,不写的话则会生成默认析构函数

拷贝构造函数

拷贝构造函数的概念

拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)。

在下述代码中,d1调用拷贝构造函数将d的内容拷贝给自己,虽然没有定义拷贝构造函数,然而系统调用默认的拷贝构造函数。

拷贝构造函数的特征

拷贝构造函数也是特殊的成员函数,特征如下:

1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
3.若未显示定义,系统生成默认的拷贝构造函数 (默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝)

如果拷贝构造函数的参数为为传值,那么在传值的过程会去调用拷贝构造函数去拷贝一个临时变量,每一次调用都会去在调用拷贝构造函数,以此类推,则会引起无穷无尽的调用拷贝构造函数。所以拷贝构造函数必须传引用

data(data &a)为语法要求,不会为a开辟空间,仅当a的别名使用,不会触发其他函数调动
如果不加&,对象初始化对象就会调动拷贝构造函数,就得引发下一个a,这样下去就没完没了了,不允许产生无限递归的现象。

4.如果说编译器生成默认拷贝构造函数就可以实现拷贝的话,是不是自己就不用写了?对于下面这种类,程序运行是崩溃的,那么我们就需要用深拷贝来解决。

赋值运算符重载

运算符重载

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

函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型operator操作符
示例:

说白了运算符重载就是要给
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型或者枚举类型的操作数
用于内置类型的操作符,其含义不能改变,例如:内置类型+,不能改变其含义
*,::,sizeof,、以上5个运算符不能重载

运算符重载实际上就是给这个所谓的运算符赋一个其它的涵义
t4给t3赋值其实可以直接调动成员函数的方法,类似于t4.Assign(t3),t4.Assign(t3)与t4.operator=(t3)的结果是一致的。但是在C++中更想要简单一点(不希望调函数,比如t1+t2,我们不希望写成add(t1,t2),而是直接t1+t2),其实在这里,t4=t3 中的=已经不是一个“=”了,而是 operator= 但是这里的operator可以省略,所以说 这里的 = 就不是等号了,而是一个“函数”。

问题1:

上述代码中——Test * operator = (const Test &t) 这个const可不可以不要?,还有&
当然可以取消,取消不影响代码结果,但是为了程序的运行效率,取消&的话,会多调用一次拷贝构造函数,注意析构函数是从后往前析构也就是说加了&会提升程序的运行效率(少一次调用拷贝构造,当然也少一次析构,不开辟空间,不花费时间)具体示例如下:

因为引用的对象我们一般是不希望被改变的,因此加上const,if(this !=&t)是防止自己给自己赋值

对于上述代码,return *this返回后 this指针的值是存在的,所以上述Test后面可以加&进行引用,所以说能不能引用返回,就看函数结束后,被引用的对象还活着没有而且建议用引用,因为可以少调动一次拷贝构造函数。

综上:赋值运算符主要有四点:
1.参数类型
2.返回值
3.检测是否给自己赋值
4.返回*this
5.一个类如果没显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝

对于如下这么多对象,他们构造,拷贝构造,析构的顺序及对应的流程如下:

下面这种办法少产生一次拷贝构造的调用,就是说拷贝构造产生的无名临时变量,直接就是t4的值。而不是像上述代码一样多拷贝了一次,然后再将拷贝后的值给t4,这样增加了程序的运行效率

运算符重载就是给+重新定义一个新的功能
重载+

下面为运算符重载的相关代码;
// An highlighted block
class MyInt;
ostream& operator<<(ostream &out, const MyInt &t);

class MyInt
{
	friend ostream& operator<<(ostream &out, const MyInt &t);//声明友元函数

public:
	MyInt(int i = 0)
	{
		m_i = i;

	}

public:
	MyInt operator+(const MyInt &x)
	{
     	return MyInt(m_i + x.m_i);
	}
	MyInt operator-(const MyInt &x)
	{
		return MyInt(m_i - x.m_i);
	}
	MyInt operator*(const MyInt &x)
	{
		return MyInt(m_i * x.m_i);
	}
	MyInt operator/(const MyInt &x)
	{
		return MyInt(m_i / x.m_i);
	}
	MyInt operator%(const MyInt &x)
	{
		return MyInt(m_i % x.m_i);
	}


public:
	MyInt& operator+=(const MyInt &x)
	{
		m_i += x.m_i;
		return *this;
	}

	MyInt& operator-=(const MyInt &x);
	MyInt& operator*=(const MyInt &x);
	MyInt& operator/=(const MyInt &x);
	MyInt& operator%=(const MyInt &x);


public:
	//a > b  x.operator>(b)
	bool operator>(const MyInt &x)
	{
		return m_i > x.m_i;
	}
	bool operator<=(const MyInt &x)
	{
		return !(*this > x);//当前对象比x大然后取反就是小于等于就是真
	}
	bool operator<(const MyInt &x)
	{
		return m_i < x.m_i;
	}
	bool operator>=(const MyInt &x)
	{
		return !(*this < x);
	}
	bool operator==(const MyInt &x)
	{
		return m_i == x.m_i;
	}
	bool operator!=(const MyInt &x)
	{		
		return m_i != x.m_i;
	}

public:
	MyInt& operator++() //ǰ++
	{
		m_i++;
		return *this;
	}
	MyInt operator++(int) //++
	{
		MyInt tmp = *this;
		m_i++;
		return tmp;
	}

private:
	int m_i;
};



ostream& operator<<(ostream &out, const MyInt &t)//友元函数实现
{
	out << t.m_i;
	return out;
}

void main()
{
	MyInt a = 10;
	MyInt b = 20;
	MyInt c;

	c = a + b;
	c = a - b;
	c = a * b;
	c = a / b;
	c = a % b;

	cout << "c = " << c << endl;
	a += b;

	if (a > b)
		cout << "a > b" << endl;
	else
	    cout << "a <= b" << endl;


	MyInt x = a++;
	MyInt y = a--;
}

定义复数类的运算符重载实现:


对于Complex y = c1+10来说可以,但是Complex y = 10+c1就不可以。c1是个对象,+是函数,函数要被对象才能驱动,c1+10 实际上是c1.operator。10不能构造成临时对象,10+的话就不匹配重载的+号了,而是想象成普通的运算符了。
当然如果非要Complex y = 10+c1成立的话,这里需要用到友元函数来操作,如下:

重载为友元函数,且友元函数必须给出成员的参数,成员函数和友元不一样,不需要加类的作用域限定符::

下面运算符输出流的重载,它通常作为某个类的友元函数出现

下面为C++输出的时候,cout不管任何情况就可以直接输出各种类型的结果,会自动匹配类型。
cout其实是ostream的一个对象(endl同样也是一个对象的概念)而且是一个全局对象,所以引入头文件后都可以使用。

ostream其实是这个东西的一个命名方式,是一个基本输出流对象 basic_ostream

表面上看起来它什么都不管,实际上背后可以生成各种类型的类型,相当于调用了不同的运算符重载,这些底层的代码我们平时是看不到的。

下面举相关个别运算符重载:


ostream是输出的一种类型,cout是out的对象,为什么out可以引用cout,因为他们两个是同一种类型,那为什么要重载为友元,是因为希望第一个参数就是cout,第二个参数才是对象,如果不重载友元,那么第一个参数只能用对象去驱动它。最后把cout返回,然后继续往后输出。

Const成员

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

const修饰的方法我们称之为常方法 ,如下代码我们在函数后面加了const实际上就是对内部的this指针变量加了限制不可改变。

普通的对象调动常方法是没有问题的,常对象调用普通方法就不行

那么什么时候写成常方法?
要修改数据成员的话则不能写成常方法,如果只为读而不改,则强烈建议写成常方法。c++采用最佳匹配原则,常方法和普通方法可以共存。

补充

六种类的默认成员函数:
构造函数,析构函数,拷贝构造函数,运算符重载之外的另外两种为:

普通对象取地址符,常对象取地址符

下面为六种类的默认函数的基本代码表示:

以上是关于C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读的主要内容,如果未能解决你的问题,请参考以下文章

C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读

C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读

C++初阶:类和对象(中篇)构造函数 | 析构函数 | 拷贝构造函数 | 赋值运算符重载

C++类和对象

C++类和对象—— 类的6个默认成员函数及日期类的实现

C++类和对象中