C语言从青铜到王者第六篇·详解C指针
Posted ·潇
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言从青铜到王者第六篇·详解C指针相关的知识,希望对你有一定的参考价值。
本篇前言
“指针是C语言的灵魂,精通指针也就基本精通了C语言。”
指针确实非常重要,但是它只是一个工具,是帮助我们分配内存的工具,而了解指针,并且通过指针熟练的操作内存,这才是我们努力的方向。
文章目录
指针到底是什么?
编程语言的本质是什么?
我们可以把计算机当成一个外国人,而我们想和他沟通必须用同一种语言。虽然目前有些AI看似已经能够直接理解汉语,但是这些AI程序仍然是用计算机语言来编写的。也就是说,我们暂时并不能指望计算机来直接理解我们的语言,所以我们要想与之沟通,并让它为我们服务,就必须学习它的语言。
那在C语言中,计算机是如何理解我们描述给它的问题的呢?
首先,计算机是通过数据化处理来认识这个世界的。万事万物,在计算机眼里都是数据
比如,有这样一个事实:“我有一个苹果的,它的价格是2元”
我们把苹果的价格设为一个变量apple_price,那代码就可以写成:
int apple_price = 2;
int apple_price = 2;
这句话用我们的“人话”来说,就是“苹果的单价是2元”,而对于计算机来说,就是“在内存中开辟了一块4字节大小的空间,这个空间是专门划分给一个叫apple_price的变量的,这个变量的数据类型是整型,然后在这块空间中按照整型变量的存放规则放入了值为2(十进制)的二进制数00000000 00000000 00000000 00000010”
内存是什么?
刚刚那句“计算机说的话”用图像来表示就是:
那内存又是什么呢?我们可以把内存想象成一系列的储存0或1的“格子”(图中的所有格子都表示内存),每8个格子代表一个内存单元(图中的一行格子),每开辟一块变量的空间就是划定一段连续的格子(比如图中的灰色格子就是开辟一个int
型变量的空间),用来存放新的01序列,这些新的01序列就会根据特定的规则存放数据,比如int
型数据(有符号整型)就是需要32个格子,代表32位二进制序列,序列首元素代表符号,1正0负,其余的按照二进制规则表示数字
而计算机能读懂的其实就是这一连串的01序列,也叫“机器语言”,我们今后所写的所有程序,实际上都会被翻译成成千上亿的0和1储存在这些格子中,我们操作变量,修改代码,本质上就是改变这些0和1的储存位置
下图就是“计算机眼里的世界”
那看起来计算机做的工作就是不断的往空的格子里放0/1。而我们知道存放东西的目的是为了我们将来把它取出。而想要快速高效的取出就意味着这些0和1必须整整齐齐的按照一定顺序的摆放在格子里。这就像我们的快递柜一样,我们把包裹放入指定的有编号的柜子,将来再通过查找编号将其取出。
所以继续思考一个问题:内存是如何有序的存放这一系列的0和1的呢?
实际上内存也是一个大快递柜,它的每一个柜子都有编号,这个编号就是我们说的“内存的地址”
每个内存单元的编号就是地址
内存会像上图一样给内存单元编号,第一个内存单元就是1号,第二个就是2号…但是问题是计算机有成千上亿的内存单元,而计算机又只认识二进制语言(0或1),所以我们是不能用十进制的数字来编号的,必须用二进制来表示
C语言规定:在32位系统上,地址是32位二进制序列,在64位系统上,地址是64位二进制序列
也就是说,在32位系统上,每个地址是32位二进制序列,所以最多可以表征232个内存单元,一个内存单元的大小是一个字节,也就是1B,210字节就是1KB,220字节就是1MB,230字节就是1GB,2^32字节就是4GB,所以32位系统最多可以有4GB的内存
当然啦,随着科技的进步,我们对内存的需求远远超过了4GB。我们现在的电脑基本都是64位系统,也就是地址都是64位二进制序列,最多可以表征264个内存单元,也就是4×232GB的内存,这个数量在大多数场景下是够用的。当然对于为了满足某些特殊需求而设计出来的超级计算机,内存又会呈指数级增长了。
我们在编译环境下查看地址时,一般都是16进制。这是因为32个0/1写出来过于繁琐,一般编译器展示给我们的序列都是8位16进制(64位编译器就是16位16进制)的序列
指针是什么?
理解了上面的东西后,我们终于可以来说指针是什么了
看下面一段代码:
#include<stdio.h>
int main()
int a = 10;
int* pa = &a;
*pa = 20;
return 0;
&a
中的&
是取地址操作符,可以把a的地址取出来(得到快递柜的编号)。int*
是一种特殊的数据类型,这种类型的变量专门用来存放int
型变量的地址
所以int* pa = &a;
的意思就是“把a所在内存的地址取出来放到pa中”。(把这个编号记下来)
*pa
是解引用操作,就是通过pa中的地址找到这块地址所对应的内存空间(对着编号打开了这个快递柜)
所以*pa = 20;
的意思就是通过pa中存放的地址(也就是a的地址)找到这块地址所对应的内存空间(也就是a),然后把20放入这块空间(把20这个数字放入快递柜)
所以此时a的值实际上已经变成了20(快递柜里的数字已经从10变成了20)
上面这段程序中的变量pa就是指针
指针就是一个存放地址的变量
指针,也叫做指针变量,数据类型为指针类型的,专门用来存放地址的变量,大小在32位系统上为4字节,在64位系统上为8字节
通过操作指针,我们就可以存放一块内存的地址,就可以很方便的通过地址来访问和管理这块内存。C语言能够通过指针直接管理内存,这也是C语言常常是底层编码语言的原因。
指针类型
指针变量的数据类型就是指针类型
int*
char*
short*
...
指针类型的大小
指针变量是用来储存地址的,因为地址是32位(在32位机器上),所以指针类型的大小是4字节
做个实验验证一下:
#include<stdio.h>
int main()
printf("%d\\n", sizeof(int*));
printf("%d\\n", sizeof(char*));
printf("%d\\n", sizeof(short*));
printf("%d\\n", sizeof(long*));
printf("%d\\n", sizeof(float*));
printf("%d\\n", sizeof(double*));
return 0;
指针类型决定解引用的访问权限
不同指针类型的特殊性体现在两点:
一就是指针类型决定了指针解引用的访问权限
看下面的程序
#include<stdio.h>
int main()
int a = 0x11223344;
int* pa = &a;
*pa = 0;
return 0;
将程序运行到图示位置
此时调出内存查看器发现a所在的内存中为:
继续运行
发现a所在的内存中变成了00 00 00 00
当我们修改一下程序,用char*
型的指针接收地址时:
#include<stdio.h>
int main()
int a = 0x11223344;
char* pa = &a;
*pa = 0;
return 0;
运行到图示位置
发现只有一个字节的数据被改变了。也就是说,char*
类型的指针只能访问一字节的内存。实际这是因为char
类型的大小就是一字节。所以指针类型对应的数据类型的大小决定了其解引用时能够访问的内存大小
指针类型决定指针的步长
指针类型的第二个特殊性就是决定了指针“走一步走多远”,也就是指针的步长
看下面这段程序
#include<stdio.h>
int main()
int arr[10] = 0 ;
int* p = arr;
char* pc = arr;
printf("%p\\n", p);
printf("%p\\n", p + 1);
printf("%p\\n", pc);
printf("%p\\n", pc + 1);
return 0;
结果为
7C→80 4字节
7C→7D 1字节
差别的根源就是这两个指针的指针类型一个是int*
一个是char*
通过以上两个对指针类型的了解,我们可以用下面的写法来操作数组元素:
#include<stdio.h>
int main()
int arr[10] = 0 ;
char* p = arr;
int i = 0;
for (i = 0; i < 40 ; i++)
*(p + i) = i;
return 0;
运行一下看看
发现成功的逐字节的存入了数字123456…40(显示出来是16进制)
野指针
指针指向的位置是不明确的指针就是野指针。
野指针的出现会干扰我们对于内存的管理,所以我们需要了解野指针的成因然后规避野指针的出现
野指针成因
1.指针未初始化
#include<stdio.h>
int main()
int* p;
*p = 20;
return 0;
局部变量指针变量p未初始化,默认为随机值
此时再对p解引用,就是非法访问内存
一般编译器也会直接报错
2.指针越界访问
#include<stdio.h>
int main()
int arr[10] = 0 ;
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++)
*p = i;
p++;
return 0;
循环到第十一次时,指针就不指向数组了,此时解引用访问,就是非法访问内存,第十一个指针就是野指针
3.指针指向的空间被释放
#include<stdio.h>
int* test()
int a = 10;
return &a;
int main()
int* p = test();
*p = 20;
return 0;
a是局部变量,出函数即被销毁,再次通过a的地址操作内存就是非法访问内存
规避野指针的方法
1.指针一旦使用必须初始化。不知道初始化什么地址就初始化为NULL
空指针,明确知道初始化的值,那就直接取地址初始化。
2.小心越界访问内存的行为。比如循环访问内存时,看清楚最后一次循环访问的内存有没有越界。
3.指针指向空间一旦销毁就要把指针置成空指针。这个在设定局部变量指针时要特别注意。
4.空指针不可以直接解引用访问,因为空指针指向的空间不存在于内存中。
对于情况4,我们可以写下面的程序进行指针有效性的检查:
if (p != NULL)
//操作
指针的状态只能是合法的可操作指针和空指针,也就是说,指针变量里的地址必须有明确的指向
指针运算
讲到这里,对于指针本身的我们认识的差不多了。接下来我们要学习如何运用指针进行运算。
指针+ - 整数
指针±整数就是进行单位步长的移动(具体程序可以参考前面“指针类型决定指针的步长”)
指针的关系运算
指针的关系运算就是比较两个地址的大小。
地址在一个数组里是随着数组下标的增加由低到高变化的
比如下面的程序通过指针的关系运算来初始化数组元素:
int main()
int arr[10] = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ;
int* p = arr;
int* p_end = arr + 10;
while (p < p_end)
printf("%d\\n", *p);
p++;
return 0;
指针 -
指针
指针-指针就是两个指针之间的元素数量
int main()
int arr[10] = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ;
printf("%d\\n", &arr[9] - &arr[0]);
return 0;
结果是9,表明第10个元素和第1个元素之间有9个元素(不是8个)。
注意:指向相同空间的指针才可以相减
例如下面的程序利用这个技巧求字符串长度:
int my_strlen(char* str)
char* start = str;
while (*str != '\\0')
str++;
return str - start;
const 修饰指针
看一个简单的程序,用指针修改变量值
#include<stdio.h>
int main()
int num = 10;
int* p = #
*p = 20;
return 0;
如果我们规定num
的值不能改,按理说只需要给num
加const
把它变成常变量即可
#include<stdio.h>
int main()
const int num = 10;
int* p = #
*p = 20;
printf("%d", num);
return 0;
结果还是改了。这是怎么回事呢?
这种写法的问题在于我们不应该用const修饰num变量,因为const修饰的变量只是不能被直接赋值符=
影响,但是仍然可以通过指针访问的形式修改它的值。
自然而然的,既然不修饰num,那就只能修饰num的指针了
下面介绍const修饰指针的两种不同的写法
const 放在 * 前面
表示指针指向的内存不能通过指针被改变:
#include<stdio.h>
int main()
int num = 10;
const int* p = #
*p = 20;
printf("%d", num);
return 0;
这个写法就是我们前面程序想要达到的效果。
const 放在 * 和变量名之间
表示指针变量p不能被更改:
#include<stdio.h>
int main()
int num = 10;
int a = 1;
int* const p = #
p = &a;
printf("%d", num);
return 0;
这种写法约束的是指针变量p,而不是整型变量num
二级指针和多级指针
指针变量的地址,存放到另一个指针变量中,第二指针变量就是二级指针,同理,第三指针变量就是三级指针
二级指针的指针类型就是再加一个*
,比如int
型二级指针的数据类型就是int**
,同理,三级指针是int***
int main()
int a = 10;
int* pa = &a;
int** ppa = &pa;//二级指针
int*** pppa = &ppa;//三级指针
return 0;
再复杂的程序中可能会用到多级指针。暂时了解即可
指针与字符串
字符串与指针的结合有三个特殊点:
- 1.字符串可以直接存入指针变量。此时指针中存入的是字符串首元素的地址,解引用所访问的也是字符串的首元素所在内存
#include<stdio.h>
int main()
char* ps = "I love C";
printf("%c\\n", *ps);
return 0;
虽然解引用可以访问这块内存,但是我们是无法改变这块内存的值:
原因是这种写法实际上并没有开辟一块新的内存。对于常量12345…等数字和ABCD…abcd…等字母,计算机为了节省空间,直接给它们分配了固定的内存。而对于一个常量,计算机同样也只会开辟固定的内存来存储它们。这些内存都只可读不可写(写就是修改的意思)。
2.打印字符串的首地址,可以直接获取整个字符串
#include<stdio.h>
int main()
char* ps = "I love C";
printf("%s\\n", ps);
return 0;
做个实验:
#include<stdio.h>
int main()
char *ps = "I love C";
printf("%p", ps );
return 0;
#include<stdio.h>
int main()
char *ps = "I love C";
printf("%p", ps + 1);
return 0;
#include<stdio.h>
int main()
char *ps = "I love C";
printf("%p", ps + 2);
return 0;
所以对于变量,计算机会开辟一块连续的内存用于存放,而对于常量,计算机选择的是直接访问固定位置的内存。
3.常量字符串的地址相同
#include<stdio.h>
int main()
char* ps1 = "I love C";
char* ps2 = "I love C";
printf("%p\\n", ps1);
printf("%p\\n", ps2);
return 0;
原理也是因为存入的是同样的常量字符串,所以直接访问了同一块内存,地址自然是一样的。
指针与数组
指针与数组的结合也有以下几个特殊点
指针数组
整型数组是由整型组成的数组,
字符数组的由字符组成的数组,
所以指针数组就是由指针组成的数组
int main()
int a = 0;
int* arr[10] = &a ;
return 0;
arr
就是指针数组
指针数组的使用场景:
下面这段程序就演示了怎么通过指针数组模拟二维数组:
#include<stdio.h>
int main()
int a[] = 1,2,3,4,5 ;
int b[] = 2,3,4,5,6 ;
int c[] = 3,4,5,6,7 ;
int* arr[3] = a,b,c ;
int i = 0;
for (i = 0; i < 3; i++)
int j = 0;
for (j = 0; j < 5; j++)
printf("%d ", *(arr[i] + j));
//printf("%d ", arr[i][j]);
printf("\\n");
return 0;
数组指针
整型指针是指向整型的指针
字符指针是指向字符的指针
所以数组指针是指向数组的指针
数组指针的指针类型写法有点复杂。看下面的程序
#include<stdio.h>
int main()
int arr[10] = 10 ;
int(*parr)[10] = &arr;
return 0;
parr
就是数组指针,存放的是arr
数组的地址
数组的类型是int [10]
,所以int(* ) [10]
就是这个数组指针的数据类型。注意数组指针的指针名要写在括号内
上面这个例子是整型数组的指针,那么问题来了:指针数组的指针是什么呢?看下面的写法
#include<stdio.h>
int main()
int a =以上是关于C语言从青铜到王者第六篇·详解C指针的主要内容,如果未能解决你的问题,请参考以下文章