类和对象(2)(六个默认成员函数)

 个人主页:Jason_from_China-CSDN博客

所属栏目:C++系统性学习_Jason_from_China的博客-CSDN博客

所属栏目:C++知识点的补充_Jason_from_China的博客-CSDN博客

类的默认成员函数

概念概述

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

包括

构造函数

概念概述

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

构造函数的特点:

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

注意:

C++ 把类型分成内置类型 (基本类型) 和自定义类型。内置类型就是语言提供的原生数据类型,如:int/char/double/ 指针等,自定义类型就是我们使用 class/struct 等关键字自己定义的类型。

构造函数的实现

不带参数构造函数

//.h

///默认构造函数的使用
class MyClass
{
public:
	//默认构造函数
	MyClass()//类名相同,没有参数,叫做无参数构造函数
	{
		_year = 1999;
		_month = 12;
		_day = 1;
		cout << "默认构造函数的自动调用:" ;
		cout << _year << "/" << _month << "/" << _day << endl;
	}

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

//.cpp
int main()
{
	MyClass d1;
	return 0;
}

此时我们发现这里在初始化的时候自动调用无参数构造函数(也就是默认构造函数),当然这里初始化成功

带参数构造函数

//.h
class MyClass1
{
public:
	MyClass1(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "带参数构造函数的调用:";
		cout << _year << "/" << _month << "/" << _day << endl << endl;
	}

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

//.cpp
int main()
{
	//不带参数构造函数
	MyClass d1;

	//带参数构造函数
	MyClass1 d2(2024, 9, 14);//带参构造函数这里是需要传递一个参数

	return 0;
}

半缺省构造函数

//.h
//半缺省构造函数(从右往左进行赋值)
class MyClass2
{
public:
	MyClass2(int year, int month, int day = 28)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "半缺省构造函数(从右往左进行赋值):";
		cout << _year << "/" << _month << "/" << _day << endl << endl;
	}

private://私有变量,只能类里面的函数进行访问
	int _year;
	int _month;
	int _day;
};

//.cpp
int main()
{
	//不带参数构造函数
	MyClass d1;
	
	//带参数构造函数
	MyClass1 d2(2024, 9, 14);

	//半缺省构造函数(从右往左进行赋值),赋值的时候,从左往右哦,就是为了和构造函数的缺省参数进行分开
	MyClass2 d3(2000, 2);
	return 0;
}

半缺省构造参数(从右往左进行赋值),赋值的时候,从左往右哦,就是为了和构造函数的缺省参数进行分开

全缺省构造函数

//全缺省构造函数(从右往左进行赋值)
class MyClass2
{
public:
	MyClass2(int year, int month, int day = 28)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "半缺省构造参数(从右往左进行赋值):";
		cout << _year << "/" << _month << "/" << _day << endl << endl;
	}

private://私有变量,只能类里面的函数进行访问
	int _year;
	int _month;
	int _day;
};

int main()
{
	//不带参数构造函数
	MyClass d1;
	
	//带参数构造函数
	MyClass1 d2(2024, 9, 14);

	//半缺省构造函数(从右往左进行赋值)
	MyClass2 d3(2000, 2);

	//全缺省构造函数
	MyClass3 d4;
	return 0;
}

默认构造函数

无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造。

总结:

简单的说就是,在创建对象初始化的时候,会自动调用的函数,带参数构造函数不是默认构造函数,是需要调用的时候传参的。默认构造函数在调用创建对象的时候是不需要传参的。上面我们的代码很清晰了。

构造函数注意事项

对于自定义类型来说,构造函数往往是需要自己实现的。因为自定义类型的初始化可能较为复杂,需要根据特定的逻辑来进行成员变量的初始化等操作。例如,一个包含多个成员变量且有特定初始化需求的自定义类,通常需要定义构造函数来确保对象在创建时被正确初始化。

 

对于内置类型来说是不需要的。内置类型如 int、char、double 等,在创建对象时如果不进行显式初始化,它们会被自动赋予默认值(对于数值类型通常为 0,对于指针类型通常为 nullptr 等)。当然,如果需要特定的初始值,也可以在定义变量时进行显式初始化,但一般情况下不需要专门为内置类型定义构造函数。

析构函数(~)

概念概述

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

析构函数的特点:

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

什么时候需要析构函数

当有资源申请的时候,就需要进行析构函数,析构函数是和构造函数一样的,内置类型不需要析构,自定义类型需要进行析构,也需要自己写析构函数,但是调用的时候是会自己调用的。

像如下图:此时开辟了空间,产生了资源的使用,所以就需要析构函数,如果在main函数调用结束的时候没有进行销毁资源,就会导致资源的泄露,这是很麻烦的。所以此时我们需要进行析构函数。

什么时候不需要析构函数 

1,没有资源申请的时候

2,内置类型不做处理

3,两个栈实现队列的时候,内置类型不做处理,自定义类型会调用栈的析构(可以在队列的析构里面写一行,cout

但是这里有一个特殊情况,就是两个栈实现队列下面有其他资源

这里的指针需要析构,因为指向一个空间,需要把这个资源释放了

析构函数的使用

书写,其实就是C语言的函数的销毁

调用,main函数在结束的时候,会自动调用析构函数,因为这里有资源的申请

多个对象的时候,析构顺序

当有资源需要进行析构的时候,你没有写内容,但是定义了析构函数,也是会调用的,只是没有析构成功(析构函数的自动调用:当一个对象的生命周期结束时,如果该对象的类定义了析构函数,那么析构函数会被自动调用。这是由C++运行时环境保证的。如果没有定义析构函数,那么不会有任何特别的函数被调用来处理对象的销毁。)

规定来说哦,后定义的先析构

所以这里是st2先析构,st1后析构

析构函数调用的时候-注意事项

  1. 规定来说哦,后定义的先析构
  2. 类的析构函数调用一般是按照调用的相反顺序进行调用,但是需要注意static对象的存在,因为static改变了对象的生存作用域,需要等待程序结束的时候才会释放对象
  3. 全局变量先于局部对象进行构造。

    (在 C++ 中,在程序启动时,全局变量的构造在进入main函数之前完成

    也就是说,先构造全局变量,然后程序的执行流程进入main函数,再构造main函数中的局部变量等。)

  4. 局部对象按照出现的顺序进行构造,无论是否为static。(简单的说,也就是在main,调用Func函数,Func函数会按照后定义的先析构顺序进行析构)
  5. 题型精讲:

    全局变量优先局部进行构造,所以往往最后进行析构。(C)
    局部变量先构造的后析构。(A B)
    被静态修饰的变量因为static改变了对象的生存作用域,需要等待程序结束的时候才会释放对象(D)

拷贝构造函数

概念概述

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

同时C++规定了,这里是规定,传值传参必须调用拷贝构造

拷贝构造函数的特点

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

拷贝函数的使用

拷贝构造的实现其实是很简单,这里先举出实例讲解,之后会进行解释,主要就是传参的时候,如果传递的是传值传参,那么需要用引用进行接收,不然会导致死循环从而导致崩溃

下面的的代码里面首先我们实现声明和实现的分离

//.h
//拷贝构造函数的实现
class Date
{
public:
	//构造函数的实现
	Date();
	//拷贝构造的实现
	Date(Date& d);

	//打印函数的实现
	void print();
private:
	int _year;
	int _month;
	int _day;
};

//构造函数的实现
Date::Date()
{
	_year = 2000;
	_month = 2;
	_day = 28;
	cout << "构造函数的实现";
	cout << _year << "/" << _month << "/" << _day << endl << endl;
}

//拷贝构造的实现
Date::Date(Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

//打印函数的实现
void Date::print()
{
	cout << _year << "/" << _month << "/" << _day << endl << endl;
}

//.cpp
int main()
{
	//构造函数的实现
	Date d1;
	
	//拷贝构造的实现:1
	Date d2(d1);
	d2.print();

	//拷贝构造的实现:2
	Date d3 = d2;
	d3.print();
	return 0;
}

运行代码,可以清楚的看见,两个拷贝构造都实现成功,这两种形式,看看你喜欢哪一种拷贝构造实现形式

拷贝构造的过程

//.h

//拷贝构造函数的实现
class Date
{
public:
	//构造函数的实现
	Date();
	//拷贝构造的实现
	Date(Date& d);

	//打印函数的实现
	void print();
private:
	int _year;
	int _month;
	int _day;
};

//构造函数的实现
Date::Date()
{
	_year = 2000;
	_month = 2;
	_day = 28;
	cout << "构造函数的实现";
	cout << _year << "/" << _month << "/" << _day << endl << endl;
}

//拷贝构造的实现
Date::Date(Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

//打印函数的实现
void Date::print()
{
	cout << _year << "/" << _month << "/" << _day << endl << endl;
}


//.cpp
void Func(Date& d)
{
	cout << "拷贝构造函数的调用" << endl;
	d.print();
}
int main()
{
	//构造函数的实现
	Date d1;
	
	//拷贝构造的实现:1
	Date d2(d1);
	d2.print();

	//拷贝构造的实现:2
	Date d3 = d2;
	d3.print();


	//传值传参的调用
	Func(d3);
	return 0;
}

这里我们观察拷贝构造是如何完成的

第一步,类类型的,传值传参

第二步,返回函数

全过程图解

在这个例子中,void Func(Date& d)中的&可以写也可以不写,但加上引用可以避免不必要的拷贝构造函数调用,提高效率。

如果写成void Func(Date d),在调用Func函数时,会调用拷贝构造函数将实参对象拷贝一份传递给函数参数d,可能会有一定的性能开销,尤其是当Date类比较复杂时。

如果写成void Func(Date& d),则是通过引用传递参数,不会调用拷贝构造函数,只是传递了对象的引用,效率更高。综上所述,从性能角度考虑,建议加上引用符号&。

所以可以进行与优化,我们函数依旧采取引用接收,减少拷贝

拷贝构造函数的原理解释

首先我们需要明确一点就是,C++在传值传参的时候会调用拷贝构造

什么是传值传参,顾名思义肯定是传递数值。

如果我们传值传参,但是接收的话不采取引用接收,就会导致一直情况

拷贝构造和指针

class MyClass
{
public:
	MyClass();
	MyClass(MyClass* M);
	//打印函数的实现
	void print()
	{
		cout << _year << "/" << _month << "/" << _day << endl << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
//构造函数
MyClass::MyClass()
{
	_year = 1999;
	_month = 2;
	_day = 28;
	cout << "MyClass构造函数的实现";
	cout << _year << "/" << _month << "/" << _day << endl << endl;
}
//拷贝构造,和指针的联合实现
MyClass::MyClass(MyClass* M)
{
	_year = M->_year;
	_month = M->_month;
	_day = M->_day;
}

int main()
{
	//构造函数的实现
	Date d1;
	
	//拷贝构造的实现:1
	Date d2(d1);
	d2.print();

	//拷贝构造的实现:2
	Date d3 = d2;
	d3.print();


	//传值传参的调用
	Func(d3);

	//拷贝构造和指针的联合实现
	MyClass M1;
	MyClass M2 = M1;
	M2.print();
	return 0;
}

拷贝构造函数需要加上conts和不加const的区别

这里是有一点难度的

接下来 我们给出一个代码,但是这个代码是一个典型的错误代码:此时我们发现是报错的

这里解决报错的原因需要在拷贝构造函数上面加上const,因为这里存在权限放大的行为,之前我们就说过权限可以缩小,但是不能放大。

这里就产生了权限放大的问题,所以我们需要解决权限放大的问题

接下来我们上正确的代码

class MyClass
{
public:
	MyClass(MyClass* M);
	MyClass(int year, int month, int day);
	MyClass(const MyClass& M);
	//打印函数的实现
	void print()
	{
		cout << _year << "/" << _month << "/" << _day << endl << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
//构造函数
MyClass::MyClass(int year = 1999, int month = 2, int day = 28)
{
	_year = year;
	_month = month;
	_day = day;
	cout << "MyClass构造函数的实现";
	cout << _year << "/" << _month << "/" << _day << endl << endl;
}
//拷贝构造,和指针的联合实现,这里就会导致这里只是一个普通的构造函数
//MyClass::MyClass(MyClass* M)
//{
//	_year = M->_year;
//	_month = M->_month;
//	_day = M->_day;
//}
//拷贝构造的实现
MyClass::MyClass(const MyClass& M)
{
	_year = M._year;
	_month = M._month;
	_day = M._day;
}


//拷贝构造函数的实现
class Date
{
public:
	//构造函数的实现
	Date();
	//拷贝构造的实现
	Date(const Date& d);

	//打印函数的实现
	void print();
private:
	int _year;
	int _month;
	int _day;
};

//构造函数的实现
Date::Date()
{
	_year = 2000;
	_month = 2;
	_day = 28;
	cout << "构造函数的实现";
	cout << _year << "/" << _month << "/" << _day << endl << endl;
}

//拷贝构造的实现
//Date::Date(Date* const this, Date& d)
Date::Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

//打印函数的实现
void Date::print()
{
	cout << _year << "/" << _month << "/" << _day << endl << endl;
}

void Func(Date& d)
{
	cout << "拷贝构造函数的调用" << endl;
	d.print();
}

Date F()
{
	Date d1;
	return d1;
}

MyClass f()
{
	MyClass M1(12, 12, 12);
	return M1;
}
int main()
{
	//构造函数的实现
	Date d1;
	
	//拷贝构造的实现:1
	Date d2(d1);
	d2.print();

	//拷贝构造的实现:2
	Date d3 = d2;
	d3.print();


	//传值传参的调用
	Func(d3);

	拷贝构造和指针的联合实现
	MyClass M1(2000, 1, 1);//初始化2000
	MyClass M2;//默认初始化 1999
	MyClass M3 = M1;
	//M2.print();
	M3.print();

	//const在拷贝构造函数里面的使用
	cout << "拷贝构造const的实现"<<endl;
	MyClass M4 = f();
	Date d4 = F();

	return 0;
}

运算符重载

概念概述

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

一元二元三元运算符:

一、一元运算符

 
  1. 算术运算符:
    • 正号(+):例如+a,一般对数值类型起作用,通常意义不大,因为数值默认就是正的。
    • 负号(-):如-b,用于取负值。
  2. 逻辑运算符:
    • 逻辑非(!):例如!c,对布尔值进行取反操作,如果操作数为 true,则结果为 false;如果操作数为 false,则结果为 true
  3. 自增自减运算符:
    • 前置自增(++a):先将变量的值加 1,然后再使用变量的值。
    • 后置自增(a++):先使用变量的值,然后再将变量的值加 1。
    • 前置自减(--a):先将变量的值减 1,然后再使用变量的值。
    • 后置自减(a--):先使用变量的值,然后再将变量的值减 1。
  4. 地址运算符:
    • 取地址符(&):例如&d,用于获取变量的内存地址。
  5. 间接寻址运算符:
    • 解引用符(*):如果有一个指针变量 p*p 用于访问指针所指向的对象。
 

二、二元运算符

 
  1. 算术运算符:
    • 加法(+):如 a + b,用于数值相加或字符串连接(对于 C++ 中的 std::string 对象)。
    • 减法(-):如 a - b,用于数值相减。
    • 乘法(*):如 a * b,用于数值相乘。
    • 除法(/):如 a / b,用于数值相除。
    • 取余(%):如 a % b,用于求整数除法的余数。
  2. 位运算符:
    • 按位与(&):如 a & b,对两个操作数的每一位进行与操作。
    • 按位或(|):如 a | b,对两个操作数的每一位进行或操作。
    • 按位异或(^):如 a ^ b,对两个操作数的每一位进行异或操作。
    • 左移(<<):如 a << b,将 a 的二进制表示向左移动 b 位。
    • 右移(>>):如 a >> b,将 a 的二进制表示向右移动 b 位。
  3. 关系运算符:
    • 等于(==):如 a == b,判断两个操作数是否相等。
    • 不等于(!=):如 a!= b,判断两个操作数是否不相等。
    • 大于(>):如 a > b,判断 a 是否大于 b
    • 小于(<):如 a < b,判断 a 是否小于 b
    • 大于等于(>=):如 a >= b,判断 a 是否大于等于 b
    • 小于等于(<=):如 a <= b,判断 a 是否小于等于 b
  4. 逻辑运算符:
    • 逻辑与(&&):如 a && b,当且仅当两个操作数都为 true 时,结果为 true
    • 逻辑或(||):如 a || b,当至少一个操作数为 true 时,结果为 true
  5. 赋值运算符:
    • 简单赋值(=):如 a = b,将 b 的值赋给 a
    • 复合赋值(如 +=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=):例如 a += b 相当于 a = a + b
 

三、三元运算符(条件运算符)

 

条件运算符(?:),例如 a? b : c,如果 a 为 true,则结果为 b;如果 a 为 false,则结果为 c

内置类型和自定义类型

内置类型(简单类型):

一般情况下系统对于内置类型,直接就有对应的指令,会自动识别进行比较,核心就是比较,这里就是i和j直接进行比较 存到ret里面返回

自定义类型(按照你自己的比较方式,按照自己的需求,进行比较):

1,自定义类型不能直接转换成指令,就不能直接进行比较。尤其是像日期类里面的,年 月 日的比较,更是无法直接进行比较,你是需要返回更大的日期,还是更小的,你是需要加减,多少进位一次,这些都不是编译器决定的,是右你自己决定的。编译器不能直接进行识别。

2,内置类型比较简单,可以直接转换识别,但是自定义类型就不是

3,为了可以自己对自定义类型进行比较,语法结构就是:operator ➕运算符

自定义类型运算符重载的使用

原因:

日期类的比较日期,这里会存在一个问题,就是类外面无法访问私有变量,所以这里我们有三种解决办法

  • get函数
  • 重载为类的成员函数
  • 友元函数

这里我们以重载为成员函数为例子进行举例,至于经常用到的友元函数,类的第三章我们会讲到:

这里还有一个关键点,我们直接上代码(这里发现报错)

  

原因:可以放到类里面,重载为成员函数,但是直接拷贝放到类里面,会导致报错,因为隐藏了this指针,隐藏的this指针会指向第一个参数

  

解决:

但是此时调用,从全局调用,变成了成员函数的调用,调用方式发生了改变

此时d1传给了this,d2传给了d

试验一下 发现没有问题

//.h文件
//运算符重载,比较大小的实现,不在类里面实现
class Date
{
public:
	Date(int year = 1000, int month = 1, int day = 1);//声明给,定义不给
	Date(Date& d);
	void print();
	bool operator<(const Date& d);

	//比较日期大小(重载为成员函数)
private:
	int _year;
	int _month;
	int _day;
};

//.cpp实现文件
#include"类和对象的通篇实现.h"
//构造函数的实现(全缺省构造函数的实现)
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
//拷贝构造的实现
Date::Date(Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
//打印函数的实现
void Date::print()
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

//日期类的比较日期的大小,this-> < d.year 此时返回
bool Date::operator<(const Date& d)
{
	//比较年
	if (this->_year < d._year)
	{
		return true;
	}
	else
	{
		if (this->_year == d._year && this->_month < d._month)//比较月
		{
			return true;
		}
		else if (this->_year == d._year && this->_month < d._month && this->_day < d._day)//比较日
		{
			return true;
		}
	}
	return false;
}



//.cpp测试文件
//运算符重载,比较大小
int main()
{
	//构造和拷贝构造函数的使用
	Date d1(1000, 2, 1);
	d1.print();
	Date d2 = d1;
	d2.print();

	//比较年月日
	Date d3(999, 1, 1);
	bool ret1 = d1.operator<(d3);
	cout << ret1 << endl;

	Date d4(1100, 1, 1);
	bool ret2 = d1.operator<(d4);
	cout << ret2 << endl;
	return 0;
}

重载成员函数至少有一个类类型的形参

前置++和后置++的区分

在C++里面,d++和++d,在汇编层,其实都是一样的,都是,operater++,所以是无法是分辨的

所以我们需要在书写的时候做出分辨

这里重载的函数,函数名是一样的

前置++,和后置++,运算符的复用

前置++,直接进行复用

后置++

1,保存之前的数值(拷贝下来)

2,然后进行++

3,最后返回++之前的数值

总结

注意,

在 C++ 中,后置 ++ 运算符重载函数的参数必须是 int 类型,这是 C++ 语言的规定,不能使用 double 或其他类型。

这样规定的原因主要是为了保持语言的一致性和可识别性。编译器通过参数为 int 来区分前置 ++ 和后置 ++ 的重载版本。如果允许使用其他类型,会使编译器难以确定到底是哪种 ++ 操作,增加了语言的复杂性和不确定性。

传递 int 类型参数通常只是作为一个占位符,实际在函数实现中一般不会用到这个参数的值,它的存在仅仅是为了满足语法要求以区分前置和后置自增操作。所以一般情况下,都是默认传递 int 类型参数,而不是 0 或者 1 这样的特定值,并且也不能传递 double 等其他类型。

代码的实现:

这里声明一下,上面图片的讲解的代码,是实现+=的,所以可以直接使用this+=1,这里并没有实现+=的函数重载,所以这里我们就会跑到对象里面实现day的+1

//.h文件
//运算符重载,比较大小的实现,不在类里面实现
class Date
{
public:
	Date(int year = 1000, int month = 1, int day = 1);//声明给,定义不给
	Date(Date& d);
	void print();
	bool operator<(const Date& d);
	Date& operator++();
	Date& operator++(int);
	//比较日期大小(重载为成员函数)
private:
	int _year;
	int _month;
	int _day;
};


//.cpp实现文件
#include"类和对象的通篇实现.h"
//构造函数的实现(全缺省构造函数的实现)
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
//拷贝构造的实现
Date::Date(Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
//打印函数的实现
void Date::print()
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

//日期类的比较日期的大小,this-> < d.year 此时返回
bool Date::operator<(const Date& d)
{
	//比较年
	if (this->_year < d._year)
	{
		return true;
	}
	else
	{
		if (this->_year == d._year && this->_month < d._month)//比较月
		{
			return true;
		}
		else if (this->_year == d._year && this->_month < d._month && this->_day < d._day)//比较日
		{
			return true;
		}
	}
	return false;
}

//前置++
Date& Date::operator++()
{
	this->_day += 1;
	return *this;
}
//后置++
Date& Date::operator++(int)
{
	Date tmp = *this;
	this->_day += 1;
	return tmp;
}


//.cpp测试文件
//运算符重载,比较大小
int main()
{
	//构造和拷贝构造函数的使用
	Date d1(1000, 2, 1);
	d1.print();
	Date d2 = d1;
	d2.print();

	//比较年月日
	Date d3(999, 1, 1);
	bool ret1 = d1.operator<(d3);
	cout << ret1 << endl;

	Date d4(1100, 1, 1);
	bool ret2 = d1.operator<(d4);
	cout << ret2 << endl;

	//前置++和后置++
	cout << "前置++和后置++" << endl;
	d4.print();//1100 ,1,1
	
	d4.operator++();
	d4.print();

	Date ret = d4.operator++(1);
	ret.print();
	d4.print();

	return 0;
}

赋值运算符重载

概念概述

赋值运算符重载的特点:

  1. 成员函数:赋值运算符重载必须定义为类的成员函数。
  2. 参数:建议将参数声明为const类型的类引用,以避免不必要的拷贝。
  3. 返回值:应有返回值,且建议为当前类类型的引用,这样可以支持连续赋值操作,并提高效率。

编译器自动生成的赋值运算符:

  • 如果没有显式实现赋值运算符重载,编译器会提供一个默认实现。
  • 默认赋值运算符对内置类型成员变量执行值拷贝或浅拷贝。
  • 对自定义类型成员变量,会调用其赋值运算符重载函数。

特定情况下的赋值运算符重载:

  • 对于像Date这样只有内置类型成员的类,编译器自动生成的赋值运算符通常足够使用。
  • 对于像Stack这样包含指向资源的成员的类,需要自定义赋值运算符以实现深拷贝。
  • 对于像MyQueue这样包含自定义类型成员的类,如果这些成员的赋值运算符已经正确实现,通常不需要为MyQueue显式实现赋值运算符重载。

额外技巧:

  • 如果一个类显式实现了析构函数并释放资源,通常也需要显式实现赋值运算符重载,以确保资源被正确管理。

赋值运算符重载的使用以及注意事项

1,有返回值,建议写成const类类型的引用。因为C++规定类类型的传值传参,会调用拷贝构造,传递引用会减少拷贝(提高效率)

2,连续赋值的返回值

d3=d1;

返回值是d3,所以我们写代码的时候,要注意返回值,很可能是this

第一次赋值

第二次赋值

3,当返回的节点是d2,如何实现返回d2(用this,this本身指向的就是返回值)(这里注意:有返回值就支持连续赋值,但是返回的时候,需要注意)

注意:这里返回是可以用引用返回的,因为赋值运算符重载是两个已有的对象。出去作用域是依旧存在的(这里代码是传值返回,会增加拷贝)

4,这里还有一个问题,就是d1可以复制給給d1,所以为了防止自己給自己赋值 ,会进行判断

代码实现

//.h文件
//运算符重载,比较大小的实现,不在类里面实现
class Date
{
public:
	Date(int year = 1000, int month = 1, int day = 1);//声明给,定义不给
	Date(Date& d);
	void print();

	bool operator<(const Date& d);

	//比较日期大小(重载为成员函数)
	Date& operator++();
	Date& operator++(int);

	//赋值运算符重载(这里是可以加上const的,因为这里改变的是this不是d)
	Date& operator=(const Date& d);

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


//.cpp实现文件
#include"类和对象的通篇实现.h"
//构造函数的实现(全缺省构造函数的实现)
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
//拷贝构造的实现
Date::Date(Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
//打印函数的实现
void Date::print()
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

//日期类的比较日期的大小,this-> < d.year 此时返回
bool Date::operator<(const Date& d)
{
	//比较年
	if (this->_year < d._year)
	{
		return true;
	}
	else
	{
		if (this->_year == d._year && this->_month < d._month)//比较月
		{
			return true;
		}
		else if (this->_year == d._year && this->_month < d._month && this->_day < d._day)//比较日
		{
			return true;
		}
	}
	return false;
}

//前置++
Date& Date::operator++()
{
	this->_day += 1;
	return *this;
}
//后置++
Date& Date::operator++(int)
{
	Date tmp = *this;
	this->_day += 1;
	return tmp;
}

//赋值运算符重载(这里是可以加上const的,因为这里改变的是this不是d)
Date& Date::operator=(const Date& d)
{
	//地址不一样才进行赋值
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;
}





//.cpp测试文件
//运算符重载,比较大小
int main()
{
	//构造和拷贝构造函数的使用
	Date d1(1000, 2, 1);
	d1.print();
	Date d2 = d1;
	d2.print();

	//比较年月日
	Date d3(999, 1, 1);
	bool ret1 = d1.operator<(d3);
	cout << ret1 << endl;

	Date d4(1100, 1, 1);
	bool ret2 = d1.operator<(d4);
	cout << ret2 << endl;

	//前置++和后置++
	cout << "前置++和后置++" << endl;
	d4.print();//1100 ,1,1
	
	d4.operator++();
	d4.print();

	Date ret = d4.operator++(1);
	ret.print();
	d4.print();


	//赋值运算符重载 
	cout << "赋值运算符重载" << endl;
	Date d5(1, 1, 1);
	d5.print();
	d5 = d4;//这样写也是对的
	//d5.operator=(d4);//这样写也是对的
	d5.print();
	 
	return 0;
}

赋值运算符重载什么时候需要自己实现

注意:默认的赋值运算符重载也会完成浅拷贝(有资源,就需要自己完成赋值运算符重载)

1,内置类型不指向什么资源,不需要自己实现,编译器自动生成的就可以实现

2,栈和深拷贝,都需要自己写,和拷贝构造非常相似

3,如果写了析构函数,那么赋值运算符重载就需要自己写

几个函数之间进行对比

1、构造一般都需要自己写,自己传参定义初始化

2、析构,构造时有资源申请(如malloc或者fopen)等,就需要显示写析构函数

3、拷贝构造和赋值重载,显示写了析构,内部管理资源,就需要显示实现深拷贝

const成员函数

const引用常量  

使用规则

  1. 引用常量对象:可以引用一个常量对象,但必须使用常量引用(const引用)。这是因为常量引用保证了不会通过引用来修改被引用的对象。

  2. 常量引用的灵活性:常量引用不仅可以引用常量对象,也可以引用普通(非常量)对象。这是因为引用的权限可以缩小(即常量引用可以绑定到非常量对象),但不能放大(即非常量引用不能绑定到常量对象)。

  3. 引用临时对象:在某些表达式中,如 int& rb = a * 3;int& rd = static_cast<int>(d);,表达式的结果可能被存储在临时对象中。这些临时对象是未命名的,由编译器创建用于存储表达式的结果。

  4. 临时对象的常性:C++标准规定,临时对象是常量,因此不能通过非常量引用来引用它们。只有常量引用可以绑定到临时对象。

  5. 权限放大问题:尝试通过非常量引用来引用临时对象会导致编译错误,因为这相当于权限放大,违反了C++的引用规则。

  6. 临时对象的定义:临时对象是编译器为了存储表达式的求值结果而创建的未命名对象。它们在表达式结束后通常会立即销毁,但通过常量引用可以延长其生命周期。

C++ const引用,权限放大问题

权限放大是问题,因为是不允许权限放大的

const会修饰的变量只能是可读的

#include<iostream>
#include<stdio.h>
int main()
{
	
	//不能进行权限放大,但是我们可以进行权限平移
	const int& a22 = a11;
	cout << "a22=" << a22 << endl << endl;
	cout << "&a11=" << &a11 << endl << "&a22=" << &a22 << endl << endl;//此时我们发现,地址依旧是一样的,因为这是一个别名的引用

	return 0;
}

C++ const引用,权限缩小

权限缩小不是问题,因为是允许权限缩小的

#include<iostream>
#include<stdio.h>
int main()
{

	//权限的缩小
	int a33 = 10;
	++a33;
	cout << "a33=" << a33 << endl;
	const int& a44 = a33;//此时权限缩小
	//++a44;//此时报错,因为权限缩小之后,内容不可修改
	cout << "&a33=" << &a33 << endl << "&a44=" << &a44 << endl << endl;

	return 0;
}

单纯的拷贝不存在权限问题

这里就是一个单纯的拷贝,没有引用,不存在权限问题

 const可以给常量取别名

给常量取别名的意义

  1. 保护数据:通过使用 const 引用,可以确保函数不会意外修改传入的常量参数。
  2. 提高效率:对于大型的数据结构,使用 const 引用作为函数参数可以避免不必要的拷贝,提高程序的运行效率。
  3. 延长生命周期:对于临时对象,使用 const 引用可以延长其生命周期,使其在引用的生命周期内保持有效。
const int original = 10; // 定义一个常量

const int& ref = original; // 用const引用给常量取别名
const int& refToTemp = 100; // 正确:临时对象可以绑定到const引用
// int& nonConstRef = 100; // 错误:临时对象不能绑定到非const引用

const引用(类型转化导致产生临时变量)

在C++中,"类型转换产生临时变量" 指的是在某些表达式中,由于类型不匹配,编译器会创建一个临时对象来存储表达式的结果,以便能够进行后续的操作。这个临时对象通常是一个右值(r-value),它只能被绑定到 const 引用上。

注意(后面拷贝构造会进行讲解)

  • 拷贝也会产生临时对象
  • 传值返回,表达式的结果中间都会产生临时对象

C++ const修饰变量 

前言

const在星号左边 修饰的是指向的内容

const在星号的右边 修饰的是指针本身,但是实参

修饰指针的const不存在权限问题

只有指向内容的const才有权限问题

权限的缩小

#include<iostream>
using namespace std;
//const修饰变量
class Date
{
public:
	Date();
	//void _print(Date* const this)
	void _print() 
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date::Date()
{
	 _year = 1;
	 _month = 1;
	 _day = 1;
}


int main()
{
	Date d1;
	d1._print();
	return 0;
}

这里有一个经典的权限缩小的问题,图解:

类里面的函数的带有this指针的,但是这里的指针是不能修饰内容的,所以一般是指向修饰的指针,我们在函数调用之前这里对象是可以修改可以读写的,但是调用函数之后就产生了变化,从而产生了权限的缩小

权限的放大问题导致报错

代码

这里显示报错,解释:

这里的关键在于,这里创建对象的时候我们用const进行了修饰,也就是导致这里构造成功之后是无法进行更改的,但是我们调用printf函数的时候,这里修饰的不是内容,也就是内容还是可读可写的,所以会导致权限的放大,在编译器里面,权限的放大是不允许的,但是权限的缩小是可以的

解决办法:

#include<iostream>
using namespace std;
//const修饰变量
class Date
{
public:
	Date();
	//void _print(Date* const this)
	void _print() const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date::Date()
{
	 _year = 1;
	 _month = 1;
	 _day = 1;
}


int main()
{
	Date d1;
	d1._print();
	
	const Date d2;
	d2._print();

	return 0;
}

此时我们发现编译器就不报错了,我们在函数后面加上const

这里加上const的意思就是,等同于在指针左边加上const,内容是不允许修改了,也就是此时变成只能读不能写了,此时就是权限的平移

这里在日期类的实现里面体现的很深入

修饰指针的const不存在权限问题

const修饰的好处+局限性

这里需要注意一下,对于const的修饰我们应该遵守几个原则

1,能加上的尽量加上

2,需要进行修改的,不要加上const,比如输入输出流,-=和+=,这样自身需要改变的,不能加上const

C++ &运算符重载

概念概述:

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

图解

 代码讲解

#include<iostream>
using namespace std;
//&运算符的重载
class Date
{
public:
	Date();
	Date* operator&()
	{
		return nullptr;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date::Date()
{
	_year = 1;
	_month = 1;
	_day = 1;
}
int main()
{
	const Date d1;
	cout << &d1 << endl << endl;
	Date d2;
	cout << &d2 << endl;

	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值