【C语言】理解指针(1)

(一)内存,指针变量和地址 

(二)指针变量类型的意义及const修饰

 (三)指针运算及野指针

  (四)assert断言及指针的使⽤和传址调⽤

一.内存,指针变量和地址

1.内存和地址

在生活中,我们有门牌号就可以快速找到房间,而在计算机中,将内存分为一个个单字节大小的内存单元(一个字节占8个比特位,一个比特位可以存放一个二进制位),每个内存单元都有一个编号以便CPU快速找到一个内存空间,我们称这个编号为地址。与此同时,在C语言中我们又将地址称为指针。

常见的单位换算如下:

1Byte=8bit;

1KB=1024B;

1MB=1024KB;

1GB=1024MB;

1TB=1024GB;

1PB=1024TB;

......

我们可以补充一个地址总线,以VS2022为例,我们使用X86环境时机器是32位,32位有机器有32根地址总线,每根线“1”代表有电磁脉冲,“有”,“0”代表没有电磁脉冲,“无”,这样可以组成2^32种含义,每种含义代表一个地址。

2.取地址符号&

在C语言中,我们创建一个变量,就是向内存申请一个空间。

 无论是32位还是64位的机器,普通int类型都是4个字节,且每个字节都有对应的地址。

(上面是向内存申请4个字节的空间,来存放整型10)

我们试一下取出a的地址:

 

 实际上,取出的a的地址是a较小的地址,a的实际地址为:

008FFBA4

008FFBA5

008FFBA6

008FFBA7

虽然整型变量占4个字节,但是只要我们知道了他的第一个地址,就能顺藤摸瓜访问他的其他地址,其他的数据。

3.指针变量

为了方便使用,我们把地址存放在指针变量中,存放在指针变量中的值,都会被理解为地址。

这里我们引入一个解引用操作符“*”,它可以解开该元素对应的地址,进而找到这个元素,或者定义这个是指针变量。

 int*代表这个变量(p)是指针变量,*代表p是指针类型,p是a的地址,int代表p直接指向的a是整型类型(可以理解为给p解引用的结果类型)。

*p代表把p解引用,即顺藤摸瓜通过a的地址直接找到a(*p=a)。(以便未来要使用)

这样可以通过指针变量p来改变a的值,如:

*p=0;

printf("%d",a);

输出结果是a为0。

我们再引入一个指针变量的大小,前面我们提到32位环境下一般有32个地址总线,可以存放32个二进制位,也就是4个字节,地址在32位环境下是32个比特位,以后用这个地址可以访问它指向的元素。在64位环境下同理,指针变量大小是8个字节。

经过实验证明,在同一环境下,任何指针变量的大小是一样的。

4.指针解引用

#include"20230812定义.h"
int main()
{
    int a = 10;
    int* p = &a;
    *p = 0;
    char ch = "abcdef";
    char* pc = &ch;
    *pc = 0;
    return 0;
}

由于指针类型不同,int*类型的可以把a的地址改变4个字节,char*类型的可以把ch地址改变1个字节。

我们看,整型指针+1跳过4个字节,刚好对应一个整型所需的四个字节,而字符指针+1跳过1个字节,刚好对应一个char类型所需的一个字节,真的就像“一地一址”。 

也就是说,指针类型决定了跨越的幅度有多大。

二.指针变量类型的意义及const修饰

1.意义:

有助于我们以后对不同类型元素的访问。

2.const修饰

 const在*左边,即指针变量解引用的值不可变(即原元素的a或b或c的值不可以改变但可以后期改变它的地址),const在*的右边,即指针变量解引用的值可以变,但相应的地址不能改变。(即涉及到能不能通过指针改变这个元素的值,能不能通过指针改变这个元素的地址)

三.指针运算及野指针

1.指针运算

指针有三种基本运算,分别是:

a.指针+-元素

b.指针-指针

c.指针的关系运算

a.指针+-元素

其实我们可以这样想:我们用地址就是为了有朝一日能够取出地址对应的元素,而对于一个集合里的元素,都有唯一确定的地址,就像一个坑一个蛋。

举个例子:在32位环境下,int*,char*都要占用4个内存,但是元素对应的地址+-一个整数,到时候我们用循环打印地址的解引用,就可以有选择的去打印元素。

 这样就有点像地址间距对应元素了。(刚好一个int类型元素占4个字节,一个char类型元素占1个字节)

我们也拿一维数组看一下效果:

 效果还是很明显的。

b.指针-指针

当然指针的相减也是有一定规则的,一定要是在内存中连续存放的元素才好地址相减,比如一个字符串中“hellobit”中的8个单词就是在内存中连续存放的,通过如上的例子加一个循环就可以很方便的打印每个单词了,但如果它们不是连续存放的话就相减一下不知道是什么鬼了。

这样我们又学到了新的求字符串长度的方法:偷偷把他们的地址传参到函数中,通过函数把形参和实参产生联系,再结合连续内存的特点指针相减确定元素个数或字符个数。(如果不传指针就比较难产生联系)

c.指针关系运算

我们看一下用地址循环打印:

 又是一种新方法。

2.野指针

野指针就是没有经过初始化的指针变量,因为你永远不知道他要指向谁,指向哪个空间。

*为什么会产生野指针?

a.没有初始化

我们看一下下面这段代码:

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

系统报错了,说局部变量“p”未初始化。

系统都看不下去了。

b.指针越界访问

如:

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

 多出来两个乱的数字,很明显是指针越界访问了,这样的指针明显是野指针,很危险,容易造成数据泄露。

 c.指针指向的空间释放

如下代码:

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

形参是实参的一份临时拷贝,形参用完被销毁,可是还要返回一个地址,这就不讲武德了。

*怎么避免野指针?

a.初始化指针

int main()
{
    int a = 113;
    int* p = &a;
    int* q = NULL;

int *o;
    return 0;
}

 对比一下,显然初始化更好。

b.不允许指针越界

 这样就相对安全些。

c.指针变量不再使用时,及时安置为“空”,且在使用前检验其有效性

如下:

int main()
{
    int arr[10] = { 3,1,6,3,7,9,0,4,2,3 };
    int* p = arr;
    int i = 0;
    for (i=0;i<10;i++)
    {
        *(p++) = i;
    }
    p = NULL;//p越界,设为空
    p = arr;//重新取,再使用
    if (p != NULL)
    {
        //多一层保护
    }
    return 0;
}

d.避免返回局部变量的地址

就像前面那个在函数里还返回n的地址,就很不讲武德。

四.assert断言及指针的使用和传址调用

a.assert断言

使用asert断言时需要包含头文件assert.h。

如图:

 不满足assert括号里的条件系统会自动报错,并显示在哪个文件里哪一行。

假如我要让它失效,在它的头文件前面这么弄:

 

 这样运行系统就不会报错了。

b.传址调用

以前我们在学习函数传参的时候,用的是传值调用,而我们为什么要学习传址调用呢?

例如,我们可以写个函数来交换两个变量的值:

 这是为什么?

前面我们学习函数传参的知识:函数传参时形参是实参的一份拷贝,用完后会销毁,你说交换又有什么用呢?

如果我们偷偷地“伪造”一个地址传过去,那就可以搞定这个问题,这是指针联系了函数内和函数外,达到了“偷梁换柱”的效果。

如下图:

#include"20230812定义.h"
void Add(int* pa, int* pb)
{
	int tmp = 0;
	tmp = *pa;
	*pa =*pb ;
    *pb = tmp;
}
int main()
{
	int a = 5;
	int b = 9;
	printf("交换前:a=%d,b=%d\n", a, b);
	Add(&a, &b);
	printf("交换后:a=%d,b=%d\n", a, b);
	return 0;
}

 当形参类型和实参类型都是指针类型时,函数内的内容按照置换原则,地址就像纸盒,里头的内容就像纸巾,我们在函数里面只是把纸巾互换位置,但是纸盒变不了,这样函数执行完就发现盒子不变,纸巾换掉了。(和传值调用的思路差不多,就是传值变为传地址,传值时连纸盒也没有,换了纸有个毛线用)(给形参里的值有了容身之处)

c.strlen模拟

如下图:

#include"20230812定义.h"
int my_strlen(const char* p)//我不希望里面的元素改变
{
	char* d = p;
	assert(p);//不要让p变为空指针而使程序崩溃
	int count = 0;
	while (*p!='\0')
	{
		p++;
		count++;
	}
	return count;
}
int main()
{
	char arr[4] = "abc";
	int c = my_strlen(arr);//上下两个表达是等效的,传数组(名)相当于传地址
	int d = my_strlen("abc");
	printf("c=%d\n", c);
	printf("d=%d\n", d);
	return 0;
}

计算器原理。

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值