【C++】类的默认成员函数(上)

在这里插入图片描述
🔥博客主页 小羊失眠啦.
🎥系列专栏《C语言》 《数据结构》 《C++》 《Linux》 《Cpolar》
❤️感谢大家点赞👍收藏⭐评论✍️


在这里插入图片描述

文章目录

  • 一、默认成员函数
  • 二、构造函数
    • 构造函数的概念及特性
  • 三、析构函数
    • 析构函数的特性
  • 四、拷贝构造函数
    • 拷贝构造函数的特性

一、默认成员函数

上一章中我们谈到,如果一个类中什么成员也没有,那么这个类就叫作空类。其实这么说是不太严谨的,因为一个类不可能什么都没有

当我们定义好一个类,不做任何处理时,编译器会自动生成以下6个默认成员函数

  • 默认成员函数:如果用户没有手动实现,则编译器会自动生成的成员函数。

在这里插入图片描述

  • 构造函数:主要完成初始化工作;
  • 析构函数:主要完成清理工作;
  • 拷贝构造:使用一个同类的对象初始化创建一个对象;
  • 赋值重载:把一个对象赋值给另一个对象;
  • 取地址重载普通对象取地址操作;
  • 取地址重载(const):const对象取地址操作;

本章我们将学习四个默认成员函数——构造函数析构函数——拷贝构造赋值重载


二、构造函数

在C语言阶段,我们实现的数据结构时,有一件事很苦恼,就是每当创建一个stack对象(之前叫作定义一个stack类型的变量)后,首先得调用它的专属初始化函数StackInit来初始化对象。

typedef int dataOfStackType;

typedef struct stack
{
	dataOfStackType* a;
	int top;
	int capacity;
}stack;

void StackInit(stack* ps);
//...

 int main()
 {
	 stack s;
	 StackInit(&s);
	 //...
	 return 0;
 }

这不免让人觉得有点麻烦。在C++中,构造函数为我们很好的解决了这一问题。

构造函数的概念及特性

构造函数是一个特殊的成员函数。构造函数虽然叫作构造,但是其主要作用并不是开辟空间创建对象,而是初始化对象

构造函数之所以特殊,是因为相比于其它成员函数,它具有如下特性

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时,编译器自动调用对应的构造函数
  4. 构造函数可以重载

举例

class Date
{
public:
	//无参的构造函数
	Date()
	{};
	//带参的构造函数
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	int _year;
	int _month;
	int _day;
};

void TestDate()
{
	Date d1;//调用无参构造函数(自动调用)
	Date d2(2023, 3, 29);//调用带参构造函数(自动调用)
}

特别注意

  • 创建对象时编译器会自动调用构造函数,若是调用无参构造函数,则无需在对象后面使用()。否则会产生歧义:编译器无法确定你是在声明函数还是在创建对象

错误示例

//错位示例
Date d3();
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
class Date
{
public:
	//若用户没有显示定义,则编译器自动生成。
	/*Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/

private:
	int _year;
	int _month;
	int _day;
};
  1. 默认生成构造函数,对内置类型成员不作处理;对自定义类型成员,会调用它的默认构造函数
  • C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int、char、double…,自定义类型就是我们使用class、struct、union等自己定义的类型。

举例

默认构造函数对内置类型

class Date
{
public:
	//此处不对构造函数做显示定义,测试默认构造函数
	/*Date()
	{}*/

	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
void TestDate1()
{
	Date d1;
	d1.print();
}

在这里插入图片描述

  • 如图所示,默认构造函数的确未对内置类型做处理。

默认构造函数对自定义类型

class stack
{
public:
	//此处对stack构造函数做显示定义
	stack()
	{
		cout <<"stack()" << endl;
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class queue
{
public:
	//此处不对queue构造函数做显示定义,测试默认构造函数
	/*queue()
	{}*/
private:
	//自定义类型成员
	stack _s;
};

void TestQueue()
{
	queue q;
}

在这里插入图片描述

  • 如图所示,在创建queue对象时,默认构造函数对自定义成员_s做了处理,调用了它的默认构造函数stack()

这一波蜜汁操作让很多C++使用者感到困惑与不满,为什么要针对内置类型和自定义类型做不同的处理呢?终于,在C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:

  1. 内置类型成员变量在类中声明时可以给默认值

举例

class Date
{
public:
//...
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	//使用默认值
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
void TestDate2()
{
	Date d2;
	d2.print();
}

在这里插入图片描述

  • 默认值:若不对成员变量做处理,则使用默认值。
  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

举例

class Date
{
public:
	//无参的默认构造函数
	//Date()
	//{

	//}

	//全缺省的默认构造函数
	Date(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

默认构造函数:

  1. 无参的构造函数
  2. 全缺省的构造函数
  3. C++编译器生成的无参的构造函数

即三种必须要有一种,如果没有默认的构造函数(写的构造函数不是无参的,也不是全缺省的)就会报错


三、析构函数

析构函数构造函数的特性相似,但功能有恰好相反。构造函数是用来初始化对象的,析构函数是用来销毁对象的。

  • 需要注意的是,析构函数并不是对对象本身进行销毁(因为局部对象出了作用域会自行销毁,由编译器来完成),而是在对象销毁时会自动调用析构函数,对对象内部的资源做清理(例如stack _s中的int* a)。

同样,有了析构函数,我们再也不用担心创建对象(或定义变量)后由于忘记释放内存而造成内存泄漏了。

举例

class Stack
{
public:
	Stack()
	{
		//...
	}
	void Push(int x)
	{
		//...

	}
	bool Empty()
	{
		// ...

	}
	int Top()
	{
		//...
	}
	void Destory()
	{
		//...
	}
private:
	// 成员变量
	int* _a;
	int _top;
	int _capacity;
};

void TestStack()
{
	Stack s;
	st.Push(1);
	st.Push(2);
	//过去需要手动释放
	st.Destroy();
}

析构函数的特性

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数
  3. 无返回值
  4. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
  5. 析构函数不能重载

举例

class Date
{
public:
	Date()
	{
		cout << "Date()" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void TestDate3()
{
	Date d3;
	//d3生命周期结束时自动调用构造函数
}

在这里插入图片描述

  1. 编译器生成的默认析构函数,对自定类型成员调用它的析构函数

举例

class stack
{
public:
	//此处对stack构造函数做显示定义
	stack()
	{
		cout <<"stack()" << endl;
		_a = nullptr;
		_top = _capacity = 0;
	}
	~stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class queue
{
public:
	//此处不对queue构造函数做显示定义,测试默认构造函数
	/*queue()
	{}*/
private:
	//自定义类型成员
	stack _s;
};

void TestQueue1()
{
	queue q;
}

在这里插入图片描述

  • 这里可能有小伙伴会好奇:为什么析构函数不像构造函数那样区分内置类型与自定义类型呢
    答案是:因为内置类型压根不需要我们担心清理工作,在其生命周期结束时会自动销毁。而自定义类型需要担心,因为自定义类型里可能含有申请资源(例如:malloc申请内存须手动释放)。
  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如stack类。

四、拷贝构造函数

同样,拷贝构造函数也属于6个默认成员函数,而且拷贝构造函数构造函数的一种重载形式

  • 拷贝构造函数的功能就如同它的名字——拷贝。我们可以用一个已存在的对象来创建一个与已存在对象一模一样的新的对象

举例

class Date
{
public:
	//构造函数
	Date()
	{
		cout << "Date()" << endl;
	}
	//拷贝构造函数
	Date(const Date& d)
	{
		cout << "Date()" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	//析构函数
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void TestDate()
{
	Date d1;
	//调用拷贝构造创建对象
	Date d2(d1);
}

在这里插入图片描述

拷贝构造函数的特性

拷贝构造函数作为特殊的成员函数同样也有异于常人的特性:

  1. 拷贝构造函数是构造函数的重载
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用。若使用传值的方式,则编译器会报错,因为理论上这会引发无穷递归

错误示例

class Date
{
public:
	//错误示例
	//如果这样写,编译器就会直接报错,但我们现在假设如果编译器不会检查,
	//这样的程序执行起来会发生什么
	Date(const Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void TestDate()
{
	Date d1;
	//调用拷贝构造创建对象
	Date d2(d1);
}
  • 当拷贝构造函数的参数采用传值的方式时,创建对象d2,会调用它的拷贝构造函数d1会作为实参传递给形参d。不巧的是,实参传递给形参本身又是一个拷贝,会再次调用形参的拷贝构造函数…如此便会引发无穷的递归。

在这里插入图片描述

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝或者值拷贝

举例

class Date
{
public:
	//构造函数
	Date(int year = 0, int month = 0, int day = 0)
	{
		//cout << "Date()" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	//未显式定义拷贝构造函数
	/*Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}*/
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void TestDate()
{
	Date d1(2023, 3, 31);
	//调用拷贝构造创建对象
	Date d2(d1);
	d2.print();
}

在这里插入图片描述

  • 有的小伙伴可能会有疑问:编译器默认生成的拷贝构造函数貌似可以很好的完成任务,那么还需要我们手动来实现吗?
    答案是:当然需要。Date类只是一个较为简单的类且类成员都是内置类型,可以不需要。但是当类中含有自定义类型时,编译器可就办不了事儿了。
  1. 类中如果没有涉及资源申请时,拷贝构造函数写不写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝

错误示例

class stack
{
public:
	stack(int defaultCapacity=10)
	{
		_a = (int*)malloc(sizeof(int)*defaultCapacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top =  0;
		_capacity = defaultCapacity;
	}
	~stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
	void push(int n)
	{
		_a[_top++] = n;
	}
	void print()
	{
		for (int i = 0; i < _top; i++)
		{
			cout << _a[i] << " ";
		}
		cout << endl;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

void TestStack()
{
	stack s1;
	s1.push(1);
	s1.push(2);
	s1.push(3);
	s1.push(4);
	s1.print();

	stack s2(s1);
	s2.print();
	s2.push(5);
	s2.push(6);
	s2.push(7);
	s2.push(8);
	s2.print();
}

在这里插入图片描述

如图所示,这段程序的运行结果是程序崩溃了,且通过观察发现,是在第二次析构时出现了错误。其实出现错误的原因是在第二次析构时对野指针进行free了。

一个小tip

  • 多个对象进行析构的顺序如同一样,先创建的对象后析构,后创建的对象先析构

为什么会出现对野指针进行free呢?

  • 原因是,对象s1与对象s2中的成员_a,指向的是同一块空间。在s2析构完成后,这块空间已经被释放,此时的s1._a就是野指针。这就是浅拷贝导致的后果。

理解浅拷贝

编译器默认生成的拷贝构造函数是按字节序拷贝的,在创建s2对象时,仅仅是把s1._a的值赋值给s2._a并没有重新开辟一块与s1._a所指向的空间大小相同内容相同的空间。我们把前者的拷贝方式称为浅拷贝后者称为深拷贝

在这里插入图片描述

当开启监视窗口来观察这一过程,我们可以看到s2在进行push时,s1的内容也在跟着改变,且s1._a=s2._a

在这里插入图片描述

正确的做法

class stack
{
public:
	stack(int defaultCapacity=10)
	{
		_a = (int*)malloc(sizeof(int)*defaultCapacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top =  0;
		_capacity = defaultCapacity;
	}
	//用户自己定义拷贝构造函数
	stack(const stack& s)
	{
		_a= (int*)malloc(sizeof(int) * s._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}

		memcpy(_a, s._a, sizeof(int) * s._capacity);
		_top = s._top;
		_capacity = s._capacity;
	}
	~stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
	void push(int n)
	{
		_a[_top++] = n;
	}
	void print()
	{
		for (int i = 0; i < _top; i++)
		{
			cout << _a[i] << " ";
		}
		cout << endl;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
  1. 拷贝构造函数典型调用场景
  • 使用已存在对象创建新对象;
  • 函数参数类型为类类型对象;
  • 函数返回值类型为类类型对象。

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/425386.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

#QT(DEMO2-登录界面)

1.IDE&#xff1a;QTCreator 2.实验&#xff1a;DEMO登录 3.记录 Line Edit输入不换行 密码框输入如下设置: 运行效果 4.代码

使用Matplotlib绘制圆环图

圆环图是饼图的修改版&#xff0c;中间区域被切掉。圆环图更关注使用弧的面积来以最有效的方式表示信息&#xff0c;而不是饼图&#xff0c;饼图更关注比较切片之间的比例面积。圆环图在空间方面更有效&#xff0c;因为圆环图内部的空白空间可用于显示有关圆环图的一些附加信息…

【C++那些事儿】深入理解C++类与对象:从概念到实践(中)| 默认构造函数 | 拷贝构造函数 | 析构函数 | 运算符重载 | const成员函数

&#x1f4f7; 江池俊&#xff1a; 个人主页 &#x1f525;个人专栏&#xff1a; ✅数据结构冒险记 ✅C那些事儿 &#x1f305; 有航道的人&#xff0c;再渺小也不会迷途。 文章目录 1. 类的6个默认成员函数2. 构造函数2.1 概念2.2 特性 3. 析构函数3.1 概念3.2 特性 4. 拷贝…

flutter 文字一行显示,超出换行

因为app有多语言&#xff0c;中文和其他语言长度不一致&#xff0c;可能导致英文会很长。 中文样式 英文样式 代码 Row(mainAxisAlignment: MainAxisAlignment.end,crossAxisAlignment: CrossAxisAlignment.end,children: [Visibility(visible: controller.info.fee ! null,ch…

产品经理岗位的任职资格和职业规划

产品经理主要是商业银行以客户为导向的&#xff0c;具体负责组织银行某一金融产品线的创新设计、生产营销和管理服务的工作。这类人士主要负责应用实施工作&#xff0c;其中产品线由一系列的产品构成&#xff0c;公司的产品经理主要分为全过程产品创新设计专家、全过程产品生产…

驾驭栈和队列,首先逃不开这些(有效括号、栈和队列相互模拟、循环队列)

一.有效括号 . - 力扣&#xff08;LeetCode&#xff09; 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须…

力扣74. 搜索二维矩阵(二分查找)

Problem: 74. 搜索二维矩阵 文章目录 题目描述思路复杂度Code 题目描述 思路 思路1&#xff1a;映射为一维数组二分查找 1.由于题目矩阵中的元素整体是升序的&#xff0c;我们可以将其放置在一个大小为 m n m \times n mn的一维数组array中进行二分查找 2.对应的映射关系是ar…

微信小程序,h5端自适应登陆方式

微信小程序端只显示登陆(获取opid),h5端显示通过账户密码登陆 例如: 通过下面的变量控制: const isWeixin ref(false); // #ifdef MP-WEIXIN isWeixin.value true; // #endif

(C语言)qsort函数模拟实现

前言 我们需先了解qsort函数 qsort函数详解&#xff1a;http://t.csdnimg.cn/rTNv9 qsort函数可以排序多种数据类型&#xff0c;很是神奇&#xff0c;这是为什么&#xff0c;我们在里模拟实现这样的功能 目录 1. qsort函数模拟实现 2. 我们使用bubble_sort函数排序整形数…

ruoyi 图片等文件资源读取

老是忘&#xff0c;记录一下 ResourcesConfig 文件下 /** 本地文件上传路径 */ registry.addResourceHandler(Constants.RESOURCE_PREFIX "/**").addResourceLocations("file:" RuoYiConfig.getProfile() "/"); /*** 资源映射路径 前缀*/ …

【Web安全靶场】xss-labs-master 1-20

xss-labs-master 其他靶场见专栏 文章目录 xss-labs-masterlevel-1level-2level-3level-4level-5level-6level-7level-8level-9level-10level-11level-12level-13level-14level-15level-16level-17level-18level-19level-20 level-1 第一关没有进行任何限制&#xff0c;get请求…

python复盘(1)

1、变量名的命名规则 #3、变量名的命名规则&#xff1a;可以用中文作为变量名&#xff1b;其他和go语言一样 # 变量名可以用数字、字母、下划线组成&#xff0c;但是数字不能作为开头 # 变量名不能使用空格&#xff0c;不能使用函数名或关键字 # 变量名最好能表示出他的作用2、…

Qt/事件分发器/事件过滤器

事件分发器 //事件分发器bool event(QEvent* e); //事件分发器&#xff1a;&#xff1a;用途 分发事件 bool MyLabel::event(QEvent* e) {if(e->type() QEvent::MouseButtonPress){//如果是鼠标摁下 拦截事件 不向下分发QMouseEvent* ev static_cast<QMouseEvent*>…

python类型注解,多态详解

目录 1.类型注解 1.变量的类型注解 2.函数&#xff08;方法&#xff09;的类型注解 3.union类型 2.多态 抽象类 1.类型注解 1.变量的类型注解 #基础数据类型注解 import randomv1 : int 10 v2 : str "aaa" v3 : bool True#类对象类型注解 class student:p…

基于springboot+vue的公寓报修管理系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

奇舞周刊第521期:“一切非 Rust 项目均为非法”

奇舞推荐 ■ ■ ■ 拜登&#xff1a;“一切非 Rust 项目均为非法” 科技巨头要为Coding安全负责。这并不是拜登政府对内存安全语言的首次提倡。“程序员编写代码并非没有后果&#xff0c;他们的⼯作⽅式于国家利益而言至关重要。”白宫国家网络总监办公室&#xff08;ONCD&…

6020一拖二快充线:手机充电的革命性创新

在快节奏的现代生活中&#xff0c;手机已不仅仅是一个通讯工具&#xff0c;更是我们工作、学习和娱乐的得力助手。然而&#xff0c;手机的电量问题一直是困扰着我们的难题。为了解决这个问题&#xff0c;市场上出现了一种名为“一拖二快充线”的充电设备&#xff0c;它不仅具备…

用JavaScript动态提取视频中的文字

现阶段整个社会短视频&#xff0c;中视频为王&#xff0c;文字传播虽然被弱化&#xff0c;但在业务中还是有一定的传播价值&#xff0c;今天就来讲一讲如何使用js动态提取视频中的字幕。 先来看看效果&#xff1a; 屏幕录制2024-02-29 15.40.18 一&#xff0c;tesseract.js介…

SpringMVC 学习(十一)之数据校验

目录 1 数据校验介绍 2 普通校验 3 分组校验 4 参考文档 1 数据校验介绍 在实际的项目中&#xff0c;一般会有两种校验数据的方式&#xff1a;客户端校验和服务端校验 客户端校验&#xff1a;这种校验一般是在前端页面使用 JS 代码进行校验&#xff0c;主要是验证输入数据…

Zookeeper3:客户端命令

文章目录 客户端命令连接服务端Zookeeper客户端内置命令 ls - 节点信息 客户端命令 连接服务端Zookeeper //客户端连接服务端zookeeper 默认连的本机2181端口的zookeeper cd /opt/module/zookeeper-3.9.1/bin && sh zkCli.sh//客户端连接远程服务端zookeeper cd /op…