C++类和对象(中篇)

Posted Suk-god

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类和对象(中篇)相关的知识,希望对你有一定的参考价值。

文章目录

类的6个默认成员函数

class Date ;
看起来这只是一个普通的空类!
但是当真里面什么都没有吗??
其实不然,尽管它是一个空类,编译器依旧会为其生成6个默认的成员函数

具体如下:

默认成员函数的特点?
就是 用户自己没有写,编译器会自动生成。一旦用户显式提供,那么就不再生成
下面我们一 一详细理解每个函数

构造函数

是一个特殊的成员函数

主要特点:

  1. 函数名与类名相同
  2. 没有返回值类型
  3. 创建类类型的对象的时候由编译器自动调用
  4. 在对象整个生命周期中只调用一次
  5. 构造函数可以重载
    也可以在定义时给一个全缺省参数(无参 && 全参一次搞定)

代码验证:
情景一:使用编译器提供的默认构造函数,查看实例化一个对象到底做了什么

情景二:自定义一个构造函数,查看实例化对象做了什么

实验:

情景三:在上例的基础上,以Date d1的方式初始化会怎样?

情景四:用户实现一个构造函数,既可以传递参数,也适用于无参构造(全缺省构造)

此时就不会有报错了

注:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

调用场景

创建新对象的时候,由编译器自动调用,并且在整个生命周期内只调用1次

编译器生成的默认构造函数到底有用没?

答案是有用的!
情景模拟:
前提:有两个类,分别是Time和Date类,Time类的对象作为Date类的一个成员变量。Date类没有自定义构造函数,Time类自定义了构造函数,我们实例化一个Date类的对象,观察现象:

分析构造过程如下:

析构函数

概念

是一个特殊的成员函数
构造函数功能相反,析构函数不是完整对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作

特点

  1. 函数名是 ~类名
  2. 无参数,无返回值
  3. 一个类有且仅有1个析构函数。若未显式定义,系统会自动生成默认的析构函数
  4. 对象生命周期结束时,C++编译系统自动调用析构函数
  5. 编译器自动生成的析构函数,对会自定义类型成员调用它的析构函数
    验证:
    在Date类中自定义析构函数,观察析构函数的调用场景



    冷静分析一下,我们发现,我们实例化的对象d是在main函数的栈帧上开辟的空间,那我们在函数调用完毕后该函数的栈帧也就被释放,那么在该栈帧上的变量也就随之消亡。因此,在这种情况下,析构函数也就可有可无了。
    但是析构函数还是有它的用武之地的~~看下面的例子

调用场景

前提:我们使用C++的方式实现一个栈的结构。它的具体操作如下:

//stack.h
#pragma once

#include <iostream>

using namespace std;

typedef  int SDataType;

class Stack

public:
	Stack();
	~Stack();
	void StackPush(SDataType x);
	void StackPop();
	SDataType StackTop();
	int StackSize();
	int StackEmpty();//1:空 0:非空
private:
	void StackExpand();
private:
	SDataType* _array;
	int _size;
	int _capacity;
;

//测试函数
void TestStack();
//stack.cpp
#include "stack.h"

Stack::Stack()

	_array = (SDataType*)malloc(sizeof(SDataType)* 3);
	if (nullptr == _array)
	
		perror("malloc");
		return;
	
	_size = 0;
	_capacity = 3;


void Stack::StackExpand()

	//1、申请新空间(这里按照原空间的2倍申请)
	int newCapacity = _capacity * 2;
	SDataType* tmp = (SDataType*)malloc(sizeof(SDataType)*newCapacity);
	if (nullptr == tmp)
	
		perror("malloc");
		return;
	
	//2、将旧空间的值copy的新空间
	for (int i = 0; i < _capacity; i++)
	
		tmp[i] = _array[i];
	
	//3、释放旧空间
	free(_array);
	//4、使用新空间
	_array = tmp;
	_capacity = newCapacity;


Stack::~Stack()

	if (_array)
	
		free(_array);
		_size = 0;
		_capacity = 0;
	


void Stack::StackPush(SDataType x)

	if (_size == _capacity)
	
		//扩容
		StackExpand();
	
	_array[_size++] = x;

void Stack::StackPop()

	if (!StackEmpty())
	
		_size--;
	

SDataType Stack::StackTop()

	if (StackEmpty())
	
		perror("StackEmpty!");
		return -1;
	
	return _array[_size - 1];

int Stack::StackSize()

	return _size;

int Stack::StackEmpty()//1:空 0:非空

	return 0 == _size;


void TestStack()

	Stack s;
	s.StackPush(1);
	s.StackPush(2);
	s.StackPush(3);
	s.StackPush(4);
	s.StackPush(5);
	s.StackPush(6);

	cout << s.StackSize() << endl;
	cout << s.StackTop() << endl;

	s.StackPop();
	s.StackPop();

	cout << s.StackSize() << endl;
	cout << s.StackTop() << endl;



对于上述实现的代码,我们着重关注一下它的析构函数

我们可以看到,该析构函数对程序从堆上申请的资源进行了释放。如果没有该析构函数,那就会发生内存泄露。
因此,我们可以得到:
只要程序涉及到资源的申请,那么析构函数必须要程序员手动实现,默认的析构函数是无法达到我们的需求的。

拷贝构造函数

概念

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

特性

  1. 拷贝构造函数是构造函数的重载形式
  2. 拷贝构造函数的参数只有一个,并且必须使用引用传参,使用传值传参就会发生无穷递归
  3. 若未显式定义,系统生成默认的拷贝构造函数。
    默认的拷贝构造函数对象按内存存储,按字节序完成拷贝,这种拷贝我们叫做浅拷贝或者值拷贝
    浅拷贝在不涉及资源申请的类中可以正常使用,但是在涉及资源申请的类中是无法正常使用的
    后者会在释放申请资源的时候,调用两次析构函数,导致程序崩溃
  4. 默认生成的拷贝构造不能满足申请资源的情况,因此需要我们实现深拷贝

典型调用场景

  1. 使用已经存在的对象创建新对象
  2. 函数参数类型为类类型对象
  3. 函数返回值类型为类类型对象

运算符重载

为什么要有运算符重载

一句话,就是为了提高代码的可读性

基本形式

  • 是一个具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
  • 函数名: 关键字operator后面加需要重载的运算符符号
  • 函数原型: 返回值类型 operator操作符(参数列表)

注意事项

  1. 不能通过连接其他符号来创建新的操作符:比如 operator @
  2. 重载操作符必须有一个类类型或者枚举类型的操作数
  3. 用于内置类型的操作符,其含义不能改变。例如内置类型的+,不能改变其含义
  4. 作为类成员的重载函数时,其形参看起来比操作数数目少一,成员函数的操作符有一个默认的形参this限定为第一个形参
  5. 5个运算符不能重载

赋值操作符重载

  1. 形式
 返回值类型  operator=(参数)....
  1. 参数类型
    一般都是类类型对象的引用
    相较于传值这样就会少了形参实例化的拷贝构造
  2. 返回值
    一般返回 *this
    是类类型对象的引用
  3. 检测是否在给自己赋值
    if(this != &d)

来一个赋值操作符重载的例子

一个类如果没有显示定义赋值运算符重载函数,编译器也会生成一个,完成对象按字节序的值拷贝(浅拷贝)
和前面一样,对于需要申请资源的类来说,赋值运算符重载依旧需要我们自己编写,否则程序在最后析构的时候就会发生崩溃。

日期类实现

date.h

#pragma once

#include <iostream>

using namespace std;

class Date

public:
	Date(int year = 2001, int month = 5, int day = 7);
	void Print();

	// 获取某年某月的天数
	int GetMonthDay(int year, int month);
	// 拷贝构造函数
	// d2(d1)
	Date(const Date& d);
	// 赋值运算符重载
	// d2 = d3 -> d2.operator=(&d2, d3)
	Date& operator=(const Date& d);
	// 日期+=天数
	Date& operator+=(int day);
	// 日期+天数
	Date operator+(int day);
	// 日期-天数
	Date operator-(int day);
	// 日期-=天数
	Date& operator-=(int day);
	// 前置++
	Date& operator++();
	// 后置++
	Date operator++(int);
	// 后置--
	Date operator--(int);
	// 前置--
	Date& operator--();
	// >运算符重载
	bool operator>(const Date& d);
	// ==运算符重载
	bool operator==(const Date& d);
	// >=运算符重载
	inline bool operator >= (const Date& d);
	// <运算符重载
	bool operator < (const Date& d);
	// <=运算符重载
	bool operator <= (const Date& d);
	// !=运算符重载
	bool operator != (const Date& d);
	// 日期-日期 返回天数
	int operator-(const Date& d);
private:
	int _year;
	int _month;
	int _day;
	
;

date.cpp

#include "date.h"

Date::Date(int year, int month, int day)

	//检查日期的合法性
	if (year >= 0 && month > 0 && month <= 12 && day > 0 && day <= GetMonthDay(year, month))
	
		_year = year;
		_month = month;
		_day = day;
	
	else
	
		cout << "非法日期" << endl;
	

void Date::Print()

	cout << _year << "-" << _month << "-" << _day << endl;


// 拷贝构造函数
// d2(d1)
Date::Date(const Date& d)

	_year = d._year;
	_month = d._month;
	_day = d._day;


// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date&  Date::operator=(const Date& d)

	if (this != &d)
	
		_year = d._year;
		_month = d._month;
		_day = d._day;
	

	return *this;


// 获取某年某月的天数
inline int Date::GetMonthDay(int year, int month)

	static int dayArray[13] =  0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ;
	int day = dayArray[month];
	if (2 == month && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
	
		day = 29;
	
	return day;


/
// 日期+=天数
Date&  Date::operator+=(int day)

	if (day < 0)
	
		*this -= -day;
	
	else
	
		_day += day;
		while (_day > GetMonthDay(_year,_month))
		
			//产生进位,调整为正确的日期格式
			//减掉当月的天数,然后将月+1
			_day -= GetMonthDay(_year, _month);
			++_month;
			//判断月是否越界,月满了的话年+1
			if (_month > 12)
			
				++_year;
				_month = 1;
			
		
	
	return *this;

// 日期+天数
Date  Date::operator+(int day)

	//日期不能发生变化,返回的是日期加上天数的结果(因此无法使用引用返回)
	//可以复用+=
	Date tmp(*this);
	tmp += day;

	return tmp;


// 日期-=天数
Date& Date::operator-=(int day)

	if (day < 0)
	
		*this += -day;
	
	else
	
		_day -= day;
		while (_day <= 0)
		
			//月先--,因为借的是上一个月的天数
			--_month;
			if (_month == 0)
			
				--_year;
				_month = 12;
			
			_day += GetMonthDay(_year, _month);
		
	
	return *this;

// 日期-天数
Date Date::operator-(int day)

	//拷贝构造当前对象
	Date tmp(*this);
	//复用-=
	tmp -= day;

	return tmp;

///


// 前置++
Date&  Date::operator++()

	*this += 1;
	return *this;

// 后置++(int是一个占位符,不起实际作用)
Date  Date::operator++(int)

	Date tmp(*this);
	*this += 1;
	return tmp;

// 后置--
Date Date::operator--(int)

	Date tmp(*this);
	*this -= 1;
	return tmp;

// 前置--
Date& Date::operator--()

	*this -= 1;
	return *this;

/


// >运算符重载
bool Date::operator>(const Date& d)

	if (_year > d._year)
	
		return true;
	
	else if (_year == d._year)
	
		if (_month > d._month)
		
			return true;
		
		else if (_month == d._month)
		
			if (_day > d._day)
			
				return true;
			
		
	
	return false;

// ==运算符重载
bool Date::operator==(const Date& d)

	return _year == d._year &&
		_month == d._month &&
		_day == d._day;

// >=运算符重载
inline bool Date::operator >= (const Date& d)

	return *this > d || *this == d;

// <运算符重载
bool Date::operator < (const Date& d)

	return !(*this >= d);

// <=运算符重载
bool Date::operator <= (const Date& d)

	return *this < d || *this == d;

// !=运算符重载
bool Date::operator != (const DateC++从青铜到王者第三篇:C++类和对象(中篇)

C++初阶:类和对象(中篇)构造函数 | 析构函数 | 拷贝构造函数 | 赋值运算符重载

在 c++ 中将对象分配给一个值时会发生啥 [关闭]

C++从入门到入土第四篇:类与对象(中篇)

在java中的继承上下文中通过对象调用类成员时会发生啥

c++ 当在一个线程中写入并在第二个线程中读取同一个对象时会发生啥? (它安全吗?)[重复]