05. C语言数组

news/2024/10/11 6:33:59


数组用于将多个数据集中存储,方便管理,此文将集中存储任何类型数据的语句都称为数组,数组根据存储数据的类型和方式分为同型数组、结构体、共用体、枚举。

 


【同型数组】

同型数组也直接称为数组,用于存储多个类型相同的数据,数组内的数据称为数组元素,数组元素占用连续的虚拟地址,每个元素都有一个数组内部编号,称为数组下标,数组元素通过“数组名+下标”的方式调用,下标从0开始分配,第一个元素下标为0。

#include <stdio.h>
int main()
{
    int a[5] = {1,2,3,4,5};      //int指定元素类型,a为数组名,[]符号内指定元素个数,{}符号内设置数组元素的值
    a[0] = 0;                    //使用“数组名[下标]”调用数组元素
    
    int b[5] = {1,2,3};          //只为部分元素赋值,未赋值的元素编译器自动赋值为0
    
    int c[] = {1,2,3,4,5};       //定义数组并赋值,可以不指定长度,编译器以赋值元素数量确定数组长度
    
    int d[5];                    //定义数组不赋值,但是必须指定数组长度
    
    const int e[3] = {1,2,3};    //常量数组,不能修改内部元素
    
    return 0;
}

注意:数组下标从0开始计算,数组元素数量从1开始计算,比如 int a[5],数组a有5个元素,下标最大为4。


调用数组元素时,下标可以使用常量、变量两种数据指定,变量的值可以在程序执行期间临时确定,默认情况下两种方式都不会进行数组越界访问检查,若程序需要使用变量指定数组下标,则应该首先判断变量的值,防止超过数组长度导致越界访问。

#include <stdio.h>
int main()
{
    int a[5] = {1,2,3,4,5};
    unsigned int b;
    
    printf("输入访问下标\n");
    scanf("%d",&b);            //终端输入函数,输入b的值,以回车结束
    
    /* 数组下标从0开始,下标上限为数组长度-1 */
    if(b > 4)
    {
        printf("禁止越界访问\n");
    }
    else
    {
        printf("a[%d]的值为%d\n", b, a[b]);
    }
    
    return 0;
}

使用变量作为下标时,编译器使用如下指令调用数组元素:

mov  edx, DWORD PTR [rbp+rax*4-0x20]

rbp-0x20 为数组的虚拟地址,rax为数组下标,4为数组元素长度,rax乘以4定位到元素所在数组的内部地址,rbp-0x20+rax*4定位到数组元素的虚拟地址。

多维数组

数组可以嵌套定义,在一个数组中存储其它数组,这种数组称为多维数组,多维数组存储的其它数组的长度和元素类型需要相同。
二维数组的元素是一维数组,三维数组的元素是二维数组,以此类推,还可以有四维数组。

#include <stdio.h>
int main()
{
    /* 定义二维数组,其中包含3个一维数组,一维数组的长度为5 */
    int a[3][5] =
    {
        {1, 2, 3, 4, 5},
        {11, 12, 13, 14, 15},
        {21, 22, 23, 24, 25}
    };
    
    /* 二维数组使用两个下标调用元素,这里调用第1个一维数组的第2个元素 */
    a[0][1] = 0;
    
    /* 定义三维数组,内部包含3个二维数组,每个二维数组包含4个一维数组,每个一维数组包含5个元素 */
    int b[3][4][5] =
    {
        {{0,0,0,0,0}, {0,0,0,0,0}, {0,0,0,0,0}, {0,0,0,0,0}}, 
        {{1,1,1,1,1}, {1,1,1,1,1}, {1,1,1,1,1}, {1,1,1,1,1}},
        {{2,2,2,2,2}, {2,2,2,2,2}, {2,2,2,2,2}, {2,2,2,2,2}}
    };
    
    /* 调用三维数组元素 */
    b[2][3][4] = 0;    return 0;
}

有些教材将二维数组比喻为一张表,将三维数组比喻为一个立体结构,其实这样解释是不准确的,甚至基于这个思路就无法理解三维以上的数组。
二维数组将多个一维数组集中存储,三维数组将多个二维数组集中存储,四维数组将多个三维数组集中存储,内存单元并没有分为平面结构和立体结构,多维数组的存储也是线性的,所有的数组元素全部依次排列在虚拟地址中。

比如定义一个二维数组:

int a[3][3] = {{1,2,3},{4,5,6},{7,8,9}};

内部元素的存储顺序如下:

a[0][0]
a[0][1]
a[0][2]
a[1][0]
a[1][1]
a[1][2]
a[2][0]
a[2][1]
a[2][2]

使用变量调用二维数组元素

#include <stdio.h>
int main()
{
    int a[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}};
    unsigned int b,c;
    scanf("%u%u", &b, &c);      //输入变量b、c的值
    
    if(b<3 && c<3)
    {
        printf("元素值为:%d\n", a[b][c]);    //使用两个变量调用数组元素
    }
    
    return 0;
}

调用二维数组元素步骤如下:
1.变量b乘以12,定位到二维数组内部地址。
2.变量c乘以4,定位到一维数组内部地址。
3.二维数组地址+b+c。

汇编代码如下:

mov    eax,DWORD PTR [rsp+0x8]           ;变量b写入eax
mov    edx,DWORD PTR [rsp+0xc]           ;变量c写入edx
lea    rax,[rax+rax*2]                   ;b*3
add    rax,rdx                           ;b+c
mov    esi,DWORD PTR [rsp+rax*4+0x10]    ;rsp+0x10为二维数组地址,rax*4为元素占用的数组内部地址

编译器对调用二维数组元素的复杂数学运算进行了优化,首先b乘以3,之后与c相加,此时b和c都需要再乘以4,所以让b+c的结果统一乘以4,再与二维数组地址相加得出元素具体地址。

 


【字符串】

存储字符编码的char类型数组称为字符串,字符串变量的元素经常需要修改,不同时期的值会占用字符串不同的长度,为了确定字符串使用了哪些数组元素,C语言规定字符串需要在已用元素之后添加一个空字符(字符编码为0),程序通过空字符的位置确定字符串有效字符长度。

空字符只在程序内部使用,将字符串存储在文件中时一般不存储空字符。

#include <stdio.h>
int main()
{char a[10] = "ali";    //使用多个字符赋值,字符放在""符号内,编译器转换为对应的UTF8编码,未赋值元素自动赋值为0char b[3] = "ali";     //错误,没有剩余位置放置空字符char c[50] = "阿狸\0喜羊羊";    //\0表示空字符printf("%s\n", c);            //输出“阿狸”,空字符及之后的字符不会使用char d[10] = {1, 2, 3};       //为元素单独赋值,一般表示当做普通数组,而非字符串return 0;
}

若字符串的长度太大,不方便单行存储,可以使用多行存储,有以下两种方式。

#include <stdio.h>
int main()
{
    /* 使用\符号换行存储,\符号以及换行字符不算入字符串元素,但是多行之间的空格算入字符串,此方式需要保证多行之间没有空格 */
    char a[50] = "阿狸\
喜羊羊";
    
    /* 使用多组""符号换行存储,多组""符号之间的换行和空格都不算入字符串 */
    char b[50] = "阿狸"
    "喜羊羊";
    
    return 0;
}

转义字符

有些字符起控制作用,不用于显示,比如回车、换行,这些字符无法直接定义,可以使用转义字符表示。
有些字符与C语言关键词重复,比如 \ ' " ? 符号,这些字符也不能直接编写,需要使用转义字符表示。

转义字符以\符号开始(注意不是/符号),之后定义转义字符类型,常用转义字符如下:

\r,表示回车,将打印位置移动到最左侧
\n,表示换行,将打印位置向下移动一行
\b,表示退格,将打印位置向左移动一个字符
\0,表示空字符
\t,表示水平空白
\v,表示垂直空白
\',表示 ' 符号
\",表示 " 符号
\?,表示 ? 符号
\\,表示 \ 符号

#include <stdio.h>
int main()
{
    char a[] = "ali\n";    //"ali\n"会被编译器转换为如下字符编码:97,108,105,10,0
    printf(a);             //输出ali并换行
    
    return 0;
}

 


【结构体】

结构体用于存储一组类型不同的数据,可以理解为异型数组,因为结构体成员的类型不同、长度不同,所以结构体成员不能使用下标的方式调用,每个结构体成员都有自己的名称,使用“结构体名+成员名”的方式调用结构体成员。

编写代码时经常需要使用多个成员类型相同的结构体,比如存储一个班级学生的属性信息,此时需要使用几十个内部成员相同的结构体,为了简化结构体定义代码,C语言将结构体的定义分为两个部分:声明部分、实体部分。
1.声明部分,定义结构体成员的类型、名称,声明是一种伪指令,不会被编译为任何数据,作用只是简化定义多个成员类型相同的结构体、无需重复指定成员类型、成员名称,此部分也称为结构体类型,表示其用于确定结构体的整体长度以及内部成员的类型。
2.实体部分,根据声明部分定义具体的结构体,也称为结构体实例。

#include <stdio.h>
int main()
{/* 使用struct关键词定义结构体的声明部分和实体部分,zoo为声明部分的名称,成员的类型和名称放在{}符号内,声明语句以;符号结尾 */struct zoo{char name[50];      //学生姓名char gender[10];    //学生性别float score;        //考试分数int rank;           //考试排名};struct zoo ali = {"阿狸", "男", 90, 3};    //使用声明定义结构体实例ali.score = 91;                           //使用“实例名.成员名”调用内部成员struct zoo taozi = {"桃子", "女", 92};     //只为部分成员赋值时,剩余成员编译器默认赋值为0,这一点与同型数组相同const struct zoo xyy = {"喜羊羊", "男", 95, 1};   //常量结构体,定义时赋值,之后不能修改struct zoo zoo[3] = {ali, taozi, xyy};           //结构体作为数组元素printf("%s\n", zoo[0].name);                     //调用方式struct zoo x = ali;    //结构体可以引用其它同类型实例赋值x = xyy;               //结构体可以整体修改,若结构体中定义了常量成员则禁止整体修改return 0;
}


可以使用typedef关键词为结构体类型设置另一个名称。

#include <stdio.h>
int main()
{
    typedef struct zoo
    {
        char name[50];
        int age;
    } student;
    
    student ali = {"阿狸", 8};
    student xyy = {"喜羊羊", 9};
    
    return 0;
}

匿名结构体

声明部分可以不设置名称,称为匿名结构体类型,匿名结构体类型需要在声明时定义所有实例,之后不能再定义新的实例。

#include <stdio.h>
int main()
{
    struct
    {
        char name[50];
        int age;
    } ali={"阿狸", 8}, xyy={"喜羊羊", 9};    return 0;
}

有名称的结构体类型也可以使用这种简化方式定义实例。

嵌套结构体

结构体内部可以定义另一个结构体实例,这种功能很好理解,另外结构体的声明部分也可以嵌套定义,从而将结构体内的数据继续分类管理。

#include <stdio.h>
int main()
{
    struct zoo
    {
        char name[50];      //姓名
        char gender[10];    //性别
        
        /* grades记录各科考试成绩,嵌套结构体声明需要直接定义出所有实例,确定占位长度,但是不能在内部直接赋值 */
        struct grades
        {
            float score;   //分数
            int rank;      //排名
        } math, chinese, english;
    };
    
    struct zoo ali = {"阿狸", "男", {90,1}, {80,2}, {70, 3}};
    
    printf("阿狸的数学分数为:%f\n", ali.math.score);
    
    return 0;
}

功能等同于如下代码,只不过下面代码中的 struct grades 是公用的,struct zoo 之外的代码也可以使用。

#include <stdio.h>
int main()
{
    struct grades
    {
        float score;
        int rank;
    };
    
    struct zoo
    {
        char name[50];
        char gender[10];
        
        struct grades math, chinese, english;
    };
    
    struct zoo ali = {"阿狸", "男", {90,1}, {80,2}, {70, 3}};
    
    printf("阿狸的数学分数为:%f\n", ali.math.score);
    
    return 0;
}

结构体的实际长度

结构体在内存中存储时的长度并非所有成员长度的总和,因为结构体成员的长度不同,地址对齐值也不同,多个成员中间往往会有地址对齐额外占用的内存单元,所以结构体的实际长度会大于内部成员长度的总和,可以使用sizeof查询结构体的长度。

#include <stdio.h>
int main()
{
    struct k
    {
        int i;
        char c;
    };
    
    printf("k的长度为%d字节\n", sizeof(struct k));    //在x86-64计算机中k类型的长度为8字节
    
    return 0;
}

实现数组整体修改

C语言规定数组只能在定义时整体赋值,之后不能整体修改,两个数组之间也不能引用赋值,即使两个数组的元素类型、元素数量都相同也不行,但是同类型的结构体实例之间可以互相引用赋值,若需要整体修改数组,除了使用库函数或者使用循环代码外还可以将数组放在结构体内。

#include <stdio.h>
int main()
{
    typedef struct
    {
        char name[50];
    } string;
    
    /* 注意:实际项目中数组成员赋值应该做越界访问检查,这里简化代码,不做检查 */
    
    string ali = {"阿狸"};
    string xyy = {"喜羊羊"};
    
    string x = ali;
    printf("%s\n", x.name);    //输出阿狸
    
    x = xyy;
    printf("%s\n", x.name);    //输出喜羊羊
    
    return 0;
}

变长数组成员

结构体内的数组成员可以在声明时不指定长度,而是在定义实例时确定长度,长度可以使用变量指定,若多个同类型结构体实例为变长数组成员设置了不同的长度,则他们本质上是不同类型的结构体实例,他们的长度不同,他们之间互相引用赋值时会产生错误。

使用注意事项:
1.变长数组成员不能单独出现,否则结构体长度默认为0,不能编译。
2.变长数组成员最多定义一个,并且只能定义在结构体成员的末尾。
3.使用sizeof计算结构体长度时,变长数组成员不计入结构体长度。

此类结构体实例不能定义为函数局部数据,不能存储在栈中,存储方式如下:
1.若变长数组成员可以在编译期间确定长度,可以将结构体实例定义为全局成员、静态局部成员,放在全局数据区存储。
2.若变长数组成员不能在编译期间确定长度,需要在程序执行期间向操作系统申请内存存储,注意结构体总长度需要加上变长数组成员长度。

#include <stdio.h>
struct zoo
{
    int age;
    char name[];
};
struct zoo ali = {8, "阿狸"};
struct zoo xyy = {9, "喜羊羊"};
int main()
{
    printf("%s:%d岁\n%s:%d岁\n\n", ali.name, ali.age, xyy.name, xyy.age);
    return 0;
}

位字段

结构体也可以用于存储自定义长度的数据,这种结构体称为位字段,编译器会将多个自定义长度的数据进行整合,最先定义的数据排在最低位,最后定义的数据排在最高位,中间不会进行地址对齐,一般用于设置计算机底层功能相关数据。

#include <stdio.h>
int main()
{
    struct k
    {
        int a : 4;    //int表示成员a最大可以设置为32位长度,而非固定为32位长度,这里设置为4位二进制数字长度
        int b : 2;    //b长度2位
        int c : 2;    //c长度2位
        int d : 8;    //d长度8位,结构体总长度16位,等于2字节
    };
    
    struct k k1 = {1,2,1,5};    //为每个自定义长度数据赋值
    
    printf("0x%hX\n", k1);      //输出0x561,转二进制 = 0101 01 10 0001,按自定义的4个长度将二进制数据分为4组观察,每组的值对应k1设置的值
    
    return 0;
}

 


【共用体】

使用汇编语言编写代码时,可以使用指令随意操作一段内存数据,比如可以进行如下操作:
1.重复使用一段内存,一个数据不再使用后,其占用的内存空间用于存储其它数据。
2.将一段内存中的数据当做任意类型使用,比如有符号整数、无符号整数、浮点数、同型数组、异型数组。

C语言不像汇编那样灵活,所以C语言提供了共用体,使用共用体可以实现上述两种功能,当然使用指针一样可以实现随意操作内存,从而实现共用体的功能,但是使用指针太过繁琐。

共用体的使用方式类似结构体,也是先声明后使用,共用体的声明部分用于设置共用体可以存储、转换的数据类型,使用共用体转换数据类型时只能在声明中的类型之间转换。

#include <stdio.h>
int main()
{/* 使用union关键词定义共用体的声明和实体部分,共用体的长度为其中最大成员的长度 */union aliun{int i;unsigned int u;float f;};union aliun ali1;          //定义共用体实例,不可直接赋值ali1.i = -1;               //通过成员名确定共用体的类型,之后为共用体赋值printf("%d\n", ali1.i);    //共用体作为有符号int类型使用,输出-1printf("%u\n", ali1.u);    //共用体作为无符号int类型使用,输出4294967295ali1.f = 3.14;             //之前的数据不再使用后,其占用的内存再次利用,存储一个浮点数printf("%f\n", ali1.f);return 0;
}

实现某些底层功能时,可能需要将一个数据的类型在数组与单个数据之间进行转换,此时可以使用共用体。

#include <stdio.h>
int main()
{/* 定义匿名共用体 */union{char c[4];int i;} x;x.c[0] = 1;x.c[1] = 2;x.c[2] = 3;x.c[3] = 4;printf("0x%x\n", x.i);    //转换为int类型,输出0x4030201,实际存储值为0x04030201return 0;
}

 


【枚举】

枚举,意为一一列举,将一个变量可以使用的值全部列举出来,之后此变量只能使用其中之一进行赋值,枚举用于限制一个变量的可用值,防止为变量设置一个不合适的值导致功能出错,这个限制是由编译器提供的,对程序进行逆向分析并修改时并不存在任何限制。

定义枚举时无需指定数据类型,枚举默认为int类型,并且不能修改为其它类型。

枚举同样是先声明后使用,声明部分用于设置枚举变量的可用值,每个可用值都有一个名称,之后通过声明部分定义枚举变量,为枚举变量赋值时只能使用声明中设置的可用值。

#include <stdio.h>
int main()
{
    enum alienum{m1=1, m2=2, m3=3};    //声明枚举,alienum为声明部分的名称,{}符号内设置可用值
    enum alienum a = m1;               //定义枚举变量,a为实体部分的名称,使用声明部分设置的可用值名称进行赋值
    enum alienum b = m2;
    a = m2;                            //修改枚举变量的值
    a = b;
    
    return 0;
}


声明部分设置的可用值等于定义的int常量,每个常量都有自己的名称,若只设置常量名而不为常量赋值,则编译器默认从0开始依次为每个常量赋值,声明部分的常量在语意上属于函数的局部常量,这些常量可以直接使用,并且不能与其它局部数据同名。

#include <stdio.h>
int main()
{int a;//enum alienum{a, b, c};    //错误,与已经存在的变量a同名enum alienum{m1, m2, m3};   //正确printf("%d\n%d\n%d\n", m1, m2, m3);    //输出0、1、2return 0;
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/27517.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

linux22-IP地址和主机名

linux22-IP地址和主机名IP地址,查看本机IP地址主机名, 查看与修改域名解析,配置主机名映射虚拟机配置固定IPIP地址 联网计算机的网络地址, 用于在网络中定位 ifconfig 查看本机的ip地址, 如果无法使用ifconfig命令,可以安装net-tools # 安装net-tools (CentOS为yum) apt instal…

linux23-网络传输

linux23-网络传输使用ping检查服务器是否连通使用wget下载文件使用curl发起网络请求ping ping [-c num] ip或主机名-c 检查次数, 不使用-c选项会无限次数持续价差参数: 被检查的服务器IP地址或主机名地址检查baidu.com是否联通 ping baidu.com检查baidu.com是否联通, 检查三次 …

linux19-systemctl

linux19-systemctlsystem control, 控制应用的启动,停止,开机自启, 能被systemctl管理的软件,一般称之为服务 systemctl start | stop | status | enable | disable 服务名选项:start 启动stop 关闭status 查看状态enable 开启开机自启disable 关闭开机自启restart 重启系统内置…

linux20-ln软链接

linux20-ln软链接在系统中创建软链接, 可以将文件, 文件夹链接到其他位置, 类似Windows系统中的快捷方式 ln -s 参数1 参数2-s , soft , 创建软链接, 不添加时创建硬链接 (硬链接不允许指向目录) 参数1, 被链接的文件或文件夹, 不可以使用相对路径 参数2, 要链接去的目的地将/e…

linux21-日期时间

linux21-日期时间date 查看系统时间更改/etc/localtime 修改时区ntp自动校准时区date 查看系统时间 date [-d] [+格式化字符串]-d, 通过给定的字符串显示日期, 一般用于日期计算, 支持以下时间标记year 年month 月day 天hour 小时minute 分钟second 秒格式化字符串%Y 年%y 年份…

npm创建项目

创建项目创建项目目录 首先新建一个文件夹,这里存放着我们的项目。创建项目文件 这里不使用任何项目模板,相当于使用空模板。 进入这个文件夹,再cmd中运行npm init。 然后按照提示输入package name,项目名等等。每输入一个就回车。完成之后目录下会出现一个package.json项目…

springboot为什么要用延迟导入?

Spring Boot使用了多种方式来实现自动配置,其中DeferredImportSelector接口是这些机制之一。 DeferredImportSelector是ImportSelector的一个扩展,它允许延迟导入配置类直到所有@Configuration类都被处理完毕。这对于某些自动配置类需要在应用程序上下文的创建过程中的后期阶…

深入学习和理解Django视图层:处理请求与响应

title: 深入学习和理解Django视图层:处理请求与响应 date: 2024/5/4 17:47:55 updated: 2024/5/4 17:47:55 categories:后端开发tags:Django 请求处理 响应生成 模板渲染 表单处理 中间件 异常处理第一章:Django框架概述 1.1 什么是Django? Django是一个高级的Python Web框架…