C语言(指针篇)

本文深入浅出地介绍了指针的基本概念,包括指针的定义、类型、运算等内容,并详细讲解了野指针产生的原因及预防措施。此外,还探讨了指针与数组、二级指针、指针数组的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

简介:

1:指针是什么?

2:指针和指针类型

3:野指针:

3.1:野指针的成因

2、指针越界访问

3.2:如何避免“野指针”

4:指针运算

4.1 指针+ / -运算

5:指针与数组

6:二级指针

7:指针数组


简介:

1:指针是什么?

对于指针有两个需要知道的点:

1:指针就是一个最小内存单元的编号(对于最小内存单元的解释如下:)

2:通常我们说的指针其实就是一个“地址”,这个地址就是指向具体数据的位置。

那么指针变量到底怎么使用?所谓的指针变量到底存放什么东西?下面我们就来唠唠:

指针变量的使用:对于指针变量的使用其实就是对于变量地址的存储,顾名思义就是“存放地址的变量”,具体怎么使用呢?看下图:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int num = 100;
	int* p = &num;   //此时p就是一个指针变量,数据类型 *变量名 = &变量名 , 就代表一个指针变量存 
                       储的是一个变量的地址
	return 0;
}

很多同学就会注意到&这个符号,&变量名,就是为了取一个变量的内存地址

使用 数据类型* 变量名 = &需要取地址的具体变量名

例如:int* pa = &num;

下面是调试过程:

我们可以看到指针变量p指向的位置和num的地址是一致的,就说明此时指针变量指向的就是整型num的具体内存地址。

那么指针变量和整型在内存空间中到底是怎么存在的呢?

那么有的同学就会问?那么32位机器下指针的大小是多少呢?可以表示的地址有多少个呢?

对于32位机器我们通常认为每根地址线在内存中存在的地址是有两种电平构成的“高电平(1)”和“低电平(0)”,对于32位机器可以表达的地址数其实就是:2^32个 , 并且是以下列的形式保存的:

 一次编译能够存储的地址数:

2^32Byte = 2^32 / 1024KB = 2^32 / 1024 / 1024MB = 2^32 / 1024 / 1024 / 1024GB = 2^2GB = 4GB;


2:指针和指针类型

我们先来谈谈指针的大小是多少?

指针的大小基于不同的编译环境,大小也随之不一致。

我们在VS下使用的环境有两种:X86 / X64

我们在两种环境下的指针大小是不一致的:

X86环境就是32位操作平台,在32位操作平台下指针的大小是:32bit = 4 Byte

X64环境就是64位操作平台,在64位操作平台下指针的大小是:64bit = 8byte

具体如下图:


那么还有一个问题?我买了好多同学就会说,指针不就是为了存储地址的吗?那么为啥会出现这么多的指针类型?

我来给大家举个例子大家就明白了! 

以下例子都基于此代码块:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int a = 0x11223344;  //0x表示16位进制,存储的是地址
	int* pa = &a;   //pa指向a变量的地址
	*pa = 0;
	return 0;
}

 开始调试:

此时pa还没有指向a的地址:

现在pa指向a的地址:

 此刻*pa(解引用)给a二次赋值:

 我们发现使用解引用(*)可以改变变量在空间地址中的数值。


此时我们同学就会说:对呀!确实全部都改变了,但是这和不同类型的指针有个啥关系呢?

请看下面:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int a = 0x11223344;  //0x表示16位进制,存储的是地址
	char* pa = &a;
	*pa = 0;
	return 0;
}

 开始调试:

此时pa还没有指向a的地址:

 现在pa指向a的地址:

  此刻*pa(解引用)给a二次赋值(关键):

至此结束,我们发现看来指针类型的不一致是需要存在的。

指针的类型是有意义的!!!


 那么下面继续讨论一下不同的指针类型具体有哪些玩法?

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int a = 0x11223344;  //0x表示16位进制,存储的是地址
	int* p1 = &a;
	char* p2 = &a;
	printf("p1 =   %p\n", p1);
	printf("p1+1 = %p\n", p1+1);
	printf("p2 =   %p\n", p2);
	printf("p2+1 = %p\n", p2+1);
	return 0;
}

再用物理内存的形式为大家展示“不同类型的指针拥有不同的步进单位”:

此时我们就知道原来不同的指针类型是有意义的,并且不同的指针类型的“步进单位”(步进:向前移动的内存长度)也不一致。


那么有的同学就会说:int和float在空间中占用的大小都是4byte,那么是不是这两个指针变量就可以混用呢?

我们看下面的代码和调试过程就会了解:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int a = 10;
	int* p1 = &a;
	float* p2 = &a;
	*p1 = 100;
	*p2 = 100.0;
	return 0;
}

调试过程(请直接看截图中的文字解释):

从上面的调试过程我们看出,int型虽然和float型的指针/数据类型大小一致,但是实际上他们在内存中的存在方式还是不一样,这就从另一个方面说明,指针类型的不一致是有意义的。


3:野指针:

野指针就是没有具体指向的一种指针。

3.1:野指针的成因

1、指针未初始化:

我们通常如果在程序设计中要是没有初始化变量的话,变量的内部就会存储的是“随机数值”

而指针也是如此,要是一个指针指向的空间内存的地址不确定,我们就称他为“野指针”。

举例说明:

从现在开始我会使用“示例代码 + 调试过程”的形式为大家解释说明有关的知识点:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int* p;     //指针变量p并没有初始化
	*p = 10;
	printf("%d", *p);
	return 0;
}

 此刻我们看到的p其实就是一个没有初始化的指针(“野指针”)。

2、指针越界访问

我们在设置数组的长度的时候,总是要求不能越界访问,要是使用一个指针指向数组的时候也同样如此:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int arr[10] = { 0 };  //此时arr的内部存在10个0
	int* p = arr;
	for (int i = 0; i <= 10; i++ ) {  //这样遍历数组的时候会遍历11次
		*p = i;
		p++;
	}

	return 0;
}

3:指针被释放的时候,形成野指针

当我们使用free释放指针的时候,此时的指针p就会变成一个野指针,指向“垃圾值”

 使用free()之后我们发现出现“野指针”所以此时指针指向的内存空间中的地址已经发生了改变,指针不再指向从前的地址。

其实还有一种情况下会出现“野指针”的情况:

定义一个指针来接收地址的时候,在一个外部函数的内部创建变量,将局部变量的地址传递给指针变量,此时由于外部函数的结束,外部函数内部的局部变量就被销毁了,此时指针变量指向的地址就又会变成一个“垃圾地址”

3.2:如何避免“野指针”

野指针的成因在上述已经解释了,我们只需要针对性地去改正这些问题就可以了!

1、指针初始化:

2、使用指针不知道指向什么的时候,就指向NULL:

VS对于NULL的定义是:

 NULL就是0

3、不能对空指针赋值:

4、在指针访问数组的时候,注意“步进”:

注意一点!!!我们在函数结束的时候,其实内存中的数据被OS(操作系统)回收的时候,返回来的数值就不会是原来函数内部的局部变量的数值了。

4:指针运算

对于指针的运算其实通俗的来说的话,就是指针指向的地址的改变(也就是指针在合法的位置进行前后的指向)

4.1 指针+ / -运算

下面我们用这个例子来进行说明:

#define _CRT_SECURE_NO_WARNINGS
#define N_num 5
#include<stdio.h>
int main() {
	float arr[N_num];
	float* p;
	for (p = &arr[0]; p < &arr[N_num]; ) {
		*p++ = 6;  //*p++  拆分之后就是*p = 6 ; p++
	}
	for (p = &arr[0]; p < &arr[N_num]; p++) {
		printf("%f\n", *p);
	}
	return 0;
}

分析上述代码:

我们让p这个指针从&arr【0】这个地址开始,一直到&arr【N_num】的地址,每次都往数组的内部存入6,最后进行打印。

实际上指针的偏移还有很多种情况,下面一一列举:

1:指针的自增

先定义一个指针和一个数组:

int* p = NULL;   //此刻的p是一个野指针,为了避免野指针的出现,把他置空

int arr[10] = { 0 };

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int* p = NULL;  //避免野指针
	int arr[10] = { 0 };
	p = arr;   //此刻让p指向arr的首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);  //求出arr的数组长度
	for (int i = 0; i < sz; i++) {
		*p = 6;
		p++;
	}
	for (p = &arr[0]; p < &arr[10]; p++) {
		printf("%d\n", *p);
	}
	return 0;
}

 解析:

2:指针的数量后移

对于指针不止一种遍历方式,指针还可以通过整型的偏移来进行赋值(p + i)

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

​

4.2: 指针 - 指针类型

指针既然是指向内存地址的一个变量,那么对于内存地址的表示也是16进制的表示,指针之间相减其实就是“两个指针之间元素的个数”。

示例如下:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int arr[10] = { 0 };
	int* p1 = &arr[0];
	int* p2 = &arr[9];
	printf("%d\n", p2 - p1);   //指针之间的差值是元素下标的差值
	printf("%d\n", p1 - p2);
	return 0;
}

 对于第一种(p2 - p1)和第二种(p1-p2)就是指针之间元素的下标的矢量值:

 应该会有很多小伙伴就会说:“为啥就是9/-9 , 那为啥不是36 / -36呢?”

对咯!就是这个问题,你看看指针类型是什么?是int*类型的呀,原子类型是int

所以说两个相同类型的指针相减就是两个int类型的变量之间的差值(矢量差),所以就是指针指向的不同元素的下标的差值。

由此我们就了解到,只有相同类型的指针相减的差值才有意义,要是一个double类型的指针与一个int类型的指针相减,本质上就是没有意义的:

举例而言:在同一个数组里面,类型是一致的,要是出现两种类型的指针就会出现

在举一个例子:

对于我们经常使用的strlen()这个函数,我们知道这个函数的功能就是求'\0'之前的元素数,那么我们模拟一个my_strlen()就必须使用指针的相加减。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int my_strlen(char* p) {
	char* str = p;
	while (*p != '\0') {
		p++;
	}
	char* end = p;  //此时str走到的位置就是\0的位置了,此时使用end来进行接收最后元素的地址

	return end - str;  //返回元素下标的差值
}
int main() {
	char arr[10] = "abcdefg";
	int len = my_strlen(arr);
	printf("arr数组内部有%d个元素", len);
	return 0;
}

 

 4.3:指针的关系运算

指针之间的关系运算其实更应该 富有意义,而不是胡乱一气。

比如说:

指针相减其实就是表示同一种类型的指针,两个指针之间的元素数

但是指针相乘/相除/相加就很抽象,他的意义是什么呢?(目前在计算机领域还没有出现,但是不代表以后哦!)

再来一个例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int arr[10] = { 0 };
	int* p1;
	for (p1 = &arr[10]; p1 > &arr[0]; ) {
		*(--p1) = 6;
	}
	return 0;
}

 我们允许让指针指向数组的后面的非法位置,但是不允许指向数组的前面的非法位置。

但是转头一变,我们发现同样在数组的前面的位置要是设置指针来进行访问的话,也不是不行?

合法但是不合理,在编译器下虽然是正常编译的,但是却不是最标准的。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int arr[10] = { 0 };
	int* p1;
	for (p1 = &arr[-1]; p1 < &arr[10]; ) {
		*(++p1) = 6;
	}
	return 0;
}

我在想是不是因为当指针刚开始指向数组的第一个元素之前的地址的时候,是不是会影响其他元素,因为地址是从低往高走的,所以是不是会访问到其他的在数组第一个元素之前的地址上的一部分数值。(tips:仅个人猜想)


5:指针与数组

对于指针和数组之间的联系,当指针指向数组的时候,就是指向数组的首元素的地址,两者的地址是一致的。

举例如下:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int* p;
	int arr[10] = { 6 };
	p = arr;
	printf("%p\n", p);
	printf("%p\n", arr);
	return 0;
}

还有一个对于指针地址的表示(不同的表示方法,表示同一块相同的地址):

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int arr[10] = { 0 };
	int* p = arr;
	for (int i = 0; i < 10; i++) {
		printf("%p  ------------->  %p\n", &arr[i], p + i);
	}
	return 0;
}

直接指向的具体数组的元素的地址和指针移动的地址一致(&arr[i]  ==  p + i)

6:二级指针

我们经常使用指针来表示一个内存空间中的地址,但是你们听说过二级指针吗?

其实二级指针和指针是一样的,只不过指向的对象不一致了而已,举一个下列的例子:

int a = 10;   

int* p1 = &a;    //此时p1所指向的地址就是a的地址

int** p2 = &p1;   //此时p2指向的地址就是p1的地址

此时的p2就是二级指针 , p1就是一级指针 

我们继续使用下面的代码来仔细聊聊“一级指针”和“二级指针”。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int num = 10;
	int* p1 = &num;
	int** p2 = &p1;
	printf("未改变前:%d\n", *p1);
	*p1 = 20;
	printf("改变一级指针之后:%d\n", *p1);
	**p2 = 300;
	printf("改变一级指针之后:%d\n", **p2);
	return 0;
}

下面我们再来聊聊指针类型的区分:

int a = 10;   

int* p1 = &a;    //此时p1所指向的地址就是a的地址

int** p2 = &p1;   //此时p2指向的地址就是p1的地址

解释如图所示:

7:指针数组

我们经常听到“整型数组”、“字符数组”这类词汇,但是我们此时引进的“指针数组”,这个词还是很新鲜的吧!

其实都一样的!

“整型数组”:数组的修饰类型是什么?对咯!是整型,那就说明这个数组的内部存放的都是整型

“指针数组”:数组的修饰类型是什么?对咯!是指针,那就说明这个数组的内部存放的都是指针

那么此时我们再举一个例子大家就明白“指针数组”的好处在哪里?

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int a = 10;
	int b = 20;
	int c = 30;
	int* p1 = &a;
	int* p2 = &b;
	int* p3 = &c;
	return 0;
}

下面就是“指针数组”的showtime!!!

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int a = 10;
	int b = 20;
	int c = 30;
	int* p1 = &a;
	int* p2 = &b;
	int* p3 = &c;
	int* arr[10] = { p1 , p2 , p3};
	//使用“解引用”来取数组内部的数值
	printf("%d\n", *arr[0]);
	printf("%d\n", *arr[1]);
	printf("%d\n", *arr[2]);
	//使用数组的下标来打印不同变量的地址
	printf("%p      ----------------------->%p\n", arr[0] , p1);
	printf("%p      ----------------------->%p\n", arr[1] , p2);
	printf("%p      ----------------------->%p\n", arr[2] , p3);
	return 0;
}

我们要是想要修改这个“指针数组”的内部的元素的数值的时候,我们就可以直接“解引用”来修改(因为本来“指针数组”内部存放的就是地址,我们对他的地址进行“解引用”就是直接访问这个地址的元素的数值)

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

 这就实现了“指针地址”所在的元素的数值了。


对于我们二维数组的表示,其实使用“指针数组”也可以实现

具体看实例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	int arr1[4] = { 1,2,3,4 };
	int arr2[4] = { 5,6,7,8 };
	int arr3[4] = { 9,10,11,12 };
	//使用一个指针数组将一维数组存入
	int* parr[3] = { arr1 , arr2 , arr3 };
	for (int i = 0; i < 3; i++) {
		for (int j = 0; j < 4; j++) {
			printf("%d ", parr[i][j]);  //此时的parr[i]其实就只是起到了一个一维数组地址的作用
			                            //后面的parr[i][j]中的j其实就是一维数组arr系列的元素的 
                                          下标
		}
		printf("\n");
	}
	return 0;
}

具体图例如下图所示:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值