这是今天的学习笔记,主要学习了结构体,链表这两部分。
目录
一 C语言的内存布局规律
根据内存地址从低到高分别划分为:
代码段( Text segment )
数据段( Initialized data segment )- BSS 段( Uninitialized data segment )
栈( Stack )
堆( Heap )
代码段:代码段( Text segment )通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
数据段:数据段( Initialized data segment )通常用来存放已经初始化的全局变量和局部静态变量。
BSS 段:BSS 段( Bss segment / Uninitialized data segment )通常是指用来存放程序中未初始化的全局变量的一块内存区域。 BSS 是英文 Block Started by Symbol 的简称,这个区段中的数据在程序运行前将被自动初始化为数字。
堆:堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩展或缩小。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上;当利用 free 等函数释放内存时,被释放的内存从堆中被剔除。
栈:平时可能经常听到堆栈这个词,一般指的就是这个栈。栈是函数执行的内存区城,通常和堆共享同一片区域。同时要注意函数入栈的顺序是从右往左,仔细看上图。
堆和栈的区别
a.申请方式:
——堆由程序员手动申请
——栈由条统自动分配
b.释放方式:
——堆由程序员手动释放
——栈由系统自动释放
c.生存周期:
——堆的生存周期由动态申请到程序员主动释放为止,不同函数之间均可自由访问
——栈的生存周期由函数调用开始到函数返回时结束,函数之间的局部变量不能互相访问
d.发展方向:
——堆和其它区段一样,都是从低地址向高地址发展一栈则相反,是由高地址向低地址发展
二 宏定义
C语言的三大预处理:宏定义;文件包含;条件编译。
实质:机械替换。宏定义分为带参数和不带参数两种情况
不带参数的情况:就是我们熟悉的直接替换操作。
例如:#define PI 3,这个宏定义的作用是把程序中出现的 PI 在预处理阶段全部替换成 3。
带参数的情况:C 语言允许宏定义带有参数,在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数,这点和函数有些类似。
例如:#define MAX(x, y) (((x) > (y)) ? (x) : (y)),这个宏定义的作用是求出 x 和 y 两个参数中比较大的那一个。写的时候名字和参数要紧密连接在一起,参数和参数可以有空格,因为分开就是替换的意思了。
宏定义就是机械的替换,并没有类型,就是整个程序没有出现之前的预处理阶段,程序并没有被运行就直接被替换。参数是不同的,形参和实参是两个不同的变量是通过栈来进行传输的,因为是变量要个他们指定特定的内存。
#include<stdio.h>
#define S(x)x*x//(X) ((X)*(X))才对
int main(void)
{
int x;
printf("请输入一个整数:");
scanf("%d",&x);
printf("%d平方的值是:%d\n",x,S(x));
printf("%d平方的值是:%d\n",x+1,S(x+1));//S(x+1)是x+1*x+1没有括号
}
请输入一个整数:6
6平方的值是:36
7平方的值是:13
三 内联函数
1.内联函数:宏是直接在预编译阶段替换到程序中的并不会和函数一起申请栈空间,从这个角度来看宏调用效率比较高。但是出错的概率是比较大的,为了使效率更高C语言引入内敛函数来解决效率问题。原因是普通函数是从main开始调用在返回,而内联函数是直接展开往下走。
定入内敛函数的方法也就是在函数前加入 inline 就行。
不过内联函数也不是万能的,内联函数虽然节省了函数调用的时间消耗,但由于每一个函数出现的地方都要进行替换,因此增加了代码编译的时间。另外,并不是所有的函数都能够变成内联函数。现在的编译器就算你不写 inline,它也会自动将一些函数优化成内联函数。
2.# 和 ##:# 和 ## 是两个预处理运算符。在带参数的宏定义中,# 运算符后面应该跟一个参数,预处理器会把这个参数转换为一个字符串。## 运算符被称为记号连接运算符,可以使用它来连接多个参数。
3.可变参数:之前我们学习了如何让函数支持可变参数,带参数的宏定义也是使用可变参数的。
语法:#define SHOWLIST(…) printf(#__VA_ARGS__)
#include <stdio.h>
#define SHOWLIST(...) printf(# __VA_ARGS__)
int main(void)
{
SHOWLIST(FishC, 520, 3.14\n);
return 0;
}
FishC, 520, 3.14
可变参数是支持空参数的。
四 结构体
结构体:结构体是新的数据类型,由不同类型组合而成。
struct Student
{
int num; //学号为整型
char name[20]; //姓名为字符串
char sex; //性别为字符型
int age; //年龄为整型
float score; //成绩为实型
char addr[30]; //地址为字符串
}; //注意最后有一个分号
结构体的声明、定义、初始化、引用
1.结构体的声明
声明一个结构体类型的一般形式
struct 结构体名
{
成员表列
};
2.结构体的定义在定义变量:
当有新的数据类型存在的时候,我们就可以像使用int数据类型一样,用这种数据类型定义相应的变量,并在其中存放具体的数据。有3种方法定义结构体类型变量:
a.先声明结构体类型,再定义该类型的变量。同时声明一个结构体,结构体成员也可以使用另一个结构体。
声明了一个结构体类型 struct Student,可以用它来定义变量。例如:
b.在声明类型的同时定义变量:
struct 结构体名
{
成员表列
} 变量名表列;
声明类型和定义变量放在一起进行,能直接看到结构体的结构,比较直观,在写小程序时用此方式比较方便,但写大程序时,往往要求对类型的声明和对变量的定义分别放在不同的地方,以使程序结构清晰,便于维护,所以一般不多用这种方式。
struct Student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
} studentl, student2;
c.不指定类型名而直接定义结构体类型变量
struct
{
成员表列
} 变量名表列;
指定了一个无名的结构体类型,它没有名字(不岀现结构体名)。显然不能再以此结构体类型去定义其他变量。这种方式用得不多。
3.初始化并引用结构体变量:
1.初始化一个变量和数组:
int a=520;
int array[5]={1,2,3,4,5}
结构体也是一个內型当然也可以初始化
2.struct Student={
10101,
"Li lin",
'M',
"Beijing Road"
}; //初始化
a. 在定义结构体变量时可以对它的成员初始化。初始化列表是用花括号括起来的一些常量,这些常量依次赋给结构体变量中的各成员。
b. 可以引用结构体变量中成员的值,引用方式为
结构体变量名.成员名
“.”是成员运算符,它在所有的运算符中优先级最高,因此可以把sl.num作为一个整体来看待,相当于一个变量。
c. 对结构体变量的成员可以像普通变量一样进行各种运算。
struct student s1, s2;
s2.score = sl.score;
sum = sl.score + s2.score;
sl.age + +;
d. 如果成员本身又是一个结构体类型,则要用若干个成员运算符,一级一级地找到最低一级的成员。
e. 同类的结构体变量可以互相赋值。
struct student s1, s2;
s2 = s1;
#include <stdio.h>
int main()
{
struct Student
{
long int num;
char name[20];
char sex;
char addr[20];
}s1 = { 10101,"Li lin",'M',"Beijing Road" }; //定义结构体变量并初始化
printf("num:%d\n", s1.num);
printf("name:%s\n", s1.name);
printf("sex:%c\n", s1.sex);
printf("s1ddr:%s\n\n", s1.addr);
return 0;
}
num:10101
name:Li lin
sex:M
内存对齐:对三十二位系统来说对齐系数通常是四个字节
#include <stdio.h>
struct Student
{
char a;
int b;
char c;
}s1 = { "x",'520',"0" };//s1 = { "x","0",'520' }修改后定义结构体变量并初始化
int main()
{
printf("size of s1=%d\n", sizeof(s1));
return 0;
}
五 结构体数组和结构体指针
结构体嵌套:
struct Date //声明一个结构体类型 struct Date
{
int month; //月
int day; //日
int year; //年
};
struct Student //声明一个结构体类型 struct Student
{
int num;
char name[20];
char sex;
int age;
struct Date birthday; //成员 birthday 属于 struct Date 类型
char addr[30];
}sq;
特别注意
结构体输出嵌套时是:
printf("%d-%d-%d\n",sq.Date.month.day.year);
结构体数组:
第一种方法是在声明结构体的时候进行定义:
struct 结构体名称
{
结构体成员;
} 数组名[长度];
第二种方法是先声明一个结构体类型(比如上面 Book),再用此类型定义一个结构体数组:
struct 结构体名称
{
结构体成员;
};
struct 结构体名称 数组名[长度];
结构体指针:指向结构体变量的指针我们称之为指针。
struct Book *pt; 声明一个Book结构体类型的指针变量pt
pt=&Book;
通过结构体指针访问结构体成员有两种方法:
(*结构体指针).成员名 和 结构体指针->成员名
第一种方法由于点号运算符(.)比指针的取值运算符(*)优先级要高,所以要使用小括号先对指针进行解引用,让它先变成该结构体变量,再用点运算符去访问其成员。相比之下,第二种方法更加方便和直观。不知道大家有没有发现,第二种方法使用的成员选择运算符(->)自身的形状就是一个箭头,箭头具有指向性,所以我们一下子就把它跟指针联系起来。需要注意的是,两种方法在实现上是完全等价的,所以无论你习惯使用哪一种方法都可以访问到结构体的成员。
但切记,点号(.)只能用于结构体,而箭头(->)只能用于结构体指针,这两个就不能混淆。
#include<stdio.h>
#include<string.h>
int main()
{
struct Student
{
long num;
char name[20];
char sex;
float score;
};
struct Student stu_l;
struct Student* p; //定义指向 struct Student 类型数据的指针变P
p = &stu_l; //初始化结构体指针变量p
stu_l.num = 10101;
strcpy_s(stu_l.name,sizeof(stu_l.name), "Li Lin");
stu_l.sex='M';
stu_l.score = 89.5;
printf("结构体变量输出(stu_l.成员)\nNo.:%ld\nname:%s\nsex:%c\nscore:%5.1f\n", stu_l.num, stu_l.name, stu_l.sex, stu_l.score);
printf("\n指针变量输出((*p).成员)\nNo.:%ld\nname:%s\nsex:%c\nscore:%5.1f\n",(*p).num,(*p).name,(*p).sex,(*p).score);
printf("\n指针变量箭头输出(p->成员)\nNo.:%ld\nname:%s\nsex:%c\nscore:%5.1f\n", p->num, p->name, p->sex, p->score);
//为了使用方便和直观, C 语言允许把(*p).num用p->num 表示,“->”称为指向运算符
return 0;
}
六 传递结构体变量和结构体指针
我们都知道函数调用的时候参数的传递就是一个值传递的过程,将实参赋值给形参。如果结构体变量可以作为函数的参数作为传递的时候,那么结构体变量也可以相互赋值。
#include <stdio.h>
int main()
{
struct Student
{
int x;
int y;
}t1,t2;//定义结构体变量 t1 t2
t1.x=2;
t1.y=3;
t2=t1;
printf("t2.x=%d,t2.y=%d",t2.x,t2.y);
return 0;
}
t2.x=2,t2.y=3
#include <stdio.h>
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char title[128];
char author[40];
float price;
struct Date date;
char publisher[40];
};
struct Book getInput(struct Book book);
void printBook(struct Book book);
struct Book getInput(struct Book book)
{
printf("请输入书名:");
scanf("%s", book.title);
printf("请输入作者:");
scanf("%s", book.author);
printf("请输入售价:");
scanf("%f", &book.price);
printf("请输入出版日期:");
scanf("%d-%d-%d", &book.date.year, &book.date.month, &book.date.day);
printf("请输入出版社:");
scanf("%s", book.publisher);
return book;
}
void printBook(struct Book book)
{
printf("书名:%s\n", book.title);
printf("作者:%s\n", book.author);
printf("售价:%.2f\n", book.price);
printf("出版日期:%d-%d-%d\n", book.date.year, book.date.month, book.date.day);
printf("出版社:%s\n", book.publisher);
}
int main(void)
{
struct Book b1, b2;
printf("请录入第一本书的信息...\n");
b1 = getInput(b1);
putchar('\n');
printf("请录入第二本书的信息...\n");
b2 = getInput(b2);
printf("\n\n录入完毕,现在开始打印验证...\n\n");
printf("打印第一本书的信息...\n");
printBook(b1);
putchar('\n');
printf("打印第二本书的信息...\n");
printBook(b2);
return 0;
}
传递指向结构体变量的指针:在最开始的时候,C语言是不允许直接将结构体作为参数传递给函数的。当初有这么一个限制主要是出于对程序执行效率上的考虑。因为结构体变量的尺寸可以是很大的,那么在函数调用的过程中将会导致空间和时间上的开销也相对是巨大的。既然传递结构体变量可能导致程序的开销变大,那么应该使用指针!
动态的申请结构体:使用malloc函数为结构体分配存储空间,动态地在堆里面给结构体分配存储空间。
七 单链表
1.链表:是一种常见的重要的数据结构。它是动态地进行存储分配的一种结构。
为什么要使用链表? 数组存储的弊端:1.固定长度,连续存储。2.链表根据需要开辟内存单元。
2.链表的组成:
(1) 用户需要用的实际数据;
(2) 下一个结点的地址。
3.链表的存储与查找:
用结构体实现链表:
struct Student
{
int num;
float score;
struct Student* next; // next 是指针变量, 指向结构体变量
};
A.建立简单的静态链表
#include<stdio.h>
struct Student //声明结构体类型 struct Student
{
int num;
float score;
struct Student* next;
};
int main()
{
struct Student a, b, c, * head, * p; //定义 3 个结构体变量 a, b, c 作为链表的结点
a.num = 10101; a.score = 89.5;
b.num = 10103; b.score = 90;
c.num = 10107; c.score = 85;
head = &a; //将结点 a 的起始地址赋给头指针 head
a.next = &b;
b.next = &c;
c.next = NULL;
p = head; //P也指向a结点
do
{
printf("%d%8.1f\n", p->num, p->score); //输出 P 指向的结点的数据
p = p->next; //使 P 指向下一结点
} while (p != NULL);
return 0;
}
B.建立动态链表
B1在单链表中插入元素(头插法)
在单链表中插入元素,事实上只需要修改指针的指向即可:
B2尾插法
单链表的头插法,就是将数据插入到单链表的头部位置。那么相对应的还有另一个种方法:尾插法 将数据插入到单链表的尾部位置。
搜索单链表:有时候我们可能会对单链表进行搜索操作,比如输入这个书名或者作者的名字,可以找到相关的节点数据
B3插入节点到指定的位置
我们之前说单链表和数组相比较的话,最大的优势就是插入元素到指定位置的效率。对于数组来说,插入一个元素到指定的位置,需要将其后面所有的元素都挨个儿移动一次,效率之低下可想而知。相比之下,单链表的效率就要高很多了。因为对于单链表来说,只需要轻轻地改动一下指针即可。
while 函数用于找到符合条件的链表节点,也就是在有序的链表中找到比传入的value更大的值,然后停下来;如果没有,则在链表的尾部位置停止(current == NULL 时结束循环)。由于单链表一旦指向下一个节点,就没办法回头了,所以我们使用 previous 变量来记录 current 节点的上一个节点。最后判断一下 previous 变量,如果为 NULL 的话,说明 while 循环它压根儿就没进去过,有两种情况,要么是这是一个空链表(current == NULL),或者该值比当前链表中所有的节点的 value 成员都小,无论是哪一种情况,我们都将该值插入为单链表的第一个节点即可。
B4 在单链表中删除元素
我们的单链表应该支持删除某一个节点的数据。删除某个节点的数据,其实也是修改指针的事,两个步骤:修改待删除节点的上一个节点的指针,将其指向待删除节点的下一个节点。释放待删除节点的内存空间。