C++中默认的成员函数

在C++中,有隐含的this指针,以及6个默认的成员函数。

一.this指针

首先我们来了解下this指针,1.在C++类中,每个成员函数都有一个默认的指针形参this指针,并且this指针是成员函数的第一个形参,this指针是隐式的(我们在类中声明或者定义成员函数的时候,不需要显式将this指针进行传参),对于我们的构造函数来说比较特殊,不存在这个隐含的this指针。

2.编译器会对成员函数进行处理,在对象调用该成员函数的时候,对象的地址会作为实参传递给成员函数的第一个this指针。

我们简单的用一个日期类了解一下this指针:

#include<iostream>
using namespace std;
class Date{
public:
	void Show()
	{
		this->_year=2018;
		this->_month=3;
		this->_day=24;
		cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	d.Show();
	system("pause");
	return 0;
}

在这个例子中,Show函数并没有传参,但是由于存在隐含的this指针,使我们可以用this指针来设置对象的成员变量。

二.六个默认成员函数

C++中的默认成员函数有六个,分别为:构造函数,拷贝构造函数,析构函数,赋值运算符的重载,取地址操作符的重载,以及const修饰的取地址操作符的重载等。接下来,我们依次进行分析。

(一).构造函数

在声明一个类的对象时,编译程序需为要对象分配存储空间,即进行必要的初始化。对于成员变量,一般来说为了保持数据的封装性,数据我们都会将它设置为私有的,那么如果我们想在类的外面对它进行初始化,就必须有一个公有的成员函数来进行,C++中提供了一个默认的构造函数来初始化类的成员。

#include<iostream>
using namespace std;
class Date{
public:
	//无参构造函数
	Date()
	{}
	Date (int year,int month,int day)//带参构造函数
	{
		_year=year;
		_month=month;
		_day=day;
	}
	Date (int year=1997,int month, int day)//半缺省构造函数
	{
	  _year=year;
	  _month=month;
	  _day=day;
	}
	Date(int year=1997,int month=1,int day=1)//全缺省构造函数
	{
		_year=year;
		_month=month;
		_day=day;
	}
    void Show()
	{
		cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	d.Show();
	system("pause");
	return 0;
}

系统默认的构造函数为Date(){},当我们不定义构造函数的时候,例如定义一个类对象Date d;程序会去自动调用默认的构造函数,但是这会造成一个问题,运行结果会出现随机值,如图:


那为了避免出现随机值的这种情况,我们需要有一个自定义类型的构造函数,上面的程序中我给出了四种不同的构造函数:分别为无参构造函数,带参构造函数,半缺省构造函数以及全缺省构造函数。全缺省构造函数可以达到我们预期的效果,在我们只定义了对象但是并没有对其进行初始化的时候,全缺省构造函数可以使用缺省参数对它进行初始化,在上面的代码中我们使用全缺省后的运行结果为:


虽然构造函数可以进行重载,但是无参构造函数和全缺省的构造函数不能同时存在,因为如果我们定义了 Date d;这样的对象后,会产生二义性,编译系统无法确定应该调用哪一个构造函数。

构造函数的特征如下:1.构造函数名必须与类名相同。

                                  2.它可以有任意类型的参数,但不能具有返回值。

                                  3.构造函数在对象实例化时会自动调用,不需要用户显试的进行调用。

                                  4.构造函数可以重载

                                  5.如果类中定义了构造函数,那么系统就不会生成默认的构造函数。如果没有定义,系统会默认生成一个                                      构造函数 。                

                                 6.无参的构造函数和全缺省的构造函数都认为是缺省构造函数,并且只能存在一个(二义性问题)

                                 7.构造函数与普通成员函数一样,函数定义可以在类中亦可以在了类外。

                                 8.构造函数一般声明为公有成员,但他不需要像其他成员函数一样显式的被调用,它在定义对象时同时被                                    调用,而且只执行一次。

(二).拷贝构造函数

 拷贝构造函数的作用是在创建一个新对象时,同时使用一个已经存在的对象对它进行初始化操作;例如上例中main函数中定义了:

int main()
{
	Date d1(2018,3,24);
	d1.Show();
	//Date d2=d1;
	Date d2(d1);
	d2.Show();
	system("pause");
	return 0;
}

运行结果为:


自定义的拷贝构造函数形式为:类名::类名(const 类名& 对象名){函数体}

调用拷贝构造的一般形式为:代入法:类名 对象2(已存在对象1)或者赋值法:类名 对象2=已存在对象1

其实一般来讲系统默认的拷贝已经可以让我们日常使用了,但是存在特殊情况,我们知道拷贝构造函数调用时就是对数据成员进行逐一赋值,这是浅拷贝。但是如果类中存在指针类型的数据,这种方法便会产生错误(对同一块空间释放两次内存),这就是深浅拷贝的问题,后面我会深入的写一篇关于深浅拷贝问题的博客。

拷贝构造的特点:

1.拷贝构造函数函数名与类名相同,没有返回值,所以其实拷贝构造函数是构造函数的重载。

2.拷贝构造只有一个参数,并且是同类对象的引用,如果以传值的方式进行传参,将会无穷递归,导致栈溢出。

3.若没有显示定义拷贝构造,编译系统会默认生成一个拷贝构造函数,缺省的拷贝构造函数会依次拷贝类成员进行初始化。

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

1.当用类的一个对象去初始化另外一个对象时。(一般情况)

2.当函数的形参是类得对象,调用函数进行形参与实参的结合时。

3.当函数的返回值是对象,函数执行完返回调用者时。

(三).析构函数

析构函数:当一个对象生命周期结束(函数调用完毕或者整个程序执行完或者exit()退出)时,C++编译系统会自动调用一个成员函数,这个特殊的成员函数为析构函数。如果说构造函数是用来为对象分配空间,进行初始化的特殊成员函数,那么析构函数就是用于撤销对象时执行一些清理任务,释放空间的。析构函数的一般定义形式为:~类名(){函数体}

析构函数的特点:

1.析构函数前面与类名相同,但它前面必须加上~;

2.析构函数没有参数也没有返回值。

3.由于析构函数没有参数,所以析构函数不支持重载,因此一个类中有且仅有一个析构函数。如果未定义,则系统默认生成。

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

5.析构函数内部并不是删除对象,而是做一些清理工作,例如类中的指针时用new动态开辟出来的空间,我们就需要在析构函数中进行显式的释放。例如:

class Array{
	Array(int size)
	{
		_p=new int[size];
	}
	//具有清理功能的析构函数
	~Array()
	{
		if(_p!=NULL)
		{
			delete []_p;//释放空间
		}
	}
private:
	int *_p;

};

(四)赋值运算符的重载

为了增强程序的可读性,C++支持运算符的重载。

赋值运算符是双目运算符,如果用户没有自定义的赋值运算符函数,系统会自动生成一个默认的赋值运算符函数实现对象的赋值操作。

例如我们可以用一个日期类来看一下:

#include<iostream>
using namespace std;
class Date{
public:
	Date(int year=1997,int month=1,int day=1)//全缺省构造函数
	{
		_year=year;
		_month=month;
		_day=day;
	}
	Date& operator=(const Date& d)
	{
		if(this!=&d)
		{
			this->_year=d._year;
			this->_month=d._month;
			this->_day=d._day;
		}
		return *this;
	}
    void Show()
	{
		cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2018,3,24);
	Date d2;
	//赋值运算符的重载
	d2=d1;
	d2.Show();
	//拷贝构造
	Date d3=d1;
	Date d3(d1);
	d3.Show();
	system("pause");
	return 0;
}

我们一定要区分清楚什么时候调用的是拷贝构造,什么时候调用的是赋值运算符的重载,简单来说,拷贝构造是在创建对象的同时并用一个已存在对象进行初始化。而赋值运算符的重载是先创建出一个对象,再对其进行赋值操作。

在上面的赋值运算符重载中,它的返回值为Date&,*this出了作用域还存在,这样做可以节省空间并且更高效,它必需具有返回值,如果我们进行了类似于d3=d2=d1的操作,试想一下,如果没有返回值这样是不是就不行了。

和拷贝构造函数一样,通常情况下默认的赋值运算符是可以胜任工作的,但是对于许多重要的实用类来说,仅有默认的赋值运算符函数是不够的,指针悬挂就是一个典型问题,其实还是深浅拷贝问题,这里我不再多说。

(五)取地址运算符的重载

Date* operator&()
{
return this;
}

(六)const修饰的取地址符的重载

const Date* operater&() const
{
return this;
}

三.类的成员变量有两种初始化的方式:

1.初始化列表

2.构造函数体内进行赋值

前面我们已经讲了构造函数,接下来我主要说一下初始化列表,初始化列表是以一个冒号开始,接着一个逗号分隔数据成员,每个数据成员防在()里进行初始化。例如:

//利用成员初始化列表对对象进行初始化
	Date(int year,int month,int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

初始化列表更高效,为什么?首先,成员初始化列表写法简单,尤其是初始化的数据成员比较多时更显示其优越性。其次,性能问题,对于内置类型,如int,char,float,double等,这两种差别不是很大,但对于类中自定义类型,初始化列表比在构造函数体内初始化少了一次调用默认构造函数的过程,这对于数据密集的类来说是非常高效的。

例如:

#include<iostream>
using namespace std;
class A{
public:
	A()//构造
	{
		cout<<"A()"<<endl;
		_a=0;
	}
	A(const A& a)//拷贝构造
	{
		cout<<"A(const A& a)"<<endl;
		_a=a._a;
	}
private:
	int _a;
};
class Date{
public:
	//利用构造函数进行初始化
	Date(int year,int month,int day,const A& b)
	{
		_year=year;
		_month=month;
		_day=day;
		_a=b;
		cout<<"构造函数初始化"<<endl;
	}
	//利用成员初始化列表对对象进行初始化
	Date(int year,int month,int day,const A& b)
		:_year(year)
		,_month(month)
		,_day(day)
		,_a(b)
	{
		cout<<"初始化列表"<<endl;
	}
    void Show()
	{
		cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
	A _a;
};
int main()
{
	A a;
	Date d1(2018,3,24,a);
	d1.Show();
	system("pause");
	return 0;
}

在使用构造函数初始化时,结果为:


在使用成员初始化列表初始化时,结果为:


有些成员变量必须放在初始化列表里面:

1.被const修饰的常量成员变量(常量创建时必须初始化)

2.引用类成员变量(引用在定义时必须初始化)

3.没有缺省构造函数的类成员变量

(四)最后我再介绍一下关于类成员初始化的顺序

#include<iostream>
using namespace std;
class Date{
public:
	Date(int i)
		:_mem2(i)
		,_mem1(_mem2)
	{
		cout<<"_mem1:"<<_mem1<<endl;
		cout<<"_mem2:"<<_mem2<<endl;
     }
private:
	int _mem1;
	int _mem2;
};
int main()
{
	Date d(24);
	system("pause");
	return 0;
}
运行结果:


出现这种情况的原因是成员变量是按照声明顺序依次进行初始化的,而非初始化列表中出现的顺序,我们应当避免这种情况的发生。















猜你喜欢

转载自blog.csdn.net/qq_39344902/article/details/79675139