C 语言中的指针与数组

栏目: C · 发布时间: 5年前

内容简介:本文主要总结了《The C Programming Language》和谭浩强主编的《C 程序设计》教材中指针和数组相关章节的内容。在 C 语言中,指针与数组之间有着非常密切的关系,一般来说,通过数组下标能完成的任何操作都可以通过指针来实现。本文将介绍指针与数组的概念和关系,以及一些相关的问题。目录如下:在 C 语言中,数组用于表示

本文主要总结了《The C Programming Language》和谭浩强主编的《C 程序设计》教材中指针和数组相关章节的内容。

在 C 语言中,指针与数组之间有着非常密切的关系,一般来说,通过数组下标能完成的任何操作都可以通过指针来实现。本文将介绍指针与数组的概念和关系,以及一些相关的问题。目录如下:

  • 数组
  • 指针
  • 指针与数组的关系
  • 字符串与数组
  • 字符串与指针
  • 指针常量与常量指针
  • 指针函数与函数指针
  • 指针数组与指向指针的指针
  • 空指针与野指针

数组

在 C 语言中,数组用于表示 相同类型的有序数据 的集合,定义方式如下:

类型名 数组名[常量表达式];

例如:

int a[10];

它表示定义了一个整型数组,数组名为 a ,该数组中有 10 个元素。换句话说,它定义了一个由 10 个元素组成的集合,这 10 个元素存储在相邻的内存区域中,名字分别 a[0]a[1] 、…、 a[9] ,如下图所示:

C 语言中的指针与数组

其中, a[i] 表示该数组中的第 i 个元素(i 从 0 开始计数)。

此外,给数组元素进行初始化有如下几条规则:

  • 在定义数组时对数组元素赋予初值,例如:
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  • 也可以只给一部分元素赋值,例如:
int a[10] = {0, 1, 2, 3, 4};

表示定义 a 数组有 10 个元素,并给前 5 个元素赋初值,后 5 个元素值默认为 0。但是我们无法跳着给某些元素赋值,例如 int a[5] = {,,3,4,5}; 是错误的写法。

  • 给数组中的元素全部赋予相同的某一个值,例如:
int a[10] = {2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
int a[10] = {2}; // 与上面等价
  • 给数组赋初值时,如果数据的个数已经确定,则可以不指定数组的长度:
int a[5] = {1, 2, 3, 4, 5}; // 可以写成如下形式
int a[] = {1, 2, 3, 4, 5};  // 声明时省略了数组的长度

上述第二种写法,花括号中有 5 个数,编译器在编译时会根据此自动定义 a 数组的长度为 5。但如果要定义的数组的长度与提供的初值的个数不同,则数组长度不能忽略。例如,想定义数组长度为 10,就不能省略数组长度的定义,而必须写成: int a[10] = {1, 2, 3, 4, 5}; ,表示初始化前五个元素,而后五个元素为 0。

指针

指针是一种保存变量地址的变量,定义方式如下:

类型名 * 指针变量名;

在程序中定义了一个变量,在编译时,系统会给这个变量分配内存单元。编译系统根据程序中定义的变量类型,分配一定长度的内存空间(不同类型的长度不同,一般字符类型为 1 个字节,整型为 2 个或 4 个字节等)。在内存区的每一个存储单元都有一个编号,这就是 “地址” 的概念,而指针变量就是用于存放“变量地址”的变量。

如下图所示,如果变量 c 的类型为 char ,在内存中存放的位置为图中的位置,我们可以定义一个指针变量 p,指向 c 的存储位置。

char * p = &c;

C 语言中的指针与数组

指针主要有两个运算符:

&
*

例如上述例子中, &c 表示变量 c 的地址, *p 表示指针变量 p 所指向的存储单元的内容(即 p 所指向的变量 c 的值)。

此外,在定义指针时声明的类型,表示该指针所指向的地址存放的内容的数据类型。而所有指针变量自身的类型都为整型,其所占的大小(字节数)在不同位数的操作系统中不一样。

思考一个问题,既然指针变量是用来存放地址的(且它自身的类型都为整型),那么好像只需要指定其为“指针型变量”即可,为什么定义指针时还要声明其指向内容的类型呢?

如前面所述,不同类型的数据在内存中所占的字节数是不同的,而对于指针的“移动”或“加减运算”,例如“使指针移动 1 个位置”或者“使指针值加 1”,这里的 “1” 代表什么呢?

“指针加 1”,表示与指针所指向的数据相邻的下一个数据的地址。举个例子,如果指针是指向一个整型变量(假设为 2 个字节),那么“使指针移动 1 个位置”意味着移动 2 个字节,“使指针值加 1” 意味着使地址值加 2 个字节。而如果指针是指向一个浮点型变量(假设为 4 个字节),则增加的不是 2 而是 4 个字节了。因此必须指定指针变量所指向的变量的类型,即 “基类型” ,这样才能准确地对指针进行相关位移操作。

最后需要注意的是, 一个指针变量只能指向同一个类型的变量 ,即不能把声明为指向字符型的指针指向整型等其它类型的变量。

指针与数组的关系

在第一小节中,声明了一个数组 int a[10]; ,假设这里我们又定义了一个指针变量 pa 如下:

int *pa;

则说明 pa 是一个指向整型数据的指针,那么赋值语句:

pa = &a[0];

表示将指针 pa 指向数组 a 的第 0 个元素,也就是说, pa 的值为数组元素 a[0] 的地址,如下图:

C 语言中的指针与数组

那么赋值语句 int x = *pa; 表示将把数组元素 a[0] 中的值复制到变量 x 中,与 int x = a[0]; 是等价的。

如果 pa 指向数组中的某个特定元素,那么,根据指针运算的定义, pa+1 将指向下一个元素, pa+i 将指向 pa 所指向的数组元素之后的第 i 个元素,而 pa-i 将指向 pa 所指向的数组元素之前的第 i 个元素。

因此,如果指针 pa 指向 a[0] ,那么 *(pa+1) 引用的是数组元素 a[1] 的内容, pa+i 是数组元素 a[i] 的地址, *(pa+i) 引用的是数组元素 a[i] 的内容,如下图所示:

C 语言中的指针与数组

无论数组 a 中的元素类型或者数组长度是什么,上面结论都成立。“指针加 1”就意味着, pa+1 表示 pa 所指向的元素的下一个元素。

数组下标和指针运算之间具有密切的对应关系。 C 语言规定,数组名代表数组中首个元素(即序号为 0 的元素)的地址

所以,执行赋值语句 pa = &a[0]; 后, paa 具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址,因此, pa = &a[0]; 也可以写成 pa = a;

对数组元素 a[i] 的引用也可以写成 *(a+i) 的形式。实际上,在编译过程中,编译器也是先把 a[i] 转换成 *(a+i) 这种指针表示形式然后再进行求值,所以这两种形式是 等价的

当我们对 a[i]*(a+i) 分别进行取址运算,可以知道 &a[i]& *(a+i) (简化为 a+i )也是相同的, a+i 表示 a 之后第 i 个元素的地址。

相应的,如果 pa 是一个指针,那么,在表达式中也可以在它的后面添加下标。 pa[i]*(pa+i) 是等价的。简而言之,一个通过(数组和下标)实现的表达式可以等价地通过(指针和偏移量)实现。

最后,需要注意指针和数组名两者的一个 不同之处

指针是一个变量,因此对于赋值语句 pa = a 和自增运算 pa++ 都是合法的,但是数组名不是变量,所以类似 a = pa 或者 a++ 等语句是非法的。

以上,希望对你理解指针和数组的关系能有所帮助。

下面我们将继续介绍与它们两个相关的其他一些知识点。

字符串与数组

在 C 语言中,有一种基本数据类型叫“字符型数据”,用 char 表示。

  • 字符常量

用单引号括起来的一个字符,例如 ‘a’、’A’、’5’、’?’、’\n’、’\0’ 等都是字符常量,总共有 128 个符号(其中有 32 个符号是不可显示的控制符)。

  • 字符变量

字符型变量用来存放字符常量,但 它只能存放一个字符 ,不要以为在一个字符变量中可以存放一个字符串(包含若干个字符)。字符变量的定义如下:

char c1, c2;
c1 = 'a';
c2 = 97; // 没有看错,这是合法的,c2 变量此时也存放字符 'a'

在所有的编译系统中都规定以一个字节来存放一个字符,或者说,一个字符变量在内存中占一个字节。

  • 字符串常量

或许你已经发现了,在 C 语言中,基本数据类型并没有“字符串”类型,因此只能通过“字符数组”的方式来存放字符串。字符串常量是通过双引号括起来的字符序列。例如:”hello, world!”、”CHINA” 等等。

注意:不要将字符常量和字符串常量混淆,’a’ 是字符常量,”a” 是字符串常量,两者是不同的,更不能把一个字符串常量赋给一个字符变量。

char c;
c = 'a'; // 合法
c = "a"; // 错误的
c = "CHINA"; // 也是错误的

字符串常量是一个字符数组。C 语言中又规定,在每一个字符串常量的结尾加一个“字符串结束标志” \0 ,以便系统据此判断字符串是否结束。例如,有一个字符串常量 “hello”,它在内存中是这样存储的:

C 语言中的指针与数组

它占的内存单元不是 5 个字符,而是 6 个字符,最后一个字符为 \0 。所以字符串常量占据的存储单元数比其字面量(双引号内的字符数)大 1。不过用 C 语言的 strlen 函数对一个字符串取长度,获得的数值并没有包括终止符 \0

  • 字符串变量(字符数组)

因为没有“字符串类型”,所以在 C 语言中,并没有真正意义上的“字符串变量”!本节我们来介绍一下字符数组的概念及与字符串的关系。

用来存放字符型数据的数组称为“字符数组”,字符数组中的每一个元素存放一个字符。

如前面所述,C 语言中是将字符串作为字符数组来处理的。所以,对于一个字符数组,如果其结尾为空字符 \0 ,那么就可以把它视为一个“字符串变量”(并不严谨),例如:

char str[6] = {'h', 'e', 'l', 'l', 'o', '\0'};

注:对于一个字符数组不管有多长,从头开始遍历,一旦遇到结束空字符 \0 就表示字符串结束, \0 前面的字符组成字符串, \0 后面的字符元素将被忽略。所以程序一般都是通过靠检测 \0 的位置来判断字符串是否结束,而不是根据字符数组的长度来决定字符串的长度。

所以有了结束标志 \0 后,字符数组的长度就显得不那么重要了。当然了,字符串实际长度加上 \0 的总长度是不能超过存放它的字符数组的长度的。

另外,我们也可以使用字符串常量来初始化字符数组:

char str[] = {"hello"};

此时,系统会自动在字符串常量的末尾加上一个结束空字符 \0 。也可以省略花括号,直接写成:

char str[] = "hello";

上述两种写法与下面的初始化写法等价:

char str[] = {'h', 'e', 'l', 'l', 'o', '\0'};

但与下面的写法是不等价:

char str[] = {'h', 'e', 'l', 'l', 'o'};

前者的长度为 6,而后者的长度为 5。

需要说明的是,对于字符数组,并不要求它的最后一个字符为 \0 ,甚至都可以不包含 \0 。但如果用字符数组来存储 字符串常量 ,则必须在末尾有 \0 结束标示符,通常系统会自动加上,也可以人为添加。

字符串与指针

前面一节讲了可以用字符数组来表示和存储字符串,而数组又与指针有着密切的联系。所以,也可以用指针来表示和管理字符串。

对于如下定义:

char str[] = "hello";

我们知道 str 是数组名,它代表着字符数组的首个元素的地址。因此我们可以定义一个字符指针指向它:

char *pStr = &str[0];

C 语言中的指针与数组

根据前面讨论,此时 pStr = str 。因此,我们可以不定义字符数组,而直接定义一个字符指针,用字符指针指向字符串中的字符,如下:

char *pStr = "hello";

其中, pStr 指向字符 ‘h’ 的地址, *pStr 的值,即 pStr[0] ,为 ‘h’, pStr+1 指向字符 ‘e’ 的地址; *(pStr+1) 的值,即 pStr[1] ,为 ‘e’,依次类推,最后一个字符为结束空字符 \0

这里, pStr 只是一个字符型指针变量,它指向字符串 “hello” 的第一个字符的地址(即:存放字符串常量 “hello” 的字符数组的首个元素的地址)。而 不能理解为 :“pStr 是一个字符串变量,在定义时把 “hello” 这几个字符赋给该字符串变量。”。

最后,在 C 语言中,对字符和字符串的输出格式符分别为 %c%s ,例如:

char c = 'a';
char *str = "hello, world!";
printf("%c", c);
printf("%s", str);

指针常量与常量指针

  • 指针 _常量_

指针本身是一个常量,它指向的地址不可以发生变化,但指向的地址的内容可以变化,声明方式:

int * const p;
  • 常量 _指针_

指向常量的指针,也称为 常指针 ,即指针指向的地址对应的值不可变,但指针可以指向其它(常量)地址,声明方式:

int const * p;
const int * p;
  • 指向常量的常指针
const int * const p;

指针函数与函数指针

  • 指针 _函数_

它是一个函数,即返回指针值的函数。对于一个函数,可以返回一个整型值、字符值、实型值等,也可以返回指针类型的数据,即地址,声明形式如下:

类型名 * 函数名(参数列表)

示例:

int * func(int x, int y);

此处, func 为函数名,调用它后将得到一个指向整型数据的指针(地址)。

  • 函数 _指针_

它是一个指针,即指向函数的指针,声明形式如下:

类型名 (* 指针变量名)(函数参数列表)

示例:

int a, b;
int max(int, int);   // 声明一个函数 max(假设已在其它地方实现)
int (* p)(int, int); // 声明一个函数指针 *p
p = max;             // 把函数名 max 赋值给函数指针 *p
a = max(1, 2);       // 通过函数名调用
b = (* p)(1, 2);     // 通过函数指针调用

与数组名代表数组首个元素地址类似, 函数名代表该函数的入口地址 ,赋值语句 p = max; 的作用是将函数 max 的入口地址赋给指针变量 p。

此时,p 和 max 都指向函数的开头,调用 * p 就是调用 max 函数。

因此,函数的调用可以通过函数名调用,也可以通过函数指针调用,上述 a = max(1, 2);b = (* p)(1, 2) 本质上是一样的。

另外,需要说明的是, int (* p)(int, int); 表示定义了一个指向函数的指针变量 p,它并不是就固定指向哪一个函数的,而只是表示定义了这样一个类型的变量,它是专门用来存放函数的入口地址的。在程序中把另一个函数(该函数的返回值应该是整形,且有两个整形参数)的地址赋给它,它就指向这一个函数。也就是,一个函数指针变量可以先后指向同类型的不同函数。

指针数组与指向指针的指针

  • 指针数组

对于一个数组,如果其元素均为指针类型的数据,称其为“指针数组”,也就是说,指针数组中的每一个元素都相当于一个指针变量,声明方式如下:

类型名 * 数组名[数组长度];

例如: int * p[4]; 表示声明一个数组 p ,它有 4 个元素,每个元素的类型为 int * 指针类型,即每个元素都可指向一个整型变量。

注意这里不要写成 int (* p)[4] ,它表示一个指向一维数组(数组长度为 4)的指针变量。

指针数组的使用场景:比较适合用于存放若干个字符串(也可理解为是“字符串数组”),使字符串处理更加方便灵活。例如:

char * names[] = {"Li Lei", "Han Meimei", "Kang Zubin"};

指针数组另一个重要作用是:作为 main 函数的形式参数,如下:

int main (int argc, char * argv[]);
  • 指向指针的指针

在理解了指针数组的概念的基础上,下面介绍指向指针数据的指针变量,简称为“指向指针的指针”。

例如前面定义的指针数组:

char * names[] = {"Li Lei", "Han Meimei", "Kang Zubin"};

它的示意图如下所示:

C 语言中的指针与数组

这里, names 是一个指针数组,它的每一个元素都是一个指针类型的数据(值为地址),指向某一个字符串常量(字符数组),而数组中每个元素都有相应自己的地址。

根据前面定义,数组名 names 代表该指针数组首个元素的地址, names+i 是元素 name[i] 的地址。所以, 指针数组的数组名,本身就是一个指向指针的指针。

此外,我们也可以定义一个指针变量 p,让它指向指针数组的元素(而数组的元素也是指针),那么 p 就是一个指向指针型数据的指针变量。

char ** p = &names[0]; // 等价于
char ** p = names;

上述 p 前面有两个 号,它相当于 ` ( p) ,其中 p` 是指针变量的声明形式,它表示定义了一个指向字符数据的指针变量,现在在它前面又加了一个 * 号,表示指针变量 p 此时是指向“一个字符指针变量”的,即 p 是一个“指向指针的指针”。(有点绕,可慢慢理解)

接下来对双指针 p 的相关操作就相当于是对指针数组 names 的操作,我们不再赘述。

空指针与野指针

  • void 指针类型

ANSI 标准增加了一种 void 指针类型,即可定义一个指针变量,但它不指定它是指向哪一种类型数据的。

它可以用来指向一个抽象的类型的数据,在将它的值赋给另一个指针变量时,要进行强制类型转换使之适合于被赋值的变量的类型,例如:

char *p1;
void *p2;
// ...
p1 = (char *)p2;

同样可以用 (void *)p1 将 p1 的值转成 void * 类型,例如: p2 = (void *)p1; ,也可以将一个函数的返回值定义一个为 void * 类型,例如:

void * func(char ch1, char ch2);

表示函数 func 返回的是一个地址,它指向“空类型”,如果需要引用此地址,需要根据情况对之进行类型转换,例如 char *p1 = (char *)func(ch1, ch2);

  • 空指针

上述介绍的 void * 表示空指针类型,它可以转换为其他指向类型。而在 C 语言中又双叒叕定义, (void *)0 表示的空指针常量。

如果 p 是一个指针变量,当它的值为空指针常量时,即 p = (void *)0 ,则此时称 p 为空指针,表示 p 指向一个空地址,即地址 0 (不是常数 0),或者说指向 NULL

  • 野指针

根据定义,野指针是指向一个已删除的对象或未申请访问受限内存区域的指针,也就是它指向了不合法的内存区域。野指针也称作“迷途指针”或者“悬空指针”。

对于一个指针 p,当它所指向的对象被释放或者收回,但是对该指针没有作任何的修改(比如没有置为 NULL ),以至于该指针仍旧指向已经回收的内存地址,此时 p 就成为一个野指针。后续如果再对指针 p 进行操作,可能就会造成程序崩溃或产生不可预知的结果。

题外话

之前在网上看到这样一条段子:

C 语言中的指针与数组

我当时简单分析了一下,野指针一般指向一个已被删除的对象,说明它曾经有过对象,现在分手了而已,不至于太惨。如果骂:“你 TM 就是一个没有对象的空指针!”,可能会更惨一些,:ghost:

总结

本文简要介绍了 C 语言中指针与数组的概念、关系,以及与它们相关的一些知识点,如有不当之处,欢迎指出,更多细节强烈推荐阅读《The C Programming Language》和谭浩强主编的《C 程序设计》教材这两本书,你将会有更多收获。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Out of their Minds

Out of their Minds

Dennis Shasha、Cathy Lazere / Springer / 1998-07-02 / USD 16.00

This best-selling book is now available in an inexpensive softcover format. Imagine living during the Renaissance and being able to interview that eras greatest scientists about their inspirations, di......一起来看看 《Out of their Minds》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具