C++-----类的构造函数、析构函数、拷贝构造函数以及赋值运算符重载函数

目录

构造函数

析构函数

拷贝构造函数

赋值运算符重载函数


构造函数 

我们知道对于C语言,初始化结构体变量,我们可以在结构体外直接对变量进行初始化(变量默认为共有属性);如下所示:

struct Complex    //定义结构体Complex
{
    int real;
    int imag;
};

Complex com1 = {2,3};   //初始化结构体变量

但是对于C++而言,为了满足封装的思想,我们通常将数据成员设置为私有属性(private0)或者保护属性(protected),此时我们对于类的对象的初始化,便不能直接在类外进行初始化。但是我们使用对象又必须先定义才能使用,那该怎么办呢???别急,C++提供了一个好方法,那就是利用构造函数完成对对象的初始化。

在建立对象的同时,采用构造函数给数据成员初始化,通常有以下两种形式。首先我们定义一个简单的复数类:

class Complex   //定义一个Complex类
{
public:
    Complex(double r,double i)    //定义构造函数
	{
		real = r;
		image = i;
	}
private:
    double real;
    double image;
};

形式一:

类名 对象名[(实参表)]                     //这里的“类名”与构造函数相同,“实参表”是为构造函数提供实际参数

Complex com(2,3);

形式二:

类名 *指针变量名 = new 类名[(实参表)]

Complex *pa = new Complex(2,3);

我们对其做出解释:形式一当我们定义类的对象com时调用构造函数Complex,分别给数据成员real和image赋初值2,3;形式二稍微复杂一点,其过程为编译系统开辟了一段内存空间,并在此空间存放了一个Complex对象,同时调用了该类的构造函数给数据成员赋初值。但是注意:这个对象是没有名字的,我们称之为无名对象。但是该对象有地址,这个地址存放在指针变量pa中。访问用new动态建立的对象一般是不用对象名的,直接通过指针访问就行了。

紧接着我们对构造函数做出说明:

  • 构造函数的名字必须与类名相同,否则编译器把他当作一般的成员函数来处理。
  • 构造函数没有返回值,在定义构造函数时,是不能说明它的类型的,甚至说明为void也不行。
  • 构造函数的功能就是完成数据成员的初始化。
  • 构造函数和普通函数一样,既可以写在类中,也可以写在类外,但是写在类外记得加上类的作用域。
  • 构造函数一般声明为公有成员,但他不需要也不能像其他函数一样被显式的调用,他是在定义对象的同时自动调用的,而且仅仅执行一次。
  • 在实际应用中,通常需要给每个类定义构造函数。如果没有定义构造函数,则编译器系统自动生成一个默认构造函数。这个默认构造函数不带任何参数,函数体也是空的,它只能为对象开辟数据成员的存储空间,而不能给对象中的数据成员赋初值。
  • 构造函数可以不带参数。例如Complex::Complex(){}
  • 构造函数可以重载

 除了通过构造函数中的赋值语句给数据成员赋值,例如:

Complex::Complex(double r,double i)    //定义构造函数
   {
       real = r;
       image = i;
   }

我们还可以通过成员初始化列表对数据成员进行初始化,如下所示:

Complex(double r,double i):real(i),image(i)    //定义构造函数
{}

简单来看,以上完成的功能是一样的,都是给数据成员real和image赋值,那么为什么我们要使用成员初始化列表,什么时候用初始化成员列表来初始化数据成员呢?

C++的某些类型的成员时不允许在构造函数中用赋值语句进行直接赋值的,如:对于用const修饰的数据成员,或者用引用类型的数据成员,则不能使用赋值语句直接赋值。例如:

class A
{
public:
    A(int x1)
    { 
         x1 = x;
         rx = x1;       //错误,该数据成员是引用类型
         pi = rx;       //错误,该数据成员是const类型
    }
private:
    int x;
    int ℞
    const double pi;
};

说明:数据成员是按照他们在类中声明的顺序进行初始化的,与他们在成员初始化列表中列出的位置无关。

举例说明:

#include<iostream>
using namespace std;

class D
{
public:
	D(int i):mem2(i),mem1(mem2 + 1)
	{
		cout << "mem1:" << mem1 << endl;
		cout << "mem2:" << mem2 << endl;
	}
private:
	int mem1;
	int mem2;
};

int main()
{
	D d(15);
	return 0;
}

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

运行结果

说明:

按照上述说明,数据成员是按照他们在类中声明的顺序进行初始化的,与他们在成员初始化列表中列出的位置无关。那么首先初始化的应该是mem1,而mem1 = mem2 + 1;而mem2此时还没有初始化,因此所得结果是随机数,而不是16。

析构函数

析构函数也是一种特殊的成员函数。他执行的操作正好和构造函数相反,通常执行一些清理任务,如释放分配给对象的内存空间等。我们先对其进行一些说明。;

  • 析构函数名与类名相同,但他前面必须加一个波浪号(~).
  • 析构函数不返回任何返回值。在定义析构函数时,和构造函数一样,不能指定其返回值,甚至是void也不行。
  • 析构函数没参数,他不能被重载
  • 每个函数都必须都必须有一个析构函数,若没有显示定义析构函数,则编译系统会自动生成一个默认的析构函数,例如:Comolex::~Complex(){}对于大多数类来说,这个函数就可以满足要求,但是当数据成员占有外部资源时,就需要自己显示定义一个系析构函数。
class String
{
public:
	String(char *s)    //构造函数
	{
		if(s != NULL)
		{
            str = new char[strlen(s)+1];
		    strcpy(str,s);
		}
		else
		{
			str = new char[1];
			*str = '\0';
		}
	}
	~String()    //析构函数,数据成员持有外部资源,显示定义析构函数
	{
		delete str;
	}
private:
	char *str;
};
  • 除了在对象出main函数作用域时,对象被撤销,系统会调用析构函数,还用两种情况,系统也会自动调用:
  1. 如果一个对象被调用在一个函数体内,当这个函数调用结束时,将释放对象,析构函数被自动调用;
  2. 若一个函数是使用new运算符动态创建的,在使用delete运算符释放它时,delete会自动调用析构函数。

下面给出完整的复数类代码:

#include<iostream>
#include<cmath>
using namespace std;

class Complex   //定义一个Complex类
{
public:
    Complex(double r,double i)    //定义构造函数
	{
		real = r;
		image = i;
		cout << "construct Complex" << endl;
	}
	double abscomplex()
	{
		double tmp = real * real + image * image;
		return sqrt(tmp);
	}
	~Complex()           //定义析构函数
	{
		cout << "deconstruct ~Complex" << endl;
	}
private:
    double real;
    double image;
};

int main()
{
	Complex A(3.0,4.0);    
	Complex *pa = new Complex(6.0,8.0);

	cout << A.abscomplex() << endl;
	cout << pa->abscomplex() << endl;
	delete pa;
	return 0;
}

 拷贝构造函数

 拷贝构造函数是一种特使的构造函数,当在建立新对象时,用已存在的对象去初始化新对象,这个过程就要使用拷贝构造函数。拷贝构造函数由系统自动调用,不能被显式调用。

调用拷贝构造函数的三种情况:

<1>当用类的一个对象去初始化该类的另外一个对象时,拷贝构造函数将会被调用。例如:

  1. Complex com(com2);     //用对象com2初始化com
  2. Complex com2 = com3; //用对象com3初始化com2

<2>当函数的形参是类的对象时,在调用函数进行形参和实参结合时,拷贝构造函数将会被调用。例如:

void fun1(Complex p)    //形参是一个对象
{
	p.abscomplex();   //调用abscomplex()
}

int main()
{
	Complex com(2.0,3.0);
	fun1(com);
	return 0;
}

在main()函数中,执行语句fun1(com);便会调用拷贝构造函数,自我理解,实参向形参传参的过程其实就是p = com,还是等价于第一种情况。如果类中有显示定义的拷贝构造函数,就调用显示定义的拷贝构造函数,如果未定义,就调用系统自动生成的拷贝构造函数。

<3>当函数的返回值是类的对象,在函数调用完毕将返回值带回函数调用处时。再次就会调用拷贝构造函数,将此对象复制给一个临时对象并传到该函数的调用处。例如:

Complex fun1()
{
	Complex p1(6.0,8.0);
	return p1;        //返回值是一个对象
}

int main()
{
	Complex com(2.0,3.0);
	com = fun1();
	return 0;
}

由于对象com是在函数fun1中定义的,当函数调用结束时,p1的生命周期便结束了,因此在函数fun1()结束前,执行return p1时,将会调用拷贝构造函数将p1的值复制到一个临时对象中,这个临时对象是在编译系统早主程序中建立的。函数运行到结束时,对象p1消失,但临时对象将会通过p1 = fun1()将他的值赋给对象p1(这里其实是做出了相关优化,相关博文会在后续更新);执行完这个语句后,临时对象的使命也就完成了,该临时对象自动调用析构函数。

拷贝构造函数基本格式:

类名(const 类名 & 对象名)

在此我们将String类添加拷贝构造函数,代码如下:

class String
{
public:
	String(char *s)    //构造函数
	{
		if(s != NULL)
		{
            str = new char[strlen(s)+1];
		    strcpy(str,s);
		}
		else
		{
			str = new char[1];
			*str = '\0';
		}
	}

	String(const String &s)   //拷贝构造函数
	{
		str = new char[strlen(s.str) + 1];
		strcpy(str, s.str);
	}

	~String()    //析构函数,数据成员持有外部资源,显示定义析构函数
	{
		delete str;
	}
private:
	char *str;
};

 

赋值运算符重载函数

关于运算符重载在这里我们不做详细介绍,我们仅简单简单介绍赋值运算符重载函数的功能,函数基本形式以及函数的分析。

 赋值运算符重载函数的作用为将已有的对象赋给另外一个对象。例如:

#include<iostream>
using namespace std;

class Complex
{
	...
};

int main()
{
	Complex A(2.0,3.0);
	Complex B(3.0,4.0);
	B = A;
	return 0;
}

基本形式:

类名 & operator=(const 类名 &对象名)

我们给出String类的赋值运算符重载函数代码:

String& operator=(const String &s)
	{
		if (this == &s)
			return *this;

		delete[]str;

		str= new char[strlen(s.str) + 1];
		strcpy(str, s.str);
		return *this;
	}

写一个赋值运算符重载函数通常分三步走,我称之为”三分天下,一统河山“。

  1. 首先检查自我复制,通过查看赋值运算符右边的地址(&s)是否与接收对象(this)的地址相同。如果相同,程序将返回*this,然后结束。
  2. 如果地址不同,函数将释放str指向的内存,这是因为稍后将把一个新字符串的地址赋给str。如果不首先使用delete运算符,则上述字符串将保留在内存中。由于程序中不再包含指向该字符串的指针,因此这些内存将被浪费掉。
  3. 与拷贝构造函数类似,为新字符串分配足够的空间,然后将赋值运算符右边的对象中的字符串赋值到新的内存单元中去,程序返回*this并结束。

最后我们做出总结:构造函数、析构函数、拷贝构造函数都是一些特殊的函数,他不由我们显式调用,而是由系统自动调用;当数据成员持有外部资源时,我们需要显示定义析构函数和拷贝构造函数,避免资源泄漏。

扩展:拷贝构造函数和赋值运算符重载函数可以解决浅拷贝问题,关于浅拷贝,后面将陆续更新。 

发布了40 篇原创文章 · 获赞 124 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/FDk_LCL/article/details/89514819