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

指针入门指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (204投票s)

2000年6月29日

viewsIcon

1792999

downloadIcon

6344

关于在 C 和 C++ 中使用指针的文章。

什么是指针?

指针本质上与其他变量没有区别。但它们的不同之处在于,它们不包含实际数据,而是包含一个指向信息所在内存位置的指针。这是一个非常重要的概念。许多程序和思想的设计都依赖于指针作为基础,例如链表。

入门

如何定义指针?嗯,和其他变量一样,只不过在其名称前面加上一个星号。所以,例如,下面的代码创建了两个指针,它们都指向一个整数。

int* pNumberOne;
int* pNumberTwo;

注意到两个变量名前面的“p”前缀了吗?这是一种约定,用来表示该变量是一个指针。现在,让我们让这些指针真正指向一些东西。

pNumberOne = &some_number;
pNumberTwo = &some_other_number;

&”(和号)符号应读作“地址”,它返回变量在内存中的地址,而不是变量本身。因此,在这个例子中,pNumberOne 被设置为等于 some_number 的地址,所以 pNumberOne 现在指向 some_number

现在,如果我们想引用 some_number 的地址,我们可以使用 pNumberOne。如果我们想通过 pNumberOne 引用 some_number 的值,我们就必须说 *pNumberOne* 是解引用指针,应读作“指向的内存位置”,除非是在声明中,例如 int *pNumber 这一行。

到目前为止我们学到了什么:一个例子

呼!要消化这么多内容。如果您不理解任何概念,我建议您再仔细阅读一遍。指针是一个复杂的主题,可能需要一些时间才能掌握。这是一个演示上述思想的示例。它是用 C 语言编写的,不包含 C++ 扩展。

#include <stdio.h>

void main()
{
    // declare the variables:
    int nNumber;
    int *pPointer;

    // now, give a value to them:
    nNumber = 15;
    pPointer = &nNumber;

    // print out the value of nNumber:
    printf("nNumber is equal to : %d\n", nNumber);

    // now, alter nNumber through pPointer:
    *pPointer = 25;

    // prove that nNumber has changed as a result of the above by 
    // printing its value again:
    printf("nNumber is equal to : %d\n", nNumber);
}

请阅读并编译上面的代码示例。请确保您理解它的工作原理。然后,当您准备好时,请继续阅读!

陷阱!

看看您能否找出下面程序中的错误。

#include <stdio.h>

int *pPointer;

void SomeFunction();
{
    int nNumber;
    nNumber = 25;    

    // make pPointer point to nNumber:
    pPointer = &nNumber;
}

void main()
{
    SomeFunction(); // make pPointer point to something

    // why does this fail?
    printf("Value of *pPointer: %d\n", *pPointer);
}

这个程序首先调用 SomeFunction 函数,该函数创建一个名为 nNumber 的变量,然后让 pPointer 指向它。然而,问题就出在这里。当函数退出时,nNumber 会被删除,因为它是一个局部变量。局部变量在执行离开定义它们的块时总是被删除。这意味着当 SomeFunction 返回到 main() 时,该变量就被删除了。所以 pPointer 指向的变量曾经存在,但现在不再属于这个程序了。如果您不理解这一点,最好重新回顾一下局部变量和全局变量,以及作用域。这个概念也很重要。

那么,如何解决这个问题呢?答案是使用一种称为动态分配的技术。请注意,C 和 C++ 之间存在差异。由于现在大多数开发人员都使用 C++,因此下面的代码使用的是 C++ 方言。

动态分配

动态分配也许是理解指针的关键。它用于分配内存,而无需定义变量然后让指针指向它们。虽然这个概念可能看起来令人困惑,但它其实很简单。以下代码演示了如何为整数分配内存。

int *pNumber;
pNumber = new int;

第一行声明了指针 pNumber。第二行然后为整数分配内存,并让 pNumber 指向这个新内存。这里是另一个例子,这次使用的是 double

double *pDouble;
pDouble = new double;

公式每次都一样,所以您在这方面不太可能出错。然而,动态分配的不同之处在于,您分配的内存不会在函数返回时或执行离开当前块时被删除。因此,如果我们使用动态分配重写上面的示例,我们可以看到它现在可以正常工作了。

#include <stdio.h>

int *pPointer;

void SomeFunction()
{
    // make pPointer point to a new integer
    pPointer = new int;
    *pPointer = 25;
}

void main()
{
    SomeFunction(); // make pPointer point to something
    printf("Value of *pPointer: %d\n", *pPointer);
}

请阅读并编译上面的代码示例。请确保您理解它的工作原理。当调用 SomeFunction 时,它会分配一些内存,并让 pPointer 指向它。这一次,当函数返回时,新内存将保持不变,所以 pPointer 仍然指向有用的东西。动态分配就是这些了!请确保您理解了这一点,然后继续阅读,了解其中的“苍蝇”,以及为什么上面的代码仍然存在严重错误。

内存的来来去去

总会有复杂的问题,这个问题可能会变得相当严重,尽管它很容易解决。问题在于,虽然使用动态分配分配的内存会被方便地保留下来,但它实际上永远不会自动被删除。也就是说,内存将一直分配,直到您告诉计算机您已经完成了使用。其结果是,如果您不告诉计算机您已经完成了使用内存,那么它将浪费其他应用程序或您应用程序的其他部分可以使用的空间。这最终会导致系统因内存耗尽而崩溃,所以这非常重要。释放您用完的内存非常简单。

delete pPointer;

就是这样。但是,您必须小心,要传递一个有效的指针,也就是说,一个确实指向您已分配内存的指针,而不是任何无效的垃圾。尝试删除已经释放的内存是很危险的,可能会导致您的程序崩溃。所以,这里是再次更新的示例,这次它不会浪费任何内存。

#include <stdio.h>

int *pPointer;

void SomeFunction()
{
// make pPointer point to a new integer
    pPointer = new int;
    *pPointer = 25;
}

void main()
{
    SomeFunction(); // make pPointer point to something
    printf("Value of *pPointer: %d\n", *pPointer);

    delete pPointer;
}

只差一行代码,但这一行至关重要。如果您不删除内存,就会出现所谓的“内存泄漏”,即内存逐渐泄漏并且在应用程序关闭之前无法重新使用。

将指针传递给函数

将指针传递给函数的能力非常有用,而且非常容易掌握。如果我们编写一个程序,它接受一个数字并对其加五,我们可能会写出类似下面的代码。

#include <stdio.h>

void AddFive(int Number)
{
    Number = Number + 5;
}

void main()
{
    int nMyNumber = 18;
    
    printf("My original number is %d\n", nMyNumber);
    AddFive(nMyNumber);
    printf("My new number is %d\n", nMyNumber);
}

然而,问题在于 AddFive 中引用的 Number 是传递给函数的变量 nMyNumber 的副本,而不是变量本身。因此,Number = Number + 5 这行代码对变量的副本加五,而不会影响 main() 中的原始变量。尝试运行程序来证明这一点。

为了解决这个问题,我们可以将指向内存中数字存储位置的指针传递给函数,但我们必须修改函数,使其期望一个指向数字的指针,而不是一个数字。要做到这一点,我们将 void AddFive(int Number) 改为 void AddFive(int* Number),添加星号。这是修改后的程序。请注意,我们必须确保传递 nMyNumber 的地址而不是数字本身?这是通过添加 & 符号来完成的,正如您所回忆的,它被读作“地址”。

#include <stdio.h>
void AddFive(int* Number)
{
    *Number = *Number + 5;
}

void main()
{
    int nMyNumber = 18;
    
    printf("My original number is %d\n", nMyNumber);
    AddFive(&nMyNumber);
    printf("My new number is %d\n", nMyNumber);
}

尝试自己构思一个示例来演示这一点。请注意 AddFive 函数中 *Number 前面的重要性?这是告诉编译器我们想对 Number 变量指向的数字加五,而不是对指针本身加五。关于函数的最后一件事是,您也可以从它们返回指针,如下所示。

int * MyFunction();

在这个例子中,MyFunction 返回一个指向整数的指针。

指向类的指针

关于指针还有几个其他的注意事项,其中之一是结构体或类。您可以这样定义一个类。

class MyClass
{
public:
    int m_Number;
    char m_Character;
};

然后,您可以这样定义一个 MyClass 类型的变量。

MyClass thing;

您应该已经知道了。如果不知道,请尝试阅读这方面的内容。要定义一个指向 MyClass 的指针,您可以使用。

MyClass *thing;

……正如您所期望的那样。然后,您需要分配一些内存,并让这个指针指向该内存。

thing = new MyClass;

问题就出在这里:那么如何使用这个指针呢?嗯,通常您会写“thing.m_Number”,但对于指针来说,您不能这样做,因为 thing 不是一个 MyClass,而是指向它的指针。因此,thing 本身不包含一个名为 m_Number 的变量;而是它指向的结构体包含 m_Number。因此,我们必须使用不同的约定。那就是将 .(点)替换为 ->(短横线后跟一个大于号)。下面的例子展示了这一点。

class MyClass
{
public:
    int m_Number;
    char m_Character;
};

void main()
{
    MyClass *pPointer;
    pPointer = new MyClass;

    pPointer->m_Number = 10;
    pPointer->m_Character = 's';

    delete pPointer;
}

指向数组的指针

您也可以创建指向数组的指针。这可以通过以下方式完成。

int *pArray;
pArray = new int[6];

这将创建一个指针 pArray,并使其指向一个包含六个元素的数组。另一种不使用动态分配的方法如下。

int *pArray;
int MyArray[6];
pArray = &MyArray[0];

请注意,您可以简单地写 MyArray,而不是写 &MyArray[0]。当然,这只适用于数组,并且是 C/C++ 语言实现方式的结果。一个常见的陷阱是写 pArray = &MyArray;,但这不正确。如果您这样做,您最终会得到一个指向数组的指针(没错,是双重指针),这绝对不是您想要的。

使用指向数组的指针

一旦您有了指向数组的指针,如何使用它呢?好吧,假设您有一个指向整数数组的指针。指针最初会指向数组中的第一个值,如下面的示例所示。

#include <stdio.h>

void main()
{
    int Array[3];
    Array[0] = 10;
    Array[1] = 20;
    Array[2] = 30;

    int *pArray;
    pArray = &Array[0];

    printf("pArray points to the value %d\n", *pArray);
}

要使指针移到数组中的下一个值,我们可以说 pArray++。我们也可以,正如你们中的一些人可能已经猜到的,说 pArray + 2,这将使数组指针向前移动两个元素。需要小心的是,您知道数组的上限是什么(在本例中是 3),因为当您使用指针时,编译器无法检查您是否越过了数组的末尾。您很容易因此导致系统崩溃。这里是再次显示的示例,这次展示了我们设置的三个值。

#include <stdio.h>

void main()
{
    int Array[3];
    Array[0] = 10;
    Array[1] = 20;
    Array[2] = 30;

    int *pArray;
    pArray = &Array[0];

    printf("pArray points to the value %d\n", *pArray);
    pArray++;
    printf("pArray points to the value %d\n", *pArray);
    pArray++;
    printf("pArray points to the value %d\n", *pArray);
}

您也可以减去值,所以 pArray - 2 是从 pArray 当前指向的位置向前两个元素。但是,请确保您对指针进行加减运算,而不是对其值进行加减运算。这种使用指针和数组的运算在循环中使用时最有用,例如 forwhile 循环。

另外请注意,如果您有一个指向某个值的指针,例如 int* pNumberSet,您可以将其视为一个数组。例如,pNumberSet[0] 等同于 *pNumberSet;同样,pNumberSet[1] 等同于 *(pNumberSet + 1)

关于数组的最后一个警告是,如果您使用 new 为数组分配内存,如下面的示例所示。

int *pArray;
pArray = new int[6];

……您必须使用以下方法删除它。

delete[] pArray;

请注意 delete 后面的 []。这告诉编译器它正在删除整个数组,而不仅仅是单个元素。只要涉及数组,就必须使用此方法;否则,您将面临内存泄漏。

最后的话

最后一点:您不得删除您没有使用 new 分配的内存,如下面的示例所示。

void main()
{
    int number;
    int *pNumber = number;
    
    delete pNumber; // wrong - *pNumber wasn't allocated using new.
}

常见问题解答

问:为什么我对 newdelete 会收到“未定义符号”错误?

答:这最可能是由于您的源文件被编译器解释为纯 C 文件。newdelete 运算符是 C++ 的一项新功能。通常可以通过确保您的源文件名使用 **.cpp 扩展来解决此问题。

问:newmalloc 有什么区别?

答:new 是 C++ 中独有的关键字,现在是(除了使用 Windows 的内存分配例程之外)分配内存的标准方法。除非绝对必要,否则您不应在 C C++ 应用程序中使用 malloc。由于 malloc 不是为 C++ 的面向对象特性设计的,使用它为类分配内存将阻止类的构造函数被调用,这只是可能出现的问题的一个例子。由于使用 mallocfree 会带来问题,而且它们现在基本上已过时,因此本文档不再详细讨论它们。我建议尽可能避免使用它们。

问:我可以使用 freedelete 一起使用吗?

答:您应该使用与分配内存的例程相对应的例程来释放内存。例如,只对使用 malloc 分配的内存使用 free,只对使用 new 分配的内存使用 delete,依此类推。

参考文献

引用在某种程度上超出了本文档的范围。但是,由于阅读本文档的人经常问我关于引用的问题,我将简要讨论它们。它们与指针非常相关,因为在许多情况下,它们可以作为更简单的替代方案。如果您还记得上面提到的,我提到“&”(和号)在声明中除外时应读作“地址”。在声明中出现时,例如下面所示,它应读作“引用”。

int& Number = myOtherNumber;
Number = 25;

引用就像是 myOtherNumber 的指针,只不过它被自动解引用了。因此,它的行为就像它本身是实际值类型而不是指针类型一样。下面显示了使用指针的等效代码。

int* pNumber = &myOtherNumber;
*pNumber = 25;

指针和引用的另一个区别是您不能“重置”引用。也就是说,在声明之后,您不能更改它指向的内容。例如,下面的代码将输出“20”。

int myFirstNumber = 25;
int mySecondNumber = 20;
int &myReference = myFirstNumber;

myReference = mySecondNumber;

printf("%d", myFristNumber);

在类中,引用的值必须在构造函数中按以下方式设置。

CMyClass::CMyClass(int &variable) : m_MyReferenceInCMyClass(variable)
{
    // constructor code here
}

摘要

这个主题一开始很难掌握,所以值得至少看两遍:大多数人不会立即理解它。以下是主要要点。

  1. 指针是指向内存区域的变量。<lie>您通过在变量名前加一个星号(*)来定义指针(即 int *number)。
  2. 您可以通过在变量前加一个和号(&)来获取任何变量的地址,即 pNumber = &my_number
  3. 星号,除非在声明中(例如 int *number),应读作“指向的内存位置”。
  4. 和号,除非在声明中(例如 int &number),应读作“地址”。
  5. 您可以使用 new 关键字分配内存。
  6. 指针必须与您希望它们指向的变量类型相同;因此,int *number 不会指向 MyClass
  7. 您可以将指针传递给函数。
  8. 您必须使用 delete 关键字删除您分配的内存。
  9. 您可以使用 &array[0]; 获取指向现有数组的指针。
  10. 您必须使用 delete[] 删除动态分配的数组,而不能只使用 delete

这不是一份绝对完整的指针指南。还有一些其他内容我可以更详细地介绍,例如二级指针,还有一些我选择根本不介绍的内容,例如函数指针,我认为它们对于新手文章来说太复杂了。还有一些很少使用到的内容,新手最好不要被这些繁琐的细节所困扰。

就是这样!尝试运行此处提供的程序,并构思一些您自己的示例。

更新

  • 2002年6月29日 - 文章首次发布于 The Code Project
  • 2002年7月10日 - 添加了 FAQ,对文章进行了少量修改以提高清晰度,更新了放错位置的源代码片段,添加了关于引用的部分

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.