目录
一、C语言程序设计概述
1、C语言程序的基本结构
#include<stdio.h>
void main()
{
int a,b,c,sum; //定义变量
a=1;b=3;c=5; //以下3行为c语句
sum=a+b+c;
printf("sum = %d\n",sum);
}
其中# include<stdio.h>为标准输入输出头文件,main表示“主函数”。
程序从main()开始执行, 函数体由大括号{}括起来。
//…表示行注释 (/ *… */表示块注释)。
第6行中“%d”是输入输出的“格式字符串”,用来指定输入输出时的数据类型和格式,“\n”是换行符,printf函数中括弧内最右端sum是要输出的变量,现在它的值为9,因此输出一行信息为:sum = 9
2、C语言规范
- 一个C语言程序有且只有一个main函数,是程序运行的起点。
- 编译预处理不是C语言的一部分,不占运行时间,不要加分号。C语言编译的程序称为源程序,它以ASCII数值存放在文本文件中。
- define PI 3.1415926; 这个写法是错误的,一定不能出现分号。
- 在函数中不可以再定义函数。
- 算法:可以没有输入,但是一定要有输出。
- break可用于结束循环结构和switch语句。
- 逗号运算符的级别最低,赋值的级别倒数第二。
- 标识符的要求是由字母,数字,下划线组成,并且第一个必须为字母或是下划线。
- C语言只有八、十、十六进制,没有二进制。但是运行时候,所有的进制都要转换成二进制来进行处理。C语言中的八进制规定要以0开头,八进制是没有8的,逢8进1。C语言中的十六进制规定要以0x开头。
- C语言小数点两边有一个是零的话,可以不用写。
3、标识符与关键字
编程时给变量或者函数起的名字就是标识符。
标识符可以是字母(A~Z,a~z)、数字(0~9)、下划线_组成的字符串,并且第一个字符必须是字母或下划线。在使用标识符时还有注意以下几点:
- 标识符的长度最好不要超过8位,因为在某些版本的C中规定标识符前8位有效,当两个标识符前8位相同时,则被认为是同一个标识符。
- 标识符是严格区分大小写的。例如Imooc和imooc 是两个不同的标识符。
- 标识符最好选择有意义的英文单词组成做到"见名知意",不要使用中文。
- 标识符不能是C语言的关键字。
二、C语言的基本数据类型与表达式
1、基本数据类型
基本类型 | 表示形式 | 格式说明 | 字节数 |
---|---|---|---|
短整型 | short | %hd | 2字节 |
整型 | int | %d | 4字节 |
长整型 | long | %ld | 4字节 |
单精度实型 | float | %f | 4字节 |
双精度实型 | double | %lf | 8字节 |
字符型 | char | %c | 1字节 |
2、常量与变量
变量是以标识符的形式来表示其类型,在c语言中,是用类型说明语句对变量进行定义,其定义形式如下:
其中,类型说明符是c语言的一个有效的数据类型,如整型说明符为int,字符型类型说明符为char等。
变量表的形式是:
即:用逗号分隔的变量名的集合,最后用一个分号结束定义。
例如:
3、运算符与表达式
1.算术运算符 | ( + - * / % ) |
---|---|
2.关系运算符 | ( > < == >= <= != ) |
3.逻辑运算符 | ( ! && 丨丨) |
4.位运算符 | (<< >> ~ 丨 ^ & ) |
5.赋值运算符 | ( = ) |
6.条件运算符 | ( ? :) |
7.逗号运算符 | ( , ) |
8.指针运算符 | ( * 和 & ) |
3.1、算数运算符与算数表达式
+ | (加法运算符,或正值运算符。如3+5、+3) |
---|---|
- | (减法运算符,或负值运算符。如5-2、-3) |
* | (乘法运算符。如3*5) |
/ | (除法运算符。两边都是整型的话,结果就是一个整型。如果有一边是小数,那么结果就是小数) |
% | (模运算符,或称求余运算符,%两侧均应为整型数据,如7%4的值为3) |
3.2、赋值运算符与赋值表达式
3.2.1、赋值运算符
赋值符号“=”就是赋值运算符,它的作用是将一个数据赋给一个变量。如“a=3”的作用是执行一次赋值操作。把常量3赋给变量a,也可以将一个表达式的值赋给一个变量。
在赋值符“=”之前加上其他运算符,可以构成复合的运算符。例如:
3.2.2、赋值表达式
由赋值运算符将一个变量和一个表达式连接起来的式子称为“赋值表达式”。
它的一般形式为:
例如:
赋值表达式也可以包含复合的赋值运算符,如:
4、数据类型转换
4.1、自动类型转换
char类型数据转换为int类型数据遵循ASCII码中的对应值,ASCII码请查看WIKI。
4.2、强制类型转换
可以利用强制类型转换运算符将一个表达式转换成所需类型。;如:
其一般形式为:
注意:
- 表达式应该用括号括起来,如果写成(int)x+y,则只将x转换成整型,然后与y相加
- 转换后不会改变原数据的类型及变量值,只在本次运算中临时性转换。
- 强制转换后的运算结果不遵循四舍五入原则
例如:
4.3、自增、自减运算符
三、顺序结构程序设计
1、数据输入输出
1.1、数据的输入:
1.2、数据的输出:
四、选择结构程序设计
1、关系运算符
2、逻辑运算符
逻辑运算的值也是有两种分别为“真”和“假”,C语言中用整型的1和0来表示。其求值规则如下:
- 与运算(&&)
- 或运算(||)
- 非运算(!)
3、if 语句
(一)单分支if语句(如果满足某个条件,就做某件事情,不满足就向下执行)
if(条件)
{执行语句;}
(二)双分支if语句(如果满足某个条件,就做某件事情,否则就做另外一件事情)
if(条件)
{执行语句1;}
else
{执行语句2;}
(三)多分支if语句(满足哪一个条件,就做另相应的语句)
if(条件1)
{执行语句1;}
else if(条件2)
{执行语句2;}
else if(条件3)
{执行语句3;}
else if(条件4)
{执行语句4;}
else
{执行语句5;}
(四)if的嵌套(根据具体情况对if~else ~语句进行灵活的应用)
if(条件1)
if(条件2)
{执行语句a;}
else
{执行语句b;}
else
if(条件3)
{执行语句c;}
else
{执行语句d;}
4、switch 语句
switch(表达式)
{
case 值1:{语句1;break;}
case 值2:{语句2;break;}
case 值3:{语句3;break;}
......
case 值n:{语句n;break;}
default:{语句n+1;}
}
五、循环结构程序设计
1、while 语句
while(条件) 例如:求1+2+3+...100
{ int n=1,s=0;
循环体 while(n<=100)
} {
s=s+n;
n=n+1;
}
2、do…while 语句
do 例如:求1+2+3+...100
{ int n=1,s=0;
循环体 do
}while(条件); {
s=s+n;
n=n+1;
}while(n<=100);
3、for 语句
for(循环变量初值;循环变量范围;步长) 例如:求1+2+3+...100
{ int n,s=0;
循环体 for(n=1;n<=100;n++)
} {
s=s+n;
}
4、break、continue和goto语句
4.1、使用break可以强制退出循环,其用法必须配合if来使用
for(i=1;i<n;i++)
{
......
if(tj)
{
......
break;
}
......
}
4.2、结束语句之continue语句
运行结果:
continue语句的作用是结束本次循环开始执行下一次循环。
break语句与continue语句的区别是:break是跳出当前整个循环,continue结束本次循环开始下一次循环。
4.3、循环的嵌套
for(i=1;i<n;i++)
{
......
for(j=1;j<m;j++)
{
......
}
......
}
六、函数的定义与调用
1、函数的定义
1.1、有参与无参
1.2、形参与实参
形参是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数,实参是在调用时传递该函数的参数。
函数的形参和实参具有以下特点:
- 形参只有在函数内部有效。
- 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。因此应预先用赋值等办法使实参获得确定值。
- 实参和形参在数量上,类型上,顺序上应严格一致。
例如:以下函数multiDouble(int x)实现把x乘以2再返回给函数调用处。
1.3、返回值
函数的返回值是指函数被调用之后,执行函数体中的程序段所取得的并返回给主调函数的值。
函数的返回值要注意以下几点:
- 函数的值只能通过return语句返回主调函数。return语句的一般形式为:
-
函数值的类型和函数定义中函数的类型应保持一致。如果两者不一致,则以函数返回类型为准,自动进行类型转换。
-
没有返回值的函数,返回类型为void。
注意:void函数中可以有执行代码块,但是不能有返回值,另void函数中如果有return语句,该语句只能起到结束函数运行的功能。其格式为:return;
2、函数的调用
- 无参函数调用:函数名() 例如:f1()
- 有参函数调用:函数名(实参列表)
这里的实参可以是一个具体的值,或者是一个已经有值的变量,也可以是一个表达式。
例如:f2(5) 或者 int n=20;f2(n); - 有返回值、有参调用:类型变量=函数名(实参列表) 例如:int s,a=1,b=2;s=f3(a,b);
3、函数的递归调用
3.1、递归调用(一)
递归就是一个函数在它的函数体内调用它自身。执行递归函数将反复调用其自身,每调用一次就进入新的一层。
例如:计算n的阶乘可以使用以下代码:
运行结果为:5的阶乘=120
3.2、递归函数(二)
我们对上一小节中求5的阶乘这个例子进行一下剖析,看一看他的运算过程:
程序在计算5的阶乘的时候,先执行递推,当n=1或者n=0的时候返回1,再回推将计算并返回。由此可以看出
递归函数必须有结束条件。
递归函数特点:
- 每一级函数调用时都有自己的变量,但是函数代码并不会得到复制,如计算5的阶乘时每递推一次变量都不同;
- 每次调用都会有一次返回,如计算5的阶乘时每递推一次都返回进行下一次;
- 递归函数中,位于递归调用前的语句和各级被调用函数具有相同的执行顺序;
- 递归函数中,位于递归调用后的语句的执行顺序和各个被调用函数的顺序相反;
- 递归函数中必须有终止语句。
一句话总结递归:自我调用且有完成状态。
4、变量的作用域与存储方式
4.1、作用域
**局部变量也称为内部变量。**局部变量是在函数内作定义说明的。其作用域仅限于函数内, 离开该函数后再使用这种变量是非法的。在复合语句中也可定义变量,其作用域只在复合语句范围内。
**全局变量也称为外部变量,**它是在函数外部定义的变量。它不属于哪一个函数,它属于一个源程序文件。其作用域是整个源程序。
4.2、存储方式
C语言根据变量的生存周期来划分,可以分为静态存储方式和动态存储方式。
静态存储方式:是指在程序运行期间分配固定的存储空间的方式。**静态存储区中存放了在整个程序执行过程中都存在的变量,**如全局变量。
动态存储方式:是指在程序运行期间根据需要进行动态的分配存储空间的方式。**动态存储区中存放的变量是根据程序运行的需要而建立和释放的,**通常包括:函数形式参数;自动变量;函数调用时的现场保护和返回地址等。
C语言中存储类别又分为四类:自动(auto)、静态(static)、寄存器的(register)和外部的(extern)。
- 用关键字auto定义的变量为自动变量,auto可以省略,auto不写则隐含定为“自动存储类别”,属于动态存储方式。如:
- 用static修饰的为静态变量,如果定义在函数内部的,称之为静态局部变量;如果定义在函数外部,称之为静态外部变量。如下为静态局部变量:
七、数组
1、一维数组(遍历)
1.一维数组:名字相同且带有下标的一组变量
2.定义形式:类型名 数组名[长度]
例如:int a[10] //a数组中包含有10个整型变量,名字分别是a[0]、a[1]、a[2]......a[9], 这里每一个a[n]都是一个普通的整型变量
3.一维数组的初始化:定义数组的时候就给每一个数组元素赋值
int a[5]={2,4,6,8,10};
4.对a数组中的每一个元素依次进行操作的方法:
int i;
for(i=0;i<9;i++)
{
...a[i]...
}
2、二维数组(遍历)
1.二维数组:名字相同且带有2个下标的一堆变量,逻辑上可以看出n行m列的二维表格。
2.定义形式:类型名 数组名[行长度][列长度]
例如:int a[3][4]
//这里a数组中包含有3*4=12个数组元素,名字分别是:
a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]
3.二维数组的初始化:
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12};
4.对a数组的访问,需要定义2个下标变量i和j,a[i][j]代表第i行第j列的元素。
按行的顺序访问: 按列的顺序访问:
int i,j; int i,j;
for(i=0;i<3;i++) for(j=0;j<4;j++)
{ {
for(j=0;j<4;j++) for(i=0;i<3;i++)
{ {
...a[i][j]... ...a[i][j]...
} }
} }
3、字符数组和字符串
1.字符串:字符型的一维数组,可以存储一串字符,并以“\0”做为结束标志。
2.字符串的输入与输出
char str1[20],str2[20],str3[20];
gets(str1); //字符串的输入
scanf("%s",str2); //字符串的输入
puts(str1); //字符串的输出
printf("%s",str2); //字符串的输出
3.对字符串的访问:如有char s1[20];int n;
字符串的长度:n=strlen(s1);
第一个字符:s1[0];
最后一个字符:s1[n-1];
"\0"的位置:s1[n];
4.字符串常用函数:
注意:用以下函数需要用#include “string.h”引入头文件。
strcat(str1,str2); //连接字符串
strcmp(str1,str2); //比较两个字符串的大小
strlen(str1); //球字符串的长度
char c="a";strchr(str1,c);//在字符串中查找一个字符,并返回其位置。
strcpy(str3,str1); //复制字符串
strstr(str1,str2); //在字符串str1中查找str2的位置
4、数组作为函数的参数
数组可以由整个数组当作函数的参数,也可以由数组中的某个元素当作函数的参数:
1、整个数组当作函数参数,即把数组名称传入函数中,例如:
2、数组中的元素当作函数参数,即把数组中的参数传入函数中,例如:
数组作为函数参数时注意以下事项:
1、数组名作为函数实参传递时,函数定义处作为接收参数的数组类型形参既可以指定长度也可以不指定长度。
2、数组元素作为函数实参传递时,数组元素类型必须与形参数据类型一致。
八、指针
1、指针的概念
1.指针:变量的地址
2.指针变量:能存指针的变量
3.指针变量的定义:int *p,x;
4.对于变量指针的操作:
p=&x; //赋值,把x的地址赋给p,即p指向x
*p: // 通过p间接访问x
*p=*p+10: //将x的值加上10
2、指向一维数组的指针
1.定义与赋值:int a[20],*p;p=a;
2.数组名a就是数组的首地址,也就是a[0]的地址
3.利用指针访问数组:
int a[10],*p,i;
p=a;
for(i=0;i<10;i++)
{
p[i]; //相当于*(p+i)
}
3、指向字符串的指针
1.定义:char s[20],*p;
2.赋值:p=s; //p指向字符串
3.利用指针访问字符串:
char s[20],*p;
p=s;
while(*p) //相当于p!=0或p!="\0"
{
...*p...;
p++;
}
九、结构体数据类型与链表
1、结构体类型的定义
我们能不能使用一个变量存储一个学生的学号,姓名,性别,年龄,出生年月等个人的基本信息(也就是一个变量代表一个学生,一个变量能查找一个学生的所有信息),像我们学的int、float等基本数据类型是做不到的,你可能会想到数组,但是数组元素的数据类型是相同的,我们的姓名需要使用字符串来存储,年龄是需要int类型的来存储,所以数组也不行。于是C语言便允许用户建立由不同类型数据组合的组合型数据结构——结构体类型
定义格式:
struct 结构体名{
成员列表
};//一定要注意这个分号
注意两个概念:
2、结构体类型变量
比如我们要定义一个结构体类型用于来存储一个学生的信息:
struct Date{ //用于存储学生的出生年月
int month;
int day;
int year;
};
struct student{ //用于存储学生的信息
int num;
char name[20];
char sex;
int age;
struct Date birthday;//结构体嵌套
char addr[30];
}
已经定义结构体类型(相当于一个模板),我们有三种定义结构体类型变量(相当于一个学生的实体)的方法:
- 先声明结构体类型,再定义结构体变量
#include<stdio.h>
struct Date{ //用于存储学生的出生年月
int month;
int day;
int year;
};
struct student{ //用于存储学生的信息
int num;
char name[20];
char sex;
int age;
struct Date birthday;//结构体嵌套
char addr[30];
};
int main()
{
struct student student1;//定义结构体变量student1
}
- 在声明类型的同时定义变量
struct 结构体名{
成员列表
}变量名列表;
例子:
struct student{ //用于存储学生的信息
int num;
char name[20];
char sex;
int age;
struct Date birthday;//结构体嵌套
char addr[30];
}student1,student2;//student1与student2是定义的变量
- 不直接类型名直接定义结构体类型变量
struct{
成员列表
}变量名列表;
注意:当我们使用这种方法定义结构体类型时,变量只能在变量名列表处定义,不能再定义其他的变量,因为这种方式定义的结构体类型没有结构体名
2.1、结构体变量的使用
结构体变量的使用:
#include<stdio.h>
struct Date{ //用于存储学生的出生年月
int month;
int day;
int year;
};
struct student{ //用于存储学生的信息
int num;
char name[20];
char sex;
int age;
struct Date birthday;//结构体嵌套
char addr[30];
};
int main()
{
struct student student1;//定义结构体变量student1
printf("请输入学生的学号:");
scanf("%d",&student1.num);
printf("请输入学生的姓名:");
scanf("%s",student1.name);
printf("请输入学生的性别(M/W):");
getchar();//吸收回车
scanf("%c",&student1.sex);
printf("请输入学生的年龄:");
scanf("%d",&student1.age);
printf("请输入学生的地址:");
scanf("%s",student1.addr);
printf("请分别输入学生出生年、月、日:");
scanf("%d%d%d",&student1.birthday.year,&student1.birthday.month,&student1.birthday.day);
printf("该学生的学号为:%d\n",student1.num);
printf("该学生的姓名为:%s\n",student1.name);
printf("该学生的性别为:%c\n",student1.sex);
printf("该学生的年龄为:%d\n",student1.age);
printf("该学生的地址为:%s\n",student1.addr);
printf("该学生的出生年月为:%d %d %d",student1.birthday.year,student1.birthday.month,student1.birthday.day);
}
//输入输出:
请输入学生的学号:1001
请输入学生的姓名:张三
请输入学生的性别(M/W):M
请输入学生的年龄:18
请输入学生的地址:南阳
请分别输入学生出生年、月、日:2000 10 12
该学生的学号为:1001
该学生的姓名为:张三
该学生的性别为:M
该学生的年龄为:18
该学生的地址为:南阳
该学生的出生年月为:2000 10 12
--------------------------------
3、结构体类型数组
3.1、结构体类型数组的定义
- 在定义结构体类型的时候定义结构体数组:
struct 结构体名{
成员列表
}数组名[数组长度];
- 使用结构体类型名定义结构体数组:
结构体类型名 数组名[数组长度];
3.2、结构体类型数组的初始化
其实和int等类型的数组初始化差不多,举个栗子:
#include<stdio.h>
struct student{
int num;
char name[20];
};
int main()
{
struct student stu[2]={1001,"张三",1002,"李四"};
//或者 struct student stu[2]={{1001,"张三"},{1002,"李四"}};
for(int i=0;i<2;i++)
{
printf("第%d个学生的学号是:%d\n",i+1,stu[i].num);
printf("第%d个学生的姓名是:%s\n",i+1,stu[i].name);
}
}
//输出结果:
第1个学生的学号是:1001
第1个学生的姓名是:张三
第2个学生的学号是:1002
第2个学生的姓名是:李四
--------------------------------
4、结构体类型指针
4.1、指向结构体变量的指针
例子:
#include<stdio.h>
struct student{
int num;
char name[20];
};
int main()
{
struct student *stu;
struct student s;
printf("请输入学生的学号:");
scanf("%d",&s.num);
printf("请输入学生的姓名:");
scanf("%s",s.name);
stu=&s;
printf("该学生的学号为:%d\n",stu->num);
//等同于 printf("该学生的学号为:%d\n",(*stu).num);
printf("该学生的姓名为:%s",stu->name);
//等同于 printf("该学生的学号为:%s\n",(*stu).name);
}
4.2、指向结构体数组的指针
例子:
#include<stdio.h>
struct student{
int num;
char name[20];
};
int main()
{
struct student stu[2]={1001,"张三",1002,"李四"};
struct student *p;
int i=1;
for(p=stu;p<stu+2;p++)
{
printf("第%d个学生的学号是:%d\n",i,p->num);
printf("第%d个学生的姓名是:%s\n",i,p->name);
i++;
}
}
5、结构体与函数
5.1、结构体指针作为函数参数
结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。
计算全班学生的总成绩、平均成绩和以及 140 分以下的人数:
#include <stdio.h>
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}stus[] = {
{"Li ping", 5, 18, 'C', 145.0},
{"Zhang ping", 4, 19, 'A', 130.5},
{"He fang", 1, 18, 'A', 148.5},
{"Cheng ling", 2, 17, 'F', 139.0},
{"Wang ming", 3, 17, 'B', 144.5}
};
void average(struct stu *ps, int len);
int main(){
int len = sizeof(stus) / sizeof(struct stu);
average(stus, len);
return 0;
}
void average(struct stu *ps, int len){
int i, num_140 = 0;
float average, sum = 0;
for(i=0; i<len; i++){
sum += (ps + i) -> score;
if((ps + i)->score < 140) num_140++;
}
printf("sum=%.2f\naverage=%.2f\nnum_140=%d\n", sum, sum/5, num_140);
}
//运行结果:
sum=707.50
average=141.50
num_140=2
6、链表
6.1、什么是链表
当一个班级有50个人,那么我们需要定义数组长度为50的结构体数组来存储这些学生的信息,如果新来了几个学生,那么我们则需要再重新定义一个更大的数组,比如60个大小(你不能申请一个足够大的数组,这样做太浪费内存空间)。这时候就可以用我们的链表存储。
看下面一段代码:
struct student{
int num;
char name[20];
struct student *next;
}
上面一段代码中,结构体类型是struct student,存储着学生的基本信息,里边还有一个struct student *next成员变量,他是一个指向struct student类型数据的指针,也就是说,他的存储结构可以如下所示:
这种能够把数据之间进行连接的数据结构称为链表,链表中的每一个元素称为结点,每个结点中存放指针的空间称为指针域,存放其他信息的空间称为数据域。
一般的链表有头指针,头结点,尾结点指向NULL,如下:
我们可以把链表想象成一个火车,头结点相当于火车头,火车头负责连接第一节车厢,火车头里边不放乘客只是与第一个存放乘客的车厢建立联系(相当于数据域不赋值,指针域指向第一个结点)
实例:
#include<stdio.h>
struct student{ //用于存储学生的信息
int num;
char name[20];
struct student *next;
};
int main()
{
struct student stu2={1002,"李四",NULL};
struct student stu1={1001,"张三",&stu2};
struct student stu0;
stu0.next=&stu1;
struct student *head=&stu0;
struct student *p=head->next;//此时p指向第一个结点stu1
int i=1;
while(p!=NULL)//遍历学生信息
{
printf("第%d个学生的学号为:%d",i,p->num);
printf("第%d个学生的姓名为:%s\n",i,p->name);
i++;
p=p->next;
}
}
//输出:
第1个学生的学号为:1001第1个学生的姓名为:张三
第2个学生的学号为:1002第2个学生的姓名为:李四
--------------------------------
上面程序对应下图:
6.2、静态链表,动态链表
所有结点都是在程序中定义的,不是我们自己申请的内存(由系统自动分配内存空间),用完后系统自动释放,这种链表称为静态链表。如6.1的例子就是如此,所谓动态链表就是我们手动开辟内存存放结点,需要回收时我们手动释放的链表。
6.3、链表的创建
代码实现:
//声明节点结构
typedef struct Link{
int elem;//存储整形元素
struct Link *next;//指向直接后继元素的指针
}link;
//创建链表的函数
link * initLink(){
link * p=(link*)malloc(sizeof(link));//创建一个头结点
link * temp=p;//声明一个指针指向头结点,用于遍历链表
//生成链表
for (int i=1; i<5; i++) {
//创建节点并初始化
link *a=(link*)malloc(sizeof(link));
a->elem=i;
a->next=NULL;
//建立新节点与直接前驱节点的逻辑关系
temp->next=a;
temp=temp->next;
}
return p;
}
从实现代码中可以看到,该链表是一个具有头节点的链表。由于头节点本身不用于存储数据,因此在实现对链表中数据的"增删查改"时要引起注意。
6.4、链表的插入元素
根据添加位置不同,可分为以下 3 种情况:
- 插入到链表的头部(头节点之后),作为首元节点;
- 插入到链表中间的某个位置;
- 插入到链表的最末端,作为链表中最后一个数据元素;
虽然新元素的插入位置不固定,但是链表插入元素的思想是固定的,只需做以下两步操作,即可将新元素插入到指定的位置:
- 将新结点的 next 指针指向插入位置后的结点;
- 将插入位置前结点的 next 指针指向插入结点;
例如,我们在链表 {1,2,3,4} 的基础上分别实现在头部、中间部位、尾部插入新元素 5,其实现过程如图 :
从图中可以看出,虽然新元素的插入位置不同,但实现插入操作的方法是一致的,都是先执行步骤 1 ,再执行步骤 2。
代码实现:
//p为原链表,elem表示新数据元素,add表示新元素要插入的位置
link * insertElem(link * p, int elem, int add) {
link * temp = p;//创建临时结点temp
//首先找到要插入位置的上一个结点
for (int i = 1; i < add; i++) {
temp = temp->next;
if (temp == NULL) {
printf("插入位置无效\n");
return p;
}
}
//创建插入结点c
link * c = (link*)malloc(sizeof(link));
c->elem = elem;
//向链表中插入结点
c->next = temp->next;
temp->next = c;
return p;
}
注意:
由于有了头结点,所以插入的时候,在第一个结点前插入就和所有结点进行了统一。
6.5、链表的删除元素
从链表中删除指定数据元素时,实则就是将存有该数据元素的节点从链表中摘除,但作为一名合格的程序员,要对存储空间负责,对不再利用的存储空间要及时释放。因此,从链表中删除数据元素需要进行以下 2 步操作:
- 将结点从链表中摘下来;
- 手动释放掉结点,回收被结点占用的存储空间;
其中,从链表上摘除某节点的实现非常简单,只需找到该节点的直接前驱节点 temp,执行一行程序:
temp->next=temp->next->next;
例如,从存有 {1,2,3,4} 的链表中删除元素 3,则此代码的执行效果如图 2 所示:
代码实现:
//p为原链表,add为要删除元素的值
link * delElem(link * p, int add) {
link * temp = p;
//遍历到被删除结点的上一个结点
for (int i = 1; i < add; i++) {
temp = temp->next;
if (temp->next == NULL) {
printf("没有该结点\n");
return p;
}
}
link * del = temp->next;//单独设置一个指针指向被删除结点,以防丢失
temp->next = temp->next->next;//删除某个结点的方法就是更改前一个结点的指针域
free(del);//手动释放该结点,防止内存泄漏
return p;
}
我们可以看到,从链表上摘下的节点 del 最终通过 free 函数进行了手动释放。
同样的因为有了头结点,才会有对所有结点的删除实现统一。
6.6、链表的查找元素
在链表中查找指定数据元素,最常用的方法是:从表头依次遍历表中节点,用被查找元素与各节点数据域中存储的数据元素进行比对,直至比对成功或遍历至链表最末端的 NULL(比对失败的标志)。
//p为原链表,elem表示被查找元素、
int selectElem(link * p,int elem){
//新建一个指针t,初始化为头指针 p
link * t=p;
int i=1;
//由于头节点的存在,因此while中的判断为t->next
while (t->next) {
t=t->next;
if (t->elem==elem) {
return i;
}
i++;
}
//程序执行至此处,表示查找失败
return -1;
}
注意,遍历有头节点的链表时,需避免头节点对测试数据的影响,因此在遍历链表时,建立使用上面代码中的遍历方法,直接越过头节点对链表进行有效遍历。
6.7、链表的更新元素
更新链表中的元素,只需通过遍历找到存储此元素的节点,对节点中的数据域做更改操作即可。
//更新函数,其中,add 表示更改结点在链表中的位置,newElem 为新的数据域的值
link *amendElem(link * p,int add,int newElem){
link * temp=p;
temp=temp->next;//在遍历之前,temp指向首元结点
//遍历到待更新结点
for (int i=1; i<add; i++) {
temp=temp->next;
}
temp->elem=newElem;
return p;
}
6.8、链表的添加元素
头插法:
link *(link * p){
link * temp=p;//temp初始化为头指针
int count;
printf("请输入要插入的元素数:\n");
scanf("%d",&count);
for(int i=0;i<count;i++){
link *a=(link*)malloc(sizeof(link));
a->elem=i;//假设添加的元素等于本轮的i
a->next=temp->next;
temp->next=a;
}
return p;
}
尾插法:
link *(link * p){
link * temp=p;//temp初始化为头指针
int count;
printf("请输入要插入的元素数:\n");
scanf("%d",&count);
while(temp->next!=NULL){
temp=temp->next;
}
for(int i=0;i<count;i++){
link *a=(link*)malloc(sizeof(link));
a->elem=i;//假设添加的元素等于本轮的i
temp->next=a;
a->next=NULL;
temp=a;
}
return p;
}