一、简介
C语言是在1978年,K&R为了编写UNix操作系统,从B语言的基础上改进而来。
在1989年被美国美国国家标准协会ANSI标准化,称之为C89(第二版)。
1999年再次标准化C99(第二版)
2011年再次被标准化C11(第三版)
目前linux系统,单片机开发等底层开发都使用C语言开发,搭配 GNU 的 C/C++ 编译器使用。
二、语言特点
从上到下执行,有预处理器#,分支、循环、指针三大法宝。作用域以代码块来分开
三、C编译器语法:包括了预处理器#
1.数据类型:决定存储单元和如何解释存储的数据。
基本数据类型:整数(int)、小数(float)、字符(char).
枚举数据类型:枚举(enum)
void类型:void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。
派生类型:数组(array)、指针(*p)、结构体(struct)
typedef关键字:可以自己定义数据类型
2.变量:某一块内存的别名
1.命名规则:数字、字母、下划线组成且不能以数字开头。
2.声明与定义:不分配存储空间,定义:即有赋初值,并分配存储空间。
对变量而言只有extern 是才是声明,其余全是定义。函数不带{}时就是声明,带{}时就是定义。
3.左值和右值:左值是地址,右值是数据,故在赋值(=)时,右值不可以在左边只能在右边,而左值均可以,因为赋值语句(a = b),对左边的变量是写操作,对右边的变量是读操作。
3.常量:其值在程序运行期间不可修改
在程序期间其值不可以修改,有整数常量10,小数常量,字符常量'a',字符串常量“abcd”
对字符串常量编译器会自动在最后加NUL=\0作为结尾,在声明char array[]时数组长度要比字符串多1个用来存储\0.
4.存储类型:
可调整变量、函数的存储位置、局部变量的生命周期、全局变量的作用域。
①对局部变量,默认是atuo,进入函数使用变量,退出函数释放变量。加static后,内存不会被释放,且值只能初始化一次。相当于常量啦。
②对全局变量:默认是static,其作用域是整个源程序,加static修饰作用域限制为本文件内部且只能初始化一次。
③对函数:普通函数在每个没调用周期内维持一份,加static后在内存中只有一份,这一点没太多优势。
5.运算符:5类
1.算术运算符:加、减、乘、除(/)、取余(%)、自增(++)、自减(--)
2.关系运算符:大于(>)、小于(<)、等于(==)、不等于(!=)、大于等于(>=)、小于等于(<=)
3.逻辑运算符:与(&&)、或(||)、非(!)
4.位运算:位与(&)、位或(|)、位异或(^)、按位取反(~)、按位左移动(<<)、按位右移(>>)
5.简单赋值运算:=
6.复合赋值运算: 算术运算符、位运算符与简单运算符号组合成新的运算符号
+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=
7.杂项运算符: 求变量内存单元sizeof()、取地址符&、指针运算符*、如果A为真?则值为x:否则为y。逗号表达式A,B,C;返回C的值。
条件表达式如一组if(条件为真) 执行x,否则else执行y
6.分支语句:2个
C 语言把任何非零和非空的值假定为 true,把零或 null 假定为 false。
if...else,和swtich....case、条件表达式?:
7.循环语句:
计数循环:for ( ; ; )
条件循环while()、 do ....while()
死循环:while(1) 或者for( ; ; )for循环条件为空默为真。
8.退出循环语句:
break;退出本级循环、退出swtich
continue;终止本次循环,开始下一次循环
goto 语句标号: 跳转到语句标号执行
9.作用域规则:
1.大括号内部的为局部变量,包括函数的大括号或者块。作用域再块内部有效,存放再动态区(栈)
2.大括号外部的为全局变量,存在静态区,作用域为整个程序,static可以修改为本文件内部。
3.函数形参为函数{}内部的局部变量
4.当全局变量和局部变量同名时,使用局部变量。
10.数组:申请一篇连续的内存单元,且每个内存单元相等,即同类型变量。
1.数组名为数组地址,即数组首元素的地址
2.按照列来存放,故一位数组可以省略数组单元长度,二位数组第二位列长度不可以省略。
3.静态数组再申明时指定内存块大小,动态内存根据需要手动分配。
11.枚举:默认从0开始增加1,也可以指定
声明:enum 枚举名{ }枚举变量名;此处枚举名相当于结构名,相当于类型名。
声明时:枚举名和变量名至少有一个,可以同时存在。结构体定义一样。
ege:enum Day{ MON,TUE,WED,THU,FRI,SAT,SUN}day;其中Day和day最多只能省略一个。
12.指针变量:用来存放普通变量地址的变量,普通变量是用来存值的。
13.函数指针和回调函数:
1.函数指针:存储函数地址的变量。声明将指向的函数名改为指针变量即可。
typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型
2.回调函数,函数指针作为另外一个函数的参数,需要先注册,再使用。
此函数中回调函数是getNextRandomValue产生随机数的函数。因为给函数指针传递的是此函数的地址。
14.字符串:字符串实际上是使用空字符 \0 结尾的一维字符数组
空字符(Null character)又称结束符,缩写 NUL,是一个数值为 0 的控制字符,\0 是转义字符,意思是告诉编译器,这不是字符 0,而是空字符。
ege: char site[] = "RUNOOB";
结尾的\0由编译器自动添加。
15.结构体:指向自己的数据结构,构建链表注意理解
声明:struct 结构名{ 成员}结构变量,此处类型名是struct 结构名。
指向结构自己,为了构建链表、树等高级数据结构,每个节点都有相同的数据类型
16.共用体union
成员占用同一块内存,内存大小由最大的成员决定,可以存储不同类型的变量。即在这块内存上不用区分数据类型了。共用体变量访问用.指针用->
17.结构体位域:为了节省内存定义的,按二进制位分配存储单元
struct 位域名
{
类型 变量名:宽度 //类型名只能为signed \unsigned int,宽度不能操作类型宽度
}
18.typedef 与#define异同
1.typedef与#define均可对类型取别名,但是#define还可以给数值取别名,而typedef不行。
2.#define由预处理器解释,typedef由编译器解释
3.语法相反:typedef 原名 新名字;#define 新名 原名;
19.预处理器:文本替换工具,在编译器之前替换,均以#开头
1.处理指令:
#define: 定义宏
#include :包含一个源代码文件
#undef :取消已定义的宏
#ifdef、#endif如果宏以定义返回真与#endif搭配使用
#ifndef:如果宏没有定义返回真
#if、#elif、#else与if 、else if、else一样
#error:预处理时遇到#error停止编译,并答应#error后面定义的信息(与编译器有关)
#pragma:预处理时遇到#pragma可以打印信息,但是运行时不打(与编译器有关)
2.预处理器运算符
延续运算符(\):宏太长可以使用\来延续,相当于在一行上
字符串常量化运算符(#):将形参转换为字符串常量
标记粘贴运算符(##):将两个参数拼接为一个参数。
3.参数化的宏:可以定义宏函数
4.预处理器内置的宏:
__DATA__:当前日期,以"MMM DD YYYY"表示的字符常量
__TIME__:当前日期,一个”“HH:MM:SS"格式表示的字符常量
__FILE__:当前文件名,一个字符串常量
__LINE__:当前行号,一个十进制常量
—STDC__,当以ANSI标准编译时,则定义为1
20.头文件:
#include<file>:编译器到自己根目录去寻找file
#include"file":编译器到当前工程空间下寻找file
21.类型转换说明符:重新分配有效存储单元
1.类型转换分为隐式转换和显示转换。对算术运算符存在隐式转换,对赋值运算符和逻辑运算符&&\||不存在隐式转换。隐式转换常常用于char 、int、long、long、foat、double类型之间转换
2.显示转换由程序员自己决定转换:(new_type) 变量名
22.递归函数:即函数调用自己,一直循环。
计算数学问题时很有帮助:如数的阶乘、斐波那契数列。
①计算数的阶乘:
#include <stdio.h>
double factorial(unsigned int i)
{
if(i <= 1)
{
return 1;
}
return i * factorial(i - 1);
}
int main()
{
int i = 14;
printf("%d 的阶乘为 %f\n", i, factorial(i));
return 0;
}
②计算斐波那契数列:
#include <stdio.h>
int fibonaci(int i)
{
if(i == 0)
{
return 0;
}
if(i == 1)
{
return 1;
}
return fibonaci(i-1) + fibonaci(i-2);
}
int main()
{
int i;
for (i = 0; i < 10; i++)
{
printf("%d\t\n", fibonaci(i));
}
return 0;
}
23.C语言命令行参数:
编译时可以从外部给main函数传递参数:参数之间用空格隔开,想要传入空格时用双引号转义。
//或者 int main(int agc,char ** argv)
int main(int argc,char *argv[])//agrc缓存传入的参数,argv[0]存储程序名称,
//argv[1]存储第一个参数的地址。故argc至少大于1
{
return 0;
至此:C语言编译器自带的语法已经写完,下一部分写,标准库提供 的函数接口。
二、库文件常用函数说明
1.输入输出头文件:<stdio.h>
C语言与linux一样,把一切设备当文件,因此外设就是一个个的文件指针代替。stdin、stdout、stderr这三个文件指针,在编译时时自动打开。
stdin:键盘指针,表示从键盘读入数据。
stdout:屏幕指针,表示输出到屏幕(输出到标准串口)
stderr:你的屏幕指针,表示输出错误信息到你的屏幕(存在多个telnet连接时)
1.通用输入输出函数:printf--scanf
printf("输出格式化列表",参数);scanf("格式化读入列表",接受缓存地址)
int scanf(const char *format, ...) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。读到空格时停止读入。
int printf(const char *format, ...) 函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。
#include <stdio.h>
int main( ) {
char str[100];
int i;
printf( "Enter a value :");
scanf("%s %d", str, &i);
printf( "\nYou entered: %s %d ", str, i);
printf("\n");
return 0;
}
2.输入输出单个字符:getchar()&&putchar
int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。
int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符
#include <stdio.h>
int main( )
{
int c;
printf( "Enter a value :");
c = getchar( );
printf( "\nYou entered: ");
putchar( c );
printf( "\n");
return 0;
}
3.输入输出字符串:gets()&&puts()
char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。
int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。
#include <stdio.h>
int main( )
{
char str[100];
printf( "Enter a value :");
gets( str );
printf( "\nYou entered: ");
puts( str );
return 0;
}
2.读写文本文件:<stdio.h>打开、关闭、读文件、写文件
1.打开文件:FILE *fopen( const char *filename, const char *mode );
filename 是字符串,用来命名文件,访问模式 mode 的值可以是下列值中的一个:
r:只读,w:只写,文件不存在时创建文件,从头开始覆盖写入。a:追加写入,文件不存在时创建
+:补全读写权限,创建和从头开始、追加不变:r+,w+,a+
如果是二进制文件:"rb"、"wb"、"rb+"、"r+b"、"wb+"、"w+b"、“ab+”、“a+b”
2.关闭文件: int fclose( FILE *fp );
如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。
3.写入单个字符:int fputc( int c, FILE *fp );
函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF
4.写入字符串:int fputs( const char *s, FILE *fp );
函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF
使用 int fprintf(FILE *fp,const char *format, ...) 函数把一个字符串写入到文件中。
注意:请确保您有可用的 tmp 目录,如果不存在该目录,则需要在您的计算机上先创建该目录。
/tmp 一般是 Linux 系统上的临时目录,如果你在 Windows 系统上运行,则需要修改为本地环境中已存在的目录,例如: C:\tmp、D:\tmp等
#include <stdio.h>
int main()
{
FILE *fp = NULL;
fp = fopen("/tmp/test.txt", "w+");
fprintf(fp, "This is testing for fprintf...\n");
fputs("This is testing for fputs...\n", fp);
fclose(fp);
}
5.读单个字符:int fgetc( FILE * fp );
fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF
6.读字符串:char *fgets( char *buf, int n, FILE *fp );
函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。
如果这个函数在读取最后一个字符之前就遇到一个换行符 '\n' 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。
使用 int fscanf(FILE *fp, const char *format, ...) 函数来从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。
#include <stdio.h>
int main()
{
FILE *fp = NULL;
char buff[255];
fp = fopen("/tmp/test.txt", "r");
fscanf(fp, "%s", buff);
printf("1: %s\n", buff );
fgets(buff, 255, (FILE*)fp);
printf("2: %s\n", buff );
fgets(buff, 255, (FILE*)fp);
printf("3: %s\n", buff );
fclose(fp);
}
3.读写二进制文件:<stdio.h>
size_t fread(void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);
size_t fwrite(const void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);
这两个函数都是用于存储块的读写 - 通常是数组或结构体
4.错误处理: errno.h
C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。
errno、perror() 和 strerror()
C 语言提供了 perror() 和 strerror() 函数来显示与 errno 相关的文本消息。
- perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
- strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
让我们来模拟一种错误情况,尝试打开一个不存在的文件。您可以使用多种方式来输出错误消息,在这里我们使用函数来演示用法。另外有一点需要注意,您应该使用 stderr 文件流来输出所有的错误。
#include <stdio.h>
#include <errno.h>
#include <string.h>
extern int errno ;
int main ()
{
FILE * pf;
int errnum;
pf = fopen ("unexist.txt", "rb");
if (pf == NULL)
{
errnum = errno;
fprintf(stderr, "错误号: %d\n", errno);
perror("通过 perror 输出错误");
fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
}
else
{
fclose (pf);
}
return 0;
}
错误号: 2
通过 perror 输出错误: No such file or directory
打开文件错误: No such file or directory
5.可变参数:stdarg.h
需要使用 stdarg.h 头文件,该文件提供了实现可变参数功能的函数和宏。具体步骤如下:
- 定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
- 在函数定义中创建一个 va_list 类型变量,该类型是在 stdarg.h 头文件中定义的。
- 使用 int 参数和 va_start() 宏来初始化 va_list 变量为一个参数列表。宏 va_start() 是在 stdarg.h 头文件中定义的。
- 使用 va_arg() 宏和 va_list 变量来访问参数列表中的每个项。
- 使用宏 va_end() 来清理赋予 va_list 变量的内存。
常用的宏有:
-
va_start(ap, last_arg)
:初始化可变参数列表。ap
是一个va_list
类型的变量,last_arg
是最后一个固定参数的名称(也就是可变参数列表之前的参数)。该宏将ap
指向可变参数列表中的第一个参数。 -
va_arg(ap, type)
:获取可变参数列表中的下一个参数。ap
是一个va_list
类型的变量,type
是下一个参数的类型。该宏返回类型为type
的值,并将ap
指向下一个参数。 -
va_end(ap)
:结束可变参数列表的访问。ap
是一个va_list
类型的变量。该宏将ap
置为NULL
。 -
现在编写一个带有可变数量参数的函数,并返回它们的平均值:
-
#include <stdio.h> #include <stdarg.h> double average(int num,...) { va_list valist; double sum = 0.0; int i; /* 为 num 个参数初始化 valist */ va_start(valist, num); /* 访问所有赋给 valist 的参数 */ for (i = 0; i < num; i++) { sum += va_arg(valist, int); } /* 清理为 valist 保留的内存 */ va_end(valist); return sum/num; } int main() { printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5)); printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15)); }
6.内存管理: <stdlib.h>
- 程序员可以对内存进行操作,包括分配、释放、移动和复制等。
1.分配内存:单位字节
void *malloc(int num);在堆区中分配num个未初始化的内存(字节)
void *calloc(int num, int size);在内存中分配num个大小为size的连续内存空间,并初始化为0。(字节)
void *realloc(void *address, int newsize);将原来的内存大小改为nuwsize(字节)
2.释放内存:void free(void *address);
3.复制内存:void *memcpy(void*dest, const void *src, size_t n);
从源地址复制n个字节到dest中,并返回指向dest的指针。
4.移动内存: void *memmove(void *str1, const void *str2, size_t n)
从 str2 复制 n 个字符到 str1,但是在重叠内存块这方面,memmove() 是比 memcpy() 更安全的方法。如果目标区域和源区域有重叠的话,memmove() 能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,复制后源区域的内容会被更改。如果目标区域与源区域没有重叠,则和 memcpy() 函数功能相同。
三、C89、C95、C99、C11演变过程:
标准库接口编写目的:C标准库的定义是让C编译器开发商之间的C程序能相互移殖, C标准库系统API都是动态链接库(相互各态关系),因此各个编译器都支持这些函数,在使用时只需要包含其头文件即可。常用C语言编译器有GCC、Clang、Intel C++ Complier支持到C11
1.C89的15个头文件
1. <assert.h>
2. <ctype.h>
3. <errno.h>
4. <float.h>
5. <limits.h>
6. <locale.h>
7. <math.h>
8. <setjmp.h>
9. <signal.h>
10. <stdarg.h>
11. <stddef.h>
12. <stdio.h>
13. <stdlib.h>
14. <string.h>
15. <time.h>
2.C95新增头文件:
1. <iso646.h>
2. <wchar.h>
3. <wctype.h>
3.C99新增头文件:
1. <complex.h>
2. <fenv.h>
3. <inttypes.h>
4. <stdbool.h>
5. <stdint.h>
6. <tgmath.h>