访问一个变量的值能通过它的变量名获取:
int a = 10;
printf("a的值是:%d\n",a);
也可以通过访问它在内存中的地址获取它的值:
int a = 10;
printf("a在内存中的地址是:0x%p\n",&a);//输出结果:a在内存中的地址是:0x000000000061FE1C
printf("a的值是:%d\n",*(&a));//输出结果:a的值是:10
其中,
*
是取值运算符,它可以把内存地址中的数据“取出来”。
如果定义一个变量来存储地址,这个变量就叫指针变量。
指针就是地址,地址就是指针。指针变量就是存放指针的变量(也可以说是存放地址的变量)
地址就是内存单元的编号
定义指针变量
定义一个指针变量:
int *p;
p
是变量的名字, int *
表示p变量存放的是int
类型变量的地址
语句int * p
不表示定义了一个名字叫做*p
的变量,正确理解应该是: p
是变量名, p
变量的数据类型是int *
类型(int *
类型就是值存放int
变量地址的类型)
补充理解:这里的
*
是一个标识符,用来告诉系统这是一个指针变量,是用来保存别人的地址的。和取值运算符*
虽然长得一样,但两者用法不同。标识符的作用只产生在指针变量定义或声明的时候,其它时候则都是用作取值运算符。
定义一个整形变量a
并把它的地址赋给指针变量p
:
int a = 10;
int *p;
p = &a;
printf("a的值是:%d\n",*p);//这里的*是取值运算符
printf("a的地址是:%p\n",p);
p = &a
的解读:
p
保存了a
的地址, 因此说:p
指向了`a``- ``p
不是
a,
a也不是
p。 修改
p的值不影响
a的值,修改
a的值也不会影响
p`的值
定义语句之外的*p
的解读:
-
*p
就是以p
的内容为地址的变量补充理解:定义语句之外的
*
符号,作用是“取值”,*p
就是“把地址是p
的数据取出来” -
p
指向了a
后,*p
就完全等同于a
,在所有出现*p
的地方都可以替换成a
,在所有出现a
的地方都可以替换成*p
指针变量的一些性质
-
指针变量就是存放内存单元编号的变量,也可以说指针变量就是存放地址的变量
-
指针和指针变量是两个不同的概念,但是通常叙述的时候会把指针变量简称为指针,实际它们的含义并不一样
-
指针的本质就是一个【操作受限】的非负整数
操作受限指的是指针不能进行加、乘、除操作,因为拿地址来相加、相乘和相除没啥意义,只有相减有意义(要保证是同一块连续空间中的不同存储单元),因为能算出两者相隔多少单元。
比如同一个学校的三年11班与三年1班(同一块连续空间中的不同存储单元),两个地址相加、相乘和相除没啥意义,但是相减就能算出两个相隔了10个班
-
指针变量自身也有地址
int *p; printf("指针变量p的地址是:%p\n",&p);
-
定义语句
int * p
的正确解读:int * p; //p是变量的名字, int * 表示p变量存放的是int类型变量的地址 int i = 3;
此时如果写
p = i;
或p = 55;
会报错,因为类型不一致,p
只能存放int
类型变量的地址,不能存放int
类型变量的值int * p; char ch = 'A';
此时如果写
p = &ch;
也会报错,因为p
只能存放**int
类型变量的地址,不能存放char
**类型变量的地址 -
指针变量一定要初始化或赋予指向后再操作:
int * p; int i = 5; *p = i; printf("%d\n",*p);
上述写法是不正确的,指针变量
p
没初始化,也没定指向,因此p此时是个垃圾值(我们不知道他指向的是哪个地址)。系统给指针变量p
和整形变量i
在内存中开辟了空间给我们操作,但是语句*p = i
修改了一个不属于我们可操作的单元,因此可能会出现不可估量的错误。 -
其实一维数组的名称是个指针常量,它存放的是一维数组第一个元素的地址
int a[5]; printf("a的地址是:%p",a); printf("a[0]的地址是:%p",a[0]);
输出结果:
a的地址是:000000000061FE00 a[0]的地址是:000000000061FE00
因此如果想定义一个指针存放
a
数组第一个元素的地址可以直接这样写:int * p = a;
访问数组a的第二个和第三个元素的时候也可以这样写:
p[1]
,p[2]
-
如果p是个指针变量,则
p[i]
永远等价于*(p+i)
#include <stdio.h> int main(void) { int p[6] = {1,2,3,4,5,6}; printf("p的地址是:%p\n",p); printf("p[0]的地址是:%p\n",&p[0]); for(int i=0;i<6;i++) { printf("p[%d]=%d,地址是:%p\n",i,*(p+i),p+i); } return 0; }
输出结果:
p的地址是:000000000061FE00 p[0]的地址是:000000000061FE00 p[0]=1,地址是:000000000061FE00 p[1]=2,地址是:000000000061FE04 p[2]=3,地址是:000000000061FE08 p[3]=4,地址是:000000000061FE0C p[4]=5,地址是:000000000061FE10 p[5]=6,地址是:000000000061FE14
可以看出,数组名
p
就是一个地址,它存放着第一个元素的地址。数组p
里面的元素地址是连续的,地址相隔4个字节(因为int
类型占4个字节)值得注意的是,如果地址相减,得出的结果是:两者距离有多少个单元。比如用数组第二个元素的地址减第一个元素的地址
p-(p+1)
(等价于&p[1]-&p[0]
),结果是1,而不是4,因为相减后编译器还会做一步隐式操作,就是再除以sizeof(int)
(因为p[1]
和p[0]
都是int
类型)。至于为啥还要做这一步隐式操作,只能说C语言就是这样规定的,人家就这么设计的 -
数组的名称是指针常量,和指针变量不同
利用指针变量
p
打印数组元素:int arr[3] = {1,2,3}; int * p = arr; for(int i=0; i<3; i++) printf("%d ",*p++);
此时可以正常打印出数组的元素,但不可以用下方代码打印出数组元素
int arr[3] = {1,2,3}; for(int i=0; i<3; i++) printf("%d ",*arr++)
因为
arr
是数组名,是常量,不能拿来加减 -
sizeof(数组名)
和sizeof(指针变量)
结果不同
int arr[] = {1,2,3};
int * p;
p = arr;
printf("sizeof(arr)结果是:%d\n",sizeof(arr));
printf("sizeof(p)结果是:%d\n",sizeof(p));
输出结果是:
sizeof(arr)结果是:12
sizeof(p)结果是:8
sizeof(arr)
是输出整个arr
数组占多少个字节
sizeof(p)
是输出指针变量p
本身占多少字节
-
指针变量不管是什么类型,本身所占的大小都是一样的,在64位计算机中,指针变量都是占8字节
#include <stdio.h> int main(void) { int a = 1; double b = 2; char c = 'A'; int * p = &a; double * q = &b; char *r = &c; printf("指针变量p所占的大小是:%d\n",sizeof(p)); printf("指针变量q所占的大小是:%d\n",sizeof(q)); printf("指针变量r所占的大小是:%d\n",sizeof(r)); return 0; }
输出结果:
指针变量p所占的大小是:8 指针变量q所占的大小是:8 指针变量r所占的大小是:8
为什么是8个字节呢:
这里只简单说明一下:在64位的计算机内存中,地址编号是从000…0(总共64个0)开始,到最后一个111…1(总共64个1),也就是说只要0或1有64个,就能确定内存中所有的地址编号。而64位就等于8字节,因此所有的指针变量本身所占的大小都是8个字节。
再详细的可参考这篇文章:C语言之指针(5)指针变量占几个字节
-
尽管所有类型的指针变量自身都是占8个字节(在64位计算机中),但是在定义指针变量的时候依然要区分类型,因为指针变量的类型决定了【指向空间的大小】。
#include <stdio.h> int main(void) { int a = 123456; int *p = &a; char *c = &a; printf("变量p的值是:%p;*p的值是:%d\n",p,*p); printf("变量c的值是:%p;*c的值是:%d\n",c,*c); return 0; }
编译的时候,语句
char *c = &a;
会产生警告:warning: initialization of ‘char *’ from incompatible pointer type ‘int *’ [-Wincompatible-pointer-types]输出结果如下:
变量p的值是:000000000061FE0C;*p的值是:123456 变量c的值是:000000000061FE0C;*c的值是:64
可以看到尽管
int *
类型和char *
类型的指针变量都能存储变量a
的地址,但是通过指针去访问变量的值的时候就会出错。这是因为取值运算符*
会根据指针变量的类型访问不同大小的内存空间,int
类型占4个字节,char
类型占一个字节,char
类型的指针只能访问到1个字节的内容,而变量a
是int
类型,在内存空间占4个字节,因此用char
类型的指针访问变量a
的时候访问的内存空间不全。 -
如果确定某个地址存放的东西是需要用来操作的,可以直接定义一个指针来存放这个地址,但是地址也需要强制转换一下:
int * p = (int *)000000000061FE1C;
小例子
例一:互换两个数
#include <stdio.h>
void func(int *p, int *q)//形参名字是p和q
{
int t;
t = *p;
*p = *q;
*q = t;
}
int main(void)
{
int a = 3;
int b = 4;
func(&a,&b);
printf("a=%d,b=%d\n",a,b);
return 0;
}
传入func()
的是变量a
和变量b
的地址,func()
根据地址去操作变量a
和变量b
的值,因此尽管a
和b
是主函数内定义的变量,但是func()
依然能改变它们的值
例二:设计一个能打印数组的函数
#include <stdio.h>
int func(int *pArr,int len)
{
for(int i=0; i<len; i++)
printf("%d ",*(pArr+i));//*(pArr+i)等价于pArr[i]
}
int main(void)
{
int data[5] = {1,2,3,4,5};
int len = sizeof(data)/sizeof(data[0]);
func(data,len);
return 0;
}
例三:将数组中的n个元素按逆序存放
#include <stdio.h>
void func(int *arr, int len)
{
int t;
for(int i=0; i<len/2; i++)
{
t = *(arr+i);
*(arr+i) = *(arr+len-1-i);
*(arr+len-1-i) = t;
}
}
void printArr(int *arr, int len)
{
for(int i=0; i<len; i++)
printf("%d ",*(arr+i));
}
int main(void)
{
int arr[] = {1,2,3,4,5,6,7};
int len = sizeof(arr)/sizeof(*arr);
func(arr,len);
printArr(arr,len);
return 0;
}
补充
多级指针
#include <stdio.h>
int main(void)
{
int i = 10;
int * p = &i;
int ** q = &p;
int *** r = &q;
return 0;
}
变量i
是int
类型
变量p
是int *
类型,它存的是变量i
的地址(指向i
)
变量q
是int **
类型,它存的是变量p
的地址(指向p
)
变量r
是int ***
类型,它存的是变量q
的地址(指向q
)
使用多级指针作为函数的参数时,要注意*
号有多少个:
#include <stdio.h>
func(int **q)//注意func的参数有两个星号
{
...;
}
int main(void)
{
int i = 10;
int * p = &i;
func(&p);//调用func函数时传入参数&p
return 0;
}
在func1
中,p
是int *
类型,它是一个指向整形变量i
的指针
&p
就是p
的地址,因此说&p
就是一个指向p
的指针,它指向一个【指向整形变量i
的指针】,因此&p
是int **
类型。
二维数组的地址
int a[3][4] = {{1,3,5,7},{9,11,13,15},{17,19,21,23}};
a
数组有3行,即有3个“行元素”:a[0]
,a[1]
,a[2]
。a
是数组名,数组名a
是第一个元素a[0]
的地址
而a[0]
,a[1]
,a[2]
又分别是一个一维数组。它们各自都有4个元素:
a[0]:a[0][1]=1 a[0][2]=3 a[0][3]=5 a[0][4]=7
a[1]:a[1][1]=9 a[1][2]=11 a[1][3]=13 a[1][4]=15
a[2]:a[2][1]=17 a[2][2]=19 a[2][3]=21 a[2][4]=23
因此a[0]
是数组a
的首元素,也是一维数组{1,3,5,7}的数组名,a[1]
是{9,11,13,15}的数组名;a[2]
是{17,19,21,23}的数组名
数组名是数组首元素的地址,因此a[0]
也是数组{1,3,5,7}这个数组中第一个元素的地址,即&a[0][0]
。同理,a[1]
等价于&a[1][0]
,a[2]
等价于&a[2][0]
-
a
是二位数组的名字,也是二维数组首元素a[0]
的地址,所以a和a[0]
的值相同 -
a[0]
本身也是数组{1,3,5,7}的名,因此a[0]
的值和它的首元素的地址(&a[0][0]
)相同 -
a[0]
是一个【占用一个int大小的对象】的地址,a
是一个【占用三个int大小的对象】的地址占用一个int大小的对象是指整型数
a[0][0]
,因此a[0]
等于&a[0][0]
占用三个int大小的对象是指
a
这个数组,因为a
这个数组有三个元素:a[0]
,a[1]
,a[2]
-
*a
得到的是a[0],因为a[0]
是a
数组的首元素,而a[0]
是地址,因此打印*a
得到的是一个地址*a
也可以理解为:取出{1,3,5,7}这整个数组。因此*a
得到的是{1,3,5,7}这个数组的地址,也就是a[0]
-
a+1
是从a[0]
偏移到a[1]
int a[3][4] = {{1,3,5,7},{9,11,13,15},{17,19,21,23}}; printf("%p\n",a); printf("%p\n",a+1); 输出结果: 000000000061FDF0 000000000061FE00
把结果转换为十进制,可以看到两者差了16字节,也就是偏移了一个一维数组
-
a[0]+1
是从a[0][0]
偏移到a[0][1]
int a[3][4] = {{1,3,5,7},{9,11,13,15},{17,19,21,23}}; printf("%p\n",a[0]); printf("%p\n",a[0]+1); 输出结果: 000000000061FDF0 000000000061FDF4
把结果转换为十进制,可以看到两者差了4字节,也就是偏移了一维数组中的一个元素
-
小结一下,对于二位数组
a
:*a
得到的是a[0]
a
+1偏移的是一个一维数组a[0]
+1偏移的是一维数组中的一个元素
指向二位数组的指针(数组指针)
定义一个指针p指向一维数组,并利用指针自加打印数组
int arr[] = {1,2,3,4,5};
int * p;
p = arr;
for(int i=0;i<5;i++)
printf("%d ",*p++);
指针p
每次自加1,就会偏移1个元素
定义一个指针p
指向二维数组首元素,并利用指针自加打印数组
int arr[2][3] = {{11,22,33},{44,55,66}};
int * p;
int i,j;
p = &arr[0][0];
for(i=0;i<2;i++)
for(j=0;j<3;j++)
printf("%d\n",*p++);
此时指针p
每次自加1,偏移的也是1个元素
如果直接把数组名arr
赋给指针p
:
int arr[2][3] = {{11,22,33},{44,55,66}};
int * p;
int i,j;
p = arr;
for(i=0;i<2;i++)
for(j=0;j<3;j++)
printf("%d\n",*p++);
上述代码尽管依旧能输出整个二位数组,但是会有警告出现:
warning: assignment to 'int *' from incompatible pointer type 'int (*)[3]'
修改成如下代码警告就不会出现:
int arr[2][3] = {{11,22,33},{44,55,66}};
int (* p)[3];
int i,j;
p = arr;
for(i=0;i<2;i++)
for(j=0;j<3;j++)
printf("%d\n",*(*(p+i)+j));
p
称为数组指针,此时p
+1偏移的是1个一维数组
补充:
-
数组指针才是真正等同于二维数组的数组名
-
二级指针不能简单粗暴指向二维数组
指向函数的指针(函数指针)
定义函数在编译时,系统会为这个函数分配一段存储空间,这段存储空间的起始地址(也称入口地址)称为这个函数的指针
-
与“数组名就是数组地址”类似,函数名也是函数的地址
-
定义一个指针指向函数,同样要注意指针的类型
int func(int a, int b){...} int (*p)(int a ,int b); p = func;
函数指针使用方法例:用函数指针的方式调用自定义函数
#include <stdio.h>
void func1()
{
printf("Hello World!");
}
int main(void)
{
void (*p)();
p = func1;
(*p)();
return 0;
}
-
语句
void (*p)()
表示定义了一个指针,准备用它来指向func1
函数,因此指针前面的类型与小括号里面的参数要和func1
函数一致。定义函数指针小括号里面的参数是形式参数 -
语句
(*p)()
就是在调用func1
函数
存放指针的数组(指针数组)
数组里的元素都是指针类型,这个数组就是指针数组。
定义一个指针数组:
int * p[4]
理解:先看p[4]
,告诉系统这是个数组,长度为4,数组里面的元素都是int *
型
定义一个指针数组,里面的元素指向a
,b
,c
三个变量:
int a=1, b=2, c=3;
int * p[3] = {&a,&b,&c};
for(int i=0;i<3;i++)
printf("%d ",*p[i]);
定义一个指针数组,里面的元素指向函数
#include <stdio.h>
int func1(int a, int b){...}
int func2(int a, int b){...}
int func3(int a, int b){...}
int main(void)
{
int (*pArr[3])(int a, int b) = {func1,func2,func3};
return 0;
}
int (*pArr[3])(int a, int b)
理解:先看pArr
与[]
组合([]
的优先级高于*
),告诉系统它是一个数组,然后再与*
组合,说明数组里元素的类型是指针,后面的参数列表说明每个指针都准备用于指向函数,并且这些函数的返回值类型为int
返回指针的函数(指针函数)
函数最终返回一个指针类型的数据,这个函数就叫做指针函数
定义一个指针函数:
int * func(int a, int b)
func
是函数名,调用完func
函数后最终会得到一个int *
类型的指针
理解:先看func(int a, int b)
,这告诉系统func
是个函数,这个函数最终返回一个 int *
类型的指针,也可以理解为最终返回一个整型数据的地址。