C | 数组和指针

目录

一、数组

1.1 初始化数组

1.2 指定初始化器(C99)

1.3 给数组元素赋值

1.4 数组边界

1.5 指定数组的大小

二、多维数组

2.1 初始化二维数组

​编辑

2.2 其他多维数组

三、指针和数组

四、函数、数组和指针

4.1 使用指针形参

4.2 指针表示法和数组表示法

五、指针操作

六、保护数组中的数据

6.1 对形式参数使用const

6.2 const的其他内容

七、指针和多维数组

7.1 指向多维数组的指针

7.2 指针的兼容性

八、变长数组(VLA)

九、复合字面量


一、数组

定义:数组由数据类型相同的一系列元素组成

声明数组:告诉编译器数组中内含多少元素和这些元素的类型。编译器根据这些信息正确地创建数组。

数组类型:普通变量可以使用的类型,数组元素都可以用。

/* 一些数组声明*/
int main(void)
{
float candy[365]; /* 内含365个float类型元素的数组 */
char code[12]; /*内含12个char类型元素的数组*/
int states[50]; /*内含50个int类型元素的数组 */
...
}

/* 访问数组中的元素*/
candy[0]; //表示candy数组的第1个元素

1.1 初始化数组

只储存单个值的变量有时也称为标量变量(scalar variable),如下:

int fix = 1;
float flax = PI * 2;

C使用新的语法来初始化数组:

int main(void)
{
int powers[8] = {1,2,4,6,8,16,32,64}; /* 从ANSI C开始支持这种初
始化 */
...
}

/* day_mon1.c -- 打印每个月的天数 */
#include <stdio.h>
#define months 12
int main(void)
{
	int days[months] = { 31, 28, 31, 30, 31, 30,
		31, 31, 30, 31, 30, 31 };
	int index;
	for (index = 0; index < months; index++)
		printf("month %2d has %2d days.\n", index + 1,
			days[index]);
	getchar();
	return 0;
}
//这个程序还不够完善,每4年打错一个月份的天数(即,2月份的天数)

运行结果:

month  1 has 31 days.
month  2 has 28 days.
month  3 has 31 days.
month  4 has 30 days.
month  5 has 31 days.
month  6 has 30 days.
month  7 has 31 days.
month  8 has 31 days.
month  9 has 30 days.
month 10 has 31 days.
month 11 has 30 days.
month 12 has 31 days.

注:用const声明和初始化数组,可以创建只读数组。一旦声明为const,便不能再给它赋值。

const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};

初始化失败的例子:

/* no_data.c -- 为初始化数组 */
#include <stdio.h>
#define SIZE 4
int main(void)
{
	int no_data[SIZE]; /* 未初始化数组 */
	int i;
	printf("%2s%14s\n", "i", "no_data[i]");
	for (i = 0; i < SIZE; i++)
		printf("%2d%14d\n", i, no_data[i]);
	getchar();
	return 0;
}

运行结果:

 i    no_data[i]
 0    -858993460
 1    -858993460
 2    -858993460
 3    -858993460

在使用数组元素之前,必须先给它们赋初值。编译器使用的值是内存相应位置上的现有值。

注意:可以把数组创建成不同的存储类别(storage class)本章描述的数组属于自动存储类别,意思是这些数组在函数内部声明,且声明时未使用关键字static。对于一些其他存储类别的变量和数组,如果在声明时未初始化,编译器会自动把它们的值设置为0。

初始化列表中的项数应与数组的大小一致。一个不一致的例子:

/* no_data.c -- 为初始化数组 */
#include <stdio.h>
#define SIZE 4
int main(void)
{
	int some_data[SIZE] = { 1492, 1066 }; /* 缺少元素 */
	int i;
	printf("%2s%14s\n", "i", "some_data[i]");
	for (i = 0; i < SIZE; i++)
		printf("%2d%14d\n", i, some_data[i]);
	getchar();
	return 0;
}

运行结果:

 i    some_data[i]
 0    1492
 1    1066
 2    0
 3    0

如果部分初始化数组,剩余的元素就会被初始化为0。

如果初始化列表的项数多于数组元素个数,编译器视为错误。

可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数,如下:

//程序清单10.4 day_mon2.c程序
/* day_mon2.c -- 让编译器计算元素个数 */
#include <stdio.h>
int main(void)
{
	const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31 };
	int index;
	for (index = 0; index < sizeof days / sizeof days[0]; index++)
		printf("Month %2d has %d days.\n", index + 1, days[index]);
	getchar();
	return 0;
}

运行结果:

Month  1 has 31 days.
Month  2 has 28 days.
Month  3 has 31 days.
Month  4 has 30 days.
Month  5 has 31 days.
Month  6 has 30 days.
Month  7 has 31 days.
Month  8 has 31 days.
Month  9 has 30 days.
Month 10 has 31 days.

  • 如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小。
  • sizeof运算符给出它的运算对象的大小(以字节为单位)。整个数组的大小除以单个元素的大小就是数组元素的个数。
  • 自动计数的弊端:无法察觉初始化列表中的项数有误。

1.2 指定初始化器(C99)

C99 增加新特性:指定初始化器(designated initializer)。可以初始化指定的数组元素。对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为0。

 //designate.c -- 使用指定初始化器
#include <stdio.h>
#define months 12
int main(void)
{
	int days[months] = { 31, 28,[4] = 31, 30, 31, [1] = 29 };
	int i;
	for (i = 0; i < months; i++)
		printf("%2d %d\n", i + 1, days[i]);
	getchar();
	return 0;
}

运行结果:

1 31

2 29

3 0

4 0

5 31

6 30

7 31

8 0

9 0

10 0

11 0

12 0

指定初始化器的两个重要特性。第一,如果指定初始化器后面有更多的值,那么后面这些值将被用于初始化指定元素后面的元素。第二,如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化。

如果未指定数组大小,编译器会把数组的大小设置为足够装得下初始化的值。

 //designate.c -- 不指定数组大小
#include <stdio.h>
int main(void)
{
	int days[] = { 31, 28,[4] = 31, 30, 31, [1] = 29 };
	int i;
	for (i = 0; i < sizeof days/sizeof days[0]; i++)
		printf("%2d %d\n", i + 1, days[i]);
	getchar();
	return 0;
}

运行结果:

1 31

2 29

3 0

4 0

5 31

6 30

7 31

1.3 给数组元素赋值

声明数组后,可以借助数组下标(或索引)给数组元素赋值。

/* 给数组的元素赋值 */
#include <stdio.h>
#define SIZE 50
int main(void)
{
int counter, evens[SIZE];
for (counter = 0; counter < SIZE; counter++)
evens[counter] = 2 * counter;
...
}

C 不允许把数组作为一个单元赋给另一个数组,除初始化以外也不允许使用花括号列表的形式赋值。错误的赋值形式:

/* 一些无效的数组赋值 */

#define SIZE 5

int main(void)

{

int oxen[SIZE] = {5,3,2,8}; /* 初始化没问题 */

int yaks[SIZE];

yaks = oxen; /* 不允许 */

yaks[SIZE] = oxen[SIZE]; /* 数组下标越界 */

yaks[SIZE] = {5,3,2,8}; /* 不起作用 */

1.4 数组边界

在使用数组时,要防止数组下标超出边界。因为编译器不会检查出这种错误(但是,一些编译器发出警告,然后继续编译程序)。

// bounds.c -- 数组下标越界
#include <stdio.h>
#define SIZE 4
int main(void)
{
	int value1 = 44;
	int arr[SIZE];
	int value2 = 88;
	int i;
    int j;
	printf("value1 = %d, value2 = %d\n", value1, value2);
	for (i = -1; i <= SIZE; i++)
		arr[i] = 2 * i + 1;
	for (j = -1; j < 7; j++)
		printf("%2d %d\n", j, arr[j]);
	printf("value1 = %d, value2 = %d\n", value1, value2);
	printf("address of arr[-1]: %p\n", &arr[-1]); 
    printf("address of arr[4]: %p\n", &arr[4]);
	printf("address of value1: %p\n", &value1);
	printf("address of value2: %p\n", &value2);
	getchar();
	return 0;
}
//本程序使用gcc编译,循环打印没有显示-1

运行结果:

value1 = 44, value2 = 88

0 1

1 3

2 5

3 7

4 9

5 5

6 5

value1 = 9, value2 = -1

address of arr[-1]: 0061FF00

address of arr[4]: 0061FF14

address of value1: 0061FF14

address of value2: 0061FF00

在C标准中,使用越界下标的结果是未定义的。这意味着程序看上去可以运行,但是运行结果很奇怪,或异常中止。使用越界的数组下标会导致程序改变其他变量的值。不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止。编译器不对此报错,因为不检查边界,C 程序可以运行更快。

数组元素的编号从0开始。最好是在声明数组时使用符号常量来表示数组的大小,这样做能确保整个程序中的数组大小始终一致。

1.5 指定数组的大小

在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式,是由整型常量构成的表达式。sizeof表达式被视为整型常量,但是(与C++不同)const值不是。另外,表达式的值必须大于0。

int n = 5;

int m = 8;

float a1[5]; // 可以

float a2[5*2 + 1]; //可以

float a3[sizeof(int) + 1]; //可以

float a4[-4]; // 不可以,数组大小必须大于0

float a5[0]; // 不可以,数组大小必须大于0

float a6[2.5]; // 不可以,数组大小必须是整数

float a7[(int)2.5]; // 可以,已被强制转换为整型常量

float a8[n]; // C99之前不允许

float a9[m]; // C99之前不允许

C90标准的编译器不允许后两种声明方式。而C99标准允许这样声明,这创建了一种新型数组,称为变长数组(variable-length array)或简称 VLA(C11 放弃了这一创新的举措,把VLA设定为可选,而不是语言必备的特性)。

二、多维数组

为了方便表示数据,可以使用数组的数组。以二维数组为例:

/* rain.c -- 计算每年的总降水量、年平均降水量和5年中每月的平
均降水量 */
#include <stdio.h>
#define months 12 // 一年的月份数
#define years 5 // 年数
int main(void) {
	// 用2010~2014年的降水量数据初始化数组
	const float rain[years][months] =
	{
		{ 4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4,
		3.5, 6.6 },
		{ 8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9,
		1.4, 7.3 },
		{ 9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3,
		6.1, 8.4 },
		{ 7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7,
		4.3, 6.2 },
		{ 7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3,
		2.6, 5.2 }
	};
	int year, month;
	float subtot, total;
	printf(" year rainfall (inches)\n");
	for (year = 0, total = 0; year < years; year++)
	{ // 每一年,各月的降水量总和
		for (month = 0, subtot = 0; month < months;
		month++)
			subtot += rain[year][month];
		printf("%5d %15.1f\n", 2010 + year, subtot);
		total += subtot; // 5年的总降水量
	}
	printf("\nthe yearly average is %.1f inches.\n\n",
		total / years);
	printf("monthly averages:\n\n");
	printf(" jan  feb  mar  apr  may  jun  jul  aug\
  sep  oct");
	printf("  nov  dec\n");
	for (month = 0; month < months; month++)
	{ // 每个月,5年的总降水量
		for (year = 0, subtot = 0; year < years;
		year++)
			subtot += rain[year][month];
		printf("%4.1f ", subtot / years);
	}
	printf("\n");
	getchar();
	return 0;
}

运行结果:

 year rainfall (inches)
 2010            32.4
 2011            37.9
 2012            49.8
 2013            44.0
 2014            32.9

the yearly average is 39.4 inches.

monthly averages:

 jan  feb  mar  apr  may  jun  jul  aug  sep  oct  nov  dec
 7.3  7.3  4.9  3.0  2.3  0.6  1.2  0.3  0.5  1.7  3.6  6.7

2.1 初始化二维数组

const float rain[YEARS][MONTHS] =
{
{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6},
{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3},
{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4},
{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2},
{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2}
};

如果某列表中的数值个数超出了数组每行的元素个数,则会出错,但是这并不会影响其他行的初始化。初始化时也可省略内部的花括号,只保留最外面的一对花括号。只要保证初始化的数值个数正确,初始化的效果与上面相同。但是如果初始化的数值不够,则按照先后顺序逐行初始化,直到用完所有的值。后面没有值初始化的元素被统一初始化为0。

2.2 其他多维数组

处理三维数组要使用3重嵌套循环,处理四维数组要使用4重嵌套循环。对于其他多维数组,以此类推。

三、指针和数组

数组与指针的关系:数组表示法其实是在变相地使用指针。例如,数组名是数组首元素的地址。

 flizny == &flizny[0]; // 数组名是该数组首元素的地址

//程序清单10.8 pnt_add.c程序
// pnt_add.c -- 指针地址
#include <stdio.h>
#define SIZE 4
int main(void)
{
	short dates[SIZE]; 
    short * pti;
	short index;
	double bills[SIZE];
	double * ptf;
	pti = dates; // 把数组地址赋给指针
	ptf = bills;
	printf("%23s %15s\n", "short", "double");
	for (index = 0; index < SIZE; index++)
		printf("pointers + %d: %10p %10p\n", index, pti +
			index, ptf + index);
	getchar();
	return 0;
}

运行结果:

                  short          double
pointers + 0:   010FF780   010FF740
pointers + 1:   010FF782   010FF748
pointers + 2:   010FF784   010FF750
pointers + 3:   010FF786   010FF758

我们的系统中,地址按字节编址,short类型占用2字节,double类型占用8字节。在C中,指针加1指的是增加一个存储单元。这是为什么必须声明指针所指向对象类型的原因之一。只知道地址不够,因为计算机要知道储存对象需要多少字节(即使指针指向的是标量变量,也要知道变量的类型,否则*pt 就无法正确地取回地址上的值)。

 指针的值是它所指向对象的地址。

在指针前面使用*运算符可以得到该指针所指向对象的值。

指针加1,指针的值递增它所指向类型的大小(以字节为单位)。

C 语言标准在描述数组表示法时确实借助了指针。也就是说,定义ar[n]的意思是*(ar + n)。

dates + 2 == &date[2] // 相同的地址
*(dates + 2) == dates[2] // 相同的值

不要混淆 *(dates+2)和*dates+2。间接运算符(*)的优先级高于+

*(dates + 2) // dates第3个元素的值
*dates + 2 // dates第1个元素的值加2

指针表示法和数组表示法是两种等效的方法。适时使用数组表示法和指针表示法:

//适时使用数组表示法和指针表示法
/* day_mon3.c -- uses pointer notation */
#include <stdio.h>
#define MONTHS 12
int main(void)
{
	int days[MONTHS] = { 31, 28, 31, 30, 31, 30,
		31, 31, 30, 31, 30, 31 };
	int index;
	for (index = 0; index < MONTHS; index++)
		printf("Month %2d has %d days.\n", index + 1,
			*(days + index)); //与 days[index]相同
	getchar();
	return 0;
}

四、函数、数组和指针

使用数组作为形参的函数,其形参的类型为指针,有两种表示方法:用指针表示数组名;用数组名表示指针。

int sum(int * ar, int n)

int sum(int   ar[], int n)

注:只有在函数原型函数定义头中,才可以用int ar[]代替int * ar

函数原型可以省略参数名,所以下面4种原型都是等价的:

int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);

在函数定义中不能省略参数名。下面两种形式的函数定义等价:

int sum(int *ar, int n)
{
// 其他代码已省略
}
int sum(int ar[], int n)
{
//其他代码已省略
}

// sum_arr1.c -- 数组元素之和
// 如果编译器不支持 %zd,用 %u 或 %lu 替换它
#include <stdio.h>
#define SIZE 10
int sum(int ar[], int n);
int main(void)
{
	int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19,
		26, 31, 20 };
	long answer;
	answer = sum(marbles, SIZE);
	printf("The total number of marbles is %ld.\n", answer);
	printf("The size of marbles is %zd bytes.\n",
		sizeof marbles);
	getchar();
	return 0;
}
int sum(int ar[], int n) // 这个数组的大小是?
{
	int i;
	int total = 0;
	for (i = 0; i < n; i++)
		total += ar[i];
	printf("The size of ar is %zd bytes.\n", sizeof ar);
	return total;
}

运行结果:

The size of ar is 4 bytes.
The total number of marbles is 190.
The size of marbles is 40 bytes.

整个marbles的大小是40字节。但是,ar才4字节。这是因为ar并不是数组本身,它是一个指向 marbles 数组首元素的指针。我们的系统中用4 字节储存地址。marbles是一个数组, ar是一个指向marbles数组首元素的指针,利用C中数组和指针的特殊关系,可以用数组表示法来表示指针ar。

4.1 使用指针形参

函数要处理数组必须知道何时开始、何时结束。现有两种方法:1、指针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。2、传递两个指针,第1个指针指明数组的开始处,第2个指针指明数组的结束处。

指针形参是变量,这意味着可以用索引表明访问数组中的哪一个元素。

/* sum_arr2.c -- 数组元素之和 */
#include <stdio.h>
#define SIZE 10
int sump(int * start, int * end);
int main(void)
{
	int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19,
		26, 31, 20 }; 
    long answer;
	answer = sump(marbles, marbles + SIZE);
	printf("The total number of marbles is %ld.\n", answer);
	getchar();
	return 0;
}
/* 使用指针算法 */
int sump(int * start, int * end)
{
	int total = 0;
	while (start < end)
	{
		total += *start; // 把数组元素的值加起来
		start++; // 让指针指向下一个元素
	}
	return total;
}

运行结果:

The total number of marbles is 190.

start是指向int的指针,start递增1相当于其值递增int类型的大小。

还可以把循环体压缩成一行代码:

total += *start++;
或者

total += *(start++);//更清楚

一元运算符*和++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是*start。如果使用*++start,顺序则反过来,先递增指针,再使用指针指向位置上的值。如果使用(*start)++,则先使用start指向的值,再递增该值,而不是递增指针。

/* order.c -- 指针运算中的优先级 */
#include <stdio.h>
int data[2] = { 100, 200 }; 
int moredata[2] = { 300, 400 };
int main(void)
{
	int * p1, *p2, *p3;
	p1 = p2 = data;
	p3 = moredata;
	printf(" *p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
	printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n", *p1++, *++p2,
		(*p3)++);
	printf(" *p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
	getchar();
	return 0;
}

运行结果:

 *p1 = 100, *p2 = 100, *p3 = 300
*p1++ = 100, *++p2 = 200, (*p3)++ = 300
 *p1 = 200, *p2 = 200, *p3 = 301

4.2 指针表示法和数组表示法

ar[i]和*(ar+1)这两个表达式都是等价的。无论ar是数组名还是指针变量,这两个表达式都没问题。但是,只有当ar是指针变量时,才能使用ar++这样的表达式

指针表示法(尤其与递增运算符一起使用时)更接近机器语言,因此一些编译器在编译时能生成效率更高的代码。

五、指针操作

// ptr_ops.c -- 指针操作
#include <stdio.h>
int main(void)
{
	int urn[5] = { 100, 200, 300, 400, 500 };
	int * ptr1, *ptr2, *ptr3;
	ptr1 = urn; // 把一个地址赋给指针
	ptr2 = &urn[2]; // 把一个地址赋给指针
				// 解引用指针,以及获得指针的地址
	printf("pointer value, dereferenced pointer, pointer\
 address:\n");
	printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
	// 指针加法
	ptr3 = ptr1 + 4;
	printf("\nadding an int to a pointer:\n");
	printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));
	ptr1++; // 递增指针
	printf("\nvalues after ptr1++:\n");
	printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
	ptr2--; // 递减指针
	printf("\nvalues after --ptr2:\n");
	printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);
	--ptr1; // 恢复为初始值
	++ptr2; // 恢复为初始值
	printf("\nPointers reset to original values:\n");
	printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);
	// 一个指针减去另一个指针
	printf("\nsubtracting one pointer from another:\n");
	printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 =\
%td\n", ptr2, ptr1, ptr2 - ptr1);
		// 一个指针减去一个整数
	printf("\nsubtracting an int from a pointer:\n");
	printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 -
		2);
	getchar();
	return 0;
}

运行结果:

pointer value, dereferenced pointer, pointer address:
ptr1 = 0097F7A0, *ptr1 =100, &ptr1 = 0097F794

adding an int to a pointer:
ptr1 + 4 = 0097F7B0, *(ptr1 + 4) = 500

values after ptr1++:
ptr1 = 0097F7A4, *ptr1 =200, &ptr1 = 0097F794

values after --ptr2:
ptr2 = 0097F7A4, *ptr2 = 200, &ptr2 = 0097F788

Pointers reset to original values:
ptr1 = 0097F7A0, ptr2 = 0097F7A8

subtracting one pointer from another:
ptr2 = 0097F7A8, ptr1 = 0097F7A0, ptr2 - ptr1 =2

subtracting an int from a pointer:
ptr3 = 0097F7B0, ptr3 - 2 = 0097F7A8

上面程序演示了指针变量的 8种基本操作。除了这些操作,还可以使用关系运算符来比较指针。

赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。注意,地址应该和指针类型兼容。

解引用:*运算符给出指针指向地址上储存的值。

取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。

指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。

递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。

指针减去一个整数:可以使用-运算符从一个指针中减去一个整数。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。

递减指针:除了递增指针还可以递减指针。

指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同

比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。

注意:

编译器不会检查指针是否仍指向数组元素。C 只能保证指向数组任意元素的指针和指向数组后面第 1 个位置的指针有效。但是,如果递增或递减一个指针后超出了这个范围,则是未定义的。

千万不要解引用未初始化的指针。

int * pt;// 未初始化的指针

*pt = 5; // 严重的错误

第2行的意思是把5储存在pt指向的位置。但是pt未被初始化,其值是一个随机值,所以不知道5将储存在何处。这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃。切记:创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。

六、保护数组中的数据

编写函数时,通常都是直接传递数值,只有程序需要在函数中改变该数值时,才会传递指针。对于数组别无选择,必须传递指针,因为这样做效率高。

6.1 对形式参数使用const

如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const

int sum(const int ar[], int n); /* 函数原型 */
int sum(const int ar[], int n) /* 函数定义 */

使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。
一般而言,如果编写的函数需要修改数组,在声明数组形参时则不使用const;如果编写的函数不用修改数组,那么在声明数组形参时最好使用const。

/* arf.c -- 处理数组的函数 */
#include <stdio.h>
#define SIZE 5
void show_array(const double ar[], int n);
void mult_array(double ar[], int n, double mult);
int main(void)
{
	double dip[SIZE] = { 20.0, 17.66, 8.2, 15.3, 22.22 };
	printf("The original dip array:\n");
	show_array(dip, SIZE); 
    mult_array(dip, SIZE, 2.5);
	printf("The dip array after calling mult_array():\n");
	show_array(dip, SIZE);
	getchar();
	return 0;
}
/* 显示数组的内容 */
void show_array(const double ar[], int n)
{
	int i;
	for (i = 0; i < n; i++)
		printf("%8.3f ", ar[i]);
	putchar('\n');
}
/* 把数组的每个元素都乘以相同的值 */
void mult_array(double ar[], int n, double mult)
{
	int i;
	for (i = 0; i < n; i++)
		ar[i] *= mult;
}

运行结果:

The original dip array:
  20.000   17.660    8.200   15.300   22.220
The dip array after calling mult_array():
  50.000   44.150   20.500   38.250   55.550

注意该程序中两个函数的返回类型都是void。虽然mult_array()函数更新了dip数组的值,但是并未使用return机制。

6.2 const的其他内容

把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};

const double locked[4] = {0.08, 0.075, 0.0725, 0.07};

const double * pc = rates; // 有效

pc = locked; //有效

pc = &rates[3]; //有效

只能把非const数据的地址赋给普通指针

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};

const double locked[4] = {0.08, 0.075, 0.0725, 0.07};

double * pnc = rates; // 有效

pnc = locked; // 无效

pnc = &rates[3]; // 有效

这个规则非常合理。否则,通过指针就能改变const数组中的数据。

对函数的形参使用const不仅能保护数据,还能让函数处理const数组。另外,不应该把const数组名作为实参传递给形参为非const的函数:

C标准规定,使用非const标识符修改const数据导致的结果是未定义的。

const还有其他的用法。例如,可以声明并初始化一个不能指向别处的指针,关键是const的位置

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};

double * const pc = rates; // pc指向数组的开始

pc = &rates[2]; // 不允许,因为该指针不能指向别处

*pc = 92.99; // 没问题 -- 更改rates[0]的值

可以用这种指针修改它所指向的值,但是它只能指向初始化时设置的地址。

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};

const double * const pc = rates;

pc = &rates[2]; //不允许

*pc = 92.99; //不允许

在创建指针时还可以使用const两次,该指针既不能更改它所指向的地址,也不能修改指向地址上的值。

七、指针和多维数组

int zippo[4][2]; /* 内含int数组的数组 */

zippo和zippo[0]的值相同 ,但zippo + 1和zippo[0] + 1的值不同。因为zippo指向的对象占用了两个int大小,而zippo[0]指向的对象只占用一个int大小。

解引用一个指针(在指针前使用*运算符)或在数组名后使用带下标的[]运算符,得到引用对象代表的值。

*zippo就是&zippo[0][0]。

**zippo等价于*&zippo[0][0],相当于zippo[0][0]。

简而言之,zippo是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针是就是双重间接(doubleindirection)的例子。

/* zippo1.c -- zippo的相关信息 */
#include <stdio.h>
int main(void)
{
	int zippo[4][2] = { { 2, 4 },{ 6, 8 },{ 1,
		3 },{ 5, 7 } };
	printf("zippo = %p, zippo + 1 = %p\n", zippo,
		zippo + 1);
	printf("zippo[0] = %p, zippo[0] + 1 =\
%p\n",zippo[0], zippo[0] + 1);
	printf("*zippo = %p, *zippo + 1 = %p\n", *zippo, *zippo + 1);
	printf("zippo[0][0] = %d\n", zippo[0][0]);
	printf("*zippo[0] = %d\n", *zippo[0]);
	printf("**zippo = %d\n", **zippo);
	printf("zippo[2][1] = %d\n", zippo[2][1]);
	printf("*(*(zippo+2) + 1) = %d\n", *(*(zippo + 2) + 1));
	getchar();
	return 0;
}

运行结果:

zippo = 010FFE98, zippo + 1 = 010FFEA0
zippo[0] = 010FFE98, zippo[0] + 1 =010FFE9C
*zippo = 010FFE98, *zippo + 1 = 010FFE9C
zippo[0][0] = 2
*zippo[0] = 2
**zippo = 2
zippo[2][1] = 3
*(*(zippo+2) + 1) = 3

 如果程序恰巧使用一个指向二维数组的指针,而且要通过该指针获取值时,最好用简单的数组表示法,而不是指针表示法。

7.1 指向多维数组的指针

声明一个指针变量pz指向一个二维数组

int (* pz)[2]; // pz指向一个内含两个int类型值的数组

以上代码把pz声明为指向一个数组的指针,该数组内含两个int类型值。在声明中使用圆括号是因为[]的优先级高于*

int * pax[2]; // pax是一个内含两个指针元素的数组,每个元素都指向int的指针

这行代码声明了两个指向int的指针。

/* zippo2.c -- 通过指针获取zippo的信息 */
#include <stdio.h>
int main(void)
{
	int zippo[4][2] = { { 2, 4 },{ 6, 8 },{ 1,
		3 },{ 5, 7 } };
	int(*pz)[2];
	pz = zippo;
	printf("pz = %p, pz + 1 = %p\n", pz,\
		pz + 1);
	printf("pz[0] = %p, pz[0] + 1 = %p\n", pz[0],
		pz[0] + 1);
	printf("pz[1] = %p, pz[1] + 1 = %p\n", pz[1],
		pz[1] + 1);
	printf("*pz = %p, *pz + 1 = %p\n", *pz, *pz + 1);
	printf("pz[0][0] = %d\n", pz[0][0]);
	printf("*pz[0] = %d\n", *pz[0]);
	printf("**pz = %d\n", **pz);
	printf("pz[2][1] = %d\n", pz[2][1]);
	printf("*(*(pz+2) + 1) = %d\n", *(*(pz + 2) + 1));
	getchar();
	return 0;
}

运行结果:

pz = 010FF7B0, pz + 1 = 010FF7B8
pz[0] = 010FF7B0, pz[0] + 1 = 010FF7B4
pz[1] = 010FF7B8, pz[1] + 1 = 010FF7BC
*pz = 010FF7B0, *pz + 1 = 010FF7B4
pz[0][0] = 2
*pz[0] = 2
**pz = 2
pz[2][1] = 3
*(*(pz+2) + 1) = 3

虽然pz是一个指针,不是数组名,但是也可以使用 pz[2][1]这样的写法。可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名:

zippo[m][n] == *(*(zippo + m) + n)
pz[m][n] == *(*(pz + m) + n)

7.2 指针的兼容性

指针之间的赋值比数值类型之间的赋值要严格。

int n = 5;

double x;

int * p1 = &n;

double * pd = &x;

x = n; // 隐式类型转换

pd = p1; // 编译时错误

int * pt;

int (*pa)[3];

int ar1[2][3];

int ar2[3][2];

int **p2; // 一个指向指针的指针

有如下的语句:

pt = &ar1[0][0]; // 都是指向int的指针

pt = ar1[0]; // 都是指向int的指针

pt = ar1; // 无效

pa = ar1; // 都是指向内含3个int类型元素数组的指针

pa = ar2; // 无效

p2 = &pt; // both pointer-to-int *

*p2 = ar2[0]; // 都是指向int的指针

p2 = ar2; // 无效

把const指针赋给非const指针不安全,因为这样可以使用新的指针改变const指针指向的数据。但是把非const指针赋给const指针没问题,前提是只进行一级解引用

p2 = p1; // 有效 -- 把非const指针赋给const指针

但是进行两级解引用时,这样的赋值也不安全

const int **pp2;

int *p1;

const int n = 13;

pp2 = &p1; // 允许,但是这导致const限定符失效(根据第1行代码,不能通过*pp2修改它所指向的内容)

*pp2 = &n; // 有效,两者都声明为const,但是这将导致p1指向n(*pp2已被修改)

*p1 = 10;//有效,但是这将改变n的值(但是根据第3行代码,不能修改n的值)

标准规定了通过非const指针更改const数据是未定义的。

C++不允许把const指针赋给非const指针。而C则允许这样做,但是如果通过p1更改y,其行为是未定义的。 

// array2d.c -- 处理二维数组的函数
#include <stdio.h>
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS], int rows);
void sum_cols(int[][COLS], int); // 省略形参名,没问题
int sum2d(int(*ar)[COLS], int rows); // 另一种语法
int main(void)
{
	int junk[ROWS][COLS] = { { 2, 4, 6, 8 },
	{ 3, 5, 7, 9 },
	{ 12, 10, 8, 6 }
	};
	int i, k;
	sum_rows(junk, ROWS);
	sum_cols(junk, ROWS);
	printf("Sum of all elements = %d\n", sum2d(junk,
		ROWS));
	getchar();
	return 0;
}
void sum_rows(int ar[][COLS], int rows)
{
	int r;
	int c;
	int tot;
	for (r = 0; r < rows; r++)
	{
		tot = 0;
		for (c = 0; c < COLS; c++)
			tot += ar[r][c];
		printf("row %d: sum = %d\n", r, tot);
	}
}
void sum_cols(int ar[][COLS], int rows)
{
	int r;
	int c; int tot;
	for (c = 0; c < COLS; c++)
	{
		tot = 0;
		for (r = 0; r < rows; r++)
			tot += ar[r][c];
		printf("col %d: sum = %d\n", c, tot);
	}
}
int sum2d(int ar[][COLS], int rows)
{
	int r;
	int c;
	int tot = 0;
	for (r = 0; r < rows; r++)
		for (c = 0; c < COLS; c++)
			tot += ar[r][c];
	return tot;
}

运行结果:

row 0: sum = 20
row 1: sum = 24
row 2: sum = 36
col 0: sum = 17
col 1: sum = 19
col 2: sum = 21
col 3: sum = 23
Sum of all elements = 80

一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中的值,也可以在第1对方括号中写上大小,但是编译器会忽略该值

int sum4d(int ar[][12][20][30], int rows);

int sum2(arr3x4 ar, int rows); // 与下面的声明相同

int sum2(int ar[3][4], int rows); // 与下面的声明相同

int sum2(int ar[][4], int rows); // 标准形式

typedef int arr4[4]; // arr4是一个内含 4 个int的数组

typedef arr4 arr3x4[3]; // arr3x4 是一个内含3个 arr4的数组

注意:下面的声明不正确

int sum2(int ar[][], int rows); // 错误的声明

编译器会把数组表示法转换成指针表示法。例如,编译器会把 ar[1]转换成 ar+1。编译器对ar+1求值,要知道ar所指向的对象大小。

八、变长数组(VLA)

C规定,数组的维数必须是常量,不能用变量。C99新增了变长数组(variable-length array,VLA),允许使用变量表示数组的维度。

变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern存储类别说明符。而且,不能在声明中初始化它们。

注意:变长数组不能改变大小

一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度。

声明一个带二维变长数组参数的函数:

int sum2d(int rows, int cols, int ar[rows][cols]); // ar是一个变长数组(VLA)

int sum2d(int ar[rows][cols], int rows, int cols); // 无效的顺序

int sum2d(int, int, int ar[*][*]); // ar是一个变长数组(VLA),省略了维度形参名

ar的声明要使用rows和cols,所以在形参列表中必须在声明ar之前先声明这两个形参。C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度。

//vararr2d.c -- 使用变长数组的函数
#include <stdio.h>
#define ROWS 3
#define COLS 4
int sum2d(int rows, int cols, int ar[rows][cols]);
int main(void)
{
	int i, j;
	int rs = 3;
	int cs = 10;
	int junk[ROWS][COLS] = {
		{ 2, 4, 6, 8 },{ 3, 5, 7, 9 },
		{ 12, 10, 8, 6 }
	};
	int morejunk[ROWS - 1][COLS + 2] = {
		{ 20, 30, 40, 50, 60, 70 },
		{ 5, 6, 7, 8, 9, 10 }
	};
	int varr[rs][cs]; // 变长数组(VLA)
	for (i = 0; i < rs; i++)
		for (j = 0; j < cs; j++)
			varr[i][j] = i * j + j;
	printf("3x5 array\n");
	printf("Sum of all elements = %d\n", sum2d(ROWS,
		COLS, junk));
	printf("2x6 array\n");
	printf("Sum of all elements = %d\n", sum2d(ROWS -
		1, COLS + 2, morejunk));
	printf("3x10 VLA\n");
	printf("Sum of all elements = %d\n", sum2d(rs, cs,
		varr));
	getchar();
	return 0;
}
// 带变长数组形参的函数
int sum2d(int rows, int cols, int ar[rows][cols])
{
	int r;
	int c; int tot = 0;
	for (r = 0; r < rows; r++)
		for (c = 0; c < cols; c++)
			tot += ar[r][c];
	return tot;
}

运行结果:

3x5 array
Sum of all elements = 80
2x6 array
Sum of all elements = 315
3x10 VLA
Sum of all elements = 270

和传统的语法类似,变长数组名实际上是一个指针。

注:const和数组大小

C90标准不允许(也可能允许)在声明数组时使用const变量。数组的大小必须是给定的整型常量表达式,可以是整型常量组合。C99/C11 标准允许在声明变长数组时使用 const 变量。所以该数组的定义必须是声明在块中的自动存储类别数组。变长数组还允许动态内存分配,这说明可以在程序运行时指定数组的大小。普通 C数组都是静态内存分配,即在编译时确定数组的大小。

九、复合字面量

字面量是除符号常量外的常量。例如,5是int类型字面量, 81.3是double类型的字面量,'Y'是char类型的字面量,"elephant"是字符串字面量。

C99新增了复合字面量(compound literal)

int diva[2] = {10, 20};//普通的数组声明

(int [2]){10, 20} // 复合字面量

(int []){50, 20, 90} // 内含3个元素的复合字面量

去掉声明中的数组名,留下的int [2]即是复合字面量的类型名。

初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数。

因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它。使用指针记录地址就是一种用法。

int * pt1;
pt1 = (int [2]) {10, 20};

还可以把复合字面量作为实际参数传递给带有匹配形式参数的函数。

int sum(const int ar[], int n);
...
int total3;
total3 = sum((int []){4,4,4,5,5,5}, 6);

把信息传入函数前不必先创建数组,这是复合字面量的典型用法。

// flc.c -- 有趣的常量
#include <stdio.h>
#define COLS 4
int sum2d(const int ar[][COLS], int rows);
int sum(const int ar[], int n);
int main(void)
{
int total1, total2, total3;
int * pt1;
int(*pt2)[COLS];
pt1 = (int[2]) { 10, 20 };
pt2 = (int[2][COLS]) { {1, 2, 3, -9}, { 4, 5, 6, -8 } };
total1 = sum(pt1, 2);
total2 = sum2d(pt2, 2);
total3 = sum((int []){ 4, 4, 4, 5, 5, 5 }, 6);
printf("total1 = %d\n", total1);
printf("total2 = %d\n", total2);
printf("total3 = %d\n", total3);
return 0;
}
int sum(const int ar [], int n)
{
int i;
int total = 0;
    for (i = 0; i < n; i++)
        total += ar[i];
return total;
}
int sum2d(const int ar [][COLS], int rows)
{
int r;
int c;
int tot = 0;
for (r = 0; r < rows; r++)
    for (c = 0; c < COLS; c++)
        tot += ar[r][c];
return tot;
}

运行结果:

total1 = 30

total2 = 4

total3 = 27

复合字面量是提供只临时需要的值的一种手段。复合字面量具有块作用域,这意味着一旦离开定义复合字面量的块,程序将无法保证该字面量是否存在。也就是说,复合字面量的定义在最内层的花括号中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HaGoq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值