指针和数组基本等价的原因在于指针算术(pointer arithmetic)和C++内部处理数组的方式。首先,我们来看一看算术。将整数变量加1后,其值将增加1;但将指针变量加1后,增加的量等于它指向的类型的字节数。将指向double的指针加1后,如果系统对double使用8个字节存储,则数值将增加8;将指向short的指针加1后,如果系统对short使用2个字节存储,则指针值将增加2。程序清单4.19演示了这种令人吃惊的现象,它还说明了另一点:C++将数组名解释为地址。
程序清单4.19 addpntrs.cpp
//addpntrs.cpp -- pointer addition
#include
int main()
{
using namespace std;
double wages[3] = { 10000.0, 20000.0, 30000.0 };
short stacks[3] = { 3,2,1 };
//Here are two ways to get the address of an array
double* pw = wages; //name of an array = address
short* ps = &stacks[0]; //or use address operator
//with array element
cout << "pw = " << pw << ", *pw = " << *pw << endl;
pw = pw + 1;
cout << "add 1 to the pw pointer:\n";
cout << "pw = " << pw << ", *pw = " << *pw << endl << endl;
cout << "ps = " << ps << ", *ps = " << *ps << endl;
ps = ps + 1;
cout << "add 1 to the ps pointer:\n";
cout << "ps = " << ps << ", *ps = " << *ps << endl << endl;
cout << "access two elements with array notation\n";
cout << "stacks[0] = " << stacks[0]
<< ", stacks[1] = " << stacks[1] << endl;
cout << "access two elements with pointer notation\n";
cout << "*stacks = " << *stacks
<< ", *(stacks + 1) = " << *(stacks + 1) << endl;
cout << sizeof(wages) << " = size of wages array\n";
cout << sizeof(pw) << " = size of pw pointer\n";
return 0;
}
下面是该程序的输出:
pw = 008FFBE4, *pw = 10000
add 1 to the pw pointer:
pw = 008FFBEC, *pw = 20000
ps = 008FFBD4, *ps = 3
add 1 to the ps pointer:
ps = 008FFBD6, *ps = 2
access two elements with array notation
stacks[0] = 3, stacks[1] = 2
access two elements with pointer notation
*stacks = 3, *(stacks + 1) = 2
24 = size of wages array
4 = size of pw pointer
在多数情况下,C++将数组名解释为数组第1个元素的地址。因此,下面的语句将pw声明为指向double类型的指针,然后将它初始化为wages——wages数组中第1个元素的地址:
double* pw = wages;
和所有数组一样,wage也存在下面的等式:
wages = &wages[0] = address of first element of array
为表明情况确实如此,该程序在表达式&stacks[0]中显式地使用地址运算符来将ps指针初始化为stacks数组的第1个元素。
接下来,程序查看pw和*pw的值。前者是地址。后者是存储在该地址中的值。由于pw指向第1个元素,因此*pw显示的值为第1个元素的值,即10000。接着,程序将pw加1。正如前面指出的,这样数字地址值将增加8,这使得pw的值为第2个元素的地址。因此,*pw现在的值是20000——第2个元素的值(参见下图,为使该图更为清晰,对其中的地址值做了调整)。
此后,程序对ps执行相同的操作。这一次由于ps指向的是short类型,而short占用2个字节,因此将指针加1时,其值将增加2。结果是,指针也指向数组中下一个元素。
注意:
将指针变量加1后,其增加的值等于指向的类型占用的字节数。
现在来看一看数组表达式stack[1]。C++编译器将该表达式看作是*(stacks + 1),这意味着先计算数组第2个元素的地址,然后找到存储在那里的值。最后的结果便是stacks[1]的含义(运算符优先级要求使用括号,如果不使用括号,将给*stacks加1,而不是给stacks加1)。
从该程序的输出可知,*(stacks + 1)和stacks[1]是等价的。同样,*(stacks + 2)和stacks[2]也是等价的。通常,使用数组表示法时,C++都执行下面的转换:
arrayname[i] becomes *(arrayname + i)
如果使用的是指针,而不是数组名,则C++也将执行同样的转换:
pointername[i] becomes *(pointername + i)
因此,在很多情况下,可以相同的方式使用指针名和数组名。对于它们,可以使用数组方括号表示法,也可以使用解除引用运算符(*)。在多数表达式中,它们都表示地址。区别之一是,可以修改指针的值,而数组名是常量:
pointername = pointername + 1; //valid
arrayname = arrayname + 1; //not allowed
另一个区别是,对数组应用sizeof运算符得到的是数组的长度,而对指针引用sizeof得到的是指针的长度,即使指针指向的是一个数组。例如,在程序清单4.19中,pw和wages指向的是同一个数组,但对它们应用sizeof运算符得到的结果如下:
24 = size of wages array << displaying sizeof wages
4 = size of pw pointer << displaying sizeof pw
这种情况下,C++不会将数组名解释为地址。
数组的地址
对数组取地址时,数组名也不会被解释为其地址。等等,数组名难道不被解释为数组的地址吗?不完全如此:数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址:short tell[10]; //tell an array of 20 bytes cout << tell << endl; //displays &tell[0] cout << &tell << endl; //displays address of whole array
从数字上说,这两个地址相同;但从概念上说,&tell[0]是一个2字节内存块的地址,而&tell是一个20字节内存块的地址。因此,表达式是tell+1将地址值加2,而表达式&tell+1将地址值加20。换句话说,tell是一个short指针(*short),而&tell是一个这样的指针,即指向包含20个元素的short数组(short(*)[20])。
您可能会问,前面有关&tell的类型描述是如何来的呢?首先,您可以这样声明和初始化这种指针:short(*pas)[20] = &tell; //pas points to array of 20 shorts
如果省略括号,优先级规则将使得pas先于[20]结合,导致pas是一个short指针数组,它包含20个元素。因此括号是必不可少的。其次,如果要描述变量的类型,可将声明中的变量名删除。因此,pas的类型为short(*)[20]。另外,由于pas被设置为&tell,因此*pas与tell等价,所以(*pas)[0]为tell数组的第一个元素。
总之,使用tell来创建数组以及使用指针来访问不同的元素很简单。只要把指针当作数组名对待即可。然而,要理解为何可以这样做,将是一种挑战。要想真正了解数组和指针,应认真复习它们的相互关系。
刚才已经介绍了大量指针的知识,下面对指针和数组做一总结。
要声明指向特定类型的指针,请使用下面的格式:
typeName* pointerName;
下面是一些示例:
double* pn; //pn can point to a double value
char* pc; //pc can point to a char value
其中,pn和pc都是指针,而double*和char*是指向double的指针和指向char的指针。
应将内存地址赋给指针。可以对变量名应用&运算符,来获得被命名的内存的地址,new运算符返回未命名的内存的地址。
下面是一些示例:
double* pn; //pn can point to a double value
double* pa; //so can pa
char* pc; //pc can point to a char value
double bubble = 3.2;
pn = &bubble; //assign address of bubble to pn
pc = new char; //assign address of newly allocated char memory to pc
pa = new double[30]; //assign address of 1st element of array of 30 double to pa
对指针解除引用意味着获得指针指向的值。对指针应用解除引用或间接值运算符(*)来解除引用。因此,如果像上面的例子中那样,pn是指向bubble的指针,则*pn是指向的值,即3.2。
下面是一些示例:
cout << *pn; //print the value of bubble
*ps = ‘S’; //place ‘S’ into the memory location whose address is pc
另一种对指针解除引用的方法是使用数组表示法,例如,pn[0]与*pn是一样的。决不要对未被初始化为适当地址的指针解除引用。
如果pt是指向int的指针,则*pt不是指向int的指针,而是完全等同于一个int类型的变量。pt才是指针。
下面是一些示例。
int* pt = new int; //assign an address to the pointer pt
*pt = 5; //stores the value 5 at that address
在多数情况下,C++将数组名视为数组的第一个元素的地址。
下面是一个示例:
int tacos[10]; //now tacos is the same as &tacos[0]
一种例外情况是,将sizeof运算符用于数组名引用时,此时将返回整个数组的长度(单位为字节)。
C++允许将指针和整数相加。加1的结果等于原来的地址值加上指向的对象占用的总字节数。还可以将一个指针减去另一个指针,获得两个指针的差。后一种运算将得到一个整数,仅当两个指针指向同一个数组(也可以指向超出结尾的一个位置)时,这种运算才有意义;这将得到两个元素的间隔。
下面是一些示例:
int tacos[10] = { 5,2,8,4,1,2,2,4,6,8 };
int* pt = tacos; //suppose pt and tacos are the address 3000
pt = pt + 1; //now pt is 3004 if an int is 4 byte
int* pe = &tacos[9]; //pe is 3036 if an int is 4 byte
pe = pe - 1; //now pe is 3032, the address of tacos[8]
int diff = pe - pt; //diff is 7, the separation between tacos[8] and tacos[1]
使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置:
int tacos[10]; //static bingding, size fixed at compile time
使用new[]运算符创建数组时,将采用动态联编(动态数组),即将在运行时为数组分配空间,其长度也将在运行时设置。使用完这种数组后,应采用delete[]释放其占用的内存:
int size;
cin >> size;
int* pz = new int [size]; //dynamic binding, size set at run time
...
delete [] pz; //free memory when finished
使用方括号数组表示法等同于对指针解除引用:
tacos[0] means *tacos means the value at address tacos
tacos[3] means *(tacos + 3) means the value at address tacos + 3
数组名和指针变量都是如此,因此对于指针和数组名,既可以使用指针表示法,也可以使用数组表示法。
下面是一些示例:
int* pt = new int[10]; //pt points to block of 10 ints
*pt = 5; //set element number 0 to 5
pt[0] = 6; //reset element number 0 to 6
pt[9] = 44; //set tenth element (element number 9) to 44
int coats[10];
*(coats + 4) = 12; //set coats[4] to 12
数组和指针的特殊关系可以扩展到C-风格字符串。请看下面的代码:
char flower[10] = “rose”;
cout << flower << “s are red\n”;
数组名是第一个元素的地址,因此cout语句中的flower是包含字符r的char元素的地址。cout对象认为char的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0)为止。总之,如果给cout提供一个字符的地址,则它将从该字符开始打印,直到遇到空字符为止。
这里的关键不在于flower是数组名,而在于flower是一个char的地址。这意味着可以将指向char的指针变量作为cout的参数,因为它也是char的地址。当然,该指针指向字符串的开头,稍后将核实这一点。
前面的cout语句中最后一部分的情况如何呢?如果flowe是字符串第一个字符的地址,则表达式 “s are red\n” 是什么呢?为了与cout对字符串输出的处理保持一致,这个用引号括起的字符串也应当是一个地址。在C++中,用引号括起的字符串像数组名一样,也是第一个元素的地址。上述代码不会将整个字符串发送给cout,而只是发送该字符串的地址。这意味着对于数组中的字符串、用引号括起的字符串常量以及指针所描述的字符串,处理的方式是一样的,都将传递它们的地址。与逐个传递字符串中的所有字符相比,这样做的工作量确实要少。
注意:
在cout和多数C++表达式中,char数组名、char指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。
程序清单4.20演示了如何使用不同形式的字符串。它使用了两个字符串库中的函数。函数strlen()我们以前用过,它返回字符串的长度。函数strcpy()将字符串从一个位置复制到另一个位置。这两个函数的原型都位于头文件cstring(在不太新的实现中,为string.h)中。该程序还通过注释指出了应尽量避免的错误使用指针的方式。
程序清单4.20 ptrstr.cpp
//ptrstr.cpp -- using pointers to strings
#include
#include //declare strlen(), strcpy()
int main()
{
using namespace std;
char animal[20] = "bear"; //animal holds bear
const char* bird = "wren"; //bird holds address of string
char* ps; //uninitialized
cout << animal << " and "; //display bear
cout << bird << "\n"; //display wren
//cout << ps << "\n"; //may display garbage, may cause a crash
cout << "Enter a kind of animal:";
cin >> animal; // ok if input <20 chars
//cin >> ps; Too horrible a blunder to try; ps doesn't point to allocated space
ps = animal; //set ps to point to string
cout << ps << "!\n"; //ok, same as using animal
cout << "Before using strcpy():\n";
cout << animal << " at " << (int*)animal << endl;
cout << ps << " at " << (int*)ps << endl;
ps = new char[strlen(animal) + 1]; //get new storage
strcpy(ps, animal); //copy string to new storage
cout << "After using strcpy():\n";
cout << animal << " at " << (int*)animal << endl;
cout << ps << " at " << (int*)ps << endl;
delete[] ps;
return 0;
}
下面是该程序的运行情况:
bear and wren
Enter a kind of animal:fox
fox!
Before using strcpy():
fox at 010FFA20
fox at 010FFA20
After using strcpy():
fox at 010FFA20
fox at 012127F0
程序说明
程序清单4.20中的程序创建了一个char数组(animal)和两个指向char的指针变量(bird和ps)。该程序首先将animal数组初始化为字符串“bear”,就像初始化数组一样。然后,程序执行了一些新的操作,将char指针初始化为指向一个字符串:
const char* bird = “wren”; //bird holds address of string
记住, “wren”实际表示的是字符串的地址,因此这条语句将“wren”的地址赋给了bird指针。(一般来说,编译器在内存留出一些空间,以存储程序源代码中所有用引号括起的字符串,并将每个被存储的字符串与其地址关联起来。)这意味着可以像使用字符串“wren”那样使用指针bird,如下面的示例所示:
cout << “A concerned “ << bird << “ speaks\n”;
字符串字面值是常量,这就是为什么代码在声明中使用关键字const的原因。以这种方式使用const意味着可以用bird来访问字符串,但不能修改它。第7章将详细介绍const指针。最后,指针ps未被初始化,因此不指向任何字符串(正如您知道的,这通常是个坏主意,这里也不例外)。
接下来,程序说明了这一点,即对于cout来说,使用数组名animal和指针bird是一样的。毕竟,它们都是字符串的地址,cout将显示存储在这两个地址上的两个字符串(“bear”和“wren”)。如果激活错误显示ps的代码,则将可能显示一个空行、一堆乱码,或者程序将崩溃。创建未初始化的指针有点像签发空头支票:无法控制它将被如何使用。
对于输入,情况有点不同。只有输入比较短,能够被存储在数组中,则使用数组animal进行输入将是安全的。然而,使用bird来进行输入并不合适:
下面讨论一下第二点。C++不能保证字符串字面值被唯一地存储,也就是说,如果在程序中多次使用了字符串字面值“wren”,则编译器将可能存储该字符串的多个副本,也可能只存储一个副本。如果是后面一种情况,则将bird设置为指向一个“wren”,将使它只是指向该字符串的唯一一个副本。将值读入一个字符串可能会影响被认为是独立的、位于其他地方的字符串。无论如何,由于bird指针被声明为const,因此编译器将禁止改变bird指向的位置中的内容。
试图将信息读入ps指向的位置将更糟。由于ps没有被初始化,因此并不知道信息将被存储在哪里,这甚至可能改写内存中的信息。幸运的是,要避免这种问题很容易——只要使用足够大的char数组来接收输入即可。请不要使用字符串常量或未被初始化的指针来接收输入。为避免这些问题,也可以使用std::string对象,而不是数组。
警告:
在将字符串读入程序时,应使用已分配的内存地址。该地址可以是数组名,也可以是使用new初始化过的指针。
接下来,请注意下述代码完成的工作:
ps = animal; //set ps to point to string
...
cout << animal << " at " << (int*)animal << endl;
cout << ps << " at " << (int*)ps << endl;
它将生成下面的输出:
fox at 010FFA20
fox at 010FFA20
一般来说,如果给cout提供一个指针,它将打印地址。但如果指针的类型为char*,则cout将显示指向的字符串。如果要显示的是字符串的地址,则必须将这种指针强制转换为另一种指针类型,如int*(上面的代码就是这样做的)。因此,ps显示为字符串“fox”,而(int*)ps显示为该字符串的地址。注意,将animal赋给ps并不会复制字符串,而只是复制地址。这样,这两个指针将指向相同的内存单元和字符串。
要获得字符串和副本,还需要做其他工作。首先,需要分配内存来存储该字符串,这可以通过声明另一个数组或使用new来完成。后一种方法使得能够根据字符串的长度来指定所需的空间:
ps = new char[strlen(animal) + 1]; //get new storage
字符串“fox”不能填满整个animal数组,因此这样做浪费了空间。上述代码使用strlen()来确定字符串的长度,并将它加1来获得包含空字符时该字符串的长度。随后,程序使用new来分配刚好足够存储该字符串的空间。
接下来,需要将animal数组中的字符串复制到新分配的空间中。将animal赋给ps是不可行的,因为这样只能修改存储在ps中的地址,从而失去程序访问新分配内存的唯一途径。需要使用库函数strcpy():
strcpy(ps, animal); //copy string to new storage
strcpy()函数接受2个参数。第一个是目标地址,第二个是要复制的字符串地址。您应确定,分配了目标空间,并有足够的空间来存储副本。在这里,我们用strlen()来确定所需的空间,并使用new获得可用的内存。
通过使用strcpy()和new,将获得“fox”的两个独立副本:
fox at 010FFA20
fox at 012127F0
另外,new在离animal数组很远的地方找到了所需的内存空间。
经常需要将字符串放到数组中。初始化数组时,请使用=运算符;否则应使用strcpy()或strncpy()。strcpy()在前面已经介绍过,其工作原理如下:
char food[20] = “carrots”; //initialization
strcpy(food, “flan”); //otherwise
注意,类似下面这样的代码可能导致问题,因为food数组比字符串小:
strcpy(food, “a picnic basket filled with many goodies”);
在这种情况下,函数将字符串中剩余的部分复制到数组后面的内存字节中,这可能会覆盖程序正在使用的其他内存。要避免这种问题,请使用strncpy()。该函数还接受第3个参数——要复制的最大字符数。然而,要注意的是,如果该函数在到达字符串结尾之前,目标内存已经用完,则它将不会添加空字符。因此,应该这样使用该函数:
strncpy(food, “a picnic basket filled with many goodies”, 19);
food[19] = ‘\0’;
这样最多将19个字符复制到数组中,然后将最后一个元素设置成空字符。如果该字符串少于19个字符,则strncpy()将在复制完该字符串之后加上空字符,以标记该字符串的结尾。
警告:
应使用strcpy()或strncpy(),而不是赋值运算符来将字符串赋给数组。
您对使用C-风格字符串和cstring库的一些方面有了了解后,便可以理解为何使用C++ string类型更为简单了:您不用担心字符串会导致数组越界,并可以使用赋值运算符而不是函数strcpy()和strncpy()。
在运行时创建数组优于在编译时创建数组,对于结构也是如此。需要在程序运行时为结构分配所需的空间,这也可以使用new运算符来完成。通过使用new,可以创建动态结构。同样,“动态”意味着内存是在运行时,而不是编译时分配的。由于类与结构非常相似,因此本节介绍的有关结构的技术也适用于类。
将new用于结构由两步组成:创建结构和访问其成员。要创建结构,需要同时使用结构类型和new。例如,要创建一个未命名的inflatable类型,并将其地址赋给一个指针,可以这样做:
inflatable* ps = new inflatable;
这将把足以存储inflatable结构的一块可用内存的地址赋给ps。这种句法和C++的内置类型完全相同。
比较棘手的一步是访问成员。创建动态结构时,不能将成员运算符句点用于结构名,因为这种结构没有名称,只是知道它的地址。C++专门为这种情况提供了一个运算符:箭头成员运算符(->)。该运算符由连字符和大于号组成,可用于指向结构的指针,就像点运算符可用于结构名一样。例如,如果ps指向一个inflatable结构,则ps->price是被指向的结构的price成员(参见下图)。
提示:
有时,C++新手在指定结构成员时,搞不清楚何时应使用句点运算符,何时应使用箭头运算符。规则非常简单。如果结构标识符是结构名,则使用句点运算符;如果标识符是指向结构的指针,则使用箭头运算符。
另一种访问结构成员的方法是,如果ps是指向结构的指针,则*ps就是被指向的值——结构本身。由于*ps是一个结构,因此(*ps).price是该结构的price成员。C++的运算符优先规则要求使用括号。
程序清单4.21使用new创建一个未命名的结构,并演示了两种访问结构成员的指针表示法。
程序清单4.21 newstrct.cpp
//newstrct.cpp -- using new with a structure
#include
struct inflatable //structure definition
{
char name[20];
float volume;
double price;
};
int main()
{
using namespace std;
inflatable* ps = new inflatable; //allot memory for structure
cout << "Enter name of inflatable item: ";
cin.get(ps->name, 20); //method 1 for member access
cout << "Enter volume in cubic feet: ";
cin >> (*ps).volume; //method 2 for member access
cout << "Enter price: $";
cin >> ps->price;
cout << "Name: " << (*ps).name << endl; //method 2
cout << "Volume: " << ps->volume << " cubic feet\n"; //method 1
cout << "Price: $" << ps->price << endl; //method 1
delete ps;
return 0;
}
下面是该程序的运行情况:
Enter name of inflatable item: Fabuloud Frodo
Enter volume in cubic feet: 1.4
Enter price: $27.99
Name: Fabuloud Frodo
Volume: 1.4 cubic feet
Price: $27.99
下面介绍一个使用new和delete来存储通过键盘输入的字符串的示例。程序清单4.22定义了一个函数getname(),该函数返回一个指向输入字符串的指针。该函数将输入读入到一个大型的临时数组中,然后使用new[]创建一个刚好能够存储该输入字符串的内存块,并返回一个指向该内存块的指针。对于读取大量字符串的程序,这种方法可以节省大量内存(实际编写程序时,使用string类将更容易,因为这样可以使用内置的new和delete)。
假设程序要读取100个字符串,其中最大的字符串包含79个字符,而大多数字符串都短得多。如果用char数组来存储这些字符串,则需要1000个数组,其中每个数组的长度为80个字符。这总共需要80000个字节,而其中的很多内存没有被使用。另一种方法是,创建一个数组,它包含1000个指向char的指针,然后使用new根据每个字符串的需要分配相应数量的内存。这将节省几万个字节。是根据输入来分配内存,而不是为每个字符串使用一个大型数组。另外,还可以使用new根据需要的指针数量来分配空间。就目前而言,这有点不切实际,即使是使用1000个指针的数组也是这样,不过程序清单4.22还是演示了一些技巧。另外,为演示delete是如何工作的,该程序还用它来释放内存以便能够重新使用。
程序清单4.22 delete.cpp
//delete.cpp -- using the delete operator
#include
#include //or string.h
using namespace std;
char* getname(void); //function prototype
int main()
{
char* name; //create pointer but no storage
name = getname(); //assign address of string to name
cout << name << " at " << (int*)name << "\n";
delete[] name; //memory freed
name = getname(); //reuse freed memory
cout << name << " at " << (int*)name << "\n";
delete[] name; //memory freed again
return 0;
}
char* getname() //return pointer to new string
{
char temp[80]; //temporary storage
cout << "Enter last name: ";
cin >> temp;
char* pn = new char[strlen(temp) + 1];
strcpy(pn, temp); //copy string into smaller space
return pn; //temp lost when function ends
}
下面是该程序的运行情况:
Enter last name: Fredeldumpkin
Fredeldumpkin at 0091DDC0
Enter last name: Pook
Pook at 0091F1C0
来看一下程序清单4.22中的函数getname()。它使用cin将输入的单词放到temp数组中,然后使用new分配新内存,以存储该单词。程序需要strlen(temp)+1个字符(包括空字符)来存储该字符串,因此将这个值提供给new。获得空间后,getname()使用标准库函数strcpy()将temp中的字符串复制到新的内存块中。该函数并不检查内存块是否能够容纳字符串,但getname()通过使用new请求合适的字节数来完成了这样的工作。最后,函数返回pn,这是字符串副本的地址。
在main()中,返回值(地址)被赋给指针name。该指针是在main()中定义的,但它指向getname()函数中分配的内存块。然后,程序打印该字符串及其地址。
接下来,在释放name指向的内存块,main()再次调用getname()。C++不保证新释放的内存就是下一次使用new时选择的内存,从程序运行结果可知,确实不是。
在这个例子中,getname()分配内存,而main()释放内存。将new和delete放在不同的函数中通常并不是个好办法,因为这样很容易忘记使用delete。不过这个例子确实把new和delete分开放置了,只是为了说明这样做也是可以的。
为了解该程序的一些更为微妙的方面,需要知道一些有关C++是如何处理内存的知识。下面介绍一些这样的知识,这些知识将在第9章做全面介绍。
根据用于分配内存的方法,C++有3种管理数据内存的方式:自由存储、静态存储和动态存储(有时也叫作自由存储空间或堆)。在存在时间的长短方面,以这3种方式分配的数据对象各不相同。下面简要地介绍每周类型(C++11新增了第四种类型——线程存储,这将在第9章简要地讨论)。
在函数内部定义的常规变量使用自动存储空间,被称为自动变量(automatic variable),这意味着它们在所属的函数被调用时自动产生,在该函数结束时消亡。例如,程序清单4.22中的temp数组仅当getname()函数活动时存在。当程序控制权回到main()时,temp使用的内存将自动被释放。如果getname()返回temp的地址,则main()中的name指针指向的内存将很快得到重新使用。这就是在getname()中使用new的原因之一。
实际上,自动变量是一个局部变量,其作用域为包含它的代码块。代码块是被包含在花括号中的一段代码。到目前为止,我们使用的所有代码都是整个函数。然而,在下一章将会看到,函数内也可以有代码块。如果在其中的某个代码定义了一个变量,则该变量仅在程序执行该代码块中的代码时存在。
自动变量通常存储在栈中。这意味着执行代码块时,其中的变量将依次加入到栈中,而在离开代码块时,将按相反的顺序释放这些变量,这被称为后进先出(LIFO)。因此,在程序执行过程中,栈将不断地增大和缩小。
静态存储是整个程序执行期间都存在的存储方式。使变量成为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字static:
static double fee = 56.50;
在K&R C中,只能初始化静态数组和静态结构,而C++ Release 2.0(及后续版本)和ANSI C中,也可以初始化自动数组和自动结构。然而,一些您可能已经发现,有些C++实现还不支持对自动数组和自动结构的初始化。
第9章将详细介绍静态存储。自动存储和静态存储的关键在于:这些方法严格地限制了变量的寿命。变量可能存在于程序的整个生命周期(静态变量),也可能只是在特定函数被执行时存在(自动变量)。
new和delete运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,这在C++中被称为自由存储空间(free store)或堆(heap)。该内存池同用于静态变量和自动变量的内存是分开的。程序清单4.22表明,new和delete让您能够在一个函数中分配内存,而在另一个函数中释放它。因此,数据的生命周期不完全受程序或函数的生存时间控制。与使用常规变量相比,使用new和delete让程序员对程序如何使用内存有更大的控制权。然而,内存管理也更复杂了。在栈中,自动添加和删除机制使得占用的内存总是连续的,但new和delete的相互影响可能导致占用的自由存储区不连续,这使得跟踪新分配内存的位置更苦难。
栈、堆和内存泄露
如果使用new运算符在自由存储空间(或堆)上创建变量后,没有调用delete,将发生什么情况呢?如果没有调用delete,则即使包含指针的内存由于作用域规则和对象生命周期的原因而被释放,在自由存储空间上动态分配的变量或结构也将继续存在。实际上,将会无法访问自由存储空间中的结构,因为指向这些内存的指针无效。这将导致内存泄漏。被泄漏的内存将在程序的整个生命周期内都不可使用;这些内存被分配出去,但无法收回。极端情况(不过不常见)是,内存泄漏可能会非常严重,以至于应用程序可用的内存被耗尽,出现内存耗尽错误,导致程序崩溃。另外,这种泄漏还会给一些操作系统或在相同的内存空间中运行的应用程序带来负面影响,导致它们崩溃。
即使是最好的程序员和软件公司,也可能导致内存泄漏。要避免内存泄漏,最好是养成这样一种习惯,即同时使用new和delete运算符,在自由存储空间上动态分配内存,随后便释放它。C++智能指针有助于自动完成这种任务,这将在第6章介绍。
注意:
指针是功能最强大的C++工具之一,但也最危险,因为它们允许执行对计算机不友好的操作,如使用未经初始化的指针来访问内存或者试图释放同一个内存块两次。另外,在通过实践习惯指针表示法和指针概念之前,指针是容易引起迷惑的。由于指针是C++编程的重要组成部分,本书后面将更详细地讨论它。本书多次对指针进行了讨论,就是希望您能够越来越熟悉它。