默认使用向量。 --Alex Stepanov
C++标准库中最有用的是向量。一个向量提供一系列指定类型的元素。你可以通过它的索引(下标)找到一个元素,使用push_back()来扩展向量,使用size()来获得一个向量中的元素数量,以及防止对超出范围的向量元素的访问。标准库向量是一个方便的、灵活的、有效的(在时间和空间上)、静态的、类型安全的元素容器。
向量的基本知识
我们开始对向量的循序渐进的设计,考虑一个非常简单的用途。
vector<double> age(4); age[0] = 0.23; age[1] = 22.0; age[2] = 27.2; age[3] = 54.2;
很明显,我们创建一个有四个double类型元素的向量,并且给这四个元素分别赋值为0.33,22.0,27.2和54.2。将这四个元素编号为0、1、2和3。对于C++标准库容器中的元素编号只能从0开始。从0开始编号是很常用的,它是C++程序员之间的普遍约定。一个向量中的元素数量称为它的大小。因此,age的大小为4。一个向量中的元素被编号(索引)为0到size-1。例如,age中的元素被编号为0到age.size()-1。鉴于此,我们可以定义自己的第一个版本的vector类:
class vector { int sz; double * elem; public: vector(int s); int size() const {return sz;}; }
运算符sizeof
那么,一个int实际占用多少内存?一个指针?操作符sizeof会回答这些问题:
cout << "the size of char is " << sizeof(char) << ' ' << sizeof(‘a') << "\n"; cout << "the size of int is " << sizeof(int) << ' ' << size(2+2) << '\n'; int * p = 0; cout << "the size of int * is " << sizeof(int *) << ' ' << sizeof(p) << '\n';
正如你看到的,我们可以将sizeof用于一个类型名或表达式。对于一个类型名来说,sizeof给出这种类型对象大小;对于一个表达式来说,sizeof给出表达式结果的大小。sizeof的结果是一个正整数,sizeof(char)单元的大小被定义为1。在情况下,一个char被保存在一个字节中,因此sizeof会报告占用的字节数。每个C++实现并不保证一种类型的大小相同。
自由空间分配
我们要求使用new操作符从空闲空间中分配内存:
(1)new操作符返回一个指向被分配内存的指针。
(2)一个指针指向一个特定类型的对象。
(3)一个指针并不知道它指向多少个元素。
(4)new操作符可以为单个元素或一系列(数组)元素分配内存。
new操作符可以为单个元素或一系列(数组)元素分配内存。分配的对象数量可能是变化的。由于允许我们在运行时选择分配多少个对象,因此它是很重要的。
这是“实际的解释”。理论上的解释是“允许为指针分配不同类型将导致类型错误。
我们永远要保证使用对象之前为它赋一个值,也就是说,我们希望确认指针被初始化,并且指针指向的对象被初始化。思考下面的代码:
double * p0; double * p1 = new double; double * p2 = new double(5.5); double * p3 = new double[5];
空指针
如果你没有其他指针用于初始化一个指针,那么使用0:
double * p0 = 0;
当0被赋给一个指针时,0被称为空指针。我们经常通过检测指针是否是0,以判断一个指针是否有效(如它是否指向什么东西)。
自由空间释放
new操作符会从自由空间中分配内存。由于一台计算机的内存是有限的,因此在使用结束后将内存释放回自由空间通常是个好主意。这样自由空间可以将这些内存重新用于新的分配。对于大型的程序和长时间运行的程序来说,这种自由空间的重新使用是很重要的。例如:
double * calc(int res_size, int max) { double * p = new double[max]; double * res = new double[res_size]; return res; }
double * r = calc(100,1000);
在写操作时,每次调用calc()会造成分配给p的double数组“泄漏”。将内存返回自由空间的操作符称为delete。我们对于一个指针使用delete返回new分配的内存,以使这些内存可以用于未来的分配。现在,这个例子变为:
double * calc(int res_size, int max) { double * p = new double[max]; double * res = new double[res_size]; delete [] p; return res; } double * r = calc(100,1000); delete[] r;
顺便说一句,这个例子证明使用自由内存的一个主要原因:我们可以在一个函数中创建对象,并将它们传送给函数的调用者。这里有两种形式的delete:
(1)delete p 释放由new分配给单个对象的内存。
(2)delete[] p释放由new分配给数组对象的内存。
对于程序员来说,使用正确的方式是一件乏味的工作。删除一个对象两次是一个糟糕的错误。
int * p = new int(5); delete p; delete p;
第二个delete p带来两个问题:
(1)因此自由空间管理器可能会改变它的内部数据结构,导致无法再次正确地执行delete p,你已不再拥有指针所指向的对象。
(2)自由空间管理可能已“回收”p指向的内存,因此p现在可能指向其他对象;删除其他对象(由程序的其他部分所拥有)将会在你的程序中引起错误。
这两个问题都发生在实际的程序中;它们不只是在理论上有可能性。
删除空指针不会做任何事(因为空指针不指向一个对象),因此删除空指针是无害的。
int * p = 0;
delete p;
delete p;
为什么我们都会被自由内存所困扰?编译器不能指出我们什么时候不需要一段内存,并在没有人工干预的情况下将它回收吗?它可以。这个过程称为自动垃圾收集或垃圾收集。不幸的是,自动垃圾收集并不是免费的,并且不是对所有应用都是理想的。如果你实际需要自动垃圾收集,你可以将一个自动垃圾收集器插入你的C++程序。好的垃圾收集器是有效的。
在什么时候不泄漏内存是重要的?一个“永远”运行的程序不能承受泄漏内存。一个不能有内存泄漏的操作系统是“永远运行的”程序的例子,大多数的嵌入式系统也是这样。很多程序使用库作为系统的一部分,因此库也不能出现内存泄漏的问题。一般来说,所有程序都不产生内存泄漏是个好主意。很多程序将泄漏的原因归结于马虎,但是,这并没有切中要点。当你一种操作系统上运行程序,在程序结束时会将所有内存自动返回给系统。这会带来一个问题,如果你知道你的程序不会使用比可用更多的内存,你可能“合理的”决定泄漏内存直一以操作系统为你释放内存。但是,你如果你决定要这样做,确定你所估计的内存消耗是正确的,否则人们将有好的理由认为你是草率的。
析构函数
在一个对象的类创建时会隐式调用构造函数,当一个对象离开作用域时会隐式调用析构函数。构造函数用于确认一个对象是否被正确创建和初始化。与之相反,析构函数用于确认一个对象是否被正确销毁。
class vector { int sz; double * elem; public: vector(int s):sz(s),elem(new, double[s]) { for(int i = 0; i < s; ++i) elem[i] = 0; } ~vector(){delete [] elem;} }
有了这个定义,我们就可以这样使用vector了:
void f3(int n) { int * p = new int [n]; vector v(n); delete [] p; }
delete[]看起来相当繁琐并且容易出错。对于vector,我们不必使用new分配内存,以及在函数结束时使用delete[]释放内存。vector已经做了这些工作,而且做得更好。特别是,vector不能忘记调作它的析构函数释放元素使用的内存。处理资源来说很重要,这些资源需要使用前申请和使用后释放缓冲区空间等。这些工作由它们的析构函数完成,每个“拥有”资源的类都需要一个析构函数。如果一个类的成员拥有一个析构函数,则在包含这个成员的对象销毁时调用这个析构函数。
struct Customer{ string name; vector<string> addresses; // ... }; void some_fct() { Customer fred; }
成员和基类的析构函数在从派生类的析构函数(无论用户定义或编译器生成)中被隐式调用。基本上,所有的规则可以被总结为:“当对象被销毁时,析构函数被调用”(当离开作用域时,当delete被调用时等)。
析构函数从概念上来说是简单的,但它是大多数有效的C++编译技术的基础。它的基本思想是简单的:
(1)无论一个类对象需要使用哪种资源,这种资源都要在构造函数中获得。
(2)在对象的生命期中,它可以释放资源和获得新的资源。
(3)在对象的生命期结束后,析构函数释放对象拥有的所有资源。
作为一个经验法则:如果你有一个带有虚函数功能的类,则它需要一个虚的析构函数。具体原因是:
1)如果一个类有虚函数功能,它经常作为一个基类使用。
2)如果它是一个基类,它的派生类经常使用new来分配。
3)如果一个派生类对象使用new来分配,并且通过一个指向它的基类的指针来控制,那么它经常通过一个指向它的基类的指针来删除它。
注意,析构函数是通过delete来隐式或间接调用。它们并不是直接调用。这样会省去很多麻烦的工作。
访问向量元素
为了使vector可以使用,我们需要一种读和写元素的方法。对于初学者来说,我们可以提供简单的get()和set()成员函数:
class vector { private: int sz; double * elem; public: vector(int s):sz(s),elem(new double[s]){} ~vector() {delete [] elem;} int size() const{return sz;} double get(int n){return elem[n];} void set(int n, double v) {elem[n] = v;} };
get()和set()都可以访问元素,在 elem指针上使用[]操作符:elem[n]。
现在,我们可以生成一个double型的vector并使用它:
vector v(5); for(int i = 0; i < v.size(); ++i) { v.set(i, 1.1*i); cout << "v[" << i << "]==" << v.get(i) << '\n'; }
这里将会输出:
v[0] == 0 v[1] == 1.1 v[2] == 2.2 v[3] == 3.3 v[4] == 4.4
这仍是一个相当简单的vector,相对于常用的下标符号来说,使用get()和set()的代码是很难看的。但是,我们的目的是从小的和简单的程序开始,沿着这个方式逐步测试和扩充我们的程序。这种发展和反复测试的策略可以减少错误和调试过程。
vector *f(int s) { vector * p = new vector(s); return p;} void ff() {vector * q = f(4); delete p;}
注意,当我们删除一个vector时,它的析构函数会被调用。
vector * p = new vector(s); delete p;
在自由空间中创建vector, new操作符:
首先为一个vector分配内存。
然后,激活vector的构造函数初始化vector;构造函数为vector的元素分配内存,并初始化这些元素。
删除vector, delete操作符:
首先激活vector的析构函数;这个构构函数激活这些元素的析构函数(如果它们有析构函数),然后释放这些元素使用的内存。
然后,释放vector使用的内存。
类型混用:无类型指针和指针类型转换
在使用指针自由空间分配的数组时,我们非常接近硬件层面。基本上,我们对指针的操作(初始化、分配、*和[])直接映射为机器指令。在这个层次,语言只提供一点描述上的便利,以及由类型系统提供的编译时的一致性。偶尔,我们不得放弃这最后一点保护。
通常,我们不希望在没有类型系统的保护下工作,但是有时没有其他选择。我们有时也会遇到一些情况,我们需要面对没有根据安全类型设计的老的代码。在这种情况下,我们需要两样东西:
1)一种指向不知道自己保存的何种对象的内存的指针。
2)一种操作,对于这类指针,它告诉编译器指针指向哪种(未证实)的操作类型。
类型void *的含义是“指向编译器不知道类型的那些内存”。当我们想在两段代码之间传输一个地址,并且不知道每段代码实际的类型时,就可以使用void *。
指针和引用
我们可以将一个引用看做是一个自动解引用的、不可改变的指针或是一个对象的别名。指针和引用在以下几个方面不同:
(1)为一个指针赋值为改变指针的值(不是指针指向的值)。
(2)为了得到一个指针,你通常需要使用new或&。
(3)为了访问一个指针指向的对象,你可以使用*或[]。
(4)为一个引用赋值会改变引用指向的值(不是引用自身的值)。
(5)在初始化一个引用之后,你不能让引用指向其他对象。
(6)为引用赋值执行深度复制(赋值给引用的对象);为指针赋值不是这样(赋值给指针自身)。
(7)注意空指针。
引用和指针都是通过使用内存地址来实现的。它们只是在地址的使用上不同,为编程人员提供稍有不同的功能。
我们如何选择使用引用参数还是指针参数呢?不幸的是,每种方法都有自己的优点和缺点,因此在这方面仍没有明确的答案。你需要根据程序和可能的用途来决定。因此,实际的答案是“选择依赖于函数的性质”:
(1)对于小的对象,倾向于传递值。
(2)对于“没有对象”(用0表示)是有效参数的函数,使用一个指针参数(记着对0进行测试)。
(3)否则,使用一个引用参数。
C++提供了相应的机制:在每个成员函数中,标识符this就是指向用户调用成员函数所用对象的指针。