本节主要讲解下结构体的一些易错点和重要内容
一般的结构体定义:定义类型+变量
struct student
{
long stuID;
char stuName[10];
char stuSex;
char birthYear;
int mathScore;
}stu1;
可以用typedef取别名,方便更快的定义结构体变量
typedef struct student
{
long stuID;
char stuName[10];
char stuSex;
char birthYear;
int mathScore;
}Stu;
Stu可不是结构体变量,而是前面这个结构的别名
,也就是这个结构:
struct student
{
long stuID;
char stuName[10];
char stuSex;
char birthYear;
int mathScore;
};
的别名—Stu.
然后定义结构体变量方便多了:
Stu stu1;
//等价于
struct student stu1;
这里需要注意的就是 不能
嵌套 同类型的结构体
C语言不允许结构体嵌套同类型的结构体,因为这样会导致递归定义
,无法确定结构体的大小。
举例 说明:
typedef struct Node {
int value;
struct Node child;
} Node;
这个结构体定义中,Node 结构体包含一个整型的 value 成员变量,以及另一个类型为 Node 的结构体 child 成员变量。但是,由于 Node 结构体中包含 Node 类型的成员变量 child,而 Node 结构体的大小又依赖于 child 成员变量的大小,这样会导致无限递归,最终无法确定结构体的大小,从而无法在程序中定义这个结构体。
解决方法:结构体指针
如果确实需要嵌套同类型的结构体,可以使用结构体指针来代替结构体本身的成员变量,这样就能够避免结构体无限递归的问题。例如,我们可以将前面的例子改为:
typedef struct Node {
int value;
struct Node *child;
} Node;
这个结构体定义中,Node 结构体包含一个整型的 value 成员变量,以及一个类型为 Node* 的指针成员变量 child,该指针指向另一个 Node 类型的结构体。这样,Node 结构体的大小不再依赖于 child 成员变量的大小,因为指针的大小是固定的。
使用指针来表示结构体之间的引用关系是一种比较常用的方式,它不仅能够解决结构体嵌套同类型结构体的问题,而且也能够更灵活地表示数据结构之间的关系。
另外注意下嵌套下的结构体变量初始化:用多个括号
Stu stu1={1000,"xi",'M',{1991,5,19},"95"};
在 C 语言中,结构体变量通常不能使用 {} 进行整体赋值。这是因为结构体可能包含不同类型的成员变量,它们的内存布局可能不是连续的,这导致在整体赋值时无法确定每个成员变量在内存中的位置和对齐方式。
不过,如果结构体中的所有成员变量都是同一类型,并且它们在结构体中是连续存储的,那么可以使用 {} 进行整体赋值。
typedef struct {
int x, y;
} Point;
Point p = { 10, 20 };
在这个例子中,结构体 Point 包含两个整型成员变量 x 和 y,它们在结构体中是连续存储的,并且都是整型类型,因此可以使用 {} 进行整体赋值。
❗︎需要注意的是,如果结构体中包含指针类型的成员变量,那么整体赋值通常不是一个好的选择,因为它可能会导致指针指向错误的内存区域。
解决方法:
可以使用 逐个成员变量的赋值方式,或者使用函数来初始化
结构体变量。
举例说明:结构体person 包含 char int类型
typedef struct {
char *name;
int age;
} Person;
Person p = { "Tom", 20 }; // 错误的写法,会导致指针指向错误的内存区域
这是因为结构体中的 name 成员变量是一个指针类型,它在内存中保存的是一个地址,而不是实际的字符串内容。因此,当我们使用 {} 进行整体赋值时,会导致 name 指针指向错误的内存区域,从而产生未定义的行为。
Person p;
p.name = "Tom";
p.age = 20;
或者
Person createPerson(char *name, int age) {
Person p;
p.name = name;
p.age = age;
return p;
}
Person p = createPerson("Tom", 20);
因为字符数组不能用一个字符串字面量来直接赋值。字符数组指针可以直接赋值。— 因为没有确定内存大小
举例:
#include
#include
typedef struct {
int id;
char name[20];
} Student;
int main() {
Student s;
s.id = 1001;
strcpy(s.name, "Tom");
printf("Student ID: %d\n", s.id);
printf("Student Name: %s\n", s.name);
return 0;
}
在这个例子中,我们使用 strcpy 函数将字符串 “Tom” 复制到结构体 Student 的 name 成员变量中。最终输出结果为:
数组名就是数组指针的首地址,所以当我们使用 scanf 函数读取一个字符串时,对于字符数组类型的变量,我们不需要在 %s 格式控制符后面使用取地址符号 &。
这是因为在 C 语言中,数组变量名代表着数组首元素的地址,而数组名本身就是一个指针,所以可以直接传递数组变量名作为参数。因此,在使用 scanf 函数读取字符串时,我们可以直接将数组名作为参数传递给 %s 格式控制符。
举例:
在这个例子中,studentName 是一个字符数组类型的变量,在使用 scanf 函数读取该变量的值时,我们可以使用以下代码:
char studentName[10]; scanf("%s",stu.studentName);
这里我们没有使用取地址符号 &,因为 stu.studentName 本身就是一个字符数组的首地址。
(配合#pragma pack内存对齐)
平台原因(移植原因) : 不是所有的硬件平台都能访问任意地址上的任意数据的;某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常。比如,当一个平台要取一个整型数据时只能在地址为4的倍数的位置取得,那么这时就需要内存对齐,否则无法访问到该整型数据。
性能原因: 数据结构(尤其是栈):应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐内存,处理器需要作两次内存访问;而对齐的内存访问仅需一次。
最大对齐数
。
首先,一般都是向较小的数取对齐数,例如,int大小为4,系统指定的对齐数为8. 8 > 4,所以取4为对齐数
,就像:
红色和绿色的是存了的地址,白色的就是浪费的空间,所以说对齐方式很浪费空间,可是按照计算机的访问规则,这种方式提高了效率。
从上可以看出,该结构体的大小为:1 + 4 + 1 + 3(浪费的空间(白色)) = 9,然后通过法则三知道9是不行的,要偏移到12,因为总大小要是最大对齐数的整数倍。
综上 结构体的大小为:1 + 4 + 1 + 3 + 4(偏移的大小) = 12.
在这之前咱先了解一下联合体大小计算规则:联合体中最大成员所占内存的大小且必须为最大类型所占字节的最小倍数。
联合体在结构体里面比较特殊,他可以作为最大的对齐数,联合体大小为8,系统指定的对齐数为8,所以最大对齐数为8,然后可以根据上面的内存格子数一数。
uoion U先取最大类型 64位 8 字节double ,char[7]占7个字节,所以8字节够用了。
综上结构体的大小为:1 + 3 + 4 + 8 + 1 + 7(偏移量) = 24
指向结构体变量的指针
举例:
struct student stu1 ={1,“李芳”,45);
struct student *p;
p= &stu1:
(1)用*p访问结构成员
(*p).num = 36
(2) 用指向运算符“->”访问结构成员。
p->num = 36;
当p=&stu1时,以下三条语句相同:
stu1.num = 36;
(*p).num =36;
p->num= 36
(*p) 表示指针 p 所指向的结构体对象。由于 -> 运算符的优先级高于 * 运算符,因此在使用 * 运算符获取指针所指向的结构体对象时,需要使用圆括号将 *p 括起来
而箭头运算符其实是指针运算符 -> 和成员访问运算符 . 的组合,可以简化代码书写。
也就是(*p).的简化
也就是是说防止成为野指针!
如果没有为指针变量所指向的结构体分配内存空间,那么指针变量就是一个野指针,它所指向的内存空间可能已经被其他进程或线程占用,或者它所指向的内存空间根本不存在。在这种情况下,如果尝试访问指针所指向的结构体的成员,就会产生不可预期的行为,甚至会导致程序崩溃。
因此,在使用结构体指针访问结构体成员之前,需要先分配内存空间,确保指针变量所指向的结构体已经正确地分配了内存空间。
举例:
#include
#include
#include
typedef struct {
int id;
char name[10];
} Student;
int main() {
// 分配内存空间
Student *p = (Student *)malloc(sizeof(Student));
if (p == NULL) {
printf("Failed to allocate memory.\n");
return 1;
}
// 访问结构体成员
p->id = 1;
strcpy(p->name, "Tom");
// 输出结构体成员
printf("Student ID: %d\n", p->id);
printf("Student Name: %s\n", p->name);
// 释放内存空间
free(p);
p=NULL;
return 0;
}
在这个例子中,我们先使用 malloc 函数分配了一个 Student 结构体大小的内存空间,并将指针 p 指向该内存空间。然后,我们使用箭头运算符 -> 访问了结构体指针 p 所指向的结构体的成员,并给这些成员赋值。最后,我们输出了结构体成员的值,并使用 free 函数释放了分配的内存空间。最后把指针赋为NULL确保不会被乱用。
允许多个不同变量使用同一内存区的功能称为 联合(union)
。
这些不同类型的变量共享同一段内存,因此每一时刻只有一个成员起作用,有效成员将在执行期间确定。
#include
#include
union sample
{
short i; //2
char ch; //1
float f; //4
}sa;
int main()
{
printf("%d\n",sizeof(sa));
}
联合成员的引用和结构成员的引用形式相同,一般为下列三种形式:
联合变量名.成员名
指向联合变量的指针->成员名
(*指向联合变量的指针).成员名
注意:联合成员彼此不是并存的,任一时刻共用体变量中只含有其中一个成员,该成员是最近一次存入联合的那一个成为当前成员。
联合体中的多个成员共用同一块内存空间,但同一时间只有一个成员有效,也就是说,联合体的大小是所有成员中占用内存最大的那个成员的大小,联合体变量的地址是所有成员的起始地址。在使用联合体时,只能访问其中一个成员,访问其他成员会导致不可预期的结果。
举例:
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
printf("data.i = %d\n", data.i);
data.f = 3.14;
printf("data.f = %f\n", data.f);
strcpy(data.str, "hello");
printf("data.str = %s\n", data.str);
return 0;
}
在该例中,联合体 Data 中有三个成员:一个整型变量 i,一个浮点型变量 f,以及一个字符数组 str。
由于联合体的特性,这三个成员共用同一块内存空间,所以在存储一个成员的同时,其他成员的值都会被覆盖掉。在上面的代码中,首先存储了一个整型变量 10,然后又存储了一个浮点型变量 3.14,最后又存储了一个字符串 “hello”,每次存储都会覆盖之前的值。因此,输出结果为:
data.i = 10
data.f = 3.140000
data.str = hello
再举个例子:
#include
#include
#include
union Data {
int n;
double a;
char c1;
char c[20];
}u;
int main() {
u.n=5; // 此刻,只有成员n有意义
printf("%d\n",u.n);
printf("%f\n",u.a);
printf("%c\n\n",u.c1);
u.c1='a'; //此时,只有成员c1有意义
printf("%d\n",u.n);
printf("%f\n",u.a);
printf("%c\n",u.c1);
printf("%s\n\n",&u.c1);
strcpy(&u.c1,"abcdefg");
printf("%d\n",u.n);
printf("%f\n",u.a);
printf("%c\n",u.c1);
printf("%s\n\n",&u.c1);
//此刻,&u.c1是字符指针,代表一个字符串
//给一个联合成员赋值会破坏其他成员的值
}
C语言不允许使用联合变量作为函数参数,但可以使用指向共用体变量的指针。
由于 union 变量的成员共享内存空间,因此在函数内部如果修改了其中一个成员的值,可能会影响其他成员的值。因此,在使用 union 变量作为函数参数时,需要特别小心,确保不会出现这种问题。
因为联合体变量的大小取决于其中最大的成员变量的大小,而在函数调用时,传递的参数需要在栈中分配一段内存,但由于联合体变量的大小不固定,因此无法在栈上分配足够的内存。
为了避免这个问题,可以使用指向联合体变量的指针作为函数参数。因为指针的大小是固定的,所以可以在栈上分配足够的内存来存储指向联合体变量的指针。在函数内部,可以通过这个指针来访问联合体变量的成员。
举例:
#include
#include
#include
typedef union {
int num;
char ch;
double dbl;
} UnionType;
void foo(UnionType *ptr) {
// 使用指向联合变量的指针访问联合成员
printf("%d\n", ptr->num);
}
int main() {
UnionType myUnion;
myUnion.num = 10;
foo(&myUnion); // 将指向联合变量的指针作为参数传递给函数
return 0;
}
得到结果10输出
#include
//结构体
struct u //u表示结构体类型名
{
char a; //a表示结构体成员名
int b;
short c;
}U1;
//U1表示结构体变量名
//访问该结构体内部成员时可以采用U1.a=1;其中"点"表示结构体成员运算符
//联合体
union u1 //u1表示联合体类型名
{
char a; //a表示联合体成员名
int b;
short c;
}U2;
//U2表示联合体变量名
//访问该联合体内部成员时可以采用U2.a=1;其中"点"表示联合体成员运算符
//主函数
int main(){
printf("%d\n",sizeof(U1));
printf("%d\n",sizeof(U2));
return 0;
}
/*程序运行结果是:
12
4*/
(1)结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
(2). 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的
#include
//联合体
union u1
{
char a;
int b;
short c;
}U2;
//主函数
int main(){
U2.a='a';
printf("%c%c\n",U2.b,U2.c);//输出aa
U2.a='b';
printf("%c%c\n",U2.b,U2.c);//输出bb
U2.b=0x4241;
printf("%c%c\n",U2.a,U2.c);//输出AA
return 0;
}
也可以见上面的文字
上面的没写全,补充下:
union大小计算准则:
1、至少要容纳最大的成员变量
2、必须是所有成员变量类型大小的整数倍 —— 记住是成员变量类型 char c[7]算 1
代码中U3至少容纳最大e[5]=20字节,同时变量类型最大值是整数倍,即double(字节数是8)的整数倍,因而sizeof(U3)=24。
#include
//联合体
union u2
{
char a; //1
int b; //4
short c; //2
double d; //8
int e[5]; //20
}U3;
//主函数
int main(){
printf("%d\n",sizeof(U3));//输出24
return 0;
}
见专栏文章
union大小端模式
下面介绍下枚举类型enum
将只有几种可能值的变量定义为枚举类型
比如一周有周1,2,3,4,5,6,7等
类型定义格式:
enum 类型名{值列表}
例如:
enum Weekday{Sun, Mon, Tue, Wed, Thu, Fri , Sat}; //定义枚举类型
enum Weekday someDay; //定义一个Weekday类型的变量someDay
在枚举类型中,花括号中的标识符都是整形常量,称为枚举常量,不能赋值。
默认下第一个值从0开始;
也就是顺序是从0开始,里面有定义顺序后,从后面+1:
举例:
#include
#include
#include
enum color{red,yellow,blue,green,white};
int main(){
enum color c1,c2;
c1=yellow;
c2=white;
printf("%d,%d\n",c1,c2);
return 0;
}
输出结果是1,4
但是如果里面有标明顺序,那么就按照标明的序号+1:
#include
#include
#include
enum color{red,yellow,blue=4,green,white};
int main(){
enum color c1,c2;
c1=yellow;
c2=white;
printf("%d,%d\n",c1,c2);
return 0;
}
输出结果为 1,6
使用枚举类型的目的是提高程序的可读性,使程序具有自注释性
枚举类型变量的赋值:
可以使用枚举值列表中的一个数给枚举变量赋值.
还可以在条件语句中使用等:
if(someday==Tue)
{语句序列}
等等
举例见专栏 C语言实现
#include
#include
#include
enum color{red,yellow,blue=4,green,white};
int main(){
enum color c1,c2;
c1=yellow;
c2=white;
printf("%d,%d\n",c1,c2);
printf("%d\n",sizeof(c1));
printf("%d\n",sizeof(color));
return 0;
}
它的内存大小是4
因为color是个枚举类型,c1是它的枚举变量,而枚举变量代表的是一个整数。
枚举类型本质上是一种整数类型,其内部表示为整数值。
在C语言中,枚举类型的底层数据类型默认为int,而int类型在32位系统中通常为4字节。因此,color类型的大小也为4字节。
但是也不是绝对的,在C语言中编译器不同可能导致不同
在C语言中,枚举类型的大小默认为int类型,如果枚举成员的数量不足以占用一个int类型的所有位,那么编译器可能会为枚举类型分配比int类型更小的内存空间。
#include
enum my_enum {
ENUM_MEMBER_A,
ENUM_MEMBER_B
};
int main() {
printf("Size of my_enum: %d\n", sizeof(enum my_enum));
return 0;
}
在这个例子中,我们定义了一个枚举类型my_enum,它包含了两个成员:ENUM_MEMBER_A和ENUM_MEMBER_B。在main函数中,我们使用sizeof运算符来获取my_enum类型的大小,并打印输出结果。
运行这个程序,输出结果为:
Size of my_enum: 4
在这个例子中,my_enum类型的大小为4字节,因为它的底层数据类型是int类型。但是,由于my_enum类型只有两个成员,它们的值分别为0和1,因此它们只需要1位二进制数就可以表示。因此,在某些编译器中,my_enum类型的大小可能会被优化为1字节。