1. 指针和自由存储空间
指针是一个变量,其存储的是值的地址,而不是值本身。
对于常规变量,只需要对变量应用地址运算符(&)就可以获得它的位置。(*)运算符被称为间接值或解除引用运算符,将其应用于指针,可以得到该地址处存储的值。
假设 manly 是一个指针,则 manly 表示的是一个地址,而 *manly 表示存储在该地址处的值。
示例代码:
int updates = 6;
int *p_updates;
p_updates = &updates;
cout << "Values: updates = " << updates;
cout << ", *p_updates = " << *p_updates << endl;
cout << "Address: &updates = " << &updates;
cout << ", p_updates = " << p_updates << endl;
*p_updates = *p_updates + 1;
cout << "Now updates = " << updates << endl;
运行结果:
Values: updates = 6, *p_updates = 6
Address: &updates = 0x7ffeefbff4bc, p_updates = 0x7ffeefbff4bc
Now updates = 7
int 变量 updates 和指针变量 p_updates 相当于一枚硬币的两面,updates 表示值,并使用 & 运算符来获得地址;而变量 p_updates 表示地址,并使用 * 运算符来获得值。由于 p_updates 指向 updates,因此 *p_updates 和updates 完全等价。
1. 1 指针的声明
指针声明必须指定指针指向的数据类型,如:
int * p_updates;
* 运算符两边的空格可选的,传统上,C 程序员使用:int *ptr;
,强调 ptr 是一个 int 类型的值。而很多 C++ 程序员使用:int* ptr;
。强调 int 是一种指向 int 的指针。
但是int* p1, p2;
声明创建一个指针(p1)和一个 int 变量(p2)。
1. 2 指针的初始化
可以在声明语句中初始化指针,这种情况下,被初始化的是指针,而不是它指向的值。
int higgens = 5;
int * pt = &higgens;
注意:一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。
1. 3 指针和数字
指针不是整型,虽然计算机通常把地址当作整数来处理,但指针与整数时截然不同的类型。不能简单地将整数赋给指针:
int * pt;
pt = 0xB8000000; // type mismatch
1. 4 使用 new 来分配内存
在运行阶段为一个 int 值分配未命名的内存,并使用指针来访问这个值,可以使用 C++ 的new 运算符:
int * pn = new int;
new 运算符根据类型来确定需要多少字节的内存,然后找到这样的内存,并返回其地址。接下来,降低至赋给 pn,pn 是被声明为指向 int 的指针。现在 pn 是地址,而 *pn 是存储在那里的值。
为一个数据对象(可以是结构,也可以是基本类型)获得并制定分配内存的通用格式如下:
typeName * pointer_name = new typeName;
1. 5 使用 delete 释放内存
当需要内存时,可以使用 new 来请求。当使用完内存后,可以使用 delete 运算符将内存还给内存池。使用 delete 时,后面要加上指向内存块的指针
int * ps = new int;
...
delete ps;
这将会释放 ps 指向的内存,但不会删除指针 ps 本身。一定要配对地使用 new 和 delete,否则将发生内存泄漏,即,被分配的内存再也无法使用。
不要尝试释放已经释放的内存块,这样做的结果将是不确定的,意味着什么情况都可能发生。
注意:只能用 delete 来释放使用 new 分配的内存,然而,对空指针使用 delete 是安全的。
使用 delete 的关键在于,将它用于 new 分配的内存。这并不意味着要使用用于 new 的指针,而是用于 new 的地址。对于下面的代码:
int * ps = new int;
int * pq = ps;
delete pq;
一般来说,不要创建两个指向同一个内存块的指针,因为这将增加错误地删除同一个内存块两次的可能性。
1. 6 使用 new 来创建动态数组
创建方法:
int * psome = new int [10];
new 返回第一个元素的地址。在上面的例子中,该地址被赋给指针 psome。访问其中的元素的方法:对于第 1 个元素,可以使用 psome[0] ,*psome;对于第 2 个元素,可以使用 psome[1]。
对于 new创建的数组,应使用另一种格式的 delete 来释放:
delete [] psome;
使用 new 和 delete 时,应遵守如下规则:
- 不要使用 delete 来释放不是 new 分配的内存。
- 不要使用 delete 释放同一个内存块两次。
- 如果使用 new [] 为数组分配内存,则应使用 delete [] 来释放。
- 如果使用 new 为一个实体分配内存,则应使用 delete 来释放。
- 对空指针应用 delete 是安全的。
为数组分配内存的通用格式如下:
type_name * pointer_name = new type_name [num_elements];
2. 指针、数组和指针算术
C++ 将数组名解释为数组第 1 个元素的地址,对于下面的语句将 pw 声明为指向 double 类型的指针,然后将它初始化为 wages——wages 数组中第 1 个元素的地址:
double * pw = wages;
对于 pw 和 *pw,前者是地址,后者是存储在该地址中的值。由于 pw 指向第 1 个元素,因此 *pw 现实的值为第 1 个元素的值。将 pw 加 1,数字地址将增加 8,是的 pw 的值为第 2 个元素的地址。
和所有数组一样,wages 也存在下面的等式:
wages = &wages[0] = address of first element of array;
通常,使用数组表示法时,C++ 都执行如下转换:
arrayname[i] become *(arrayname + i)
如果使用的是指针,而不是数组名,则 C++ 也将指向同样的转换:
pointername[i] becomes *(pointername + i)
使用 new 创建动态结构
创建方法如下:
inflatable * ps = new inflatable;
对于访问成员,创建动态结构时,不能将成员运算符据点用于结构名,因为这种结构没有名称,只是知道它的地址,因此,需要使用箭头成员运算符(->)。如果 ps 指向一个 inflatable 结构,则 ps->price 是被指向的结构的 price 成员。
另一种访问结构成员的方法是,如果 ps 是指向结构的指针,则 *ps 就是被指向的值——结构本身,由于 *ps 是一个结构,因此 (*ps).price 是该结构的 price 成员。C++ 的运算符优先规则要求使用括号。
3. 数组的替代品
3. 1 模板类 vector
vector 是使用 new 创建动态数组的替代品,vector 确实使用 new 和 delete 来管理内存,但这种工作是自动完成的。
使用 vector 对象,必须包含头文件 vector;vector 包含在名称空间 std 中。模板使用不同的语法来指出它存储的数据类型。vector 类使用不同的语法来指定元素数。
示例:
#include <vector>
using namespace std;
vector<int> vi; // create a zero-size array of int
int n;
cin >> n;
vector<double> vd(n); // create an array of n doubles
vector对象在插入或添加值时自动调整长度。
通用的声明方法是:
vector<typeName> vt(n_elem);
3. 2 模板类 array
vector 的效率稍低,如果需要长度固定的数组,使用数组是更佳的选择,但不那么方便和安全。
array 对象的创建语法如下:
#include <array>
using namespace std;
array<int, 5> ai;
array<double, 4> ad = {1.2, 2.1, 3.43, 4.3};
通用语法为:
array<typeName, n_elem> arr;
3. 3比较数组、vector 对象和 array 对象
- 无论数组、vector 对象还是 array 对象,都可以使用标准数组表示法来访问各个元素。
- array 对象和数组存储在相同的内存区域(即栈)中,而 vector 对象存储咋另一个区域(自由存储区或堆)中。
- 可以将一个 array 对象赋给另一个 array 对象;而对于数组,必须逐元素复制数据。
- 数组不会检查超界错误,可以访问位置位于数组之外的内存。因此数组的行为是不安全的。
- vector 和 array 对象可以禁止超界现象,即使用 at() 函数,可以在运行期间补货非法索引,而程序默认将中断。
- 可以使用函数 begin() 和 end(),用来确定边界,以免无意间超界。