栏目分类:
子分类:
返回
文库吧用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
文库吧 > IT > 软件开发 > 后端开发 > C/C++/C#

C语言结构体(用户自定义数据类型)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C语言结构体(用户自定义数据类型)

目录

1.结构体设计

1.1结构体类型的基本形式

1.2结构体设计进阶形式

2.结构体的使用(成员变量访问)

2.1结构体变量

2.1.1结构体变量的初始化

2.1.2结构体变量的成员变量的访问( ”. “ 成员访问符)

2.2结构体指针

2.2.1结构体指针初始化

2.2.2指向结构体成员运算符 “->”

2.2.3结构体指针重命名

3.结构体与数组的结合

3.1结构体数组

3.2指向结构体数组的指针 

4.结构体大小

 扩展:


        在C语言中有多种基本数据类型(内置数据类型)(如char、int、float等)。在一般的程序中,我们总使用这些被设定好的数据类型来定义变量使用。但是这些变量除非我们主观给它们赋予联系,要不然它们都是相互独立、无内在联系的。但是在实际生活中,很多数据是有联系的、成组出现的。例如,一个人有姓名,年龄,性别,手机号等。如果我们将姓名、年龄、性别和手机号这四个数据存储在四个变量中,这四个变量在系统中没有任何联系,无法将这些数据联系起来。所以人们想要把这些数据保存为一个组合的数据,在一个变量中储存多个数据,方便使用。

        所以,C语言允许用户自己建立由不同数据类型组成的组合型数据结构,就是结构体。结构体的成员变量在内存中存储地址连续。

1.结构体设计

1.1结构体类型的基本形式
struct 结构体名{
	成员列表;//属性
};

         这就是结构体类型的一般形式,”struct 结构体名” 合起来就是自定义的结构体类型,如int型,我们在声明一个int型变量a时使用 int  a,如果我们想声明一个结构体变量b就用 struct 结构体名  b 。

        成员列表里就是我们想组合存储在一起的各个类型的数据的声明。如我们要设计一个Student结构体类型保存学生的信息,其中包含姓名(字符串型)、年龄(int型)和成绩(int型)。

strcut Student{
	char name[10];//10字节 数组 存放字符
	int age;//4字节
	int score;//4字节
};

        但是由于每个人的姓名长短不一,尤其可能有少数民族的学生名字特别长,字符数组的长度得随之而变。所以为了避免使用字符数组存储姓名占用内存过大,或数组长度不够的问题,人们常使用指针来存储姓名字符串。( 用指针来存储姓名字符串就是系统先确认内存中有没有这个字符串,如果有的话,就将这个字符串的地址返回给指针,如果没有的话,就将这个字符串存入内存数据区,然后将这个字符串的地址返回给指针。这既不会浪费,也不用担心数组长度不够。)

strcut Student{
	const char *name;//4字节 指针 指向常量字符串
	int age;//4字节
	int score;//4字节
};

1.2结构体设计进阶形式

        如上面,我们设计好了一个Student结构体类型,如果我们想一个结构体型变量stu需要使用代码struct Student stu,这比较麻烦,struct看起来十分多余,所以我们可以使用关键字typedef,将结构体类型struct Student 重命名为student,这样用student就可以声明一个struct Student型变量,使用起来就很方便。

typedef struct Student student;//类型重命名
student stu1;//声明一个struct Student型变量

         可以将结构体设计与结构体类型重命名组合起来,就是如以下形式:

typedef struct Student {//合并
	const char* name;
	int age;
	int score;
}student;    //重命名为student

2.结构体的使用(成员变量访问)

2.1结构体变量

2.1.1结构体变量的初始化

        结构体变量的定义可以通过 结构体类型 变量名 = { 成员数据,成员数据,......} 进行。如:

student stu1 = { "张三",10,100 };
//等价于struct Student stu1 = { "张三",10,100 };

        赋值的操作如上,与普通类型变量一样 。如 stu1 = { "李四",10,10 }; 。 

        假设,现在struct Student结构体类型没有被重命名,结构体的设计,变量声明和初始化就能合并在一起。如:

struct Student {
	const char* name;
	int age;
	int score;
}stu = { "zs",9,10 };

        因为在结构体类型重命名时的student与此时的变量stu处于同一部位,所以不能在重命名时同时完成变量的定义。

2.1.2结构体变量的成员变量的访问( ”. “ 成员访问符)

        结构体变量的成员变量通过成员访问符"."来访问,格式:结构体变量名.成员变量名 。例如要查看的变量stu的姓名、年龄和成绩:

typedef struct Student {
	const char* name;
	int age;
	int score;
}student;

int main() {
	student stu = { "张三",9,10 };
	printf("%s,%d,%dn", stu.name, stu.age, stu.score);
}

 

        也可以对变量值进行修改:stu.age = 10; stu.score = 11; 

2.2结构体指针

2.2.1结构体指针初始化

        结构体数据类型也有指针,其大小也只与操作系统有关(32位系统,4字节大小;64位系统8字节大小)。结构体指针指向结构体变量,先解引用,再通过成员运算符进行访问。如:

struct Student stu = { "张三",9,10 };
struct student *p = &stu;//以变量地址对指针进行初始化

printf("%s,%d,%d", (*p).name, (*p).age, (*p).score);

 

        因为运算符“*”在执行解引用功能时的优先级,比结构体成员运算符“.”的优先级低,所以使用(*p).name 满足其访问操作的顺序。

2.2.2指向结构体成员运算符 “->”

        因为通过(*p).name的形式来访问结构体变量的成员变量十分麻烦,所以C语言为结构体指针专门提供了一个指向结构体成员运算符 “->”。这样就可以直接通过p->name的形式访问到变量stu的name。

2.2.3结构体指针重命名

        因为使用struct student *p 的形式来声明一个结构体指针p比较麻烦,使用也可以使用typedef关键字,将struct student *重命名为一个新的类型,如将struct student *重命名为PStu。这样就可以用PStu直接声明一个指针变量。如:

student stu = { "张三",9,10 };
typedef struct Student* PStu;
PStu p = &stu;

         然后,结构体类型指针的重命名也可以和结构体类型的重命名一样,与结构体的设计相结合:

typedef struct Student {
	const char* name;
	int age;
	int score;
}student,*PStu;
int main(){
    student stu = { "张三",9,10 };
	PStu p = &stu;
}

3.结构体与数组的结合

3.1结构体数组

         结构体类型与基本类型一样,也可以定义数组,用来存储结构体类型变量,数组的一个单元格保存一整个结构体变量。并且数组中变量个数也可以通过int len = sizeof(arr) / sizeof(arr[0]);计算。如:

student arr[] = { {"张三",9,10},{"李四",10,10} ,{"王五",10,11} };

3.2指向结构体数组的指针 

        如同普通数组,我们也可以使用相同类型指针指向数组,通过指针解引用访问数组中的结构体变量。指针变量保存着数组的首地址,指针变量每加一,指针向后偏移一个数组单元格大小(即结构体变量大小)的地址。再解引用,达到遍历整个数组。

student arr[] = { {"张三",9,10},{"李四",10,10} ,{"王五",10,11} };
int len = sizeof(arr) / sizeof(arr[0]);
PStu p = arr;

        其访问形式有两种:(*(p + i)).name<=>(p + i)->name 。

 

4.结构体大小

        结构体变量的大小并不是我们想象的,将其成员变量所占字节大小加起来就是结构体的大小。因为CPU在读取内存时,不是按一个字节一个字节地读取的,这样太没有效率了,而是按照以2、4、8的倍数来读取的。(蓝色部分为个人理解,可跳过)

        并且我认为CPU在读取时,一次读取一个完整数据,或者多个完整数据,至少是同属于一个数据的一部分。不会将其他数据与一个数据的一部分一起读取,这样很没有规则(在深入了解了CPU与内存间的关系后应该会有更好的理解)。

        假设现在有char型a、int型b、long long型c在内存中连续存放着,这样只有CPU是按1字节1字节读取(上面说了不合理),或者读取的字节数大于这三个数据的总大小(如果数据特别多呢,难道每次要根据数据的实际情况来看如何读取数据吗)才能满足上面的条件。

        所以,在存储变量时,为了CPU的读写方便,系统会根据一些规则来存储数据。

        结构体变量在内存中的存储基本遵循以下三个规则(即内存对齐方式)(百度可知):

        1. 结构体变量的首地址,必须是结构体变量中,最大基本数据类型的成员所占字节数的整数倍。

        2. 结构体变量中每个成员相对结构体首地址的偏移量,都是该成员基本数据类型所占字节数的整数倍,如不够整数倍会增添空格凑数到离其大小最近的整数倍。

        3. 结构体总大小为结构体变量中最大基本数据类型所占字节的整数倍,如不够整数倍会增添空格凑数到离其大小最近的整数倍。。

例如:

typedef struct Test1 {
	char a;//一字节
	short b;//两字节
	int c;//四字节
}test1;
typedef struct Test2 {
	short d;
	int e;
	char f;
}test2;
typedef struct Test3 {
	int g;
	long long h;//八字节
	short i;
	char j;
}test3;
	
test1 t1; test2 t2; test3 t3;

        如果按我们原本的想法t1和t2都应该是7字节大小,t3是15字节。但并不是这样,我们会发现,t1是8字节大小,t2却是12字节大小,t3是24字节大小。

        验证:假设结构体变量的起始地址都是0x000。

         对于struct Test1:a在存储进内存时,其相对于其首地址的偏移量为0字节,是a的大小的整数倍,所以将a直接存入;b在存储进内存时,前方只有一字节大小的变量a,b相对于结构体首地址的偏移量才是1字节,不是b本身大小(2字节)的整数倍,为满足规则2,所以用一字节空格填补,此时再将b存入;c在存入内存时,前方有a和b,还有一字节空格,所以其偏移量为4字节,满足c本身大小的整数倍,所以将c直接接着存入。此时,结构体的总大小为8字节,结构体最大成员变量大小为4字节,满足规则3。所以结构体的大小为8字节。

         对于struct Test2:d在存入内存时,相对于结构体首地址的偏移量为0字节,所以d直接存入;e在存入内存时,因为此时前方只有变量d,e相对首地址的偏移量为2字节,不是e本身大小的整数倍,所以用2字节空格来填充,再将e存入;f在存入时,相对首地址的偏移量为8字节,所以将f直接存入。此时,结构体大小为9字节,不是最大成员变量e大小的整数倍,所以填充3字节空格。所以结构体大小为12字节,并且结构体中成员变量的顺序也会影响结构体总大小。

        对于struct Test3:g在存入内存时,相对于结构体首地址的偏移量为0字节,所以g直接存入;h在存入内存时,因为此时前方只有变量g,h相对首地址的偏移量为4字节,不是h本身大小的整数倍,所以用4字节空格来填充,再将h存入;i在存入时,相对首地址的偏移量为16字节,所以将i直接存入;j在存入时,相对首地址的偏移量为18字节,所以将j直接存入。此时,结构体大小为19字节,不是最大成员变量h大小的整数倍,所以填充5字节空格。所以结构体大小为24字节。

 扩展:

        其实,上述分析都是建立在Visual Studio平台的默认内存对齐是按8字节大小对齐的。假如我们将对齐方式修改为按4字节。则struct Test3的大小会发生改变。

#pragma pack(4)// #pragma pack(字节)  对齐方式开始 预处理指令
	typedef struct Test3 {
		int g;
		long long h;
		short i;
		char j;
	}test3;
#pragma pack()//对齐方式结束 预处理指令

         #pragma pack(字节) 为C语言设置内存对齐字节大小的预处理指令。为什么按4字节对齐的话,struct Test3大小变为16字节了呢?

        下面我画出唯一可能的对齐方式:

         它们成员变量本身的大小就有15字节了,所以只可能填充一个空格。并且,变量的顺序不能改变。

        1. 这个空格,没有理由出现在 g 之前、 h 和 i 之间,因为这样肯定会使 g 和 i 相对首地址的偏移量必然不是自身大小的整数倍,也和4字节没任何关系;

        2. 因为 j 的大小为1字节,所以存入 j 时肯定没必要在i和j之间填充空格;

        3. 如果空格填充在 g 和 h 之间,那 h 相对首地址的偏移量为5字节,这既不是4字节,也不是 h 本身大小8字节,并且还使 i 的偏移量变成13字节,还得在 h 和 i 之间填充空格,所以这个可能性也不存在。

        所以,这个空格只能是因为此时结构体大小是15字节,不够16字节,所以填充进去的。此时,16字节也是4字节的倍数。

        综上,我们进行分析:在存入 g 时,是按 g 相对首地址的偏移量为 g 本身大小(4字节)的0倍进行判断的;在存入 h 时,是按偏移量为4字节的1倍进行判断的;在存入 i 时,是按偏移量为 i 本身大小6倍或4字节的3倍进行判断的;在存入 j 时,是按偏移量为 j 本身大小的14倍进行判断的。

        所以,我们猜测在碰到成员变量比我们设定的4字节大时,按照4字节进行相对首地址的偏移量判断;碰到相等或小于4字节大小的成员变量时,按照成员变量的大小进行偏移量判断的;并且,当结构体成员中有比我们设定的4字节大的,结构体的总大小为我们设定的字节大小的整数倍。

        下来,我们就struct Test2对我们的猜想进行验证:

        当对齐方式为4字节时,test2中没有比4字节大的成员变量,所以test2的大小还是12字节。当对齐方式为2字节时,test21中有e为4字节,所以在存储 e 时,其相对首地址的偏移量为2字节,满足猜想,所以将e之间存储在 d 之后;直接存储 f ;再填充1字节空格,将结构体总大小补充为8字节,2的四倍。

#pragma pack(4)//将对齐方式修改为4字节
	typedef struct Test2 {
		short d;
		int e;
		char f;
	}test2;
#pragma pack()
	printf("%dn", sizeof(test2));

#pragma pack(2)//将对齐方式改为2字节
	typedef struct Test21 {//Test21与Test2完全相同
		short d;
		int e;
		char f;
	}test21;
#pragma pack()
	printf("%dn", sizeof(test21));

 

         运行结果满足猜想,所以我们需要对结构体变量的存储规则进行修改(MIN表示{}中的最小值):

        1. 变量首地址,必须是MIN{结构体最大基本数据类型,指定对齐方式}所占字节数的整数倍。
        2. 每个成员变量相对于结构体首地址的偏移量,都是MIN{该成员变量整数倍,指定对齐方式}.
        3. 结构体总大小为  MIN(结构体最大基本数据类型整数倍,指定对齐方式的整数倍}

        读者自己可以在编码过程中对修改后的规则进行验证。

转载请注明:文章转载自 www.wk8.com.cn
本文地址:https://www.wk8.com.cn/it/1037249.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 wk8.com.cn

ICP备案号:晋ICP备2021003244-6号