IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++类和对象(中—1) —— 构造函数、析构函数、拷贝构造函数 -> 正文阅读

[C++知识库]C++类和对象(中—1) —— 构造函数、析构函数、拷贝构造函数

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.如果类的设计涉及到了申请空间,那么拷贝构造函数一定要写,并且执行的操作是深拷贝

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-25 23:05:06  更:2022-09-25 23:06:29 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 10:02:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码