C++学习笔记:C++类的相关内容

本文最后更新于:2024年3月29日 上午

类和面向对象编程

面向对象编程(通常缩写为OOP)的本质是,根据要解决的问题范围内所涉及到的对象来编写程序。而基本类型的变量不允许为真实世界中的对象(甚至想象中的对象)建立完整的模型,比如我们不可能仅用 int、double 或其他基本数据类型给篮球队员建模,我们需要使用几个不同类型的值,对篮球队员进行有意义的描述。

结构提供了一种可能的解决方案。C++中结构类型还可以把函数作为其定义的一个组成部分。但是这并不是面向对象编程赋予的所有内容。

除了用户定义的类型这个理念以外,面型对象编程还隐含地组合许多更重要的理念(最著名的有封装和数据隐藏、继承和多态性)。而结构并没有涵盖全部内容,下面简单说明这些OOP概念的含义。

封装

一般情况下,给定类型的对象定义需要组合一些不同的内容,使该对象成为我们希望的那个实体。

  • 对象包含一组特定的数据值,来详细描述该对象,以符合我们的要求。对于一个盒子,它有 3 个尺寸:长度、宽度和高度。对于航空母舰,就需要更多的数据来描述。
  • 对象还包含一组函数,这些函数可以使用或改变对象的数据值。它们定义了可以应用于对象的一组操作,即可以对对象做什么,或不能做什么。

给定类型的每个对象都组合了下述内容:一组数据值,作为数据成员;一组操作,作为函数成员。把这些数据值和函数打包到一个对象中,就称为封装

图 1 展示了银行一个贷款账户的对象的例子:

图1 封装的例子

本例中,一个数据成员包含债务余额,另一个数据成员包含利率。每个对象还包含一组成员函数,它们定义了对象上的操作,可以计算利息并把它加到债务余额上。

数据隐藏

理想情况下,LoanAccount 对象的数据成员应不直接受外界的干扰,银行当然不希望贷款账户的余额或者利率被随意修改,因此数据成员只能以可控制的方式来修改。

在一般情况下,不允许访问对象的数据值,这称为数据隐藏

继承

继承是根据一个类型定义另一个类型的能力。

例如,假定定义了一个 BankAccount 类型,它包含的成员可以处理银行账户的许多事务。而继承允许把类型 LoanAccount 创建为 BankAccount 的一个特殊类型,即把 LoanAccount 定义为像是一个 BankAccount,但它有一些额外的属性和自己的函数。LoanAccount 类型继承了 BankAcount 的所有成员,BankAccount 就称为它的基类。LoanAccount 派生于 BankAccount。每个 LoanAccount 对象都包含了 BankAccount 对象的所有成员,它还可以定义自己的新成员,或重新定义继承下来的函数,使它们在自己的环境下更有意义。

扩展这个例子,再创建一个新类型 CheckingAccount,它给 BankAccount 添加了新特性。如图 2 所示:

图2 继承的例子

在这个例子中,CheckingAccount 添加了一个数据成员 overdraftFacility,这是该类型唯一的数据成员。另外,两个派生的类都重新定义了从基类继承而来的 calcInterest() 成员函数(未显示函数实现,不太直观),这是可行的,因为计算和处理支票账户的利息所涉及的事务与贷款账户有所不同。

多态性

多态性表示在不同的时刻有不同的形态。C++ 中的多态性总是涉及到使用指针或引用来调用对象的成员函数。这种函数调用在不同的时刻有不同的效果——函数调用有多种不同的形式。这种机制仅适合于派生于通用类型的对象,例如 BankAccount 类型。多态性意味着,属于一组继承性相关的类的对象可以通过基类指针和引用来传送和操作。

在上面的例子中,LoanAccount 和 CheckingAccount 对象都可以使用 BankAccount 的指针或引用来传送。而该指针或引用可以用于调用它指向的对象所继承的成员函数。下面用一个例子来说明这个概念。

假定在 BankAccount 类型的基础上定义了 LoanAccount 和 CheckingAccount 类型,并定义了这些类型的对象 debt 和 cash,如图 3 所示。由于这两个类型都基于 BankAccount 类型,因此指向 BankAccount 的变量类型(如图 3 中的 pAcc)就可以用于存储这两个对象的地址。

图3 多态性的例子

多态性的优点是通过 pAcc->calcInterest() 调用的函数会根据 pAcc 指向的对象发生变化。如果它指向 LoanAccount 对象,就调用该对象的 calclnterest() 函数,利息就是从账户中借的。如果它指向 CheckingAccount 对象,结果就完全不同,因为会调用该对象的 calclnterest() 函数,利息会加到账户中。通过指针调用哪个函数并不是在编译程序时确定的,而是在程序执行时才确定。

定义类

和定义结构一样,只需要把关键字 struct 换成关键字 class即可,如下所示:

1
2
3
4
5
6
7
8
9
10
class Box{
double length;
double width;
double height;

//Function to calculate the volume of a box
double volume() {
return length * width * height;
}
};

这是仅仅把结构的定义中关键字 struct 替换成 class,但是这样定义无法访问该对象的任何成员。在类的外部不能引用任何数据成员或调用函数 volume(),否则,代码无法编译。

这是因为,类的所有成员在默认情况下都是隐藏的,而结构的成员在默认情况下是公共的。为了在类外部的函数中访问类对象的成员,必须使用关键字 public 把该成员声明为类的公共成员。

修改上述 Box 类的定义,把类的成员声明为公共成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Box
{
public:
double length;
double width;
double height;

// Function to calculate the volume of a box
double volume()
{
return length * width * height;
}
};

关键字 public 是一个访问指定符(或者叫做访问限定符、访问修饰符)。访问指定符确定类的成员是否能在程序的各个部分访问。公共的类成员可以直接在类的外部访问,因此这些成员是不隐藏的。为了把类成员指定为公共的,可以使用关键字 public 后跟一个冒号。在类的定义中,这个访问指定符后面的所有类成员都是公共的,直到使用另一个访问指定符为止。

另外两个访问指定符是 private 和 protected,即私有的类成员和受保护的类成员,它们都是隐藏的,但是私有的成员只能由类和友元函数访问,而受保护的类成员可由派生类访问。

构造函数

构造函数在定义类的新实例时调用,它可以在创建新对象时初始化它,确保数据成员仅包含有效的值。

如果在类的定义中包含一个构造函数,类的对象就不能用花括号中的数据值进行初始化。

类的构造函数常常与包含它的类同名。例如函数 Box() 就是类 Box 的构造函数。另外,构造函数没有返回值,因此没有返回类型。为构造函数指定返回类型是错误的,甚至不能把它写为 void 。类构造函数的主要作用是在创建类对象时,为它的所有数据元素赋予并验证初始值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//程序示例1:使用类构造函数
#include <iostream>
using std::cout;
using std::endl;

// Class to represent a box
class Box
{
public:
double length;
double width;
double height;

// Constructor
Box(double lengthValue, double widthValue, double heightValue)
{
cout << "Box constructor called" << endl;
length = lengthValue;
width = widthValue;
height = heightValue;
}

// Function to calculate the volume of a box
double volume()
{
return length * width * height;
}
};

int main()
{
Box firstBox(80.0, 50.0, 40.0);
// Calculate the volume of the box
double firstBoxVolume = firstBox.volume();
cout << "Size of first Box object is "
<< firstBox.length << " by " << firstBox.width << " by "
<< firstBox.height
<< endl;
cout << "Volume of first Box object is " << firstBoxVolume
<< endl;
return 0;
}

在 main() 中,下面的语句声明了对象 firstBox:

1
Box firstBox(80.050.040.0);

数据成员 length、width 和 height 的初始值放在对象名后面的括号中,它们会作为参数专送给构造函数。在调用构造函数时,会显示第一行输出信息,以证明类定义的确调用了刚才加到类中的构造函数。

在类定义中声明了一个构造函数后,就不能再使用列表来初始化对象的数据成员了。

默认的构造函数

声明的每个类至少有一个构造函数,因为类的对象总是用构造函数创建的。

如果没有为类定义构造函数,编译器就会提供一个默认的构造函数,用于创建类的对象。

默认的构造函数没有参数。在声明类时,只要没有定义类的构造函数,编译器就会自动提供一个默认的构造函数。一旦添加了自己的构造函数,编译器就假定该构造函数是默认的构造函数,因此不再提供构造函数。下面的例子演示了默认的构造函数如何影响代码。

下面在程序中定义第二个 Box 对象,但其初始值与 firstBox 不同。修改程序示例 1 ,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main()
{
Box firstBox(80.0, 50.0, 40.0);
// Calculate the volume of the box
double firstBoxVolume = firstBox.volume();
cout << "Size of first Box object is "
<< firstBox.length << " by " << firstBox.width << " by "
<< firstBox.height
<< endl;
cout << "Volume of first Box object is " << firstBoxVolume
<< endl;

Box smallBox;
smallBox.length = 10.0;
smallBox.width = 5.0;
smallBox.height = 4.0;
// Calculate the volume of the small box
cout << "Size of small Box object is"
<< smallBox.length << "by"
<< smallBox.width << "by"
<< smallBox.height
<< endl;
cout << "Volume of small Box object is " << smallBox.volume()
<< endl;

return 0;
}

新的代码试图创建一个新对象 smallBox,但其声明并未给构造函数提供初始值,而是用 3 个赋值语句来显示设置数据成员,但是,这段代码编译会出错。如下所示:

编译器会一直寻找默认的构造函数(即没有参数的构造函数),但是,我们已经给类声明了一个构造函数,所以编译器不会生成默认的构造函数。

编译器生成的默认构造函数什么也不做——尤其是,默认的构造函数不会初始化所创建的对象中非类类型的数据成员。类类型的数据成员只能调用其默认构造函数才能初始化。这非常不理想:我们的目标是控制包含在变量中的值,如果计划使用默认的构造函数来创建对象,就要以其他方式提供变量值。为了使 main() 的新版本能正常工作,可以在类定义中添加自己的默认构造函数。

下面把我们自己的默认构造函数添加到上一个例子中。之后,再添加调用默认构造函数的代码,并在以后初始化数据成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//程序示例2:定义及使用默认构造函数
#include <iostream>
using std::cout;
using std::endl;

// Class to represent a box
class Box
{
public:
double length;
double width;
double height;

// Constructor
Box(); // Default Constructor
Box(double lengthValue, double widthValue, double heightValue);

// Function to calculate the volume of a box
double volume();
};

Box::Box()
{
cout << "Default constructor called" << endl;
length = width = height = 1.0;
}

Box::Box(double lengthValue, double widthValue, double heightValue)
{
cout << "Box constructor called" << endl;
length = lengthValue;
width = widthValue;
height = heightValue;
}

double Box::volume()
{
return length * width * height;
}

int main()
{
Box firstBox(80.0, 50.0, 40.0);
// Calculate the volume of the box
double firstBoxVolume = firstBox.volume();
cout << "Size of first Box object is "
<< firstBox.length << " by " << firstBox.width << " by "
<< firstBox.height
<< endl;
cout << "Volume of first Box object is " << firstBoxVolume
<< endl;

Box smallBox;
smallBox.length = 10.0;
smallBox.width = 5.0;
smallBox.height = 4.0;
// Calculate the volume of the small box
cout << "Size of small Box object is"
<< smallBox.length << " by "
<< smallBox.width << " by "
<< smallBox.height
<< endl;
cout << "Volume of small Box object is " << smallBox.volume()
<< endl;

return 0;
}

在程序的这个版本中,我们提供了自己的构造函数,因此编译器不会提供默认的构造函数。

重要的是,我们有了自己的默认构造函数。

默认构造函数的一个主要特性是,在调用它时不需要指定参数列表,甚至不需要括号。上面的语句仅指定了类类型 Box 和对象名 smallBox,因此会调用默认构造函数。

这个例子被忽略的一点是重载了构造函数,Box 类有两个构造函数,它们仅参数列表不同。

默认的初始化值

可以为类的成员函数指定参数的默认值,包括构造函数在内。

如果把成员函数的定义放在类定义中,就可以把参数的默认值放在函数头里。

如果在类定义中仅包含了函数的声明,则默认的参数值就应放在声明中,而不应放在函数定义中。

在前面例子的默认构造函数中,Box 对象的默认尺寸是一个单位的盒子,即所有的边长都是 1。修改上一个例子的类定义,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Box
{
public:
double length;
double width;
double height;

// Constructor
Box(); // Default Constructor
Box(double lengthValue = 1.0, double widthValue = 1.0,
double heightValue = 1.0);

// Function to calculate the volume of a box
double volume();
};

注意,上述代码编译时会报错!!!

产生错误的原因是,Box smallBox;这个语句对两个构造函数的调用都是合法的。

像这样调用包含默认参数值的构造函数,不能与调用没有参数的默认构造函数区分开。没有指定任何参数,就意味着编译器无法区分这两个调用。

在构造函数中使用初始化列表

前面是在类的构造函数体中用显式的赋值语句来初始化对象的成员。

还有另一种可用的技术,即使用初始化列表。下面用类 Box 的构造函数来说明。

1
2
3
4
5
// Constructor definition using an initializer list
Box::Box(double lvalue, double wvalue, double hvalue) : length(lvalue), width(wvalue), height(hvalue)
{
cout << "Box constructor called" << endl;
}

数据成员的值不是在构造函数体的赋值语句中设置的。它们用函数表示法指定为初始化值,并显示在初始化列表中,作为函数头的一部分。例如,成员 length 通过 lvalue 的值进行初始化。注意构造函数的初始化列表与参数列表用冒号分隔开,每个初始化值用逗号分隔开。

实际上,这不仅仅是表示法不同,在初始化的方式上也有根本的区别。

在使用构造函数体中的赋值语句初始化数据成员时,首先要创建该数据成员(如果这是类的一个实例,就调用构造函数),再执行赋值语句。

而在使用初始化列表时,数据成员在创建时,就用初始值对它进行初始化。这要比在构造函数体中使用赋值语句的效率高得多,特别是在数据成员是一个类实例时,更是如此。

在构造函数中初始化参数的技术非常重要还有另一个原因。它是为某些类型的数据成员设置值的唯一方式。

使用 explicit 关键字

类的构造函数只有一个参数是非常危险的,因为编译器可以使用这种构造函数把参数的类型隐式转为类类型。在某些情况下,这会产生不良的后果。

假设定义一个类,该类定义了立方体的盒子,即所有的边长都相等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Cube
{
public:
double side;

Cube(double side);

double volume();
bool compareVolume(Cube aCube);
};

Cube::Cube(double length) : side(length) {}

double Cube::volume()
{
return side * side * side;
}

bool Cube::compareVolume(Cube aCube)
{
return volume() > aCube.volume();
}

构造函数只需要一个 double 类型的参数。显然,编译器可以使用构造函数把 double 值转换为 Cube 对象。该类还定义了 volume() 函数和 compareVolume() 函数,后者比较当前对象和另一个作为参数传送的Cube,如果当前对象的体积比较大,就返回 true。

下面运行下面这一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//程序示例3:explicit 关键字
#include <iostream>
using std::cout;
using std::endl;

class Cube
{
public:
double side;

Cube(double side);

double volume();
bool compareVolume(Cube aCube);
};

Cube::Cube(double length) : side(length) {}
double Cube::volume()
{
return side * side * side;
}

bool Cube::compareVolume(Cube aCube)
{
return volume() > aCube.volume();
}

int main()
{
Cube box1(5.0);
Cube box2(2.0);

//比较 box1 和 box2 两个对象的体积大小
if (box1.compareVolume(box2))
cout << endl
<< "box1 is larger";
else
cout << endl
<< "box1 is not larger";

/*
如果有人误解compareVolume() 函数,认为其把当前对象的体积与一个数字比较,如下所示比较 box1 的体积与 50.0。
*/
if (box1.compareVolume(50.0))
cout << endl
<< "Volume of box1 is greater than 50";
else
cout << endl
<< "Volume of box1 is not greater than 50";

return 0;
}

运行结果如下所示:

后面一部分代码很显然并不满足要求,但是编译器仍会编译这段代码,因为这个构造函数可以把参数 50.0 转换为一个 Cube 对象。编译器生成的代码如下所示:

1
2
3
4
5
6
7
8
9
if (box1.compareVolume(Cube(50.0))){
cout << endl
<< "Volume of box1 is greater than 50"
<< endl;
} else {
cout << endl
<< "Volume of box1 is not greater than 50"
<< endl;
}

函数并没有把 box1 对象的体积与 50.0 相比较,而是与 Cube(50.0) 的体积即 125000.0 进行比较。实际结果与期望的完全不同。但把构造函数声明为 explicit,就可以避免这种情况:

1
2
3
4
5
6
7
8
9
10
class Cube
{
public:
double side;

explicit Cube(double side);

double volume();
bool compareVolume(Cube aCube);
};

将程序修改为上面这一段代码,编译器会报错:

这样可以避免单个参数的构造函数进行隐式类型转换。

类的私有成员

一般情况下,在面向对象编程中,最好尽可能地把类的数据成员设置为私有成员。

类的公共成员一般是函数,有时称为类的接口。类接口提供了处理和操作类对象的方式,确定可以对对象做什么,以及对象可以完成什么任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//程序示例4:使用带有私有成员的类
#include <iostream>
using std::cout;
using std::endl;

// Class to represent a box
class Box
{
public:
// Constructor
Box(double lengthValue = 1.0, double widthValue = 1.0, double heightValue = 1.0);

// Function to calculate the volume of a box
double volume();

private:
double length;
double width;
double height;
};

Box::Box(double lValue, double wValue, double hValue) : length(lValue), width(wValue), height(hValue)
{
cout << "Box constructor called" << endl;
// Ensure positive dimensions
if (length <= 0.0)
length = 1.0;
if (width <= 0.0)
width = 1.0;
if (height <= 0.0)
height = 1.0;
}

double Box::volume()
{
return length * width * height;
}

int main()
{
cout << endl;

Box firstBox(2.2, 1.1, 0.5);
Box secondBox;
Box *pthirdBox = new Box(15.0, 20.0, 8.0);

cout << "Volume of first box="
<< firstBox.volume()
<< endl;

cout << "Volume of second box="
<< secondBox.volume()
<< endl;

cout << "Volume of third box="
<< pthirdBox->volume()
<< endl;

delete pthirdBox;
return 0;
}

成员函数的定义可以在类的外部,这不会影响类成员的可访问性。无论把成员函数的定义放在什么地方,都可以在类的成员函数的函数体中访问类的所有成员。

访问私有类成员

把类的数据成员声明为私有成员是比较极端的做法。这样可以完全禁止对它们进行未经授权的修改,但也存在一个严重的限制:如果不知道某个Box对象的尺寸,就永远都不知道。一定要这样保密吗?

要解决这个问题,并不需要把数据成员用 public 关键字来声明,只需添加一个类的成员函数,返回数据成员的值即可。为了访问 Box 对象的尺寸,只需在类定义中添加 3 个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Box
{
public:
// Constructor
Box(double lengthValue = 1.0, double widthValue = 1.0, double heightValue = 1.0);

// Function to calculate the volume of a box
double volume();

// Function to provide the values of data members
double getLength() { return length; }
double getwidth() { return width; }
double getHeight() { return height; }

private:
double length;
double width;
double height;
};

在Box类中添加了几个函数,返回数据成员的值。也就是说,数据成员的值可以访问,但不能修改它们,以维护类的完整性。

这种函数通常把定义放在类定义中,因为它们非常短,在默认情况下是内联函数。因此,获取数据成员值所涉及的系统开销就非常小。提取数据成员值的函数通常称为访问器成员函数。

默认的副本构造函数

假定下面的语句声明并初始化 Box 对象 firstBox:

1
Box firstBox(15.0, 20.0, 10.0);

现在要创建另一个 Box 对象,它与第一个对象完全相同。换言之,就是用 firstBox 初始化第二个 Box 对象。下面看看把 Box 对象用作构造函数的参数时会发生什么情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//程序示例5:创建对象的副本
#include <iostream>
using std::cout;
using std::endl;

// Class to represent a box
class Box
{
public:
// Constructor
Box(double lengthValue = 1.0, double widthValue = 1.0, double heightValue = 1.0);

// Function to calculate the volume of a box
double volume();

// Function to provide the values of data members
double getLength() { return length; }
double getwidth() { return width; }
double getHeight() { return height; }

private:
double length;
double width;
double height;
};

Box::Box(double lValue, double wValue, double hValue) : length(lValue), width(wValue), height(hValue)
{
cout << "Box constructor called" << endl;
// Ensure positive dimensions
if (length <= 0.0)
length = 1.0;
if (width <= 0.0)
width = 1.0;
if (height <= 0.0)
height = 1.0;
}

double Box::volume()
{
return length * width * height;
}

int main()
{
cout << endl;

Box firstBox(2.2, 1.1, 0.5);
Box secondBox(firstBox);

cout << "Volume of first box = "
<< firstBox.volume()
<< endl;

cout << "Volume of second box = "
<< secondBox.volume()
<< endl;

return 0;
}

程序运行结果如下:

程序运行结果与期望的一样,两个盒子有相同的体积。

但是,从输出可以看出,构造函数仅调用了一次(用于创建 firstBox)。那么,secondBox 对象是如何创建的?

其机制与没有定义构造函数时的情形类似,编译器会提供一个默认的构造函数来创建对象,这里则是编译器生成了所谓副本构造函数的默认版本。副本构造函数完成了我们希望完成的工作——创建类的一个对象,用该类已有的一个对象对它进行初始化。副本构造函数的默认版本会复制已有对象中的每个成员,以创建新的对象。副本构造函数可以用这种方式复制任何数据类型。

默认的副本构造函数适用于简单的类,例如这里的Box类,但在许多情况下,例如类把指针作为其成员,副本构造函数就会产生不良的后果。对于这种类,副本构造函数会在程序中产生严重的错误,此时,就需要为类定义自己的副本构造函数。

副本构造函数可以从类的已有对象中创建该类的新对象,定义它需要一种特殊的方法,本节不介绍。

友元

有时可以把某些选定的函数看作类的“荣誉成员”,允许它们访问类对象中非公共的成员,就好像它们是类的成员一样。这种函数称为类的友元。友元可以访问类对象的任意成员,无论这些成员的访问指定符是什么。有两种情形需要考虑:

  • 把一个函数指定为类的友元
  • 把整个类指定为另一个类的友元

在后者中,友元类的所有成员函数与原类的一般成员有相同的访问权限。下面首先看看作为友元的单个函数。

类的友元函数

如果某个函数不是类的一个成员,但可以访问类的所有成员,这个函数就称为该类的友元函数。为了把函数看作是类的友元函数,必须在类定义中用关键字 friend 来声明它。

注意:类的友元函数是一个全局函数,也可以是另一个类的成员。但是,函数不能是包含它的类的友元函数,因此,访问指定符不能应用于类的友元。

实际上,对友元函数的需求是比较有限的。当函数需要访问两个不同对象的内部时,才需要把该函数声明为这两个类的友元。这里在比较简单的情形中使用它们,在这个情形中实际上并不需要使用友元,但演示了友元的操作。

假定要在 Box 类中实现一个友元函数,计算 Box 对象的表面积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//程序示例6:用友元计算表面积
#include <iostream>
using std::cout;
using std::endl;

// Class to represent a box
class Box
{
public:
// Constructor
Box(double lengthValue = 1.0, double widthValue = 1.0, double heightValue = 1.0);

// Function to calculate the volume of a box
double volume();

// Friend Function
friend double boxSurface(const Box &theBox);

private:
double length;
double width;
double height;
};

Box::Box(double lValue, double wValue, double hValue) : length(lValue), width(wValue), height(hValue)
{
cout << "Box constructor called" << endl;
// Ensure positive dimensions
if (length <= 0.0)
length = 1.0;
if (width <= 0.0)
width = 1.0;
if (height <= 0.0)
height = 1.0;
}

double Box::volume()
{
return length * width * height;
}

int main()
{
cout << endl;

Box firstBox(2.2, 1.1, 0.5);
Box secondBox;
Box *pthirdBox = new Box(15.0, 20.0, 8.0);

cout << "Volume of first box = "
<< firstBox.volume()
<< endl;

cout << "Surface area of first box = "
<< boxSurface(firstBox)
<< endl;

cout << "Volume of second box = "
<< secondBox.volume()
<< endl;

cout << "Surface area of second box = "
<< boxSurface(secondBox)
<< endl;

cout << "Volume of third box = "
<< pthirdBox->volume()
<< endl;

cout << "Surface area of third box = "
<< boxSurface(*pthirdBox)
<< endl;

delete pthirdBox;
return 0;
}

// friend function to calculate the surface area of a Box object
double boxSurface(const Box &theBox)
{
return 2.0 * (theBox.length * theBox.width +
theBox.length * theBox.height +
theBox.height * theBox.width);
}

注意在函数 boxSurface() 的定义中,把 Box 对象作为一个参数传送给该函数,指定访问对象的数据成员。因为友元函数不是类成员,所以数据成员不能仅通过其名称来引用。它们必须用对象名来限定,其方法与在一般函数中访问类的公共成员一样。友元函数与一般函数一样,但友元函数可以不受限制地访问类中的所有成员。

友元类

可以把整个类声明为另一个类的友元。友元类的所有成员函数都可以不受限制地访问原类的成员。

例如,假定定义一个类 Carton,为了让类 Carton 的成员函数访问 Box 类的成员,只需在 Box 类定义中包含一个把 Carton 声明为 Box 的友元类的语句即可:

1
2
3
4
5
6
7
8
class Box
{
// Public members of the class..

friend class Carton

// Private members of the class..
};

友元关系并不是一个互惠的安排。类 Carton 中的函数现在可以访问 Box 类的所有成员,但 Box 类的函数不能访问类 Carton 中的私有成员。类之间的友元关系是不能传递的,即类 A 是类 B 的友元,类 B 又是类 C 的友元,但类 A 不是类 C 的友元。

友元类的一个常见用法是一个类的功能与另一个类的功能高度缠绕在一起。链表基本上涉及到两个类类型:存储一个对象列表(通常称为节点)的 List 类,和定义节点的 Node 类。List 类需要在每个 Node 对象中设置一个指针,使该指针指向下一个节点,从而把 Node 对象组合在一起。把 List 类声明为定义节点类的友元,可以使 List 类的成员直接访问 Node 类的成员。

this 指针

在 Box 类中,根据类定义中的类成员名编写了 volume() 函数。但已创建的每个 Box 类型的对象都包含这些成员,该函数必须有一种机制来引用调用它的那个对象的成员。换言之,在 volume() 函数中的代码引用类的 length 成员时,必须有一种方式引用调用函数的对象成员,而不是引用其他对象。

在执行任何类成员函数时,该函数都会自动包含一个隐藏的指针,称为 this,该指针包含了调用该函数的对象的地址。例如,在下面的语句中:

1
cout << firstBox.volume();

函数 volume() 中的指针 this 就包含了 firstBox 的地址。在为另一个 Box 对象调用该函数时,this 指针就设置为包含该对象的地址。

也就是说,在执行 volume() 函数的过程中访问数据成员 length,该数据成员就表示为 this->length,这是完全限定的对象成员的引用。编译器会把必要的指针名 this 添加到函数的成员名中。换言之,编译器把函数实现为:

1
2
3
double Box::volume(){
return this->length * this->width * this->height;
}

也可以把函数改写成显式使用指针 this,但这是不必要的。

但是在一些情况下,需要使用该指针。例如,在成员函数有多个相同类类型的参数,或者在需要返回当前对象的地址情况下都要使用 this 指针。

下面在一个例子中显式使用 this 指针,看看它的工作原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//程序示例7:显式使用 this
#include <iostream>
using std::cout;
using std::endl;

// Class to represent a box
class Box
{
public:
// Constructor
Box(double lengthValue = 1.0, double widthValue = 1.0, double heightValue = 1.0);

// Function to calculate the volume of a box
double volume();

// Function to compare two Box objects
int compareVolume(Box &otherBox);

private:
double length;
double width;
double height;
};

Box::Box(double lValue, double wValue, double hValue) : length(lValue), width(wValue), height(hValue)
{
cout << "Box constructor called" << endl;
// Ensure positive dimensions
if (length <= 0.0)
length = 1.0;
if (width <= 0.0)
width = 1.0;
if (height <= 0.0)
height = 1.0;
}

double Box::volume()
{
return length * width * height;
}

// Function to compare two Box objects
// If the current Box is greater than the argument,1 is returned
// If they are equal,0 is returned
// If the current Box is less than the argument,-1 is returned
int Box::compareVolume(Box &otherBox)
{
double vol1 = this->volume(); // Get current Box volume
double vol2 = otherBox.volume(); // Get argument volume
return vol1 > vol2 ? 1 : (vol1 < vol2 ? -1 : 0);
}

int main()
{
cout << endl;

Box firstBox(17.0, 11.0, 5.0);
Box secondBox(9.0, 18.0, 4.0);

cout << "the first box is "
<< (firstBox.compareVolume(secondBox) >= 0 ? "" : "not ")
<< "greater than the second box."
<< endl;

cout << "Volume of first box = "
<< firstBox.volume()
<< endl;

cout << "Volume of second box = "
<< secondBox.volume()
<< endl;
return 0;
}

在这个例子中使用 this 指针,只是为了说明它的存在,这里不显式使用同样可以。

从函数中返回 this

如果把成员函数的返回类型指定为类类型的指针,就可以从函数中返回 this。这是为对象连续调用成员函数提供的一个非常有用的功能。下面举一个例子。

假定给 Box 类添加一个变异函数,设置盒子的长度、宽度和高度,并让这些函数返回this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//类定义
class Box
{
public:
// Constructor
Box(double lengthValue = 1.0, double widthValue = 1.0, double heightValue = 1.0);

// Function to calculate the volume of a box
double volume();

// Function to compare two Box objects
int compareVolume(Box &otherBox);

//Mutator functions
Box* setLength(double lvalue);
Box* setWidth(double wvalue);
Box* setHeight(double hvalue);

private:
double length;
double width;
double height;
};

//··········

//函数实现
Box* Box::setLength(double lvalue){
if(lvalue > 0) length = lvalue;
return this;
}

Box* Box::setWidth(double wvalue){
if(wvalue > 0) width = wvalue;
return this;
}

Box* Box::setHeight(double hvalue){
if(hvalue > 0) height = hvalue;
return this;
}

下面就可以在一个语句中修改 Box 对象的所有尺寸:

1
2
3
Box aBox(10, 15, 25);
Box* pBox = &aBox;
pBox->setLength(20)->setWidth(40)->setHeight(10);

变异函数返回 this 指针,所以可以使用一个函数的返回值调用另一个函数。

const 对象和 const 成员函数

再次考虑 compareVolume() 成员函数,由于不能修改参数,因此在类定义中应把它声明为 const:

1
2
3
4
5
class Box{
//Reset of the class as before...

int compareVolume(const Box& otherBox);
}

但是,编译器会报错:

如果把一个对象指定为 const,就是告诉编译器不要修改它。在为常量对象 otherBox 调用函数 volume() 时,编译器必须把 otherBox 的地址通过 this 指针传送给函数,但不能保证函数不修改该对象。错误消息是不能转换 this 指针,因为编译器在默认情况下不能改变对象的 const 性质。

对于声明为 const 的对象,只能调用也声明为 const 的成员函数。const 成员函数不会修改调用它的对象。要把成员函数声明为 const,需要在类定义中在函数声明的最后加上关键字const 。对于处理 const 参数的 compareVolume() 函数,必须把 volume() 函数声明为 const。这样,类定义就变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Box
{
public:
// Constructor
Box(double lengthValue = 1.0, double widthValue = 1.0, double heightValue = 1.0);

// Function to calculate the volume of a box
double volume() const;

// Function to compare two Box objects
int compareVolume(const Box& otherBox);

private:
double length;
double width;
double height;
};

//··········

//volume()函数也要使用 const 关键字
double Box::volume() const
{
return length * width * height;
}

类中的 mutable 数据成员

如果把对象声明为 const,就只能调用 const 成员函数。不能修改对象的数据成员值,因为它们也是 const。但是,在一些情况下,即使对象声明为 const,也需要修改类中一些选定的数据成员。

例如,一个对象从远程源(如另一台计算机)上获取数据,并把该数据存储在一个提供内部缓冲区的数据成员中。类的一个对象需要更新其内部缓冲区,即使该对象声明为 const,也是如此。

为了满足这两种情况的要求,需要完成两个任务。首先需要从 const 对象中提取出一个特定的数据成员,其次需要在 const 成员函数中修改该数据成员的值,但成员函数的 const 声明不变。为此,可以把该数据成员声明为 mutable

为了说明其应用的方式,下面举一个简单的例子。假定为了安全起见,每次调用任何成员函数时,都在对象的一个数据成员中记录一个时间戳。该对象表示对某大厦的可控访问。此时把该对象声明为 const,但仍用时间截记录对象上一次使用的情况。

为此,把存储时间戳的数据成员声明为 mutable,这样就在该类的声明为 const 的对象中提取了一个数据成员,并允许通过 const 成员函数修改它。具体方法是在成员声明中使用关键字 mutable,如下所示:

1
2
3
4
5
6
7
8
9
10
class SecureAccess
{
public:
bool isLocked()const;
// More of the class definition..

private:
mutable int time;
// More of the class definition..
}

成员函数 isLocked() 的实现代码如下所示:

1
2
3
4
5
6
bool SecureAccess::isLocked() const
{
time = getcurrentTime(); // store time of function call
return lockstatus();
// Return the state of the door
}

假定如果门被锁上,lockStatus() 函数就返回 true,否则就返回 false。数据成员 time 声明为 mutable,它可以放在赋值语句的左边。在声明为 const 的成员函数中,只有声明为 mutable 的数据成员才能放在赋值语句的左边。

下面创建类的对象,把它声明为 const,调用成员函数 isLocked():

1
2
const SecureAccess mainDoor;
bool doorState = mainDoor.isLocked();

由于 mainDoor 对象是 const,所以只能调用它的 const 成员函数。类 SecureAccess 中的任何 const 成员函数都可以修改存储在成员 time 中的值,而不必考虑对象是否声明为 const。如果 time 没有声明为 mutable,则任何 const 成员函数试图修改它,都会产生一个编译错误。

类的对象数组

声明类的对象数组的方式与声明其他类型的数组的方式完全相同。类对象数组的每个元素都是独立创建的,为此,编译器要为每个元素调用默认构造函数。编译器不允许在定义语句中初始化数组。下面用一个例子来说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//程序示例8:创建对象数组
#include <iostream>
using std::cout;
using std::endl;

// Class to represent a box
class Box
{
public:
// Constructor
Box();
Box(double lengthValue, double widthValue, double heightValue);

// Function to calculate the volume of a box
double volume() const;

// Function to compare two Box objects
int compareVolume(const Box &otherBox) const;

private:
double length;
double width;
double height;
};

Box::Box()
{
cout << "Default constructor called" << endl;
length = width = height = 1.0;
}

Box::Box(double lValue, double wValue, double hValue) : length(lValue), width(wValue), height(hValue)
{
cout << "Box constructor called" << endl;
// Ensure positive dimensions
if (length <= 0.0)
length = 1.0;
if (width <= 0.0)
width = 1.0;
if (height <= 0.0)
height = 1.0;
}

double Box::volume() const
{
return length * width * height;
}

// Function to compare two Box objects
// If the current Box is greater than the argument,1 is returned
// If they are equal,0 is returned
// If the current Box is less than the argument,-1 is returned
int Box::compareVolume(const Box &otherBox) const
{
double vol1 = this->volume(); // Get current Box volume
double vol2 = otherBox.volume(); // Get argument volume
return vol1 > vol2 ? 1 : (vol1 < vol2 ? -1 : 0);
}

int main()
{
cout << endl;

Box firstBox(17.0, 11.0, 5.0);
Box boxes[5];

cout << "Volume of first box = "
<< firstBox.volume()
<< endl;

const int count = sizeof boxes / sizeof boxes[0];

cout << "The boxes array has " << count << " elements."
<< endl;

cout << "Each element occupies " << sizeof boxes[0] << " bytes."
<< endl;

for (int i = 0; i < count; i++)
cout << "Volume of boxes[" << i << "]= "
<< boxes[i].volume()
<< endl;
return 0;
}

在输出中可以看出,创建每个数组元素时都会调用一次默认构造函数。这个语句定义了 5 个对象,每个对象的类型都是 Box。由于定义的是一个数组,因此不能为构造函数提供参数。而由于没有提供参数,编译器就使用默认的构造函数创建数组中的 5 个对象。

类的静态成员

类的数据成员和成员函数都可以声明为 static。类的静态数据成员可以在类的范围内存储数据,这种数据独立于类类型中的任何对象,但可以由这些对象访问。它们把类作为一个整体来记录类的属性,而不是记录各个对象的属性。使用静态数据成员可以存储类的特定常量,或存储类中对象的一般信息,例如类中有多少个对象等。

静态成员函数有一种独立于单个类对象的计算能力,但如果需要,任何类对象都可以调用该静态成员变量。如果该函数是一个公共成员,还可以从类的外部调用。静态成员函数的一个常见用法是无论是否声明了类的对象,都可以操作静态数据成员。

类的静态数据成员

类的静态数据成员与作为一个整体的类相关,而与类的各个对象无关。

在把类的数据成员声明为 static 时,静态数据成员就只定义一次,而且即使类没有创建对象实例,该静态数据成员依然存在。每个静态数据成员都可以在已创建的任何类对象中访问,并在已有的所有对象之间共享。对象包含类的每个原数据成员的独立副本,但无论定义了多少个类对象,静态数据成员总是只有一个。

使用静态数据成员可以记录在类的范围内使用的信息。静态数据成员的一个用途是计算有多少个类对象存在。在类定义中添加如下语句,为 Box 类添加一个静态数据成员:

1
static int objectCount;//count of objects in existence

现在有一个问题。如何初始化静态数据成员?不能把它放在类声明中,类声明只是对象的一个蓝图,不允许初始化值。也不能在构造函数中初始化它,因为每次调用构造函数时,都要递增这个数据成员。而且,即使不存在对象,这个数据成员也存在(此时并没有调用构造函数)。同样,还不能在另一个成员函数中初始化它,因为成员函数与对象相关,而该数据成员应在创建任何对象之前初始化。

这个问题的答案是在类声明的外部初始化它,初始化语句如下所示:

1
int Box::objectCount = 0//Initialize static member of class Box

注意:关键字 static 并没有包含在定义中。

即使静态数据成员被指定为私有成员,仍可以以这种方式初始化它。事实上,这也是初始化它的惟一方式。当然,由于它是私有的,就不能在类的外部直接访问 objectCount。

由于这个语句定义并初始化了类的静态成员,因此该成员在程序中就只能定义一次。因此,定义它的语句就应放在 Box.cpp 文件中。

类的静态成员函数

把函数成员声明为 static,就可以使它独立于类的对象。与静态数据成员一样,即使没有创建类的对象,类的静态成员函数也存在。声明类中的静态函数非常简单,只需使用关键字 static 即可。

注意:静态成员函数不能声明为 const。因为静态成员函数与类的对象无关,它没有 this 指针,所以不能使用 const 关键字。

静态函数的优点在于:即使不存在类的对象,它们也存在,并且可以调用。可以把类名作为限定符来调用静态成员函数。

它与一般成员函数的区别是,静态函数不能访问调用它的对象。为了让静态成员函数访问类的对象,需要把它作为该函数的一个参数传送。之后,就必须使用限定的名称在静态函数中引用类对象的成员(就象一般全局函数在访问公共数据成员一样)。


资料来源:《C++入门经典(第 3 版)》


C++学习笔记:C++类的相关内容
https://summersong.top/post/5fb690a4.html
作者
SummerSong
发布于
2022年7月27日
更新于
2024年3月29日
许可协议