10. 数组和指针


一、数组

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

  • 需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型。

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

  • 方括号([])表明 candy、code 和 states 都是数组,方括号中的数字表明数组中的元素个数。

    如,float candy[365]; // 内含 365 个 float 类型元素的数组

  • 要访问数组中的元素,通过使用数组下标数(也称为索引)表示数组中的各元素。

  • 数组元素的编号从 0 开始,所以 candy[0] 表示 candy 数组的第 1 个元素,candy[364] 表示第 365 个元素,也就是最后一个元素。

1.1 初始化数组

  • 只储存单个值的变量有时也称为标量变量。

  • 如,int nums[8] = {1, 2, 3, 4, 5, 6, 7, 8};

    用以逗号分隔的值列表(用花括号括起来)来初始化数组, 各值之间用逗号分隔。在逗号和值之间可以使用空格。根据上面的初始化, 把 1 赋给数组的首元素(powers[0]),以此类推推

  • 当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为 0。

  • 如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值;如果部分初始化数组,剩余的元素就会被初始化为 0。

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

  • sizeof days 是整个数组的大小,sizeof days[0] 是数组中一个元素的大小(以字节为单位)。

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

  • 使用 const 声明数组

    • 用 const 声明和初始化数组的话,这个数组就是只读数组,程序在运行过程中不能修改数组中的内容。
    • 和普通变量一样,应该使用声明来初始化 const 数据。
    • 使用数组前必须先初始化它。与普通变量类似,在使用数组元素之前,必须先给它们赋初值。
    • 译器使用的值是内存相应位置上的现有值,系统不同,输出的结果可能不同。
    • 初始化列表中的项数应与数组的大小一致。
  • 注意 存储类别警告

    • 数组和其他变量类似,可以把数组创建成不同的存储类别。
    • 不同的存储类别有不同的属性,所以不能把本章的内容推广到其他存储类别。
    • 对于一些其他存储类别的变量和数组,如果在声明时未初始化,编译器会自动把它们的值设置为0。

1.2 指定初始化器(C99)

  • C99 增加了一个新特性:指定初始化器,利用该特性可以初始化指定的数组元。

  • C99 规定,可以在初始化列表中使用带方括号的小标指明待初始化的元素。

    如, int arr[6] = {[5] = 212}; // 把 arr[5] 初始化为 212

  • 对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为 0。

    #include <stdio.h>                                             输出:1    31
    #define MONTHS 12                                                   2    29
                                                                        3    0
    int main()                                                          4    0
    {                                                                   5    31
        int days[MONTHS] = {31, 28, [4] = 31, 30, 31, [1] = 29};        6    30
        int i;                                                          7    31
                                                                        8    0
        for (i = 0; i < MONTHS; i++)                                    9    0
            printf("%2d %d\n", i + 1, days[i]);                         10   0
                                                                        11   0 
        return 0;                                                       12   0
    }                                                                   13   0
    /*如果指定初始化器后面有更多的值,那么后面的这些值将被用于初始化指定元素后面的元素
    [4] = 31, 30, 31,在 days[4] 被初始化为 31 后,days[5] 和 days[6] 将分别被初始化为 30 和 31
    如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化
    
  • 如果没有指定元素的大小,编译器会把数组的大小设置为足够装得下初始化的值。

    int stuff[] = {1, [6] = 23}; 
    int staff[] = {1, [6] = 4, 9, 10}; 
    //stuff数组 有7个元素,编号为0~6;而staff数组有9个元素)
    

1.3 给数组元素赋值

  • 声明数组后,可以借助数组下标(或索引)给数组元素赋值。
  • C 不允许把数组作为一个单元赋给另一个数组,除初始化外也不允许使用花括号列表的形式赋值。

1.4 数组边界

  • 在使用数组时,要防止数组下标超出边界。也就是说,必须确保下标是有效的值。
  • 编译器不会检查数组下标是否使用得当。在C标准中,使用越界下标的结果是未定义的。这意味着程序看上去可以运行,但是运行结果很奇怪,或异常中止。
  • 如果编译器在运行时添加额外代码检查数组的每个下标值,这会降低程序的运行速度。
  • 数组元素的编号从 0 开始,最好是在声明数组时使用符号常量来表示数组的大小。

1.5 指定数组的大小

  • 在 C99 标准之前,声明数组时只能在方括号中使用整型常量表达式。
  • 整型常量表达式,是由整型常量构成的表达式。
  • sizeof 表达式被视为整型常量,但是 const 值不是(与 C++ 不同)。
  • 表达式的值必须大于 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之前不允许
  • C99 标准允许后两种的声明,这创建了一种新型数组,称为变长数组或简称 VLA(C11 把 VLA 设定为可选,而不是语言必备的特性)。
  • 声明 VLA 时不能进行初始化。

二、多维数组

/* rain.c -- 计算每年的总降水量、年平均降水量和5年中每月的平均降水量 */ 
#include <stdio.h> 660
#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 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"); 
return 0; }
  • 程序使用了两个嵌套for循环。第1个嵌套for循环的内层循环,在year不 变的情况下,遍历month计算某年的总降水量;而外层循环,改变year的 值,重复遍历month,计算5年的总降水量。这种嵌套循环结构常用于处理二 维数组,一个循环处理数组的第1个下标,另一个循环处理数组的第2个下标。
  • 第2个嵌套for循环和第1个的结构相同,但是内层循环遍历year,外层循 环遍历month。记住,每执行一次外层循环,就完整遍历一次内层循环。因 此,在改变月份之前,先遍历完年,得到某月 5 年间的平均降水量,以此类推

2.1 初始化二维数组

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

  • 初始化二维数组 dates(第6行-第11行)

    • 这个初始化使用了 5 个数值列表,每个数值列表都用花括号括起来。

    • 如果某列表中的数值个数超出了数组每行的元素个数,则会出错,但是并不会影响其他行的初始化。

    • 初始化时可以省略内部的花括号,只保留最外面的一堆花括号,要保证初始化的数值个数正确。

    • 如果初始化的数值不够,则按照先后顺序逐行初始化,直到用完所有的值,后面没有值初始化的元素被统一初始化为 0。

2.2 其他多维数组

  • 声明一个三维数组:int nums[10][20][30];

    nums 内含 10 个元素,每个元素是内含 20 个元素的数组,这 20 个数组元素中的每个元素是内含 30 个元素的数组。

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


三、指针介绍(原9.7)

  • 指针是一个值为内存地址 的变量(或数据对象)。

  • 指针变量的值是地址,正如char 类型变量的值是字符,int 类型变量的值是整数。

  • 要创建指针变量,先要声明指针变量的类型。

  • 假设一个指针变量名是 ptr,

    对:ptr = &pooh; //把 pooh 的地址赋给 ptr。

    对于这条语句,我们说 ptr “指向” poohptr 和 &pooh 的区别是 ptr 是变量,而 &pooh 是常量。

3.1 间接运算符:*

  • 地址运算符:&

    • 一般注解:后跟一个变量名时,&给出该变量的地址。

    • 示例:&nurse 表示变量 nurse 的地址

  • 地址运算符:*****

    • 一般注解: 后跟一个指针名或地址时,*给出储存在指针指向地址上的值。

    • 注意:不要把间接运算符和二元乘法运算符(*)混淆,虽然它们使用的符号相同,但语法功能不同。

    • 示例:

    int pooh = 24;
    int * ptr;    // 声明一个指针变量 ptr
    int val;
    ptr = &pooh;  // 把 pooh 的地址赋给 ptr
    val = * ptr;  // 把 ptr 指向的值赋给变量 val
    printf("%d\n", val);
    //输出:24
    //语句 ptr = &pooh; 和 val = * ptr; 放在一起相当于语句:val = pooh
    

3.2 声明指针

  • 声明指针必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。

  • 程序必须知道储存在指定地址上的数据类型。long 和 float 可能占用相同的存储空间,但是它们储存数字却大相径庭。

    int * pi;   // pi 是指向 int 类型变量的指针
    char * pc;  // pc 是指向 char 类型变量的指针
    float * pf, * pg;  // pf、pg 都是指向 float 类型变量的指针
    
  • 类型说明符表明了指针所指向对象的类型,星号()表明声明的变量是一个指针。
    如:int * pi;,声明的意思是 pi 是一个指针,
    pi 是 int 类型

  • * 和指针名之间的空格可有可无。通常,在声明时使用空格,在解引用变量时省略空格。

  • 指针实际上是一个新类型,不是整数类型。

3.3 使用指针在函数间通信

//程序清单9.15 swap3.c程序 
//swap3.c -- 使用指针解决交换函数的问题  
#include <stdio.h> 
void interchange(int * u, int * v); //该函数传递的不是x和y的值,而是它们的地址
int main(void) 
{
    int x = 5, y = 10; 
    printf("Originally x = %d and y = %d.\n", x, y); 
    interchange(&x, &y); // 把地址发送给函数
    printf("Now x = %d and y = %d.\n", x, y); 
    return 0; 
}
void interchange(int * u, int * v) 
{
    int temp; 
    temp = *u; // temp获得 u 所指向对象的值。u的值是&x,所以u指向x。
    //不要写temp = u;,这条语句赋给temp的是x的地址,不是x的值。函数要交换的是x和y的值,不是地址。
    *u = *v; //这条语句相当于: x = y;
    *v = temp; 
}
  • **void interchange(int , int );

    • 一般而言,可以把变量相关的两类信息传递给函数。

    • 第1种,function1(x); ,传递的是x的值。

    • 第2种,function2(&x);,传递的是x的地址。

    • 第1种形式要求函数定义中的形式参数必须是一个与x的类型相同的变量:int function1(int num)

      第2种形式要求函数定义中的形式参数必须是一个指向正确类型的指 针:int function2(int * ptr)

    • 如果要计算或处理值,那么使用第 1 种形式的函数调用;

      如果要在被调函数中改变主调函数的变量,则使用第2种形式的函数调用。

  • 变量:名称、地址和值

    • 变量的名称、地址和变量的值之间关系密切。

    • 编写程序时,可以认为变量有两个属性:名称和值。

      计算机编译和加载程序后,认为变量也有两个属性:地址和值。

    • 在许多语言中,地址都归计算机管,对程序员隐藏。然而在 C 中,可以通过&运算符访问地址,通过*运算符获得地址上的值。

    • 简而言之,普通变量把值作为基本量,把地址作为通过&运算符获得的 派生量,而指针变量把地址作为基本量,把值作为通过*运算符获得的派生量。


四、指针和数组

  • 数组表示法其实是在变相地使用指针。

  • 数组名是数组首元素的地址。即,如果 flizny 是一个数组,下面的语句成立:

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

    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); 
    
    return 0; 
}
/* 输出:          short          double 
pointers + 0: 0x7fff5fbff8dc 0x7fff5fbff8a0 
pointers + 1: 0x7fff5fbff8de 0x7fff5fbff8a8 
pointers + 2: 0x7fff5fbff8e0 0x7fff5fbff8b0 
pointers + 3: 0x7fff5fbff8e2 0x7fff5fbff8b8
  • 在 C 中,指针加 1 指的是增加一个存储单元。对数组而言,这意味着加 1 后的地址是下一个元素的地址,而不是下一个字节的地址。
  • 指针指向的是标量变量,也要知道变量的类型,否则 *pti 就无法正确地取回地址上的值。
  • 指针的值是它所指向对象的地址。地址的表达方式依赖于计算机内部的硬件。许多计算机都是按字节编址,意思是内存中的每个字节都按顺序编号。一个较大对象的地址通常是该对象第一个字节的地址。
  • 在指针前面使用 * 运算符可以得到该指针所指向对象的值。
  • 指针加 1,指针的值递增它所指向类型的大小(以字节为单位)

  • 以上关系表明了数组和指针的关系十分密切,可以使用指针标识数组的元素和获得元素的值。从本质上看,同一个对象有两种表示法。实际上,C 语言标准在描述数组表示法时确实借助了指针。
  • 定义 ar[n] 的意思是 (ar + n),(ar + n) 的意思是“到内存的 ar 位置,然后移动 n 个单元”
  • 间接运算符()的优先级高于 +,所以 days+2 相当于 (days) + 2,(days + 2) 是 days 第 3 个元素的值,*days + 2 是 days 第 1 个元素的值加 2
//程序清单10.9 day_mon3.c程序 
// 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]相同 
    return 0; 
}
/* 输出:Month 1 has 31 days.
        Month 2 has 28 days.
        Month 3 has 31 days.
        ......   
        Month 11 has 30 days.
        Month 12 has 31 days.        
这里,days是数组首元素的地址,days + index是元素days[index]的地址,而*(days + index)则是该元素的值,相当于days[index]。
for循环依次引用数组中的每个元素,并打印各元素的内容。
posted @ 2022-02-13 00:12  fidelity_233  阅读(132)  评论(0)    收藏  举报