C语言期末复习指南

按照软件学院的惯例,C语言期末考试是不直接考概念填空题和选择题的。

所以我们今天要讲的基本是贴近实际题目的情况:

基础篇

1.C的输入输出

程序从main函数开始执行之类的概念相信已经不必多说了,所以我们先看到输入输出的一些考察点:

举例,通常情况下最多用到的输入函数是scanf:

#include <stdio.h>
int main(void)
{
int a;
scanf("%d",&a);
}

通常考察的也就是格式控制地址符&,保持前面的格式控制和要存储的变量类型一致就OK了,另外,除了指针以外,变量前都是要加地址符&的。这种情况在改错题可能会遇到,还有自己写代码的时候也不能犯错。

输出一般也就是用到printf:

#include <stdio.h>
int main(){
int a = 0;
int* b = 1;
printf("The variable is %d and %d", a, *b);
}

主要也是考察一个格式控制的问题,如果是指针记得加*

2.C的判断–>条件语句

C语言内置的条件语句有两个:

  • if
  • switch
if语句:

一般在读程序题里考察:

#include <stdio.h>

int main()
{
int x = 1, y = 2;
for (; x < 10; x++)
{
x += 2;
if (x > 7)
break;
if (x == 6)
continue;
y *= x;
}
printf("%d %d\n", x, y);
return 0;
}

这里的输出是9 6;

首先要明确if的工作方式:

if(A)
{
B;
}
else if (C)
{
D;
}
else
{
E;
}

程序执行到if的位置之后,

首先判断A的真假:

  • 如果为则执行B,然后结束if…else代码;

  • 如果为则顺序向下判断C的真假,重复上述步骤;

  • 在若干个else if之后如果没有找到真,就执行else里的E语句。

如果有多个if嵌套,用这样的逻辑去一层一层推导,就不会出问题。

switch语句:
#include <stdio.h>

int main ()
{
/* 局部变量定义 */
char grade = 'B';

switch(grade)
{
case 'A' :
printf("很棒!\n" );
break;
case 'B' :
case 'C' :
printf("做得好\n" );
break;
case 'D' :
printf("您通过了\n" );
break;
case 'F' :
printf("最好再试一下\n" );
break;
default :
printf("无效的成绩\n" );
}
printf("您的成绩是 %c\n", grade );

return 0;
}

switch更像是一个特殊化的if,用于简化只需要判断某一个变量的值的情况,需要注意的是,正常情况下switch的每一个case都需要以break结束,否则无法跳出,而default段可以有效的防止错误的变量造成的无法跳出switch

3.C的循环语句

C当中主要有三个基本的循环语句:

  • while循环
  • for循环
  • do...while循环
while循环:
while(condition)
{
statement(s);
}

condition是判断语句,和if里那个判断语句一样,为真(1/true)则继续,为假(0/false)则终止,大括号内的语句也就是每次循环要执行的内容。

举个例子:

#include <stdio.h>

int main ()
{
/* 局部变量定义 */
int a = 10;

/* while 循环执行 */
while( a < 20 )
{
printf("a 的值: %d\n", a);
a++;
}

return 0;
}

要注意的点是,while结束的时候的a值是多少?

尽管程序打印的a是到19就没有了,但是最后循环结束的a值应该是20,也就是刚好使得判断条件为假的值。

分析循环类的代码执行,最好画出每次循环的变量的情况,避免出错。

for循环:

for循环其实和while循环大差不差,也就是多了几个更直观的表示方式:

for ( init; condition; increment )
{
statement(s);
}

init是循环开始前(刚执行到for语句时)要执行的语句,比如赋初值之类;condition还是判断语句,和while循环里那个一样;increment就是每次循环固定要执行的语句,比如大家经常见到的i++,目的是增强可读性,作用和把这个语句直接放进大括号最后一行没有区别。

do…while循环:

do...while循环也是一个特殊版的while循环:

do
{
statement(s);

}while( condition );

它的特点是不管条件如何,第一次执行到这段代码的时候一定会执行一次statement,因为它是先执行,再判断。同样的,用之前讲的while的例子来看,最后a的值不会是20,而是19。(满足条件的最大值)

4.C的作用域规则和函数

作用域

C语言的作用域,主要是对三个概念的理解:

  • 局部变量
  • 全局变量
  • 形式参数
局部变量:

在某个函数或块的内部声明的变量称为局部变量

#include <stdio.h>

int main ()
{
/* 局部变量声明 */
int a, b;
int c;

/* 实际初始化 */
a = 10;
b = 20;
c = a + b;

printf ("value of a = %d, b = %d and c = %d\n", a, b, c);

return 0;
}

记住,看到int、char之类的变量声明语句时,就限定它的作用范围。在这里,所有的变量 abcmain()函数的局部变量,如果有其他的函数存在,是不能直接使用这里的a b c的。

全局变量:

如果觉得自己搞不清楚全局变量的,记住,放在函数外面的,也就是没有放在大括号里的就是全局变量,这个变量是大家都可以使用的。

*注意:如果在函数里有和全局变量同名的局部变量定义,在函数内以局部变量优先。

#include <stdio.h>

/* 全局变量声明 */
int g;

int main ()
{
/* 局部变量声明 */
int a, b;

/* 实际初始化 */
a = 10;
b = 20;
g = a + b;

printf ("value of a = %d, b = %d and g = %d\n", a, b, g);

return 0;
}
形式参数:

形式参数本身起一个传递的作用,它的名称只对使用它的函数有意义。形式参数控制传入的参数的类型和数目,它在函数内可以被认为是一个局部变量,它的值由传入的变量确定并与传入变量的名称无关。

#include <stdio.h>

/* 全局变量声明 */
int a = 20;

int main ()
{
/* 在主函数中的局部变量声明 */
int a = 10;
int b = 20;
int c = 0;
int sum(int, int);

printf ("value of a in main() = %d\n", a);
c = sum( a, b);
printf ("value of c in main() = %d\n", c);

return 0;
}

/* 添加两个整数的函数 */
int sum(int a, int b)
{
printf ("value of a in sum() = %d\n", a);
printf ("value of b in sum() = %d\n", b);

return a + b;
}
函数:

理解了前面关于作用域的意义之后,函数的重点也就是学会声明和调用函数。

*声明需要在调用之前。

声明调用举例:

/*返回的值类型 函数名(参数1,参数2……)
{
要执行的代码;
返回值;
}*/

int sum_ab(int a, int b);

int main()
{
int i = 10, j = 9;
//调用:
int new = sum_ab(i, j); //(这里i,j的类型要和声明的参数类型一致)
printf("%d", new);
}

int sum_ab(int a, int b)
{
sum = a + b;
return sum;
}

如果不需要返回值,void就好了。

5.数组

在C语言中,当我们需要用一个变量存储多个数据的时候,我们就需要用到数组

数组可以存储一个固定大小的相同类型元素的顺序集合。

数组的声明很容易:

//比如这里声明一个整形的数组
int main(void){
int a[10] = {0};
//二维数组
int b[10][10] = {0};
}

C语言中数组的声明必须确定数组长度,并且长度在声明之后不能再改变,数组的内容可以用大括号{}来表示,各元素用逗号,隔开。

数组容易考察的点:

数组按照下标索引来访问内容,起始的元素为下标为0

如果要将一维数组传递给函数,可以有三种形式参数:

  • (int *param) 指针
  • (int param[10]) 数组
  • (int param[]) 首地址

二维数组的传入则相对复杂一点,这里不多讲解。

进阶篇

1.指针

什么是指针?

在C语言中,每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问的地址,它表示了在内存中的一个地址。

指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,我们必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

type *var-name;

因为指针存储的是地址,所以要访问指针变量指向的内容要用*

其实平时我们经常见到指针:

scanf函数:

int main(void)
{
int a;
scanf("%d", &a);
}

这里的&a就相当于一个指针。

所以我们也可以这样写代码:

int main(void)
{
int *a;
scanf("%d", a);
}

效果是一样的,只不过要访问输入的值要加个*

定义指针的时候,良好的习惯是对其进行初始化,对于指针来说,初始化的值应为NULL或者一个变量的地址:

int main(void)
{
int *a = null;
int b = 10;
int *c = b;
//这样的初始化指针是不能被调用的,否则会出错:
int *d = 10;
}

那么,说了这么多,指针有啥用呢?

指针和数组:

首先:

任何能由数组下标来实现的操作都能用指针来完成

一个通过数组和下标实现的表达式可以等价地通过指针及其偏移量来实现:

int main(void)
{
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = NULL;
p = a; //a直接表示数组的首地址
//用下标访问数组
int i;
for (i = 0; i < 10; i++)
{
printf("%d", a[i]);
}
//用指针访问数组
while (p <= &a[9])
{
printf("%d", *p);
p++;
}
}

指针数组和数组指针:

指针数组是数组,数组的每个元素都是一个指针。

数组指针是指针,指向一个数组。

指针和函数:

首先,记住指针是指向一个内存地址的,是物理实体。

C语言的所有参数均是以“传值调用”的方式进行传递的,这意味着函数将获得参数值的一份拷贝,如果是普通变量,则是传递值,如果是指针变量,则是传递地址。

我们来看看这两段代码:

clude <stdio.h>

int changevalue(int a)
{
a = 100;
return 0;
}
int main(void)
{
int a = 10;
changevalue(a);
printf("%d", a);
}

我们直接定义整形变量a=10,然后调用changevalue函数修改a的值,再在main中打印出a的值。

打印的结果为:

10

a的值并没有发生改变。

再看看这一段代码:

#include <stdio.h>

int changevalue(int *a)
{
*a = 100;
}
int main(void)
{
int b = 10;
int *a;
a = &b;
changevalue(a);
printf("%d", *a);
}

我们选用指针来操作,输出的结果:

100

为什么会有这样的差别呢?

因为当changevalue函数被调用时,修改的变量是a这个指针变量,也就是对指定的内存空间进行了修改,这样a指向的值就发生了改变,尽管它们不在同一个作用域内。

这里我们就可以体会到指针的一个优点:

指针参数使得被调函数能够访问和修改主调函数中对象的值。

而这一点常常成为考点,在读程序题中可能要求给出指针变量最后的值,考查我们对指针在各种作用域中发生的变化。

另外,这里给出指针传递数组的例子:

  • 一维数组:

    数组的变量名可以直接表示数组的首地址,而数组在内存中顺序存储,所以我们可以以指针形式传入数组

    void print_out(int *a, int length)
    {
    int i = 0;
    for (i = 0; i < length; i++)
    {
    printf("%d", *(a + i));
    //通常这样访问也是可以的,结果完全一样
    printf("%d", a[i]);
    }
    }

    int main(void)
    {
    int a[5] = {0, 1, 2, 3, 4};
    int length = sizeof(a) / sizeof(a[0]);
    print_out(a, length);
    }

    缺点是必须传入数组长度,不能直接通过传入的指针确定数组的长度

  • 二维数组:

    二维数组用指针传递的条件稍显严格,需要指明列数(即第二维的上限)

    其他操作和一维数组类似

    void print_b(int (*a)[5], int n, int m)
    {
    int i, j;
    for (i = 0; i < n; i++)
    {
    for (j = 0; j < m; j++)
    printf("%d ", a[i][j]);
    printf("\n");
    }
    }

函数指针:指向的对象是函数的指针

例如:

void echo(char *msg)
{
printf("%s",msg);
}
int main(void)
{
void (*p)(char*) = echo;
p("Hello!");
//等价于
//echo("Hello!");
return 0;
}

这样可以简化函数的调用。我们也可以在函数参数中添加其他函数的函数指针,在不同的作用域内使用。

2.结构体

什么是结构体?

结构是 C 编程中另一种用户自定义的可用的数据类型,它允许我们存储不同类型的数据项。

比如我们想要描述一个物体的多个特征,比如一个叫Kevin的人:

  • 他的id
  • 他的年龄
  • 他的外号

简单使用结构体

我们在定义这样的一个结构体时,要遵循下面的规范:

struct tag { //这里是结构体变量类型,可以用于定义新的同类型结构体
member-list
member-list
member-list
...
} variable-list ;//这里是结构体名,访问这个结构体需要用到的是这个字段

实例:

struct Person{
int id;
int age;
char nickname[20];
}Kevin = {1, 19, "He Jiefang"};

这里的Person是大的类型,而KevinPerson这个类型里的一个变量

为了访问结构的成员,我们使用成员访问运算符.:

Kevin.id

Kevin.age

Kevin.nickname

使用举例:

#include <stdio.h>
#include <string.h>

struct Person
{
int id;
int age;
char nickname[20];
} Kevin = {1, 19, "He Jiefang"};

int main(void)
{
struct Person Drunk;

Drunk.id = 2;
Drunk.age = 19;
//字符串的赋值使用strcpy函数
strcpy(Drunk.nickname, "Cen Cen");
//访问成员并输出
printf("Kevin's nickname is: %s\n", Kevin.nickname);
printf("Drunk's nickname is: %s\n", Drunk.nickname);
}

当然,结构体也可以作为函数的参数传入,它支持常规的变量所支持的应用方式,这里就不展开细讲了。

结构体与指针(链表)

结构体一旦和指针联系起来,我们就不得不提到链表了。

链表是一种数据结构,它和数组的主要区别在于是否线性。

*数组是顺序存储在内存中的数据结构,特点是访问快,结构简单,我们可以直接访问它位于某一位置的数据。

链表中的每个元素我们称为节点(node)

它具有如下特点:

  • n个节点离散分配

  • 每一个节点之间通过指针相连

  • 每一个节点有一个前驱节点和一个后继节点

  • 首节点没有前驱节点,尾节点没有后继节点

是不是感觉有点困惑?没有关系,我们接着往下看。

//首先定义一个简单的结构体,作为一个节点。
struct link{
int data; //定义数据域
struct link *next; //定义指针域,存储直接后继的节点信息
};

我们可以看到,在这个节点中存在一个和这个节点的结构体类型相同的指针*next,它将指向下一个节点。

举个例子:

//首先定义一个简单的结构体,作为一个节点。
struct Node{
int data; //定义数据域
struct Node *next; //定义指针域,存储直接后继的节点信息
};

int main(void)
{
struct Node *node1;
struct Node *node2;
//对结构体指针我们可以之间用->符号来访问结构体成员
node1->next = node2;
//像这样,我们就把node1内存储下一个节点的指针变成了node2的指针,这两个节点就连接起来了
}

在正式创建链表之前,我们先认识一下这几个概念:

  • 首节点:存放第一个有效数据的节点
  • 头节点:在单链表的第一个结点之前附设一个结点,它没有直接前驱,称之为头结点,头结点的数据域可以不存储任何信息,指针域指向第一个节点(首节点)的地址。头结点的作用是使所有链表(包括空表)的头指针非空
  • 尾节点:存放最后一个有效数据的节点
  • 尾指针:指向尾节点的指针

首节点主要是为了和头节点区分,头节点通常无意义,相当于数组a[10]中的变量名a起到的作用。

#include <stdio.h>
#include <string.h>

struct Node{
int data; //定义数据域
struct Node *next; //定义指针域,存储直接后继的节点信息
};

//定义头尾节点为全局变量便于使用
struct Node* head =NULL;
struct Node* end = NULL;

//添加新节点的函数
void AddNewNode(int data)
{
//创建一个临时节点,并分配内存空间
struct Node* temp = (struct Node*)malloc(sizeof(stuct Node));


temo->data = a; //对节点数据赋值
temp->next = NULL;

if (head->next == NULL)
head->next = temp; //如果现在一个节点也没有,直接添加到头节点后面
else
end->next = temp; //如果现在已经有节点,添加到尾节点的后面
end = temp; //最后保证尾节点始终指向最后一个节点
}

其实单链表的创建逻辑很简单:

graph TB
创建一个节点并对节点数据赋值-->c[判断链表是否为空]
c-->为空-->赋值给头节点的下一个节点-->a[head->next = temp]
c-->不为空-->赋值给尾节点的下一个节点-->b[end->next = temp]
a-->保证尾节点始终指向最后一个节点
b-->保证尾节点始终指向最后一个节点