一、函数
什么是函数:function
函数就是一段具有某一项功能的代码集合,它是C语言中管理代码的最小单位,把具有某项功能的若干行代码封装在函数中方便管理代码且方便重复调用。
函数的分类:
标准库函数:
C语言标准委员会为C语言以函数形式提供了一些基础功能,这些函数被封装在libc.so库文件中,使用时需要导入对应的头文件,它们的详细介绍在man手册的第3章节。
系统函数:
操作系统为程序员提供了一些系统API,可以以函数形式调用,但它们不是真正的函数,讲UNIX系统环境编程时会详细讲解,它们的详细介绍在man手册的第2章节。
第三方库函数:
一些公司或开源组织实现的一些常用工具供程序员使用。
MD5 、 JSON 序列化反序列化、XML配置文件
自定义函数:
为了更方便的管理、调用代码,降低开发难度,程序员自己封装的一些函数。
常用标准库函数介绍:
标准库中除上封装了函数,还提供一些头文件,里面是对函数的说明。
stdio.h 输入输出相关功能的函数:
int printf(const char *format, ...); 功能:输出数据到终端 format:提示信息+占位符+转义字符组成 ...:若干个变量名或数据 返回值:成功输出的字符个数 int scanf(const char *format, ...); 功能:从终端读取数据 format:一般情况下,只需要占位符即可,除了占位符以外的信息,在输入数据时要原样补出。 ...:若干个变量的地址 返回值:成功读取的变量个数 int getchar(void); int putchar(int c); int puts(const char *s);
stdlib.h 实用的库函数:
int system(const char *command); 功能:调用系统命令,命令执行完成后,该函数才返回 返回值:成功返回0,失败返回-1。 system("clear"); // 清屏命令 int rand(void); 功能:从系统中获取随机数 0~RAND_MAX 返回值:都是正整数,如果需要负数或浮点数,需要程序员自已处理。 void srand(unsigned int seed); 功能:所谓的随机数就是把所有整数打乱顺序,从某个位置获取,默认从1位置获取,程序运行时如果"位置"不改变,获取随机数与上次一样,为了保证每次运行时,提供的位置都发生变化,一般把time函数的返回值提供给srand作为随机数的种子。 srand(time(NULL)); int abs(int j); 功能:计算并返回j的绝对值
ctype.h 字符类型的判断函数:
int isdigit( int ch ); 功能:判断是否是数字字符 int islower( int ch ); 功能:判断是否是小写字母 int isupper( int ch ); 功能:判断是否是大写字母
time.h 时间日期相关的函数:
time_t time(time_t *tloc); 功能:获取当前系统的时间,返回自 1970年1月1日 00:00:00 到现在一共过了多少秒,格林时间+8小时就是北京时间。 time(NULL) struct tm *localtime(const time_t *timep); 功能:把秒数据时间转换成年月日、时分秒
math.h 数学相关的函数 :
double pow(double x, double y); 功能:计算出x的y次数 double sqrt(double x); 功能:计算x的平方根 double ceil(double x); 功能:向上取整,返回大于x的最小整数 double floor(double x); 功能:向下取整,返回小于x的最大整数 注意:使用这些函数在编译时必须有-lm参数 libm.so
练习1:根据time(NULL)的返回值,计算出北京时间 yyyy-mm-dd hh:mm:ss
秒数换算成天数-》
#include <stdio.h> #include <stdbool.h> #include <time.h> int main(int argc,const char* argv[]) { int sec = time(NULL); int day = (sec/3600+8)/24+1; int year = 1970; bool is_leap = 0==year%4 && 0!=year%100 || 0==year%400; while(day >= 365+is_leap) { year++; day -= 365+is_leap; is_leap = 0==year%4 && 0!=year%100 || 0==year%400; } char arr[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; arr[2] += is_leap; int month = 1; while(day > arr[month]) { day -= arr[month++]; } printf("%d-%d-%d %d:%d:%d\n",year,month,day, (sec/3600+8)%24,sec/60%60,sec%60); }
练习2:随机产生10个[100,1000]之间的随机数,只允许调用10次rand
#include <stdio.h> #include <stdlib.h> #include <time.h> int main(int argc,const char* argv[]) { srand(time(NULL)); for(int i=0; i<10; i++) { printf("%d\n",rand() % 901 + 100); // [a,b] rand()%(b-a+1)+a } }
作业1:实现一个双色球的机选功能,红球1-33,选出6个不能重复,蓝球1-16选出一个
#include <stdio.h> #include <stdlib.h> #include <time.h> int main(int argc,const char* argv[]) { srand(time(NULL)); char red[6] = {},cnt = 0; printf("red:"); while(cnt < 6) { int num = rand()%33+1; int i=0; for(i=0; i<cnt; i++) { if(num == red[i]) break; } if(i == cnt) { red[cnt++] = num; printf("%d ",num); } } printf("blue: %d\n",rand()%16+1); }
自定义函数:
有两情况适合把代码封装成自定义函数: 1、代码量过多,一般代码量超过50行就要考虑封装成函数,方便管理代码,提高代码的安全性(程序员平均每50行会出现一个BUG)。 2、如果一个代码需要在不同位置多次执行,为了防止出现代码冗余,就要把它封装成函数,方便重复使用,也能降低可执行文件的大小。
函数声明:
返回值类型 函数名(参数列表);
1、根据函数的功能为函数取名字,在Linux系统下函数名全部小写,多个单词用下划线分隔。 2、参数列表,指的是函数执行时,调用者需要传递它的数据,此时重点关注的是参数的类型,在函数声明时可以忽略参数的名,如果函数执行时不需要调用者传递数据则写void。
返回值类型 函数名(类型名 形参名1,类型名 形参名2,...); 返回值类型 函数名(int n1,int n2,...); 返回值类型 函数名(void);
3、返回值类型,指的是函数的执行结果是什么类型的数据,如果函数没有返回值,则写void。 4、函数声明就是告诉编译器该函数的格式,方便编译器检查调用者的使用是否正确。
5、一般函数声明放在main函数之前
函数定义:
返回值类型 函数名(类型 参数名) { // 函数体 } // 如果函数的定义出现在调用之前,函数声明可以省略
函数调用:
函数名(实参);
1、调用者会把实参赋值给形参变量。 2、函数的返回值会放置在调用位置,可立即使用,也可用变量保存。
练习3:实现一个判断是否是素数的函数,调用它打印出100-1000之间的所有素数
#include <stdio.h> #include <stdbool.h> bool is_prime(int num); int main(int argc,const char* argv[]) { for(int i=100; i<1000; i++) { if(is_prime(i)) printf("%d ",i); } bool flag = is_prime(9999); } bool is_prime(int num) { for(int i=2; i<=num/2; i++) { if(0 == num%i) return false; } return true; }
作业2:输入两个日期yyyy-mm-dd,计算相差多少天,考虑封装函数
#include <stdio.h> #include <stdint.h> #include <stdbool.h> #include <stdlib.h> // 判断闰平年 bool is_leap(uint16_t year); // 计算日期天数函数 uint32_t date_to_days(uint16_t year,uint8_t month,uint8_t day); int main(int argc,const char* argv[]) { uint16_t y = 0; uint8_t m = 0,d = 0; printf("请输入一个日期(yyyy-mm-dd)"); scanf("%hu-%hhu-%hhu",&y,&m,&d); uint32_t day = date_to_days(y,m,d); printf("请再输入一个日期(yyyy-mm-dd)"); scanf("%hu-%hhu-%hhu",&y,&m,&d); printf("间隔了%d天\n", abs((int)date_to_days(y,m,d)-(int)day)); } // 计算日期天数函数 uint32_t date_to_days(uint16_t year,uint8_t month,uint8_t day) { uint32_t sum = day; for(int y=1; y<year; y++) { sum += 365 + is_leap(y); } char arr[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; arr[1] += is_leap(year); for(int m=0; m<month-1; m++) { sum += arr[m]; } return sum; } // 判断闰平年 bool is_leap(uint16_t year) { return 0==year%4 && 0!=year%100 || 0==year%400; }
自定义函数要注意的问题:
1、函数的命名空间互相独立,函数之间传参是单向值传递(实参给形参赋值),所以两个函数之间不能通过传参共享变量。
#include <stdio.h> int func(int num) { printf("func:%d\n",num); num = 100; printf("func:%d\n",num); printf("func:%p\n",&num); return 0; } // 实现一个交换两个变量的值的函数 void swap(int n1,int n2) { printf("swap:n1:%d n2:%d\n",n1,n2); int temp = n1; n1 = n2; n2 = temp; printf("swap:n1:%d n2:%d\n",n1,n2); } int main(int argc,const char* argv[]) { int num = 10; printf("main:%d\n",num); printf("main:%p\n",&num); func(num); printf("main:%d\n",num); int num1 = 10,num2 = 20; printf("num1:%d num2:%d\n",num1,num2); swap(num1,num2); printf("num1:%d num2:%d\n",num1,num2); }
2、C语言中如果函数的参数列表是空的,则意味着该函数提供任意类型、多个参数都可以调用,容易给调用者造成误会,影响代码的可读性,如果函数执行时不需要调用者传递数据则参数列表要写void,不要空着。
#include <stdio.h> int func(void) { printf("func\n"); } int main(int argc,const char* argv[]) { func(); //func(10); //func(3.14,10); //func(3.14,"hehe",10); }
3、如果函数有返回值但没有写return语句,调用该函数时依然有返回值。 当调用一个有返回值的函数时,系统会为调用者和被调用者约定一个空间用于存储返回值,而return语句的作用就是把一个数据存储到这个空间,如果没有写return语句,调用者依然会从共用空间读取返回值,只是读取到的数据是随机的。
#include <stdio.h> int func(void) { //return 10; } int main(int argc,const char* argv[]) { int num = func(); printf("%d\n",num); }
gcc -Wall -Werror xxx.c 可以防止漏写return语句。
xxx.c:x:x: error: control reaches end of non-void function [-Werror=return-type]
4、当使用数组作为函数的参数传递时,它的长度信息就丢失了,(数组会蜕变成指针),无法使用sizeof计算数组的长度,需要调用者额外提供一个参数作为数组的长度。
void show_arr(int arr[],size_t len) { for(int i=0; i<len; i++) { printf("%d ",arr[i]); } } int main(int argc,const char* argv[]) { int arr[] = {1,2,3,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5}; show_arr(arr,sizeof(arr)/sizeof(arr[0])); }
练习4:定义一个函数,功能是对一个数组进行升序排序
5、当函数中使用数组作为参数传递是,是"址传递" ,是可以被函数所共享数组
6、当使用二维数组作为函数的参数时,C语言规则定义二维数组时必须有列数,所以要行、列数在前,数组在后,并且把列数设置给数组。
void show_arr(int row,int col,int arr[][col]) { for(int i=0; i<row; i++) { for(int j=0; j<col; j++) { printf("%d ",arr[i][j]); } printf("\n"); } } int main(int argc,const char* argv[]) { int arr[2][5] = { {1,2,3,4,5}, {2,3,4,5,6} }; show_arr(2,5,arr); return 0; }
项目二:五子棋游戏
数据分析:
1、定义15*15的二维数组,作为棋盘 char ' * ' ' @ ' ' $ '
2、定义记录落子位置的变量
3、定义一个棋子角色变量 char role = '@'
业务逻辑分析:
1、定义15*15的二维数组,作为棋盘 char ' * ' ' @ ' ' $ ' 2、定义记录落子位置的变量 3、定义一个棋子角色变量 char role = '@' // 初始化棋盘 void init_board(void); // 显示棋盘 void show_board(void); // 落子 如果位置非法,需继续重新落子,直到成功后才能返回 void get_key(void); // 判断五子连珠 bool is_win(void); int main() { for(;;) { 1、清理屏幕、显示棋盘 2、落子 3、判断是否五子连珠 是:提示+结束程序 4、交换角色 } }
要求:每个功能实现函数来调用
如何通过方向键控制光标:
#include <stdio.h> #include <getch.h> #include <stdlib.h> int main(int argc,const char* argv[]) { system("clear"); printf(" * * * * * * * * * *\n"); printf(" * * * * * * * * * *\n"); printf(" * * * * * * * * * *\n"); printf(" * * * * * * * * * *\n"); printf(" * * * * * * * * * *\n"); printf(" * * * * * * * * * *\n"); printf(" * * * * * * * * * *\n"); printf(" * * * * * * * * * *\n"); printf(" * * * * * * * * * *\n"); printf(" * * * * * * * * * *\n"); int x = 5, y = 5; for(;;) { printf("\33[%d;%dH",x,y*2); switch(getch()) { case 183: x--; break; case 184: x++; break; case 186: y--; break; case 185: y++; break; case 10: //回车 } } }