我们可以对指针这样定义:
通过指针中存放的首地址,应用程序顺利地找到某个变量。就好像我最近认识了一位朋友,他叫我有空去他家坐坐,然后,他留下了地址。某个周末我正闲着,忽然想起这位朋友,于是,我就根据他留的地址去找他,结果,当我来到傻B街230号出租房时,里面走出一个我不认识的人,于是,我问他我这位朋友去哪了,陌生人说,我刚租了这房子,你找的可能是前一位租户吧。
所以,指针所指向的地址,有可能是变量B,也有可能是变量F,或者变量S,指针是房东,可以把房子租给B,C,或F,它可以动态为变量分配内存,也可以把变量销毁(delete),交不起房租就滚蛋(析构函数)。
从上面的故事中,我们看到指针的两个用途:索引内存和分配内存。
看看下面这个例子。
#include <stdio.h>
void main()
{
int* pint = new int(100);
printf(" *pint的值:%d\n", *pint);
printf(" pint的值:0x %x\n", pint);
getchar();
}
你猜猜,它运行后会出现什么?

我们看到了,pint里面存的就是整型100的首地址,因为它是int*,是指向int的指针,所以指针知道,找到首地址后,我只关注从首地址开始,连续的4个字节,后面的我不管了,因为我只知道int有四个字节。上面的例子,我们看到pint的值就是0x10f1968,这就是整型100在内存中的首地址,所以,100所拥有的内存块可能是:
0x10f1968 , 0x10f1969, 0x10f196A, 0x10f196b
总之是连续的内存块来保存这4个字节。
new int(100),表示指针pint在首地址为0x10f1968的内存区域创建了一个4个字节的区域,里面保存的值就是整型100,所以,pint取得的就是100的首地址,而加上*号就不同了,看看上面的例子,*pint的值就是100了。这样一来,我们又得到一个技巧:
利用指针标识符 * 放在指针变量前即可获得指针所指地址中存储的实际值。
我都大家一个很简单的技巧。看看下面两行代码。
int *p = new int(200);
int p = 200;
因为 * 放在类型后或放在变量名前面都是可以的,即int* pint和int *pint是一个道理。这样一来,我们不妨把int *pint 看作int (*pint),将整个*pint看作一个整体,这样看上去是不是和下面的声明很像?
int a = 30;
所以,int* p = new int(30)中,*p返回的值就是int的本值30,而p则只是返回30的首地址。
再看看下面的代码:
#include <stdio.h>
void main()
{
int* arrint = new int[3];
arrint[0] = 20;
arrint[1] = 21;
arrint[2] = 22;
for(int i =0; i < 3; i++)
{
printf(" 数组[%d] = %d\n", i, arrint[i]);
}
delete [] arrint; // 清理内存
getchar();
}
现在你可以猜猜它的运行结果是什么。

从上面的代码我们又看到了指针的第三个功能:创建数组。
上例中,我创建了有三个元素的数组。在使用完成后,要使用delete来删除已分配的内存,所以,我们的第一个例子中,其实不完善,我们没有做内存清理。
int* pint = new int(100);
/****/
delete pint;
为什么指针可以创建数组?前面我提到过,指针是指向首地址的,那么你想想,我们的数组如果在堆上分配了内存,它们是不是也按一定次序存放在一块连续的内存地址中,整个数组同样构成了一段内存块。
很多书和教程都把这个符号叫引用,但我不喜欢翻译为引用,因为引用不好理解,如果叫取地址符,那我估计你就会明白了,它就是返回一个变量的首地址。
看看例子:
#include <stdio.h>
void main()
{
int a = 50;
int* p = &a;
printf(" a的值:%d\n", a);
printf(" p的值:0x_%x\n", p);
getchar();
}
我们不能直接对指针变量赋值,要把变量的地址传给指针,就要用取地址符&。上面的代码中我们声明了int类型的变量a,值为50,通过&符号把变量a的地址存到p指针中,这样,p指向的就是变量a的首地址了,故:a的值的50,而p的值就应该是a的地址。
那么,这样做有啥好处呢?我们把上面的例子再扩展一下,变成这样:
#include <stdio.h>
void main()
{
int a = 50;
int* p = &a;
printf(" a的值:%d\n", a);
printf(" p的值:0x_%x\n", p);
/* 改变指针所指向的地址块中的值,就等于改变了变量的值 */
*p = 250;
printf(" a的新值:%d\n", a);
getchar();
}
先预览一下结果。

不知道大家在这个例子中发现了什么?
我们定义了变量a,值为50,然后指针p指向了a的首地址,但注意,后面我只是改变了p所指向的那块内存中的值,我并没有修改a的值,但是,你看看最后a的值也变为了250,想一想,这是为什么?
很多书,包括一些计算机二级的考试内容,那些傻S砖家只是想出一大堆与指针相关的莫名其妙的考题,但很让人找不到指针在实际应用到底能干什么,我估计那些砖家自己也不知识吧。所以,我们的考试最大的失败,就是让学生不知识学了有什么用。
上面介绍了指针可以存首地址,可以分配内存,可以创建数组,还说了取地址符&,那么,这些东西有什么用呢?你肯定会问,我直接声明一个变量也是要占用内存的,那我为什么要吃饱了没事干还要用指针来存放首地址呢?
好,我先不回答,我们再说说函数的参数传递。看看下面这样的例子。
#include <stdio.h>
void fn(int x)
{
x += 100;
}
void main()
{
int a = 20;
fn(a);
printf(" a : %d\n", a);
getchar();
}
我们希望,在调用函数fn后,变量a的值会加上100,现在我们运行一下,看看结果:

我们可能会很失望,为什么会这样?我明明是把20传进了fn函数的,为什么a的值还是不变呢?不用急,我们再把代码改一下:
#include <stdio.h>
void fn(int x)
{
printf(" 参数的地址:0x_%d\n", &x);
x += 100;
}
void main()
{
int a = 20;
fn(a);
printf(" a : %d\n", a);
printf(" a的地址:0x_%x\n", &a);
getchar();
}
运行结果如下:

看到了吗?变量a和fn函数的参数x的地址是不一样的,这意味着什么呢?这说明,变量a的值虽然传给了参数x,但实际上是声明了一个新变量x,而x的值为20罢了,最后加上100,x的中的值是120,但a的值没有变,因为在函数内被+100的根本不是变量a,而是变量x(参数)。
这样,就解释了为什么么函数调用后a的值仍然不变的原因。
那么,如何让函数调用后对变量a作修改,让它变成120呢?这里有两个方法:
(1)指针法。把参数改为指针类型。
#include <stdio.h>
void fn(int* x)
{
*x += 100;
}
void main()
{
int a = 20;
fn(&a);//用取地址符来传递,因为指针是保存地址的
printf(" a : %d\n", a);
getchar();
}
这里要注意,把变量传给指针类型的参数,要使用取地址符&。
那么,这次运行正确吗?

好了,终于看到想要的结果了。
(2)引用法,就是把参数改为&传递的。
#include <stdio.h>
void fn(int& x)
{
x += 100;
}
void main()
{
int a = 20;
fn(a);//直接传变量名就行了
printf(" a : %d\n", a);
getchar();
}
可以看到,这样的运行结果也是正确的。

不管是类还是结构(其实结构是一种特殊的类),它们在创建时还是要创建内存的,但是,创建类的对象也有两种方式,直接声明和用指针来分配新实例。
#include <iostream>
using namespace std;
class Test
{
public:
Test();
~Test();
void Do(char* c);
};
Test::Test()
{
cout << "Test对象被创建。" << endl;
}
Test::~Test()
{
cout << "Test对象被销毁。" << endl;
}
void Test::Do(char* c)
{
cout << "在" << c << "中调用了Do方法。" << endl;
}
void Func1()
{
Test t;
t.Do("Func1");
/*
当函数执行完了,t的生命周期结束,发生析构。
*/
}
void Func2()
{
Test* pt = new Test;
pt -> Do("Func2");
/*
用指针创建的对象,就算指针变量的生命周期结束,但内存中的对象没有被销毁。
因此,析构函数没有被调用。
*/
}
int main()
{
Func1();
cout << "---------------------" << endl;
Func2();
getchar();
return 0;
}
我们来看看这个例子,首先定义了一个类Test,在类的构造函数中输出对象被创建的个息,在发生析构时输出对象被销毁。
接着, 我们分别在两个函数中创建Test类的对象,因为对象是在函数内部定义的,根据其生命周期原理,在函数返回时,对象会释放,在内存中的数据会被销毁。理论上是这样的,那么,程序实际运行后会如何呢?

这时候我们发现一个有趣的现象,在第一个函数直接以变量形式创建的对象在函数执行完后被销毁,因为析构函数被调用;可是,我们看到第二个函数中并没有发生这样的事,用指针创建的对象,在函数完成时居然没有调用析构函数。
直接创建对象,变量直接与类实例关联,这样一来,当变量的生命周期结束时,自然会被处理掉,而用指针创建的实例,指针变量本身并不存储该实例的数据,它仅仅是存了对象实例的首地址罢了,指针并没有与实例直接有联系,所以,在第二个函数执行完后,被销毁的是Test*,而不是Test的对象,仅仅是保存首地址的指针被释放了而已,而Test对象依然存在于内存中,因此,在第二个函数完成后,Test的析构函数不会调用,因为它还没死呢。
那么,如何让第二个函数在返回时也销毁对象实例呢?还记得吗,我前文中提过。对,用delete.。
void Func2()
{
Test* pt = new Test;
pt -> Do("Func2");
delete pt;
}
现在看看,是不是在两个函数返回时,都能够销毁对象。

现在你明白了吧?
由此,可以得出一个结论:指针只负责为对象分配和清理内存,并不与内存中的对象实例有直接关系。