这篇笔记主要是介绍C语言中的结构体,还涉及补码、算法、链表等。阅读本文预计需要 12 分钟。
Day07 郝斌C语言自学视频之C语言的结构体
结构体
为什么需要结构体
在表示一些复杂的事物,普通的基本类型无法满足实际要求时,或者使用基本类型会繁琐、重复、代码量大时,使用结构体可以极大的节省我们的工作量。
什么叫结构体
把一些基本类型数据组合在一起形成的新的复合数据类型
,这个叫做结构体
。
如何去定义结构体
定义结构体有如下 3 种方式,推荐使用第一种方式。
例 定义结构体
/*
时间:2020年2月25日11:20:13
结构体的定义
*/
# include <stdio.h>
// 第一种方式
struct Student
{
int age;
float score;
char sex;
};
// 第二种方式
struct Student2
{
int age;
float score;
char sex;
} st2;
// 第三种方式
struct
{
int age;
float score;
char sex;
} st3;
int main(void)
{
struct Student st = {80, 66.6, 'F'};
/*
int age;
float score;
char sex;
int age2;
float score2;
char sex2;
*/
return 0;
}
/*
在VSCode中的输出结果是:
--------------------------
--------------------------
*/
怎样使用结构体变量
赋值和初始化:定义的同时可以整体赋初值。如果定义完之后,则只能单个的赋初值。这个跟数组类似。
如何取出结构体变量中的每一个成员【重点】:
结构体变量名.成员名
指针变量名->成员名
(第二种方式更常用)
其中指针变量名->成员名
在计算机内部会被转化成 (*指针变量名).成员名
的方式来执行。所以说这两种方式是等价的。
例 获取结构体变量的成员值
/*
时间:2020年2月25日14:21:48
获取结构体的成员
*/
# include <stdio.h>
// 第一种方式 这只是定义了一个新的数据类型,并没有定义变量
struct Student
{
int age;
float score;
char sex;
};
int main(void)
{
struct Student st = {80, 66.6, 'F'}; // 初始化 定义的同时赋初值
struct Student * pst = &st; // &st不能改成st
pst->age = 88; // 第二种方式
st.score = 66.6f; // 第一种方式 66.6在C 语言中默认是double类型,如果希望一个实数是float类型,则必须在末尾加f或F,因此66.6是double类型,66.f或66.F是float类型
printf("%d %f\n", st.age, pst->score);
return 0;
}
/*
在VSCode中的输出结果是:
--------------------------
88 66.599998
--------------------------
*/
说明:
-
pst->age
在计算机内部会被转化成(*pst).age
,没有什么为什么,这就是->
的含义,这是一种硬性规定。 所以pst->age
等价于(*pst).age
也等价于st.age
。 -
pst->age
的含义:pst
所指向的那个结构体变量的age
的这个成员。
结构体变量和结构体指针作为函数参数传递的问题
推荐使用结构体指针变量(地址)作为函数参数来传递。传值还是传地址,传值内存的开销更大,执行速度也会变慢。传地址内存的开销小,速度快。不过传地址可能出现原始数据被误修改的风险。
例 送地址还是发送值
/*
时间:2020年2月25日15:22:23
示例:
发送地址还是发送值
目的:
指针的优点之一:
传递数据,耗用内存小,执行速度快
*/
# include <stdio.h>
# include <string.h>
struct Student
{
int age;
char sex;
char name[100];
}; // 分号不能省
void InputStudent(struct Student *);
void OutputStudent(struct Student *);
int main(void)
{
struct Student st;
printf("%d\n", sizeof(st));
InputStudent(&st); // 对结构体变量输入 必须发送st的地址
OutputStudent(&st); // 对结构体变量输出 可以发送st的地址也可以发送st的内容,但为了减少内存的耗费,也为了提高执行速度,推荐发送地址
return 0;
}
void OutputStudent(struct Student * pst)
{
printf("%d %c %s\n", pst->age, pst->sex, pst->name);
}
void InputStudent(struct Student * pstu) // pstu 只占4个字节
{
(*pstu).age = 10;
strcpy(pstu->name, "张三");
pstu->sex = 'F';
}
/*
// 本函数无法修改主函数 st 的值,所以本函数是错误的
void InputStudent(struct Student stu)
{
stu.age = 10;
strcpy(stu.name, "张三"); // 不能写成 stu.name = "张三";
stu.sex = 'F';
}
*/
/*
在VSCode中的输出结果是:
--------------------------
108
10 F 张三
--------------------------
*/
结构体变量的运算
结构体变量不能相加,不能相减,也不能相乘除。但结构体变量可以相互赋值。
例
struct Student
{
int age;
char sex;
char name[100];
}; // 分号不能省
struct Student st1, st2;
// st1+st2; st1*st2; st1/st2; 都是错误的
st1 = st2 // 或者st2 = st1 都是正确的
动态构造存放学生信息的结构体数组
动态构造一个数组,存放学生的信息,然后按分数排序输出。
/*
时间:2020年2月25日17:51:12
动态构造一个数组,存放学生的信息,然后按分数排序输出。
*/
# include <stdio.h>
# include <madcapcl.h>
struct Student
{
int age;
float score;
char name[100];
};
int main(void)
{
int len;
struct Student * pArr;
int i, j;
struct Student t;
// 动态的构造一维数组
printf("请输入学生的个数:\n");
printf("len = ");
scanf("%d", &len);
pArr = (struct Student *)malloc(len *sizeof(struct Student));
// 按学生成绩升序排序 冒泡算法
for (i=0; i<len; ++i)
{
printf("请输入%d个学生的信息:\n", i+1);
printf("age = ");
scanf("%d", &pArr[i].age);
printf("name = ");
scanf("%s", pArr[i].name); // name 是数组名,本身就已经是数组首元素的地址,所以 pArr[i].name 不能写成 &pArr[i].name
printf("score = ");
scanf("%f", &pArr[i].score);
}
for (i=0; i<len-1; ++i)
{
for (j=0; j<len-1-i; ++j)
{
if (pArr[j].score > pArr[j+1].score) // > 是升序, < 是降序
{
t = pArr[j];
pArr[j] = pArr[j+1];
pArr[j+1] = t;
}
}
}
printf("\n\n学生的信息是:\n");
// 输出
for (i=0; i<len; ++i)
{
printf("第%d个学生的信息是:\n", i+1);
printf("age = %d\n", pArr[i].age);
printf("name = %s\n", pArr[i].name);
printf("score = %f\n", pArr[i].score);
printf("\n");
}
return 0;
}
/*
在VSCode中的输出结果是:
--------------------------
请输入学生的个数:
len = 3
请输入1个学生的信息:
age = 10
name = zhangsan
score = 66.6
请输入2个学生的信息:
age = 20
name = lisi
score = 55.5
请输入3个学生的信息:
age = 30
name = wangwu
score = 99.9
学生的信息是:
第1个学生的信息是:
age = 20
name = lisi
score = 55.500000
第2个学生的信息是:
age = 10
name = zhangsan
score = 66.599998
第3个学生的信息是:
age = 30
name = wangwu
score = 99.900002
--------------------------
*/
枚举
什么是枚举
把事物所有可能的取值一一列举出来。枚举知道即可,用的不多。
怎样使用枚举
例 枚举的使用
/*
时间:2020年2月25日18:25:26
枚举的使用
*/
# include <stdio.h>
// 只定义了衣蛾数据类型,并没有定义变量,该数据类型的名字叫做 enum WeekDay
enum WeekDay
{
MonDay, TuesDay, WednesDay, ThursDay, FriDay, SaturDay, SunDay
};
int main(void)
{
// int day; // day 定义成int类型不合适
enum WeekDay day = WednesDay;
printf("%d\n", day);
return 0;
}
/*
在VSCode中的输出结果是:
--------------------------
2
--------------------------
*/
枚举的优缺点
使用枚举可以限制用户的输入,使得代码更安全。但是使用枚举代码书写麻烦,用的也不多。
其他
原码
原码也叫 符号-绝对值码。最高位表示0表示正 1表示负,其余二进制位是该数字的绝对值的二进制位。原码简单易懂,但是加减运算复杂,存在加减乘除四种运算,增加了CPU的复杂度,零的表示不唯一。
反码
反码运算不便,也没有在计算机中应用。
移码
移码表示数值平移 n 位,n 称为移码量。移码主要用于浮点数的阶码的存储。
补码
- 已知十进制求二进制
1) 求正整数的二进制
除2取余,直至商为零,余数倒序排序。
2) 求负整数的二进制
先求与负数相对应的正整数的二进制代码,然后将所有位取反,末尾加1,不够位数时,左边补1。
3) 求零的二进制
全是零。 - 已知二进制求十进制
如果首位是0,则表明是正整数,按普通方法求。
如果首位是1,则表明是负整数,将所有位取反末位加1,所得数字就是该负数的绝对值。
如果全是0,则对应的十进制数字就是0.
二进制全部为0的含义
- 数值0.
- 字符串结束标记符
"\0"
. - 空指针
NULL
。NULL
表示的是 0 ,而这个 0 不代表数字 0 ,而表示内存单元的编号 0 。
我们计算机规定了,以0为编号的存储单元的内容不可读,不可写。
算法
通俗定义:解题的方法和步骤。
狭义定义:对存储的数据的操作。对不同的存储结构,要完成某一个功能所执行的操作是不一样的。比如:
要输出数组中所有的元素的操作和输出链表中所有元素的操作肯定是不同的。
这说明:算法是依附于存储结构的,不同的存储结构,所执行的算法是不一样的
。
广义定义:广义的算法也叫泛型。无论数据是如何存储的,对该数据的操作都是一样的。这体现了分层
的思想。我们至少可以用两种结构来存储数据,数组和链表。
链表
数组 VS 链表
区别 | 数组 | 链表 |
---|---|---|
特点 | 内存连续 | 内存不连续 |
优点 | 下标索引速度快,占用内存少 | 插入和删除元素效率高,不需要连续内存 |
缺点 | 插入和删除元素效率低,需要连续内存 | 查找某个位置的元素效率低,占用内存更多 |
首节点:存放第一个有效数据的节点。
尾节点:存放最后一个有效数据的节点。
头节点:头结点的数据类型和首节点的类型是一模一样的。头结点是首节点前面的那个节点。头结点并不存放有效数据。设置头结点的目的是为了方便对链表的操作。
头指针:存放头节点地址的指针变量。
注意:确定一个链表需要一个参数,即头指针
。
位运算符
运算符 | 中文名称 | 解释 | 举例 |
---|---|---|---|
& | 按位与 | 都是1为1,否则为0 | 21&7 = 5 |
| | 按位或 | 有1为1,全0为0 | 1|1=1 |
~ | 按位取反 | 0变1,1变0 | ~3=-4 |
^ | 按位异或 | 相同为0,不同为1 | 10=1;11=0;0^0=0 |
<< | 按位左移 | i<<1,表示把i的所有二进制位左移一位,右边补0 | 左移n为相当于乘以2的n次方 |
>> | 按位右移 | i>>1, 表示把i的所有二进制位右移一位,左边一般补0(也可能补1) | 右移n为相当于除以2的n次方,前提是数据不能丢失 |
面试题:
A) i = i*8;
B) i = i<<3;
请问上述两个语句,哪个语句执行的速度快?
答:B快。
位运算符的现实意义:通过位运算符我们可以对数据的操作精确到每一位。
期末考试部分题目
-
什么叫分配内存,什么叫释放内存?
答:操作系统把某一块内存空间的使用权分配给该程序叫分配内存。
操作系统把分配给该程序的内存空间的使用权利收回,该程序就不能够再使用这一块内存空间,这叫释放内存。
附注:释放内存不是把该内存的内容清零。 -
变量为什么必须得初始化?
答:不初始化,则变量通常就是垃圾值。 -
详细说明系统是如何执行: int i = 5;这个语句的
答:VSCode软件请求操作系统为i分配存储空间,操作系统会在内存中寻找一块空闲的区域,把该区域当做i来使用,VSCode会把i和这块空闲区域关联起来,今后对字母i的操作就是对这块空闲区域操作,把5存储到字母i所关联的内存区域中。
附注:所谓内存区域也就是内存的一块存储单元。 -
请详细列出C语言的所有基本类型。
答:int long int short int char float double -
在printf函数中int用%d输出,请问:long int char double float分别用什么输出?
答:%ld %c %lf %f -
函数的优点是什么?
答:1. 避免重复操作。2. 有利于程序模块化。 -
谈谈你对函数的理解
答:函数是为了减少重复代码,实现代码的模块化,balabala… -
什么是指针,什么是地址,什么是变量指针,三者之间的关系。
答:地址是内存单元的编号。指针就是地址。指针和地址是同一个概念。
指针变量是存放内存单元编号的变量。指针变量和指针是两个完全不同的概念。只不过人们通常把指针变量简称指针。 -
写出静态变量和动态变量的异同。
答:相同: 都需要分配内存。不同:
静态变量是由系统自动分配,自动释放,程序员无法在程序运行的过程中手动分配,也无法在程序运行的过程中手动释放。
静态变量是在栈中分配的。
只有在函数终止之后,静态变量的存储空间才会被系统自动释放。
动态变量是由程序员手动分配,手动释放,程序员可以在程序运行的过程中手动分配,也可以在程序运行的过程中手动释放。
动态变量是在堆中分配的。
程序员可以在函数执行的过程当中的任何一个时刻手动的释放动态变量的空间,不需要等到函数终止时才释放。 -
C语言中哪些知识点是我们学习的重点,请一一列出来。
答:1. 流程控制。2. 函数。3. 指针。4. 静态内存和动态内存。
【说明】
-
本学习笔记整理自B站郝斌老师的《郝斌C语言自学教程》片段P151-P180。
-
笔记中所有代码均在windows10操作系统,在VSCode编辑器中通过测试。具体VSCode C语言开发环境搭建方法请参照我的另一篇CSDN博客——Windows10下利用Visual Studio Code搭建C语言开发环境。
后记
如果对你有所帮助,欢迎关注我的公众号。这个公众号主要是慢慢分享和记录自己学习编程的笔记,比如:C,Python,Java等。后续也会分享自己面试以及在职场上的成长心得。