初识C语言·指针(1)

本文详细解析了内存地址的概念,介绍了指针变量、取地址操作符、不同类型指针的解引用,探讨了const修饰指针的规则,以及野指针的成因和规避方法。同时讲解了assert断言的作用和指针在函数调用中的传址与传值区别。
摘要由CSDN通过智能技术生成

1 内存和地址

2 指针变量和解引用操作符

3 指针变量类型的意义

4 const修饰指针

5 指针运算

6 野指针

7 assert断言

8 指针的使用和传址调用


1)

介绍内存之前,不妨看一下生活的例子,一栋小区,每个房间都有自己的编号,倘若不知道每个房间的号码,就只能挨个挨个访问,效率极其低下,但是当我们知道了每个房间的编号,就可以快速访问(当然是你需要钥匙,,)

在计算机中,计算机的cpu处理数据就是从内存里面读取的,那么相应的,生活中每个门牌号可以叫做地址,计算机中,内存单元是字节,字节在内存中的位置,即内存单元编号,就像是生活中的门牌号,我们知道了就可以进行访问。

生活中,门牌号是地址,计算机中,字节位置是地址,当然,在C语言里面,地址有一个全新的名字,叫指针。 

可以这样理解:内存单元编号 = 地址 = 指针。就像这样

2)

cpu和内存之前传输数据不是说一下就传过去了的,它们之间的联系是许多的线,比如地址总线,数据总线,控制总线。

我们今天只需要关心一种线,地址总线。简单理解就是32位机器有32根线,线的状态只有0或者1,那么总共表示的含义就是2^32种,每种对应一个地址,64位机器同理,那么问题来了,地址也是会有大小的,不同的环境下指针的大小也是不一样的。稍后,您就知道了。


2

1) 取地址操作符 和 %p

我们现在知道了内存与地址的关系,那么我们应该如何取到我们想要的地址呢?这里就需要用到取地址操作符了,&

你一看,欸这不是按位与吗?是的,它也代表取地址,不然你看,我们使用scanf的时候,为什么加这个,就是为了取地址出来,然后把“外卖”送进去咯。

现在来看看一个整型在内存中的地址吧。

在调试的内存中,我输入&a,然后回车这么一按。

你看吧,在64位机器下,这就是a的地址,因为是0a 00 00 00 就是a的4个字节,因为VS是小端存储,所以0a在前面,a是16进制的10,明了的吧?我们这下就给a的内存看的明明白白的了。

所以a的占的四个字节的地址是 ……74 ……75 ……76 …… 77,那么我们打印它的地址看看呢?

可以看到,打印地址用到的占位符是%p,但是有细心的读者会发现地址怎么和刚才的不一样,因为程序运行的时候,地址是随机分配的。

但是!不是有4个字节吗,怎么打印了一个地址,这是因为打印地址的时候只打印低地址。

不信?你试试呢~

所以当我们知道了第一个字节的地址,要访问后面的地址,那不就顺腾摸瓜吗?

2)指针变量和解引用操作符

那我们现在知道了某个元素的地址,并且取出来了这个地址,那这个地址我们应该存在哪里呢?

答案是存在指针变量里面。

int a = 10;
int* p = &a;

这里的p就是一个指针变量,因为a是整型类型的,所以p是int* ,同理,如果a是char类型的,p的类型就是char*,那按道理来说,p存了a的地址,打印出来的结果应该是一样的,来试试。

哦吼,一样的。

那现在就好玩了,我们知道了a的地址,也就是我们有了任意访问a的权限,也就是说我们可以通过地址改变a的值,来试试。

你看,*p = 20,就是修改了a的值,*是解引用操作符,这个* 和int* 里面的*可不是一样的嗷,这个*就是一个操作符,int*这是一个整体,代表一个类型。

你甚至可以这样理解,*p就是a,解引用就像是你得到了a的地址,然后你去了a的房间对他任意操作一样。

现在修改a的值又多了一种途径,写代码方式就会有更多可能性了。

我们刚才提及,p是指针变量,那么p的大小是多大呢?现在介绍的就是最开始的内容了。32位平台下地址总线有32根,一根对应一个bit,那么32个bit就是4个字节,同理,64位平台就是64根线,64个bit位,8个字节,事实真的如此吗?看看咯

你看,x64和x86的环境下,指针变量的大小是不是如刚才所说,一个是8一个是4。

注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。


3

1)不同指针的解引用

既然同一平台下,指针变量的大小是一样的,那么为什么还有不同的指针类型呢?

先看两段代码,在内存中调试看一下呢?

int main()
{
	int a = 257;
	int* pa = &a;
	*pa = 0;
	return 0;
}
int main()
{
	int a = 257;
	char* pa = &a;
	*pa = 0;
	return 0;
}

先调试第一段代码。

改动前是01 01 00 00 ,改动后是00 00 00 00 

再看看第二段代码。

欸?第二段为什么只改动了一个字节?

想必客官已经猜出来了,这与指针类型有关,int*的指针能一个改动四个字节,但是char*的指针一次就指针改动一个字节,所以不难推测short类型的指针一次可以修改2个字节,long*一次可以修改4个或8个字节。

那么就更好玩了,我不仅可以访问,我还可以有选择的访问。

#include <stdio.h>
int main()
{
 int n = 10;
 char *pc = (char*)&n;
 int *pi = &n;
 
 printf("%p\n", &n);
 printf("%p\n", pc);
 printf("%p\n", pc+1);
 printf("%p\n", pi);
 printf("%p\n", pi+1);
 return 0;
}

再来看看这段代码,同上面的是一个道理,指针+-整数,打印出来的也是地址,但是不同类型的指针变量跳过的字节数是不一样的。

2)void*指针

当我们用char*来接收int的地址的时候,是会有警告的,

但是当我们用void*来接受就不会,你可以形象的理解void*为垃圾桶,啥啥都能往里面装。

所以void*被称为泛型指针,即是无具体类型的指针。但是void*也是有代价的,它不能进行指针+-整数的运算,也不能进行解引用操作。

看吧,直接就报错了。

那void*是不是没有用呢?还是那句话,存在即合理,后面会对它进行着重介绍。


4

1)const修饰变量

const是C语言里面的关键字,介绍指针必定少不了它,它的作用可以粗略的理解为修饰谁谁就变成了常变量,本质是变量,但是不能对其进行修改,一改,就报错。

看吧,说明a已经不能被修改了。

那么,现在把它运用到指针里面,先是修饰变量。

int main()
{
	const int a = 10;
	int* pa = &a;
	*pa = 20;
	printf("%d\n", a);
	return 0;
}

语法上,const修饰的是不能被修改的,但是我们可以通过地址,间接的对它进行修改。

当然,有点打破语法规则的感觉。


2)const修饰指针变量

i) const在前面

什么?const修饰还分为前面后面的?是的,当const在最前面,像这样

int main()
{
	int a = 10;
	const int* pa = &a;
	*pa = 20;
	printf("%d\n", a);
	return 0;
}

也就是const修饰*pa,也就是const 修饰了a,这下a就哦豁了,间接修改的方法也不行了。

这样也是一样的,只要const在*的前面,a的值就不能通过地址的方式进行修改了。

所以const在前面的时候,修饰的是*pa,会导致*pa指向的元素不能被修改。

ii)const在后面

第二种情况就是const放在了*的后面,同理,在*前面就是控制了*pa,那么在后面控制的就是pa,乍一看好像没区别,那就错辣!这次控制的是pa,pa是干嘛的,pa是用来指向地址的,那么const修饰了它,就会导致pa只能指向a的地址了,不能指向其他的地址了。

看看。没有修饰*pa可以修改。

int main()
{
	int a = 10;
	int* const pa = &a;
	*pa = 20;
	printf("%d\n", a);
	return 0;
}

而当我重新创建了一个变量b,想要pa的地址变成b的地址,系统就报错了,哦豁,pa不能被修改了,这下pa就只能死心塌地的跟着a了。

int main()
{
	int a = 10;
	int* const pa = &a;
	*pa = 20;
	int b = 10;
	pa = &b;
	printf("%d\n", a);
	return 0;
}

总结一下,const在*前面,修饰的是*pa,会导致a的值无法被改变,也即是指针变量指向的元素的值为定值,const在*后面,修饰的是pa,会导致pa存的地址无法被改变,也就是指针变量无法被修改。


5

指针运算分为3种,1是指针+-整数,2是指针-指针,3是指针的关系运算,且听我一一道来。

i)指针+-整数

文章最开头已经提及到指针+-整数,我们可以通过指针+-整数访问不同的空间,那么举例数组就是最好的例子。

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* pa = arr;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(pa + i));
	}
	return 0;
}

数组名就是首元素的地址,我们用int* pa来接收这个地址,因为指针类型是整型,所以加i就会跳过

4 * i个字节,也就可以完美访问整个数组了。

当然,写成*pa + i是错误的,*的优先级比+高,系统会先对pa解引用在加一个i,所以加个()是很好的选择。


ii)指针 - 指针

有人就问了,欸为什么没有指针+指针呢?不急,看看这串代码。

#include <stdio.h>
int my_strlen(char *s)
{
 char *p = s;
 while(*p != '\0' )
 p++;
 return p-s;
}
int main()
{
 printf("%d\n", my_strlen("abc"));
 return 0;
}

我们利用指针 - 指针求一下字符串的长度,我们传过去一个常量字符串“abc”,传过去的实际上是abc的首元素地址,所以我们使用char*的指针来接收。

定义p是首元素的指针,通过遍历找\0的方式,让*p指向\0,最后指针相减,得到的恰好是字符串的长度。

看吧,感觉就跟4 - 1一样是吧?但是需要注意的是,这里的指针指向的最好是同一块空间,不然能相减但是毫无意义。回到最开始问题,为什么指针不相加呢?能啊,但是好像,毫无意义?

指针 - 指针可以理解为它们之间的元素个数,当然,指针一定是同一块空间,也要是相同类型的指针。

iii)指针的关系运算

int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];
 int sz = sizeof(arr)/sizeof(arr[0]);
 while(p<arr+sz) //指针的⼤⼩⽐较
 {
 printf("%d ", *p);
 p++;
 }
 return 0;
}

关系运算无非就是比较谁的地址大谁的地址小,数组名就是首元素地址,首元素地址加上数组元素个数,指向了\0,令p是首元素地址,然后就是比较咯,这个我相信是很容易理解的,就不多介绍了。


6

野指针这玩意儿才厉害,不注意的话给程序直接整崩。

i)野指针的成因

1 指针变量未初始化

指针变量没有初始化的话,那你说它存的是谁的地址呢?反正没有给它地址,就随便存咯,跟深海鱼一样,反正没有人看到,随便长咯。

看吧,如果不初始化,指针变量的值是个随机值,且指向的位置不确定,是比较危险的。

2 超出范围越界访问

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	for (int i = 0; i < 11; i++)
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

指针变量在原本的空间待得好好的,结果给人玩脱了,到其他空间去了,也就是越界访问了,这个时候p也就是野指针了。

3 指针指向的空间释放

int* test()
{
 int n = 100;
 return &n;
}
int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;
}

*p用来接收返回的n的地址,可是函数test一旦结束,内存为函数test创建的函数栈帧也被销毁了,n的地址指向的空间被销毁,值呢?肯定就没有咯。

肯定有人问了,为什么在打印*p的前面加一个打印666呢?因为当函数test的栈帧被释放之后,可能还没来得及利用,你马上调用,说不定是行得通的,但是不要以为写对了,是运气比较好而已。

这个时候p就是一个野指针了,指向的哪里自己都不知道了。

以上就是野指针的成因。

ii)如何有效的规避野指针

1)指针初始化

对症下药呗,第一种情况是指针没有初始化,那么我们就对它初始化,那初始化什么?随机赋一个值咯。当然要是地址。

还可以直接给NULL,就是把指针置为空指针的意思,C语言中NULL是0,无法使用的。

nt main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;
 return 0;
}

这里的p2是不能使用的,使用就报错了,但是你就说它有没有变成野指针吧。

2)小心指针越界

还是对症下药,指针越界访问数组了,那么我们就控制好不让它越界就行了。

3)指针闲置的时候置成空指针

int main()
{
	int num = 10;
	int* p = &num;
	*p = 20;
	printf("%d ", *p);
	p = NULL;
	return 0;
}

但后面需要重新用到该指针的时候,再重新赋值给它一个地址就行了。


7

assert就像是检察官一样,能判断指针是野指针还是正常的指针,但是assert使用需要引用头文件assert.h,该头文件有对assert的定义。

使用方法很简单,如下

int main()
{
	int num = 0;
	int* p = &num;
	assert(p != NULL);
	printf("%d ", *p);
	return 0;
}

这是正常的情况。

那么我们把该指针故意置成空指针呢?

int main()
{
	int num = 10;
	int* p = NULL;
	assert(p != NULL);
	printf("%d ", *p);
	return 0;
}

它就会在assert那行进行报错,告诉你是哪个指针置成了空指针。

当你进行多次测试后,发现不需要检测了,但是assert挺多,又不想挨个挨个删,这个时候只需要

使用 #define NDEBUG

#define NDENUG
int main()
{
	int num = 10;
	int* p = &num;
	assert(p != NULL);
	printf("%d ", *p);
	return 0;
}

它的作用就是让asset作用消失。

当然,assert最好是在debug版本使用,毕竟这个是用来调试的。release版本不建议使用。assert常被称为“断言”,断言嘛,断言哪个是对的咯。


指针是访问地址的,但是不仅仅可以访问地址,还可以通过地址做出你意想不到的事。

比如利用指针模拟实现strlen函数。

int my_strlen(char* p)
{
	int count = 0;
	while (*p != '\0')
	{
		p++;
		count++;
	}
	return count;
}
int main()
{
	int ret = my_strlen("abcdefg");
	printf("%d ", ret);
	return 0;
}

指针指向第一个元素,然后让它每次自增,count就自增一次,指针指向\0结束,count也不再自增,最后返回count的值。怎么样,对初学者算是意想不到的效果吧?

那么,我们知道函数传参的时候有传址或者传值。

传值

int Add(int x, int y)
{
	x = 20, y = 30;
	return x + y;
}
int main()
{
	int a = 0, b =0;
	int c = Add(a, b);
	printf("%d %d %d", a, b, c);
	return 0;
}

这里的a 和 b的值不会被修改的。

因为这里是传值调用,只是传了两个值过去而已,地址没过去,所以不会被修改。

那么,传址就会让a , b的值受到改变。

int Add(int* x, int* y)
{
	*x = 20, *y = 30;
	return *x + *y;
}
int main()
{
	int a = 0, b =0;
	int c = Add(&a, &b);
	printf("%d %d %d", a, b, c);
	return 0;
}

这里传参传的就是a,b 的地址,用int* 的指针来接收,通过解引用操作改变a,b的值,顺便进行个加法。

而数组传参因为传的是首元素地址,所以也是传址调用,读者可自行进行一下实验。


感谢阅读!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值