1. 结构体
1.1 结构体类型的创建
在讲结构体的创建前,先通过数组与结构体的对比,对结构体有一个初步的了解。(1)数组是指一些数据类型相同的元素的集合,而结构体是不同数据类型的成员变量的集合;(2)数组中的元素是连续存放在内存中,而结构体的成员变量不一定要连续存放(因为要内存对齐,什么是内存对齐在讲内存对齐时解释);(3)数组进行读取元素时是通过下标的方式读取,而结构体的成员变量的读取方式并不是通过下标的方式,而是通过点操作符或指向操作符。下面介绍结构体的创建格式:
struct tag
{
member_list;
}variable_list;
其中strcut表示关键字,tag表示定义该结构体的名称,member_list表示成员变量表,variable_list表示定义该结构体的变量名。
(1)结构体变量和结构体指针变量的定义:
struct A
{
int a;
char b;
float c;
}x;
//上面自定义了一个结构体struct A,该结构体的成员变量有int a,char b,float c,x表示数据类型为自定义数据类型struct A所定义的变量。
(2)结构体名称tag的说明:
结构体名称tag可以省略吗?其实是可以的,但你一旦在定义结构体时省略了结构体名称tag,就需要定义结构体变量,否则之后没法利用你所定义的结构体去定义该结构体变量。对于省略结构体名称tag的结构体称为匿名结构体类型。在代码编写时,不建议定义匿名结构体类型,即需要有具体的结构体名称tag。
举例定义一个匿名结构体类型:
struct //省略了tag
{
int a;
char b;
float c;
}*p,x;
(3)member_list成员变量列表的说明:
在window下,成员列表不可以为空;在linux下,成员列表可以为空,但变量没有意义,因为你所定义的结构体无成员变量也就表示你说定义的结构体变量所占字节为0,那么它将没有任何意义。故我们要求定义结构体时必须至少定义一个成员变量。
(4)variable_list结构体变量的说明:
定义了结构体变量,那么可不可以定义结构体指针呢?答案是肯定的。先拿整型来说明一下指针的定义: int* p; //p即为所定义的整型指针
也可以用类似方法定义结构体指针变量,举例如下:
struct A
{
int a;
char b;
float c;
}*p; //p就是结构体指针变量
那么问题又来了,可不可以结构体变量和结构体指针一起定义呢?我们依然通过内置数据类型char为例来同时定义字符型变量和字符型指针:
char* p,x; //同时定义了字符型指针变量p和字符型变量x
那么是不是我们所预期的:p为字符型指针变量和x为字符型变量呢?我们可以通过sizeof来验证,字符型指针变量p所占字节为4,而字符型变量x所占字节为1。具体代码如下:
#include<stdio.h>
int main()
{
char* p,x;
printf(“p所占字节为:%d\n”,sizeof(p));
printf(“x所占字节为:%d\n”,sizeof(x));
return 0;
}
运行结果为:
p所占字节为:4
x所占字节为:1
所以一次可以定义多个变量,中间用逗号隔开,结构体也是同样的,如下:
struct A
{
int a;
char b;
float c;
}*p,x; //结构体指针变量p和结构体变量x
1.2 结构体成员
结构体成员可以是基本的数据类型(int,char,float...)、数组、指针、甚至可以是其他结构体。
(1)结构体变量访问成员是通过点操作符进行的,点操作符接受两个操作数:结构体变量以及结构体成员,举例如下:
struct Student
{
char name[20];
int age;
}; //结构体类型的定义,这里还没有定义该结构体变量
struct Student stu; //定义结构体变量,此时stu有两个成员变量name和age。
下面通过点操作符访问stu的成员变量name和age:
#include<stdio.h>
int main()
{
struct Student stu;
strcpy(stu.name,“xietongxue”);
stu.age=20;
printf(“name=%s\n”,stu.name); //使用.访问name成员
printf(“age=%d\n”,stu.age); //使用.访问age成员
return 0;
}
运行结果为:
name=xietongxue
age=20
(2)当得到的不是结构体变量而是指向结构体的指针(结构体指针变量)时,通过指向操作符访问该结构体的成员,举例如下:
struct Student
{
char name[20];
int age;
}; //结构体类型的定义,这里还没有定义该结构体变量
struct Student* pstu; //定义结构体指针变量,此时pstu指向两个成员变量name和age。
利用之前操作指针变量的方法,可以利用解引用的方式访问成员,即:
strcpy((*pstu).name,”xietongxue”);
(*pstu).age=20;
但是这样做会觉得麻烦,解引用之后再用点操作符,有没有直接一点的方法呢?所以就有了指向操作符。下面通过指向操作符访问pstu所指向的成员变量name和age:
#include<stdio.h>
int main()
{
struct Student* pstu;
strcpy(stu->name,“xietongxue”);
stu->age=20;
printf(“name=%s\n”,stu->name);//使用.访问name成员
printf(“age=%d\n”,stu->age);//使用.访问age成员
return 0;
}
运行结果为:
name=xietongxue
age=20
1.3 结构体的自引用
在结构体的成员中是否可以包含自身的结构体呢?代码如下:
struct B
{
int a;
char b;
struct B c;
};
可以吗?如果可以,那么是不是能通过sizeof(structB)计算该结构体的大小?那大小是多少呢?大家可以在自己的电脑上运行一下,其实根本不能运行,故结构体的成员中不能包含自身的结构体。
正确的结构体自引用是通过结构体指针的方式,代码如下:
struct B
{
int a;
char b;
struct B*c; //结构体指针,指向该结构体
};
1.4 对结构体类型使用typedef
typedef是关键字,可以自定义名称(也可以理解为重命名),如可以将数据类型int自定义其名称为myint,并使用myint,代码如下:
typedef int myint;
#include<stdio.h>
int main()
{
myint data=10;
printf(“data=%d\n”,data);
return 0;
}
运行结果为:
data=10
下面对结构体类型使用typedef关键字:
typedef struct Node
{
int data;
struct Node* next;
}Node; //Node是对struct Node的重命名
Node new_node;//定义结构体变量,相当于struct Node new_node;
1.5 结构体变量的定义和初始化
定义结构体变量的两种方式:
struct massage
{
char name[20];
int age;
char sex[4];
}m1; //定义结构体类型的同时定义了结构体变量m1
struct massage m2; //后定义的结构体变量m2
初始化结构体变量,定义结构体变量的同时赋初值:
struct massage m1={“xietongxue”,20,”女”};
再举例说明:
struct Node
{
int data;
struct massage m1;
struct Node*next;
}new_node1={20,{“xietongxue”,20,”女”},NULL}; //结构体嵌套初始化
struct Node new_node2={20,{“zh”,18,”男”},NULL}; //结构体嵌套初始化
下面的这样的方式可以吗?
struct Node new_node2;
new_node2={20,{“zh”,18,”男”},NULL};
显然是不可以,就像数组的初始化赋值一样,不能为:
int arr[3];
arr={1,2,3};
这样的做法是错误的,故结构体变量的初始化赋值也不可以这样。
总结:结构体变量的初始化和数组的初始化类似;结构体变量里有结构体结构体变量时,初始化是{}套{};结构体变量只允许整体初始化,不允许整体赋值。
1.6 结构体内存对齐
在已经了解了结构体后,需要讨论如何计算结构体的大小,这就牵扯到之前所遗留的问题:结构体内存对齐。内存对齐就是把某个变量对齐到内存的指定位置,那为什么会有结构体内存对齐问题呢?为什么存在内存对齐呢?
(1)存在内存对齐的原因:
一是平台问题(移植问题):不是所有的硬件平台都能访问到任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常;二是性能问题:为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问只需要一次访问即可。下面通过例子解释上面说的什么意思?
了解了为什么需要内存对齐问题之后,下面来掌握结构体内存对齐的规则。
(2)结构体的对齐规则:
1.第一个成员在结构体变量偏移量为0的地址处,即默认第一个成员是内存对齐的。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=编译器默认的一个对齐数与该成员大小的较小值。VS中默认值为8,Linux中默认值为4。
3.结构体总大小为最大对齐数(所有成员变量的对齐数中的最大的对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
介绍了结构体的对齐规则之后,做一些练习,去实际计算结构体的大小。
(3)计算结构体的大小(windows下计算,默认对齐数为8)
举例说明:
struct s1
{
char c;
//占1个字节,对齐数为1,从偏移量0开始存放,此时总共占1个字节
int i;
//占4个字节,对齐数为4,从偏移量4开始存放,此时总共占8个字节
char a;
//占1个字节,对齐数为1,从偏移量8开始存放,此时总共占9个字节
};
//最大对齐数为4,而9不是4的整数倍,故结构体大小为12
简单地画出内存图加以说明:
struct s2
{
char c1;
//占1个字节,对齐数为1,从偏移量0开始存放,此时总共占1个字节
struct s1 s1;
//占12个字节,对齐数为4(由结构体struct s1的最大对齐数得),从偏移量为4开始存放,此时总共存放16个字节
double d;
//占8个字节,对齐数为8,从偏移量16开始存放,此时总共占24个字节
};
//最大对齐数为8,而24是8的整数倍,故结构体大小为24
1.7 结构体传参
直接上代码演示:
注:
(1)以下代码在linux操作系统下的执行的。
(2)time ./a.out:检测./a.out执行了多长时间,time指令的功能:检测执行指令的时间
(3)可以通过运行时间判断哪种方法比较好
(1)结构体变量传参
//下面演示结构体变量传参
struct S
{
int data[1024*1024];
char c;
}; //定义结构体类型struct S
void fun1(struct S s)
{
;
}
int main()
{
struct S s; //定义结构体变量s
int i=0;
for(i=0;i<1024;i++)
{
fun1(s); //结构体变量传参
}
return 0;
}
注:调用一个有结构体变量作为参数的函数,结构体变量在传参时,需要形参实例化从而产生临时变量,故需要花费很多时间,造成性能问题。
(2)结构体指针传参
//下面演示结构体变量传参
structS
{
int data[1024*1024];
char c;
}; //定义结构体类型struct S
void fun2(struct S* s)
{
;
}
int main()
{
struct S s; //定义结构体变量s
int i=0;
for(i=0;i<1024;i++)
{
fun2(&s); //结构体指针传参
}
return 0;
}
注:调用一个有结构体指针作为参数的函数,结构体指针在传参时,只需要形参实例化产生一个临时变量,故不需要花费多少时间,也不会造成性能问题。
结论:结构体传参时,要传结构体的地址,而不是结构体变量。
2. 位段
2.1 什么是位段
位段的声明和结构体类似,但也有不同之处:
(1)位段的成员必须是整型,如:int、unsigned int、signed int、char、unsigned char、signed char。
(2)位段的成员名后面有一个冒号和数字。
如下:
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
}; //定义一个位段
2.2 位段的内存分配
(1)位段的成员均为整型类型:int、unsigned int、signed int、char、unsigned char、signedchar。
(2)位段的空间上是按照需要以4个字节(int)或1个字节(char)的方式开辟。
那么上述所定义的位段A占多少个字节呢?怎么计算?下面通过画图的方式,讲解怎么计算位段大小?
2.3 位段的跨平台问题
(1)int位段被当做有符号整型还是无符号整型是不确定的。
(2)位段中最大位的数目是不能确定的(16位机最大16,32位机最大32,64位机最大64),写成27在16位机下是有问题的。
(3)位段中的成员在内存中是从左向右分配bit位还是从右向左分配bit位的标准尚未定义。
(4)当一个结构体包含两个位段,第一个位段成员所占bit位比较大,无法容纳于第一个位段成员剩余的bit位时,是舍弃剩余的位还是利用,这也是不确定的。(之前计算位段大小时,我们采用的方式是舍弃)
3. 枚举
3.1 什么是枚举
枚举就是将可能的取值一一列举出来,比如:
一周的星期几是可以一一列举出来的;
性别有男和女是可以一一列举出来的;
一年有12个月份是可以一一列举出来的。
3.2 枚举类型的定义
enum 枚举名
{
//枚举成员用逗号隔开
};
举例如下:
enum Day //枚举星期
{
Mon,
Tue,
Wed,
Thr,
Fri,
Sat,
Sun,
};
enum Sex //枚举性别
{
MALE,
FEMALE,
};
enum Color //枚举颜色
{
RED,
GREEN,
BLUE,
BLACK,
WHITE,
};
注:
(1)以上定义的enum Day,enum Sex,enum Color都是枚举类型。
(2)花括号{}中的内容是枚举类型的可能取值,也叫枚举常量,这些枚举常量默认从0开始依次递增1,也可以在定义时赋初值。(3)定义的枚举变量的赋值必须从枚举常量中取值,提高代码的可读性。
关于枚举类型的应用:
(1)枚举常量默认从0开始
enum Color //枚举类型enum Color的定义
{
RED,
BLUE,
GREEN,
WHITE,
};
int main()
{
enum Color color=BLUE; //枚举变量的定义并初始化
printf("RED=%d\n",RED);
printf("BLUE=%d\n",BLUE);
printf("GREEN=%d\n",GREEN);
printf("WHITE=%d\n",WHITE);
return 0;
}
(2)对枚举常量进行赋初值
enum Color //枚举类型enum Color的定义
{
RED=2, //枚举常量的赋值
BLUE,
GREEN=10, //枚举常量的赋值
WHITE,
};
int main()
{
enum Color color=BLUE; //枚举变量的定义并初始化
printf("RED=%d\n",RED);
printf("BLUE=%d\n",BLUE);
printf("GREEN=%d\n",GREEN);
printf("WHITE=%d\n",WHITE);
return 0;
}
3.3 枚举的优点
(1)增强代码的可读性以及可维护性
(2)便于调试
(3)使用方便,一次可以定义多个常量
(4)与#define定义的标识符比较,枚举类型有类型的检查,更加严谨。
4. 联合
4.1 联合类型的定义
联合类型的特点为联合中的成员公用同一块内存,联合也称为共用体。
Union 联合类型的名称
{
//联合类型的成员变量
};
比如:
union Un //定义联合类型union Un
{
int a; //成员变量
char b; //成员变量
};
int main()
{
union Un un; //联合变量的定义
printf("%d\n",sizeof(union Un); //计算联合的大小
return 0;
}
那么运行结果是什么呢?5?8?还是多少?
由于联合的成员公用同一块内存空间,故联合类型的大小至少是最大成员变量的大小(至少是因为联合也需要内存对齐);当最大成员变量的大小不是最大对齐数的整数倍时,就要内存对齐到最大对齐数的整数倍。
画内存图分析上述的联合类型的大小:
程序运行结果:
以上内容就是我所分享的关于自定义类型的介绍,如有错误,欢迎纠正!