首页 > 编程语言 >C++类和对象(中),重点之重点,类和对象的掌握就看这一节了!

C++类和对象(中),重点之重点,类和对象的掌握就看这一节了!

时间:2024-09-22 16:23:38浏览次数:11  
标签:const 函数 对象 C++ 运算符 int Date 重点 构造函数

[前言] 

类中有六大默认成员函数,默认成员函数就是用户不显示实现,编译器也会自动生成的成员函数

目录

[前言] 

一、构造函数

二、析构函数

三、拷贝构造函数

四、赋值运算符重载

五、取地址运算符重载


一、构造函数

构造函数虽然叫构造,但是它并不是开空间创建对象,而是在对象实例化的时候初始化

 构造函数的本质就是为了替代Init函数

 构造函数的特点:

  • 函数名和类名相同
  • 对象实例化的时候系统会自动调用对应的构造函数
  • 构造函数无返回值( 不需要写void)

构造函数我们不写,系统也会自动生成一个构造函数,但是一旦我们在类中写了,系统就将不在生成,其目的都是为了初始化

系统生成的这个构造函数在初始化的时候就分两种情况:

  • 内置类型成员(int/char...):系统对其初始化没有要求,是否初始化,全看编译器
  • 自定义类型成员(class/struct...):系统会调用默认构造函数来初始化,如果没有默认构造函数,就将报错

这里又有一个新的专业名词,默认构造函数。那啥是默认构造函数嘞?

默认构造函数分为三种:

  • 无参的构造函数
  • 全缺省的构造函数
  • 我们不写使,编译器默认生成的构造函数

以上三种都被称为默认构造函数,但是需要注意的是,默认构造函数有且仅有一个能存在,它们不能同时存在 

总结一下就是,不传参就能调用的函数就是默认构造函数 

class Date
{
public:

	//Date()  //无参构造函数
	//{
	//	_year = 1;
	//	_month = 1;
	//	_day = 1;
	//}

	Date(int year = 1, int month = 1, int day = 1)//全缺省构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//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;   //使用无参构造函数的时候,后面的小括号就不需要加了
	Date d2(2024, 9, 8);

	
	d1.Print();
	d2.Print();

	return 0;
}

唠叨一句,因为编译器的自动生成的默认构造函数不靠谱 ,所有构造函数还是得我们自己写

二、析构函数

析构函数不是把对象本身给销毁,而是把对象中的资源清理释放,以免造成资源泄露。

 析构函数的功能有点像之前我们学的Destroy函数。

析构函数的特点:

  • 析构函数的函数是类名前面加~
  • 无参数无返回值
  • 一个类只能有一个析构函数
  • 对象生命周期结束的时候,系统会自动调用析构函数

 若没有定义析构函数,系统会自动生成默认的析构函数。

析构函数对成员的处理也分为两种情况:

  • 内置类型成员:析构函数对其不做处理,因为没有资源需要释放
  • 自定义类型成员:会调用这个自定义类型里面的析构函数

注意哈,无论我们写不写析构函数,这个自定义类型成员,都只会调用自己类型的析构函数 

typedef int STDataType;

class Stack
{

public:
	Stack(int n = 4) //构造函数
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	~Stack()
	{
		free(_a); //由于上面申请了资源,所以最后要清理释放
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	int _capacity;
	int _top;

};

//用两个栈实现队列
class MyQueue 
{
public:
	//由于成员变量都是自定义类型成员,所以写不写析构函数都无所谓
private:
	Stack pushst; //自定义类型成员只会调用自己的析构函数,也就是Stack里面的析构函数
	Stack popst;
};


int main()
{
	Stack st;
	MyQueue mq;

	return 0;
}

如上代码,C++中有规定后定义的要先析构,也就是这里MyQueue mq先析构,Stack st后析构

如果类中没有申请资源的话,我们就可以不写析构函数,直接使用编译器默认生成的析构函数即可,但是一旦有资源申请时,必须要自己写析构函数,否则就会造成资源的泄露

三、拷贝构造函数

拷贝构造函数其实就是构造函数的重载,是一个特殊的构造函数。

特殊就特殊在其第一个参数必须是自身类类型的引用。并且其他参数都要有缺省值。

class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day =1) 
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//由于成员变量都是内置类型,故析构函数可以不写

	//拷贝构造函数
	Date(const Date& d)
	{
        //const是为了避免权限被放大,毕竟有些时候传的可能是临时对象,而临时对象具有常性
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 9, 11);

	Date d2(d1); //拷贝构造一个d2
	Date d3 = d1; //拷贝构造的另外一种写法,构造一个d3

	return 0;
}

若没有显示定义拷贝构造函数,编译器也会自动生成。

自动生成的拷贝构造函数对成员的处理分为两种:

  • 内置类型:浅拷贝/值拷贝,也就是一个字节一个字节的拷贝
  • 自定义类型:调用该类型的拷贝构造

 注!若类里面的成员变量都是内置类型,我们就可以不用定义拷贝构造函数了,但是有特殊情况,就比如Date和Stack都是只有内置类型,但Date不用定义拷贝构造,Stack却需要。这是因为Stack这样的类里面指向了一定的空间,如果我们还是使用浅构造的话,再拷贝一个对象出来,两个对象指向同一个空间,显然不合理,故得使用深拷贝(就像之前随机链表的复制一样)

 有个小技巧就是,我们看有没有显示定义析构函数,只要有析构函数我们就定义拷贝构造函数,如果没有,我们也不需要写拷贝构造函数。

现在我们来说一下为啥拷贝构造函数的第一个参数必须要是类类型的引用?

在上图的代码中,如果我们想要调用func,就先得传参

C++规定类类型的传值传参必须调用拷贝构造函数

所以在传参前我们就得调用拷贝构造了,如果这个时候拷贝构造函数的一个参数是类类型而不是类类型的引用,那么我们在调用拷贝构造函数的时候还得再传一个参数,但是根据规定,要先拷贝构造,所以以此往复就是传参、调用拷贝构造函数、传拷贝构造函数里面的参数、调用拷贝构造函数、传参...... 

当然了,为了避免后续引发的无穷递归调用的现象,当我们把拷贝构造函数的第一个参数错误的写成类类型的时候,编译器就会直接报错了

如果第一个参数是类类型的引用就完全不会有这种顾虑,因为在调用一次拷贝构造函数的时候,咱就给这个实参取了一个别名

在 自定义类型进行拷贝行为时,是必须得调用拷贝构造函数的。

自定义类型的拷贝行为有两种:

  • 传值传参
  • 传引用返回

传值传参:

void Func1(Date d)
{
	//...
}


int main()
{
	Date d1(2024, 9, 11);

	Func1(d1);

	return 0;
}

传引用返回:

Date& Func2()
{
	//...
}

int main()
{
	Date d1(2024, 9, 11);

	Date ret = Func2();

	return 0;
}

注!局部对象不能返回它的引用

局部对象在函数结束的时候就销毁了,如果这个时候再次引用它返回,这里的引用就类似于一个野指针。

故当传引用返回的时候一定要确保该返回对象在函数结束后还继续存在

总而言之就是传值传参怎么传都可以,但是传引用返回 一定要小心和细心。

四、赋值运算符重载

在聊赋值运算符重载之前,我们先来了解一下什么是运算符重载

我们知道内置类型可以直接通过运算符进行运算,但是如果我们想把类类型也通过这样的方式运算,简单的使用运算符就不可取了。

C++规定,对类类型对象使用运算符时,必须转换成调用对应的运算法重载

运算符重载也可以理解为运算符重载函数,特点如下:

  • 具有特殊的名字:名字由operator和一个运算符共同构成
  • 参数个数,同那个运算符的作用对象一样多
  • 运算符重载作为成员函数时,第一个运算对象默认传给了隐藏的this指针(也就是参数看上去少了一个)
  • 运算符重载函数至少有一个参数是类类型的

 

有些时候我们必不可少的会用类类型进行运算,比如日期的加减,但是由于编译器不知道这个类的比较方式应该是什么样的,所以这个运算比较的方法就由我们自己去定义,由此啊运算符重载的出现是很有必要的

 如下,简单写一个运算符重载函数:

class Date
{
public:
	
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	bool operator<(const Date& d) //运算符重载函数
	{
		if (_year < d._year)
		{
			return true;
		}
		else if (_year == d._year && _month < d._month)
		{
			return true;
		}
		else if (_year == d._year && _month == d._month && _day < d._day)
		{
			return true;
		}

		return false;
	}


private:
	int _year;
	int _month;
	int _day;
};



int main()
{
	Date d1(2024, 9, 11);
	Date d2(2024, 9, 18);

	bool ret1 = d1.operator<(d2);  //显示调用运算符重载对应的运算符重载函数
	bool ret2 = d1 < d2;  //即使不显示调用,在编译的时候也会转换成
	
	return 0;
}

 关于运算符重载呢,还有点想说的:

  1. 不能使用新的运算符,比如说@
  2. 有五个运算符不能实现重载,分别是  .*::  ,sizeof?: .
  3. 重载++运算符时,为了区分前置++和后置++,重载后置++时,要增加一个int的形参


 好的,了解完运算符重载函数,现在我们就进入赋值运算符重载的世界~~~

赋值运算符重载是一个运算符重载,规定赋值运算符重载必须是成员函数

因为赋值运算符重载是一个默认成员函数

注!要将赋值运算符重载函数和拷贝构造函数进行区分,前者用于完成两个已经存在的对象的直接拷贝赋值,而后者用于一个已经存在的对象给另一个将要创造的对象拷贝

 赋值运算符重载函数的一些特点:

  • 参数要用const来修饰,
  • 有返回值,其目的是为了能够支持连续赋值的操作
  • 建议参数和返回值都写成引用的形式,减少拷贝(指的是另外又创一个空间),提高效率
  • 没有显示实现的时候,编译器会自动生成

赋值运算符重载的自动生成的内部规则,和拷贝构造函数类似,也是分两种情况,这里不再重复赘述,可以返回到【三、拷贝构造函数】那块再看一遍

class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}


	//拷贝构造函数
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}


    //赋值运算符重载函数
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this; //this是地址,所以要有个解引用
	}


private:
	int _year;
	int _month;
	int _day;
};



int main()
{
	Date d1(2024, 9, 11);
	Date d2(2024, 9, 18);

	//赋值运算符重载
	d2 = d1;

	//拷贝构造
	Date d3 = d1;
	
	return 0;
}

到此,我们先总结一下以上的四种默认成员函数:

  • 构造一般需要自己写,自己传参定义初始化
  • 对于析构来说,如果构造时涉及到了资源的申请,那就需要自己写析构了
  • 如果自己显示写了析构,那么拷贝构造赋值重载也需要自己写

五、取地址运算符重载

取地址运算符重载作为默认成员函数中的一个函数来说,还是非常简单的。不过在讲取地址运算重载之前呢,要先讲一个const成员函数,而这个const成员函数是一个比较重要的小知识点。

 const修饰的成员函数就是const成员函数,其形式就是在成员函数的后面加上一个const。

注意!如果是const成员函数,那么其声明和定义的后面都要加const

那啥是const成员函数嘞?

先来回顾一个知识,那就是

  • const在*前面修饰的就是指向的内容,const在*的右边修饰的就是指针本身
  • 修饰指针本身的const不存在权限的放大和缩小,只有修饰指向内容的const才存在

之前我们聊到的const说的一直都是const引用相关的内容,但是如果传一个const修饰的对象给成员函数嘞?

既然是const修饰,那么如果函数只是简单的接收就相当于把权限放大了,很显然这就是错误的了,因此成员函数为了能够接收一个const修饰的对象,必须发生相应的调整,即把成员函数变成const成员函数

const成员函数当中的const,实际修饰的是隐含this指针指向的内容,也就是对该成员函数中出现的成员变量不能进行修改,那么我们刚刚接收的const修饰的对象传进来的时候就不怕被修改了

之前在类和对象上我们有学到一个知识点是this指针,在Date类中,成员函数默认隐藏的this指针的形式是Date* const this,这里的const修饰的就是指针本身了,也就是说传过来的指针是不被允许发生改变的,不能够在指向其他地址

由const修饰过的成员函数,在Date类中,其this指针就会由Date* const this变为const Date* const this

最后在多一嘴,在C++中,加const修饰的原则就是,能加的尽量都给加上

注!成员函数里面需要修改成员变量的时候不能加const,其他情况皆可以加const 

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// void Print(const Date* const this) const
	void Print() const //const成员函数就是在函数后面加const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	// 这⾥⾮const对象也可以调⽤const成员函数,也就是⼀种权限的缩⼩ 
	Date d1(2024, 7, 5);
	d1.Print();

	const Date d2(2024, 9, 21);
	d2.Print();
	return 0;
}


好的,说完了const成员函数,咱就简单说一下取地址运算符重载

取地址运算符重载分为两种

  • 普通取地址符重载
  • const取地址符重载

const取地址符重载也就是取一个地址,但是这个对象被const修饰了

其实正常情况来说,编译器自动生成的取地址符重载就已经完全够我们使用了我们自己是不需要取定义的

但是,如果你不想让别人取到地址,那么你就可以自己去显示实现

如下,带大家看一下,编译器自动生成的取地址符重载是啥样的

class Date
{
public:

	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date* operator&()
	{
		return this;
		// return nullptr;
	}

	const Date* operator&()const  //const修饰返回值,表面返回值作为常量返回,不允许被修改
	{
		return this;
		// return nullptr;
	}
private:
	int _year; // 年 
	int _month; // ⽉ 
	int _day; // ⽇ 
};

int main()
{
	Date d1(2024, 7, 5);
	const Date d2(2024, 9, 21);

	cout << &d1 << endl;
	cout << &d2 << endl; //会自动匹配最适合自己的取地址符重载函数

	return 0;
}

标签:const,函数,对象,C++,运算符,int,Date,重点,构造函数
From: https://blog.csdn.net/2401_83883881/article/details/142026901

相关文章

  • JavaScript中的Math对象详解
    JS中的算术运算基本运算:加减乘除求余数,+-*/%.复杂运算:通过Math对象的属性定义的函数和常量来实现。代码实现:<!DOCTYPEhtml><html><head><metacharset="utf-8"><title>3.1.3JS中的算术运算</title></head><body>......
  • 【C++】二叉搜索树的底层以及实现
    个人主页文章目录⭐一、二叉搜索树的概念......
  • C++速通LeetCode中等第10题-轮转数组(四种方法)
    方法一:巧用deque双向队列容器classSolution{public:voidrotate(vector<int>&nums,intk){deque<int>q;inttmp;if(nums.size()>1){for(autonum:nums)q.push_back(num);......
  • MySQL 用户、权限管理,C/C++连接与使用
    目录用户用户管理查询所有用户查看当前用户查看当前连接数创建用户删除用户修改密码规则查看规则/策略规则说明临时设置持久设置修改密码权限数据库提供的权限列表查看权限给用户授权回收用户权限使用C语言连接库的安装CAPImysql_initmysql_real_connectmysql_closemysql_querym......
  • C++自助洗衣店-计算机毕业设计源码35120
    目 录摘要1绪论1.1选题背景与意义1.2研究现状1.3论文结构与章节安排2 自助洗衣店管理系统系统分析2.1可行性分析2.1.1技术可行性分析2.1.2 经济可行性分析2.1.3操作可行性分析2.2系统功能分析2.2.1功能性分析2.2.2非功能性分析2.3 系......
  • C++容器list底层迭代器的实现逻辑~list相关函数模拟实现
    目录1.两个基本的结构体搭建2.实现push_back函数3.关于list现状的分析(对于我们如何实现这个迭代器很重要)3.1和string,vector的比较3.2对于list的分析3.3总结4.迭代器类的封装5.list容器里面其他函数的实现6.个人总结7.代码附录1.两个基本的结构体搭建首先就是我......
  • 【蓝桥杯】2024.9.22算法赛——灵魂问题\全栈项目小组(C++)
    一、灵魂问题题目灵魂问题题目分析1.要求输出一个整数2.题目在玩脑筋急转弯,关键句子标出来了——糖什么的根本不重要。所以咖啡不加糖,答案是0!!!代码#include<iostream>usingnamespacestd;intmain(){ cout<<0; return0;}二、全栈项目小组题目全栈项目小组......
  • C++ 线程池
    #include<iostream>#include<string>#include<memory>#include<vector>#include<thread>#include<queue>#include<functional>#include<mutex>usingnamespacestd;classThreadPool{public:Thread......
  • C++:数组与字符串
    一、数组         数组是一种存储若干元素的数据类型,在诸多编程语言中存在,其显著的特点是元素通常是在物理层面上连续存储的(逻辑上的数组,比如链表,可能不是),并且具有极快的元素访问速度。    数组通常是同构的(homogenous ),即数组中的元素都是同一类型的,......
  • [数据结构与算法·C++] 笔记 1.4 算法复杂性分析
    1.4算法复杂性分析算法的渐进分析数据规模n逐步增大时,f(n)的增长趋势当n增大到一定值以后,计算公式中影响最大的就是n的幂次最高的项其他的常数项和低幂次项都可以忽略大O表示法函数f,g定义域为自然数,值域非负实数集定义:如果存在正数c和n,使得对任意的......