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

指针(二)

2018年02月12日 ⁄ 综合 ⁄ 共 21798字 ⁄ 字号 评论关闭

 

C/C++中数组和指针类型的关系
 一个整数类型数组如下进行定义:

int a[]={1,2,3,4};
  如果简单写成:

a;//数组的标识符名称
  这将代表的是数组第一个元素的内存地址,a;就相当于&a[0],它的类型是数组元素类型的指针,在这个例子中它的类型就是int*

  如果我们想访问第二个元素的地址我们可以写成如下的两种方式:

&a[1];

a+1//注意这里的表示就是将a数组的起始地址向后进一位,移动到第二个元素的地址上也就是a[0]到a[1]的过程!
  数组名称和指针的关系其实很简单,其实数组名称代表的是数组的第一个元素的内存地址,这和指针的道理是相似的!

  下面我们来看一个完整的例子,利用指针来实现对数组元素的循环遍历访问!

#include <iostream>
using namespace std;

void main(void)
{
int a[2]={1,2};

int *pb=a; //定义指针*pb的地址为数组a的开始地址

int *pe=a+2; //定义指针*pb的地址为数组a的结束地址

cout << a << "|" << a[0] << "|" << *(a+1) << "|" << pb << "|" << *pb <<endl;

while (pb!=pe) //利用地址进行逻辑判断是否到达数组的结束地址
{
cout << *pb << endl;
pb++; //利用递增操作在循环中将pb的内存地址不断向后递增
}
cin.get();
}
C/C++中字符指针数组及指向指针的指针的含义

就指向指针的指针,很早以前在说指针的时候说过,但后来发现很多人还是比较难以理解,这一次我们再次仔细说一说指向指针的指针。

  先看下面的代码,注意看代码中的注解:

#include <iostream>
#include <string>
using namespace std;

void print_char(char* array[],int len);//函数原形声明

void main(void)
{
//-----------------------------段1-----------------------------------------
char *a[]={"abc","cde","fgh"};//字符指针数组
char* *b=a;//定义一个指向指针的指针,并赋予指针数组首地址所指向的第一个字符串的地址也就是abc\0字符串的首地址
cout<<*b<<"|"<<*(b+1)<<"|"<<*(b+2)<<endl;
//-------------------------------------------------------------------------

//-----------------------------段2-----------------------------------------
char* test[]={"abc","cde","fgh"};//注意这里是引号,表示是字符串,以后的地址每加1就是加4位(在32位系统上)
int num=sizeof(test)/sizeof(char*);//计算字符串个数
print_char(test,num);
cin.get();
//-------------------------------------------------------------------------
}

void print_char(char* array[],int len)//当调用的时候传递进来的不是数组,而是字符指针他每加1也就是加上sizeof(char*)的长度

{
for(int i=0;i<len;i++)
{
cout<<*array++<<endl;
}
}
  下面我们来仔细说明一下字符指针数组和指向指针的指针,段1中的程序是下面的样子:

char *a[]={"abc","cde","fgh"};
char* *b=a;
cout<<*b<<"|"<<*(b+1)<<"|"<<*(b+2)<<endl;
  char *a[]定义了一个指针数组,注意不是char[], char[]是不能同时初始化为三个字符的,定义以后的a[]其实内部有三个内存位置,分别存储了abc\0,cde\0,fgh\0,三个字符串的起始地址,而这三个位置的内存地址却不是这三个字符串的起始地址,在这个例子中a[]是存储在栈空间内的,而三个字符串却是存储在静态内存空间内的const区域中的,接下去我们看到了char* *b=a;这里是定义了一个指向指针的指针,如果你写成char *b=a;那么是错误的,因为编译器会返回一个无法将char*
*[3]转换给char *的错误,b=a的赋值,实际上是把a的首地址赋给了b,由于b是一个指向指针的指针,程序的输出cout<<*b<<"|"<<*(b+1)<<"|"<<*(b+2)<<endl;

  结果是

abc
cde
fgh
  可以看出每一次内存地址的+1操作事实上是一次加sizeof(char*)的操作,我们在32位的系统中sizeof(char*)的长度是4,所以每加1也就是+4,实际上是*a[]内部三个位置的+1,所以*(b+1)的结果自然就是cde了,我们这时候可能会问,为什么输出是cde而不是c一个呢?答案是这样的,在c++中,输出字符指针就是输出字符串,程序会自动在遇到\0后停止.

  我们最后分析一下段2中的代码,段2中我们调用了print_array()这个函数,这个函数中形式参数是char *array[]和代码中的char *test[]一样,同为字符指针,当你把参数传递过来的时候,事实上不是把数组内容传递过来,test的首地址传递了进来,由于array是指针,所以在内存中它在栈区,具有变量一样的性质,可以为左值,所以我们输出写成了,cout<<*array++<<endl;当然我们也可以改写为cout<<array[i]<<endl,这里在循环中的每次加1操作和段1代码总的道理是一样的,注意看下面的图!

  到这里这两个非常重要的知识点我们都说完了,说归说,要想透彻理解希望读者多动手,多观察,熟能生巧。

  下面是内存结构示意图:

C/C++中利用空指针简化代码,提高效率
这里的写法,可以避免使用 for 循环,减少栈空间内存的使用和减少运行时的计算开销!

#include <iostream>
#include <string>
using namespace std;

void print_char(char* array[]);//函数原形声明

void main(void)
{
char* test[]={"abc","cde","fgh",NULL};//这里添加一个NULL,表示不指向任何地址,值为0
print_char(test);
cin.get();
}

void print_char(char* array[])
{
while(*array!=NULL)
{
cout<<*array++<<endl;
}
}

C/C++中函数指针的含义
函数存放在内存的代码区域内,它们同样有地址,我们如何能获得函数的地址呢?

  如果我们有一个int test(int a)的函数,那么,它的地址就是函数的名字,这一点如同数组一样,数组的名字就是数组的起始地址。

  定义一个指向函数的指针用如下的形式,以上面的test()为例:

int (*fp)(int a);//这里就定义了一个指向函数的指针

  函数指针不能绝对不能指向不同类型,或者是带不同形参的函数,在定义函数指针的时候我们很容易犯如下的错误。

int *fp(int a);//这里是错误的,因为按照结合性和优先级来看就是先和()结合,然后变成了一个返回整形指针的函数了,而不是函数指针,这一点尤其需要注意!

  下面我们来看一个具体的例子:

#include <iostream>
#include <string>
using namespace std;

int test(int a);

void main(int argc,char* argv[])
{
cout<<test<<endl;//显示函数地址
int (*fp)(int a);
fp=test;//将函数test的地址赋给函数学指针fp
cout<<fp(5)<<"|"<<(*fp)(10)<<endl;
//上面的输出fp(5),这是标准c++的写法,(*fp)(10)这是兼容c语言的标准写法,两种同意,但注意区分,避免写的程序产生移植性问题!
cin.get();
}

int test(int a)
{
return a;
}
  typedef定义可以简化函数指针的定义,在定义一个的时候感觉不出来,但定义多了就知道方便了,上面的代码改写成如下的形式:

#include <iostream>
#include <string>
using namespace std;

int test(int a);

void main(int argc,char* argv[])
{
cout<<test<<endl;
typedef int (*fp)(int a);//注意,这里不是生命函数指针,而是定义一个函数指针的类型,这个类型是自己定义的,类型名为fp
fp fpi;//这里利用自己定义的类型名fp定义了一个fpi的函数指针!
fpi=test;
cout<<fpi(5)<<"|"<<(*fpi)(10)<<endl;
cin.get();
}

int test(int a)
{
return a;
}
  函数指针同样是可以作为参数传递给函数的,下面我们看个例子,仔细阅读你将会发现它的用处,稍加推理可以很方便我们进行一些复杂的编程工作。

//-------------------该例以上一个例子作为基础稍加了修改-----------------------------
#include <iostream>
#include <string>
using namespace std;

int test(int);

int test2(int (*ra)(int),int);

void main(int argc,char* argv[])
{
cout<<test<<endl;
typedef int (*fp)(int);
fp fpi;
fpi=test;//fpi赋予test 函数的内存地址

cout<<test2(fpi,1)<<endl;//这里调用test2函数的时候,这里把fpi所存储的函数地址(test的函数地址)传递了给test2的第一个形参

cin.get();
}

int test(int a)
{
return a-1;
}

int test2(int (*ra)(int),int b)//这里定义了一个名字为ra的函数指针
{
int c=ra(10)+b;//在调用之后,ra已经指向fpi所指向的函数地址即test函数
return c;
}
  利用函数指针,我们可以构成指针数组,更明确点的说法是构成指向函数的指针数组,这么说可能就容易理解的多了。

#include <iostream>
#include <string>
using namespace std;

void t1(){cout<<"test1";}
void t2(){cout<<"test2";}
void t3(){cout<<"test3";}
void main(int argc,char* argv[])
{
void* a[]={t1,t2,t3};
cout<<"比较t1()的内存地址和数组a[0]所存储的地址是否一致"<<t1<<"|"<<a[0]<<endl;

cout<<a[0]();//错误!指针数组是不能利用数组下标操作调用函数的

typedef void (*fp)();//自定义一个函数指针类型
fp b[]={t1,t2,t3}; //利用自定义类型fp把b[]定义趁一个指向函数的指针数组
b[0]();//现在利用指向函数的指针数组进行下标操作就可以进行函数的间接调用了;
cin.get();
}
  仔细看上面的例子可能不用我多说大家也会知道是怎么一会事情了,最后我们做一个重点小结,只要记住这一点,对于理解利用函数指针构成数组进行函数间接调用就很容易了!

void* a[]={t1,t2,t3};
cout<<"比较t1()的内存地址和数组a[0]所存储的地址是否一致"<<t1<<"|"<<a[0]<<endl;

cout<<a[0]();//错误!指针数组是不能利用数组下标操作调用函数的
  上面的这一小段中的错误行,为什么不能这么调用呢?

  前一篇教程我们已经说的很清楚了,不过在这里我们还是复习一下概念,指针数组元素所保存的只是一个内存地址,既然只是个内存地址就不可能进行a[0]()这样地址带括号的操作,而函数指针不同它是一个例外,函数指针只所以这么叫它就是因为它是指向函数指向内存的代码区的指针,它被系统授予允许与()括号操作的权利,进行间接的函数调用,既然函数指针允许这么操作,那么被定义成函数指针的数组就一定是可以一样的操作的。

关于指针和内存的几个问题
一、"delete p" 会删去 "p" 指针,还是它指到的资料,"*p" ?

该指针指到的资料。"delete" 真正的意思是:「删去指针指到的东西」(delete the thing pointed to by)。同样的英文误用也发生在 C 语言的「释放」指标所指向的记忆体("free(p)"真正的意思是:"free_the_stuff_pointed_to_by(p)" )。

二、能 "free()" 掉由 "new" 配置到的、"delete" 掉由 "malloc()" 配置到的记忆体吗?

不行。在同一个程式里,使用 malloc/free 及 new/delete 是完全合法、合理、安全的;但 free 掉由 new 配置到的,或 delete 掉由 malloc 配置到的指标则是不合法、不合理的。

三、为什麽该用 "new" 而不是 malloc() ?

建构子/解构子、型别安全性、可被覆盖(overridability)。建构子/解构子:和 "malloc(sizeof(Fred))" 不同,"new Fred()" 还会去呼叫Fred 的建构子。同理,"delete p" 会去呼叫 "*p" 的解构子。

型别安全性:malloc() 会传回一个不具型别安全的 "void*",而 "new Fred()" 则会传回正确型态的指标(一个 "Fred*")。
可被覆盖:"new" 是个可被物件类别覆盖的运算子,而 "malloc" 不是以「各个类别」作为覆盖的基准。

四、为什麽 C++ 不替 "new" 及 "delete" 搭配个 "realloc()" ?

避免你产生意外。当 realloc() 要拷贝配置区时,它做的是「逐位元 bitwise」的拷贝,这会弄坏大
部份的 C++ 物件。不过 C++ 的物件应该可以自我拷贝才对:用它们自己的拷贝建构子或设定运算子。

五、该怎样配置/释放阵列?

用 new[] 和 delete[] :

Fred* p = new Fred[100];
//...
delete [] p;

每当你在 "new" 运算式中用了 "[...]" 的话,你就 *!*必须*!* 在 "delete" 陈述中使用 "[]" 。这语法是必要的,因为「指向单一元素的指标」与「指向一个阵列的指标」在语法上并无法区分开来。

六、万一我忘了将 "[]" 用在 "delete" 由 "new Fred[n]" 配置到的阵列,会发生什麽事?

灾难。这是程式者的--而不是编译器的--责任,去确保 new[] 与 delete[] 的正确配对。若你弄错了,编译器不会产生任何编译期或执行期的错误讯息。堆积(heap)被破坏是最可能的结局,或是更糟的,你的程式会当掉。

七、成员函数做 "delete this" 的动作是合法的(并且是好的)吗?

只要你小心的话就没事。所谓的「小心」是:
1) 你得 100% 确定 "this" 是由 "new" 配置来的(而非 "new[]",亦非自订的 "new" 版本,一定要是最原始的 "new")。
2) 你得 100% 确定该成员函数是此物件最後一个会去呼叫的。
3) 做完自杀的动作 ("delete this;") 後,你不能再去碰 "this" 的物件了,包括资料及运作行为在内。
4) 做完自杀的动作 ("delete this;") 後,你不能再去碰 "this" 指标了。换句话说,你不能查看它、将它与其他指标或是 NULL 相比较、印出其值、对它转型、对它做任何事情。

很自然的,这项警告也适用於:当 "this" 是个指向基底类别的指标,而解构子不是virtual 的场合。

八、该怎麽用 new 来配置多维阵列?

有很多方法,端视你对阵列大小的伸缩性之要求而定。极端一点的情形,如果你在编译期就知道所有阵列的维度,你可以静态地配置(就像 C 一样):

class Fred { /*...*/ };

void manipulateArray()
{
Fred matrix[10][20];

//使用 matrix[i][j]...

//不须特地去释放该阵列
}

另一个极端情况,如果你希望该矩阵的每个小块都能不一样大,你可以在自由记忆体里配置之:

void manipulateArray(unsigned nrows, unsigned ncols[])
//'nrows' 是该阵列之列数。
//所以合法的列数为 (0, nrows-1) 开区间。
//'ncols[r]' 则是 'r' 列的行数 ('r' 值域为 [0..nrows-1])。
{
Fred** matrix = new Fred*[nrows];
for (unsigned r = 0; r < nrows; ++r)
matrix[r] = new Fred[ ncols[r] ];

//使用 matrix[i][j]...

//释放就是配置的反动作:
for (r = nrows; r > 0; --r)
delete [] matrix[r-1];
delete [] matrix;
}

九、怎样确保某类别的物件都是用 "new" 建立的,而非区域或整体/静态变数?

确定该类别的建构子都是 "private:" 的,并定义个 "friend" 或 "static" 函数,来传回一个指向由 "new" 建造出来的物件(把建构子设成 "protected:",如果你想要有衍生类别的话)。

class Fred { //只允许 Fred 动态配置出来
public:
static Fred* create() { return new Fred(); }
static Fred* create(int i) { return new Fred(i); }
static Fred* create(const Fred& fred) { return new Fred(fred); }
private:
Fred();
Fred(int i);
Fred(const Fred& fred);
virtual ~Fred();
};

main()
{
Fred* p = Fred::create(5);
...
delete p;
}

浅谈指针的特点

什么是指针?

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

开始学习

  如何定义一个指针呢?就像你定义一个其它变量一样,只不过你要在指针名字前加上一个星号。我们来看一个例子:
  下面这个程序定义了两个指针,它们都是指向整型数据。

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来删除动态分配的数组。

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

问与答(指针)

 1、什么是指针
  指针是一种数据类型,与其它的数据类型不同的是指针是一种“用来存放地址值的”变量。举一个简单的例子:
如果定义了一个整型变量,根据整型变量的特点,它可以存放的数是整数。
如:int a; a=100; 这样就把整型常量赋给了变量a。但是如果写成这样:a=123.33;就会出问题,最后输出变量a的值结果是123。现在说到指针,其实地址值也是一个整型数,如某某变量的地址值为36542,说明这个变量被分配在内存地址值为36542的地方。能不能这样进行推理,既然地址值也是整型数,整型变量正好可以用来存放整型数,那不是一个整型变量可以用来存放地址的值吗。程序写成下面这样:
  int a,b;
a=&b;
很明显,这样写是错误的。原因在于不能简单地把地址理解为整型数。
应有这样的对应关系: 地址值<--->指针;  整型数<--->int 型变量。
所以有这样的说法:“指针就是地址”(指针就是存放地址值的一种数据类型)
  下面是一段正确的程序:
  int a,*p;
p=&a; /*把变量a的地址值赋给指针p*/

2、什么是void指针
  void的意思就是“无值”或“无类型”。void指针一般称为“通用指针”或“泛指针”。之所以有这样的名字是因为使用void指针可以很容易地把void指针转换成其它数据类型的指针。例如在为一个指针分配内存空间的时候:
int *p;
p=(int *)malloc(......); 本来函数malloc的返回值是void类型,在这里通过在前面加上一个带括号的int*就把void*类型转换成了int*类型。
  所以不能简单的把void看成“无”的意思。void数据类型是一种很重要的数据类型。

  3、指针可以相加减吗
  可以相互加减。但是一定要作有意义的运算。当二个指针指向同一个数组的时候,它们相加减是有意义的。如果二个指针分别指向二个不同的数组,那么指针之间的相加减就没有什么意义。指向同一个数组时,其相加减的结果为二个指针之间的元素数目。

  4、什么是NULL指针
  NULL指针是不指向任何一个地址的指针。这样的指针一般是允许的。当一个指针为NULL的时候,不要对它进行存取。

  5、什么是“野”指针
  野指针是不由程序员或操作者所能控制的指针。当在一个程序里面定义了一个指针而又没有给这个指针一个具体地址指向的时候,这个指针会随意地指向一个地址,这样的指针就是一个野指针。如果这个地址后面的内存空间没有什么重要的数据则不会造成不好的后果,但是一旦这里面存放了有用的数据,那么这些数据随时都有被野指针存取的危险,如果这样,数据就会被破坏,程序也会崩溃。所以在程序里面是一定要禁止任何野指针的存在。当定义了一个指针的时候,要马上给这个指针分配一个内存地址的指向。这样程序才不会因为指针而出现意外。

  6、NULL的值是什么
  NULL不是被定义为0就是被定义成(void *)0,这二种值基本上是一样的。
  如有这样的语句: if(p==NULL) 或者写成 if(p==0) 其作用是一样。

7、什么是“内存泄漏”
  当定义了一个指针的时候,立即要为这个指针分配一个内存空间。这只防止了野指针的产生。当一个指针使用完毕要立即释放掉这个指针所占用的内存空间---这有二方面的意义:  1)避免了内存空间的泿费; 2)防止了内存泄漏。为什么会产生内存泄漏:如果没有及时释放掉指针所占用的内存空间,而在下次使用这个指针时又给这个指针分配了内存空间,这样的次数一多,内存空间就慢慢被消耗掉了。所以形象地称这种现象为内存泄漏。
  如下面这样一个程序:
  void *p;
for(;;)
p=malloc(20); /*这20个字节的内存空间是随意指定的*/
这样的一个小程序,大家不要随便运行它。你可以在集成环境中单步调试运行,可以看一下每步运行后的结果。可以看到,每一次循环都会“吃掉”20个字节的内存,无数次之后,再多的内存也慢慢地“泄漏”,最后没有内存可用就死机。(与这个程序配合需要一段检测整机总的内存容量的程序,以观察内存总量的变化。这里虽然没有这一段程序,但是看得到每次分配的内存地址值是不相同的)

8、near指针和far指针
在DOS下(实模式)地址是分段的,每一段的长度为64K字节,刚好是16位(二进制的十六位)。
near指针的长度是16位的,所以可指向的地址范围是64K字节,通常说near指针的寻址范围是64K。
far指针的长度是32位,含有一个16位的基地址和16位的偏移量,将基地址乘以16后再与偏移量相加,(所以实际上far指针是20位的长度。)即可得到far指针的1M字节的偏移量。所以far指针的寻址范围是1M字节,超过了一个段64K的容量。例如一个far指针的段地址为0x7000,偏移量为0x1244,则该指针指向地址0x71224.如果一个far指针的段地址是0x7122,偏移量为0x0004,则该指针也指向地址0x71224。
如果没有指定一个指针是near或far,那么默认是near。所以far指针要显式指定。far指针工作起来要慢一些,因为每次访问一个far指针时,都要将数据段或程序段的数据交换出来。另外,far指针的运算也比较反常,例如上面讲到的far指针指向同一个地址,但是比较的结果却不相同。

9、什么时候使用far指针
当使用小代码或小数据存储模式时,不能编译一个有很多代码或数据的程序。因为在64K的一个段中,不能放下所有的代码与数据。为了解决这个问题,需要指定以far函数或far指针来使用这部分的空间(64K以外的空间)。许多库函数就是显式地指定为far函数的形式。far指针通常和farmalloc()这样的内存分配函数一起使用。 

问与答(指针)

 1、什么是指针
  指针是一种数据类型,与其它的数据类型不同的是指针是一种“用来存放地址值的”变量。举一个简单的例子:
如果定义了一个整型变量,根据整型变量的特点,它可以存放的数是整数。
如:int a; a=100; 这样就把整型常量赋给了变量a。但是如果写成这样:a=123.33;就会出问题,最后输出变量a的值结果是123。现在说到指针,其实地址值也是一个整型数,如某某变量的地址值为36542,说明这个变量被分配在内存地址值为36542的地方。能不能这样进行推理,既然地址值也是整型数,整型变量正好可以用来存放整型数,那不是一个整型变量可以用来存放地址的值吗。程序写成下面这样:
  int a,b;
a=&b;
很明显,这样写是错误的。原因在于不能简单地把地址理解为整型数。
应有这样的对应关系: 地址值<--->指针;  整型数<--->int 型变量。
所以有这样的说法:“指针就是地址”(指针就是存放地址值的一种数据类型)
  下面是一段正确的程序:
  int a,*p;
p=&a; /*把变量a的地址值赋给指针p*/

2、什么是void指针
  void的意思就是“无值”或“无类型”。void指针一般称为“通用指针”或“泛指针”。之所以有这样的名字是因为使用void指针可以很容易地把void指针转换成其它数据类型的指针。例如在为一个指针分配内存空间的时候:
int *p;
p=(int *)malloc(......); 本来函数malloc的返回值是void类型,在这里通过在前面加上一个带括号的int*就把void*类型转换成了int*类型。
  所以不能简单的把void看成“无”的意思。void数据类型是一种很重要的数据类型。

  3、指针可以相加减吗
  可以相互加减。但是一定要作有意义的运算。当二个指针指向同一个数组的时候,它们相加减是有意义的。如果二个指针分别指向二个不同的数组,那么指针之间的相加减就没有什么意义。指向同一个数组时,其相加减的结果为二个指针之间的元素数目。

  4、什么是NULL指针
  NULL指针是不指向任何一个地址的指针。这样的指针一般是允许的。当一个指针为NULL的时候,不要对它进行存取。

  5、什么是“野”指针
  野指针是不由程序员或操作者所能控制的指针。当在一个程序里面定义了一个指针而又没有给这个指针一个具体地址指向的时候,这个指针会随意地指向一个地址,这样的指针就是一个野指针。如果这个地址后面的内存空间没有什么重要的数据则不会造成不好的后果,但是一旦这里面存放了有用的数据,那么这些数据随时都有被野指针存取的危险,如果这样,数据就会被破坏,程序也会崩溃。所以在程序里面是一定要禁止任何野指针的存在。当定义了一个指针的时候,要马上给这个指针分配一个内存地址的指向。这样程序才不会因为指针而出现意外。

  6、NULL的值是什么
  NULL不是被定义为0就是被定义成(void *)0,这二种值基本上是一样的。
  如有这样的语句: if(p==NULL) 或者写成 if(p==0) 其作用是一样。

7、什么是“内存泄漏”
  当定义了一个指针的时候,立即要为这个指针分配一个内存空间。这只防止了野指针的产生。当一个指针使用完毕要立即释放掉这个指针所占用的内存空间---这有二方面的意义:  1)避免了内存空间的泿费; 2)防止了内存泄漏。为什么会产生内存泄漏:如果没有及时释放掉指针所占用的内存空间,而在下次使用这个指针时又给这个指针分配了内存空间,这样的次数一多,内存空间就慢慢被消耗掉了。所以形象地称这种现象为内存泄漏。
  如下面这样一个程序:
  void *p;
for(;;)
p=malloc(20); /*这20个字节的内存空间是随意指定的*/
这样的一个小程序,大家不要随便运行它。你可以在集成环境中单步调试运行,可以看一下每步运行后的结果。可以看到,每一次循环都会“吃掉”20个字节的内存,无数次之后,再多的内存也慢慢地“泄漏”,最后没有内存可用就死机。(与这个程序配合需要一段检测整机总的内存容量的程序,以观察内存总量的变化。这里虽然没有这一段程序,但是看得到每次分配的内存地址值是不相同的)

8、near指针和far指针
在DOS下(实模式)地址是分段的,每一段的长度为64K字节,刚好是16位(二进制的十六位)。
near指针的长度是16位的,所以可指向的地址范围是64K字节,通常说near指针的寻址范围是64K。
far指针的长度是32位,含有一个16位的基地址和16位的偏移量,将基地址乘以16后再与偏移量相加,(所以实际上far指针是20位的长度。)即可得到far指针的1M字节的偏移量。所以far指针的寻址范围是1M字节,超过了一个段64K的容量。例如一个far指针的段地址为0x7000,偏移量为0x1244,则该指针指向地址0x71224.如果一个far指针的段地址是0x7122,偏移量为0x0004,则该指针也指向地址0x71224。
如果没有指定一个指针是near或far,那么默认是near。所以far指针要显式指定。far指针工作起来要慢一些,因为每次访问一个far指针时,都要将数据段或程序段的数据交换出来。另外,far指针的运算也比较反常,例如上面讲到的far指针指向同一个地址,但是比较的结果却不相同。

9、什么时候使用far指针
当使用小代码或小数据存储模式时,不能编译一个有很多代码或数据的程序。因为在64K的一个段中,不能放下所有的代码与数据。为了解决这个问题,需要指定以far函数或far指针来使用这部分的空间(64K以外的空间)。许多库函数就是显式地指定为far函数的形式。far指针通常和farmalloc()这样的内存分配函数一起使用。 

正确使用指针

指针就是地址。
按类型来分指针有 int、char 、float等基本类型。
对于扩充的数据类型则有struct 等。
指针的类型决定了指针操作时该指针指向地址变化的规律。
例: int a, *p; //定义了一个整型指针以后就可以写为 p=&a; 这个好理解。
麻烦的是指针与数组结构等结合起来了之后情况就变得复杂起来,如下例:
int arr[10], *p; p=arr; 此时把数组的地址赋给了指针p,指针p就指向了数组的首地址。现在假设数组的首地址值是3452,则指针p的值必然是3452。那么 p+1 表示指针移动指向了数组的下一个元素,那么p+1的值是什么?初学指针时对这一点很容易搞错不加思索的回答既然p==3452,那么p+1就等于3452+1==3453,

如果这样去认识指针就大错特错了。这里的p+1不是简单的算术运算,它表示这样一个意思——指针移动了一个元素准确地说是指针移动了一个整型元素。一个整型变量占多少字节内存:2 个字节,所以在这里指针的地址变化为一个整型变量那么它的地址自然要在原来的地址值上加 2 ,所以指针移动一个整型元素后地址值应为3452+2,即指针p的值为3454。

上面讲的是整型指针的情形,对于字符型指针呢?其实只要对上面所讲的道理真正理解了,字符型指针也就好理解了。例子如下:
char aa[10], *p; p=aa; 同样假设数组的首地址为3452,那么p+1 的值可以这样考虑,指针移动一个字符的地址,而一个字符占一个字节的内存,所以p+1的值就为3452+1=3453。

可以说上面的二种情形还好理解,对于指向二维数组的指针以及指向结构的指针又如何去正确理解呢?

当指针与二维数组连在一起的时候情形就变得复杂了许多。因为数组名代表了
数组的起始地址,如 char arr[5][6]; 那么数组名 arr就是这个二维数组的首地
址。初学指针的朋友对这个问题总是弄不明白,既然二维数组名arr是一个地址,而
指针变量就是存放地址的,把二维数组的地址赋给同样数据类型的指针不就可以了
吗,于是就有这样的写法:char arr[5][6], *p; p=arr; 这样写肯定是错误的,有的
朋友可能对这样写是错误的也明白,他们基于这样的理解:一个二维数组里的数组
元素也是表示一个地址,于是得出结论,二维数组名是一个二级指针,是地址的地
址,进而引申出如下写法:
char arr[5][6]; char **p; p=arr; 然而很对不起,这样写同样是错误
的,如果你不相信,你可以把你认为正确的代码输入到里面,编译一下,肯定是通
不过的。

那么为什么上面的这些理解是错误的呢?错在对指针基本概念理解停留在表面。下
面为了把这个问题说清楚一点,我们可以把指针的类型归纳为二个特征:
1、基本数据类型如(char、int、 float等);
2、扩充数据类型(如一维数组、二维数组、结构等)
例1:
int arr[4][5]; //定义了一个二维数组
int *p; //定义了一个整型指针
// 下面该怎样把数组的地址赋给指针?因为定义的是一个int 指针,所以只能写为
p=arr[0]; //想一想为什么?

这里要讲的就是把地址赋给指针时要注意的问题,p是 int 类型的指针,它只能指向 int 这个基本数据类型。有的朋友或许要问,这个二维数组不也是 int 类型吗?是的但是这个二维数组除了是 int 之外,它的类型全称应该是 int 二维数组,arr[0]是int 一维数组,arr[0]这个一维数组的各元素才是基本的 int 数据类型, p=arr[0]就是把这个一维数组第一个元素的地址赋给了int 类型的指针 p,. 数据类型完全一样才能赋值。那么显而易见可以有下面的写法,注意指针是怎样指向各数组元素的:

char arr[4][5]={"abc","def","ghi","jkl","mno"};
char *p=arr[0];
for(i=0;i<20;i++)
printf("%c", *(p+i) ); //仔细观察输出的值是怎样变化的
//因为定义的是一个字符型指针,那么1、必须使这个指针指向与其对应的字符型数
据类型; 2、指针每增加一个单位的地址值 ,如p+1表示指向下一个字符的地
址。所以printf()语句输出的结果为" abcdefghijklmno" ,是一个字符一个字符输出的。
。。
下面举一个整型指针的例子:
int arr[3][3]={ {1,2,3},{4,5,6},{7,8,9}};
int *p=arr[0]; //数据类型相同,可以赋地址值
for(i=0;i<9;i++)
printf("%d", *(p+i)); // 逐个的输出数组元素

。。
--------------------------------------------------------------
下面将要讲的是直接指向一个二维数组的指针它有哪些特点:
一个二维数组,它的每一个数组元素都是一个一维数组,一个整型二维数组可以写为:
int arr[3][3]; 即 {arr[0], arr[1], arr[2] }
我们现在想定义一个指针,使得这个指针有这样的特性—— 指针 p指向arr[0],
指针 p+1 指向arr[1] ,指针 p+2指向arr[2], 也就是指针每移动一个单位的地址就指向下一个一维数组,那么这个指针必须满足下面二个条件:1、必须是整型 2、必须每移动一个单位的地址时实际上移动一个一维数组的长度即3个整型量。那么这个指针可定义为如下形式:

int (* p) [3] ; // 定义了一个指向二维数组的指针,这个二维数组中的一维数组有3个元素。
p=arr; // 把二维数组的地址赋给指针 p
如果二维字符数组初始化是 char arr[3][4]={"abc","def","ghi"};
所以可以如下写:
*(p+0) //是数驵 a[0] 的首地址 printf("%s", *p); 输出字符串 “"abc"
*(p+1) //数组a[1]的首地址 printf("%s", *(p+1)); 输出字符串 "def"
*(p+2) //数组 a[2]的首地址 printf("%s",*(p+2)); 输出字符串 "ghi"

如果要用这个二维数组的指针逐个的输出字符可以写为:
*(*(p+0)+0) //第一个字符 a
*(*(p+0)+1) //第二个字符 b
*(*(p+0)+2) //第三个字符 c
*(*(p+0)+3) //第四个字符 d
*(*(p+0)+4) //第五个字符 e
.................. 依此类推

----------------------------------

当指针指向结构时的情形。。。

如果对指向数组的指针完全理解了,那么对指向结构的指针也就很好理解了。实际上一个指向结构的指针更容易理解。

设定义了一个结构如下,有一个结构数组,三个结构。
struct student{ int a;char *b;}stru[3]={{1,"abc"},{2,"def"},{3,"ghi"}};
struct student *p=stru;

p+0 //第一个结构的地址
p+1 //第二个结构的地址
p+2 //第三个结构的地址

抱歉!评论已关闭.