C语言快速复盘

C语言

C语言广泛用于底层软件开发,国际最新标准是C11,常用C90。常用编译器MSVC。VS2019集成了MSVC。

.c是源文件

.h是头文件

一个工程可以有多个.c文件,但是多个.c文件只能有一个main函数。

数据类型

  1. char:字符型数据类型,大小为1字节

  2. short :短整型 ,大小为2字节

  3. int :整型,大小为4字节,注意:int自动向下取整

  4. long 长整型 ,大小为4字节

  5. long long 长长整型,大小为8字节

  6. float 单精度浮点型,大小为4字节,用%f。

    float f = 45.4;
    print("%f\n",f);
    
        float i;
        printf("%.3f",i);//bao'liu
    
  7. double 双精度浮点型,大小为8字节,用%lf。

    注意:通过scanf函数的%m格式控制可以指定输入域宽,输入数据域宽(列数),按此宽度截取所需数据;通过printf函数的%0格式控制符,输出数值时指定左面不使用的空位置自动填0。

    修饰符格式说明:

    1、M

    %md 以宽度m输出整型数,不足m时,左补空格

    2、0m

    %0md 以宽度m输出整型数,不足m时,左补零

    3、m,n

    %m.nf 以宽度m输出实型小数,小数位为n位

    输入:
    20130225 
    输出:
    year=2013
    month=02
    date=25   
    #include<stdio.h>
    int main(){
         int year = 0, month = 0, day = 0;
        scanf("%4d%2d%2d", &year, &month, &day);
        //使用%0可以填充前导0
        printf("year=%d\n", year);
        printf("month=%02d\n", month);
        printf("date=%02d\n", day);
        return 0;
    }
    

    image-20220428144413212

常量、变量

大括号外面定义的是全局变量,内部是局部变量,当局部变量和全局变量名字冲突时局部优先。

生命周期

全局变量的生命周期就是整个程序

常量

注意:const修饰的常变量,本质上是变量

enum Sex{
    MALE,
    FEMALE
};

#define MAX=10000;

字符串

    char arr1[] = "abc";
	char arr2[] = {'a','b','c','\0'};
	printf("%s\n", arr1);
	printf("%s\n", arr2);
	int lengh=strlen(arr1);

	printf("%d\n", lengh);//3
	printf("%d\n", strlen(arr2));//3

转义字符

printf("%c\n",'\130');//输出X,X的ASCII码是88,130是八进制
\xdd对于16进制

操作符

算数操作符:±*/

移位操作符:>> <<

#include<stdio.h>
int main() {
    int n;
    scanf("%d",&n);
    printf("%d",2<<(n-1));
  //在2进制中,2的表示为00000010
  //我们只使用移位符来完成2的n次方,仅需使这个1往左移动。
  //如4:00000100;8:00001000
  //要注意为什么这里是(n-1)
  //因为2本身已经在第二位了,是2的1次方。
    return 0;
}

位操作符:& |

^:异或运算符:

​ 1、两个相同的数异或为0

​ 2、a^b=c c^a=b c^b=a

练习:不使用第三个变量交换两个变量的值

//使用异或操作完成变量交换。
int main(){
    int a=12,b=34,temp;
    printf("Original result: a=%d,b=%d\n",a,b);
    temp=a^b;//a^b=c
    a=temp^a;//c^a=b
    b=temp^b;//c^b=a
    printf("Transformed result: a=%d,b=%d\n",a,b);
    return 0;
}
//但是由于temp在赋值的整个过程中没有发生改变,因此可以无需设置临时变量
可以写成
    a=a^b;//a作为临时变量存储a与b异或的结果
    b=b^a;//b与a异或得到a存储在b中
    a=b^a;//得到b存储在a中

单目操作符 :! sizeof

int arr[10] = {0};
printf("%d\n",sizeof(arr));// 40 的是数组的总大小,单位是字节
printf("%d\n",)

~:按位取反

    int a = 0;
	printf("%d\n", ~a);//-1 
//把所有二进制数字取反 000000000000000000000000000000000 111111111111111111111111111111111111
//整数在内存中是补码,-1源码10000000000000001 反码1111111111110 补码111111111111111111111,正整数的原码 反码 补码相同


(类型):强制类型转换

& 取地址

双目运算符:&& ||

三目操作符:?:

逗号表达式:整个表达式的运算结果是最后一个表达式的结果

关键字

  1. auto:自动的,每个局部变量都是auto修饰的

  2. extern:申明外部符号

  3. register:寄存器关键字,建议经常用的数据放在寄存器

  4. union:联合体

  5. volatile:

  6. sizeof

    但是define include不是关键字,是预处理指令

  7. typedof:类型重定义

  8. 栈区存储局部变量和函数的参数,堆区存储动态内存分配的,静态区:全局变量和static修饰的变量。

    static修饰局部变量,改变了局部变量的生命周期,修饰全局变量,只能在源文件的作用域内使用

    全局变量,在其他源文件内部可以使用,但是被static就不能被使用。

    define定义常量和宏

    #define MAX 1000;
    #define ADD(X,Y) ((X)+(Y)) //完成替换工作,
    
    

指针

一个内存单元是一个字节,一个内存单元只有一个地址与之对应,

image-20220426234715095

	int a = 10;
	printf("%p\n", &a);//专门打印地址
	int* pa = &a;//pa叫做指针变量,*说明pa是指针变量 int说明pa指向的对象是int类型
	*pa = 20;//*解引用操作,*pa就是通过pa里面的地址找到a
	char ch = 'w';
	char * pc = &ch;

注意:指针变量的大小都是相同的,因为指针是用来存放地址的,指针需要多大空间取决于地址的存储需要多大空间,32位机器需要四个字节,而64位则需要8个字节

结构体

    struct Stu s = { "张三",20,58.5 };//结构体初始化
	printf("1 :%s %d %lf\n", s.name, s.age, s.score);//
	struct Stu *ps=&s;
	printf("2: %s %d %lf\n",(*ps).name,(*ps).age,(*ps).score);
	printf("3: %s %d %lf\n", ps->name, ps->age, ps->score);

随机数

起点设置:srand((unsigned int)time(NULL));
int ret = rand()%100+1;//1-100的范围

辗转相除法

	int m = 0;
	int n = 0;
	scanf("%d%d", &m, &n);
	int t = 0;
	while (t = m % n) {

		m = n;
		n = t;

	}
	printf("最大公约数:%d\n", n);

goto语句

只能在单个函数中使用,建议在跳出嵌套循环中使用。

函数

库函数

IO函数:

字符串函数:strcmp比较字符串是否相等 strlen求字符串长度

字符操作函数:toupper,void * memset ( void * ptr, int value, size_t num );

内存操作函数:memcpy

时间函数:

数学函数:

其他库函数:

函数嵌套调用和链式访问

二分查找的函数:

int binary_search(int a[],int k,int s) {//int a[] 本质上就是int *a
	int left = 0;
	int right = s - 1;
	while (left <= right) {
		int mid = (left + right) / 2;
		if (a[mid] > k) {
			right = mid - 1;
		}
		else if (a[mid] < k) {
			left = mid + 1;
		}
		else {
			return mid;
		}

	}
	return -1;


		
}
int main(){
  int arr[] = {1,2,3,4,5,6,7,8,9,10};
	int key = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);//使用此行代码求数组长度,
    //找到了就返回下标,找不到就返回-1
	int ret = binary_search(arr,key,sz);//数组arr传参传递的不是数组本身,仅仅传递了数组首元素的地址
	if (ret == -1) {
		printf("找不到\n");

	}
	else {
		printf("找到了,下标是%d\n", ret);
	}  
}

链式访问:一个函数返回值作为另一个函数的参数

函数的声明和定义

在实际的项目中,函数的声明一般放在头文件中。

写一个计算器:

A程序员写加法

B程序员写减法

C程序员写除法

D程序员写乘法

但是四个程序不可能在一个文件中写代码,A程序员需要在源文件创建一个add.c的源文件用于完成实际业务代码编写,然后在头文件中创建一个add.h的头文件用户完成声明

add.c
add.h 函数声明一般放在头文件中,告诉编译器有一个函数叫什么名字,但是具体该函数实现与否不重要
test.c中代码用到add就要#include "add.h"

这样写的好处是:可以发布非开源版本代码。

步骤是:

  1. 创建一个新项目编写好代码,在.h中作好说明,然后单击项目属性选择lib

  2. 然后编译,发给买家。

  3. 买家在使用时首先要导入静态库

    #include<binary_search.h>//先引入头文件
    #pragma comment(lib,"binary_search.lib")//引用静态库
    

这样就可以做代码的隐藏。

递归(非常重要)

  1. 什么是递归?

    递归将一个大型复杂的的问题层层转换为一个与原问题相似的规模较小的问题求解,递归策略只需要少量的程序就可以米哦啊书出解题过程中所需要的多次重复计算。

  2. 递归的两个必要条件

    • 存在限制条件,当满足这个限制条件,递归将不再继续
    • 每次递归完以后越来越接近该限制条件

    练习1:接受一个整数值,按照顺序打印其每一位。

    #include<stdio.h>
    void print(unsigned int n) {
    	if (n>9) {
    		print(n/10);
    	}
    	printf("%d ",n%10);
    
    }
    int main() {
    	//1. 接受一个整数值,按照顺序打印其每一位。
    	unsigned int num = 0;
    	scanf("%u", &num);
    	//递归时函数自己调用自己,所以写一个函数实现自己调用自己
    	print(num);//该函数可以打印参数部分数字的每一位
    
    
    	return 0;
    }
    
    • 栈溢出:栈区里面放局部变量,函数的形参也是放在栈区。而堆区是进行动态内存分配的,静态区放全局变量和静态变量。

    • 每一个函数调用都在栈区分配一块空间,所以写递归必须要有跳出条件,但是还得有一个逼近跳出条件,其次递归层次不能太深。

    练习2:编写函数不允许创建临时变量,求字符串的长度

    int my_strlen(char* str)
    {
    	if (*str != '\0') {
    		return 1 + my_strlen(str+1);
    	}
    	else {
    		return 0;
    	}
    }
    int main() {
    	//模拟实现一个strlen函数
    	char arr[] = "bit";
    	printf("%d\n", my_strlen(arr));
    
    }
    

练习4:求第n个斐波那契数列。(不考虑溢出)

int Fib(int n) {
	if (n <= 2) {
		return 1;
	}
	else {
		return Fib(n - 1) + Fib(n - 2);
	}
}
int main() {
	int n;
	scanf("%d",&n);
	int ret = Fib(n);
	printf("%d\n",ret);
	return 0;

}

​ 算法分析:当计算数值过大时,进行了大量重复计算。

*练习5:编写一个函数reverse_string(char string)

void reverse_string(char * str) {
	char tmp = *str;
	int len = strlen(str);
	*str = *(str + len - 1);
	*(str + len - 1) = '\0';
	if (strlen(str + 1) >= 2) {
		reverse_string(str + 1);
	}
	*(str + len - 1) = tmp;	
}

数组

一维数组

int main{
char ch5[] = "bit";
	char ch6[] = { 'b','i','t' };
	printf("%s\n", ch5);
	printf("%s\n", ch6);
}
bit
bit烫烫烫烫蘠it

一维数组在内存中的存储:连续的

数组名是数组首元素的地址,是一个地址。

二维数组

int main{
	int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12};
	int arr[3][4] = { 1,2,3,4,5,6 };//不完全初始化会自动在后面补0
	int arr[3][4] = { {1,2},{3,4},{4,5} };//自动补0,行可以省略,但是列不可以省略
}

数组作为函数参数

注意:数组传参传递的其实是数组首元素的地址,本质上是一个指针,在函数中的形参就不是一个数组了,而是一个指针。

数组名是什么:就是数组首元素的地址,但是sizeof(数组名)计算的时候数组名就是整个数组了,是整个数组所占字节大小。其次&数组名也是代表整个数组。

数组传参无论是一维数组还是二位数组,实参写的是数组名,形参可以写数组也可以写指针,但本质是指针。

操作符详解

移位操作符:<< 和 >>:

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

右移操作符:

  1. 算术右移:右边丢弃左边补原符号位

    int main() 
    {
    	int a = -1;
    	int b = a >> 1;
    	printf("b=%d\n", b);//b=-1,结果还没有发生变化,说明时算术右移
    }
    

    整数的二进制表示方式:

    原码:直接根据数值写出的二进制序列就是原码

    反码:原码的符号位不变,其他位按位取反

    补码:反码+1

    负数:-1,存放在内存中,存放的是二进制的补码。

    首先-1的原码是:10000000000000000000000000000001。反码:11111111111111111111111111111110

    补码:111111111111111111111111,所以-1在内存中存储形式全是1。

    当-1逻辑右移时,右边丢弃左边补原符号位,结果还是全1,没有变化,所以打印输出还是-1.

  2. 逻辑右移:右边丢弃左边补0

位操作符

& | ^:

^:按二进制位进行异或 a^a=0, 0^a=0;

赋值操作符

= += -=

单目操作符

! + - & sizeof(是一个操作符不是函数,还可以计算数组大小) ++ – (类型) *

~:对一个数字的二进制位按位取反。

int main() 
{
	int a = 13;
	//00000000 00000000 00000000 00001101表示的二进制位是13
	//把a的二进制位的第五位置为1
	//00000000 00000000 00000000 00010000 或运算
	a = a | (1 << 4);
	printf("%d\n", a);//29
	//把a的二进制位的第五个位置又换成0
	//00000000 00000000 00000000 00011101
	//11111111 11111111 11111111 11101111做且运算
	a = a & ~(1<<4);
	printf("%d\n", a);//13
	return 0;
}

关系操作符

条件操作符

逗号表达式

表达式求值

隐式类型转换

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

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

char a,b,c;
a = b + c;//b和c的值被提升为整型,然后再执行加法运算,加法运算完成后,结果将被截断,然后再存储在a中。

​ 表达式的整型运算要在CPU相应的计算器件内执行,CPU中ALU的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。通用寄存器是难以直接实现两个8比特字节直接相加运算。所以,表达式中的各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行计算。

int main() 
{
	char a = 3;
	char b = 127;//计算时发生整型提升
	char c = a + b;
	printf("%d\n", c);//-126
	//计算时发生整型提升。a:00000000 00000000 00000000 00000011  ,但是b只能存放8位,为00000011
	//b:00000000 00000000 00000000 01111111 a存放8位:01111111
	//整型提升是按照变量数据位的符号位提升的。,无符号高位补0
	//a:00000000 00000000 00000000 00000011 
	//b:00000000 00000000 00000000 01111111
	//c:00000000 00000000 00000000 10000010
	//截断:10000010,按照整型打印,就要发生整型提升,c高位补1,
	//c的补码:11111111 11111111 11111111 10000010
    //c的反码:11111111 11111111 11111111 1000
	//c的原码:10000000 00000000 00000000 01111110 :-126

	return 0;
}

操作符优先级

作为程序员不应该写出歧义表达式:

a*b+c*d+e*f;
c + --c;

int main() 
{
	int i = 1;
	int ret = (++i) + (++i) + (++i);
	printf("%d\n", ret);

}

对应的汇编代码:

	int i = 1;
00991888  mov         dword ptr [i],1  
	int ret = (++i) + (++i) + (++i);
0099188F  mov         eax,dword ptr [i]  
00991892  add         eax,1  
00991895  mov         dword ptr [i],eax  
00991898  mov         ecx,dword ptr [i]  
0099189B  add         ecx,1  
0099189E  mov         dword ptr [i],ecx  
009918A1  mov         edx,dword ptr [i]  
009918A4  add         edx,1  
009918A7  mov         dword ptr [i],edx  
009918AA  mov         eax,dword ptr [i]  
009918AD  add         eax,dword ptr [i]  
009918B0  add         eax,dword ptr [i]  
009918B3  mov         dword ptr [ret],eax  
	printf("%d\n", ret);
009918B6  mov         eax,dword ptr [ret]  
009918B9  push        eax  
009918BA  push        offset string "%d\n" (0997B30h)  
009918BF  call        _printf (09910CDh)  
009918C4  add         esp,8  

指针初阶

  1. 指针是什么?

    ​ 地址指向了改内存单元,所以把地址形象称为指针。

    ​ 存放地址的变量又称为指针变量。

    ​ 指针变量在32位平台是4个字节在64位平台是8个字节,与指针变量的类型无关。

  2. 指针和指针类型

    int main() 
    {
    	int a = 0x11223344;
    	char* pc = &a;
    	*pc = 0;
    	return 0;
    }
    
    1. image-20220522163337296
    2. 是char*的指针,解引用时只改变了一个字节。

image-20220522163702980

  • 指针类型决定了指针解引用的权限有多大
  • 指针类型决定了指针走一步能走多远
  • 未来选择什么指针应该根据想要的功能来实现,比如说,你希望按照一个字节一个字节来访问,应该按照char *来访问

野指针

指针指向的位置是不可知。

int* test()
{
	int a = 10;
	return &a;
}
int main() 
{
	int* p = test();
	*p = 20;//注意放能放,但是已经非法访问内存了
	return 0;
}

如何避免野指针出现:

  1. 指针初始化 当不知道指针应该初始化为什么地址的时候,直接初始化为NULL。

  2. 小心指针越界,C语言不会检查数组的越界行为

  3. 指针指向空间释放及时置为NULL。

  4. 指针使用之前检查有效性

    if(p!=null)
    

    指针运算

    • 指针±整数

      根据指针类型所代表的变量类型+对应字节数

    • 指针-指针

      int main()
      {
      	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
      	printf("%d\n", &arr[9] - &arr[0]);//指针减去指针的情景前提是两个指针指向同一块空间 9
      
      	return 0;
      }
      

      strlen的实现

      int my_strlen(char* str) 
      {
      	char* start = str;
      	while (*str != '\0') 
      	{
      		str++;
      	}
      	return str - start;
      }
      int main() 
      {
      	int len = my_strlen("abc");//穿abc实际上传的是abc字符串首字符a的地址
      	printf("%d\n",len);
      }
      
    • 指针的关系运算

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

      int main()
      {
      	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
      	int* p = arr;
      	printf("%d\,", arr[2]);
      	printf("%d\n", p[2]);
      	return 0;
      
      }
      

      二级指针

      什么是二级指针

      int main() 
      {
      	int a = 10;
      	int* pa = &a;//pa是指针变量,就要在内存开辟空间
      	int* *ppa = &pa;//取出pa指针变量在内存中的起始地址,ppa就是一个二级指针变量 *ppa==pa *(*ppa)==a
      	
      }
      

      指针数组

      int main() 
      {
      	int* parr[5];
      	char* pch[5];
      
      }
      

      结构体初阶

      数组是一组相同类型元素的集合,这些值被称为成员变量。

      结构体:也是一些值得集合,但是值的类型可以不同。

      struct B
      {
      	char c;
      	short s;
      	double d;
      };
      struct Stu
      {
      	结构的成员变量
      	struct B sb;
      	char name[20];
      	int age;//年龄
      	char id[20];
      }s1,s2;//s1和s2也是结构体的变量
      s1,s2是全局变量,
      int main() 
      {
      	//s是局部变量
      	struct Stu s = { {'w',20,3.14},"张三",30,20202562};//初始化
      	printf("%c", s.sb.c);
      	printf("%s", s.id);
      
      	struct Stu* ps = &s;
      	printf("%c\n",(*ps).sb.c);
      	//ps是指针
      	printf("%c\n", ps->sb.c);
      	return 0;
      	//
      }
      

      结构体传参

      参数传参的时候,参数是需要压栈的,如果传结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。

      结论:结构体传参的时候,要传递结构体的地址

      调试

      image-20220527160012046

    debug:调试版本

    release:发布版本

    debug和release有什么区别:debug包含调试信息,大小较大,release版本不能调试

    Windows环境调试

    F5:启动调式,经常用来直接跳到下一个断点处

    F9:创建取消断点作用

    调用堆栈

    image-20220527174544692

    ​ 栈区内存先使用高地址空间,在使用低地址空间,而数组访问先使用低地址再使用高地址

int main() 
{
	int i = 0;
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//
	for (i = 0; i <= 0; i++) 
	{
		arr[i] = 0;
		printf("hehe\n");//这段代码会出现死循环,但是先定义数组再定义i就会报错
        //因为i和arr是两个变量,局部变量是放在栈区的,先使用高地址空间在使用低地址空间
        //数组随着下标增长地址是由低到高变化的。
	}
	return 0;
}

比较标准模拟实现一个strcpy:字符串拷贝

#include<assert.h>
char* my_strcpy(char* dest, const char* src)//const修饰变量,不能被修改,本质上还是变量
//const放在*左边,修饰的是*p,表示指针指向的内容,是不能通过指针来改变的,但是指针变量本身是可以改变的,但是放在右边表示指针变量
//变量不能改变
//strcopy库函数其实返回目标空间的起始地址
{//改造
	assert(src!=NULL);//断言
	assert(dest != NULL);//断言
	char* ret = dest;
	while (*dest++ = *src++)
	{
		;
	}
	*dest = *src;
	return ret;
}

int main() 
{
	char arr1[20] = "xxxxxxxxx";
	char arr2[] = "hello";//把\0也拷贝进来了
	//strcpy(arr1,arr2);
	
	printf("%s\n", my_strcpy(arr1, arr2));
	return 0;
}

模拟实现strlen函数

#include<stdio.h>
#include<assert.h>
int my_strlen(const char* arr) 
{
	//*(arr+3) = '3';
	int i = 0;
	assert(arr != NULL);
	while (*arr++ != '\0') 
	{
		i++;
	}
	return i;
}
int main() 
{
	char arr[20] = "xxxdsdsxxxxxxxxx";
	printf("%d\n", my_strlen(arr));
}

编译常见作用:

编译型错误:直接拼接经验就可以直接搞定,基本就是语法错误

链接型错误:要不这个符号就不存在,要不就写错了

运行时错误:需要逐步调试

例:计算一个整数的二进制补码1的个数

int NumberOf(int n) 
{
	int count = 0;
	int i = 0;
	for (i = 0; i < 32; i++) 
	{
		if ((n >> i) & 1 == 1) 
		{
			count++;
		}
	}
	return count;
}
int main() 
{
	int n = -1;
	printf("%d\n", NumberOf(n));
	return 0;
}

改良版

int NumberOf(int n) 
{
	int count = 0;
	while (n) 
	{
		n = n & (n - 1);//
		count++;
	}
	return count;
}
int main() 
{
	int n = -1;
	printf("%d\n", NumberOf(n));
	return 0;
}

写一个代码判断一个数字是不是2的n次方。(谷歌笔试题)

int NumberOf(int n)
{
    if(k&(k-1)){
       printf("不是2的n次方");
    }else{
        printf("是的2的n次方");
    }
}

求两个数二进制位不同个数

int NumberOf(int n)
{
	int count = 0;
	while (n)
	{
		n = n & (n - 1);
		count++;
	}
}
int main() 
{
	int m = 0;
	int n = 0;
	scanf("%d %d", &m, &n);
	int ret = m ^ n;//相同为0,相异为1
	//统计一下,ret的二进制中有几个1,就说明m和n的二进制位中有几个位不同
	int count = 0;
	count = NumberOf(ret);
	printf("%d\n", count);
	return 0;
}

获取一个整数所有奇数位和偶数位

int main() 
{
	int n = 0;
	scanf("%d", &n);
	//获取n的二进制位的奇数位和偶数位
	int i = 0;
	//打印了所有偶数位
	for (i = 31; i >= 1; i -= 2) 
	{
		printf("%d ", (n >> i) & 1);
	}
	//打印所有奇数位
	printf("\n");
	for (i = 30; i >= 0; i -= 2) 
	{
		printf("%d ", (n >> i) & 1);
	}

	return 0;

}

将一句英语倒叙

void reverse(char* left, char* right) 
{
	while (left<right) 
	{
		char tmp = 0;
		tmp = *left;
		*left = *right;
		*right = tmp;
		left++;
		right--;	
	}

}
int main() 
{
	char arr[100] = { 0 };
	gets(arr);
	int len = strlen(arr);
	reverse(arr,arr+len-1);
	//每个单词逆序
	char* start = arr;
	while (*start) 
	{
		char* end = start;
		while (*end != ' ' && *end!='\0') 
		{
			end++;
		}//逆序一个单词
		reverse(start, end - 1);
		if (*end == ' ')
			start = end + 1;
		else
			start = end;
		start = end + 1;
	}
	printf("%s\n",arr);

}

数据的存储

引入:

image-20220621180927180

int main() 
{
	int i = 0;
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	for (i = 0; i <= 12; i++) 
	{
		arr[i] = 0;
		printf("haha\n");
	}
	return 0;
}

以上代码再debug版本会出现死循环,因为i和arr是局部变量,局部变量是放在栈区的,**栈区的使用习惯是先使用高地址再使用低地址,**而数组下标是由第地址向高地址变化的,当数组越界访问可能访问到变量i的内存空间,i的数字改变了。

而release版本就不会出现死循环,会对地址进行优化,i的地址会小于arr[9]的地址。

所以程序员编译通过但是测试不一定通过。

整型家族

整型在内存中如何存储?

大端小端存储:

当数据大于一个字节时,把数据的地位字节序的内容存放在高地址处,高位字节序存放在低地址处是大端存储;把数据的低位字节序低地址处是小段存储。

写代码判断机器的字节序

int main() 
{
	int a = 1;
	char* p =(char*)&a;//用char*指针拿a的第一个字节的地址
	if (*p == 1) 
	{
		printf("小段存储");
	}
	else 
	{
		printf("大端存储");
	}

	return 0;
}

例题:以下代码输出什么?

int main() {
    char a = -1;//-1的补码是111111111111111111111111111111截断为11111111
    signed char b = -1;//11111111
    unsigned char c = -1;//11111111
    //以上存储进去的都是11111111,
    //
    printf("%d %d %d", a, b, c);//-1 -1 255
}//1.char到底是unsigned char 还是signed char 取决于编译器
//而int就是signed int
//2.signed char前面全部补充1,11111111111111111111111111解读为-1


int main() {
    char a = -128;
    printf("%u", a);
    return 0;//4294967168
}

int main() 
{
	char a[1000];
	int i;
	for (i = 0; i < 1000; i++) 
	{
		a[i] = -1 - i;//用整型进行数值运算然后进行存储,当存储到(-127-1)会发生截断,变成128
//-1,-2,-3,-4 ....-127,128,127....1,0   127+128=255
	}
	printf("%d", strlen(a));//strlen就是找0,有就0前面就有多少个元素
	return 0;
}

浮点数家族

根据IEEE754标准,任意浮点数V可以表示下面的形式:

​ 例如:5.5可以表示为1.0112^2(-1)

指针进阶

更加深入的探讨指针。

字符指针

int main() 
{
	char str1[] = "hello bit.";
	
	const char* ps = "hello bit";//此hello world是来自字符串常量,不能修改

	return 0;
}

指针数组和数组指针

注:

int main(){
    int arr[10]={0};
    int *p1=arr;
    int (*p2)[10]=&arr;
    printf("%p\n",arr);
    printf("%p\n",&arr);
    return 0;
}

//数组名是数组首元素的地址

//但是有两个例外 sizeof(数组名)-表示计算的是整个数组的大小

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

void  buf(char** s)//新建一个房子,该房子内容是指向char*类型的地址
{
    *s = "message";//""常量的类型就是char*
}
int main()
{
    char* sm;//指针是变量变量必须初始化
    buf(&sm);//传递该指针变量的地址,用户给该地址的内存房子赋地址,是的该指针变量指向有效值,该有效值是指向char类型的地址,使得其初始化
    printf("%s\n", sm);
    char** s;//s房子的内容是垃圾值,s必须初始化,保存的是地址,该地址的内容是一个指向char的地址
    s = (char**)malloc(sizeof(char*));//动态分配了一个地址
    *s = "hello world";
    printf("%s", *s);
}

注意区别指针数组和数组指针的区别:

int arr[5];//整型数组
int *parr1[10];//存放整型指针的数组
int (*parr2)[10];//数组指针,该指针能够指向一个数组,数组10个元素,每个元素的类型是int
int (*parr3[10])[5];//是一个存放数组指针的数组,该数组能够存放十个数组指针,每个数组指针能够指向一个数组,该数组有五个元素,每个元素是int类型

以下传参是正确的:

void test(int arr[]) {}
void test(int arr[10]) {}
void test(int *arr) {}
void test2(int *arr[20]) {}
void test2(int* *arr) {}
int main() 
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
	return 0;
}

例:使用指针打印数组内容

int main() 
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int sz = sizeof(arr)/sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++) 
	{
		printf("%d ",*(p + i));
	}

	return 0;
}

函数指针:

指向函数的指针,存放函数地址的指针

int Add(int x, int y) 
{
	return x + y;
}
int main() 
{
	int a = 10;
	int* pa = &a;
	char ch = 'w';
	char* pc = &ch;

	int arr[10] = {0};
	int (*parr)[10] = &arr;//parr指向数组指针,存放的数组的地址
	//函数指针存放函数地址

	printf("%p\n",&Add);
	//注意:数组名 !=&数组名
	//函数名==&函数名
	int (*pf)(int, int) = &Add;
	//函数指针一般
	int ret=(*pf)(2, 5);
	printf("%d ",ret);
	ret = pf(3,5);
	printf("%d", ret);

	return 0;
}
int main() 
{
	(*(void(*)())0)();
	//调用0地址处的函数,改函数无参,返回值为void,0被解析为函数地址

	return 0;
}

指向函数指针数组的指针

函数指针的数组本质上是数组。

int (*p) (int ,int);//函数指针
int(*p2[4])(int ,int);//函数指针的数组
int(*(*p3)[4])(int,int)=&p2;//取出的是函数指针数组的地址
//p3就是一个指向函数指针的数组的指针

回调函数

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

qsort函数的演示:qsort是C语言的库函数,是快速排序的实现,

这是冒泡排序算法

void bubble_sort(int arr[],int sz) 
{
	int i = 0;
	
	for (i = 0; i < sz - 1; i++) 
	{
		for (int j = 0; j < sz-i-1; j++) 
		{
			if (arr[j] > arr[j + 1]) 
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
void print_arr(int arr[],int sz) 
{
	int i=0;
	for (int i = 0; i < sz; i++) 
	{
		printf("%d ",arr[i]);
	}
	printf("\n");

}
int main() 
{
	int arr[10] = {9,8,7,6,5,4,3,2,1,0};
	int sz = sizeof(arr) / sizeof(arr[0]);
	print_arr(arr, sz);
	bubble_sort(arr, sz);
	print_arr(arr,sz);
	return 0;
}

qsort函数C语言编译器函数库自带的排序函数。qsort 的函数原型是void qsort(voidbase,size_t num,size_t width,int(__cdeclcompare)(const void*,const void*)); 是base所指数组进行排序。qsort函数包含在C 标准库 - <stdlib.h>中。

qsort函数的使用如下:

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int cmp_int(const void* e1, const void* e2) 
{
	return *(int*)e1 - *(int*)e2;
}
void test1() 
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);//base放待排序数组的第一个对象的地址,num待排序数据元素的个数,size表示一个元素有几个字节,
	//函数指针是指向来比较待排序数据的两个元素的函数,返回大于0
	print_arr(arr, sz);

}
struct Stu
{
	char name[20];
	int age;
};
int sort_by_age(const void* e1, const void* e2) 
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
void test2() 
{
	//排序结构体数据
	struct Stu s[] = { {"张三",30},{"李四",34},{"王五",20} };
	//先对s进行按照年龄来排序
	int sz = sizeof(s)/sizeof(s[0]);
	qsort(s,sz,sizeof(s[0]),sort_by_age);

	return 0;
}
//void qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*)) {}
int main() 
{
	
	test1();//测试一组整型数据排序
	test2();//测试一组结构体数据
	return 0;
}

模仿qsort函数实现冒泡排序排序任意类型数据

void swap(char *buf1,char* buf2,int width)
{
	int i = 0;
	for (i = 0; i < width; i++) 
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
void bubble_sort(void *base,int sz,int width,int (*cmp)(const void *e1,const void *e2)) 
{
	int i = 0;
	for (i=0;i<sz-1;i++) 
	{
		for (int j=0;j<sz-1-i;j++) 
		{
			//两个元素的比较,因为排序的数据不知到如何比较,所以就让冒泡排序的使用者来使用这个函数
			if (cmp((char *)base+j*width,(char *)base+(j+1)*width) > 0)//让cmp的参数把被比较的两个元素的地址传过去,有cmp函数进行比较 
			{
				//交换,第一个元素大于第二个元素
				swap((char*)base + j * width, (char*)base + (j + 1) * width,width);


			}
		}
	}
}
struct Stu
{
	char name[20];
	int age;
};
int sort_by_age(const void* e1, const void* e2) 
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
void print_Stu(struct Stu s[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf(" %s ", s[i].name);
		printf("%d", s[i].age);
	}
	printf("\n");

}
void test3() 
{
	//排序结构体数据
	struct Stu s[] = { {"张三",30},{"李四",35},{"王五",20} };
	//先对s进行按照年龄来排序
	int sz = sizeof(s)/sizeof(s[0]);
	print_Stu(s,sz);
	bubble_sort(s,sz,sizeof(s[0]),sort_by_age);//按照升序排序
	print_Stu(s, sz);
	return 0;
}

int main() 
{
	
	test3();//用冒泡排序比较学生结构体里面的数据

	return 0;
}

例题


int main() 
{
	int a[] = {1,2,3,4};
	printf("%d\n",sizeof(a));//a是数组名,
	printf("%d\n",sizeof(a+0));//a+0代表第一个元素的地址,计算的是指针的大小   8/16
	printf("%d\n",sizeof(*a));//计算的是第一个元素的大小
	printf("%d\n", sizeof(a + 1));//计算的是指针的大小
	printf("%d\n", sizeof(a[1]));//计算的是第二个元素的大小

	printf("%d\n",sizeof(&a));//虽然是数组的地址但也是地址,sizeof计算的是一个地址的大小
	printf("%d\n",sizeof(*&a));//计算的是一个数组的大小 16
	printf("%d\n",sizeof(&a+1));//4/8 &a是一个数组指针,&a+1是跳过整个数组之后下一个空间的位置
	printf("%d\n", sizeof(&a[0]));//4/8,取的是第一个元素的地址
	printf("%d\n",sizeof(&a[0]+1));//第二个元素的地址



	char arr[] = { 'a','b','c','d','e','f' };//数组在初始化直接放的 不考虑\0
	printf("%d\n",sizeof(arr));//计算的是数组的大小
	printf("%d\n", sizeof(arr + 0));//arr就是数组名表示首元素的地址 arr+0还是首元素的地址,
	//字符的地址也是地址,只要是地址大小都是固定的
	printf("%d\n", sizeof(*arr));//1 首元素的大小
	printf("%d\n",sizeof(arr[1]));//1
	printf("%d\n",sizeof(&arr));//4/8
	printf("%d\n", sizeof(&arr + 1));//跳过了一个数组,但仍然是指针,该指针指向数组
	printf("%d\n", sizeof(&arr[0] + 1));//该指针指向数组的第二个元素
	return 0;
}

int main() 
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n",strlen(arr));//随机值 arr传过去的是数组首元素地址, 但是strlen找不到\0 所以是随机值
	printf("%d\n",strlen(arr+0));//arr+0还是数组首元素的地址
	printf("%d\n",strlen(*arr));//*arr表示数组首元素,是char类型与strlen参数类型不匹配
	printf("%d", strlen(&arr));//随机值
	printf("%d", strlen(&arr+1));//随机值-6
	printf("%d",strlen(&arr[0]+1));//随机值-1

}
int main() 
{
	char *arr = "abcdef";//[a,b,c,d,e,f,\0]
	printf("%d\n",sizeof(arr));//算的是指针变量的大小 4/8
	printf("%d\n",sizeof(arr+1));// 还是个地址 4/8
	printf("%d\n",sizeof(*arr));//1
	printf("%d\n",sizeof(arr[0]));//等价于*(arr+0)
	printf("%d\n",sizeof(&arr));//4/8
	printf("%d\n",sizeof(&arr+1));//4/8
	printf("%d\n",sizeof(&arr[0]+1));// 第二元素的地址 4/8 
	return 0;
}

例题:

int main() 
{
	int a[5] = {1,2,3,4,5};
	int* ptr = (int*)(&a+1);
	printf("%d,%d",*(a+1),*(ptr-1));//2 5
	return 0;
}
int main() 
{
	int a[4] = {1,2,3,4};
	int* ptr1 = (int*)(&a + 1);
	int* ptr2 = (int*)((int)a+1);
	printf("%x,%x",ptr1[-1],*ptr2);
	return 0;
}
int main() 
{
	int a[5][5];
	int(*p)[4];
	p = a;
	printf("%p,%d\n",&p[4][2]-&a[4][2],&p[4][2]-&a[4][2]);//FFFFFFFC -4
	return 0;
}

经典例题

左旋字符串

#include<assert.h>
void reverse(char* left,char* right) 
{
	assert(left);
	assert(right);
	while (left<right) 
	{
		char temp = *left;
		*left = *right;
		*right = temp;
		left++;
		right--;
	}
}
void string_left_rotate(char * str,int k) 
{//三步反转法
	int n = strlen(str);
	reverse(str,str+k-1);
	reverse(str+k,str+n-1);
	reverse(str,str+n-1);

}
int main() 
{
	char arr[10] = {"ABCDEF"};
	int k = 2;
	string_left_rotate(arr,k);
	printf("%s\n",arr);

	return 0;
}

int main() 
{
	int aa[2][5] = {1,2,3,4,5,6,7,8,9,10};//首先这是一个二维数组,aa是一个数组名,本质上是二维数组首地址的常量
	int* ptr1 = (int*)(&aa + 1);//&数组名取出的是指向的数组的地址,+1就会跳过整个数组
	int* ptr2 = (int*)(*(aa + 1));//相当于aa[1],跳过前五个元素指向6
	printf("%d,%d",*(ptr1 - 1),*(ptr2 - 1));//ptr1指向10后面第四个字节的地址-1,就会指向10
	return 0;
}



字符串函数

size_t strlen(const char* str)

  • 注意返回值是无符号类型

strcat()追加字符串

char* my_strcat(char* dest,const char* src) 
{
	char* ret = dest;
	assert(dest && src);
	while (*dest) 
	{
		dest++;
	}
	while (*dest++ = *src++)//追加字符串 包含\0
	{
		;
	}
	return ret;
}

strcmp函数//长度不受限制的字符串函数

int my_strcmp(const char* s1,const char* s2)
{
	while (*s1 == *s2)
	{
		if (*s1 == '\0') 
		{
			return 0;
		}
		s1++;
		s2++;
	}
	if (*s1 > *s2) 
	{
		return 1;
	}
	else 
	{
		return -1;
	}
}
int main() 
{
	int ret = my_strcmp("abb","accccc");
	printf("%d", ret);
	return 0;
}

char * strncpy ( char * destination, const char * source, size_t num );

Copy characters from string

Copies the first num characters of source to destination. If the end of the source C string (which is signaled by a null-character) is found before num characters have been copied, destination is padded with zeros until a total of num characters have been written to it.

https://cplusplus.com/reference/cstring/strncpy/?kw=strncpy

结构体

结构体的内存对齐

1.第一个成员永远放在结构体在内存中存储位置的0偏移处开始。

2.从第二个成员往后的所有成员,都放在一个对其数(成员的大小和默认对齐数的较小值)的整数倍的地址处。

3.结构体的总大小是结构体所有成员对齐数中最大的那个对齐数的整数倍。

目的:用空间换时间

枚举

联合体

union Un 
{
	char c;
	int i;
};
int main() 
{
	union Un u;

	printf("%d\n",sizeof(u));//4,说明i和c共用了一块空间,联合体大小至少是最大成员的大小,所以联合体在同一时间只能用一个
	//当最大成员大小不是最大对齐数的整数倍,联合体依然要对其要最大最大对齐数的整数倍
    
	return 0;
}

可以用巧妙联合体判断大小端。

动态内存分配

内存分为几个区域,栈放局部变量和函数形参,堆区动态内存开辟,malloc free realloc calloc都是在堆区进进行创建;全局变量和静态变量都是在静态方法区。

malloc

int main()
{
    
    return 0;
}    

realloc函数

  • relloc函数的出现让动态内存管理更加灵活。

移位

 左移时总是移位和补零,无论是有符号类型数据还是无符号类型数据都统称为逻辑左移。
 右移时无符号数是移位和补零,此时称为逻辑右移;
 右移时而有符号数大多数情况下是移位和补最左边的位(也就是补最高有效位),移几位就补几位,此时称为算术右移。

gcc 编译四大步骤

  1. 预处理 -E gcc -E xxx.c -o xxx.i

    ​ 1)按照头文件展开,不检查语法错误。可以展开任意文件,不管对不对,只要写如何文件都可以#include展开

    ​ 2)宏定义替换 将宏名替换为宏值

    ​ 3)替换注释

    ​ 4)展开条件编译

  2. 编译 -S gcc -E hello.c -o hello.s

    ​ 1)将C程序翻译为汇编指令

    ​ 2)编译过程会逐行检查语法错误,整个4个步骤是编译中最耗时的。

  3. 汇编

    ​ 1)将汇编指令翻译为二进制代码

  4. 链接

    1)数据段合并

    ​ 2)数据地址回填

    ​ 3)库引入

变量

  1. 变量定义会开辟内存空间,声明不会开辟内存空间。
  2. 在变量使用之前,必须有定义
  3. 如果变量声明前有extern则无法定义

类型限定符

  • extern :表示声明。对于变量是没有内存空间,不能提升。
  • const:限定一个变量为只读变量。
  • Volatile:防止编译器优化代码
  • register:定义一个寄存器变量

const

const向右修饰,被修饰的部分为只读。

  • const int *p; //可以修改p,不可以修改 *p
    
  • int const *p;//同上
    
  • int *const p;//可以修改*p,不可以修改P
    

strstr函数

char *strstr(char *str,char *substr)//在str中找substr位置,返回一个第一次出现位置后面所有字符串
    
char* my_strstr(char *str,char *substr)
{
    char *pstr = str;//遍历str指针
    char *temp = str;//记录回滚位置
    char *psub = substr;//遍历substr指针
    while(*pstr)
    {
        temp = pstr;
        while(*pstr == *psub && *pstr != '\0')
        {
            pstr++;
            psub++;
        }
        if(*psub == '\0')
        {
            return temp;
        }
        psub = substr;
        pstr = temp;
        pstr++;
    }
    return NULL;
}

strcat函数

char *strcat(char *dest, char *src);//将src的内容拼接到dest后面,返回拼接后的字符串
char *my_strcat(char *dest,char *src)
{
    char* temp = dest;
    if(!dst || !src)
    {
        return NULL;
    }
    while(*dest)dest++;
    while(*dest++ = *src++);
    return temp;
}

strcmp函数

int strcmp(const char *s1,const char *s2);//比较s1和s2两个字符串,s1 大于 s2返回1,s1小于s2返回-1

strtok函数

char *stock(char *str,const char *delim);//第一次拆分传入源字符串,第1+次拆分传入NULL
/*
拆分 www.dasd.dasf.fs.$fdsffsdfsd
*/
char str[] = "www.dasd.dasf.fs.$fdsffsdfsd";
char *p = strtok(str,". $");
while(p != NULL)
{
    p = strtok(NULL,". $");
    prinf("p = %s\n",p);
}

字符串转浮点数

int main()
{
    char str1[] = "10";
    int num1 = atoi(str1);
    
    char str2[] = "0.123";
    double num2 = atof(str2);
    
    char str3[] = "123L";
    long num3 = atol(str3);
}

内存4区模型

  • 代码段:程序源代码(二进制形式)
  • 数据段:只读数据段 。.data:初始化为非0的全局变量和静态变量。 .dss:初始化为0,未初始化的全局变量和静态变量。.rodata:只读数据段
  • stack:栈。在其之上开辟栈帧,由操作系统自动分配,自定释放。
  • heap:堆。给用户自定义数据提供数据,空间足够大,C用malloc()申请

动态创建二维数租:

int main()
{
    int **p = (int**)malloc(sizeof(int*)*3);
    for(int i = 0; i < 3;i++)
    {
        p[i] = (int*)malloc(sizeof(int)*5);
        
    }
    //使用空间
    for(int i = 0;i < 3;i++) 
    {
        for(int j = 0;j < 5;j++)
        {
            p[i][j] = 1;
        }
    }
    //读数据
    for(int i = 0;i < 3;i++)
    {
        for(int j = 0 ; j < 5;j++)
        {
            printf("%d ",*(*(p+i)+j));
        }
        printf("\n");
    }
    for(int i = 0; i < 3;i++)
    {
        free(p[i]);
    }
    free(p);
    p = NULL;
    return 0;
}

共用体

#include<stdio.h>
union Test
{
	
};

文件

input:读操作,输入操作。

output:写操作,输出操作。

读写文件时,都是从缓冲区读写,有利于文件读写效率。

换行操作:windows \r\n

linux \n

C语言:\n

读写文件与printf与scanf的关联

printf----屏幕----标准输出

scnaf----键盘-----标准输入

perror—屏幕----标准错误

系统文件:

标准输出 - stdout - 1

标准输入 - stdin - 0

标准错误 - stderr --2

应用程序启动时,自动被打开,程序执行结束时,自动被关闭。

文件指针和普通指针区别:

借助文件操作函数fopen改变野指针 空指针情况。

操作文件,使用文件读写函数

文件分类

  • 文本文件
  • 二进制文件

文件操作一般步骤:

1.打开文件fopen();

fopen函数

详细介绍:https://baike.baidu.com/item/c%E8%AF%AD%E8%A8%80fopen%E5%87%BD%E6%95%B0/1775995

FILE* __cdecl fopen(
        _In_z_ char const* _FileName,
        _In_z_ char const* _Mode
        );
/*
参1:访问路径+文件名
文件访问路径:
	绝对路径:从系统磁盘的根盘符开始   C:/Users/....
	相对路径:若是xxx.exe执行,相对于exe文件所在目录位置,若是vs下则在.c文件目录下生成
	
参2:对应的文件打开权限
r:表示只读打开,文件不存在报错,存在则以只读方式打开
w:只写打开,文件不存在创建一个空文件,文件存在 清空并打开
r+:读写,不创建文件
w+:读写,创建文件
b:操作文件是一个二进制文件
成功:返回打开文件的指针
失败:返回NULL
*/

以下为二级指针配合文件练习
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>

int getFileLines(FILE *pfile)
{
    if(pfile == NULL)
    {
        return -1;
    }
    char buf[1024] = {0};
    int lines = 0;
    while(fgets(buf,1024,pfile) != NULL)
    {
        lines++;
    }
    fseek(pfile,0,SEEK_SET);
    return lines;
}

//读取数据
void readFileData(FILE *pfile,int len,char **pArray)
{
    if(pfile == NULL)
    {
        return;
    }
    char buf[1024] = {0};
    int index = 0;
    while(fgets(buf,1024,pfile)!=NULL)
    {
        int currentLen = strlen(buf)+1;
        char *currentstr = malloc(sizeof(char)*currentLen);
        strcpy(currentstr,buf);
        pArray[index] = currentstr;
        index++;
        memset(buf,0,1024);
    }
}
void show(char** pArray,int len)
{
    for(int i = 0;i < len;i++)
    {
        printf("%s\n",pArray[i]);
    }
}
void test01()
{
    FILE *pFILE = fopen("test.txt","r");
    if(pFILE == NULL)
    {
        perror("fopen failed");
        getchar();
        exit(-1);
    }
    //统计下有效行数
    int len = getFileLines(pFILE);
    //读取数据放在parray中
    char **pArray = malloc(sizeof(char*)*len);
    readFileData(pFILE,len,pArray);
     show(pArray,len);
     free(pArray);
     fclose(pFILE);
}

int main()
{
    test01();
    return 0;
}

2.读写文件fopen fgets,fread,fwrite

fputc()函数
int fputc(int ch,FILE *stream);
/*
参1:待写入的字符
参2:打开文件的fp
返回值:返回的是int类型,成功 写入文件的ASCII值  失败-1
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main() 
{

	FILE* fp = fopen("text.txt","w");
	if (fp == NULL) 
	{
		perror("fopen error");
		getchar();
		return -1;
	}
    char ch = 'a';
    while (ch <= 'z')
    {
        fputc(ch,fp);
        ch++;
    }
    
	return 0;
}
fgetc()函数
int fgetc(FILE *stream);
/*
参1:待读取的文件指针
返回值:成功:字符对应的ASCII码,失败-1
每成功读取光标后移
文件的结束标记 EOF--》-1
*/
void read_file()
{
    FILE *fp = fopen("test.txt","r");
    if(fp == NULL)
    {
        perror("fopen error!");
    }
    char ch = fgetc(fp);
    
    while (ch != EOF)
    {
        printf("%d\n",ch);
        ch = fgetc(fp);
    }
    fclose(fp);

}

文件读写练习:

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

void test01()
{
    //写文件
    FILE *pfile = fopen("./test01.txt","w+");
    if(pfile == NULL)
    {
        return;
    }
    char buf[] = "this is a first test";
    for(int i = 0;i < strlen(buf);i++)
    {
        fputc(buf[i],pfile);
    }
    fclose(pfile);
    //读文件
    FILE* fread = fopen("./test01.txt","r");
    if(fread == NULL)
    {
        return;
    }
    char ch;
    while ((ch = fgetc(fread))!=EOF)//EOF end of file
    {
        printf("%c",ch);
    }
    fclose(fread);
    
}




int main()
{
    test01();
    return 0;
}
feof()函数
int feof(FILE *stream);//判断是否到达文件结尾,既可以判断文本文件,也可以判断二进制
//既可以判断文本文件也可以判断,到达文件结尾返回1,没到达返回0
//对feof()来说,它的工作原理是,站在光标所在位置,向后看看还有没有字符,注意:EOF也算字符,空文件自带EOF。如果有,返回0;如果没有,返回非0。它并不会读取相关信息,只是查看光标后是否还有内容,不会移动光标
注意:eof读取字符具有滞后性  
    while((ch = feof(fread))!=EOF)
    {
        printf("%c",ch);
    }
    

rewind()函数

将文件光标放在文件开头

#include<stdio.h>

int main()
{
	FILE *p = fopen("1.txt","r");
	getc(p);//方便使用feof判断文件是否为空
	if(feof(p))
	{
		printf("文件为空");
	}
	else
	{
		rewind(p);
		int a;
		fscanf(p,"%d",&a);
		printf("%d",c);
		
	}
	return 0;
}

3.关闭文件 fclose();

fgets函数

函数原型

char *fgets(char *str, int n, FILE *stream);

参数

  • str– 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
  • n– 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
  • stream– 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。

功能

从指定的流 stream 读取一行,并把它存储在str所指向的字符串内。当读取(n-1)个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。在读取到n-1个字符时或者读到\n时,str末尾会自动添加\0.注意输入\n时\n也到保存在str里面

https://baike.baidu.com/item/fgets/10942211?fr=aladdin

int main()
{
    char buf = {10};
    printf("%s",fgets(buf,10,stdin));
    return 0;
}

fputs()函数

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

返回值:该函数返回一个非负值,如果发生错误则返回 EOF(-1)。

(1)str:这是一个数组,包含了要写入的以空字符终止的字符序列。

(2)stream:指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流

写出一个字符串,如果字符串没有换行标记

练习1:获取用户键盘输入,用户写入“:wq”终止用户输入,将之前的数据保存一个文件

/*获取用户键盘输入,用户写入“:wq”终止用户输入,将之前的数据保存一个文件*/
/*

*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main() 
{
    FILE *fp = fopen("test01.txt","w");
    if(fp == NULL)
    {
        perror("fopen error");
        return -1;
    }
    char buf[4096] = {0};
    while(1)
    {
        fgets(buf,4096,stdin);//最多从标准输入中读取4095个字符或者当读取到\n时自动结束,buf末尾添加\0
        if(strcmp(buf,":wq\n")==0)//添加‘\n’是为了用户输入wq:\n时,fgets才结束,buf里面要保存\n
        {
            break;
        }
        fputs(buf,fp);
    }
    fclose(fp);
	return 0;
}

回顾:

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

void test01()
{
    FILE *f_write = fopen("./test01.txt","w");
    if(f_write == NULL)
    {
        return;
    }
    char *buf[] = {
        "aaaaa\n",
        "ssssss\n",
        "dddddddddd\n",
        "ffffffffff\n",
    };
    for(int i = 0; i < 4;i++)
    {
        fputs(buf[i],f_write);
    }
    fclose(f_write);
    FILE *f_read = fopen("./test01.txt","r");
    if(f_read == NULL)
    {
        return;
    }
    while(!feof(f_read))
    {
        char buf[1024] = {0};
        fgets(buf,1024,f_read);
        printf("%s",buf);

    }
    fclose(f_read);

}
int main()
{
    test01();
    return 0;
}

练习2:文件版四则运算

/*将文本文件的加减乘除四则运算解答出来*/
/*
1.封装write_file函数,将四则运算表达式写入
	FILE* fp = fopen("dsdds.txt","w");
	
2.封装read_file函数
   1)
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void write_file()
{
    FILE *fp = fopen("test.txt","w");
    if(fp == NULL)
    {
        perror("fopen error");
        getchar();
        exit(-1);
    }

    fputs("1+1=\n",fp);
    fputs("10/2=\n",fp);
    fputs("2*4=\n",fp);
    fputs("39-23=\n",fp);


    fclose(fp);
    
}
void read_file()
{
    char buf[4096] = {0};
    char result[4096] = {0};
    char sum_res[4096] = {0};
    int a,b;
    char ch;
    int ret;
    FILE *fp = fopen("test.txt","r");//获取文件指针
    if(fp == NULL)
    {
        perror("fopen error");
        return;
    }
    while(1)
    {
        fgets(buf,4096,fp);//buf = "10/2=\n\0"//读出文件内容到buf
        if(feof(fp))
        {
            break;//结束标志
        }
        sscanf(buf,"%d%c%d=\n",&a,&ch,&b);//a = 10 ch = '\'  b = 2 拆分buf字符串 
        switch(ch)
        {
            case '/':ret = a/b;break;
            case '+':ret = a+b;break;
            case '-':ret = a-b;break;
            case '*':ret = a*b;break;
        }
        sprintf(result,"%d%c%d=%d\n",a,ch,b,ret);//10/2=5   按照格式重新拼接字符串并保存在result上
        strcat(sum_res,result);//将多个表达式拼接到一个字符串
       
    }
    fclose(fp);//将只有表达式没有结果的文件关闭
    fp = fopen("test.txt","w");//重新链接文件,并清空原有
    if(fp == NULL)
    {
        perror("fopen error");
        return;
    }
    fputs(sum_res,fp);//既有表达式又有结果的内容写入文件中

}

int main()
{
    write_file();//创建文件
    read_file();//读取文件
    return 0;
}
    

sprintf()函数

char buf[1024];
sprintf(buf,"%d = %d%c%d\n",10+5,10,'+',5);//按照格式输入到buf里面
//根据参数字符串格式来转换并格式化数据,然后将结果输出到str的指定空间,直到出现字符串结束符\0为止
printf是将将键盘输入的内容刷在屏幕上   而sprintf将内容输出在缓存区中   而fprintf是将数据按照格式写入到文件

fprintf()函数

FILE *fp = fopen("text01.txt","w");
	printf("%d%c=%d\n",10,'*',7,10*7);




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


int main()
{
    FILE *fp = fopen("abc.c","w");
    if(fp == NULL)
    {   
        perror("fopen erroer");
        return -1;
    }
    fprintf(fp,"%d%c%d=%d\n",10,'*',2,20);
    fclose(fp);
    return 0;
}

sscanf()函数

int m;
scanf("%d",&m);
char str = "98";
sscanf(str,"%d",&m);//从标准输入按照格式匹配字符串相应值
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void teast01() 
{
	char* str = "12345abcd";
	char buf[1024] = {0};
	sscanf(str,"%*d%s",buf);
	printf("%s\n",buf);


}
void test02() 
{
	char* str = "abcde 12345";//忽略遇见了 代表忽略结束
	char buf[1024] = {0};
	sscanf(str,"%*s%s",buf);
	printf("%s\n",buf);
}

void test03() 
{
	char* str = "12345abcde";
	char buf[1024] = {0};
	sscanf(str,"%6s",buf);
	printf("%s\n",buf);//12345a
}

void test04() 
{
	char* str = "12345abcdea";//只要匹配失败了就不会继续匹配了
	char buf[1024] = { 0 };
	sscanf(str, "%*d%[a-c]", buf);
	printf("%s\n", buf);//abc
	
}

void test06() 
{
	char* str = "Addaadffdsa";
	char buf[1024] = {0};
	sscanf(str,"%[^a]",buf);
	printf("%s\n",buf);

}

void test07() 
{
	char* ip = "127.0.0.1";
	int num1 = 0;
	int num2 = 0;
	int num3 = 0;
	int num4 = 0;

	sscanf(ip,"%d.%d.%d.%d",&num1,&num2,&num3,&num4);
	printf("%d %d %d %d\n",num1,num2,num3,num4);
}

void test08() 
{
	char* str = "dasdfasf#zhangtao@12345";//过滤出zhangtao
	char buf[1024] = {0};
	sscanf(str,"%*[^#]#%[^@]",buf);
	printf("%s\n",buf);
}

void test09() 
{
	char* str = "hello@gmail.com";
	char buf1[1024] = {0};
	char buf2[1024] = {0};
	sscanf(str,"%[a-z]@%s",buf1,buf2);
	printf("%s\n%s\n",buf1,buf2);
}

int main() 
{
	teast01();
	test02();
	test04();
	test06();
	test07();
	test08();
	test09();
	return 0;
}

fscanf()函数

scanf -- 从键盘按照格式写入变量
sscanf -- 从字符串中匹配字符串
    
fscanf(fp,"%d%c%d=%d\n",&a,&ch,&b,&c);//从文件中按照格式匹配字符串赋予相应变量

if(feof(fp)) break;
  • 与fputs函数的区别:
char buf = {10};
printf("%s",fgets(buf,10,stdin));

练习:文件排序

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
void write_rand()
{
    FILE *fp = fopen("test.txt","w");
    if(!fp)
    {
        perror("fopen error");
        return -1;
    }
    srand(time(NULL));//随机数种子
    for(int i = 0; i < 10; i++)
    {
        fprintf(fp,"%d\n",rand()%100);
    }
    fclose(fp);
}

void BubbleSort(int A[],int n)
{
    int flag;
    for(int i = n-1;i >= 1;i--)
    {
        flag = 0;
        for(int j = 1;j<=i;j++)
        {
            if(A[j-1] > A[j])
            {
                int temp = A[j];
                A[j] = A[j-1];
                A[j-1] = temp;
                 flag = 1;
            }
        }
        if(flag == 0)
        {
            return;
        }
    }
}

void read_file()
{
    FILE *fp = fopen("test.txt","r");
    if(!fp)
    {
        perror("OPEN ERROR");
        getchar();
        exit(-1);
    }
    int A[10];
    int i = 0;
    while(1)
    {
        fscanf(fp,"%d\n",&A[i++]);
        if(feof(fp))
        {
            break;
        }

        
    }
    for(size_t i = 0;i<10;i++)
    {
        printf("%d\n",A[i]);
    }
    fclose(fp);
    BubbleSort(A,10);
    printf("=========================================\n");
    for(size_t i = 0;i<10;i++)
    {
        printf("%d\n",A[i]);
    }
    //写入文件
    fp = fopen("test.txt","w");
    if(!fp)
    {
        perror("OPEN ERROR");
        getchar();
        exit(-1);
    }
    for(int i = 0;i<10;i++)
    {
        fprintf(fp,"%d\n",A[i]);
    }
}

int main()
{
   write_rand();
   read_file();
    return 0;
}

fwrite()函数

回忆:

fgetc — fputc

fgets—fgets

fprintf – fscanf 默认处理文本文件

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
//参1:待写出数据的地址   参2:待写出数据的大小  参3:写出的个数   参4:文件 通常将参2传1 参3传实际写入字节数
//返回值:成功:个数   失败:0
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>


typedef struct student
{
    int age;
    char name[10];
    int num;
}STU;

int main()
{
    STU stu[4] = {
        18,"badi",111,
        20,"ssa",222,
        22,"eeee",333,
        23,"hggg",444,
    };
    FILE *fp = fopen("test03.txt","w");
    if(!fp)
    {
        perror("fopen error");
        getchar();
        exit(-1);
    }
    int ret = fwrite(&stu[0],1,sizeof(stu)*4,fp);
    if(ret == 0)
    {
        perror("fwrite error");
        return -1;
    }
    printf("ret = %d\n",ret);

    fclose(fp);
    return 0;
}

fread()函数

size_t fread( void *restrict buffer, size_t size, size_t count, FILE *restrict stream );
//参1:读取到数据存储的位置,参2:一次读取的字节数  参3:读取的次数  参4:文件   返回值:成功:实际读取的字节数(一般情况为参数3,当小于参数3大于0说明读到文件末尾)   失败:0,0也说明到达文件末尾,
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>

typedef struct student
{
    int age;
    char name[10];
    int num;
}STU;

void write_struct()
{
     STU stu[4] = {
        18,"badi",111,
        20,"ssa",222,
        22,"eeee",333,
        23,"hggg",444,
    };
    FILE *fp = fopen("test03.txt","w");
    if(!fp)
    {
        perror("fopen error");
        getchar();
        exit(-1);
    }
    int ret = fwrite(&stu[0],1,sizeof(stu),fp);
    if(ret == 0)
    {
        perror("fwrite error");
        return -1;
    }
    printf("ret = %d\n",ret);

    fclose(fp);
}
//一次读取一个元素
void read_struct()
{
    FILE *fp = fopen("test03.txt","r");
    if(!fp)
    {
        perror("fopen error");
        getchar();
        exit(-1);
    }
    STU s1;
    int ret = fread(&s1,sizeof(STU),1,fp);//读取1次,读取的的字节数为sizeof(STU),返回值为1就是参数3
    printf("ret == %d\n",ret);
    printf("age = %d name = %s num = %d\n",s1.age,s1.name,s1.num);
}


//读所有元素
void read_struct2()
{
    FILE *fp = fopen("test03.txt","r");
    if(!fp)
    {
        perror("fopen error");
        getchar();
        exit(-1);
    }
    STU s1[10];
    int i = 0;
    while(1)
    {
        int ret = fread(&s1[i],1,sizeof(STU),fp);
        if(ret == 0)//替代feof来判断读到文件结尾
        {
            break;
        }
        printf("age = %d name = %s num = %d\n",s1[i].age,s1[i].name,s1[i].num);
        i++;

    }
    fclose(fp);
}
int main()
{
    write_struct();
    read_struct2();
    return 0;
}

练习:

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

void test01()
{
    FILE *f_write = fopen("./test01.txt","w");
    if(f_write == NULL)
    {
        return;
    }
    char *buf[] = {
        "aaaaa\n",
        "ssssss\n",
        "dddddddddd\n",
        "ffffffffff\n",
    };
    for(int i = 0; i < 4;i++)
    {
        fputs(buf[i],f_write);
    }
    fclose(f_write);
    FILE *f_read = fopen("./test01.txt","r");
    if(f_read == NULL)
    {
        return;
    }
    while(!feof(f_read))
    {
        char buf[1024] = {0};
        fgets(buf,1024,f_read);
        printf("%s",buf);

    }
    fclose(f_read);

}
int main()
{
    test01();
    return 0;
}

练习:大文件拷贝

​ 已知一个任意类型的文件,对该文件复制产生一个相同的新文件

								1. 打开两个文件,一个“r”,一个“w”
								1. 从r中fread,fwrite到w文件中。
								1. 判断到达文件结尾
								1. 结束
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
void myFile_cp()
{
    FILE *rfp = fopen("C:\\Users\\xxxxxx","rb");//注意:拷贝二进制文件+b
    if(!rfp)
    {
        perror("rfp open error!");
        getchar();
        exit(-1);
    }
    FILE *wfp = fopen("copy.png","wb");
    if(!wfp)
    {
        perror("wfp open error");
        getchar();
        exit(-1);
    }
    char buf[128] = {0};
    while(1)
    {
        memset(buf,0,sizeof(buf));
        int ret = fread(buf,1,sizeof(buf),rfp);
        if(ret == 0)
        {
            break;
        }
        fwrite(buf,1,ret,wfp);//注意是ret,注意是写出读到的返回值
    }
    fclose(wfp);
    fclose(rfp);
}
int main()
{
    myFile_cp();
    return 0;
}

fseek()函数

文件读写指针:在一个文件内只有一个,在一个文件内只有一个。,读可能改变文件指针位置,写任然有可能改变

fseek将文件读写指针放在任意位置。

int fseek(FILE *stream, long offset, int fromwhere);
参1:文件   参2:偏移量    参3:whence  SEEK_SET:文件起始位置      SEEK_SET:当前位置    SEEK_END:文件结尾位置
返回值:成功:0 失败:-1

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


typedef struct student
{
    int age;
    char name[10];
    int num;
}STU;
int main()
{
    STU stu[4] = {
        18,"badi",111,
        20,"ssa",222,
        22,"eeee",333,
        23,"hggg",444,
    };

    FILE *fp = fopen("test03.txt","wb+");//有写有读
    if(!fp)
    {
        perror("fopen error");
        getchar();
        exit(-1);
    }
    int ret = fwrite(&stu[0],1,sizeof(stu),fp);//写四个结构体进文件
    if(ret == 0)
    {
        perror("fwrite error");
        return -1;
    }
    printf("ret = %d\n",ret);
    fseek(fp,sizeof(STU),SEEK_SET);//从文件起始位置向后偏移一个结构体
    STU s1;
    fread(&s1,1,sizeof(s1),fp);
    printf("age = %d ,name = %s, num = %d\n",s1.age,s1.name,s1.num);
    fclose(fp);
    return 0;
}

ftell()函数

返回:从文件当前读写位置到文件起始位置的偏移量

借助ftell()+fseek()来获取文件大小

int len = ftell(fp);

rewind()函数

void rewind(FILE *stream):回卷读写指针,将读写指针移到起始位置

获取文件大小

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


int main()
{
    FILE *fp = fopen("test03.txt","r");
    fseek(fp,0,SEEK_END);//将文件指针放在文件结尾
    int len = ftell(fp);
    printf("文件大小len = %d\n",len);
    fclose(fp);
    return 0;
}

LInux和Windows的区别

  1. 对于二进制文件操作,Windows需要使用“b”,Linux下二进制和文本没区别。
  2. Windows下,回车\r,换行\n.Linux下回车换行就一个\n。

文件

获取文件状态

打开文件,对于系统而言,系统资源消耗过大。

int stat(const char *path,struct stat *buf);
参1:访问文件的路径     参2:文件属性结构体    返回值:成功 0 失败-1
struct stat buf;
stat("待打开的文件",&buf);

文件缓冲区

内存中的一块区域。

标准输入–stdout–标准输出缓冲区。写给屏幕的数据,都是先存缓冲区的,由缓冲区一次性刷新到物理设备。

标准输入–stdin–标准输入缓冲区。从键盘读取的数据直接读到缓冲区中,由缓冲区给程序提供数据。

预读入、缓输出

行缓冲:printf() 遇到\n就会将缓冲区中的数据刷新到物理设备

全缓冲:文件。缓冲区存满,数据刷新到物理设备上

无缓冲:perror。缓冲区中只要有数据,立即刷新到物理设备。

手动刷新缓冲区:int fflush(FILE *stream);成功:0 失败:-1

文件关闭,缓冲区自动刷新,手动刷新:实时刷新

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值