65.9K
CodeProject 正在变化。 阅读更多。
Home

初学者指针指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.41/5 (11投票s)

2009年7月28日

CPOL

7分钟阅读

viewsIcon

38226

本文旨在帮助可能受益于理解指针的初学者

前言

C 和 C++ 语言大量使用指针,很多人发现理解起来很困难。Java 语言不使用指针,因为 Java 语言的开发者坚持认为指针 会导致 buggy 代码。要理解如何使用指针,有两个基本概念需要重申:变量和内存。在典型的 x86 32 位操作系统中,有 2 的 32 次方(4294967296 或 4 GB)个可能的内存位置。这些内存地址是顺序分配的。也就是说,计算机内存被划分为顺序编号的内存位置。这些内存地址位置不能物理移动,因此必须引用其中的一些位置。看下面的语句

int number = 5; 

这里分配了一块内存来存储一个整数,你可以使用变量名 number 来访问它。值 5 存储在该 32 位内存位置中。计算机使用地址来引用该区域。数据存储在该特定地址将取决于你的计算机以及你使用的操作系统和编译器。尽管变量名在源程序中是固定的,但在不同的系统(即使是相同架构的系统)上,该地址很可能不同。最基本地说,可以存储地址的变量称为指针,而存储在指针中的地址通常是另一个变量的地址。要获取变量的内存地址,请使用地址运算符(&),它返回对象在内存中的地址。下面是说明地址运算符用法的代码

#include <iostream>
int main()
{
   using namespace std;
   short shortVar = 5;
   long  longVar=65535;
   long sVar = -65535;
   cout << "shortVar:\t" << shortVar;
   cout << "\tAddress of shortVar:\t";
   cout <<  &shortVar  << endl;
   cout << "longVar:\t"  << longVar;
   cout  << "\tAddress of longVar:\t" ;
   cout <<  &longVar  << endl;
   cout << "sVar:\t\t"     << sVar;
   cout << "\tAddress of sVar:\t" ;
   cout <<  &sVar     << endl;
  return 0;
}

使用 cl.exe 编译器时,结果是

  • shortVar: 5 shortVar 的地址: 0012FF34
  • longVar: 65535 longVar 的地址: 0012FF38
  • sVar: -65535 sVar 的地址: 0012FF3C

声明并赋值了三个变量。每个变量都需要三个“cout”的控制台输出块来打印变量的值和变量的地址。当你声明一个变量时,编译器会根据数据类型确定分配多少内存。

将变量的地址存储在指针中

每个变量都有一个地址。即使不知道具体地址,你也可以将变量的地址存储在指针中。在此要强调一点。C++ 基于 C 语言。C 也使用“&”运算符,但实际上它被认为是一个一元运算符,因为它在使用“=”运算符时只指向变量初始化的一侧。二进制指针(使用 * 运算符的指针)指向变量初始化的两侧。假设 howOld 是一个整数。要声明一个名为 pAge 的指针(就像声明变量一样)来保存它的地址,你可以这样写

int *pAge = 0;

这声明 pAge 是一个指向 int 数据类型的指针。pAge 被声明为保存整数的地址。在该示例中,pAge 被初始化为零。值为零的指针称为空指针。安全的编程实践规定指针应始终初始化。要使指针保存地址,必须将地址分配给它。在当前示例中,你必须显式将 hOld 的地址分配给 pAge,如下所示

short  int howOld = 60;         // declare and initialize a variable
short int  * pAge = 0;          // declare a pointer
pAge = &howOld;                 // place howOld’s address in pAge 

我们看到 howOld 的值是 50,而 pAge 拥有 howOld 的地址。因此,指针是一个变量,除了它存储的数据之外,它与任何其他变量没有区别。尽管 * 是一个二进制指针,但在 C++ 中,它被称为间接运算符(或解引用运算符)。当对指针进行解引用时,会检索指针地址处的值。指针提供对它所存储地址的变量值的间接访问。也就是说,指针变量 pAge 前面的间接运算符(*)表示“存储在该位置的值”。间接引用解引用我们指向的变量,并返回该内存位置的值。下面是一个使用非常基础的 Visual C++ 语法的示例。注意“stdafx.h”头文件。此文件位于 MFC 目录中。为了使代码能够(在命令行上)编译并通过并识别此头文件,我通过键入‘notepad stdafx.h’复制了一个副本,然后将其复制到默认的 c:\Program Files\Microsoft Visual Studio 9.0\VC\Include 目录。

#include "stdafx.h"
int main(int argc, _TCHAR*  argv[])
{
   int j = 5;
   printf("    j : %8.X  (value)\n", j);    	// the value is what is stored at 
					// the memory location
   printf("   &j : %8.X  (address)\n", &j); 	// the address is that memory location  

   int *p = &j;                            	// p is another variable that 
					// has its own address
                                            	// stored at pointer p is the address of 
					// the variable j, hence its name 
   printf("   p  :   %8.X (value)\n", p);
   printf("  &p  :   %8.X (address)\n", &p);
   printf("  *p   :   %8.X (indirection)\n", *p);      // indirection
    return 0;
 }

我使用 Cl.exe 编译器带 /EHsc 开关在命令行上编译了此代码

    j :          5  (value)
   &j :        5  (address)
   p  :        12FF38 (value)
  &p  :      12FF3C (address)
  *p   :          5 (indirection)   //note dereferencing p retrieves the value of j, or 5

什么是引用?

引用是一个别名;当你创建一个引用时,你用另一个对象的名称(在本例中是变量 j)来初始化它。从那时起,该引用就充当了目标的替代名称,你对该引用的任何操作实际上都是对目标进行的。查看下面的代码

#include "stdafx.h"
int main(int argc, _TCHAR*  argv[])
{
   int j = 5;
   printf("    j : %8.X  (value)\n", j);  
   printf("   &j : %8.X  (address)\n", j); 
   int *p = &j;  
   printf("   p  :   %8.X (value)\n", p);
   printf("  &p  :   %8.X (address)\n", &p);
   printf("  *p   :   %8.X (indirection)\n", *p); 
  // now we use r as a "reference to variable j
    int &r = j;
   printf("   r  : %8.X (value)\n", r);
   printf("   &r : %8.X (address)\n", &r);
   getc(stdin);  // the getc() function holds the DOS prompt out without disappearing
   return 0;
 }

现在我们来检查输出

    j :         5  (value)
   &j :         5  (address)
   p  :        12FF34 (value)
  &p  :        12FF3C (address)
  *p   :        5 (indirection)
   r  :         5 (value)
   &r :        12FF34 (address)

首先,请注意 p 的地址与该指针指向的地址不同。地址很接近,因为它们都在堆栈上。所以 j 在地址 12FF34 处存储 5。声明的指针 p 指向该地址。p 的地址与其存储的值(即 j 的值的地址)不同。接下来,我们通过变量 r 声明了一个对 j 的引用。r 的值与 j 相同,因为它充当了目标的别名或替代。因此,r 的地址与指针 p 的值相同:即 j 的地址。现在我们来看一下双重间接引用(使用 **p)的使用

#include "stdafx.h"
class CTest
{
public:
    CTest() { printf("CTest constructor\n"); }
    ~CTest() { printf("CTest destructor\n"); }
};

void RunTest()
{
    CTest test;
    CTest *ptest = new CTest();
    delete ptest;
}

int _tmain(int argc, _TCHAR* argv[])
{
//    RunTest();

    int i = 1;
    printf("   i : %8.X (value)\n", i);
    printf("  &i : %8.X (address)\n", &i);

    int *p = &i;
    printf("   p : %8.X (value)\n", p);
    printf("  &p : %8.X (address)\n", &p);
    printf("  *p : %8.X (indirection)\n", *p);

    int &r = i;
    printf("   r : %8.X (value)\n", r);
    printf("  &r : %8.X (address)\n", &r);

    int **pp = &p;
    printf("  pp : %8.X (value)\n", pp);
    printf(" &pp : %8.X (address)\n", &pp);
    printf(" *pp : %8.X (indirection)\n", *pp);
    printf("**pp : %8.X (double indirection)\n", **pp);
              *p = 2;
    printf("   i : %8.X (value)\n", i);
             **pp = 3;
    printf("   i : %8.X (value)\n", i);

/*    char str[] = "This is a test string";
    char *p = str;
              printf(" str = \"%s\"\n", str);
    printf(" str = %8.X (address)\n", str);
    printf("*str = %8.X (indirection)\n", *str);
    printf("   p = %8.X (value)\n", p);
    printf("  *p = %8.X (indirection)\n", *p);
              p[4] = '1';
    printf(" str = \"%s\"\n", str);
             *(p + 4) = '2';
    printf(" str = \"%s\"\n", str);*/
               getc(stdin);
    return 0;
}

字符字符串的声明目前被注释掉了。请注意,我们实例化了一个类,构造了对象,然后使用了 delete 关键字来调用析构函数。下面是内存中显示的输出图像

Pointers.jpg

C++ 代码可能容易出现内存泄漏

局部变量位于堆栈上,函数参数(构成堆栈帧)也位于堆栈上。在 C++ 中,几乎所有的剩余内存都分配给了自由存储区,也称为堆。当调用一个函数时,它会将参数压入堆栈。函数返回调用时,堆栈会自动清理。也就是说,函数返回到堆栈上的返回地址,然后指令指针(IP 寄存器)指向的下一条指令就可以执行了。局部变量不会持久存在,当函数返回时,其局部变量会被销毁。这很好,因为它使程序员无需管理内存空间,但也很糟糕,因为它使得函数很难为其他对象或函数创建对象而不会产生将对象从堆栈复制到调用者的目标对象的返回值所带来的额外开销。因此,堆栈会自动清理,但堆在应用程序结束前不会自动清理,并且有责任在你完成对任何已保留内存的使用后将其释放。这就是析构函数的重要性所在,因为它们提供了一个可以回收类中分配的任何堆内存的地方。你通过使用 new 关键字在 C++ 中分配堆内存。new 后面跟着你想分配的对象的类型,这样编译器就知道需要多少内存。new 的返回值是内存地址。因为我们知道内存地址存储在指针中,所以 new 的返回值也应该赋给一个指针。要在堆上创建一个无符号 short int,你可以这样写

short int  *pPointer;
pPointer = new short int;

那么这意味着什么呢?这意味着我们使用 delete 关键字来归还内存。也就是说,当你完成对一块内存的使用后,你必须将其释放回系统。通过调用 delete 来操作指针来实现这一点。delete 将内存返回给堆(自由存储区)。所以,为了防止内存泄漏,请使用 delete 关键字来释放你使用 new 关键字分配的任何内存。例如

delete pPointer

现在如果我们快速看一下类实例化和对象构造,并将其添加到上面使用的代码中,我们会得到

#include "stdafx.h"
class CTest
{
public:
    CTest() { printf("CTest constructor\n"); }
    ~CTest() { printf("CTest destructor\n"); }
};

void RunTest()
{
    CTest test;
    CTest *ptest = new CTest();
    delete ptest;
}

int _tmain(int argc, _TCHAR* argv[])
{
    char str[] = "This is a test string";
    char *p = str;

    printf(" str = \"%s\"\n", str);
    printf(" str = %8.X (address)\n", str);
    printf("*str = %8.X (indirection)\n", *str);
    printf("   p = %8.X (value)\n", p);
    printf("  *p = %8.X (indirection)\n", *p);

    p[4] = '1';
    printf(" str = \"%s\"\n", str);

    *(p + 4) = '2';
        printf(" str = \"%s\"\n", str);

    getc(stdin);
    return 0;
}

输出

str = "This is a test string"
 str =   12FF20 (address)
*str =       54 (indirection)
   p =   12FF20 (value)
  *p =       54 (indirection)
 str = "This1is a test string"
 str = "This2is a test string"

使用 new 关键字从堆分配的内存通过使用 delete 关键字被释放。最后,这里有更多分配和释放内存的基础代码

c:\>type con > free_memory.cpp
#include <iostream>
int main()
{
   using namespace std;
   int localVariable = 5;
   int * pLocal= &localVariable;
   int * pHeap = new int;
   *pHeap = 7;
   cout << "localVariable: " << localVariable << endl;
   cout << "*pLocal: " << *pLocal << endl;
   cout << "*pHeap: " << *pHeap << endl;
   delete pHeap;
   pHeap = new int;
   *pHeap = 9;
   cout << "*pHeap: " << *pHeap << endl;
   delete pHeap;
   return 0;
}
^Z
//////////////////////////////////////////////////////////////////////////
c:\>cl /EHsc free_memory.cpp
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

free_memory.cpp
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:free_memory.exe
free_memory.obj
//////////////////////////////////////////////////////////////////////////////////////
c:\>free_memory.exe
localVariable: 5
*pLocal: 5
*pHeap: 7
*pHeap: 9

如果有人开发过 .NET,那么原生代码指针最接近的主题可能是委托类型。如果有人有兴趣学习 MFC 或 ATL 框架,那么指针是一个很好的起点。一旦你对指针有了扎实的掌握,那么其他更高级的编程主题就会变得容易得多。

© . All rights reserved.