C语言中,有内置类型,也就是C语言本身支持的现成的类型,例如int , char, double, float 等,当然,仅有内置类型是不够的,例如,我们想表示一个变量人,但是现有的类型不支持我们表示,因此,我们引入了自定义类型,自定义类型分为:
cpp
//结构体 struct
//枚举 - enum
//联合体 - union
现在,我们介绍结构体的内容。
1.结构体类型的声明,创建和初始化
结构体声明的方式如下:
cpp
strcut tag //tag是结构体的类型
{
member - list //成员,一个或多个
}vaiable - list //变量名
例如,我们描述一个学生:
cpp
struct Stu
{
char name[20];
int age;
char sex[5];
};
或者,我们可以描述一本书
cpp
struct Book
{
char name[20];
char author[20];
float price;
char id[13];
//...
};
当我们想尝试创建变量时候,可以有哪些方式来创建呢?
(1)创建全局变量的方式
cpp
struct Book
{
char name[20];
char author[20];
float price;
char id[13];
//...
}b3,b4; //全局变量
struct Book b2; //全部变量创建
这样进行创建时,得到的都是全局变量
(2)局部变量创建的方式
cpp
int main()
{
struct Book b1 = {"hello world","zhangsan",18.8,"PG1001"}; //局部变量创建
struct Book b6 = { .id = "PG1002",.author = "lisi",.name = "我叫李四",.price = 88.8 };
struct Book b[5];//数组
r
这里创建的b1和b6都是局部变量,甚至我们可以创建一个结构体的数组,当然,我们在进行变量初始化的时候,也可以有上面两种初始化的方式,第一个是按照顺序初始化,第二个是.+对应内容进行初始化
2.结构体的特殊声明
在结构体的声明时,可以出现不完全声明 - 匿名结构体类型,它的形式如下:
cpp
struct
{
char c;
int i;
double d;
}s;
这里,我们就创建了一个匿名结构体,后续我们使用的时候,直接用s即可
cpp
int main()
{
printf("%c %d %f", s.c, s.i, s.d);
return 0;
}
那么,我们如果进行这样的声明:
cpp
struct
{
char c;
int i;
double d;
}s;
struct
{
char c;
int i;
double d;
}*ps;
cpp
int main()
{
ps = &s; //编译器会把上面两个定义为不同的,因此非法
return 0;
}
这样是不可以的,因为编译器会把以上的两个声明当成两个完全不同的类型,因此是非法的,一般来说,匿名结构体类型,如果没有对结构体重命名的话,基本只能使用一次。
3.结构体自引用的方式
数据结构是数据在内存中存储和组织的结构,有线性数据结构:顺序表,链表,栈,队列
有树形数据结构:二叉树,还有图等
我们可以定义链表的结点:
cpp
typedef struct
{
int data;
struct Node* next; //下一个节点的地址,结构体指针
}Node;
这种书写方式,后续我们可以直接用Node来定义链表的结构体了
当然,也可以写成这样:
cpp
struct Node
{
int data;
struct Node* next; //下一个节点的地址,结构体指针
};
typedef struct Node Node;
请注意,匿名的结构体类型是不能实现结构体的自引用效果的
4.结构体内存对齐
现在我们来讨论结构体的大小,它是符合结构体的对齐规则的:
对齐规则:
1.结构体第一个成员对齐的偏移量为0
2.其他成员变量要对其到某个数字的整数倍的地址处
对齐数 = 编译器默认的一个对其数与成员变量大小的较小值
VS默认为8,Linux中gcc没有默认对齐数,对齐数就是成员自身大小
3.结构体总大小必须为最大的对齐数的整数倍
4.如果嵌套了结构体,嵌套的结构体对齐到自己成员中最大对齐数的整数倍,结构体整体大小就是所有最大对齐数整数倍
现在,我们来举出一个例子,来判断结构体的大小吧
cpp
struct S
{
char c1;//1
int i;//4
char c2;//1
};
这个结构体是占据12个字节的
这个结构体
cpp
struct S
{
char c1;//1
char c2;//1
int i;//4
};
它占据几个字节呢?
我们再看这个结构体
cpp
struct S
{
double d;//8
char c;//1
int i;//4
};
现在,我们看这个结构体的大小
这是一个结构体中有另一个结构体
那么,为什么存在内存对齐呢?
事实上,这是拿空间换取时间的做法,
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。
在设计结构体的时候,我们尽量让占用空间小的成员集中在一起,可以节省空间
修改默认对齐数
我们可以修改默认对齐数,方法如下:
cpp
#pragma pack(1) //设置默认对齐数
//修改默认对齐数
int main0()
{
return 0;
}
#pragma pack() //取消
5.结构体的传参
在结构体中,我们传参时尽量传入结构体的地址,这样可以节省时间和空间
例如,我们定义了一个结构体
cpp
struct S1
{
int arr[100];
int n;
double d;
};
现在我们想写一个函数,输出这个结构体的某些值
我们一般会这样写:
cpp
void print1(struct S tmp) //空间和时间的双重浪费
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", tmp.arr[i]);
}
printf("%d ", tmp.n);
peintf("%lf ", tmp.d);
}
但是,tmp会进行拷贝,这样不仅占用空间,也造成了时间的浪费,但如果我们传入的是结构体的地址呢?
cpp
void print2(const struct S* ps)
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", ps->arr[i]);
}
printf("%d ", ps->n);
printf("%lf", ps->d);
}
这样,就可以了
6.结构体实现位段
什么是位段?
位段的声明和结构是类似的,有两个不同: 1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以 选择其他类型。 2. 位段的成员名后边有⼀个冒号和⼀个数字。
例如,结构体的形式如下:
cpp
struct S
{
int _a;
int _b;
int _c;
int _d;
};
而位段的形式如下:
cpp
struct S
{
int _a:2; //只占两个比特位
int _b:5; //只占五个比特位
int _c:10;
int _d:30;
};
冒号后面的代表这个参数占据的空间大小
那现在,我们就来判断一下这个位段的大小吧!
但是,位段存在跨平台问题,原因如下:
- int位段被当成有符号数还是⽆符号数是不确定的。 2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会 出问题。 3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。 4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的。
网络协议中,位段可以有很好的效果
在位段的使用时,我们应该注意,某些位段并不在字节初始的位置,而一个字节对应一个地址,因此我们不能直接使用scanf的方式给位段赋值,我们应该先创建并赋值一个变量,然后利用 = 来赋值