现在的位置: 首页 > 综合 > 正文

C++指针

2013年06月08日 ⁄ 综合 ⁄ 共 12266字 ⁄ 字号 评论关闭

 

这是一篇我所见过的关于指针的最好的入门级文章,它可使初学者在很短的时间内掌握复杂的指针操作。虽然,现在的Java、C#等语言已经取消了指针,但作为一个C++程序员,指针的直接操作内存,在数据操作方面有着速度快,节约内存等优点,仍是很多C++程序员的最爱。指针就像是一把良剑,就看你怎么去应用它!

 

什么是指针?

  其实指针就像是其它变量一样,所不同的是一般的变量包含的是实际的真实的数据,而指针是一个指示器,它告诉程序在内存的哪块区域可以找到数据。这是一个非常重要的概念,有很多程序和算法都是围绕指针而设计的,如链表。

开始学习

  如何定义一个指针呢?就像你定义一个其它变量一样,只不过你要在指针名字前加上一个星号。我们来看一个例子:

  下面这个程序定义了两个指针,它们都是指向整型数据。

int* pNumberOne;

int* pNumberTwo;

  你注意到在两个变量名前的“p”前缀了吗?这是程序员通常在定义指针时的一个习惯,以提高便程序的阅读性,表示这是个指针。现在让我们来初始化这两个指针:

pNumberOne = &some_number;

pNumberTwo = &some_other_number;

  &号读作“什么的地址”,它表示返回的是变量在内存中的地址而不是变量本身的值。在这个例子中,pNumberOne 等于some_number的地址,所以现在pNumberOne指向some_number。如果现在我们在程序中要用到some_number,我们就可以使用pNumberOne。

 

我们来学习一个例子:

  在这个例子中你将学到很多,如果你对指针的概念一点都不了解,我建议你多看几遍这个例子,指针是个很复杂的东西,但你会很快掌握它的。

  这个例子用以增强你对上面所介绍内容的了解。它是用C编写的(注:原英文版是用C写的代码,译者重新用C++改写写了所有代码,并在DEV C++ 和VC++中编译通过!)

#include <iostream.h>

void main()

{

    // 声明变量:

    int nNumber;

    int *pPointer;

    // 现在给它们赋值:

    nNumber = 15;

    pPointer = &nNumber;

   //打印出变量nNumber的值:

    cout<<"nNumber is equal to :"<< nNumber<<endl;

    // 现在通过指针改变nNumber的值:

    *pPointer = 25;

   //证明nNumber已经被上面的程序改变

   //重新打印出nNumber的值:

   cout<<"nNumber is equal to :"<<nNumber<<endl;

}

  通读一下这个程序,编译并运行它,务必明白它是怎样工作的。如果你完成了,准备好,开始下一小节。

 

 

陷井!

  试一下,你能找出下面这段程序的错误吗?

#include <iostream.h>

int *pPointer;

void SomeFunction();

{

     int nNumber;

     nNumber = 25;

     //让指针指向nNumber:

     pPointer = &nNumber;

}

void main()

{

      SomeFunction(); //为pPointer赋值

      //为什么这里失败了?为什么没有得到25

      cout<<"Value of *pPointer: "<<*pPointer<<endl;

}

  这段程序先调用了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 <iostream.h>

int *pPointer;

void SomeFunction()

{

      // 让指针指向一个新的整型

      pPointer = new int;

     *pPointer = 25;

}

void main()

{

      SomeFunction(); // 为pPointer赋值

      cout<<"Value of *pPointer: "<<*pPointer<<endl;

}

  通读这个程序,编译并运行它,务必理解它是怎样工作的。当SomeFunction 调用时,它分配了一个内存,并让pPointer指向它。这一次,当函数返回时,新的内存区域被保留下来,所以pPointer始终指着有用的信息,这是因为了动态分配。但是你再仔细读读上面这个程序,虽然它得到了正确结果,可仍有一个严重的错误。

分配了内存,别忘了回收

  太复杂了,怎么会还有严重的错误!其实要改正并不难。问题是:你动态地分配了一个内存空间,可它绝不会被自动删除。也就是说,这块内存空间会一直存在,直到你告诉电脑你已经使用完了。可结果是,你并没有告诉电脑你已不再需要这块内存空间了,所以它会继续占据着内存空间造成浪费,甚至你的程序运行完毕,其它程序运行时它还存在。当这样的问题积累到一定程度,最终将导致系统崩溃。所以这是很重要的,在你用完它以后,请释放它的空间,如:

delete pPointer;

  这样就差不多了,你不得不小心。在这你终止了一个有效的指针(一个确实指向某个内存的指针)。

  下面的程序,它不会浪费任何的内存:

#include <iostream.h>

int *pPointer;

void SomeFunction()

{

    // 让指针指向一个新的整型

    pPointer = new int;

    *pPointer = 25;

}

void main()

{

    SomeFunction(); //为pPointer赋值

    cout<<"Value of *pPointer: "<<*pPointer<<endl;

    delete pPointer;

} 

  只有一行与前一个程序不同,但就是这最后一行十分地重要。如果你不删除它,你就会制造一起“内存漏洞”,而让内存逐渐地泄漏。

  (译者:假如在程序中调用了两次SomeFunction,你又该如何修改这个程序呢?请读者自己思考)

传递指针到函数

 

 

  传递指针到函数是非常有用的,也很容易掌握。如果我们写一个程序,让一个数加上5,看一看这个程序完整吗?:

#include <iostream.h>

void AddFive(int Number)

{

      Number = Number + 5;

}

void main()

{

      int nMyNumber = 18;

      cout<<"My original number is "<<nMyNumber<<endl;

      AddFive(nMyNumber);

      cout<<"My new number is "<<nMyNumber<<endl;

      //得到了结果23吗?问题出在哪儿?

}

  问题出在函数AddFive里用到的Number是变量nMyNumber的一个副本而传递给函数,而不是变量本身。因此, " Number = Number + 5" 这一行是把变量的副本加了5,而原始的变量在主函数main()里依然没变。试着运行这个程序,自己去体会一下。

  要解决这个问题,我们就要传递一个指针到函数,所以我们要修改一下函数让它能接受指针:把'void AddFive(int Number)' 改成 'void AddFive(int* Number)' 。下面就是改过的程序,注意函数调用时要用&号,以表示传递的是指针:

#include <iostream.h>

void AddFive(int* Number)

{

     *Number = *Number + 5;

}

void main()

{

     int nMyNumber = 18;

     cout<<"My original number is "<<nMyNumber<<endl;

     AddFive(&nMyNumber);

     cout<<"My new number is "<<nMyNumber<<endl;

}

  试着自己去运行它,注意在函数AddFive的参数Number前加*号的重要性:它告诉编译器,我们是把指针所指的变量加5。而不并指针自己加5。

 

  最后,如果想让函数返回指针的话,你可以这么写:

int * MyFunction();

  在这句里,MyFunction返回一个指向整型的指针。

 

指向类的指针

  指针在类中的操作要格外小心,你可以用如下的办法定义一个类:

 

class MyClass

{

  public:

  int m_Number;

  char m_Character;

};

 

  接着你就可以定义一个MyClass 类的变量了:

MyClass thing;

  你应该已经知道怎样去定义一个指针了吧:

MyClass *thing;

 

  接着你可以分配个内存空间给它:

thing = new MyClass;

 

  注意,问题出现了。你打算怎样使用这个指针呢,通常你可能会写'thing.m_Number',但是thing是类吗,不,它是一个指向类的指针,它本身并不包含一个叫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[0] 也可以简写成 MyArray ,都表示是数组的第一个元素地址。但如果写成pArray = &MyArray可能就会出问题,结果是 pArray 指向的是指向数组的指针(在一维数组中尽管与&MyArray[0]相等),而不是你想要的,在多维数组中很容易出错。

在数组中使用指针

  一旦你定义了一个指向数组的指针,你该怎样使用它呢?让我们来看一个例子,一个指向整型数组的指针:

#include <iostream.h>

void main()

{       

int Array[3];

Array[0] = 10;

Array[1] = 20;

Array[2] = 30;

int *pArray;

pArray = &Array[0];

cout<<"pArray points to the value %d/n"<<*pArray<<endl;

}

  如果让指针指向数组元素中的下一个,可以用pArray++.也可以用你应该能想到的pArray + 1,都会让指针指向数组的下一个元素。要注意的是你在移动指针时,程序并不检查你是否已经移动地超出了你定义的数组,也就是说你很可能通过上面的简单指针加操作而访问到数组以外的数据,而结果就是,可能会使系统崩溃,所以请格外小心。

  当然有了pArray + 1,也可以有pArray - 1,这种操作在循环中很常用,特别是while循环中。

  另一个需要注意的是,如果你定义了一个指向整型数的指针:int* pNumberSet ,你可以把它当作是数组,如:pNumberSet[0] 和 *pNumberSet是相等的,pNumberSet[1]与*(pNumberSet + 1)也是相等的。

 

  在这一节的最后提一个警告:如果你用 new 动态地分配了一个数组,

int *pArray;

pArray = new int[6];

  别忘了回收,

delete[] pArray;

  这一句是告诉编译器是删除整个数组而不一个单独的元素。千万记住了。

后话

  还有一点要小心,别删除一个根本就没分配内存的指针,典型的是如果没用new分配,就别用delete:

void main()

{

  int number;

  int *pNumber = number;

  delete pNumber; // 错误 - *pNumber 没有用new动态分配内存.

}

常见问题解答

Q:为什么我在编译程序时老是在 new 和 delete语句中出现'symbol undefined' 错误?

A:new 和 delete都是C++在C上的扩展,这个错误是说编译器认为你现在的程序是C而不C++,当然会出错了。看看你的文件名是不是.cpp结尾。

Q:new 和 malloc有什么不同?

A:new 是C++中的关健字,用来分配内存的一个标准函数。如果没有必要,请不要在C++中使用malloc。因为malloc是C中的语法,它不是为面向对象的C++而设计的。

Q:我可以同时使用free 和 delete吗?

A:你应该注意的是,它们各自所匹配的操作不同。free只用在用malloc分配的内存操作中,而delete只用在用new分配的内存操作中。

引用(写给某些有能力的读者)

  这一节的内容不是我的这篇文章的中心,只是供某些有能力的读者参考。

  有些读者经常问我关于引用和指针的问题,这里我简要地讨论一下。

  在前面指针的学习中,我们知道(&)是读作“什么的地址”,但在下面的程序中,它是读作“什么的引用”

int& Number = myOtherNumber;

Number = 25;

  引用有点像是一个指向myOtherNumber的指针,不同的是它是自动删除的。所以他比指针在某些场合更有用。与上面等价的代码是:

int* pNumber = &myOtherNumber;

*pNumber = 25;

  指针与引用另一个不同是你不能修改你已经定义好的引用,也就是说你不能改变它在声明时所指的内容。举个例子:

int myFirstNumber = 25;

int mySecondNumber = 20;

int &myReference = myFirstNumber;

myReference = mySecondNumber;//这一步能使myReference 改变吗?

cout<<myFristNumber<<endl;//结果是20还是25?

  当在类中操作时,引用的值必须在构造函数中设定,例:

CMyClass::CMyClass(int &variable) : m_MyReferenceInCMyClass(variable)

{

  // constructor code here

}

总结

  这篇文章开始可能会较难掌握,所以最好是多读几遍。有些读者暂时还不能理解,在这儿我再做一个简要的总结:

  指针是一个指向内存区域的变量,定义时在变量名前加上星号(*)(如:int *number)。

  你可以得到任何一个变量的地址,只在变量名前加上&(如:pNumber = &my_number)。

  你可以用'new' 关键字动态分配内存。指针的类型必须与它所指的变量类型一样(如:int *number 就不能指向 MyClass)。

  你可以传递一个指针到函数。必须用'delete'删除你动态分配的内存。

  你可以用&array[0]而让指针指向一个数组。

  你必须用delete[]而不是delete来删除动态分配的数组。

  文章到这儿就差不多结束了,但这些并不就是指针所有的东西,像指向指针的指针等我还没有介绍,因为这些东西对于一个初学指针的人来说还太复杂了,我不能让读者一开始就被太复杂的东西而吓走了。好了,到这儿吧,试着运行我上面写的小程序,也多自己写写程序,你肯定会进步不小的!

 

源文档 <http://hi.baidu.com/surgeon/blog/item/7d1849360888e1dba3cc2b5b.html>

 

 

 

 

介绍

  曾经碰到过让你迷惑不解、类似于int * (* (*fp1) (int) ) [10];这样的变量声明吗?本文将由易到难,一步一步教会你如何理解这种复杂的C/C++声明:我们将从每天都能碰到的较简单的声明入手,然后逐步加入const修饰符和typedef,还有函数指针,最后介绍一个能够让你准确地理解任何C/C++声明的“右左法则”。需要强调一下的是,复杂的C/C++声明并不是好的编程风格;我这里仅仅是教你如何去理解这些声明。注意:为了保证能够在同一行上显示代码和相关注释,本文最好在至少1024x768分辨率的显示器上阅读。

  基础

  让我们从一个非常简单的例子开始,如下:

  int n;

  这个应该被理解为“declare n as an int”(n是一个int型的变量)。

  接下去来看一下指针变量,如下:

  int *p;

  这个应该被理解为“declare p as an int *”(p是一个int *型的变量),或者说p是一个指向一个int型变量的指针。我想在这里展开讨论一下:我觉得在声明一个指针(或引用)类型的变量时,最好将*(或&)写在紧靠变量之前,而不是紧跟基本类型之后。这样可以避免一些理解上的误区,比如:

  int*    p,q;

  第一眼看去,好像是p和q都是int*类型的,但事实上,只有p是一个指针,而q是一个最简单的int型变量。

  我们还是继续我们前面的话题,再来看一个指针的指针的例子:

  char **argv;

  理论上,对于指针的级数没有限制,你可以定义一个浮点类型变量的指针的指针的指针的指针...

  再来看如下的声明:

 

int RollNum[30][4];

int (*p)[4]=RollNum;

int *q[5];

  这里,p被声明为一个指向一个4元素(int类型)数组的指针,而q被声明为一个包含5个元素(int类型的指针)的数组。

 

  另外,我们还可以在同一个声明中混合实用*和&,如下:

int **p1; // p1 is a pointer    to a pointer    to an int.

int *&p2; // p2 is a reference to a pointer    to an int.

int &*p3; // ERROR: Pointer    to a reference is illegal.

int &&p4; // ERROR: Reference to a reference is illegal.

  注:p1是一个int类型的指针的指针;p2是一个int类型的指针的引用;p3是一个int类型引用的指针(不合法!);p4是一个int类型引用的引用(不合法!)。

  const修饰符

  当你想阻止一个变量被改变,可能会用到const关键字。在你给一个变量加上const修饰符的同时,通常需要对它进行初始化,因为以后的任何时候你将没有机会再去改变它。例如:

const int n=5;

int const m=10;

  上述两个变量n和m其实是同一种类型的--都是const int(整形恒量)。因为C++标准规定,const关键字放在类型或变量名之前等价的。我个人更喜欢第一种声明方式,因为它更突出了const修饰符的作用。

  当const与指针一起使用时,容易让人感到迷惑。例如,我们来看一下下面的p和q的声明:

const int *p;

int const *q;

  他们当中哪一个代表const int类型的指针(const直接修饰int),哪一个代表int类型的const指针(const直接修饰指针)?实际上,p和q都被声明为const int类型的指针。而int类型的const指针应该这样声明:

int * const r= &n; // n has been declared as an int

  这里,p和q都是指向const int类型的指针,也就是说,你在以后的程序里不能改变*p的值。而r是一个const指针,它在声明的时候被初始化指向变量n(即r=&n;)之后,r的值将不再允许被改变(但*r的值可以改变)。

  组合上述两种const修饰的情况,我们来声明一个指向const int类型的const指针,如下:

const int * const p=&n // n has been declared as const int

  下面给出的一些关于const的声明,将帮助你彻底理清const的用法。不过请注意,下面的一些声明是不能被编译通过的,因为他们需要在声明的同时进行初始化。为了简洁起见,我忽略了初始化部分;因为加入初始化代码的话,下面每个声明都将增加两行代码。

char ** p1;            //      pointer to      pointer to      char

const char **p2;          //      pointer to      pointer to const char

char * const * p3;         //      pointer to const pointer to      char

const char * const * p4;      //      pointer to const pointer to const char

char ** const p5;         // const pointer to      pointer to      char

const char ** const p6;      // const pointer to      pointer to const char

char * const * const p7;      // const pointer to const pointer to      char

const char * const * const p8; // const pointer to const pointer to const char

  注:p1是指向char类型的指针的指针;p2是指向const char类型的指针的指针;p3是指向char类型的const指针;p4是指向const char类型的const指针;p5是指向char类型的指针的const指针;p6是指向const char类型的指针的const指针;p7是指向char类型const指针的const指针;p8是指向const char类型的const指针的const指针。

  typedef的妙用

  typedef给你一种方式来克服“*只适合于变量而不适合于类型”的弊端。你可以如下使用typedef:

typedef char * PCHAR;

PCHAR p,q;

  这里的p和q都被声明为指针。(如果不使用typedef,q将被声明为一个char变量,这跟我们的第一眼感觉不太一致!)下面有一些使用typedef的声明,并且给出了解释:

typedef char * a; // a is a pointer to a char

typedef a b();     // b is a function that returns

            // a pointer to a char

typedef b *c;     // c is a pointer to a function

            // that returns a pointer to a char

typedef c d();     // d is a function returning

            // a pointer to a function

            // that returns a pointer to a char

typedef d *e;     // e is a pointer to a function

            // returning a pointer to a

            // function that returns a

            // pointer to a char

e var[10];       // var is an array of 10 pointers to

            // functions returning pointers to

            // functions returning pointers to chars.

  typedef经常用在一个结构声明之前,如下。这样,当创建结构变量的时候,允许你不使用关键字struct(在C中,创建结构变量时要求使用struct关键字,如struct tagPOINT a;而在C++中,struct可以忽略,如tagPOINT b)。

typedef struct tagPOINT

{

    int x;

    int y;

}POINT;

POINT p; /* Valid C code */

  函数指针

  函数指针可能是最容易引起理解上的困惑的声明。函数指针在DOS时代写TSR程序时用得最多;在Win32和X-Windows时代,他们被用在需要回调函数的场合。当然,还有其它很多地方需要用到函数指针:虚函数表,STL中的一些模板,Win NT/2K/XP系统服务等。让我们来看一个函数指针的简单例子:

int (*p)(char);

  这里p被声明为一个函数指针,这个函数带一个char类型的参数,并且有一个int类型的返回值。另外,带有两个float类型参数、返回值是char类型的指针的指针的函数指针可以声明如下:

char ** (*p)(float, float);

  那么,带两个char类型的const指针参数、无返回值的函数指针又该如何声明呢?参考如下:

void * (*a[5])(char * const, char * const);

  “右左法则”[重要!!!]

The right-left rule: Start reading the declaration from the innermost parentheses, go right, and then go left. When you encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole declaration has been parsed.

  这是一个简单的法则,但能让你准确理解所有的声明。这个法则运用如下:从最内部的括号开始阅读声明,向右看,然后向左看。当你碰到一个括号时就调转阅读的方向。括号内的所有内容都分析完毕就跳出括号的范围。这样继续,直到整个声明都被分析完毕。

  对上述“右左法则”做一个小小的修正:当你第一次开始阅读声明的时候,你必须从变量名开始,而不是从最内部的括号。

  下面结合例子来演示一下“右左法则”的使用。

int * (* (*fp1) (int) ) [10];

  阅读步骤:

1. 从变量名开始 -------------------------------------------- fp1

2. 往右看,什么也没有,碰到了),因此往左看,碰到一个* ------ 一个指针

3. 跳出括号,碰到了(int) ----------------------------------- 一个带一个int参数的函数

4. 向左看,发现一个* --------------------------------------- (函数)返回一个指针

5. 跳出括号,向右看,碰到[10] ------------------------------ 一个10元素的数组

6. 向左看,发现一个* --------------------------------------- 指针

7. 向左看,发现int ----------------------------------------- int类型

  总结:fp1被声明成为一个函数的指针,该函数返回指向指针数组的指针.

  再来看一个例子:

int *( *( *arr[5])())();

  阅读步骤:

1. 从变量名开始 -------------------------------------------- arr

2. 往右看,发现是一个数组 ---------------------------------- 一个5元素的数组

3. 向左看,发现一个* --------------------------------------- 指针

4. 跳出括号,向右看,发现() -------------------------------- 不带参数的函数

5. 向左看,碰到* ------------------------------------------- (函数)返回一个指针

6. 跳出括号,向右发现() ------------------------------------ 不带参数的函数

7. 向左,发现* --------------------------------------------- (函数)返回一个指针

8. 继续向左,发现int --------------------------------------- int类型

  总

抱歉!评论已关闭.