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

2025-06-04 03:50

本文主要是介绍C++/类与对象/默认成员函数@构造函数的用法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《C++/类与对象/默认成员函数@构造函数的用法》:本文主要介绍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的所www.chinasem.cn有成员变量(也就是日期),运行程序,结果如下

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

验证结论是,对象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;
		_mhttp://www.chinasem.cnonth = month;
		_day = day;
	}

这时,我们创建对象2

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

时,编译器便会找到成员函数2,完成对象2的成员变量的初始化。(clue:成员函数2不是默认构造函数)

运行结果如下:

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

到此,我们实现了构造函数的全部功能, 

  • 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()" &China编程lt;< 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,否则将导致编译器报错,因为编译器不知道该调用哪一个构造函数。

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

隐式构造函数

通过前面的介绍,我们能够得知,隐式构造函数就是,当用户自己没有编写定义构造函数时,编译器自动生js成的默认构造函数。

下面我们验证一下编译器自动生成的默认构造函数,在背后干了个啥。

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();
}

运行程序,结果如下:

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

运行结果表明,编译器自动生成的,隐式构造函数做了以下的事情:

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(成员变量为年、月、日),对象之间拥有自己特定的记录的年、月、日,才有意义,如果所有对象都是统一固定的,那将没有意义了,因为不管创编程建多少对象,表示的都是同一个信息。  

而成员函数是实现一些功能,比如日期的加减等等,而这些功能函数对于所有的对象来说,都是通用的,因此如果每个对象都存入这些通用的函数,是不是对于内存很不友善,于是在类中的成员函数的内存是不计入对象中的内存中的,成员函数是被存到代码区,供所有类的对象使用。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持China编程(www.chinasem.cn)。

这篇关于C++/类与对象/默认成员函数@构造函数的用法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154896

相关文章

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

JDK21对虚拟线程的几种用法实践指南

《JDK21对虚拟线程的几种用法实践指南》虚拟线程是Java中的一种轻量级线程,由JVM管理,特别适合于I/O密集型任务,:本文主要介绍JDK21对虚拟线程的几种用法,文中通过代码介绍的非常详细,... 目录一、参考官方文档二、什么是虚拟线程三、几种用法1、Thread.ofVirtual().start(

Python函数作用域与闭包举例深度解析

《Python函数作用域与闭包举例深度解析》Python函数的作用域规则和闭包是编程中的关键概念,它们决定了变量的访问和生命周期,:本文主要介绍Python函数作用域与闭包的相关资料,文中通过代码... 目录1. 基础作用域访问示例1:访问全局变量示例2:访问外层函数变量2. 闭包基础示例3:简单闭包示例4

JavaScript对象转数组的三种方法实现

《JavaScript对象转数组的三种方法实现》本文介绍了在JavaScript中将对象转换为数组的三种实用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友... 目录方法1:使用Object.keys()和Array.map()方法2:使用Object.entr

Java8 Collectors.toMap() 的两种用法

《Java8Collectors.toMap()的两种用法》Collectors.toMap():JDK8中提供,用于将Stream流转换为Map,本文给大家介绍Java8Collector... 目录一、简单介绍用法1:根据某一属性,对对象的实例或属性做映射用法2:根据某一属性,对对象集合进行去重二、Du

Python中isinstance()函数原理解释及详细用法示例

《Python中isinstance()函数原理解释及详细用法示例》isinstance()是Python内置的一个非常有用的函数,用于检查一个对象是否属于指定的类型或类型元组中的某一个类型,它是Py... 目录python中isinstance()函数原理解释及详细用法指南一、isinstance()函数

python中的高阶函数示例详解

《python中的高阶函数示例详解》在Python中,高阶函数是指接受函数作为参数或返回函数作为结果的函数,下面:本文主要介绍python中高阶函数的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录1.定义2.map函数3.filter函数4.reduce函数5.sorted函数6.自定义高阶函数

Python中的sort方法、sorted函数与lambda表达式及用法详解

《Python中的sort方法、sorted函数与lambda表达式及用法详解》文章对比了Python中list.sort()与sorted()函数的区别,指出sort()原地排序返回None,sor... 目录1. sort()方法1.1 sort()方法1.2 基本语法和参数A. reverse参数B.

vue监听属性watch的用法及使用场景详解

《vue监听属性watch的用法及使用场景详解》watch是vue中常用的监听器,它主要用于侦听数据的变化,在数据发生变化的时候执行一些操作,:本文主要介绍vue监听属性watch的用法及使用场景... 目录1. 监听属性 watch2. 常规用法3. 监听对象和route变化4. 使用场景附Watch 的