结构体
在C语言中有许多的类型,诸如:char、short、int、float、double……这些类型可以表述特定的类型,但是在现实中存在着一些复杂的对象,无法单单用这些类型来描述,比如:人、书籍……这些复杂对象就可以通过结构体来抽象出它的特征,然后加以描述。
结构体的声明
结构体的声明格式如下所示:
struct tag
{
成员;
……
}变量名……;
这里的tag就是你自定义的一个变量名,成员就是构成这个结构体的各个类型,而变量就是尼在声明一个结构体的同时就可以创建这个结构体类型的变量。
举个例子:
struct Book
{
char name[20];
char author[20];
int price;
}b1;//分号一定要记得加!!!
struct Book b1;
这就是我所声明的一本书的类型,包含了书的书名、作者、价格,当然根据需要还可以更加详细的声明,而在声明结构体类型之后,就顺带创建了一个该类型的变量b1,当然这不是必须的,你也可以只声明类型,不创建变量,等到main函数里再创建变量也可以,或者声明了类型之后再创建变量也可以,但是记住在main函数外创建的变量就都是全局变量了。
声明匿名结构体
例如:
struct
{
char name[20];
char author[20];
int price;
}b1;
这里就是声明一个匿名结构体类型的方法,只要不加tag就可以了,但是需要注意的是,声明匿名结构体类型,一定要在声明时就创建变量,后续不能通过该类型创建变量了,可以理解为一次性的,如果该类型你只使用一次,那么可以考虑使用匿名结构体类型。
注意:
struct
{
char name[20];
char author[20];
int price;
}b1;
struct
{
char name[20];
char author[20];
int price;
}b2;
以上两个匿名结构体虽然成员都是一致的,然是它们仍是两个不同类型的结构体,也就是说,即使两个匿名结构体的成员列表完全相同,但是它们仍是不同类型的结构体。
结构体的自引用
下面这样的自引用代码是否可行呢?
struct Node
{
int val;
struct Node next;
};
答案是不可行的,因为这个结构体包含了一个和自己一样的结构体,那么该结构体又包含了一个和自己一样的结构体,这样就无限的循环套娃下去了。
所以应该改用指针的方法该自引用:
struct Node
{
int val;
struct Node* next;
};
通过该类型的指针,这样就可以做到自引用而不会无限循环了。
结构体变量的初始化
- 在声明结构体的同时创建变量并初始化:
struct Book
{
char name[20];
int price;
}b1 = {"abcde",50};
这样在声明的同时创建了一个struct Book类型的结构体变量,并完成了初始化,把“abcde”赋值给了name这个数组,把50赋值给了price。
- 用结构体类型创建变量并初始化:
struct Book b2 = {"aaaaa",30};
int main()
{
struct Book b3 = {"ccccc",50};
return 0;
}
这样也是初始化结构体的一种方法,当然在main函数外的是全局变量,在main函数内的是局部变量。
- 嵌套初始化:
在结构体中是可以包含另外一个结构体类型作为成员的,嵌套初始化如下:
struct tag1
{
int x;
int y;
};
struct tag2
{
int a;
struct tag1;
char b[10];
}t1 = {10,{1,2},"hello"};
这就是结构体的嵌套初始化,其实很简单,这里的10是对tag2里的a进行赋值,然后因为tag1是一个结构体,所以对其进行赋值时加一个花括号,花括号里面的1就是对tag1里面的x进行赋值,2就是对tag1里面的y进行赋值,最后的“hello”就是对tag2里面的数组b进行赋值,只需记住对结构体赋值时加上花括号就可以了。
- 不按成员顺序进行赋值:
以上初始化都是按照成员列标的顺序来进行初始化的,但是我们也可以无序的进行初始化,想先初始化哪个就先初始化哪个:
struct Book
{
char a[20];
char b[20];
int c;
};
int main()
{
struct Book b1 = { .c = 20, .b = "hello", .a = "world", };
return 0;
}
通过以上方法就可以做到无序初始化结构体。
typedef类型重定义结构体变量名
现在我们已经会声明一个结构体和对其进行赋值了,但是我们结构体类型不想基础类型一样很简短,我们每次用结构体创建变量的时候都要加上struct,这多多少少有点不简洁了,但是我们可以通过typedef来重定义结构体类型,使得我们再创建结构体变量时不需要再加上struct,具体代码如下:
typedef struct Book
{
char name[20];
int price;
}Book;
int main()
{
Book b1;
return 0;
}
typedef 重定义了struct Book这个结构体类型,给他起了一个别名Book,我们在使用时直接使用Book创建结构体变量即可。
结构体的内存对齐
大家都知道,在32位平台下,int所占大小为4byte,char所占大小为1byte,double所占大小为8byte……
那么结构体所占大小是多少呢?
假设我们有这样一个结构体类型:
struct tag
{
char a;
int b;
char c;
short d;
};
那么该结构体的大小是多少呢?
在vs2019编译器x86平台下,该结构体的大小是12byte,嘶——按道理来说不是char+int+char+short吗?那就是1+4+1+2=8byte,如果不了解结构体的对齐那么确实会得出这样的结果,但是在结构体中是存在对齐的,那么对齐又是什么呢?
所谓的对齐就是结构体的成员要对齐到对齐数的整数倍处,结构体的第一个成员默认对齐都偏移量为0的地址处,往后的成员对齐到对齐数的整数倍处,而对齐数= 编译器默认的对齐数 和 该成员的大小的较小值。
看下图:
char a是第一个成员,所以在0偏移量处,char占一个字节,所以灰色的空间就是char所占的空间,而第二个成员是int b,int的大小是4与编译器默认的对齐数8比较,4较小,所以要对齐到4的倍数处,所以int b所占的空间就从偏移量为4的位置开始,一共占4个字节,到第三个成员是char c,char的大小是1与编译器默认的对齐数8比较,1较小,所以要对齐到1的倍数处,而8是1的倍数,char c 所占大小是一个字节,所以偏移量8这个位置就是char c 的所占空间,到了最后一个成员short d的大小是2个字节与默认对齐数8比较,2较小,所以它的对齐数就是2,要对齐到2的整数倍处,而偏移量9处不是2的倍数,所以浪费掉,到偏移量10处是2的倍数,所以从偏移量10处开始,占两个字节,那么蓝色区域就是short d 所占的空间。
这就是结构体的对齐规则。
如果认为默认的对齐数不合理的话,我们也可以手动修改默认的对齐数,具体代码如下:
#pragama pack(1)//设置默认对齐数为1
struct tag
{
char a;
int b;
char c;
short d;
};
#pragma pack()//取消设置默认对齐数,变回原来的对齐数
如果设置默认对齐数为1,那么就是简单的类型所占大小的相加了,也就是1+4+1+2=8。
建议在设计结构体的时候,尽量让占用空间小的成员集中在一起,这样就可以减少一些空间的浪费。
结构体传参
结构体作为参数时,可以有两种传参方法,一种是传结构体,一种是传结构体指针:
struct tag
{
char a[100];
int b;
};
//传结构体
void test1(struct tag t)
{
......
};
//传结构体指针
void test2(struct tag* pt)
{
......
};
int main()
{
struct tag t;//假设已经初始化
test1(t);//传结构体
test2(&t);//传结构体地址
return 0;
};
直接传结构体的话,形参也会开辟一块和实参一样的空间用来存放结构体,如果实参过大的话,那么形参开辟的空间也会很大,这样占用空间大小就会很多,性能也就降低了,但是传地址的话,就是一个指针无非就是4/8个字节而已,所占空间大小大大的降低了,性能也会提升,所以建议结构体作为参数传参的话,直接传地址即可,如果害怕传地址会修改原来的结构体变量的话,那么只需加上const修饰即可,这样该结构体指针指向的结构体的值就无法被修改了。
//传结构体指针
void test2(const struct tag* pt)
{
......
};
最后
谢谢大家的阅读,如果有哪里写的不对的地方也欢迎大家指正!(不过感觉也没多少人会看就是了)