数组用于将多个数据集中存储,方便管理,此文将集中存储任何类型数据的语句都称为数组,数组根据存储数据的类型和方式分为同型数组、结构体、共用体、枚举。
【同型数组】
同型数组也直接称为数组,用于存储多个类型相同的数据,数组内的数据称为数组元素,数组元素占用连续的虚拟地址,每个元素都有一个数组内部编号,称为数组下标,数组元素通过“数组名+下标”的方式调用,下标从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编码,未赋值元素自动赋值为0
char 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类型使用,输出-1
printf("%u\n", ali1.u); //共用体作为无符号int类型使用,输出4294967295
ali1.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,实际存储值为0x04030201
return 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、2
return 0;
}
标签:struct,05,int,元素,C语言,数组,长度,结构 From: https://www.cnblogs.com/alixyy/p/18172580