目录

前文列表

程序编译流程与 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. #数位dp,高精度#洛谷 2235 [HNOI2002]Kathy函数

    题目 分析 首先这个\(f\)函数其实求的是二进制下的回文数,简单证明一下 设\(n\)在二进制下的回文数为\(n'\),第一二条显然 第三条\(f(2n)=f(n)\Rightarrow \over ...

  2. org.xml.sax.SAXParseException; lineNumber: 11;

    org.xml.sax.SAXParseException; lineNumber: 11; 点击clean  然后再启动

  3. 【鸿蒙生态千帆起】HarmonyOS系统级地图与位置服务,赋能广大开发者

     在"与HarmonyOS同行,开放生态,共赢未来"为主题的HUAWEI Developer Day(简称HDD)沙龙中,Petal Maps为开发者们带来了在HarmonyOS下 ...

  4. 全面支持JS/eTS应用开发,DevEco Studio 3.0 Beta4新版本发布

    原文:https://mp.weixin.qq.com/s/j5Cl48ZxzEmnnpfoM0pKJg ,点击链接查看更多技术内容. HUAWEI DevEco Studio(后文简称DevEco ...

  5. 重学c#系列——linq(4) [三十]

    前言 简单介绍一下linq 查询表达式. 正文 上文其实已经介绍了查询表达式了. 但是呢,这里就介绍一些复杂一点的. 这里不会去介绍查询表达式,而是直接介绍一些复杂的. let 字句. static ...

  6. 重新点亮shell————文本搜索[九]

    前言 简单整理一下文本搜索. 正文 文本搜索需要学下面: 元字符 扩展元字符 文件的查找命令find 例子1: 例子2(通配符): 例子3(正则表达): 例子4(可以根据文件类型匹配): 例子5(找到 ...

  7. .gitignore 基础

    前言 gitignore文件有几种方式生成. 1.在创建版本库的时候生成. 2.自己手动添加: windows环境下,不能把文件直接改名为.gitignore,会提示你必须输入文件名.所以我们先新建一 ...

  8. mmcls/mmdet模型部署至 TorchServe

    mmcls/mmdet模型部署至 TorchServe 官方教程:模型部署至 TorchServe - MMClassification 0.23.2 文档 接口说明: serve/inference ...

  9. 2024-04-21:用go语言,给一棵根为1的树,每次询问子树颜色种类数。 假设节点总数为n,颜色总数为m, 每个节点的颜色,依次给出,整棵树以1节点做头, 有k次查询,询问某个节点为头的子树,一共

    2024-04-21:用go语言,给一棵根为1的树,每次询问子树颜色种类数. 假设节点总数为n,颜色总数为m, 每个节点的颜色,依次给出,整棵树以1节点做头, 有k次查询,询问某个节点为头的子树,一共 ...

  10. 【笔记】Java相关大杂烩①

    [笔记]Java相关大杂烩 Java 程序的执行流程是? *.java 文件-->*.class 文件-->类装载器-->字节码校验器-->解释器-->操作系统平台 Ja ...