【C语言速成】9. 数组和指针

数组

数组是保存一组连续的、相同类型的变量的一种容器,当有大量相同类型的数据需要处理时,手动的一个个创建变量、用变量非常麻烦,这时候就该考虑用数组了。

创建数组

数组的创建跟变量的创建差不多,但是数组名后边要跟一个中括号,里边写着它包含的元素的数量。

类型 数组名[元素数量];

例如,创建一个叫a的保存100个int变量的数组:

int a[100];

这样数组就创建完了。这时候它里边的元素还没有被初始化,也就是说现在数组的所有元素的值都是未知的。
使用没初始化的元素可能会造成意想不到的后果:程序可能会崩溃,也有可能会从元素里找出一个谁也想不出来的值,所以有时候有必要在创建数组时给数组里的元素赋初始值:

类型 数组名[元素数量n] = {元素1值, 元素2值, 元素3值, ... 元素n值};

比如:

int a[4] = {9, 8, 7, 6}; // 创建一个叫a的有4个元素的int数组,并依次把元素的值设定成9,8,7,6

如果大括号里没有各元素的初始值,或者初始值的数量达不到数组的元素个数,那么每个没初始化的元素将会被编译器根据类型进行默认初始化(比如int默认初始化后是0,float默认初始化后是0.000000,char默认初始化后是那个ASCII为0的空字符):

类型 数组名[元素数量] = {}; // 所有元素默认初始化
类型 数组名[元素数量] = {元素1值, 元素2值}; // 第一个和第二个元素按值初始化,剩下的元素默认初始化

使用数组

使用数组非常简单,数组名后边跟一个中括号,里边写第几个元素的编号,得到的就是第几个元素的值。

数组名[元素编号]

比如打印一个叫a的int数组里第一个元素的值,可以用:

printf("%d", a[0]);

需要注意的是,C语言的数组跟数学里的数列有些许不同:数列的第一个元素编号为1,而数组的第一个元素编号为0,例如数组a的第一个元素是a[0],第四个元素是a[3]
在对数组的元素进行操作时一定要检查元素编号是否在该数组定义的范围内。假设数组长度为n,那么它的元素编号范围就是0~n-1。这一点必须注意,C语言对于越过数组边界(操作的元素编号大于等于数组长度或者小于0)读取或写入数据的行为是不管的。这比操作没初始化的变量更危险:变量创建了但是不初始化,至少电脑给它分配的那部分还没初始化的内存是属于它的,而越界操作数组外的那些实际上不存在的元素,就是在操作不属于数组自己的内存。那些内存可能属于其它变量,也可能是程序正常运行所必须的关键数据,由此产生的后果比使用没初始化的变量更严重。

示例

输入一个整数n,依次打印1到n的所有整数
enter image description here

输入和输出

【你】6(回车)
【电脑】1 2 3 4 5 6 


指针

指针是一种数据类型,它保存一个内存地址和该地址对应数据的数据类型。

其实指针的本质就是一个带特殊属性的,能保存无符号整数的,能表示的数据范围足够大的数据类型(类似unsigned long,但是不会有人会闲的没事用指针类型直接保存和使用整数的)

内存地址、取地址符和解引用符

内存地址和指针的通俗解释

电脑运行时产生的数据保存在内存里,你程序的数据也不例外。内存可以被比喻成一个有好多房间的公寓,每个房间的门牌号就是一个内存地址。保存变量的值就像让变量入住公寓一样,分给它一个或者相邻的多个房间(有些能表示的数据范围大的变量比如long long占的空间多,就像它带的东西多一样,一个房间装不下,那就多分几个房间然后把房间之间的墙拆掉),分给变量的第一个房间的房间号就是这个变量的内存地址。
如果继续拿公寓举例,那么指针就像门钥匙一样,钥匙上写着它负责的房间的号码和这个房间里住的是什么变量。

变量创建和分配内存的流程

每当创建一个变量时,电脑都会找一段没人用的内存区域分给这个变量供它以后存取数据。假设int型的数据在某个机器上占用32比特(英文写成“bit”,8比特=1字节)的空间,那么当一个叫a的int变量创建时,电脑会首先看看内存条里还有哪段内存没人用,假如它发现内存地址第10000字节~第20000字节没人用,它就把第10000字节~第10004字节,总共这4个字节标记为变量a的内存区域,然后把a的内存地址记为10000供日后使用。

取地址符

把取地址符“&”加在一个变量名前面,就得到了这个变量的内存地址。假如有一个变量a,要获取它的内存地址只需要使用“&a”。

解引用符

解引用符“*”的用法跟取地址符差不多,但是只适用于加到指针变量的变量名前边。它用来“解引用”一个指针,得出它保存的内存地址里的那个数据的值。
假如int类型在某机器上占用32bit空间,有一个指针b保存的内存地址是10000,对应数据类型int,“*b”的结果就是在内存第10000字节~第10004字节的int变量的值。

指针的创建和赋值

创建一个指针,只规定它指向哪种数据类型,没有规定它指向哪个内存地址:

数据类型* 指针名; // 不初始化内存地址

创建一个指针,规定它指向的数据的类型,并且用某个数据的内存地址给它赋值:

数据类型* 指针名 = 内存地址; // 初始化内存地址

指针的使用

用上文提到的解引用符去解引用指针就可以得到它指向的数据的值。比如有这样一段代码:

int a = 123;
int* b = &a; // 创建指向int型数据的指针b,并把它的值初始化为a的内存地址
printf("%d", *b); // 解引用b,得到a的值并用printf输出

程序输出的数不是b的内存地址也不是a的内存地址,而是a的值“123”。这就是指针最常用的用法。

指针能解决什么问题

假设要实现一个无返回值的函数,它接受一个int参数并让这个参数变成自己的平方。按照之前的经验,这个函数应该写成这样:

void pf(int a)
{
  a = a * a;
}

然而如果你真的这样实现了这个函数,那么当调用函数的时候你就会发现这个函数貌似不起作用,变量被传给这个函数以后没有变成自己的平方,就像什么都没发生一样:
enter image description here
在这个程序里q被初始化为10,之后被传进pf函数。按理来说pf函数执行完后q应该变成100,但是运行这个程序时你会发现程序输出的仍然是10。这是因为:C语言里调用函数的时候传进去的变量不是你传的变量本身,而是一个复制出来的,值跟你传进去的变量的值一样的新变量。也就是说,在函数体内对参数进行操作时,实际上是对新变量进行操作。
那么如何解决这个问题呢?答案是:不传递变量的值,直接传递变量的内存地址,在函数体内用指向这个变量的指针进行操作就相当于直接对这个变量进行操作。把pf函数的参数类型由int改为指向int的指针,以后调用函数时传递变量的内存地址,就可以在函数体内对函数体外的变量进行操作了:
enter image description here
修正后的程序的运行结果是100,这就证明问题被解决了。

数组和指针的联系

在一些情况下,数组可以当成指针来用,指针也可以当成数组来用。

理论1

如果在程序中单独使用数组名,它就表示这个数组首元素的内存地址。
假设有这样一个数组:

int a[4] = {9,8,7,6};

那么据上文所述,可以创建一个int指针b,给它赋值为“a”,这样的结果应该会让b指向a的首元素,也就是a[0]

int* b = a;

为了验证这个结论,可以用printf函数输出解引用b得到的结果:
enter image description here
程序会输出“9”,这就证明上文的理论正确。

理论2

对指针进行加减会让指针指向的内存地址按数据类型进行加减。
假如int类型在某机器上占用32bit空间,数组a的首元素内存地址为第10000字节,根据上文的初始化语句,b将会指向内存第10000字节。如果给b加上一个整数n,b所指的内存地址将会向后移动32 / 8 * n = 4n字节,指向内存第10000+4n字节。
根据对数组的定义(数组的元素是在内存里连续线性保存的)以及这个理论,如果b++,那么b将会指向a的第二个元素,也就是a[1]。下面继续附加一行代码来验证这个理论: enter image description here
程序的输出的是“8”,这就证明这个理论正确。

理论3

对指针进行下标操作(像数组一样在名称后加中括号,里边写元素编号)等价于把指针当成数组,操作理论上该编号对应的内存地址保存的元素的值。
假设目前的代码如上文所述,b现在保存的内存地址应该是内存第10004字节,指向a的第二个元素(a[1])。那么对b执行下标操作b[2]就相当于取得b保存的内存地址10004 + 32 / 8 * 2 = 10012字节处保存的数据,即a[3]。继续追加两行代码验证这个理论:
enter image description here
程序输出的第二行是“6”,就是a[3]的值,这就证明这个理论正确。

数组和指针的套娃

众所周知:数组是一种数据类型,指针也是一种数据类型,数组保存数据,指针指向数据。以此可得:存在保存数组的数组和指向指针的指针,以及保存保存数组的数组的数组以及指向指向指针的指针的指针,以此类推,理论上想套多少层就套多少层。

int a[4][7]; // 一个“4行3列的int数组”。实际上是一个4元素数组,它保存的数据类型是3元素int数组。
int** i; // 一个“二维int指针”。它指向一个指向int变量的指针。
标签