c语言中的指针

访问一个变量的值能通过它的变量名获取:

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的解读:

  1. p保存了a的地址, 因此说:p指向了`a``
  2. ``p不是aa也不是p。 修改p的值不影响a的值,修改a的值也不会影响p`的值

定义语句之外的*p的解读:

  1. *p就是以p的内容为地址的变量

    补充理解:定义语句之外的*符号,作用是“取值”,*p就是“把地址是p的数据取出来”

  2. p指向了a后,*p 就完全等同于a,在所有出现*p的地方都可以替换成a,在所有出现a的地方都可以替换成*p

指针变量的一些性质

  1. 指针变量就是存放内存单元编号的变量,也可以说指针变量就是存放地址的变量

  2. 指针和指针变量是两个不同的概念,但是通常叙述的时候会把指针变量简称为指针,实际它们的含义并不一样

  3. 指针的本质就是一个【操作受限】的非负整数

    操作受限指的是指针不能进行加、乘、除操作,因为拿地址来相加、相乘和相除没啥意义,只有相减有意义(要保证是同一块连续空间中的不同存储单元),因为能算出两者相隔多少单元。

    比如同一个学校的三年11班与三年1班(同一块连续空间中的不同存储单元),两个地址相加、相乘和相除没啥意义,但是相减就能算出两个相隔了10个班

  4. 指针变量自身也有地址

    int *p;
    printf("指针变量p的地址是:%p\n",&p);
    
  5. 定义语句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**类型变量的地址

  6. 指针变量一定要初始化或赋予指向后再操作:

    int * p;
    int i = 5;
    *p = i;
    printf("%d\n",*p);
    

    上述写法是不正确的,指针变量p没初始化,也没定指向,因此p此时是个垃圾值(我们不知道他指向的是哪个地址)。系统给指针变量p和整形变量i在内存中开辟了空间给我们操作,但是语句*p = i修改了一个不属于我们可操作的单元,因此可能会出现不可估量的错误。

  7. 其实一维数组的名称是个指针常量,它存放的是一维数组第一个元素的地址

    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]

  8. 如果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语言就是这样规定的,人家就这么设计的

  9. 数组的名称是指针常量,和指针变量不同

    利用指针变量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是数组名,是常量,不能拿来加减

  10. 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本身占多少字节

  1. 指针变量不管是什么类型,本身所占的大小都是一样的,在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)指针变量占几个字节

  2. 尽管所有类型的指针变量自身都是占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个字节的内容,而变量aint类型,在内存空间占4个字节,因此用char类型的指针访问变量a的时候访问的内存空间不全。

  3. 如果确定某个地址存放的东西是需要用来操作的,可以直接定义一个指针来存放这个地址,但是地址也需要强制转换一下:

    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的值,因此尽管ab是主函数内定义的变量,但是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;
}

变量iint类型

变量pint *类型,它存的是变量i的地址(指向i

变量qint **类型,它存的是变量p的地址(指向p

变量rint ***类型,它存的是变量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中,pint *类型,它是一个指向整形变量i的指针

&p就是p的地址,因此说&p就是一个指向p的指针,它指向一个【指向整形变量i的指针】,因此&pint **类型。

二维数组的地址

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]

  1. a是二位数组的名字,也是二维数组首元素a[0]的地址,所以a和a[0]的值相同

  2. a[0]本身也是数组{1,3,5,7}的名,因此a[0]的值和它的首元素的地址(&a[0][0])相同

  3. a[0]是一个【占用一个int大小的对象】的地址,a是一个【占用三个int大小的对象】的地址

    占用一个int大小的对象是指整型数a[0][0],因此a[0]等于&a[0][0]

    占用三个int大小的对象是指a这个数组,因为a这个数组有三个元素:a[0]a[1]a[2]

  4. *a得到的是a[0],因为a[0]a数组的首元素,而a[0]是地址,因此打印*a得到的是一个地址

    *a也可以理解为:取出{1,3,5,7}这整个数组。因此*a得到的是{1,3,5,7}这个数组的地址,也就是a[0]

  5. 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字节,也就是偏移了一个一维数组

  6. 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字节,也就是偏移了一维数组中的一个元素

  7. 小结一下,对于二位数组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个一维数组

补充:

  • 数组指针才是真正等同于二维数组的数组名

  • 二级指针不能简单粗暴指向二维数组

指向函数的指针(函数指针)

定义函数在编译时,系统会为这个函数分配一段存储空间,这段存储空间的起始地址(也称入口地址)称为这个函数的指针

  1. 与“数组名就是数组地址”类似,函数名也是函数的地址

  2. 定义一个指针指向函数,同样要注意指针的类型

    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 *类型的指针,也可以理解为最终返回一个整型数据的地址。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值