C语言 - 初阶指针

指针就是地址,口语中的指针一般指的是指针变量


前言

本篇笔记重点描述C语言指针的具体使用。


一、指针是什么

地址也可以形象的称为指针。

代码示例:

int main(){
	int a = 10;//a占4个字节
	int * pa = &a;//拿到的是a的四个字节中第一个字节的地址
	*pa = 20;
//指针pa所指向的a是整型类型的,所以这里需要用int指针类型
	return 0;
}

总结:
指针是用来存放地址的,地址是唯一标示一块地址空间的。
指针在32位平台是4个字节,在64位平台是8个字节。


二、指针和指针类型

2.1 指针类型的意义

代码示例:

int main(){
	int * pa;
	char * pc;
	float * pf;
	
	printf("%d\n", sizeof(pa));
	printf("%d\n", sizeof(pc));
	printf("%d\n", sizeof(pf));
//打印结果为4 4 4
	return 0;
}

既然打印结果都是4,那指针类型的意义又是什么?

接下来我们就探讨一下指针类型的意义到底是什么。

int main(){
	int arr[10] = { 0 };
	int *p = arr;
	char *pc = arr;
	printf("%p\n", p);
	printf("%p\n", p+1);
//p+1打印出来的结果间隔为4个字节
	printf("%p\n", pc);
	printf("%p\n", pc+1);
//pc打印出来的结果间隔为1个字节
	return 0;
}

由此我们可知,指针类型的意义:

1、指针类型决定了:指针解引用的权限由多大
2、指针类型决定了,指针走一步,能走多远(步长)
字符指针+1相当于跳过一个字符,所以这里只加了1,而int指针+1是跳过四个,所以+4。

总结:指针类型决定了,解引用权限和步长。

2.2 指针的解引用

代码示例:

int main(){
	int arr[10] = { 0 };
	int *p = arr;
	int i = 0;
	for(i = 0; i < 10; i++){
		*(p + i) = 1//p+i其实是下标为i的地址
}
}

这里的代码充分运用了指针类型来解引用p,再把1赋值给数组arr。


三、野指针

野指针的概念:

野指针是指指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)

3.1 野指针成因

1.指针未初始化
#include <stdio.h>
int main(){
//这里的p就是一个野指针
	int * p;
//p是一个局部的指针变量,局部变量不初始化的话,默认是随机值
	*p = 20;//非法访问内存了
	return 0;
}

野指针成因:

指针未初始化
如果不初始化变量的话,这里的p就会变成非法访问的野指针。

简单理解,你想在外面酒店住房没掏钱怎么能住呢

2.越界访问
#include <stdio.h>
int main(){
	int arr[10] = { 0 };
	int * p = arr;
	int i = 0;
	for(i = 0; i <= 10; i++){
	//这里i=10的时候越界了
		*p = i;
		p++;
}
	return 0;
}

代码分析:

这里的p解应用的时候,指针会变成越界访问,越界就会变成野指针。

3.指针指向的空间释放

当p指向的空间被释放后,再去指向这个空间就会变成野指针。

举个例子:你找了个女朋友,加了她微信电话号码,后来有一次你们吵架分手了,你再给她狂打电话,发微信,这就造成了非法骚扰了。

4.因生命周期变成的野指针
#include <stdio.h>
int* test(){
	int a = 10;
	return &a;
}
int main(){
	int * p = test();
	*p = 20;
	
	return 0;
}

代码解析:

1、代码先进来main函数,进去main函数后立刻调用test函数。
2、进入test函数后创建了局部变量a,把a的地址返回去了。
3、地址返回去以后放到p变量里面。
4、但是一旦返回去以后局部变量a的生命周期就已经结束了。
5、此时空间已经被销毁,空间不属于你了,属于非法访问内存。

3.2 如何规避野指针

如何避免野指针:

1、指针初始化
2、小心指针越界
3、指针指向空间释放即使置NULL
4、指针使用之前检查有效性

所以当一个指针变量你不知道它指向什么地方的时候,你可以让它置成了空指针,这样可以避免野指针的出现。

代码示例:

#include <stdio.h>
int main(){
//当你不知道p应该初始化为什么地址的时候,直接初始化为NULL
	int *p = NULL;
	
//明确知道初始化的值
	int a = 10;
	int * ptr = &a;
//C语言本身是不会检查数据的越界行为的

	int *p = NULL;
	*p = 10;
//这个NULL的方法并不是万无一失的,NULL为0,但是我们用户是没有这个NULL的权限的,所以还是会报错
return 0;
}

四、指针运算

4.1 指针±整数

指针+整数的代码示例:

#include <stdio.h>
int main(){
	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int *p = arr;
	int * pent = arr + 9;
	while(p<=pend){
		printf("%d\n", *p);
		p++;
//打印结果为1 2 3 4 5 6 7 8 9 10
}
return 0;
}

4.2 指针-指针

指针-指针代码示例:

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

指针-指针得到的是两个指针之间的元素个数(这是语法,没有为什么)

指针-指针错误使用示例:

#include <stdio.h>
int main(){
	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	char c[5];
	printf("%d\n", &arr[ 9 ] - &c[ 5 ]);
	//虽然可以打印出东西,但是会有警告
return 0;
}

指针和指针相减的前提是两个指针指向同一块空间

那么要如何灵活应用指针减指针呢,下面我们来看看同一个效果实现的两种代码实现方法。

计数器的方法:

#include <stdio.h>
#include <string.h>
//计数器的方法
int my_strlen(char* str){
	int count = 0;
	while(*str != '\0'){
		count++;
		str++;
	}
return count;
}

int main(){
	//strlen(); - 求字符串长度
	//递归
	int len = my_strlen("abc");
	printf("%d\n", len);

	return 0;
}

指针减指针的方法:

#include <stdio.h>
#include <string.h>
//计数器的方法
int my_strlen(char* str){
	char* start = str;
	while(*str != '\0'){
		str++;
	}
return str - start;
}

int main(){
	//strlen(); - 求字符串长度
	//递归
	int len = my_strlen("abc");
	printf("%d\n", len);

	return 0;
}

代码解析:

先把str指针的地址放到start指针里面,然后用两个指针相减,即可得到字符串长度了。

4.3 指针的关系运算

指针加指针有没有意义呢,其实是没有意义的。就好像日期可以加减天数,日期可以减日期,但日期加日期就没有意义了。

下面我们来看两段代码:

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

将代码简化一下:

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

代码解析:

这两种写法其实是不一样的,第一种写法的指针最后停止的地方是指向第一个元素。第二种写法是停在了第一个元素之前的地址了。
第二种写法有可能会越界,应该要避免这样的写法。

五、指针和数组

数组名是数组首元素的地址

正因为数组名是数组首元素地址,所以我们可以利用指针来写这么一串代码:

int main(){
	int arr[ 10 ] = { 0 };
	int *p = arr;
	int i = 0;
	for(i = 0; i < 10; i++){
		*(p + i) = i;
}
	for(i = 0; i < 10; i++){
		printf("%d ", *(p + i));
		//打印结果为0 1 2 3 4 5 6 7 8 9
}
return 0;
}

代码解析:

p+i,其实就是数组的下标,*(p + i )就是解引用后赋值i给数组下标所对应的元素

关于写法问题:

int main(){
	int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int * p = arr;//数组名
//[ ]是一个操作符,2和arr是两个操作数
//就和a+b能写成b+a一样
	printf("%d\n", 2[arr]);
	printf("%d\n", arr[2]);
//arr[2] <==> *(arr+2) <==> *(p+2) <==> *(2+p) <==> *(2+arr) == 2[arr]
//2[arr] <==> *(2+arr)


	return 0;
}

在编译器处理的时候arr[2]会转化成*(arr+2)
*(arr+2)可以转化成(2+arr)可以转化成2[arr]


六、二级指针

当指针指向的对象类型是什么类型*前面就写什么类型。当一个指针指向另一个指针的时候,这个指针就叫二级指针。

代码示例:

int main(){
	int a = 10;
	int* pa = &a;//pa是指针变量,一级指针
//pa指向的那个a是整型(int),这个时候*的前面就需要写个int
	int* *ppa = &pa;//pa也是个变量,&pa取出pa在内存中起始地址
//pa的类型整体是int*,所以*ppa前面就需要写个int*。
	return 0}

在上述代码中,pa叫一级指针,ppa就叫二级指针。

以此类推的就是三级指针,后面也可以继续有四级指针五级指针,语法上是可以的。

代码示例:

int main(){
	int a = 10;
	int* pa = &a;//pa是指针变量,一级指针
//pa指向的那个a是整型(int),这个时候*的前面就需要写个int
	int* *ppa = &pa;//pa也是个变量,&pa取出pa在内存中起始地址
//pa的类型整体是int*,所以*ppa前面就需要写个int*。
	int** *pppa = &ppa;
//ppa的类型整体是int**,所以*ppa前面就需要写个int**。
	return 0}

七、指针数组

指针数组是指针还是数组呢?

答案是数组,是存放指针的数组。

代码示例:

int main(){
	int arr[10];//整形数组 - 存放整形的数组就是整形数组
	char ch[5];//字符数组 - 存放的是字符
	//指针数组 - 存放指针的数组
	int* parr[5];//整形指针的数组
	char* pch[5];//字符指针的数组
	
	return 0;
}

存放指针的数组就是指针数组,先认识有整型指针数组的这么一个概念就够了,这就是初阶指针。至于如何应用,放到后面再说。


总结

1、指针就是用来存放地址的。
2、指针类型决定了,解引用权限和步长。
3、写代码尽量不要写野指针这种代码,即指针指向的位置是不可知的。
4、在指针运算中,有意义的只有指针加整数,指针减指针,指针减指针是没有意义的。
5、数组名是数组首元素的地址
6、当指针指向的对象类型是什么类型*前面就写什么类型。当一个指针指向另一个指针的时候,这个指针就叫二级指针;
7、以此类推的就是三级指针,后面也可以继续有四级指针五级指针,语法上是可以的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值