c语言结构体学习整理(结构体初始化,结构体指针)

出处: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结构体成员是指针类型变量


1、C语言结构体

数据经常以成组的形式存在。例如,雇主必须明了每位雇员的姓名、年龄和工资。如果这些值能够存储在一起,访问起来会简单一些。但是,如果这些值的类型不同(就像现在这种情况),它们无法存储于同一个数组中。在C中,使用结构可以把不同类型的值存储在一起。

1.1几种常用的结构体定义

//结构体名也叫标识符

struct 结构体名{
    结构体所包含的变量或数组
};

struct{
    结构体所包含的变量或数组
    //后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名
    //这样做书写简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量。
} name1, name2;

tepedef struct{
    结构体所包含的变量或数组
} name;
/* 声明变量时只能:name Stu*/

tepedef struct 结构体名{
    结构体所包含的变量或数组
} name;
/* 声明变量时可:name Stu 或者 struct 结构体名 Stu*/

struct 结构体名 *变量名;

总结:最好带有结构体名进行定义声明。


结构与数组的区分:

聚合类型:能够同时存储超过一个的单独数据。

C语言提供了两种类型的聚合数据类型,那就是数组和结构。那么这两者之间有什么区分呢?

数组:它是相同类型的元素的集合;他的每个元素是通过下表引用或指针间接访问来选择(因元素长度相同,课通过下表访问);

结构:是具有不同类型的成员;因结构的成员长度不同,所以不内容能通过下标访问,是通过名字访问;结构的变量属于标量类型

 

1.2注意:

[注意]

  1. 结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;结构体变量才包含实实在在的数据,才需要内存来存储。
  2. 结构体中各成员的定义和之前的变量定义一样,但在定义时也不分配空间
  3. 结构体变量的声明需要在主函数之上或者主函数中声明,如果在主函数之下则会报错
  4. c语言中的结构体不能直接进行强制转换,只有结构体指针才能进行强制转换
  5. 相同类型的成员是可以定义在同一类型下的

 5.例如:


struct Student
{ 
	int number,age;//int型学号和年龄
	char name[20],sex;//char类型姓名和性别
	float score;
};

2、关于结构体变量的定义和引用

在编译时,结构体的定义并不分配存储空间,对结构体变量才按其数据结构分配相应的存储空间


 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的整数倍)

3、结构体的存储(内存对齐)

  1. 结构体整体空间是占用空间最大的成员(的类型)所占字节数的整数倍。
  2. 结构体的每个成员相对结构体首地址的偏移量(offset)都是最大基本类型成员字节大小的整数倍,如果不是编译器会自动补齐,

关于这个我们简单介绍下:

偏移量----偏移量指的是结构体变量中成员的地址和结构体变量首地址的差。即偏移字节数,结构体大小等于最后一个成员的偏移量加上他的大小,第一个成员的偏移量为0

struct S1
{
    char a;

    int b;

    double c;
};

这里char a 偏移量为1 之后为int b 因为偏移量1不为int(4)的整数倍,所以会自动补齐,而在 double c 时,偏移量为8 是double(8)的整数倍,所以不用自动补齐 最后求得结构体得大小为 16

如图所示:

å¨è¿éæå¥å¾çæè¿°

结构体对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=编译器默认的对齐数与该成员大小的较小值。VS中默认对齐数为8,linux默认对齐数为4.
  3. 结构体总大小为最大对齐数(每个成员都有一个对齐数)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

3.1结构体成员优化

你可以在声明中对结构的成员列表重新排列,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现。这种做法可以最大限度地减少因边界对齐而带来的空间损失。例如,下面这个结构

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{
"....."
"....."
"....."
  ...
};

 

4、结构体自引用

在一个结构内部包含一个类型为该结构本身的成员是否合法呢?这里有一个例子,可以说明这个想法。
 

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;

5、结构体的不完整声明

偶尔,你必须声明一些相互之间存在依赖的结构。也就是说,其中一个结构包含了另一个结构的一个或多个成员。和自引用结构一样,至少有一个结构必须在另一个结构内部以指针的形式存在。

问题在于声明部分:如果每个结构都引用了其他结构的标签,哪个结构应该首先声明呢?

解决方案是使用不完整声明(incomplete declaration):它声明一个作为结构标签的标识符。然后,我们可以把这个标签用在不需要知道这个结构的长度的声明中,如声明指向这个结构的指针。接下来的声明把这个标签与成员列表联系在一起。

struct B;

struct A{
    struct B*partner;
    /*other declarations*/
);

struct B{
    struct A *partner;
    /*other declarations */
);

在A的成员列表中需要标签B的不完整的声明。一旦A被声明之后,B的成员列表也可以被声明。

6、结构体传参

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);

这次传递给函数的是一个指向结构的指针。指针比整个结构要小得多,所以把它压到堆栈上效率能提高很多。

传递指针另外需要付出的代价:是我们必须在函数中使用间接访问来访问结构的成员。结构越大,把指向它的指针传递给函数的效率就越高。

结论

结构体传参有两种形式,一个是传结构体,一个是传地址,但是建议选择传地址。因为在栈帧知识中,函数传参,是需要压入栈的,但是如果传结构体对象时,结构体过大,所占空间也就过大,会导致性能下降。

结构体传参,是不会发生降级,要将参数设置为结构体的地址。


 

7、结构体变量的初始化

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)
}

7.1定义时赋值

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};

7.2定义结构体之后逐个赋值

stu1.name="王伟";
stu1.sex='M';
stu1.number=12305;
//也可用strcpy函数进行赋值
strcpy(stu1.name,"王伟");

7.3定义之后任意赋值

可参考这篇博文:https://blog.csdn.net/qq_36588941/article/details/90736496

 struct Student stu1={
  .name="Wang",
  .number=12345,
  .sex='W', 
 };//可以对任意变量赋值

这样写的好处时不用按照顺序来进行初始化,而且可以对你想要赋值的变量直接进行赋值,而不想赋值的变量可以不用赋值

需要注意的是如果在定义结构体变量的时候没有初始化,那么后面就不能全部一起初始化了;

 

8、结构体数组及其初始化

8.1结构数组初始化

结构体数组与结构体变量区别只是将结构体变量替换为数组

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}
  };
  

这样子是错误的!!!!

8.2数组初始化

char str[20];
str="I love you";/* 这样会修改数组的地址,但是数组的地址分配之后是不允许改变的 */
//数组名是常量,不允许被赋值

8.3四种方法

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

9、结构体与指针

我们知道,指针指向的是变量所占内存的首地址,在结构体中,指针指向的是结构体变量的起始地址,当然也可指向结构体变量的元素

9.1指向结构体变量的指针

定义形式一般为
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的值进行运算,然后再加一

9.2向结构体数组的指针

在我们想要用指针访问结构体数组的第n个数据时可以用

struct Student stu1[5];
struct Student*p;
p=stu[n];
(++p).number//是指向了结构体数组下一个元素的地址

9.3结构体成员是指针类型变量

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));//内存初始化

这里我们说一下,同学们看书的时候一般不会看到,
如果我们定义了结构体指针变量,他没有指向一个结构体,那么这个结构体指针也是要分配内存初始化的,他所对应的指针类型结构体成员也要相应初始化分配内存

你可能感兴趣的:(C,语言)