现在的位置: 首页 > 综合 > 正文

高质量C++编程_第9章_类的构造函数、析构函数与赋值函数

2013年12月02日 ⁄ 综合 ⁄ 共 3656字 ⁄ 字号 评论关闭

1、构造函数

任意一个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)

如果不显式定义这些函数,则编译器为类产生四个缺省函数:缺省的无参数构造函数、缺省的拷贝构造函数、缺省的析构函数、缺省的赋值函数

举例:

A(void); // 缺省的无参数构造函数
A(const A& a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A& operate =(const A& a); // 缺省的赋值函数

手动编写这四个函数的原因:这四个默认的函数均采用“位拷贝”的方式来实现,倘若类中含有指针变量,相当于只把地址赋给另一个变量,会出现内存泄露和野指针的情况。

拷贝时指针可能遇到的问题:

(1) 左值中的指针指向的地址没有被释放,导致内存泄露

(2) 赋值后,左值和右值指向同一块空间,任何一方对内存的改变都会影响另外一方,结果难以预料。

(3) 调用析构函数时,会对同一块内存释放两次。

2、构造函数的初始化表

格式:

类名::构造函数名(参数表):(数据成员名1(初始值1),数据成员名2(初始值2),…… )
{
	函数体
}

举例:

 class A
 {
 public:
 	A(int x,int y):m_x(x),m_y(y)
 	{ }
 private:
 	int m_x;
 	int m_y;
 };

书写位置:函数参数表之后,却在函数体 {} 之前

调用时间:该表里的初始化工作发生在函数体内的任何代码被执行之前

构造函数初始化表的使用规则:

(1) 如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数.

(2) 类的const常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化

(3) 非内部数据类型的数据成员的初始化可以采用初始化表实现,效率高

(4) 内部数据类型的数据成员的初始化可以采用函数体内赋值两种方式,效率一样,当更加直观

注意:只有构造函数才可以使用初始化表对成员初始化

举例:说明非内存成员使用初始化表实现的效率

使用初始化表初始化

#include <iostream>
using namespace std;
class A
{
public:
	A()
	{
		cout<<"A::A()"<<endl;
	}
	A(const A& other)
	{
		cout<<"A::A(const A& other)"<<endl;
	}

	A& operator=(const A& other)
	{
		cout<<"A::operator="<<endl;
		return *this;
	}
};

class B
{
public:
	B(const A& a):m_a(a)
	{
		cout<<"B::B()"<<endl;
	}
private:
	A m_a;
};

int main()
{
	A a; //调用A的构造函数
	B b(a);  //调用类B的构造函数和A的拷贝构造函数(A m_a = a;)
	/*
	输出结果:
	A::A() //调用A的构造函数
	A::A(const A& other) //执行类B的构造函数的初始化列表-调用A的拷贝构造函数(A m_a = a;)
	B::B() //执行类B的构造函数的大括号里面的
	*/
	system("pause");
	return 1;
}

不使用初始化表初始化类成员

#include <iostream>
using namespace std;
class A
{
public:
	A()
	{
		cout<<"A::A()"<<endl;
	}
	A(const A& other)
	{
		cout<<"A::A(const A& other)"<<endl;
	}

	A& operator=(const A& other)
	{
		cout<<"A::operator="<<endl;
		return *this;
	}
};

class B
{
public:
	B(const A& a)
	{
		m_a  = a;
		cout<<"B::B()"<<endl;
	}
private:
	A m_a;
};

int main()
{
	A a; 
	B b(a);  
	/*
	输出结果:
	A::A() //执行A a;
	A::A() //执行A m_a;
	A::operator=    //执行B的构造函数中的m_a  = a;
	B::B()  //执行B的构造函数中的 cout<<"B::B()"<<endl;;
	*/
	system("pause");
	return 1;
}

例子说明:在类中有对象成员时,

当使用初始化列表初始化该对象成员时,编译器会使用A m_a(a)的方式定义病初始化对象成员,此时会调用拷贝构造函数

当使用函数体内赋值初始化对象成员时,编译器会先定义A对象 A m_a,之后使用 m_a = a 的方式初始化对象成员m_a,此时会调用A的构造函数和赋值函数。

如果不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?

可以将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。当写的程序调用私有的拷贝构造函数和赋值函数时,编译器将指出错误,因为外界不可以操作A的私有函数。

举例:

#include <iostream>
using namespace std;
class A
{
public:
	A()
	{
		cout<<"A::A()"<<endl;
	}
private:
	A(const A& other)
	{
		cout<<"A::A(const A& other)"<<endl;
	}

	A& operator=(const A& other)
	{
		cout<<"A::operator="<<endl;
		return *this;
	}
};


int main()
{
	A a; //调用A的构造函数
	//A aa = a; //报错:A::A”: 无法访问 private 成员
	A aaa;
	//aaa = a; //报错:A::operator =”: 无法访问 private 成员
	system("pause");
	return 1;
}

注意:如果把A的构造函数也设为私有函数,则在执行语句A a时也会报错,即类将不能定义对象。

3、如何在派生类中实现类的基本函数

注意:基类的构造函数、析构函数、赋值函数都不能被派生类继承。

如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:

(1) 派生类的构造函数应在其初始化表里调用基类的构造函数。

(2) 基类与派生类的析构函数应该为虚函数(实现多态)

(3) 编写派生类的赋值函数和拷贝构造函数时,注意不要忘记对基类的数据成员进行重新赋值。

注意:调用基类的拷贝构造函数和赋值函数时,传参都是派生类对象。

写派生类的构造函数时,为了给基类传参,需要在初始化表中传如派生类对象。

写派生类的赋值函数时,显式调用基类的赋值函数,参数也是派生类对象。

举例:说明注意事项 1 和 注意事项 3

#include <iostream>
using namespace std;
class A
{
public:
	A(int x)
	{
		m_x = x;
	}
	A(const A& other)
	{
		m_x = other.m_x;
	}

	A& operator=(const A& other)
	{
		
		//检测自赋值
		if (this == &other)
		{
			return *this;
		}

		//对自身成员进行赋值
		m_x = other.m_x;

		//返回本对象的引用
		return *this;
	}
	void display()
	{
		cout<<"m_x = "<<m_x<<endl;
	}
private:
	int m_x;
};

class B : public A
{
public:
	B(int x,int y): A(x)
	{
		m_y = y;
	}
	B(const B& other) : A(other) //***别忘了***,而且传参是派生类的对象
	{
		m_y = other.m_y;
	}

	B& operator=(const B& other)
	{
		//检测自赋值
		if (this == &other)
		{
			return *this;
		}

		//对派生类成员进行赋值  ***别忘了***
		A::operator=(other);
		//static_cast<A&>(*this) = other;//也可以,详细见effective c++

		//对自身成员进行赋值
		m_y = other.m_y;

		//返回本对象的引用
		return *this;
	}

	void display()
	{
		A::display();
		cout<<"m_y = "<<m_y<<endl;
	}
private:
	int m_y;
};

int main()
{
	B b(1,2);
	b.display();
	/*输出:m_x = 1,m_y = 2*/
	B bb(3,4);
	bb.display();
	/*输出:m_x = 3,m_y = 4*/
	bb = b;
	bb.display();
	/*输出:m_x = 1,m_y = 2*/
	system("pause");
	return 1;
}

注意事项:如果在赋值语句 和 构造函数中不对基类进行赋值,基类中的变量将变成随机数。

4、总结:

1、一个空类,编译器会默认为其生成四个缺省函数:缺省的无参数构造函数、缺省的拷贝构造函数、缺省的析构函数、缺省的赋值函数。

2、只有构造函数才能使用初始化表为成员赋值

3、类中的对象成员使用初始化表为其赋值,效率高;而内部成员在函数体内为其赋值,更加直观。

4、基类的构造函数、析构函数、赋值函数都不能被派生类继承。

5、在有派生的类中,写派生类的拷贝构造函数和赋值函数时,不要忘了基类成员赋值。

抱歉!评论已关闭.