【C++深入浅出】类和对象中篇(六种默认成员函数、运算符重载)

news2024/11/24 8:29:45


目录

一. 前言 

二. 默认成员函数

三. 构造函数

3.1 概念

3.2 特性

四. 析构函数

4.1 概念

4.2 特性

五. 拷贝构造函数

5.1 概念

5.2 特性

六. 运算符重载

6.1 引入

6.2 概念

6.3 注意事项

6.4 重载示例

6.5 赋值运算符重载

6.6 前置++和后置++运算符重载

七. const成员函数

7.1 问题引入

7.2 定义方式

7.3 使用细则

 八. 取地址运算符重载


一. 前言 

        上期我们介绍了一些关于类的基础知识,学会了如何定义一个类,体会到了面向对象中封装的特征。本期我们将继续类和对象的学习,重点讨论C++类中的成员函数,并在下期我们将自己动手实现一个类----日期类

        话不多说,上菜咯!!!

二. 默认成员函数

        如果一个类中什么成员都没有,我们将其称之为空类

//空类
class Date
{

};

         但是空类中真的什么都没有吗?实则不然。任何类在什么都不写时,编译器会自动生成6个默认成员函数。默认成员函数:用户没有显式实现,编译器会自动生成的成员函数。如下所示:

        接下来的内容,我们就对这6个默认成员函数进行逐一分析

三. 构造函数

3.1 概念

        我们来看看下面的日期类:

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(2022, 7, 5);
	d1.Print();
	Date d2;
	d2.Init(2022, 7, 6);
	d2.Print();
	return 0;
}

        对于上面Date类,我们发现我们每次创建一个对象后,都要通过Init 方法给对象设置日期,这未免显得过于麻烦,那能否在对象创建时,就同步将信息设置进去呢?

        使用构造函数就能很好的进行解决。构造函数是一个特殊的成员函数,函数名与类名相同,创建类对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。其形式如下:

class Date
{
public:
	//Date的构造函数
	Date()
	{
		//进行初始化
        //...
	}
};

3.2 特性

        构造函数是特殊的成员函数,需要注意的是,构造函数虽然名为构造,但是其主要任务并不是创建对象开辟空间,而是初始化对象

        构造函数有如下特征

  1. 函数名与类名相同
  2. 没有返回值,void也不能有:
    class Date
    {
    public:
    	//Date的构造函数
    	Date()
    	{
    		
    	}
    	void Date(){} //错误写法,没有返回值
    };
  3. 对象实例化时编译器会自动调用对应的构造函数
  4. 构造函数支持重载,可以匹配不同的初始化信息。从参数来看,主要分为无参构造函数带参构造函数
    class Date
    {
    public:
    	// 1.无参构造函数
    	Date()
    	{}
    	// 2.带参构造函数
    	Date(int year, int month, int day)
    	{
    		_year = year; 
    		_month = month;
    		_day = day;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	//1.创建对象
    	//2.调用相应的构造函数
    	Date d1; //调用无参构造函数
    	Date d2(2023, 8, 22); //调用带参构造函数
    }

    需要注意的是,调用无参的构造函数时,对象后面无需带(),否则会变成函数声明:

    Date d1; //调用无参构造函数
    
    Date d3(); //声明一个没有形参的函数d3,它的返回值类型为Date
  5. 构造函数是默认成员函数。如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

    class Date
    {
    public:
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d1; //调用编译器自动生成的默认构造函数,默认构造是无参的,相匹配
    
        Date d2(2023, 8, 22); //该行代码会报错,没有匹配的带参构造函数
    }

    而如果我们显式地定义了构造函数,编译器就不会自动生成无参的默认构造函数,如下:

    class Date
    {
    public:
    	//显式定义带参的构造函数
    	Date(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d1;  //该行代码会报错,没有匹配的默认构造函数
    
    	Date d2(2023, 8, 22); //调用带参的构造函数
    }

  6. 编译器自动生成的默认构造函数对内置类型不会进行初始化,如:int,char,double等等;而对于自定义类型,会去调用该自定义类型的默认构造函数。

    class Time
    {
    public:
    	Time() //Time类的默认构造函数
    	{
    		_hours = 0;
    		_minute = 0;
    		_second = 0;
    	}
    private:
    	int _hours;
    	int _minute;
    	int _second;
    };
    class Date
    {
    public:
    
    private:
        //内置类型
    	int _year;
    	int _month;
        //自定义类型
    	Time _day;
    };
    
    int main()
    {
    	Date d; //调用编译器自动生成的默认构造函数
    	return 0;
    }

    我们发现Date的默认构造函数对_year和_month没有进行初始化,依然是随机值,而对_day则去调用了Time类的默认构造函数,将其成员变量初始化为0。我们可以通过调试进一步进行验证:

    默认构造函数调试

  7. 值得一提的是:在C++11中,针对默认构造函数对内置类型不进行初始化的缺陷进行了改进,支持内置类型的成员变量在类中声明时给默认值。如下:

    class Date
    {
    public:
    	void Print()
    	{
    		cout << _year << '-' << _month << '-' << _day << endl;
    	}
    private:
    	int _year = 0; //声明时给默认值
    	int _month = 0;
    	int _day = 0;
    };
    
    int main()
    {
    	Date d;
    	d.Print();
    	return 0;
    }

  8.  构造函数也支持给缺省值无参的构造函数和全缺省的构造函数都称作默认构造函数。而默认构造函数只能有一个,故二者不能同时存在。举例如下

    class Date
    {
    public:
    	Date() //无参的构造函数
    	{
    		_year = 2023;
    		_month = 8;
    		_day = 22;
    	}
    	Date(int year = 2023, int month = 8, int day = 22) //全缺省的构造函数
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d1(2024); //编译通过,调用全缺省的构造函数
    	Date d2; //这里编译会报错,d2调用默认构造函数,但存在两个默认构造函数,编译器不知道调用哪个
    	return 0;
    }

    小贴士:一般我们显式定义构造函数时,习惯将构造函数写成全缺省的,以提高代码的健壮性

四. 析构函数

4.1 概念

        构造函数是在对象创建时对其进行初始化,有初始化便有销毁,析构函数的作用就是在对象生命周期结束时,完成对象中资源的清理和释放。和构造函数一样,析构函数由编译器自动调用。下面是Stack类的构造函数和析构函数的实现

class Stack
{
public:
	Stack(size_t capacity = 4) //构造函数,初始化一个栈,写成全缺省的形式
	{
		_array = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_top = 0;
	}
	~Stack() //析构函数,在类名前加~号
	{
		free(_array); //堆上动态申请的空间需要由用户自行释放
		//下面的代码也可以不写,栈上的空间操作系统会自动释放
		_array = nullptr;
		_capacity = _top = 0;
	}
private:
	int* _array;
	int _capacity;
	int _top;
};

4.2 特性

        析构函数也是特殊的成员函数,其特征如下:

  1. 析构函数的函数名是在类名前加上字符 ~
  2. 析构函数既没有返回值,也没有参数
  3. 与构造函数相反,析构函数不能支持重载,一个类中有且只能有一个析构函数。参考构造函数的特征,当用户没有显式地定义析构函数,编译器会自动生成一个默认的析构函数。
  4. 当对象的生命周期结束时,C++编译器会自动调用析构函数。
    class Stack
    {
    public:
    	Stack(){
    		cout << "Stack()" << endl;
    	}
    	~Stack(){
    		cout << "~Stack()" << endl;
    	}
    private:
    	int* _array;
    	int _capacity;
    	int _top;
    };
    
    int main()
    {
    	Stack s;
    	return 0;
    }

    当s对象创建时编译器自动调用构造函数,当s对象生命周期结束时编译器自动调用析构函数,效果如下:

  5. 和构造函数类似,编译器默认生成的析构函数不会对内置类型成员进行清理,最终由操作系统自动进行回收即可;而对于自定义类型成员,默认析构函数会去调用它的析构函数,保证其内部每个自定义类型成员都可以正确销毁。

     回到我们之前的日期类

    class Time
    {
    public:
    	Time() //Time类的默认构造函数
    	{
    		cout << "Time()" << endl;
    	}
    	~Time() //Time类的析构函数
    	{
    		cout << "~Time()" << endl;
    	}
    private:
    	int _hours;
    	int _minute;
    	int _second;
    };
    class Date
    {
    public:
    	//没有显式写出构造函数和析构函数,使用编译器自动生成的
    
    private:
    	int _year;
    	int _month;
    	Time _day;
    };
    
    int main()
    {
    	Date d; //调用编译器自动生成的默认构造函数
    	return 0;
    }

    尽管我们没有直接创建Time类的对象,但依然调用了Time类的构造函数和析构函数。这是因为Date类中的_day成员是Time类的对象,在Date类的默认构造函数和默认析构函数中,会去调用Time类这个自定义类型的构造函数和析构函数,对_day成员进行初始化清理工作。

  6. 如果类中没有动态申请内存时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成内存泄漏,比如Stack类

  7. 类的析构函数一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放static对象。

         Q:假设已经有A,B,C,D 4个类的定义,则程序中A,B,C,D析构函数调用顺序为?

C c;
int main()
{
	A a;

	B b;

	static D d;

    return 0;

}

答案是BADC。解析如下:

1、全局变量优先于局部变量进行构造,因此构造的顺序为cabd

2、析构的顺序和构造的顺序相反

3、static和全局对象需在程序结束才进行析构,故会放在局部对象之后进行析构

综上:析构的顺序即为BADC。


五. 拷贝构造函数

5.1 概念

        在现实生活中,可能存在一个与你长相,我们称其为双胞胎

         那我们在创建类对象时,能不能创建一个和已有对象一模一样的对象呢?Ctrl+CCtrl+V想必没有人不喜欢吧嘿嘿这就要谈到我们的拷贝构造函数惹。

         拷贝构造函数:只有单个形参,该形参是对本类型对象的引用(一般常用const修饰),在用已存在的类对象创建新对象时编译器会自动调用拷贝构造函数。

class Date
{
public:
	Date() {};
	Date(const Date& d) //Date的拷贝构造函数
	{
		_day = d._day;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1; 
	Date d2(d1); //用d1拷贝构造d2
	return 0;
}

5.2 特性

         拷贝构造函数也是属于特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须是本类对象的引用使用传值方式编译器会直接报错
    因为会引发无穷递归调用
    //拷贝构造函数的写法
    Date(const Date d) // 错误写法:编译报错,会引发无穷递归
    {
    	_year = d._year;
    	_month = d._month;
    	_day = d._day;
    }
    
    Date(const Date& d) // 正确写法
    {
    	_year = d._year;
    	_month = d._month;
    	_day = d._day;
    }
  3. 未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对内置类型成员会按照其在内存存储的字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝;而对于自定义类型成员,会去调用它的拷贝构造函数,完成拷贝。
    class Time
    {
    public:
    	Time()
    	{
    		_hour = 1;
    		_minute = 1;
    		_second = 1;
    	}
    	Time(const Time& t)
    	{
    		_hour = t._hour;
    		_minute = t._minute;
    		_second = t._second;
    		cout << "Time::Time(const Time&)" << endl;
    	}
    private:
    	int _hour;
    	int _minute;
    	int _second;
    };
    class Date
    {
    private:
    	// 内置类型
    	int _year = 2023;
    	int _month = 1;
    	int _day = 1;
    	// 自定义类型
    	Time _t;
    };
    int main()
    {
    	Date d1; //d1调用默认的构造函数进行初始化
    
    	// 用已经存在的d1拷贝构造d2,此时会调用Date类的拷贝构造函数
    	// 但Date类并没有显式定义拷贝构造函数,因此编译器会给Date类生成一个默认的拷贝构造函数
    	Date d2(d1);
    	return 0;
    }
    我们可以通过监视窗口来查看d2对象的拷贝情况可以看出,编译器默认生成的拷贝构造函数不仅会对自定义类型成员进行拷贝(通过调用相应的拷贝构造函数),也会对内置类型成员进行拷贝(按字节序的浅拷贝)。

    默认拷贝构造函数调试

  4. 咦?既然编译器默认生成的拷贝构造函数已经可以很好的完成拷贝了,那我们还需要显式实现拷贝构造函数吗对于上面的日期类确实已经足够了,让我们再回到之前实现的Stack类
    class Stack
    {
    public:
    	Stack(size_t capacity = 4)
    	{
    		_array = (int*)malloc(capacity * sizeof(int));
    		if (nullptr == _array)
    		{
    			perror("malloc申请空间失败");
    			return;
    		}
    		_size = 0;
    		_capacity = capacity;
    	}
    	~Stack()
    	{
    		if (_array)
    		{
    			free(_array);
    			_array = nullptr;
    			_capacity = 0;
    			_size = 0;
    		}
    	}
    private:
    	int* _array;
    	size_t _size;
    	size_t _capacity;
    };
    int main()
    {
    	Stack s1;
    	Stack s2(s1);
    	return 0;
    }

    当我们兴冲冲地运行代码时,诶,程序居然崩溃

     为什么呢?这就涉及到了浅拷贝按字节序拷贝的缺陷,如下图所示

     那这种浅拷贝的问题要如何解决呢?

    一般有两种解决方案:深拷贝或者引用计数


    所谓深拷贝,就是手动再申请一段空间,然后将原空间的内容依次拷贝到新空间中,最后让s2的_array指针指向这个新空间。这种方法避免了一块空间被多个对象指向的问题。

    引用计数,就是在类中额外增加一个变量count记录堆空间被引用的次数,只有当引用次数变为1时,我们才对这段空间进行释放。这种方法避免了一块空间被多次释放的问题。


    现在我们对这两种方式有个初步的印象即可,后续我们会详细讲解。不过无论是深拷贝还是引用计数,都是编译器默认生成的拷贝构造函数无法做到的,需要我们显式地实现拷贝构造函数。

  5. 拷贝构造函数有三个典型的调用场景:使用已存在的对象创建新对象函数形参为类对象函数返回值为类对象

    class Date
    {
    public:
    	Date(int year, int minute, int day)
    	{
    		cout << "Date(int,int,int):" << this << endl;
    	}
    	Date(const Date& d)
    	{
    		cout << "Date(const Date& d):" << this << endl;
    	}
    	~Date()
    	{
    		cout << "~Date():" << this << endl;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    Date Test(Date d)
    {
    	Date temp(d);
    	return temp;
    }
    int main()
    {
    	Date d1(2022, 1, 13);
    	Test(d1);
    	return 0;
    }

    结论:为了提高效率,减少拷贝构造的次数,一般对象传参时,我们尽量使用引用传参,函数返回时也是根据实际场景,能用引用返回尽量使用引用返回


六. 运算符重载

6.1 引入

        对于内置类型,我们可以使用==、>号运算符判断它们的大小关系,可以使用+,-号运算符对其进行加减......如下所示

int main()
{
	int a = 10;
	int b = 20;
	a = a + 10;
	b = b - 10;
	cout << (a == b);
	cout << (a > b);
	//还可以使用许许多多的运算符进行操作,这里就不一一挪列了
	//...
	return 0;
}

        但对于自定义类型来说,也就是我们的类,这些运算符仿佛都失效

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 8, 24);
	Date d2(2023, 8, 25);
	d1 = d1 + 10; //d1类对象使用+号运算符
	d1 == d2; //d2类对象使用==号运算符
}

        很明显编译器报错了,这是因为对于几个固定的内置类型,编译器知道它们的运算规则,而对于我们自定义的类型,编译器并不知道它的运算规则,例如d1+10究竟是年份+10还是月份+10呢?编译器无法进行确定,故报错。

        有一种很简单的解决方法就是给类定义成员函数,通过调用成员函数来实现我们想要的运算,如下所示:

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void AddYear(int val) 
	{
		_year += val;
	}
	bool isSame(const Date& d)
	{
		return _year == d._year && _month == d._month && _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 8, 24);
	Date d2(2023, 8, 24);
	d1.AddYear(1); //年份+1
	cout << d1.isSame(d2); //比较d1和d2是否相等
}

        上面的方式的确可以解决问题,但还是不够直观,每次进行运算都需要调用函数,代码未免有点挫有没有什么方法可以让类使用运算符进行运算吗?

        这就不得不谈到我们的主角----运算符重载

6.2 概念

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

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

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

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

通过运算符重载,我们可以对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型,且不改变原有的功能。

6.3 注意事项

        进行运算符重载需要注意以下几点:

  1. 不能通过连接其他符号来创建新的运算符:比如使用operator@创建新的运算符@
  2. 重载操作符必须有一个类类型参数
  3. 不能改变内置类型运算符的含义。例如:内置的整型+,不能改变其含义
  4. 不能改变操作符的操作数个数。一个操作符有几个操作数,其重载时函数就有几个参数
  5. 运算符重载也可以作为类成员函数。作为类成员函数重载时,其形参看起来会比操作数数目少一个,因为成员函数的第一个参数为隐藏的this指针
  6. 有五个运算符不支持重载,它们分别是:.:: sizeof?: .*
     

6.4 重载示例

         下面我们来看看日期类==号运算符的重载示例

         1、作为全局函数重载

class Date
{
public:
	Date(int year = 2000, 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(2022, 8, 24);
	Date d2(2023, 8, 24);
	cout << (d1 == d2);
	return 0;
}

当运算符重载作为全局函数时,由于我们难免需要对成员变量进行访问,我们需要类的成员函数是共有的,可这难免会破坏了类的封装性


当然,我们还可以使用友元函数来解决,关于友元在下篇会介绍到。不过最推荐的还是将其重载为成员函数

         2、作为成员函数重载

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// bool operator==(Date* const this, const Date& d2)
	// this指向调用的对象
	bool operator==(const Date& d) //重载为成员函数
	{
		//类内访问成员变量不受访问限定符限制
		return _year == d._year && _month == d._month && _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 8, 24);
	Date d2(2023, 8, 24);
	//下面两种写法是等价的
	cout << (d1 == d2);
	cout << d1.operator==(d2);
	return 0;
}

6.5 赋值运算符重载

        在使用类的过程中,当我们想将一个类赋值给另一个类时,我们便可以对=赋值运算符进行重载。其重载格式如下:

  • 参数类型:const T&,传递引用可以提高传参效率,const保证原来的类对象不会被修改
  • 返回值类型:T&,使用引用返回可以提高返回的效率,有返回值目的是为了支持链式访问
  • 一般会检测是否是自己给自己赋值,如果是,则不进行任何操作
  • 最终返回的是本身的引用,即*this,便于进行连续赋值
    class Date
    {
    public:
    	Date(int year = 2023, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	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;
    };
    
    int main()
    {
    	Date d1;
    	Date d2, d3;
    	d3 = d2 = d1; //调用赋值运算符重载
    }

         下面是赋值运算符重载的几点注意事项

  1. 赋值运算符只能重载为成员函数不能重载成全局函数
    // 赋值运算符重载成全局函数
    Date& operator=(Date& left, const Date& right)
    {
    	if (&left != &right)
    	{
    		left._year = right._year;
    		left._month = right._month;
    		left._day = right._day;
    	}
    	return left;
    }
    为什么呢?实际上,在我们前面介绍的默认成员函数中,赋值运算符重载也在其中。因此,当我们在类中没有显式地实现,编译器会自动生成默认的重载函数。而如果用户又自行在类外写一个赋值运算符重载函数,就会和编译器默认生成的默认赋值运算符重载冲突,故赋值运算符只能重载为成员函数
  2. 编译器默认生成的赋值运算符重载函数,其规则和拷贝构造函数相似。对于内置类型,是按照字节序进行拷贝的;而对于自定义类型,则是去调用它的赋值运算符重载函数
  3. 那么既然编译器默认生成的赋值运算符重载已经可以完成字节序的拷贝了,那还要显式地进行实现吗?答案显然是需要的。和拷贝构造函数一样,在实现Stack这种有内存管理的类,我们需要自行实现赋值运算符重载,否则就会因为浅拷贝的问题导致程序崩溃
  4. 反之,如果程序不涉及内存管理,赋值运算符重载是否显式实现都可以

        下面来个小问题试试对拷贝构造的理解:

int main()
{
	Date d1, d2;
	Date d3 = d1; //这里调用的是拷贝构造还是赋值重载呢?
	d2 = d1; //这里呢?
	return 0;
}

答:第一问调用的是拷贝构造函数,第二问调用的是赋值重载。

解析:拷贝构造函数用已存在的对象去构造新对象,而d3就是我们需要构造的新对象,第一问就是用d1对象去构造d3对象,故调用拷贝构造,这种写法与Date d3(d1)等价。而赋值运算符载函数两个已存在对象之间进行赋值,d1和d2都是已经存在的对象,故d2=d1调用的是赋值重载。

6.6 前置++和后置++运算符重载

        前置++

        下面我们来尝试对Date类实现前置++运算符的重载,用于对天数进行自增。所谓前置++,就是先自增1再返回结果,如下:

class Date
{
public:
	Date(int year = 2023, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date& operator++() //前置++运算符重载
	{
		_day += 1; //先自增  (注意:这里为了演示先忽略日期进位,进位处理请看Date类的模拟实现)
		return *this; //返回自身,为了提高效率,用引用返回
	}
private:
	int _year;
	int _month;
	int _day;
};

        后置++

        而后置++,就是先返回结果再进行自增。但有个问题:前置++和后置++都是一元运算符,即只有一个操作数:对象本身。在作为成员函数重载时,重载函数就是无参的,那编译器要如何区分是前置++还是后置++呢?

int main()
{
	Date d;
	//编译器要如何区分哪个operator++()函数是前置++,哪个又是后置++ ???
	++d; //相当于d.operator++()
	d++; //也相当于d.operator++()
	return 0;
}

        为此,C++做了特殊规定:后置++重载时多增加一个int类型的参数用于占位,但调用函数时该参数不用传递,编译器会自动传递。

class Date
{
public:
	Date(int year = 2023, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date operator++(int) //后置++运算符重载,int用于占位区分
	{
		Date temp(*this); //由于要返回+1前的结果,所以先对对象进行拷贝
		_day += 1; //然后天数+1
		return temp; //然后将+1前的对象返回。由于temp出了函数就销毁了,故不能用引用返回
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	++d; //相当于d.operator++()
	d++; //相当于d.operator++(int),int类型参数由编译器自动传递
	return 0;
}

七. const成员函数

7.1 问题引入

        我们来看下面的代码:

class Date
{
public:
	void Print()
	{
		cout << "void Print()" << endl;
	}
private:
	int _year = 2023;
	int _month = 1;
	int _day = 1;
};

int main()
{
	const Date d;
	d.Print();
	return 0;
}

        上面的代码编译时会进行报错,报错原因如下

由于对象d被const所修饰,故其类型为const Date,表示不能对类的任何成员进行修改。当d调用Print函数时,传入的实参就是d的地址,即const Date*类型的指针,而在Print函数中,用于接收的this指针却是Date*类型的,这无疑是一种权限的放大,故编译器会进行报错。


那要怎么解决这个问题呢?很简单,给this指针加上const进行修饰即可。


7.2 定义方式

        由于this指针是 "非静态成员函数" 的隐藏形参,我们无法显式地去定义this指针,因此C++规定,在成员函数后面加上const代表它为const成员函数,其this指针的类型为const A* this,编译器会自动进行识别处理


         回到上面的代码,当我们在Print函数后加上const后,程序就正常运行啦

class Date
{
public:
	void Print() const //this指针的类型是const Date*
	{
		cout << "void Print() const" << endl;
	}
private:
	int _year = 2023;
	int _month = 1;
	int _day = 1;
};

int main()
{
	const Date d;
	d.Print();
	return 0;
}


7.3 使用细则

  1. const修饰的成员函数和非const修饰的成员函数可以构成函数重载,调用时会调用最匹配的函数,一般用作读写的分离
    class Date
    {
    public: 
    	void Print()  //非const成员函数
    	{
    		cout << "void Print()" << endl;
    	}
    	void Print() const  //const成员函数
    	{
    		cout << "void Print() const" << endl;
    	}
    private:
    	int _year = 2023;
    	int _month = 1;
    	int _day = 1;
    };
    
    int main()
    {
    	const Date d1;
    	Date d2;
    	d1.Print(); //const类型的对象调用cosnt成员函数
    	d2.Print(); //非const类型的对象调用非const成员函数
    	return 0;
    }

  2. 建议给只读成员函数加上const修饰,即内部不涉及修改成员变量的函数 

  3. 构造函数不能加const修饰。构造函数是对成员变量进行初始化的,显然会涉及到成员变量的修改。


 八. 取地址运算符重载

        取地址运算符重载有两个版本,一个是const的,一个是非const的。这两个成员函数也是我们一开始讲的默认成员函数,当用户没有显式定义时,编译器会自动生成。

class Date
{
public:
	Date* operator&() //非const版本,this指针类型为Date*
	{
		return this;
	}
	const Date* operator&()const //const版本,this指针类型为const Date*
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

这两个版本的成员函数和上面的不同,一般使用编译器默认生成的取地址重载即可。只有特殊情况下,才需要显式定义,比如想让别人获取到指定的内容!

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

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

相关文章

【Rust日报】2023-09-07 Servo 项目将加入欧洲 Linux 基金会

Servo 项目将加入欧洲 Linux 基金会 Servo 项目由 Mozilla Research 于 2012 年创建&#xff0c;是除编译器本身之外的首个主要 Rust 代码库&#xff0c;自此成为实验性网络引擎设计的标志。Servo 的主要组件已被集成到 Firefox 网络浏览器中&#xff0c;其若干解析器和其他底层…

渗透测试基础之永恒之蓝漏洞复现

渗透测试MS17-010(永恒之蓝)的漏洞复现 目录 渗透测试MS17-010(永恒之蓝)的漏洞复现 目录 前言 思维导图 1,渗透测试 1,1,什么是渗透测试? 1.2,渗透测试的分类: 1.3,渗透测试的流程 1.3.1,前期交互 1.3.2,情报收集 1.3.3,威胁建模 1.3.4,漏洞分析 1.3.5,漏洞验…

软件设计模式(五):代理模式

前言 代理模式是软件设计模式的重中之重&#xff0c;代理模式在实际应用比较多&#xff0c;比如Spring框架中的AOP。在这篇文章中荔枝将会梳理有关静态代理、动态代理的区别以及两种实现动态代理模式的方式。希望能对有需要的小伙伴有帮助~~~ 文章目录 前言 一、静态代理 二…

自定义Dynamics 365实施和发布业务解决方案 - 1. 准备工作

在当前的商业世界中,竞争每时每刻都在加剧每个企业都必须找到在竞争中保持领先的直观方法。其中之一企业面临的主要挑战是在以便为客户提供更好的服务。在这样一个竞争激烈、要求苛刻的时代环境中,对客户关系管理软件的需求是正在增加。 Dynamics 365的CE功能强大且适应性强…

使用JS实现一个简单的观察者模式(Observer)

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 手撸Observer⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领…

MySQL数据库——存储引擎(1)-MySQL体系结构、存储引擎简介

目录 MySQL体系结构 连接层 服务层 引擎层 存储层 存储引擎简介 概念 语句 演示 下面开始学习进阶篇的第一个内容——存储引擎 分为四点学习&#xff1a; MySQL体系结构存储引擎简介存储引擎特点存储引擎选择 MySQL体系结构 连接层 最上层是一些客户端和链接服务&am…

小米和金山集团董事长雷军访问武汉:加大投资力度,深化务实合作

小米集团创始人雷军一行在9月6日到访了武汉&#xff0c;受到了当地政府的热情欢迎。武汉方面表示&#xff0c;小米、金山集团作为全球知名的企业集团&#xff0c;与武汉有着良好合作基础。未来&#xff0c;武汉希望小米、金山集团持续深耕武汉&#xff0c;加大投资力度&#xf…

主页整理:8月1日---9月10日

目录 8月1日17点 8月1日20点 8月3日13点 8月3日18点 8月15日19点 8月28日9点 8月28日18点 8月29日8点 8月29日9点 9月2日21点 9月5日17点 9月9日18点 9月10日7点 粉丝变化数 8月1日17点 8月1日20点 8月3日13点 8月3日18点 8月15日19点 8月28日9点 8月28日18点…

Element-ui container常见布局

1、header\main布局 <template> <div> <el-container> <el-header>Header</el-header> <el-main>Main</el-main> </el-container> </div> </template> <style> .el-header { …

日常开发小汇总(3)js类型判断

1.typeof 能判断出字符串、数字、方法和undefined&#xff0c;array、null、object判断不出 let num 1;let str "x";let fn function user(){}let arr [1,2]let obj {name:"zhangs"}let und;let nul null;console.log(typeof num) //numberconsole.l…

深度、广度优先遍历(邻接表)

#include<stdio.h> #include<stdlib.h> #include<iostream> #include<queue> #define MAXVEX 20 typedef char VertexType; using namespace std;//边表结点 typedef struct EdgeNode{int adjvex;struct EdgeNode *next; }EdgeNode;//顶点结点 typedef…

Spring Cloud:构建微服务的最佳实践

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

手机木马远程控制复现

目录 目录 前言 系列文章列表 渗透测试基础之永恒之蓝漏洞复现http://t.csdn.cn/EsMu2 思维导图 1&#xff0c;实验涉及复现环境 2,Android模拟器环境配置 2.1,首先从官网上下载雷电模拟器 2.2,安装雷电模拟器 2.3, 对模拟器网络进行配置 2.3.1,为什么要进行配置…

vagrant 虚拟机扩容磁盘

vagrant 虚拟机扩容磁盘 修改配置安装插件存储扩容 修改配置 参考博客:https://blog.csdn.net/marina_1/article/details/122238721 vagrant 版本 PS D:\vagrant\workplace\node2> vagrant --version Vagrant 2.3.7修改vagrant虚拟机配置文件Vagrantfile&#xff0c;添加磁…

互斥锁,自旋锁,读写锁

目录 互斥体 互斥锁 属性 使用流程&#xff08;相关API函数&#xff09; 互斥锁初始化和销毁的函数原型&#xff1a; 互斥锁的上锁和解锁的函数原型为&#xff1a; pthread_mutex_init() 定义 函数原型 返回值 初始化 pthread_mutex_destroy() 定义 函数原型 pt…

EasyAVFilter代码示例之将视频点播文件转码成HLS(m3u8+ts)视频点播格式

以下是一套完整的视频点播功能开发源码&#xff0c;就简简单单几行代码&#xff0c;就可以完成原来ffmpeg很复杂的视频点播转码调用流程&#xff0c;而且还可以集成在自己的应用程序中调用&#xff0c;例如java、php、cgo、c、nodejs&#xff0c;不需要再单独一个ffmpeg的进程来…

mongodb7.0安装全过程详解

mongodb安装全过程详解 安装mongodb环境安装mongodb shell 安装mongodb环境 进入mongodb官网下载社区版服务器 点击跳转下载地址 选择适应自己系统的版本&#xff0c;然后下载 下图为下载后的文件&#xff0c;双击进行安装 需要注意的两个点 ①选择完整安装 ②不下载mongo…

C语言深入理解指针(非常详细)(五)

目录 回调函数qsort使用举例qsort函数的模拟实现sizeof和strlen的对比sizeofstrlensizeof和strlen的对比一道关于sizeof的题 回调函数 回调函数就是一个通过函数指针调用的函数 如果你把函数的指针&#xff08;地址&#xff09;作为参数传递给另一个函数&#xff0c;当这个指…

状态管理Pinia

Vue3 状态管理 - Pinia 1. 什么是Pinia Pinia 是 Vue 的专属的最新状态管理库 &#xff0c;是 Vuex 状态管理工具的替代品 2. 手动添加Pinia到Vue项目 后面在实际开发项目的时候&#xff0c;Pinia可以在项目创建时自动添加&#xff0c;现在我们初次学习&#xff0c;从零开始…

DockerFile打包项目实战解析,一文读懂dockerfile打包

文章目录 一、简介二、DockerFile基础1. DockerFile的作用和特点2. DockerFile语法介绍3. 基本指令&#xff1a;FROM、RUN、COPY、ADD等 三、编写具体项目使用DockerFile四、构建镜像1.项目构建默认命令dockerfile文件非默认命令dockerfile文件小知识 2. 使用ENTRYPOINT和CMD指…