指针、数组和指针算术
指针和数组基本等价的原因在于指针算术(pointer arithmetic)和C++内部处理数组的方式。
将整数变量加1后,其值将增加1;但将指针变量加1后,增加的量等于它指向的类型的字节数。
将指向double的指针加1后,如果系统对double使用8个字节存储,则数值将增加8;将指向short的指针加1后,如果系统对short使用2个字节存储,则指针值将增加2。
程序4.19
#include<iostream>
int main()
{
using namespace std;
double wages[3] = { 10000.0,20000.0,30000.0 };
short stacks[3] = { 3,2,1 };
//这里有两种获取数组地址的方式
double * pw = wages; //数组名=地址
short * ps = &stacks[0]; //第一个元素的地址
cout << "pw = " << pw << ", *pw = " << *pw << endl;
pw = pw + 1;
cout << "add 1 to the pw pointer:\n";
cout << "pw = " << pw << ", *pw = " << *pw << "\n\n";
cout << "ps = " << ps << ", *ps = " << *ps << endl;
ps = ps + 1;
cout << "add 1 to the ps pointer:\n";
cout << "ps = " << ps << ", *ps = " << *ps << "\n\n";
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";
cin.get();
return 0;
}
1. 程序说明
在多数情况下,C++将数组名解释为数组第一个元素的地址。因此,下面的语句将pw声明为指向double类型的指针,然后将它初始化为wages——wages数组第一个元素的地址:
double * pw = wages;
和所有数组一样,wages也存在下面的等式:
wages = &wages[0] = 数组第一个元素的地址
从该程序输出可知,*(stacks+1)和stacks[1]等价,*(stacks+2)和stacks[2]等价。
通常,使用数组表示法时,C++都执行下面的转换:
arrayname[i] 转换为 *(arrayname + i)
如果使用的是指针,而不是数组名,C++也将执行同样的转换:
pointername[i] 转换为 *(pointername + i)
因此,在很多情况下,可以用相同的方式使用指针名和数组名,对于它们,可以使用数组方括号表示法,也可以使用接触引用运算符(*)。在多数表达式中,它们都表示地址。区别之一是,可以修改指针的值,而数组名是常量:
pointername = pointername + 1; //有效
arrayname = arrayname + 1; //不允许
另一个区别是,对数组使用sizeof运算符得到的是数组的长度,而对指针应用sizeof得到的是指针的长度,即使指针指向的是一个数组。这种情况下,C++不会将数组名解释为地址。
2. 指针小结
1.声明指针
声明指向特定类型的指针:
typeName * pointerName;
e.g.
double * pn;
char * pc;
2.给指针赋值
将内存地址赋给指针。可以对变量名应用&运算符,来获得被命名的内存的地址,new运算符返回未命名的内存的地址。
double * pn; //pn指向一个double值
double * pa; //pa指向一个double值
char * pc; //pc指向一个char值
double bubble = 3.2;
pn = &bubble; //将bubble的地址赋给pn
pc = new char; //使用new将一块char内存分配给pc
pa = new double[30]; //将一个有30个double元素的数组的第一个元素的地址赋给pa
3.对指针解除引用
对指针解除引用意味着获得指针指向的值。
cout << *pn; //打印bubble的值
*pc = 'S'; //将'S'放入pc指向的内存块中
另一种对指针解除引用的方法是使用数组表示法,例如,pn[0]与*pn是一样的。
不要对未初始化为适当地址的指针解除引用。
4.区分指针和指针所指向的值
如果pt是指向int的指针,则*pt不是指向int的指针,而是完全等同于一个int类型的变量。
int * pt = new int;
*pt = 5;
5.数组名
多数情况下,C++将数组名视为数组的第一个元素的地址。
一种例外情况是,对数组名使用sizeof时返回的是数组的长度(字节)。
6.指针算术
C++允许将指针和整数相加,加1的结果等于原来的地址值加上指向的对象占用的总字节数。
还可以将一个指针减去两一个指针,获得两个指针的差,此时将得到一个整数,仅当两个指针指向同一个数组时,这种运算才有意义;这将得到两个元素的间隔。
int tacos[10] = {5,2,8,4,1,2,2,4,6,8};
int * pt = tacos; //假设pt和tacos是地址3000
pt = pt + 1; //现在pt是3004(如果int是4bytes)
int *pe = &tacos[9]; //pe是3036(如果int是4bytes)
pe= pe - 1; //现在pe是3032,即tacos[8]的地址
int diff = pe - pt; //diff值为7,即tacos[8]和tacos[1]之间的间隔
7.数组的动态联编和静态联编
使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置:
int tacos[10]; //静态联编,在编译时设置数组长度
使用new[]运算符创建数组时,将采用动态联编(动态数组),即在运行时为数组分配空间,其长度也在运行时设置。使用完这种数组后,应使用delete[]释放其占用的内存:
int size;
cin >> size;
int * pz = new int [size]; //动态联编,在运行时设置长度
...
delete [] pz; //使用结束后释放内存
8.数组表示法和指针表示法
使用方括号数组表示法等同于对指针解除引用:
tacos[0] = *tacos = 地址tacos处的值
tacos[3] = *(tacos + 3) = 地址(tacos+3)处的值
数组名和指针变量都是如此,因此对于指针和数组名,既可以使用指针表示法,也可以使用数组表示法。
int * pt = new int [10]; //pt指向10个int的内存块
*pt = 5; //设置pt[0]的值为5
pt[0] = 6; //重新设置pt[0]的值为6
pt[9] = 44; //设置第10个元素pt[9]的值为44
int coats[10];
*(coats + 4) = 12; //设置coats[4]的值为12
3. 指针和字符串
数组和指针的特殊关系可以扩展到C-风格字符串:
char flower[10] = "rose";
cout << flower << "s are red\n"; //输出roses are red
数组名是第一个元素的地址,因此cout语句中的flower是字符r(char元素)的地址。cout对象认为char的地址是字符穿的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0)为止。
总之,如果给cout提供一个字符的地址,则将从该字符开始打印,直到遇到空字符(\0)为止。
为了与cout对字符串输出的处理保持一致,后面用引号括起的字符串(”s are red\n”)也应当是一个地址。在C++中,用引号括起的字符串像数组名一样,也是第一个元素的地址。
上述代码不会将整个字符串发送给cout,而只是发送该字符串的地址。对于数组中的字符串、用引号括起的字符串常量以及指针所描述的字符串,处理方式是一样的,都将传递它们的地址。与逐个传递字符串中的所有字符相比,这样的工作量要少很多。
在cout和多数C++表达式中,char数组名、char指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。
程序4.20
#include<iostream>
#include<cstring>
int main()
{
using namespace std;
char animal[20] = "bear"; //animal保存bear
const char * bird = "wren"; //bird指针为字符串"wren"的地址
char * ps; //未初始化
cout << animal << " and "; //显示bear
cout << bird << "\n"; //显示wren
//cout << ps <<"\n"; //可能显示垃圾,可能导致崩溃
cout << "Enter a kind of animal: ";
cin >> animal; //输入<20个字符
//cin>>ps; //可能导致严重的后果,ps没有志向被分配的空间
ps = animal; //将ps指向字符串
cout << ps << "!\n";
cout << "Before using strcpy():\n";
cout << animal << " at " << (int *) animal << endl;
cout << ps << " at " << (int *)ps << endl;
ps = new char[strlen(animal) + 1]; //获得新的存储区
_CRT_SECURE_NO_WARNINGS; strcpy(ps, animal); //将字符串复制到新的存储区
cout << "After using strcpy():\n";
cout << animal << " at " << (int *)animal << endl;
cout << ps << " at " << (int *)ps << endl;
delete[] ps;
cin.get();
cin.get();
return 0;
}
程序说明
“wren”实际上表示的是字符串的地址,因此bird指针保存的是”wren”的地址。这意味着可以向使用字符串”wren”那样使用指针bird,如cout<<"A concerned "<<bird<<" speaks\n";
字符串字面值是常量,因此在声明中使用了关键字const。以这种方式使用const意味着可以用bird来访问字符串,但不能修改它。
一般来讲,如果给cout提供一个指针,它将打印地址。但如果指针类型为char*,则cout将显示指向的字符串。
如果要显示字符串的地址,则必须将这种指针强值转换为另一种指针类型,如int*。
将animal赋给ps并不会复制字符串,而只是复制地址,这样,这两个指针将指向相同的内存单元和字符串。
strcpy()函数接受两个参数,第一个是目标地址,第二个是要复制的字符串的地址。
经常需要将字符串放到数组中。初始化数组时,使用=运算符;否则应使用strcpy()或strncpy()。
char food[20] = "carrots"; //初始化
strcpy(food, "flan");
如果food数组比要复制的字符串小:strcpy(food, "a picnic filled with many goodies");
可能会出问题。在这种情况下,函数将字符串的剩余部分复制到数组后面的内存字节中,这可能会覆盖程序正在使用的其他内存。
要皮面这种问题,应使用strncpy(),该函数还接受第三个参数——要复制的最大字符数。要注意的是,如果该函数在到达字符串结尾之前,目标内存已经用完,则它不会添加空字符,因此,应这样使用该函数:
strncpy(food, "a picnic filled with many goodies", 19);
food[19]='\0';
这样最多将19个字符复制到数组中,将最后一个元素设置成空字符。如果该字符串少于19个字符,则strncpy()将在复制完该字符串后加上空字符。
4. 使用new创建动态结构
在运行时创建数组优于在编译时创建数组,对结构也是如此。
将new用于结构由两步组成:创建结构和访问其成员。
要创建结构,需要同时使用new和结构类型,例如,要创建一个未名明的inflatable类型,并将其地址赋给一个指针:
inflatable * ps = new inflatable;
访问成员 创建动态结构时,不能将成员运算符句点用于结构名,因为这种结构没有名称,只是知道它的地址。C++专门为这种情况提供了一个运算符:箭头成员运算符(->)。该运算符可用于指向结构的指针,就像点运算符可用于结构名一样。例如,如果ps指向一个inflatable结构,则ps->price是被指向结构的price成员。
另一种访问结构成员的方法:ps是指向结构的指针,则*ps就是被指向的值——结构本身,因此*ps是一个结构,(*ps).price就是该结构的price成员。
程序4.21
#include<iostream>
struct inflatable
{
char name[20];
float volume;
double price;
};
int main()
{
using namespace std;
inflatable * ps = new inflatable; //为结构分配内存
cout << "Enter name of inflatable item: ";
cin.get(ps->name, 20); //获取成员的方法1
cout << "Enter volume in cubic feet: ";
cin >> (*ps).volume; //获取成员的方法2
cout << "Enter price: $";
cin >> ps->price;
cout << "Name: " << (*ps).name << endl;
cout << "Volume: " << ps->volume << " cubic feet\n";
cout << "Price: $" << ps->price << endl;
delete ps;
cin.get();
cin.get();
return 0;
}
使用new和delete
下面介绍一个用new和delete来存储通过键盘输入的字符串的示例。
程序4.22定义了一个函数getname(),该函数返回一个指向输入字符串的指针。该函数将输入读入到一个大型的临时数组中,然后使用new[]创建一个刚好能够存储该输入字符串的内存块,并返回一个指向该内存块的指针。这种方法可以节省大量内存(实际编写程序时,使用string类更容易,因为这样可以使用内置的new和delete)。
程序4.22
#include<iostream>
#include<cstring>
using namespace std;
char * getname(void); //函数原型
int main()
{
char * name;
name = getname();
cout << name << " at " << (int *)name << endl;
delete[] name;
name = getname();
cout << name << " at " << (int *)name << endl;
delete[] name;
cin.get();
cin.get();
return 0;
}
char * getname()
{
char temp[80];
cout << "Enter last name: ";
cin >> temp;
char * pn = new char[strlen(temp) + 1];
strcpy(pn, temp);
return pn;
}
5. 自动存储、静态存储和动态存储
5.1自动存储
在函数内部定义的常规变量使用自动存储空间,被称为自动变量(automatic variable)。它们在所属函数被调用时自动产生,在该函数结束时消亡。
例如,程序4.22中的temp数组仅在getname()函数活动时存在。当程序回到main()时,temp使用的内存将自动被释放。
实际上,自动变量是一个局部变量,其作用域为包含它的代码块。
自动变量通常存储在栈中。
5.2 静态存储
静态存储是整个程序执行期间都存在的存储方式。
使变量成为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字static:
static double fee = 56.50;
5.3 动态存储
new和delete运算符提供了一种比自动变量和静态变量更灵活的方法。
它们管理一个内存池,这在C++中被称为自由存储空间(free store)或堆(heap)。该内存池和用于静态变量、自动变量的内存是分开的。
程序4.22表明,new和delete可以在一个函数中分配内存,而在另一个函数中释放它。
在栈中自动添加和删除机制使得占用的内存总是连续的,但new和delete的相互影响可能导致占用的自由存储区不连续,这使得跟踪新分配内存的位置更困难。