是啊,不可避免的,我们要学习指针了。关于指针的概念,我们已经在第一章节 理解程序中的数据 课题中大概的介绍过了,我想它并不是一个很难的概念,如果对指针没有任何一点概念的朋友可以试着百度一下,再看一下我们以前的章节。
之所以把指针放到现在来讲,一方面是因为,到现在我们所学的知识,可以允许我把一个完整的指针及其相关的知识展现给大家而不需将一个知识点打乱到各个别的章节中;再一方面就是我们接下来的要学习的继承、多态等特性刚好需要这方面的的知识,省的我们再回头复习,当然,主要原因还是我没有信心能将这个专题写好。
是的,我们在管理内存,管理一些数据结构等等,很多情况都要使用指针,我们这个专题,就专门来讨论下指针的问题。
让我们再来回顾下,指针的一些概念。
一、 什么是指针(指针与变量)。
很多的教科书上说,指针就是一个保存别的变量地址的一个变量,直白点说,指针就是地址。关于什么是变量、什么是地址、什么是数据类型的问题,我想大家应该都明白的,这里我就节省一些篇幅。
好,我们看一下指针的定义格式:
数据类型 *指针变量名; // 单纯的使用指针变量名就是操作地址,给变量名带上*就是取内容。
好,既然指针时用来保存别的变量的地址的,那我们可以很容易的给一个指针变量赋值,以及对一个指针变量进行操作(见代码:Exp01):
#include "stdafx.h"
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
int nNum = 0;
int *pnNum = NULL;
pnNum = &nNum; // 将nNum的地址保存到pnNum变量中;
scanf("%d", &nNum); // 输入一个数值
printf("nNum变量的地址: 0x%08X\r\n", &nNum); // 输出nNum变量的地址
printf("nNum变量的内容: %d\r\n", nNum); // 输出输入的变量的内容
printf("pnNum的地址: 0x%08X\r\n", &pnNum); // 输出指针变量pnNum的地址
printf("pnNum的内容: 0x%08X\r\n", pnNum); // 输出指针变量pnNum的内容
printf("pnNum内容的内容: %d\r\n", *pnNum); // 输出指针变量pnNum内容的内容
return 0;
}
好,贴一下运行结果,让我们仔细比较这两个变量之间的联系。
看明白了么?是不是可以理解成pnNum指向了nNum变量呢?由此,我们可以说,pnNum是nNum的指针。
如果大家真的明白了他们的关系,大家不妨试着写写nNum指针的指针的指针……
当然,如果内存学的够好的朋友可以写一下这两个变量在栈的排列方式……
二、 指针与数组
忘记在哪本书中说的: 指针和数组是近亲。现在越来越发现,确实是这个样子的,下面让我来带着大家领略下它们的风采。
1. 数组的基本用法
关于数组,我想大家应该太不陌生了,我们第一节课就讲述了数组的用法。
在这里,我们为了节省篇幅,我仅简单扼要的回顾一下数组相关的知识点:
1、 数组是用来存放相同数据类型的数据集合
2、 数组名就是这一数据集合的首地址(是常量)。
3、 数组的下标从0开始,数组中的数据是按照数组的下标顺序依次存放。
如下面的程序:
#include <windows.h>
int main()
{
char szaddrName[] = "52pojie.cn";
char *pszTitle = "Null";
MessageBoxA(NULL, szaddrName , pszTitle, MB_OK);
return 0;
}
OK,我们看下他们的存放方式有什么区别:
哈哈,是不是一样呢?
2. 数组和指针的互操作
char g_szaddrName[] = "52pojie.cn";
char *g_pszTitle = "Null";
// 用指针操作数组
void TestPoint()
{
char *pszPoint = g_szaddrName;
while (*pszPoint)
{
printf("%c", *pszPoint++);
}
printf("\r\n");
}
// 用数组操作指针
void TestArr()
{
char *pszPoint = g_szaddrName;
int nLength = strlen(pszPoint);
for (int i = 0; i < nLength; i++)
{
printf("%c", pszPoint[i]);
}
printf("\r\n");
}
3. 二维数组的基本用法
OK,明白了这些,我们举一个简单走迷宫的例子来说明二维数组的存储和使用(具体代码见Exp03):
我们先定义一个地图:
/************************************************************************/
// 迷宫地图数据
// 0表示墙
// 1表示可以行走的路
// 2表示已经走过的路
// 3表示返回的路
/************************************************************************/
int g_MazeMapData[13][13] = {
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1},
{0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0},
{0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0},
{0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0},
{0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0},
{0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0},
{0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0},
{0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0},
{0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0},
{0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0},
{0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0},
{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
有了地图,我们需要一些标记,用来确定如:入口点坐标,出口在哪里,指定的坐标经过没有等等问题:
#define IN_POS_X 1 // 入口点X坐标
#define IN_POS_Y 12 // 入口点Y坐标
#define OUT_POS_X 12 // 出口点X坐标
#define OUT_POS_Y 1 // 出口点Y坐标
#define UNKNOWN 1 // 从来没走过的路
#define PASS 2 // 经过的标记
#define BACK 3 // 返回的标记
#define GO 0 // 前进
#define COMEBACK 1 // 后退
接下来,我们需要考虑下,实现走路的方式:
/************************************************************************/
// 按照 上、右、下、左的顺序寻路
// 参数含义:
// nFlag: GO 表示前进,COMEBACK表示返回
// 返 回 值:
// 1 : 向上走,2: 向右走, 3: 向下走, 4: 向左走, 0:异常(出地图了,不移动)
/************************************************************************/
int MoveTo(int nFlag)
{
if(nFlag == 0)
{
// 上
if (g_MazeMapData[g_nCurPosX][g_nCurPosY-1] == UNKNOWN)
{
g_MazeMapData[g_nCurPosX][g_nCurPosY] = PASS;
g_nCurPosY--;
return 1;
}
// 右
if (g_MazeMapData[g_nCurPosX+1][g_nCurPosY] == UNKNOWN)
{
g_MazeMapData[g_nCurPosX][g_nCurPosY] = PASS;
g_nCurPosX++;
return 2;
}
// 下
if (g_MazeMapData[g_nCurPosX][g_nCurPosY+1] == UNKNOWN)
{
g_MazeMapData[g_nCurPosX][g_nCurPosY] = PASS;
g_nCurPosY++;
return 3;
}
// 左
if (g_MazeMapData[g_nCurPosX-1][g_nCurPosY] == UNKNOWN)
{
g_MazeMapData[g_nCurPosX][g_nCurPosY] = PASS;
g_nCurPosX--;
return 4;
}
}
else
{
// 上
if (g_MazeMapData[g_nCurPosX][g_nCurPosY-1] == PASS)
{
g_MazeMapData[g_nCurPosX][g_nCurPosY] = BACK;
g_nCurPosY--;
return 1;
}
// 右
if (g_MazeMapData[g_nCurPosX+1][g_nCurPosY] == PASS)
{
g_MazeMapData[g_nCurPosX][g_nCurPosY] = BACK;
g_nCurPosX++;
return 2;
}
// 下
if (g_MazeMapData[g_nCurPosX][g_nCurPosY+1] == PASS)
{
g_MazeMapData[g_nCurPosX][g_nCurPosY] = BACK;
g_nCurPosY++;
return 3;
}
// 左
if (g_MazeMapData[g_nCurPosX-1][g_nCurPosY] == PASS)
{
g_MazeMapData[g_nCurPosX][g_nCurPosY] = BACK;
g_nCurPosX--;
return 4;
}
}
return 0;
}
最后,我们只需要一个函数,来确定是否到达出口,或者判断下有没有出口并给出相应提示:
/************************************************************************/
// 开始走迷宫
// 如果找到出口了,返回1,否则返回0
// 如果程序异常中断了,返回-1
/************************************************************************/
int Start()
{
while (1)
{
// 开始走路
if (MoveTo(GO) == 0)
{
MoveTo(COMEBACK);
}
for(int i=0; i<=OUT_POS_X; i++)
{
for(int j = 0; j <= IN_POS_Y; j++)
{
printf("%d ", g_MazeMapData[i][j]);
}
printf("\r\n");
}
Sleep(500);
system("cls");
// 如果当前的坐标是出口坐标表示找到出口了
if (g_nCurPosX == OUT_POS_X && g_nCurPosY == OUT_POS_Y)
{
return 1;
}
// 如果又退回入口位置了,表示没有找到出口
if (g_nCurPosX == IN_POS_X && g_nCurPosY == IN_POS_Y)
{
return 0;
}
}
return -1;
}
我相信,通过这个程序,足够让大家理解并掌握二维数组及多维数组的用法了。下面,我们来考虑一些具体的问题。
4. 数组的寻址方式
相信好多朋友同我一样,很抽象的将二维数组在内存中的模样就想像成二维的平面了,三维数组就想想成立方体……,却忽略掉它们其实都是线性存储的。
这样,无论是我们在以后的编码过程中还是在逆向分析中确定当前操作的是哪个元素就尤为重要的。(比如通过单循环来遍历一个二维数组等等)。
其实,如果真要说起它的寻址方式,还真的挺容易的,不信,听我慢慢道来:
a) 一维数组的寻址
比如,已知在自己身前10米处有一栅门,门后面有一排边长为1米的正方形箱子,求第四个箱子离自己有多远,哈哈,简单吧:相信很多朋友脱口就能说出来是:10+1*4 = 14米
那再问,一个数组:int ary[10] = {0}; //已知 ary的地址是:0x12ff68,求ary[3]的地址是多少?。
0x12ff68+sizeof(int)*3 = 0x12ff68+0x0C = 0x12ff74
b) 二维维数组的寻址
问: 有十个箱子,没个箱子中放十个苹果,求第七个箱子中第三个苹果是这些箱子中的第几个苹果。
这个问题是不是就相当于有A[10][10], 求A[6][2]是第几个元素。即:6*10+3 = 63
由此推理,我们可以推演一下它的寻址公式:
如:有数组:type ary[x][y]; 则ary[x][z]的地址是:
Ary[x]addr = ary addr + sizeof(type[y])*x = ary addr + sizeof(type)*x*y
Ary[x][z]addr = ary[x]addr + sizeof(type)*z
由此,将公式优化一下:
Ary[x][z]addr = ary addr + sizeof(type)*y*x + sizeof(type)*z;
= ary addr + sizeof(type)*(x*y+z);
因此,二维数据的寻址公式就是:Ary[x][z]addr = ary addr + sizeof(type)*(x*y+z);
此公式中x*y+z就是求第几个元素的公式。
举个例子实验一下:
如:int ary[3][4] = {0};已知数组首地址是:0x12ff68,求ary[1][2]是第几个元素,并求出其在内存的地址。
套公式:
1、1*4+2 = 6 ,所以是第六个元素。
2、0x12ff68+sizeof(int)*(1*4+2) = 0x12ff68+ 4 * 6 = 0x12ff68 + 0x18 = 0x12ff80,地址是:0x12ff80
5. 浅谈指针数组
只简单说明下用法,详细信息大家自己摸索,下满举个应用的例子,一堆字符串的排序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/************************************************************************/
// 按照字符顺序排序
/************************************************************************/
int SortTaxis(char *szNameBuf[], int nCount)
{
int x = 0, y = 0;
char *szTmpBuf = NULL;
if (szNameBuf == NULL)
{
return 0;
}
if (nCount <= 0)
{
return 0;
}
for (x = 0; x < nCount-1; x++)
{
for (y = x+1; y < nCount; y++)
{
if (strcmp(szNameBuf[x], szNameBuf[y]) > 0)
{
szTmpBuf = szNameBuf[x];
szNameBuf[x] = szNameBuf[y];
szNameBuf[y] = szTmpBuf;
}
}
}
return 0;
}
int main()
{
int i = 0;
char *szNameBuf[] = {
"Kalkwasser",
"John Parsons",
"Nathan Bryce",
"Edward",
"John Mackintosh",
"Henry Ford",
"William Sanders",
"Nathaniel",
"George",
"Tom Rees",
"Clementia",
"Mary Jones",
"Natasha",
"DrMudd",
"Charlie Chaplin",
"Julius Caesar",
"Krakatoa",
"James Brady",
"John Paul II",
"Freud",
"Tom Rees",
"pineapple lily",
"Antony"};
SortTaxis(szNameBuf, 23);
for (i = 0; i < 23; i++)
{
puts(szNameBuf[i]);
}
return 0;
}
简单的评论下,这样写的代码简单精简,效率高,因为它交换的是指针,用的是指针数组,与二维数组相比,它节省空间……
三、 指针与函数
1. 用指针作为函数的参数
我想,不论是指针作为参数还是引用作为参数,都有值得让我们来调试它一下,弄清楚其原理,由于这些比较简单,所以具体的调试过程留给大家,希望大家也能得到与我一样的结论:传递的是地址,存在一份地址的拷贝。
具体使用指针作为参数的参考代码可以见上面指针数组的函数:
int SortTaxis(char *szNameBuf[], int nCount)
2. 用指针作为函数的返回类型
返回一个指针类型是可行的,但是我相信初学者一定像我一样,都会犯一个同样的错误:返回局部变量的地址,如下代码:
char * stringcat(char *pszStr1, char *pszStr2)
{
if (!pszStr1 || !pszStr2)
{
return NULL;
}
char pszResultStr[128] = {0};
strcpy(pszResultStr, pszStr1);
strcat(pszResultStr, pszStr2);
return pszResultStr;
}
这段代码漏洞很多,我希望我的读者朋友不会写出这样的代码。
1、 首先它定义的是数组,空间大小可能越界造成栈溢出漏洞。
2、 它返回的是一个栈地址,容易被覆盖。
我希望大家能调试一下这段代码(Exp05)就想我以前调试分析别的代码一样,大家自己分析下,问题出在哪里。
3. 使用函数指针
经过上面的一翻学习,我相信,大家已经很清楚的明白,我们的指针不仅能够指向栈数据区,而且还可以指向堆数据区乃至全局数据区,当然,我相信大家也肯定能够理解指针也可以指向代码区,因为我们早在学习函数的专题中,用数组存放我们代码的机器码并执行过它,数组名不就是数组的首地址么,函数名不也是地址么,地址不就是指针么。
好,不多废话,看代码(Exp06):
int sum(int nNum1, int nNum2)
{
return nNum1 + nNum2;
}
int main()
{
int nSum = sum(1, 2);
printf("%d\r\n", nSum);
return 0;
}
想必,这个是我写过的最简单的函数了,不用加注释,大家肯定能猜出来这段代码的含义(如果真的不知道它的功能,那……)。
现在我们需要写一个指针,让它指向这个函数,应该怎么写呢?
回想一下我们以前说过的话,一个函数名,就是一个地址,也就是一个int类型的常量,倘若如果我们直接给一个整型指针赋值一个函数名,编译器检查应该是类型报错,我们先测试一下:
int *pFun = sum; // error C2440: 'initializing' : cannot convert from 'int (__cdecl *)(int,int)' to 'int *'
很明显,类型转换出错了,按照错误提示,我们修改一下:
int (__cdecl *pFun)(int,int) = sum; // 哈哈,编译通过。
我们知道,__cdecl是C调用方式(可以省略的.),OK,我们现在测试一下,这个函数指针能不能用:
int (*pFun)(int,int) = sum;
nSum = pFun(3, 8);
printf("%d\r\n", nSum);
OK,大功告成。
四、 指针的扩展用法
1. 函数指针类型的定义
我想,像上面这样定义一个指针,一定非常的麻烦的,如果我们在牵扯到一些数据类型转换,那更是让人头疼。比如下面的代码:
int (WINAPI * call)(HWND,LPSTR,LPSTR,UINT);
HMODULE hDll=LoadLibrary("user32.dll");
call=(int(WINAPI * )(HWND,LPSTR,LPSTR,UINT))GetProcAddress(hDll,"MessageBoxA");
FreeLibrary(hDll);
(*call)(NULL,"HI,I AM FROM USER32.DLL","TEST",MB_OK);
当然,这还是简单的,还有更复杂足以让我们头晕的代码,就不以此列举了。这就迫使我们定义一个相关的数据类型,这样以后只要像定义一个变量那样使用就可以了,比如上面的代码可以变成:
typedef int (WINAPI * call)(HWND,LPSTR,LPSTR,UINT);
call mycall; //定义一个函数指针变量
HMODULE hDll=LoadLibrary("user32.dll");
mycall=(call)GetProcAddress(hDll,"MessageBoxA");
FreeLibrary(hDll);
(*mycall)(NULL,"HI,I AM FROM USER32.DLL","TEST",MB_OK);
这样就让人好理解,同样也精简了代码。
我相信,对于typedef这个关键字,我们一定不陌生,因为在本系列的第一小节中,我们已经提到过它了。它的基本用法格式是: typedef 旧数据类型 新数据类型
通过上面的编译错误:'int (__cdecl *)(int,int)' to 'int *' 我们可以知道,int (__cdecl *)(int,int)本来就是数据类型。而 int (__cdecl *PFUN_TYPE)(int,int) 中的PFUN_TYPE就是新的数据类型。
这样,我们最开始的代码就又可以写成:
typedef int (*PFUN_TYPE)(int,int);
PFUN_TYPE pFun;
pFun = sum;
int nSum = pFun(3, 8);
printf("%d\r\n", nSum);
2. 成员函数指针的使用
从进入C++的课程以来,我们讲述了类中成员函数的调用方式,它们比起普通的函数多了一个传递this指针的过程,因此,我们刚才说的定义函数指针的方法对于成员函数而言是无效的。其解决方法也自然的就变成了怎么传递一个this指针呢?
回想一下,我们在类外定义成员函数的代码,比如:
bool CExample::SetNum(int nFirst, int nSec)
{
m_nFirstNum = nFirst;
m_nSecNum = nSec ;
return true;
}
对,它限定了作用域,所以它表示它仍然是类内的一个函数,那我们的函数指针也仿造它来写一个。
typedef void (Person::*PBASEFUN_TYPE)(); // 这里的*是为了与静态成员函数区分
好,我们编写如下代码(见Exp06):
class Person;
typedef void (*PFUN_TYPE)();
typedef void (Person::*PBASEFUN_TYPE)();
class Person
{
public:
PBASEFUN_TYPE *m_pFunPoint;
Person()
{
m_pFunPoint = (PBASEFUN_TYPE*)new PFUN_TYPE[2];
m_pFunPoint[0] = (PBASEFUN_TYPE)sayHello;
m_pFunPoint[1] = (PBASEFUN_TYPE)sayGoodbye;
}
void sayHello()
{
printf("person::Hello\r\n");
}
void sayGoodbye()
{
printf("person::Goodbye\r\n");
}
};
这样,我们就简单的实现了一个成员函数的指针数组,算是为我们学习类的多态性中虚函数大点儿基础,相信大家应该能看明白吧。
调用测试一下:
Person thePer;
(thePer.*thePer.m_pFunPoint[0])();
五、 几点注意事项
1、 一定要注意函数指针指向内容的释放以免防止内存泄露。
2、 注意及时将不用的指针赋值为NULL,以避免野指针的问题。
六、 小结
指针为我们的编程提供了诸多的技巧,简化了我们的代码。当然,于此同时,它也有反面的作用:当今软件中的内存错误及不稳定,90%以上是由于指针操作失误而导致的,这足以见得指针的破坏力。
因此,有效、合理、安全的使用指针是每个C程序员必备的基本素质。