类和对象(中)

目录

[一 . 类的默认成员函数](#一 . 类的默认成员函数)

[二 . 构造函数](#二 . 构造函数)

[三 . 析构函数](#三 . 析构函数)

[四 . 拷贝构造函数](#四 . 拷贝构造函数)

[4.1 写法以及相关问题](#4.1 写法以及相关问题)

[4.2 自动生成拷贝构造](#4.2 自动生成拷贝构造)

一 . 类的默认成员函数

默认成员函数就是用户没有显示实现编译器会自动生成的成员函数称为默认成员函数 。 一个类 ,在不写的情况下 , 编译器会默认生成以下的 6 个 默认成员函数 , 需要注意的是这 6个中最重要的是前 4 个 , 最后两个取地址重载不重要 , 稍微了解就好 。 其次 C++11 以后 还会增加两个默认成员函数 , 移动构造 和 移动赋值, 后续更新 ...

二 . 构造函数

**构造函数是特殊的成员函数 ,**需要注意的是 ---> 构造函数虽然名称为 构造 , 但 是构造函数的主要任务并不是开辟空间,创造对象(我们常使用的局部对象是栈帧创建时 , 空间就开好了) , 而是对象实例化时 初始化对象 。

构造函数的本质是要替代我们以前 Stack 和 Date 类中写的 Init 函数的功能 , 构造函数自动调用的特点就完美的替代了 Init 。

构造函数的特点 :

  • 函数名 与 类名相同 。
  • 返回值 。 ( 返回值啥都不需要给 , 也不需要写 void )
  • 对象实例化时 系统 会 自动调用 对应的构造函数 。
  • 构造函数可以重载
  • 如果类中没有显示定义构造函数 , 则C++编译器会自动生成一个无参的默认构造函数 ,一旦用户显示定义 ----> 编译器将不再生成 。
  • 无参构造函数 , 全缺省构造函数 , 在不写构造函数时编译器默认生成的构造函数都叫做默认构造函数 。 但是这三个函数有且只有一个存在 , 不能同时存在 。无参构造函数和全缺省构造函数虽然构成函数重载 , 但是调用时会存在歧义 。**注意 !注意 !注意 ! 默认构造函数并不只有编译器默认生成的那个叫默认构造 , 实际上无参构造函数 , 全缺省构造函数也是默认构造函数 ,**总结以下 ----> 就是不传实参就可以调用的构造就叫默认构造 。
  • 不写构造函数时 , 编译器默认生成的构造 , 对内置类型 成员变量的初始化没有要求 , 也就是是否初始化是不确定看编译器 。 对于自定义类型 成员变量 , 要求 调用这个成员变量的默认构造函数初始化 。 如果这个成员变量 , 没有默认构造函数 , 那么就会报错 , 需要初始化这个成员变量 , 需要使用初始化列表才能解决 , 初始化列表是啥 ? 后续更新 ....

接下来使用 日期类构造函数的特点 详细讲解 --->

实现日期类的构造函数 :

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class Date
{
	
public:
	//无参构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	//带参的构造函数
	Date(int year,int month , int day)
	{
		_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();

	Date d2(2024 , 11 , 16);
	d2.Print();
	return 0;
}

构造函数是放在public 里面 , 不然的话 , 调用不了 :

构造函数可以重载 ,为啥 ?

----> 因为函数可以有不同的初始化的方式 ,对象实例化的时候会 调用对应的构造函数

思考1 :

思考 :当全缺省函数添加进来时 , 是否可以正常调用 ? ---> 不可以噢~

class Date
{
	
public:
	//无参构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	//带参的构造函数
	Date(int year,int month , int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//全缺省构造函数
	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;
};

思考 : 什么情况下需要自己写构造 ?

//日期类没有写构造函数时 
// -->发现并没有成功初始化!!!

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class Date
{
public:
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

所以成员变量为内置类型的时候需要自己写构造函数 , 仅对于某些编译器可能会初始化 , 但是没啥保障 , 还得自己来 !

默认构造函数不是只有 编译器自动调用!!!

啥时候不需要自己写构造 ?

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
private:
		 STDataType * _a;
		 size_t _capacity;
		 size_t _top;
		
};
//两个栈实现一个队列
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;
};

int main()
{
	//调用了栈的初始化 -- MyQueue不需要写构造,默认生成的就够用了
	MyQueue mq;
	return 0;
}

三 . 析构函数

析构函数与构造函数功能相反 , **析构函数不是完成对对象本身的销毁,**比如局部对象是存在栈帧的 , 函数结束栈帧销毁 , 它就释放了 , 不需要我们管 , C++规定对象再销毁时会自动调用析构函数 , 完成对对象中的资源清理释放工作 。 析构函数的功能类比我们之前Stack 实现的Destory 的功能 , 而像Date 没有 Destory , 其实就是没有资源需要释放 , 所以严格说Date , 是不需要析构函数的 。

析构函数的特点 :

  • 析构函数名是在类名前加上字符~
  • 无参数无返回值 。 (这里与构造类似 , 也不需要加void)
  • 一个类只能有 一个 析构函数 。 若未显式定义,系统会自动生成默认的析构函数 。
  • 对象生命周期结束时 , 系统会自动调用析构函数**。**
  • 与构造函数类似 , 我们不写构造函数时 , 编译器会自动生成析构函数 , 但是对内置类型成员不做处理 , 自定类型成员会调用他的析构函数 。
  • 需要注意的时 , 显示写析构函数时 , 对于自定义类型成员也会调用自定义类型中的析构 , 也就是说自定义类型成员无论什么情况都会自动调用析构函数 。
  • 如果类中 没有 申请资源时 , 析构函数可以不写 , 直接使用编译器生成的默认析构函数 , 如 Date ; 如果默认生成的析构就可以用 , 也就不需要显示写析构函数 , 如MyQueue ; 但是有资源申请时 , 一定要自己写析构 , 否则会造成资源泄漏 , 如Stack 。
  • 一个局部域的多个对象 , C++规定 后定义 的先析构 。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

// 两个栈实现一个队列
class MyQueue
{
	// 不需要写构造,默认生成就可以用
	// 不需要写析构,默认生成就可以用
private:
	Stack _pushst;
	Stack _popst;
};

int main()
{

	MyQueue mq1;

	return 0;
}

四 . 拷贝构造函数

如果一个构造函数的 第一个参数 是 自身类类型的引用 , 且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数 , 也就是说拷贝构造 ( 拷贝初始化 ) 是一个特殊的构造函数 。

拷贝构造的特点 :

  • 拷贝构造函数 是构造函数的一个重载 。
  • 拷贝构造函数的参数 只有一个且必须是类 类型对象的引用 ,使用传值方式编译器直接报错 , 因为语法上会引发 无穷递归调用
  • C++ 规定自定义类型对象进行拷贝行为必须调用 拷贝构造 , 所以这里自定义类型传值传参 和 返回都会调用拷贝构造完成 。
  • 若未显式定义拷贝构造 , 编译器会 自动生成 拷贝构造函数 。 自动生成的拷贝构造对内置类型成员变量会完成 值拷贝/浅拷贝(一个字节一个字节拷贝) , 对自定义类型成员变量会调用他的拷贝构造 。

4.1 写法以及相关问题

举 日期类为例 :

Date (const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}

什么时候需要用到拷贝构造函数(拷贝初始化,完成对象的拷贝) ?

-----> 通过一个对象 初始化 新创建的对象 ( 以下代码是通过d1 初始化 d2)

#define _CRT_SECURE_NO_WARNINGS 1
#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 d2(d1)
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024,11,16);
	d1.Print();

	Date d2(d1);
	d2.Print();
	return 0;
}

C++ 规定自定义类型对象进行拷贝行为必须调用 拷贝构造 , 所以这里自定义类型 传值传参 和 传值返回 都会调用拷贝构造完成 。

可以通过控制台 , 调试观察 , 是否调用了 拷贝构造 , 何时调用的拷贝构造 。

#define _CRT_SECURE_NO_WARNINGS 1
#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 d2(d1)
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

void func1(Date d)
{

}

//传返回值 -- 也会调用拷贝构造
//传值返回不是返回 d , 而是d 的拷贝
//d 拷贝的临时对象 返回d
Date func2()
{
	Date d;
	//...
	return d;
}
int main()
{
	Date d1(2024, 11, 16);
	func1(d1);
	func1(d1);
	//C++规定 -- 无论传参还是直接初始化,只要是一个自定义类型对象去初始化另一个自定义类型对象的时候
	//要调用拷贝对象
	//调用func1的时候 , 先传参 , 调用拷贝构造
	return 0;
}

思考 :

1 ) 为什么 必须 加 &

**--->**不加 & , 会发生无穷递归 !!!

注 : 如果编译器此时没报错 , 但也运行不过去 , 因为发生了无穷递归 (没有返回条件)。

传值返回 会产生 一个临时对象 调用拷贝构造 ;

传值引用返回 , 返回的是返回对象的别名(引用) , 没有产生拷贝!

但是如果返回对象是一个当前函数局部域的局部对象 ,函数结束就销毁了 ,那么使用引用返回是有问题的 ,这时的引用相当于也引用 , 类似一个野指针一样 。 传引用返回可以减少拷贝 , 但是一定要确保返回对象 , 在当前函数结束后还在 ,才能用 引用返回 。

语法上 , 引用没有开空间 ,是 取别名 。

2 ) 为什么建议加上 const

举个例子 : 如果我给了一个张三的蓝本 , 给你去造一个张三出来 , 但是因为某一个不小心的错误 , 把张三 变成了 李四了 , 给我造了个李四出来 , 还把我原先给的张三的蓝图 改成了 李四的蓝图 , 这就和本意不符合了。

加上const 的好处 :

1 ) 程序更健壮了 ,防止对象被错误修改

2 ) 避免因为权限扩大而报错

注意 : 拷贝构造的第一个参数必须是 类 类型对象的引用 , 可以再后面加参数 , 但此时的参数必须是缺省的!

	Date(const Date& d,int x= 1)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

4.2 自动生成拷贝构造

1 ) 啥是深拷贝 ? 啥是浅拷贝 ?

浅拷贝 : 只拷贝对象的数据 , 资源不进行拷贝

深拷贝 : 不仅拷贝对象的数据 , 而且拷贝资源

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

2 ) 如果自定义类型 , 使用自动生成的拷贝构造 --> 浅拷贝 , 会怎样 ?

会崩 ~

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}


	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}

	void Pop()
	{
		_a[_top - 1] = -1;
		--_top;
	}

	int Top()
	{
		return _a[_top - 1];
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

int main()
{

	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	st1.Push(4);

	Stack st2(st1);
	
	//st1.Pop();
	//st1.Pop();

	//cout<<st2.Top()<<endl;
	return 0;
}

会导致新对象的修改影响原对象 !

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}


	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}

	void Pop()
	{
		_a[_top - 1] = -1;
		--_top;
	}

	int Top()
	{
		return _a[_top - 1];
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

int main()
{

	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	st1.Push(4);

	Stack st2(st1);
	
	st1.Pop();
	st1.Pop();

	cout<<st2.Top()<<endl;
	return 0;
}

3 ) 为啥要用深拷贝 ?

1 )避免空间被释放多次

2 ) 避免新对象的修改 , 影响原对象

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;


typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	// st2(st1)
	Stack(const Stack& st)
	{
		// 需要对_a指向资源创建同样大的资源再拷贝值
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}


	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}

	void Pop()
	{
		_a[_top - 1] = -1;
		--_top;
	}

	int Top()
	{
		return _a[_top - 1];
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

int main()
{

	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	st1.Push(4);

	Stack st2(st1);
	return 0;
}

3 ) 需要写拷贝构造的小 tip

如果一个类显示实现了析构 并 释放资源 , 那么它就需要显示写拷贝构造 , 否则就不需要。

下面列举三个类 :

1 ) 日期类 (Date) : 成员变量全是内置类型的 , 且没有指向什么资源 , 编译器自动生成的拷贝构造就可以完成需要的拷贝 。不需要再额外写拷贝构造 。

2 )Stack 类 : 编译器自动生成的拷贝构造 --- 值拷贝/浅拷贝 , 不符合需求 , 所以需要自己写深拷贝 ( 对指向的资源也拷贝) 。

3 ) MyQueue 类 : 内部主要是自定义类型Stack 成员 , 编译器自动生成的拷贝构造会调用 Stack 的拷贝构造 , 也不许要我们显示实现MyQueue 的拷贝构造 。

MyQueue 类 :

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;


typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	// st2(st1)
	Stack(const Stack& st)
	{
		// 需要对_a指向资源创建同样大的资源再拷贝值
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}


	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}

	void Pop()
	{
		_a[_top - 1] = -1;
		--_top;
	}

	int Top()
	{
		return _a[_top - 1];
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;
};

int main()
{

	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	st1.Push(4);

	Stack st2(st1);

	MyQueue q1;
	MyQueue q2(q1);
	return 0;
}
相关推荐
monkey_meng38 分钟前
【Rust中的策略模式实现】
开发语言·rust·策略模式
C_eeking2 小时前
Ubuntu23.10下解决C语言调用mysql.h问题
c语言·mysql·adb
吃汉堡吃到饱2 小时前
【Android】EventBus事件总线用法浅析
android·开发语言
这猪好帅2 小时前
【项目组件】第三方库——websocketpp
开发语言·c++
w_outlier3 小时前
线程(二)【线程控制】
linux·c++·多线程·线程控制
YxVoyager3 小时前
【OpenGL】OpenGL简介
c++·windows·图形渲染
IT猿手4 小时前
多目标优化算法:多目标鹅算法(MOGOOSE)求解UF1-UF10,提供完整MATLAB代码
开发语言·算法·matlab·多目标算法
水w4 小时前
VuePress v2 快速搭建属于自己的个人博客网站
开发语言·前端·vue·vuepress
MinBadGuy4 小时前
【GeekBand】C++设计模式笔记11_Builder_构建器
c++·设计模式