1.结构体的声明
1.1结构体的基础知识
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以使不同类型的变量
生活中描述:
人:名字+性别+年龄+身高+身份证号码+地址...
书:书名+作者+出版社+定价+书号...
这些复杂对象不能通过内置类型直接描述和表示,于是就有了结构体。
1.2结构体的声明
例如描述一个学生:
struct Stu { char name[20];//姓名 int age;//年龄 char sex[5];//性别 char id[20];//学号 };//注意分号不能丢
其中name,age,sex,id是结构体的成员变量,用来描述结构体对象的相关属性。
上述的strcut Stu是我们的创建的结构体类型,那在主函数中我们就可以通过这个类型创建结构体变量。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> struct Stu { char name[20];//姓名 int age;//年龄 char sex[5];//性别 char id[20];//学号 };//注意分号不能丢 int main() { struct Stu s1; return 0; }
上述代码中的s1就是我们创建的结构体变量,当然我们也有另外一种方式,就是在创建结构体类型struct Stu的时候,直接在它的大括号外面捎带着创建结构体变量s2,s3和s4。(如下)
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> struct Stu { char name[20];//姓名 int age;//年龄 char sex[5];//性别 char id[20];//学号 }s2, s3, s4;//s2,s3,s4就是结构体变量 - 全局变量 int main() { struct Stu s1;//局部变量 return 0; }
代码中的s1,s2,s3,s4都是结构体变量,只不过s1是局部变量,s2,s3和s4是全局变量。
我们也可以在创建结构体类型时,用 typedef 对其进行类型重命名,
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> typedef struct Stu { char name[20];//姓名 int age;//年龄 char sex[5];//性别 char id[20];//学号 }Stu;//类型重命名为Stu int main() { struct Stu s1;//局部变量 Stu s2; return 0; }
上述代码用 typedef 将struct Stu重命名为Stu,那在主函数中创建结构体变量时就可以直接用Stu来代替struct Stu,代码中的s2就是这样创建的。
注意:如果使用 typedef 的时候,结构体类型的大括号外面不是结构体变量,而是类型的重命名
1.3结构体成员的类型
结构体的成员可以是标量、数组、指针,甚至是其他结构体
struct S { int a; char arr[5]; int* p; }; struct B { char ch[10]; struct S s; double d; }; int main() { return 0; }
1.4结构体变量的定义和初始化
struct S { int a; char arr[5]; int* p; }s1 = {100,"bit",NULL}; struct S s2 = { 99,"hello",NULL }; int main() { struct S s3 = { 1,"abc",NULL }; return 0; }
上述代码的s1,s2和s3都是结构体变量,{}中是对它们的初始化。
有人这时候有问题了,如果不想按照结构体类型中规定的顺序初始化怎么办?
那就要换一种写法了:
struct S { int a; char arr[5]; int* p; }s1 = { .p = NULL,.a = 100,.arr = "bit" }; struct S s2 = { .arr = "hello",.p = NULL,.a = 99 }; int main() { struct S s3 = { .a = 1,.p = NULL,.arr = "abc" }; return 0; }
这两段代码的作用是一样的,我们可以将它们打印一下,不过这就涉及到结构体成员的访问了。
2.结构体成员的访问
前面我们讲过两个结构体成员访问操作符,即 . ->
下面我们来打印一下试试:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> struct S { int a; char arr[5]; int* p; }s1 = { .p = NULL,.a = 100,.arr = "bit" }; struct S s2 = { .arr = "hello",.p = NULL,.a = 99 }; int main() { struct S s3 = { .a = 1,.p = NULL,.arr = "abc" }; printf("%d %s %p\n", s1.a, s1.arr, s1.p); printf("%d %s %p\n", s2.a, s2.arr, s2.p); printf("%d %s %p\n", s3.a, s3.arr, s3.p); return 0; }
运行结果:
想要访问结构体成员,用结构体变量 . 结构体成员名就可以了。
我们也可来尝试对结构体类型 B 创建变量 sb ,初始化并打印一下:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> struct S { int a; char arr[5]; int* p; }s1 = { .p = NULL,.a = 100,.arr = "bit" }; struct S s2 = { .arr = "hello",.p = NULL,.a = 99 }; struct B { char ch[10]; struct S s; double d; }; int main() { /*struct S s3 = { .a = 1,.p = NULL,.arr = "abc" }; printf("%d %s %p\n", s1.a, s1.arr, s1.p);*/ struct B sb = { "thanks",{20,"abcd",NULL},3.14 }; printf("%s %d %s %p %lf", sb.ch, sb.s.a, sb.s.arr, sb.s.p, sb.d); return 0; }
运行结果:
上述代码,因为在struct B 中嵌套了struct S s;所以我们在{}中初始化时要将对struct S s初始化的内容再用个{}包起来。在打印时用两次结构体成员访问操作符(例如:sb.s.a)
以上是点操作符的使用,还有->操作符是在接受一个地址(指针)时使用,
下面看一段代码:
struct Stu { char name[20]; int age; }; void set_stu(struct Stu t) { t.age = 20; t.name = "王小明"; } int main() { struct Stu s = { 0 }; set_stu(s); return 0; }
我们写一个函数set_stu(),对结构体变量s赋值,那上面的代码对吗?
当然不对。
注意这句赋值语句:t.name = "王小明";出现了错误,我们说name是数组名,那你能直接把“王小明”赋给数组名吗?不能,应该是赋给数组名name所在的空间才对!
以前我们学过字符串拷贝strcpy这个函数,这里就可以用了。
struct Stu { char name[20]; int age; }; void set_stu(struct Stu t) { t.age = 20; strcpy(t.name,"王小明");//字符串拷贝 } int main() { struct Stu s = { 0 }; set_stu(s); printf("%s %d", s.name, s.age); return 0; }
问题又来了,这样写就对了吗?
运行一下:
结果是0,为什么呢?
那是因为我们在上述代码中使用的是传值调用,前面我们讲过形参是实参的一份临时拷贝,对形参的改变不影响实参的值。
如果用传址调用就可以了:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> struct Stu { char name[20]; int age; }; void set_stu(struct Stu* t) { t->age = 20; strcpy(t->name,"王小明"); } int main() { struct Stu s = { 0 }; set_stu(&s); printf("%s %d", s.name, s.age); return 0; }
因为传址调用,传给形参的是地址,我们在访问结构体变量时就用到了->操作符
下面我们在看一段->操作符的使用示例:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> struct Stu { char name[20]; int age; char sex[5]; }; void print(struct Stu* t) { printf("%s %d %s\n", t->name, t->age, t->sex); printf("%s %d %s\n", (*t).name, (*t).age, (*t).sex); } int main() { struct Stu s = { "翠花",25,"女" }; print(&s); return 0; }
注意:代码中,t->name 和 (*t).name 的作用是相同的。
3.结构体传参
我们在上面的讲解中已经包含了结构体传参,实际上,结构体传参有两种方式,一种是传结构体,一种是传地址。
那我们直接上代码:
struct S { int data[1000]; int num; }; struct S s = { {1,2,3,4},1000 }; //结构体传参 void print1(struct S s) { printf("%d\n", s.num); } //结构体地址传参 void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { print1(s);//传结构体 print2(&s);//传地址 return 0; }
按道理这两种方式都可以实现传参,但是print1和print2那个更好些呢?
答案是:首选print2函数。
大家想一下,我们在传参时,形参是实参的一份临时拷贝,而如果传过去的结构体特别大,我们需要开辟同样大小的空间,这是不是对空间的一种浪费?而拷贝也是需要时间的,对时间也是一种浪费。而传地址的话,我们只需要传递4/8个字节大小的地址,然后根据地址找到实参的空间就行。
所以结构体传参时,传结构体的地址是一种比较好的选择。
原因:
函数传参的时候,参数是需要压栈的。
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
今天就学到这里,未完待续。。。