当前位置: 首页 > news >正文

C++类和对象(中—1) —— 构造函数、析构函数、拷贝构造函数

目录

  • 1.类的6个默认函数
  • 2.构造函数
    • 2.1构造函数的概念与基本使用
    • 2.2编译器自动生成的默认构造函数
    • 2.3默认构造函数
  • 3.析构函数
    • 3.1析构函数的概念与基本使用
    • 3.2编译器自动生成的默认析构函数
    • 3.3析构函数的使用环境
  • 4.拷贝构造函数
    • 4.1拷贝构造函数的概念
    • 4.2拷贝构造函数的基本使用
    • 4.3浅拷贝
    • 4.4深拷贝

1.类的6个默认函数

在一个空类当中,是真的没有任何成员吗?并不是,空类当中会生成6个默认函数。
在这里插入图片描述
默认成员函数不会显示地展示给我们看,而是编译器自动生成的隐式的成员函数。

本片文章将会介绍4个内容,分别为:构造函数、析构函数、拷贝构造函数以及运算符重载。这四个内容将是学习C++碰到的第一块难啃的骨头。

2.构造函数

2.1构造函数的概念与基本使用

构造函数没有构造什么东西,它只是帮我们完成初始化的工作。同样是初始化,我们为什么不能自己写一个初始化呢?正是因为这样,所以才要有构造函数,构造函数是编译器自动调用的函数,不需要我们去手动调用。也就是说,在我们定义对象的时候,就完成了初始化的工作。

class A
{
public:
	void Init()
	{
		_a = 0;
		_b = 0;
	}
private:
	int _a;
	int _b;
};
int main()
{
	A a1;
	a1.Init();//我们需要手动初始化
	return 0;
}
class A
{
public:
	A()//构造函数
	{
		_a = 0;
		_b = 0;
	}
private:
	int _a;
	int _b;
};
int main()
{
	A a1;//自动调用构造函数完成初始化
	return 0;
}

构造函数的函数名与类名同名,并且没有返回值(返回类型不是void,而是函数名前没有任何返回值)。在我们定义对象的时候,编译器自动调用构造函数完成初始化工作。

并且构造函数支持重载

class A
{
public:
	A()//无参构造函数
	{
		_a = 0;
		_b = 0;
	}
	A(int a, int b)//有参构造函数
	{
		_a = a;
		_b = b;
	}
	void Show()
	{
		cout << _a << " " << _b << endl;
	}
private:
	int _a;
	int _b;
};
int main()
{
	A a1(1, 2);
	a1.Show();
	A a2;//构造函数没有参数时的正确写法
	a2.Show();

	//A a3();//这种写法是错误的,因为会与函数声明混淆
	return 0;

![在这里插入图片描述](https://img-blog.csdnimg.cn/8973f595c39d421f91fb497542fab60e.pn

总结:
1.构造函数编译器自动调用的完成初始化工作的函数
2.构造函数的函数名与类名相同,且不具有返回值
3构造函数支持重载

2.2编译器自动生成的默认构造函数

当我们不手动定义构造函数的时候,编译器会自动生成一个无参的默认构造函数。如果我们手动定义构造函数,那么编译器将不会自动生成默认构造函数。编译器生成的构造函数会起到什么效果呢?

class A
{
public:
		//没有手动定义构造函数
	void Show()
	{
		cout << _a << " " << _b << endl;
	}
private:
	int _a;
	int _b;
};
int main()
{
	A a1;
	a1.Show();
	return 0;
}

在这里插入图片描述
可以看到自动生成的默认构造函数没有起任何作用?事实上真是这样吗?

class B
{
public:
	B()
	{
		_x = 1;
		_y = 1;
		cout << _x << " " << _y << endl;
	}
private:
	int _x;
	int _y;
};
class A
{
public:
		//没有手动定义构造函数
	void Show()
	{
		cout << _a << " " << _b << endl;
	}
private:
	int _a;
	int _b;
	B b1;
};
int main()
{
	A a1;
	a1.Show();
	return 0;
}

在这里插入图片描述
可以看到,我们似乎调用了B类的构造函数,然后再调用Show函数。这说明了编译器自动生成的默认构造函数不对内置类型处理,而是调用自定义类型的构造函数

在以前版本的C++语言中,如果我们不手动定义构造函数的话,那么编译器生成的默认构造函数看起来就似乎就没有意义了。而C++标准委员会也认识到了这一点,所以在此基础上打了一个补丁,即:内置类型的成员变量可以给定缺省值

class SeqList
{
public:
	//不给定构造函数
private:
	int* a = (int*)malloc(sizeof(int));//并没有实质的开辟空间
	//将类实例化后才会开辟空间
	int size = 20;
};

int main()
{
	SeqList sl;//在定义对象时,对象的成员变量将会使用缺省值
	return 0;
}

总结:
1.当我们没有手动定义构造函数时,编译器会自动生成一个默认构造函数
2.编译器生成的默认构造函数不对内置类型进行处理,编译器会调用自定义类型的构造函数
3.C++11中,可以为类中的成员变量添加缺省值,目的是为了弥补编译器生成的默认构造函数的不足

2.3默认构造函数

并不是编译器自动生成的默认构造函数才称为默认构造函数。无参的、全缺省的构造函数都可以称为默认构造函数。与缺省参数和函数重载一样,我们不要定义具有矛盾的构造函数。

class Date
{
public:
	Date()
	{
		_year = 2000;
		_month = 1;
		_day = 1;
	}
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	return 0;
}

3.析构函数

3.1析构函数的概念与基本使用

与构造函数相反,析构函数的作用是对空间的释放。它也是编译器自动调用的。不过我们需要注意,我们并不是销毁对象本身,而是去释放向操作系统申请的空间。例如我们使用malloc函数申请得到一块空间,那么在程序结束之前,需要使用free函数释放掉这一块空间,否则有可能会造成内存泄漏。

class Stack
{
public:
	~Stack()//析构函数的写法
	{
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
		cout << "析构函数" << endl;
	}
private:
	int* _a=(int*)malloc(sizeof(int)*_capacity);
	int _top=0;
	int _capacity=4;
};
int main()
{
	Stack s1;
	return 0;
}//当s1对象的栈帧销毁时,编译器自动调用析构函数

在这里插入图片描述
由此可见,析构函数的函数名与类名相同,不过需要在函数名之前加上~,并且没有返回值;析构函数在对象的所处的函数栈帧销毁时自动调用;析构函数不支持重载,每一个类有且只有一个析构函数

3.2编译器自动生成的默认析构函数

与构造函数一样,当我们没有显示定义析构函数时,编译器会自动生成一个默认析构函数。同样的,默认析构函数不会对内置类型进行处理,而是调用自定义类型中的析构函数

class Stack
{
public:
	~Stack()//析构函数的写法
	{
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
		cout << "析构函数" << endl;
	}
private:
	int* _a=(int*)malloc(sizeof(int)*_capacity);
	int _top=0;
	int _capacity=4;
};

class A
{
public:
	//没有显示定义析构函数
private:
	int _a=0;
	int _b=0;
	Stack s1;
};
int main()
{
	Stack A;
	return 0;
}//栈帧销毁时,调用A类的自定义类型的析构函数

3.3析构函数的使用环境

当类中没有向系统申请空间时,我们不需要定义任何析构函数,因为函数栈帧销毁时,会连同在栈帧中的局部变量一起销毁。就比如日期类。

当类中存在向系统申请的空间时,我们必须定义析构函数。因为栈帧的销毁不影响堆中的空间。例如栈类、队列类。

4.拷贝构造函数

4.1拷贝构造函数的概念

假如我们有两个整型变量,我们要使这两个变量的值相等,我们赋值运算符即可。

	int a = 3;
	int b = a;//使用赋值运算符将a的值赋给b

那么对象与对象之间能否进行赋值呢?可以,不过我们需要拷贝构造函数,我们可以自定义对象与对象之间的哪些成员赋值。

拷贝构造函数,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

4.2拷贝构造函数的基本使用

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d)//拷贝构造函数
	{
		//自定义对象与对象之间哪些成员赋值
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022,9,24);
	Date d2(d1);//拷贝构造函数的使用
	return 0;
}

可以看到,拷贝构造函数构造函数的重载,它的参数是对象的引用。为什么一定要对象的引用?一方面是节省形参和指针所开辟的空间,第二个原因在于,如果形参不使用引用或指针,那么形参将会是实参的一份临时拷贝。那么形参的值将从实参拷贝过来,既然是拷贝,那么就会调用拷贝构造函数,从而造成死递归。

在这里插入图片描述
我们可以观察一下d2对象的成员是否与d1对象的成员一致:
在这里插入图片描述

4.3浅拷贝

当我们只需要对值进行拷贝的时候,我们就可以显示定义拷贝构造函数的浅拷贝。当我们没有显示定义拷贝构造函数时,编译器会自动生成一个默认拷贝构造函数,这个默认拷贝构造函数是进行浅拷贝的函数

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//当我们没有显示定义拷贝构造函数时
	//编译器会生成一个进行浅拷贝的默认拷贝构造函数
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022,9,24);
	Date d2(d1);//拷贝构造函数的使用
	return 0;
}

在这里插入图片描述
如果我们的类是一个栈类,那么浅拷贝不足以帮助我们完成任务:

class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_a = (int*)malloc(capacity * sizeof(int));
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_top = 0;
		_capacity = capacity;
	}
	void Push(int x)
	{
		_a[_top++] = x;
	}
	Stack(Stack& s)//如果我们使用浅拷贝
	{
		_a = s._a;
		_top = _top;
		_capacity = s._capacity;
	}
private:
	int* _a ;
	int _top;
	int _capacity;
};

int main()
{
	Stack s1;
	Stack s2(s1);
	return 0;
}

在这里插入图片描述
可以看到,如果使用浅拷贝,s2中的_a并没有指向s2独立开辟的空间,而是指向s1中开辟的空间。即浅拷贝只拷贝了地址。那么这就会造成s1中的对象改变,s2指向的对象也会改变。这并不是我们想要看到的结果。
在这里插入图片描述
并且,如果我们定义了析构函数,栈是后进先出。所以函数结束时,会先释放s2,然后再释放s1。又因为s2和s1的_a指向的是同一块空间,而s2已经对这块空间释放过了,那么s1再释放,就会发生错误。

4.4深拷贝

同样以栈类作为案例:

class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_a = (int*)malloc(capacity * sizeof(int));
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_top = 0;
		_capacity = capacity;
	}
	void Push(int x)
	{
		_a[_top++] = x;
	}
	Stack(const Stack& s)//我们不希望改变实参,使用const修饰
	{
		_a = (int*)malloc(sizeof(int) * s._top);
		assert(_a);
		memcpy(_a, s._a, sizeof(int) * s._capacity);
		_top = s._top;
		_capacity = s._capacity;
	}
private:
	int* _a ;
	int _top;
	int _capacity;
};

我们使用深拷贝便能将每个对象的空间独立开来。
在这里插入图片描述

总结:
1.当我们的类没有涉及申请空间的时候,可以不写拷贝构造函数或者定义浅拷贝的拷贝构造函数
2.如果类的设计涉及到了申请空间,那么拷贝构造函数一定要写,并且执行的操作是深拷贝

相关文章:

  • SsmAjaxJson分页效果的操作(第十七课)
  • sklearn机器学习——day19
  • GrapeCity Documents for PDF (GcPDF)
  • el与data的两种写法
  • 超常用的网络工具命令汇总
  • java-php-python-springboo动物在线领养网站计算机毕业设计
  • JavaScript try-catch 处理错误和异常指南
  • Python文件的读写及常用文件的打开方式
  • MyBatis 中 #{} 和 ${} 的区别看完这篇文章一目了然
  • 实时即未来,车联网项目之原始终端数据实时ETL【二】
  • python 的re.findall的Bug以及解决方法
  • 在Windows系统上部署DHCP服务器
  • Java多线程~CAS的原理及其应用
  • [CSS]盒子模型
  • 【 C++ 】开散列哈希桶的模拟实现
  • 【399天】跃迁之路——程序员高效学习方法论探索系列(实验阶段156-2018.03.11)...
  • 4月23日世界读书日 网络营销论坛推荐《正在爆发的营销革命》
  • express + mock 让前后台并行开发
  • If…else
  • LeetCode18.四数之和 JavaScript
  • Linux学习笔记6-使用fdisk进行磁盘管理
  • Python 使用 Tornado 框架实现 WebHook 自动部署 Git 项目
  • REST架构的思考
  • Webpack入门之遇到的那些坑,系列示例Demo
  • web标准化(下)
  • win10下安装mysql5.7
  • 阿里云Kubernetes容器服务上体验Knative
  • 仿天猫超市收藏抛物线动画工具库
  • 构建工具 - 收藏集 - 掘金
  • 关于Flux,Vuex,Redux的思考
  • 记一次用 NodeJs 实现模拟登录的思路
  • 浅谈Kotlin实战篇之自定义View图片圆角简单应用(一)
  • 设计模式(12)迭代器模式(讲解+应用)
  • 小程序01:wepy框架整合iview webapp UI
  • 用jquery写贪吃蛇
  • Mac 上flink的安装与启动
  • postgresql行列转换函数
  • 数据库巡检项
  • ​520就是要宠粉,你的心头书我买单
  • #define、const、typedef的差别
  • #我与虚拟机的故事#连载20:周志明虚拟机第 3 版:到底值不值得买?
  • #中国IT界的第一本漂流日记 传递IT正能量# 【分享得“IT漂友”勋章】
  • $jQuery 重写Alert样式方法
  • %3cscript放入php,跟bWAPP学WEB安全(PHP代码)--XSS跨站脚本攻击
  • (2)关于RabbitMq 的 Topic Exchange 主题交换机
  • (30)数组元素和与数字和的绝对差
  • (三)c52学习之旅-点亮LED灯
  • (十六)串口UART
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (循环依赖问题)学习spring的第九天
  • (原)本想说脏话,奈何已放下
  • .“空心村”成因分析及解决对策122344
  • .mat 文件的加载与创建 矩阵变图像? ∈ Matlab 使用笔记
  • .Net Core 中间件验签
  • .Net Core和.Net Standard直观理解