C Primer Plus 学习笔记 第10章 数组和指针

这一章连续出现数组和指针两个大的。

全书共分17章,这是关于本书第10章内容的博客。本章介绍了关于数组和指针的内容。虽然前面一些地方已经有了一些关于数组和指针的内容,但是这一章将更详细地介绍。博客的目录和书上目录是相似的。此系列博客的代码都在Visual Studio 2022环境下编译运行。

我目前大一刚刚结束,水平有限,博客中若有错误或者总结不到位的地方也请见谅。

目录

10.1 数组

10.1.1 初始化数组

10.1.2 指定初始化器(C99)

10.1.3 给数组元素赋值

10.1.4 数组边界

10.1.5 指定数组的大小

10.2 多维数组

10.2.1 初始化二维数组

10.2.2 其他多维数组

10.3 指针和数组

10.4 函数、数组和指针

10.4.1 使用指针形参

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

10.5 指针操作

10.6 保护数组中的数据

10.6.1 对形式参数使用const

10.6.2 const的其他内容

10.7 指针和多维数组

10.7.1 指向多维数组的指针

10.7.2 指针的兼容性

10.7.3 函数和多维数组

10.8 变长数组(VLA)

10.9 复合字面量


10.1 数组

数组由数据类型相同的一系列元素组成。

声明数组的格式是:

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

方括号表明这是一个数组。

要访问数组中的元素,通过数组下标表示。下标从0开始,到个数-1为止。

10.1.1 初始化数组

用花括号括起来的,以逗号分隔的值列表初始化数组,各个值之间用逗号分隔。第一个值赋给首元素,第二个值赋给第二个元素,以此类推。

下面的程序初始化一个数组。

#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]);
	return 0;
}

有时候需要把数组设置为只读,程序只能从数组中检索值,不能修改数组中的元素。此时需要用const声明和初始化数组。用const修饰的变量应该在声明后立即初始化。

使用数组元素前必须赋初始值,否则数组元素的值是随机值。

初始化列表的项数应该与数组的大小一致。如果初始化列表的值少于数组元素的个数(称为部分初始化),剩余元素会被初始化为0。如果初始化列表的项数多于数组元素的个数,编译器会视为错误。

如果初始化数组时省略方括号中的数字,编译器会自动匹配数组大小和初始化列表中的项数。

整个数组大小除以单个元素的大小就是数组元素的个数。

10.1.2 指定初始化器(C99)

指定初始化器可以初始化指定的数组元素。可以在初始化列表中使用带方括号的下标指明待初始化的元素。如果指定初始化器后面有更多的值,后面的值会被用于初始化指定元素后面的元素。如果再次初始化指定元素,那么最后的初始化会取代前面的初始化。

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

下面程序用指定初始化器初始化数组。

#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]);
	return 0;
}

10.1.3 给数组元素赋值

声明数组后,也可以不初始化,用下标给数组元素赋值。

C语言不允许把数组作为一个单元赋值给另一个数组,即使这两个数组类型大小均相同。

除初始化外,不允许使用花括号列表的方式赋值。

10.1.4 数组边界

使用数组时要防止下标超出边界,下标必须在范围内。使用越界下标的行为是未定义的,可能导致程序异常终止。

10.1.5 指定数组的大小

C99之前声明数组时方括号中只能用整型常量表达式。整型常量表达式是由整型常量构成的表达式,sizeof表达式被视为整型常量。但是const修饰的整型变量不是。表达式的值必须大于0。

C99允许在方括号中使用整型常量,创建了一种新的数组,称为变长数组,简称VLA。C11把VLA设定为可选。VS中不能在方括号中使用整型变量。

10.2 多维数组

二维数组是数组的数组,声明二维数组的方式是:

元素类型名 数组名[行数][列数]

二维数组可以理解为是一个数量是行数的数组,数组的每个元素是有列数个元素,元素是类型名类型的数组。

二位数组是按顺序存储的,从第一个有列数个元素的数组开始,到最后一个有列数个元素的数组。

访问二维数组的元素通常使用嵌套循环,一个循环处理第一个下标,另一个循环处理第二个下标。

这是一个使用二维数组的例子,用二维数组完成一些计算。

#include<stdio.h>
#define MONTHS 12
#define YEARS 5
int main(void)
{
	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;
	}

	printf("\nThe yearly average is %.1f inches.\n\n", total / YEARS);
	printf("MONTHLY AVERAGE:\n\n");
	printf(" Jan Feb Mar Apr May Jun Jul Aug Sep Oct ");
	printf("Nov Dec\n");
	for (month = 0; month < MONTHS; month++)
	{
		for (year = 0, subtot = 0; year < YEARS; year++)
			subtot += rain[year][month];
		printf("%4.1f", subtot / YEARS);
	}
	printf("\n");
	return 0;
}

10.2.1 初始化二维数组

初始化二维数组建立在初始化一维数组的基础上。

可以用花括号括起来,里面用逗号分隔,每部分再用花括号括起来,里面是逗号分隔的值,相当于对几个一维数组进行初始化。此时如果某列表的元素个数小于列数,则此一维数组的其余元素被赋值为0。如果某列表的数值个数超出了列数,会出错但不影响其他行。

初始化也可以省略内部的花括号,编译器将按顺序赋值。如果数量不够,剩余元素会赋值为0。

10.2.2 其他多维数组

可以声明三维数组等维数更多的数组,此时数组名后有多个方括号。此时要使用多重循环。

10.3 指针和数组

计算机的硬件指令非常依赖地址,指针在某种程度上将指令以更接近机器的方式表达,因此更有效率。

指针能有效地处理数组,数组表示法其实是变相使用指针。

数组名就是数组首元素的地址。数组名是常量,不会改变值,但是可以赋值给指针变量。

指针加1加一个存储单元,加1后的地址是下一个元素的地址,而不是下一个字节的地址。

指针的值是指向对象的地址,地址的表示方式依赖于硬件,一般是按字节编址,每个字节按顺序编号。一个较大对象的地址是该对象第一个字节的地址。

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

数组和指针的关系很密切,可以用指针标识数组的元素和获得元素的值。

下面程序用指针表示法访问数组。

#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));
	return 0;
}

数组表示法和指针表示法是等效的。

10.4 函数、数组和指针

如果要编写处理数组的函数,实参是数组名。此时形参应该是一个指针,指针指向元素的类型应该和数组元素的类型相同。这种函数最好加一个形参存储数组的大小。

另外只有在函数原型和函数定义头中,才可以在数组名后加[]代替*数组名。这两种方法等价。

下面程序用函数访问数组,函数返回数组元素和。

#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);
	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;
}

10.4.1 使用指针形参

函数处理数组要知道合适开始何时结束,除了传一个变量表示数组大小外,还可以传一个指针,该指针指明数组的结束处。

C语言保证给数组分配空间时,指向数组后面第一个位置的指针仍然有效,但是不能访问该位置。

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

处理数组的函数实际使用指针作为参数,但是可以选择使用数组表示法或指针表示法。

数组表示法让函数处理数组的意图更明显,使用指针表示法可能更自然。指针表示法更接近机器语言。

10.5 指针操作

C语言提供了一些指针操作。

可以用关系运算符比较指针,但两个指针应指向相同类型的对象。

可以把地址赋给指针,可以用数组名、带&的变量名、另一个指针进行赋值。地址要和指针类型兼容。

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

指针变量也有自己的地址和值。&运算符给出指针本身的地址。

可以把指针和整数相加相减(相减的情况下指针必须在左侧),整数会和指针指向类型的大小相乘,然后把结果和地址相加减。如果相加减的结果超出了数组的范围,结果是未定义的。

对指针可以使用递增递减,前缀后缀都可以。

可以计算两个指针的差值,求差的两个指针一般指向同一个数组的不同元素。差值就是两个指针间隔元素的个数。差值的单位是数组类型的单位。如果两个指针指向不同数组可能导致错误。

一定不要解引用未初始化的指针,否则可能导致很严重的错误。创建指针时系统只分配了存储指针本身的内存,没有分配存储数据的内存。使用指针之前必须先初始化。

10.6 保护数组中的数据

编写处理基本类型的函数时,通常直接传递数值,只有程序中需要在函数中改变数值时,才会传递指针。

处理数组时一般传递指针,这样效率高。但是这样可能意外修改原数组中的值。

10.6.1 对形式参数使用const

如果不需要修改数组中的数据内容,在函数原型和函数定义头声明形式参数时要使用关键字const。使用const后不能在函数中修改数组元素的值。此时const要求函数在处理数组时视为常量。是否使用const应根据是否需要修改而定。

10.6.2 const的其他内容

指向const的指针不能用于改变值,但是可以指向别处。指向const的指针通常用在函数形参中,表明函数不会通过指针改变数据。

把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的。但是只能把非const数据的地址赋给普通指针(但是VS可以,并且可以通过指针修改原数据)。

使用非const标识符修改const数据的结果是未定义的。

可以声明并初始化一个不能指向别处的指针,将const放在*和指针名中间。但是此时可以通过指针更改指向变量的值。

创建指针可以使用const两次,使指针既不能修改指向的地址,又不能修改指向地址上的值。

10.7 指针和多维数组

处理多维数组的函数也要用到指针。增加数组的维数会增加指针的复杂度,需要多重指针。

得到二维数组中某元素的值需要使用两个方括号或使用两个*。此时最好使用数组表示法。

10.7.1 指向多维数组的指针

声明指向一维数组的指针的格式是:

数组元素类型  (* 指针名)  [元素个数]

使用括号的原因是[]的优先级高于*,如果不带括号,表示的是包含方括号中数量个指向前面类型的指针的数组。

可以用数组表示法或指针表示法表示一个数组元素,可以使用数组名,也可以使用指针名。

10.7.2 指针的兼容性

指针之间的赋值比数值类型之间的赋值要严格。指向不同类型元素的指针不能赋值。

把const指针赋给非const指针不安全,用非const指针更改const数据的行为是未定义的。把非const类型指针赋给const指针没有问题,前提是只是用一级解引用。

10.7.3 函数和多维数组

下面程序使用几个函数处理二维数组,形参的表示方法有所区别。

#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 }
	};

	sum_rows(junk, ROWS);
	sum_cols(junk, ROWS);
	printf("Sum of all elements = %d\n", sum2d(junk, ROWS));
	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;
}

声明多重数组时,只有第一个方括号的数字可以省略。

10.8 变长数组(VLA)

变长数组可以用变量表示数组的维度。变长数组必须是自动存储类别,不能在声明中初始化。

C11中变长数组是可选的,VS不支持变长数组。VS中声明数组必须用常量。VS也不允许声明数组时的方括号中是const修饰的变量。

10.9 复合字面量

C99标准新增了复合字面量,字面量是指除明示常量外的常量。

对于数组,复合字面量类似数组初始化列表,前面是用括号括起来的类型名。

可以用复合字面量创建匿名数组。格式是:

(数组元素类型 [元素个数]) {初始化列表}

复合字面量也可以省略大小,编译器会计算数组当前元素个数。

复合字面量是匿名的,不能先创建再使用,必须在创建的同时使用,比如可以赋给一个指针,也可以作为一个实参。

复合字面量是提供临时需要值的一个手段,具有块作用域。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值