本文摘自www.codeproject.com,原文名为《A Beginner's Guide to Pointers 》,学习之余做了翻 译,如有转载请标明出处。
什么是指针?
本质上来说,指针变量和其他变量是一样的。但是,它的不同点在于指针变量并不是包含实际的数据,而是包含一个指向其他数据存放在内存的地址。这是一个很重要的概念。很多程序的设计和思想都是基于指针的,例如链表。
开始
如何定义一个指针变量? 除了要加‘*’在变量名的前面,它和别的变量定义是相同的。因此,举一个范例来说明一下,下面的的代码创建了两个指向整型的指针变量。
int* pNumberOne;
int* pNumberTwo;
注意到这两个变量名都是以字母‘p’打头的吗?这是命名的惯例,以表明这是一个指针变量。现在,让这些指针变量确切地指向一些内容。
pNumberOne = &some_number;
pNumberTwo = &some_other_number;
取址符‘&’,应该念成“the address of”,它返回某个变量在内存中的地址,而不是变量本身。因此,在上面的例子中pNumberOne赋值为some_number的地址,所以pNumberOne现在指向some_number。
现在如果我们想引用some_number的地址,只要用pNumberOne就行了。 反过来如果你想通过pNumberOne来引用some_number ,必须用“*pNumberOne”语句来实现。 星号符‘*’它应该念成“the memory location pointed to by”。不过也有例外,就是在初始化申明时,例如“int *pNumber”。
现在为止我们已经学了些什么: 一个例子
呀! 已经涉及很多了。我提醒你如果你对这些概念还不是很理解,就应该再仔细通读一遍。 指针是较为复杂的一块内容,因此需要投入一定的精力才能掌握它。下面用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函数。在函数 SomeFunction中,创建了变量 nNumber 并让指针pPointer指向它。但是问题就出在这里。nNumber 是局部变量,因此在跳出函数时会被删除。对于局部变量来说,当程序执行离开它们被定义的内存区域时,局部变量将被删除释放。这意味着当程序执行从 SomeFunction返回main()时,局部变量将被删除,其所对应内存空间将被释放。因此 pPointer 正在指向过去是的、但现在已经不再属于该程序的内存空间。如果你对这一点不是很清楚,最好要去复习一下局部变量和全局变量以及它们的作用域的内容。这个概念也很重要。
那么,如何来解决上面的问题呢? 答案是运用动态分配(dynamic allocation)技术。请明确一点,这在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;
这就是释放内存需要的所有操作。然而你不得不非常小心,确保你传递的是一个有效的指针,具体地说,该指针必须指向你曾经分配出去的那些内存空间,而且不是任何垃圾内存(已被释放的内存)。尝试delete那些已被释放的内存是十分危险的,这能够导致程序崩溃。 因此下面又有一个例子,这个更新的版本将不会浪费任何内存空间。
#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;//Attention!
}
尽管只是一行的不同,但这一行非常重要。如果不释放(delete)这段内存,就有可能导致“内存泄露(memory leak)”,内存将慢慢“漏光”,不能重复使用,直到整个程序被关闭。
把指针传递给函数
把指针传递给函数的能力是很重要的,但是很容易掌握。如果我们本意是想给一个数加5,可能会写出类似下面的代码:
#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”这行代码只是给变量nMyNumber的拷贝加5 , 对在 main()函数中的原始变量并没有产生影响。可以通过运行上面的程序来证明。
为了避免出现这一问题,我们需要传递一个指向该变量在内存中地址的指针给函数, 但是还需要改变函数的参数形式以表明它需要传递一个指向变量的指针,而不再是某个变量。为做到这点,我们可以把函数 void AddFive(int Number)修改为 void AddFive(int* Number),加了一个星号(*)。下面的还是这个例子,但是作了相应的修改。注意到我们必须保证传递的是 nMyNumber的地址,不是它本身了吗?。完成这个操作还需要加上取址符‘&’,可以读成“the address of”。
#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指向的数加5,而不是指针本身加5。最后一件事是你还能返回(return)这些指针变量,像这样:
int * MyFunction();
在这个例子中, 函数MyFunction 返回一个指向整型变量的指针。
指向类的指针
关于指针有一大堆需要注意的地方,其中一点就是结构体或类。可以通过下面的方法来定义一个类:
class MyClass
{
public:
int m_Number;
char m_Character;
};
然后你就能定义MyClass 类型的变量(也称对象):
MyClass thing;
这些你应该已经知道。如果不是,就要试着学习相关内容。定义一个指向 MyClass类型的指针:
MyClass *thing;
...这看起来是预料之中的。然后你可能会分配一些内存空间,并使该指针指向这段内存:
Collapse
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,并使它指向包含6元素的数组。还有其他方法, 不使用动态分配,如下所诉:
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所指当前值的前两个数。不过,你要确保你加或减的是指针,而不是组数元素的值。这种处理指针的方法在(while和for)这样的循环中用到数组时时非常有用的。
需要注意的是,如果你有指向某个值的指针,例如,int* pNumberSet,可以像数组一样来对他进行操作。例如,pNumberSet[0]等价于 *pNumberSet;相似的, pNumberSet[1]等价于 *(pNumberSet + 1)。
对于数组,最后一个需要注意的是如果你用new分配内存空间,就像下面的例子:
int *pArray;
pArray = new int[6];
//必须用下面的方法来释放:
delete[] pArray;
注意 delete之后“ []”。这告诉编译器要释放整个数组的空间,而不仅仅是一个元素。只要是涉及到数组,就必须要用这个方法来处理,否者最终会导致内存泄露。
最后的话
最后需要注意的地方:你绝不能delete一个不是用new分配的内存空间,像下面的例子:
void main()
{
int number;
int *pNumber = number;
delete pNumber; // wrong - *pNumber wasn't allocated using new.
}
常见问题FAQ
Q: 为什么在使用new和delete时出现未定义符号(“symbol undefined”)的错误?
A: 这最有可能是编译器把你的源文件当做纯C文件在解释了。new 和delete 操作符是C++的特点。一般情况下可以通过把你的源文件扩展为“.cpp”文件来解决。
Q: new 和malloc有什么不同之处?
A: new 是只在C++中出现的关键字,而且现在已经是分配内存空间的标准处理方法了(除了在Windows编程中的分配内存自成一套以外)。你绝不能在C和C++都出现的应用程序中使用malloc ,除非它确实是必需的。因为 malloc 它的设计不支持C++面向对象的特点,用它为类(Class)分配内存将不会调用构造函数,而且这还仅仅是引发的众多问题中的一个。因为malloc 和 free设计的目的和思路已过时,所以使用它们所引发的一些问题的细节本文将不作讨论。在任何情况下我都不建议使用它们。
Q: 我能把 free 和 delete 一起使用吗?
A: 你释放内存的方式必须和分配这段内存的方式配对。举例来说,用free只能释放是malloc分配出去的内存,delete只能用在用new分配的内存上进行操作,等等。
引用
引用,从某种角度讲,已超出本文的范围了。但是,因为被很多人问了N次关于它的问题了,我简单地论述一下。它和指针有非常大的联系,多数情况下,它可以用来当做一个简单的替换物(alternative)。回想一下上面内容,我提到在变量申明时符号“&”可以念成“the address of”。在这里,像下面例子中的申明,符号“&”可以念成“a reference to”。
Collapse
int& Number = myOtherNumber;
Number = 25;
引用像一个指向 myOtherNumber的指针, 除了它是自动间接引用的。 所以引用表现得就像是它指向的数本身,而不是指针类型的。用指针来实现相同的效果,代码如下:
Collapse
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. 指针本身也是变量,它指向内存中的某段地址。你可以通过在变量名前加星号符‘*’来定义一个指针(比如 int *number)。
2. 通过在变量名前加取址符(&),你可以得到任何一个变量在内存的中的地址(比如 pNumber = &my_number)。
3. 除了在申明时,星号符(*)(比如 int *number)都应该念成“the memory location pointed to by”。
4. 除了在申明时,取址符(‘&’) (比如 int &number)都应念成“the address of”。
5. 你可以用关键字new来分配内存空间。
6. 指针必须只能指向与之类型相同的变量类型;因此,int *number只能指向整型变量,不能指向 MyClass类型的变量。
7. 你能向函数传递指针。
8. 你必须用delete来删除释放你分配出去的内存空间。
9. 通过语句“&array[0]”你能得到指数组的指针。
10. 你必须用delete[]来删除释放用动态分配创建的数组,而不仅仅是delete。
本文算不上是一篇真正意义上的指针介绍。还有一些其他内容,比如指向指针的指针,指向函数的指针,本想具体展开,但考虑到作为一篇入门级的文章,这些内容显得过于复杂了。还有些内容,对于初学者来说很少遇到,可以不于理会,以免被这些很难理解的细节问题搞混。
好了。我的呈现到此为止!赶快用自己的例子动手尝试一下吧。