C++入门之类和对象(中)

C++入门之类和对象(中)

文章目录

  • C++入门之类和对象(中)
    • [1. 类的6个默认对象](#1. 类的6个默认对象)
    • [2. 构造函数](#2. 构造函数)
      • [2.1 概念](#2.1 概念)
      • [2.2 特性](#2.2 特性)
      • [2.3 补丁](#2.3 补丁)
    • [3. 析构函数](#3. 析构函数)
      • [3.1 概念](#3.1 概念)
      • [3.2 特性](#3.2 特性)
      • [3.3 总结](#3.3 总结)
    • [4. 拷贝构造函数](#4. 拷贝构造函数)
      • [4.1 概念](#4.1 概念)
      • [4.2 特性](#4.2 特性)
      • [4.3 总结](#4.3 总结)

1. 类的6个默认对象

如果一个类中什么都没有,那么这个类就是一个空类。但是,任何类如果什么都不写的话,编译器会自动生成6个默认成员函数

默认成员函数:用户没有显式实现(用户没有写),编译器自动生成的成员函数被称为默认成员函数

cpp 复制代码
class Data{};

2. 构造函数

2.1 概念

假设有以下类:

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	void Init(int year = 2024, int month = 4, int day = 15)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};


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

	Date d2;
	d2.Init(2024, 5, 1);
	d2.Print();

	return 0;
}

上述类中是使用Init函数对类进行初始化,Init使用全缺省参数,如果没有传值的话,使用缺省值初始化,但是对于这这种类,就算不初始化也不会有什么问题,但是对于顺序表,链表等,如果不初始化就会报错,往往我们会容易忘记调用初始化,这时候,构造函数就派上用场了

构造函数是一种特殊的函数,名字与类名相同,没有返回值(在默认成员函数中,没有返回值指的都是不写),在创建类对象时由编译器自动调用,保证每个成员都有一个初始值,并且在类对象整个生命周期只会调用一次

2.2 特性

构造函数是一种特殊的函数,构造函数并不是用于开辟空间创建对象,而是为对象进行初始化

特征

  1. 函数名与类名相同
  2. 函数没有返回值(不写返回值)
  3. 对象实例化时编译器会自动调用
  4. 构造函数可以重载(可以根据需求写多个初始化方式)
  5. 如果类中没有显式定义构造函数(没有写),编译器就会自动生成一个无参的构造函数,反之,编译器则不会生成
  6. 由编译器生成的无参构造函数,不会对类中的内置类型(int char等等)进行处理,但是对调用类中自定类型(class struct union等等)的构造函数,如果类中自定类型还是没有则也不处理
    C++中没有规定对自定类型(class struct union等等)初始化成0或者其他,取决于编译器的实现
  7. 无参构造函数和全缺省的构造函数,由编译器自动生成构造函数都可以被称为默认参构造函数,但是默认参构造函数只能存在一个

示例1:

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	//Date(int year, int month, int day) (这种写法会报错,这种不是默认的构造函数)
	Date(int year = 2024, int month = 4, int day = 15)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1;//调用全缺省的构造函数(2024-4-15)
	d1.Print();

	return 0;
}

示例2:

cpp 复制代码
#include <iostream>
using namespace std;
class Time
{
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time a;
};


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

	return 0;
}

示例3:

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	Date()
	{
	}
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};


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

	return 0;
}

存在多个构造函数,报错

2.3 补丁

由于不对内置类型进行初始化,所以在C++ 11中,打了一个补丁,允许内置成员在声明时可以给一个默认值

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year = 2024;
	int _month = 4;
	int _day = 15;
};


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

	return 0;
}

用声明时的默认值初始化(2024-4-15)

总结:

一般情况下,构造函数都要由我们自己实现,少部分情况下可以不用实现(如果类中只有自定类型,而这个自定类型内部存在构造函数),例如:MyQueue

3. 析构函数

3.1 概念

析构函数是与构造函数相反的一种特殊函数,析构函数不是对对象进行销毁,局部变量的销毁是由编译器处理的,而是析构函数是对对象中资源的清理,且会在对象销毁时自动调用

3.2 特性

  1. 在类名前面加上~
  2. 无参数无返回值(不写返回值)
  3. 一个类只有一个析构函数,如果没有显式定义(没有写),则编译器会自动生成默认析构函数(由于没有参数,析构函数不能重载)
  4. 在对象生命周期结束时,编译器会自动调用析构函数
  5. 与构造函数相似的是,析构函数不会对内置类型进行处理,对自定类型则是调用其析构函数

示例:

cpp 复制代码
#include <iostream>
using namespace std;
class Stack
{
public:
	Stack(int n = 4)
	{
		cout << "Stack()" << endl;
		int* tmp = (int*)malloc(sizeof(int) * n);
		if (nullptr == tmp)
		{
			perror("malloc fail");
			return;
		}

		_arr = tmp;
		_capacity = n;
		_size = 0;
	}
	void Push(int x)
	{
		//扩容
		_arr[_size] = x;
		_size++;
	}
	~Stack()
	{
		cout << "~Stack()" << endl; //方便查看
		if (_arr) //防止被多次销毁,加个判断
		{
			free(_arr);
			_arr = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	int* _arr;
	int _size;
	int _capacity;
};
int main()
{
	Stack s;
	s.Push(1);
	s.Push(2);
	s.Push(3);
	s.~Stack();
	return 0;
}

析构函数也是可以显式调用的

3.3 总结

  1. 在没有需要资源清理的时候可以不写析构函数,
    a. 如Date类,没有需要清理的内置类型
    b.没有需要清理的内置类型,剩下的其他自定类型中存在析构函数,如MyQueue,也不需要写析构函数
    2.有资源清理就要写析构函数,如Stack,List

4. 拷贝构造函数

4.1 概念

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

4.2 特性

  1. 是构造函数的一种重载形式(函数名与类型一致)
  2. 拷贝构造函数的参数只能有一个,且得是类类型对象的引用,否则在使用拷贝构造函数会直接报错(引发无穷递归调用)
  3. 如果没有显式定义,编译器会自动生成默认的拷贝构造函数,默认的拷贝构造函数会对内置类型进行处理,按内存存储按字节序完成拷贝,也被称为浅拷贝或者值拷贝

示例1:

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Init(int year = 2024, int month = 4, int day = 15)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2024,4,15);

	Date d2 = d1; //与下面创建对象d3是等价的,两种写法
	d2.Print();

	Date d3(d1);
	d3.Print();

	return 0;
}

示例2:

cpp 复制代码
#include <iostream>
using namespace std;
class Stack
{
public:
	Stack(int n = 4)
	{
		cout << "Stack()" << endl;
		int* tmp = (int*)malloc(sizeof(int) * n);
		if (nullptr == tmp)
		{
			perror("malloc fail");
			return;
		}

		_arr = tmp;
		_capacity = n;
		_size = 0;
	}
	void Push(int x)
	{
		//扩容
		_arr[_size] = x;
		_size++;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		if (_arr)
		{
			free(_arr);
			_arr = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	int* _arr;
	int _size;
	int _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);

	Stack s2 = s1;

	return 0;
}

代码运行结果:

报错

编译器生成的默认拷贝构造是不够用,在上述代码中,s2对象使用s1对象的拷贝,由于是浅拷贝,会将s1中的内容原封不动的拷贝给s2,因此s1和s2调用的是同一块空间,在调用析构函数时,s1将空间释放了,但是s2中存放的还是s1的空间,因为还会再释放一次,一块内存空间的多次释放,会造成程序奔溃。同时在对任意一个栈中push数据的时候,另一个栈中的size是不会加的,但是共用的是同一块空间,数据丢失等等问题

示例3:错误写法

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, 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;
};

Date Func(Date d)
{
	Date tmp(d);
	return tmp;
}

int main()
{
	Date d1(2024, 4, 15);
	Func(d1);

	return 0;
}

在返回一个局部变量时,由于局部变量出作用域就销毁了,所以会将局部变量拷贝给一个临时变量,在给拷贝给临时变量时,又会调用拷贝构造函数,在调用拷贝构造函数时,又会将返回值拷贝给一个临时变量,造成无穷递归

4.3 总结

  1. 在类中,如果没有涉及需要资源管理的内置类型,是可以不写拷贝构造函数的,编译器自动生成的浅拷贝就够用,但是一旦涉及,就需要自己实现拷贝构造函数了
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
相关推荐
爪哇学长3 分钟前
解锁API的无限潜力:RESTful、SOAP、GraphQL和Webhooks的应用前景
java·开发语言·后端·restful·graphql
老赵的博客11 分钟前
QT 自定义界面布局要诀
开发语言·qt
gma99916 分钟前
brpc 与 Etcd 二次封装
数据库·c++·rpc·etcd
ö Constancy20 分钟前
设计LRU缓存
c++·算法·缓存
p-knowledge27 分钟前
建造者模式(Builder Pattern)
java·开发语言·建造者模式
网络安全(king)33 分钟前
【Python】【持续项目】Python-安全项目搜集
开发语言·python·安全
工业甲酰苯胺34 分钟前
Python脚本消费多个Kafka topic
开发语言·python·kafka
麻花201344 分钟前
C#之WPF的C1FlexGrid空间的行加载事件和列事件变更处理动态加载的枚举值
开发语言·c#·wpf
_黎明1 小时前
【Swift】字符串和字符
开发语言·ios·swift
C++忠实粉丝1 小时前
计算机网络socket编程(2)_UDP网络编程实现网络字典
linux·网络·c++·网络协议·计算机网络·udp