《随笔五》——C++中的“构造函数、析构函数、拷贝构造函数、Const成员数据和引用成员数据初始化、成员初始化的顺序”

目录

构造函数

限制对象创建

拷贝构造函数

调用拷贝构造函数

为Const成员数据初始化 和 引用成员数据初始化应注意的问题



构造函数


●  构造函数的主要功能是: 为对象分配空间,也可用来为类数据成员赋初值。 该函数没有返回类型,不能有return语句;甚至 void也不行。 而且不能是 const 和 static 成员函数。

●  在实际应用中,一般都要给类声明和定义构造函数,如果没有声明和定义,编译系统就自动生成一个默认的构造函数,这个默认的构造函数不带任何参数,只能给对象开辟一个存储空间, 而不能为对象中的数据成员赋初值, 此时数据成员的值是随机的。 而且,该默认的构造函数是内联函数。 编译器创建的默认构造函数也被称为合成默认构造函数。

这个合成的默认构造函数将按照如下规则初始化类的数据成员:

如果存在类内的初始值,用它来初始化成员。
否则,默认初始化该成员

 

如果在类中显式声明了构造函数,无论是否有参数,编译器都不会再为之生成任何形式的构造函数, 如果你还需要不带参数的默认构造函数,可以手动声明一个。

Sales_data() = default;​​​​​​​

可以这样定义一个自己的默认构造函数,我们定义这个构造函数的目的仅仅是因为我们既需要其他形式的构造函数, 也需要默认的构造函数。 我们希望这个函数的作用完全等同于之前使用的合成默认构造函数。

 在C++11新标准中,如果我们需要默认的行为, 那么可以通过在参数列表后面写上 = default 来要求编译器生成构造函数。

其中,= default 既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果 = default 在类的内部,则默认构造函数是内联的; 如果它在类的外部,则该成员默认情况下不是内联的。
 

● 默认构造函数是指不需要指定实参就能被调用的构造函数, 这并不意味着它不能接受实参。 只意味着构造函数的每个参数都有一个默认值与之关联。

         以下函数都是默认的构造函数
	Time() {} 没有参数的构造函数
	   参数被指定的默认构造函数
	Time(int size = 9){}
	Time(double re=5.2,double im=0.5){}

注意: 对象所占据的内存空间只是用于存放数据成员, 成员函数不在每一个对象中存储副本。

 


默认构造函数的作用


● 对象被默认初始化或值初始化时自动执行默认构造函数。

默认初始化在以下情形使用:

当我们在块作用域内不使用任何初始值定义一个非静态变量(参见2.2.1节,第39页)或者数组时(参见3.5.1节,第101页)。

当一个类本身含有类类型的成员且使用合成的默认构造函数时(参见7.1.4节,第 235页)。

当类类型的成员没有在构造函数初始值列表中显式地初始化时(参见7.1.4节,第 237页)。

 

值初始化在以下情况下发生:

在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时(参见3.5.1节,第101页)。

当我们不使用初始值定义一个局部静态变量时(参见6.1.1节,第185页)。

当我们通过书写形如T( )的表达式显式地请求值初始化时,其中T是类型名  (vector的一个构造函数只接受一个实参用于说明vector大小(参见3.3.1节第88页), 它就是使用一个这种形式的实参来对它的元素初始化器进行值初始化).

所以,类必须包含一个默认构造函数以便在上述情况下使用,在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。

 


●  下面看一个数据成员被初始化的程序:

class Time
{
public:
	unsigned short hour;
	unsigned short minute;
	unsigned short second;
	const char *ptr;
};
int main()
{
	Time myTime = { 100,200,300 ,"huang"}; 用显示初始化列表初始化公有的数据成员
	cout << myTime.hour << " " << myTime.minute << " " << myTime.second <<" " << myTime.ptr << endl;
	system("pause");
	return 0;
}

根据数据成员被声明的顺序,显示初始化列表中的值一一对应初始化。

如果这样初始化就错误:

Time myTime = { "huang",100,200,300 }; 用显示初始化列表初始化公有的数据成员

显示初始化列表有两个缺点: 它只能被应用到所有数据成员都是公有的类的对象上。它要求程序员的显示干涉,增加了意外(忘了提供初始化表) 和 错误(弄错了初始值顺序)的可能性。

在某些应用中,通过显示列表初始化,用常量值初始化大型数据结构比较有效。

 


注意该程序:

class X
{
public:
	X(int size = 156)
	{
		a = size;
	}
	X()
	{
		a = 10;
	}
private:
	int a;
};
int main()
{
	X a; //创建X类的对象a
	X b(10);//创建另一个X类的对象b
	system("pause");
	return 0;
}

X类有两个互相矛盾的构造函数,  为对象a 调用哪一个构造函数? 两个都可以调用,所以会有编译错误。

 


限制对象创建


● 构造函数的可访问性由其声明所在的访问区来决定。 可以通过把相关的构造函数放到非公有访问区内, 从而限制或显式禁止某些形式的对象创建。

● 注意: 一般构造函数通常被声明为公有,如果是私有,那么在main函数中创建对象不能为其成员初始化, 因为没有访问权限。

如果说构造函数时私有的,那么该函数就不能被其他类或者全局函数所使用。而创建C++ 实例需要调用构造函数。 如果构造函数私有,除了类自己的方法外,其他类不能构造这个类的实例。所以,在一般情况下,构造函数是私有,其他类使用它就很困难了。

但是, 有时候不希望其他对象能够实例化一个类,就是这个类只需要一个实例的时候, 为了避免其他外部类创建多个实例的时候,通过把构造函数定义为私有的。

 


拷贝构造函数


●  该函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本类的对象的引用。  其作用是使用一个已经存在的对象去初始化同类的另一个新对象。

●  其形式为:

类名(类名 &对象名)
{
  //statement
}

注意: 其中 “&对象名 ” 表示对一个对象的引用, 该参数是一个已经初始化的类对象。该参数(对象的引用)是一个不可变的const类型。拷贝构造函数要调用基类的拷贝构造函数和成员函数。

如果没有定义类的拷贝构造函数,系统会在必要时自动生成一个默认的拷贝构造函数,该函数的功能是: 把已存在的对象中数据成员的值都一一对应复制到新建立的对象中。 因此,可以说完成了同类对象的创建, 两个对象中的数据成员的值相同,即完全相同的属性。

注意: 隐式的拷贝构造函数和显式声明的拷贝构造函数的不同在于对成员的关联方式。  

显式声明的拷贝构造函数关联的只是被实例化的类成员的缺省构造函数, 除非另外一个构造函数在类初始化或构造列表时被调用。

 


调用拷贝构造函数


● 一般来说,以下三种情况拷贝构造函数会被调用:

用类的对象去初始化该类的另一个对象时。

函数的形参是类的对象时,函数调用过程中进行形参和实参的复制时。

函数的返回值是类的对象, 函数执行完进行返回时。

下面看一个示例程序:

class Point
{
public:
	Point(int xx = 0,int yy = 0)
	{
		X = xx;
		Y = yy;
		cout << "构造函数被调用!" << endl << endl;
	}
	Point(const Point &p)
	{
		X = p.X; 为新创建的对象的成员赋初值
		Y = p.Y;
		cout << "拷贝构造函数被调用!" << endl << endl;
	}
	int getX() { return X; }
	int getY() { return Y; }
private:
	int X, Y;

};
void fun(Point pt)
{
	cout <<"输出fun 函数中的值"<< pt.getX() << endl << endl;
}
Point myFun()
{
	Point pt(1, 5); 调用构造函数
	return pt; 函数返回值是类对象,返回函数时,拷贝构造函数被调用
}
int main()
{
	第一种情况时
	Point A(2, 3); 调用构造函数
	Point B(A); 调用拷贝构造函数
	cout << B.getX() << endl << endl;

	第二种情况时
	Point C(5, 6);  调用构造函数
	fun(C);  函数的形参为类的对象, 当调用函数时,拷贝构造函数被调用

	第三种情况时
	Point D = myFun();
	cout << "输出对象D的X的值:" << D.getX() << endl << endl;
	system("pause");
	return 0;
}

//第三种情况时
Point D  = myFun();

 在以上这种情况,函数的返回值为什么会调用拷贝构造函数?

表面上函数 “ myFun() ” 将pt 返回给了主函数, 但是 pt 是函数 “ myFun() ” 的局部对象,离开建立它的函数 “myFun()” 以后生命周期就结束了, 内存就会被释放掉, 不可能在返回主函数中继续生存。

所以在处理这种情况时, 编译系统会在主函数中创建一个临时的无名对象,该临时对象的生命周期只在函数调用所处的表达式中, 即表达式 “D  = myFun()” 中。  执行语句 “return pt” 时, 实际上是调用拷贝构造函数将 pt 的值拷贝到临时对象中。 函数 “myFun()” 运行结束时, 对象pt 被销毁, 但是临时对象 会存在于表达式 D  = myFun() 中。 计算完这个表达式后, 临时对象的使命也就完成了, 该临时对象便自动消失。

● 注意:构造函数不可以改变它所引用的对象, 因为当一个对象以传递值的方式传入一个函数时, 拷贝构造函数自动地被调用来生成函数中的对象。 除了当对象传入函数时会被隐式调用外, 拷贝构造函数在对象被函数返回时也同样被调用。


为Const成员数据初始化 和 引用成员数据初始化应注意的问题


●  如果说某一个类中的有一个 const成员数据 和 一个引用成员数据,我们是无法在构造函数中的函数体中初始化的,const成员数据必须在构造函数的初始化列表中初始化,引用成员也一样下面来看代码示例:

如果有一个类是这样定义的:

class A
{
public:
	A(int pram1, float pram2, int &pram3, int pram4);
private:
	int a;
	int &b;
	const int c;
	float  d;
};

假如在构造函数中对四个私有变量进行赋值则通常会这样写:

Ab(int pram1, float pram2, int &pram3, int pram4)
	{
		a = pram1;
		b = pram3;
		c = pram4;
		d = pram2;
	}

但是,这样是编译不过的。因为常量和引用必须初始化而不能赋值。所以上面的构造函数的写法只是简单的赋值,并不是初始化。

正确写法应该是:

Ab::Ab(int pram1, float pram2, int &pram3, int pram4) :b(pram3),c(pram4)
{
	a = pram1;
	d = pram2;
}

凡是有引用类型的成员变量或者常量类型的变量的类,不能有缺省构造函数。默认构造函数没有对引用成员提供默认的初始化机制,也因此造成引用未初始化的编译错误。并且必须使用初始化列表进行初始化const对象,引用成员也一样。

看一下整个程序是怎样的:

class A
{
	 public:
		  A(int pram1, int pram2,int &param3);
		  void getVal()
		  {
			  cout << "输出a的值:" << a << "\n输出b的值:" << b << "\n输出c的值:" << c 
				   <<"\n输出d的值:" << d << endl;
		  }
	 private:
		  int a = 10;
		  float d = 5.5;
		  int &b;
		  const int c = 20;
};
// 注意param3必须是一个引用传递,不能是值传递,如果是值传递b直接引用临时变量param3了,
//在构造函数结束后这个临时变量param3就不存在了,而b仍然引用着,故就成了垃圾值了。
//所以用引用传递后 param3就变成引用tt了,即b也引用了tt, 这样就正确了

A::A(int pram1, int pram2 , int &param3) : c(pram2),b(param3) 

{
	a = pram1;
}
int main()
{
	int tt = 15;
	A myaa(5, 6,tt);
	myaa.getVal();
	system("pause");
	return 0;
}

输出a的值:5
输出b的值:15
输出c的值:6
输出d的值:5.5


还可以直接在类定义中直接初始化 const 数据成员、以及引用成员 ; 以及也可以初始化普通的数据成员。

class A
{
	 public:
		  A(int pram1);
		  void getVal()
		  {
			  cout << "输出a的值:" << a << "\n输出b的值:" << b << "\n输出c的值:" << c 
				   <<"\n输出d的值:" << d << endl;
		  }
	 private:
		  int a = 10;
		  float d = 5.5;
		  int &b = a;
		  const int c = 20;
};
A::A(int pram1) 
{
	a = pram1;
}
int main()
{
	A myaa(5);
	myaa.getVal();
	system("pause");
	return 0;
}

输出结果为:
输出a的值:5
输出b的值:5
输出c的值:20
输出d的值:5.5

如果说在类定义中我们直接用初始值初始化了成员数据,那么如果在创建类实例的时候,还为它提供初始化值,那么就会覆盖在类中定义的初始值。

仔细看上面的代码, 我们给 a 在类定义中初始化为10, 然后创建对象的时候,给了一个实参,该实参在构造函数中初始化a为5, 那么输出a的值时就是5.  因为b是a的别名,所以输出是5.


一个类的成员是一个对象成员(内嵌对象)——如何为它初始化


●  如果说一个类的成员是一个对象成员(即内嵌对象),如何为这样的内嵌对象调用函数, 这也是由初始化语法来完成的。

class D
{

public:
	D(const char _name[], int _snn):name(0),snn(0)
	{
		
	}
private:
	char *name;
	int snn;
		
};

class A
{
private:
	int weight;
	D myD; //如何为它初始化

public:
	A(const char _name[], int _snn, int _weight) :myD(_name, _snn)
	{
		weight = _weight;
	}

};

在A 中有一个数据成员是对象成员,在类A中用初始化列表初始化 myD的数据成员,也只能用初始化列表初始化。因为myD的类型是D, 所以调用的是类D的构造函数。

使用内前对象必须遵守的规则

● 如果一个类中包含其他类的对象(成员对象), 那么必须在该类的构造函数中的初始化阶段, 为它所使用的所有内嵌对象调用合适的构造函数,

注意: 在继承时, 对基类成员和成员对象 的初始化必须在 初始化阶段中进行,

● 如果实现者在调用内嵌对象的构造函数时失败, 编译器将设法为内嵌对象调用默认构造函数( 如果有可用且可访问的默认构造函数)

● 如果上面的都不成功, 则构造函数的实现是错误的(导致编译错误)

● 每个内嵌对象的析构函数, 将由包含该对象的类的析构函数自动调用, 无需程序员干预

 


成员初始化的顺序


● 成员初始化的顺序: 构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体顺序,成员初始化的顺序与他们在类定义中出现的顺序一致,第一个成员先初始化,然后第二个,以此类推。

构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。

class X
{
    int i;
    int j;
public:
    X(int val) :j(val), i(j) {} //未定义的:i在j之前被初始化
};

● 实际上,i先被初始化,因此这个初始值的效果是试图使用未定义的值 j 初始化 i 

● 最好令构造函数初始值的顺序与成员声明的顺序保持一致,而且如果可能的话,尽量避免使用某些成员初始化其他成员。如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

 

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值