class stack_id; //stack_id is a type
//no details about stacks or stack_ids are known here
stack_id create_stack(int size); //make a stack and return its identifier
destroy_stack(stack_id);
void push( stack_id,char)
char pop(stack_id)
相对于以往那些无结构的混乱风格,这当然是一次重大的改进。然而,通过这种方式实现的“类型”又明显地和语言的内建类型有区别。每一个类型管理模块都必须分别定义自己的机制来生成自己的“变量”;这里没有什么明确的方法可以赋予变量以标识符,也不可能让编译器和编程环境了解变量的名字。同时,没有办法让这些变量服从常用的变量作用域规则和参数传递规则。
通过模块机制建立起来的类型在很多重要的方面都和内建类型存在区别,同时,它获得的支持也远比内建类型获得要低级得多。例如:
void f()
{
stack_id s1;
stack_id s2;
s1 = create_stack(200);
//Oops: forgot to create s2
3.1初始化和清除
一旦类型的表示被隐藏了起来,则必须提供一个机制来执行对变量的初始化。一个简单的方案是要求用户在使用一个变量之前先调用一个特定的函数来初始化它。例如:
class vector{
int sz;
int* v;
public:
void init(int size); // call init to initialize sz and v before the first use of a
//vector
//...
}
vector v;
//don't use v here
v.init(10);
//use v here
这容易导致错误并且不够优雅。好一点的方案允许类型的设计者为初始化提供一个特别的函数;给定了这个函数,分配和初始化一个变量变成了同一个操作。这个特定的函数经常被称为构造函数。在某些场合初始化一个对象可能并不是十分简单的,这样就常常需要一个对等的操作来在对象被最后一次使用之后执行清除。在C++中,这样的一个清除函数称为析构函数。考虑一个vector类型:
class vector{
int sz;
int* v;
public:
vector(int); //constructor
~vector(); //destructor
int& operator[](int index);
};
vector的构造函数可以定义为分配空间,象这样:
vector::vector(int s)
{
if( s<=0 ) error("bad vector size' );
sz = s;
v = new int; //allocate an array of "s" integers
}
vector的析构函数释放这部分空间
vector::~vector()
{
(译注:此处最好是delete []v;)
delete v; //deallocate the memory pointed to by v
}
C++不支持垃圾收集,这种允许一个类型自己管理存储空间而不需要用户来干预的技术是一个补偿。存储管理是构造/析构函数经常执行的操作,但是它们也常常用来执行与此无关的事情。
3.2赋值和初始化
对于很多类型而言,控制其初始化和清除过程就已经足够了,但并不是所有的类型都如此。有时候控制拷贝过程也是十分必要的,考虑vector:
vector v1[100];
vector v2 = v1; //make a new vector v2 initialized to v1
v1 = v2; //assign v2 to v1
在这里必须有机制来定义v2初始化和对v1赋值的含义,当然也可以选择提供机制来禁止这种拷贝。理想的情况是,这两种机制都存在。例如:
class vector{
int *v;
int sz;
public:
//....
void operator=(vector&); //assignment
vector(vector&); //initialization
};
给出了用户定义的操作来解释vector的赋值和初始化。赋值可以象这样定义:
( 译注:由于在上文class vector中operator=(vector&a)声明为void类型,所以这里的定义最好为
void vector:perator(vector&a) )
vector:perator=(vector&a) //check size and copy elements
{
if( sz != a.sz ) error( "bad vector size for = " );
for( int = 0; i<sz;i ++) v = a.v;
}
虽然赋值操作可以依赖于一个“旧的 ”的vector对象,但初始化操作就必须有所不同,例如:
vector::vector(vector& a) // initialize a vector from another vector
{
sz = a.sz;
v = new int[sz];
for( int i = 0; i < sz; i++ ) v=a.v; //copy elements
}
在C++中,一个形如X(X&) 的构造函数定义了从X的一个对象出发构造X的另一个对象的初始化过程。除了明确地构造X的对象之外,X(X&)也被用来处理传值的传参过程和函数的返回值。
在C++中,可以通过将赋值声明为私有来禁止对于对象的赋值操作。
class X{
void operator=(X&); //only members of x can
X(X&); //copy an x
//...
public:
//...
}
Ada不支持构造,析构,对赋值的重载和用户定义的参数传递和返回机制,这严重限制了用户自定义类型的种类,同时强迫程序员回到“数据隐藏”技术,就是说,用户必须设计和使用类型管理模块而不是真正的类型。
3.3参数化类型
为什么我们要定义一个整数类型的vector呢?要知道,用户常常需要一个对于vector的作者而言类型未知的vector。因此,vector应当采用一种可以将“类型”作为参数来引用的表达方式加以定义:
class vector<class T>{ //vector of elements of type T
T* v;
int sz;
public:
vector( int s)
{
if( s<= 0 ) error( "bad vector size" );
v = new T[sz = s ]; //allocate an array of "s" "T"s
}
T& opeartor[](int i);
int size() { return sz; }
//...
}
特定类型的vector可以象这样定义和使用:
vector<int> v1(100); //v1 is a vector of 100 integers
vector<complex> v2(200); //v2 is a vector of 200 complex numbers
v2[ i ] = complex(v1[x], v1[y]);
Ada,Clu和ML支持参数化类型。不幸的是,C++不支持(译注,现在的C++标准支持参数化类型,称为模板);这里使用的记号只是为了演示;但在必要时,可以使用宏来模拟参数化类型。和那些指定了所有类型的类比起来这样做并没有在运行时引入更多的开销。
一般来说,一个参数化类型总会依赖于参数类型的某些方面。例如,vector的有些操作假定参数类型定义了赋值操作。那么人们如何保证这一点呢 一种方案是要求参数化类型的设计者表明这种依赖关系。例如,“T必须是一种定义了赋值操作的类型”。另一个好一点的办法让参数化类型的规格和参数类型的规格彼此独立,编译器可以检测到对不存在操作的调用,并且可以给出相应的错误提示。例如:
cannot define vector(non_copy):perator[](non_copy&) :
type non_copy does not have operator=
这种技术使得我们可以在“操作”这个级别上处理参数类型和参数化类型之间的依赖性。例如,我们可能定义一个具有排序功能的vector,排序操作可能用到参数类型的<,<= 和=操作。然而,只要不调用vector的排序功能,我们还是可以使用一个没有<操作的类型来参数化vector。
从参数化类型中生成的每一个类型之间是彼此独立的,这是一个问题。例如,vector<char>和vector<complex>之间完全无关。理想的情况是,人们可以表达并且利用从同一个参数化类型中生成的各个类型之间具有的共性,例如,vector<char>和vector<complex>都具有一个和类型无关的size()操作。从vector的定义中推导出size可以被实例类型共用是可能的,但其过程并不简单。解释型的语言或者同时支持参数化类型和继承机制的语言在这个方面具有优势。