第2章 类与对象

学习目标

★ 了解面向对象程序设计思想

★ 掌握类的定义

★ 掌握对象的创建与使用

★ 理解C++中类的封装

★ 理解this指针的作用机制

★ 掌握自定义构造函数的定义与调用

★ 掌握构造函数的重载

★ 掌握含有类成员对象的构造函数的定义与调用

★ 掌握析构函数

★ 掌握拷贝构造函数的定义与调用

★ 理解深拷贝与浅拷贝的区别

★ 掌握const与static修饰类的成员

★ 掌握友元函数的定义与调用

★ 了解友元类的定义与使用

面向对象是程序开发领域中的重要思想,这种思想符合人类认识客观世界的逻辑,是当前计算机软件工程学的主流思想。C++在设计之初就是一门面向对象语言,了解面向对象程序设计思想对于学习C++开发至关重要。在面向对象中,类和对象是非常重要的两个概念,本章将针对面向对象中的类和对象进行详细的介绍。

2.1 面向对象程序设计思想

面向对象是一种符合人类思维习惯的程序设计思想。现实生活中存在各种形态不同的事物,这些事物之间存在着各种各样的联系。在程序中使用对象映射现实中的事物,利用对象之间的关系描述事物之间的联系,这种思想就是面向对象。

面向过程是分析出解决问题所需要的步骤,然后用函数把这些步骤一一实现,使用的时候依次调用就可以了。面向对象不同于面向过程,它是把构成问题的事物按照一定规则划分为多个独立的对象,然后通过调用对象的方法解决问题。当然,一个应用程序会包含多个对象,通过多个对象的相互配合即可实现应用程序所需的功能,这样当应用程序功能发生变动时,只需要修改个别对象就可以了,使代码更容易维护。

面向对象程序设计思想有三大特征:封装、继承和多态。下面针对这三个特征进行简单的介绍。

1. 封装

封装是面向对象程序设计思想最重要的特征。封装就是隐藏,它将数据和数据处理过程封装成一个独立性很强的模块,避免外界直接访问对象属性而造成耦合度过高以及过度依赖。

封装是面向对象的核心思想,将对象的属性和行为封装起来,行为对外提供接口,不需要让外界知道具体的实现细节。例如,用户使用电脑,只需要通过外部接口连接鼠标、键盘等设备操作电脑就可以了,无须知道电脑内部的构成以及电脑是如何工作的。

2. 继承

继承主要描述的是类与类之间的关系,通过继承无须重新编写原有类,就能对原有类的功能进行扩展。例如,有一个交通工具类,该类描述了交通工具的特性和功能,而小汽车类不仅包含交通工具的特性和功能,还应该增加小汽车特有的功能,这时可以让小汽车类继承交通工具类,在小汽车类中单独添加小汽车特有的属性和方法就可以了。继承不仅增强了代码复用性,提高了开发效率,而且也为程序的维护提供了便利。

在软件开发中,继承使软件具有开放性、可扩充性,这是数据组织和分类行之有效的方法,它简化了类和对象的创建工作量,增强了代码的可重用性。

3. 多态

多态是指事物的多种表现形态。例如,上课铃声响起后,各科老师准备去不同的班级上课,上体育课的学生在操场站好了队等体育老师发布口令,上文化课的学生听到铃声后回到各自的班级,这就是多态。

在面向对象程序设计思想中,多态就是不同的对象对同一信息产生不同的行为。多态是面向对象技术中的一个难点,很多初学者都难以理解。面向对象的多态特性使得开发更科学、更符合人类的思维习惯,更有效地提高软件开发效率,缩短开发周期,提高软件可靠性。

上述特征适用于所有的面向对象语言,深入了解这些特征是掌握面向对象程序设计思想的关键。面向对象的思想只有通过大量的实践去学习和理解才能真正领悟。

2.2 初识类和对象

在面向对象程序设计思想中,类和对象是非常重要的两个概念。如果要掌握C++这门面向对象的程序设计语言,有必要先学习类和对象。类和对象的关系,如同建筑设计图纸与建筑物的关系,类是对象的模板,对象是类的实体。本节将针对类和对象进行详细讲解。

2.2.1 类的定义

类是对象的抽象,是一种自定义数据类型,它用于描述一组对象的共同属性和行为。类的定义格式如下所示:

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

如果函数名前没有类名和作用域限定符“::”,则函数不是类的成员函数,而是一个普通的函数。

为了大家更好地理解成员的访问规则,下面针对权限控制符进行具体介绍。

2.2.2 对象的创建与使用

定义了类,就相当于定义了一个数据类型。类与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创建类对象

类是自定义数据类型,与基本数据类型的使用方式相同,也可以使用new创建类对象。例如,例2-1定义的Student类,可以使用new创建Student类对象,示例代码如下所示:

Student* ps = new Student;      //使用new创建类对象 
//…其他功能代码 
delete ps;         //使用delete释放对象 

2.3 封装

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的姓名和年龄将其输出。