[知识点] 1.5.1 类与对象

总目录 > 1  语言基础 > 1.5  C++ 进阶 > 1.5.1  类与对象

前言

这篇文章首先主要介绍 C++ 中最重要的特性——类 class,以及和它密切相关的对象。C++ 刚刚诞生时就被人们称作 “C With Class”,足以看出 class 对 C++ 意义之大。其实之前学了很久的 C++ 却基本没用过 class,以至于在看一些博客的代码时就很懵,后来才知道 class 和 struct 其实是比较类似的,但是功能更强大,只是说竞赛里一般不太需要用到。

更新日志

20200920 - 完善内容,增加 抽象与封装,对象应用,友元,接口与实现的分离 的介绍。

子目录列表

1、抽象与封装

2、类与结构体

3、数据成员与成员函数

4、对象

5、构造函数和析构函数

6、静态成员

7、this 指针

8、对象应用

9、友元

 

1.5.1  类与对象

1、抽象与封装

在 1.3  C++ 与面向对象程序设计 中,我们介绍了面向对象语言的四大特征,其中以抽象与封装最为重要。

① 抽象

抽象是人们认识客观事物的一种常用方法,即在描述事物时,有意去掉次要部分与具体细节,而抽取出与所关注问题相关的重要特征进行考察,并以这些特征代表该事物。计算机软件开发中采用的抽象方法主要有过程抽象数据抽象两种。

> 过程抽象

过程抽象是面向过程程序设计采用的 “以功能为中心” 的抽象方法,将整个系统的功能划分成若干部分,每部分由若干过程(函数)完成,强调过程的功能设计,而忽略实现的详细细节。

过程抽象的结果给出了函数名称、接受参数和能提供的功能,函数使用者只需知道这些尽可能进行函数调用。

> 数据抽象

数据抽象是面向对象程序设计方法, 采用 “以数据为中心” 的抽象方法,忽略事物与当前问题域无关的、不重要的部分与细节,抽取同类事物与当前所研究问题相关联的、共有的基本特征与行为,形成关于该事物的抽象数据类型(ADT, Abstruct Data Type)

举个生动的例子:2020新生报到,现在学校要对所有新生的一些信息进行统计,方便信息化管理。每个人都是独一无二的,有人高有人矮,有人单眼皮有人双眼皮,有人A型血有人B型血,想通过几句话来描述一个人的所有特征或者将其与其他人区别开来,不是个简单的事情。但对于学校而言,单双眼皮并不是学校需要关注了解的特征,完全可以忽略掉;高矮并不是信息化管理很重要的特征,也可以忽略掉……而最后抽取出来的关键特征,比如新生们的性别、祖籍、家庭住址,所在学院、班级、宿舍号等等,才是学校所需求的。

我们假设学校最后选取:姓名、性别、祖籍、宿舍号等特征。抽象姓名时,只关注姓名本身,而忽略其属于两个字、三个字还是其他;抽象性别时,只考虑男女之分(?;抽象祖籍时,只考虑省份与地级市,而忽略更详细的地址……并分别使用 name, sex, native, dorm 来表示,这就是抽象的过程。

按照同样的方法,我们可以将所有人的行为也进行抽象。假如本学期对新生进行体测,需要统计大家 1000 米跑的时长,那么定义 run() 函数表示 1000 米跑这个行为,只关注时长,而忽略诸如跑步动作、状态等无关特征,这也是抽象。

众所周知,这些数据一般情况下是不会公开而是作为“内部机密”存在,所以除非被授权,这些特征数据默认情况下是隐藏的。抽象会将提取出来的特征数据隐藏起来而不让用户知道或操作,但会提供操作或获取这些数据的功能函数。这就好比于向他人借钱,你当然不会直接在他钱包里拿钱,而是他从钱包里拿出钱再给你。那么,这样的功能函数有一个特定的名称 —— 接口(Interface)

熟悉 Java 的就知道,接口在 Java 中是实际存在的关键字,可以结合了解。计算机对数据的操作无非是写入和读出,那么我们将接口也分为两种类型,对于任意一个特征数据 x,定义两个接口 —— setx(),表示设置(写入)该数据;getx(),表示获取(读出)该数据,它们的一般格式分别为:

T x;
void setx(T o) {
    x = o;
} 
T getx() {
    return x;
} 

T 表示任意数据类型。

最后,我们给 2020 新生这个群体命个名 —— Student,然后就完成了对他们的抽象,最终抽象结果为:

类型名:Student

特征数据:string name(姓名);bool sex(性别);string native(祖籍);int dorm(宿舍号);

特征行为:void run()(体测 1000 米跑)。

综上,我们将抽象更具体的定义为:抽象是将对象可以被观察到的行为设计成对应抽象数据类型的一组接口访问函数的过程。

② 封装

接口为用户提供各项功能,但抽象这个过程本身并不关心接口是具体如何实现而只是对其有所定义,而这一任务,由封装实现。

抽象与封装为一组互补的概念。抽象关注对象的外部视图,封装关注对象的内部实现。只有封装后的抽象数据类型才是可以用来进行程序设计的数据类型,它由两部分组成:接口与实现。接口显露在外,实现封装细节。

抽象,封装……如此缥缈的动词,在计算机语言,更具体地,在 C++ 语言中,是如何实现的呢?

③ 类

C++ 通过类(class)来实现封装。类这个名称相当形象:比如上述例子,2020 新生属于一类群体,我们将其抽象出来的根本目的,是体现这一类群体的共同特征与特征的差异。所以,抽象数据类型名 Student 又被称为类名。我们写入与读出的所有特征与行为,均是建立在这个类的基础上。

类的基本结构为:

class 类名 {
public:
    公有成员;
private:
    私有成员; 
};

其封装机制大致为:类名是 ADT 的名称,“{};” 中的内容为全体成员,成员分为:数据成员、成员函数,两者分别对应上述特征数据特征行为。成员之间可以相互访问,不受限制,其中 public: 后的成员为公有成员,即外部也可以访问的成员,一般用于实现接口功能;private: 后的成员为私有成员,即对用户隐藏的成员,一般用于保存数据。对于 public 与 private 的具体含义,下面的 类与结构体 部分会介绍。

根据这个模板,我们将上述 ADT 以类的形式体现:

class Student {
public:
    void setName(string n) {
        name = n;
    }
    void setSex(bool s) {
        sex = s;
    }
    void setNative(string nt) {
        native = nt;
    }
    void setDorm(int d) {
        dorm = d;
    }
    string getName() {
        return name;
    }
    bool getSex() {
        return sex;
    }
    string getNative() {
        return native;
    }
    int getDorm() {
        return dorm;
    }
    void run() {
        ...
    }
private:
    string name;
    bool sex;
    string native;
    int dorm;
};

经过抽象与封装后的 Student,便是一个可以被实际使用的数据类型了,同诸如 int, double 等基本数据类型一样,可以被声明,声明时会申请到一块内存区域。

 

2、类与结构体

① struct

在 1.2  C 语言数据类型 中,我们已经介绍了 C 语言中的结构体这个概念,观察上述的类的结构,不难发现与结构体实在太像了。它们的设计思路其实是完全不同的,毕竟一个是面向对象语言,一个是面向过程语言,但事实上,C++ 保持自身设计核心的同时为了向下完全兼容,将结构体 struct 进行了功能扩展,使其与类 class 的差异小了许多,同样可以实现抽象数据类型的封装。

在 C 语言中,如果一定要类比 class,struct 可以实现 class 中最基础的数据成员功能;而 C++ 中,struct 与 class 几乎没太多差异,唯一差别在于默认的访问说明符不同。

② 访问说明符

访问说明符用于说明类中的各个成员的访问权限,分为 public, private, protected 三种。

  > public: 表示公有成员,用于实现接口功能,可以被任何类内与类外的函数访问;

  > private: 表示私有成员,用于实现信息隐藏,只能被类内成员(以及以后会提到的友元函数)访问;

  > protected: 表示保护成员,在 1.5.2  继承与多态 中将会有介绍。

struct 和 class 的区别在于,在不显式定义访问权限的情况下,struct 默认是 public,而 class 默认是 private。这似乎挺好理解,struct 是为了向下兼容 C 语言,而 C 语言的 struct 本身并无隐藏数据一说,所有数据均可直接访问,故 C++ 中的 struct 也默认是公有数据;而 class 是 C++ 新增的用来实现封装的,默认成员均为隐藏,故默认是私有数据。

class A {
[private:]
    int a;
public:
    int b;
};

struct B {
[public:]
    int a;
private:
    int b;
};

同一访问说明符可以被多次定义。一旦定义了,接下来所有成员均为该说明符定义的权限,直到下一次定义。

class 并不是 C++ 的专利,在 Java、Python、Rube、PHP 等语言中,均使用 class 进行 ADT 封装。

 

3、数据成员与成员函数

以一段类定义进行引入。

class Object {
public:
    int value;
    void print() {
        cout << value << endl;
        return;
    }
    void update(int);
} a[MAXN];

void Object :: update(int _value) {
    value = _value;
}

① 数据成员

上述代码中,int value 为类 Object 的一个数据成员。数据成员可以为任何数据类型,但不能是:自身类对象;constexpr 常量;auto 推断定义;register 寄存器类型;extern 外部类型。

C++ 11 前,数据成员是不能被赋初值的,C++ 11 后已经被允许。

因为和 C 中的 struct 的各种变量基本为一个格式,也就不再过多赘述。

② 成员函数

成员函数是 class 独有的特性,表示专属于这个类的函数。代码中的两个函数分别为成员函数两种定义方式:类内定义类外定义。上述代码中,print 属于类内定义,即在类中直接声明并定义;update 属于类外定义,即在类中只进行声明,而在声明完整个类之后再进行定义。

两种定义方式除形式外,功能上区别不大,类内定义在条件允许的情况下默认为内联函数(关于内联函数请参见 1.2  C 语言数据类型 中的函数部分),而类外定义并不是,除非加上 inline 的限定词。采用类外定义进行声明时,成员函数原型中的形参名可以省略,如上述代码直接写的 (int);同时在函数名前一定加上 “类名 :: ” 以表示这是属于这个类的函数。

③ 常量成员函数

如果不希望成员函数修改数据成员的值,可以将其设置为常量成员函数,在函数名后加上一个 const 即可,如:

class Object {
public:
    int value;
    void print() const {
        cout << value << endl;
        return;
    }
    void update(int) const;
} a[MAXN];

请将其与函数的常量参数区分开来,常量参数只是限制对参数的更改,而常量成员函数限制该函数内部对所属类的数据成员的修改。

常量函数只有在类的成员函数中存在,普通函数并不能这样定义(本身也没有意义)。

④ 成员函数重载

和普通函数一样,成员函数也可以重载。

关于函数重载请参见 1.2  C 语言数据类型 中的函数部分。

 

4、对象

类描述的是一类事物的共有属性与行为,本身是个抽象的概念;而这类事物中的每一个个体,则是实际存在的,我们将这些个体,成为对象。

我们常说,对象是类的实例,类是对象的模板。看起来太抽象,举几个例子:计算机语言是一个类,而 C++ 是一个对象,Java 也是一个对象。2020 新生是一个类,而这群新生里每一位同学,都是属于这个类的一个对象,cyh 是一个对象,sh 也是一个对象。如下代码,在声明了 Student 类后,我们再声明两个对象 cyh, sh。

class Student {
    ...
    int dorm;
public:
    ...
    void setDorm(int d) {
        dorm = d;
    }
    int getDorm() {
        return dorm;
    }
};

Student cyh;
Student sh;

定义对象的方法和定义一个普通变量没有区别,而且不难看出,类与对象的关系实质上就是数据类型与变量的关系。从广义上讲,在面向对象程序设计中,你可以把所有数据类型定义的变量都称作对象。

② 对象访问

访问对象的数据成员和成员函数有两种方式,例如上述代码,如果需要设置或者获取 cyh 的宿舍号,可以:

// int d1 = cyh.dorm();
int d2 = cyh.getDorm();
cyh.setDorm(101);

一般形式为:

对象名.数据成员名

对象名.成员函数名(实参表)

L1 在这个类中当然是不正确的,因为 dorm 是 private 属性,这里只是为了体现访问数据成员的形式。

③ 对象指针

对象也可以被定义为指针,如下:

1 Student *p = &jk;
2 int g1 = p -> grade;
3 int g2 = (*p).grade;
4 int g3 = p -> getGrade();
5 int g4 = (*p).getGrade();

其中,g1 和 g2 是等价的,g3 和 g4 是等价的。

当然,在类外访问的前提是它是 public 属性的。

④ 对象赋值

相同类的对象之间可以直接赋值。对象赋值只是对数据成员值的复制,赋值后的两个对象并无任何其他联系。

 

5、构造函数与析构函数

① 构造函数

构造函数是类的一种特殊成员函数,会在每次创建类的新对象时自动调用执行,且不允许被显式调用。其名称与类名称保持一致,且不存在返回值,一般用于预处理或者为成员变量设置初始值。它同样可以类外定义。在 C++ 11 之前,这是唯一一种为数据成员赋初值的方式。

 1 class Vector {
 2 public:
 3     int x, y;
 4     Vector(int _x, int _y) {
 5         x = _x, y = _y;
 6     }
 7 } a[MAXN];
 8 
 9 int main() {
10     Vector x = Vector(1, 2);
11     return 0;
12 }

同一个类可以构造多个拥有不同个数或不同类型的参数的函数,即重载构造函数,如下面的代码:

class Tdate {
public:
    Tdate();
    Tdate(int d);
    Tdate(int m, int d);
    Tdate(int m, int d, int y);
protected:
    int month, day, year;
};

使用构造函数注意如下问题:

  > 定义对象并赋初值时,必须严格与存在的构造函数的参数列表一一对应;

  > 构造函数不能有任何返回类型,包括 void;

  > 定义对象数组或用 new 创建动态对象时也会调用构造函数,同时,定义数组对象时类必须有默认构造函数(下面会有介绍);

  > 构造函数一般为公有成员。

设置初始值还可以使用初始化列表,其一般格式为:

类名(参数表): 成员1(初值1), 成员2(初值2), ...

举个例子:

Vector(int _x, int _y) : x(_x), y(_y) {}

和上面代码的 L4~L6 是完全等价的。

② 默认构造函数

上面我们提到,构造函数可以不带参数,这样的构造函数属于默认构造函数。除此之外,如果为所有的形参都提供了默认值,那样的构造函数也属于默认构造函数。

> 无参构造函数

如果一个类没有任何显式构造函数,则系统会为其生成一个默认构造函数,其格式为:

class X {
    X() {} 
};

同样可以自己定义,比如:

 1 class Vector {
 2 public:
 3     int x, y;
 4     Vector() {
 5         cout << "this is a Vector" << endl;
 6     }
 7 } a[MAXN];
 8 
 9 int main() {
10     Vector x;
11     return 0;
12 }

程序运行时,会输出一个 “this is a Vector”。

因为 class 中的所有变量是不会进行初始化的,所以直接新创建一个对象并且其初始值是未知的,则默认的 x, y 并非为 0,这对后面程序的编写可能有影响,所以一般会在默认构造函数里对所有变量进行一次 0 赋值。

class Vector {
public:
    int x, y;
    Vector() {
        x = 0, y = 0;
    }
} a[MAXN];

> 默认参数构造函数

构造函数的参数列表中的参数是可以设定默认值的。如果所有参数均被设定默认值,这样的构造函数被称作默认参数构造函数,属于默认构造函数的一种。比如:

class Vector {
public:
    int x, y;
    Vector(int _x = 0, int _y = 0) {}
} a[MAXN];

③ 析构函数

析构函数是与类同名的另一个特殊成员函数,作用与构造函数完全相反,其作用往往是清理善后,比如在建立对象时使用了 new 动态内存空间,析构函数就会自动执行 delete 来销毁内存空间。当对象结束其声明时,系统会自动执行析构函数。其一般格式为:

~类名() {}

与构造函数的形式区别在于前面加上了一个 “~” 符号。系统默认自带析构函数,所以一般情况下不需要定义,这样的析构函数被称为合成的析构函数。比如:

class Vector {
public:
    int x, y;
    Vector(int _x, int _y) : x(_x), y(_y) {}
    Vector() : x(0), y(0) {}
    //~Vector() {} 合成
的析构函数
} a[MAXN];

但如果需要对每一个类声明结束后进行某些操作,也可以自行定义,比如:

class Vector {
public:
    int x, y;
    ~Vector() {
        cout << "Vector end." << endl;
    }
};

定义析构函数时注意:

  > 只能由系统自动调用,不能显式调用;

  > 一般为公有成员;

  > 没有返回类型;

(上面和构造函数都是一样的)

  > 没有参数表;

  > 不能被重载,一个类只能有一个析构函数;

④ 赋值运算符函数

在面向对象程序设计中,对象的赋值、复制是很普遍的。和定义对象时使用构造函数一样,这些操作也有对应的函数,其中,赋值使用的是赋值运算符函数。

为显式定义赋值运算符函数时,系统同样会自动生成一个默认函数,被称作合成赋值运算符函数,该函数以按位复制(bit-by-bit)的方式实现各个非静态成员的复制。在大多数情况下,合成赋值运算符函数是没有问题的,但如果包含指针数据成员时,就会引发 “指针悬挂” 的问题。

所谓的指针悬挂,用一个例子来说明:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 class String {
 5     char *ptr;
 6     int len;
 7 public:
 8     String(char *p, int l) {
 9         ptr = new char[strlen(p) + 1];
10         strcpy(ptr, p);
11         len = l;
12     }
13     ~String() {
14         delete ptr;
15     }
16     void print() {
17         cout << ptr << endl;
18     }
19 };
20 
21 int main() {
22     String a("aaa", 3);
23     {
24         String b("bbbb", 4);
25         b = a;
26         b.print();
27     }
28     a.print();
29     return 0;
30 }

对于这个类,其合成赋值运算符函数可以认为是:

String& String :: operator = (const String &s) {
    ptr = s.ptr;
    len = s.len;
    return *this;
} 

关于其中出现的 this 指针,下面会有介绍。

在运行到 String b = a 时,系统就会调用该函数,并将 a 和 b 对象的成员指针 ptr 指向同一动态存储区域。而在 b 的生命期结束后,通过析构函数,a, b 同时指向的内存将会被收回,不仅 b 已经没有意义,a 的指向也没有了内容,所以最后在输出 a 时会出现乱码。这样的情况即 “指针悬挂”。

如果解决这样的问题?重载赋值运算符函数即可。通过显式提供函数,特殊地处理指针问题,在类中加上如下代码,即可覆盖原有的合成赋值运算符函数。

String& operator = (const String &s) {
    if (this == &s)
        return *this;
    delete ptr;
    ptr = new char[strlen(s.ptr) + 1];
    strcpy(ptr, s.ptr);
    return *this;
}

⑤ 复制构造函数

先看下面的代码。

String a("aaa", 3);
String b("bbbb", 4);
b = a;
String c = a;

不考虑指针悬挂问题,从结果而言,b 和 c 是一样的,都是变成了 a 的模样;但从本质上来看,定义直接赋初值和定义后赋值其实是不同的 —— 对于 b,其调用的是上述的赋值运算符函数;对于 c,其调用的是下面要介绍的复制(拷贝)构造函数。

其实平时使用的基础数据类型的变量,两者也是有区别的,只是一般情况下不加区分也没有太多影响而已。

复制构造函数也是一种特殊的构造函数,用于根据已存在的对象初始化一个新建对象

还是上面介绍指针悬挂的代码,如果我们将 b 的定义后赋值改为直接赋值,且不显式定义复制构造函数,那么系统会调用默认的合成复制构造函数,如下:

String :: String(const String &s) {
    ptr = s.ptr;
    len = s.len;
}

和赋值运算符函数非常相像,都是将所有成员按位复制(bit-by-bit),不同的是它是属于构造函数,所以也要遵守构造函数的规定,同时其参数常常是 const 类型的本类对象的引用。同样地,合成复制构造函数也可能出现指针悬挂问题。为了解决这个问题,同样可以显式提供复制构造函数,格式如下:

String(const String &s) {
    ptr = new char[strlen(s.ptr) + 1];
    strcpy(ptr, s.ptr);
    len = s.len;
}

即在复制时分配新的内存,这样在析构函数删除指针时,两者相对独立。

合成函数和显式提供的函数的关系,类似于把 D 盘里的文件创建一个桌面快捷方式复制一个新的文件放在桌面

上述所有函数和前面的普通成员函数一样,都可以先声明后定义

⑥ 对象定义

介绍完各个函数,是时候把定义对象的所有方法列举一下,如下形式都是合法的:

1 Vector x;
2 Vector x = Vector(1, 2);
3 Vector x(1, 2);
4 Vector x = {1};
5 Vector y(x);
6 Vector y = x;
7 Vector z;
8 z = x;

L1 调用的是默认构造函数

L2, L3, L4 调用的是普通构造函数,L2, L3 完全等价,且需要有对应的双参数构造函数;L4 只适用于单个参数或者只有一个参数没有设定默认值的情况。

L5, L6 调用的是复制构造函数,且完全等价。

L7, L8 调用的是赋值运算符函数

 

6、静态成员

前面我们介绍了数据成员和成员函数,它们的特点在于,对于所在类的所有对象均存在且独立的,比如 Student 类定义了一个 id 表示学号和一个 Grade 表示成绩,则无论是 jk 还是 bebe 还是其他任何 Student,都会有一个独立的学号,分别是 jk.id, bebe.id,独立的成绩,分别是 jk.Grade, bebe.Grade。

而如果我们在成员前面加上一个 static 限定词,它就被称作静态成员,就不再满足上述条件。比如下面这个 static int 类型的 total:

class Student {
public:
    int Id, Grade;
    int getGrade() {
        return Grade;
    }
    static int total; 
};

不属于任意一个对象只属于这个类,即使类没有对象,这个成员依然存在,相当于类中的全局变量;而非静态成员是属于对象的,即每个对象都有费静态成员的一个拷贝。

静态成员在声明时不会被分配空间,就像声明类一样,所以需要单独进行定义,并且往往在类外进行定义(原则上必须在这样),定义时不再需要加上 static 限定词。它可以用于统计类中对象的个数,如下代码:

class Student {
public:
    int Id, Grade;
    Student() { total++; }
    static int total; 
};

int Student :: total;

int main() {
    Student jk;
    Student hzq;
    cout << Student :: total;
    return 0;
}

最后的输出结果为 2。定义时是可以赋初值的,否则默认为 0。

静态成员函数同理,也是属于整个类。同时,其函数内只能访问该类的静态成员,而非静态函数可以访问静态成员。

正因为静态成员属于整个类,那么访问它们的时候既可以通过对象访问,也可以通过类名访问。

 

7、this 指针

类的每个对象都有属于自己的数据成员,有多少个对象就有多少个数据成员的副本;但,成员函数只有一个副本,所有对象都共用这个副本。那么,我们平时在调用时,似乎并没有告诉成员函数,当前是哪一个对象调用的;其实,编译器已经自动加上了一个标记,而这个标记被称作 this 指针。

this 指针是类中一种特殊的指针,表示指向调用该函数的对象自身,即成员函数所属的类对象的首地址。举个例子,我们声明了一个构造函数,下面是我们编写时的代码和编译器处理后的实际代码:

class Vector {
    int x, y;
public:
    void move(int a, int b) {
        x = a, y = b;
    }
};

class Vector {
    int x, y;
public:
    inline void move(Vector *this, int a, int b) {
        this -> x = a, this -> y = b;
    }
};

然后,我们调用这个函数时,编写的代码和实际代码分别为:

v1.move(10, 20);
v2.move(5, 5);

move(&v1, 10, 20);
move(&v2, 5, 5);

因为 this 指针往往是由编译器自行添加到成员函数参数表的,所有称它为隐式指针;但实际上,它也可以被显式调用,一般用于:

> 返回对象地址或引用

下面给出一个具有返回本类对象的指针的 Date 类:

 1 class Date {
 2     int y, m, d;
 3 public:
 4     Date& sety(int _y) {
 5         y = _y; 
 6         return *this;
 7     }
 8     Date& setm(int _m) {
 9         m = _m; 
10         return *this;
11     }
12     Date& setd(int _d) {
13         d = _d; 
14         return *this;
15     }
16 };
17 
18 d.sety(2020).setm(9).setd(20);

使用 this 指针返回对象地址,可以连续调用成员函数,如 L18。注意不要遗漏写在类名后的取地址符或引用符。

> 避免二义性

比如构造函数的参数与类的数据成员 x, y 命名相同,那么程序也无法理解你到底要调用哪一个,也就产生了二义性,这时候可以在类的数据成员前显式使用 this 指针,如下代码:

Vector(int x, int y) {
    this -> x = x; this -> y = y;
}

当然这种情况一般都是可以通过更换参数名来避免。

 

8、对象应用

前面已经提到过,类是一种自定义数据类型,本质上和基本数据类型没有区别,也就是说同样可以定义:对象数组、对象指针、对象函数和类对象成员等等。以类对象成员举例,比如先声明了一个 Salary 类表示工资,再声明了一个 Worker 类表示工人,工人中有一个 Salary 类的对象作为数据成员,表示工人的工资。

class Salary {
public:
    double Wage, Subsidy; 
};

class Worker {
private:
    char *name;
    int age;
    Salary salary;
};

 

9、友元

关系很好的朋友向你借钱时,你可能会不假思索;陌生人或者失散多年的好友列表跳出一句 “在吗?” 时,你肯定会深思熟虑。

当然这也并不是一个很好的例子(

前面提到过,访问 private 成员的方式是通过作为 public 成员的接口。而如果访问很频繁,每次都需要调用接口;或者是访问的成员较多,我们要给每个成员都定义一个接口的话,代码会变得相当冗长。那么,为了解决这个问题,C++ 提供了为某些函数开放特权的功能,使其能够直接访问 private 和 public 成员,这样的函数被称作友元(friend)。

友元函数并不属于成员函数,在声明时不受任何访问说明符的限制,在类外定义时不能把类名写在前面。举个例子:

class Point {
    int x, y;
public:
    ...
    friend int getd(Point p1, Point p2) {
        return ((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y));
    }
};

Point p1(1, 2), p2(3, 4);

cout << getd(p1, p2);

一个类还可以是另一个类的友元,即友元类。但这个关系不具有逆向性和传递性。

posted @ 2020-04-21 14:56  jinkun113  阅读(465)  评论(0编辑  收藏  举报