来源:远方网络 | 2005-3-7 22:26:53 | (有2505人读过)
计算机中有一个被广泛使用的基本概念一直是保持固定不变的:存储区(memory)是word或byte的序列,通过被称为地址的整数来进行索引。现代的计算机——即近20年内设计的计算机——都倾向于对函数调用使用栈的方法进行直接的支持;而更为甚之的是,所有流行的机器都有一些诸如输入、输出之类的重要设施,这些设施与传统的面向word或byte的计算和存储模型并不能很好的相适应。这些设施总是需要通过特殊的机器指令或者特别的语义来访问存储区。而从一个高级语言的角度来看,不管用那种方法,对这些设施的使用者而言都是麻烦的,而且与特定机器的体系结构有关。
在对这方面的处理上,C是最为成功的一门语言,它所提供的程序设计模型能够与机器包含的模型很好的相适应。C提供独立于机器体系结构的、语言层面的方法;这种方法能与关键的硬件方法相对应:字符用byte,整数用word,指针用于地址,函数用于对程序的抽象。C还摒弃了一些在这方面具有约束性的语言特性,使得程序员可以在必要时亲自操纵特定的硬件层细节。在那些依赖并利用机器的某些特定设计而得到益处的领域里,C就显得相对更容易学和更容易使用一些。更何况,C是如此容易实现——它几乎已经变得随处可得了。
3.1 数组和指针(Arrays and Pointers)
在C中,一个数组只是一片存储区域。例如:
int v[10]; // 10个int型变量的数组
v[3] = 1; // 将1赋值给v[3]
int x = v[3]; // 从v[3]读取元素值
表示下标的标记 [] 用在声明中,表示声明的对象是数组;用在表达式中,表示数组的元素。
一个C语言中的指针是指一个变量,它存放着某个存储位置的地址。例如:
int* p; // p是一个指向int型对象的指针
p = &v[7]; // 将v[7]的地址赋给p
*p = 4; // 通过p来向v[7]写入数据
int y = *p; // 通过p来读取v[7]的数据
指针的提领(dereference,“指向”的意思)标记 * 用在声明中,表示声明的对象是指针;用在表达式中,表示取指针所指向的那个元素。
这可以用下图表示:
C++引借了C的这种颇为简单的、与机器结构极为近似的存储方案,同时也引借了C在表达式、控制结构以及函数等方面所使用的方案。例如,我们可以像下面这样写一个函数,其功能是在vector里查找一个元素并返回一个指向匹配元素的指针:
int* find(int v[], int vsize, int val) //在v中查找val
{
for(int i = 0; i < vsize; i++) //从0到vsize-1的循环
if (v[i] == val) return &v[i]; //如果找到val,就返回指向元素的指针
return &v[vsize]; //如果没找到,就返回v末端元素的指针
}
++运算符意即“增量”。因此“C++”这个名称可以理解为“多于C”、“换代的C”或者“C之加强版”。“C++”的发音是“See Plus Plus”(译注:拟音作“斯伊 普拉斯 普拉斯”)。
find()函数可以像这样使用:
int count[] = {2, 3, 1, 9, 7, 3, 3, 0, 2};
int count_size = 9;
void f()
{
int* p = find(count, count_size, 7); //在count中查找7
int* q = find(count, count_size, 0); //在count中查找0
*q = 4;
// …
}
对于诸如find()这样的函数,C++标准程序库提供了更为通用的函数版本;详见§6.3。像上面代码中f()那样被声明为void的函数不返回任何值。
3.2 存贮(Storage)
在C和C++中,有三种使用存储区的基本方式:
l [静态存储区(Static Memory)]
在静态存储区中,连接器(linker)根据程序的需求为对象分配空间。全局变量、静态类成员以及函数中的静态变量都被分配在该区域中。一个在该区域中分配的对象只被构造一次,其生存期一直维持到程序结束。在程序运行的时候其中的地址是固定不变的。在使用线程(thread,共享地址空间的并发物)的程序里,静态对象可能会引起一些问题,因为这时的静态对象是被共享的,要对其正常访问就需要进行锁定操作。
l [自动存储区(Automatic Memory)]
函数的参数和局部变量被分配在此。对同一个函数或区块的每一处调用,其在该区域内都有自己单独的位置。这种存储被自动创建和销毁;因而才叫做“自动存储区”。自动存储区也被称为是“在栈上的(be on the stack)”。
l [自由存储区(Free Store)]
在该区域中,程序必须明确的为对象申请空间,并可以在使用完毕之后释放申请到的空间(使用new和delete运算符)。当程序需要其中更多的空间时,就使用new向操作系统提出申请。通常情况下,自由存储区(也被称作动态存储区或者堆(heap))在一个程序的生存期内是不断增大的,因为其间被其它程序占用的空间从来都不被归还给操作系统。例如:
int g = 7; //全局变量,分配在静态存储区中
void f()
{
int loc = 9; //局部变量,分配在栈(stack)中
int* p = new int; //变量被分配在自由存储区中
// …
delete p; //归还p所指向的区域,以便重新使用
}
对程序员来说,自动存储区和静态存储区总是被隐式的使用,这种方式既简单又一目了然。真正有趣的问题是应该如何管理自由存储区。分配空间(使用new运算符)是很简单的,但在去配的时候,则必须有一个完善的归还空间的方案;否则的话,存储空间最终会被耗尽(特别是在长时间运行的程序中)。
对于这个问题,最简单的解决方案就是使用与自由存储区中的对象相对应的自动对象来处理分配和去配。基于此,许多container在实现的时候,都被作为自由存储区中对象的掌控者(handle)。例如,一个string(§6.1)被用来管理自由存储区中的字符序列:
这个string可以自动的对各个元素所需要的空间进行分配和释放。例如:
void g()
{
string s = “Time flies when you’re having fun”; //创建string对象
// …
} //隐式的销毁string对象
§4.2.1中关于Stack的例子将演示构造函数和析构函数如何被用来管理存储区中元素的生存期。所有标准container(§6.2)——诸如vector、list和map等——都能以这种简便的方式实现。
当这种简单、规整且有效的方案仍然不够用的时候,程序员也许会使用一种所谓的“内存管理器”,以查找那些不再被引用的(译注:即废弃了的)对象,并通过它来收回被那些对象占用的空间,以便将其用于存放新的对象。这种内存管理机制通常被称为垃圾自动收集机制(automatic garbage collection),或者简称为垃圾收集机制(garbage collection)。当然,相应的这种内存管理器就被称为垃圾收集器(garbage collector)。目前有很多适用于C++的垃圾收集器之商业产品或免费实现可供使用,但是垃圾收集器并不是一个典型C++实现的标准部件。
3.3 编译、连接和执行(Compile, Link, and Execute)
传统的C或C++程序由若干源文件组成,它们会被分别编译成为目标文件。这些目标文件最后再被连接到一起,生成程序的可执行文件形式。
每一个被单独编译的程序段(program fragment)必须包含足够的信息,使其能与其它的程序段正确的连接在一起。编译器在编译独立的程序段(翻译单元translation unit)时,会检查大部分语言规则方面的问题。连接器(linker)进行的检查是为了保证:不同编译单元中各个名称的完整性;每一个名称与被正确定义的东西相关联。典型的C++运行期环境几乎不对执行的代码做任何检查。如果需要在运行期进行某些检查,程序员必须自己在源文件中提供相关的代码。
C++解释器和动态连接器也只能轻微的缓解这种“无法进行自动的运行期检查”之情况,即将某些检查推迟到第一次使用相关代码段的时候再进行。
例如,我们可以写一个简单的求阶乘的程序,将其划为一个独立的源文件fact.c:
//fact.c文件
#include “fact.h”
long fact(long f) //利用递归计算阶乘
{
if (f > 1)
return f*fact(f-1);
else
return 1;
}
每一个被单独编译的程序段都有一个接口,这个接口包含了使用这个程序段所需的尽量少但足够用的信息。对于fact.c这个程序段而言,这个接口包含了fact()函数的声明,声明被放在fact.h文件中:
//fact.h文件
long fact(long);
每一个需要使用这个程序段的翻译单元都用#include语句将这个接口包含进来。此外,我还更倾向于在翻译单元中用#include包含另一个接口,使得编译器能够更早的对不完整性进行诊断。
我们可以像这样使用fact()函数:
//main.c文件
#include “fact.h”
#include <iostream>
int main()
{
std::cout << “factorial(7) is “ << fact(7) << ‘\n’;
return 0;
}
main()函数是程序的起始点;iostream属于标准C++输入/输出程序库;std::cout是标准的字符输出流(§6.1)。运算符<<(“放入”的意思)将数值转换成字符串并将字符串输出。执行这个程序可以使
factorial(7) is 5040
这样的结果出现在输出端,并且后跟一个换行符(换行符用’\n’表示)。
这个例子中各程序段的关系可以用下图表示:
3.4 型别检查(Type checking)
C++提供并依赖于静态型别检查机制。这即是说,编译器会在程序开始执行之前进行大部分语言规则方面的检查工作。程序中的每一个实体(entity)都具有一种型别,并且必须以符合此种型别特征的方式被使用。例如:
int f(double); // f()函数接受一个双精度浮点数作为引数(argument)
// 并返回一个整型值
float x = 2.0; // x是一个单精度浮点数
string s = “2”; // s是一个字符串
int i = f(x); // I是一个整型数
编译器会从中发现不完整的用法,保证语言本身定义的或用户自定义的转换操作被正常实施。例如:
void g()
{
s = “a string literal”; // OK:将string literals转换成string
s = 7; // 错误:无法将int转换成string
x = “a string literal”; // 错误:无法将string literals转换成float
x = 7.0; // OK
x = 7; // OK:将int转换成float
f(x); // OK:将float转换成double
f(i); // OK:将int转换成double
f(s); // 错误:无法将string转换成double
double d = f + i; // OK:将int型对象与float型对象相加
string s2 = s + i; // 错误:无法将int型对象与string对象相加
}
对于用户自定义型别,用户具有很大的自主性,可以自己决定应该定义哪些操作和转换过程(§6.1)。因此,编译器可以完全像检测内建型别那样去检测用户自定义型别的完整性。
|