目录

前文列表

程序编译流程与 GCC 编译器

C 语言编程 — 基本语法

C 语言编程 — 基本数据类型

C 语言编程 — 变量与常量

C 语言编程 — 运算符

C 语言编程 — 逻辑控制语句

C 语言编程 — 函数

指针

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

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

指令是 C/C++ 编程最大的特色,通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。我们之所以需要指针,主要是由 C 语言中函数的工作方式决定的。C 语言函数的参数全部是通过值传递的。也就是说,传递给函数的实际是实参的拷贝。对于 int、long、char 此类基本数据类型以及用户自定义的结构体数据类型而言是成立的。这种方式适用于绝大多数情况,但也会偶尔出现问题:

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

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

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

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

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

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

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

引入了指针之后,C/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

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

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

声明指针

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 值是一个良好的编程习惯,称为空指针。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 为空,则完成 */

指针的算术运算

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 程序员的程序中指针后面的星星数 * 作为其水平的评分。

初级水平的人写的程序可能只会用到像 char* 或是奇怪的 int* 等一级指针,所以他们被称为一星程序员。而大多数中级的程序员则会用到诸如 lval** 这类的二级指针,所以他们被称为二星程序员。但据说能用三级指针的就真的很少见了,你可能会在一些伟大的作品中见到,这些代码的妙处凡夫俗子自然也是体会不到的。果真如此,三星程序员这个称号真是极大的赞誉了。

但据我所知,还没有人用到过四级指针。

C 语言编程 — 高级数据类型 — 指针的更多相关文章

  1. R语言编程艺术# 数据类型向量(vector)

    R语言最基本的数据类型-向量(vector) 1.插入向量元素,同一向量中的所有的元素必须是相同的模式(数据类型),如整型.数值型(浮点数).字符型(字符串).逻辑型.复数型等.查看变量的类型可以用t ...

  2. C语言编程中函数指针的定义及使用

    C语言中函数指针的定义: typedef int (*funcPtr)(int, int)表示定义了一个函数指针funcPtr,这个函数指针只能指向如下: int add(int, int).int ...

  3. C语言语法笔记 – 高级用法 指针数组 指针的指针 二维数组指针 结构体指针 链表 | IT宅.com

    原文:C语言语法笔记 – 高级用法 指针数组 指针的指针 二维数组指针 结构体指针 链表 | IT宅.com C语言语法笔记 – 高级用法 指针数组 指针的指针 二维数组指针 结构体指针 链表 | I ...

  4. 计算机专业C语言编程学习重点:指针化难为易

    C语言是面向过程的,而C++是面向对象的 C和C++的区别: C是一个结构化语言,它的重点在于算法和数据结构.C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现 ...

  5. C语言编程入门之--第四章C语言基本数据类型

      导读:C语言程序中经常涉及一些数学计算,所以要熟悉其基本的数据类型.数据类型学习起来比较枯燥,不过结合之前的内存概念,以及本节的字节概念,相信数据类型也就不难理解了.本章从二进制的基本概念开始,然 ...

  6. C语言中数组与指针的异同之处!你不知道的编程奥秘~

    C语言的数组和指针一直是两个容易混淆的东西,当初在学习的时候,也许为了通过考试会对指针和数组的一些考点进行突击,但是很多极其细节的东西也许并不是那么清楚.本篇侧重点在于分析数组与指针的关系,什么时候数 ...

  7. 华为C语言编程规范

    DKBA华为技术有限公司内部技术规范DKBA 2826-2011.5C语言编程规范2011年5月9日发布 2011年5月9日实施华为技术有限公司Huawei Technologies Co., Ltd ...

  8. linux 操作系统下c语言编程入门

    2)Linux程序设计入门--进程介绍 3)Linux程序设计入门--文件操作 4)Linux程序设计入门--时间概念 5)Linux程序设计入门--信号处理 6)Linux程序设计入门--消息管理  ...

  9. Go 语言的基本数据类型

    Go 语言的基本数据类型 0)变量声明 var 变量名字 类型 = 表达式 例: 其中“类型”或“= 表达式”两个部分可以省略其中的一个. 1)根据初始化表达式来推导类型信息 2)默认值初始化为0. ...

  10. 第二章 C语言编程实践

    上章回顾 宏定义特点和注意细节 条件编译特点和主要用处 文件包含的路径查询规则 C语言扩展宏定义的用法 第二章 第二章 C语言编程实践 C语言编程实践 预习检查 异或的运算符是什么 宏定义最主要的特点 ...

随机推荐

  1. OpenHarmony 3.1 Release版本关键特性解析——OpenHarmony新音视频引擎——HiStreamer

    OpenAtom OpenHarmony(以下简称"OpenHarmony")是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目,目标是面向全场景 ...

  2. OpenHarmony 3.1 Release版本关键特性解析——ArkUI框架又有哪些新增能力?

     ArkUI 是一套 UI 开发框架,它提供了开发者进行应用 UI 开发时所必须的能力.随着 OpenAtom OpenHarmony(以下简称"OpenHarmony") 3.1 ...

  3. Seaborn风格设置

    官方网站:seaborn: statistical data visualization - seaborn 0.11.2 documentation (pydata.org) Seaborn是基于m ...

  4. 上新啦!KIT!

    上新啦!KIT!近期KIT上新榜单请查收~ 商业推广深度转化事件回传助力用户精细运营,健康数据开放提升运动健康服务体验.手语服务新增非手控部分-- 更多功能请点击 了解更多详情>> 访问华 ...

  5. K8S 性能优化-K8S Node 参数调优

    前言 K8S 性能优化系列文章,本文为第四篇:Kubernetes Node 性能优化参数最佳实践. 系列文章: <K8S 性能优化 - OS sysctl 调优> <K8S 性能优 ...

  6. 报名开启 | HarmonyOS第一课“营”在暑期系列直播

    <HarmonyOS第一课>2023年再次启航! 特邀HarmonyOS布道师云集华为开发者联盟直播间 聚焦HarmonyOS 4版本新特性 邀您一同学习赢好礼! 你准备好了吗? ↓↓↓预 ...

  7. 最后一站qsnctfwp

    题目附件 图片一: 图片二: 根据图片一判断出位置为南昌市,地铁线路为4号线 根据题目名判断出搜索范围为白马山站或鱼尾洲站 通过百度地图全景地图查看两站环境,发现白马山站以工业区为主,鱼尾洲站以住宅区 ...

  8. c# 后端与前端时间戳的转换

    C# DateTime与时间戳转换 C# DateTime与时间戳的相互转换,包括JavaScript时间戳和Unix的时间戳. 1. 什么是时间戳 首先要清楚JavaScript与Unix的时间戳的 ...

  9. SELECT...FROM 表 a,( SELECT...FROM...WHERE...) tc...的一些注意以及多字段之间的模糊查询

    将sql查询结果作为一个表来查询的时候的一些注意事项 因为工作,发现了这种sql的写法,但是有的时候感觉并不是自己想要的结果,自己试着玩了属于是 简单来说,这个查询并不是拼接结果的,而是将结果按照一个 ...

  10. 力扣570(MySQL)-至少有5名直接下属的经理(简单)

    题目: Employee 表包含所有员工和他们的经理.每个员工都有一个 Id,并且还有一列是经理的 Id. 给定 Employee 表,请编写一个SQL查询来查找至少有5名直接下属的经理.对于上表,您 ...