★ 了解面向对象程序设计思想
★ 掌握类的定义
★ 掌握对象的创建与使用
★ 理解C++中类的封装
★ 理解this指针的作用机制
★ 掌握自定义构造函数的定义与调用
★ 掌握构造函数的重载
★ 掌握含有类成员对象的构造函数的定义与调用
★ 掌握析构函数
★ 掌握拷贝构造函数的定义与调用
★ 理解深拷贝与浅拷贝的区别
★ 掌握const与static修饰类的成员
★ 掌握友元函数的定义与调用
★ 了解友元类的定义与使用
面向对象是程序开发领域中的重要思想,这种思想符合人类认识客观世界的逻辑,是当前计算机软件工程学的主流思想。C++在设计之初就是一门面向对象语言,了解面向对象程序设计思想对于学习C++开发至关重要。在面向对象中,类和对象是非常重要的两个概念,本章将针对面向对象中的类和对象进行详细的介绍。
面向对象是一种符合人类思维习惯的程序设计思想。现实生活中存在各种形态不同的事物,这些事物之间存在着各种各样的联系。在程序中使用对象映射现实中的事物,利用对象之间的关系描述事物之间的联系,这种思想就是面向对象。
面向过程是分析出解决问题所需要的步骤,然后用函数把这些步骤一一实现,使用的时候依次调用就可以了。面向对象不同于面向过程,它是把构成问题的事物按照一定规则划分为多个独立的对象,然后通过调用对象的方法解决问题。当然,一个应用程序会包含多个对象,通过多个对象的相互配合即可实现应用程序所需的功能,这样当应用程序功能发生变动时,只需要修改个别对象就可以了,使代码更容易维护。
面向对象程序设计思想有三大特征:封装、继承和多态。下面针对这三个特征进行简单的介绍。
1. 封装
封装是面向对象程序设计思想最重要的特征。封装就是隐藏,它将数据和数据处理过程封装成一个独立性很强的模块,避免外界直接访问对象属性而造成耦合度过高以及过度依赖。
封装是面向对象的核心思想,将对象的属性和行为封装起来,行为对外提供接口,不需要让外界知道具体的实现细节。例如,用户使用电脑,只需要通过外部接口连接鼠标、键盘等设备操作电脑就可以了,无须知道电脑内部的构成以及电脑是如何工作的。
2. 继承
继承主要描述的是类与类之间的关系,通过继承无须重新编写原有类,就能对原有类的功能进行扩展。例如,有一个交通工具类,该类描述了交通工具的特性和功能,而小汽车类不仅包含交通工具的特性和功能,还应该增加小汽车特有的功能,这时可以让小汽车类继承交通工具类,在小汽车类中单独添加小汽车特有的属性和方法就可以了。继承不仅增强了代码复用性,提高了开发效率,而且也为程序的维护提供了便利。
在软件开发中,继承使软件具有开放性、可扩充性,这是数据组织和分类行之有效的方法,它简化了类和对象的创建工作量,增强了代码的可重用性。
3. 多态
多态是指事物的多种表现形态。例如,上课铃声响起后,各科老师准备去不同的班级上课,上体育课的学生在操场站好了队等体育老师发布口令,上文化课的学生听到铃声后回到各自的班级,这就是多态。
在面向对象程序设计思想中,多态就是不同的对象对同一信息产生不同的行为。多态是面向对象技术中的一个难点,很多初学者都难以理解。面向对象的多态特性使得开发更科学、更符合人类的思维习惯,更有效地提高软件开发效率,缩短开发周期,提高软件可靠性。
上述特征适用于所有的面向对象语言,深入了解这些特征是掌握面向对象程序设计思想的关键。面向对象的思想只有通过大量的实践去学习和理解才能真正领悟。
在面向对象程序设计思想中,类和对象是非常重要的两个概念。如果要掌握C++这门面向对象的程序设计语言,有必要先学习类和对象。类和对象的关系,如同建筑设计图纸与建筑物的关系,类是对象的模板,对象是类的实体。本节将针对类和对象进行详细讲解。
类是对象的抽象,是一种自定义数据类型,它用于描述一组对象的共同属性和行为。类的定义格式如下所示:
class 类名
{
权限控制符:
成员;
};
关于类定义格式的具体介绍如下。
(1)class是定义类的关键字。
(2)类名是类的标识符,其命名遵循标识符的命名规范。
(3)类名后面的一对大括号,用于包含类的成员,类的所有成员要在这一对大括号中声明。类中可以定义成员变量(也称为属性)和成员函数(也称为方法),成员变量用于描述对象的属性,成员函数用于描述对象的行为。
(4)声明类的成员时,通常需要使用权限控制符限定成员的访问规则,权限控制符包括public、protected和private,这三种权限控制符的权限依次递减。
(5)大括号的后面的一个分号“;”用于表示类定义的结束。
下面根据上述格式定义一个学生类,该类描述的学生属性包括姓名、性别和年龄等,行为包括学习、考试等,具体定义代码如下所示:
class Student //定义学生类Student
{
public: //公有权限
void study(); //声明表示学习的成员函数
void exam(); //声明表示考试的成员函数
private: //私有权限
string _name; //声明表示姓名的成员变量
int _age; //声明表示年龄的成员变量
};
上述代码定义了一个简单的学生类Student,该类中有_nam e、_age两个成员变量,它们是类的私有成员;除了成员变量,该类还定义了study()和exam()两个成员函数,它们是类的公有成员。通常情况下,类的成员函数在类中声明,在类外实现。在类外实现成员函数,必须在返回值之后、函数名之前加上所属的类作用域,即“类名::”,表示函数属于哪个类。在类外实现成员函数的格式如下所示:
返回值类型 类名::函数名称(参数列表)
{
函数体
}
}
例如,在类外实现类Student的成员函数,示例代码如下所示:
void Student::study() //类外实现study()成员函数
{
cout << "学习C++" << endl;
}
void Student::exam() //类外实现exam()成员函数
{
cout << "C++考试成绩100分" << endl;
}
如果函数名前没有类名和作用域限定符“::”,则函数不是类的成员函数,而是一个普通的函数。
为了大家更好地理解成员的访问规则,下面针对权限控制符进行具体介绍。
定义了类,就相当于定义了一个数据类型。类与int、char等数据类型的使用方法是一样的,可以定义变量,使用类定义的变量通常称为该类的对象。
对象的定义格式如下所示:
类名 对象名;
在上述格式中,对象的命名遵循标识符的命名规范。
下面创建一个表示学生类Student的对象,示例代码如下所示:
Student stu;
上述代码中,创建了类的对象stu之后,系统就要为对象分配内存空间,用于存储对象成员。每个对象都有成员变量和成员函数两部分内容。成员变量标识对象的属性,比如创建两个Student类对象stu1和stu2,由于两个学生的姓名、性别、年龄都不同,因此在创建对象时应当为每个对象分配独立的内存空间存储成员变量的值。
成员函数描述的是对象的行为,每个对象的行为都相同,比如学生对象stu1和stu2都具有学习、考试行为。如果为每个对象的成员函数也分配不同的空间,则必然造成浪费。因此,C++用同一块空间存放同类对象的成员函数代码,每个对象调用同一段代码。对象与成员之间的内存分配示意图如图2-1所示。

图2-1 对象与成员之间的内存分配示意图
为对象分配了内存空间之后,就可以向这块内存空间中存储数据了。存储数据的目的是访问数据,即访问对象的成员。对象的成员变量和成员函数的访问可以通过“。”运算符实现,其格式如下所示:
对象名.成员变量 对象名.成员函数
在上述格式中,通过“。”运算符既可以访问对象的成员变量也可以调用对象的成员函数。下面通过案例演示类的定义、对象的创建及对象的成员访问,如例2-1所示。
例2-1 Student.cpp
1 #include<iostream>
2 using namespace std;
3 class Student //定义学生类Student
4 {
5 public: //公有类型
6 void study(); //声明表示学习的成员函数
7 void exam(); //声明表示考试的成员函数
8 string _name; //声明表示姓名的成员变量
9 int _age; //声明表示年龄的成员变量
10 };
11 void Student::study() //类外实现study()成员函数
12 {
13 cout << "学习C++" << endl;
14 }
15 void Student::exam() //类外实现exam()成员函数
16 {
17 cout << "C++考试成绩100分" << endl;
18 }
19 int main()
20 {
21 Student stu; //创建Student类对象stu
22 stu._name = "张三"; //设置对象stu的姓名
23 stu._age = -20; //设置对象stu的年龄
24 cout << stu._name << stu._age << "岁" << endl;
25 stu.study(); //调用study()成员函数
26 stu.exam(); //调用exam()成员函数
27 return 0;
28 }
例2-1 运行结果如图2-2所示。

图2-2 例2-1运行结果
在例2-1中,第3~10行代码定义了学生类Student,该类中有两个公有成员变量_nam e和_age,分别表示学生的姓名和年龄,有两个公有成员函数study()和exam()。第11~18行代码是在类外实现类的成员函数。第21~23行代码,在m ain()函数中创建Student类对象stu,并设置对象stu的_nam e和_age值。第24~26行代码通过对象stu调用对象的成员函数,输出对象stu的信息。由图2-2可知,程序成功创建了对象stu,并输出了对象stu的信息。
类是自定义数据类型,与基本数据类型的使用方式相同,也可以使用new创建类对象。例如,例2-1定义的Student类,可以使用new创建Student类对象,示例代码如下所示:
Student* ps = new Student; //使用new创建类对象 //…其他功能代码 delete ps; //使用delete释放对象
C++中的封装是通过类实现的,通过类把具体事物抽象为一个由属性和行为结合的独立单位,类的对象会表现出具体的属性和行为。在类的封装设计中通过权限控制方式实现类成员的访问,目的是隐藏对象的内部实现细节,只对外提供访问的接口。在例2-1中,第23行代码将对象stu的年龄值设置为−20,这在语法上不会有任何问题,程序可以正常运行,但在现实生活中明显不合理。为了避免这种情况,在设计类时,要控制成员变量的访问权限,不允许外界随意访问。
通过权限控制符可以限制外界对类的成员变量的访问,将对象的状态信息隐藏在对象内部,通过类提供的函数(接口)实现对类中成员的访问。在定义类时,将类中的成员变量设置为私有或保护属性,即使用private或protected关键字修饰成员变量。使用类提供的公有成员函数(public修饰的成员函数),如用于获取成员变量值的getXxx()函数和用于设置成员变量值的setXxx()函数,操作成员变量的值。
下面修改例2-1,使用private关键字修饰类的成员变量,并提供相应的成员函数访问类的成员变量,如例2-2所示。
例2-2 package.cpp
1 #include<iostream>
2 using namespace std;
3 class Student //定义学生类Student
4 {
5 public: //公有类型
6 void study(); //声明表示学习的成员函数
7 void exam(); //声明表示考试的成员函数
8 void setName(string name); //声明设置姓名的成员函数
9 void setAge(int age); //声明设置年龄的成员函数
10 string getName(); //声明获取姓名的成员函数
11 int getAge(); //声明获取年龄的成员函数
12private: //私有类型
13 string _name; //声明表示姓名的成员变量
14 int _age; //声明表示年龄的成员变量
15 };
16 void Student::study() //类外实现study()成员函数
17 {
18 cout << "学习C++" << endl;
19 }
20 void Student::exam() //类外实现exam()成员函数
21 {
22 cout << "C++考试成绩100分" << endl;
23 }
24 void Student::setName(string name) //类外实现setName()成员函数
25 {
26 _name = name;
27 }
28 void Student::setAge(int age) //类外实现setAge()成员函数
29 {
30 if (age < 0 || age > 100)
31 {
32 cout << "_name" << "年龄输入错误" << endl;
33 _age = 0;
34 }
35 else
36 _age = age;
37 }
38 string Student::getName() //类外实现getName()函数
39 {
40 return _name;
41 }
42 int Student::getAge() //类外实现getAge()函数
43 {
44 return _age;
45 }
46 int main()
47 {
48 Student stu; //创建Student类对象stu
49 stu.setName("张三"); //设置对象stu的姓名
50 stu.setAge(-20); //设置对象stu的年龄
51 //调用成员函数getName()和getAge()获取对象stu的姓名、年龄,并输出
52 cout << stu.getName() << stu.getAge() << "岁" << endl;
53 stu.study(); //调用成员函数study()
54 stu.exam(); //调用成员函数exam()
55 Student stu1; //创建Student类对象stu1
56 stu1.setName("李四");
57 stu1.setAge(22);
58 cout << stu1.getName() << stu1.getAge() << "岁" << endl;
59 stu1.study();
60 stu1.exam();
61 return 0;
62 }
例2-2运行结果如图2-3所示。

图2-3 例2-2运行结果
例2-2是对例2-1的修改,将Student中的成员变量_nam e和_age定义为私有成员,并定义了公有成员函数setNam e()、setAge()、getNam e()和getAge(),分别用于设置和获取对象的姓名和年龄。第28~37行代码,在实现setAge()时,对传入的参数age进行了判断处理,如果age>100或age<0,则输出“年龄输入错误”的信息,并将_age值设置为0。第48~52行代码,创建对象stu,调用setNam e()函数和setAge()函数,分别用于设置对象stu的_nam e和_age;调用getNam e()函数和getAge()函数,分别用于获取对象stu的_nam e和_age。第56~60行代码,创建Student类对象stu1,设置其姓名和年龄,并获取对象stu1的姓名和年龄将其输出。
由图2-3可知,当设置对象stu的年龄为−20时,程序提示年龄输入错误,并将表示年龄的_age设置为0;当设置对象stu1的_age为22时,程序正确输出对象stu1的年龄。
在例2-2中,_nam e和_age成员为私有成员,因此不能通过对象直接访问,如果仍然像例2-1中的第22~24行代码一样,直接通过对象访问_nam e和_age,编译器会报错,如图2-4所示。

图2-4 访问私有成员_name和_age所报错误
在例2-2中,程序创建了两个对象stu和stu1,通过对象stu调用getNam e()函数获取的姓名是“张三”,通过对象stu1调用getNam e()函数获取的姓名是“李四”。在调用过程中,getNam e()函数可以区分到底是对象stu还是对象stu1调用,是通过this指针实现的。
this指针是C++实现封装的一种机制,它将对象和对象调用的非静态成员函数联系在一起,从外部看来,每个对象都拥有自己的成员函数。当创建一个对象时,编译器会初始化一个this指针,指向创建的对象,this指针并不存储在对象内部,而是作为所有非静态成员函数的参数。例如,在例2-2中,当创建对象stu时,编译器会初始化一个this指针指向对象stu,通过stu调用成员函数setNam e()与getNam e()时,编译器会将this指针作为两个函数的参数,编译后的函数代码可以表示为如下形式:
void Student::setName(Student* this,string name)
{
this->_name = name;
}
string Student::getName(Student* this)
{
return this->_name;
}
上述代码演示的过程是隐含的,由编译器完成。当对象stu调用成员函数时,指向对象stu的this指针作为成员函数的第一个参数,在成员函数内部使用对象属性时,编译器会通过this指针访问对象属性。
实现类的成员函数时,如果形参与类的属性重名,可以用this指针解决。例如,在例2-2中,类的成员变量为_nam e和_age,setNam e()函数和setAge()函数的形参为nam e和age,可以进行区分。如果将类的成员变量改为nam e和age,则这两个成员变量和setNam e()函数、setAge()函数的形参重名,在赋值时无法区分(nam e=nam e,age=age),此时可以使用this指针进行区分,示例代码如下:
void Student::setName(string name)
{
this->name = name;
}
string Student::getName()
{
return this->_name;
}
如果类的成员函数返回值为一个对象,则可以使用return*this返回对象本身。
构造函数是类的特殊成员函数,用于初始化对象。构造函数在创建对象时由编译器自动调用。C++中的每个类至少要有一个构造函数,如果类中没有定义构造函数,系统会提供一个默认的无参构造函数,默认的无参构造函数体也为空,不具有实际的初始化意义。因此,在C++程序中要显示定义构造函数。本节将针对构造函数的定义与具体用法进行详细讲解。
构造函数是类的特殊成员函数,C++编译器严格规定了构造函数的接口形式,其定义格式如下所示:
class 类名
{
权限控制符:
构造函数名(参数列表)
{
函数体
}
... //其他成员
};
关于构造函数定义格式的说明,具体如下。
(1)构造函数名必须与类名相同。
(2)构造函数名的前面不需要设置返回值类型。
(3)构造函数中无返回值,不能使用return返回。
(4)构造函数的成员权限控制符一般设置为public。
如果在类中提供了自定义构造函数,编译器便不再提供默认构造函数。自定义构造函数时,可以定义无参构造函数,也可以定义有参构造函数,下面分别进行讲解。
1. 自定义无参构造函数
自定义无参构造函数时,可以在函数内部直接给成员变量赋值。下面通过案例演示无参构造函数的定义与调用,如例2-3所示。
例2-3 noPara.cpp
1 #include<iostream>
2 #include<iomanip>
3 using namespace std;
4 class Clock //定义时钟类Clock
5 {
6 public:
7 Clock(); //声明无参构造函数
8 void showTime(); //声明显示时间的成员函数
9 private:
10 int _hour; //声明表示小时的成员变量
11 int _min; //声明表示分钟的成员变量
12 int _sec; //声明表示秒的成员变量
13 };
14 Clock::Clock() //类外实现无参构造函数
15 {
16 _hour=0; //初始化过程,将成员变量初始化为0
17 _min=0;
18 _sec=0;
19 }
20 void Clock::showTime() //类外实现成员函数
21 {
22 cout<<setw(2)<<setfill('0')<<_hour<<":"
23 <<setw(2)<<setfill('0')<<_min<<":"
24 <<setw(2)<<setfill('0')<<_sec<<endl;
25 }
26 int main()
27 {
28 Clock clock; //创建对象clock
29 cout<<"clock:";
30 clock.showTime(); //通过对象调用成员函数showTime()显示时间
31 return 0;
32 }
例2-3运行结果如图2-5所示。
在例2-3中,第7行代码声明了一个无参构造函数;第14~19行代码在类外实现构造函数,在构造函数体中直接将初始值赋给成员变量;第28~30行代码在m ain()函数中创建了对象clock,并通过对象调用showTim e()成员函数显示初始化时间。由图2-5可知,对象clock的初始化时间为00:00:00,因为创建clock对象调用的是无参构造函数,无参构造函数将时、分、秒都初始化为0。

图2-5 例2-3运行结果
2. 自定义有参构造函数
如果希望在创建对象时提供有效的初始值,可以通过定义有参构造函数实现。下面修改例2-3,将无参构造函数修改为有参构造函数,以演示有参构造函数的定义与使用,如例2-4所示。
例2-4 parameter.cpp
1 #include<iostream>
2 #include<iomanip>
3 using namespace std;
4 class Clock //定义时钟类Clock
5 {
6 public:
7 Clock(int hour, int min, int sec); //声明有参构造函数
8 void showTime(); //用于显示时间的成员函数
9 private:
10 int _hour; //声明表示小时的成员变量
11 int _min; //声明表示分钟的成员变量
12 int _sec; //声明表示秒的成员变量
13 };
14 Clock::Clock(int hour, int min, int sec) //类外实现有参构造函数
15 {
16 _hour=hour; //初始化过程,将初始值直接赋值给成员变量
17 _min=min;
18 _sec=sec;
19 }
20 void Clock::showTime() //类外实现成员函数
21 {
22 cout<<setw(2)<<setfill('0')<<_hour<<":"
23 <<setw(2)<<setfill('0')<<_min<<":"
24 <<setw(2)<<setfill('0')<<_sec<<endl;
25 }
26 int main()
27 {
28 Clock clock1(10,20,30); //创建对象clock1,传入初始值
29 cout<<"clock1:";
30 clock1.showTime(); //通过对象调用成员函数showTime()显示时间
31 Clock clock2(22,16,12); //创建对象clock2,传入初始值
32 cout<<"clock2:";
33 clock2.showTime(); //通过对象调用成员函数showTime()显示时间
34 return 0;
35 }
例2-4运行结果如图2-6所示。

图2-6 例2-4运行结果
在例2-4中,第7行代码声明了有参构造函数;第14~19行代码在类外实现有参构造函数,将参数赋值给成员变量,在创建对象时调用有参构造函数,用户可以传入初始值(参数)完成对象初始化。第28~33行代码,创建了两个Clock对象clock1和clock2,这两个对象在创建时,传入了不同的参数,因此各个对象调用成员函数showTim e()显示的初始化时间是不一样的。
需要注意的是,在实现构造函数时,除了在函数体中初始化成员变量,还可以通过“:”运算符在构造函数后面初始化成员变量,这种方式称为列表初始化,其格式如下所示:
类::构造函数(参数列表): 成员变量1(参数1), 成员变量2(参数2),…, 成员变量n(参数n)
{
构造函数体
}
在例2-4中,使用列表初始化实现成员变量初始化的方式如下所示:
Clock::Clock(int hour, int min, int sec):_hour(hour),_min(min),_sec(sec)
{
//...
}
在C++中,构造函数允许重载。例如,Clock类可以定义多个构造函数,示例代码如下所示:
class Clock //定义时钟类Clock
{
public:
//构造函数重载
Clock();
Clock(int hour);
Clock(int hour, int min);
Clock(int hour, int min, int sec);
void showTime(); //声明显示时间的成员函数
private:
int _hour; //声明表示小时的成员变量
int _min; //声明表示分钟的成员变量
int _sec; //声明表示秒的成员变量
};
当定义具有默认参数的重载构造函数时,要防止调用的二义性。下面修改例2-4,在Clock类中定义重载构造函数,并且其中一个构造函数具有默认参数,在创建对象时,构造函数调用会产生二义性,如例2-5所示。
例2-5 overload.cpp
1 #include<iostream>
2 #include<iomanip>
3 using namespace std;
4 class Clock //定义时钟类Clock
5 {
6 public:
7 //声明重载构造函数
8 Clock(int hour, int min);
9 Clock(int hour, int min, int sec=0);
10 void showTime(); //声明显示时间的成员函数
11 private:
12 int _hour; //声明表示小时的成员变量
13 int _min; //声明表示分钟的成员变量
14 int _sec; //声明表示秒的成员变量
15 };
16 Clock::Clock(int hour, int min):_hour(hour),_min(min)
17 {
18 cout<<"调用两个参数的构造函数"<<endl;
19 _sec=10;
20 }
21 Clock::Clock(int hour, int min, int sec=0) //类外实现构造函数
22 {
23 cout<<"调用三个参数的构造函数"<<endl;
24 _hour=hour;
25 _min=mi
26 _sec=sec;
27 }
28 void Clock::showTime() //类外实现成员函数showTime()
29 {
30 cout<<setw(2)<<setfill('0')<<_hour<<":"
31 <<setw(2)<<setfill('0')<<_min<<":"
32 <<setw(2)<<setfill('0')<<_sec<<endl;
33 }
34 int main()
35 {
36 Clock clock(8,0); //创建对象clock,传入初始值
37 cout<<"clock:";
38 clock.showTime(); //通过对象调用成员函数显示时间
39 return 0;
40 }
运行例2-5时,编译器会报错,如图2-7所示。
在例2-5中,第8行代码声明了一个构造函数,该构造函数有两个参数;第9行代码声明了一个构造函数,该构造函数有三个参数,且第三个参数有默认值;第16~27行代码,在类外实现了这两个构造函数;第36行代码,在m ain()函数中创建一个对象clock,传入两个参数,编译器无法确认调用的是第8行的构造函数还是第9行的构造函数,因此无法通过编译。

图2-7 例2-5编译器报错
C++允许将一个对象作为另一个类的成员变量,即类中的成员变量可以是其他类的对象,这样的成员变量称为类的子对象或成员对象。含有成员对象的类的定义格式如下所示:
class B
{
A a; //对象a作为类B的成员变量
... //其他成员
}
创建含有成员对象的对象时,先执行成员对象的构造函数,再执行类的构造函数。例如,上述格式中,类B包含一个类A对象作为成员变量,在创建类B对象时,先执行类A的构造函数,将类A对象创建出来,再执行类B的构造函数,创建类B对象。如果类A构造函数有参数,其参数要从类B的构造函数中传入,且必须以“:”运算符初始化类A对象。
在类中包含对象成员,能够真实地描述客观事物之间的包含关系,比如描述学生信息的类,类中的成员除了姓名、学号属性,还包含出生日期。在定义学生类的时候,可以先定义一个描述年、月、日的出生日期类,再定义学生类,将出生日期类的对象作为学生类的成员变量。
下面通过案例演示含有成员对象的类的构造函数的定义与调用,如例2-6所示。
例2-6 Student.cpp
1 #include<iostream>
2 using namespace std;
3 class Birth //定义出生日期类Birth
4 {
5 public:
6 Birth(int year,int month, int day); //构造函数
7 void show(); //声明成员函数show()显示日期
8 private:
9 int _year;
10 int _month;
11 int _day;
12 };
13 //类外实现构造函数
14 Birth::Birth(int year, int month, int day)
15 :_year(year),_month(month),_day(day)
16 {
17 cout<<"Birth类构造函数"<<endl;
18 }
19 //类外实现show()函数
20 void Birth::show()
21 {
22 cout<<"出生日期:"<<_year<<"-"<<_month<<"-"<<_day<<endl;
23 }
24 class Student //定义学生类Student
25 {
26 public:
27 //构造函数
28 Student(string name, int id, int year, int month, int day);
29 void show();
30 private:
31 string _name;
32 int _id;
33 Birth birth;
34 };
35 //类外实现构造函数
36 Student::Student(string name, int id, int year, int month, int day)
37 :birth(year,month,day)
38 {
39 cout<<"Student类构造函数"<<endl;
40 _name=name;
41 _id=id;
42 }
43 //类外实现show()函数
44 void Student::show()
45 {
46 cout<<"姓名:"<<_name<<endl;
47 cout<<"学号:"<<_id<<endl;
48 birth.show();
49 }
50 int main()
51 {
52 Student stu("lili",10002,2000,1,1); //创建学生对象stu
53 stu.show(); //显示学生信息
54 return 0;
55 }
例2-6运行结果如图2-8所示。

图2-8 例2-6运行结果
在例2-6中,第3~12行代码定义了出生日期类Birth,该类有3个成员变量,分别是_year、_month、_day,并且定义了有参数的构造函数;第24~34行代码定义了学生类Student,该类有3个成员变量,分别是_name、_id、birth,其中birth是类Birth的对象。此外,Student类还定义了一个构造函数。由于成员对象birth的构造函数有3个参数,这3个参数要从类Student的构造函数中获取,因此Student类的构造函数共有5个参数。第36~42行代码用于实现Student类的构造函数,birth成员对象必须通过“:”运算符在Student构造函数后面初始化,无法在Student构造函数体中赋值。第52~53行代码,在main()函数中创建Student类对象stu,并通过对象stu调用成员函数show()显示学生信息。
由图2-8可知,学生对象成功创建且显示出了学生信息。创建对象stu时,先调用Birth类构造函数,之后才调用Student类构造函数。
创建对象时,系统会为对象分配所需要的内存空间等资源,当程序结束或对象被释放时,系统为对象分配的资源也需要回收,以便可以重新分配给其他对象使用。在C++中,对象资源的释放通过析构函数完成。析构函数的作用是在对象被释放之前完成一些清理工作。析构函数调用完成之后,对象占用的资源也被释放。
与构造函数一样,析构函数也是类的一个特殊成员函数,其定义格式如下所示:
class 类名
{
~析构函数名称();
... //其他成员
}
关于析构函数的定义,有以下注意事项。
当程序结束时,编译器会自动调用析构函数完成对象的清理工作,如果类中没有定义析构函数,编译器会提供一个默认的析构函数,但默认的析构函数只能完成栈内存对象的资源清理,无法完成堆内存对象的资源清理。因此,在程序中往往需要自定义析构函数。析构函数的调用情况主要有以下几种。
①在一个函数中定义了一个对象,当函数调用结束时,对象应当被释放,对象释放之前编译器会调用析构函数释放资源。
②对于static修饰的对象和全局对象,只有在程序结束时编译器才会调用析构函数。
③对于new运算符创建的对象,在调用delete释放时,编译器会调用析构函数释放资源。
析构函数的调用顺序与构造函数的调用顺序是相反的。在构造对象和析构对象时,C++遵循的原则是:先构造的后析构,后构造的先析构。例如,连续创建了两个对象A1和A2,在创建时,先调用构造函数构造对象A1,再调用构造函数构造对象A2;在析构时,先调用析构函数析构对象A2,再调用析构函数析构对象A1。
下面通过案例演示析构函数的定义与调用,如例2-7所示。
例2-7 Rabbit.cpp
1 #define _CRT_SECURE_NO_WARNINGS
2 #include<iostream>
3 using namespace std;
4 class Rabbit //定义兔子类Rabbit
5 {
6 public:
7 Rabbit(string name,const char* pf); //声明构造函数
8 void eat();
9 ~Rabbit(); //声明析构函数
10 private:
11 string _name; //声明表示兔子名字的成员变量
12 char* _food; //声明表示兔子食物的成员变量
13 };
14 Rabbit::Rabbit(string name, const char* pf)
15 {
16 cout<<"调用构造函数"<<endl;
17 _name=name;
18 _food=new char[50]; //为_food指针申请空间
19 memset(_food,0,50); //初始化_food空间
20 strcpy(_food,pf); //将参数pf指向的数据复制到_food中
21 }
22 void Rabbit::eat()
23 { //类外实现成员函数
24 cout<<_name<<" is eating "<<_food<<endl;
25 }
26 Rabbit::~Rabbit() //类外实现析构函数
27 {
28 cout<<"调用析构函数,析构"<<_name<<endl;
29 if(_food != NULL)
30 delete []_food;
31 }
32 int main()
33 {
34 Rabbit A("A","luobo");
35 A.eat();
36 Rabbit B("B","baicai");
37 B.eat();
38 return 0;
39 }
例2-7运行结果如图2-9所示。
在例2-7中,第4~13行代码,定义了一个兔子类Rabbit,该类有两个成员变量,分别是_name、_food,有一个构造函数、一个析构函数和一个普通成员函数eat();第14~21行代码在类外实现构造函数。在实现构造函数时,由于第二个成员变量_food是字符指针变量,因此在赋值时,要先使用new运算符为_food指针申请一块内存空间并初始化,再将参数pf指向的数据复制到_food指向的空间;第22~25行代码在类外实现eat()函数;第26~31行代码在类外实现析构函数,在析构函数中,使用delete运算符释放_food指向的内存空间。第34~37行代码,在main()函数中,分别创建两个对象A和B,然后调用成员函数eat()实现吃食物的功能。
在创建对象的过程中,对象A与对象B除了对象本身所占用的内存空间,还各自拥有一块new运算符在堆上申请的空间,对象A与对象B占用的内存空间如图2-10所示。

图2-9 例2-7运行结果

图2-10 对象A与对象B占用的内存空间
程序运行结束后,编译器会调用析构函数释放对象资源,在释放时,先释放_food指向的内存空间,再释放对象所占用的内存空间。
由图2-9可知,程序成功调用了析构函数,并且析构的顺序是先析构对象B,再析构对象A。
在程序中,经常使用已有对象完成新对象的初始化。例如,在定义变量inta=3后,再定义新变量intb=a。在类中,需要定义拷贝构造函数才能完成这样的功能。接下来,本节将针对拷贝构造函数进行详细讲解。
拷贝构造函数是一种特殊的构造函数,它具有构造函数的所有特性,并且使用本类对象的引用作为形参,能够通过一个已经存在的对象初始化该类的另一个对象。拷贝构造函数的定义格式如下所示:
class 类名
{
public:
构造函数名称(const 类名& 对象名)
{
函数体
}
... //其他成员
};
在定义拷贝构造函数时,为了使引用的对象不被修改,通常使用const修饰引用的对象。下面通过案例演示拷贝构造函数的定义与调用,如例2-8所示。
例2-8 copy.cpp
1 #include<iostream>
2 using namespace std;
3 class Sheep //定义绵羊类Sheep
4 {
5 public:
6 Sheep(string name,string color); //声明有参构造函数
7 Sheep(const Sheep& another); //声明拷贝构造函数
8 void show(); //声明普通成员函数
9 ~Sheep(); //声明析构函数
10 private:
11 string _name; //声明表示绵羊名字的成员变量
12 string _color; //声明表示绵羊颜色的成员变量
13 };
14 Sheep::Sheep(string name, string color)
15 {
16 cout<<"调用构造函数"<<endl;
17 _name=name;
18 _color=color;
19 }
20 Sheep::Sheep(const Sheep& another) //类外实现拷贝构造函数
21 {
22 cout<<"调用拷贝构造函数"<<endl;
23 _name=another._name;
24 _color=another._color;
25 }
26 void Sheep::show()
27 {
28 cout<<_name<<" "<<_color<<endl;
29 }
30 Sheep::~Sheep()
31 {
32 cout<<"调用析构函数"<<endl;
33 }
34
35 int main()
36 {
37 Sheep sheepA("Doly","white");
38 cout<<"sheepA:";
39 sheepA.show();
40 Sheep sheepB(sheepA); //使用sheepA初始化新对象sheepB
41 cout<<"sheepB:";
42 sheepB.show();
43 return 0;
44 }
例2-8运行结果如图2-11所示。
在例2-8中,第3~13行代码定义了一个绵羊类Sheep,该类有两个成员变量,分别是_nam e、_color。此外,该类还声明了有参构造函数、拷贝构造函数、普通成员函数show()和析构函数;第20~25行代码,在类外实现拷贝构造函数,在函数体中,将形参sheepA的成员变量值赋给类的成员变量;第37~39行代码,在m ain()函数,创建了Sheep类对象sheepA,并输出sheepA的信息;第40行代码创建Sheep类对象sheepB,并使用对象sheepA初始化对象sheepB,在这个过程中编译器会调用拷贝构造函数;第41~42行代码输出对象sheepB的信息。

图2-11 例2-8运行结果
由图2-11可知,对象sheepA与对象sheepB的信息是相同的。程序首先调用构造函数创建了对象sheepA,然后调用拷贝构造函数创建了对象sheepB。程序运行结束之后,调用析构函数先析构对象sheepB,然后析构对象sheepA。
当涉及对象之间的赋值时,编译器会自动调用拷贝构造函数。拷贝构造函数的调用情况有以下三种。
(1)使用一个对象初始化另一个对象。例2-8就是使用一个对象初始化另一个对象。
(2)对象作为参数传递给函数。当函数的参数为对象时,编译器会调用拷贝构造函数将实参传递给形参。
(3)函数返回值为对象。当函数返回值为对象时,编译器会调用拷贝构造函数将返回值复制到临时对象中,将数据传出。
拷贝构造函数是特殊的构造函数,如果程序没有定义拷贝构造函数,C++会提供一个默认的拷贝构造函数,默认拷贝构造函数只能完成简单的赋值操作,无法完成含有堆内存成员数据的拷贝。例如,如果类中有指针类型的数据,默认的拷贝构造函数只是进行简单的指针赋值,即将新对象的指针成员指向原有对象的指针指向的内存空间,并没有为新对象的指针成员申请新空间,这种情况称为浅拷贝。
浅拷贝在析构指向堆内存空间的变量时,往往会出现多次析构而导致程序错误。C++初学者自定义的拷贝构造函数往往实现的是浅拷贝。下面通过案例演示浅拷贝,如例2-9所示。
例2-9 simple.cpp
1 #define _CRT_SECURE_NO_WARNINGS
2 #include<iostream>
3 #include<string.h>
4 using namespace std;
5 class Sheep //定义绵羊类Sheep
6 {
7 public:
8 Sheep(string name,string color,const char* home); //声明有参构造函数
9 Sheep(const Sheep& another); //声明拷贝构造函数
10 void show(); //声明普通成员函数
11 ~Sheep(); //声明析构函数
12 private:
13 string _name; //声明表示绵羊名字的成员变量
14 string _color; //声明表示绵羊颜色的成员变量
15 char* _home; //声明表示绵羊家的成员变量
16 };
17 Sheep::Sheep(string name, string color,const char* home)
18 {
19 cout<<"调用构造函数"<<endl;
20 _name=name;
21 _color=color;
22 //为指针成员home分配空间,将形参home的内容复制到_home指向的空间
23 int len=strlen(home)+1;
24 _home=new char[len];
25 memset(_home,0,len);
26 strcpy(_home,home);
27 }
28 Sheep::Sheep(const Sheep& another) //类外实现拷贝构造函数
29 {
30 cout<<"调用拷贝构造函数"<<endl;
31 _name=another._name;
32 _color=another._color;
33 _home=another._home; //浅拷贝
34 }
35 void Sheep::show()
36 {
37 cout<<_name<<" "<<_color<<" "<<_home<<endl;
38 }
39 Sheep::~Sheep()
40 {
41 cout<<"调用析构函数"<<endl;
42 if(_home!=NULL)
43 delete []_home;
44 }
45 int main()
46 {
47 const char *p = "beijing";
48 Sheep sheepA("Doly","white",p);
49 cout<<"sheepA:";
50 sheepA.show();
51 Sheep sheepB(sheepA); //使用sheepA初始化新对象sheepB
52 cout<<"sheepB:";
53 sheepB.show();
54 return 0;
55 }
运行例2-9中的程序,程序抛出异常,在第43行代码处触发异常断点,如图2-12所示。
例2-9是对例2-8的修改,在绵羊类Sheep中增加了一个char类型的指针变量成员_hom e,用于表示绵羊对象的家。增加了_hom e成员变量之后,类Sheep的构造函数、拷贝构造函数、析构函数都进行了相应修改。第17~27行代码实现构造函数,在构造函数内部,首先为_hom e指针申请堆内存空间,然后调用strcpy()函数将形参hom e的内容复制到_hom e指向的空间。第28~34行代码实现拷贝构造函数,在拷贝构造函数内部,对指针成员只进行了简单的赋值操作,即浅拷贝。第39~44行代码实现析构函数,在析构函数内部,使用delete运算符释放_hom e指向的内存空间。第47~53行代码,在m ain()函数中,先创建对象sheepA,再创建对象sheepB,并用对象sheepA初始化对象sheepB。
在这个过程中,使用对象sheepA初始化对象sheepB是浅拷贝过程,因为对象sheepB的_hom e指针指向的是对象sheepA的_hom e指针指向的空间。浅拷贝过程如图2-13所示。

图2-12 触发异常断点

图2-13 浅拷贝过程
由图2-13可知,在浅拷贝过程中,对象sheepA中的_hom e指针与对象sheepB中的_hom e指针指向同一块内存空间。当程序运行结束时,析构函数释放对象所占用资源,析构函数先析构对象sheepB,后析构对象sheepA。在析构sheepB对象时释放了_hom e指向的堆内存空间的数据,当析构sheepA时_hom e指向的堆内存空间已经释放,再次释放内存空间的资源,程序运行异常终止,即存储“beijing”的堆内存空间被释放了两次,因此程序抛出异常,这种现象被称重析构(double free)。
所谓深拷贝,就是在拷贝构造函数中完成更深层次的复制,当类中有指针成员时,深拷贝可以为新对象的指针分配一块内存空间,将数据复制到新空间。例如,在例2-8中,使用对象sheepA初始化对象sheepB时,为对象sheepB的指针_hom e申请一块新的内存空间,将数据复制到这块新的内存空间。下面修改例2-9中的拷贝构造函数,实现深拷贝过程。修改后的拷贝构造函数代码如下所示:
Sheep::Sheep(const Sheep& another) //类外实现拷贝构造函数
{
cout<<"调用拷贝构造函数"<<endl;
_name=another._name;
_color=another._color;
//完成深拷贝
int len = strlen(another._home)+1;
_home=new char[len];
strcpy(_home,another._home);
}
拷贝构造函数修改之后,再次运行程序,程序不再抛出异常。在深拷贝过程中,对象sheepB中的_hom e指针指向了独立的内存空间,是一份完整的对象拷贝,如图2-14所示。

图2-14 深拷贝过程
由图2-14可知,对象sheepA中的_hom e指针与对象sheepB中的_hom e指针指向不同的内存空间,在析构时,析构各自对象所占用的资源不会再产生冲突。
前面学习的类中,成员变量都是我们比较熟悉的简单类型,比如int、char*等,但很多时候为描述比较复杂的情况,例如,只允许类的成员函数读取成员变量的值,但不允许在成员函数内部修改成员变量的值,此时就需要使用const关键字修饰成员函数;或者,类中的成员变量在多个对象之间共享,此时就需要使用static关键字修饰成员变量。本节将针对const和static关键字修饰类的成员进行讲解。
生活中有许多的数据是不希望被改变的,如圆周率、普朗克常数、一个人的国籍等。同样,在程序设计中有些数据也不希望被改变,只允许读取。对于不希望被改变的数据,可以使用const关键字修饰。在类中,const既可以修饰类的成员变量,也可以修饰类的成员函数。下面对这两种情况分别进行讲解。
1.const修饰成员变量
使用const修饰的成员变量称为常成员变量。对于常成员变量,仅仅可以读取第一次初始化的数据,之后是不能修改的。常成员变量通常使用有参构造函数进行初始化。
下面通过案例演示const关键字修饰类的成员变量,如例2-10所示。
例2-10 constMember.cpp
1 #include<iostream>
2 using namespace std;
3 class Person //定义类Person
4 {
5 public:
6 Person(string name,int age,string addr); //声明有参构造函数
7 const string _addr; //声明表示住址的常成员变量
8 ~Person(); //声明析构函数
9 private:
10 const string _name; //声明表示姓名的常成员变量
11 const int _age; //声明表示年龄的常成员变量
12 };
13 //类外实现构造函数
14 Person::Person(string name,int age,string addr):
15 _name(name),_age(age),_addr(addr)
16 {
17 cout<<"初始化const修饰的成员变量"<<endl;
18 cout<<"name:"<<_name<<endl;
19 cout<<"age:"<<_age<<endl;
20 cout<<"addr:"<<_addr<<endl;
21 }
22 Person::~Person(){} //类外实现析构函数
23 int main()
24 {
25 Person p1("张三",18,"北大街");
26 p1._addr="南大街";
27 return 0;
28 }
运行例2-10,编译器会报错,如图2-15所示。

图2-15 例2-10编译器报错
在例2-10中,第3~12行代码定义了一个类Person,该类有三个常成员变量:_nam e、_age和_addr。第14~21行代码,在类外实现类的构造函数,类的常成员变量在构造函数中完成初始化,即创建对象时完成初始化。第25行代码创建Person类对象p1,在创建对象时完成了三个常成员变量的初始化。这是创建对象后初始化常成员变量的唯一机会,常成员变量一旦初始化就不能再改变。第26行代码试图修改常成员变量,因此程序会报错。
2.const修饰成员函数
使用const修饰的成员函数称为常成员函数。与修饰成员变量不同的是,修饰成员函数时,const位于成员函数的后面,其格式如下:
返回值类型 函数名() const;
在常成员函数内部,只能访问类的成员变量,而不能修改类的成员变量;并且,常成员函数只能调用类的常成员函数,而不能调用类的非常成员函数。
类中定义的成员函数若与常成员函数名相同则构成重载,常成员函数只能由const修饰的对象进行访问。
下面通过案例演示const关键字修饰类的成员函数,如例2-11所示。
例2-11 constFunc.cpp
1 #include<iostream>
2 using namespace std;
3 class Person //定义类Person
4 {
5 public:
6 Person(string ,int,string,string); //声明有参构造函数
7 ~Person(); //声明析构函数
8 const string _addr; //声明表示住址的常成员变量
9 void myInfor() const; //声明显示个人信息的常成员函数
10 void myInfor(); //声明显示个人信息的普通成员函数
11 void place(); //声明显示住址的普通成员函数
12 private:
13 const string _name; //声明表示姓名的常成员变量
14 const int _age; //声明表示年龄的常成员变量
15 string _favFruit; //声明表示水果的普通成员变量
16 };
17 //有参构造函数初始化对象
18 Person::Person(string name,int age,string addr,string favFruit):
19 _name(name), _age(age),_addr(addr),_favFruit(favFruit)
20 {
21 }
22 void Person::myInfor() const //类外实现常成员函数myInfor()
23 {
24 //_favFruit="榴莲";
25 //place();
26 cout<<"我叫"<<_name<<"今年"<<_age<<"岁"<<"我喜欢吃"<<_favFruit<<endl;
27 }
28 Person:: ~Person(){} //类外实现析构函数
29 void Person::myInfor() //类外实现普通成员函数myInfor()
30 {
31 _favFruit="桃子";
32 cout<<"我叫"<<_name<<"今年"<<_age
33 <<"岁"<<"我喜欢吃"<<_favFruit<<endl;
34 place();
35 }
36 void Person::place() //类外实现普通成员函数place()
37 {
38 cout<<"我住在"<<_addr<<endl;
39 }
40 int main()
41 {
42 Person p1("张三",18,"北大街","苹果"); //创建对象p1
43 p1.myInfor(); //调用普通成员函数myInfor()
44 const Person p2("李四",18,"南大街","橘子"); //创建常对象p2
45 p2.myInfor(); //调用常成员函数myInfor()
46 return 0;
47 }
例2-11运行结果如图2-16所示。

图2-16 例2-11运行结果
在例2-11中,第3~16行代码定义了Person类,该类中定义了三个成员变量,其中_nam e和_age是常成员函数。此外,第9~10行代码声明了Person类两个重载的成员函数m yInfor(),第9行代码的m yInfor()函数为常成员函数,第10行代码的m yInfor()函数为普通成员函数。
第22~27行代码在类外实现常成员函数m yInfor(),在函数内部,输出各个成员变量的值。需要注意的是,类的常成员函数不能修改成员变量的值,也不能调用非常成员函数,如第24~25行代码,如果取消注释,程序就会报错。
第29~35行代码在类外实现普通成员函数m yInfor(),在函数内部,可以像第31行代码那样修改成员变量的值,也可以像第34行代码那样调用非常成员函数。
第42~43行代码创建对象p1,并通过p1调用m yInfor()函数,由图2-16可知,对象p1调用的是普通成员函数m yInfor()。第44~45行代码创建常对象p2,并通过p2调用m yInfor(),由图2-16可知,常对象p2成功调用了常成员函数m yInfor()。
类中的成员变量,在某些时候被多个类的对象共享,实现对象行为的协调作用。共享数据通过static实现,用static修饰成员后,创建的对象都共享一个静态成员。例如,设计学生类时,可以定义一个成员变量用于统计学生的总人数,由于总人数应该只有一个有效值,因此完全不必在每个学生对象中都定义一个成员变量表示学生总人数,而是在对象以外的空间定义一个表示总人数的成员变量让所有对象共享。这个表示总人数的成员变量就可以使用static关键字修饰。static既可以修饰类的成员变量,也可以修饰类的成员函数,下面分别对这两种情况进行讲解。
1.static修饰成员变量
static修饰的静态成员变量只能在类内部定义,在类外部初始化。静态成员变量在调用时,可以通过对象和类进行访问。由于static成员变量存储在类的外部,计算类的大小时不包含在内。
下面通过案例演示static关键字修饰类的成员变量,如例2-12所示。
例2-12 staticMember.cpp
1 #include<iostream>
2 using namespace std;
3 class Student //定义学生类Student
4 {
5 public:
6 Student(string name); //声明有参构造函数
7 ~Student(); //声明析构函数
8 static int _sum; //声明表示学生总数的静态成员变量
9 private:
10 string _name; //声明表示学生姓名的成员变量
11 };
12 //类外实现Student类有参构造函数
13 Student::Student(string name)
14 {
15 this->_name=name;
16 _sum++;
17 }
18 Student::~Student(){} //类外实现析构函数
19 int Student::_sum = 0; //类外初始化静态成员变量_sum
20 int main()
21 {
22 Student stu1("张三");
23 Student stu2("李四");
24 cout<<"人数是:"<<stu1._sum<<endl; //通过对象访问静态成员变量
25 cout<<"人数是:"<<stu2._sum<<endl;
26 cout<<"人数是"<<Student::_sum<<endl; //通过类访问静态成员变量
27 cout<<"stu1的大小是:"<<sizeof(stu1)<<endl;
28 return 0;
29 }
例2-12运行结果如图2-17所示。
在例2-12中,第3~11行代码定义了学生类Student,其中,第8行代码定义了静态成员变量_sum。第13~17行代码在类外部实现有参构造函数,每当创建对象时,_sum的值自动加1,用于统计建立Student类对象的数目。第19行代码在类外部初始化_sum的值为0。第22~23行代码创建了两个对象stu1和stu2。第24~25行代码通过对象stu1和stu2访问静态成员变量_sum,由图2-17可知,对象stu1和对象stu2访问到的静态成员变量_sum值均为2。

图2-17 例2-12运行结果
第26行代码通过类的作用域访问静态成员变量_sum,由图2-17可知,通过类的作用域访问到的静态成员变量_sum值也为2。第27行代码计算对象stu1的大小,由图2-17可知,对象stu1的大小为28,静态成员变量并不包含在对象中。
2.static修饰成员函数
类中定义的普通函数只能通过对象调用,无法使用类调用。使用static修饰的成员函数,同静态成员变量一样,可以通过对象或类调用。
静态成员函数可以直接访问类中的静态成员变量和静态成员函数,对外提供了访问接口,实现了静态成员变量的管理。需要注意的是,静态成员函数属于类,不属于对象,没有this指针。
下面通过案例演示static关键字修饰类的成员函数,如例2-13所示。
例2-13 staticFunc.cpp
1 #include<iostream>
2 #include<math.h>
3 using namespace std;
4 class Point //定义坐标点类Point
5 {
6 public:
7 Point(float x,float y);
8 ~Point();
9 static float getLen(Point& p1,Point& p2); //声明静态成员函数
10 static float _len; //声明静态成员变量_len
11 private:
12 float _x;
13 float _y;
14 };
15 float Point::_len=0;
16 Point::Point(float x=0,float y=0):_x(x),_y(y) //类外实现有参构造函数
17 {
18 cout<<"初始化坐标点"<<endl;
19 }
20 Point::~Point(){}
21 float Point::getLen(Point &p1,Point &p2) //类外实现有参构造函数
22 {
23 float x=abs(p1._x-p2._x);
24 float y=abs(p1._y-p2._y);
25 _len=sqrtf(x*x+y*y);
26 return _len;
27 }
28 int main()
29 {
30 Point p1(1,2);
31 Point p2(6,8);
32 cout<<Point::getLen(p1,p2)<<endl;
33 return 0;
34 }
例2-13运行结果如图2-18所示。
例2-13中,第4~14行代码定义了类Point,其中,第9行代码定义了静态成员函数getLen(),用于获取两个坐标点之间的距离;第10行代码定义了静态成员变量_len,用于存储两个坐标点之间的距离。第16~19行代码在类外实现有参构造函数,初始化坐标点的值,默认值为0。第21~27行代码,在类外实现getLen()函数,计算传入的两个坐标p1和p2之间的距离,并将结果保存到变量_len中。第30~31行代码初始化坐标点p1和p2。第32行代码调用getLen()函数计算两个坐标点之间的距离。由图2-18可知,程序成功初始化两个坐标点,并计算出了两个坐标点之间的距离。

图2-18 例2-13运行结果
使用static const修饰符组合修饰类成员,既实现了数据共享又达到了数据不被改变的目的。此时,修饰成员函数与修饰普通函数格式一样,修饰成员变量必须在类的内部进行初始化。示例如下:
class Point
{
public:
const static float getLen();
private:
const static float area;
};
const float area=3600;
类中的成员通过权限控制符实现了数据的封装,若对象要访问类中的私有数据,则只能通过成员函数实现。这种方式实现了数据的封装却增加了开销,有时候需要通过外部函数或类直接访问其他类的私有成员,为此C++提供了友元,使用友元可以访问类中的所有成员,函数和类都可以作为友元。
友元函数可以是类外定义的函数或者是其他类中的成员函数,若在类中声明某一函数为友元函数,则该函数可以操作类中的所有数据。
接下来分别讲解类外定义的普通函数作为类的友元函数和类成员函数作为友元函数的用法。
1. 普通函数作为友元函数
将普通函数作为类的友元函数,在类中使用friend关键字声明该普通函数就可以实现,友元函数可以在类中任意位置声明。普通函数作为类的友元函数的声明格式如下所示:
class 类名
{
friend 函数返回值类型 友元函数名(形参列表);
... //其他成员
}
下面通过案例演示普通函数作为友元函数的用法,如例2-14所示。
例2-14 friendFunc.cpp
1 #include<iostream>
2 using namespace std;
3 class Circle
4 {
5 friend void getArea(Circle &circle); //声明普通函数getArea()为友元函数
6 private:
7 float _radius;
8 const float PI=3.14;
9 public:
10 Circle(float radius);
11 ~Circle();
12 };
13 Circle::Circle(float radius=0):_radius(radius)
14 {
15 cout<<"初始化圆的半径为:"<<_radius<<endl;
16 }
17 Circle::~Circle(){}
18 void getArea(Circle &circle)
19 {
20 //访问类中的成员变量
21 cout<<"圆的半径是:"<<circle._radius<<endl;
22 cout<<"圆的面积是"<<circle.PI*circle._radius*circle._radius<<endl;
23 cout<<"友元函数修改半径:"<<endl;
24 circle._radius=1;
25 cout<<"圆的半径是:"<<circle._radius<<endl;
26 }
27 int main()
28 {
29 Circle circle(10);
30 getArea(circle);
31 return 0;
32 }
例2-14运行结果如图2-19所示。
在例2-14中,第3~12行代码定义了关于圆的类Circle,其中圆的半径_radius和圆周率PI是私有成员;第5行代码在类中声明了友元函数getArea(),用于计算圆的面积;第18~26行代码是getArea()函数的实现;第29行代码创建对象circle,并初始化圆的半径为10;第30行代码调用友元函数getArea()计算圆的面积,面积计算完成后,修改圆的半径为1。从图2-19可以看出,普通函数作为友元函数访问了类中的私有成员,且具有修改私有成员的权限。

图2-19 例2-14运行结果
2. 其他类的成员函数作为友元函数
其他类中的成员函数作为本类的友元函数时,需要在本类中表明该函数的作用域,并添加友元函数所在类的前向声明,其语法格式如下:
class B; //声明类B
class A
{
public:
int func(); //声明成员函数func()
};
class B
{
friend int A::func(); //声明类A的成员函数func()为友元函数
}
下面通过案例演示类的成员函数作为其他类的友元函数的用法,如例2-15所示。
例2-15 friendMember.cpp
1 #include<iostream>
2 #include<math.h>
3 using namespace std;
4 class Point;
5 class Circle
6 {
7 public:
8 float getArea(Point &p1,Point &p2); //声明计算面积的成员函数
9 private:
10 const float PI=3.14;
11 };
12 class Point
13 {
14 //声明类Circle的成员函数getArea()为友元函数
15 friend float Circle::getArea(Point &p1,Point &p2);
16 public:
17 Point(float x,float y);
18 ~Point();
19 private:
20 float _x;
21 float _y;
22 };
23 Point::Point(float x=0,float y=0):_x(x),_y(y) //实现Point类的构造函数
24 {
25 cout<<"初始化坐标点"<<endl;
26 }
27 Point::~Point(){}
28 float Circle::getArea(Point &p1,Point &p2)
29 {
30 double x=abs(p1._x-p2._x); //获取横轴坐标间的距离
31 float y=abs(p1._y-p2._y); //获取纵轴坐标间的距离
32 float len=sqrtf(x*x+y*y); //计算两个坐标点之间的距离
33 cout<<"获取两个坐标点之间的距离是"<<len<<endl;
34 return len*len*PI; //友元函数访问私有成员变量PI
35 }
36 int main()
37 {
38 Point p1(5,5);
39 Point p2(10,10);
40 Circle circle;
41 float area=circle.getArea(p1,p2);
42 cout<<"圆的面积是:"<<area<<endl;
43 return 0;
44 }
例2-15运行结果如图2-20所示。

图2-20 例2-15运行结果
在例2-15中,第4行代码声明类Point;第5~11行代码定义了圆类Circle;第12~22行代码定义了坐标点类Point,其中第15行代码将Circle类中的成员函数getArea()声明为友元函数。第28~35行代码是getArea()函数的实现,函数的参数为Point类对象的引用,该函数计算两个坐标点距离的绝对值,然后以距离作为圆的半径,计算圆的面积后返回。其中,第34行在计算圆的面积时访问了Circle类中的私有成员PI。第38~39行代码初始化坐标点p1和p2。第40~41行代码,创建对象circle,并通过对象circle调用友元函数getArea()计算圆的面积。由图2-20可知,程序成功计算出了两个坐标点之间的距离,并以此距离作为半径计算出了圆的面积。
除了可以声明函数为类的友元函数,还可以将一个类声明为友元类,友元类可以声明在类中任意位置。声明友元类之后,友元类中的所有成员函数都是该类的友元函数,能够访问该类的所有成员。
与声明友元函数类似,友元类也是使用关键字friend声明,其语法格式如下:
class B; //类B前向声明
class A
{
};
class B
{
friend class A; //声明类A是类B的友元类
}
下面通过案例演示友元类的用法,如例2-16所示。
例2-16 friendClass.cpp
1 #include<iostream>
2 using namespace std;
3 class Time //定义Time类,描述时分秒
4 {
5 public:
6 Time(int hour, int minute, int second); //声明有参构造函数
7 friend class Date; //声明类Date为友元类
8 private:
9 int _hour, _minute, _second;
10 };
11 class Date //定义Date类
12 {
13 public:
14 Date(int year, int month, int day); //声明有参构造函数
15 void showTime(Time& time); //声明显示时间的成员函数
16 private:
17 int _year, _month, _day;
18 };
19 Date::Date(int year, int month, int day) //实现Date类构造函数
20 {
21 _year = year;
22 _month = month;
23 _day = day;
24 }
25 void Date::showTime(Time& time)
26 {
27 cout << _year << "-" << _month << "-" << _day
28 << " " << time._hour << ":" << time._minute
29 << ":" << time._second << endl;
30 }
31 Time::Time(int hour,int minute,int second) //实现Time类构造函数
32 {
33 _hour = hour;
34 _minute = minute;
35 _second = second;
36 }
37 int main()
38 {
39 Time time(17,30,20); //创建Time对象
40 Date date(2019,10,31); //创建Date对象
41 date.showTime(time); //调用showTime()显示年月日、时分秒信息
42 return 0;
43 }
例2-16运行结果如图2-21所示。

图2-21 例2-16运行结果
在例2-16中,第3~10行代码定义了Tim e类,该类有三个成员变量_hour、_m inute和_second,分别表示时、分、秒;此外,Tim e类还声明了Date友元类;第11~18行代码定义了Date类,Date类有三个成员变量_year、_m onth和_day,分别用于表示年、月、日。第19~30行代码在类外实现Date类的构造函数和成员函数showTim e();第31~36行代码在类外实现Tim e类的构造函数;第39~40行代码分别创建对象tim e和date;第41行代码通过对象date调用成员函数showTim e(),并以对象tim e作为参数。由图2-21可知,程序成功设置了日期和时间,并且成功输出了日期和时间。
从面向对象程序设计来讲,友元破坏了封装的特性。但由于友元简单易用,因此在实际开发中较为常用,如数据操作、类与类之间消息传递等,可以提高访问效率。使用友元需要注意以下几点:
①友元声明位置由程序设计者决定,且不受类中public、private、protected权限控制符的影响。
②友元关系是单向的,即类A是类B的友元,但B不是A的友元。
③友元关系不具有传递性,即类C是类D的友元,类E是类C的友元,但类E不是类D的友元。
④友元关系不能被继承。
本章作为面向对象程序设计的基础,首先介绍了面向对象程序设计思想;其次讲解了类与对象的相关知识,包括类的概念与定义、对象的创建与使用;然后讲解了类的成员函数,包括构造函数、析构函数、拷贝构造函数;接着讲解了const与static关键字修饰类成员的用法;最后讲解了友元的相关知识,包括友元函数与友元类。
本章是学习C++面向对象程序设计的基础,大家要深入理解、掌握本章内容,为学习后续内容奠定坚实的基础。
一、填空题
1. 面向对象程序设计的三大特征是___、___、___。
2. 定义类的关键字为___。
3. 类的成员访问权限控制符包括___、___、___三种。
4. 完成类对象初始化的成员函数是___。
5. 类对象之间的赋值可以通过___实现。
二、判断题
1. 析构函数必须要有返回值。( )
2. 定义构造函数之后,类不再提供默认的构造函数。( )
3. 类的常成员函数可以调用类的非常成员函数。( )
4. 类的友元函数通过friendly关键字定义。( )
5. 类的友元函数不能访问类的私有成员。( )
三、选择题
1. 关于面向对象程序设计方法,下列说法中正确的是( )。
A.在数据处理过程中,采用的是自顶向下、分而治之的方法
B.将整个程序按功能划分为几个可独立编程的子模块
C.以“对象”和“数据”为中心
D.数据和处理数据的过程代码是分离的、相互独立的实体
2. 阅读下列程序:
#include<iostream>
using namespace std;
class MyClass {
public:
MyClass() { cout << 'A'; }
MyClass(char c) { cout << c; }
~MyClass() { cout << 'B'; }
};
int main()
{
MyClass p1, *p2;
p2 = new MyClass('X');
delete p2;
return 0;
}
程序的运行结果为( )。
A.ABX
B.ABXB
C.AXB
D.AXBB
3. 下列情况下,会调用拷贝构造函数的是( )(多选)。
A.创建类的对象
B.用一个对象去初始化同一类的另一个对象
C.类的对象生命周期结束
D.函数的返回值是类的对象,函数执行完成返回调用
4. 关于静态成员,下列说法中错误的是( )。
A.静态成员不属于对象,是类的共享成员
B.静态数据成员要在类外初始化
C.静态成员函数拥有this指针
D.非静态成员函数也可以操作静态数据成员
5. 关于友元,下列说法中正确的是( )。
A.类可以定义友元函数和友元类
B.友元函数只能调用类的成员函数,不能访问类的成员变量
C.友元类只能访问类的成员变量,不能调用类的成员函数
D.以上说法都不对
四、简答题
1. 简述你对面向对象程序设计的三大特征的理解。
2. 简述一下什么是浅拷贝与深拷贝。
五、编程题
设计一个Bank类,实现银行某账户的资金往来账目管理。程序要求完成以下操作。
①创建账户:包括账号、创建日期、余额(创建账户时存入的钱数)。
②存钱:执行存钱操作,并记录存钱日期和存钱数目。
③取钱:执行取钱操作,并记录取钱日期和取钱数目。
④查询交易明细:查询近一个月的账户交易记录。
①设计Bank类私有成员变量:账号、日期、余额。
②定义一个数组存储每一次存钱、取钱的交易记录,以便查询。