首页 > 其他分享 >类和对象(中)

类和对象(中)

时间:2024-08-17 11:22:57浏览次数:6  
标签:函数 对象 运算符 析构 重载 拷贝 构造函数

目录

1. 类的默认成员函数

2. 构造函数 

3. 析构函数

4. 拷贝构造函数

5. 赋值运算符重载

5.1  运算符重载

5.2 赋值运算符重载

5.3 日期类实现

6. 取地址运算符重载

6.1 const成员函数

6.2 取地址运算符重载


1. 类的默认成员函数

默认函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解以下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:

  • 我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
  • 编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?

2. 构造函数 

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并 不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化 对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数⾃动调用的特点就完美的替代的了Init。

构造函数的特点:

  1. 函数名与类名相同。
  2. 无返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
  3. 对象实例化时系统会自动调用对应的构造函数。
  4. 构造函数可以重载。
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显示定义编译器将不再生成。
  6. 无参构造函数,全缺省构造函数,我们不写构造时编译器默认生成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数,全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造。
  7. 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表。

说明:C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
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;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	//构造函数对象实例化的时候自动调用
	//调用无参 - 没有参数的时候不能加括号,加括号就报错,
	//因为存在歧义,Date f1();这是函数的声明还是对象的定义
	//规定不能这么写
	//对象加参数
	Date d1;//不传参
	Date d2(2024,8,8);//传参

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

	Date d3(2024);//传一部分参数
	d3.Print();

	return 0;
}
/*
我们以后对于一个类,要初始化这个对象,那么我们应该去写它的
构造函数,并且定义对象的时候就自动调用,好处就是写了构造,
对象就一定被初始化了,
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
上面这两个函数构成函数重载,函数名相同参数不同,但是它们调用的时候会
存在歧义,所以不能这样写,如果无参的时候调用的话既可以调用第一个,又可
以调用第二个,所以不知道调谁,不会把无参的和全缺省的一起写。

构造一般情况下可以提供多个,但是其中一个提供全缺省的会非常的好,如果调用
不传参,它会全部用缺省的,如果想显示调用,就传参,其次还可以只传一部分参数
*/

 

 

我们之前用C语言写过两个栈实现一个队列。

 对内置类型会调默认构造,对内置类型不确定,那么内置类型和自定义类型在一起会怎么样呢?

3. 析构函数

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

析构函数的特点:

  1. 析构函数名是在类名前面加上~。
  2. 无参数返回值。(跟构造函数一样,也不需要加void)
  3. 一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,系统会自动调用析构函数。
  5. 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用他的析构函数。
  6. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
  7. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date。如果默认生成的析构就可以用,也不需要显示写析构,如MyQueue。但是有资源申请时,一定要自己写析构,否则会造成资源泄露,如Stack。
  8. 一个局部域的多个对象,C++规定后定义的先析构。

日期类的析构函数: 

日期类其实是不需要析构函数的,我们只是强行写了一个,那么哪些类需要析构函数呢?

栈的析构函数: 

默认生成的析构函数: 

自定义类型的析构函数: 

 编译器自动调用析构:

对比一下用C++和C语言实现的Stack解决之前括号匹配问题isValid,我们发现有了构造函数和析构函数确实方便了很多,不会在忘记调用Init和Destroy函数了,也方便了不少。 

using namespace std;
// ⽤最新加了构造和析构的C++版本Stack实现
bool isValid(const char* s) {
Stack st;
while (*s)
{
	if (*s == '[' || *s == '(' || *s == '{')
	{
		st.Push(*s);
	}
	else
	{
	    //右括号⽐左括号多,数量匹配问题
		if (st.Empty())
		{
			return false;
		}
		//栈⾥⾯取左括号
		char top = st.Top();
		st.Pop();
		// 顺序不匹配
		if ((*s == ']' && top != '[')
			|| (*s == '}' && top != '{')
			|| (*s == ')' && top != '('))
		{
			return false;
		}
	}
		++s;
}
	//栈为空,返回真,说明数量都匹配左括号多,右括号少匹配问题
		return st.Empty();
}
// ⽤之前C版本Stack实现
bool isValid(const char* s) {
	ST st;
	STInit(&st);
       while (*s)
    {
	    //左括号⼊栈

	    if (*s == '(' || *s == '[' || *s == '{')
	    {
		    STPush(&st, *s);
	    }
	    else //右括号取栈顶左括号尝试匹配
        {
		    if (STEmpty(&st))
		    {
			    STDestroy(&st);
			    return false;
		    }
		    char top = STTop(&st);
		    STPop(&st);
		    //不匹配

		    if ((top == '(' && *s != ')')
		    || (top == '{' && *s != '}')
		    || (top == '[' && *s != ']'))
		    {
		    	STDestroy(&st);
		    	return false;
		    }
	    }
		++s;
    }
    //栈不为空,说明左括号⽐右括号多,数量不匹配
    bool ret = STEmpty(&st);
    STDestroy(&st);
    return ret;
}
int main()
{
	cout << isValid("[()][]") << endl;
	cout << isValid("[(])[]") << endl;
	return 0;
}

4. 拷贝构造函数

如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数 也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。

拷贝构造的特点:

  1. 拷贝构造函数是构造函数的⼀个重载。
  2. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
  3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
  4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成 员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
  5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型 Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现 MyQueue的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就 需要显示写拷贝构造,否则就不需要。
  6. 传值返回会产生一个临时对象调用拷贝构造,传引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用 引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。

日期类拷贝构造: 

传值拷贝会形参无穷递归: 

使用指针实现拷贝: 

拷贝初始化的两种写法:

 

 

 

5. 赋值运算符重载

5.1  运算符重载

  • 当运算符被用于类类型的对象时,C++允许我们通过运算符重载的形式指定新的含义,C++规 定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
  • 运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
  • 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
  • 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数比运算对象少一个。
  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
  • 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
  • .*   ::   sizeof   ?:   .  注意以上5个运算符不能重载。
  • 重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x, int y)
  • ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意 义,但是重载operator*就没有意义。
  • 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
  • 重载载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性,重载为全局函数把ostream/istream放在第一个形参位置就可以了,第二个形参位置当类类型对象。

重载日期是否小于日期:

 

重载日期是否相等: 

.* :: sizeof ?: .这5个运算符不能重载: 

 重载++运算符:

5.2 赋值运算符重载

赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷贝赋值,这⾥要注意跟 拷贝构造区分,拷贝构造⽤于⼀个对象拷贝初始化给另⼀个要创建的对象。

赋值运算符重载的特点:

  1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝。
  2. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了⽀持连续赋值场景。
  3. 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
  4. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就 可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部 主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载, 也不需要我们显示实现MyQueue的赋值运算符重载。这里还有⼀个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。

拷贝构造和赋值重载的区别:

int main()
{
	Date d1(2024, 8, 12);

	//拷贝初始化 - 调用拷贝构造
	Date d2(d1);

	//这个是拷贝构造 -> 一个已经存在的对象初始化给另一个要创建的对象
	Date d4 = d1;

	Date d3(2024, 8, 12);

	//赋值运算符重载 -> 用于完成两个已经存在的对象之间的拷贝赋值
	d1 = d3;
	return 0;
}
/*
拷贝构造:拿一个已经存在的对象取拷贝另一个现在要创建初始化的对象
赋值运算符重载:两个已经存在的对象,把一个已经存在的对象拷贝给另一个已经
存在的对象
*/

赋值运算符重载的实现:

日期合法性的检查:

5.3 日期类实现

C Plus Plus: C++program code - Gitee.comicon-default.png?t=N7T8https://gitee.com/Axurea/c-plus-plus/tree/master/2024_8_12_DateClass

6. 取地址运算符重载

6.1 const成员函数

  • 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
  • const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。const修饰Date类的Print成员函数,Print隐含的this指针由Date* const this变为const Date* const this。

#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 Print(const Date* const this) const
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
const
int main()
{
	// 这里非const对象也可以调用const成员函数是一种权限的缩⼩
	Date d1(2024, 7, 5);
	d1.Print();
	const Date d2(2024, 8, 5);
	d2.Print();
	return 0;
}

6.2 取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。

class Date
{
public:
	Date* operator&()
	{
		return this;
		// return nullptr;
	}
	const Date* operator&()const
	{
		return this;
		// return nullptr;
	}
private:
	int _year;
	int _month;
	int _day;

};

标签:函数,对象,运算符,析构,重载,拷贝,构造函数
From: https://blog.csdn.net/m0_74271757/article/details/141002467

相关文章

  • C++类和对象(中)
    前言:我们学习了类和对象的上部分,对类和对象有了一些认识,接下来了解类和对象的中间部分,构造函数,析构函数,拷贝构造,赋值构造这部分也比较重要,我们需要牢牢掌握,一起加油吧!1.类的默认成员函数默认成员函数就是我们不用写系统自动生成的函数,我们不写的情况下编译器会默认生成6......
  • 22. 面向对象之多态
    1.多态1.1概念多态指的是一类事物有多种形态比如动物有多种形态:人、猴、鸭1.2代码示例fromabcimportABC,abstractmethod#对于程序来说,定义一个基类可以有多个子类classAnimal(ABC):@abstractmethoddefrun(self):pass@abstractmethod......
  • JAVA面向对象|(一)Java接口 Interface
    目录一、概述(一)概念(二)特点(三)接口VS类二、使用 (一)类实现接口(二)接口间的多继承 演示(三)标记接口 1.标记接口 定义2.标记接口 作用3.标记接口 应用参考文章:Java接口_w3cschool一、概述(一)概念        Java接口是一种抽象类型,是一系列方法的声明......
  • JAVA面向对象思想
    封装一、概念封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。封装最主要的功能在于我们能修改自己的实现代......
  • JS DOM 对象的节点操作
    目录一、什么是加载时间onload二、各种节点的获取方法1、元素节点的获取(1)通过标签名获取:       document.getElementsByTagName('标签名')(2)通过id获取         document.getElementById('id的名称')2、文本节点的获取举个栗子3、兄弟节点(1)nextSiblin......
  • 遍历数组对象
    前提条件:数组对象中的id唯一1consttree=[2{id:1},3{id:2,4children:[5{id:3,6children:[7{id:4,8children:[9{i......
  • 高级java每日一道面试题-2024年8月15日-设计模式篇-设计模式与面向对象原则的关系是什
    如果有遗漏,评论区告诉我进行补充面试官:设计模式与面向对象原则的关系是什么?我回答:在设计模式与面向对象原则的关系中,两者紧密相连且相互促进。面向对象的原则为设计模式的形成提供了理论基础和指导思想,而设计模式则是这些原则在特定问题域中的具体实践和实现方式。下......
  • 面向对象设计原则
    面向对象设计原则总结单一职责原则(SRP)不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。一句话总结:不能为图代码量少......
  • Python实现CNN(卷积神经网络)对象检测算法
    目录1.引言2.对象检测的基本原理2.1对象检测的目标2.2常见对象检测方法2.2.1基于滑动窗口的传统方法2.2.2基于区域提议的现代方法2.2.3单阶段检测器2.3本次实现的检测方法3.代码实现3.1环境准备3.2数据准备与预处理3.3构建CNN模型3......
  • JSONUtil、JsonArray应用 (全网最全面的解析方式汇总) - 调用第三方接口后,获取的结果
    背景:近期开发的内容涉及到了我们平台对其他平台提供接口的调用,然后也涉及到接口提供方的验签等操作;还有我们的加签操作等。今天记录一下调用三方接口后返回的接口如何解析;怎么拿到自己想要的东西。其实调用三方接口分为几步1、采用哪种方式调用三方接口,这种依赖于第三方......