C语言之指针(初阶)

203 阅读15分钟

指针

  • 1.指针是什么
  • 2.指针和指针类型
  • 3.野指针
  • 4.指针运算
  • 5.指针和数组
  • 6.二级指针
  • 7.指针数组

1.指针是什么

指针是什么?

指针理解的2个要点:

  1. 指针是内存中的一个最小单元的编号,也就是地址
  2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量

总结:指针就是地址,口语中说的指针通常是指针变量,指针变量就是一个用来存放地址的变量。

type *var_name;

  • type :指针基类型,C/C++的数据类型,如:int、char、都变了、float等,但是不管数据类型是哪种,其指针值都是代表一个地址。
  • var_name:变量名称
  • *:用于声明指针

那我们就可以这样理解:

内存

1bit—————————比特位

1byte=8bit——————字节

1KB=1024byte

1MB=1024KB

1GB=1024MB

1TB=1024GB

1PB=1024TB

微信图片_20241110134653.jpg

指针变量

我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量

指针变量就是一个用来存放地址的变量

指针变量里面存放的是地址,而通过地址,就可以找到一个内存单元

#include<stdio.h>
int main()
{
	int a = 10;
	int *pa=&a;
	//pa是一个指针变量,用来存放地址的
	printf("%p\n",&a);
	return 0;
}

int a =10;向内存申请4个字节,存储10,

&a 取出a的地址

printf("%p\n",&a); 打印a的地址

int* pa=&a;

int说明指向对象为int类型,pa指向a,a为int类型 *说明p是指针变量

总结:

  • 指针变量是用来存放地址的,地址是唯一标示一块地址空间的。
  • 指针的大小在32位平台是4个字节,在64位平台是8个字节。

2.指针和指针类型

这里我们讨论指针类型,我们都知道,变量有不同的类型,整形,浮点型等。那指针没有吗类型呢?有的。

当有这样的代码:

int num = 10;
p = &num;

要将&num保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?

1.指针变量的大小

我们给指针变量相应的类型,计算指针变量的大小

#include<stdio.h>
int main()
{
	char* pa = NULL;
	short* pb = NULL;
	int* pc = NULL;
	double* pd = NULL;

	//sizeof 返回的值的类型是无符号整型 unsigned int  
	//%zu  专门用来打印sizeof
	printf("%zu\n", sizeof(pa));
	printf("%zu\n", sizeof(pb));
	printf("%zu\n", sizeof(pc));
	printf("%zu\n", sizeof(pd));

	return 0;
}

结果:

在x86 32位的机器下全为4 在x64 64位的机器下全为8

既然不论什么样类型指针变量,它的大小都一样,那我们弄一个统一指针不就好了吗?C语言为什么还要分类呢?说明我们前面写的类型,每一种指针变量类型都是有意义的,并不是多余的,那它们有什么意义呢,我们接着学习。

2.指针变量类型的意义

指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小都是一样的,为什么还要有各 种各样的指针类型呢?其实指针类型是有特殊意义的。

1.2指针的解引用

对比下面两段代码,在调试时观察内存的变化

//代码1
#include<stdio.h>
int main()
{
	int a = 0x11223344;
	//16进制的写法,1个16进制数字能翻译成4个二进制位,一个字节代表8个二进制位,2个16进制数代表一个字节
	int* pa = &a;
	*pa = 0;

	return 0;
}
//代码2
#include<stdio.h>
int main()
{
	int a = 0x11223344;
	char* pc = &a;
	*pc = 0;

	return 0;
}

看看调试结果:我们可以看到,代码1会将a的4个字节全改为0,但是代码2只是将a的第一个字节改为0.

由此我们可以得出

结论:指针的类型决定了,对指针解引用的时候有多大权限(一次操作几个字节)。

比如:char*的指针解引用只能访问一个字节,而int *的指针解引用能访问4个字节,double *指针解引用时能访问8个字节……

疑问:如果相同长度的类型混用可以吗?比如float*int*?

#include<stdio.h>
int main()
{
	int a = 0;
	
	int* pa = &a;//pa解引用访问4个字节,pa+1跳过4个字节
	float* pf = &a;//pf解引用访问4个字节,pf+1也跳过4个字节
	*pa=100;
    *pf=100.0;    
    
	return 0;
}

int*float *是不是可以通用? 不能 站在pa的角度它认为它里面放的时整形数据,站在pf的角度它认为它里面放的是浮点型的数据 浮点数和整数在内存中的存储方式是所差异的,所以它们对有内存的解读方式是有所差异的。

1.2指针 + - 整数

下面代码,调试观察地址的变化。

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

	printf("\n");

	printf("pc=%p\n", pc);
	printf("pc+1=%p\n", pc+1);


	return 0;
}

代码运行结果如下:

屏幕截图 2024-11-11 145205.png 我们可以看出,int*类型的指针变量+1跳过了4个字节,char*类型的指针变量+1跳过了1个字节。这就是指针变量的类型差异带来的变化。

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

3.野指针

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

理解野指针的时候,你可以把野指针想成“野狗”,是没有主人的,是危险的。

3.1野指针的成因

3.1.1.指针未初始化

#include<stdio.h>
int main()
{
	int* p;//p有初始化,就意味着没有明确指向
    //局部变量没有初始化,默认为随机值
	*p = 20;//非法访问内存
	return 0;
}

3.1.2.指针越界访问

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i < 11; i++)//循环11次
	{
		*p = i;
		p++;
	}
	return 0;
}

屏幕截图 2024-11-11 212148.png

3.1.3.指针指向的空间释放

#include<stdio.h>
int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	return 0;
}

微信图片_20241112183823.jpg

a空间是局部的,出了函数后就会将操作空间还给操作系统,p保存了a空间的地址,p就有能力去找到那块空间,但是不能使用这块空间,无这块空间的使用权限了。

抽象理解:p和a是好朋友,p知道a的电话,但是有一天p和a掰了,p再打这个电话时,可能对面就不是a了,这个电话被别人用了。

我们来打印*p看看:

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

结果:

屏幕截图 2024-11-12 190404.png 可以看到*p的结果的结果就不为10了;说明a的地址被占用了。

3.2如何规避野指针

3.2.1. 指针初始化

如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULLNULL 是C语言中定义的一个标识符常量,值是0,0也地址,这个地址是无法使用的,读写该地址会报错。

空指针:在C语言中,空指针是一个特殊的指针值,表示指针不指向任何有效的内存地址。NULL来定义,在标准C库中NULL实际上是一个值为0的常量。

如下代码(错误使用)

#include<stdio.h>

int main()
{
	int* p2 = NULL;
	*p2 = 100;
	return 0;
}

原因:NULL 是C语言中定义的一个标识符常量,值是0,0也地址,这个地址是无法使用的,读写该地址会报错。

当一个指针变量不知道初始化什么值时,我们初始化空指针,当使用这个指针时用法如下代码:

#include<stdio.h>

int main()
{
	int* p3 = NULL;
	if (p3 != NULL)
	{
		*p3 = 100;
	}
	return 0;
}

3.2.2.小心指针越界

一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

使用指针的时候一定要注意边界,通过指针访问的内存是不能越界的。

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

当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的一个规则就是:只要是NULL指针就不去访问, 同时使用指针之前可以判断指针是否为NULL。 我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找一棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。 不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗,有点危险;对于指针也是,在使用之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使用,如果不是我们再去使用。

#include<stdio.h>

int main()
{
	int i = 0;
	int arr[10] = { 1,2,3,4,5,6,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;
}

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

参照上方示例

3.2.5.指针使用前检查有效性

4.指针的运算

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

  • 指针 + -整数
  • 指针-指针
  • 指针的关系运算

4.1指针 +- 整数

示例1:

#include<stdio.h>
#define N_VALUES 5

int main()
{
	float values[N_VALUES];
	float* vp;
	for (vp = &values[0]; vp < &values[N_VALUES];)
	{
		*vp++=0;//也可写为*(vp++);
		//*vp=0;
		//vp++;
	}
	int sz = sizeof(values) / sizeof(values[0]);
	int i = 0;
	for (int i = 0; i < sz; i++)
	{
		printf("%d ",values[i]);

	}
	return 0;
}

图解:

微信图片_20241112194857.jpg

打印该数组的结果为:

屏幕截图 2024-11-15 191230.png

这里的数组没有越界访问,只是借用values[5]的地址并没有访问,其中vp++是将数组的地址向后移动1步。

*vp++( *vp)++的区别:

*vp++:*vp;vp++ ;分为两步,++作用于vp

(*vp)++:先对vp解引用,找到vp指向的对象,对vp指向的内容++

示例2:

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

结果:

屏幕截图 2024-11-12 195703.png 示例3:

数组下标的不同访问方式

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int* p = &arr[0];
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		*p = 1;
		p++;
	 //*(p+i)=1;
	}
	return 0;
}

4.2指针-指针

就像日期-日期得到天数一样,指针和指针可以相减,指针-指针的绝对值是指针和指针之间==元素的个数==。 指针-指针的前提是两个指针指向同一块空间(比如同一个数组)。

看如下代码:

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[9] - &arr[0]);

	printf("%d\n", &arr[0] - &arr[9]);

	return 0;
}

结果为:

屏幕截图 2024-11-12 201530.png 有结果可以看出,指针-指针的绝对值是指针和指针之间元素的个数,这样的表达更准确。

应用:写一个函数求字符串长度。(本质是模拟实现strlen函数)

方法一:直接用strlen求出长度

 #include<stdio.h>
 int main()
 {
     int len = strlen("abcdef");
     printf("%d\n", len);
     return 0;
 }

方法二:创建临时变量count


#include<stdio.h>
int my_strlen(char*str)
{
	int count=0;
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	int len = my_strlen("abcdef");
	printf("%d\n",len);
	return 0;
}

方法三:递归

#include<stdio.h>
int my_strlen(char*str)
{
	if (*str != 0)
	{
		return 1 + my_strlen(str + 1);
	}
	else
		return 0;
	
}
int main()
{
	int len = my_strlen("abcdef");
	printf("%d\n",len);
	return 0;
}

方法四:指针-指针

#include<stdio.h>
int my_strlen(char*s)//传的是首字符的地址
{
	char* p = s;
	while (*p != '\0')
		p++;//p='\0'的地址
	return p - s;


}
int main()
{
	printf("%d\n", my_strlen("abc"));
	return 0;
}

结果都是:

屏幕截图 2024-11-12 204101.png

4.3指针的关系运算

如下代码:

//指针的关系运算
#include<stdio.h>

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

结果为:1 2 3 4 5 6 7 8 9 10

> < >= <= == !=

探讨如下代码:

#include<stdio.h>
#define N_VALUES 5

int main()
{
	float values[N_VALUES];
	float* vp;
	for (vp = &values[N_VALUES]; vp > &values[0];)
	{
		*--vp = 0;
	}
	int sz = sizeof(values) / sizeof(values[0]);
	int i = 0;
	for (int i = 0; i < sz; i++)
	{
		printf("%d ",values[i]);

	}
	return 0;
}
//原代码
for (vp = &values[N_VALUES]; vp > &values[0];)
	{
		*--vp = 0;
	}
//简化后的代码
for (vp = &values[N_VALUES-1 ]; vp > &values[0];vp--)
	{
		*vp = 0;
	}

这样简化可行吗?

实际上,在绝大多数的编译器上是可以顺利完成的,然而我们还是应该避免这样写,因为标准并不保证可行。

标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置进行比较。

屏幕截图 2024-11-12 213907.png

5.指针和数组

数组:一组相同类型元素的集合 指针变量:是一个存放地址的变量

看个例题:

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("arr =      %p\n", arr);
	printf("&arr[0] =  %p\n", &arr[0]);
	return 0;
}

运行结果:

屏幕截图 2024-11-12 234907.png 可见数组名和数组首元素的地址是一样的。

结论:数组名表示的是数组首元素的地址。(两种情况除外)

  • sizeof(数组名),算的是整个数组的大小,单位是字节。
  • &数组名,取的是整个数组的地址。

那么这么写代码是可行的:

int arr[10]={1,2,3,4,5,6,7,8,9,10};
int*p=arr;//p存放的是数组首元素的地址

既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个数组就成为了可能。

例如:

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

运行结果:

屏幕截图 2024-11-13 000155.png 说明&arr[i]p+i的地址是一样的,数组的形式虽然是``arr[i] ,但计算时还是通过*(arr+1)`来计算的。

6.二级指针

指针变量也是变量,是变量就有地址,指针变量的地址存放在哪里呢?

这就是二级指针:二级指针是用来存放一级指针变量的地址

#include<stdio.h>
int main()
{
	int a = 10;
	int*pa = &a;//pa是一个指针变量,一级指针变量
	int** ppa = &pa;//ppa是用来存放pa的地址的,ppa是一个二级指针变量
    //将a里面的值改为20
	//ppa要找到a需要进行两次解引用
    //*ppa是解引用一次,找到pa
    **ppa = 20;//解引用2次

	printf("%d\n", a);


	return 0;
}

图解:

屏幕截图 2024-11-13 003234.png int* *int*说明ppa指向的对象是int*类型*说明ppa是指针

对于二级指针的运算有:

  • *ppa 通过对ppa中的地址进行解引用,这样 找到的是 pa*ppa 其实访问的就是 pa
int *pa = 20;
*ppa = &pa;
//等价于 pa = &a;
  • ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

7.指针数组

指针数组是指针还是数组? 我们类比一下,

整型数组:是存放整型的数组

字符数组:是存放字符的数组。

那指针数组呢?

指针数组:是存放指针的数组。

指针数组的每个元素都是用来存放地址(指针)的。

若a,b,c一个一个取地址太麻烦了,我们可以创建一个整型数组来放10,20,30等等的元素,那是否存在一个指针数组用来存放指针(地址)的呢?

存在!

include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int arr[10];

	int* pa = &a;
	int* pb = &b;
	int* pc = &c;
	int* parr[10];
	//parr就是存放指针的数组
	//指针数组
	int* parr[10] = { &a,&b,&c };
    int i = 0;
	return 0;
}

如下图:

屏幕截图 2024-11-13 005607.png

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	//int*pa=&a;
	//int*pb=&b;
	//int*pc=&c;
	int* parr[10] = { &a,&b,&c };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", *(parr[i]));
	}
	return 0;
}

模拟二级数组:

#include<stdio.h>

int main()
{
	int arr1[4] = { 1,2,3,4 };
	int arr2[4] = { 2,3,4,5 };
	int arr3[4] = { 3,4,5,6 };
	int* parr[3] = { arr1,arr2,arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 4; j++)
		{
			printf("%d ", parr[i][j]);//*(parr[i]+j)
		}
		printf("\n");
	}
	return 0;
}

结果及解析:

微信图片_20241114181623.jpg 为什么不用解引用操作符?因为arr[i] <===>*(arr+i)等价