类
这一部分介绍了 c o n s t用于类的两种办法。程序员可能想在一个类里建立一个局部常量,将
它用在常数表达式里,这个常数表达式在编译期间被求值。然而, c o n s t的意思在类里是不同
的,所以必须使用另一技术—枚举,以达到同样的效果。
我们还可以建立一个类对象常量( c o n s t)(正如我们刚刚看到的,编译器总是建立临时类
对象常量) 。但是,要保持类对象为常量却比较复杂。编译器能保证一个内部数据类型为常量,
但不能控制一个类中错综复杂的事物。为了保证一个类对象为常量,引进了 c o n s t成员函数:
对于一个常量对象,只能调用 c o n s t成员函数。
7.4.1 类里的const和enum
常数表达式使用常量的情况之一是在类里。典型的例子是在一个类里建立一个数组,并用
c o n s t代替 # d e f i n e建立数组大小以及用于有关数组的计算。并把数组大小一直隐藏在类里,这
样,如果用 s i z e表示数组大小,就可以把 s i z e这个名字用在另一个类里而不发生冲突。然而预
处理器从这些 # d e f i n e被定义的时起就把它们看成全程的,所以如用 # d e f i n e就不会得到预期的效
果。
起初读者可能认为合乎逻辑的选择是把一个 c o n s t放在类里。但这不会产生预期的结果。在
一个类里, c o n s t恢复它在 C中的一部分意思。它在每个类对象里分配存储并代表一个值,这个
值一旦被初始化以后就不能改变。在一个类里使用 c o n s t的意思是“在这个对象寿命期内,这
是一个常量”。然而,对这个常量来讲,每个不同的对象可以含一个不同的值。
这样,在一个类里建立一个 c o n s t时,不能给它初值。这个初始化工作必须发生在构造函数
里,并且,要在构造函数的某个特别的地方。因为 c o n s t必须在建立它的地方被初始化,所以
在构造函数的主体里, c o n s t必须已初始化了,否则,就只有等待,直到在构造函数主体以后
的某个地方给它初始化,这意味着过一会儿才给 c o n s t初始化。当然,无法防止在在构造函数
主体的不同地方改变 c o n s t的值。
1. 构造函数初始化表达式表
构造函数有个特殊的初始化方法,称为构造函数初始化表达式表,起初用在继承里(继承
是以后章节中有关面向对象的主题)。构造函数初始化表达式表— 顾名思义,是出现在构造
函数的定义里的—是一个出现在函数参数表和冒号后,但在构造函数主体开头的花括号前的
“函数调用表”。这提醒人们,表里的初始化发生在构造函数的任何代码执行之前。这是把所有
的c o n s t初始化的地方,所以类里的 c o n s t正确形式是:
class fred {const size;
public :
fred();
};
fred::fred() : size(100{}
开始时,上面显示的构造函数初始化表达式表的形式容易使人们混淆,因为人们不习惯看到一个内部数据类型有一个构造函数。
2. 内部数据类型“构造函数”
随着语言的发展和人们为使用户定义类型像内部数据类型所作的努力,有时似乎使内部数
据类型看起来像用户定义类型更好。在构造函数初始化表达式表里,可以把一个内部数据类型
看成好像它有一个构造函数,就像下面这样:
class B {int i;
public:
B(int I);
};
B::B(int I) : i(I) {}
这在初始化c o n s t数据成员时尤为典型,因为它们必须在进入函数体前被初始化。我们还可以把这个内部数据类型的“构造函数”(仅指赋值)扩展为一般的情形,可以写:
float pi (3.14159);
把一个内部数据类型封装在一个类里以保证用构造函数初始化,是很有用的。例如,下面是一
个i n t e g e r类:
class integer{int i;
public:
integer(int I = 0);
};
integer::integer(int I) : i(I) {}
现在,如果建立一个i n t e g e r数组,它们都被自动初始化为零:integer I[100];
与f o r循环和 m e m s e t ( )相比,这种初始化不必付出更多的开销。很多编译器可以很容易地把它优
化成一个很快的过程。
7.4.2 编译期间类里的常量
因为在类对象里进行了存储空间分配,编译器不能知道 c o n s t的内容是什么,所以不能把它
用作编译期间的常量。这意味着对于类里的常数表达式来说, c o n s t就像它在C中一样没有作用。
我们不能这样写:
class bob {const size = 100; // illegal
int array[size]; // illegal
}
//...
在类里的 c o n s t意思是“在这个特定对象的寿命期内,而不是对于整个类来说,这个值是不变的( c o n s t)”。那么怎样建立一个可以用在常数表达式里的类常量呢?一个普通的办法是使
用一个不带实例的无标记的 e n u m。枚举的所有值必须在编译时建立,它对类来说是局部的,
但常数表达式能得到它的值,这样,我们一般会看到:
class bunch {enum { size = 1000 };
int i[size];
};
使用 e n u m是不会占用对象中的存储空间的,枚举常量在编译时被全部求值。我们也可以明确地建立枚举常量的值:
enum { one=1,two=2,three};
对于整型 e n u m,编译器从最后一个值继续计数,所以枚举常量 t h r e e将取值3。
下面这个例子表明了在一个串指针栈里的 e n u m的用法:
//: SSTACK.CPP -- Enums inside classes#include <string.h>
#include <iostream.h>
class StringStack {enum { size = 100 };
const char* stack[size];
int index;
public:
StringStack();
void push(const char* s);
const char* pop();
};
StringStack::StringStack() : index(0) {memset(stack, 0, size * sizeof(char*));
}
void StringStack::push(const char* s) {if(index < size)
stack[index++] = s;
}
const char* StringStack::pop() {if(index > 0) {
const char* rv = stack[--index];
stack[index] = 0;
return rv;
}
return 0;
}
const char* iceCream[] = {"pralines & cream",
"fudge fipple",
"jamocha almond fudge",
};
const ICsz = sizeof iceCream/sizeof *iceCream;
main() {StringStack SS;
for(int i = 0; i< ICsz; i++)
SS.push(iceCream[i]);
const char* cp;
while((cp = SS.pop()) != 0)
cout << cp << endl;
}
注意p u s h ( )带一个const char*参数, p o p ( )返回一个const char*, s t a c k保存const char*。如果不是这样,就不能用 s t r i n g s t a c k保存i c e C r e a m里的指针。然而,它不让程序员做任何事情以改
变包含在 S t r i n g s t a c k里的对象。当然,不是所有的串指针栈都有这个限制。
虽然会经常在以前的程序代码里看到使用 e n u m技术,但C + +还有一个静态常量 static const,
它在一个类里产生一个更灵活的编译期间的常量。这一点在将第 9章描述。
• 枚举的类型检查
C中的枚举是相当原始的,只涉及整型值和名字,但不提供类型检查。在 C + +里,正如我
们现在所期望的,类型概念是十分重要的,枚举正是这样要求的。我们建立了一个已命名的枚
举时,我们就已经有效地建立了一个新的类型,就像一个类一样:在编译单元被翻译期间,枚
举名字将成为一个保留字。
另外, C + +中的枚举有一个比 C中更严格的类型检查。假如我们有一个称为 a的枚举类型
c o l o r,就会注意这一点。在 C中可以写 a + +,但在 C + +中不能这样写。这是因为枚举自增正在
执行两个类型转换,其中一个类型在 C + +中是合法的,另一个是不合法的。首先,枚举的值隐
蔽地从c o l o r转换到 i n t,然后值增 1 ,然后 i n t又转回到 c o l o r。在C + +中,这样做是不允许的,因
为c o l o r是一个与 i n t不同的类型,无法知道 b l u e加1恰好出现在颜色表里。如果要对 c o l o r加1 ,那
么它应该是一个类(有自增操作),而不是一个 e n u m。不论什么时候写出了隐含对 e n u m进行类
型转换的代码,编译器都把它标记成危险的活动。
共用数据类型有类似的附加类型检查。
7.4.3 const对象和成员函数
可以用 c o n s t限定类成员函数,这是什么意思呢?为了搞清楚这一点,必须首先掌握 c o n s t
对象的概念。
用户定义类型和内部数据类型一样,都可以定义一个 c o n s t对象。例如:
const int i=1 ;
const blob B(2);
136 C + +编程思想
下载
这里, B是类型 b l o b的一个c o n s t对象。它的构造函数被调用,且其参数为“ 2”。由于编译器强
调对象为 c o n s t的,因此它必须保证对象的数据成员在对象寿命期内不被改变。可以很容易地
保证公有数据不被改变,但是怎么知道哪个成员函数会改变数据?又怎么知道哪个成员函数对
于c o n s t对象来说是“安全”的呢?
如果声明一个成员函数为 c o n s t函数,则等于告诉编译器可以为一个 c o n s t对象调用这个函
数。一个没有被特别声明为 c o n s t的成员函数被看成是将要修改对象中数据成员的函数,而且
编译器不允许为一个 c o n s t对象调用这个函数。
然而,不能就此为止。仅仅声明一个函数在类定义里是 c o n s t的,不能保证成员函数也如此
定义,所以编译器迫使程序员在定义函数时要重申 c o n s t说明。 ( c o n s t已成为函数识别符的一部
分,所以编译器和连接程序都要检查 c o n s t)。为确保函数的常量性,在函数定义中,如果我们
改变对象中的任何成员或调用一个非 c o n s t成员函数,编译器都将发出一个出错信息,强调在
函数定义期间函数被定义成 c o n s t函数。这样,可以保证声明为 c o n s t的任何成员函数能够按定
义方式运行。
c o n s t放在函数声明前意味着返回值是常量,但这不合语法。必须把 c o n s t标识符放在参数
表后。例如:
class X {int i;
public:
int f() const;
};
关键字c o n s t必须用同样的方式重复出现在定义里,否则编译器把它看成一个不同的函数:int X::f() const {return i; }
如果f ( )试图用任何方式改变 i或调用另一个非 c o n s t成员函数,编译器把它标记成一个错误。
任何不修改成员数据的函数应该声明为 c o n s t函数,这样它可以由 c o n s t对象使用。
下面是一个比较 c o n s t和非c o n s t成员函数的例子:
//: QUOTER.CPP -- Random quote selection
#include <iostream.h>
#include <stdlib.h> // Random number generator
#include <time.h> // To seed random generator
class quote {int lastquote;
public:
quoter();
int Lastquote() const;
const char* quote();
};
quote::quoter(){lastquote = -1;
time_t t;
srand((unsigned) time(&t)); // Seed generator
}
int quote::Lastquote() const {return lastquote;
}
const char* quoter::quote() {static const char* quotes[] = {
"Are we having fun yet?",
"Doctors always know best",
"to support the idea"
};
const qsize = sizeof quotes/sizeof *quotes;
int qnum = rand() % qsize;
while(lastquote >= 0 && qnum == lastquote)
qnum = rand() % qsize;
return quotes[lastquote = qnum];
}
main() {quoter q;
const quoter cq;
cq.Lastquote(); // OK
// ! cq.quote(); // Not OK; non const function
for(int i = 0; i< 20; i++)
cout << q.quote() << endl;
}
构造函数和析构函数都不是 c o n s t成员函数,因为它们在初始化和清理时,总是对对象作些修改。 q u o t e ( )成员函数也不能是 c o n s t函数,因为它在返回说明里修改数据成员 l a s t q u o t e。然而
L a s t q u o t e ( )没做修改,所以它可以成为 c o n s t函数,而且也可以被c o n s t对象c q安全地调用。
• 按位和与按成员 c o n s t
如果我们想要建立一个 c o n s t成员函数,但仍然想在对象里改变某些数据,这时该怎么办
呢?这关系到按位 c o n s t和按成员 c o n s t的区别。按位 c o n s t意思是对象中的每个位是固定的,所
以对象的每个位映像从不改变。按成员 c o n s t意思是,虽然整个对象从概念上讲是不变的,但
是某个成员可能有变化。当编译器被告知一个对象是 c o n s t对象时,它将保护这个对象。这里
我们要介绍在 c o n s t成员函数里改变数据成员的两种方法。
第一种方法已成为过去,称为“强制转换 c o n s t”。它以相当奇怪的方式执行。取 t h i s(这个
关键字产生当前对象的地址)并把它强制转换成指向当前类型对象的指针。看来 t h i s已经是我
们所需的指针,但它是一个 c o n s t指针,所以,还应把它强制转换成一个普通指针,这样就可
以在运算中去掉常量性。下面是一个例子:
//: CASTAWAY.CPP -- "Casting away" constness
class Y {int i,j;
public:
Y() {i = j = 0;}
void f() const;
};
void Y::f() const {//! i++; // Error -- const member function
((Y*)this)->j++; //OK: cast away const-ness
}
main() {const Y yy;
yy.f(); // Actually changes it!
}
这种方法可行,在过去的程序代码里可以看到这种用法,但这不是首选的技术。问题是:t h i s没有用 c o n s t修饰,这在一个对象的成员函数里被隐藏,这样,如果用户不能见到源代码
(并找到用这种方法的地方),就不知道发生了什么。为解决所有这些问题,应该在类声明里使
用关键字m u t a b l e,以指定一个特定的数据成员可以在一个 c o n s t对象里被改变。
//: MUTABLE.CPP -- The "mutable" keyword
class Y {int i;
mutable int j;
public:
Y() { i = j = 0; }
void f() const;
};
void Y::f() const {//! i++; // Error -- const member function
j++; // OK: mutable
}
main() {const Y yy;
yy.f(); // Actually changes it!
}
7.4.4 只读存储能力
如果一个对象被定义成 c o n s t对象,它就成为被放进只读存储器( R O M)中的一个候选,
这经常是嵌入式程序设计中要考虑的重要事情。然而,只建立一个 c o n s t对象是不够的— 只
读存储能力的条件非常严格。当然,这个对象还应是按位 c o n s t的,而不是按成员 c o n s t的。如
果只通过关键字 m u t a b l e实现按成员常量化的话,就容易看出这一点。如果在一个 c o n s t成员函
数里的 c o n s t被强制转换了,编译器可能检测不到这个。另外,
1) class 或s t r u c t必须没有用户定义的构造函数或析构函数。
2) 这里不能有基类(将在关于继承的章节里谈到) ,也不能有包含用户定义的构造函数或
析构函数的成员对象。
在只读存储能力类型的 c o n s t对象中的任何部分上,有关写操做的影响没有定义。虽然适当
形式的对象可被放进 R O M里,但是目前还没有什么对象需要放进 R O M里。
7.5 可变的( volatile)
v o l a t i l e的语法与 c o n s t是一样的,但是 v o l a t i l e的意思是“在编译器认识的范围外,这个数
据可以被改变”。不知何故,环境正在改变数据(可能通过多任务处理),所以, v o l a t i l e告诉编
译器不要擅自做出有关数据的任何假定—在优化期间这是特别重要的。如果编译器说: “我
已经把数据读进寄存器,而且再没有与寄存器接触”。一般情况下,它不需要再读这个数据。
但是,如果数据是 v o l a t i l e修饰的,编译器不能作出这样的假定,因为可能被其他进程改变了,
它必须重读这个数据而不是优化这个代码。
就像建立c o n s t对象一样,程序员也可以建立 v o l a t i l e对象,甚至还可以建立 const volatile对
象,这个对象不能被程序员改变,但可通过外面的工具改变。下面是一个例子,它代表一个类,
这个类涉及到硬件通信:
//: VOLATILE.CPP -- The volatile keywordclass comm {
const volatile unsigned char byte;
volatile unsigned char flag;
enum { bufsize = 100 };
unsigned char buf[bufsize];
int index;
public:
comm();
void isr() volatile;
char read(int Index) const;
};
comm::comm() : index(0),byte(0),flag(0) {}
// Only a demo; won't actually work// As an interrupt service routine:
void comm::isr() volatile {
if(flag) flag = 0;
buf[index++] = byte;
// Wrap to beginning of buffer:
if(index >= bufsize) index = 0;
}
char comm::read(int Index) const {if(Index < 0 || Index >= bufsize)
return 0;
return buf[Index];
}
main() {volatile comm Port;
Port.isr(); // OK
//! Port.read(0); // Not OK;
// read() not volatile
}
就像const一样,我们可以对数据成员、成员函数和对象本身使用 v o l a t i l e,可以并且也只能为v o l a t i l e对象调用 v o l a t i l e成员函数。
函数 i s r ( )不能像中断服务程序那样使用的原因是:在一个成员函数里,当前对象( t h i s)
的地址必须被秘密地传递,而中断服务程序 I S R一般根本不要参数。为解决这个问题,可以使
i s r ( )成为静态成员函数,这是下面章节讨论的主题。
v o l a t i l e的语法与 c o n s t是一样的,所以经常把它们俩放在一起讨论。为表示可以选择两个
中的任何一个,它们俩通称为 c - v限定词。