C++学习-入门到精通-【7】类的深入剖析

发布于:2025-05-18 ⋅ 阅读:(21) ⋅ 点赞:(0)

C++学习-入门到精通-【7】类的深入剖析



一、Time类的实例研究

Time.h

#pragma once // case 1,用于“包含防护”,使得该头文件不会被其他源头文件多次包含
// case 2, 使用下面的语句能达到同样的效果
// #ifndef TIME_H
// #define TIME_H
//		....
// #endif
// or
//#if !define(TIME_H)
//#define TIME_H
//
//#endif

class Time {
public:
	Time(); // 构造函数
	void setTime(int, int, int); // 设置时间
	void printUniversal() const; // 打印universal-time format的时间 -- 格林威治时间格式
	void printStandard() const; // 打印standard-time format的时间 -- 标准时间格式
private:
	unsigned int hour;
	unsigned int minute;
	unsigned int second;
};

Time.cpp

#include "Time.h"
#include <stdexcept>
#include <iostream>
#include <iomanip>
using namespace std;

Time::Time()
	:hour(0), minute(0), second(0)
{

}

void Time::setTime(int h, int m, int s)
{
	if ((h >= 0 && h < 24) &&
		(m >= 0 && m < 60) &&
		(s >= 0 && s < 60))
	{
		hour = h;
		minute = m;
		second = s;
	}
	else
	{
		throw invalid_argument(
			"hour,minute and/or second was out of range."
		);
	}
}

void Time::printUniversal()const
{
	cout << setfill('0') << setw(2) << hour << ":"
		<< setw(2) << minute << ":"
		<< setw(2) << second << endl;
}

void Time::printStandard()const
{
	cout << ((hour == 0 || hour == 12) ? 12 : (hour % 12)) << ":"
		<< setfill('0') << setw(2) << minute << ":"
		<< setw(2) << second 
		<< (hour < 12 ? " AM" : " PM") << endl;
}

在这里插入图片描述

在这里插入图片描述

上面代码的set成员函数中使用了一个在头文件中定义的invalid_argument类,定义了一个该类的对象用以在函数的实参不合法时抛出一个类型为invalid_argument的异常。客户代码中可以使用之前使用过的try...catch语句来捕获该异常。跟在类名称后面的圆括号表示对该类的构造函数的一个调用,其中我们指定一个用户自定义的错误信息字符串。在异常对象被创建之后,此throw语句立即终止函数setTime,然后异常返回到尝试设置时间的代码处。

在printUniversal函数中使用流操纵符setfill,它用于指定当输出位宽大于输出整数值中数字个数时,所显示的填充字符。因为默认情况下数的输出为右对齐,所以填充字符出现在数中数字的左边。当使用left设置为左对齐时,填充字符出现在右边。在这段代码中,如果hour为2,则输出02。注意setfill是一个黏性设置。

提示
每一个黏性设置在不需要之后,应该手动将它恢复为以前的设置,以防出现预期之外的错误。

上面的成员函数都是在类定义的外部定义的。它们通过二元作用域分辨运算符“绑定”到该类,这样的成员函数仍在该类的作用域中。
但是当成员函数定义在类定义的体内时,该成员函数被隐式地声明为inline的,但是它到底是不是内联由编译器决定。

大家可以发现上面的两个print函数都没有参数,但是它们都知道要打印的数据是什么——数据成员,成员函数是有权访问同一个类的数据成员的。所以在面向对象语言中,使用成员函数可以减少传递错误参数、错误的参数类型或错误的参数个数的可能性。

test.cpp

#include <iostream>
#include <stdexcept>
#include "Time.h"

using namespace std;

int main()
{
	Time t; // 实例化一个Time类的对象

	cout << "The initial Universal time is ";
	t.printUniversal();
	cout << "\nThe initial Standard time is ";
	t.printStandard();

	t.setTime(13, 27, 6);

	cout << "Universal time after setTime is ";
	t.printUniversal();
	cout << "\nStandard time after setTime is ";
	t.printStandard();

	try
	{
		t.setTime(99, 99, 99);
	}
	catch (invalid_argument& e)
	{
		cout << "Exception: " << e.what() << endl;
	}

	cout << "\n\nAfter attempting invalid settings: "
		<< "\nUniversal time is ";
	t.printUniversal();
	cout << "\nStandard time is ";
	t.printStandard();

	cout << endl;
}

运行结果:
在这里插入图片描述
在上面的客户代码中,我们尝试使用setTime函数设置一个无效时间99:99:99,从结果中可以看到setTime函数抛出了invalid_argument异常,并在客户代码中的try...catch语句块中处理了该异常。由该异常的what成员函数打印异常的错误信息。

二、组成和继承

类通常不需要从头开始创建。类可以将其他类的对象包含进来作为其成员,或者由其他能为该类提供可以使用的属性和行为的类派生而来。

包含其他类的对象作为类的成员称为组成,或者称为聚合;
从已有的类派生出新的类称为继承。

对象的大小
对象只包含类中的数据(一些数据成员,static const的数据成员并不保存在对象中,直接保存在类中)。编译器只创建独立于类的所有对象的一份成员函数的副本。该类的所有对象共享这份副本。每个对象只需保存自己的类数据的副本即可。因为在对象之间这些数据是不同的,但是函数代码是不可修改的,所以可以被类的不同对象共享。

三、类的作用域和类成员的访问

类的数据成员和成员函数属于该类的作用域。

在类的作用域内,类的成员可以被类的所有成员函数直接访问,也可以通过名字引用。
在类的作用域外,public类成员通过对象的句柄之一而引用。句柄可以是对象名称、对象的引用或者对象的指针。

类作用域和块作用域

在成员函数中声明的变量具有块作用域,只有该函数知道它们。如果成员函数定义了与类作用域内变量同名的另一个变量,那么在函数中块作用域中的变量将隐藏类作用域中的变量。如果想要访问这样被隐藏起来的变量,可以通过在其名前加类名和二元作用域分辨运算符(::)的方法而访问。同样的被隐藏起来的全局变量可以通过一元作用域分辨运算符(::)来访问。

圆点成员选择运算符(.)和箭头成员选择运算符(->)

加点成员选择运算符(.)前面可以加对象名称或对象的引用,如此就可以访问该对象的成员。
箭头成员选择运算符(->)前面加对象的指针,则可以访问该对象的成员。

通过对象、引用、指针访问类的public成员

考虑一个含有一个public setBalance成员函数的Account类。

Account account;
Account &accountRef = account;
Account *accountPtr = &account;

程序员可以通过如下代码调用成员函数setBalance

// case 1
account.setBalance(123.45);
// case 2 
accountRef.setBalance(123.45);
// case 3
accountPtr->setBalance(123.45);

访问函数和工具函数

访问函数
访问函数可以读取或显示数据。访问函数另一个常见用法是测试条件是真是假,常常称这样的函数为判定函数。例如,任何容器类中都有的isEmpty函数就是判定函数。

工具函数
工具函数(也称助手函数),是一个用来支持类的其他成员操作的private成员函数。它们是用来给成员函数实现功能提供服务的,并不希望被类的客户所使用,所以需要声明为private。

四、具有默认实参的构造函数

Time.h

class Time {
public:
	Time(int = 0, int = 0, int = 0); // 有三个默认实参的构造函数
	void setTime(int, int, int); // 设置时间
	void setHour(int); // 设置小时(在有效性检查之后)
	void setMinute(int); // 设置分钟(在有效性检查之后)
	void setSecond(int); // 设置秒数(在有效性检查之后)

	unsigned int getHour() const;
	unsigned int getMinute() const;
	unsigned int getSecond() const;

	void printUniversal() const; // 打印universal-time format的时间
	void printStandard() const; // 打印standard-time format的时间
private:
	unsigned int hour;
	unsigned int minute;
	unsigned int second;
};

Time.cpp

#include <iostream>
#include <stdexcept>
#include <iomanip>
#include "Time.h"

using namespace std;

Time::Time(int h, int m, int s)
{
	setTime(h, m, s);
}

void Time::setTime(int h, int m, int s)
{
	setHour(h);
	setMinute(m);
	setSecond(s);
}

void Time::setHour(int h)
{
	if (h >= 0 && h < 24)
	{
		hour = h;
	}
	else
	{
		throw invalid_argument("hour must be 0-23.");
	}
}

void Time::setMinute(int m)
{
	if (m >= 0 && m < 60)
	{
		minute = m;
	}
	else
	{
		throw invalid_argument("minute must be 0-59.");
	}
}

void Time::setSecond(int s)
{
	if (s >= 0 && s < 60)
	{
		second = s;
	}
	else
	{
		throw invalid_argument("second must be 0-59.");
	}
}

unsigned int Time::getHour() const
{
	return hour;
}

unsigned int Time::getMinute() const
{
	return minute;
}

unsigned int Time::getSecond() const
{
	return second;
}

void Time::printUniversal() const
{
	cout << setfill('0') << setw(2) << getHour() << ":"
		<< setw(2) << getMinute() << ":"
		<< setw(2) << getSecond() ;
}

void Time::printStandard() const
{
	cout << setw(2) << ((getHour() == 0 || getHour() == 12) ?  0 : (getHour() % 12))<< ":"
		<< setfill('0') << setw(2) << getMinute() << ":"
		<< setw(2) << getSecond() << (getHour() < 12 ? "AM" : "PM");
}

上面代码中的构造函数有三个默认实参。而一个类中只能有一个默认所有实参的构造函数,因为默认所有实参的构造函数是一个默认构造函数,而一个类只能有一个默认构造函数。

其中构造函数并没有直接访问这些private的数据成员,而是使用hour、minute、second各自的设置和获取函数。大家可能会疑惑同一个类中的成员函数和数据成员不是在同一个作用域下吗,应该是可以直接访问的,为何要用一个函数来访问这些成员呢?这不是多此一举吗。

举个例子:
当前的setTime函数是接收3个参数来设置时间的,现在如果要进行修改,只使用一个int类型参数(记录从午夜0点开始的经过的秒数)来设置时间。此时就只需要修改那些直接访问数据成员的函数,其他的函数(比如printUniversal和printStandard)就不需要修改,这样就能便于代码维护且降低改变类的实现时产生编程错误的可能性。

测试代码 test.cpp

#include "Time.h"
#include <iostream>
#include <stdexcept>

using namespace std;

int main()
{
	Time t1; // 全部使用默认实参
	Time t2(1); // 传入一个实参(第一个)
	Time t3(21,34); // 传入两个实参(前二个)
	Time t4(12,45,14); // 传入三个实参

	cout << "Constructed with:\n\nt1: all arguments defaulted\n"
		<< "Universal time is ";
	t1.printUniversal();
	cout << "\nStandard time is  ";
	t1.printStandard();
	
	cout << "\n\nt2: hour specified; other arguments defaulted\n"
		<< "Universal time is ";
	t2.printUniversal();
	cout << "\nStandard time is  ";
	t2.printStandard();

	cout << "\n\nt3: hour and minute specified; second defaulted\n"
		<< "Universal time is ";
	t3.printUniversal();
	cout << "\nStandard time is  ";
	t3.printStandard();

	cout << "\n\nt4: all arguments specified\n"
		<< "Universal time is ";
	t4.printUniversal();
	cout << "\nStandard time is  ";
	t4.printStandard();

	// 试图用一个无效时间初始化Time类型的变量
	try
	{
		Time t5(24, 66, 88);
	}
	catch (invalid_argument& e)
	{
		cerr << "\n\nException while initializing t5: " << e.what() << endl;
	}
}

运行结果:
在这里插入图片描述

上面的测试代码中几个变量的初始化还可以使用之前我们提到过的列表初始化器,例如,上面的代码可以用下面的语句进行初始化:

	Time t2{1}; 
	Time t3{21, 34}; 
	Time t4{12, 45, 14};
	// or 
	Time t2 = { 1 };
	Time t3 = { 21, 34 };
	Time t4 = { 12, 45, 14 };

上面的代码中程序员更倾向于使用没有等号的写法。

重载的构造函数和委托构造函数

在之前的章节中我们介绍过了重载函数。类的构造函数和成员函数也可以被重载。通常,重载的构造函数允许用不同类型和(或)数量的实参初始化对象。如果要重载构造函数需要在类的定义中为构造函数提供相应的函数原型,并为各个重载的版本提供独立的构造函数的定义。这同样适用于成员函数。

对于上面代码Time类的构造函数,它有三个形参并均有默认实参。但除了上面使用方法之外,还可以将这个构造函数定义为函数原型如下的四个重载的构造函数。

Time(); // 有3个默认实参的构造函数
Time(int); // 指定小时的构造函数
Time(int, int); // 指定小时和分钟的构造函数
Time(int, int, int); // 所有参数均指定的构造函数

正如类的成员函数可以调用其他成员函数来实现功能,C++11中构造函数也可以调用同一个类的其他构造函数来实现功能。这样构造函数被称为委托构造函数,它将自己的工作委托给其他构造函数。这种机制对于重载的构造函数具有相同的代码时很有用。而在此之前的解决方法是将这些相同的代码定义在一个private的工具函数中,供所有的构造函数调用。

上面声明的前三个构造函数都可以将工作委托给最后一个构造函数。

Time::Time()
	: Time(0, 0, 0)
{
}

Time::Time(int h)
	: Time(h, 0, 0)
{
}

Time::Time(int h, int m)
	: Time(h, m, 0)
{
}

注意:使用委托构造函数时,不可以使用初始化列表初始化其他的数据成员,所有的数据成员只能通过委托的构造函数进行初始化。
在这里插入图片描述

五、析构函数

析构函数是另一种特殊的成员函数。类的析构函数的名字是在类名之前加上一个~字符,该字符是按位取反运算符。在某种意义上析构函数和构造函数互补。析构函数不接收任何参数,也没有返回任何值。

当对象撤销时,类的析构函数会被隐式的调用。例如,当程序的执行离开类的实例化自动对象所在的作用域时,自动对象就会撤销,这时就会发生析构函数的隐式调用。实际上析构函数本身并不释放对象占用的内存空间,它只是在系统回收对象的内存空间之前执行扫尾工作。

之前的所有代码中都没有显式的定义一个析构函数,但是每个类都会有一个析构函数。如果程序员没有显式的定义,那么编译器将会生成一个“空的”析构函数。而这些析构函数到底有什么用呢?隐式生成的析构函数又有什么呢?在下一章我们会对一些包含动态分配内存的类或者使用其他系统资源的类构建合适的析构函数,在组成和继承中对隐式生成的析构函数的作用进行说明。

六、何时调用构造函数和析构函数

编译器隐式的调用构造函数和析构函数。这些函数的调用发生的顺序由执行过程进行和离开对象实例化的作用域的顺序决定。一般而言,析构函数的调用顺序和相应的构造函数的调用顺序相反。但是对象的存储类别可以改变调用构造函数的顺序。

下面我们就从一个例子中看一下构造函数和析构函数的执行顺序:

CreateAndDestrory.h

#pragma once

#include <string>

class CreateAndDestory
{
public:
	CreateAndDestory(int, std::string); // 构造函数,接收两个参数,一个表示对象id,一个是描述语句
	~CreateAndDestory(); // 析构函数
private:
	int objectID;
	std::string message;
};

CreateAndDestory.cpp

#include <iostream>
#include <string>
#include "CreateAndDestory.h"

using namespace std;

CreateAndDestory::CreateAndDestory(int id, string messageStr)
	: objectID(id), message(messageStr)
{
	// 输出该实例化对象的信息
	cout << "objectID is " << objectID 
		<< "\tconstructor runs\t" << message 
		<< endl;
}

CreateAndDestory::~CreateAndDestory()
{
	// 当ID等于1或6时,输出一个换行
	cout << ((objectID == 1 || objectID == 6) ? "\n" : "");

	cout << "objectID " << objectID 
		<< "\tdestructor runs\t" << message << endl;
}

test.cpp

#include <iostream>
#include "CreateAndDestory.h"

using namespace std;

CreateAndDestory first(1, "(global before main)");

void create();

int main()
{
	cout << "\nMAIN FUNCTION: EXECUTION BEGINS" << endl;
	CreateAndDestory second(2, "(local automatic in main)");
	static CreateAndDestory third(3, "(local static in main)");

	create();

	cout << "\nMAIN FUNCTION: EXECUTION RESUMES" << endl;
	CreateAndDestory forth(4, "(local automatic in main)");
	cout <<	"\nMAIN FUNCTION: EXECUTION ENDS" << endl;
}

void create()
{
	cout << "\nCREATE FUNCTION: EXECUTION BEGINS" << endl;
	CreateAndDestory fifth(5, "(local automatic in create)");
	static CreateAndDestory sixth(6, "(local static in create)");
	CreateAndDestory seventh(7, "(local automatic in create)");
	cout << "\nCREATE FUNCTION: EXECUTION ENDS" << endl;
}

运行结果:
在这里插入图片描述

从结果中可以看出构造函数和析构函数的调用顺序确实和上述的顺序一样,按执行顺序先后对这些对象进行实例化,如果没有存储类别的变动,析构函数与构造函数的调用相反,上面的代码中,id为1、3和6的存储类别是静态的,所以它们会在程序执行结束时才调用构造函数来进行内存释放之前的扫尾工作。而相同的存储类别的对象,它们的析构函数的调用顺序刚好和构造函数的调用顺序相反。

全局作用域内对象的构造函数和析构函数

全局作用域内定义的对象的构造函数,在文件内任何其他函数(包括main函数)开始执行之前调用。当main函数执行结束时,相应的析构函数被调用。但是使用exit函数可以使用程序立即结束,不执行自动对象的析构函数。当程序检测到输入中有错误,或者程序要处理的文件不能打开时,常常使用这个函数来终止程序。
在这里插入图片描述

局部对象的构造函数和析构函数

当程序执行到自动局部变量的定义处时,该对象的构造函数被调用;当程序执行离开对象的作用域时,相应的析构函数被调用。当程序每次进入或者离开自动对象的作用域时,自动对象的构造函数或者析构函数就会被调用。如果程序的终止是由调用exit函数或者abort函数而完成,那么自动对象的析构函数将不会被调用。

static局部对象的构造函数和析构函数

static局部对象的构造函数只被调用一次(只在程序第一次执行到该对象的定义处被调用)。而相应的析构函数的调用发生在main函数结束或者程序调用exit函数时。如果调用abort函数终止程序,那么static对象的析构函数将不会被调用。

在这里插入图片描述

七、Time类实例研究:微妙的陷阱——返回private数据成员的引用或指针

对象的引用就是该对象名称的别名。因此它是一个左值可以在赋值语句的左边使用。下面我们对之前的Time类进行一些简化,将其作为一个错误示例来展示。

Time.h

class Time {
public:
	Time(int = 0, int = 0, int = 0); // 有三个默认实参的构造函数
	void setTime(int, int, int);
	unsigned int getHour() const;
	unsigned int& badsetHour(int);
private:
	unsigned int hour;
	unsigned int minute;
	unsigned int second;
};

Time.cpp

#include "Time.h"
#include <iostream>
#include <stdexcept>

using namespace std;

Time::Time(int h, int m, int s)
{
	setTime(h, m, s);
}

void Time::setTime(int h, int m, int s)
{
	if (h >= 0 && h < 24)
	{
		hour = h;
	}
	else
	{
		throw invalid_argument("hour was out of range.");
	}

	if (m >= 0 && m < 60)
	{
		minute = m;
	}
	else
	{
		throw invalid_argument("minute was out of range.");
	}

	if (s >= 0 && s < 60)
	{
		second = s;
	}
	else
	{
		throw invalid_argument("second was out of range.");
	}
}

unsigned int Time::getHour() const
{
	return hour;
}

unsigned int& Time::badsetHour(int h)
{
	if (h >= 0 && h < 24)
	{
		hour = h;
	}
	else
	{
		throw invalid_argument("hour was out of range.");
	}

	return hour;
}

test.cpp

#include <iostream>
#include "Time.h"

using namespace std;

int main()
{
	Time t(11, 23, 34);

	unsigned int &hourRef = t.badsetHour(20);
	cout << "Hour before modification: " << t.getHour() << endl;
	hourRef = 30; // 为小时设置一个无效的值
	cout << "Hour after modification: " << t.getHour() << endl;
	
	// 将badsetHour函数返回的引用作为一个左值使用
	t.badsetHour(12) = 75;
	cout << "\n\n**************************\n"
		<< "POOR PROGRAMMING PRCTICE!!!!!\n"
		<< "t.badsetHour(12) as a lvalue, invalid hour: "
		<< t.getHour() 
		<< "\n**************************" << endl;
}

运行结果:

在这里插入图片描述

从上面的结果中可以看到在程序中我们直接使用了类的private的数据成员,这破坏了类的封装性。同样的使用指针一样会导致在main中能够直接访问到类的private类型的数据成员。

八、使用=运算符对类对象进行赋值

在C++中可以使用赋值运算符进行两个相同类型的对象之间的赋值。默认情况下,这样的赋值通过逐个成员赋值的方式进行,即赋值运算符右边对象的每个数据成员逐一赋值给赋值运算符左边对象中的同一数据成员。但是,当类中存在动态分配内存的数据成员时,使用这种默认的赋值方法会产生严重的错误。这种错误该如何解决我们会在下一章节中介绍。

下面给出使用逐个成员赋值的例子。

Date.h

#pragma once

class Date
{
public:
	// month, day, year
	Date(int = 1, int = 1, int = 1900);
	void print() const;
private:
	int month;
	int day;
	int year;
};

Date.cpp

#include <iostream>
#include "Date.h"

using namespace std;

Date::Date(int m, int d, int y)
	: month(m), day(d), year(y)
{

}

void Date::print() const
{
	cout << month << "/" 
		<< day << "/"
		<< year << endl;
}

test.cpp

#include <iostream>
#include "Date.h"

using namespace std;

int main()
{
	Date date1(7, 5, 2077);
	Date date2;

	cout << "date1 = ";
	date1.print();
	cout << "date2 = ";
	date2.print();

	date2 = date1;

	cout << "\nAfter default memberwise assignment." << endl;
	cout << "date2 = ";
	date2.print();
}

运行结果:

在这里插入图片描述

九、const对象和const成员函数

声明为const的对象是不可修改的,任何尝试修改一个const对象的操作都将导致编译错误。

提示:任何修改const对象的企图在编译时就会被发现,而不是直到执行时才会导致错误

对于const对象,C++编译器不允许进行成员函数的调用,除非调用的这个成员函数本身也声明为const。这一点是非常严格的,即使调用的函数并不会修改对象也不行,这也是我们为什么要将不修改对象的成员函数声明为const的原因。

在这里插入图片描述

注意一个声明为const的对象是可以使用构造函数来进行初始化的,虽然构造函数无法声明为const,且当构造函数中调用一个非const的函数来初始化对象时,也是被允许的。

十、组成:对象作为类的成员

一个闹钟类的对象是需要知道什么时候让闹钟响起的,因此可以将一个Time类的对象纳入闹钟类的定义中将其作为它的一个成员。这种功能被称为“组成”,有时也称为“有”关系。一个类可以将其他类的对象作为成员。

下面将介绍构造函数如何通过成员初始化列表完成将参数传递给对象成员构造函数的任务。

成员对象是以在类的定义中的声明顺序(不是在构造函数的成员初始化器列表中列出的顺序)且在包含它们的对象(有时称为宿主对象)构造之前建立。也就是一个对象会在其包含的所有数据成员创建之后才被创建。

下面使用Date类及Employee类演示组成。

Date.h

#pragma once

class Date
{
public:
	// 声明为static const的数据成员可以在类定义中进行初始化
	// 该数据成员存储一年中的有多少个月
	static const unsigned int monthPerYear = 12;
	Date(int = 1, int = 1, int = 1900);
	void print() const;
	~Date(); // 析构函数
private:
	unsigned int month;
	unsigned int day;
	unsigned int year;

	// 声明一个工具函数,用于判断该月份对应的日期是否合法
	unsigned int checkDay(int) const;
};

Date.cpp

#include <iostream>
#include <stdexcept>
#include <array>
#include "Date.h"

using namespace std;

Date::Date(int m, int d, int y)
{
	if (m > 0 && m <= monthPerYear)
	{
		month = m;
	}
	else
	{
		throw invalid_argument("month should be 1-12.");
	}

	year = y;

	day = checkDay(d);

	cout << "Date object constructor for date ";
	print();
	cout << endl;
}

void Date::print() const
{
	cout << month << "/"
		<< day << "/"
		<< year;
}

Date::~Date()
{
	cout << "Date object destructor for date ";
	print();
	cout << endl;
}

unsigned int Date::checkDay(int d) const
{
	static const array<int, monthPerYear + 1> daysPerMonth =
	{ 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

	if (d > 0 && d <= daysPerMonth[month])
	{
		return d;
	}

	// 月份为2月,且日期为29号,此时判断是否为闰年
	if (month == 2 && d == 29 &&
		(year % 400 == 0 ||
			(year % 4 == 0 && year % 100 != 0)))
	{
		return d;
	}

	// 不符合上述两种情况就是参数d不合法
	throw invalid_argument("Invalid day for current month and year.");
}

Employee.h

#pragma once

#include <string>
#include "Date.h"

class Employee
{
public:
	// 包含firstName、lastName、birthDate、hireDate
	Employee(const std::string &, const std::string &,
		const Date &, const Date &);
	void print() const;
	~Employee();
private:
	std::string firstName;
	std::string lastName;
	Date birthDate;
	Date hireDate;
};

Employee.cpp

#include <iostream>
#include "Employee.h"

using namespace std;

// 使用const的引用,可以避免参数复制导致的开销,且声明为const可以防止这些实参被修改
Employee::Employee(const string& first, const string& last, const Date& birth, const Date& hire)
	:firstName(first),
	lastName(last),
	birthDate(birth),
	hireDate(hire)
{
	cout << "Employee object constructor: "
		<< firstName << ' ' << lastName << endl;
}

void Employee::print() const
{
	cout << lastName << ", " << firstName << ", "
		<< "Hire: ";
	hireDate.print(); // 调用Date类的成员函数
	cout << ", Birthday: ";
	birthDate.print();
	cout << endl;
}

Employee::~Employee()
{
	cout << "Employee object destructor: "
		<< lastName << ' ' << firstName << endl;
}

注意在上面的Employee类的构造函数中传入两个Date类的实参,是被传递给了对应对象的构造函数——复制构造函数。大家可以发现在Date类中,我们并没有定义一个接收Date类对象的参数的构造函数,所以编译器会给每个类提供一个默认的复制构造函数,该函数将构造函数的参数对象的每个成员复制给将要初始化的对象的相应成员。在之后一章我们会介绍如何定义一个自己的复制构造函数。

test.cpp

#include <iostream>
#include "Employee.h"

using namespace std;

int main()
{
	Date birth(8, 17, 2005);
	Date hire(3,14, 2024);
	Employee manager("Bob", "Blue", birth, hire);

	cout << endl;
	manager.print();
}

运行结果:

在这里插入图片描述

可以看到在程序结束时,输出了四条Date类对象的销毁信息,说明包含其他类的对象的对象在初始化时的确会调用一次构造函数(复制构造函数)。

还记得我们之前说过的使用成员初始化器列表进行初始化和在构造函数体中进行初始化的区别吗?

如果对象的构造函数没有在初始化器列表中初始化,而是在函数体中进行初始化。实际上,该对象会进行两次初始化,先调用默认的构造函数,对所有数据成员进行初始化,然后再执行构造函数中的语句进行初始化。

所以这就存在问题:
当程序员显式定义了构造函数,但是没有定义默认的构造函数,在不使用初始化器列表进行初始化时,此时程序就会调用默认的构造函数来进行初始化,因为程序员自己定义了构造函数,所以编译器并不会生成默认的构造函数,此时出现编译错误;

十一、friend函数和friend类

类的friend函数(友元函数)在类的作用域之外定义,却具有访问类的非public(以及public)成员的权限。单独的函数、整个类或其他类的成员函数都可以被声明为另一个类的友元。

friend的声明

在类定义中函数原型前加保留字friend,就将函数声明为该类的友元。
要将一个类中的所有成员函数声明为另一个类的友元,应该在另一个类的定义中加入一条声明。例如现在要将ClassTwo中所有的成员函数声明为ClassOne的友元,应该在ClassOne的声明中加入friend class ClassTwo;

友元关系是授予的不是索取的。也就是说,若使类B成为类A的友元,类A必须显式地声明类B是它的友元。另外,友元关系既不对称也不传递。即如果类A是类B的友元,类B是类C的友元,并不能得出类B是类A的友元、类C是类B的友元的结论,也不能得出类A是类C的友元的结论。

使用friend函数修改类的private数据

下面例子中定义了一个友元函数setX来设置Count类中的private数据x的值。友元声明可以出现在类的任何地方,但是习惯于将友元设置在类的定义最上面,甚至出现在pubilic成员函数声明之前。

Count.h

#pragma once

class Count
{
	// Count类的友元函数
	friend void setX(Count &, int);
public:
	Count();
	void print() const;
private:
	int x;
};

Count.cpp

#include <iostream>
#include "Count.h"

using namespace std;

Count::Count()
	:x(0) // 将x初始化为0
{
	// empty body
}

void Count::print() const
{
	cout << x << endl;
}

void setX(Count& count, int val)
{
	count.x = val;
}

test.cpp

#include <iostream>
#include "Count.h"

using namespace std;

int main()
{
	Count counter;

	cout << "counter.x after instantiation: ";
	counter.print();

	setX(counter, 8);

	cout << "counter.x after call to setX friend function: ";
	counter.print();
}

运行结果:

在这里插入图片描述

重载友元函数

可以指定重载函数为类的友元。每个打算成为友元的重载函数必须在类的定义时显式地声明为类的一个友元。

提示:即使一个友元函数的原型在类定义内出现,它仍然不是成员函数。

十二、使用this指针

从上面的代码中我们可以看到对象的成员函数可以操作对象的数据成员。那么在调用成员函数时,它们是如何知道是哪个对象的数据成员要被操作呢?

每个对象都可以使用一个被称为this(一个C++的保留字)的指针来访问自己的地址。对象的this指针并不是对象的一部分,也就是this指针占用的内存大小不会反映到在对象进行sizeof运算得到的结果中。相反,this指针作为一个隐式的参数(被编译器)传递给对象的每个非static成员函数。

使用this指针来避免名字冲突

对象隐式地使用this指针或者显式地使用this指针来引用它们的数据成员和成员函数。一个常见的this指针的明确应用就是用来避免类数据成员和成员函数的参数之间的名字冲突。例如,在Time类中,成员函数setHour的形参名为hour与数据成员hour同名,在该函数中使用hour = hour;进行hour数据成员的赋值时,实际上执行的语句是this->hour = hour;,它明确的指出前面的hour是类的数据成员,且由于当局部变量和作用域比它更大的变量在该作用域使用时,局部变量会隐藏作用域更的变量,使用this指针使得该成员函数可以访问该数据成员。

所以为了代码的简洁和可维护性,在设置形参名时,不要让本地变量名称隐藏数据成员。

this指针的类型

this指针的类型取决于对象的类型及使用this的成员函数是否被声明为const。例如在Employee类的非const成员函数中,this指针具有的类型为Employee * const(指针指向的地址不能发生改变,该地址对应的内容可以改变)。在Employee类的const成员函数中,this指针具有类型为const Employee * const(地址和地址对应的内容都不能发生改变)。

隐式和显式的使用this指针来访问对象的数据成员

下面给出一个例子来演示隐式和显式的使用this指针。

Test.h

#pragma once

class Test
{
public:
	explicit Test(int = 0);
	void print() const;
private:
	int x;
};

Test.cpp

#include <iostream>
#include "Test.h"

using namespace std;

Test::Test(int val)
	: x(val)
{
	// empty body
}

void Test::print() const
{
	cout << "        x = " << x << endl;
	cout << "  this->x = " << this->x << endl;
	cout << "(*this).x = " << (*this).x << endl;
}

test.cpp

#include <iostream>
#include "Test.h"

using namespace std;

int main()
{
	Test testObject(12);

	testObject.print();
}

运行结果:

在这里插入图片描述

使用this指针使串联的函数调用成员成为可能

this指针的另一种用法是使串联的成员函数调用成为可能,也就是多个函数在同一个语句中被调用。下面对Time类进行修改,使得设置函数setTime、setHour、setMinute和setSecond返回一个对Time对象的引用,以便进行串联的成员函数调用。上述的成员函数都在其函数体的最后一条语句返回*this,返回类型为Time &

Time.h

class Time {
public:
	Time(int = 0, int = 0, int = 0); // 有三个默认实参的构造函数

	Time& setTime(int, int, int); // 设置时间
	Time& setHour(int); // 设置小时(在有效性检查之后)
	Time& setMinute(int); // 设置分钟(在有效性检查之后)
	Time& setSecond(int); // 设置秒数(在有效性检查之后)

	unsigned int getHour() const;
	unsigned int getMinute() const;
	unsigned int getSecond() const;

	void printUniversal() const; // 打印universal-time format的时间
	void printStandard() const; // 打印standard-time format的时间
private:
	unsigned int hour;
	unsigned int minute;
	unsigned int second;
};

Time.cpp

#include <iostream>
#include <stdexcept>
#include <iomanip>
#include "Time.h"

using namespace std;

Time::Time(int h, int m, int s)
{
	setTime(h, m, s);
}

Time& Time::setTime(int h, int m, int s)
{
	setHour(h);
	setMinute(m);
	setSecond(s);
	
	return *this;
}

Time& Time::setHour(int h)
{
	if (h >= 0 && h < 24)
	{
		hour = h;
	}
	else
	{
		throw invalid_argument("hour must be 0-23.");
	}

	return *this;
}

Time& Time::setMinute(int m)
{
	if (m >= 0 && m < 60)
	{
		minute = m;
	}
	else
	{
		throw invalid_argument("minute must be 0-59.");
	}

	return *this;
}

Time& Time::setSecond(int s)
{
	if (s >= 0 && s < 60)
	{
		second = s;
	}
	else
	{
		throw invalid_argument("second must be 0-59.");
	}

	return *this;
}

unsigned int Time::getHour() const
{
	return hour;
}

unsigned int Time::getMinute() const
{
	return minute;
}

unsigned int Time::getSecond() const
{
	return second;
}

void Time::printUniversal() const
{
	cout << setfill('0') << setw(2) << getHour() << ":"
		<< setw(2) << getMinute() << ":"
		<< setw(2) << getSecond();
}

void Time::printStandard() const
{
	cout << ((getHour() == 0 || getHour() == 12) ? 12 : (getHour() % 12)) << ":"
		<< setfill('0') << setw(2) << getMinute() << ":"
		<< setw(2) << getSecond() << (getHour() >= 12 ? " PM" : " AM");
}

test.cpp

#include <iostream>
#include "Time.h"

using namespace std;

int main()
{
	Time t;

	// 串联调用
	t.setHour(18).setMinute(30).setSecond(22);

	cout << "Universal time: ";
	t.printUniversal();

	cout << "\nStandard time: ";
	t.printStandard();

	cout << "\n\nNew Standard time: ";
	t.setTime(20, 20, 20).printStandard();
}

运行结果:

在这里插入图片描述

十三、stastic类成员

对于类的每个对象来说,一般都满足一条规则,即它们各自拥有类所有数据成员的一份副本。但是,有一个例外——static数据成员,这些变量仅有一份副本供类的所有对象共享。

使用类范围数据的动机

下面我们举个例子来说明,假设现在有一个关于火星人和虫族的游戏,当火星人意识到至少有5个火星人存在时,每个火星人将获得勇气buff,增加对虫族造成的伤害降低虫族对自己造成的伤害;当存在的火星人少于5个时,火星人获得胆怯debuff,每个火星人都会变成怯战蜥蜴,降低对虫族造成的伤害,增加虫族对自己造成的伤害。因此每个火星人都需要知道火星人的数量martianCount。我们可以将martianCount作为Martian类的每个实例的数据成员。如果真是这样做的话,每个Martian对象都将有一份独立的该数据成员的副本。每次创建新的Martian对象时,都不得不更新所有Martian对象的数据成员martianCount,这就需要每个Martian对象都具有或者可以访问内存中其他Martian对象的句柄。所以这些多余的副本将浪费空间,并且更新每份单独的副本也将浪费时间。为此,我们将martianCount声明为stastic。这样使得martianCount成为类范围的数据。每个Martian类对象都可以访问martianCount,就好像它就是这个类对象的数据成员一样,但是实际上只有一份副本由C++进行维护。这样就节省了空间。此外通过用Martian构造函数使static变量martianCount的值自增,通过Martian的析构函数使martianCount的值自减,从而节省时间。

静态数据成员的作用域和初始化

尽管类的static数据成员看上去就像是全局变量,但是它们只在类的作用域中起作用。静态数据成员必须被精确地初始化一次。基本类型的static数据成员默认情况下会被初始化为0。在C++11之前,static const的int或enum类型的数据成员能够在声明的时候进行初始化,而其他的静态数据成员必须在全局命名空间(类定义之外)中被定义和初始化。在C++11中,类内初始化能允许你在类定义中变量声明的位置初始化它。注意类类型的static数据成员(即static成员对象),如果这个类类型具有默认的构造函数,那么这样的数据成员无须初始化(进行初始化也会出错),因为它们的默认构造函数将会被调用。

注意仅声明为static的数据成员无法在类内进行初始化,必须是声明static const的数据成员才行

访问静态数据成员

类的private和protected的static成员通常通过类的public成员函数或者类的友元访问。即使没有任何类的对象存在时,类的static数据成员仍然存在。当没有类的对象存在时,要访问public的static数据成员可以在前面加上类名和二元作用域分辨运算符(::)来访问。比如,之前提到的martianCount是Martian类的一个public static数据成员,就可以使用Martian::martianCount来访问。

但是当没有类的对象存在,要访问private或protected的static类成员时,应提供public static类成员函数,并通过在函数名前面加上类名和二元作用域分辨运算符来调用该函数。每个static成员函数都是类的一项服务,而不是类的特定对象的一项服务。

即使不存在已实例化的类的对象,类的static数据成员和static成员函数仍存在并且可以使用。

静态数据成员的使用

下面对Employee类提供一个static的数据成员count用于记录雇员的人数。

Employee.h

#include <string>

class Employee
{
public:
	// 雇员的姓和名
	Employee(const std::string&, const std::string&);
	~Employee();
	std::string getFirstName() const;
	std::string getLastName() const;

	// 提供一个static的成员函数用于获取static的数据成员count
	unsigned int static getCount(); // 静态成员函数不可以使用类型限定符
private:
	std::string firstName;
	std::string lastName;

	// 该类的所有对象共享该数据成员
	static unsigned int count;
};

Employee.cpp

#include <string>
#include <iostream>
#include "Employee.h"

using namespace std;

// 初始化static成员count为0,该类定义时没有一个雇员,所以初始化为0
unsigned int Employee::count = 0;

// 定义static成员函数,用于获取static成员count,它作为成员函数可以直接访问所有数据成员
unsigned int Employee::getCount()
{
	return count;
}

Employee::Employee(const string& first, const string& last)
	: firstName(first),
	lastName(last)
{
	cout << "Employee constructor for " 
		<< getFirstName() << ' ' << getLastName() << " called" << endl;
	// 对count自增
	count++;
}

Employee::~Employee()
{
	cout << "Employee destructor for " 
		<< getFirstName() << ' ' << getLastName() << " called" << endl;
	// count自减
	count--;
}

string Employee::getFirstName() const
{
	return firstName;
}

string Employee::getLastName() const
{
	return lastName;
}

test.cpp

#include <iostream>
#include "Employee.h"

using namespace std;

int main()
{
	cout << "Number of employeess before instantiation of any objects is "
		<< Employee::getCount() << endl;

	{
		// 实例化两个Employee对象
		Employee e1("Bob", "Griffin");
		Employee e2("Alice", "Baker");

		cout << "Number of employees after objects are instantiated is "
			<< Employee::getCount() << endl;

		cout << "\n\nEmployee 1: "
			<< e1.getFirstName() << ' ' << e1.getLastName() << endl
			<< "Employee 2: "
			<< e2.getFirstName() << ' ' << e2.getLastName() << "\n\n";
	}

	cout << "Number of employees after objects are deleted is "
		<< Employee::getCount() << endl;
}

运行结果:

在这里插入图片描述

提示
static成员函数不具有this指针,因为static数据成员和static成员函数独立于类的任何对象而存在。this指针必须指向类的具体的对象。但是当static的成员函数被调用时,可能内存中并不存在该类的任何对象。