C语言初阶秘籍6

Hello,大家好!!!这里是小周为您带来的呕心沥血之作------C语言秘籍!!

C语言秘籍分为初阶和高阶两部!!跟着小周学定会让你C语言功力大成,称霸武林,话不多说,我们接着上回继续开讲!!!!!

六、指针

1、指针是什么

指针理解的2个要点:

  1. 指针是内存中一个最小单元的编号,也就是地址

  2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量

总结:指针就是地址,口语中说的指针通常指的是指针变量

指针变量

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

int main()
{
	int a = 100;
	int * pa = &a;//pa是专门用来存放地址(指针)的,这里的pa就被称为指针变量
	//对变量a,取出它的地址,可以使用&操作符
	//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个指针变量
	//*说明pa是指针变量,int说明pa指向的那个对象是int类型
	return 0;
}

2、指针和指针类型

那么每个类型的指针变量的大小又是多少呢?

int main()
{
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(short*));
	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(long*));
	printf("%d\n", sizeof(float *));
	printf("%d\n", sizeof(double*));

	return 0;
}

指针变量在32为平台下是4个字节 在64为平台下是8个字节

不懂的同学可以翻看之前的初识C语言的指针部分

总结:

指针变量是用来存放地址的,地址是唯一标示一个内存单元的。

指针的大小在32位平台是4个字节,在64位平台是8个字节

那指针类型又有什么意义呢?我们来写一段代码告诉大家究竟有什么意义

int main()
{
	int a = 0x11223344;//0x开头的是16进制数字
	int* pa = &a;
	*pa = 0;
	return 0;
}

我们发现a中的内容就是11223344,虽然内存种是反过来的,这和我在后面要为大家讲的大端存储和小端存储有关,这里先不用考虑。

对*pa修改,将其赋值为0

成功通过修改指针变量,将a的值进行修改,我们再来看下面的代码

int main()
{
	int a = 0x11223344;//0x开头的是16进制数字
	char* pa = &a;
	*pa = 0;
	return 0;
}

我们发现char*的指针只修改了a的一个字节

int*指针解引用访问4个字节

char*指针解引用访问4个字节

结论:指针类型可以决定指针解引用的时候访问多少个字节(指针的权限)

指针的定义方式如下:

type  *  p;
*说明p是指针变量
type:1、p指向的对象的类型
     2、p解引用的时候访问的对象的大小是sizeof(type)
2.1 指针+-整数
int main()
{
	int a = 0x11223344;//0x开头的是16进制数字

	int* pa = &a;
	char* pc = &a;
	printf("%p\n", pa);
	printf("%p\n", pc);

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

	return 0;
}

这段代码运行的结果是什么呢?大家可以自己动手算一算

因为a的地址是固定的,所以前两个打印的结果是一样的,后两个不一样是因为不同的指针类型跳过的字节数并不一样,字符指针跳过一个字节,整型指针跳过四个字节

所以通过加减整数,又可以发现指针类型的另外一个意义:

指针类型决定了指针+/-n操作时的步长,跳过n*sizeof(type)这么多个字节

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

2.2 指针的解引用
int main()
{
	int a = 10;
	int* pa = &a;
	*pa = 20;
	printf("%d\n", a);
	return 0;
}

解引用使用*操作符,这个在操作符的章节讲过,这里就不再介绍

3、野指针

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

3.1 野指针成因
3.1.1 指针未初始化
int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
}

运行发现

如果指针一开始初始化没有确定的值,可以把指针赋值为NULL

3.1.2 指针越界访问
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i <= 11; i++)
	{
		//当指针指向的范围超出数组arr的范围时,p就是野指针
		*(p++) = i;
	}
	return 0;
}

运行后发现

出问题了,这就是野指针导致的

3.1.3 指针指向的空间释放
int* test()
{
	int a = 110;
	return &a;
}

int main()
{
	int* p = test();
	printf("%d\n", *p);

	return 0;
}

运行后可以知道

虽然运行成功得到了结果,但是返回的确实临时变量的地址,这也是造成野指针的一种情况,我们要注意

3.2 如何规避野指针
  1. 指针初始化

  2. 小心指针越界

  3. 指针指向空间释放,及时置NULL

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

  5. 指针使用之前检查有效性

需要注意的是:

1、明确知道指针应该初始化为谁的地址,就直接初始化

2、不知道指针初始化为什么值就暂时初始化为NULL

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

	int* ptr = NULL;//ptr是一个空指针,没有指向任何有效的空间。这个指针不能直接使用
	//int* ptr2;//野指针

	if (ptr != NULL)
	{
		//使用
	}

	return 0;
}

4、指针运算

  • 指针+- 整数

  • 指针-指针

  • 指针的关系运算

4.1 指针+- 整数

在指针和指针类型中做了介绍,在这里再举个例子

int main()
{
	int arr[10] = { 0 };
	//不使用下标访问数组,用指针实现
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		*p = i;
		p++;//p = p+1
	}
	p = arr;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));//p+i
	}

	/*for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}*/

	return 0;
}

通过指针+1的方式,来实现访问整个数组,如图所示

扩展一点知识点

int arr[10];
int * p = arr;
*(p+i)	p是数组首地址,+i跳过相应的整型,然后解引用取出对应元素
所以*(p+i) 	等价于	 	arr[i]
    因为p和arr是一样的,存的都是数组的首元素地址,所以arr也是指针
所以*(p+i) 	等价于  	*(arr+i),*(arr+i) 	又等价于 	arr[i]
    虽然写代码的时候写的是数组,但是本质上计算机计算的时候也会按照指针的方式来计算

现在我们分析知道,arr[i] 等价于 *(arr+i)

再根据加法的交换律知道*(arr+i) 等价于 *(i+arr),

又因为arr[i]能写成*(arr+i),

那么*(i+arr)是不是就可以写成i[arr]呢?我们来验证一下

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int i = 0;
	int *p = arr;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i[arr]);//[]仅仅是一个操作符,但不建议这种写法
		
	}
	printf("\n");
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);//[] 操作符

	}
	printf("\n");
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));//[] 操作符

	}
	printf("\n");
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(i + p));//[] 操作符

	}
	printf("\n");
	//2+3 --> 3+2
	//arr[i] --> i[arr]
	return 0;
}

运行发现这四种打印的效果是一样的,所以这4种方式是互相等价的

4.2 指针-指针
//地址-地址
//指针-指针

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

	return 0;
}

指针-指针得到的数值的绝对值:是指针和指针之间的元素个数

int main()
{
	int arr[10] = { 0 };
	char ch[5] = { 0 };
	printf("%d\n", &ch[4] - &arr[0]);//err
	return 0;
}

这个结果运行出来是负数

指针-指针运算的前提条件是:指针和指针指向了同一块空间

还记得如何求字符串长度吗?之前我们用创建计数器和递归来实现,现在我们再换一种方式

int my_strlen(char* s)//方法一
{
	char* start = s;
	while (*s != '\0')
	{
		s++;
	}
	return s - start;
}

int my_strlen(char* s)//方法一修改循环判断条件
{
	char* start = s;
	while (*s)//a b c d e f \0->0
	{
		s++;
	}
	return s - start;
}

int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr);
	printf("%d\n", len);

	return 0;
}

有同学对上述方法进行优化

int my_strlen(char* s)
{
	char* start = s;
	while (*s++);
	return s - start;
}

int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr);
	printf("%d\n", len);

	return 0;
}

这回能准确的求出字符串长度吗?大家觉得结果是多少呢?

运行后发现这结果不对呀,明明就6个字符,结果为什么是7呢?那是因为*s++在最后一次循环遇到\0时先使用再+1,导致跳出循环时s又向后移动了一个字节,所以结果是7

int my_strlen(char* s)
{
	char* start = s;
	while (*s++);
	return s - start - 1;//只要将最后的\0减去即可
}
4.3 指针的关系运算

地址是有大小的,指针的关系运算就是比较指针的大小

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values[N_VALUES]; vp > &values[0];)
{
    *--vp = 0;
}

上述代码实现的效果如图:

代码简化, 这将代码修改如下:

for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
    *vp = 0;
}

上述代码实现的效果如图:

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

标准规定:

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

5、指针和数组

指针和数组分别是什么?

指针变量就是指针变量,不是数组,指针变量的大小是4/8个字节,是专门用来存放地址的

数组就是数组,不是指针,数组是·1一块连续的空间,可以存放一个或者多个类型相同的数据

二者关系:

数组中,数组名其实是数组首元素的地址,数组名 == 地址 == 指针

当我们知道数组首元素的地址时,又因为数组是连续存放的,所以通过指针就可以遍历访问数组,数组是可以通过指针来访问的

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

数组名是数组首元素的地址,但是有两个例外,分别是sizeof(数组名)和&数组名

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int i = 0;

	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	for (i = 0; i < sz; i++)
	{
		printf("%p == %p\n", p+i, &arr[i]);
	}

	return 0;
}

这段代码的运行结果就证明了数组是可以通过指针来访问的,p+i 其实计算的是数组 arr 下标为i的地址

6、二级指针

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

这就是二级指针

int main()
{
	int a = 10;
	int * p = &a;//p是一级指针变量,指针变量也是变量,变量是在内存中开辟空间的,是变量就有地址
	int* * pp = &p;//pp就是二级指针变量,二级指针变量就是用来存放一级指针变量的地址
    //*表示pp是指针变量,int*表面pp指向的那个对象是int*类型
	return 0;
}

我们调试一下看看

通过图解大家一下子就能明白上述的关系

对于二级指针的运算有:

  • 通过对p中的地址进行解引用,找到a,*p其实访问的就是a

  • 通过对pp中的地址进行解引用找到p ,然后对p进行解引用操作,找到的是a,*pp其实访问的就是p

7、指针数组

指针数组是指针还是数组?答案:是数组。是存放指针的数组。

数组我们已经知道整形数组,字符数组等

整型数组 - 存放整型的数组 字符数组 - 存放字符的数组

指针数组 - 存放指针(地址)的数组

int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "hello zhou";
	char arr3[] = "trust yourself";

	char* parr[] = { arr1, arr2, arr3 };

	char** p = parr;

	return 0;
}

看完这个图是不是就清楚多啦,i是为了后续代码中打印数组

int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "hello zhou";
	char arr3[] = "trust yourself";

	//指针数组
	char* parr[] = { arr1, arr2, arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s\n", parr[i]);
	}
	//%s 打印字符串,只要给字符数组的首地址,就会把整个字符串打印出来
	return 0;
}

运行结果如下

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };

	//指针数组
	int* parr[] = { arr1, arr2, arr3 };

	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			//printf("%d ", *(parr[i] + j));
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}

	return 0;
}

这段代码是整型数组的例子,特别注意和二维数组可不一样!

指针的讲解就到这里啦!!!如果这篇文章对你有收获的话,就给小周一键三连吧!!!!!

敬请期待接下来的结构体重点篇章哦!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值