【C语言】指针基础:为什么说指针是C语言的灵魂?

【C语言】指针基础:为什么说指针是C语言的灵魂?

C语言学习

指针基础

友情链接:C语言专栏

前言:指针——编程世界的“双刃剑”在C/C++的世界里,指针像是一把瑞士军刀:它能让你直接操纵内存,写出高效灵活的代码,但也可能因为一个疏忽,让程序瞬间崩溃。有人说指针是“程序员的终极武器”,也有人说它是“bug的万恶之源”——这种矛盾的存在,恰恰说明了它的重要性。

无论你是被指针“折磨”的初学者,还是想深入理解指针底层的老手,这篇博客都会提供清晰的逻辑和实用的代码示例。让我们从“指针即地址”这一句话开始,逐步拆解它的威力与陷阱。

“指针是带类型的地址,而理解指针,就是理解程序的灵魂。”

一、什么是指针?首先来回答指针是什么?

指针是内存中的最小单元的编号,也就是地址;平时口语中的指针,通常是指的指针变量,是用来存放内存地址的变量。那么内存是什么?

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。

所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。

为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。

请添加图片描述那如何定义指针变量呢?

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

变量就是指针变量。

示例:(以int为例)

代码语言:javascript复制int main()

{

int i = 0;

int* pi = &i;//取出i的起始地址放入指针变量pi中。

//这里为什么要说起始地址,因为变量i的大小为4个字节,这里是将i的第一个字节的地址存在pi中

return 0;

}那指针变量的大小是多少?(重点):

对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电

平(低电压)就是(1或者0);

那么32根地址线产生的地址就会是:

代码语言:javascript复制00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

...

11111111 11111111 11111111 11111110

11111111 11111111 11111111 11111111这里就有2的32次方个地址。

每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==

2^32 /1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空闲进行编址。

同样的方法,对于64位机器。

这里我们就明白:

代码语言:javascript复制在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以

一个指针变量的大小就应该是4个字节。

那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地

址。总结:

指针是用来存放地址的,地址是唯一标示一块地址空间的。指针的大小在32位平台是4个字节,在64位平台是8个字节注:如果看的不是很懂,可以再看看:初识C语言中指针部分

二、指针和指针类型咱们以前学的变量都是由类型的,比如整形、浮点型等等,那么指针有类型吗,那肯定有的,

比如:

代码语言:javascript复制int a = 10;

p = &a;将变量a的地址存放在指针变量p中,那么p的类型是什么。

这里我们说,指针变量对应的类型是由变量的类型确定的,也就是:

char* 类型的指针是为了存放 char 类型变量的地址。

short* 类型的指针是为了存放 short 类型变量的地址。

int* 类型的指针是为了存放 int 类型变量的地址。等等。

那为什么要有指针类型呢?或者说,指针类型有什么用呢?

2.1、指针±整数直接看代码:

代码语言:javascript复制#include

int main()

{

char b = 0;

char* pc = &b;

int a = 10;

int* pi = &a;

printf("%p\n", &b);

printf("%p\n", pc);

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

printf("%p\n", &a);

printf("%p\n", pi);

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

return 0;

}输出结果:

在这里插入图片描述

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

2.2、指针解引用看代码:

代码语言:javascript复制#include

int main()

{

int n = 0x11223344;//16进制

char* pc = (char*)&n;

int* pi = &n;

*pc = 0;

printf("%#X\n", n);//以16进制输出

//*pi = 0;

//printf("%#X\n", n);

return 0;

}输出:

在这里插入图片描述

说明*pc=0只改变了起始的8位(即1字节/char大小);

再看第二个代码:

代码语言:javascript复制#include

int main()

{

int n = 0x11223344;//16进制

char* pc = (char*)&n;

int* pi = &n;

//*pc = 0;

//printf("%#X\n", n);

*pi = 0;

printf("%#X\n", n);

return 0;

}输出结果:

在这里插入图片描述

即*pi=0将n的整个大小(int大小/4字节)都改为0。

总结:

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

比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

三、野指针首先,来看一下什么是野指针:

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

那再说什么情况下会形成野指针:来看一下野指针的成因

3.1、野指针的成因指针未初始化代码语言:javascript复制#include

int main()

{

int* p;//局部变量指针未初始化,默认为随机值

*p = 10;//这样赋值是没有意义的,也容易导致出错

return 0;

}指针越界访问代码语言:javascript复制#include

int main()

{

int arr[10] = { 0 };

int* p = arr;

int i = 0;

for (i = 0; i <= 11; i++)

{

//当指针指向的范围超出数组arr的范围时,p就是野指针

*(p++) = i;

}

return 0;

}指针指向的空间释放

这就会涉及到动态内存开辟,这一部分不了解的同学可以先看看动态内存分配。

看代码:代码语言:javascript复制int main() {

int *ptr = malloc(sizeof(int));

*ptr = 5;

printf("free之前: %d\n", *ptr);

free(ptr); // 释放内存

// ptr现在是一个野指针

// 危险操作!可能崩溃或输出随机值

printf("free之后: %d\n", *ptr);

ptr = NULL; // 正确做法:释放后置空

return 0;

}3.2、如何规避野指针指针初始化小心指针越界指针指向空间释放即使置NULL前三个在上面已经说过了,主要看一下最后两个:

避免返回局部变量的地址代码语言:javascript复制int* test()

{

int a = 10; // a 是局部变量,存储在栈上

int* p = &a; // p 指向 a 的地址

return p; // 返回局部变量的地址

}

int main()

{

int* p = test(); // p 现在指向一个已经被释放的栈内存

*p = 20; // 解引用野指针,未定义行为(UB)

return 0;

}指针使用之前检查有效性代码语言:javascript复制#include

int main()

{

int a = 10;

int* p = &a;

if (p != NULL)//判断有效性

{

*p = 20;

}

return 0;

}四、指针运算咱们主要以代码为例

4.1、指针±整数代码语言:javascript复制int arr[5] = {10, 20, 30, 40, 50};

int *ptr = arr; // ptr 指向 arr[0]

ptr = ptr + 2; // ptr 现在指向 arr[2](即 30)

ptr = ptr - 2; // ptr 现在指向 arr[0](即 10)总结:

指针加减的步长取决于指向的类型(int* 步长是 4,char* 步长是 1)。

4.2、指针-指针这里咱们简单的模拟实现一下strlen()函数:

strlen()简介:

求字符串长度,遇到\0就停止,不算\0

代码语言:javascript复制#include

int my_strlen(char* pc)

{

char* p = pc;

while (*p != '\0')

{

p++;

}

return p - pc;//指针-指针

}

int main()

{

char arr[] = "abcdefg";

int length = my_strlen(arr);

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

return 0;

}总结:

指针-指针得到的是指针之间的元素个数;

但是要注意,不是所有的指针都能相减,必要时指向同一块空间的两个指针。

4.3、指针的关系运算咱们来看两个代码对比

预处理:

代码语言:javascript复制#define N_VALUES 5

float values[N_VALUES];

float *vp;代码语言:javascript复制for(vp = &values[5]; vp > &values[0];)

{

*--vp = 0;

}对于上面这个代码,咱们可能会想着,进行简化(如下):

代码语言:javascript复制for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)

{

*vp = 0;

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

它可行。

标准规定:

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

指向第一个元素之前的那个内存位置的指针进行比较。

也就是说,咱们第一次写的代码是符合标准规定的。

五、指针和数组看代码:

代码语言:javascript复制#include

int main()

{

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

printf("%p\n", arr);

printf("%p\n", &arr[0]);

return 0;

}看输出:

请添加图片描述

可见数组名和数组首元素的地址是一样的。

总结:数组名表示的是数组首元素的地址。(2种情况除外,【数组详解】中详细解释)

那么咱们这样写代码是可行的:

代码语言:javascript复制int arr[10] = {1,2,3,4,5,6,7,8,9,0};

int *p = arr;//p存放的是数组首元素的地址六、二级指针说起二级指针,有些人可能回畏惧,二级指针说白了,就是(一级)指针的指针(地址),像咱们上面说的,是变量就会有地址,那指针变量也是变量,那指针变量的地址存放在哪里呢?

这就是二级指针:

代码语言:javascript复制int a = 10;

int* pa = &a;//一级指针

int** ppa = &pa;//二级指针对于一级指针中的一些操作也是适用于二级指针的,比如:解引用操作*

代码语言:javascript复制int b = 20;

*ppa = &b;//等价于 pa = &b;另外一种用法:

代码语言:javascript复制**ppa = 30;

//等价于*pa = 30;

//等价于a = 30;注:三级指针、四级……都同理。

七、指针数组先回答一个问题:

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

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

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

那指针数组是怎样的?

代码语言:javascript复制int* arr[5];

//arr是一个数组,有五个元素,每个元素是一个整形指针元素总结指针是C语言的精髓,理解指针就是理解程序的灵魂。掌握这些核心要点,你就能驾驭这把"双刃剑"!

附录上文链接《操作符详解:从基础到高阶,一篇搞定!》

下文链接《指针进阶1:数组与指针》

专栏C语言专栏

相关推荐