目录
1、指针概述
1.1 内存地址
1.2 内存和指针
2、指针变量
2.1 初识指针变量
2.2 指针的大小
2.3 使用指针变量
2.4 指针的运算
3、指针与函数
3.1 指向函数的指针
3.2 返回指针的函数
4、void 指针类型
访问内存中的数据有两种方式:直接访问和间接访问。直接访问就是通过变量来实现,因为变量是内存中某一块存储区域的名称;间接访问就是通过指针来实现。指针并不是用来存储数据的,而是用来存储数据在内存中的地址,可以通过访问指针达到访问内存中数据的目的。
计算机程序中使用的所有数据,必须存储在计算机的存储单元中,并且应能从计算机的存储单元中取出。每个存储单元都有唯一的地址,这就好比街道上每家每户都会有自己的门牌号一样。如下图所示,内存中从地址 1234 到地址 1237 中,存储了一个整型数值 66,而在内存地址1238中,存储了一个字符 “ A ”。
计算机内存被划分成按顺序编号的内存单元,这就是地址。如果在程序中定义了一个变量,在对程序进行编译时,系统就会给这个变量分配内存单元。
变量是内存中某一块存储区域的名称,对变量赋值就相当于把值存储到该存储区域中。如下图。
分析:表达式的进行如图所示,第 1 步是告诉计算机在存储中开辟一个整数的空间地址从1234到1237,也就是4个字节,第 2 步把数值 66 存储在这块存储区域,这块区域的名称叫做 value 。该表达式显示了变量和内存的直接联系。
不同的计算机使用不同的复杂的方式对内存进行编号,通常程序员不需要了解给定变量的具体地址,编译器会处理细节问题。在 C 语言中,只需要使用操作运算符 &(取地址运算符),就会返回一个对象在内存中的地址,如:&value,返回的地址指的是该存储区域的起始地址,对变量 value 来说就是1234。
变量的内存地址就是变量的指针(指针就是地址(口语中说的指针通常指的是指针变量))。如果有一个变量专门用来存放另一个变量的指针,则称为指针变量(指针变量就是用来存放地址(存放在指针中的值都被当成地址处理))。下图所:
p 是一个指针变量。变量 p 中存放的是变量 value 的指针(地址),指针变量 p 就是指向 value 的地址。
定义指针变量的一般形式:
指向数据类型 *指针变量名;
指针和变量的关系。代码和对应图如下:
int i;/*声明变量i*/
int* p1;/*声明指向整型变量的指针p1*/
char c;/*声明变量c*/
char* p2;/*声明指向字符型变量的指针p2*/
p1=&i;/*指针赋值*/
p2=&c;/*赋值指针*/
定义指针变量时,需注意:
(1)如果有int *p,指针变量名是 p,而不是 *p,*p是表达式。
(2)在定义指针变量时必须明确其指向的数据类型。 赋值的正确方式,如:
int i;
char c;
int* p;/*声明的是指向整型变量的指针p*/
p=&i;/*指针赋值时就须是整型变量的地址(除非有代码本身有特殊的需求)*/
/*p=&c;this is error*/
(3)指针变量中只能存放指针(地址),不要将一个非零数(或者任何其他非地址类型的数据)赋值给一个指针变量。如:
//int*p=1;/*this is error*/
int* p=NULL;/*正确的赋值形式,表示指针指向空*/
注意:指针变量 p 被初始化为NULL,值为NULL的指针称之为空指针。一般来说,所有的指针变量在定义时都必须初始化,否则就会成为野指针( 野指针概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的))。为了避免野指针,如果不明确要给指针赋什么值,就赋值为NULL。
通过上面的学习已经了解了内存是如何分配地址的,内存地址就是指针这一概念,也引出了指针变量的概念,专门用来存储变量地址的变量称之为指针变量。
指针变量也是变量。假如该指针变量名叫做 p,变量 p 的用途比较特殊,它很 “ 无私 ”,它代表的存储单元中存储的是另外一个变量的地址,假如该变量名叫 value,可以理解为这个指针变量 p 指向变量 value。当然可以直接访问 value,从而获得 value 的数据,也可以间接地通过访问 p 来获得 value 的数据。 如:
#include
int main()
{
int value = 1;
int* p = &value;
*p = *p + 1;
printf("value=%d,*p=%d", value, *p);
return 0;
}
分析:代码中的 p = &value,表示指针变量 p 指向变量 value,在这里是使用 & 取地址运算符进行关联的;代码 *p = *p + 1 表示改变指针变量 p 指向的变量的值,在这里是使用 * (取内容)指针运算符间接访问变量 value。
注 意:* 称为指针运算符,它表示获取指针变量所指向的变量的值,这是非常重要的。
假设已经存在一个整型变量 value,那么要得到指向变量 value 的指针变量 p,如下:
int* p=NULL;
p=&value;
也可在声明指针变量 p 时,直接对其初始化,如下:
int* p=&value;
对于指针变量 p,如果在程序中使用了指针运算符,也就是使用了 *p,那么 *p 表示指针 p 指向的存储单元的值,也就是变量 value 的值,从数值意义上理解,二者是一样的。
看一段代码并对其进行分析:
#include
int main()
{
/*对变量进行声明和赋值*/
int value = 1;
int temp = 2;
int* p1 = &value;
int* p2 = &temp;
int* p = NULL;
printf("未交换前*p1、*p2值为:> ");/*打印出未交换地址时p1、p2存储的地址所对应单元的内容(所指向的存储单元的值)*/
printf("*p1 = %d,*p2 = %d\n\n", *p1, *p2);
if (value < temp)/*符合条件,对地址进行交换*/
{
p = p2;
p2 = p1;
p1 = p;
}
printf("交换后*p1、*p2值为:> ");/*打印出交换地址后p1、p2存储的地址所对应单元的内容(所指向的存储单元的值)*/
printf("*p1 = %d,*p2 = %d\n\n", *p1, *p2);
printf("value、temp的值:> ");
printf("value = %d,temp = %d\n", value , temp);
return 0;
}
分析:为什么交换后输出的 value 和 temp 的值没有变化,而 *p1 和 *p2 的值发生了改变呢?这是因为,这里交换的是 p1 和 p2 中存储的数据,p1 里存储的数据 &value 变成了 &temp,p2里存储的数据由 &temp 变成了 &value,这就意味着,p1 指向由指向变量 value 改为了指向变量 temp,p2 指向则由指向变量 temp 改为了指向变量 value,但此时 value 和 temp 所表示的存储区域的值并没有变化,所以发生了 value 和 temp 不变,而 *p1 和 *p2 交换的结果。
在32位的机器上,地址是 32 个 0 或者 1 组成二进制序列,那地址就得用 4 个字节的空间来存储,所以一个指针变量的大小就应该是 4 个字节;在64位机器上,有64个地址线,那一个指针变量的大小是 8 个字节,才能存放一个地址。
总结:
(1)指针是用来存放地址的,地址是唯一标示一块地址空间的。
(2)指针的大小在32位平台是4个字节,在64位平台是8个字节。
修改上面的例子,如下:
#include
int main()
{
/*对变量进行声明和赋值*/
int value = 1;
int temp = 2;
int* p1 = &value;
int* p2 = &temp;
int* p ;
printf("未交换前*p1、*p2值为:> ");/*打印出未交换时p1、p2存储的地址所对应单元的内容(所指向的存储单元的值)*/
printf("*p1 = %d,*p2 = %d\n\n", *p1, *p2);
if (value < temp)/*符合条件,对p1和p2所指向的存储单元的值进行交换*/
{
*p = *p2;
*p2 = *p1;
*p1 = *p;
}
printf("交换后*p1、*p2值为:> ");/*打印出交换后p1、p2存储的地址所对应单元的内容(所指向的存储单元的值)*/
printf("*p1 = %d,*p2 = %d\n\n", *p1, *p2);
printf("value、temp的值:> ");
printf("value = %d,temp = %d\n", value, temp);
return 0;
}
分析:运行,弹出错误对话框,为什么? 我们注意到了修改的代码和原来的代码的 if 语句中,交换的并不是指针变量 p1 和 p2 自身存储的值(也就是所存储的地址)(原来的代码是这样交换的 ),而是交换 p1 和 p2 所指向的存储单元的值。也就是说,我们试图去交换变量 value 和 temp 的值,但是指针变量 p 只是声明了,但没有明确的指向,那它很可能指向了一个内存中不存在的存储区域,这样执行 *p = *p1 时就会把变量 value 的值存储在一个不存在的存储区域内,程序自然会报错。
再次对代码进行修改 ,如下:
#include
int main()
{
/*对变量进行声明和赋值*/
int value = 1;
int temp = 2;
int* p1 = &value;
int* p2 = &temp;
int p ;
printf("未交换前*p1、*p2值为:> ");/*打印出未交换时p1、p2存储的地址所对应单元的内容(所指向的存储单元的值)*/
printf("*p1 = %d,*p2 = %d\n\n", *p1, *p2);
if (value < temp)/*符合条件,对p1和p2所指向的存储单元的值进行交换*/
{
p = *p2;
*p2 = *p1;
*p1 = p;
}
printf("交换后*p1、*p2值为:> ");/*打印出交换后p1、p2存储的地址所对应单元的内容(所指向的存储单元的值)*/
printf("*p1 = %d,*p2 = %d\n\n", *p1, *p2);
printf("value、temp的值:> ");
printf("value = %d,temp = %d\n", value, temp);
return 0;
}
分析: 运行,又会出现什么情况?程序正常执行,没有异常错误报出,原因是变量 p 不再是指针变量,它的存储区域是声明 p 时程序分配好的、明确的。交换后输出的 value 、 temp、 *p1 和 *p2 的值都发生了改变。这次是交换 p1 和 p2 所指向的存储单元的值。也就是说,交换变量 value 和 temp 的值,所以最后导致了value 、 temp、 *p1 和 *p2 的值都发生了改变。
关于指针变量的运算方法,指针变量自身又是怎么运算的呢?指针的运算就是地址的运算。由于这一特点,指针运算不同于普通变量,只允许有限的几种运算。除了可以对指针赋值外,指针的运算还包括移动指针、两个指针相减、指针与指针或指针与地址之间进行比较等。可以通过将指针加减一个整数,或者通过对指针赋值来移动指针。
如:假设 p 是一个指针 ,p+n、p-n、p++、p--、++p和--p等,其中n是一个整数。
将指针 p 加上或者减去一个整数 n,表示 p 向地址增加或减小的方向移动 n 个元素单元,从而得到一个新的地址,使其能访问新地址中的数据。每个数据单元的字节数取决于指针的数据类型。
假设有五个变量a、b、c、d 和 e 都是整型数据 int 类型,它们在内存中占据一块连续的存储区域,地址从 1234 到 1250(为什么地址从 1234 到 1250,原因:一个字节给一个对应的地址),每个变量占用 4 个字节。指针变量 p 指向变量a,也就是 p 的值是1234,那么 p+1 按照前面的介绍,表示 p 向地址增加的方向移动了 4 个字节,从而指向一个新的地址,这个值就是 1238 指向了变量 b (变量从 a 到 e 占用一块连续的存储区域);同理,p+2地址就是 1242,再次增加了 4个字节,指向了变量 c,依次类推。 图如下:
指针和指针类型:
变量有不同的类型,整型、字符型、浮点型等。指针也有类型,char* 类型的指针是为了存放 char 类型变量的地址。 short* 类型的指针是为了存放 short 类型变量的地址。 int* 类型的指针是为了存放 int 类型变量的地址。那么指针的类型有什么意义呢?指针的类型和指针的运算有着莫大的关系。
例1:看代码总结知识
#include
int main()
{
int n = 10;
char* pchar = &n;
int* pint = &n;
printf("%p\n", &n);
printf("%p\n", pchar);
printf("%p\n", pchar + 1);
printf("%p\n", pint);
printf("%p\n", pint + 1);
return 0;
}
分析:从结果可以看出&n、pchar、pint的地址都是00000073567BFCA4的。字符指针pchar+1变成了00000073567BFCA5,整型指针pint+1变成了00000073567BFCA8;可以看出,字符指针+1向后走了1个字节,跳过一个字符(字符指针+2跳过两个字符,依次类推);整型指针+1向后走了4个字节,跳过一个整型(整型指针+2跳过两个整型,依次类推)。不同类型的指针+1或者+n向走的字节数是不一样的。同理,减也是也是一样的。
总结: 指针的类型决定了指针向前或者向后走一步有多大(距离)。
例2:对比两段代码总结知识
(1)
#include
int main()
{
int n = 0x11223344;/*把一个十六进制数放到 n 里面*/
int* pint = &n;
*pint = 0;
return 0;
}
F10调试,打开内存,地址那里:&n,这个时候可以看到 n 的地址和内存里的四个字节存储的数据。
再按F10调试,将指针 pint 进行解引用操作后赋值为零,可看到内存里的四个字节存储的数据都变为0;
(2)
#include
int main()
{
int n = 0x11223344;
char* pchar = &n;
*pchar = 0;
return 0;
}
换成字符指针,存储 n 的地址, F10调试,打开内存,地址那里:&n,这个时候可以看到 n 的地址和内存里四个字节存储的数据,内存里存储的数据和上例一摸一样。
再按F10调试,将指针 pchar 进行解引用操作后赋值为零,仅看到内存里的一个字节存储的数据变为0,其他三个字节存储的数据都不发生改变。
对比上面的两段代码可以得出:指针类型决定了指针进行解引用操作的时候,能访问空间的大小。
总结:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
用指针变量可以指向一个函数。函数在程序编译时被分配了一个入口地址,这个函数的入口地址就称为函数的指针。 (函数指针---是指向函数的指针---存放函数地址的指针)
函数指针变量常用的用途之一是把指针作为参数传递到其他函数。指向函数的指针可以作为参数,以实现函数地址的传递,这样就能够在被调用的函数中使用实参函数。
指向函数的指针的代码,例:
#include
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
int (*pAdd)(int, int) = Add;/*指向函数的指针Add函数(&Add和Add是等效的,都是取出Add的地址)*/
printf("Add函数的地址为:%p\n", Add);
printf("用来存放Add函数地址的函数指针变量pAdd中的值为:%p\n", pAdd);
c = (*pAdd)(a, b);/*Add函数的返回值*/
printf("%d", c);
return 0;
}
分析:从结果可看出,pAdd成功存储了Add的地址;“ int (*pAdd)(int,int); ”说明pAdd是一个指向函数的整型指针(函数是:int Add(int x,int y)函数的类型是 int 。若函数是:int* Add(int* x,int* y),则指向函数的指针为:int* (*pAdd)(int*,int*))。“ c=(*pAdd)(a,b); ”说明pAdd确切指向函数Add,相当于调用了c=Add(a,b);
指向函数的指针作为参数,以实现函数地址的传递,例:
/***** 修改冒泡排序,使它可以排序任何类型的数据,此例是对一个数组元素进行升序排序*****/
#include
int cmp_int(const void* e1,const void* e2 )
{
//比较两个整型的大小
return (*(int*)e1 - *(int*)e2);
}
void sawp(char* e1, char* e2,int width)
{
/*实现两个元素的交换*/
int i = 0;
for (i = 0; i < width; i++)
{
char temp = *e1;
*e1 = *e2;
*e2 = temp;
e1++;
e2++;
}
}
void bubble_sort(void *base,int num,int width,int (*cmp)(const void *e1,const void *e2))
{
int i = 0;
int j = 0;
for (i = 0; i < num - 1; i++)//趟数
{
for (j = 0; j < num - i - 1; j++)//一趟比较的次数
{
if ((*cmp)((char*)base + j * width, (char*)base + (j + 1) * width)>0)
{
sawp((char*)base + j * width, (char*)base + (j + 1) * width,width);
}
}
}
}
void test()
{
int i = 0;
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);/*计算数组元素个数*/
bubble_sort(arr,sz,sizeof(arr[0]),cmp_int);/*对数组进行升序排序*/
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
test();
return 0;
}
这里对于排序算法就不做解释分析了,主要目的是进一步了解函数指针的用途。
函数可以返回数值型、字符型、布尔型(bool:C语言里是用整数来代替布尔结果这一个角色的,只要数字非0都表示为 true ,0表示 false ;倒过来说,表示 true 的在执行期间会被转换为数字1处理, false 则用0代替。)等数据,也可以带回指针型的数据叫做返回指针值的函数。定义形式为:
类型名 *函数名(参数列表);
例如下式表示的含义是:Add函数调用后返回值的数据类型是整型指针。
int* Add(int* x,int* y);
返回指针的函数,例如:
#include
int* Add(int* x, int* y)
{
/*返回指针的函数*/
static int temp = 0;
temp = (*x) + (*y);
*x = 66;
*y = 99;
return &temp;
}
int main()
{
int a = 10;
int b = 20;
int* value = NULL;
value=Add(&a, &b);/*用一个指针变量接收Add函数返回的地址*/
printf("Value: %d\n", *value);/*拿到地址对地址进行解引用操作*/
printf("a: %d\n", a);
printf("b: %d\n", b);
return 0;
}
分析:从结果可以看出,函数Add接收a、b两个数的地址,对地址进行解引用操作,求这两个数的和,并将 temp 的地址作为 Add 的函数返回值。函数只能有一个返回值,然而我们却偏偏希望返回给主函数3个值,还有两个就是a、b重新赋值的,使用的方法叫做引用。
int a=10;
int b=20;
int* value = NULL;
value=Add(&a, &b);/*参数&a、&b就是引用,用来接收形参*x、*y*/
在函数 Add() 中,“ *x=66; *y=99; ”就是把 66 和 99 存放在指针变量 x、y 所指向的存储单元中,也就是存放在实参a、b中。
通过本例子的引用方法,给我们开发程序带来很大的便利,特别是需要调用函数返回多个返回值时,我们可以根据需要灵活使用。
void 指针类型,可以用来指向一个抽象类型的数据,在将它的值赋给另一个指针变量时要进行强制类型转换,使之适合于被赋值的变量的类型。(void*)为 “ 无类型指针 ”。
通过一个例子来说明 void 指针类型的含义和用法,代码如下:
#include
int main()
{
char* pchar = NULL;
void* pvoid = NULL;/*在将 pvoid 的值赋给另一个指针变量时要进行强制类型转换,使之适合于被赋值的变量的类型。*/
pchar = (char*)pvoid;/*对 pvoid 进行强制类型转换,使之适合于 pchar 的变量的类型。(pchar 变量的类型是 char*)*/
return 0;
}
同样可以使用 (void*)pchar 将 pchar 转换成 void* 类型,代码如下:
pvoid=(void*)pchar;
不只对变量可以使用 void 指针类型,也可以将一个函数定义为 void* 类型。如:下面的代码表示函数 funtion 返回的是一个地址,它指向空类型,如需要引用此地址,也需要根据情况对返回的地址进行类型转换。如下:对 funtion 函数调用得到的地址要进行以下转换。
#include
void* funtion(char ch1, char ch2)
{
//everything
//return 地址
}
int main()
{
char ch1 = 'A';
char ch2 = 'B';
char* pchar = NULL;
pchar = (char*)funtion(ch1,ch2);/*对 funtion 函数调用得到的地址要进行转换,使之适合于 pchar 的变量的类型。( pchar 变量的类型是 char*)*/
return 0;
}
对于void 指针类型的使用,需要注意以下几点:
(1)void* 类型的指针,可以接收任意类型的地址。如:
#include
int main() { /*void* 类型的指针接收下列的其他类型的地址是没有问题的*/ int a = 10; char ch = 'W'; void* pvoid1 = &a; void* pvoid2 = &ch; return 0; } (2)void* 类型的指针,不能进行解引用操作。
(3)void* 类型的指针,不能进行加减(+-)整数的操作。