入职培训总结之C基础

一、Hello world:数据类型-程序结构

C语言的优点

  • 与硬件结合紧密,“原生”
  • 执行速度快,适合对性能要求高的场景
  • 编译出的可执行文件比较小

C缺点

  • 开发周期长
  • 长期运行的风险高,相比有垃圾回收机制的JAVA,更容易发生内存泄漏
  • 可移植性不高(指的是需要交叉编译,不能像python一样一次编写随处运行),

C应用场景

  • 服务器软件
  • 大部分嵌入式软件软件
  • 神经网络算法底层
  • 游戏
  • 嵌入式设备
  • ……

一个hello world

1
2
3
4
5
#include<stdio.h>

int main(){
printf("hello, world\n");//printf: print-format,格式化输出
}

C程序中三种头文件:

  • C库标准头文件,如<math.h>;
  • 第三方库头文件,如<opencv2/opencv.h>;
  • 自定义头文件,如"head.h"

头文件使用尖括号和双引号的区别,主要在于编译时头文件的查找路径,前者是从系统环境变量PATH中找,后者是先从当前路径找(GCC也可以指定路径)

C程序从代码到可执行文件的过程

  • coding,编写好源代码

  • compile

    • 预处理,宏定义展开、头文件展开、条件编译&去处代码中的注释

      gcc -E helloworld.c -o helloworld.i

    • 编译,生成汇编文件(中间文件)

      gcc -S helloworld.i -o helloworld.s

    • 汇编,根据汇编文件生成目标文件(obj)

      gcc -c helloworld.s -o helloworld.obj

  • link,将目标文件所依赖的库进行链接,生成最终的可执行文件

    gcc helloworld.c -o gelloworld

  • 程序调用,./helloworld

C的数据类型

  • 基本类型
    • 整形:short,int,long,long long(64字节)
    • 实型:float(4字节),double(8字节)
    • 字符类型:char
  • 构造类型
    • 数组
    • 结构体struct
    • 共用体union
    • 枚举enum
  • 指针
  • 空类型:void
  • 自定义类型:typedef,常见于struct

为什么C中不同数据类型很重要?因为C是一个强类型的语言,数据类型能决定:

  • 内存占用大小
  • 内存中的布局
  • 不同数据行为,这一点对程序员来说是最直观的。比如只有整数才能进行取模操作、只有指针类型能存储其他变量的地址等

下面是一个计算圆面积的程序

1
2
3
4
5
6
7
8
9
#include<stdio.h>
#include<stdlib.h>

int main(){
float r;//定义变量,必须先指定类型
const float PI = 3.14f;//常量前加const
scanf("%f", &r);
printf("%lf", PI*r*r);
}

image-20230224164111111

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<stdio.h>
#include<stdlib.h>

int main(){
{
//算术运算符注意事项:1.防止越界; 2.除数不能为0; 3.取模操作只能用于整数;
int a = INT_MAX;
int b=2;
//printf("%d\n", (a+b)/2);//错误,a+b会发生越界,最后输出一个负数
printf("%d\n", b+(a-b)/2);//正确

//4.a++是先取a的值,再a=a+1;++a:先a=a+1,再取值;
a=0;
int c = a++;//c=0
int d = ++a;//d=2
}
{//关系运算符
int a=9,b=3,c=5;
// if(a<b<c){//错误,不能连接比较,下面的结果是会输出a<b<c,原因:a<b为0,0<c为1,满足条件
// printf("a<b<c\n");
// }
if("a<=b&&b<=c"){
printf("a<=b<=c\n");
}
if(0 == a){//为了以防万一写的时候写成了=赋值,一般把常量写到关系运算符左边
printf("a=0\n");
}
}
{//逻辑运算符
//1.&&和||也叫短路求值
int a = 0, b = 1;
if(a > 1 && b++){
printf("b=%d\n",b);//b=1,因为a>1为假,被短路了
}
}
{//逻辑运算符
int score=70;
char* result=score>=60?"及格":"不及格";
printf("%s",result);

int a,b,c;
c = (a = 0, b = 10);
printf("c=%d\n",c);//c=10,因为逗号表达式从左到右计算,返回最右边的结果
}
{//赋值运算符及扩展
int a;
a = 10;
a = a + 10;
a += 10;//上面的方式会创建一个临时变量,执行两次赋值操作

printf("%d\n",sizeof a);
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(int));//三种形式结果一样,看个人习惯
}
{//强制类型转换,注意事项:类型转换指的是赋值的类型转换,用于赋值的变量本身并没有发生类型转换
float f = 10.55f;
int i = (int)(f);//i=10
int j = (int)(f+0.5);//i=11
printf("i=%d\n",i);
printf("j=%d",j);
}
}

三种基本的程序结构

  • 顺序结构

  • 选择(分支)结构,if/else,switch/case,

  • 循环结构,分为当型循环for,while,和直到型循环(至少执行一次)do/while

这里注意for循环的基本结构:

1
2
3
for(init;condition;increment){
todo;
}

这里是先执行todo,再执行increment,while同理。

循环结构中的跳转:break与continue,其中

  • continue用在循环结构中,可结束本次循环,跳到循环开始处重新判断是否继续;
  • break可用在switch/case语句中,可跳出一层的switch结构,也可用在循环结构中,可跳出一层循环结构

break的妙用:剔除字符串末尾的空格、制表符

1
2
3
4
5
6
7
8
9
/* trim: remove trailing blanks,tabs, newlines */
int trim ( char s[]){
int n;
for (n = strlen (s) - 1; n >= 0; n--)
if (s[n] != ' ' & & s[n] != ' \t' & & s[n] != '\n ' )
break;
s[n+1]='\0 ';
return n;
}

continue的妙用:减少程序if/else嵌套层数,如果没有的话可能还需要将测试颠倒过来或缩进到另一层

1
2
3
4
for (i =0; i <n; i++)
if (a[i] < 0)/* skip negative elements */
continue;
... /* do positive elements *

循环结构示例:斐波那契数列

1
2
3
4
5
6
7
8
9
int fib(int n){
int a=1,b=1;
for(int i=3;i<=n;i++){
int tmp=b;
b=b+a;
a=temp;
}
return b;
}

二、数组

为什么需要数组?因为需要一种数据结构,来统一地管理、统计相同类型的数据

数组:相同类型有序数据的集合,是一种构造类型

一维数组

数组初始化:完全初始化(所有元素全部赋值),部分初始化(默认从左到右,如果只给了一个值则全赋一个值),默认初始化(不显式指定数组长度,根据后面大括号内元素个数确定数组大小)

1
2
3
int a[5]={1,2,3,4,5};
int b[5]={0};
int c[]={1,2,3};

数组的长度必须是一个常量表达式

虽然数组定义时和访问时都使用中括号[],这两种情况下的性质其实是不一样的:前者是定义符,后者是运算符

不同于Python等语言数组越界时会抛出异常,C中如果越界访问的话是会直接使用内存空间对应位置的数据。

小练习:开关灯

假设有N盏灯(N为不大于5000的正整数),从1到N按顺序依次编号,初始时全部处于开启状态;

有M个人(M为不大于N的正整数)也从1到M依次编号。
第一个人(1号)将灯全部关闭,第二个人将编号为2的倍数的灯打开,第三个人将编号为3的倍数的灯做相反处理(开着的关闭,关闭的打开)。依编号递增顺序,以后的人都和3号一样,将凡是自己编号倍数的灯做相反处理。

请问:当第M个人操作之后,哪几盏灯是关闭的,按从小到大输出其编号。

样例输入10 10
样例输出1 4 9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#define MAX 5000
int lights [MAX + 1];
int main(){
int N= 10;
int M= 10;
for (int n = l; n<= N; n++)//初始化全部开灯
lights [n] = l;
for (int m = l; m<=M; m++)//m个人
for(int n = l; n <= N; n++)//n盏灯
if(n % m == 0 )
lights[n] = (lights[n]== 0 ? 1 : o);

for (int n = l; n<= N; n++)
if (0==lights[n])//常量的比较放在前面
printf("%d",n);
return 0;
}

二/多维数组

认识二维数组之前,先明确一个问题:一维数组的类型是什么?

简单来说,对一维数组而言,数组的声明方式就是数组的类型

比如int a[5]={0}那么int[5]就是这个一维数组的类型

扩展到二维数组,这样就好理解了:数组是相同类型数据的有序集合,二维数组就是若干相同类型的一维数组嵌套

  • 定义方式与一维数组相似,类型名字加长度
  • 初始化方式上,就是循环初始化行数个一维数组
  • 访问方式上,如果访问最小单元,则先行坐标再列坐标,或者以行坐标为索引,获取的是一维数组

二维数组的应用场景:

  • 字符串数组
    C中没有string类型,一般用char[]替代,这里注意sizeof的返回值是长度加一。一个字符串用数组表示,字符串的数组就是二维数组了。
    小应用,用二维数组排序字符串数组

    image-20230225115748768
  • 图像处理

    比如模糊化操作,原理:将二维平面上像素块赋值为周围九宫格值的平均,就达到了模糊处理的效果

三、函数与指针

函数

函数可以将大的计算任务分解为若干较小任务,便于程序设计人员基于函数进一步构造程序,而不需要重新编写一写代码。一个设计得当的函数可以把程序中不需要了解的具体操作细节进行影藏,从而使得整个程序结构更加清晰,并降低程序的难度。

函数定义的基本格式:

1
2
3
4
返回值类型 函数名(参数声明表){
声明和语句
返回语句
}

关于参数的传递过程,有形参和实参两种,前者在定义时出现,后者在调用赋值的时候用到。

对应三种不同的头文件,函数也分三种类型:

  • 标准库函数,比如math.h提供的round,string.h提供的strcpy

  • 第三方库函数,比如sqlite的strftime函数,opencv的imread、imshow函数

  • 自定义函数,与具体开发应用和场景紧密相关,有时仅仅是第三方库函数的精简、封装。

函数递归

一个函数在函数体内调用自身就叫做递归调用。递归必须要有终止条件,否则会无尽头调用下去,最终导致栈溢出。一个最简单的递归例子就是Fibonacci数列的递归解法

1
2
3
4
int Fib(int n){
if(n==1 || n==2) return 1;
return Fib(n-1)+Fib(n-2);
}

小练习:分数加法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<stdio.h>
#include<stdlib.h>

int gcd(int a,int b){
if(0==b) return a;
return gcd(b,a%b);
}


void add(int a,int b,int c,int d){
if( 0 == b*d ){
printf("param error!\n");
return;
}

int a2=a*d;
int b2=b*d;
int c2=c*b;
int d2=b*d;

int g=gcd(a2+c2,b2);
int e=(a2+c2)/g;
int f=b*d/g;

printf("%d/%d + %d/%d = %d/%d\n",a,b,c,d,e,f);
}

void main(void){
add(1,3,1,4);
}

分数加法

指针

所谓指针,关键在于一个“指”字,即其存放的不是变量的值,而是指向变量的地址。

指针的使用,通常先定义,再绑定,使用时通过在指针名前添加*来获取指向变量的值(关于指向变量的具体位置,我们反而不那么在意了。)

C是强类型语言,变量定义后其类型不得改变,那么指针也是一样,一开始定义的是什么类型,后面也要用什么类型去访问

1
2
3
4
5
6
7
8
int main(){
float f=10.5f;
float *pf=&f;
*pf=10;
printf("%d\n",f);//0
printf("%f\n",f);//10.000000
return 0;
}

const与指针(*)的结合使用

  • const+*,表示指向常量的指针,只能读值而不能用于改值

    1
    2
    3
    int a = 0;
    const int *p = &a;//等价于 int const *p=&a;
    *p = 10;//错误
  • *+const,表示指针常量,指针指向的对象地址不能改变

    1
    2
    3
    4
    int a = 0;
    int b = 0;
    const int *p2 = &a;
    p2 = &b;//错误
  • const + * + const表示指向常量的指针常量,是前两者的结合

    1
    2
    3
    4
    int a=0,b=0;
    const int * const p3 = &a;
    p3 = &b;//错误
    p3 = 10;//错误

指针作为函数的参数传递,一般出现在“引用传递”的场景中(与之相对的是值传递)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<Stdio.h>
#include<sdlib.h>
#include<string.h>

void swap1(int,int);
void swap2(int*, int*);
//函数定义,出现的是函数形参
void swap1(int a, int b){
int tmp;
tmp = b;
b = a;
a = tmp;
}
void swap2(int* a, int* b){
int tmp;
tmp = *b;
*b = *a;
*a = tmp;
}

int main(){
int a=5,b=10;
swap1(a,b);//函数调用,函数实参,参数传递。实参给形参赋值
printf("a=%d, b=%d\n",a,b);
swap2(&a,&b);
printf("a=%d, b=%d",a,b);
}

上面的例子中,swap1不希望子函数修改原变量的值,而swap2恰好相反,两者的参数传递策略不同。

指针与数组

  • 对于一维数组而言,数组名就是一个指针,可以用它来访问数组元素的值,通过自增资减进行下标的移动;

  • 对于二维数组,每一行就是一个一维数组。因此数组名[行坐标]可作为一个指针。

    1
    2
    3
    4
    int a[5];
    int *p=a;
    int b[3][4];
    int (*p)[4]=b;
  • 指向指针的指针,也叫二级指针

    1
    2
    3
    4
    5
    int c=0;
    int *pc=&c;
    int *ppc=&pc;
    *(*ppc)=5;
    printf("%d",c);//5

函数指针的应用

  • 指针减法:不论是一维数组还是二维数组,指针之间都可以进行减法获取两指针之间元素的个数。指针的加法没有意义。指针自增自减以及指针减法都应注意数组的边界问题。

  • void指针:

    • 定义时不指定指针类型,
    • 不用来指向某个变量的地址,只是存储一个地址。不能通过加*的方式取出变量的值。
    • 在读值的时候必须通过强制类型转换才能取出值
    1
    2
    3
    int a=5;
    void *p =&a;
    printf("%d",*(int *)p);//5
  • 函数指针

    函数在内存中也有一段地址存放,因此我们同样可以用指针去指向它。下面是一个简单的实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int add(int a, int b){
    return a + b;
    }
    int sub(int a, int b){
    return a - b;
    }
    int main(){
    int (*p)(int,int) = a;
    p(1,2);//3
    p=b;
    (*p)(1,2);//-1
    }

    在此例中,p作为指针先后指向了add、sub两个函数,既可以通过指针名直接调用函数。也可以通过*取出实际的函数进行调用。那么为什么需要用函数指针呢?更实际的场景见下。

    • 表驱动编程

      假设有这样一个需求:用户输入两个数字,一个运算符,程序根据运算符进行计算,返回计算结果。以四则运算为例,我们除了要为加减乘除都实现一个不同的函数外,还要使用if/else或者switch/case进行判断,代码结构会变得很冗余

      但转换一个思路,我们将函数与指针进行绑定,再将这些指针有序存放到数组中,那么我们在主程序中就可以仅仅根据此数组不同下标来实现对不同运算函数的调用。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      #include<stdio.h>
      #include<stdlib.h>

      int add(int num1, int num2){
      return num1+num2;
      }
      int sub(int num1, int num2){
      return num1-num2;
      }
      int mult(int num1, int num2){
      return num1*num2;
      }
      int divide(int num1, int num2){
      return num2==0?0:num1/num2;
      }
      int main(){
      int num1, num2;
      char op;
      int res;
      //加减乘除运算符在ascii码中的关系:* + . - . /
      int (*pf[6])(int,int) ={mult,add,NULL,sub,NULL,divide};

      (void)scanf(" %d %c %d",&num1,&op,&num2);

      res=pf[op-'*'](num1,num2);
      printf("%d %c %d = %d .\n",num1,op,num2,res);
      return 0;
      }
    • 作为函数参数传递。

      另外一个场景:商品评分并排序,需求是这样的:要能够对商品进行多维度(销量、价格、评分)的评分,在排序呈现时支持切换排序指标。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <memory.h>

      typedef struct{
      float value;
      float star;
      int sales;
      }Goods;

      int byvalue(const void* item1, const void* item2){
      float diff = ((const Goods*)item1)->value - ((const Goods*)item2)->value;
      if(diff > 0.0f)
      return 1;
      else
      return -1;
      return 0;
      }
      int bystar(const void* item1, const void* item2){
      float diff = ((const Goods*)item1)->star - ((const Goods*)item2)->star;
      if(diff > 0.0f)
      return 1;
      else
      return -1;
      return 0;
      }
      int bysales(const void* item1, const void* item2){
      return ((const Goods*)item1)->sales - return ((const Goods*)item2)->sales
      }

      int main(){
      Goods goods[3]={{150.0f,4.5f,50},{80.0f,4.0f,200},{120.0f,4.7f,100}};
      qsort(goods,3,sizeof(Goods),bystar);//byvalue,bysales……

      for (int i=0;i<3;i++){
      printf("[%d] value: %f, star: %f, sales: %d\n",
      i+1, goods[i].value, goods[i].star, goods[i].sales,)
      }
      return 0;
      }

      在上面这个例子中,关于商品的排序使用到的是qsort函数,但qsort是标准库提供的方法,我们并不需要去针对不同(int、float)类型的数据自己实现不同的排序方法,而是只需要定义排序算子(即是正序还是逆序)作为回调即可。

四、内存

内存,堆,堆栈

编写完的程序,在执行的时候会加载到内存中,加载的规则是怎样的呢?
C程序中,内存会被分为几个区块,如下图所示

C程序内存分配

一般,函数内通过类似int a=0;定义的局部变量,如果是字符串常量,全局变量,会存放到全局数据区,临时变量会提前分配到栈区,对于需要根据运行情况临时申请空间的变量,这种策略叫做动态分配,会存放到堆区。

malloc/calloc

两者都是用于动态申请内存空间,区别有二:

  • malloc调用时一个参数,指定申请空间大小即可,而calloc是将申请空间分为固定大小的若干块;
  • malloc不对申请的空间作初始化,calloc默认初始化为0。
1
2
3
void *malloc(size_t size);
void *calloc(size_t count, size_t size);
void free(void *memblock);

动态空间申请后一定要判断返回值是否为空,使用完必须要手动free。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
int main(){
char *str;
str=malloc(_MAX_PATH);
//string=(char*)malloc(_MAX_PATH);c++中必须显示转换返回值类型
if(str==NULL){
printf("Insufficient Memory!\n");
return -1;
}

……
free(str);

long *buffer;
buffer=(long*)calloc(40,sizeof(long));
if(buffer==NULL){
printf("Insufficient Memory!\n");
return -1;
}

……
free(str);
return 0;
}

调用堆栈,call stack

以visual studio为例子,讲解了断点调试,根据调用堆栈来排查问题的方法。

VisualStudio断点调试

栈的特点:先进后出,先加载,调用的函数在栈下方,后加载,被调用的函数在栈上方。main函数,在栈最底部。

下面有三个getString函数,直观展示了数据区,堆区与栈区的特点和区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>

char* getString1(){
char str[]="string";
return str;
}
char* getString2(){
char *str="string";
return str;
}
char* getString3(){
char *str=malloc(128);
strcpy(str,"string");
return str;
}
int main(){
char* str=getString1();
printf("getString: %s\n",str);//会输出乱码
}

栈区内存会自动释放,全局数据区和堆不会

小练习:锯齿数组

锯齿数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>

int main(){
int row;
int **arr;
printf("请输入行数:\n");
scanf("%d",&row);
arr=(int**)malloc(sizeof(int*)*row);
int *rows=(int*)malloc(sizeof(int)*row);//假定每行最多有行数列
for(int r=0;r<row;r++){
printf("请输入第%d行元素个数: \n",r);
scanf("%d",rows+r);//rows[r],即第r行元素个数
arr[r]=(int*)calloc(rows[r],sizeof(int));
}
for(int r=0;r<row;r++){
for(int c=0;c<row){
printf("%d ",arr[r][c]);
}
printf("\n");
}
for(int r=0;r<row;r++){
free(arr[r]);
}
free(arr);
free(rows);
}

注意先释放每一行的”列数组”,再释放指向第一列的”行数组”,否则会失去对“列数组”的控制

五、结构体

以学生成绩为例,不同学生,不同科目,考试成绩有很多,而且每条成绩有姓名、科目、分数等,杂乱无章就像没有拼起来的拼图。

使用结构体对一条成绩记录进行定义,不同的成绩间就可以进行整理排序了

1
2
3
4
5
6
struct structname
{
type member1;
type member2;
……
};
  • 结构体是构造类型,具体类型:struct关键字+结构体名,比如上面说就是struct structname

  • 结构体初始化,注意按定义顺序

  • 结构体变量访问方式:.

  • 指针访问结构体,.->

  • 结构体typedef妙用

  • 结构体嵌套,赋值时可直接赋类似于二维数组,访问时需要先指向内结构体名,再指到内层内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct Date{
int Year;
int Month;
int Day;
}Date;

typedef struct Student{
char *name;
char *gender;
int age;
int studentid;
Date birthday;
}Student;

Student stu={"张琴",女,18,2018033065,{1999,2,8}};
strcpy(stu.name,"张芳芳");
stu.birthday.Day=20;

结构体的比较,不能使用==,应当自己实现比较函数,只有每个属性值都相等才返回true。比较函数使用引用传递,防止拷贝整个结构体。

1
2
3
4
5
6
7
8
9
10
11
int equalDate(const date *d1, const date *d2){
return d1->Year==d2->Year && d1->Month==d2->Month && d1->Day==d2->Day
}
int main(){
Date d1={2020,8,15};
Date d2={2020,8,15};
if(equalDate(d1,d2)){
printf("相同的日期!");
}
return 0;
}

为什么结构体不能用==比较?看下面的数据对齐规则。

数据对齐规则

也就是说,结构体在内存中为了“叠放整齐”,在存储的时候会填成一个“方块”,对于那些长度不够的成员,其间会填充一些字节。这些字节对于我们的程序而言是透明的,其内容并不可知。而当我们使用==,会在内存中去逐位(bit)比较是否相等,因此比较失败也就是必然的了。

六、文件

  • 文件即持久化存储在磁盘上,可移动,可读取的数据,分为文本文件和二进制文件两种形式。文本文件“所见即所得”,可以用任何文本编辑器打开,写入什么字符,关闭打开还是什么字符。
  • 在Windows与Unix环境下,对换行符的理解是不同的
    • Windows下,\n会被展开成<CR><LF>两个控制字符,相当于\r\n
    • Unix环境下则仅仅是<LF>,不会展开。
  • 二进制文件不管在什么平台下,\n都是精确的<LF>,但一般需要特定的软件或工具才能读出其中的数据并以人类可读的形式展示出来

CR: carriage return

LF: line feed

文件操作相关函数一览(不要求记忆,只需记得有这些功能的函数,使用时会查找即可)

函数名 原型 参数说明 使用举例/注意事项
fopen FILE *fopen(const char *filename. const char *mode); 文件名、处理模式
文件名路径可绝对可相对
处理模式分为:文件处理模式
其中默认为文本模式打开
返回值为指明流的指针或NULL,使用后应当立即判断返回值是否为NULL。
fclose int fclose(FILE *stream); 文件指针 关闭成功返回0
fread size_t fread(void *buffer,size_t size,size_tcount,FILE *stream); 读取内容的缓冲区,每次读取数据块的大小,读取的次数,要读取的数据流(文件指针) size的单位是byte
返回读取成功的块数
假如一个文件大于128字节,则
1=fread(buf,128,1,fp)
128=fread(buf,1,128,fp)
文本文件和二进制文件均可读
fwrite size_t fread(void *buffer,size_t size,size_tcount,FILE *stream); 同上 同上
rewind void rewind(File *stream); 将读写光标(位置指示器)移到文件开头 文件指针不能使用“加减移动”,必须控制偏移量进行光标移动
fseek int fseek(FILE *stream,long offset,int origin); 文件指针,
偏移量(复数靠近文件头,正数靠近文件尾)
偏移起始位置偏移起始位
ftell long ftell(FILE *stream) 文件指针 返回文件位置指示器(读取光标)的当前值
fprintf int fprintf(FILE *stream,const char *format [, argument]…); 文件指针,输出格式[,输出变量]
fscanf int fscanf(FILE *stream,const char *format [, argument]…); 文件指针,读取格式[,输入变量] 与fread的区别:1.主要是可以格式化读取,并且不是以块为单位进行读;2.只能处理文本文件

小练习1:学生数据

从链表读取学生数据,输出至文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct student{
char name [16];
int id;
}student;

int write2tofile(const student* stu,const char* file){
FILE* fp = fopen(file,"w+");
if(NULL == fp)
return 0;
fprintf(fp, "%s %d", stu->name,stu->id);
fclose(fp);
return 1;
}

int readfromfile (Student* stu,const char* file){
FILE* fp = fopen (file, "r");
if(NULL == fp)
return 0;
if (2 != fscanf(fp,"%s %d", stu->name,&stu->id))
fclose(fp);
return 0;

fclose(fp);
return 1;
}

int main(){
student s = {"Zhang-San", 100 };
writetofile(&s, "info.txt");

student s2;
readfromfile(&s2, "info.txt");
printf("%s %d", s2.name,s2.id);
return 0;
}

小练习2:解析MP3信息

MP3信息 解析MP3信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<string.h>
#include<stdio.h>
void main (){
int i; //用于循环计数
struct MUSIC_TITAL_INFO{
char ID3v1TAG[3]; //TAG
char MUSIC_NAME[30] ; //存储标题信息char Artist[ 30];//存储歌手信息char Album [ 30];//存储专辑信息
char YEAR[4];//存储年代信息
char Comments [28];//存储备注信息,28个字节(有时为30字节)char Genre;1/存储音乐风格信息,保留位,1个字节
char Reserved;//保留位,1个字节(有时没有意思
char Track;//音轨(曲号)保留位,1个字节(有时没有)
}st1;

FILE* fp;
fp = fopen ( "music.mp3", "rb" );
fseek (fp,-128L,SEEK_END);//定位到距末尾128字节处
fread (&st1,128,1,fp); //结构体中的变量全是char
//fprintf("%s->%s->%s->%s->%s->%s->%c->%c->%c. \n",\
// st1.ID3v1TAG,st1.MUSIC_NAME,st1.Artist,st1.Album, st1.YEAR,st1.Comments, l/ /
//st1.Genre,st1.Reserved,st1.Track) ;
//打印TAG
printf ( "TAG: " );
for(i = 0; i <3;i++){
printf("%c",st1.ID3V1TAG[i]);
}
……
}

七、位运算与宏

位运算定义:对变量进行展开,以位为单位进行运算

常见的位运算:

  • 移位

    • <<左移,低位补0,高位丢弃,相当于数字乘以2
    • >>向右移,低位丢弃,无符号数高位补0,有符号数,高位补0或补1取决于计算机系统,相当于数字除以2
  • 按位取反~

  • 按位相与&

  • 按位相或|

  • 按位异或^

注意,位运算只能操作整数

位运算常见应用

  • 位开关。一个int变量有32位,将每一位都看成是一个开关,则一个整数就可以控制32个开关。

  • 不引入临时变量交换两个数

    1
    2
    3
    a=a^b;
    b=a^b;
    a=a^b;
  • 判断奇偶

    若一个整数为奇数,则最后一位必为1,反之则必为0;使之与0x01按位相与,则得到最后一位1或0,由此判断奇偶性if(a&0x01){return true}

  • 判断一个无符号整数是否为2的整幂数

    2的整幂数有这样的特点:所有位中只有一位为1,其余位全为0,将其减一,则该1位的低位全变成1,本位变0(高位也为0),使二者相与,则结果必为全0if(!(a&(a-1))){return true;}

  • 找单独不相同的数字

预处理:文件包含,宏,条件编译

前面学习过C程序编译构建的过程,其中就有预处理这个环节。这个环节主要的工作是:查找包含的头文件,处理宏和条件编译。这里重点关注宏。

宏就是一种定义,给一个数据起一个名字。比如#define MAX_LEN 100

在预编译时,会对宏执行替换操作:将源程序中使用宏名的地方替换为宏体

宏可分为带参和不带参两种,

  • 不带参的宏会原封不动地替换,定义的是什么就替换成什么

    1
    2
    3
    4
    5
    6
    7
    8
    #include<stio.h>
    #define NUM (M+3)*M/2
    #define N 2
    #define M N+1

    void main(){
    printf("%d\n",NUM);
    }
  • 带参的宏,一般形式:#define 宏名(参数表)宏体,这种形式就好像一个带参数的函数体。

    1
    2
    #define S(a,b) a*b
    area = S(3,2);

    宏展开后,变为:area=3*2

那使用宏定义的好处在哪,我们为什么要用宏而不是直接用参数或函数呢?

  • 能够提高代码的可读性,数字本身并没有什么实际的意义。通过宏定义起个名,就能知道它代表的是什么。比如#define PI 3.14159
  • 能减少书写错误。通过在main函数之前使用#define定义一个常量PI表示3.14159那么编译器是不会出错的,使用宏定义后,只要一处校验对了,其他相同的错误也就解决了。
  • 提高程序维护性:有时需要将某个特定数据(比如数据的大小)在程序中出现的所有实例统统加以修改,如果使用宏定义的话只需要修改一处即可,反之则需要全文查找修改。
  • 提高运行速度:函数调用会带来较大的系统开销,有时我们希望有一个程序块,看上去像一个函数,但却没有函数调用的开销。

使用宏的两点注意事项:

  • 宏中出现的所有参数、宏值都要带上括号

    1
    2
    3
    4
    5
    6
    7
    8
    //#define PF(x) x*x
    //#define PF(x) (x)*(x)
    #define PF(x) ((x)*(x))

    void main(){
    int a=2,b=3,c;
    c=PF(a+b)/PF(a+1);//预期:((a+b)*(a+b))/((a+1)*(a+1))
    }
    • 第一种宏定义:a+b*a+b/a+1*a+1
    • 第二种宏定义:(a+b)*(a+b)/(a+1)*(a+1)
    • 第三种宏定义:((a+b)*(a+b))/((a+1)*(a+1))。可以看到,只有这种是符合预期的
  • 不要使用表达式调用宏

1
2
3
4
5
6
7
#define max(a,b) ((a)>(b)?(a):(b))
……
int i=5;
m=max(i++,10);//n=6,符合预期

int i=5;
n=max(i++,1);//n=7,与预期不一致

八、常用C标准库函数

所谓标准库,是将各个C系统提供的库标准化之后的一组标准头文件,这些头文件中仅仅列出了库函数的原型及相关类型、符号常量、全局变量等,库文件的实现是二进制代码,链接时才将库文件中所用到的功能改装配到可执行文件中。编程中要尽可能使用经过长期考验的标准库。常用到的库函数包括:assert.h、ctype.h、conio.h、float.h、math.h、stdlib.h、stdio.h、string.h、time.h……

下面就来看一下string.h中为我们操作字符串提供了哪些接口。

  • strcpy/strncpy,两者区别在于后者需要指定从原字符串中拷贝的位数,注意原字符串需要有足够长的buffer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include<string.h>
    #include<stdio.h>
    #include<stdlib.h>
    int main(void){
    char src[]= "hi";
    char dest[6] = "abcdef" ;//无空字符
    strncpy(dest, src, 5);//写入五个字符'h', 'i', '\0','\0','\0'到dest

    printf("strncpy(dest,src, 5) to a 6-byte dest gives : ");
    for (size_t n= 0; n< sizeof(dest);++n){
    char c = dest[n];
    c ? printf("'%c'",c) : printf("'\\0'");//转义字符
    //输出:'h','i','\0','\0','\0'
    }

    printf("\nstrncpy(dest2, src, 2) to a 2-byte dst gives : ");
    char dest2[2];
    strncpy(dest2, src,2);//截断:写入二个字符'h', 'i',到dest2
    for (size_t n= 0; n < sizeof(dest2);++n) {
    char c = dest2[n];
    c ? printf("'%c'",c) : printf("'\\0'");
    //输出:'h','i'
    }
    printf("In");
    }
  • strcat/strncat,将两个字符串进行链接,注意目的字符串需要有足够的空间

    1
    2
    3
    4
    5
    6
    7
    int main(){
    char str[50]="Hello ";
    char str2[50]="World!";
    strcar(str,str2);
    strncat(str," Goodbye, world!");
    puts(str);//"Hello World! Go"
    }
  • strcmp/strncmp,比较原则:以ASCII码的大小(即字典序)为依据,按位比较字符串的每一位(如果指定了长度为n则比较前面n位),不能用指针直接比较大小!

    返回0:两个字符串(前n位)相同

    返回值小于0:第一个字符串更“小”

    返回值大于0:第一个字符串更“大”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <string.h>
    #include <stdio.h>
    void demo(const char* lhs, const char* rhs){
    int rc = strcmp (ihs, rhs) ;
    const char* rel = rc< 0 ? "precedes" : rc > 0 ? "follows" : "equals";
    printf ("[ %s] %s [ %s] \n", lhs, rel, rhs) ;
    }

    int main (void){
    const char* string = "Hello World!";
    demo(string, "Hello!");
    demo(string, "Hello");
    demo(string, "Hello there") ;
    demo("Hello,everybody ! " + 12"Hello,somebody ! " + 11);
    }
  • strlen,该函数返回字符串第一个\0之前的字符数,注意sizeof(str)返回的是该变量所占的字节数,不能用来计算长度

  • strchr/strrchr,查找字符串中某个字符第一次/最后一次出现的位置(r:reverse)。

    atrchr
  • strstr,在主串(匹配串)中查找子串(模式串)

    strstr
  • strtok,分割字符串,该方法会改变原字符串(实际原理是替换为\0),因此不能传入指向字符串常量的指针

    strtok
  • ……

九、线性表

所谓线性表,指的是数据各元素之间不是随机存储,而是存在一定顺序,可以按照一定方式连接起来。

  • 一种方式是所有元素在内存空间中连续,叫做顺序存储

    这种方式对应的数据结构叫数组

  • 另一种方式不需要物理上连续,但前面的元素能够找到后面的元素,叫做是链式存储

    对应的数据结构叫做链表

一条链表,最基本的元素:节点,可以分为指针域和数据域,指针域用于存储指向下一节点的指针;数据域用于存放自身的数据(可能是基本类型,也可能是构造类型)

注意链表两个概念:头指针与头结点的区别。前者是链表的必要元素,指向链表的头,但它是“虚拟”的;后者是可选项,如果存在则对应一个实体,即:

  • 链表的第一个节点,如果不存放数据,专门用于链表的指向和管理,这就叫做头节点
  • 如果第一个节点也存放了数据,就称这条链表没有头结点
  • 一般来说,有头结点的链表管理起来更加方便

关于链表的初始化过程:

  • 首先要创建一个头指针,指向链表的第一个结点(如果赋值就是头结点,这里统称为head),head指向NULL
  • 然后要将所有数据,构建成为链表节点,插入链表,具体又分为两种方式:
    • 新节点总是插入在head与head的下一个节点之间,即新节点总是靠近head一侧,称为头插法
    • 不记录head节点,而是不断跟踪最后一个节点tail,新节点总是tail的下一个节点,称为尾插法

关于链表的释放(销毁):

  • while循环,每次先用一个临时变量temp记录头指针head
  • head指针移到下一节点
  • 释放掉temp
  • 当head指向NULL时,退出循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include<stdio.h>
typedef struct Link{
int elem;
struct Link* next;
}link;

link* initLink(){
linl* head = (link*)malloc(sizeof(link));//创建一个头结点
if(!head)
return NULL;
head->next = NULL;
for(int i=10;i>=1;i--){
link* node = (link*)malloc(sizeof(link));
if(!node){//创建失败,清空所有节点,退出
void freeLink(link* head);
freeLink(head);
return NULL;
}
node->elem = i;
node->next = head->next;
head->next = node;
}
return head;
}
void freeLink(link* head){
while (head){
link* node = head;
head = head->next;
free(node);
}
}
void display(const link* head){
const link* node = head;
while (node = node->next){
printf("%d->", node->elem)
}
printf("NULL\n");
}
int removenode(link* head, int elem){
link* cur = haead->next;
link* pre = head;
while (cur && cur->elem != elem){
pre = cur;
cur = cur->next;
}

if (cur){
pre->next = cur->next;
free(cur);
return 1;
}

return 0;
}
int main(){
link* head = initLink();
display(head);
removenode(head, 5);
display(head);
freeLink(head);
head = NULL;
return 0;
}

十、C综合练习

题目描述

现有一个二进制文件GBTL.dat,其中存放了中国导航一些道路的数据(道路编号、道路名、岔路等),具体见下:

image-20230301121903690

现要求将数据文件中的信息读出,并提供如下功能:

  1. 排序:读取GTBL.dat, 根据LinkID重新排序输出到新的二进制文件,格式同”逆引表格式”。

  2. 检索
    a. 根据LinkID查找指定的Link的相关情报并输出到控制台或者文件(文本格式)。
    b. 查找指定 交叉Link列表示Class番号 的所有Link的集合。
    c. 查找岔路数> n 的所有Link的集合, n由用户输入。
    d. 指定道路名称检索。
    输出格式:

    1
    #linked=1234;roadnameflag=1;brunch=2;dispclass=3; roadname=青年大街# 

    (如果没有名称 (roadnameflag == 0 ), 则不输出roadname=青年大街这个条目) 如果查到的纪录的个数>5个,则输出到指定文件中(文件放在当前目录中,请用 searchresultxxx.txt 命名, xxx是检索次数的记录,比如第一次检索,则xxx是 001, 以此类推。)

  3. 更新:从Link情报输入文件中读取指定的Link情报,插入到GTBL.dat中并保存,如果对应的LinkID已经存在,则替换,否则插入。(文件的格式参照ReveseTableFormat.xls 中的”Link情报输入文件格式”sheet, sourcelink.txt 是一个例子文件,大家可以自己编写这个文件)。 具体的操作是每按一次回车,就从文件中读取下一个Link的情报,输出到界面,并且执行插入或者替换操作。这些插入的记录需要保存到GTBL.dat和排序后的文件中。

逆引表格式

示例:#linkid=1234;roadnameflag=1;brunch=2;dispclass=3;roadname=青年大街#

以这样格式存储的多条纪录的排列,用回车符分开

字段名 解释
linkid 这条道路的标号
roadnameflag 这条道路有无道路名称,1=有,0=无
bruch 这条道路的岔路数
dispclass 这条道路的分类番号
roadtname 这条道路的名称

程序实现

  1. 程序基础框架

  2. 读取部分示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    #define _CRT_SECURE_NO_WARNINGS

    #include <stdio.h>
    #include <assert.h>

    #include <stdint.h>
    #include <stdio.h>

    #include <stdlib.h>
    #include <memory.h>

    #define MC_UINT16(__data__) ( (__data__)>>8 | (__data__)<<8 )
    #define MC_UINT32(__data__) ( (__data__)>>24 | (((__data__)>> 8)&0xFF00) | \
    (((__data__)<< 8)&0x00FF0000) | (((__data__)<<24)&0xFF000000) )
    /*
    31-8 Reserved
    7 路线名称有无Flag
    6-4 岔路数
    3-0 交叉Link列表示Class番号
    */
    #define ROAD_NAME_FLAG(__data__) ((((__data__) & 0x80) >> 7) & 0x01)
    #define BRUCH(__data__) ((((__data__) & 0x70) >> 4) & 0x07)
    #define DISPCLASS(__data__) ((__data__) & 0x07)

    typedef struct Link
    {
    uint16_t size;
    uint32_t id;
    uint16_t nsize;
    uint32_t info;
    char* name;
    }Link;

    void printLink(const Link* link)
    {
    //#linkid=1234;roadnameflag=1;brunch=2;dispclass=3;roadname=青年大街#
    printf("#linkid=%d;roadnameflag=%d;brunch=%d;dispclass=%d;roadname=%s#",
    link->id,
    ROAD_NAME_FLAG(link->info),
    BRUCH(link->info),
    DISPCLASS(link->info),
    ROAD_NAME_FLAG(link->info) ? link->name : "");
    }

    int readLink(FILE* fp, Link * link)
    {
    size_t ret = 0;
    if (fread(&link->size, sizeof(uint16_t), 1, fp) != 1)
    return -1;
    link->size = MC_UINT16(link->size);
    if (link->size == 0xFFFF)
    return -1;

    if (fread(&link->id, sizeof(uint32_t), 1, fp) != 1)
    return -1;
    link->id = MC_UINT32(link->id);

    if (fread(&link->nsize, sizeof(uint16_t), 1, fp) != 1)
    return -1;
    link->nsize = MC_UINT16(link->nsize);

    if (fread(&link->info, sizeof(uint32_t), 1, fp) != 1)
    return -1;
    link->info = MC_UINT32(link->info);
    if (!ROAD_NAME_FLAG(link->info))
    link->nsize = 0;

    if (link->nsize > 0)
    {
    if ((link->name = (char*)malloc(link->nsize + 1))!=NULL)
    {
    if (fread(link->name, (link->nsize), 1, fp) != 1)
    {
    free(link->name);
    link->name = NULL;
    return -1;
    }
    link->name[link->nsize] = '\0';
    }
    else {
    return -1;
    }
    }

    uint16_t left = link->size - 12 - link->nsize;
    assert(left >= 0);
    fseek(fp, left, SEEK_CUR);

    return 0;
    }

    int main()
    {
    FILE* fp = fopen("GTBL.dat", "rb");
    Link link;
    while (!feof(fp))
    {
    memset(&link, 0, sizeof(Link));
    if (readLink(fp, &link) != -1)
    {
    printLink(&link);
    printf("\n");
    }

    }
    fclose(fp);
    return 0;
    }

End。


入职培训总结之C基础
https://dockingyuan.top/2023/03/22/入职培训总结之C基础/
作者
Yuan Yuan
发布于
2023年3月22日
许可协议