[C++]类和对象(2)--------构造函数

文章详细介绍了C++中的默认成员函数,特别是构造函数的作用、特点以及如何通过初始化列表和explicit关键字控制隐式类型转换。构造函数用于对象创建时设置初始值,而默认构造函数在无显式定义时自动生成。
摘要由CSDN通过智能技术生成

1.类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。
任何类在什么都不写时,编译器会自动生成以下 6 个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数

2.构造函数

首先先来看下面的一段代码:

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.Init(2024, 5, 2);
	d1.Print();

	return 0;
}
对于 Date 类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置 信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
特征:
1.无返回值
2.函数名与类名相同

3.对象实例化的时候编译器可以自动调用构造函数.

4.构造函数可以支持重载

那么上面的代码可以改成下面的形式:

class Date
{
public:
	Date(int year=2019, int month=2, int day=10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024,5,2);
	Date d2;
	d1.Print();
	d2.Print();
	return 0;
}

上面的代码我给了构造函数参数了全缺省,这样我们在调用时就算不穿参数,编译器也会自动调用其构造函数,即在对象实例化时调用了构造函数。

class Date
{
public:
	/*Date(int year=2019, int month=2, int day=10)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024,5,2);
	Date d2;
	d1.Print();
	d2.Print();
	return 0;
}

就算我们不实现构造函数,编译器也会调用默认的构造函数。

总结:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

我们看了上面的代码可能会产生疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,默认生成的构造函数,对内置类型不做处理,自定义类型会去调用它的默认构造函数。

面对这种情况C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在 类中声明时可以给默认值

class Date
{
public:
	//Date(int year=2019, int month=2, int day=10)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year= 2022;
	int _month=4;
	int _day=5;
};

int main()
{
	//Date d1(2024,5,2);
	Date d2;
	//d1.Print();
	d2.Print();
	return 0;
}

来看上面的代码,我们可以给声明的成员变量给缺省值,这样就弥补了我们没有构造函数,编译器自动生产的构造函数却是初始化随机值的问题

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数

class Date
{
public:
	Date()
	{
		_year = 2000;
		_month = 5;
		_day = 2;
	}
	Date(int year = 2000, int month = 5, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
// 以下测试函数能通过编译吗?
int main()
{
	Date d1;
	return 0;
}

答案是不能的,因为会引起编译歧义。虽然这两个函数构成了函数重载,两个构造函数都符合调用条件,但编译器不知道要调用哪一个,故报错了。

2.1构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值

class Date
{
public:
Date(int year, int month, int day)
 {
     _year = year;
     _month = month;
     _day = day;
 }
private:
int _year;
int _month;
int _day;
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

这时可以引用初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式

class Date
{
public:
	Date(int year,int month,int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
		
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year = 2022;
	int _month = 4;
	int _day = 5;
};

int main()
{
	Date d1(2024,5,2);
	//Date d2;
    d1.Print();
	//d2.Print();
	return 0;
}
注意:
1. 每个成员变量在初始化列表中 只能出现一次 ( 初始化只能初始化一次 )
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
      引用成员变量
      const 成员变量
     自定义类型成员 ( 且该类没有默认构造函数时 )
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使 用初始化列表初始化。
初始化列表是每个成员变量定义初始化的位置
class Date
{
public:
	Date(int year,int month,int day)
		:_year(year)
		,_month(month)
		,_day(day)
		,n(1)
	{}
		
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
		cout << n << endl;
	}
private:
	int _year = 2022;
	int _month = 4;
	int _day = 5;
	const int n;
};

int main()
{
	Date d1(2024,5,2);
	//Date d2;
    d1.Print();
	//d2.Print();
	return 0;
}

上面我给了成员变量缺省值,其实:

缺省值的本质就是给初始化列表用的

可以看到我们在初始化时都是先走初始化列表的。

2.2成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

来看下面的代码:

class A
{
public:
	A(int a)
		:_aa2(a)
		,_aa1(_aa2)
	{}
	void Print() 
	{
		cout << _aa1 << " " << _aa2 << endl;
	}
private:
	int _aa1;
	int _aa2;

};
	
int main()
{
	A aa(1);
	aa.Print();

	return 0;
}

看看代码运行结果是如何呢?

直接看结果:

结果是随机值和1

在这个例子中,A类有两个整型成员变量:_aa1和_aa2。在构造函数中,_aa2被初始化为传入的参数a的值,而_aa1被初始化为_aa2的值。

然而,成员变量的初始化顺序是由它们在类中声明的顺序决定的,而不是它们在初始化列表中出现的顺序。在A类中,_aa1在_aa2之前声明,因此_aa1会先于_aa2初始化。

这意味着当_aa1(_aa2)执行时,_aa2还没有被初始化,所以_aa1的值是未定义的。然后,_aa2被初始化为1.

因此,当调用aa.Print();时,输出的第一个值(_aa1的值)是未定义的,而第二个值(_aa2的值)是1。在实际执行时,未定义的值可能是内存中该位置的任何值,这取决于编译器和运行时环境。

要修正这个问题,应该按照成员变量在类中声明的顺序初始化它们,或者更改成员变量的声明顺序以反映期望的初始化顺序。例如:

将顺序调整好就可以了。

在这个修改后的版本中,_aa1会先被初始化为1,然后_aa2会被初始化为_aa1的值,即1。所以Print函数会输出1 1

3.隐式类型转换

来看看下面这段代码:

class A
{
public:
	A(int a)
		:_a(a)
	{}
	void Print()
	{
		cout << _a << endl;
	}

private:
	int _a;
};

int main()
{
	A aa1(2);
	A aa2 = 3;
	aa1.Print();
	aa2.Print();

	return 0;
}

其结果是 2 3 

这里aa2直接被赋值了,那么为什么呢?

在C++中,如果一个类的构造函数只需要一个参数(或所有参数除了第一个外都有默认值),那么这个构造函数允许从构造函数参数类型到类类型的隐式转换。

这行代码演示了隐式类型转换。虽然看起来像是将整数3赋值给aa2,实际上C++编译器解释为使用3作为参数调用C类的构造函数来初始化aa2。这是因为C(int a)构造函数允许从int到C的隐式转换。


改初始化通常发生在使用=操作符进行对象初始化的场景中。不同于直接初始化(直接调用构造函数),初始化涉及到源对象到目标对象的潜在类型转换和赋值操作

类型转换:编译器使用3调用A的构造函数创建一个临时的A类型对象。
拷贝构造函数:这个临时对象然后用于初始化aa2。

最后的结果应该是常数3与aa2有一个与aa2相同类型的中间常量,这个常量也会调用构造函数,而当这个这个常量给予aa2时 (类比为:A aa2 = tmp(相当与中间常量))时会进行拷贝构造,最后应该是构造函数加拷贝构造函数都调用才对,但实际上编译器对此连续的构造+拷贝构造会进行优化,最后优化成只有调用构造函数。

所以明白了中间的原理,那么下面的代码可行吗?

class A
{
public:
	C(int x)
		:_x(x)
	{}
	
private:
	int _x;
};

int main()
{
	C& aa3 = 2;
	return 0;
}
实际上我们知道这其中会有隐式类型转换,那么中间就会有一个只读的临时常量,

引用的基本要求:在C++中,引用必须绑定到一个已经存在的对象上。引用本质上是对象的别名,它不能像指针那样可以更改指向的对象。

引用与临时对象:尽管临时对象(如通过类型转换创建的临时C对象)可以被绑定到const引用上(即const A&),但它们不能直接绑定到非const引用(A&)上。这是为了防止通过非const引用对临时对象进行修改,因为这种修改通常没有意义(临时对象在表达式结束后就销毁了),而且引用的权限被放大了,这是语法所不允许的

正确的用法:如果你的意图是创建一个A类型的临时对象,并将其绑定到引用上,正确的语法应该使用const引用:


const A& aa2 = 3; // 依赖于A(int)构造函数的隐式类型转换

4.explicit关键字

构造函数不仅可以构造与初始化对象, 对于接收单个参数的构造函数,还具有类型转换的作用 。接收单个参数的构造函数具体表现:
1. 构造函数只有一个参数
2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
3. 全缺省构造函数

那么如果不想让隐式类型转换发生,我们就需要用 explicit修饰构造函数,禁止类型转换

单参构造函数,没有使用explicit修饰,具有类型转换作用

C++11及以后版本版本支持多个参数隐式类型转换

class A
{
public:
	//explicit A(int a,int b = 2)
	 A(int a,int b = 2)
		:_a(a)
		 ,_b(b)
	{
		cout << "A(int a)" << endl;
	}


	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
	void Print()const
	{
		cout << _a <<" "<<_b << endl;
	}

private:
	int _a;
	int _b;
};

int main()
{
	A x = {1,3};
	x.Print();

	return 0;
}

想让隐式类型转换发生,可以加上explicit关键字

  • 59
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WEP_Gg

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值