























我个人认为,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 语言的变量类型代表了一个特定的内存长度,字符串的长度是不固定的,所以没法给“字符串类型”一个长度设定。
那么 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 语言的变量就是一块内存,其实数组也是一块内存,数组的长度表示这块内存可以分成多少段(元素)来访问,数组的类型表示每段(元素)的长度是多少。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。