c语言指针的基础知识(上)

本文详细介绍了计算机内存中的地址、指针概念,包括内存空间的编址方式、指针变量的取地址和解引用操作,以及不同类型的指针(如int*、char*和void*)的特点。此外,文章还探讨了野指针的形成原因、避免方法以及如何正确处理局部变量地址的返回,以确保代码的健壮性。
摘要由CSDN通过智能技术生成

指针

内存 指针 地址的关系

内存

假如有一栋宿舍楼,这栋宿舍楼一共有100个房间,你现在在一个宿舍里面,现在朋友要找你玩他不可能一个一个房间的找你,你会告诉他你在哪个楼的哪个宿舍。
比如:你在203宿舍你会告诉你的朋友你在2楼的203宿舍,他就可以根据你给的地址找到你。

将上面的案例对应到计算机中
计算机上的cpu在处理数据时,是先在内存中读取数据然后,将数据传到cpu中,数据经过处理后会放回内存中。
在这里插入图片描述

内存中每一个字节就是一个内存空间,每个内存空间都有一个与之对应的地址。有了这个对应的地址就可以通过cpu快速找到这个内存空间。
上图内存空间前面的一堆数字就是这个内存空间的地址,在c语言中也称为指针。
所以地址==指针
计算机中能够识别,储存运算的都是二进制。
计算机的单位:
bit ;(比特位),里面存放的是以0,1组成的二进制序列。
byte
kb
mb
gb
tb

对于单位之间的转换:
1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
就相当于一个内存空间中有一个字节也就是8bit

编址

在这里插入图片描述
CPU访问内存中的某个字节空间,必须知道这个 字节空间在内存的什么位置,⽽因为内存中字节 很多,所以需要给内存进⾏编址(就如同宿舍很 多,需要给宿舍编号⼀样,然后别人就可以根据这个编号找到你)。

我们先理解地址总线

我们可以简单理解,地址总线就是将硬件和硬件通过一根线链接起来,32位机器有32根地址总线, 每根线只有两态,表⽰0,1【电脉冲有⽆】,那么 ⼀根线,就能表⽰2种含义,2根线就能表⽰4种含 义,依次类推。32根地址线,就能表⽰2^32种含 义,每⼀种含义都代表⼀个地址。
64位机器有64根地址总线, 每根线只有两态,表⽰0,1【电脉冲有⽆】,那么 ⼀根线,就能表⽰2种含义,2根线就能表⽰4种含 义,依次类推。64根地址线,就能表⽰2^64种含 义,每⼀种含义都代表⼀个地址。
地址信息被下达给内存,在内存上,就可以找到 该地址对应的数据,将数据在通过数据总线传⼊
CPU内寄存器。

总结

1.内存会被划分为一个个内存空间,每个内存空间大小是1个字节。
2.每个内存空间都有一个编号。编号=地址=指针。

指针变量和地址

取地址操作符(&)

当创建一个变量的时候,本质就是向内存开辟一块空间,

int a = 10;//想内存申请一块内存空间,一共需要4个内存空间

在这里插入图片描述
下面就显示了申请的空间所对应的地址,而a=10,所对应的地址就是0x000000F4F2CFFAD4 ,也就是4个内存空间中最小的那个地址。
在代码中要怎样取到a这个地址呢?
就直接&(变量名)就可以了

int a = 10;
printf("%p", &a);

运行出来的数字就是a的地址。
上面代码是用%p进行对地址的打印,用%x也可以实现

	int a = 10;
	printf("%p\n", &a);
	printf("%x", &a);

在这里插入图片描述
区别就是%p不会省略前面的0,%x会省略前面的0。

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

指针变量

假如我们创建了一个变量a,我们要将这个变量的地址储存起来方便以后使用,这时我们就要将地址存放到指针变量中。


	int a = 10;//将10存放到a中,a是变量名,a的类型是int
	//那我们要存放地址的话也应该是将地址存放到某个变量名中,然后变量名要有一个类型,这时我们可以这样写
	int* p=&a;//将a的地址取出来放到p中,p是用int*修饰的,也就是p的类型是int*
	          //所以就可以看出*是用来创建指针变量的

p是一个指针变量,里面存放着a的地址,星号(*)是说明这个变量类型是指针。
p虽然是指针变量但是也是一个变量,创建的时候也需要开辟空间,所以p里面不仅存放着a的地址他也有自己的地址。
也就是指针变量里面存放的都是地址。

解引用操作

我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针) 指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符(*),也叫间接访问操作符。

int a = 10;
int* p = &a;
*p;//解引用操作(间接访问操作)通过*p可以直接指向a,也就是*等价a
   //那我们就可以通过*p改变a的值
*p = 100;
printf("%d", a);
return 0;

在这里插入图片描述

指针变量的大小

int a = 10;
int* p = &a;
*p;//解引用操作(间接访问操作)通过*p可以直接指向a,也就是*等价a
   //那我们就可以通过*p改变a的值
*p = 100;
printf("%d", a);
return 0;

在上面的代码中指针变量p也是一个变量,所以p在创建时,也会向内存申请空间,而只要是变量就会有大小,那么指针变量的大小是?
指针变量中存放的是地址,那么地址的存放需要多大空间,指针变量的大小就是多大。

  • 32位机器上,地址就是32个0/1组成的二进制序列,要存储起来就要有32个bit,32个bit就是4byte。那么指针变量的大小也就是4字节
  • 64位机器上,地址就是64个0/1组成的二进制序列,要存储起来就要有64个bit,64个bit就是8byte。那么指针变量的大小也就是8字节

先在32位环境中进行测试

int main()
{
	int a = 0;
	int* p = &a;

	printf("%d \n", sizeof(int*));//大小为4
	printf("%d \n", sizeof(p));//大小为4
	printf("%p \n", p);
	return 0;
}

在这里插入图片描述
然后在64位环境中进行测试

int main()
{
	int a = 0;
	int* p = &a;

	printf("%d \n", sizeof(int*));//大小为8
	printf("%d \n", sizeof(p));//大小为8
	printf("%p \n", p);
	return 0;
}

在这里插入图片描述

注意:指针变量的大小和类型无关,只要是指针变量大小都是相同的(4/8)。

int main()
{
	char a = 0;
	char* p = &a;

	printf("%zd \n", sizeof(char*));//大小为8
	printf("%zd \n", sizeof(p));//大小为8
	printf("%p \n", p);
	return 0;
}

在这里插入图片描述
可以看出在64位环境下,即使指针变量的类型是char,它的大小也是8。

指针变量类型的意义

既然不管什么指针变量类型他的大小都是相同的,那为什么要存在不同的指针类型呢?

指针的解引用

接下来对比两段代码在内存中的变化。

int main()
{
	int n = 0x11223344;
	int* pi = &n;
	*pi = 0;
	return 0;
}
int main()
{
	int n = 0x11223344;
	char* pi = &n;
	*pi = 0;
	return 0;
}

在这里插入图片描述
可以看出当指针类型为int时,将pi解引用后修改的是4个字节的数值;
当指针类型为char时,将pi解引用后修改的是1个字节的数值。
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。

指针±整数

观察下面这段代码地址的变化。

int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;

	printf("&n=     %p\n", &n);
	printf("pc=     %p\n", pc);
	printf("pc+1=   %p\n", pc + 1);
	printf("pi=     %p\n", pi);
	printf("pi+1    %p\n", pi + 1);
	return 0;
}

在这里插入图片描述
发现,int类型的指针变量+1后地址跳过了4个字节,char类型的指针变量+1后跳过了1个字节.
假如有一个 int * p
p+2 相当于跳过了2个int类型的数据。
相当于跳过了2sizeof(int)也就是24=8个字节。

void*指针

当我们写代码的时候发现int * p 这个p指的就是int类型的指针 float * pi 这个pi指的就是float 类型的指针,那么 void * pa 这个是接收的什么类型的指针呢?
在指针类型中有一种指针叫void指针,void是一种无具体类型的指针,也叫泛型指针,这种类型的指针可以接收任何类型的指针。但是它有限制void*的指针不能进行±整数和解引用操作。

int main()
{
	int n = 10;
	char* p = &n;//编译器会报错
	void * pi =&n;//程序会正常运行
	return 0;
}

在这里插入图片描述
当使用void8接收数据时,因为void他的类型是不确定的,那么对它进行±整数的操作它应该跳过几个字节呢?当它进行解引用时它应该访问几个字节?这些都是不确定的所以void*指针不能进行+ -解引用操作。
在这里插入图片描述

const修饰指针

const修饰变量

int main()
{
	int m = 0;
	m = 20;//变量可以随意修改
	const i = 0;
	i = 20;//当变量被const修饰后,i的值就不能随意改变了,会报错
	return 0;
}

在这里插入图片描述
可以发现,const的作用就是对变量进行限制,使变量不可以随意发生改变。const是一种常属性。因为即使i被const修饰后,值不能随意发生改变了但他的本质还是变量。i也叫常变量。
这时如果还是想改变i的值,我们可以用i的地址,去修改i。

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


输出结果:在这里插入图片描述
但是,用const修饰i的原因是不让i的值发生改变,那么如果可以通过i的地址修改i的话,那么const修饰i就没有意义了,所以应该让即使有了i的地址但也不能对i进行改变,那么我们就可以让const修饰指针变量。

const修饰指针变量

当使用const修饰指针变量时,const放在*左边或者右边都可以,但表达的是两种意义。

const在*左边

当const在*左边时可以写成 const int * p也可以写成int const * p,两者表达的意义是相同的。

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

在这里插入图片描述
在这里插入图片描述

如果const放在*号左边,这时想对指针内的内容进行修改(解引用),系统不会正常执行,会报错,但指针指向的对象可以改变。

const在*右边

当const在*号右边时,可以写成int * const p = &i。

	int main()
{
	const int i = 0;
	const int j = 0;
	int* const p = &i;
	*p = 20;
	printf("%d \n", i);
	return 0;
}

在这里插入图片描述

int main()
{
	const int i = 0;
	const int j = 0;
	int* const p = &i;
	int* const p = &j;
	return 0;
}

当const在*右边时,可以对指针内容的本身进行修改(解引用),但不可以改变指向的对象。

*左右边都有const
int main()
{
	const int i = 0;
	const int* const p = &i;
	*p = 20;
	int j = 0;
	p = &j;
	return 0;
}

在这里插入图片描述
当const在*左右边时,不可以对指针内容的本身进行修改(解引用),也不可以改变指向的对象。

指针运算

指针的基本运算有三种

  1. 指针± 整数
  2. 指针-指针
  3. 指针的关系运算

指针± 整数

因为数组在内存中是连续存放的所以知道了第一个元素的地址就可以知道剩下元素的地址。

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];//将数组首元素地址存放在指针变量p中
	int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素的个数
	for (int i = 0;i < sz;i++)
	{
		printf("%d ", *(p + i));//p是数组首元素的地址,那么p+i就是数组对应下标的地址,对p+i进行解引用,就可以得到对应的值
	}

	return 0;
}

在这里插入图片描述
上面的代码,当i是0时p指向数组第一个元素,当i是1时,p指向数组第二个元素,以此类推,(p+i)就可以指向arr的所有元素。

指针-指针

指针 - 指针得到的是指针和指针之间元素的个数。
计算下面代码。

int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[9] - &arr[0]);
	return 0;
}

因为指针-指针计算的时指针和指针之间的元素个数,arr[9]和arr[0]之间有9个元素,所以结果就是9.
在这里插入图片描述
上面是大地址-小地址的情况,如果是小地址-大地址呢?

int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[0] - &arr[9]);
	return 0;
}

在这里插入图片描述
发现会得到一个负数,所以指针-指针的绝对值是指针和指针之间的元素个数。

实现strlen函数
#include<string.h>
int str_len(char* p)//获得数组首元素地址
{
	char* a = p;//将p的地址存放到指针变量中。
	while (*p != '\0')//因为字符串的长度是遇到\0停止运算的,也就是当p指向的地址的值是\0时跳出循环。
	{
		p++;
	}
	return p - a;//用大地址-小地址得到的就是元素之间的个数
}
int main()
{
	char arr[] = "abcdefg";
	//数组名是首元素地址,所以arr==&arr[0]
	int len = str_len(arr);
	printf("%d ", len);
	return 0;
}

指针的关系运算

指针的关系运算就是两个指针比较大小。
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;//将数组arr的首元素地址存放到p中
int sz = sizeof(arr) / sizeof(arr[0]);
while (p <= arr + sz-1)//刚刚将数组首元素地址存放到了p中,那么数组最后一个地址就是arr+sz-1
{
printf("%d ", *(p++));//依次打印数组内容
}
return 0;
}

野指针

野指针概念

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

形成野指针的原因

指针没有初始化
int main()
{
	int* p;
	*p = 20;
	printf("%d ", *p);
	return 0;
}

在这里插入图片描述
上面代码的p虽然是指针变量,但它也是一个变量,变量如果不初始化里面存放的值是随机的0xcccccccc,没有明确指向了哪个空间,所以程序会报错,造成了野指针的出现。

指针越界访问
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
		for (i = 0; i <= 11; i++)
		{
			//当指针指向的范围超出数组arr的范围时,p就是野指针
			//当p为9的时候,它还继续自增所以超出了数组10个元素这个范围,就造成了越界访问
			*(p++) = i;
		}
		return 0;
}
指针指向的空间释放了
int* test()
{
	int n = 100;
	return &n;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

在text函数中,创建变量n时,系统会为n开辟一块空间,存放n‘的地址,这时将n的值返回到p中,然后出了这个函数n的地址就被系统回收了,这时在主函数解引用p时系统找不到p所指向的空间,所以就造成了野指针的出现。

避免野指针出现的方法

指针一定要初始化
如果明确知道指针指向哪⾥就直接赋值地址
int main()
{
	int a = 20;
	int* p = &a;
	return 0;
}

这时知道p指向的就是a,所以直接将a的地址存放到p。

NULL常量

如果不知道指针应该指向哪⾥,可以给指针赋值NULL。
c语言中,NULL是一个常量,他的值是0,0也有地址,这个地址无法使用,所以不能读写。用于当创建了一个指针,这个指针不知道要指向哪里的情况。
用NULL初始化指针通常写成

int * p =NULL;

当指针正常初始化,可以正常通过解引用指针来改变它指向的变量的值。

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

在这里插入图片描述
当指针指向的是NULL时,屏幕不能打印任何操作,所以当指针指向NULL时,不能进行解引用操作。

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

在这里插入图片描述
因为,NULL的这个特性所以我们平时使用指针可以增加一些前提条件,时=使整个代码更加安全。

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

以上代码的意思是,只要p不是空指针,就对p进行解引用操作,否则程序会报错
在这里插入图片描述

避免数组的越界

注意你向内存申请了哪些空间,就使用那些空间,不要跨界使用。

指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。

int main()
{
 int arr[10] = {1,2,3,4,5,67,7,8,9,10};
 int *p = &arr[0];
 for(i=0; i<10; i++)
 {
 *(p++) = i;
 }
 //此时p已经越界了,可以把p置为NULL
 p = NULL;
 //下次使⽤的时候,判断p不为NULL的时候再使⽤
 //...
 p = &arr[0];//重新让p获得地址
 if(p != NULL) //判断
 {
 //...
 }
 return 0; }
避免返回局部变量的地址
int* test()
{
	int n = 100;
	return &n;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

n是text函数的一个局部变量,出了函数n的地址就被回收,主函数调用时就不知道指向了哪个地址,就会造成野指针的出现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值