C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。
数组的声明并不是声明一个个单独的变量,比如 runoob0、runoob1、…、runoob99,而是声明一个数组变量,比如 runoob,然后使用 runoob[0]、runoob[1]、…、runoob[99] 来代表一个个单独的变量。
所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。
在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:
type arrayName [ arraySize ];
这叫做一维数组。arraySize 必须是一个大于零的整数常量,type 可以是任意有效的 C 数据类型。例如,要声明一个类型为 double 的包含 10 个元素的数组 balance,声明语句如下:
double balance[10];
现在 balance 是一个可用的数组,可以容纳 10 个类型为 double 的数字。
在 C 中,您可以逐个初始化数组,也可以使用一个初始化语句,如下所示:
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
大括号 { } 之间的值的数目不能大于我们在数组声明时在方括号 [ ] 中指定的元素数目。
如果您省略掉了数组的大小,编译器会根据初始化列表的项数来确定数组的大小。因此,如果:
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};
您将创建一个数组,它与前一个实例中所创建的数组是完全相同的。
如果不初始化数组,数组元素存储未初始化的值,是垃圾值。
当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为 0。
有时需要把数组设置为只读,这样,程序只能从数组中检索值,不能把新值写入数组。
要创建只读数组,应该用 const 声明和初始化数组。例如:
const int days[MONTHS] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
注意:C 不允许把数组作为一个单元赋给另一个数组,除初始化以外也不允许使用花括号列表的行式赋值。
下面是一个为数组中某个元素赋值的实例:
balance[4] = 50.0;
上述的语句把数组中第五个元素的值赋为 50.0。所有的数组都是以 0 作为它们第一个元素的索引,也被称为基索引,数组的最后一个索引是数组的总大小减去 1。以下是上面所讨论的数组的的图形表示:
下图是一个长度为 10 的数组,第一个元素的索引值为 0,第九个元素 runoob 的索引值为 8:
C99 增加了一个新特性:指定初始化器。可以初始化指定的数组元素。例如:
int arr[6] = {[5] = 212}; // 把arr[5]初始化为212
指定初始化器的重要特性:
数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如:
double salary = balance[9];
上面的语句将把数组中第 10 个元素的值赋给 salary 变量。
C 语言支持多维数组。多维数组声明的一般形式如下:
type name[size1][size2]...[sizeN];
例如,下面的声明创建了一个三维 5 . 10 . 4 整型数组:
int threedim[5][10][4];
多维数组最简单的形式是二维数组。一个二维数组,在本质上,是一个一维数组的列表。声明一个 x 行 y 列的二维整型数组,形式如下:
type arrayName [ x ][ y ];
其中,type 可以是任意有效的 C 数据类型,arrayName 是一个有效的 C 标识符。一个二维数组可以被认为是一个带有 x 行和 y 列的表格。下面是一个二维数组,包含 3 行和 4 列:
int x[3][4];
因此,数组中的每个元素是使用形式为 a[ i , j ] 的元素名称来标识的,其中 a 是数组名称,i 和 j 是唯一标识 a 中每个元素的下标。
多维数组可以通过在括号内为每行指定值来进行初始化。下面是一个带有 3 行 4 列的数组。
int a[3][4] = {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};
内部嵌套的括号是可选的,下面的初始化与上面是等同的:
int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
二维数组中的元素是通过使用下标(即数组的行索引和列索引)来访问的。例如:
int val = a[2][3];
上面的语句将获取数组中第 3 行第 4 个元素。您可以通过上面的示意图来进行验证。
学习 C 语言的指针既简单又有趣。通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。
正如您所知道的,每一个变量都有一个内存位置。而一元 & 运算符给出变量的存储地址。
如果 a 是变量名,那么 &a 就是变量的地址。可以把地址看成是变量在内存中的位置。
示例:
#include
#include
int main(void)
{
// system("chcp 65001"); /* cmd chcp 命令切换字符编码为 utf-8 以显示中文 */
int var = 10;
int *p_var = &var;
printf("var的地址:%p\n", p_var);
system("pause");
return 0;
}
运行结果:
图例:
通过上面的实例,我们了解了什么是内存地址以及如何访问它。接下来让我们看看什么是指针。
指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
type *var_name;
在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针 */
所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。
不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。下面的实例涉及到了这些操作:
示例:
#include
#include
int main(void)
{
int var = 20; /* 实际变量的声明 */
int *ip; /* 指针变量的声明 */
ip = &var; /* 在指针变量中存储 var 的地址 */
printf("var 变量的地址: %p\n", &var);
/* 在指针变量中存储的地址 */
printf("ip 变量存储的地址: %p\n", ip);
/* 使用指针访问值 */
printf("*ip 变量的值: %d\n", *ip);
system("pause");
return 0;
}
运行结果:
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。
NULL 指针是一个定义在标准库中的值为零的常量。请看下面的程序:
#include
#include
int main(void)
{
int *ptr = NULL;
printf("ptr 的地址是 %p\n", ptr);
system("pause");
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。
如需检查一个空指针,您可以使用 if 语句,如下所示:
if(ptr) /* 如果 p 非空,则完成 */
if(!ptr) /* 如果 p 为空,则完成 */
数组名是一个指向数组中第一个元素的常量指针,即数组首元素的地址。
因此,在下面的声明中:
double balance[50];
balance 是一个指向 &balance[0] 的指针,即数组 balance 的第一个元素的地址。因此,下面的程序片段把 p 赋值为 balance 的第一个元素的地址:
double *p;
double balance[10];
p = balance;
使用数组名作为常量指针是合法的,反之亦然。因此,*(balance + 4) 是一种访问 balance[4] 数据的合法方式。
一旦您把第一个元素的地址存储在 p 中,您就可以使用 p、(p+1)、*(p+2) 等来访问数组元素。下面的实例演示了上面讨论到的这些概念:
#include
#include
int main()
{
/* 带有 5 个元素的整型数组 */
double balance[5] = {1000.0, 2.0, 3.4, 17.0, 50.0};
double *p;
int i;
p = balance;
/* 输出数组中每个元素的值 */
printf("使用指针的数组值\n");
for (i = 0; i < 5; i++)
{
printf("*(p + %d) : %f\n", i, *(p + i));
}
printf("使用 balance 作为地址的数组值\n");
for (i = 0; i < 5; i++)
{
printf("*(balance + %d) : %f\n", i, *(balance + i));
}
system("pause");
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
在上面的实例中,p 是一个指向 double 型的指针,这意味着它可以存储一个 double 类型的变量。一旦我们有了 p 中的地址,*p 将给出存储在 p 中相应地址的值,正如上面实例中所演示的。
C 指针是一个用数值表示的地址。因此,您可以对指针执行算术运算。可以对指针进行四种算术运算:++、–、+、-。
假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,让我们对该指针执行下列的算术运算:
ptr++
在执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 字节。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。
我们概括一下:
我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。
下面的程序递增变量指针,以便顺序访问数组中的每一个元素:
#include
#include
const int MAX = 3;
int main(void)
{
int var[] = {10, 100, 200};
int i, *ptr;
/* 指针中的数组地址 */
ptr = var;
for (i = 0; i < MAX; i++)
{
printf("存储地址:var[%d] = %p\n", i, ptr);
printf("存储值:var[%d] = %d\n", i, *ptr);
/* 指向下一个位置 */
ptr++;
}
system("pause");
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
同样地,对指针进行递减运算,即把值减去其数据类型的字节数,如下所示:
#include
#include
const int MAX = 3;
int main(void)
{
int var[] = {10, 100, 200};
int i, *ptr;
/* 指针中最后一个元素的地址 */
ptr = &var[MAX - 1];
for (i = MAX; i > 0; i--)
{
printf("存储地址:var[%d] = %p\n", i - 1, ptr);
printf("存储值:var[%d] = %d\n", i - 1, *ptr);
/* 指向下一个位置 */
ptr--;
system("pause");
return 0;
}
}
当上面的代码被编译和执行时,它会产生下列结果:
指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。
下面的程序修改了上面的实例,只要变量指针所指向的地址小于或等于数组的最后一个元素的地址 &var[MAX - 1],则把变量指针进行递增:
#include
#include
const int MAX = 3;
int main(void)
{
int var[] = {10, 100, 200};
int i, *ptr;
/* 指针中第一个元素的地址 */
ptr = var;
i = 0;
while (ptr <= &var[MAX - 1])
{
printf("存储地址:var[%d] = %p\n", i, ptr);
printf("存储值:var[%d] = %d\n", i, *ptr);
/* 指向上一个位置 */
ptr++;
i++;
}
system("pause");
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
处理数组的函数通常都需要使用数组的原始数据,会修改原数组的元素的值。例如:
#include
#include
const int MAX = 3;
void add(int a[], int n)
{
for (int i = 0; i < n; i++)
a[i]++;
}
int main(void)
{
int var[] = {10, 100, 200};
printf("Before: ");
for (int i = 0; i < 3; i++)
printf("%d ", var[i]);
printf("\n");
add(var, 3);
printf("After: ");
for (int i = 0; i < 3; i++)
printf("%d ", var[i]);
printf("\n");
system("pause");
return 0;
}
运行结果:
其实,有时候修改数组的原始数据并不是程序的本意。我们可以对形参使用 const 来保护数组中的数据。
这样使用 const 并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。
修改上述程序的 add() 函数:
void add(const int a[], int n)
{
for (int i = 0; i < n; i++)
a[i]++;
}
再次编译和运行,报错:
定义数组时,如果数组的元素个数通过变量指定,则这样的数组称为变长数组。
例如:
int n,m;
n = 5;
int a[n]; //a就是一维变长数组
在定义变长数组之前可以通过变量指定元素个数,如果变长数组已经定义好了,则无法再改变元素个数。
注意:
复合字面量,是除了符号常量之外的常量。
例如:5是int的字面常量,1.2是double的字面常量,'a’是char的字面常量,"hello"是字符串的字面常量。一般来说,字符常量可以方便初始化变量,而且可以用于相应运算。其中较为特殊的还有数组的复合字面常量。形如:(int [2]){1, 2}
我们可以通过这样的字面常量来初始化数组:
int a[2] = {1, 3};
,这时候我们省略了等号后面的数组类型名是没有问题的。
还有可以使用这样常量作为需要的数组参数,这时候就需要标注清楚数组的类型名,对于数组内元素都已知情况下,我们也可以省略数组的元素数目:
(int []){1, 2, 5, 7, 10}
,编译器将会将其识别为int [5]类型数组。
但是符合字面量作为一种匿名类型,我们必须在创建时候使用,否则就将会被废弃,我们可以通过一个指针来接受它的地址(它本身就作为它首元素的地址,对于C语言的各种类型的数组都是如此):
int *a = (int []){1, 2, 3, 5, 10, 15};