Bootstrap

C语言结构体基础知识

C语言中有许多的内置类型,如

char(1字节)  short(2字节)  int(4字节) long(4字节/8字节) long long(8字节) float(4字节) double(8字节) long double(16/12/8字节)等C语言本身支持的现场类型,但是仅有这些的内置类型是不足的,在描述一个复杂对象,如人的特征。

所以C语言允许有自定义的类型,像结构体-struct,枚举-enum,联合体-union,本章将介绍结构体的相关知识。

1.结构体类型的声明

2.结构体变量的创建和初始化

3.结构体成员访问操作符

4.结构体内存对齐

5.结构体传参

6.结构体实现段位

1.结构体类型的声明

struct tag
{
    member-list;//加入内置类型来修饰tag
}variable-list;//variable-list是一个全局变量且类型为struct tag

列如描述一个学生:

struct Student
{
    char name[20];//学生的姓名
    int age;//年龄
    char id[20];//学号
    char sec;//性别
}a;//a同学的特征

通过内置类型的集合来描述a学生的姓名,年龄,学号,性别。

2.结构体变量的创建和初始化

#include<stdio.h>

struct Student
{
    char name[20];
    int age;
    char id[20];
    char sex[5]; 
};

int main()
{
    struct Student a={"小明",18,"1234567","男"};//按结构体中的顺序初始化
    printf("name:%s\n",a.name);
    printf("age:%d\n",a.age);
    printf("id:%s\n",a.id);
    printf("sex:%s\n",a.sex);


    struct Student b={.age=19,.name="李华",.sex="男",.id="1234"};//按指定的变量初始化
    printf("name:%s\n",b.name);
    printf("age:%d\n",b.age);
    printf("id:%s\n",b.id);
    printf("sex:%s\n",b.sex);
    
    return 0;
}

需要注意的是自定义顺序初始化是要在文件以.c为后缀的,C语言支持这样,cpp(C++)为后缀不支持 。

3.结构体的特殊声明

创建结构体的时候,可以不完全声明。

比如:

struct
{
    int b;
    char c;
    float d;
}a;



struct
{
    int b;
    char  c;
    float d;
}*p;//创建了一个匿名结构体指针(指向这个匿名结构体)

上面俩个结构体都没有名字(匿名),那么如果&a会和*p相等吗?以下是代码:

#include<stdio.h>
struct 
{
    int b;
    char c;
    char d;
}a;

struct 
{
    int b;
    char c;
    char d;
}*p;

int main()
{
    *p=&a;
    return 0;
}

结构如下:

编译器会把上面的俩个声明当成俩个不同的类型,所以是非法的。

匿名的结构体类型,如果没有对结构体重命名的话,基本上只能用一次。

#include<stdio.h>

struct
{
    int a;
    int b;
    int c;
}s={1,2,3};

int main()
{
    printf("%d,%d,%d",s.a,s.b,s.c);
    return 0;
}

因为s是你创建之时带的,所以可以在main里面打印,而后面想使用是做不到的,不知道是什么类型的结构体在main里面创建不出来(匿名是未知的)。

#include<stdio.h>

typedef struct
{
    int a;
    int b;
    int c;
}hh;//把无名氏重命名为hh
struct stu
{
    char a;
    char b;
    char c;
};
typedef struct stu S;//这样也可以达到跟上面重命名一样,把stu重命名为S
int main()
{

    hh s={1,2,3};
    printf("%d,%d,%d",s.a,s.b,s.c);
    return 0;
}

使用重命名可以在main里面创建及初始化。

4.结构体的自引用

可以在结构体中带有本身的结构体是否可以呢?

比如:

struct Node
{
    int data;
    struct Node next;
};//不要忘记在这里加分号

如果是这样的话,那么用操作符sizeof(struct Node)是多少?

这样子会陷入一个死循环,结构体变量的大小会一直叠加变为无穷的大,明显是不合理的。

struct Node
{
    int data;
    struct Node * next;
};

而这样子是没问题的,可以在结构体里面放指向与自己一类型的结构体的指针。

typedef struct Node
{
    int data;
    Node * next;
}Node;

既然重命名了,可以在里面直接用重命名的来声明可以吗?

答案是否定的,重命名是在创建变量之后的,在创建的时候对应的还是原来的名字,此时用重命名的名字会报错。

5.结构体的内存对齐

了解结构体的基本使用,接下来就是深入探讨:计算结构体的大小。

其对应的知识点是:结构体的内存对齐。

先了解结构体的对齐规则:

1.结构体的第一个成员对齐到结构体变量起始位置偏移量为零的地址处。

2.其他成员变量要对齐某个数(对齐数)的整数倍的地址处。

对齐数=编译器默认的一个对齐数与改成员的变量大小的较小值。(VS默认为8,Linux中gcc没有默认对齐数,对齐数就是本身成员变量大小)

3.结构体的总大小为最大对齐数(每个成员都有一个对齐数,结构体变量中对齐数最大的哪一个)的整数倍。

4.如果嵌套结构体,嵌套的结构体对齐数为它本身成员中最大对齐数,在以上述2看成一个对齐数知道,大小知道的成员放入,总大小同上述3.

第一个例子:

#include<stdio.h>
struct s1
{
    char a;
    int b;
    char c;
};



int main()
{
    printf("%zd\n",sizeof(struct s1));//zd--size_t(无符号整数类型),sizeof是操作符计算变量大小
    return 0;
}

首先char a放在0地址处,然后int大小为4比默认的8小(假设是vs),所以对齐数为4,要在偏移量为四的倍数放,在4处放四个字节,char 大小为1比8小,对齐数为1,直接在int后面放(任何数本身都是一的倍数,除0).最后结构体总大小要为最大对齐数的整数倍,char 为1,int 为 4, char 为1,所以最大对齐数为4,现在有九个字节不为4的倍数,而12是满足条件的最小数,所以总大小就为12个字节。(打x为浪费的空间)

第二个例子为嵌套结构体大小计算:

#include<stdio.h>
struct S3
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));

struct S4
{
 char c1;
 struct S3 s3;
 double d;
};

int main()
{
    printf("%d\n", sizeof(struct S3));
    printf("%d\n", sizeof(struct S4));
    return 0;
}

首先char 放在0地址处,结构体s3大小为16(第一个例子同理可得),s3的成员:double char int

对齐数分别为:8 1 4 ,取最大的对齐数8作为s3的对齐数。所以要放在偏移量为8的地方。double d 对齐数为8.刚好s3后面(24)满足直接连上。嵌套结构体的总大小为最大对齐数整数倍(1 8 8 取8),上图可知此时为32满足要求,所以结构体大小就为32.

介绍完了内存对齐,为什么要内存对齐呢?

1.平台原因:

不是所有硬件平台都能访问容易地址上的任意数据,某些平台只能在某些地址取某些特定类型的数据(挑食),否则会硬件异常。

2.性能原因:如果未对齐内存,在访问的时候会出现多访问的情况,如下图以每四个字节读取(假设)那么读对齐的只需要一次(是一次性)就能读完int i,而不对齐则需要俩次(第一次只读了一部分),如果对齐内存能提高读取的速率。总结:结构体的内存对齐是拿空间换时间的做法

设计结构体的时候可以把空间小的成员集中一起,如第一个例子,把int放在最后一个位置上则从大小12变为8(自行尝试)。

修改默认的对齐数(VS环境)

#pragma pack(1)//设置默认对齐数为1

#pragma pack()//取消设置,还原默认对齐数8

对齐数改为1例子:

#include<stdio.h>
struct s1
{
    char a;
    int b;
    char c;
};



int main()
{
    printf("%zd\n",sizeof(struct s1));//zd--size_t(无符号整数类型),sizeof是操作符计算变量大小
    return 0;
}

此时对齐数全为1则直接连续排放,最大的对齐数也为1,所以总大小就为6字节。

6.结构体传参

#include<stdio.h>


struct s1
{
    int arr[100];
    int num;
   
};
struct s1 s={{1,2,3,4,5,6},100};//全局变量


void printf1(struct s1 s)
{

    printf("%d\n",s.num);
}

void printf2(struct s1* s)
{

    printf("%d\n",s->num);//转地址时用->来指引到其值
}


int main()
{
    printf1(s);
    printf2(&s);
    return 0;
}

俩个打印函数得出的结果一样,但是printf2更好。

原因:
函数传参时,参数要压栈(栈区上开辟很结构体s一样大小空间取拷贝传来的数据),会有时间和空间上的系统开销。

如果传一个大小很大的结构体对象时,则需要开辟很大的空间(上图int arr[100]就有400个字节大小),导致性能下降。

传地址则只需要开辟一个指针大小的空间(指针指向结构体s),sizeof(指针)为4或者8个字节(看什么环境)。

结论:结构体传参时,传结构体地址。

7.结构体实现位段

什么是位段:1.位段的成员必须是int,unsigned int,or signed int,在C99也可以选择其他类型

2.位段的成员名后边有一个冒号和一个数字(代表这个变量只用几个bite,2则俩个bite就可以表示)。

比如:
 

struct A
{
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;

};

A就是一个位段类型。

那么A的大小是多少呢?

#include<stdio.h>



struct A
{
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;

};

int main()
{
    printf("%d\n",sizeof(struct A));
    return 0;
}

接下来先讲位段的内存分配就知道为什么A的大小为8.

位段的内存分配

1.位段的成员可以是 int unsigned int signed int 或者是 char 等类型
2  . 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
3.   位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段
#include<stdio.h>

struct S
{
    char a:3;
    char b:4;
    char c:5;
    char d:6;
};

int main()
{

    struct S s={0};
    s.a=10;
    s.b=12;
    s.c=3;
    s.d=4;
    return 0;
}

所以位段S的大小为3字节。(上面A同理为八字节,int所以每次开辟四字节,2+5+10=17位bite 占据四字节,30在占据一个四字节,加起来为八字节)。

VS是小端,所以低地址在前面,高地址在后面。3字节24位,以四位为一个数组成六位16进制。

缺点:

int 位段被当成有符号数还是
⽆符号数是不确定的。
2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会
出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃
剩余的位还是利⽤,这是不确定的。
以上就是结构体基础的知识。

本人入门不久,希望能与大家一起探讨进步,欢迎评论。

;