虽然 C 是世界上最受欢迎和广泛使用的编程语言之一,但 C++ 的发明是由一个主要的编程因素促成的:日益增加的复杂性。多年来,计算机程序变得越来越大,越来越复杂。即使 C 语言是一种优秀的编程语言,它也有其局限性。在 C 中,一旦一个程序从 20,000 行代码超过 100,000 行代码,它就变得难以管理,难以从整体上把握。C++ 的目的就是打破这个壁垒。C++ 的基本本质在于允许程序员理解、领会和管理更复杂和更大的程序。
C++ 从 C 中吸取了最好的想法,并将它们与几个新概念结合起来。结果是一种不同的组织你的程序的方式。在 C 中,一个程序是围绕着它的代码组织的(例如,“发生了什么?”)而在 C++ 中,程序是围绕其数据组织的(例如,“谁受到了影响?”).用 C 编写的程序是由其函数定义的,任何函数都可以对程序使用的任何类型的数据进行操作。在 C++ 中,程序是围绕数据组织的,基本前提是数据控制对代码的访问。因此,您定义了数据和允许对该数据进行操作的例程,而数据类型精确地定义了什么样的操作适用于该数据。
为了支持这一面向对象编程的基本原则,C++ 具有封装的特性,因此它可以将代码和它所处理的数据绑定在一起,保护它们免受外部干扰和误用。通过以这种方式链接代码和数据,创建了一个对象。因此,对象是支持封装的设备。
在一个对象中,代码/数据或两者都可以是该对象的private/public
或protected
,这只有在继承对象的情况下才起作用。我们将在本章中讨论这个访问控制和更多(比如类);继承的主题将在后面的章节中讨论。
在前一章中,我们已经讨论了通过 C++ 尽可能多地使用现有的 C 代码和库来提高生产率的必要性。一个典型的 C 库包含一个struct
和一些作用于该struct
的相关函数。到目前为止,您已经看到了 C++ 如何获取概念上与相关联的函数,并通过将函数声明放在struct
的范围内,改变调用struct
函数的方式,消除将结构地址作为第一个参数的传递,并向程序添加新的类型名(,这样您就不必为* struct
标签创建类型集)来使它们真正与*相关联。**
这都是方便的;它帮助你组织你的代码,使它更容易写和读。然而,当在 C++ 中使库更容易时,还有其他重要的问题,尤其是安全和控制的问题。本章着眼于结构中的边界问题。
设定限值
在任何关系中,重要的是要有各方都尊重的界限。当您创建一个库时,您与使用该库构建应用程序或另一个库的客户端程序员建立了关系。
在 a C struct
中,就像在 C 中的大多数事情一样,没有规则。客户端程序员可以用那个struct
做任何他们想做的事情,并且没有办法强制任何特定的行为。例如,即使你在上一章看到了名为initialize( )
和cleanup( )
的函数的重要性,客户程序员也可以选择不调用这些函数。
即使你真的希望客户端程序员不要直接操纵你的struct
的一些成员,在 C 中也没有办法阻止它。对这个世界来说一切都是赤裸裸的。
控制对成员的访问有两个原因。第一是让客户程序员不要接触他们不应该接触的工具——这些工具是数据类型的内部机制所必需的,但不是客户程序员解决特定问题所需的接口的一部分。这实际上是对客户程序员的一种服务,因为他们可以很容易地看到对他们来说什么是重要的,什么是可以忽略的。
访问控制的第二个原因是允许库设计者改变结构的内部工作,而不用担心它会如何影响客户程序员。在上一章的Stack
示例中,为了提高速度,您可能希望以大块的方式分配存储,而不是每次添加元素时都创建新的存储。如果接口和实现被清楚地分离和保护,您可以完成这一点,并且只需要客户端程序员重新链接。
C++ 访问控制
C++ 引入了三个新的关键字来设置结构中的边界:public
、private
和protected
。它们的用法和含义非常简单。这些访问说明符 只在一个结构声明中使用,它们改变所有跟在它们后面的声明的边界。无论何时使用访问说明符,后面都必须跟一个冒号。
Public
表示所有人都可以使用下面的所有成员声明。public
成员就像struct
成员。例如,清单 5-1 中的struct
声明是相同的。
清单 5-1 。C++ 的 public 就像 C 的 struct 一样
//: C05:Public.cpp
// Uses identical struct declarations
struct A {
int i;
char j;
float f;
void func();
};
void A::func() {}
struct B {
public:
int i;
char j;
float f;
void func();
};
void B::func() {}
int main() {
A a; B b;
a.i = b.i = 1;
a.j = b.j = 'c';
a.f = b.f = 3.14159;
a.func();
b.func();
} ///:∼
另一方面,private
关键字意味着除了你——该类型的创建者——之外,没有人可以访问该类型的函数成员。private
是你和客户端程序员之间的一堵砖墙;如果有人试图访问一个private
成员,他们会得到一个编译时错误。在清单 5-1 的struct B
中,您可能想要隐藏部分表示(即数据成员),只有您可以访问;你可以在清单 5-2 中看到这一点。
清单 5-2 。私有访问说明符
//: C05:Private.cpp
// Setting the Boundary
// and Hiding Portions of the Representation
struct B {
private:
char j;
float f;
public:
int i;
void func();
};
void B::func() {
i = 0;
j = '0';
f = 0.0;
};
int main() {
B b;
b.i = 1; // OK, public
//! b.j = '1'; // Illegal, private
//! b.f = 1.0; // Illegal, private
} ///:∼
虽然func( )
可以访问B
的任何成员(因为func( )
是B
的成员,因此自动授予其权限),但是像main( )
这样的普通全局函数却不能。当然,其他结构的成员函数也不能。只有在结构声明(“契约”)中明确说明的函数才能访问private
成员。
访问说明符没有规定的顺序,它们可能会出现多次。它们影响在它们之后和下一个访问说明符之前声明的所有成员。
另一个访问说明符:protected
最后一个访问说明符是protected
。protected
的行为就像private
,除了一个我们现在不能谈论的例外:“继承的”结构(不能访问 private
成员)被授予访问protected
成员的权限。这将在第 14 章引入继承时变得更加清楚。出于当前目的,考虑protected
就像private
一样。
朋友
如果您想显式授予对一个不是当前结组合员的函数的访问权限,该怎么办?这是通过在结构声明中声明一个friend
函数来实现的。重要的是,friend
声明出现在结构声明中,因为您(和编译器)必须能够阅读结构声明,并看到关于该数据类型的大小和行为的每一条规则。在任何关系中,一个非常重要的规则是“谁可以访问我的私有实现?”
该类控制哪些代码可以访问其成员。如果你不是一个friend
,没有神奇的方法从外面“闯入”;你不能声明一个新类,然后说:“你好,我是Blah
的朋友!”期待看到Blah
的private
和protected
成员。
可以将一个全局函数声明为friend
,也可以将另一个结构的成员函数,甚至整个结构声明为friend
。清单 5-3 显示了一个例子。
清单 5-3 。宣布成为朋友
//: C05:Friend.cpp
// Friend allows special access
// Declaration (incomplete type specification):
struct X;
struct Y {
void f(X*);
};
struct X { // Definition
private:
int i;
public:
void initialize();
friend void g(X*, int); // Global friend
friend void Y::f(X*); // Struct member friend
friend struct Z; // Entire struct is a friend
friend void h();
};
void X::initialize() {
i = 0;
}
void g(X* x, int i) {
x->i = i;
}
void Y::f(X* x) {
x->i = 47;
}
struct Z {
private:
int j;
public:
void initialize();
void g(X* x);
};
void Z::initialize() {
j = 99;
}
void Z::g(X* x) {
x->i += j;
}
void h() {
X x;
x.i = 100; // Direct data manipulation
}
int main() {
X x;
Z z;
z.g(&x);
} ///:∼
struct Y
有一个成员函数f( )
,它将修改一个X
类型的对象。这是一个有点难的问题,因为 C++ 编译器要求你在引用它之前声明所有的东西,所以struct Y
必须在它的成员Y::f(X*)
在struct X
中被声明为朋友之前声明。但是要声明Y::f(X*)
,必须先声明struct X
!
下面是解决方案。注意,Y::f(X*)
接受了一个X
对象的地址。这很重要,因为编译器总是知道如何传递地址,不管传递的对象是什么,地址的大小都是固定的,即使它没有关于类型大小的完整信息。然而,如果你试图传递整个对象,编译器必须看到X
的整个结构定义,才能知道它的大小和如何传递,然后才允许你声明一个像Y::g(X)
这样的函数。
通过传递一个X
的地址,编译器允许你在声明Y::f(X*)
之前做一个X
的不完整类型规范。这在《宣言》中已经实现。
struct X;
这个声明简单地告诉编译器有一个以这个名字命名的struct
,所以只要你不需要比名字更多的知识,就可以引用它。
现在,在struct X
中,函数Y::f(X*)
可以被声明为friend
没有问题。如果你试图在编译器看到Y
的完整规范之前声明它,它会给你一个错误。这是一个安全特性,用于确保一致性和消除错误。
注意另外两个friend
函数。第一个将一个普通的全局函数g( )
声明为一个friend
。但是g( )
以前没有在全局范围内声明过!事实证明,friend
可以以这种方式同时声明函数和并赋予其friend
状态。这延伸到整个结构,例如
friend struct Z;
是对Z
的不完整的类型规范,它给出了整个结构friend
的状态。
嵌套的朋友
嵌套一个结构并不会自动赋予它对private
成员的访问权。要完成这个,你必须遵循一个特定的形式:首先,声明(而不定义)嵌套结构,然后声明为friend
,最后定义结构。结构定义必须与friend
声明分开,否则它会被编译器视为非成员。清单 5-4 显示了一个例子。
清单 5-4 。嵌套的朋友
//: C05:NestFriend.cpp
// Demonstrates Nested friends
#include <iostream>
#include <cstring> // memset()
using namespace std;
const int sz = 20;
struct Holder {
private:
int a[sz];
public:
void initialize();
struct Pointer;
friend struct Pointer;
struct Pointer {
private:
Holder* h;
int* p;
public:
void initialize(Holder* h);
// Move around in the array:
void next();
void previous();
void top();
void end();
// Access values:
int read();
void set(int i);
};
};
void Holder::initialize() {
memset(a, 0, sz * sizeof(int));
}
void Holder::Pointer::initialize(Holder* rv) {
h = rv;
p = rv->a;
}
void Holder::Pointer::next() {
if(p < &(h->a[sz - 1])) p++;
}
void Holder::Pointer::previous() {
if(p > &(h->a[0])) p--;
}
void Holder::Pointer::top() {
p = &(h->a[0]);
}
void Holder::Pointer::end() {
p = &(h->a[sz - 1]);
}
int Holder::Pointer::read() {
return *p;
}
void Holder::Pointer::set(int i) {
*p = i;
}
int main() {
Holder h;
Holder::Pointer hp, hp2;
int i;
h.initialize();
hp.initialize(&h);
hp2.initialize(&h);
for(i = 0; i < sz; i++) {
hp.set(i);
hp.next();
}
hp.top();
hp2.end();
for(i = 0; i < sz; i++) {
cout << "hp = " << hp.read()
<< ", hp2 = " << hp2.read() << endl;
hp.next();
hp2.previous();
}
} ///:∼
一旦Pointer
被声明,它就被授权访问Holder
的私有成员,方法是
friend struct Pointer;
struct Holder
包含一个int
的数组,Pointer
允许你访问它们。因为Pointer
与Holder
有很强的关联,所以让它成为Holder
的成员结构是明智的。但是因为Pointer
是一个独立于Holder
的类,你可以在main( )
中创建一个以上的类,并用它们来选择数组的不同部分。Pointer
是一个结构,而不是一个原始的 C 指针,所以你可以保证它总是安全地指向Holder
内部。
标准的 C 库函数memset( )
(在<cstring>
中)在清单 5-4 的程序中使用是为了方便。对于起始地址之后的n
字节(n
是第三个参数),它将从特定地址(第一个参数)开始的所有内存设置为特定值(第二个参数)。当然,你可以简单地使用一个循环来遍历所有的内存,但是memset( )
是可用的,经过了良好的测试(所以你不太可能引入错误),并且可能比你手工编码更有效。
它是纯净的吗?
类定义给了你一个审计线索,所以你可以通过查看类来发现哪些函数有权限修改类的私有部分。如果一个函数是一个friend
,这意味着它不是一个成员,但是你无论如何都要允许修改私有数据,并且它必须在类定义中列出,这样每个人都可以看到它是一个特权函数。
C++ 是一种混合面向对象的语言,而不是一种纯粹的语言,添加friend
是为了避开突然出现的实际问题。指出这使得语言不那么“纯粹”是很好的,因为 C++ 被设计成实用的 T2,而不是渴望抽象的理想。
对象布局
第 4 章声明了为 C 编译器编写的struct
和后来用 C++ 编译的struct
将保持不变。这主要指的是struct
的对象布局——也就是说,单个变量的存储位于为对象分配的内存中。如果 C++ 编译器改变了 C struct
s 的布局,那么您编写的任何 C 代码,如果不恰当地利用了struct
中变量位置的知识,都将崩溃。
然而,当您开始使用访问说明符时,您已经完全进入了 C++ 领域,事情发生了一些变化。在一个特定的访问块(一组由访问说明符分隔的声明)中,变量保证是连续布局的,就像在 C 中一样。但是,访问块可能不会按照您声明它们的顺序出现在对象中。尽管编译器会通常完全按照您看到的方式来布置块,但这并没有什么规则,因为特定的机器架构和/或操作环境可能会明确支持private
和protected
,这可能需要将这些块放在特殊的内存位置。语言规范不想限制这种优势。
访问说明符是结构的一部分,不影响从结构中创建的对象。在程序运行之前,所有的访问规范信息都会消失;通常这发生在编译期间。在一个正在运行的程序中,对象成为“存储区域”,仅此而已。如果你真的想,你可以打破所有规则,直接访问内存,就像你在 C 里做的那样。C++ 不是为了防止你做不明智的事情;它只是为你提供了一个更容易、更令人满意的选择。
一般来说,在编写程序时,依赖任何特定于实现的东西都不是一个好主意。当您必须有特定于实现的依赖项时,将它们封装在一个结构中,以便任何移植更改都集中在一个地方。
上课了
访问控制通常被称为实现隐藏。在结构中包含函数(通常被称为封装)会产生具有特征和行为的数据类型,但是访问控制会在该数据类型中设置边界——有两个重要原因。首先是确定客户端程序员能使用什么,不能使用什么。您可以将您的内部机制构建到结构中,而不用担心客户端程序员会认为这些机制是他们应该使用的接口的一部分。
这直接导致了第二个原因,即将接口从实现中分离出来。如果该结构在一组程序中使用,但是客户端程序员除了向public
接口发送消息之外什么也不能做,那么您可以修改任何属于private
的东西,而不需要修改他们的代码。
封装和访问控制加在一起,发明了比CT0】更多的东西。我们现在处于面向对象编程的世界,在这里,一个结构描述一类对象,就像你描述一类鱼或一类鸟一样:属于这个类的任何对象都将共享这些特征和行为。这就是结构声明的含义,它描述了这种类型的所有对象的外观和行为。
在最初的 OOP 语言 Simula-67 中,关键字class
用于描述一种新的数据类型。这显然启发了 Stroustrup(C++ 语言的首席设计师)为 c++ 选择了相同的关键字,以强调这是整个语言的焦点:创建新的数据类型,而不仅仅是带有函数的CT1】。这当然看起来像是一个新关键字的充分理由。
然而,在 C++ 中使用class
几乎是一个不必要的关键字。它与struct
关键字完全相同,除了一点:class
默认为private
,而struct
默认为public
。清单 5-5 显示了产生相同结果的两个结构。
清单 5-5 。比较结构和类
//: C05:Class.cpp
// Similarity of struct and class
struct A {
private:
int i, j, k;
public:
int f();
void g();
};
int A::f() {
return(i + j + k);
}
void A::g() {
i = j = k = 0;
}
// Identical results are produced with:
class B {
int i, j, k;
public:
int f();
void g();
};
int B::f() {
return(i + j + k);
}
void B::g() {
i = j = k = 0;
}
int main() {
A a;
B b;
a.f(); a.g();
b.f(); b.g();
} ///:∼
class
是 C++ 中基本的 OOP 概念。这是本书中而不是将被设置为粗体的关键词之一——随着一个词像“类”一样频繁地重复,它变得令人讨厌。“向类的转变是如此重要,以至于 C++ 设计者们倾向于将struct
完全抛弃,但是向后兼容 C 的需要不允许这样做。
许多人更喜欢创建更像struct
-而不是 class- 的类的风格,因为您通过从public
元素开始覆盖了类的默认到private
行为,比如:
class X {
public:
void interface_function();
private:
void private_function();
int internal_representation;
};
这背后的逻辑是,读者首先看到感兴趣的成员更有意义,然后他们可以忽略任何写有private
的内容。事实上,所有其他成员都必须在类中声明的唯一原因是,编译器知道对象有多大,可以正确地分配它们,这样可以保证一致性。
然而,本书中的示例将把private
成员放在第一位,就像这样:
class X {
void private_function();
int internal_representation;
public:
void interface_function();
};
有些人甚至不厌其烦地修饰自己的私人名字,就像这样:
class Y {
public:
void f();
private:
int mX; // "Self-decorated" name
};
因为mX
已经隐藏在Y
的范围内,所以m
(对于“成员”)是不必要的。然而,在具有许多全局变量的项目中(这是您应该努力避免的,但在现有项目中有时是不可避免的),能够在成员函数定义中区分哪些数据是全局的,哪些数据是成员是有用的。
修改存储以使用访问控制
从 第 4 章 中提取例子并修改它们以使用类和访问控制是有意义的。请注意,接口的客户端程序员部分现在是如何被清楚地区分的,所以客户端程序员不可能意外地操作了他们不应该操作的类的一部分。参见清单 5-6 。
清单 5-6 。更新存储以使用访问控制
//: C05:Stash.h
// Converted to use access control
#ifndef STASH_H
#define STASH_H
class Stash {
int size; // Size of each space
int quantity; // Number of storage spaces
int next; // Next empty space
// Dynamically allocated array of bytes:
unsigned char* storage;
void inflate(int increase);
public:
void initialize(int size);
void cleanup();
int add(void* element);
void* fetch(int index);
int count();
};
#endif // STASH_H ///:∼
inflate( )
函数被设为private
,因为它只被add( )
函数使用,因此是底层实现的一部分,而不是接口。这意味着,在以后的某个时候,您可以更改底层实现来使用不同的系统进行内存管理。
除了包含文件的名称之外,头是本例中唯一更改的内容。实现文件和测试文件是相同的。
修改堆栈以使用访问控制
作为第二个例子,清单 5-7 显示了Stack
变成了一个类。现在嵌套的数据结构是private
,这很好,因为它确保了客户端程序员既不必查看它,也不必依赖于Stack
的内部表示。
清单 5-7 。将堆栈转换为类
//: C05:Stack2.h
// Nested structs via linked list
#ifndef STACK2_H
#define STACK2_H
class Stack {
struct Link {
void* data;
Link* next;
void initialize(void* dat, Link* nxt);
}* head;
public:
void initialize();
void push(void* dat);
void* peek();
void* pop();
void cleanup();
};
#endif // STACK2_H ///:∼
和以前一样,实现没有改变,所以这里不再重复。测试也是一样的。唯一改变的是类接口的健壮性。访问控制的真正价值是防止您在开发过程中越界。事实上,编译器是唯一知道类成员保护级别的东西。没有将访问控制信息分解到成员名称中,并传递给链接器。所有的保护检查都是由编译器完成的;它在运行时消失了。
请注意,呈现给客户端程序员的界面现在是真正的下推堆栈。它碰巧被实现为一个链表,但是你可以改变它,而不影响客户端程序员与之交互的内容,或者(更重要的是)一行客户端代码。
处理类别
C++ 中的访问控制允许你把接口和实现分开,但是实现隐藏只是部分的。为了正确地创建和操作对象,编译器仍然必须看到对象所有部分的声明。您可以想象一种编程语言,它只需要对象的公共接口,并允许隐藏私有实现,但是 C++ 尽可能静态地(在编译时)执行类型检查。这意味着如果出现错误,您将尽早了解。这也意味着你的程序更有效率。然而,包含私有实现有两个影响:实现是可见的,即使您不容易访问它,并且它可能导致不必要的重新编译。
隐藏实现
一些项目不能让他们的实现对客户程序员可见。它可能会在库头文件中显示公司不想让竞争对手知道的战略信息。例如,您可能正在处理一个安全性成为问题的系统,例如加密算法,并且您不想在头文件中暴露任何可能帮助人们破解代码的线索。或者你可能把你的库放在一个“敌对”的环境中,程序员无论如何都会使用指针和类型转换直接访问私有组件。在所有这些情况下,将实际结构编译在实现文件中而不是在头文件中公开是很有价值的。
减少重新编译
如果一个文件被接触(即被修改)或者如果它所依赖的另一个文件(即一个包含的头文件)被接触,您的编程环境中的项目管理器将导致该文件的重新编译。这意味着,无论何时对一个类进行更改,无论是对公共接口还是私有成员声明,都将强制对包含该头文件的任何内容进行重新编译。对于一个处于早期阶段的大型项目来说,这可能非常笨拙,因为底层的实现可能会经常改变;如果项目非常大,编译的时间会阻碍快速周转。
解决这一问题的技术有时被称为处理类——除了一个指针,即*微笑,关于实现的一切都消失了。*指针指的是一个结构,其定义和所有成员函数定义都在实现文件中。因此,只要接口不变,头文件就不会受到影响。实现可以随意更改,只需要重新编译实现文件,并与项目重新链接。
清单 5-8 包含了一个演示该技术的简单例子。头文件只包含公共接口和一个不完全指定的类的指针。
清单 5-8 。处理类别
//: C05:Handle.h
// Handle classes header file
#ifndef HANDLE_H
#define HANDLE_H
class Handle {
struct Hire; // Class declaration only
Hire* smile;
public:
void initialize();
void cleanup();
int read();
void change(int);
};
#endif // HANDLE_H ///:∼
这是客户端程序员能够看到的全部内容。这条线
struct Hire;
是不完整的类型规范或类声明(类定义包括类的主体)。它告诉编译器Hire
是一个结构名,但是它没有给出关于struct
的任何细节。这些信息只够创建一个指向struct
的指针;在提供结构体之前,您不能创建对象。在这种技术中,结构体隐藏在实现文件中(参见清单 5-9 )。
清单 5-9 。
//: C05:Handle.cpp {O}
// Handle implementation
#include "Handle.h" // To be INCLUDED from Header FILE above
#include "../require.h" // To be INCLUDED from Header FILE in *[Chapter 3](03.html)*
// Define Handle's implementation:
struct Handle::Hire {
int i;
};
void Handle::initialize() {
smile = new Hire;
smile->i = 0;
}
void Handle::cleanup() {
delete smile;
}
int Handle::read() {
return smile->i;
}
void Handle::change(int x) {
smile->i = x;
} ///:∼
Hire
是一个嵌套结构,因此必须使用范围解析来定义,例如:
struct Handle::Hire {
在Handle::initialize( )
中,存储被分配给Hire
结构,而在Handle::cleanup( )
中,该存储被释放。这个存储用来代替您通常放入类的private
部分的所有数据元素。当你编译Handle.cpp
时,这个结构定义隐藏在目标文件中,没有人能看到它。如果你改变了Hire
的元素,唯一需要重新编译的文件是Handle.cpp
,因为头文件没有被改动。
Handle
的使用类似于 any class 的使用:包含头部、创建对象和发送消息(参见清单 5-10 )。
清单 5-10 。使用 Handle 类
//: C05:UseHandle.cpp
//{L} Handle
// Use the Handle class
#include "Handle.h"
int main() {
Handle u;
u.initialize();
u.read();
u.change(1);
u.cleanup();
} ///:∼
客户端程序员唯一可以访问的是公共接口,所以只要实现是唯一改变的,文件就永远不需要重新编译。因此,尽管这不是完美的实现隐藏,但这是一个很大的改进。
审查会议
- C++ 中的访问控制为类的创建者提供了有价值的控制。该类的用户可以清楚地看到他们可以使用什么,忽略什么。然而,更重要的是确保没有客户端程序员变得依赖于类的底层实现的任何部分。如果你作为类的创建者知道这一点,你可以改变的底层实现,因为没有的客户端程序员会因为不能访问类的这一部分而受到影响。
- 当你有能力改变底层实现时,你不仅可以在以后的某个时间改进你的设计,而且你也有犯错误的自由。无论你计划和设计得多么仔细,你都会犯错误。知道犯这些错误是相对安全的意味着你会更有实验性,你会学得更快,你会更快地完成你的项目。
- 一个类的公共接口是客户程序员所看到的,所以这是类在分析和设计过程中得到“正确”的最重要的部分。但即使这样,你也有一些改变的余地。如果你第一次没有得到正确的接口,你可以添加更多的功能,只要你不删除任何客户程序员已经在他们的代码中使用的功能。*