目录
计算机程序在存储数据时必须跟踪三个基本属性:
- 信息存储在何处;
- 存储的值为多少;
- 存储的信息是什么类型;
指针是一个变量,其存储的是值的地址,而不是值本身。
那么,如何找到常规变量的地址呢?只需对变量应用地址运算符(&),就可以获得它的地址。
比如,a是一个变量,则&a是它的地址。
下面这段代码演示了地址运算符(&)的用法:
#include<iostream>
using namespace std;
int main() {
/*定义两个变量*/
int a = 3;
double b = 5.6;
/*分别输出它的值和地址*/
cout << "a = " << a << " , &a = " << &a << endl;
cout << "b = " << b << " , &b = " << &b << endl;
system("pause");
return 0;
}
运行结果如下:
显示地址时,cout使用了十六进制表示,在这个实现中,b的存储位置比a的低,两个地址的差为0019FF2C-0019FF24 = 8。这个差值是有意义的,因为b的类型是double,而这种类型在内存中占用8个字节。
之前已经说过,指针用于存储值的地址, 因此指针名表示的是地址,解除引用运算符(*)应用于指针后,可以得到该地址处存储的值。 假设 p 是一个指针,那么,&p 表示的是一个地址, *p 表示的是存储在这个地址的值。
声明和初始化指针
指针声明必须指定指针指向的数据的类型,比如,要声明一个 int 类型的指针,应该这样写:
int *p; 这表明,*p 的类型是int ,而 p 本身是一个指针,我们可以说:p 指向 int 类型
下面这段代码,演示了如何声明和初始化指针:
#include<iostream>
using namespace std;
int main() {
int a = 6; //定义int变量 a
int* p = &a; //声明 int 指针 p 并 指向 a;
/*输出变量a的值*/
cout << "a = " << a << endl;
cout << "*p = " << *p << endl;
/*输出变量a的地址*/
cout << "&a = " << &a << endl;
cout << "p = " << p << endl;
*p = *p + 1; //将p指向的值+1
cout << " *p = " << *p << endl; //输出修改后的值
system("pause");
return 0;
}
运行结果如下:
从中可知:变量 a 表示值,并使用 & 运算符来获得地址;而指针 p 表示地址,并使用 * 运算符来获得值。 因为 p 指向了 a,所以 *p 和 a 完全等价,可以像使用 int 变量那样使用 *p 。
其中的声明初始化语句: int *p = &a ;中,被初始化的是指针,而不是它指向的值,也就是说,它是将 p ( 而不是 *p ) 的值设置为 &a 。
指针的危险
在C++中 创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。为数据提供空间是一个独立的步骤,万万不可忽略。
来看下面这段代码:
int *p; //创建一个 int 指针
*p = 123; //将p指向的值修改为123
p 确实是一个指针,但它指向哪里?上述代码没有将地址赋给 p ,我们也不知道 p 现在指向哪里,那么123将被放在哪里?我们也不清楚。由于 p 没有初始化,所以它可能是任何值,程序都将它解释为存储123的地址。如果 p 所指向的地方并不是程序代码的地方,那么这种错误将可能导致一些最隐匿、最难以跟踪的 bug 。
注意:一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。
使用new来分配内存
之前我们说的指针只是在为可以通过名称直接访问的内存提供了一个别名。
然而,指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值,在这种情况下,只能通过指针来访问内存。C++用new运算符来分配内存。
我们需要告诉 new ,要为哪种数据类型分配内存,这样new将找到一个长度正确的内存块。
下面来试试为一个 int 值分配未命名的内存:
int *pn = new int ;
new int 告诉程序需要一个适合存储 int 的内存。new 运算符会根据类型(这里是 int )来确定需要多少字节的内存,然后找到这样的内存,并返回其地址。接下来将地址赋给 pn ,pn 是被声明为指向 int 的指针。
为一个数据对象(可以是结构、数组,也可以是基本类型)获得并指定分配内存的通用格式是:
typeName * pointer_name = new typeName;
下面这段程序用来演示如何使用new :
#include<iostream>
using namespace std;
int main() {
int* pn = new int; //开辟一段int内存
*pn = 111; //并赋值为111
cout << "*pn = " << *pn << endl;
cout << "pn = " << pn << endl;
cout << "size of pn = " << sizeof(pn) << endl;
cout << "size of *pn = " << sizeof(*pn) << endl;
system("pause");
return 0;
}
运行结果如下:
该程序使用 new 为 int 类型的数据对象分配内存,这是在程序运行时进行的,如果没有它,将无法访问这个内存单元。有这个指针之后,便可以像使用变量那样使用*pn 。
注意:计算机可能会由于没有足够的内存而无法满足new的请求,在这种情况下,new通常会引发异常。
使用delete释放内存
当需要内存时,可以使用 new 来请求,当使用完毕之后,可以用 delete 运算符将其释放。
使用delete时,后面要加上指向内存块的指针。
int *pn = new int; //开辟内存
...
delete pn; //释放内存
警告:一定要配对地使用 new 和 delete,否则将发生内存泄漏。只能用 delete 来释放使用 new 分配的内存。
使用new来创建动态数组
使用 new 创建数组时,如果在运行阶段需要数组,则创建它,如果不需要则不创建,这意味着数组是在程序运行时创建的,这种数组叫做动态数组。
在C++中,创建动态数组只需将数组的元素类型和元素数目告诉 new 即可,必须在类型名后加上方括号,其中包括元素数目。
例如:要创建一个包含10个 int 元素的数组:
int *arr = new int [10];
new运算符返回第一个元素的地址,该数组地址被赋给 arr 。
当程序使用完new分配的数组之后,应使用 delete 释放:
delete [] arr;
方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。
总之,使用 new 和 delete 时,应遵守以下规则:
- 不要使用 delete 来释放不是 new 分配的内存。
- 不要使用 delete 释放同一个内存块两次。
- 如果使用 new [ ] 为数组分配内存,则应使用delete [ ] 来释放。
- 对空指针应用 delete 是安全的。
那么,该如何访问动态数组中的元素呢?以上一个动态数组 arr 为例,第一个元素不成问题,因为 arr 指向数组的第一个元素,所以,*arr 就表示该动态数组的第一个元素的值。那剩余9个元素如何访问呢?其实只要把指针当作数组名使用就行。对于第二个元素,可以使用 arr [1] ,这样以此类推,访问动态数组就变得非常简便。可以这样做的原因是:C++内部使用指针来处理数组。
下面这段代码演示了如何使用 new 来创建动态数组以及使用数组表示法来访问元素。
#include<iostream>
using namespace std;
int main() {
double* p = new double[3]; //创建动态数组p
/*动态数组赋值*/
p[0] = 1.3;
p[1] = 2.3;
p[2] = 3.3;
cout << "p[1] = " << p[1] << endl;
p = p + 1;
cout << "修改之后 p[0] = " << p[0] << endl;
cout << "p[1] = " << p[1] << endl;
cout << "p[2] = " << p[2] << endl;
p = p - 1;
delete[] p;
system("pause");
return 0;
}
运型结果为:
从中可以知道:程序将指针 p 当作数组名来使用,其中 p = p + 1; 这段代码表现了数组名和指针之间的根本差别:
我们现在假设编写这样一段代码:
int arr[3] = { 1,2,3 };
arr = arr + 1;
看看编译器给我们反馈什么信息:
编译器显示这行代码( arr = arr +1)是错误的,也就是说:
我们不能修改数组名的值,但指针是变量,因此可以修改它的值。将 p+1 后, p[0] 现在指向了该第二个元素,p[2] 现在指向了未知位置。要想delete [] 释放原来开辟的正确的内存,需要将它减 1 ,指针便能指向原来的值,内存便可以正确地释放。
相邻的int地址,一般是相差 4 个字节,而 p+1 后,p指向了下一个元素的地址,这便是指针算术的特殊之处。
指针、数组、指针算术
如果我们将 int 变量加1,那么其值将加1;但如果将指针变量加 1 ,它增加的量等于它指向的类型的字节数。例如:将指向 double 的指针加1后,如果系统对 double 使用8个字节存储,则数值将增加8。
下面这段程序将演示这一现象:
#include<iostream>
using namespace std;
int main() {
/*创建两个数组*/
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 << "修改之后:" << endl;
cout << "pw = " << pw << ", *pw = " << *pw << endl << endl;
cout << "ps = " << ps << ", *ps = " << *ps << endl;
ps = ps + 1;
cout << "修改之后:" << endl;
cout << "ps = " << ps << ", *ps = " << *ps << endl << endl;
/*用数组表示法输出数组元素*/
cout << "stacks[0] = " << stacks[0]
<< ",stacks[1] = " << stacks[1] << endl;
/*用指针表示法输出数组元素*/
cout << "*stacks = " << stacks
<< ",*(stacks+1) = " << *(stacks + 1) << endl;
cout << "sizeof(wages) = " << sizeof(wages) << endl;
cout << "sizeof(stacks) = " << sizeof(stacks) << endl;
system("pause");
return 0;
}
运行结果如下:
C++ 将数组名解释为数组的第一个元素的地址,所以可以将数组名直接赋值给指针,表示该指针指向数组的第一个元素。之后查看 pw 和 *pw 的值,前者是指针,后者是存储在该地址中的值,当 pw+1 之后,pw的值增加了8,这时, pw 的值为第二个元素的地址,所以,修改后 *pw 为20000.0。
下面是图解:更为清晰直观
至此,可以得出:将指针变量加1后,其增加的值等于指向的类型占用的字节数。
而运行结果可以分析出:数组表示法 stacks[1] 和 *(stacks +1)所表示的含义相同,后者意味着
先计算数组第二个元素的地址,然后再找到存储在该地址的值。此处必须使用括号,如果不使用括号,将给*stacks+1,而不是给 stacks +1。
将sizeof 运算符用于数组名用时,将返回整个数组的长度(单位字节)。
数组的地址
数组名被解释为其第一个元素的地址,而对数组名应用地址运算符(&)时,将得到的是整个数组的地址。
请看下面这段代码:
#include <iostream>
using namespace std;
int main() {
int arr[10];
cout <<"arr = "<< arr << endl;
cout <<"&arr = " << & arr << endl;
cout << "arr +1 = "<< arr + 1 << endl;
cout << "& arr + 1 " << & arr + 1 << endl;
system("pause");
return 0;
}
运行结果如下:
从数字上说,arr 和 &arr 这两个地址相同,但是在概念上说,arr 即 &arr[0] 是一个4字节内存块的地址,而 &arr 是一个40个字节内存块的地址。因此,arr+1 ,将使地址值加4,而&arr +1 将使地址值加40。