0基础小白学C语言看这一篇就够了(C语言详讲万字!!!)

目录

一.配置C语言运行环境

二.第一个C语言程序

  一.创建新项目:

二.添加源文件:

 三.第一个C语言程序:

三.数据类型,变量的定义和使用,作用域和生命周期

 一.数据类型的分类

二.数据类型的介绍以及变量的定义和使用:变量名只能以数字下滑线和字母组成(不能以数字开头,变量名不能和C语言函数一样,变量名要有对应的意义)

1.变量的介绍:

2.变量的使用

三.作用域和生命周期

1.局部变量

2.全局变量

四.数据类型的存储

 一.数据的类型的介绍

1.整形家族:有符号和无符号的定义。

2.浮点型:

3.构造类型:(自定义类型)

4.指针类型:

5.空类型:

二.整形在内存中的存储:源码,反码,补码

练习:

三.大小端字节序的介绍及判断

四.浮点型在内存中的存储解析

五.分支

 一.if else函数

1.单分支:

2.多分支:

二.switch语句

六.循环

 一.while , do while函数 

二.for函数

三.goto语句

四.猜数字:

七.运算符

一.算数操作符

二.位移操作符

三.位操作符

四.赋值操作符

五.单目操作符

六.关系操作符

七.逻辑操作符

八.条件操作符

九.逗号表达式

十.下标引用,函数调用和结构成员

十一.表达式求值

总结:

八.函数

 一.函数是什么

二.库函数

三.自定义函数

四.函数参数

五.函数的嵌套调用和链式访问

六.函数的声明和定义

七.函数递归

九.数组

 一.一维数组

1.一维数组的创建:

2.一维数组的初始化:

3.一维数组的使用

4.一维数组在内存中的存储:

二.二维数组

三.数组越界

四.数组作为函数参数

十.指针

 一.指针和指针类型

二.野指针

三.指针运算

四.指针和数组

五.二级指针:

六.字符指针

七.指针数组

八.数组指针

九.数组参数和指针参数

十.函数指针

十一.函数指针数组

十二.回调函数

十一.结构体

 一.什么是结构体

二.以及结构体的定义和使用

三.结构体数组

四.结构体指针

十二.文件操作(输入输出流)

什么是文件

文件的打开和关闭

文件的打开:

文件的打开和关闭:

文件的输入输出流

使用fputc写文件:

使用fgetc读文件:

使用fputs写一行:

使用fgets读一行:

使用fprintf写入:

使用fscanf读取:

文件的随机读写:

文本文件和二进制文件:

文件读取结束的判定

文件缓冲区:


一.配置C语言运行环境

下载 Visual Studio Tools - 免费安装 Windows、Mac、Linux (microsoft.com)icon-default.png?t=N7T8https://visualstudio.microsoft.com/zh-hans/downloads/

二.第一个C语言程序

  一.创建新项目:

右小太阳是用来切换黑白的, 这是已经配置好的编译环境,可以在修改中添加修改环境以及添加环境,启动是用来打开项目和添加项目的....

 这里是已经配置好的C++环境可以在修改中看到....

点击启动之后来到这个界面在左边可以看到之前打开过的文件右边可以看见创建新项目这个选项。

 创建项目可以先点击空项目在点击下一步。

创建项目首先可改名字,再放置在自己找得到的位置下,最后点击创建就创建好一个新项目了。

二.添加源文件:

创建好新项目后 先找到视图再点击解决方案管理器

 然后可以先点源文件再点添加再点创建新项目。

 这里可以先点c++文件(cpp)再改名字(名字的后缀不能遗落.c代表c语言文件.cpp代表c++文件)最后点添加就行了。

这里是默认打开的

 三.第一个C语言程序:

 可按FN+ctrl+F5编译写好的程序,这里运行后是完成了一个打印hello Nowcoder!的功能。

#include<stdio.h>
int main() {
	printf("hello Nowcoder!");
}

 写好的C语言程序是文本信息不能直接运行要经过编译->链接->可执行程序,最生成一个可以执行的test.exe文件,双击就可以运行效果是和你在编译器里运行一样的。

现在来解释一下这个C语言代码:程序是从int main()函数开始的main函数下面的{}里的内容是整个程序的主体一个程序有且只有一个int main(),函数void main()这中写法是非常古老的不推荐。int main(void)也可以,表示main函数不接受任何参数,带参的main暂时接触不到,printf是一个库函数由C语言编译器提供的一个现成的函数可以直接使用功能是在屏幕上打印数据hello Nowcoder!是一个字符串双引号引起来的就是字符串,直接使用会报错要包含头文件#include<stdio.h>


三.数据类型,变量的定义和使用,作用域和生命周期

 一.数据类型的分类

char字符数据类型
short短整形
int整形
long长整型
long long更长的整形
float单精度浮点数
double双精度浮点数

二.数据类型的介绍以及变量的定义和使用:变量名只能以数字下滑线和字母组成(不能以数字开头,变量名不能和C语言函数一样,变量名要有对应的意义)

1.变量的介绍:

char a;在内存中分配了一个字节的空间并把其命名为a类型是char字符类型用来存放字符类型的数据。
short b; 在内存中分配了两个字节的空间并把其命名为b类型是short短整型用来存放整形数据.
int c;在内存中分配了四个字节的空间并把其命名为c类型是int整型用来存放整形数据.
long  d; 在内存中分配了四个字节的空间并把其命名为d类型是long长整型用来存放整形数据.
long long e;在内存中分配了八个字节的空间并把其命名为e类型是long long更长的整型用来存放整形数据.
float f; 在内存中分配了四个字节的空间并把其命名为f类型是float单精度浮点数用来存放小数.
double g; 在内存中分配了八个字节的空间并把其命名为g类型是double双精度浮点数用来存小数.

sizeof是用来求字节大小的运算符,%d是以十进制打印得到的数据,\n是换行符执行完之后就换到下一行了;

#include<stdio.h>
int main() {
	printf("数据类型的大小:\n");
	printf("char=%d\n", sizeof(char));
	printf("short=%d\n", sizeof(short));
	printf("int=%d\n", sizeof(int));
	printf("long=%d\n", sizeof(long));
	printf("long long=%d\n", sizeof(long long));
	printf("float=%d\n", sizeof(float));
	printf("double=%d\n", sizeof(double));
}

2.变量的使用

变量可以这样创建:char a;//创建了一个char类型的a变量

赋值:在创建时赋值也叫初始化:char a=‘A’;//把A字符存放进了名为a变量的空间里,也可char a;  a='A';这样给创建好的变量赋值,还可以这样赋值scanf("%c",&a);scanf是输入函数%c是以字符的形式输入给a变量;

初始化a=A此时a的值是A打印后再重新赋值a=B a的值变成B再次打印然后停在scanf("%c",&a)这等待输入

#include<stdio.h>

int main() {
	char a = 'A';
	printf("%c\n", a);
	a = 'B';
	printf("%c\n", a);
	scanf("%c", &a);
	printf("%c\n", a);
}

输入K之后scanf函数接收并赋值给a a的值变为K再打印

 

其他类型也是一样的只不过输入和输出时类型要对应比如字符 是用%c输入和输出的整形就要用%d,浮点型用%f。

#include<stdio.h>
int main() {
	int a = 100;
	printf("%d\n", a);
	scanf("%d", &a);
	printf("%d\n", a);
}

三.作用域和生命周期

1.局部变量

a变量在进入这个函数创建就只能在此函数内使用只有此函数结束了才销毁

#include<stdio.h>

int main() {
	int a;
	printf("%d", a);
}

2.全局变量

b变量是全局的可以在程序的任何地方使用程序结束才销毁。

#include<stdio.h>
int b = 3;
int main() {
	int a;
	printf("%d", a);
}


四.数据类型的存储

 一.数据的类型的介绍

char  字符数据类型 一个字节大小
short短整型   两个字节大小
 int 整形四个字节大小
long长整型四个字节大小
long long更长的整形 八个字节大小
float单精度浮点数四个字节大小
double双精度浮点数八个字节大小

为什么有int整形了还要short和long?:这是因为int在32位机器上表示21亿多而且占4个字节但有时候用不到这么大的数比如年龄所以有short来表式而且只占2个字节有时候int也不够用所以又有了long类型来表示

1.整形家族:有符号和无符号的定义。

ASCLL可以参考:ASCII 表 | 菜鸟教程 (runoob.com)https://www.runoob.com/w3cnote/ascii.html

unsigned    是无符号     unsigned没有符号位只有数值位

signed        是有符号     存储时会把最高位当成符号位其他位是数值位

char :因为字符存储的时候,存储的是ASCII码值,是整形,所以归类的时候放在整形家族里

        unsigned char     

        signed char

       char是unsigned char 还是signed因为C语言没有给出标准所以不确定在 VS编译器上是char=signed char 如果要定义无符号的则要unsigned char这样写

short

        unsigned short [int]

        signed short [int]

short=signed short  =-32878~32767

unsigned shor         =0~65535

int 

        unsigned int 

        signed int

在32位机器下:其他的范围各有不同

int =signed int   =-2147483648~2147483647

unsigned int      =0~4294967296

long

        unsigned long [int]

        signed long [int]

long =signed long

unsigned long

2.浮点型:

  • float
  • double

3.构造类型:(自定义类型)

  • >数组类型
  • >结构体类型   struct
  • >枚举类型      enum
  • >联合类型      union

4.指针类型:

  • int*    pi;
  • char* pc;
  • float* pf;
  • void* pv;

5.空类型:

void表示空类型(无类型)

通常应用于函数的返回类型,函数的参数,指针类型

二.整形在内存中的存储:源码,反码,补码

计算机中的整数有三种2进制表示方法,即源码,反码和补码。

三种表示方式均有符号位和数值位两部分,符号位都是用0表示‘正’,用1表示‘负’,而数值位正数的原,反,补码都相同。(二级制最高位是符号位其他位是数值位)

负整数的三种表示方式各不相同。

源码:直接将数值按照正负数的形式翻译成二进制就可以得到源码。

反码:将源符号位不变,其他位依次按位取反就可以得到反码。

补码:反码+1就得到补码

int num=10;//创建一个整形变量叫num,这时num向内存中申请了4个字节空间来存放数据10

//4个字节-32个二进制

//源码:00000000000000000000000000001010

//反码:00000000000000000000000000001010

//补码:00000000000000000000000000001010

int num1=-10;//创建一个整形变量叫num1,这时num1向内存中申请了4个字节空间来存放数据-10

//4个字节-32个二进制

//源码:  10000000000000000000000000001010 

//反码:111111111111111111111111111111110101 //源码转反码符号位之外其他位按位取反,反码转源码符号位之外其他位按位取反。反码+1=补码,补码-1=反码。

//补码:111111111111111111111111111111110110 

对于整数来说:数据存放内存中其实放的是补码,在计算机系统中,数值一律用补码来表示和存储,原因在于,使用补码,可以将符号位和数值域统一处理,同时加减法也可一统一处理(CPU只有加法器)此外,补码于源码相互转换,其运算过程是相同的,不需要额外的硬件电路。

可以使用&num取出num的地址因为VS为了方便展示所以显示的是16进制,0a 00 00 00这是因为倒着存的num1负书可以验证.

#include<stdio.h>
int main() {
	int numm = 10;
	int num1 = -10;
}

-10的补码是111111111111111111111111111111110110,16进制算出来是ox ff ff ff 6f而存入却是f6 ff ff ff这证实了内存中的数据是倒着存入的(后面大端小端会讲原因)

 源码计算1-1可以表示1+(-1)

1=00000000000000000000000000000001

-1=10000000000000000000000000000001

1+(-1)=10000000000000000000000000000010=-2//结果显然是错误的

补码 计算1-1可以表示1+(-1)

1= 00000000000000000000000000000001

-1=1111111111111111111111111111111111111

1+(-1)=1111111111111111111111111111111111110计

算完之后是补码转为反码是:

1111111111111111111111111111111111111

在转为源码是:

00000000000000000000000000000000=0
所以在内存中存的是补码为了方便运算

练习:

#include<stdio.h>
int main()
{
char a=-1;
signed char b=-1;
unsigned char c=-1;
printf("a=%d,b=%d,c=%d\n",a,b,c);
}

-1的补码是11111111111111111111111111111111因为a是char类型大小只有8个字节所以只有

11111111存进来了再以%d打印%d比char a大会

发生整形提升之后的补码就是10000000000000000000000011111111

转为源码是100000000000000000000000000000001=-1

signed char b和a的类型都是有符号整形所以b=a=-1

因为unsigned char c是无符号整形所以

发生整形提升之后会变成0000000000000000011111111=255最高位是0变成了正数

经过计算补码和截断之后存储再按照类型整形提升之后得到的值是-1 -1 255

#include<stdio.h>
int main() {
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	printf("a=%d,b=%d,c=%d\n", a, b, c);
}

#include<stdio.h>
int main()
{
	char a = -128;
	printf("%u\n", a);
}

-128的补码是11111111111111111111111110000000因为a是char类型大小只有8个字节所以只有10000000存进a里

在以%u(以十进制无符号打印)会发生整形提升所以打印的是:无符号数的原反补都是相同的所以直接打印

补码=源码=反码=11111111111111111111111100000000=4294967168

#include<stdio.h>
int main()
{
char a=128;
printf("%u\n",a);
}

下面因为存进a里面的值是和上面一样的所以%u整形提升后打印出来的值也是一样的

#include<stdio.h>
int main()
{
	int i = -20;
	unsigned int j = 10;
	printf("%d\n", i + j);
}

 i的补码是111111111111111111111111111101100               

unsigned int j;补码是000000000000000000000000000001010

计算过程是:

i+j=111111111111111111111111111101100 +000000000000000000000000000001010

=补码:11111111111111111111111111110110=反码:111111111111111111111111111110101

=源码:100000000000000000000000000001010=-10

#include<stdio.h>
int main()
{
unsigned int i;
    for(i=9;i>=0;i--)
    {
    printf("%u\n",i);
    }
}

死循环的原因是i 是无符号整形所以i=0也不成立出现4294967295是因为unsigned int 类型的范围是0~4294967296,所以i是0在继续减不会变成-1因为-1不再unsigned int类型的范围里所以会变成4294967295

 因为strlen到/0就不会继续了 而在ASCLL码中0就是/0实际255就是char -128道127的所有值

unsigned char i = 0;
int main()
{
	for (i = 0; i <= 255; i++)
		printf("hello world\n");
}

 死循环是因为255超unsiged的0~255的上线了

三.大小端字节序的介绍及判断

把0x 11 22 33 44存进内存一开始有这几种方法

  • 低地址                        高地址
  •  11    |    22    |    33    |    44
  •  44    |    33    |    22    |    11
  •  11    |    22    |    44    |    33
  •  11    |    44    |    33    |    22
  • 存储进来主要是为了之后方便拿出来所以只留下了这两种称为大端字节序和小端字节序(字节序是以字节为单位来存储数据的)
  •  11    |    22    |    33    |    44    大端:把一个数据的低位字节的内容,存放在高址值处,把一个高数据的高位字节内容,存放在低地址处。
  •  44    |    33    |    22    |    11    小端:把一个数据的低位字节的内容,存放在低址值处,把一个高数据的高位字节内容,存放在高地址处。
  • char ch;类型是一个字节没有顺序

因为在计算机系统中是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语音中除了8 bit 的char之外,还有16 bit的short型,32 bit的long型(要看具体编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题,因此就导致了大端和小端存储模式。

列如:一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11位高字节,0x22为低字节,对于大端模式,就将0x11放在地址值中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反,我们常用的x86结构是小端模式,而KEIL C51则为大端模式,很多的ARM,DSP都为小端模式,有些ARM处理器可以有硬件来选择是大端还是小端模式

44  |  33  |  22  |  11  因为:小端:把一个数据的低位字节的内容,存放在低址值处,把一个高数据的高位字节内容,存放在高地址处。

所以这里是按照小端模式来存储的

还以这样判断大端和小端:&a的第一个地址如果是大端的话就是00小端是01再(char*)强制类型转换因为转化的类型char只有一个字节大小所以只保留了01让后再*解引用得出值和1比较就可以判断大端和小端了

#include<stdio.h>
int main()
{
	int a = 1;
	if (*(char*)&a == 1) {
		printf("小端\n");
	}
	else {
		printf("大端\n");
	}
}

要注意在Release  x64下你电脑的杀毒软件会把你的程序认为是病毒换成Debug就行了

 

四.浮点型在内存中的存储解析

#include<stdio.h>

int main() {
	int n = 9;
	float* pFloat = (float*)&n;;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	*pFloat = 9.0;
	printf("num的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
}

浮点数的存储规则:num和*pFloat在内存中明明是同一个数,为什么浮点数和整数的读取结果会差别这么大?

根据国际标准IEEE(电子和电子工程协会)754,任意一个二进制浮点数V可以表示成下面形式·(-1)^S*M*2^E

·(-1)^S表示符号位,当S=0,v为正数,当S=1,v为负数

M表示有效数字,大于等于1,小于2

2^E表示指数位数

5.5-十进制浮点数=二进制的浮点数101.1=(-1)^0*1.011*2^2

1                0             1    .      1

=                =             =           =

1*2^2 + 0*2^1 + 1*2^0 + 1*2^-1

十进制9.0=二进制1001.0

=1.001*2^3=(-1)^0*1.001*2^3

S=0 M=1.001,E=3

IEEE 754规定:

对于32位的浮点数,最高的1位是符号位S,接着的8为指数E,剩下的23位为有效数字M 

对于64位浮点数,最高的1位是符号位S,接着的 11位是指数E,剩下的52位为有效数字M 

 IEEE 754对有效数字M和指数E,还有一些特别的规定

前面说过,1<=M<2,也就是说,M可以写成1.XXXX的形式,其中XXXX是小数部分

IEEE 754 规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的XXXX部分,比如保存1.01的时候,只保存01,等到读取的时候,在把第一位的1加上去,这样做目的是,节省1位有效数字,以32位浮点数为例,留给M只有23位1,将第一位的1舍去以后,等于可以保存24位有效数字

至于指数E,情况就比较复杂

首先,E为一个无符号整数(unsined int )

这意味着如果E为8位,它的取值范围为0~255;如果E为11为,它的取值范围为0~2047,但是我们知道,科学计算法中的E是可以取出

现负数的,所以IEEE754规定,存入内存时E的真实值必须加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023,比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001.

然后指数E从内存中取出还可以再分成三种情况:

E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,在将有效数字M前面加上第一位的1.

比如:

0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),七阶码值为-1+127=126表示为

011111110,而尾数1.0去掉整数部分为0,补齐0到23位000000000000000000000000,则其二进制表示形式为:0  011111110  000000000000000000000000

E为全0

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,

有效数字M不再加上第一位的1,而是还原为0.xxxxxxx的小数,这样是为了表示-+0,以及接近于0的很小的数字。

E为全1

这时,如果有效数字M全为0,表示+-无穷大(正负取决于符号位S);

放一个整数9在整形n里,再类型转换为float*类型取地址放进float*pFloat里是

以0 000000000 00000000000000000000101

  S     E                  M

的形式存进来的

E在内存中为全0这时E等于1-127即为真实值有效数M不再加上第一位的1而是还原为0.XXXXX成了一个很小的小数

%f打印范围没有这么小就没打印出来

9.0存进*pFloat是以0 100000001 000100000000000000的形式

以%d打印0 100000001 00010000000000000000的值也就是09567616

把9.0小数存进*pFloat里后面*pFloat打印出来自然也是9.0

#include<stdio.h>

int main() {
	int n = 9;
	float* pFloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	*pFloat = 9.0;
	printf("num的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
}

五.分支

 一.if else函数

1.单分支:

if要条件为真(True)才执行为假(False)不执行而且if只能执行后面第一条如果要执行多条就得用{}包起来

下面if的判断条件是a等于2就为真执行打印yes否则为假不执行直接跳过printf执行下一条语句

0表示假非0表示真

#include<stdio.h>
int main()
{
    int a=1;
    if(a==2)
        printf("yes");
}

下面的代码是输入一个数如果小于5为真执行if{}里的俩条printf语句如果大于或等于5就不执行if{}里的语句直接跳过

#include<stdio.h>
int main()
{
    int a;
    scanf("%d",&a);
    if(a<5)
    {
        printf("yes\n");
        printf("yes\n");
    }
}

2.多分支:

这段代码是判断输入的a是否为1,如果是1为真就只执行printf("1");不执行printf("a不等于1");如果输入的数不是1就不执行if里的只执行else里的内容,else是后离他最近的if匹配的

#include<stdio.h>
int main()
{
    int a;
    scanf("%d",&a);
    if(a==1)
    {
        printf("1");
    }
    else 
    {
        printf("a不等于1");
    }
}

代码风格很重要比如这个和小他下面的就有区分if else匹配的难度了可以多用{}

#include<stdio.h>
int main()
{
    int a=0;
    int b=0;
    if(a==2)
    {
        if(b==2)
        {
        printf("a,b的值都是2");
        }
    }
    else
    {
        if(b==2)
        {
        printf("只有b是2");
        }
        else
        {
          printf("都不等于2");
        }
    }
}
#include<stdio.h>
int main()
{
    int a=0;
    int b=0;
    if(a==2)
        if(b==2)
        printf("a,b的值都是2");
    else
        if(b==2)
        printf("只有b是2");
        else
          printf("都不等于2");
        
}

下面程序执行后输入1就执行if(a==1)的语句并不再执行他的其他分支else if  ,a为2就只执行else if(a==2)里的内容并不再继续执行后面的分支 ,如果a不等于1,2,3就执行else里的内容

当然也可以把最后一个else不要

#include<stdio.h>
int main()
{
    int a;
    scanf("%d", &a);
    if (a == 1)
    {
        printf("1");
    }
    else if (a == 2)
    {
        printf("2");
    }
    else if (a == 3)
    {
        printf("3");
    }
    else
    {
        printf("没得");
    }
}

分支语句中也可以嵌套分支比如:

#include<stdio.h>
int main()
{
    int a, b, c;
    scanf("%d%d%d", &a, &b, &c);
    if (a <= 10)
    {
        if (b <= 10)
        {
            if (c <= 10)
            {
                printf("a,b,c中全是小于等于10的值");
            }
            else
            {
                printf("a,b,c中有2个小于等于10的值");
            }
        }
        else
        {
            printf("a,b,c中有1个上小于等于十的值");
        }
    }
    else
    {
        printf("a,b,c中一个以上大于等于10的值");
    }
}

二.switch语句

switch语句也是一种分支语句,常常用于多分支情况:

比如:

  • 输入1,输出星期一
  • 输入2,输出星期二
  • 输入3,输出星期三
  • 输入4,输出星期四
  • 输入5,输出星期五
  • 输入6,输出星期六
  • 输入7,输出星期七
  • switch语句一般是从匹配case的地方开始执行
  • break;是用来跳出语句的如果没有break;则会以只从开始执行的语句执行下去知道遇到break;或者执行完
#include<stdio.h>
int main()
{
    int day=0;
    scanf("%d",&day);
    switch(day)
    {
        case 1:
            printf("星期一\n");
            break;
       case 2:
            printf("星期二\n");
            break;
       case 3:
            printf("星期三\n");
            break;
       case 4:
            printf("星期四\n");
            break;
       case 5:
            printf("星期五\n");
            break;
       case 6:
            printf("星期六\n");
            break;
       case 7:
            printf("星期七\n");
            break;
    }
}

如果表达式的值与所有的case标签的值都不匹配也就是所有的语句都被条过而已,但是如果你并不想忽略不匹配所有标签的表达式的值可以再语句列表中增加一条defaul语句当switch表达式不匹配所有的case标签的值是这个default就会执行所以每个switch语句中只能出现一条default但是他可以出现在switch里的列表的任何位置,switch允许嵌套。

#include<stdio.h>
int main()
{
    int n=1;
    int m=2;
    switch(n)
    {
    case 1:
        m++;
    case 2:
        n++;
    case 3:
        switch(n)
        {
        case 1:
            n++;
        case 2:
            m++;
            n++;
            break;
        }
    case 4:
        m++;
        break;
    default:
        break;
    }
    printf("m=%d,n=%d\n",m,n);
}

六.循环

 一.while , do while函数 

如果要打印1-100的数字用可以用while来执行流程是这样的先判断成立进去执行里面的代码执行完之后就回到while这继续判断如果成立继续进去不成立跳过这端语句

#include<stdio.h>
int main()
{
    int i = 1;
    while (i <= 100)
    {
        printf("%d ", i);
        i = i + 1;
    }
}

在while语句中的break的使用:下面的带码只会打印1,2,3,4因为到5时就会到if里的break那退出了后面的内容不会执行。

#include<stdio.h>
int main()
{
    int i=1;
    while(i<=10)
    {
    if(i==5)
    {
      break;
    }
    printf("%d ",i);
    i=i+1;
    }
}

continue的介绍:执行到continue里就会直接结束本次循环回到while里重新判断后面的代码依然不会执行,这个成是进入之后i的值先加1后判断i==5,直到i等于5进入执行continue后会不执行后面的代码直接回到循环判断那所只打印2,3,4,6,7,8,9,10。

#include<stdio.h>
int main()
{
    int i=1;
    while(i<=10)
    {
        i=i+1;
        if(i==5)
        {
            continue;
        }
        printf("%d ",i);
    }
}

do while是先进入执行执行完了再判断如果成立就继续执行否则直接退出循环,执行流程是这样的:

#include<stdio.h>
int main()
{
    int i=1;
    do
    {
        printf("%d ",i);
        i++;
    }while(i<=10);
}

break和continue在do while的应用:这里是打印了1,2,3,4,6,7,8,9因为print在break前面所已有9。

#include<stdio.h>
int main()
{
    int i=1;
    do
    {
    if(i==5)
        continue;
    printf("%d",i);
    if(i==9)
        break;
    }while(i<=10);
}

二.for函数

for(表达式1;表达式2;表达式3),表达式1为初始化部分用于初始化循环变量的,表达式2是判断条件部分用于判断循环时候终止,表达式3为调整部分,用于循环条件的调整,使用for在屏幕上打印1-10的数字是这样写的,执行流程是这样的。

#include<stdio.h>
int main()
{
    int i=0;
    for(i=1;i<=10;i++)
    {
    printf("%d ",i);
    }
}

对比for循环和while循环:可以发现while循环中依然存在循环的三个必须条件,但是由于风格问题使得三个部分可能很偏离较远查找修改就不够集中和方便所以,for循环的风格更胜一筹;for使用的频率也最高。

#include<stdio.h>
int main()
{
    int i=0;
    i=1;//功能相同
    while(i<=10)//判断部分
    {
        printf("hehe\n");
        i=i+1;//调整部分
    }
    //使用for实现了相同的功能
    for(i=1;i<=10;i++)
    {
    printf("hehe\n");
    }
}

break和continue在for循环中的使用:他们的意义和在while循环中是一样的,但是因为for的写法不同所以使用中和while中使用还是有区别的,break是打印到5就结束了,continue是不打印5其他的都打印。

#include<stdio.h>
int main()
{
    int i=0;
    for(i=1;i<=10;i++)
    {
        if(i==5)
            break;
        printf("%d ",i);
    }
}
#include<stdio.h>
int main()
{
    int i=0;
    for(i=1;i<=10;i++)
    {
    if(i==5)
       continue;
    printf("%d ",i);
    }
}

for循环的初始化部分,判断,调整部分是可以省略的但是不建议初学时省略,容易导致问题

三.goto语句

C语言提供了可以随意滥用的goto语句和标记跳转标号,goto语句最常用在嵌套结构过深的程序中,如依次跳出多层循环,跳出多层循环使用break是达不到目的,他只能从最内层循环退到上一层的循环,下面使用goto语句的一个例子,一个关机程序。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
    char input[10] = { 0 };
    system("shutdown -s -t 60");
again:
    printf("电脑将在1分钟内关机,如果输入:我是猪,就取消关机!\n请输入:>");
    scanf("%s", input);
    if (0 == strcmp(input, "我是猪"))
    {
        system("shutdown -a");
    }
    else
    {
        goto again;
    }
}

四.猜数字:

是用do while来反复游玩 switchcase选择是玩还是不玩  if来判断输入  rand和srand随机生成数后余100就得出了100以内的随机数

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
    int input = 0,random_num = rand() % 100 + 1;
    srand((unsigned)time(NULL));
    do
    {
        printf("**********************\n");
        printf("******  1.play   *****\n");
        printf("******  0.exit   *****\n");
        printf("**********************\n");
        printf("请选择>:");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            input = 0;
            while (1)
            {
                printf("请输入猜的数字>:");
                scanf("%d", &input);
                if (input > random_num)
                {
                    printf("猜大了\n");
                }
                else if (input < random_num)
                {
                    printf("猜小了\n");
                }
                else
                {
                    printf("恭喜你,猜对了\n");
                    break;
                }
            }
        case 0:
            break;
        default:
            printf("选择错误,请重新输入!\n");
            break;
           
        }
    } while (input);
}

七.运算符

操作符分类:

  1. 算数操作符
  2. 移位操作符
  3. 位操作符
  4. 赋值操作符
  5. 单目操作符
  6. 关系操作符
  7. 逻辑操作符
  8. 条件操作符
  9. 逗号表达式
  10. 下标引用,函数调用和结构成员

一.算数操作符

+(加法操作)         -(减法操作符)         *(乘法操作符)        /(除法操作符)        %(取余操作符)

除了%操作符之外,其他的几个操作符可以作用于正数和浮点数。

对于/操作符如果两个操作符都为整数,执行整数除法,而只要有浮点数执行除法就是浮点数除法。

%操作符的两个操作数必须为整数。返回的是整除之后的余数。

#include<stdio.h>
int main() {
	int a = 10, b = 15;
	float c = 1.5;
	int d = a + b;
	printf("%d\n", d);
	d = b - a;
	printf("%d\n", d);
	d = b / a;
	printf("%d\n", d);
	d = a % b;
	printf("%d\n", d);
}

d08ce0221e3e42778a42f7fec6ac7f81.png

 这样的写法是错误的:

29f8bf8debc441ae8b7631e0b76a3914.png

 49993d9d328c42f1b817462ffbe25e7f.png

二.位移操作符

左移操作符:<<     左边抛弃,右边补0。

5302996f2ccb4a3b9a20347a7c40a325.png

右移操作符:>>   逻辑位移:左边用0填充,右边丢弃。

                              算数移位:左边有原该值的符号位填充,右边丢弃。

对于位移运算符,不要移动负数位,这个是标准定义的:

int  num=10;

num>>-1;        //error

d0e3396e27774c7ab30c76734949441d.png

三.位操作符

&                //按位与

|                //按位或

^                //按位异或

他们的操作数必须是整数。

#include<stdio.h>
int main() {
	int num1 = 2;
	int num2 = 3;
	printf("%d\n", num1 | num2);
	printf("%d\n", num1 & num2);
	printf("%d\n", num1 ^ num2);
}

d461832561ce401f9b7d817d45ab0b69.png

 下面有几种计算一个整数中二进制的个数的方法。

#include<stdio.h>
int main() {
	int num = 10;
	int count = 0;
	while (num) {
		if (num % 2 == 1) {
			count++;
		}
		num = num / 2;
	}
	printf("二进制中1的个数=%d\n", count);
}

46f6b7f9743d4736baba2c426f777772.png

#include<stdio.h>
int main() {
	int num = -1;
	int i = 0;
	int count = 0;
	for (i = 0; i < 32; i++) {
		if (num & (1 << i)) {
			count++;
		}
	}
	printf("二进制中1的个数=%d\n", count);
}

fde11fff96a34323b0254cffb1350eba.png

这个程序是利用了数据类型在存储中的特性。

#include<stdio.h>
int main() {
	int num = -1;
	int i = 0;
	int count = 0;
	while (num) {
		count++;
		num = num & (num - 1);
	}
	printf("二进制中1的个数=%d\n", count);
}

035e5cb4affd46bf9c340410f1a2f257.png d2a7ac92a926424aab6c476707820069.png

四.赋值操作符

赋值操作符他可以让你对不满意的值,进行重新赋值比如:

  • int weight=120;                  //体重
  • weight=89;                        //不满意就赋值
  • double salary=10000.0;
  • salary=20000.0;                //使用赋值操作符赋值

赋值操作符可以连续使用:

  • int a=10;
  • int x=0;
  • int y=20;
  • a=x=y+1;                  //连续赋值

复合赋值符:

  • +=        -=        *=        /=        %=        >>=        <<=        &=        |=        ^=
  • 可以这样写:
  • int x=10;
  • x=x+10;
  • x+=10;        //复合赋值

可以用复合赋值符写这几种代码:

#include<stdio.h>
int main() {
	int i = 0;
	while (i < 100) {
		printf("%d\n", i);
		i += 1;
	}
}

d72dda59780d4648bf205b4e6752fdf9.png

#include<stdio.h>
int main() {
	int i = 10;
	int count = 0;
	while (i) {
		if (i % 2 == 1) {
			count++;
		}
		i /= 2;
	}
	printf("i中二进制1的个数是%d", count);
}

abf780f0087c4ff39bdf221ffb707cd9.png

五.单目操作符

  • !                        逻辑反操作
  • -                        负数
  • +                       正数
  • &                       取地址
  • sizeof                操作数的类型长度(以字节为单位)
  • ~                       对一个数的二进制按位取反
  • --                       前置,后置--
  • ++                     前置,后置++
  • *                        间接访问操作符(解引用操作符) 
  • (类型)               强制类型转换

#include<stdio.h>
int main() {
	int a = -10;
	int* p = NULL;
	printf("%d\n", !2);
	printf("%d\n", !0);
	a = -a;
	p = &a;
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(int));
	printf("%d\n", sizeof(a));
}

275d4f81e91d476297cae9b4d8c12eed.png

sizeof和数组。

1414f1888b7747e0ae06a682ede837a9.png

前置和后置--,++的区别:前置

6f84699fa3af4831bdf424f337327a15.png

 后置:

806c78214b444c4dac340b9be544ff6b.png

六.关系操作符

>

>=

<

<=

!=         用于测试“不相等”

==        用于测试“相等”

bc1ec847944f4f08804a2ffbb01a4b40.png

七.逻辑操作符

&&        逻辑与:两个为真才为真,否则都为假。

| |          逻辑或:有真就为真,都为假才为假。

1c7487b6c07c48b28ca96837b37d283c.png

八.条件操作符

exp1?exp2:exp3        exp1语句成立就为exp2不成立就为exp3

99372fd5f0ad45968492952b53daa2f9.png

九.逗号表达式

逗号表达式,就是用逗号隔开的多个表达式。

逗号表达式,从左向右依次执行,整个表达式的结果是最后一个表达式的结果。

exp1,exp2,exp3.......expN

c4095821fc5b473ea6a96cb2afe06cd1.png

十.下标引用,函数调用和结构成员

[ ]下标引用操作符

操作数:一个数组名+一个索引值

int arr[10];        //创建数组

arr[9]=10;        //实用下标引用操作数。

[ ]的两个操作数是arr和9.

这里使用了i访问了arr的每一个数。

d72fa81122fe4d0db1e19cdc562a755d.png

()函数调用操作符

  接受一个或多个操作数:第一个操作符是函数名,剩余的操作数就是传给函数的参数。

b34091b83da64c27a95362199fc194b6.png

 .         结构体.成员名

->        结构体指针->成员名

#include<stdio.h>
struct Stu
{
	char name[10];
	int age;
	char sex[5];
	double score;
};
void set_age1(struct Stu stu)
{
	stu.age = 18;
}
void set_age2(struct Stu* pStu)
{
	pStu->age = 18;//结构成员访问
}
int main()
{
	struct Stu stu;
	struct Stu* pStu = &stu;//结构成员访问
	stu.age = 20;//结构成员访问
	set_age1(stu);
	pStu->age = 20;//结构成员访问
	set_age2(pStu);
	return 0;
}

十一.表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定的。

同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。

隐式转换:

C的整形算术运算总是至少以缺省整形类型的精度来进行的。

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整形,这种转换称为整形提升。

整形提升的意义:

    表达式的整形运算要在CPU的相应运算器内执行,CPU内整形运算器(ALU)的操作数的字节长度一般是int的字节长度,同时也是CPU的通用寄存器的长度。

    因此,即使两个char类型的相加,在CPU执行时实际上也要转换为CPU内整形操作数的标准长度。

     通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整形值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

实例:

char a,b,c;

....

a=b+c;

b和c的值被提升为普通整形,然后再执行加法运算。

加法运算之后,结果被截断,然后再存储于a中。

整形提升是按照变量的数据类型的符号位来提升的:

//负数的整形提升:

char c1=-1;

变量c1的二进制(补码)中只有8个比特位;

1111111

因为char是有符号的char

所以整形提升的时候,高位补充符号位,即为1

提升之后的结果是:

1111111111111111111111111111111

//整数的证书提升:

char c2=1;

变量c2的二进制位(补码)中只有8个比特位:

00000001

因为char是有符号的char

所以整形提升的时候,高位补充符号位,即为0

提升之后的结果是

0000000000000000000000000001

//无符号整形提升,高位补0

下面代码中a,b要进行整形提升,但是c不需要整形提升

a,b整形提升之后变成了负数,所以表达式a==0xb6,b==0xb600的结果是假,但是c不发生整形提升,则表达式c==0xb60000000的结果是真。

所以程序输出的是:c

#include<stdio.h>
int main()
{
    char a = 0xb6;
    short b = 0xb600;
    int c = 0xb60000000;
    if (a == 0xb60)
        printf("a");
    if (b == 0xb600)
        printf("b");
    if (c == 0xb6000000)
        printf("c");
}

实例:

c只要参与了表达式运算就会发生整形提升,表达式+c,就会发生提升,所以sizeof(+c)是4个字节。

 表达式-c也会发生整形提升,所以sizeof(-c)是4个字节,但是sizeof(c)就是1个字节。

#include<stdio.h>
int main()
{
    char c = 1;
    printf("%u\n", sizeof(c));
    printf("%u\n", sizeof(+c));
    printf("%u\n", sizeof(-c));
}

算数转换:

    如果某个操作符的各个操作数不属于不同类型,难么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行,下面的层次体系称为寻常算术转换。

long double 

double 

float

unsigned long int

long int 

unsigned int

int

   如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另一个操作数的类型执行运算。

但是算术转换要合理,要不然会有一些潜在的问题。

float  f=3.14;

int num=f;//隐式转换,会有精度丢失

操作数的属性:

1.操作符的优先级

2.操作符的结合性

3.是否控制求值顺序。

      相邻的操作符先执行的因素是取决于他们的优先级,如果两者的优先级相同,取决于他们的结合性

操作符优先级可以参考这里:C++ 内置运算符、优先级和关联性 | Microsoft Learnhttps://learn.microsoft.com/zh-cn/cpp/cpp/cpp-built-in-operators-precedence-and-associativity?view=msvc-170

一些问题表达式

//表达式的求值部分由操作符的优先级决定。

a*b+c*d+e*f

//代码1在计算的时候,由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行

所以表达式的计算顺序是:

  1. a*b
  2. c*d
  3. a*b+c*d
  4. e*f
  5. a*b+c*d+e*f

或者:

  • a*b
  • c*d
  • e*f
  • a*b+c*d
  • a*b+c*d+e*f

表达式:

c+--c;

//同上,操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。

非法 表达式,这种表达式在不同的编译器有不同的结果

int main()

{

        int i=10;

        i=i-- - --i*(i=-3)*i++ + ++i;

        printf("i=%d\n",i);

}

代码:这个 代码虽然在大多数的编译器上求得结果都是相同的,但是上述代码answer=fun()-fun()*fun();中我们只能通过操作符的优先级得知:先算乘法,再算减法,函数的调用先后顺序无法通过操作符的优先级确定。

int fun()

{
        static int count =1;

        return ++count;

}

int main()

{
        int answer;

        answer=fun()-fun()*fun();

        printf("%d\n",answer);

}

总结:

我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。

八.函数

 一.函数是什么

维基百科中对函数定义是:子程序

 在计算机中,子程序(英语:Subroutine,procedure,funcion,routinemethod,subprogram,callable unit),是一个大型程序中的部分代码,由一个或多个语句块组成,他负责完成某项特定的任务,而且相较于其他代码,具备相对的独立性。

一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏,这些代码通常被集成为软件库。

二.库函数

我们在学习C语言的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看,这个时候我们会频繁使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。

在编程的过程中我们会频繁的做一些字符的拷贝工作(strcpy)。

在编程中我们也计算,总是会计算,n的k次方这样的运算(pow)。

像上面我们描述的基础功能,他们不是业务性的代码,我们在开发的过程中每个程序都可能

用到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开饭

这里我们简单看看:www.cplusplus.com

 使用库函数必须包含#include对应的头文件,

学会查询工具的使用:

www.cplusplus.com

http://en.cppreference.com(英文版)

http://zh.cppreference.com(中文版)

三.自定义函数

因为库函数不能干所有的事。所以有自定义函数,自定义函数和库函数一样,有函数名,返回值类型和函数参数,但是这些都是我们自己来设计的。

函数的组成:

ret_type  fun_name(paral, *)

{

statement;//语句项

}

ret_tupe     返回类型

fun_name  函数名

paral          函数参数

比如可一写一个能找出两个整数的最大值的函数

#include<stdio.h>
int get_max(int x, int y)
{
	return (x > y) ? (x) : (y);
}
int main()
{
	int num1 = 10;
	int num2 = 20;
	int max = get_max(num1, num2);
	printf("max=%d\n", max);
}

还可一写一个交换两个整型变量的函数:第Swap1不能实现,swap2却可以,这是因为Swap1接收到的是形参,而swap2是接受到的是实参

#include<stdio.h>
void Swap1(int x, int y)
{
    int tmp = 0;
    tmp = x;
    x = y;
    y = tmp;
}
void swap2(int* px, int* py)
{
    int tmp = 0;
    tmp = *px;
    *px = *py;
    *py = tmp;
}
int main()
{
    int num1 = 1;
    int num2 = 2;
    Swap1(num1, num2);
    printf("Swap1::num1=%d,num2=%d\n", num1, num2);
    swap2(&num1, &num2);
    printf("Swap2::num1=%d,num2=%d\n", num1, num2);
}

四.函数参数

传值调用:

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。

传址调用:

传值调用是把函数外部创建变量的内存地址传给函数参数的一种调用函数的方式。

这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

实际参数(实参):

真实传给函数的参数,叫实参。

实参可以是:常量,变量,表达式,函数等。

无论实参是何种类型的量,在进行函数调用时,他们都必须有确定的值,以便把这些值传给形参。

形式参数(形参):

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数,形式参数当函数调用完之后就自动销毁了,因此形式参数只在函数中用效。

这里的Swap1没有改变num1和num2的值是因为他们拿到的是num1和num2的值而已交换值的时候是交换x,y空间里的值,对num1,num2空间里的值并没有影响。

swap2改变了num1和num2的值是因为px,py拿到的值是num1,num2的地址改变*px或*py的值等于改变num1,num2的值因为他们可以通过地址找到num1,num2的空间所以*px,*py等于num1,num2,px,py是改变他们自己空间里的值。

五.函数的嵌套调用和链式访问

函数和函数之间可以根据实际需求进行组合的,也就是相互调用的,称为嵌套调用,

函数可以嵌套调用不能嵌套定义。

这里的void是没有返回值,可以接收参数。

#include<stdio.h>
void new_lien()
{
    printf("hehe\n");
}
void three_lien()
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        new_lien();
    }
}
int main()
{
    three_lien();
        return 0;
}

把一个函数的返回值作为另一个函数的参数,这样叫做链式访问。

strlen是用来计算字符串的长度的,strcat是把bit这个字符追加到arr这给数组里的。

所以计算出来的答案是8

#include<stdio.h>
#include<string.h>
int main()
{
    char arr[20]="hello";
    int ret=strlen(strcat(arr,"bit"));
    printf("%d\n",ret);

    return 0;
}
   

六.函数的声明和定义

函数声明:

告诉编译器有这个函数他叫什么,参数是什么,返回类型是什么,但是存不存在,函数声明绝地不了。

函数声明一般出现在函数使用前,要满足先声明后使用,因为函数声明放在函数后面就没用意义了。

函数声明一般放在头文件中的。

函数定义:

函数定义是指函数具体实现。

int Add(int x,int y);//函数声明

int Add(int x,int y)//函数实现

{
return x+y;

}

七.函数递归

程序调用自身的编程技巧称为递归(recursion)。

递归做为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相识的规模较小的问题来求解,递归策略只需要少量的程序就可以描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

递归必须要这两个条件:

存在限制条件,当满足这个条件的时候,递归便不再继续。

每次递归调用之后越来越接近这个限制条件。

比如按顺序打印每一位:

输入1234,输出1 2 3 4,

#include<stdio.h>
void print(int n)
{
    if (n > 9)
    {
        print(n/10);
    }
    printf("%d ", n % 10);
}
int main()
{
    int num = 1234;
    print(num);
    return 0;
}

编写函数不允许创建临时变量,求出字符串的长度。

#include<stdio.h>
int Strlen(const char* str)
{
    if (*str == '\0')
        return 0;
    else
        return  1+Strlen(str + 1);
}
int main()
{
    char* p = "abcdef";
    int len = Strlen(p);
    printf("%d\n", len);
}

n的阶乘(不考虑益出)

int factorial(int n)
{
    if(n<=1)
        return 1;
    else
        return n*factorial(n-1);
}

在调试fib函数的时候,如果参数比较大,那就会报错:stack overflow(栈溢出)这样的信息。

int count=0;
int fib(int n)
{
    if(n=3)
        count++;
    if(n<=2)
        return 1;
    else
        return fib(n-1)+fib(n-2);
}

系统分配给程序的空间是有限的,但是出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生空间耗尽的情况,这样的想象我们称为栈溢出。

将递归改成非递归就行了。

使用static对象代替nonstatic局部变量,在递归函数设计中,可以使用static对象代替nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回是产生的释放nonstatic对象的开销,而且static对象还可以保存递归调用时的中间状态,并且可为各个调用层所访问。

比如下面就拆用了,非递归的方式来实现

许多问题是以非递归的形式进行解释的,这只是因为它比非递归的形式更为清新。

但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍差些。

当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁便可以补偿它所带来的运行时开销。

//求n的阶乘
int factorial(int n)
{
    int resule = 1;
    while (n > 1)
    {
        resule *= n;
        n -= 1;
    }
    return resule;
}
//求第n个斐波那契数
int fib(int n)
{
    int result;
    int pre_result;
    int next_older_result;
    result = pre_result = 1;
    while (n > 2)
    {
        n -= 1;
        next_older_result = pre_result;
        pre_result = result;
        result = pre_result + next_older_result;
    }
    return result;
}

九.数组

 一.一维数组

1.一维数组的创建:

数组是一组相同类型元素的集合

数组的创建方式:

type_t       arr_name [const_n];

//type_t 是指数组的元素类型

//const_n  是一个常量表达式,用来指定数组的大小

数组创建的实例:

  • int arr[10];
  • int cont=10;
  • int arr2[count];        //这样数组可以正常创建吗
  • char arr3[10];
  • float arr4[1];
  • double arr5[20];

数组创建,在C99标准之前,[]中要给一个常量才可以,不能使用变量,在C99标准支持了变长数组的概念,数组的大小可以使用变量指定,但是数组不能初始化

2.一维数组的初始化:

数组初始化是指在创建数组的同时给数组的内容一些合理初始值(初始化)。

  • int arr[10]={1,2,3};
  • int arr2[]={1,2,3,4};
  • int arr3[5]={1,2,3,4,5};
  • char arr4[3]=['a',98,'c'};
  • char arr5[]={'a','b','c'};
  • char arr6[]="abcdef"'

数组在创建的时候如果不想指定数组的却低的大小就得初始化,数组的元素个数根据初始化内容来确定

但是对于下面的代码要区分,内存中如何分配。

  • char arr1[]="abc";
  • char arr2[3]={'a','b','c'};

3.一维数组的使用

#include<stdio.h>
int main()
{
    int arr[10]={0};//数组的不完全初始化
    //计算数组的元素个数
    int sz=sizeof(arr)/sizeof(arr[0]);
    //对数组内容赋值,数组是使用下标来访问的,下标从0开始,所以:
     int i=0;
    for(i=0;i<10;i++)
    {
        arr[i]=i;
    }
    for(i=0;i<10;++i)
    {
        printf("%d ",arr[i]);
    }
}

数组是使用下标来访问的,下标是从0开始。

数组的大小可以通过计算得到。

4.一维数组在内存中的存储:

#include<stdio.h>
int main()
{
    int arr[10]={0};
    int i=0;
    int sz=sizeof(arr)/sizeof(arr[0]);
    for(i=0;i<sz;++i)
    {
        printf("&arr[%d]=%p\n",i,&arr[i]);
    }
}

这里的地址随着下标的增长而以类型的大小增长可以得出:数组在内存中是连续存放的。

二.二维数组

二维数组的创建:

  1. int arr[3][4];
  2. char arr[3][5];
  3. double arr[2][4];

二维数组的初始化:

  1. int arr[3][4]={1,2,3,4};
  2. int arr[3][4]={{1,2},{4,5}};
  3. int arr[][4]={{2,3},{4,5}};二维数组初始化时 ,行可以省略,列不能省略

二维数组也是通过下标使用的:

这里是创建了一个3行每行有4个元素的数组并使用i代表行j代表列依次给arr的每个空间赋值并打印出来。

  • arr[3][4]:
  • arr[0][0]=0        arr[0][1]=1       arr[0][2]=2       arr[0][3]=3 
  • arr[1][0]=4        arr[1][1]=5       arr[1][2]=6       arr[1][3]=7 
  • arr[2][0]=8        arr[2][1]=9       arr[2][2]=10     arr[2][3]=11 

三.数组越界

数组的下标是有范围限制的,

数组的下标规定是从0开始的,如果有n个元素最后一个下标就是n-1.

所以数组的下标如果小于0,或者大于n-1,就是越界访问了,超出了数组合法空间的访问,

C语言本身是不会对数组下标越界检查的,编译器也不一定报错,但是编译器不报错,并不意味着程序就是对的,所以写代码的时候要自己做检查,二维数组的行和列也可能存在越界。

arr[10]只有arr[0]到arr[9]并没有,这里打印出-858993460这片空间并没有值所以打印出来的是这个未知数,

四.数组作为函数参数

数组名是数组首元素地址,有两个例外:

{

      sizeof(数组名),计算整个数组的大小sizeof内部单独放一个数组名,数组名表示整个  数组。

     &数组名,取出的是数组的地址,&数组名,数组名表示整个数组。

}

除此两种之外所有的数组名都表示数组首元素地址。

 知道啦上面的就可以做一个冒泡排序了:
同得出的大小在把arr的首地址传和arr的大小传给bubble_sort函数在函数内部就可以通过地址找到arr数组并对其进行排序,排序是是每个元素对他后面的每一个元素进行比如果比其大则两个元素交换位置,一直遍历完整个数组。

十.指针

 一.指针和指针类型

认识指针:

为了管理计算机的内存,内存会被划分为一个一个的内存单元,每个内存单元大小是一个字节,每一个字节都有一个编号也就是地址,也叫指针

对于32位的机器,假设每根地址线在寻找址的时候产生高电平(高点压)和低电平(低电平)就是(1或者0)

那么32根地址线产生的地址就会是

00000000000000000000000000000000

00000000000000000000000000000001

.....

111111111111111111111111111111111111

这里就有2的32次方个地址

每个地址标识一个字节,那我们就可以给(2^32Byte == 2^32/1024kB ==2^32/1024/1023MB == 2^32/1024/1024/1024GB == 4GB)4G的空间进行编址,同样64位的机器给64根地址线。

在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。

那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

指针变量是用来存放地址的, 地址是唯一标示一个内存单元的,指针的大小在32位平台式4个字节,在64位平台式8个字节

变量,数组等的创建都要在内存上开辟空间

如:int a=100;//就是在内存上创建了一个空间大小为4个字节,叫a,用来存放100

可以用pring("%p",&a);//这样打印a的地址%p就是以十六进制打印地址,&取出a的地址

指针是内存中一个最小单元的编号,也就是地址,平时用的指针,通常指的是指针变量,是用来存放地址的变量,存放在指针中的值都会被当成地址处理

有个int a = 0x11223344;变量它占4个字节,假设它的第一个字节的地址是0x0012ff40存放的是44,第二个是0x0012ff41存放的是33,第三个是0x0012ff42存放的是22,第二个是0x0012ff43存放的是11,这里倒着存的原因可以看:

(46条消息) C语言数据类型的存储_lh11223326的博客-CSDN博客https://blog.csdn.net/lh11223326/article/details/130926120?spm=1001.2014.3001.5501

&a是拿到的是他的第一个地址也就是0x0012ff40

int*pa = &a;//这里pa是一个指针变量用来存放a的地址

指针有不同的类型如,int*,float*,char*等等,sizeof函数可以用来查看大小,它们的大小都是由环境决定的在32位下大小都是4个字节,在64就是8个字节

int*的指针解引用访问4个字节,char*的指针解引用访问1个字节,指针类型可以决定指针解引用的时候访问多少个字节(指针的权限)

指针类型决定指针+-操作时的步长,整形指针+1跳过4个字节,字符指针+1跳过1个字节。

这里是先把a的地址放到int*pa指针变量里,pa是一个指针变量里面存放的是a的地址,解引用*pa就是a,所以修改*pa的值相当于修改a(解引用就是通过指针所指向的空间或者内容进行解引用,拿的的是这块空间里的值)。

二.野指针

野指针就是指针在使用前未初始化造成,指针的指向的位置是不可知的(随机的,不正确的,没有明确限制的),指针越界访问 ,指针所指向的空间释放了等等都会造成野指针

如下:pa是局部变量,局部变量不初始化的时候内容是随机值,这个pa我们称为它为野指针。

 如下,pa最先指向的的是arr第一个元素for每次循环完之后*(pa++)=i;加加后解引用,再用i赋值,每次pa++它的地址就往后走直到走到i=11时pa的指向就超出了arr的范围是arr[10]了因为只有arr[9]所以,指针就是越界访问了,指针的指向超出了arr的范围时,pa就是野指针。

 如下:因为a的空间是进入函数创建的出函数还给操作系统了,那个空间的值被a该变了变成了110但是p使用时a的空间并不存在了但是那个地址空间了的值已经被a该变了所以可以使用,所以当使用p时因为a的空间已经释放了,p指向的空间并没有,p是就一个野指针了。

把指针初始化,小心指针越界,指针指向空间释放及时置NULL(代表空),避免返回局部变量的地址,指针使用前检查有效性,可有效规避野指针。

三.指针运算

指针+-整数:

如下:创建了一个arr[10]的数组全部初始化为0,在把第一个元素的地址赋值给p指针变量,同时用sizeof计算出要改变的元素个数,使用for循环以0为开始赋值给p指向的地址的空间,赋值完之后p的指向往后移动int位就是4个字节,这样就给arr的每一个元素赋值完了。

因为数组名就是首元素的地址所以 arr[i] = *(arr+i) =*(i+arr) = i[arr](因为[]只是一个操作符而已就像2+3和3+2都一样,但是不推荐这样写)

指针-指针:得到的数组的绝对值才是它的之间的元素个数,指针-指针运算的前提是指针和指针指向同一块空间

下面得出的是9的原因是因为从arr[9]到arr[0]中间一共有9个元素。

 指针的关系运算:就是比较指针的大小

标准规定允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

四.指针和数组

  通过下面可以看见arr数组名和取地址arr的第一个元素的地址是相同的,因此数组名是首元素的地址arr就是int*类型,可以这样写int*p = arr;就是把数组的第一个元素的地址赋值给了p。

既然可以用数组名当做首元素的地址,那么就可以吧它存到指针里面,数组是在内存中连续存放的,也就可以通过 指针访问数组的每一个元素,如下:sz是计算数组元素的个数的,循环中arr的地址随着下标的变化逐渐以数组的自身的类型int大小而增大,p也是一样p+i的可以看做以p为起始地址加的i就是要跳过p类型int的大小,每次跳过的大小跟arr一样所以地址的增长和arr是一样的。

数组名是首元素的地址但是有两个例外:1.sizeof(数组名), 2.&数组名.

五.二级指针:

下面的程序是先创建一个a地址是0x005ffccc里面存放的值是10,然后把a的地址存放到一个int*一级指针变量p里面,p里面存放的是a的地址,解引用就是a的值,然后再把p存放到int**二级指针变量pp里面,pp面存放的是p的地址0X005ffcc0,而pp的地址是0X005ffcb4,pp打印就是p的地址,pp解引用一次*pp就是p存放的地址就是a的地址,pp解引用两次**pp就是a的值。

 当a的值等是10地址是0X00b6f804,p 的值是a的地址,p地址是0X006f78,*p是10,pp的值是0X006f78,*pp是0X00b6f804,**pp是10,时结果如下: 

六.字符指针

在指针的类型中我们知道有一种指针类型为字符指针char*,下面这段代码中char* p是一个字符指针存放的不是"abcdef"而是首字符a的地址,*p解引用就是a,因为它后面的字符是常量表达式不能被修改所以要加const。

 下面str1不等于str2虽然他存储的字符串相等他们是分别不同的空间(就跟你身上的100块和别人的100块)。str3等于str4的原因是他们指向的是同一个地址它们的值肯定是相同的。

七.指针数组

指针数组是一个存放指针的数组,如下

这里的arr是存放了arr1,arr2,arr3地址的指针数组,可以通过arr[0~2]访问arr1到arr3,然后再[0~4]访问arr每个元素里面的元素。

 指针数组是数组,是存放指针的数组,如下:把arr1,arr2,arr3的地址都存放在一个int*parr的数组里,parr就是指针数组,可以通过访问parr的下标得到arr1,arr2,arr3的地址,p是一个二级指针存放了parr的地址它当然也可以通过*的方式和改变指针指向来方位arr1,arr2,arr3,和parr

八.数组指针

数组指针是指针,比如整形指针是指向整形的变量的指针,存放整形变量的地址的指针变量,或字符指针是指向字符变量的指针,存放字符变量的地址的指针变量,存放什么就是什么类型的指针,数组指针是指向数组的指针,存放的是数组的地址的指针变量。

如下三个地址都是相同但是他们不是相同的类型,这是因为arr是首元素的地址也就是arr[0],&arr[0]是int*指向了一个int arr[0],最后一个是int*指向了整个数组的数组指针

他们各自加一的结果如下,因为arr是每个元素的地址所以arr+1之后的变换是加了一个int大小4个字节,因为&arr[0]中&代表的是整个数组[0]是其中一个数组的地址他本质还是int[]类型所以加了一个int 4个字节,因为&arr是整个数组的大小这个数组是int[10]所以它的大小是40&arr加1就是条过了整个数组也就是40个字节,(注)数组名只有两个情况不是首元素地址:1,sizeof(数组名),这里的数组名不是数组首元素地址,数组名表示整个数组,sizeof计算的是整个数组的大小,单位是字节,2.&数组名,这里的数组名表示整个数组,&数组名取出的是整个数组的地址,除此之外,所有的数组名都是数组首元素地址

数组指针如int(*p2)[10],p2首先和*结合成为指针,然后它指向一个int数组,所以p2是数组指针变量,数组指针可以这样使用,下面arr是数组数组名是表示数组首元素地址这个二维数组是以数组每一行为地址的所以就是第一行所有元素的地址,Print函数传参类型int(*p)[5]可以减去很多麻烦,其中p可以代表每一行解引用之后再加j可以拿出此行每个元素。

一维数组的传参形参的部分可以是数组也可以是指针,二维数组也一样,如下

九.数组参数和指针参数

一维数组传参:以下一维数组的参数可以用这几种方式接收如:

void test(int arr[]){}因为这不会真正的创建数组所以把数组大小省略了,所以可行

void test(int arr[10]){}这个也行如上大小也可以省略

void test(int *arr){}因为它拿的是首元素地址也行

void test2(int*arr[20]){}因为数组传参形参部分写成数组没有问题

void test2(int**arr){}因为arr2的每个元素都是int*一级指针所以可以用**arr来接收

二维数组传参:int arr[3][5]={0};有以下这几种方式要注意

void test(int arr[3][5]){}它与形参相同,所以可以使用

void test(int arr[][]){}因为它形参部分的行可以省略但是列不能省略,所以不行

void test(int arr[][5]){}因为二维数组是连续存放的如果没有列就不知道第二行方哪了,所以可以

void  test(int*arr){}因为它首元素的地址是一行的地址不是整个数组的地址,所以不行

void test(int*arr[5]){}它传来的数组名,只能是二维数组的形式,它的形参是指针数组,它只能是二维数组,或指针数组

void test(int(*arr)[]){}它指向每一行每行有5个所以可以

void test(int**arr){}他是二级指针所以不行

一级指针传参:比如可以用int*p来接收,当一个函数参数部分分为一级指针的时候,函数可以接收以下参数

二级指针传参:当函数为二级指针时可以接收的参数如下:

test1(&p); 中p原本就是一级指针了再取地址传的就是二级指针了,test1(pp);pp就是二级指针与形参匹配所以没有问题,test1(arr);是一级数组指针代表首元素地址也是没有问题

十.函数指针

函数指针就是指向函数的指针,如:int arr[10];int (*pa)[10]=&arr;这样就可以拿到数组的指针,函数指针也可以如下取出函数地址,&函数和函数都可以拿到函数地址

存储函数地址:函数指针和函数的格式很相似,如Add和pf,它们,返回类型 函数名(参数,参数),他们参数要相同返回类型也要相同,不同的是一个是函数指针一个是函数,这里*pf要用括号和*结合不然pf会先和()先结合他们类型就会不一样了,&Add和Add都可以拿出函数的地址

如函数类型是无类型的void参数里有数组或指针也可以这样写数组也可以不用写数字

函指针使用和函数调用差不多如:int (*pf)(int,int)=Add; Add是计算两值相加的和 m=pf(8,9);

如:有个代码是这样的  (*(void(*)() )0 )(); 这样的代码该如何解读,首先把里面的void (*) ()拿出来可看出来这是一个函数指针类型,外面的0可以看成一个int也可一个看成一个地址这里是地址个合理,如果0是地址那首先会将0强制类型转化为void(*)()类型函数指针然后解引用,再调用0地址处的这个函数因为最后那空号里是空的没用参数,

十一.函数指针数组

整形指针放在一个数组中,int*arr[5];整形指针数组,char*arr[5];字符指针数组,

函数指针数组:数组的每个元素是函数指针类型

下面创建了四个函数,Add,Sub,Mul,Div,又创建了四个函数指针用来存放这四个函数,这四个函数指针的类型,参数都一样,哪可以创建一个函数指针数组来存放这些函数指针,这个pfArr是函数指针数组,它先和方括号先结合它是个数组再和剩下的函数指针结合就是函数数组指针里面存放了Add,Sub,Mul,Div等函数的地址

函数指针数组的使用:下面使用函数指针数组实现了一个计算机功能,使用了数组坐标依次访问个个地址处的函数

指向函数指针数组的指针:比如函数指针是int(*pf)(int,int);,函数指针数组是int(*pfArr[4])(int,int);,函数指针数组的地址是int(*(*p)[4])(int,int)=&pfArr; p就是指向函数指针的指针,p先和*结合变成指针剩下的就是函数指针数组了

十二.回调函数

回调函数就是一个通过函数指针调用的函数,如果把函数的地址作为参数传递个另一个函数,当这个指针被用来调用其所指向的函数是,这个函数就是回调函数,回调函数不是由函数的实现方式直接调用,而是在特定的事件或条件所发生事由另外的一方调用的,用于对该事件的响应,

用计算机来实现回调函数,定义了Add,Sub,Mul,Div,分别用来实现加,减,乘,除,再使用一个Calc来调用这些函数,通过选择1,2,3,4然后将对应的函数地址传给函数指针pf,在用pf调用对应地址处的函数再把要计算的值传个对应地址位置的函数处理完之后返回值在输出,这就是对应事用对应功能

qsort函数可以实现各类型的排序   参数类型是

void qsort(void*base,     要排序的数组的第一个地址   

size_t num,                     要排序数组的大小

size_t size,                      要排序数组每个元素的大小

int(*compear)(const void*,const void*)                      计算前后大小进行排序

);

void*类型的指针可以接收任意类型的地址,这种类型的指针是不能直接解引用操作的,也不能直接进行指针运算,

十一.结构体

 一.什么是结构体

往往我们要定义一个人,比如它会有身高,体重,年龄等等各种不同的属性我们用int或者char都很难准确的描述一个人,所以就有了可以自定义的数据类型。

结构体(Struct)简单的讲是一种自定的数据类型,如int,float这种数据类型,但是结构体不同它更像这些类型的聚合体,之所以说结构体是自定义的数据类型是因为结构体可以跟int,float一样用来定义变量而且定义出来的变量的类型都是同一类型。

二.以及结构体的定义和使用

结构体的定义可以使用struct来定义。

定义的语法:

struct 结构体名{

        类型 变量名;

        类型 变量名;

};

结构体里面不能进行运算或者定义函数等操作只能用来定义变量,因为结构体只是一种自定义的数据类型它本质上还是数据类型并不是像函数那样的,所以只能在里面定义数据类型,比如数组,指针,整形,字符等等。实例如下:

struct stu {
	int a;
	int b;
};

如下是错误的写法:其中不能初始化,比如int里面可以初始化那就会导致定义数据的时候数据使用会出错,不能进行逻辑运算和不能定义函数是因为结构体是自定义类型它主要的本质还是类型并不是函数。 ​

知道了如何定义,那结构体如何使用呢,其实很简单只需要先定义一个结构体变量,语法如下:

struct 结构体名{

        类型 变量名;

        类型 变量名;

}结构体变量名;

还有使用结构体名进行定义变量的,语法如下:

struct 结构体名 结构体变量名;

实例代码如下:

#include<stdio.h>
struct stu {
	char* name;//姓名
	int age;//年龄
	char gender;//性别   等等
}stu1;
int main() {
	struct stu stu2;
}

我们知道定义了变量肯定就要赋值才能够使用,但是给结构体变量赋值和给其普通他变量的方式不同结构体变量可以可以在初始化阶段赋值,语法如:

struct 结构体名{

        类型 变量名;

        类型 变量名;

}结构体变量名={数据,数据};

也可以使用.号进行赋值语法如:

结构体变量名.对应的变量名=数据;

如下是代码示例:

struct stu {
    char* name;//姓名
    int age;//年龄
    char gender;//性别   等等
}stu1={"李四",18,'男'};//初始化时赋值
int main() {
    struct stu stu2;
    stu2.name = "张三";//初始化之后赋值
    stu2.name = 20;
    stu2.name = '男';
}

定义了结构体和给结构体赋值之后才可以使用结构体,使用结构体也是使用.号,语法如:

结构体名.变量名;

示例代码如下:

#include<stdio.h>
struct stu {
    char* name;//姓名
    int age;//年龄
    char gender;//性别   等等
}stu1 = { "李四",18,'男' };
int main() {
    struct stu stu2;
    stu2.name = "张三";
    stu2.age = 18;
    stu2.gender = '男';

    printf("%s和他的好兄弟%s都是%d岁了", stu1.name, stu2.name, stu1.age);
}

三.结构体数组

结构体数组跟int或者float的数组并没有太大的区别,每一个元素都是结构体也可以使用下标定位每一个结构体的位置,赋值的时的结构类似二维数组的赋值所以结构体又像一个可以存储不同类型的一维数组,初始化时赋值代码如下:

struct 结构体名{

        类型 变量名;

        类型 变量名;

}结构体变量名[3]={

{数据1,数据2},

{数据1,数据2}

};

定义好结构体变量之后赋值的代码:

结构体变量名[下标].变量名=数据;

结构体数组可以给数组长度也可不给数组长度,使用结构体数组也方法也很简单示例代码如下:

struct {
    char* name;//姓名
    int age;//年龄
    char gender;//性别
}group[2], group1[2] = { {"张三",20,'男'},
{"李四",20,'男'} };

int main() {
    int i;
    group[0].name = "小明";
    group[0].age = 18;
    group[0].gender = '男';
    group[1].name = "小红";
    group[1].age = 18;
    group[1].gender = '男';
    for (i = 0; i < 2; i++) {
        printf("%s的年纪是%d它的性别是%c", group[i].name, group[i].age, group[i].gender);
    }
}
 

四.结构体指针

结构体指针其实就是指向结构体的指针,跟int指针没啥区别都是用来指向对应类型的指针,使用结构体指针传递给函数比使用结构体传递给函数的效率更高,因为使用结构体传递给函数需要创建一块额外的空间而指针的大小始终如一并不占用太多的空间,结构体的指针定义如下:

struct 结构体名 *结构体指针名;

如果使用结构体指针,使用其中指向的变量则不能使用.号了而需要使用结构体指针专门的->用来代替.代码如下:

struct stu{
    char *name;//姓名
    int age;//年龄
};
int main(){
    struct stu*stup;
    struct stu stu1={"张三",20};
    stup=&stu1;
    printf("%s今年%d岁了\n",stup->name,stup->age);

}

十二.文件操作(输入输出流)

什么是文件

磁盘上文件是文件,但是在程序设计中,我们一般谈的文件有两种:程序文件,数据文件(从文件功能的角度来分类的):程序文件,数据文件

程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)

数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件

在C语言之前使用printf或者scanf的处理数据的时候都是以终端为对象的,即从终端的键盘输入数据,运行结构显示到显示器上,有时候我们灰白信息输出到键盘上,当需要的时候再从键盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件,一个文件要有一个唯一的标识符,以便用户识别和引用,文件名包含3部分,文件路径+文件主干+文件后缀如:c:/code/test.txt,为了方便起见,文件表示常被称为文件名。

为什么要使用文件来存储数据,在C语言中不是可以通过创建变量来存储数据,但是为什么还要使用文件来存储呢?因为C语言使用变量存储的数据下次不能再重复使用里面的数据而要解决这个问题就需要使用文件来存储数据了。

文件的打开和关闭

文件的打开:

文件指针,缓冲文件系统中,关键的概念是"文件类型指针",简称"文件指针"。

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等),这些信息是保存在一个结构体变量中,该结构体类型是由系统声明的,取名FILE,如下是VS2013编译环境提供的stdio.h头文件中有以下的文件类型申明:

#include<stdio.h>
struct _iobuf {
	char* _ptr;
	int _cnt;
	char* _base;
	int _flag;
	int _file;
	int _charbuf;
	int _bufsiz;
	char* _tmpfname;

};

typedef struct _iobuf FILE;

不同的编译器的FILE类型包含的内容不完全相同,但都大同小异。

每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关系细节。一般都是通过一个FILE指针来维护这个FILE结构的变量,这样使用起来更加方便,下面可以创建一个FILE*的指针变量:

FILE*pf;//文件指针变量

定义pf时一个指向FILE类型数据的指针变量,可以使pf指向某个文件的文件信息区(是一个结构体变量),通过该文件信息区中的信息就能够访问该文件,也就是说,通过该文件指针变量能够找到与它关联的文件。

文件的打开和关闭:

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。ANSIC规定使用fopen函数来打开文件,fclose来关闭文件。

打开文件:FILE*fopen(const char*filename,const char*mode);//其中mode的意思就是打开方式

关闭文件:int fclose(FILE*stream);

文件使用方式     含义                                                                       如果指定文件不存在
 "r"(只读)            为了输入数据,打开一个已经存在的文本文件      出错
 "w"(只写)           为了输入数据,打开一个文本文件                        建立一个新的文件
 "a"(追加)           向文本文件尾添加数据                                          建立一个新的文件
 "rb"(只读)          为了输出数据,打开一个二进制文件                    出错
 "wb"(只写)         为了输出数据,打开一个二进制文件                    建立一个新的文件
 "ab"(追加)         向一个二进制文件尾添加数据                               建立一个新的文件
 "r+"(读写)          为了读和写,打开一个文本文件                           出错
 "w+"(读写)        为了读和写,建议一个新的文件                            建立一个新的文件
 "a+"(读写)         打开一个文件,在文件尾进行读写                        建立一个新的文件
 "rb+"(读写)        为了读和写打开一个二进制文件                           出错
 "wb+"(读写)      为了读和写,新建一个新的二进制文件                 建立一个新的文件
 "ab+"(读写)      打开一个二进制文件,在文件尾进行读和写           建立一个新的文件

#include<stdio.h>
int main(){
    FILE*pf;
    //打开文件
    pf=fopen("data.txt","w");
    //判断是否打开
    if(pf!=NULL)
    {
        perror("fopen");
        return 1;
    }
    //关闭文件
    fclose(pf);
    pf=NULL;
    return 0;
}

打开文件的时候可以使用相对路径,一个点是打开当前目录下的文件,两个点事上个目录下的文件示例如:FILE*pf=fopen(".\\Debug\\data.txt","r");,其中只有一个点是本目录下的Debug文件里面的data.txt文件,打开方式为r只读。

绝对路径:打开文件也可以使用绝对路径比如:C:\Users|Administrator\Desktop\data.txt要使用绝对路径就要在\上面加一个\这样才能准确识别\

使用了fopen之后还要判断文件打开了的操作执行成功了没,如果没有就输出错误信息,然后结束程序,如果文件打开成功了那就可以使用了。

最后可以使用fclose关闭文件,然后再把文件指针置空NULL;

读写文件的时候,需要:1.打开文件。2.读写文件。3.关闭文件

使用scanf,printf进行输出输出并没有需要打开什么,因为C语言程序,只要运行起来,默认就打开3个流:

  1. 标准输入流:stdin FILE*
  2. 标准输出流:stdout FILE*
  3. 标准错误流:stderr FILE*

流:抽象的概念,水流,数据流,数据进行大规模的输入输出的时候,就像水流一样从一个地放流到另一个地方所以叫流

 功能                        函数名        适用于

  1.  字符输入函数         fgetc        所有输入流
  2.  字符输出函数         fputc        所有输出流
  3.  文本行输入函数     fgets        所有输入流
  4.  文本输出函数         fputs        所有输出流
  5.  格式化输入函数      fscanf      所有输入流
  6.  格式化输出函数      fprintf      所有输出流
  7.  二进制输入             fread       文件
  8.  二进制输出             fwrite       文件3.文件的顺序读写,按照顺序进行读与写:

文件的输入输出流

使用fputc写文件:

fputc有两个参数,一个是int类型,另一个是FILE*的文件指针类型,所以fputc要有传两个参数,一个是要输入的内容,一个是要输入的对应位置FILE指针。而里面的fputc既能进行输入操作又能进行输出操作这是因为一个是pf输入的位置是文件里,另一个是stdout标准输出流他是FILE*所以可以进行传值,stdout只是把要输入的内容输入到了终端上面,fputc的本质并没有被改变。

注意:打开文件要使用对应的方式,如下是只写所以使用w。

int fputc(int character,FILE*stream);

使用fgetc读文件:

fgetc的参数是要读取的对应的地址FILE*的地址所以可以使用流中的标准输入流,然后fgetc会返回一个int类型的数据,如果读取失败就返回EOF,下面的文件中abcd是提前保存的数据,控制面板中ab都是从data中读取的,而f则是使用stdin输入流输入的。

像上面那样输入输出的效率并不理想,所以就有了可以一次写一行可以读一行的函数,fputs和fgets,使用方法如下所示:

使用fputs写一行:

fputs接收两个参数一个是字符指针,另一个是FILE*文件指针,并且它不会自动换行,如要需要换行就需要加\n。

int fputs(const char*str,FILE*stream);​

使用fgets读一行:

fgets接收三个参数,一个字符指针用来存放返回的数据,一个int用来指明要读取数据的多少,还有一个拿数据的FILE*文件指针。

cahr*fgets(char*str,int num,FILE*stream);

使用fprintf写入:

FILE*指针是必填项不然就不知道去哪拿数据来输出,因为fprintf可以接收多个参数的原因是它的参数个数是可变的,而且格式如有几个%格式,后面就必须要有对应个数的数据。

int fprintf(FILE*stream,const char*format,....);

使用fscanf读取:

fscanf函数跟fprintf的参数类似只不过他们做的工作不同fprintf是把数据输入进文件夹,而fscanf则是拿出来然后存放(或者其他操作)。

int fscanf(FILE*stream,const char*format,....);

文件的随机读写:

fseek,ftell,rewind函数的使用:

fseek根据文件指针的位置和偏移量来定位文件指针。

int fseek(FILE*stream,long int offset,int origin);

当默认打开一个文件的时候文件指针是默认指向初始位置的也就是a前面的位置,下面的SEEK_SET是相对于当前指针位置的偏移量偏移5个数据刚好就是f,而SEEK_END则是相反位置的偏移量而-3就是往后走3个数据也就是f。

ftell返回文件指针相对于起始位置的偏移量,偏移量指的是文件指针走到第几个偏移量就是几。

long int ftell(FILE*stream);

使用ftell回到起始位置的代码如下:

rewind让文件指针的位置回到文件的起始位置。

void rewind(FILE*stream);

文本文件和二进制文件:

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。

如果要求在外存上以ASCII码的形式存储,则需要存储前转换,以ASCII字符的形式存储的文件就是文本文件。

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

如有整数10000,如果以ASCII的形式输出到磁盘,则磁盘中占用5个字符(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。

文件读取结束的判定

1.被错误使用的feof:

牢记:在文件读取过程中,不能用feof函数的返回值来判断文件的是否结束。

feof的作用是:当文件读取结束的时候,判断是读取结束的原始是否是:遇到文件尾结束。

2.文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets).如

fgetc判断是否为EOF,fgets判断返回值是否为NULL。

3.二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。如:fread判断返回值是否小于要读的个数。

如下例文件所示:

int main(void) {
	int c;//注意:int非char,要求处理EOF
	FILE* fp = fopen("test.txt", "r");
	if (!fp) {
		perror("File opening failed");
		return EXIT_FAILURE;
	}
	//fgetc当读取失败的时候或者遇到文件结束的时候,都会返回EOF
	while ((c = fgetc(fp)) != EOF) {//标准C I/O读取文件循环
		putchar(c);
	}
	//判断是什么原因结束的
	if (ferror(fp))
		puts("I/O error when reading");
	else if (feof(fp))
		puts("End of file reached successfully");
	fclose(fp);
}

二进制文件示例:

enum{SIZE=5};
int main(void) {
	double a[SIZE] = { 1.,2.,3.,4.,5. };
	FILE* fp = fopen("test.bin", "wb");//必须用二进制模式
	fwrite(a, sizeof * a, SIZE, fp);//写double的数值
	fclose(fp);
	
	double b[SIZE];
	fp = fopen("test.bin", "rb");
	size_t ret_code = fread(b, sizeof * b, SIZE, fp);//读double的数值
	if (ret_code == SIZE) {
		puts("Arry read successfully,contents:");
		for (int n = 0; n < SIZE; ++n)printf("%f", b[n]);
		putchar('\n');
	}
	else {//error handling
		if (feof(fp))
			printf("Error reading test.bin:unexpected end of file\n");
		else if (ferror(fp)) {
			perror("Error reading test.bin");
		}
	}
	fclose(fp);
}

文件缓冲区:

ANSIC标准采用"缓冲系统"处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中每一个正在使用的文件开辟一块"文件缓冲区",从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才能一起送到磁盘上,如果从磁盘向计算机读取数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等),缓冲区的大小根据C编译系统决定。

示例代码如下:

int main(){
    FILE*pf=fopen("test.txt","w");
    fputs("abcdef",pf);//先将代码放在缓冲区
    printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
    Sleep(1000);
    printf("刷新缓冲区\n");
    fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
    printf("再睡眠10秒-此时,再打开test.txt文件,文件有了内容\n");
    Sleep(1000);
    fclose(pf);
    //主:fclose在关闭文件的时候,也会刷新缓冲区
    pf=NULL;
    return 0;
}

可以得出一个结论:因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。

如果不做,可能导致读写文件的问题。

下篇建议:

C++基础入门-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/lh11223326/article/details/136971273?spm=1001.2014.3001.5502

数据结构和算法概述-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/lh11223326/article/details/136221673?spm=1001.2014.3001.5501

  • 60
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值