内容简介:本文主要总结了《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]
,如下图所示:
其中, 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 的地址, *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]
的地址,如下图:
那么赋值语句 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]
的内容,如下图所示:
无论数组 a 中的元素类型或者数组长度是什么,上面结论都成立。“指针加 1”就意味着, pa+1
表示 pa
所指向的元素的下一个元素。
数组下标和指针运算之间具有密切的对应关系。 C 语言规定,数组名代表数组中首个元素(即序号为 0 的元素)的地址 。
所以,执行赋值语句 pa = &a[0];
后, pa
与 a
具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址,因此, 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”,它在内存中是这样存储的:
它占的内存单元不是 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];
根据前面讨论,此时 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"};
它的示意图如下所示:
这里, 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 进行操作,可能就会造成程序崩溃或产生不可预知的结果。
题外话
之前在网上看到这样一条段子:
我当时简单分析了一下,野指针一般指向一个已被删除的对象,说明它曾经有过对象,现在分手了而已,不至于太惨。如果骂:“你 TM 就是一个没有对象的空指针!”,可能会更惨一些,:ghost:
总结
本文简要介绍了 C 语言中指针与数组的概念、关系,以及与它们相关的一些知识点,如有不当之处,欢迎指出,更多细节强烈推荐阅读《The C Programming Language》和谭浩强主编的《C 程序设计》教材这两本书,你将会有更多收获。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- C语言指针数组和数组指针
- 将数组和矩阵传递给函数,作为C中指针的指针和指针
- P/Invoke以指针形式传递结构体内的数组
- 一个新细节,Go 1.17 将允许切片转换为数组指针
- 数据结构和算法面试题系列-C指针、数组和结构体
- NULL 指针、零指针、野指针
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Definitive Guide to Django
Adrian Holovaty、Jacob Kaplan-Moss / Apress / 2007-12-06 / CAD 45.14
Django, the Python-based equivalent to the Ruby on Rails web development framework, is presently one of the hottest topics in web development today. In The Definitive Guide to Django: Web Development ......一起来看看 《The Definitive Guide to Django》 这本书的介绍吧!