C语言指针详析

指针的概念

·指针即为地址。每个指针所存储的内容是所指向变量的地址名
·在同一环境中,指针的内存大小是固定的。例如:64位环境即x64下,指针的大小为8Byte。因为64位代表内存与cpu间连接了64根地址总线,每根地址总线只有开和关两种状态,分别用1和0表示,占一个bit,所以64个bit位8个字节大。同理,32位即x86的环境下,指针的大小为4Byte
·例如:
在这里插入图片描述
我们还可以发现一个细节,就是内存中每个字节都对应一个地址,int类型则对应4个地址,int*指针存放的只是4个地址中的第一个地址,也就是低地址。

指针变量的创建和类型

创建一个指针变量的格式

1.·创建一个指针变量我们需要用到两个操作符:“ * ”和“ & ”。
· * 的作用为声明这是一个指针和解引用。
· & 的作用为取地址,而且对于这个操作符我们并不陌生,在使用库函数scanf时我们就常用到&。

#include<stdio.h>
int main()
{
int a=0;
scanf("%d",&a);
return 0;
}

从中我们也可以看出些许端倪:计算机修改数值的本质是通过指针实现的。
·创建指针变量的格式:
指针所指向的数据类型*指针变量名=指针所指向对象的地址
·接下来我们来创建一些基本的指针变量:

int num=10;
char a='A';
int*num2=&num;
char*a2=&a;

这样num2和a2分别是指向num和a的指针里面存储着他们的地址。
·需要注意的是被*修饰的变量会被当做地址看待,也就是说:

#include<stdio.h>
int main()
{
int a=10;
int*b=&a;
int*c=10;
printf("%p %p %p\n",&a,b,c);
return 0;
}

上述代码的结果为:
在这里插入图片描述
可以看到b和c就是地址可以被%p识别,且10被当作存储在c中的地址,故打印出000000A. (以上代码均在x86环境下运行)
2.那么创建完指针变量后该如何去使用它呢?
第一种使用方法就是取出指向变量的地址,用法如上。
第二种使用方法则是要操作指向变量的数值,这时候我们需要用到解引用操作符*。
在这里插入图片描述
可以看到*b其实就是相当于对b所存储地址指向的值进行操作,也就是等价于a。
·这时候可能就有人疑惑了,既然等价于a,那我一开始就对a操作不久行了吗,何必要用到指针呢?
–诚然这种场景下我们对原变量进行操作更为便捷,但特定场景下使用指针变量会更为合适。
接下来就由我来分析下指针的优势:

指针的优势

1.首先运用指针对指向数值操作时一种较为本质的方式,计算机内部对各种变量操作都是通过地址进行的。
2.有时候接收一个函数的返回值就正是一个指针,这时候我们就不得不对这个指针进行操作了,而无法直接对其指向的原变量进行操作。
3.当我们向一个函数传递两个int类型的值时(传值调用),该函数不能对实参修改。

void change(int a, int b)
{
	int c = a;
	a = b;
	b = c;
}
int main()
{
	int a = 10, b = 5;
	change(a, b);
	return 0;
}

上述代码并不能交换a和b的值。
但是但我们传过去的时a,b地址时(传址调用)就能交换a和b的值了。

void change(int* a, int* b)
{
	int c = *a;
	*a = *b;
	*b = c;
}
int main()
{
	int a = 10, b = 5;
	int* c = &a, d = &b;
	change(c, d);
	return 0;
}

4.可以更改const修饰的常变量的值:我们知道用const修饰一个变量后,这个变量就变成了常量,无法对其进行修改,但通过指针还是能进行修改的:

const int a=10;
int*b=&a;
*b=5;
printf("%d",a);

该代码的结果会是打印出5。
······
等等!貌似这并不是什么好事,我都用const修饰a了,当然是不想它改变了,你这不是卡bug吗?
别急,我们可以通过用const修饰指针来解决这个隐患。

const修饰指针

const修饰指针有两种位置,对应着不同的功能:
1.const在*前

int a=10;
const int*b=&a;等价于int const*b=&a;

这样修饰以后我们就不能通过指针来修改a的值了。
2.const在*后

int a=10;
const int*const b=&a;

这样修饰的指针依然可以修改a的值,但是不能修改b指向的对象是谁了。
比如:

int a=10;
int c=5;
const int*b=&a;
int*const d=&c;
我们可以进行b=&c;这样的操作但不能d=&a。

3.当然我们可以同时在两个位置上都放上const:

int a=10;
const int*const b=&a;

这样既不能通过b修改a的值也不能修改b指向的对象是谁。妥妥的控制狂!

指针的类型

·要说指针的类型我们需要复习下数组和函数的类型:
数组的类型为去掉数组名剩下的部分:

int num[10]={0};
那么该数组的类型为int [10];

同理,函数的类型亦是如此:

void fu(int ,char);
则该函数的类型为void (int,char);

所以指针的类型即为去掉指针变量名后剩下的部分:

int a=10;
int*b=&a;类型为int*
char b='A';
char*c=&b;类型为char*

二级指针

数组有二维数组,那指针自然有二级指针。
根据指针的格式我们很容易就可以得到一个二级指针:

int a=10;
int*b=&a;
int**c=&b;

·简单分析一下这串代码:
1.首先b里存储的是a的地址故b解引用后等价于a。
2.c被’*‘修饰了所以肯定是一个指针变量,而它指向的变量b的类型为int*,所以格式为int**=&b,那么c的类型为int**是一个二级指针。
3.所以*c等价于b,而*b又等价于a,所以**c等价于a。
·那多级指针也是同理,n级指针指向的变量为n-1级指针。

野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

野指针的成因

1.指针没有初始化

int*a;//没有初始化,默认为随机值
*a=20;

那么a就是一个标准的野指针。
2.数组越界访问

#include <stdio.h>
int main()
{
 int arr[10] = {0};
 int *p = &arr[0];
 int i = 0;
 for(i=0; i<=11; i++)
 {
 //当指针指向的范围超出数组arr的范围时,p就是野指针
 *(p++) = i;
 }
 return 0;
}

而且上述代码在vs2022运行的结果是死循环,这是由于栈中存储变量是由高地址到低地址存储的,数组中元素的地址随下标增大而增大。所以机缘巧合之下arr[12]的地址对应i的地址,当它被重置为0,那代码就无限地运行下去。
·可见野指针是非常危险的.
3.指针指向空间被释放

#include <stdio.h>
int* test()
{
 int n = 100;
 return &n;
}
int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;
 }

n是test里的一个局部变量,它的生命周期就是n创建到test结束,所以test调用完后n就被销毁了。这时p指向的位置是随机的。

规避野指针的方法

1.初始化指针:通常我们能会将一个暂时不知道指向哪里的指针初始化为NULL也就是0,即int * num=NULL。
2.防止数组越界以及不使用返回局部变量的地址。
3.当指针不再使用时,将它赋值为NULL。
4.那么使用一个指针的时候要检测其是否为NULL,这时候我们由两种方法:
第一种if(int*num!=NULL),这种方法缺点在于,每次使用指针都要进行一次判别且当该指针不可以用时不会告诉你不可用而是会跳过剩余代码。
第二种使用assert.h头文件下的库函数assert

#include<assert.h>
int*num=NULL;
assert(num!=NULL);

使用这个函数的好处在于,当num为空指针时,程序就会报错并且告诉你错误的行数,让你知道这是一个空指针下次就不用调用了。此外,当你知道所有空指针后就可以直接禁用assert就能节约判别的成本。禁用方式也很简单,只需要在assert.h前加上#define NDEBU。

#define NDEBUG
#include<assert.h>
int*num=NULL;
assert(num!=NULL);

指针的运算

首先需要明确的是只有相同类型的指针才能进行运算。

int num[10]={0];
int*a=num[0];
int*b=num[9];
int c=b-a;

那么c的值时多少呢,是b和a的地址差,是36还是9呢?
答案是9。
·指针间相减得到的是他们间的元素个数,他们所指向对象即为元素,这里a和b指向的对象均为int类型,int类型为4个字节,num[9]和num[0]之间相隔了36个字节也就是9个int,所以他们之间的元素个数为9.
·同理

int*a=0xFFFFFFFF
a--;
a的值就变成了0xFFFFFFFB

所以说指针类型不同,也就是±运算结果不同而已。
那么一些编译器里long的大小等于int,这时int*等价于long*。

指针数组和数组指针

一维数组的数组名理解

就本人而言,学习整型一维数组的时候我发现它与普通整型的不同之处:
在这里插入图片描述
1.数组传参调用函数的时候,在函数内改变数组的值,原数组的值也会跟着改变。
·这是不是很像前面在指针的优势中提到的传址调用!
2.在监视的窗口里,我们在主函数中只需要输入num就可以看到num里面的值,但是进入函数后num却只显示首元素的值。
·要想监视所有的元素要num,6。
3.我们发现形参接受数组的时候是"int num[]"。
·如果将[]去掉,那么num就变成了一个整型变量。我的意思是以整体的角度分析num[]是一个整型,那这样的话num是什么,[]又代表什么?
······
其实我们可以类比指针:

int a=10;
int*b=&a;

上述代码中,b是a的地址,*b就是a。也就是*b即为整型。
Okay!!
如此高度的相似性,是不是说数组名其实就是一个指针?
答案是肯定的。
·事实上数组名大多时候都是首元素地址,这就告诉我们为什么函数调用会改变数组的数值,因为这就是传址调用!
那在自定义函数中,传过去的数组就相当于指针而已,所以它不知道这个数组有多大,需要自己写出来才能监视后面的内容!(咳咳!这告诉我们自定义函数里不能求传过去的数组大小。。。
那么[]的作用就很明显了,就是解引用嘛~事实上num[0]就相当于*(num+0)。
·当然有些时候数组名并不是代表首元素地址,而是整个元素地址。分别有以下两种情况。
1.sizeof里面含数组名的时候:

int num[]={1,2,3,4,5};
int a=sizeof(num);
int b=sizeof(num+0);

num里面有五个整型,所以num的大小为20。所以a的数值为20。
刚刚我们提到num相当于首元素地址,所以num+0不还是首元素地址吗?所以b也等于20咯!—错!我们还提到sizeof里面要只含数组名,所以这个num+0相当于地址,b的大小应该为4/8。
2.&数组名

int num[]={1,2,3,4,5};
int*a=num;
int*b=&num[0];
int*c=&num;

上述代码中a、b和c的值是相同的,都是num首元素的地址。不同的是a、b代表的是首元素地址,a+1和b+1都代表num第二个元素的地址。而c代表的是整个数组的地址,所以c+1的地址是num[5]。已经越界咯!
·这里再考你个问题sizeof(&num)的值是多少呢?
······
答案是4/8啦。
·总结整型一维数组名num类型为int*,&num的类型为int(*)[5]。

指针数组

指针数组…那么他是指针还是数组呢?=.=
好吧既然是xx数组,那这肯定是个数组啦。数组里面的元素就都是同一类型的指针咯。

int a=0;b=0;
int* arr[]={&a,&b}

如上所示,数组arr里面存放的元素类型皆为int*,所以就应该写成int*arr[]。

数组指针

数组指针…这题我会,就是指针嘛!(即答)–正确的。

int num[10]={0};
int(*a)[10]=&num;

如上a就是一个数组指针变量。他的类型是int(*)[10]
其实我们发现数组指针的声明和指针数组的声明非常类型:这时指针数组->int *a[10],这时数组指针->int(*a)[10]。
其实也就是一个()区别,那么这又是为什么呢?
前面我们学到过操作符的优先级,而[]和()的优先级都是最高的,如果a和*不加()那么a先和[]结合,就被定义成数组,相反如果(*a)那么a先和*结合,就被定义为指针。ps:后面学到函数指针也是同理。
·那么数组指针又该如何使用呢?首先num是不是数组首元素地址,a是不是也是数组首元素地址,所以a等价于num。也就是给数组改个名而已,所以a[2]也等价于num[2],当然可以写成*(a+2)。

利用指针数组和数组指针实现二维数组

数组指针可以指向数组,指针数组可以指针,那么两者结合会碰撞出怎样的火花呢?

int num1[3]={1,2,3};
int num2[3]={4,5,6};
int*arr[2]={&num1,&num2};

如上述代码所示,那么arr[1][1]的值就是5咯,所以说这就是二维数组吗?很类似,但并不完全一样。
·相同点在于int arr[2][3]的话可以理解为,有两个int [3]的数组放在一起,所以arr[1][1]就是第二个数组的第二个元素咯,这和我们的代码时一样的。
·不同点:
1.二维数组里面元素的地址都是连在一起的,而我们利用指针实现的二维数组,不同数组间的地址并不会紧密相连。也就是说num2[0]-num1[2]!=1;
2.二维数组确定了每个一维数组的大小,而用指针实现的二维数组就没有这个限制:

int num1[]={0};
int num2[3]={0};
int*arr[2]={&num1,&num2};

但是这依然很接近二维数组的本质了,接下来我们来研究下二维数组究竟为何物。

二维数组

二维数组名的理解

int arr[2][3]={0};

arr代表什么?我们吧arr看成有两个一维数组int[3],按照上面用指针实现二维数组的理解,那么arr不就是第一个一维数组的地址咯,所以arr+1就是第二个一维数组的地址。这也告诉我们为什么整型二维数组要解引用两次才是整型,因为第一次解引用得到的是一维数组地址,第二次解引用得到的才是整型。
·慢着!既然arr要解引用两次才得到整型,所以arr的类型就是~~ int** !!
正确的!
那么接下来做些小问题吧。

int arr[2][3]
int a=sizeof(arr);
int b=sizeof(arr+0);
int c=sizeof(arr[0]);

那么a的值很简单,自然就是24;
b呢?前面提到arr+0相当于第一个一维数组的地址,而第一个一维数组里面有三个整型,所以b是12咯。错了!!地址啊,所以b应该是4/8.
arr[0]才是第一个数组的首元素地址,所以c才是12呀!
·假如arr的第一个数组的数组名为num,那么arr+0相当于&num,arr[0]相当于num。

二维数组传参的本质

以整型的二维数组为例:既然数组名的类型为int**,所以二维数组传参的本质就是传过去一个int**类型的指针。这也告诉我们为什么二维数组传参可以省略行但不能省略列,因为前面提到指针类型不同影响的是±运算,你只有告诉我一行数组里有几个元素,自定义函数中才能知道数组名+1后地址该如何变化。
例如:

void fu(int num[][3])
{
return;
}
int main()
{
int arr[2][3]={0};
fu(arr);
return 0;
}

这里的num是一个int**的指针,传去列为3,num+1才能像int(*)[3]+1一样跳过12个字节。

用一维数组的形式打印二维数组

二维数组和我们利用指针实现的二维数组有一个不同点在于,二维数组里的元素地址是连续排放的,利用这个性质我们可以实现用一维数组的形式打印二维数组。

#include<stdio.h>
int main()
{
	int arr[2][3] = { 1,2,3,4,5,6 };
	int sz = sizeof(arr) / sizeof(arr[0][0]);
	int i = 0;
	int* num = arr[0];
	for (i = 0; i < sz; i++)
	{
		printf("%d ", num[i]);
	}
	printf("\n");
	return 0;
}

利用这个线性存放的性质我们就得到了上述代码。

函数指针变量

int Add(int x,int y)
{
return x+y;
}
int(*fu)(int,int)=Add;//或者&Add;

我们利用()让fu先和*结合,声明这是一个指针,然后再和后面的()结合声明这是个函数指针,前面就是该函数的返回类型int,()内是该函数的形参。那么它的使用方法和数组指针类似,其实就是让Add改名为num,所以num(int,int)即可。

转移表

将指针数组和函数指针相结合就得到了转移表:

#include<stdio.h>
int Add(int x, int y) 
{
	return x + y;
}
int Minus(int x, int y)
{
	return x - y;
}
int main()
{
	int (*arr[])(int, int) = { Add,Minus };
	int input = 0, x = 0, y = 0;
	scanf("%d %d %d", &input,&x,&y);
	printf("%d\n", arr[input](x, y));
}

这便是转移表。其中Add和Minus是通过函数指针实现的,我们把这种函数称为回调函数
转移表的优缺点:
1.优点:代码简洁,逻辑性强。
2.缺点:数组要求内部元素相同,所以指针数组里的函数指针所指向的函数的返回类型和形参都要相同。这点是一个比较大的限制。

字符串指针和字符串数组的比较

char* str1 = "hello!";
char str2[] = "hello!";
char* str3 = "hello!";
char str4[] = "hello!";

·首先字符串指针和字符串数组在定义上便不一样,但是用法类似。
·其次字符串指针指向的字符串输入常量,也就是字符串指针只能打印而不能修改字符串的值,而字符串数组里的字符串是变量,既能打印也能修改数值。
·最后c语言中,不会为同一个常量开辟不同的空间,也就是说str1==str3,但是str2!=str4。因为str1和str3里的内容相同且为常量,所以他们指向的地址相同,而str2和str4虽然内容一致,但他们属于变量,而且数组本身开辟的内存空间就不一样,所以str2和str4所指向的地址不同。

void*

void*类型也是一个指针,不过不同的是它只用作存放地址名而无法进行加减乘除的运算,如果想要进行±那需要先强制类型转换。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值