C++/类与对象/默认成员函数@构造函数

名词概念:

默认构造函数:不用传参就可以调用的构造函数。有3种默认构造函数(但是只能存在一个):
       1、构造函数的参数是全缺省的
       2、构造函数是无参的
       3、编译器自动生成的(当我们没有编写构造函数时)

显式构造函数:用户自己编写的构造函数。
隐式构造函数:编译器主动生成的。注意当用户自己编写了构造函数(包括拷贝构造函数),也就是出现显示构造函数时,便不会有隐式构造函数,即编译器不会主动生成了



默认成员函数

  在C++的类中,有6个默认的成员函数。所谓的默认成员函数,就是用户自己没在类中编写的“特殊”成员函数,但是编译器会自动生成的成员函数(也叫“隐式成员函数”)。
(ps:不要跟上面的 “ 默认成员函数 ” 概念混淆了,说不清道不明的,靠自己领会了)
如下所示

#include <iostream>
using namespace std;

class Date
{

};

int main()
{
	Date d;
	// cout<<sizeof(d)<<endl;  // 对象d的内存大小是1
	return 0;
}

  我们定义了一个类Date,而在Date这个类中既没有成员变量,也没有成员函数,是一个空类。
  然而实际上,编译器自己默默地生成6个成员函数。分别是 默认构造函数默认析构函数默认拷贝构造函数默认赋值运算符重载函数默认取地址操作符重载函数默认const取地址操作符重载函数

  题外话,我们在main函数里面,创建了一个空类Date的对象(也叫实例),请问对象d的大小是多少呢?验证发现对象d的内存大小是1。面对这种情况我会产生两个疑问:
1、即然Date是空类,那由空类产生的对象不应该内存是0吗,怎么会是1?
2、即然有默认的成员函数,那成员函数不是也有内存大小吗?6个成员函数内存大小所占空间大小怎么会是1?

文章末尾对这两个疑问进行解答。

下面我们对默认构造函数,展开讲解


构造函数

概念:

  在C++中,构造函数是一种特殊类型的成员函数,用于在对象被创建时执行初始化操作。构造函数的名称与类名相同,没有返回类型,甚至不使用 void。


函数特征

函数名类名相同
② 没有返回类型
如下面所示,类中的成员函数 Date 便是该类的构造函数

#include <iostream>
using namespace std;
class Date
{
public:
    // 构造函数
	Date()
	{
	}
};

  在c语言中,如果我们创建了一个变量,没有初始化,那么变量将是随机值。因此良好的编程习惯是,创建一个变量的同时,顺便给变量初始化,让变量的值是可知的,可预测的。
  在C++中同样的道理,当我们定义了一个类,用类创建对象时,顺便的就给对象初始化了。
与C语言的区别是,初始化的这个动作,不用我们自己做,编译器自动帮我们做了 (当我们编写定义了构造函数时,编译器自动去调用构造函数;当我们没有编写构造函数时,编译器自动调用自己生成的构造函数)
可以这么理解:在C++的类中,构造函数便是给对象初始化的函数(即 构造函数 就是 初始化函数)。

为了方便理解,先从显示构造函数说起,也就是自己编写构造函数。

显示构造函数

#include <iostream>
using namespace std;

class Date
{
public:
	// 成员函数1
	Date()
	{
		// 默认构造函数
		_year=2023;
		_month=11;
		_day=29;
	}
	// 成员函数2
	//Date(int year, int month, int day)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
	// 成员函数3
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;    // 会被编译器转换为 -->  d1.Date(&d1)
	d1.Print(); // 调用类中的成员函数Print,打印对象1中的日期
	//Date d2(1976,9,9)   // 对象2
	//d2.Print(); // 调用类中的成员函数Print,打印对象2中的日期
	return 0;
}

  如上代码所示,我们用类Date实例出对象d1,然后调用类中的成员函数3打印对象d1的所有成员变量(也就是日期),运行程序,结果如下

d1对象日期打印结果
验证结论是,对象d1在创建时,编译器自动的去类里面寻找默认构造函数(也就是成员函数1),而我们在成员函数1里面完成了所有成员变量的初始化。总的效果来说编译器主动的帮我们对对象d1初始化了。

到此构造函数就可以结束了吗?你觉得该程序还有什么不好的点,可以有更友善灵活的点吗?

我们发现,不管我们用Date创建多少的对象,对象的初始化都是同一个日期2023-11-29。那么如果我要创建一个对象,但是初始化的日期是自己指定的呢?如下面创建一个对象d2,指定日期是1976-9-9。

Date d2(1976,9,9);

这时,编译器报错提示 “没有与参数列表匹配的构造函数”
这是因为前面我们创建对象d1时,编译器找的是无参的、默认构造函数,而类中的成员函数1正好就与之匹配。而现在我们创建了对象d2,但是初始化时传入了我们指定的日期参数,这时编译器去类中找的是带有参数的构造函数(也就是成员函数2,代码中我注释掉了),而我们还没有编写带参的构造函数,所以编译器找不到匹配的构造函数,便报错编译不通过了。
解决方法是,没有条件就给创建条件,我们再在类中定义一个带参的构造函数,也就成员函数2,如下:

	// 成员函数2
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

这时,我们创建对象2

Date d2(1976,9,9);
d2.Print();

时,编译器便会找到成员函数2,完成对象2的成员变量的初始化。(clue:成员函数2不是默认构造函数)
运行结果如下:
d2成员变量打印结果

到此,我们实现了构造函数的全部功能,
1、当创建对象不给定参数时,调用默认构造函数初始化;
2、当创建对象给指定参数时,调用带参数的构造函数初始化。

但是,我们发现要实现这两个功能,要写两个构造函数,有点繁琐,而且也很怪怪的,两个构造函数…那么有没有方法用一个构造函数,实现上面两个构造函数的功能呢?如下:

实现方法:使用带有全缺省参数的构造函数(为了方便,记为成员函数4)。


class Date
{
public:
	// 成员函数4
	// 更好的实现(默认构造函数:不传参就可以调用的函数) -> 全缺省
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "Date()" << this << endl; // 用来查看构造函数和析构函数 创建和销毁的顺序
	}
	// 成员函数3
	void Print()
	{
		cout <<  _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;    		// 会被编译器转换为 -->  d1.Date(&d1)
	d1.Print(); 		// 调用类中的成员函数Print,打印对象1中的日期
	Date d2(1976,9,9)   // 对象2
	d2.Print(); 		// 调用类中的成员函数Print,打印对象2中的日期
}

当创建对象不带参时,构造函数使用缺省值初始化;
当创建对象带参时,构造函数使用接收到的参数初始化。


最后,我们对以上,成员函数1、2、4进行总结分析:
1、当在类中只定义了成员函数1时,可以创建不带参数的对象,但是却无法创建指定初始值(带参数)的对象,如

Date d1(2023,11,29);

编译器会报错,错误类型是“没有与参数列表匹配的构造函数”


2、当在类中只定义了成员函数2时,可以创建指定初始值(带参数)的对象,但是却不能创建不带参数的对象,如

Date d1;

编译器会报错,错误类型是“类Date中没有默认构造函数”


3、当在类定义了成员函数1和成员函数2,就可以解决以上两种问题。

4、可以只在类中只定义成员函数4,便能实现成员函数1、2的功能。需要注意的是:当定义了成员函数4时,不能同时存在成员函数1 或 成员函数2,否则将导致编译器报错,因为编译器不知道该调用哪一个构造函数。

至此,显示构造函数学习完毕!下面继续来看看隐式构造函数。


隐式构造函数

  通过前面的介绍,我们能够得知,隐式构造函数就是,当用户自己没有编写定义构造函数时,编译器自动生成的默认构造函数。下面我们验证一下编译器自动生成的默认构造函数,在背后干了个啥。

class Time
{
public:
	// 构造函数
	Time
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
		cout<<"Time()"<<" :"<<"  ";
		cout<<_hour<<_minute<<second<<endl;	 // 将初始化后的结果打印输出
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	// 成员函数1
	void Print()
	{
		cout<<"Date:\t";
		cout <<  _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;   // 类Time 实例出的对象 _t
};

int main()
{
	Date d1;
	d1.Print();
}

运行程序,结果如下:
运行结果

运行结果表明,编译器自动生成的,隐式构造函数做了以下的事情:
1、对于内置类型的成员变量,没有做任何处理;
2、对于自定义类型的成员变量,会去调用自定义类型对象的构造函数。



下面分析代码:
1、首先我们定义了两个类,分别是类Time 和 类Date;
2、在类Time中定义了无参的构造函数Time()。在函数内,对所有成员变量都初始化为0,同时打印输出初始化的结果;
3、在类Date中,我们没有定义构造函数,由编译器自主去生成。只定义了成员函数Print(),在函数内,打印出经过编译器生成的构造函数初始化后的成员变量。注意:在类Date的成员变量中,存在着一个由类Time实例出的对象 _t;
4、在main函数中,我们用类Date实例出一个对象d1,当我们创建出对象时,编译器便会主动去调用自主产生的构造函数,对对象d1初始化。然后我们调用对象d1中的成员函数Print(),打印出初始化后的成员变量。


根据最终的结果,我们可以总结出以下的结论
编译器自主生成的隐式构造函数,是一个大型的双标现场,因为
隐式构造函数针对内置类型的成员变量并没有做任何处理,
而对于自定义类型的成员变量,会去调用自定义类型创建的对象的构造函数进行初始化。(如果自定义类型中也没有显示构造函数,则调用自定义类型中的隐式构造函数)

ps:内置类型:char 、 int 、 double 、 float …
  自定义类型 : 自己定义的类型,如类、结构体、枚举…


以上,便是对于C++中,类中6个默认成员函数之一的构造函数的学习记录。










结局彩蛋:
问题1:
  给空类Date创建的对象d分配1个字节的空间,是因为存在
虽然Date是空类,但是用Date创建出来的对象d是客观上存在的,不要主观的认为Date是空类,Date创建的对象d也就不存在了。
  即然对象d是客观上实实在在存在的,而每个对象又是独一无二的,因此每个对象在内存中都有自己独一无二的内存地址。所以编译器会为空类Date创建的对象d分配一个字节的内存,确保对象d也有自己独特的地址,保证了“确保每个对象都有独特的地址”的原则。
ps:这个字节通常被称为 “空对象占用的内存” 或者 “对象的内存对齐” 。它不包含任何用户定义的成员变量,而是用于区分不同对象在内存中的位置。


问题2:
  计算对象的大小时,是不考虑成员函数的大小的,只考虑成员变量的大小。思考一下,这样设计的用意。
  当我们用类创建很多不同的对象时,对于对象而言,类中的什么成员是必须的,而什么成员不是硬需的呢?
  对于每个对象而言,拥有各自的成员变量是必须的,比如日期类Date(成员变量为年、月、日),对象之间拥有自己特定的记录的年、月、日,才有意义,如果所有对象都是统一固定的,那将没有意义了,因为不管创建多少对象,表示的都是同一个信息。
  而成员函数是实现一些功能,比如日期的加减等等,而这些功能函数对于所有的对象来说,都是通用的,因此如果每个对象都存入这些通用的函数,是不是对于内存很不友善,于是在类中的成员函数的内存是不计入对象中的内存中的,成员函数是被存到代码区,供所有类的对象使用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值