我个人认为,C 语言指针的难以掌握的原因有以下几个:
指针的声明符号—— * 星号同时也是“解引用”操作符。在不同的语句中的含义是不一样的。
指针是一种复合类型,它需要指定自己是属于“何种变量”的指针
指针是可以运算的,其数值的变化和指针类型有关。
程序员可以使用指针读写任何地址的内存,如果不了解程序对于内存的运作机制,可能会访问了一些无法预知结果的地址,造成各种进程退出导致的故障
#include <stdio.h>
int main()
{
int i = 10;
int *p;
p = &i;
printf("var i addr: %llu, *p content: %d\n", p, *p);
*p = 20;
printf("var i now: %d\n", i);
}
var i addr: 272537222788, *p content: 10
var i now: 20
指针这种类型的变量,含义是“指向某个变量地址的变量”,也就是说,指针变量的内容,是另外一个变量的内存地址。内存地址也是一个数字,在 64 位操作系统中,长度是 8 个字节。
注意指针是一个复合类型变量,也就是说,在声明的时候,除了 * 号外,必须在前面带上一个基本变量的类型, 表示指针“指向”的变量是什么类型的 。你可以选择 char\int\float 等,如果有一些情况下无法确定“指针的类型”,可以用 void * 来声明一个“通用类型”的指针。
在上面的代码里, int *p; 声明了一个“int 类型的指针” p 变量。而 p = &i 的功能是,把变量 i 的内存地址的值,写入到变量 p 里面去。这里的 & 符号的功能是“取地址操作符”,意思就是把某个变量的地址读取出来。
在后面的代码 printf() 代码中,我们尝试打印了指针变量 p 和 *p 的内容:
我用了 %llu 作为占位符打印 p 的内容,因为 p 作为指针变量,长度是 64 位(8 个字节),比之前我们用的 int 类型变量的 4 个字节要大,所以使用了的 %llu 意思是按 long long unsigned 长无符号整数(8 个字节)的格式来打印 p 的内容。clang 编译的时候,会警告我们:变量 p 是一个指针类型变量,不是一个“长无符号整数”类型,怀疑我们可以写错了。另外一种常见的打印指针的占位符号是 %p ,如果你用 %p 来打印指针,clang 就不会抱怨了。
我也用了 %d 来打印 *p 的值。在这里 * 星号的作用是“解引用操作符”,功能是把后面的 p 的值,作为一个内存地址,然后去读取此内存地址中的值。由于 p 是“int 类型的指针”,所以 *p 的计算结果,就是一个 int 类型的值。于是可以用 %d 来打印。
解引用操作符 * 是操作指针指向内存的内容的符号,不但可以用来读取指定内存的值,同样可以用来把数据写入这块内存。后面的代码 *p = 20; 这一行,就是对 p 指向的内存,写入了数值 20。最随后的 printf() 代码中,我们可以看到变量 i 的值确实被改成了 20。
如果我们只是用 & 和 * 来操作一个变量的指针,那么指针本身作用和变量没多少差别,看起来也没什么用处。但是,如果我们需要处理的不是一个变量,而是“一批”变量,指针就是一个很有用的工具了。而所谓的“一批变量”,最常见的就是 字符串 了。
在 C 语言当中,是没有“字符串”这个变量类型的。而绝大多数的其他编程语言,都有“字符串”类型,为什么 C 语言没有呢?我认为最基本的原因是:
C 语言的变量类型代表了一个特定的内存长度,字符串的长度是不固定的,所以没法给“字符串类型”一个长度设定。
那么 C 语言要处理的最常见的文件应该如何办呢?最简单的办法就是使用“数组”。所谓“数组”,其实就是一批变量,这些变量一般是连续存在的,也就是说在内存里面互相挨着的。我们要存放一段字符串“abc”的话,就声明三个 char 类型变量组成的数组,依次在数组中放入 'a' 'b' 'c' 三个值就好了。C 语言为数组提供了“下标”的方法来代表里面的每一个变量(又叫做元素), str[0] 表示第一个变量(元素), str[1] 表示第二个变量,依次类推。
然后,用“字符数组”的方式来存放字符串,还有一个问题需要解决:就是“字符串”长度该如何表示?由于我们可以声明一个 10 个元素的字符数组,仅仅存放 5 个字符的内容,如果没有办法知道,内容在第 6 个字符处结束,那么可能会让程序处理一些完全不需要的内容。C 语言为了解决这个问题,设计了一个直观但又坑爹的方法:
把字符变量中的数字 0,认为是字符串结束的符号。
数字 0 在 ASCII 编码表当中是 NUL 符号,被认为是“没有字符”的意思。
比如我们有一个字符数组 str[10] ,长度是 10 个字节,我们如果想存放字符串"abc",那么我们需要把 str[0] 写入 'a', str[1] 写入 'b', str[2] 写入 'c', str[4] 写入 0 。这样我们从字符数组 str 的下标 0 开始读内容,一直读到第一个内容为 0 的元素为止,就是这个字符串的全部内容了,如果数组第 4 个元素以后还有内容,也被看成是没有意义的数据,直接被忽略掉。printf() 的占位符 %s 就是根据这个规矩进行工作的。下面这个例子就说明 C 字符串是怎么使用的,所以程序中打印的结果,只有 “abc”,而后面的“defghi”都不会显示:
#include <stdio.h>
int main()
{
char str[10] = {'a', 'b', 'c', 0, 'd', 'e', 'f', 'g', 'h', 'i'};
printf("str: %s ptr: %p\n", str, str);
}
运行结果:str: abc ptr: 0000004C584FFECE
上面说了 C 字符串的方案,但是和“指针”又有什么关系呢?是不是作者逐渐忘记了标题?——并不是,在上面的 printf() 代码中,有一个 %p 的占位符,打印的是字符数组变量 str,可以看到打印出来一个地址信息。实际上一个数组变量,如果不带下标的话,就是这个数组的第一个元素的指针。
之所以 C 语言如此设计,正是因为指针往往用来操作一个数组。如果我们有两块代码,都需要使用同一个数组,那么使用这个数组的第一个元素的地址,比起把数组复制来复制去要效率搞的多。下面我们可以看一下数组的写法,和指针的写法是如何对应的。
#include <stdio.h>
int main()
{
char *str = "abc";
printf("str: %s ptr: %p\n", str, str);
for (int i = 0; i < 4; i++)
{
printf("str[%d]: %c|%c\n", i, str[i], *(str+i));
}
}
str: abc ptr: 00007FF78EC07320
str[0]: a|a
str[1]: b|b
str[2]: c|c
str[3]: |
这段程序的 char* str = "abc"; 这一行,其中 "abc" 定义了一个字符数组,内容是 {'a', 'b', 'c', 0} ,而 char* str 的指针内容,指向了 "abc" 这个字符数组的第一个元素。
程序后面的 for 循环,运行了四次,每次都通过下标操作,显示了 str[0]...str[1] 的内容,这里可以看到,就算 str 不是一个字符数组变量,但还是能通过下标 [i] 的写法来读取数组的元素内容。
而后面的 *(str+i) 这段,表示的是:用 str 的值(地址)加上 i,这样就指向了数组第 i 个元素的内存,然后用 * 解引用操作符读取了对应内存的值。上面的这个过程,其实和用下标的写法 str[i] 是等价的。C 语言中的所有数组的功能,是可以使用指针来完成所有操作的。
C 语言的变量就是一块内存,其实数组也是一块内存,数组的长度表示这块内存可以分成多少段(元素)来访问,数组的类型表示每段(元素)的长度是多少。
评论区
共 16 条评论热门最新