C语言指针(1)

目录

 一、内存和地址

  1.内存

2.编址

二、指针变量与地址

1.&(取地址操作符)

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

三、指针类型的意义

1.指针的*

2.指针的+-

 3.void*指针

四、const指针

1.const修饰变量

2.const修饰指针变量

五、指针运算

1.指针-指针

2.指针运算

六、野指针

七、assert断言

八、传值调用和传址调用

1.鲁棒性

2.传值和传址调用


 指针相信了解过计算机的小伙伴们都知道,他十分的难,并且在学习计算机的路上我们无时无刻都需要他。所以今天小白想要好好说一说这个指针,帮助大家理解。

 一、内存和地址

  1.内存

   在这之前呢,小白现举一个生活中的例子,大家都知道我们要去看一场自己喜欢的电影都需要买票,而票上都有对应的座位号,可以防止我们走错,找错,防止别人占座。同时如果你中途进场可以准确找准自己的位置。同理,指针也是这个作用,它可以让CPU通过座位号准确找到一个内存空间,让程序员快速访问。

  而在计算机中我们把座位号叫做地址,而地址也就是指针。所以我们可以理解为:

  座位号 == 地址 == 指针

  在这里我们也说一下计算机中的单位换算:

而我们又知道内存与指针息息相关,内存划分为一个个的内存单元,每个内存单元的大小是1个字节,并且变量创建的本质其实是在内存中申请空间,而地址则是将这些单元命名,赋给他们编号,方便指针寻找。 

这里的0x00000001就是地址。 

2.编址

 CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节
很多,所以需要给内存进行编址(就如同座位很多,需要给座位号⼀样)。

 计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
 就如同乐器一样,上面并没有标明七阶音符,但是制造商会告诉你哪里发什么样的声音,使人们达成了一种共识,计算机也是一样。

 首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同⼯作的。所谓的协同,至少相互之间要能够进行数据传递。
 但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。
 而CPU和内存之间也是有⼤量的数据交互的,所以,两者必须也用线连起来。
 不过,我们今天关心一组线,叫做地址总线。
硬件编址也是如此:
 我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么
⼀根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含
义,每⼀种含义都代表⼀个地址。
 地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊
CPU内寄存器
。而地址总线:找内存单元是地址来做,地址总线传递地址信息,进行定位

二、指针变量与地址

1.&(取地址操作符)

 我们知道变量创建的本质是在内存中申请空间。而&取地址操作符可以取出地址让我们观察。

从图片我们可以直观的看到变量a的地址,而整型a占4个字节,所以向内存申请了4个字节用于存放10。

而且我们可以看到每个字节都有一个地址,并且是连续的,所以我们只需知道第一个地址就可以顺藤摸瓜知道其他。 

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

1.指针变量

 当我们取出一个变量的地址时,我们需要将他存储起来,所以这时候创建的变量存储地址的就叫做指针变量

而其形式为:

int a = 10;

int* pa = &a;//这里的pa就是指针变量

 2.理解指针变量

 

3.*(解引用操作符)

//int main()
//{
//	int a = 30;
//	int* pa = &a;
//	*pa = 200;//注意:这里不是多此一举,这里给变换a的值提供了一种路径,让代码更加灵活。
//	printf("%d\n", a);
//	return 0;
//}

* -- 解引用操作符(间接访问操作符),是用来pa所存放地址中,地址所指向的内容,通过pa找到a, *pa就是a,正如代码所说的那样,通过*pa来改a并不是多此一举,会多一中方法,使代码更加灵活。

4.指针变量大小

1. 指针变量是用来存放地址的,一个地址的存放需要多大空间,那么指针变量就是多大;

2.指针变量的大小取决于地址的大小。

32位平台下地址是32个bite位,指针变量大小是4个字节;

64位平台下地址是64个bite位,指针变量大小是8个字节

int main()
{
	printf("%zd\n", sizeof(int*));
	printf("%zd\n", sizeof(char*));
	printf("%zd\n", sizeof(float*));
	printf("%zd\n", sizeof(double*));
	printf("%zd\n", sizeof(_Bool*));
	return 0;
}

 

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

三、指针类型的意义

 通过前面我们可以了解到指针变量的大小和类型是无关的,到这里也许会有小伙伴要问了,既然大小都一样,那为什么还要分这么多类型呢?接下来就让我们探讨一下吧!

1.指针的*

 int*类型一次访问了4个字节,调试后可以看出会将a的4个字节全部改为0。

char*类型一次访问了1个字节,发现a的4个字节只有一个变为了0。

结论1:指针类型决定了我们进行解引用操作时到底访问了几个字节,char*访问1个,int*访问4个,数组解引用访问一个数组。 

2.指针的+-

//int main()
//{
//	int a = 0x22334455;
//	int* pa = &a;
//	char* pc = &a;
//	printf("%p\n", &a);
//	printf("%p\n", pa);
//	printf("%p\n", pc);
//
//
//	printf("&a-1=%p\n", &a-1);
//	printf("pa-1=%p\n", pa-1);
//	printf("pc-1=%p\n", pc-1);
//
//	return 0;
//}

这里我们演示减法,加法同理。我们可以看到char*-1后退了4个字节,int*-1后退了4个字节,所以这就是指针类型不同带来的差异。

结论2:指针类型决定了指针向前或者向后走一步有多大距离。 

我们也可得出一个计算式:

 3.void*指针

 void*指针又是无具体类型指针(泛型指针),它可以接受任意类型指针,但是无法直接进行指针运算,也就是无法进行指针+-以及解引用操作。

 而他最大的作用就是在于它可以接受任意类型指针,可以作为函数参数来接收不同类型数据的地址,因为当别人传过来一个数据时,当我们无法判断是什么类型数据时,可以用void*来接收,会更加安全方便。

四、const指针

1.const修饰变量

 const修饰变量的时候叫常变量。这个被修饰的变量本质上还是变量,只是不能被修改。

int main()
{
    const int num = 100;
    num = 200;//err
    printf("%d\n", num);
    return 0;
}

但是当我们用指针进行操作时,发现num的值可以被修改,但是我们希望不管是任何方法都不可以修改num的值,所以我们应该在指针层面上进行限制,让他无法被修改。

2.const修饰指针变量

 需要小伙伴们知道是const修饰指针变量,可以放在左边,也可放在右边,但是其意义是不一样的。

1.左边

 放在*左边,限制的是指针指向的内容,也就是不能通过指针变量来修改它所指向的内容,但是指针变量p本身可以改变,因为p是变量,是变量就有地址。

int main()
{
	int a = 20;
	int b = 30;
	int const* p = &a;//const指针放在*左边,限制的是指针变量p指向的内容a,也就是说此时不能通过p来改变a了
	                  //但是其本身的地址可以改变.//const int* p = &a;也属于左边。
	*p = 200;//err
	p = &b;//ok
	return 0;
}

2.右边

 放在*右边,限制的是指针变量本身,指针不能改变它的指向(地址),但是可以通过指针变量修改它所指向的内容

int main()
{
	int a = 20;
	int b = 30;
	int * const p = &a;//const指针放在*右边,限制的是指针变量p本身,也就是说此时能通过p来改变a了
	                  //但是其本身的地址不可以改变.
	*p = 200;//ok
	p = &b;//err
	return 0;
}

3.*两边都存在const

 如果都存在const,则会此限制全部,既不能修改指针所指向的内容,也不能修改指针变量本身。 

声明:这里对于p有三个相关的值

1.p:p里面放着一个地址(a的);

2.*p:p指向的那个对象;

3.&p:表示的是p变量的地址。

五、指针运算

(1)指针+-整数;(2)指针-指针;(3)指针的关系运算

 注意:没有指针+指针,因为没有意义,就拿日期来说,如果拿2024年6月7号加2025年6月7号,即得不出天数,也得不出日期,所以没意义。

1.指针-指针

  • 指针-指针的绝对值是指针和指针之间元素的个数
  • 指针-指针,其计算的前提条件是两个指针指向的是同一空间(路径)

题目:写一个代码,计算字符串的长度。

思路:

方案一:这里的数组名其实是数组首元素的地址,以及传参的本质,后面我会给大家详细介绍。

#include <string.h>
/*1*/
//循环
//size_t ZF_strlen(char* p)//size_t是无符号整型
//{
//	size_t count = 0;//计数器
//	for (*p; *p != '\0'; p++)
//	{
//		count++;
//	}
//	return count;
//}

int main()
{
	char ch[] = "abcdefg";
	size_t len = ZF_strlen(ch);//数组名其实是数组首元素的地址;ch == &ch[0],也就是第一个元素的地址
	printf("%zd\n", len);

	return 0;
}

方案二:

#include <string.h>


/*2*/
//指针++
//size_t ZF_strlen(char* p)//size_t是无符号整型
//{
//	size_t count = 0;//计数器
//	while (*p != '\0')
//	{
//		count++;
//		p++;//指针++
//	}
//	return count;
//}


int main()
{
	char ch[] = "abcdefg";
	size_t len = ZF_strlen(ch);//数组名其实是数组首元素的地址;ch == &ch[0],也就是第一个元素的地址
	printf("%zd\n", len);

	return 0;
}

 方案三:指针-指针

思路:

#include <string.h>

/*3*/
//思路:a,b,c,d,e,f,g,\0,将p的值给start,则代表第一个地址是start,end通过循环遍历,一步步接近\0,
//最后到末尾地址,在通过指针减法法则得结果
//size_t ZF_strlen(char* p)//size_t是无符号整型
//{
//	char* start = p;//将p的值给start
//	char* end = p;//将p的值给end
//	while (*end != '\0')//前几个可以直接写*end,因为\0的地址就是0,0为假,所以循环停止
//	{
//		end++;
//	}
//	return end - start;//指针-指针
//}

int main()
{
	char ch[] = "abcdefg";
	size_t len = ZF_strlen(ch);//数组名其实是数组首元素的地址;ch == &ch[0],也就是第一个元素的地址
	printf("%zd\n", len);

	return 0;
}

2.指针运算

用指打印1~10

方案一:

//int main()
//{
//	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//	int sz = sizeof(arr) / sizeof(arr[0]);
//	int* p = &arr[10];
//	int i = 0;
//	for (i = 0; i < sz; i++)
//	{
//		printf("%d ", arr[i]);
//		p = p + 1;//指针+整数
//	}
//	return 0;
//}

方案二: 

//倒着打印1~10
//int main()
//{
//	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//	int sz = sizeof(arr) / sizeof(arr[0]);
//	int* p = &arr[sz-1];
//	int i = 0;
//	for (i = 0; i < sz; i++)
//	{
//		printf("%d ", *p);
//		p = p - 1;//指针-整数
//	}
//	return 0;
//}

方案三(错误方案):用char*打印1~10,理由我一次跳过一个字节,那么我乘4也可以跳过4个字节,所以可行。但其时不可以,小伙伴们有兴趣可以试试调试一下。

//用char*打印1~10虽然你能够成功,但还是不对,因为这里数字太小,一旦数字大了,会发生错误。
//int main()
//{
//	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//	int sz = sizeof(arr) / sizeof(arr[0]);
//
//	char* p = &arr[10];
//	int i = 0;
//	for (i = 0; i < sz; i++)
//	{
//		printf("%d ", arr[i]);
//		p = p + 4;
//	}
//	return 0;
//}

六、野指针

1.指针未初始化

p是局变,但没有初始化,其值就是随机值,如果将p中存放的值当做地址,解引用后会形成非法访问。

//int main()
//{
//	int* p;
//	*p = 200;//err 未初始化
//	return 0;
//}

2.指针越界访问

//int main()
//{
//	int arr[10] = { 0 };
//	int* p = &arr[0];
//	int i = 0;
//	for (i = 0; i <= 10; i++)//越界访问
//	{
//		*p = i;
//		p++;
//	}
//	return 0;
//}

 3.指针指向的空间释放

n为局部变量,当代码调用test()结束时,n会被释放,而p却还在访问地址,则会成为野指针。

//int test()
//{
//	int n = 200;//n为局变,函数调用完之后就会被销毁,如果还去返回的话,p会成为野指针。
//	return &n;
//}
//
//int main()
//{
//	int* p = test();
//	printf("%p\n", p);
//	return 0;
//}

七、assert断言

assert.h头文件定义宏assert(),用于运行时确保程序是否符合指定条件,如果不符合就会报错终止运行,这个宏常被称为“断言”。

写法:assert(p != NULL);

作用:验证变量p是否为NULL,防止野指针的产生。

assert()接受一个表达式作为参数,用于判断表达式是否为真,如果为假就会报错,并且显示具体位置 ,所以对程序员非常友好。

如果不想要断言,只需要在头文件前加#define NDEBUG,

一般是在Debug版本中使用,在vs的Release中,直接就被优化了。

八、传值调用和传址调用

1.鲁棒性

e.g.求一个字符串长度,要求增加代码鲁棒性(使用const和assert可以增加代码的稳定)

//size_t ZF_strlen(const char* p)
//{
//	int count = 0;
//	assert(p != NULL);
//	while (*p)
//	{
//		p++;
//		count++;
//	}
//	return count;
//}

//int main()
//{
//	char arr[] = "abcdef";
//	size_t len = ZF_strlen(arr);//数组名表示数组首元素的地址
//	printf("%zd ", len);
//
//	return 0;
//}

2.传值和传址调用

传值调用:函数在使用时,是把变量本身传给了函数。

传址调用:函数在调用时,将变量的地址传给了函数。

写一个函数交换整型变量的值

传值调用:

 我们发现a和并没有交换,这是因为形参只是实参的一份临时拷贝,形参有自己的独立空间,有自己的地址,也就是说void swap2 (int x, int y),只是x,y进行了交换,与a,b无关,因为地址不同,对形参的修改不影响实参。

那应该如何解决呢?

很容易我们只需要将a和b的地址传给函数,我们就可以进行交换。

传址调用:


void swpl(int* px, int* py)//将a,b的地址传入当中,通过地址彻底交换a,b,使形参与实参的地址一致
{
	int z = 0;
	z = *px;
	*px = *py;
	*py = z;

}

int main()
{
	int a = 6;
	int b = 8;
	printf("交换前a=%d b=%d\n", a, b);
	swpl(&a, &b);
	printf("交换后a=%d b=%d\n", a, b);

	return 0;
}

到这里指针1的内容也就结束,文章很长,但干货很多,需要大家慢慢消化,在这里小白也祝大家早日理解指针,成为大牛!也希望大家多多支持,你们的支持就是我前进的动力。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值