文章目录
自定义类型:结构体,枚举,联合
在C语言中,不但有着int,float,char,double,short这样的已经定义好的类型,还有着可以自己定义的类型,下面我们就一起来看一看吧。
结构体的声明
//struct是结构体关键字
struct tag//tag是结构体标签
{
member-list;//是成员列表
}variable-list;//是结构体变量列表
具体的例子:
struct book
{
char name[10];
char tuthor[10];
}b1,b2;//创建了两个结构体类型的全局的b1,b2变量
struct book b3;//和上面创建的b1,b2没有任何区别,都是全局变量
int main()
{
struct book b4;//创建了一个结构体类型的局部变量b4
b4.name="1234";
return 0;
}
使用typedef进行结构体声明
//struct是结构体关键字
struct tag//tag是结构体标签
{
member-list;//是成员列表
}newname;//别名
具体的例子:
typedef struct number
{
int x;
int y;
}num;
int main()
{
num s1;//直接写num即可,不用写struct number
s1.x = 9;
}
注意:
一旦使用了别名,就不可以再在声明的时候就创建变量了,因为别名占据了原来创建变量的地方
特殊的声明
有一种不带标签的结构体,但是必须在声明的时候定义
struct
{
int a;
char b;
float c;
}x,y;//必须在声明的时候定义
int main()
{
//这样也是可以使用的
x.a = 1;
x.b = 2;
x.c = 3;
y.a = 0;
}
除非使用typedef
typedef struct
{
int a;
char b;
float c;
}s;
int main()
{
s x;
s y;
x.a = 1;
x.b = 2;
x.c = 3;
y.a = 0;
}
结构体的自定义
对于结构体来说,在结构体内部添加一个结构体是不是就是结构体的自定义了呢?
就像下面这样:
struct Node
{
int data;
struct Node next;
};
答案是不可以,因为结构体里面含有结构体,一层一层的嵌套,永远在套娃,所以这样不叫作结构体的自定义。
应该改成下面这种指针的模式:
struct Node
{
int data;
struct Node* next;
};
这样通过指针,我们也可以找到下一个结构体变量
结构体的初始化
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
结构体的内存对齐
计算结构体的大小的时候我们就要考虑结构体的内存对齐的因素了。
注意:这是一个很重要的考点
这时候我们就要知道结构体的内存对齐的规则:
结构体的第一个成员永远放在结构体起始位置偏移量为0的位置上。(就是放在结构体的首位置)
结构体从第二个成员开始,总是放在偏移量为对齐数的整数倍处。
对齐数=编译器的默认对齐数和变量自身大小的最小值
Linux无默认对齐数
VS的默认对齐数是8
结构体的总大小是每个成员变量的对齐数最大的整数倍
如果存在结构体的嵌套,那么嵌套的结构体对齐到自己的最大对齐数的整数倍处,整个结构体的大小就等于所有元素的最大对齐数的整数倍(包含嵌套结构体的最大对齐数)
那么,以下面得到几个为例,我们具体来分析以下:
struct S1
{
char c1;
int i;
char c2;
};
答案是12,这就是根据上面的3条规则算出来的。
但是,同样的成员变量,改变了顺序,结构体的大小会不会有变化呢?就像下面这样:
struct S2
{
char c1;
char c2;
int i;
};
我猜,很多人一开始肯定以为这个和上面的没什么区别,但是好好的套用上面的三条规则再看一下呢?
很没有想到,原来就是结构体改变了顺序,结构体的大小也会变化,这都是因为我们的结构体内存对齐的三条原则。
-
练习3
//练习3 struct S3 { double d; char c; int i; };
4. 练习4——结构体嵌套问题
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
为什么存在结构体对齐:
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址
处取某些特定类型的数据,否则抛出硬件异常。- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器
需要作两次内存访问;而对齐的内存访问仅需要一次访问。
所以,这是一种以空间换时间的方法,用以提高时间效率:
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起
修改默认对齐数
之前我们见过了 #pragma
这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
下面是例子:
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
}
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为8
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
第一个答案是12,第二个答案是6
offsetof 宏的实现
百度有这样一道面试题:
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
这个时候,我们就可以使用offsetof函数来实现
函数原型:
size_t offserof(struct s1,struct s1.n);
第一个参数是结构体变量的类型名,第二个参数是成员名
下面是具体实现的例子:
#include<stddef.h>
struct s1
{
char c;
int i;
char d;
};
int main()
{
printf("%u\n", offsetof(struct s1,c));
printf("%u\n", offsetof(struct s1, i));
printf("%u\n", offsetof(struct s1, d));
return 0;
}
//答案是0 4 8
结构体传参
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
从上面的传参可以看出,有两种形式:结构体传参,结构体地址传参
那个更加高效呢:
答案是结构体地址传参,因为传值的时候会进行实参的拷贝,拷贝到形参,在压栈的过程中,很有占用过多内存,可能发生栈溢出。
所以,在都可以达到目标效果的前提下,当然选择更加高效的传地址的方法。
位段
位段也是结构体中的一种表现形式
什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。
比如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
位段成员变量中冒号后面是要开辟的空间
同样的,我们也会考虑位段的大小问题
这个就要考虑在位段中内存是如何分配的了:
位段在内存中的分配:
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
下面以一个例子来介绍:
//一个例子
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
它们在内存中的分布是这样的。
对于这个结构体s来说,它是一个字节,一个字节开辟的。
- 首先,先开辟一个字节,先存放a占据3个bit
- 再存放b,占据4个bit
- 接着要去存放c,但是内存已经不太够用,所以需要再开辟一个新的字节
- 将占用5个bit的c放入新开辟的字节中
- 接下剩下的bit还是也不够用,再次开辟新的字节,将d放入。
接着将a,b,c,d的值按照存入的bit位放入就可以,最后在内存中显示的大小是0x620304
未开始赋值的时候是根据位段开辟了3个字节
都赋值后,
位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位
还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存
位段的应用
在网络传输的过程中,对于一个数据,我们往往要附加上一系列其他的IP地址,协议等。把他们打包成一个整体。
这时候把他们改变成位段就可以很好的解决问题了,这样就可以减少空间的浪费,挺高网络传输的效率。
枚举
枚举类型是一种特殊的结构体类型。
那么什么类型的变量适合使用枚举呢?那就是个数是有限的元素集适合使用枚举。枚举枚举,就是要把所有的变量都列举出来,所以枚举变量的个数必须是有限的。
枚举类型的定义:
和结构体类似
注意:
- 最开头写上enum,并且还要写上枚举常量的标签
- 成员常量之间用逗号分隔开,最后一个常量不做任何处理
具体的例子:
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
以上定义的 enum Day
, enum Sex
, enum Color
都是枚举类型。 {}中的内容是枚举类型的可能取值,也叫 枚举常
量 。
枚举的性质
-
枚举常量自身具有值,默认是从0开始,如果改变的话,依次加1
enum Color//颜色 { RED = 1, GREEN = 7, BLUE }; int main() { printf("%d\n", RED);//1 printf("%d\n", GREEN);//7 printf("%d\n", BLUE);//8 return 0; }
-
枚举常量最开始可以赋值
enum Color//颜色 { RED=1, GREEN=2, BLUE=4 };
-
枚举常量是常量,不可以被修改
enum Color//颜色
{
RED = 1,
GREEN = 7,
BLUE
};
int main()
{
GREEN = 6;//这样编译器会报错
return 0;
}
-
具体的使用
只能将枚举常量赋予枚举变量。
enum Color//颜色 { RED = 1, GREEN = 7, BLUE }; int main() { enum Color c = GREEN;//enum Color代表一个单独的变量,不是结构体 printf("%d", c);//7 return 0; }
枚举的优点:
我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨.
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
例子:
enum Option
{
EXIT,//0
ADD,//1
SUB,//2
MUL,//3
DIV//4
};
void menu()
{
printf("******************************\n");
printf("**** 1. add 2. sub ****\n");
printf("**** 3. mul 4. div ****\n");
printf("**** 0. exit ****\n");
printf("******************************\n");
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case ADD:
//加法
break;
case SUB:
break;
case MUL:
break;
case DIV:
break;
case EXIT:
break;
}
} while ();
return 0;
}
这样的使用枚举类型,使程序更加具有可读性,更加容易理解。
联合
联合体也叫共用体。这个联合体很不简单因为它所有的成员公用同一个内存。
联合体的声明:
union un
{
int i;
char c;
};
int main()
{
union un u;//创建一个联合体变量
printf("%p\n", &u);
printf("%p\n", &u.c);
printf("%p\n", &u.i);
return 0;
}
上面打印的三个答案是相同的。因为i和c共同使用同一块空间
判断计算机大小端
原来算法:
请大家判断以下这样是正确的吗?
int main()
{
int x = 0x11223344;
char y = (char)x;
printf("%x", y);
return 0;
}
使用强制转换类型无论如何都是取到x的第一个字节,而不是内存中第一个字节,所以这样的作法是绝对错误的。
正确的做法:(直接从给出内存中的地址来进行取字节访问)
int main()
{
int x = 0x11223344;
char* p = (char*)&x;
printf("%x\n", *p);
if (*p == 0x44)
printf("小端\n");
else
printf("大端\n");
return 0;
}
使用联合体:
union un
{
int i;
char c;
};
int main()
{
union un u;
u.i = 0x11223344;
printf("%x\n", u.c);
if (u.c == 0x44)
printf("小端\n");
else
printf("大端\n");
return 0;
}
联合体的大小计算
计算的原则:
联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
//下面输出的结果是什么?
int main()
{
printf("%d\n", sizeof(union Un1));//8,最大对齐数为4
printf("%d\n", sizeof(union Un2));//16,最大对齐数为4
}