C++好难(3):类和对象(中篇)

news2024/11/16 7:29:02

【本章目标】

  • 类的6个默认成员函数
  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 赋值运算符重载
  • const成员函数
  • 取地址及const取地址操作符重载

目录

【本章目标】

1.类的6个默认成员函数

2.构造函数

2.1概念

2.2构造函数的特性

特性一

特性二

特性三

特性四

特性五

特性六

特性七

2.3总结

3.析构函数

3.1概念:

3.2特性

特性一

特性二

特性三

特性四

特性五

4.拷贝构造函数

4.1概念

4.2拷贝构造的特性

特性一

特性二

特性三

特性四:

4.3 总结

5.赋值运算符重载

5.1运算符重载

5.2赋值运算符重载

5.3赋值运算符重载的一个特性

6. const 成员

6.1const成员函数

6.2思考

7.取地址及const取地址操作符重载

练习


1.类的6个默认成员函数

 空类里面不是什么都没有,而是会自动生成上面6个默认成员函数

主要:

这6个默认的成员函数是“缺省”的,你不写这6个函数,编译器就会自动生成,但是你如果写了某一个,那编译器就不会生成了

2.构造函数

2.1概念

对于以下Date类

class Date
{
public:
	void Init(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.Init(2023, 3, 1);
	d1.Print();

	Date d2;
	d2.Init(2023, 5, 1);
	d2.Print();
	return 0;
}

这个Date类可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

这时候就需要用到我们的构造函数

2.2构造函数的特性

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

特性一

(1)构造函数的函数名与类名相同

特性二

(2)构造函数无返回值,这里指的是返回值不用写,而不是void

特性三

(3)对象实例化时编译器自动调用对应的构造函数

当你用类创建一个对象时,编译器会自动调用该类的构造函数对新创建的变量进行初始化。

特性四

(4)构造函数可以重载

这就意味着我们可以写很多不同初始化的构造函数,当我们需要那种初始化,我们传递对应的参数即可

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(2023, 1, 1);//调用有参的构造函数
	d2.Print();

	return 0;
}

注意:在通过无参构造函数创建对象时对象后面不用加括号,否则就是函数声明

特性五

(5)默认构造函数

无参构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

class Date
{
public:
	//全缺省的构造函数
	Date(int year = 2023, int month = 5, 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 d1;
	d1.Print();

	Date d2(2023, 1, 1);
	d2.Print();
	return 0;
}

d1没有传参,会直接调用构造函数的缺省值,d2会进行赋值操作

特性六

(6)如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

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;
}

打印结果:

我们可以看到d1调用了编译器创建的默认构造函数,单初始化结构却是 -858993460,
而不是初始化为0

这时候就有人想说:这编译器生成的默认构造函数有个√8用?

这是因为编译器有一套自动生成的构造函数机制:

  • 编译器自动生成的构造函数对内置类型不做处理
  • 对自定义类型,编译器会再去调用他们自己的构造函数

内置类型就是编译器创建的基本类型,如int,char等等

自定义类型就是class、struct定义的类对象

如下代码:

class Week
{
public:
	Week()
	{
		cout << " Week()" << endl;
		_week = 1;
	}
private:
	int _week;
};

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
    // 内置类型
	int _year; // 年
	int _month; // 月
	int _day; // 日

	// 自定义类型
	Week w1;
};

int main()
{
	Date d1;
	d1.Print();

	return 0;
}

调用结果:

可以看到:

默认生成的构造函数堆内置类型成员变量不做处理,
对于自定义类型成员变量才会处理(去调用他们自己的默认构造函数)

特性七

(7)如果一个类中的成员全是自定义类型,我们就可以用默认生成的构造函数;
如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。

2.3总结

默认构造函数有三种:

  1. 我们不写,让编译器自动生成的
  2. 我们自己写的  无参  的构造函数
  3. 我们自己写的  全缺省  的构造函数

虽然我们在不写的情况下,编译器会自动生成构造函数,但是编译器自动生成的构造函数可能达不到我们想要的效果,

所以大多数情况下都需要我们自己写构造函数,并且最好是写全缺省的构造函数

3.析构函数

3.1概念:

与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

3.2特性

析构函数和构造函数一样,也是一个特殊的成员函数

我们都知道,当一个类对象销毁时,其中的局部表里也会是随着该对象的销毁而销毁

我们通常会写一个destroy函数来进行销毁操作

而析构函数就相当于destroy函数的作用

class Stack
{
public:
	// 构造函数
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}

	// 析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
	Stack st;
	return 0;
}

在数据结构中,我们实现栈时都会写一个Destroy函数再程序结束前销毁动态开辟的内存,
而如果我们在使用完后没有及时销毁,那么就可能导致内存泄露的问题

而析构函数的出现就是为了防止我们忘记销毁,对象实例化后,同构造函数一样,它不需要我们主动调用,它是在对象生命周期结束后自动调用,需要注意的是,析构函数没有参数所以不能重载。

特性一

(1)析构函数的名是在类名前面加上字符    ~  

class Date
{
public:
	// 构造函数
	Date()
	{

	}

	// 析构函数
	~Date()
	{

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

特性二

(2)析构函数无参数不能重载,无返回值

特性三

(3)对象声明周期结束时,编译器会自动第哦啊用析构函数

如下代码:

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

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	// 析构函数
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 5, 1);
	d1.Print();

	return 0;
}

编译结果:

可以看到我们没有调用~Date()这个函数,但是编译器是会默认调用这个析构函数的

特性四

(4)一个类有且只有一个析构函数。若未显示定义,系统会自动生成默认的析构函数

  • 编译器自动生成的析构函数对内置类型不做处理
  • 对于自定义类型,编译器会再去调用他们自己的析构函数

和构造函数一样

特性五

(5)先构造的后析构,后构造的先析构

析构函数的析构顺序和栈一样,先进后出

4.拷贝构造函数

4.1概念

在我们编写代码的过程中,免不了要对以一个类对象进行拷贝,生成一个新的和原来一样的对象

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用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;
	}

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

int main()
{
	Date d1(2023, 5, 1);
	Date d2(d1); // 用已存在的对象d1创建对象d2

	return 0;
}

我们这里对象 d2 就是对 对象 d1 的拷贝构造,通过调试我们就可以看到:

4.2拷贝构造的特性

特性一

(1)拷贝构造函数是构造函数的一个重载形式

因为拷贝构造函数的函数名也与类名相同

特性二

(2)拷贝构造函数的参数只有一个且必须用引用传参

传值传参会发生错误,无限递归的错误

传值传参为什么会发生无限递归呢?

 当进行传值传参的时候,我们要调用  拷贝构造函数  就需要先  传参,这时的传参是传值传参,在传参的过程中又需要进行对象的拷贝构造,如此循环发生无限递归。

这里用引用来解决了问题加const的原因是因为我们怕有些人写错顺序,导致原来的d被改变

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

特性三

(3)若未进行显示定义,系统生成默认拷贝构造函数

系统生成的默认拷贝构造函数对象,按内存存储字节完成拷贝,
这种拷贝我们叫做浅拷贝(或值拷贝)

如下代码所示:

class Date
{
public:
	// 构造函数
	Date(int year = 0, 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 d1(2023, 5, 1);
	Date d2(d1); // 用已存在的对象d1创建对象d2

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

运行结果:

在上述代码中,我们没有编写拷贝构造函数,但编译器通过自动生成的拷贝构造函数完成了d2对d1的拷贝

这就要谈谈编译器自动生成拷贝构造函数的机制:

  • 编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)
  •  对于自定义类型,编译器会再去调用他们自己的默认拷贝构造函数

特性四:

(4)编译器自动生成的拷贝构造函数不能实现 深拷贝 

什么是  浅拷贝  什么是  深拷贝

浅拷贝:对值进行拷贝,不会另外开空间

深拷贝:会另开辟一块空间,再将需要拷贝的值放入该空间中

显然,对于日期这样的类是没有必要进行深拷贝的,但对于一些复杂的数据结构,我们就不能再进行浅拷贝了,否则会出现指向同一区域的错误

如下代码:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_data = (int*)malloc(sizeof(int) * capacity);
		if (_data == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		free(_data);
		_data = nullptr;
	}
private:
	int* _data;
	int _top;
	int _capacity;
};

int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

我们用的是编译器自动生成的拷贝构造函数,进行调试我们可以看到:st2已经完成了对st1的拷贝

但是程序在运行的时候还是崩溃了

原因就是进行了浅拷贝的问题!

我们仔细看上述调试结构,会发现st1和st2指向的地址是同一块地址,这就说明再进行拷贝的时候,st2并没有开辟新的空间

这时我们不管对他们两个哪一个做出该改变,都会影响到另一个

如果在我们自己定义的析构函数是正确的情况下,当程序运行结束,st2 栈将被先析构,然后再去析构st1

而此时,st2析构的时候 st2 和 st1 指向同一块儿空间已经被释放了,那么当 st1 栈再去调用析构函数的时候,会再次对那一块空间进行释放,造成一块空间被多次释放的问题,导致程序崩溃!

显然这种结构是我们不想看到的,所以再这种复杂数据结构的拷贝过程中,编译器自动生成的拷贝构造函数就不能满足我们的要求了。

4.3 总结

我们不写,编译器会默认生成一个拷贝构造:

(1)内置类型的成员会完成值拷贝,也就是浅拷贝

像 Date 这样的类,需要的就是浅拷贝,那么编译器自动生成的拷贝构造函数就够用了,我们不需要自己写。

(2)自定义类型的成员,去调用这个成员的拷贝构造

像 Stack 这样的类,它是自己直接管理资源,那么需要自己实现深拷贝,浅拷贝的话会导致析构两次、程序崩溃的问题。

5.赋值运算符重载

运算符重载和函数重载是不一样的

  • 函数重载:支持函数名相同、参数不同的函数,可以同时使用
  • 运算符重载:自定义类型对象可以使用运算符

5.1运算符重载

创建一个日期Date类,现在要判断对象d1和对象d2是否相等要如何来做呢?

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

int main()
{
	Date d1(2023, 5, 1);
	Date d2(2023, 3, 1);

	return 0;
}

如果我们直接这样写:

肯定是错误的,可以看到编译器报的错误是没有与Date类型匹配的运算符

这时因为 运算符默认都是个内置类型使用的。自定义类型的变量用这些运算符,得自己编写运算符重载函数来实现

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名:关键字operator后面接需要重载的运算符符号。

函数原型: 返回值类型 operator操作符(参数列表) 

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@ 
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  •   . *       ::      sizeof      ? :      注意以上5个运算符不能重载。这个经常在笔试选择题中出现

下面以  d1==d2为例,通过代码的形式来学习运算符重载

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

	//==的运算符重载函数
	bool operator==(const Date& d)  // 这里等价于 bool operator==(Date* this, const Date& d2)
	{
		// 这里需要注意的是,左操作数是this指向的调用函数的对象

		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 5, 1);
	Date d2(2021, 3, 1);

	//显示调用:
	d1.operator==(d2);
	//我们一般就直接这样写
	d1 == d2;//同上,编译器会自动识别转换为 d1.operator==(d2) --> d1.operator(&d1, d2);

	cout << (d1 == d2) << endl; 
	// 注意运算符重载打印时要加括号(),因为运算符优先级的关系

	return 0;
}

当然,我们也可以将运算符重载放到类外,但这样就无妨访问到收private私有类型保护的成员变量,我们可以用将成员变量设置为public的公共类型,这样当然是不好的,等到后面学习了友元函数就可以解决

我们先将成员函数设为public来看一看,放到类外的运算符重载,代码应该怎么写

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1) // 构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
//private:    //将成员函数变为公共类
	int _year;
	int _month;
	int _day;
};

bool operator==(const Date& d1, const Date& d2)// ==运算符重载函数
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

int main()
{
	Date d1(2023, 5, 1);
	Date d2(2021, 3, 1);

	//显示调用
	operator==(d1, d2);

	//一般写法:
	d1 == d2; //同上,如果没有重载会报错,如果重载了它会转换为 operator==(d1, d2);

	cout << (d1 == d2) << endl;

	return 0;
}

可以看到在类外时,因为没有 this 指针,所以再显示调用函数时,其形参我们需要设置两个

放到类外的写法是不推荐的,因为他破坏了封装,这里只是为了让大家看看类内写法和类外写法的区别,以及理解隐藏的this参数

在编写好运算符重载函数后,无论它是在类内还是类外,我们写的时候都是直接写  d1 == d2 

这里我们可以在写一下其他的比较,比如 < , > , <=,  >= , !=

代码如下:

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

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

	// d1 < d2
	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;
		}
		else
		{
			return false;
		}
	}

	// d1 <= d2
	bool operator<=(const Date& d)
	{
		return *this < d || *this == d;
	}

	// d1 > d2
	bool operator>(const Date& d)
	{
		return !(*this <= d);
	}

	// d1 >= d2
	bool operator>=(const Date& d)
	{
		return !(*this < d);
	}

	// d1 != d2
	bool operator!=(const Date& d)
	{
		return !(*this == d);
	}

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

5.2赋值运算符重载

赋值运算符:

赋值运算符的重载还是以日期Date类未例:

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

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	//赋值运算符重载
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}

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

至于我们为什么要这样写赋值运算符重载,有以下原因:

原因1:

为什么参数类型要设置为引用,并用const进行修饰?

由于是自定义类型传参,我们如果使用 传值 传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用 引用传参(第一个参数是默认的 this 指针,我们不用管)。

其次,第二个参数,即赋值运算符的右操作数,我们在函数体内不会对其进行修改,所以最好加上 const 进行修饰。

原因2:

为什么函数要写返回值?并且函数的返回值要使用 引用返回

写返回值是因为,在赋值操作的时候我们经常会遇到连续赋值的场景,如d1 = d2 = d3 = d4

写返回值就能实现连续赋值的情况,更加模拟赋值运算符

用引用返回是为了避免不必要的拷贝,适用于出来作用域返回值没有被销毁的场景

具体可以看之前我写的c++入门那篇文章

原因3

为什么要用 if ,他是用来干嘛的?

这个 if 条件判断是用来检查,赋值是否是给自己赋值的,
因为我们在赋值操作时有时会写成:d1 = d1  的情况

为了防止不必要的操作

原因4

为什么返回的是 *this?

对于  赋值运算 d1 = d2  的情况,是将d2的值赋值给d1

对其显示处理就是:d1.operator = (d2)

又因为赋值运算的计算顺序是从右往左走的,在连续赋值的时候就会有所体现

所以我们应该返回赋值运算的左操作数,而在类中左操作数就是this指针

this是d1的地址,返回*this,就是返回d1

5.3赋值运算符重载的一个特性

一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节进行的值拷贝。

class Date
{
public:
	Date(int year = 2023, 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 d1;
	Date d2(2023, 5, 1);

	// 这里d1调用的编译器生成 operator= 完成拷贝,d2和d1的值也是一样的。
	d1 = d2;

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

	return 0;
}

结果:

可以看到赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。

6. const 成员

6.1const成员函数

我们把 const 修饰的  “成员函数”  称为 const 成员函数

const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

class Date
{
public:
	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()
{
	const Date d1(2022, 9, 1);//用const修饰对象d1,使其成员在使用时不会改变
	d1.Print();

	return 0;
}

以上代码中,我们用const来修饰类对象d1,使得d1里面的成员变量不会再之后的使用中被修改

但是再编译时会报错:

很明显我们的Print函数没有去该改变成员变量,那为什么还会报错呢?

这是因为,在 d1 对象去调用 Print 函数的时候,实参会把 d1 的地址传过去,但是 d1 是被 const 修饰的,也就是传过去的是 const Date* ;

而在 Print1 函数这边,形参部分会有一个隐含的 this 指针,也是 Date*  this
也就是把 const Date* 传给了 Date*  this,在这里属于权限的放大,所以编译会不通过。

 

那么我们应该怎么解决呢?

这里我们就可以用const来修饰成员函数:const 放在成员函数之后,实际就是修饰 this 指针:

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

    // const成员函数
	void Print1() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	const Date d1(2022, 9, 1);//用const修饰对象d1,使其成员在使用时不会改变
	d1.Print1();

	return 0;
}

那么在参数传递部分,实参还是和上面一样,形参部分因为 const 修饰的成员函数,所以就变成了 const Date*  this,那么此时就是权限相等了。

6.2思考

1. const对象可以调用非const成员函数吗?

 

不可以,就像上面的Print函数一样,会报错

非 const 成员函数,即成员函数的 this 指针没有被 const 所修饰,我们传入一个被 const 修饰的对象,使用没有被 const 修饰的 this 指针进行接收,属于权限的放大,函数调用失败。

2. 非const对象可以调用const成员函数吗?

 

可以,因为是权限缩小

我们传入 成员函数的是一个 大的,而成员函数在接受时,由于const的存在,将其 缩小

属于权限缩小,可以被执行

3. const成员函数内可以调用其它的非const成员函数吗?

 

不可以,在一个被 const 所修饰的成员函数中调用其他没有被 const 所修饰的成员函数,

也就是将一个被 const 修饰的 this 指针的值赋值给一个没有被 const 修饰的 this 指针,属于权限的放大,函数调用失败。

4. 非const成员函数内可以调用其它的const成员函数吗?

 

可以,在一个没有被 const 所修饰的成员函数中调用其他被 const 所修饰的成员函数,

也就是将一个没有被 const 修饰的 this 指针的值赋值给一个被 const 修饰的 this 指针,属于权限的缩小,函数调用成功

7.取地址及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;
	}

	//const对象 取地址操作符重载
	const Date* operator&() const
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 5, 7);
	const Date d2(2023, 5, 7);

	cout << &d1 << endl;
	cout << &d2 << endl;
	return 0;
}

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,我们作为了解、知道有这两个东西存在即可

只有特殊情况,才需要重载,比如想让别人获取到指定的内容,就可以自己实现。

练习:

根据前面所学知识的一个练习:日期类的实现

要求:

1.能获取某年某月的天数

2.写出构造函数、拷贝构造函数、析构函数

3.判断两个日期的关系: > ; < ; <= ; >= ; != ; ==

4.日期+天数、日期+=天数

5.日期-天数、日期-=天数

6.前置++、后置++,前置--,后置--

7.日期-日期,返回天数

代码如下:

#pragma once
#include<iostream>
#include<string.h>
using namespace std;


//声明:

class Date
{
public:
	void Print();

	//构造函数
	Date(int year = 2023, int month = 1, int day = 1);

	//拷贝构造函数
	Date(const Date& d);

	//析构函数
	~Date();

	bool operator<(const Date& d);
	bool operator<=(const Date& d);
	bool operator>(const Date& d);
	bool operator>=(const Date& d);
	bool operator==(const Date& d);
	bool operator!=(const Date& d);

	Date& operator+= (int day);
	Date operator+ (int day);
	Date& operator-= (int day);
	Date operator- (int day);

	// ++d
	Date& operator++();

	// d++
	Date operator++(int);

	// --d
	Date& operator--();

	// d--
	Date operator--(int);

	//日期-日期
	int operator-(const Date& d);

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







//定义:

void Date::Print()
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

int GetMonthDay(int year, int month)
{
	int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
	{
		return 29;
	}
	else
	{
		return monthArray[month];
	}
}

Date::Date(int year, int month, int day)
{
	if (month > 0 && month < 13 && (day > 0 && day <= GetMonthDay(year, month)))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "日期非法" << endl;
	}
}

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

Date::~Date()
{
	_year = 2023;
	_month = 1;
	_day = 1;
}


bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

bool Date::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;
	}
	else
	{
		return false;
	}
}

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);
}

bool Date::operator!=(const Date& d)
{
	return !(*this == d);
}


Date& Date::operator+= (int day)
{
	if (day < 0)
	{
		*this -= -day;
		return *this;
	}
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			_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;
		return *this;
	}
	_day -= day;
	while (_day <= 0)
	{
		if (--_month == 0)
		{
			--_year;
			_month = 12;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

Date Date::operator- (int day)
{
	Date tmp(*this);
	tmp -= day;
	return tmp;
}


// ++d  显示去调:d1.operator++();
Date& Date::operator++()
{
	*this += 1;//调用上面写的运算符重载
	return *this;
}

// d++  后置++的类型是编译器规定的这样写
//显示去调:d1.operator++(0);
Date Date::operator++(int)
{
	Date tmp(*this);

	*this += 1;

	return tmp;
}

// --d
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

// d--
Date Date::operator--(int)
{
	Date tmp(*this);

	*this -= 1;

	return tmp;
}

int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;

	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}
	return n * flag;
}




//测试
void text4()
{
	Date d1(2023, 5, 6);
	Date d2(2001, 3, 6);
	int life = d1 - d2;
	cout << life << endl;
}

int main()
{
	text4();



	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/498071.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Monkey Patching in Go

gomonkey 用来给函数打桩&#xff0c;这种使用一个新的方法实现来替换原来的实现逻辑&#xff0c;怎么看都觉得很神奇。举个例子&#xff0c;在单测中方法 json.Marshal 可以被 gomonkey 覆写成另一种逻辑实现&#xff0c;我准备从原理和使用的角度来看看 gomonkey。主要是来看…

LeetCode 第 344 场周赛

相当的惨烈&#xff0c;乱交 Q1 前后缀分解,用set统计不同元素的个数 class Solution {public:vector<int> distinctDifferenceArray(vector<int>& nums) {int n nums.size();vector<int> L(n 1, 0), R(n 1, 0); // 前缀不同数的个数set<int&g…

MLC LLM - 大模型本地部署解决方案

MLC LLM 是一种通用解决方案&#xff0c;它允许将任何语言模型本地部署在各种硬件后端和本地应用程序上&#xff0c;此外还提供了一个高效的框架&#xff0c;供每个人根据自己的用例进一步优化模型性能。 推荐&#xff1a;用 NSDT设计器 快速搭建可编程3D场景。 我们的使命是让…

【Python】使用Print函数制作旋转的动画

1. 引言 如果你想有效地学习Python&#xff0c;这篇文章可能不适合你。接下来的一切都可能是愚蠢、和浪费时间&#xff0c;但哪有怎么样&#xff0c;毕竟这玩意很有趣呀&#xff01; 2. 好玩的脚本 首先&#xff0c;我们来看两个好玩的Python脚本&#xff0c;如下&#xff1…

开关电源基础03:正激和反激开关电源拓扑(3)-反激拓扑

说在开头&#xff1a;关于不确定性原理 1927年2月&#xff0c;那个冬天对海森堡来说简直是一场噩梦&#xff0c;越来越多的人转向了薛定谔和他那该死的波动理论&#xff0c;把他的矩阵忘得一干二净&#xff1b;而最让他伤心和委屈的是&#xff0c;玻尔也转向了他的对立面&…

大规模并行处理架构Doris编译部署篇

目录 1 Doris编译1.1 使用 Docker 开发镜像编译&#xff08;推荐&#xff09;1.1.1 遇到的问题1.1.1 遇到的问题 1.2 直接编译&#xff08;CentOS/Ubuntu&#xff09;1.2.1 环境准备1.2.2 系统依赖&#xff08;一次性安装&#xff09;1.2.3 手动安装系统依赖1.2.3.1 CMake 3.11…

Linux驱动开发:SPI子系统

1、SPI简介 1.1 四根线 MISO&#xff1a;主设备数据输入&#xff0c;从设备数据输出。 MOSI&#xff1a;主设备数据输出&#xff0c;从设备数据输入。 SCLK&#xff1a;时钟信号&#xff0c;由主设备产生。 CS&#xff1a; 从设备片选信号&#xff0c;由主设备控制。 1.2…

《string类的使用介绍》

本文主要介绍string的常见的接口的使用 文章目录 一、什么是string类二、string类的使用1、string类对象的常见构造2、string类对象的容量操作3、string类对象的访问及遍历操作①operator[ ]的用法②迭代器③范围for 4、string类对象的修改操作①push_back②append③operator&a…

刷题笔记8| 344.反转字符串, 541. 反转字符串II, 剑指Offer 05.替换空格

344.反转字符串 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 输入&#xff1a;s ["h","e",…

索引—MySQL

文章目录 1.定义以及相关知识1.1定义1.2数据库保存数据的基本单位 2.MySQL中索引分类2.1B树和B树2.2主键索引&#xff08;聚簇索引&#xff09;2.3非聚簇索引2.4覆盖索引2.5复合索引&#xff08;联合索引&#xff09;2.6基于B树的索引2.7hash索引 1.定义以及相关知识 1.1定义 …

okio篇--总览

看源码第一步&#xff0c;先去看看官网对okio的介绍&#xff1a; Okio 首先第一段&#xff1a; okio对bytes&#xff0c;做了两种形式的封装&#xff1a;ByteString和Buffer。 其中ByteString&#xff0c;是针对字符类的数据&#xff0c;内部封装一个byte数组&#xff0c;封…

网络协议与攻击模拟-06-ICMP重定向

■0网络不可达 ■2协议不可达 类型4源抑制 类型5重定向 2、 ICMP 常见的报文 响应请求 使用 ping 请求&#xff08; type 0)响应&#xff08; type 8) 目标不可达 type 3 源抑制 源抑制则充当一个控制流量的角色&#xff0c;它通知主机减少数据报流量&#xff0c;由于 I…

Django初识

1、简介 Django&#xff0c;是用python语言写的开源web开发框架&#xff0c;并遵循MVC设计。劳伦斯出版集团为了开发以新闻内容为主的网站&#xff0c;而开发出来了这个框架&#xff0c;于2005年7月在BSD许可证下发布。这个名称来源于比利时的爵士音乐家DjangoReinhardt&#…

操作系统第二章——进程与线程(中)

和光同尘&#xff0c;与时舒卷 文章目录 2.2.1 调度的概念&#xff0c;层次知识总览调度的基本概念高级调度低级调度中级调度三层调度的联系&#xff0c;对比进程的挂起态和七状态模型知识回顾 2.2.2 进程调度的时机&#xff0c;切换与过程&#xff0c;方式知识总览进程调度的时…

【C++】第二站:类和对象(中)拷贝构造函数

文章目录 一、拷贝构造函数的概念二、拷贝构造函数的特性三、深度剖析拷贝构造函数不采用引用会无限递归的原因1.C对于传参的两个规定2.如何解开这个无穷递归 四、拷贝构造函数的其他特性五、拷贝构造的一些使用场景 一、拷贝构造函数的概念 拷贝构造函数&#xff1a;只有单个形…

2.2 Linux控制台访问CLI

系列文章目录 第1章 Linux Shell简介 第2章 Shell基础 <本章所在位置> 第3章 Bash Shell基础命令 第4章 Bash Shell命令进阶 第5章 Linux Shell深度理解 第6章 Linux环境变量 第7章 Linux文件权限 第8章 Linux文件系统的管理 第9章 Linux软件安装 第10章 Linux文本编辑器…

【MySQL】搭建出高可用性、高性能的MySQL集群要考虑的事是蛮多的,你看看会不会?

MySQL 架构设计数据同步负载均衡安全性监控和维护注意的点1. 确定节点数量和配置2. 选择合适的硬件和网络设备3. 避免单点故障4. 定期备份和恢复测试5. 定期更新和升级 Java工程师使用集群步骤最后 MySQL集群是一种高可用性、高性能的数据库解决方案&#xff0c;它可以通过多个…

基于Django实现的TMS物流管理系统(附源码下载)

基于Django实现的物流管理系统&#xff08;TMS&#xff0c;Transportation Management System&#xff09; 特点 前端基于Bootstrap 4框架和AdminLTE框架。使用MySQL作为数据库后端。实现了运单录入、发车出库、到货签收、客户签收等基本功能。拥有较为完善的报表功能和财务管…

Java—JDK8新特性—Lambda表达式【内含思维导图】

目录 JDK8新特性 2.Lambda表达式 思维导图 2.1 什么是Lambda表达式 2.2 为什么使用Lamdba表达式 2.3 Lambda表达式基本语法 2.4 类型推断 2.5 Lambda练习 2.6 Lambda常用场景 JDK8新特性 官网提供网址&#xff1a;JDK 8 Features 2.Lambda表达式 思维导图 2.1 什么是…

浅谈Dom和Bom(清晰易懂版)

DOM&#xff08;文档对象模型&#xff09; DOM 是浏览器提供的一种操作网页内容和结构的 API&#xff0c;它将 Web 页面表示为一个树形结构&#xff0c;其中每一个 HTML 元素都是一个节点&#xff0c;可以通过 DOM API 对其进行访问和操作。DOM API 包括了一系列方法和属性&am…