Essential C++ 第四章 基于对象的编程风格

一般·而言,class 由两部分组成:一组公开的(public)操作函数和运算符,以及一组私有的(private)实现细节。

4.1 如何实现一个 Class

一般而言,我们会从所谓的抽象开始。我们以栈(stack)为例,栈是一种后进先出。

Class定义由两部分组成:class 的声明,以及紧接在声明之后的主体。主体部分由一对大括号括住,并以分号结尾。主体内的两个关键字publicprivate,用来标示每个块的"menber 访问权限"。Public menmber 可以在程序的任何地方被访问,private menber 只能在

member function或是 class fried内被访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Stack.h
#ifndef CIRCLE_H
#define CIRCLE_H

//头文件
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Stack{
public:
bool push(const string&);
bool pop(string &elem);
bool top(string &elem);
bool empty() const {return _stack.empty();}
bool full() const {return _stack.size() == _stack.max_size();}
int size() const {return _stack.size();}

private:
vector<string> _stack;
};

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//stack.cpp
#include "Stack.h"

bool Stack::pop(string &elem){
if(empty()) return false;
elem = _stack.back();
_stack.pop_back();
return true;
}

bool Stack::top(string &elem){
if(empty()) return false;
elem = _stack.back();
return true;
}

bool Stack::push(const string &elem){
if(full()) return false;
_stack.push_back(elem);
return 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
//main.cpp
#include "stack.cpp"

int main(){
Stack st;
string str;

while(cin >> str && !st.full())
st.push(str);

if(st.empty()){
cout << '\n' << "Oops: no strings were read -- bailing out\n";
return 0;
}
//求栈顶
st.top(str);
if(st.size() == 1 && str.empty()){
cout << '\n' << "Oops: no strings were read -- bailing out\n";
return 0;
}
cout << '\n' << "Read in " << st.size() << " strings!\n"
<< "The strings, in reverse order: \n";

while(st.size()){
if(st.pop(str)){
cout << str << ' ';
}
}

cout << '\n' << "There are now " << st.size() << " elements in the stack!\n";
}

4.2 什么是构造函数和析构函数

构造函数

如果我们提供一个或多个特别的初始化函数,编译器就会在每次Class object 被定义出来时,调用适当的函数加以处理。这些特别的初始化函数称为构造函数。

构造函数的函数名称必须与class 名称相同。构造函数不应指定返回类型,亦不用返回任何值。它可以被重载。

下面的程序包括一个名为 Demo 的类,其构造函数除了打印消息之外什么都不做。编写它的目的就是为了演示构造函数何时执行。因为 Demo 对象是在两个 cout 语句之间创建的,所以构造函数将在这两个语句生成的输出行之间打印它的消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

class Demo{
public:
Demo(){
cout << "Now the constructor is running.\n";
}
};

int main(){
cout << "This is displayed before the object is created. \n";
Demo demoObj; // Define a Demo object
cout << "This is displayed after the object is created.\n";
return 0;
}

程序输出为:

This is displayed before the object is created. Now the constructor is running. This is displayed after the object is created.

程序中,将构造函数定义为类声明中的内联函数。当然,像任何其他类成员函数一样,也可以将其原型放在类声明中,然后将其定义在类之外。在这种情况下,需要添加函数所属类的名称和函数名前面的作用域解析运算符。但是由于构造函数的名称与类名相同,所以名称会出现两次。

以下就是在类声明之外定义 Demo 构造函数时,其函数头的样子:

1
2
3
4
Demo:: Demo ()    // 构造函数
{
cout << "Now the constructor is running. \n";
}

重载构造函数

我们知道,当两个或多个函数共享相同的名称时,函数名称被称为重载。只要其形参列表不同,C++ 程序中可能存在具有相同名称的多个函数。

任何类成员函数都可能被重载,包括构造函数。例如,某个构造函数可能需要一个整数实参,而另一个构造函数则需要一个 double,甚至可能会有第三个构造函数使用两个整数。只要每个构造函数具有不同的形参列表,则编译器就可以将它们分开。

下面的程序声明并使用一个名为 Sale 的类,它有两个构造函数。第一个构造函数的形参接受销售税率;第二个构造函数是免税销售,没有形参。它将税率设置为 0。这样一个没有形参的构造函数称为默认构造函数。

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
#include <iostream>
#include <iomanip>
using namespace std;

//Sale class declaration
class Sale{
private:
double taxRate;
public:
// 一个参数的构造函数
Sale(double rate){
taxRate = rate;
}
//默认构造函数
Sale(){
taxRate = 0.0;
}
double calcSaleTotal(double cost){
double total = cost + cost * taxRate;
return total;
}
};

int main(){
// Define a Sale object with 6% sales tax
Sale cashier1(.06);
// Define a tax-exempt Sale object
Sale cashier2;
// Format the output
cout << fixed << showpoint << setprecision(2);
// Get and display the total sale price for two $24.95 sales
cout << "With a 0.06 sales tax rate, the total\n";
cout << "of the $24.95 sale is $";
cout << cashier1.calcSaleTotal(24.95) << endl;
cout << "\nOn a tax-exempt purchase, the total\n";
cout << "of the $24.95 sale is, of course, $";
cout << cashier2.calcSaleTotal(24.95) << endl;
return 0;
}

程序输出结果:

With a 0.06 sales tax rate, the total

of the $24.95 sale is $26.45

On a tax-exempt purchase, the total

of the $24.95 sale is, of course, $24.95

注意看此程序如何定义的两个 Sale 对象:

Sale cashier1(.06);

Sale cashier2;

在 cashier1 的名称后面有一对括号,用于保存值,发送给有一个形参的构造函数。但是,在 Cashier2 的名称后面就没有括号,它不发送任何参数。在 C++ 中,当使用默认构造函数定义对象时,不用传递实参,所以不能有任何括号,即:

Sale cashier2 () ; // 错误

Sale cashier2; // 正确

默认构造函数

Sale 类需要一个默认构造函数来处理免税销售的情况,但是其他类可能并不需要这样一个构造函数。例如,如果通过类创建的对象总是希望将实参传递给构造函数。那么,在设计一个具有构造函数的类时,应该包括一个默认构造函数,这在任何时候都会被认为是一个很好的编程实践。

如果没有这样一个默认构造函数,那么当程序尝试创建一个对象而不传递任何参数时,它将不会编译,这是因为必须有一个构造函数来创建一个对象。为了创建不传递任何参数的对象,必须有一个不需要参数的构造函数,也就是默认构造函数。

如果程序员没有为类编写任何构造函数,则编译器将自动为其创建一个默认构造函数。但是,当程序员编写了一个或多个构造函数时,即使所有这些构造函数都是有参数的,编译器也不会创建一个默认构造函数,所以程序员有责任这样做。

类可能有许多构造函数,但只能有一个默认构造函数。这是因为:如果多个函数具有相同的名称,则在任何给定时刻,编译器都必须能够从其形参列表中确定正在调用哪个函数。它使用传递给函数的实参的数量和类型来确定要调用的重载函数。因为一个类名称只能有一个函数能够接受无参数,所以只能有一个默认构造函数。

一般情况下,就像在 Sale 类中那样,默认构造函数没有形参。但是,也可能有另一种的默认构造函数,其所有形参都具有默认值,所以,它也可以无实参调用。如果创建了一个接受无实参的构造函数,同时又创建了另外一个有参数但允许所有参数均为默认值的构造函数,那么这将是一个错误,因为这实际上是创建了两个“默认”构造函数。以下语句就进行了这种非法的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Sale //非法声明
{
private:
double taxRate;
public:
Sale() //无实参的默认构造函数
{
taxRate = 0.05;
}
Sale (double r = 0.05) //有默认实参的默认构造函数
{
taxRate = r;
}
double calcSaleTotal(double cost)
{
double total = cost + cost * taxRate;
return total;
};
};

可以看到,第一个构造函数没有形参,第二个构造函数有一个形参,但它有一个默认实参。如果一个对象被定义为没有参数列表,那么编译器将无法判断要执行哪个构造函数。

析构函数

析构函数是具有与类相同名称的公共成员函数,前面带有波浪符号(〜)。例如,Rectangle 类的析构函数将被命名为 〜Rectangle。

当对象被销毁时,会自动调用析构函数。在创建对象时,构造函数使用某种方式来进行设置,那么当对象停止存在时,析构函数也会使用同样的方式来执行关闭过程。例如,当具有对象的程序停止执行或从创建对象的函数返回时,就会发生这种情况。

下面的程序显示了一个具有构造函数和析构函数的简单类。它说明了在程序执行过程中这两个函数各自被调用的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

class Demo{
public:
Demo(); //构造函数
~Demo(); //析构函数
};

Demo::Demo(){
cout << "An object has just been defined,so the constructor" << " is running.\n";
}

Demo::~Demo(){
cout << "Now the destructor is running.\n";
}

int main(){
Demo demoobj;
cout << "The object now exists, but is about to be destroyed.\n";
return 0;
}

程序输出结果:

An object has just been defined,so the constructor is running.

The object now exists, but is about to be destroyed.

Now the destructor is running.

除了需要知道在对象被销毁时会自动调用析构函数外,还应注意以下事项:

  1. 像构造函数一样,析构函数没有返回类型。
  2. 析构函数不能接收实参,因此它们从不具有形参列表。
  3. 由于析构函数不能接收实参,因此只能有一个析构函数。

4.3 mutable 和 const

const修饰变量

在C++中,一般用const完全替代C语言的define,且功能更多。用const修饰变量,使之成为常量(或常数),这很简单,但结构体中成员如果用了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
#include <iostream>
using namespace std;

typedef const double CONSTANT;
CONSTANT pi = 3.14159265;

struct mytype {
const int x;
int y;
};

int main ( )
{
//pi = 3.14; //编译出错,常量不能赋值

//常量定义必须有初始值
//mytype s; //编译出错,结构体中的x成员常量没有初始值
mytype s1 = {15, 20};

//s1.x = 3 //编译出错,常量不能赋值
s1.y = 100;

const mytype s2 = {25, 30};
//s2.y = 125; //编译出错,整个s2的所有成员都变成了常量

cout << "s1: " << s1.x << ", " << s1.y << endl;
cout << "s2: " << s2.x << ", " << s2.y << endl;

return 0;
}

程序输出结果:

s1: 15, 100 s2: 25, 30

const修饰类的成员函数

用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
#include <iostream>
using namespace std;

class classA{
int x;
string y;
public:
void get(int& i, string& s) const{
int k;
k = 10; //非成员变量可以修改
//x = k; //成员变量不能修改
//y = "SJCHEN" //成员变量不能修改
i = x, s = y;
}
void set(int i, string s){
x = i;
y = s;
}
};

int main(){
classA ca;
ca.set(100, "SJCEHN");

int i;
string s;
ca.get(i, s);
cout << s << "--" << i << '\n';
return 0;
}

程序输出结果:

SJCEHN--100

mutable与const为敌

用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
#include <iostream>
using namespace std;

class classA{
int x;
mutable string y;
public:
void get(int& i, string& s) const{
int k;
k = 10; //非成员变量可以修改
//x = k; //成员变量不能修改
y = "SJCHEN"; //mutable变量能修改
i = x, s = y;
}
void set(int i, string s){
x = i;
y = s;
}
};

int main(){
classA ca;
ca.set(100, "SJCEHN");

int i;
string s;
ca.get(i, s);
cout << s << "--" << i << '\n';
return 0;
}

程序输出结果:

SJCEHN--100

4.4 什么是 this 指针

this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。

所谓当前对象,是指正在使用的对象。例如对于stu.show();,stu 就是当前对象,this 就指向 stu。

下面是使用 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
#include <iostream>
using namespace std;

class Student{
public:
void setName(string name);
void setAge(int age);
void setScore(float score);
void show();
private:
string name;
int age;
float score;
};

void Student::setName(string name){
this->name = name;
}

void Student::setAge(int age){
this->age = age;
}

void Student::setScore(float score){
this->score = score;
}

void Student::show(){
cout<<this->name<<"的年龄是"<<this->age<<",成绩是"<<this->score<<endl;
}

int main(){
Student *pstu = new Student;
pstu -> setName("李华");
pstu -> setAge(16);
pstu -> setScore(96.5);
pstu -> show();
return 0;
}

程序输出结果:

李华的年龄是16,成绩是96.5

this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 private、protected、public 属性的。

本例中成员函数的参数和成员变量重名,只能通过 this 区分。以成员函数setname(string name)为例,它的形参是name,和成员变量name重名,如果写作name = name;这样的语句,就是给形参name赋值,而不是给成员变量name赋值。而写作this -> name = name;后,=左边的name就是成员变量,右边的name就是形参,一目了然。

this 虽然用在类的内部,但是只有在对象被创建以后才会给 this 赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显式地给 this 赋值。本例中,this 的值和 pstu 的值是相同的。

我们不妨来证明一下,给 Student 类添加一个成员函数printThis(),专门用来输出 this 的值,如下所示:

1
2
3
void Student::printThis(){
cout<<this<<endl;
}
1
2
3
4
5
6
7
Student *pstu1 = new Student;
pstu1 -> printThis();
cout<<pstu1<<endl;

Student *pstu2 = new Student;
pstu2 -> printThis();
cout<<pstu2<<endl;

运行结果:

0x7b17d8

0x7b17d8

0x7b17f0

0x7b17f0

可以发现,this 确实指向了当前对象,而且对于不同的对象,this 的值也不一样。

几点注意:

  • this 是 const 指针,它的值是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的。
  • this 只能在成员函数内部使用,用在其他地方没有意义,也是非法的。
  • 只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用(后续会讲到 static 成员)。

4.5 静态类成员

我们可以使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。

静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化,如下面的实例所示。

下面的实例有助于更好地理解静态成员数据的概念:

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
#include <iostream>
using namespace std;

class Box{
private:
double length;
double breadth;
double height;
public:
static int objectCount;
//构造函数定义
Box(double l = 2.0, double b = 2.0, double h = 2.0){
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume(){
return length * breadth * height;
}
};

//初始化类 Box 的静态成员
int Box::objectCount = 0;

int main(){
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
// 输出对象的总数
cout << "Total objects: " << Box::objectCount << endl;
return 0;
}

运行结果:

Constructor called.

Constructor called.

Total objects: 2

静态成员函数

如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。

静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。

静态成员函数有一个类范围,他们不能访问类的 this 指针。您可以使用静态成员函数来判断类的某些对象是否已被创建。

静态成员函数与普通成员函数的区别:

  • 静态成员函数没有 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
#include <iostream>
using namespace std;

class Box{
private:
double length;
double breadth;
double height;
public:
static int objectCount;
//构造函数定义
Box(double l = 2.0, double b = 2.0, double h = 2.0){
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume(){
return length * breadth * height;
}
static int getCount(){
return objectCount;
}
};

//初始化类 Box 的静态成员
int Box::objectCount = 0;

int main(){
// 在创建对象之前输出对象的总数
cout << "Inital Stage Count: " << Box::getCount() << endl;
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
// 在创建对象之后输出对象的总数
cout << "Final Stage Count: " << Box::getCount() << endl;
return 0;
}

运行结果:

Inital Stage Count: 0

Constructor called.

Constructor called.

Final Stage Count: 2

4.6 Iterator Class

4.7 友元函数和友元类

私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行。这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦。

c++ 是从结构化的C语言发展而来的,需要照顾结构化设计程序员的习惯,所以在对私有成员可访问范围的问题上不可限制太死。

C++ 设计者认为, 如果有的程序员真的非常怕麻烦,就是想在类的成员函数外部直接访问对象的私有成员,那还是做一点妥协以满足他们的愿望为好,这也算是眼前利益和长远利益的折中。因此,C++ 就有了友元(friend)的概念。打个比方,这相当于是说:朋友是值得信任的,所以可以对他们公开一些自己的隐私。

友元分为两种:友元函数和友元类。

友元函数

在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样那些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。

将全局函数声明为友元的写法如下:

friend 返回值类型 函数名(参数表);

将其他类的成员函数声明为友元的写法如下:

friend 返回值类型 其他类的类名::成员函数名(参数表);

但是,不能把其他类的私有成员函数声明为友元。

关于友元,看下面的程序示例。

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
28
#include <iostream>
using namespace std;

class Student{
public:
Student(string name, int age, float score);
friend void show(Student *pstu); //将show()声明为友元类
private:
string m_name;
int m_age;
float m_score;
};

Student::Student(string name, int age, float score)
:m_name(name), m_age(age), m_score(score){}

//非成员函数
void show(Student *pstu){
cout<<pstu->m_name<<"的年龄是 "<<pstu->m_age<<",成绩是 "<<pstu->m_score<<endl;
}

int main(){
Student stu("小明", 15, 90.6);
show(&stu); //调用友元函数
Student *pstu = new Student("李磊", 16, 80.5);
show(pstu); //调用友元函数
return 0;
}

运行结果:

小明的年龄是 15,成绩是 90.6

李磊的年龄是 16,成绩是 80.5

show() 是一个全局范围内的非成员函数,它不属于任何类,它的作用是输出学生的信息。m_name、m_age、m_score 是 Student 类的 private 成员,原则上不能通过对象访问,但在 show() 函数中又必须使用这些 private 成员,所以将 show() 声明为 Student 类的友元函数。读者可以亲自测试一下,将上面程序中的第 8 行删去,观察编译器的报错信息。

注意,友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象。 下面的写法是错误的:

1
2
3
void show(){
cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl;
}

成员函数在调用时会隐式地增加 this 指针,指向调用它的对象,从而使用该对象的成员;而 show() 是非成员函数,没有 this 指针,编译器不知道使用哪个对象的成员,要想明确这一点,就必须通过参数传递对象(可以直接传递对象,也可以传递对象指针或对象引用),并在访问成员时指明对象。

2) 将其他类的成员函数声明为友元函数

friend 函数不仅可以是全局函数(非成员函数),还可以是另外一个类的成员函数。请看下面的例子:

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
#include <iostream>
using namespace std;

class Address; //提前声明Addres类

class Student{
public:
Student(string name, int age, float score);
void show(Address *addr); //将show()声明为友元类
private:
string m_name;
int m_age;
float m_score;
};

//声明Address类
class Address{
private:
string m_province; //省
string m_city; //市
string m_district; //区
public:
Address(string province, string city, string distict);
//将Student类中的成员函数show()声明为友元函数
friend void Student::show(Address *addr);
};

Student::Student(string name, int age, float score)
:m_name(name), m_age(age), m_score(score){}

void Student::show(Address *addr){
cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl;
cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"区"<<endl;
}

//实现Address类
Address::Address(string province, string city, string distict)
:m_province(province), m_city(city), m_district(distict){}

int main(){
Student stu("小明", 15, 90.6);
Address addr("陕西","西安","雁塔");
stu.show(&addr);

Student *pstu = new Student("李磊", 16, 80.5);
Address *paddr = new Address("河北", "衡水", "桃城");
pstu->show(paddr);
return 0;
}

运行结果:

小明的年龄是 15,成绩是 90.6

家庭住址:陕西省西安市雁塔区

李磊的年龄是 16,成绩是 80.5

家庭住址:河北省衡水市桃城区

本例定义了两个类 Student 和 Address,程序第 27 行将 Student 类的成员函数 show() 声明为 Address 类的友元函数,由此,show() 就可以访问 Address 类的 private 成员变量了。

几点注意: ① 程序第 4 行对 Address 类进行了提前声明,是因为在 Address 类定义之前、在 Student 类中使用到了它,如果不提前声明,编译器会报错,提示'Address' has not been declared。类的提前声明和函数的提前声明是一个道理。

② 程序将 Student 类的声明和实现分开了,而将 Address 类的声明放在了中间,这是因为编译器从上到下编译代码,show() 函数体中用到了 Address 的成员 province、city、district,如果提前不知道 Address 的具体声明内容,就不能确定 Address 是否拥有该成员(类的声明中指明了类有哪些成员)。

==这里简单介绍一下类的提前声明。一般情况下,类必须在正式声明之后才能使用;但是某些情况下(如上例所示),只要做好提前声明,也可以先使用。==

③ 一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的 private 成员。

友元类

不仅可以将一个函数声明为一个类的“朋友”,还可以将整个类声明为另一个类的“朋友”,这就是友元类。友元类中的所有成员函数都是另外一个类的友元函数。

例如将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数,可以访问类 A 的所有成员,包括 public、protected、private 属性的。

更改上例的代码,将 Student 类声明为 Address 类的友元类:

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
#include <iostream>
using namespace std;

class Address; //提前声明Addres类

class Student{
public:
Student(string name, int age, float score);
void show(Address *addr); //将show()声明为友元类
private:
string m_name;
int m_age;
float m_score;
};

//声明Address类
class Address{
private:
string m_province; //省
string m_city; //市
string m_district; //区
public:
Address(string province, string city, string distict);
//将Student类声明为Address类的友元类
friend class Student;
};

Student::Student(string name, int age, float score)
:m_name(name), m_age(age), m_score(score){}

void Student::show(Address *addr){
cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl;
cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"区"<<endl;
}

//实现Address类
Address::Address(string province, string city, string distict)
:m_province(province), m_city(city), m_district(distict){}

int main(){
Student stu("小明", 15, 90.6);
Address addr("陕西","西安","雁塔");
stu.show(&addr);

Student *pstu = new Student("李磊", 16, 80.5);
Address *paddr = new Address("河北", "衡水", "桃城");
pstu->show(paddr);
return 0;
}

关于友元,有两点需要说明:

  • 友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
  • 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。

除非有必要,一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些。