【C++】学习笔记十——指针

指针

计算机程序在存储数据时必须有3中基本属性:

  • 信息存储在何处
  • 存储的值为多少
  • 存储的信息是什么类型

定义一个简单变量可以达到上述目的。声明语句指出了值的类型和符号名,还让程序为值分配内存,并在内部跟踪该内存单元。

指针是一个变量,存储的是值的地址,而不是值本身。

程序4.14演示了用地址运算符(&)获取常规变量的地址。

程序4.14

#include<iostream>
int main()
{
    using namespace std;
    int donuts = 6;
    double cups = 4.5;

    cout << "donuts value = " << donuts;
    cout << " and donuts address = " << &donuts << endl;    //可能需要使用unsigned(&donuts)
    cout << "cups value = " << cups;
    cout << " and cups address = " << &cups << endl;

    cin.get();
    return 0;
}

这里写图片描述

使用常规变量时,值是指定的量,地址为派生量;
处理存储数据的新策略刚好相反,将地址视为指定的量,而将值视为派生的量。
一种特殊类型的变量——指针用于存储值的地址。因此,指针名表示的是地址。

*运算符被称为间接值(indirect value)或解除引用(dereferencing)运算符,将其用于指针,可以得到该地址处存储的值(这和乘法的符号相同,C++根据上下文来确定所指的是乘法还是解除引用)。例如,假设manly是一个指针,则manly表示的是一个地址,而 *manly表示存储在该地址处的值。 *manly与常规int变量等效。

程序4.15

#include<iostream>
int main()
{
    using namespace std;
    int updates = 6;
    int * p_updates;
    p_updates = &updates;

    //两种表示值的方法
    cout << "Values: updates = " << updates;
    cout << ", *p_updates = " << *p_updates << endl;

    //两种表示地址的方法
    cout << "Addresses: &updates = " << &updates;
    cout << ", p_updates = " << p_updates << endl;

    //使用指针改变值
    *p_updates = *p_updates + 1;
    cout << "Now updates = " << updates << endl;

    cin.get();
    return 0;
}

这里写图片描述

从程序4.15可知,int变量updates和指针变量p_updates只不过是同一枚硬币的两面。
变量updates表示值,并使用&运算符来获得地址;
变量p_updates表示地址,并使用*运算符来获得值。
由于p_updates指向updates,因此*p_updates和updates完全等价。可以像使用int变量那样使用*p_updates。甚至可以将值赋给 *p_updates,这样将修改指向的值updates。

1. 声明和初始化指针

计算机需要跟踪指针指向的值的类型,因此,指针声明必须指定指针指向的数据的类型:

int * p_updates;

这表明,*p_updates的类型为int。由于*运算符被用于指针,因此p_updates变量本身必须是指针。
p_updates指向int类型,p_updates的类型时指向int的指针,即int*。
p_updates是指针,而*p_updates是int。

*两边的空格是可选的。
一般C程序使用这种格式:int *ptr;,这强调*ptr是一个int类型的值。
C++一般使用:int* ptr;,这强调int*是一种类型——指向int的指针。
甚至可以这样做:int*ptr;

但是,下面的声明将创建一个指针(p1)和一个int变量(p2):

int* p1, p2;

对每个指针变量名,都需要使用一个*。

注意:在C++中,int *是一种复合类型,是指向int的指针。

可以用同样的句法来声明指向其他类型的指针:

double * tax_ptr;
char * str;

可以在声明语句中初始化指针。在这种情况下,被初始化的是指针,而不是它指向的值。也就是说,下面的语句将pt(而不是*pt)的值设置为&higgens:

int higgens = 5;
int * pt = &higgens;

程序4.16

#include<iostream>
int main()
{
    using namespace std;
    int higgens = 5;
    int * pt = &higgens;

    cout << "Value of higgens = " << higgens
        << "; Address of higgens = " << &higgens << endl;
    cout << "Value of *pt = " << *pt
        << "; Value of pt = " << pt << endl;
    cin.get();
    return 0;
}

这里写图片描述
从程序4.16可知,程序将pt(而不是*pt)初始化为higgens的地址。

2. 指针的危险

在C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向数据的内存。为数据提供空间是一个独立的步骤,忽略这一步会发生危险。如下所示:

long * fellow;
*fellow = 223323;

fellow是一个指针,但它没有明确的指向。上述代码没有将地址赋给fellow。
由于fellow没有被初始化,他可能有任何值。不管值是什么,程序都将它解释为存储223323的地址。如果fellow的值为1200,计算机将把数据放在地址1200上,即使这恰巧是程序代码的地址。fellow指向的地方很可能并不是要存储223323的地方。这种错误可能会导致一些隐匿难以跟踪的bug。

警告:一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。

3. 指针和数字

指针不是整型,虽然计算机通常把地址当做整数来处理。从概念上看,指针与整数是截然不同的类型。
不能简单地将整数赋给指针:

int * pt;
pt = 0xB8000000;     //类型不匹配

在这里,左边是指向int的指针,因此可以把地址赋给它,但右边是一个整数。
要将数字值作为地址来使用,应通过强制类型转换将数字转换为适当的地址类型:

int * pt;
pt = (int *) 0xB0000000;    //赋值语句两边都是整数的地址,因此这样的赋值有效

注意:pt是int值的地址,并不意味着pt本身是int类型。

4. 使用new来分配内存

前面都将指针初始化为变量的地址;变量是在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问的内存提供了一个别名。

指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。在这种情况下,只能通过指针来访问内存。在C语言中,可以用库函数malloc()来分配内存;在C++中仍可以这样做,但C++还有更好的方法——new运算符。

int * pn = new int;           //在运行阶段为一个int值分配未命名的内存,并使用指针来访问这个值

new int告诉程序,需要合适存储int的内存。new运算符根据类型来确定需要多少字节的内存。然后,它找到这样的内存,并返回其地址。接下来,将地址赋给pn,pn是被声明为指向int的指针。pn是地址,而*pn是存储在那里的值。

将变量的地址赋给指针

int higgens;
int * pt = &higgens;

这两种情况(pn和pt),都是将一个int变量的地址赋给了指针。在第二种情况下,可以通过名称higgens来访问该int,在第一种情况下,则只能通过该指针进行访问。
pn指向的内容没有名称,此时,我们说pn指向一个数据对象(为数据分配的内存块)。

为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下

typeName * pointer_name = new typeName;

程序4.17

//使用new
#include<iostream>
int main()
{
    using namespace std;
    int nights = 1001;
    int * pt = new int;
    *pt = 1001;

    cout << "nights values = ";
    cout << nights << ": location " << &nights << endl;
    cout << "int ";
    cout << "value = " << *pt << ": location = " << pt << endl;
    double * pd = new double;
    *pd = 10000001.0;

    cout << "double ";
    cout << "value = " << *pd << ": location = " << pd << endl;
    cout << "location of pointer pd: " << &pd << endl;
    cout << "size of pt = " << sizeof(pt);
    cout << ": size of *pt = " << sizeof(*pt) << endl;
    cout << "size of pd = " << sizeof pd;
    cout << ": size of *pd = " << sizeof(*pd) << endl;
    cin.get();
    return 0;
}

这里写图片描述

5. 使用delete释放内存

当需要内存时,可以使用new来请求;使用完内存后,可以使用delete运算符将内存归还给内存池。
使用delete时,后面要加上指向内存块的指针(这些内存块最初是用new分配的):

int * ps = new int;                  //分配内存
...                                  //使用内存
delete ps;                           //释放内存

这将释放ps指向的内存,但不会删除指针ps本身。可以将ps重新指向一个新分配的内存。
一定要配对的使用new和delete,否则会发生内存泄露(memory leak),也就是说,被分配的内存再也无法使用了。如果内存泄露严重,则程序将由于不断寻找更多内存而终止。

不要尝试释放已经释放的内存,这样做的结果是不确定的,什么情况都有可能发生。

另外,不能使用delete来释放声明变量所获得的内存:

int * ps = new int;     //ok
delete ps;              //ok
delete ps;              //not ok now
int jugs = 5;
int * pi = &jugs;       //ok
delete pi;              //不允许,内存不是由new分配的

注意:只能使用delete来释放使用new分配的内存。然而,对空指针使用delete是安全的。

使用delete的关键在于,将它用于new分配的内存。但delete并不一定要用于new的指针,而是用于new 的地址:

int * ps =new int;      //分配内存
int * pq = ps;          //指针pq也指向相同的内存块
delete pq;              //释放内存

一般来讲,不要创建两个指向同一个内存快的指针,因为这将增加错误的删除同一个内存块两次的可能性。

6. 使用new来创建动态数组

静态联编(static binding):通过声明来创建数组,数组是在编译时加入到程序中的,必须在编写程序时指定数组的长度;
动态联编(dynamic binding):使用new,如果在运行阶段需要数组,则创建它,如果不需要,则不创建,还可以在程序运行时选择数组的长度。数组是在程序运行时创建的。

6.1使用new创建动态数组

将数组的元素类型和元素数目告诉new即可。必须在类型名后加上方括号[],其中包含元素数目。
例如,要创建一个包含10个int元素的数组:

int * psome = new int [10];

new运算符返回第一个元素的地址,该地址被赋给指针psome。

当程序使用完new分配的内存块是,应使用delete释放它们。然而,对于使用new创建的数组,应使用另一种格式的delete来释放:

delete [] psome;

方括号[]告诉程序,应释放整个数组,而不仅仅是指针指向的元素。
如果使用new时,不带方括号,则使用delete时,也不应带方括号。如果使用new时带方括号,则使用delete时也应带方括号。

使用new和delete时,应遵循以下规则:

  • 不要使用delete来释放不是new分配的内存
  • 不要使用delete释放同一个内存块两次
  • 如果使用new []为数组分配内存,则应使用delete [] 来释放
  • 如果使用new []为一个实体分配内存,则应使用delete(没有方括号)来释放
  • 对空指针应用delete是安全的

指针psome只指向数组的第一个元素,因此编写程序时,应给定元素数目。

为数组分配内存的通用格式如下

type_name * pointer_name = new type_name [元素数目];

使用new运算符可以确保内存块足以存储指定数目个类型为type_name的元素,而pointer_name将指向第一个元素。

6.2 使用动态数组

int * psome = new int [10];

访问数组元素时,只要把指针当作数组名使用即可。
也就是说,对第1个元素可以使用psome[0],而不是*psome;对第2个元素可以使用psome[1],以此类推。
可以这样做的原因是,C和C++内部都使用指针来处理数组。数组和指针基本等价。

程序4.18

//对数组使用new
#include<iostream>
int main()
{
    using namespace std;
    double * p3 = new double [3];      //分配3个double的内存空间
    p3[0] = 0.2;
    p3[1] = 0.5;
    p3[2] = 0.8;
    cout << "p3[1] is " << p3[1] << ".\n";
    p3 = p3 + 1;                       //指针加1
    cout << "Now p3[0] is " << p3[0] << " and ";
    cout << "p3[1] is " << p3[1] << ".\n";
    p3 = p3 - 1;                       //指针恢复为最初
    delete [] p3;
    cin.get();
    return 0;
}

这里写图片描述
从中可知,程序将指针p3当作数组名来使用,p3[0]为第一个元素,以此类推。
下面的代码指出了数组名和指针之间的根本差别

p3 = p3 + 1;   //对指针可以,对数组名不可以

不能修改数组名的值。但指针是变量,因此可以修改它的值。
p3+1将导致它指向第二个元素而不是第一个。将它减一后,指针将指向原来的值,这样程序便可以给delete[]提供正确的地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值