出处:https://blog.csdn.net/as480133937/article/details/83473208
目录
1、C语言结构体
1.1几种常用的结构体定义
1.2注意:
2、关于结构体变量的定义和引用
3、结构体的存储(内存对齐)
3.1结构体成员优化
4、结构体自引用
5、结构体的不完整声明
6、结构体传参
结论
7、结构体变量的初始化
7.1定义时赋值
7.2定义结构体之后逐个赋值
7.3定义之后任意赋值
8、结构体数组及其初始化
8.1结构数组初始化
8.2数组初始化
8.3四种方法
9、结构体与指针
9.1指向结构体变量的指针
9.2向结构体数组的指针
9.3结构体成员是指针类型变量
数据经常以成组的形式存在。例如,雇主必须明了每位雇员的姓名、年龄和工资。如果这些值能够存储在一起,访问起来会简单一些。但是,如果这些值的类型不同(就像现在这种情况),它们无法存储于同一个数组中。在C中,使用结构可以把不同类型的值存储在一起。
//结构体名也叫标识符
struct 结构体名{
结构体所包含的变量或数组
};
struct{
结构体所包含的变量或数组
//后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名
//这样做书写简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量。
} name1, name2;
tepedef struct{
结构体所包含的变量或数组
} name;
/* 声明变量时只能:name Stu*/
tepedef struct 结构体名{
结构体所包含的变量或数组
} name;
/* 声明变量时可:name Stu 或者 struct 结构体名 Stu*/
struct 结构体名 *变量名;
总结:最好带有结构体名进行定义声明。
结构与数组的区分:
聚合类型:能够同时存储超过一个的单独数据。
C语言提供了两种类型的聚合数据类型,那就是数组和结构。那么这两者之间有什么区分呢?
数组:它是相同类型的元素的集合;他的每个元素是通过下表引用或指针间接访问来选择(因元素长度相同,课通过下表访问);
结构:是具有不同类型的成员;因结构的成员长度不同,所以不内容能通过下标访问,是通过名字访问;结构的变量属于标量类型
[注意]
- 结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;结构体变量才包含实实在在的数据,才需要内存来存储。
- 结构体中各成员的定义和之前的变量定义一样,但在定义时也不分配空间
- 结构体变量的声明需要在主函数之上或者主函数中声明,如果在主函数之下则会报错
- c语言中的结构体不能直接进行强制转换,只有结构体指针才能进行强制转换
- 相同类型的成员是可以定义在同一类型下的
5.例如:
struct Student
{
int number,age;//int型学号和年龄
char name[20],sex;//char类型姓名和性别
float score;
};
在编译时,结构体的定义并不分配存储空间,对结构体变量才按其数据结构分配相应的存储空间
struct Book
{
char title[20];//一个字符串表
示的titile 题目
char author[20];//一个字符串表示的author作者
float value;//价格表示
};//这里只是声明 结构体的定义
struct Book book1,book2;//结构体变量的定义 分配空间
book1.value;//引用结构体变量
定义结构体变量以后,系统就会为其分配内存单元,比如book1和book2在内存中占44个字节(20+20+4)具体的长度你可以在你的编译器中使用sizeof关键字分别求出来。
例如:
#include
struct Book
{
char title[20];//一个字符事表示的titile题目
char author[20];/∥一个字符表示的author作者
float value;//价格表示
};//这星只是声明结构体的定义
struct Book book1,book2;//结构体变量
int main()
{
sizet size=sizeof(int);
size t sizel=sizeof(char);
size t size2=sizeof(float);
size_t size3=sizeof(book1);
printf("int占%d\nchar占%d\nfloat占%xd\nbook1占%xd",size,size1,size2,size3);
}
注意:用sizeof关键字求结构体长度时,返回的最大基本类型所占字节的整数倍 比方说我们上面求得的为44 为 float(4个字节)的整数倍,但是我们把title修改为title[22]; 这时正常长度为46 ,但是你会发现实际求得的为48,(4的整数倍)
关于这个我们简单介绍下:
偏移量----偏移量指的是结构体变量中成员的地址和结构体变量首地址的差。即偏移字节数,结构体大小等于最后一个成员的偏移量加上他的大小,第一个成员的偏移量为0
struct S1
{
char a;
int b;
double c;
};
这里char a 偏移量为1 之后为int b 因为偏移量1不为int(4)的整数倍,所以会自动补齐,而在 double c 时,偏移量为8 是double(8)的整数倍,所以不用自动补齐 最后求得结构体得大小为 16
如图所示:
结构体对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=编译器默认的对齐数与该成员大小的较小值。VS中默认对齐数为8,linux默认对齐数为4.
- 结构体总大小为最大对齐数(每个成员都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
你可以在声明中对结构的成员列表重新排列,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现。这种做法可以最大限度地减少因边界对齐而带来的空间损失。例如,下面这个结构
struct ALIGN2{
int b;
char a;
char c;
}
所包含的成员和前面那个结构一样,但它只占用8个字节的空间,节省了33%。两个字符可以紧挨着存储,所以只有结构最后面需要跳过的两个字节才被浪费。
#include
//用typedef定义了一个结构体
typedef struct book
{
char title[20];
char author[20];
float value;
}BOOK; //这里的BOOK就等于struct book
int main()
{
B0OK book1={
“Hamlet",
"shakespeare”,
25};
printf("%s\n",book.title);
printf("%d",book.value);
};
等价于:
struct book book1{
"....."
"....."
"....."
...
};
在一个结构内部包含一个类型为该结构本身的成员是否合法呢?这里有一个例子,可以说明这个想法。
struct SELF_REF1{
int a;
struct SELF_REF1b;
int c;
}
这种类型的自引用是非法的,因为成员b是另一个完整的结构,其内部还将包含它自己的成员b。这第2个成员又是另一个完整的结构,它还将包括它自己的成员b。这样重复下去永无止境。这有点像永远不会终止的递归程序。
正确的结构体自引用如下:
struct SELF_REF2{
int a;
struct SELF_REF2*b;
int c;
};
这个声明和前面那个声明的区别在于b现在是一个指针而不是结构。编译器在结构的长度确定之前就已经知道指针的长度,所以这种类型的自引用是合法的。
警告:
警惕下面这个陷阱(没有结构体名):
typedef struct(
int a;
SELF_REF3*b;
}SELF_REF3;
这个声明的目的是为这个结构创建类型名SELF_REF3。但是,它失败了。
类型名直到声明的末尾才定义,所以在结构声明的内部它尚未定义。
解决方案是定义一个结构标签来声明b,如下所示:
typedef struct SELF_REF3_TAG{
int a;
struct SELF_REF3_TAG*b;
int c;
)SELF_REF3;
偶尔,你必须声明一些相互之间存在依赖的结构。也就是说,其中一个结构包含了另一个结构的一个或多个成员。和自引用结构一样,至少有一个结构必须在另一个结构内部以指针的形式存在。
问题在于声明部分:如果每个结构都引用了其他结构的标签,哪个结构应该首先声明呢?
解决方案是使用不完整声明(incomplete declaration):它声明一个作为结构标签的标识符。然后,我们可以把这个标签用在不需要知道这个结构的长度的声明中,如声明指向这个结构的指针。接下来的声明把这个标签与成员列表联系在一起。
struct B;
struct A{
struct B*partner;
/*other declarations*/
);
struct B{
struct A *partner;
/*other declarations */
);
在A的成员列表中需要标签B的不完整的声明。一旦A被声明之后,B的成员列表也可以被声明。
typedef struct{
char product[PRODUCT_SIZE]; //PRODUCT_SIZE = 20
int quantity;
float unit_price;
float total_amount;
} Transaction;
void print_receipt(Transaction trans)
{
printf("%s\n", trans. product);
printf("%d @%.2f total %.2f\n", trans.quantity, trans.unit_price, trans.total_amount;
}
如果current_trans是一个Transaction结构,我们可以像下面这样调用函数:
print_receipt( current_trans);
警告:
这个方法能够产生正确的结果,但它的效率很低,因为C语言的参数传值调用方式要求把参数的一份拷贝传递给函数。机器上整型和浮点型都占4个字节,那么这个结构将占据32个字节的空间。要想把它作为参数进行传递,我们必须把32个字节复制到堆栈中,以后再丢弃。
代码修改:
void print_receipt(Transaction * trans)
{
printf("%s\n", trans->product);
printf("8d e%.2f total %.2f\n", trans->quantity, trans->unit _price, trans->total_amount);
}
这个函数可以像下面这样进行调用:
print_receipt(¤t_trans);
这次传递给函数的是一个指向结构的指针。指针比整个结构要小得多,所以把它压到堆栈上效率能提高很多。
传递指针另外需要付出的代价:是我们必须在函数中使用间接访问来访问结构的成员。结构越大,把指向它的指针传递给函数的效率就越高。
结构体传参有两种形式,一个是传结构体,一个是传地址,但是建议选择传地址。因为在栈帧知识中,函数传参,是需要压入栈的,但是如果传结构体对象时,结构体过大,所占空间也就过大,会导致性能下降。
结构体传参,是不会发生降级,要将参数设置为结构体的地址。
ps:在对结构体变量初始化时,要对结构体成员一一赋值,不能跳过前面成员变量,而直接给后面成员赋初值,但是可以只赋值前面几个,对与后面未赋值的变量,如果是数值型,则会自动赋值为0,对于字符型,会自动赋初值为NULL,即‘\0’
#include
struct Student
{
char cName[28];
int number;
char csex;
float score;
}stu1;
//这里我们只对cName还有number初始化
int mai()
{
struct Student stul={"赵", 12345, };
printf("Name1:%s\nSex1:%c\nSchool Number1:%d\n分数:%f",stul.cName,stu1.csex,stul.number,stu1.score)
}
struct Student
{
char name[20];
char sex;
int number;
}stu1={"zhaozixuan",'M',12345};
//或者
struct Student
{
char name[20];
char sex;
int number;
};
struct Student stu1={"zhaozixuan",'M',12345};
stu1.name="王伟";
stu1.sex='M';
stu1.number=12305;
//也可用strcpy函数进行赋值
strcpy(stu1.name,"王伟");
可参考这篇博文:https://blog.csdn.net/qq_36588941/article/details/90736496
struct Student stu1={
.name="Wang",
.number=12345,
.sex='W',
};//可以对任意变量赋值
这样写的好处时不用按照顺序来进行初始化,而且可以对你想要赋值的变量直接进行赋值,而不想赋值的变量可以不用赋值
需要注意的是如果在定义结构体变量的时候没有初始化,那么后面就不能全部一起初始化了;
结构体数组与结构体变量区别只是将结构体变量替换为数组
struct Student
{
char name[20];
char sex;
int number;
}stu1[5]={
{"zhaozixuan",'M',12345},
{"houxiaohong",'M',12306},
{"qxiaoxin",'W',12546},
{"wangwei",'M',14679},
{"yulongjiao",'W',17857}
};
stu1[3].name[3]//表示stu1的第三个结构变量中姓名的第五个字符
//若初始化时已经是结构体数组全部元素[]中的数可以不写如stu1[]=
这样初始化也是对的:
struct Student stu1[5]={
{"zhaozixuan",'M',12345},
{"houxiaohong",'M',12306},
{"qxiaoxin",'W',12546},
{"wangwei",'M',14679},
{"yulongjiao",'W',17857}
};
注意结构体数组要在定义时就直接初始化,如果先定义再赋初值是错误的
例如:
struct Student stu1;
stu1[3]={
{"zhaozixuan",'M',12345},
{"houxiaohong",'M',12306},
{"qxiaoxin",'W',12546}
};
这样子是错误的!!!!
char str[20];
str="I love you";/* 这样会修改数组的地址,但是数组的地址分配之后是不允许改变的 */
//数组名是常量,不允许被赋值
1.定义数组时直接定义
char str[20]=“I love you”;
2.用strcpy函数进行复制
char str[20];
strcpy(str,“I love you”);
3.用memset函数进行复制
void *memset(void *s,int c,size_t n)
作用:将已开辟内存空间s的首n个字节的值设为值c。
char str[20];
memset(str,'a',20);
如果是字符类型数组的话,memset可以随便用,但是对于其他类型的数组,一般只用来清0或者填-1,如果是填充其他数据就会出错
int str[10];
memset(str,1,sizeof(str));//这样是错误的
这里我们说下这个错误:
首先我们要知道memset在进行赋值时,是按字节为单位来进行赋值的,每次填充的数据长度为一个字节,而对于其他类型的变量,比如int,占4个字节 所以sizeof(str)=40; 而用memset赋值时,将会对指向str地址的前40个字节进行赋值0x01(00000001) 的操作,把0x00000000赋值4次0x01操作变为0x01010101(00000001000000010000000100000001)
相当于给“前10个int”进行了赋值0x01010101的操作 对应十进制的16843009
所以会出很大的错误
#include
#include
#include
char str[20];
int i,num[10];
int main()
{
memset(str, 'a', 20);
for(i=0; i<20; i++)
printf("%c",str[i]);//char类型没有问题
memset(num,5,sizeof(num));
size_t size = sizeof(num);
printf("字节长度为:%d\n", size);
for(i=0; i<20; i++)
printf("%d",mum[i]);//int类型会出现很大的bug
return 0;
}
这里请务必要注意,但是如果是清零一个数组用memset还是很方便的
简单使用的话同学们用strcmp函数就行
4.使用指针(注意内存分配)
char *str;
str=“I love you”;
这两句话的本质是,在内存中开辟一段内存空间,把"I love you"放进这段内存空间,然后把这段内存空间的地址交给str,由于str是变量,所以给它赋值是合法的。
int str[10]={1};//这里只是把str的第一个元素赋值为1,其他元素默认为0
我们知道,指针指向的是变量所占内存的首地址,在结构体中,指针指向的是结构体变量的起始地址,当然也可指向结构体变量的元素
定义形式一般为
struct 结构体名 * 指针名;
比如: struct Student * p;
struct Student
{
char cName[20];
int number;
char csex;
}student1;
struct Student*p;
p=&student1;
//若为结构体数组则
struct Student stu1[5];
struct Student*p;
p=stu1;//因为stu1为结构体数组而p=stu1直接是指向stu1的首地址,就不用再加&符
结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&
用结构体指针变量访问结构体变量成员有以下两种方式:
(*p).cName //这里的括号不能少
p->cName
简单来说以下三种形式是等价的
p->cName
(*p).cName
student1.cName
p->cName //可以进行正常的运算
注意:p->number++; 是将结构体变量中number的值进行运算,然后再加一
在我们想要用指针访问结构体数组的第n个数据时可以用
struct Student stu1[5];
struct Student*p;
p=stu[n];
(++p).number//是指向了结构体数组下一个元素的地址
struct Student
{
char* Name;//这样防止名字长短不一造成空间的浪费
int number;
char csex;
}student1;
在使用时可以很好地防止内存被浪费,但是注意在引用时一定要给指针变量分配地址,如果你不分配地址,结果可能是对的,但是Name会被分配到任意的一的地址,结构体不为字符串分配任何内存存储空间具有不确定性,这样就存在潜在的危险
struct Student
{
char* Name;
int number;
char csex;
}stu,*stu;
stu.name=(char*)malloc(sizeof(char));//内存初始化
这里我们说一下,同学们看书的时候一般不会看到,
如果我们定义了结构体指针变量,他没有指向一个结构体,那么这个结构体指针也是要分配内存初始化的,他所对应的指针类型结构体成员也要相应初始化分配内存