目录

文章目录

  • 目录
  • 前文列表
  • 指针
  • 声明一个指针变量
  • 使用指针
  • 空指针
  • 悬空指针
  • 野指针
  • 指针的算术运算
  • 指向指针的指针
  • 将指针作为实际参数传入函数
  • 从函数返回指针
  • 一个古老的笑话

前文列表

《程序编译流程与 GCC 编译器》
《C 语言编程 — 基本语法》
《C 语言编程 — 基本数据类型》
《C 语言编程 — 变量与常量》
《C 语言编程 — 运算符》
《C 语言编程 — 逻辑控制语句》
《C 语言编程 — 函数》

指针

C 语言是一门 值语义 编程语言,区别于 Python 的 引用语义,参数全部是通过值传递的。也就是说,传递给函数的实际是实参的拷贝。对于 int、long、char 此类基本数据类型以及用户自定义的结构体数据类型而言是成立的。这种方式适用于绝大多数情况,但也会偶尔出现问题:

  1. 如果我们有一个巨大结构体需要作为参数传递,则每次调用函数,就会对实参进行一次拷贝,这无疑是对性能和内存的浪费。
  2. 结构体的大小终究是有限且固定的,如果我们想向函数传递一组数据,而且数据的大小总是不固定的,例如:数组(包括字符串),结构体就明显的无能为力了。

为了解决这个问题,C 语言的开发者们想出了一个聪明的办法。他们把内存想象成一个巨大的字节(Byte)数组,每个字节都可以拥有一个全局的索引值(数据的首字节的索引作为整个数据的索引)。这有点像门牌号:第一个字节索引为 0,第二个字节索引为 1,等等。

在这种情况下,计算机中的所有数据,包括变量、结构体都有相应的索引值与之对应。所以,除了将数据本身拷贝到函数参数,我们还可以只拷贝数据的索引值。在函数内部则可以根据索引值找到需要的数据本身。我们将这个索引值称为地址,存储地址的变量称为指针。使用指针,函数可以修改指定位置的内存而无需进行拷贝。

因为计算机内存的大小是固定的,表示一个地址所需要的字节数也是固定的。但是地址指向的内存的字节数是可以变化的。这就意味着,我们可以创建一个大小可变的数据结构,并将其指针传入函数,对其进行读取及修改。

所以,指针的本质只是一个数字而已。是内存中的一块数据的开始字节的索引值。指针的类型用来提示程序员和编译器指针指向的是一块什么样的数据,占多少个字节等。

要清晰区分上述绕口令一般的关系,就要弄清楚指针的本质:

  • 指针:一个变量的地址
  • 指针变量:一个存放其他变量地址的变量

引入了指针之后,C 语言就有了两种访问变量数据值的方式:

  1. 通过变量名来直接访问
  2. 通过内存地址块的指针来间接访问

指针运算相关的运算符有以下两种:

  • 取地址运算符 &:获取变量所占用的存储空间的地址,为单目运算符(只有一个操作数)。
  • 取值运算符 *:也称解引用,获取指针变量所指向的存储空间内的数据值。取值运算符的操作数只能是一个指针变量。

注意:要获取结构体指针的某个字段的值,需要使用 -> 操作符。

NOTE:取值运算和取地址运算互为逆运算。

int a = 3;
int b;
int * p = NULL;p = &a;
b = *p;

前门的文章中提到过,变量 = 变量名 + 变量值,而且 C 语言是值语义的,有别于 Python 的引用语义。所以变量名就是变量在内存中的入口地址,变量值就是变量在内存空间中实际的数值。在程序中可以使用取地址运算符 & 来获取变量的入口地址。如下:

#include <stdio.h>int main(){int var1;char var2[10] = {10, 9, 8, 7};printf("var1: %p\n", &var1);printf("var2-0: %p\n", &var2[0]);printf("var2-1: %p\n", &var2[1]);printf("var2-2: %p\n", &var2[2]);return 0;
}

运行:

$ ./main
var1: 0x7ffc857d59bc
var2-0: 0x7ffc857d59b0
var2-1: 0x7ffc857d59b1
var2-2: 0x7ffc857d59b2

可见,不同变量之间的内存空间很可能不是连续的,但同一数值内的顺序元素的空间是连续的。

指针的本质也是一个变量,其变量值是另一个变量的入口地址,即一个变量存储了另一个变量的内存地址,是为指针。数组名本质上也是一个指针,并且是常量指针,记录了数组的入口地址,且不能够被修改。

指针是 C 语言的核心和灵魂,也是洪水猛兽般存在。虽然指针的概念非常简单,但是用起来却变幻多端,神秘莫测,这使得指针看上去比实际要可怕得多。指针类型是基本数据类型的变体,只需基本数据类型的后面添加 * 后缀即可:

  • int i:整型变量
  • int *p:整型指针变量
  • int a[n]:整型数组变量,具有 n 个整型数值元素
  • int *p[n]:整型指针数组变量,具有 n 个指向整型数值的指针元素
  • int (*p)[n]:数组指针,指向整型数组的指针变量 p
  • int func():返回整型数值的函数
  • int *func():返回整型指针的指针函数
  • int (*p)():函数指针,指向函数的指针
  • int **p:指向整型指针的指针变量

声明一个指针变量

type *var-name;
  • type 是指针的基类型,是一个有效的 C 数据类型
  • var-name 是指针变量的名称
  • * 用来声明指针类型变量
int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;     /* 一个字符型的指针 */

需要注意的是,不管指针的基类型是什么,指针变量的数值的类型都是一个代表内存地址的十六进制数。指针的基类表示了指针所指向的变量或常量的数据类型。

使用指针

使用指针时会频繁进行以下几个操作:

  1. 定义一个指针变量
  2. 把变量的内存地址赋值给指针
  3. 访问指针变量存储的数值(内存地址)
#include <stdio.h>int main ()
{int  var = 20;   /* 实际变量的声明 */int  *ip;        /* 指针变量的声明 */ip = &var;  /* 在指针变量中存储 var 的地址 */printf("Address of var variable: %p\n", &var  );/* 在指针变量中存储的地址 */printf("Address stored in ip variable: %p\n", ip );/* 使用指针访问值 */printf("Value of *ip variable: %d\n", *ip );return 0;
}

运行:

Address of var variable: bffd8b3c
Address stored in ip variable: bffd8b3c
Value of *ip variable: 20

空指针

在声明指令变量的时候,如果没有确切的内存地址可以赋值,那么为指针变量赋一个 NULL 值是一个良好的编程习惯,称为空指针。NULL 指针是一个定义在标准库中的值为零的常量。

#include <stdio.h>int main ()
{int  *ptr = NULL;printf("ptr 的地址是 %p\n", ptr);return 0;
}

运行

ptr 的地址是 0x0

在大多数的操作系统上,不允许程序访问地址为 0x0 的内存,因为该内存是操作系统保留的。但按照惯例,如果指针变量的数值为 NULL 时,则假定它不指向任何东西。

判断一个空指针的方式:

if(ptr)     /* 如果 p 非空,则完成 */
if(!ptr)    /* 如果 p 为空,则完成 */

悬空指针

如果指针指向的内容被被释放了,但是指针变量依旧保存着这块内存的地址,该指针就是 “悬空指针”:

void *p = malloc(size);
assert(p);free(p);  // 现在 p 是悬空指针

如果我们再次对悬空指针进行释放,很可能会因为内存地址冲突,导致不可预知的错误,而且这种错误一旦发生,很难定位。

所以我们应该养成良好的编程习惯,杜绝悬空指针的出现:

void *p = malloc(size);
assert(p);free(p);
p = NULL;  // 避免悬空指针

这么做的好处是:一旦再次使用被释放的指针 p,就会立刻引发 “段错误”,我们马上就会引起注意并对其进行改正了。

野指针

悬空指针是指向被释放掉内存的指针,而野指针则是不确定其具体指向的指针,常见于未初始化的指针:

void *p;  // 此时 p 是野指针

因为野指针可能指向任意内存段,因此它可能会损坏正常的数据,也有可能引发其他未知错误。所以,野指针的危害性甚至比悬空指针还要严重。

我们在定义指针时,一般都要杜绝野指针的出现,即便在没有初始化数组的情况下也要使用 NULL 为指针变量进行初始化:

void *p = NULL;
void *data = malloc(size);

指针的算术运算

C 指针的本质是一个十六进制数值,所以可以对指针执行算术运算,可以对指针进行四种算术运算:++--+-

  • 指针的每一次递增,它会指向下一个元素的存储单元。
  • 指针的每一次递减,它会指向前一个元素的存储单元。
  • 指针在递增和递减时的步进(跳跃的字节数)取决于指针所指向的变量的数据类型,比如 int 就是 4 个字节。

我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。下面的程序递增变量指针,以便顺序访问数组中的每一个元素:

#include <stdio.h>const int MAX = 3;int main(){int var[] = {10, 100, 200};int i;int *ptr;/* 数组名就是一个指针,直接复制给指针类型变量 */ptr = var;for(i = 0; i < MAX; i++){printf("Address: var[%d] = %p\n", i, ptr);printf("Value: var[%d] = %d\n", i, *ptr);/* 移动到下一个位置 */ptr++;}return 0;
}

运行:

./main
Address: var[0] = 0x7ffe48f272d0
Value: var[0] = 10
Address: var[1] = 0x7ffe48f272d4
Value: var[1] = 100
Address: var[2] = 0x7ffe48f272d8
Value: var[2] = 200

可见,每递增一次,移动了 4 Byte。

同样地,对指针进行递减运算,即把值减去其数据类型的字节数,如下所示:

#include <stdio.h>const int MAX = 3;int main(){int var[] = {10, 100, 200};int i;int *ptr;/* 获得数组最后一个元素的指针,再复制给指令类型变量 */ptr = &var[MAX - 1];for(i = MAX; i > 0; i--){printf("Address: var[%d] = %p\n", i - 1, ptr);printf("Value: var[%d] = %d\n", i - 1, *ptr);/* 移动到下一个位置 */ptr--;}return 0;
}

运行:

./main
Address: var[2] = 0x7ffdbab78f88
Value: var[2] = 200
Address: var[1] = 0x7ffdbab78f84
Value: var[1] = 100
Address: var[0] = 0x7ffdbab78f80
Value: var[0] = 10

指针可以时要关系运算符进行比较,如 ==<>。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。下面的程序修改了上面的实例,只要变量指针所指向的地址小于或等于数组的最后一个元素的地址 &var[MAX - 1],则把变量指针进行递增:

#include <stdio.h>const int MAX = 3;int main ()
{int  var[] = {10, 100, 200};int  i, *ptr;/* 指针中第一个元素的地址 */ptr = var;i = 0;while ( ptr <= &var[MAX - 1] ){printf("Address of var[%d] = %x\n", i, ptr );printf("Value of var[%d] = %d\n", i, *ptr );/* 指向上一个位置 */ptr++;i++;}return 0;
}

指向指针的指针

指向指针的指针是一种多级间接寻址的实现,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际数值的内存位置。

一个指向指针的指针变量必须如下声明,在变量名前放置两个 * 号。例如,下面声明了一个指向 int 类型指针的指针:

int **var;

当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,如下面实例所示:

#include <stdio.h>int main ()
{int  var;int  *ptr;int  **pptr;var = 3000;/* 获取整型变量 var 的地址 */ptr = &var;/* 获取指向整型变量的指针变量 ptr 的地址 */pptr = &ptr;printf("Value of var = %d\n", var );printf("Value available at *ptr = %d\n", *ptr );printf("Value available at **pptr = %d\n", **pptr);return 0;
}

将指针作为实际参数传入函数

C 语言允许您传递指针给函数,只需要简单地声明函数参数为指针类型即可。

#include <stdio.h>
#include <time.h>void getSeconds(unsigned long *par);int main ()
{unsigned long sec;getSeconds(&sec);/* 输出实际值 */printf("Number of seconds: %ld\n", sec);return 0;
}void getSeconds(unsigned long *par)
{/* 获取当前的秒数 */*par = time(NULL);return;
}

从函数返回指针

类似地,C 语言允许从函数返回指针类型。只需要一个简单的函数声明:

int * myFunction(){}

需要注意的是,C 语言不支持在调用函数时返回局部变量的地址,除非定义局部变量为 static 变量。下面的函数,它会生成 10 个随机数,并使用表示指针的数组名(即第一个数组元素的地址)来返回它们:

#include <stdio.h>
#include <time.h>
#include <stdlib.h> /* 要生成和返回随机数的函数 */
int * getRandom( )
{static int r[10];int i;/* 设置种子 */srand((unsigned)time(NULL));for ( i = 0; i < 10; ++i){r[i] = rand();printf("%d\n", r[i] );}return r;
}int main ()
{/* 一个指向整数的指针 */int *p;int i;p = getRandom();for ( i = 0; i < 10; i++ ){printf("*(p + [%d]) : %d\n", i, *(p + i) );}return 0;
}

一个古老的笑话

这里有个古老的笑话,说是可以根据 C 程序员的程序中指针后面的星星数 * 作为其水平的评分。

C 语言编程 — 高级数据类型 — 指针相关推荐

  1. Go 语言编程 — 高级数据类型 — 指针

    目录 文章目录 目录 指针 空指针 双重指针 向函数传递指针参数 指针 一个指针变量指向了一个值的内存地址.类似于变量和常量,在使用指针前需要声明.定义一个指针变量. 声明一个指针变量,格式: var ...

  2. C 语言编程 — 高级数据类型 — void 类型

    目录 文章目录 目录 前文列表 void 类型 前文列表 <程序编译流程与 GCC 编译器> <C 语言编程 - 基本语法> <C 语言编程 - 基本数据类型> & ...

  3. C 语言编程 — 高级数据类型 — 字符串

    目录 文章目录 目录 前文列表 字符串 字符串拷贝 字符串比较 strcmp strncmp 前文列表 <程序编译流程与 GCC 编译器> <C 语言编程 - 基本语法> &l ...

  4. C 语言编程 — 高级数据类型 — 共用体

    目录 文章目录 目录 前文列表 共用体 定义共用体 访问共用体成员 前文列表 <程序编译流程与 GCC 编译器> <C 语言编程 - 基本语法> <C 语言编程 - 基本 ...

  5. C 语言编程 — 高级数据类型 — 结构体与位域

    目录 文章目录 目录 前文列表 结构体 定义结构体 初始化结构体变量 访问结构体成员 结构体的内存分布 将结构体作为实参传入函数 指向结构体变量的指针 位域 定义位域 使用位域结构体的成员 前文列表 ...

  6. C 语言编程 — 高级数据类型 — 枚举

    目录 文章目录 目录 前文列表 声明枚举类型 定义枚举类型的变量 枚举类型变量的枚举值 枚举在 switch 语句中的使用 将整型转换为枚举类型 前文列表 <程序编译流程与 GCC 编译器> ...

  7. C 语言编程 — 高级数据类型 — 数组

    目录 文章目录 目录 前文列表 数组 声明数组 初始化数据 访问数组元素 二维数组 指向数组的指针 将数组指针作为实参传入函数 从函数返回一个数组指针 指针数组 数组名和取数组首地址的区别 前文列表 ...

  8. Go 语言编程 — 高级数据类型 — 结构体

    目录 文章目录 目录 结构体 访问结构体成员 向函数传递结构体 结构体指针 结构体标签(Struct Tag) 结构体 Golang 中,结构体是由一系列具有相同类型或不同类型的数据构成的数据集合.与 ...

  9. Go 语言编程 — 高级数据类型 — Interface、多态、Duck Typing 与泛式编程

    目录 文章目录 目录 Golang 的接口 Interface 实例存储的是实现者的值 如何判断某个 Interface 实例的实际类型 Empty Interface Interface 与多态 I ...

最新文章

  1. oracle 让sys用户可以使用isqlplus
  2. 转:ASP自动解压RAR文件
  3. 使用conda报错:from conda.cli import main ModuleNotFoundError: No module named conda
  4. hive与hbase集成
  5. java中412是什么错_HTTP 412 错误 – 先决条件失败 (Precondition failed)
  6. 1024节日快乐~~~~
  7. ubuntu 16.04 安装 google浏览器
  8. linux 命令学习 —— 硬件外设管理(dmesg、lsusb)
  9. 前期拍摄注意的简要几点,总结了一哈,与大家分享!
  10. i9-10900K比9900K性能提升了多少?i9-10900K和i9-9900K区别对比评测
  11. 微信小程序:实现按钮点击事件
  12. 八爪鱼-自定义模式采集数据
  13. Lua调试:getinfo详解
  14. MATLAB 曲线拟合
  15. jQuery获取元素属性值为undefined
  16. VMware vCenter/vSphere/vSan/Esxi/7.0 lic许可
  17. 微信小程序自动检测新版本并静默更新,及热启动和冷启动
  18. 【渝粤题库】广东开放大学 计算机应用基础(专科) 形成性考核
  19. Linux 服务器上传下载文件到阿里网盘
  20. [Cortex-M3]-3-分散加载文件解析(.sct)

热门文章

  1. android h5使用缓存_程序员必须了解的之小程序 与 App 与 H5 之间的区别
  2. Linux下快速安装TensorFlow的教程
  3. 英特尔与Blueprint Reality共同打造混合现实视频制作工具
  4. JAVA实现斐波那契数列问题(《剑指offer》)
  5. 不用精子就能繁育后代,科学家只用1个卵细胞就培育出健康小鼠,来自上交医学院 | PNAS...
  6. 这个奇葩打字外设火了,一分钟500词比说话还快,直接被打字比赛禁用
  7. MetaHuman效果炸了!但如果只想到元宇宙,那格局有点小了
  8. AI发展进入2.0时代!英特尔在落地中总结4大经验、分享7个案例
  9. GitHub免费支持CI/CD了,开发测试部署高度自动化,支持各种语言,网友:第三方凉凉...
  10. 给GAN一句描述,它就能按要求画画,微软CVPR新研究 | 附PyTorch代码