• C语言高级


    C语言高级


    day01复习

    内存分区

    基本概念

    • 类型是对数据的抽象
    • 乐行相同的数据具有相同的表示形式
    • 程序中所有的数据都必定是数据类型
    • 数据类型可以理解为创建变量的模具,固定内存的别名
    • img

    数据类型别名

    typedef

    1. typedef

    2. typedef unsigned int u32;
      typedef struct _PERSON{
      	char name[64];
      	int age;
      }Person;
      
      void test(){
      	u32 val; //相当于 unsigned int val;
      	Person person; //相当于 struct PERSON person;
      }
      
    3. 起别名 简化struct关键字

    4. 区分数据类型

    5. 提高代码移植性

    void

    1. 对函数返回限定

    2. 对函数参数限定

    3. 不能利用void创建变量,无法给无类型变量分配内存

    4. 限定函数返回值,函数参数

    5. void * 万能指针,可以不通过强制类型转换就可以转成其他类型指针

    6. //1. void修饰函数参数和函数返回
      void test01(void){
      	printf("hello world");
      }
      
      //2. 不能定义void类型变量
      void test02(){
      	void val; //报错
      }
      
      //3. void* 可以指向任何类型的数据,被称为万能指针
      void test03(){
      	int a = 10;
      	void* p = NULL;
      	p = &a;
      	printf("a:%d\n",*(int*)p);
      	
      	char c = 'a';
      	p = &c;
      	printf("c:%c\n",*(char*)p);
      }
      
      //4. void* 常用于数据类型的封装
      void test04(){
      	//void * memcpy(void * _Dst, const void * _Src, size_t _Size);
      }
      

    sizeof

    1. 基本语法
      sizeof(变量);
      sizeof 变量;
      sizeof(类型);
      
    2. 本质:不是一个函数,是一个操作符

    3. 返回值类型 unnsigned int 无符号类型

    4. 用途:可以统计数组长度

    5. //1. sizeof基本用法
      void test01(){
      	int a = 10;
      	printf("len:%d\n", sizeof(a));
      	printf("len:%d\n", sizeof(int));
      	printf("len:%d\n", sizeof a);
      }
      
      //2. sizeof 结果类型
      void test02(){
      	unsigned int a = 10;
      	if (a - 11 < 0){
      		printf("结果小于0\n");
      	}
      	else{
      		printf("结果大于0\n");
      	}
      	int b = 5;
      	if (sizeof(b) - 10 < 0){
      		printf("结果小于0\n");
      	}
      	else{
      		printf("结果大于0\n");
      	}
      }
      
      //3. sizeof 碰到数组
      void TestArray(int arr[]){
      	printf("TestArray arr size:%d\n",sizeof(arr));
      }
      void test03(){
      	int arr[] = { 10, 20, 30, 40, 50 };
      	printf("array size: %d\n",sizeof(arr));
      
      	//数组名在某些情况下等价于指针
      	int* pArr = arr;
      	printf("arr[2]:%d\n",pArr[2]);
      	printf("array size: %d\n", sizeof(pArr));
      
      	//数组做函数函数参数,将退化为指针,在函数内部不再返回数组大小
      	TestArray(arr);
      }
      

    l 数据类型本质是固定内存大小的别名,是个模具,C语言规定:通过数据类型定义变量;

    l 数据类型大小计算(sizeof);

    l 可以给已存在的数据类型起别名typedef;

    l 数据类型的封装(void 万能类型);

    变量

    变量:既可以读又能写,一旦初始化就不能修改的对象

    • 直接修改
    • 间接修改
    • 变量定义形式: 类型 标识符, 标识符, … , 标识符

    变量名本质:一段连续内存空间别名

    通过变量名访问内存空间

    不是向变量名读写数据,而是向变量所代表的内存空间读写数据

    *修改变量的两种方式:*

    修改变量的两种方式:
        void test(){
    	
    	int a = 10;
    
    	//1. 直接修改
    	a = 20;
    	printf("直接修改,a:%d\n",a);
    
    	//2. 间接修改
    	int* p = &a;
    	*p = 30;
    
    	printf("间接修改,a:%d\n", a);
    }
    

    内存分区

    编译四步骤

    1. 预处理
    2. 编译
    3. 汇编
    4. 链接

    3段信息

    1. 代码区text
      1. 只读
    2. 数据区 data
      1. 初始化静态变量,全局静态变量和常量数据
    3. 未初始化数据区 bss
      1. 未初始化变量结合未初始化静态变量

    *总体来讲说,程序源代码被编译之后主要分成两种段:程序指令(代码区)和程序数据(数据区)。代码段属于程序指令,而数据域段和.bss段属于程序数据。*

    运行后

    ​ 程序加载到内存前,代码区混合全局区大小都是固定的,

    代码区(text segment)
    加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
    
    未初始化数据区(BSS)
    加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
    全局初始化数据区/静态数据区(data segment)
    加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
    
    栈区(stack)
    栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
    
    堆区(heap)
    堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
    

    栈区,堆区

    1. 栈 复合先进后出数据结构,编译器自动管理分配和释放,有限容量

      1. 注意事项:不要返回局部变量地址,局部变量在函数执行之后就被释放了,释放之后就没有权限操作了

      2. #char* func(){
        	char p[] = "hello world!"; //在栈区存储 乱码
        	printf("%s\n", p);
        	return p;
        }
        void test(){
        	char* p = NULL;
        	p = func();  
        	printf("%s\n",p); 
        }
        
    2. 堆 容量远远大于栈,手动开辟malloc 手动释放free

      1. 利用malloc 在堆区创建数据
      2. 利用free 释放堆区
      3. 注意 :主调函数没有分配内存,被调函数需要更高级的指针去修改低级指针,进行分配内存
    3. char* func(){
      	char* str = malloc(100);
      	strcpy(str, "hello world!");
      	printf("%s\n",str);
      	return str;
      }
      
      void test01(){
      	char* p = NULL;
      	p = func();
      	printf("%s\n",p);
      }
      
      void allocateSpace(char* p){
      	p = malloc(100);
      	strcpy(p, "hello world!");
      	printf("%s\n", p);
      }
      
      void test02(){
      	
      	char* p = NULL;
      	allocateSpace(p);
      
      	printf("%s\n", p);
      }
      

    image-20220410113146663

    堆分配内存API

    堆分配内存API:
    #include <stdlib.h>
    void *calloc(size_t nmemb, size_t size);
    功能:
    在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存	置0。
    参数:
    nmemb:所需内存单元数量
    size:每个内存单元的大小(单位:字节)
    返回值:
    	成功:分配空间的起始地址
    失败:NULL
    
    #include <stdlib.h>
    void *realloc(void *ptr, size_t size);
    功能:
    重新分配用malloc或者calloc函数在堆中分配内存空间的大小。
    realloc不会自动清理增加的内存,需要手动清理,如果指定的地址后面有连续的空间,那么就会在已有地址基础上增加内存,如果指定的地址后面没有空间,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内存,同时释放旧内存。
    参数:
    ptr:为之前用malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致
    size:为重新分配内存的大小, 单位:字节
    返回值:
    成功:新分配的堆内存地址
    失败:NULL
    示例代码:
    void test01(){
    	
    	int* p1 = calloc(10,sizeof(int));
    	if (p1 == NULL){
    		return;
    	}
    	for (int i = 0; i < 10; i ++){
    		p1[i] = i + 1;
    	}
    	for (int i = 0; i < 10; i++){
    		printf("%d ",p1[i]);
    	}
    	printf("\n");
    	free(p1);
    }
    
    void test02(){
    	int* p1 = calloc(10, sizeof(int));
    	if (p1 == NULL){
    		return;
    	}
    	for (int i = 0; i < 10; i++){
    		p1[i] = i + 1;
    	}
    
    	int* p2 = realloc(p1, 15 * sizeof(int));
    	if (p2 == NULL){
    		return;
    	}
    
    	printf("%d\n", p1);
    	printf("%d\n", p2);
    
    	//打印
    	for (int i = 0; i < 15; i++){
    		printf("%d ", p2[i]);
    	}
    	printf("\n");
    
    	//重新赋值
    	for (int i = 0; i < 15; i++){
    		p2[i] = i + 1;
    	}
    	
    	//再次打印
    	for (int i = 0; i < 15; i++){
    		printf("%d ", p2[i]);
    	}
    	printf("\n");
    
    	free(p2);
    }
    

    全局/静态

    全局静态区内变量在编译阶段已经分配好内存空间并初始化,这块内存你在程序运行器件一直存在,它主要存储全局变量,静态变量和常量

    静态区不存在未初始化的变量

    局部常变量放在栈,可以通过指针或引用进行修改,全局不能修改

    字符串存在全局/静态存储常量区

    int v1 = 10;//全局/静态区
    const int v2 = 20; //常量,一旦初始化,不可修改
    static int v3 = 20; //全局/静态区
    char *p1; //全局/静态区,编译器默认初始化为NULL
    
    //那么全局static int 和 全局int变量有什么区别?
    
    void test(){
    	static int v4 = 20; //全局/静态区
    }
    
    char* func(){
    	static char arr[] = "hello world!"; //在静态区存储 可读可写
    	arr[2] = 'c';
    	char* p = "hello world!"; //全局/静态区-字符串常量区 
    	//p[2] = 'c'; //只读,不可修改 
    	printf("%d\n",arr);
    	printf("%d\n",p);
    	printf("%s\n", arr);
    	return arr;
    }
    void test(){
    	char* p = func();
    	printf("%s\n",p);
    }
    

    static 和extern区别

    特点在运行前分配内存,程序运行结束,生命周期结束,在文件内都可以使用静态变量

    extern 可以提高变量作用域

    总结

    image-20220410115114285

    常量

    1. const 修饰的常量
    2. 全局变量
      1. 直接修改失败,间接修改语法通过,运行失败,受到常量区保护
    3. 局部变量
      1. 直接修改 失败,间接修改,成功,放在栈区

    字符串变量

    vs将多个相同字符串量看成一个

    不可以修改字符串常量

    ANSI并没有制定出字符串是否可以修改的标准,根据编译器不同,可能最终结果不同

    day02

    宏函数

    1. define MYADD(x,y) ((x) +(y))

    2. 将一些频繁短小的函数,,写成宏函数
    3. 宏函数优点:以空间换时间
    4. 普通函数有入栈,出栈时间开销

    函数调用流程

    局部变量,函数形参,函数返回地址,入栈出栈

    一个函数调用过程需要以下几个方面

    函数返回地址,函数的参数,临时变量,,保存的上下文,包括在函数调用前后保持不变的寄存器

    img

    int func(int a,int b){
    	int t_a = a;
    	int t_b = b;
    	return t_a + t_b;
    }
    
    int main(){
    	int ret = 0;
    	ret = func(10, 20);
    	return EXIT_SUCCESS;
    }
    

    image

    调用惯例

    主调函数和被调函数必须要保持一致约定,才能正确调用函数,

    调用惯例包含,出栈方,参数传递顺序,参数名称修饰

    C/C++下默认调动惯例, cdecl 从右到左,主调用函数管理出栈

    *注意: cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute_((cdecl)).*

    image-20220412111748536image-20220412111803671

    栈的生长方向和内存存放方向

    img

    //1. 栈的生长方向
    void test01(){
    
    	int a = 10;
    	int b = 20;
    	int c = 30;
    	int d = 40;
    
    	printf("a = %d\n", &a);
    	printf("b = %d\n", &b);
    	printf("c = %d\n", &c);
    	printf("d = %d\n", &d);
    
    	//a的地址大于b的地址,故而生长方向向下
    }
    
    //2. 内存生长方向(小端模式)
    void test02(){
    	
    	//高位字节 -> 地位字节
    	int num = 0xaabbccdd;
    	unsigned char* p = &num;
    
    	//从首地址开始的第一个字节
    	printf("%x\n",*p);
    	printf("%x\n", *(p + 1));
    	printf("%x\n", *(p + 2));
    	printf("%x\n", *(p + 3));
    }
    

    image

    栈底--高地址

    栈顶--低地址

    内存存放方向

    高位字节--高地址

    低位字节--低地址

    小端对齐

    指针

    指针是一种数据类型,占用内存空间,用来保存内存地址

    void test01(){
    	
    	int* p1 = 0x1234;
    	int*** p2 = 0x1111;
    
    	printf("p1 size:%d\n",sizeof(p1));
    	printf("p2 size:%d\n",sizeof(p2));
    
    
    	//指针是变量,指针本身也占内存空间,指针也可以被赋值
    	int a = 10;
    	p1 = &a;
    
    	printf("p1 address:%p\n", &p1);
    	printf("p1 address:%p\n", p1);
    	printf("a address:%p\n", &a);
    
    }
    

    野指针与空指针

    标准定义了NULL指针,作为一种特殊的指针变量,表示不指向任何东西,要使一个指针为NULL,可以给它赋值一个零值,

    空指针:不能向NULL或者非法内存拷贝数据

    野指针

    • 指针变量未初始化
    • 指针释放后未置空
    • 指针操作超越变量作用域
    • 空指针可以重复释放,野指针不可以重复释放

    指针的步长

    指针是指向内存空间的数据类型,指向的内存空间决定了指针的步长,,指针的步长指的是,指针+1时候,移动多少字节单位

    解引用,解出字节数

    自定义结构体做步长练习

    通过offsetof(结构体名称,属性) 找到属性对应的偏移量

    offsetof 引入头文件 #include<stddef.h>

    image

    day03

    指针间接赋值

    间接赋值成立的三大条件

    • 2个变量(一个普通变量,一个指针变量,或一个实参一个形参)
    • 建立关系
    • 通过*操作指针指向内存
    void test(){
    	int a = 100;	//两个变量
    	int *p = NULL;
    	//建立关系
    	//指针指向谁,就把谁的地址赋值给指针
    	p = &a;
    	//通过*操作内存
    	*p = 22;
    }
    

    定义指针变量

    void test(){
    	int b;  
    	int *q = &b; //0级指针
    	int **t = &q;
    	int ***m = &t;
    }
    

    间接赋值0-->1

    int func1(){ return 10; }
    
    void func2(int a){
    	a = 100;
    }
    //指针的意义_间接赋值
    void test02(){
    	int a = 0;
    	a = func1();
    	printf("a = %d\n", a);
    
    	//为什么没有修改?
    	func2(a);
    	printf("a = %d\n", a);
    }
    
    //指针的间接赋值
    void func3(int* a){
    	*a = 100;
    }
    
    void test03(){
    	int a = 0;
    	a = func1();
    	printf("a = %d\n", a);
    
    	//修改
    	func3(&a);
    	printf("a = %d\n", a);
    }
    

    间接复制 1-->2

    void AllocateSpace(char** p){
    	*p = (char*)malloc(100);
    	strcpy(*p, "hello world!");
    }
    
    void FreeSpace(char** p){
    
    	if (p == NULL){
    		return;
    	}
    	if (*p != NULL){
    		free(*p);
    		*p = NULL;
    	}
    
    }
    
    void test(){
    	
    	char* p = NULL;
    
    	AllocateSpace(&p);
    	printf("%s\n",p);
    	FreeSpace(&p);
    
    	if (p == NULL){
    		printf("p内存释放!\n");
    	}
    }
    
    用1级指针形参,去间接修改了0级指针(实参)的值。
    用2级指针形参,去间接修改了1级指针(实参)的值。
    用3级指针形参,去间接修改了2级指针(实参)的值。
    用n级指针形参,去间接修改了n-1级指针(实参)的值。
    

    指针做函数参数

    • 输入:主调函数分配内存

      • 分配在栈上和堆区

      • void fun(char *p /* in */)
        {
        	//给p指向的内存区域拷贝内容
        	strcpy(p, "abcddsgsd");
        }
        
        void test(void)
        {
        	//输入,主调函数分配内存
        	char buf[100] = { 0 };
        	fun(buf);
        	printf("buf  = %s\n", buf);
        }
        
    • 输出:被调用函数分配内存

      • 在被调用函数分配内存,主调函数使用

      • void fun(char **p /* out */, int *len)
        {
        	char *tmp = (char *)malloc(100);
        	if (tmp == NULL)
        	{
        		return;
        	}
        	strcpy(tmp, "adlsgjldsk");
        
        	//间接赋值
        	*p = tmp;
        	*len = strlen(tmp);
        }
        
        void test(void)
        {
        	//输出,被调用函数分配内存,地址传递
        	char *p = NULL;
        	int len = 0;
        	fun(&p, &len);
        	if (p != NULL)
        	{
        		printf("p = %s, len = %d\n", p, len);
        	}
        
        

    字符串强化训练

    //字符串基本操作
    //字符串是以0或者'\0'结尾的字符数组,(数字0和字符'\0'等价)
    void test01(){
    
    	//字符数组只能初始化5个字符,当输出的时候,从开始位置直到找到0结束
    	char str1[] = { 'h', 'e', 'l', 'l', 'o' };
    	printf("%s\n",str1);
    
    	//字符数组部分初始化,剩余填0
    	char str2[100] = { 'h', 'e', 'l', 'l', 'o' };
    	printf("%s\n", str2);
    
    	//如果以字符串初始化,那么编译器默认会在字符串尾部添加'\0'
    	char str3[] = "hello";
    	printf("%s\n",str3);
    	printf("sizeof str:%d\n",sizeof(str3));
    	printf("strlen str:%d\n",strlen(str3));
    
    	//sizeof计算数组大小,数组包含'\0'字符
    	//strlen计算字符串的长度,到'\0'结束
    
    	//那么如果我这么写,结果是多少呢?
    	char str4[100] = "hello";
    	printf("sizeof str:%d\n", sizeof(str4));
    	printf("strlen str:%d\n", strlen(str4));
    
    	//请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
    	char str5[] = "hello\0world"; 
    	printf("%s\n",str5);
    	printf("sizeof str5:%d\n",sizeof(str5));
    	printf("strlen str5:%d\n",strlen(str5));
    
    	//再请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
    	char str6[] = "hello\012world";
    	printf("%s\n", str6);
    	printf("sizeof str6:%d\n", sizeof(str6));
    	printf("strlen str6:%d\n", strlen(str6));
    }
    

    字符串结束标志\0

    sizeof strllen

    拷贝字符串

    • 利用[]

    • 利用指针

    • 1.1.1 while (*dest++ = *src++){}

    • //拷贝方法1
      void copy_string01(char* dest, char* source ){
      
      	for (int i = 0; source[i] != '\0';i++){
      		dest[i] = source[i];
      	}
      
      }
      
      //拷贝方法2
      void copy_string02(char* dest, char* source){
      	while (*source != '\0' /* *source != 0 */){
      		*dest = *source;
      		source++;
      		dest++;
      	}
      }
      
      //拷贝方法3
      void copy_string03(char* dest, char* source){
      	//判断*dest是否为0,0则退出循环
      	while (*dest++ = *source++){}
      }
      
      
      

    翻转字符串

    • 利用[]

    • 利用指针

    • image-20220414135407914

    • void reverse_string(char* str){
      
      	if (str == NULL){
      		return;
      	}
      
      	int begin = 0;
      	int end = strlen(str) - 1;
      	
      	while (begin < end){
      		
      		//交换两个字符元素
      		char temp = str[begin];
      		str[begin] = str[end];
      		str[end] = temp;
      
      		begin++;
      		end--;
      	}
      
      }
      
      void test(){
      	char str[] = "abcdefghijklmn";
      	printf("str:%s\n", str);
      	reverse_string(str);
      	printf("str:%s\n", str);
      }
      

    字符串格式化

    sprintf

    • 格式化字符串
    • sprintf(目标字符串,格式化内容,占位参数)
    • 返回值:有效字符串长度
    #include <stdio.h>
    int sprintf(char *str, const char *format, ...);
    功能:
         根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到    出现字符串结束符 '\0'  为止。
    参数: 
    	str:字符串首地址
    	format:字符串格式,用法和printf()一样
    返回值:
    	成功:实际格式化的字符个数
    	失败: - 1
        
    void test(){
    	
    	//1. 格式化字符串
    	char buf[1024] = { 0 };
    	sprintf(buf, "你好,%s,欢迎加入我们!", "John");
    	printf("buf:%s\n",buf);
    
    	memset(buf, 0, 1024);
    	sprintf(buf, "我今年%d岁了!", 20);
    	printf("buf:%s\n", buf);
    
    	//2. 拼接字符串
    	memset(buf, 0, 1024);
    	char str1[] = "hello";
    	char str2[] = "world";
    	int len = sprintf(buf,"%s %s",str1,str2);
    	printf("buf:%s len:%d\n", buf,len);
    
    	//3. 数字转字符串
    	memset(buf, 0, 1024);
    	int num = 100;
    	sprintf(buf, "%d", num);
    	printf("buf:%s\n", buf);
    	//设置宽度 右对齐
    	memset(buf, 0, 1024);
    	sprintf(buf, "%8d", num);
    	printf("buf:%s\n", buf);
    	//设置宽度 左对齐
    	memset(buf, 0, 1024);
    	sprintf(buf, "%-8d", num);
    	printf("buf:%s\n", buf);
    	//转成16进制字符串 小写
    	memset(buf, 0, 1024);
    	sprintf(buf, "0x%x", num);
    	printf("buf:%s\n", buf);
    
    	//转成8进制字符串
    	memset(buf, 0, 1024);
    	sprintf(buf, "0%o", num);
    	printf("buf:%s\n", buf);
    }
        
        
        
    

    sscanf

    #include <stdio.h>
    int sscanf(const char *str, const char *format, ...);
    功能:
        从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。
    参数:
    	str:指定的字符串首地址
    	format:字符串格式,用法和scanf()一样
    返回值:
    	成功:成功则返回参数数目,失败则返回-1
    	失败: - 1
    

    image-20220414135523438

    //1. 跳过数据
    void test01(){
    	char buf[1024] = { 0 };
    	//跳过前面的数字
    	//匹配第一个字符是否是数字,如果是,则跳过
    	//如果不是则停止匹配
    	sscanf("123456aaaa", "%*d%s", buf); 
    	printf("buf:%s\n",buf);
    }
    
    //2. 读取指定宽度数据
    void test02(){
    	char buf[1024] = { 0 };
    	//跳过前面的数字
    	sscanf("123456aaaa", "%7s", buf);
    	printf("buf:%s\n", buf);
    }
    
    //3. 匹配a-z中任意字符
    void test03(){
    	char buf[1024] = { 0 };
    	//跳过前面的数字
    	//先匹配第一个字符,判断字符是否是a-z中的字符,如果是匹配
    	//如果不是停止匹配
    	sscanf("abcdefg123456", "%[a-z]", buf);
    	printf("buf:%s\n", buf);
    }
    
    //4. 匹配aBc中的任何一个
    void test04(){
    	char buf[1024] = { 0 };
    	//跳过前面的数字
    	//先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
    	sscanf("abcdefg123456", "%[aBc]", buf);
    	printf("buf:%s\n", buf);
    }
    
    //5. 匹配非a的任意字符
    void test05(){
    	char buf[1024] = { 0 };
    	//跳过前面的数字
    	//先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
    	sscanf("bcdefag123456", "%[^a]", buf);
    	printf("buf:%s\n", buf);
    }
    
    //6. 匹配非a-z中的任意字符
    void test06(){
    	char buf[1024] = { 0 };
    	//跳过前面的数字
    	//先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
    	sscanf("123456ABCDbcdefag", "%[^a-z]", buf);
    	printf("buf:%s\n", buf);
    }
    

    指针易错点

    越界

    void test(){
    	char buf[3] = "abc";
    	printf("buf:%s\n",buf);
    }
    

    指针叠加会不断改变指针方向

    void test(){
    	char *p = (char *)malloc(50);
    	char buf[] = "abcdef";
    	int n = strlen(buf);
    	int i = 0;
    
    	for (i = 0; i < n; i++)
    	{
    		*p = buf[i];
    		p++; //修改原指针指向
    	}
    
    	free(p);
    }
    

    返回局部变量

    char *get_str()
    {
    	char str[] = "abcdedsgads"; //栈区,
    	printf("[get_str]str = %s\n", str);
    	return str;
    }
    

    同一块内存释放多次(不可以释放野指针)

    void test(){	
    	char *p = NULL;
    
    	p = (char *)malloc(50);
    	strcpy(p, "abcdef");
    
    	if (p != NULL)
    	{
    		//free()函数的功能只是告诉系统 p 指向的内存可以回收了
    		// 就是说,p 指向的内存使用权交还给系统
    		//但是,p的值还是原来的值(野指针),p还是指向原来的内存
    		free(p); 
    	}
    
    	if (p != NULL)
    	{
    		free(p);
    	}
    }
    

    const使用

    const使用,修饰形参,防止误操作

    //const修饰变量
    void test01(){
    	//1. const基本概念
    	const int i = 0;
    	//i = 100; //错误,只读变量初始化之后不能修改
    
    	//2. 定义const变量最好初始化
    	const int j;
    	//j = 100; //错误,不能再次赋值
    
    	//3. c语言的const是一个只读变量,并不是一个常量,可通过指针间接修改
    	const int k = 10;
    	//k = 100; //错误,不可直接修改,我们可通过指针间接修改
    	printf("k:%d\n", k);
    	int* p = &k;
    	*p = 100;
    	printf("k:%d\n", k);
    }
    
    //const 修饰指针
    void test02(){
    
    	int a = 10;
    	int b = 20;
    	//const放在*号左侧 修饰p_a指针指向的内存空间不能修改,但可修改指针的指向
    	const int* p_a = &a;
    	//*p_a = 100; //不可修改指针指向的内存空间
    	p_a = &b; //可修改指针的指向
    
    	//const放在*号的右侧, 修饰指针的指向不能修改,但是可修改指针指向的内存空间
    	int* const p_b = &a;
    	//p_b = &b; //不可修改指针的指向
    	*p_b = 100; //可修改指针指向的内存空间
    
    	//指针的指向和指针指向的内存空间都不能修改
    	const int* const p_c = &a;
    }
    //const指针用法
    struct Person{
    	char name[64];
    	int id;
    	int age;
    	int score;
    };
    
    //每次都对对象进行拷贝,效率低,应该用指针
    void printPersonByValue(struct Person person){
    	printf("Name:%s\n", person.name);
    	printf("Name:%d\n", person.id);
    	printf("Name:%d\n", person.age);
    	printf("Name:%d\n", person.score);
    }
    
    //但是用指针会有副作用,可能会不小心修改原数据
    void printPersonByPointer(const struct Person *person){
    	printf("Name:%s\n", person->name);
    	printf("Name:%d\n", person->id);
    	printf("Name:%d\n", person->age);
    	printf("Name:%d\n", person->score);
    }
    void test03(){
    	struct Person p = { "Obama", 1101, 23, 87 };
    	//printPersonByValue(p);
    	printPersonByPointer(&p);
    }
    

    二级指针做函数参数输入特性

    被调函数分配内存,主调函数使用

    int a = 12;

    int *b = &a

    image-20220414141834201

    c= &b

    img

    声明 **c = &b;

    二级指针做形参输出特性

    二级指针做参数的输出特性是指由被调函数分配内存

    //被调函数,由参数n确定分配多少个元素内存
    void allocate_space(int **arr,int n){
    	//堆上分配n个int类型元素内存
    	int *temp = (int *)malloc(sizeof(int)* n);
    	if (NULL == temp){
    		return;
    	}
    	//给内存初始化值
    	int *pTemp = temp;
    	for (int i = 0; i < n;i ++){
    		//temp[i] = i + 100;
    		*pTemp = i + 100;
    		pTemp++;
    	}
    	//指针间接赋值
    	*arr = temp;
    }
    //打印数组
    void print_array(int *arr,int n){
    	for (int i = 0; i < n;i ++){
    		printf("%d ",arr[i]);
    	}
    	printf("\n");
    }
    //二级指针输出特性(由被调函数分配内存)
    void test(){
    	int *arr = NULL;
    	int n = 10;
    	//给arr指针间接赋值
    	allocate_space(&arr,n);
    	//输出arr指向数组的内存
    	print_array(arr, n);
    	//释放arr所指向内存空间的值
    	if (arr != NULL){
    		free(arr);
    		arr = NULL;
    	}
    }
    

    二级指针做形参输入特性是由主调函数分配内存

    //打印数组
    void print_array(int **arr,int n){
    	for (int i = 0; i < n;i ++){
    		printf("%d ",*(arr[i]));
    	}
    	printf("\n");
    }
    //二级指针输入特性(由主调函数分配内存)
    void test(){
    	
    	int a1 = 10;
    	int a2 = 20;
    	int a3 = 30;
    	int a4 = 40;
    	int a5 = 50;
    
    	int n = 5;
    
    	int** arr = (int **)malloc(sizeof(int *) * n);
    	arr[0] = &a1;
    	arr[1] = &a2;
    	arr[2] = &a3;
    	arr[3] = &a4;
    	arr[4] = &a5;
    
    	print_array(arr,n);
    
    	free(arr);
    	arr = NULL;
    }
    

    多级指针

    //分配内存
    void allocate_memory(char*** p, int n){
    
    	if (n < 0){
    		return;
    	}
    
    	char** temp = (char**)malloc(sizeof(char*)* n);
    	if (temp == NULL){
    		return;
    	}
    
    	//分别给每一个指针malloc分配内存
    	for (int i = 0; i < n; i++){
    		temp[i] = malloc(sizeof(char)* 30);
    		sprintf(temp[i], "%2d_hello world!", i + 1);
    	}
    
    	*p = temp;
    }
    
    //打印数组
    void array_print(char** arr, int len){
    	for (int i = 0; i < len; i++){
    		printf("%s\n", arr[i]);
    	}
    	printf("----------------------\n");
    }
    
    //释放内存
    void free_memory(char*** buf, int len){
    	if (buf == NULL){
    		return;
    	}
    
    	char** temp = *buf;
    
    	for (int i = 0; i < len; i++){
    		free(temp[i]);
    		temp[i] = NULL;
    	}
    
    	free(temp);
    }
    
    void test(){
    
    	int n = 10;
    	char** p = NULL;
    	allocate_memory(&p, n);
    	//打印数组
    	array_print(p, n);
    	//释放内存
    	free_memory(&p, n);
    }
    

    位运算

    按位取反  ~  0变1  1 变0
    
    按位与  &   全1为1  一0为0
    
    按位或  |   全0为0  一1为1
    
    按位异或 ^   相同为0  不同为1
    
    位移运算
    左移运算 <<   X  乘以2 ^ X
    右移运算 >>   X  除以 2 ^X 
    有些机器用0填充高位
    有些机器用1填充高位
    如果是无符号,都是用0填充
    

    day04


    一维数组

    元素类型:数组是相同类型变量有序集合

    内存角度:连续一大片空间img

    除了两种特殊情况外,都是指向数组第一个元素的指针

    特殊情况1:sizeof 统计数组长度,返回的是整个数组长度

    特殊情况2:对数组名取地址,数组指针步长是整个数组长度

    数组名是&操作符的操作数,返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量

    int arr[10];
    //arr = NULL; //arr作为指针常量,不可修改
    int *p = arr; //此时arr作为指针常量来使用
    printf("sizeof(arr):%d\n", sizeof(arr)); //此时sizeof结果为整个数组的长度
    printf("&arr type is %s\n", typeid(&arr).name()); //int(*)[10]而不是int*
    

    数组名是指针常量,指针的指向是不可以修改的,而指针的指向的值是可以修改的

    下标引用传参数时,int arr[] 可读性更高

    数组索引下标可以是负数

    数组指针定义方式

    1. 先定义处数组类型,再通过类型定义数组指针变量

    typedef int(ARRARY_TYPE)[5];//ARRARY_TYPE 代表存放5个int类型元素的数组 的数组类型

    1. 先定义数组指针类型,再通过类型定义数组指针变量

    typedef int(*ARRARY_TYPE)[5];

    1. 直接定义数组指针变量

    int(* p )[5] = &arr;

    //方式一
    void test01(){
    
    	//先定义数组类型,再用数组类型定义数组指针
    	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    	//有typedef是定义类型,没有则是定义变量,下面代码定义了一个数组类型ArrayType
    	typedef int(ArrayType)[10];
    	//int ArrayType[10]; //定义一个数组,数组名为ArrayType
    
    	ArrayType myarr; //等价于 int myarr[10];
    	ArrayType* pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr
    	for (int i = 0; i < 10;i++){
    		printf("%d ",(*pArr)[i]);
    	}
    	printf("\n");
    }
    
    //方式二
    void test02(){
    
    	int arr[10];
    	//定义数组指针类型
    	typedef int(*ArrayType)[10];
    	ArrayType pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr
    	for (int i = 0; i < 10; i++){
    		(*pArr)[i] = i + 1;
    	}
    	for (int i = 0; i < 10; i++){
    		printf("%d ", (*pArr)[i]);
    	}
    	printf("\n");
    
    }
    
    //方式三
    void test03(){
    	
    	int arr[10];
    	int(*pArr)[10] = &arr;
    
    	for (int i = 0; i < 10; i++){
    		(*pArr)[i] = i + 1;
    
    	}
    	for (int i = 0; i < 10; i++){
    		printf("%d ", (*pArr)[i]);
    	}
    	printf("\n");
    }
    

    二维数组名

    二维数组名除了两种特殊情况以外,是指向第一个一维数组,数组指针两种特殊情况,

    sizeof 统计二维数组大小

    对数组取名称地址 int(*p)[3] [3] = &arr

    二维数组做函数参数

    //void printArray(int (*array)[3], int row, int col)

    //void printArray(int array[][3], int row ,int col)

    void printArray(int array[3][3], int row ,int col) 可读性比较高

    数组指针和指针数组

    数组指针:指向数组的指针

    指针数组:由指针组成数组

    指针数组排序

    选择排序

    从小到大

    开始认定最小值下标i 从 j = i+1 的位置起找真实最小值下标,如果计算真实最小值下标与i 不等,互换元素

    利用选择排序实现指针数组从大到小排序

    字符串对比

    if ( strcmp(pArr[max],pArr[j]) == -1)

    选择排序

    选择排序算法思路图

    结构体属性在栈上

    结构体属性在栈上

    栈区指针数组

    //数组做函数函数,退化为指针
    void array_sort(char** arr,int len){
    
    	for (int i = 0; i < len; i++){
    		for (int j = len - 1; j > i; j --){
    			//比较两个字符串
    			if (strcmp(arr[j-1],arr[j]) > 0){
    				char* temp = arr[j - 1];
    				arr[j - 1] = arr[j];
    				arr[j] = temp;
    			}
    		}
    	}
    
    }
    
    //打印数组
    void array_print(char** arr,int len){
    	for (int i = 0; i < len;i++){
    		printf("%s\n",arr[i]);
    	}
    	printf("----------------------\n");
    }
    
    void test(){
    	
    	//主调函数分配内存
    	//指针数组
    	char* p[] = { "bbb", "aaa", "ccc", "eee", "ddd"};
    	//char** p = { "aaa", "bbb", "ccc", "ddd", "eee" }; //错误
    	int len = sizeof(p) / sizeof(char*);
    	//打印数组
    	array_print(p, len);
    	//对字符串进行排序
    	array_sort(p, len);
    	//打印数组
    	array_print(p, len);
    }
    

    堆区指针数组

    //分配内存
    char** allocate_memory(int n){
    	
    	if (n < 0 ){
    		return NULL;
    	}
    
    	char** temp = (char**)malloc(sizeof(char*) * n);
    	if (temp == NULL){
    		return NULL;
    	}
    
    	//分别给每一个指针malloc分配内存
    	for (int i = 0; i < n; i ++){
    		temp[i] = malloc(sizeof(char)* 30);
    		sprintf(temp[i], "%2d_hello world!", i + 1);
    	}
    
    	return temp;
    }
    
    //打印数组
    void array_print(char** arr,int len){
    	for (int i = 0; i < len;i++){
    		printf("%s\n",arr[i]);
    	}
    	printf("----------------------\n");
    }
    
    //释放内存
    void free_memory(char** buf,int len){
    	if (buf == NULL){
    		return;
    	}
    	for (int i = 0; i < len; i ++){
    		free(buf[i]);
    		buf[i] = NULL;
    	}
    
    	free(buf);
    }
    
    void test(){
    	
    	int n = 10;
    	char** p = allocate_memory(n);
    	//打印数组
    	array_print(p, n);
    	//释放内存
    	free_memory(p, n);
    }
    
    

    二维数组线性存储特性

    void PrintArray(int* arr, int len){
    	for (int i = 0; i < len; i++){
    		printf("%d ", arr[i]);
    	}
    	printf("\n");
    }
    
    //二维数组的线性存储
    void test(){
    	int arr[][3] = {
    		{ 1, 2, 3 },
    		{ 4, 5, 6 },
    		{ 7, 8, 9 }
    	};
    
    	int arr2[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    	int len = sizeof(arr2) / sizeof(int);
    
    	//如何证明二维数组是线性的?
    	//通过将数组首地址指针转成Int*类型,那么步长就变成了4,就可以遍历整个数组
    	int* p = (int*)arr;
    	for (int i = 0; i < len; i++){
    		printf("%d ", p[i]);
    	}
    	printf("\n");
    
    	PrintArray((int*)arr, len);
    	PrintArray((int*)arr2, len);
    }
    

    二维数组3中国形式参数

    //二维数组的第一种形式
    void PrintArray01(int arr[3][3]){
    	for (int i = 0; i < 3; i++){
    		for (int j = 0; j < 3; j++){
    			printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
    		}
    	}
    }
    
    //二维数组的第二种形式
    void PrintArray02(int arr[][3]){
    	for (int i = 0; i < 3; i++){
    		for (int j = 0; j < 3; j++){
    			printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
    		}
    	}
    }
    
    //二维数组的第二种形式
    void PrintArray03(int(*arr)[3]){
    	for (int i = 0; i < 3; i++){
    		for (int j = 0; j < 3; j++){
    			printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
    		}
    	}
    }
    
    void test(){
    	
    	int arr[][3] = { 
    		{ 1, 2, 3 },
    		{ 4, 5, 6 },
    		{ 7, 8, 9 }
    	};
    	
    	PrintArray01(arr);
    	PrintArray02(arr);
    	PrintArray03(arr);
    }
    

    day05

    结构体

    加typedef 可以给结构体起别名

    不加typedef ,可以直接创建建一个结构体变量

    结构体声明 可以是匿名

    在栈区上创建和堆区创建结构体

    在栈区和堆区上创建结构体和变量数组

    结构体

    结构体类型定义

    struct Person{
    	char name[64];
    	int age;
    };
    
    typedef struct _PERSON{
    	char name[64];
    	int age;
    }Person;
    
    // 
    

    定义结构体不要直接给成员赋值,结构体是一个类型,只有根据类型定义变量时候,才能分配空间,

    结构体变量

    struct Person{
    	char name[64];
    	int age;
    }p1; //定义类型同时定义变量
    
    
    struct{
    	char name[64];
    	int age;
    }p2; //定义类型同时定义变量
    
    
    struct Person p3; //通过类型直接定义
    

    结构体变量初始化

    struct Person{
    	char name[64];
    	int age;
    }p1 = {"john",10}; //定义类型同时初始化变量
    
    struct{
    	char name[64];
    	int age;
    }p2 = {"Obama",30}; //定义类型同时初始化变量
    
    struct Person p3 = {"Edward",33}; //通过类型直接定义
    

    结构体成员使用

    struct Person{
    	char name[64];
    	int age;
    };
    void test(){
    	//在栈上分配空间
    	struct Person p1;
    	strcpy(p1.name, "John");
    	p1.age = 30;
    	//如果是普通变量,通过点运算符操作结构体成员
    	printf("Name:%s Age:%d\n", p1.name, p1.age);
    
    	//在堆上分配空间
    	struct Person* p2 = (struct Person*)malloc(sizeof(struct Person));
    	strcpy(p2->name, "Obama");
    	p2->age = 33;
    	//如果是指针变量,通过->操作结构体成员
    	printf("Name:%s Age:%d\n", p2->name, p2->age);
    }
    

    结构体赋值

    相同的两个结构体变量可以相互赋值,把一个结构体变量值拷贝另一个结构体,这两个变量还是两个独立的变量

    struct Person{
    	char name[64];
    	int age;
    };
    
    void test(){
    	//在栈上分配空间
    	struct Person p1 = { "John" , 30};
    	struct Person p2 = { "Obama", 33 };
    	printf("Name:%s Age:%d\n", p1.name, p1.age);
    	printf("Name:%s Age:%d\n", p2.name, p2.age);
    	//将p2的值赋值给p1
    	p1 = p2;
    	printf("Name:%s Age:%d\n", p1.name, p1.age);
    	printf("Name:%s Age:%d\n", p2.name, p2.age);
    }
    

    结构体深拷贝浅拷贝

    系统提供赋值操作是浅拷贝---简单值拷贝,逐字节拷贝

    如果结构体中有属性,创建在堆区,就会出现问题,在释放期间,一段内存重复释放

    有内存泄露

    解决方案:自动手动去做赋值操作,提供深拷贝

    结构体嵌套一级指针练习

    1. 在堆区创建一个,结构体指针数组

    malloc(sizeof(struct Person *) *3 )

    1. 在堆区创建处结构体变量

    malloc(sizeof(struct Person))

    1. 在堆区创建出具体姓名

    malloc(sizeof(char )*64);

    结构体属性在堆区,,深浅拷贝问题

    结构体属性在堆区-深浅拷贝问题

    打印数组释放数组

    //一个老师有N个学生
    typedef struct _TEACHER{
    	char* name;
    }Teacher;
    
    
    void test(){
    	
    	Teacher t1;
    	t1.name = malloc(64);
    	strcpy(t1.name , "John");
    
    	Teacher t2;
    	t2 = t1;
    
    	//对手动开辟的内存,需要手动拷贝
    	t2.name = malloc(64);
    	strcpy(t2.name, t1.name);
    
    	if (t1.name != NULL){
    		free(t1.name);
    		t1.name = NULL;
    	}
    	if (t2.name != NULL){
    		free(t2.name);
    		t1.name = NULL;
    	}
    }
    

    结构体嵌套一级指针练习

    结构体嵌套一级指针练习

    结构体数组

    struct Person{
    	char name[64];
    	int age;
    };
    
    void test(){
    	//在栈上分配空间
    	struct Person p1[3] = {
    		{ "John", 30 },
    		{ "Obama", 33 },
    		{ "Edward", 25}
    	};
    
    	struct Person p2[3] = { "John", 30, "Obama", 33, "Edward", 25 };
    	for (int i = 0; i < 3;i ++){
    		printf("Name:%s Age:%d\n",p1[i].name,p1[i].age);
    	}
    	printf("-----------------\n");
    	for (int i = 0; i < 3; i++){
    		printf("Name:%s Age:%d\n", p2[i].name, p2[i].age);
    	}
    	printf("-----------------\n");
    	//在堆上分配结构体数组
    	struct Person* p3 = (struct Person*)malloc(sizeof(struct Person) * 3);
    	for (int i = 0; i < 3;i++){
    		sprintf(p3[i].name, "Name_%d", i + 1);
    		p3[i].age = 20 + i;
    	}
    	for (int i = 0; i < 3; i++){
    		printf("Name:%s Age:%d\n", p3[i].name, p3[i].age);
    	}
    }
    

    结构体嵌套指针

    struct Person{
    	char* name;
    	int age;
    };
    
    void allocate_memory(struct Person** person){
    	if (person == NULL){
    		return;
    	}
    	struct Person* temp = (struct Person*)malloc(sizeof(struct Person));
    	if (temp == NULL){
    		return;
    	}
    	//给name指针分配内存
    	temp->name = (char*)malloc(sizeof(char)* 64);
    	strcpy(temp->name, "John");
    	temp->age = 100;
    
    	*person = temp;
    }
    
    void print_person(struct Person* person){
    	printf("Name:%s Age:%d\n",person->name,person->age);
    }
    
    void free_memory(struct Person** person){
    	if (person == NULL){
    		return;
    	}
    	struct Person* temp = *person;
    	if (temp->name != NULL){
    		free(temp->name);
    		temp->name = NULL;
    	}
    
    	free(temp);
    }
    
    void test(){
    	
    	struct Person* p = NULL;
    	allocate_memory(&p);
    	print_person(p);
    	free_memory(&p);
    }
    

    结构体嵌套指针二级指针练习

    结构体偏移量

    获取属性偏移

    offsetof

    (int)&(p->b) - (int)p

    通过偏移量获取内存

    结构体嵌套结构体

    //一个老师有N个学生
    typedef struct _TEACHER{
    	char name[64];
    	char** students;
    }Teacher;
    
    void create_teacher(Teacher** teacher,int n,int m){
    
    	if (teacher == NULL){
    		return;
    	}
    
    	//创建老师数组
    	Teacher* teachers = (Teacher*)malloc(sizeof(Teacher)* n);
    	if (teachers == NULL){
    		return;
    	}
    
    	//给每一个老师分配学生
    	int num = 0;
    	for (int i = 0; i < n; i ++){
    		sprintf(teachers[i].name, "老师_%d", i + 1);
    		teachers[i].students = (char**)malloc(sizeof(char*) * m);
    		for (int j = 0; j < m;j++){
    			teachers[i].students[j] = malloc(64);
    			sprintf(teachers[i].students[j], "学生_%d", num + 1);
    			num++;
    		}
    	}
    
    	*teacher = teachers;	
    }
    
    void print_teacher(Teacher* teacher,int n,int m){
    	for (int i = 0; i < n; i ++){
    		printf("%s:\n", teacher[i].name);
    		for (int j = 0; j < m;j++){
    			printf("  %s",teacher[i].students[j]);
    		}
    		printf("\n");
    	}
    }
    
    void free_memory(Teacher** teacher,int n,int m){
    	if (teacher == NULL){
    		return;
    	}
    
    	Teacher* temp = *teacher;
    
    	for (int i = 0; i < n; i ++){
    		
    		for (int j = 0; j < m;j ++){
    			free(temp[i].students[j]);
    			temp[i].students[j] = NULL;
    		}
    
    		free(temp[i].students);
    		temp[i].students = NULL;
    	}
    
    	free(temp);
    
    }
    
    void test(){
    	
    	Teacher* p = NULL;
    	create_teacher(&p,2,3);
    	print_teacher(p, 2, 3);
    	free_memory(&p,2,3);
    }
    

    结构体成员偏移量

    //一旦结构体定义下来,则结构体中的成员内存布局就定下了
    #include <stddef.h>
    struct Teacher
    {
    	char a;
    	int b;
    };
    
    void test01(){
    
    	struct Teacher  t1;
    	struct Teacher*p = &t1;
    
    
    	int offsize1 = (int)&(p->b) - (int)p;  //成员b 相对于结构体 Teacher的偏移量
    	int offsize2 = offsetof(struct Teacher, b);
    
    	printf("offsize1:%d \n", offsize1); //打印b属性对于首地址的偏移量
    	printf("offsize2:%d \n", offsize2);
    }
    
    

    结构体字节对齐

    内存对齐

    内存对齐的原因:cpu从内存读取数据时候,是一个一个字节读取,如果没有对齐,访问一个变量可能会产生二次访问

    img

    如何对齐

    • 对于标准数据类型,地址只要是长度整数倍
    • 对于非标准数据类型,结构体,要遵循一下对齐原则
      • 数组成员对齐
      • 结构体总大小
      • 结构体作为成员对齐规则

    查看对齐模数 #pragma pack(show)

    手动设置模数

    #pragma pack(show)
    显示当前packing alignment的字节数,以warning message的形式被显示。
    #pragma pack(push) 
    将当前指定的packing alignment数组进行压栈操作,这里的栈是the internal compiler stack,同事设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数组压栈。
    #pragma pack(pop) 
    从internal compiler stack中删除最顶端的reaord; 如果没有指定n,则当前栈顶record即为新的packing alignement数值;如果指定了n,则n成为新的packing alignment值
    #pragma pack(n)
    指定packing的数值,以字节为单位,缺省数值是8,合法的数值分别是1,2,4,8,16。 
    
    #pragma pack(4)
    
    typedef struct _STUDENT{
    	int a;
    	char b;
    	double c;
    	float d;
    }Student;
    
    typedef struct _STUDENT2{
    	char a;
    	Student b; 
    	double c;
    }Student2;
    
    void test01(){
    
    	//Student
    	//a从偏移量0位置开始存储
    	//b从4位置开始存储
    	//c从8位置开始存储
    	//d从12位置开存储
    	//所以Student内部对齐之后的大小为20 ,整体对齐,整体为最大类型的整数倍 也就是8的整数倍 为24
    
    	printf("sizeof Student:%d\n",sizeof(Student));
    
    	//Student2 
    	//a从偏移量为0位置开始 
    	//b从偏移量为Student内部最大成员整数倍开始,也就是8开始
    	//c从8的整数倍地方开始,也就是32开始
    	//所以结构体Sutdnet2内部对齐之后的大小为:40 , 由于结构体中最大成员为8,必须为8的整数倍 所以大小为40
    	printf("sizeof Student2:%d\n", sizeof(Student2));
    }
    

    默认对齐模数 8

    自定义数据类型 对齐规则

    第一个属性开始 从0开始偏移

    第二个属性开始 要放在该类型的大小,与对齐模数比 取最小的值 的整数倍

    所有的属性都计算完后,再整体做二次【偏移,将整体计算结果要放在结构体最大类型,与对齐模数比,取小的值的整数倍上

    结构体和嵌套结构体

    结构体嵌套结构体,子结构体放在该结构体最大类型·,和对齐模数比的整数倍上即可

    文件操作

    文件读写回顾

    文本流

    二进制流

    我们程序中,经常看到的文本方式打开文件和二进制方式打开文件仅仅体现在换行符的处理上。

    I/O函数以三种基本的形式处理数据:单个字符文本行二进制数据。对于每种形式都有一组特定的函数对它们进行处理。

    image-20220414234318644

    struct _iobuf { 
            char  *_ptr;         //文件输入的下一个位置 
            int   _cnt;          //剩余多少字符未被读取
            char  *_base;        //指基础位置(应该是文件的其始位置) 
            int   _flag;         //文件标志 
            int   _file;         //文件的有效性验证 
            int   _charbuf;      //检查缓冲区状况,如果无缓冲区则不读取 
            int   _bufsiz;       //文件的大小 
            char  *_tmpfname;    //临时文件名 
    }; 
    typedef struct _iobuf FILE;
    

    img

    文件缓冲区

    磁盘文件先送到内存中,读写时直接访问缓冲区

    img

    打开文件

    FILE * fopen(const char * filename, const char * mode);
    功能:打开文件
    参数:
    	filename:需要打开的文件名,根据需要加上路径
    	mode:打开文件的权限设置
    返回值:
    	成功:文件指针
    	失败:NULL
        
    void test(){
    	
    	FILE *fp = NULL;
    
    	// "\\"这样的路径形式,只能在windows使用
    	// "/"这样的路径形式,windows和linux平台下都可用,建议使用这种
    	// 路径可以是相对路径,也可是绝对路径
    	fp = fopen("../test", "w");
    	//fp = fopen("..\\test", "w");
    
    	if (fp == NULL) //返回空,说明打开失败
    	{
    		//perror()是标准出错打印函数,能打印调用库函数出错原因
    		perror("open");
    		return -1;
    	}
    }
    

    image-20220414234704579

    文件关闭

    int fclose(FILE * stream);
    功能:关闭先前fopen()打开的文件。此动作让缓冲区的数据写入文件中,并释放系统所提供的文件资源。
    参数:
    	stream:文件指针
    返回值:
    	成功:0
    	失败:-1
    

    按照字符读写

    • 写 fputc
    • 读 fgetc
    • while ( (ch = fgetc(f_read)) != EOF ) 判断是否到文件尾

    按行读写

    • fputs
    • fgets

    按块读写

    • 写 fwrite
      • 参数1 数据地址 参数2 块大小 参数3 块个数 参数4 文件指针
    • 读 fread

    格式化读写

    • 写 fprintf

    • 读 fscanf

    • int fputc(int ch, FILE * stream);
      功能:将ch转换为unsigned char后写入stream指定的文件中
      参数:
      	ch:需要写入文件的字符
      	stream:文件指针
      返回值:
      	成功:成功写入文件的字符
      	失败:返回-1
      
      int fgetc(FILE * stream);
      功能:从stream指定的文件中读取一个字符
      参数:
      	stream:文件指针
      返回值:
      	成功:返回读取到的字符
      	失败:-1
      
      int feof(FILE * stream);
      功能:检测是否读取到了文件结尾
      参数:
      	stream:文件指针
      返回值:
      	非0值:已经到文件结尾
      	0:没有到文件结尾
      void test(){
      
      	//写文件
      	FILE* fp_write= NULL;
      	//写方式打开文件
      	fp_write = fopen("./mydata.txt", "w+");
      	if (fp_write == NULL){
      		return;
      	}
      
      	char buf[] = "this is a test for pfutc!";
      	for (int i = 0; i < strlen(buf);i++){
      		fputc(buf[i], fp_write);
      	}
      	
      	fclose(fp_write);
      
      	//读文件
      	FILE* fp_read = NULL;
      	fp_read = fopen("./mydata.txt", "r");
      	if (fp_read == NULL){
      		return;
      	}
      
      #if 0
      	//判断文件结尾 注意:多输出一个空格
      	while (!feof(fp_read)){
      		printf("%c",fgetc(fp_read));
      	}
      #else
      	char ch;
      	while ((ch = fgetc(fp_read)) != EOF){
      		printf("%c", ch);
      	}
      #endif
      }
      

    行读写代码

    int fputs(const char * str, FILE * stream);
    功能:将str所指定的字符串写入到stream指定的文件中, 字符串结束符 '\0'  不写入文件。 
    参数:
    	str:字符串
    	stream:文件指针
    返回值:
    	成功:0
    	失败:-1
    
    char * fgets(char * str, int size, FILE * stream);
    功能:从stream指定的文件内读入字符,保存到str所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,最后会自动加上字符 '\0' 作为字符串结束。
    参数:
    	str:字符串
    	size:指定最大读取字符串的长度(size - 1)
    	stream:文件指针
    返回值:
    	成功:成功读取的字符串
    	读到文件尾或出错: NULL
    void test(){
    
    	//写文件
    	FILE* fp_write= NULL;
    	//写方式打开文件
    	fp_write = fopen("./mydata.txt", "w+");
    	if (fp_write == NULL){
    		perror("fopen:");
    		return;
    	}
    
    	char* buf[] = {
    		"01 this is a test for pfutc!\n",
    		"02 this is a test for pfutc!\n",
    		"03 this is a test for pfutc!\n",
    		"04 this is a test for pfutc!\n",
    	};
    	for (int i = 0; i < 4; i ++){
    		fputs(buf[i], fp_write);
    	}
    	
    	fclose(fp_write);
    
    	//读文件
    	FILE* fp_read = NULL;
    	fp_read = fopen("./mydata.txt", "r");
    	if (fp_read == NULL){
    		perror("fopen:");
    		return;
    	}
    
    	//判断文件结尾
    	while (!feof(fp_read)){
    		char temp[1024] = { 0 };
    		fgets(temp, 1024, fp_read);
    		printf("%s",temp);
    	}
    
    	fclose(fp_read);
    }
    

    块读写代码

    size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
    功能:以数据块的方式给文件写入内容
    参数:
    	ptr:准备写入文件数据的地址
    	size: size_t 为 unsigned int类型,此参数指定写入文件内容的块数据大小
    	nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb
    	stream:已经打开的文件指针
    返回值:
    	成功:实际成功写入文件数据的块数,此值和nmemb相等
    	失败:0
    
    size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    功能:以数据块的方式从文件中读取内容
    参数:
    	ptr:存放读取出来数据的内存空间
    	size: size_t 为 unsigned int类型,此参数指定读取文件内容的块数据大小
    	nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb
    	stream:已经打开的文件指针
    返回值:
    	成功:实际成功读取到内容的块数,如果此值比nmemb小,但大于0,说明读到文件的结尾。
    	失败:0
    typedef struct _TEACHER{
    	char name[64];
    	int age;
    }Teacher;
    
    void test(){
    
    	//写文件
    	FILE* fp_write= NULL;
    	//写方式打开文件
    	fp_write = fopen("./mydata.txt", "wb");
    	if (fp_write == NULL){
    		perror("fopen:");
    		return;
    	}
    
    	Teacher teachers[4] = {
    		{ "Obama", 33 },
    		{ "John", 28 },
    		{ "Edward", 45},
    		{ "Smith", 35}
    	};
    	
    	for (int i = 0; i < 4; i ++){
    		fwrite(&teachers[i],sizeof(Teacher),1, fp_write);
    	}
    	//关闭文件
    	fclose(fp_write);
    
    	//读文件
    	FILE* fp_read = NULL;
    	fp_read = fopen("./mydata.txt", "rb");
    	if (fp_read == NULL){
    		perror("fopen:");
    		return;
    	}
    
    	Teacher temps[4];
    	fread(&temps, sizeof(Teacher), 4, fp_read);
    	for (int i = 0; i < 4;i++){
    		printf("Name:%s Age:%d\n",temps[i].name,temps[i].age);
    	}
    
    	fclose(fp_read);
    }
    

    格式化读写

    int fprintf(FILE * stream, const char * format, ...);
    功能:根据参数format字符串来转换并格式化数据,然后将结果输出到stream指定的文件中,指定出现字符串结束符 '\0'  为止。
    参数:
    	stream:已经打开的文件
    	format:字符串格式,用法和printf()一样
    返回值:
    	成功:实际写入文件的字符个数
    	失败:-1
    
    int fscanf(FILE * stream, const char * format, ...);
    功能:从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据。
    参数:
    	stream:已经打开的文件
    	format:字符串格式,用法和scanf()一样
    返回值:
    	成功:实际从文件中读取的字符个数
    	失败: - 1
        
    
    void test(){
    
    	//写文件
    	FILE* fp_write= NULL;
    	//写方式打开文件
    	fp_write = fopen("./mydata.txt", "w");
    	if (fp_write == NULL){
    		perror("fopen:");
    		return;
    	}
    
    	fprintf(fp_write,"hello world:%d!",10);
    
    	//关闭文件
    	fclose(fp_write);
    
    	//读文件
    	FILE* fp_read = NULL;
    	fp_read = fopen("./mydata.txt", "rb");
    	if (fp_read == NULL){
    		perror("fopen:");
    		return;
    	}
    	
    	char temps[1024] = { 0 };
    	while (!feof(fp_read)){
    		fscanf(fp_read, "%s", temps);
    		printf("%s", temps);
    	}
    
    	fclose(fp_read);
    }
    

    *注意*fscanf遇到空格和换行时结束。

    随机读写代码

    int fseek(FILE *stream, long offset, int whence);
    功能:移动文件流(文件光标)的读写位置。
    参数:
    	stream:已经打开的文件指针
    	offset:根据whence来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了 文件末尾,再次写入时将增大文件尺寸。
    	whence:其取值如下:
    		SEEK_SET:从文件开头移动offset个字节
    		SEEK_CUR:从当前位置移动offset个字节
    		SEEK_END:从文件末尾移动offset个字节
    返回值:
    	成功:0
    	失败:-1
    
    long ftell(FILE *stream);
    功能:获取文件流(文件光标)的读写位置。
    参数:
    	stream:已经打开的文件指针
    返回值:
    	成功:当前文件流(文件光标)的读写位置
    	失败:-1
    
    void rewind(FILE *stream);
    功能:把文件流(文件光标)的读写位置移动到文件开头。
    参数:
    	stream:已经打开的文件指针
    返回值:
    	无返回值
    typedef struct _TEACHER{
    	char name[64];
    	int age;
    }Teacher;
    
    void test(){
    	//写文件
    	FILE* fp_write = NULL;
    	//写方式打开文件
    	fp_write = fopen("./mydata.txt", "wb");
    	if (fp_write == NULL){
    		perror("fopen:");
    		return;
    	}
    
    	Teacher teachers[4] = {
    		{ "Obama", 33 },
    		{ "John", 28 },
    		{ "Edward", 45 },
    		{ "Smith", 35 }
    	};
    
    	for (int i = 0; i < 4; i++){
    		fwrite(&teachers[i], sizeof(Teacher), 1, fp_write);
    	}
    	//关闭文件
    	fclose(fp_write);
    
    	//读文件
    	FILE* fp_read = NULL;
    	fp_read = fopen("./mydata.txt", "rb");
    	if (fp_read == NULL){
    		perror("fopen:");
    		return;
    	}
    
    	Teacher temp;
    	//读取第三个数组
    	fseek(fp_read , sizeof(Teacher) * 2 , SEEK_SET);
    	fread(&temp, sizeof(Teacher), 1, fp_read);
    	printf("Name:%s Age:%d\n",temp.name,temp.age);
    
    	memset(&temp,0,sizeof(Teacher));
    
    	fseek(fp_read, -(int)sizeof(Teacher), SEEK_END);
    	fread(&temp, sizeof(Teacher), 1, fp_read);
    	printf("Name:%s Age:%d\n", temp.name, temp.age);
    
    	rewind(fp_read);
    	fread(&temp, sizeof(Teacher), 1, fp_read);
    	printf("Name:%s Age:%d\n", temp.name, temp.age);
    
    	fclose(fp_read);
    }
    

    随机位置读写

    fseek(文件指针,偏移,起始位置)

    SEEK_SET 从头开始

    SEEK_END 从尾开始

    SEEK_CUR 从当前位置

    rewind 将文件光标置首

    error 宏 利用perror 打印错误提示信息

    文件读写注意

    1. 不要用feof按照字符方式读文件,有滞后性,会读出EOF
    2. 如果属性开辟到堆区,不要存指针到文件中,要将指针指向的内容存放到文件中

    配置文件读写案例

    配置文件格式如下:
    正式的数据以 ‘:’冒号进行分割,冒号前为key起到索引作用,冒号后为value是实值。#开头的为注释,而不是正式数据
    #英雄的Id
    heroId:1
    #英雄的姓名
    heroName:德玛西亚
    #英雄的攻击力
    heroAtk:1000
    #英雄的防御力
    heroDef:500
    #英雄的简介
    heroInfo:前排坦克
    
    struct ConfigInfo
    {
    	char key[64];
    	char value[64];
    };
    
    //获取文件有效行数
    int getFileLine(const char  * filePath)
    {
    	FILE * file = fopen(filePath, "r");
    	char buf[1024] = {0};
    	int lines = 0;
    	while (fgets(buf,1024,file) != NULL)
    	{
    		if (isValidLine(buf))
    		{
    			lines++;
    		}
    		memset(buf, 0, 1024);
    	}
    	 
    	fclose(file);
    	
    	return lines;
    
    }
    //解析文件
    void parseFile(const char  * filePath, int lines, struct ConfigInfo** configInfo)
    {
    
    	struct ConfigInfo * pConfig =  malloc(sizeof(struct ConfigInfo) * lines);
    
    	if (pConfig == NULL)
    	{
    		return;
    	}
    
    
    
    	FILE * file = fopen(filePath, "r");
    	char buf[1024] = { 0 };
    	
    	int index = 0;
    	while (fgets(buf, 1024, file) != NULL)
    	{
    		if (isValidLine(buf))
    		{
    			//解析数据到struct ConfigInfo中
    			memset(pConfig[index].key, 0, 64);
    			memset(pConfig[index].value, 0, 64);
    			char * pos = strchr(buf, ':');
    			strncpy(pConfig[index].key, buf, pos - buf);
    			strncpy(pConfig[index].value, pos + 1, strlen(pos + 1) - 1); // 从第二个单词开始截取字符串,并且不截取换行符
    			//printf("key = %s\n", pConfig[index].key);
    			//printf("value = %s\n", pConfig[index].value);
    			index++;
    		}
    		memset(buf, 0, 1024);
    	}
    
    
    
    	*configInfo = pConfig;
    
    }
    
    //获取指定的配置信息
    char * getInfoByKey(char * key, struct ConfigInfo*configInfo ,int lines)
    {
    	for (int i = 0; i < lines;i++)
    	{
    		if (strcmp(key, configInfo[i].key) == 0)
    		{
    			return configInfo[i].value;
    		}
    	}
    	return NULL;
    }
    
    //释放配置文件信息
    void freeConfigInfo(struct ConfigInfo*configInfo)
    {
    	free(configInfo);
    	configInfo = NULL;
    }
    
    //判断当前行是否为有效行
    int isValidLine(char * buf)
    {
    	if (buf[0] == '0' || buf[0] == '\0' || strchr(buf,':') == NULL)
    	{
    		return 0;// 如果行无限 返回假
    	}
    	return 1;
    }
    
    int main(){
    
    	char * filePath = "./config.txt";
    	int lines = getFileLine(filePath);
    	printf("文件有效行数为:%d\n", lines);
    
    	struct ConfigInfo * config = NULL;
    	parseFile(filePath, lines, &config);
    
    	printf("heroId = %s\n", getInfoByKey("heroId", config, lines));
    	printf("heroName: = %s\n", getInfoByKey("heroName", config, lines));
    	printf("heroAtk = %s\n", getInfoByKey("heroAtk", config, lines));
    	printf("heroDef: = %s\n", getInfoByKey("heroDef", config, lines));
    	printf("heroInfo: = %s\n", getInfoByKey("heroInfo", config, lines));
    
    
    	freeConfigInfo(config);
    	config = NULL;
    
    	system("pause");
    	return EXIT_SUCCESS;
    }
    

    day06


    链表

    链表是一种常用的数据结构,它通过指针将一些列数据节点,连接成一个数据链,相对于数组,链表具有更好的动态性

    数据用来存储数据,指针用于建立下一个结点练习

    建立链表时无需先知道数据总量的,可以随机分配空间,可以高效在链表中任意位置实时插入或删除数据

    链表的开销主要是访问顺序性和组织链的空间损失

    链表的基本概念以及分类方式

    数组与链表

    • 一次性分配一块连续的存储区域
    • 优点:随机访问元素效率高
    • 删除和插入某个元素效率低

    数组是一个静态空间:一旦分配内存,就不可以动态扩展,空间可能分配多或者分配的少,操作不精准

    对于头部插入效率低

    链表

    • 无需一次性分配一块连续的存储区域,只需要分配n块节点鵆区域,通过指针建立关系
    • 优点:不需要一块连续的存储区域
    • 删除和插入某个元素效率高
    • 缺点:访问元素效率低

    链表的分类

    • 静态链表,动态链表

      • 静态链表:所有结点是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为静态链表

      • 动态链表:所谓动态链表,是指在程序执行过程中从无到有建立起一个链表,就是一个一个开辟结点和输入各个结点数据,并建立起前后相连的关系

      • 静态

      • typedef struct Stu
        {
        	int id;	//数据域
        	char name[100];
        
        	struct Stu *next; //指针域
        }Stu;
        
        void test()
        {
        	//初始化三个结构体变量
        	Stu s1 = { 1, "yuri", NULL };
        	Stu s2 = { 2, "lily", NULL };
        	Stu s3 = { 3, "lilei", NULL };
        
        	s1.next = &s2; //s1的next指针指向s2
        	s2.next = &s3;
        	s3.next = NULL; //尾结点
        
        	Stu *p = &s1;
        	while (p != NULL)
        	{
        		printf("id = %d, name = %s\n", p->id, p->name);
        
        		//结点往后移动一位
        		p = p->next; 
        	}
        }
        

        动态

        typedef struct Stu{
        	int id;	//数据域
        	char name[100];
        
        	struct Stu *next; //指针域
        }Stu;
        
        void test(){
        	//动态分配3个节点
        	Stu *s1 = (Stu *)malloc(sizeof(Stu));
        	s1->id = 1;
        	strcpy(s1->name, "yuri");
        
        	Stu *s2 = (Stu *)malloc(sizeof(Stu));
        	s2->id = 2;
        	strcpy(s2->name, "lily");
        
        	Stu *s3 = (Stu *)malloc(sizeof(Stu));
        	s3->id = 3;
        	strcpy(s3->name, "lilei");
        
        	//建立节点的关系
        	s1->next = s2; //s1的next指针指向s2
        	s2->next = s3;
        	s3->next = NULL; //尾结点
        
        	//遍历节点
        	Stu *p = s1;
        	while (p != NULL)
        	{
        		printf("id = %d, name = %s\n", p->id, p->name);
        
        		//结点往后移动一位
        		p = p->next; 
        	}
        
        	//释放节点空间
        	p = s1;
        	Stu *tmp = NULL;
        	while (p != NULL)
        	{
        		tmp = p;
        		p = p->next;
        
        		free(tmp);
        		tmp = NULL;
        	}
        }
        

        静态链表分配在栈上

        动态链表分配到堆区

        实现链表的初始化和不带头

    带头链表

    带头链表不带头链表

    带头节点链表

    带头链表:固定一个节点作为结点(数据域不保存有效数据),起一个标志位作用,以后不管链表节点如果改变,头结点固定不变

    img

    不带头链表:头结点不固定,根据实际需要变换头结点(如在原来头结点插入新节点,然后新结点重新作为链表的头结点)

    img

    • 单向链表,双向链表,单向循环链表,双向循环链表
    • 单向
    • img
    • 双向
    • img
    • 循环
    • img

    链表节点

    • 链表节点类型实际上是结构体变量,结构体包含数据域和指针域

    • 数据域用来存数据

    • 指针域用来建立与下一个节点的联系,当此节点是尾结点时,指针域是NULL

    • typedef struct Node 
      {
      	//数据域
      	int id;
      	char name[50];
      
      	//指针域
      	struct Node *next;       
      }Node;
      

    链表基本使用

    带头结点链表和不带头节点链表

    带头好处:带着头的链表永远固定了头节点位置

    初始化链表 1.1 init_LinkList

    链表初始化解析图

    遍历链表 1.1 foreach_LinkList

    插入链表 insert_LinkList 利用两个辅助指针实现

    删除链表 delete_LinkList 利用两个辅助指针 实现删除

    清空链表 clear_LinkList 1.1 将所有有数据节点释放掉,可以在使用

    销毁链表 destroy_LinkList 1.1 将整个链表释放掉,不可以再使用

    创建链表

    typedef struct _LINKNODE
    {
    	int id; //数据域
    	struct _LINKNODE* next; //指针域
    }link_node;
    

    编写函数:link_node* init_linklist()

    建立带有头结点的单向链表,循环创建结点,结点数据域中的数值从键盘输入,以 -1 作为输入结束标志,链表的头结点地址由函数值返回.

    typedef struct _LINKNODE{
    	int data;
    	struct _LINKNODE* next;
    }link_node;
    
    link_node* init_linklist(){
    	
    	//创建头结点指针
    	link_node* head = NULL;
    	//给头结点分配内存
    	head = (link_node*)malloc(sizeof(link_node));
    	if (head == NULL){
    		return NULL;
    	}
    	head->data = -1;
    	head->next = NULL;
    
    	//保存当前节点
    	link_node* p_current = head;
    	int data = -1;
    	//循环向链表中插入节点
    	while (1){
    	
    		printf("please input data:\n");
    		scanf("%d",&data);
    
    		//如果输入-1,则退出循环
    		if (data == -1){
    			break;
    		}
    
    		//给新节点分配内存
    		link_node* newnode = (link_node*)malloc(sizeof(link_node));
    		if (newnode == NULL){
    			break;
    		}
    
    		//给节点赋值
    		newnode->data = data;
    		newnode->next = NULL;
    
    		//新节点入链表,也就是将节点插入到最后一个节点的下一个位置
    		p_current->next = newnode;
    		//更新辅助指针p_current
    		p_current = newnode;
    	}
    
    	return head;
    }
    

    遍历链表

    编写函数:void foreach_linklist(link_node* head)

    顺序输出单向链表各项结点数据域中的内容:

    //遍历链表
    void foreach_linklist(link_node* head){
    	if (head == NULL){
    		return;
    	}
    
    	//赋值指针变量
    	link_node* p_current = head->next;
    	while (p_current != NULL){
    		printf("%d ",p_current->data);
    		p_current = p_current->next;
    	}
    	printf("\n");
    }
    

    插入节点

    编写函数: void insert_linklist(link_node* head,int val,int data).

    在指定值后面插入数据data,如果值val不存在,则在尾部插入。

    链表添加节点解析图

    //在值val前插入节点
    void insert_linklist(link_node* head, int val, int data){
    	
    	if (head == NULL){
    		return;
    	}
    
    	//两个辅助指针
    	link_node* p_prev = head;
    	link_node* p_current = p_prev->next;
    	while (p_current != NULL){
    		if (p_current->data == val){
    			break;
    		}
    		p_prev = p_current;
    		p_current = p_prev->next;
    	}
    
    	//如果p_current为NULL,说明不存在值为val的节点
    	if (p_current == NULL){
    		printf("不存在值为%d的节点!\n",val);
    		return;
    	}
    
    	//创建新的节点
    	link_node* newnode = (link_node*)malloc(sizeof(link_node));
    	newnode->data = data;
    	newnode->next = NULL;
    
    	//新节点入链表
    	newnode->next = p_current;
    	p_prev->next = newnode;
    }
    

    删除节点

    编写函数: void remove_linklist(link_node* head,int val)

    删除第一个值为val的结点.

    //删除值为val的节点
    void remove_linklist(link_node* head,int val){
    	if (head == NULL){
    		return;
    	}
    
    	//辅助指针
    	link_node* p_prev = head;
    	link_node* p_current = p_prev->next;
    
    	//查找值为val的节点
    	while (p_current != NULL){
    		if (p_current->data == val){
    			break;
    		}
    		p_prev = p_current;
    		p_current = p_prev->next;
    	}
    	//如果p_current为NULL,表示没有找到
    	if (p_current == NULL){
    		return;
    	}
    	
    	//删除当前节点: 重新建立待删除节点(p_current)的前驱后继节点关系
    	p_prev->next = p_current->next;
    	//释放待删除节点的内存
    	free(p_current);
    }
    

    销毁链表

    编写函数: void destroy_linklist(link_node* head)

    销毁链表,释放所有节点的空间.

    //销毁链表
    void destroy_linklist(link_node* head){
    	if (head == NULL){
    		return;
    	}
    	//赋值指针
    	link_node* p_current = head;
    	while (p_current != NULL){
    		//缓存当前节点下一个节点
    		link_node* p_next = p_current->next;
    		free(p_current);
    		p_current = p_next;
    	}
    }
    

    函数指针

    一个函数在编译时被分配一个入口地址,这个地址就叫函数的指针,函数名代表函数入口地址

    函数三要素:名称,参数,返回值,C语言中函数有自己特定类型

    typedef int f(int, int);		// f 为函数类型
    typedef void p(int);		// p 为函数类型
    

    通过函数类型定义的变量时不能直接执行,因为没有函数体,只能通过类型定义一个函数指针指向某一个具体函数才能调用

    typedef int(p)(int, int);
    
    void my_func(int a,int b){
    	printf("%d %d\n",a,b);
    }
    
    void test(){
    
    	p p1;
    	//p1(10,20); //错误,不能直接调用,只描述了函数类型,但是并没有定义函数体,没有函数体无法调用
    	p* p2 = my_func;
    	p2(10,20); //正确,指向有函数体的函数入口地址
    }
    

    函数指针:指向函数的指针

    函数指针方式定义函数类型,根据类型定义指针变量

    先定义函数指针类型,根据类型定义指针变量

    直接定义函数指针变量

    int my_func(int a,int b){
    	printf("ret:%d\n", a + b);
    	return 0;
    }
    
    //1. 先定义函数类型,通过类型定义指针
    void test01(){
    	typedef int(FUNC_TYPE)(int, int);
    	FUNC_TYPE* f = my_func;
    	//如何调用?
    	(*f)(10, 20);
    	f(10, 20);
    }
    
    //2. 定义函数指针类型
    void test02(){
    	typedef int(*FUNC_POINTER)(int, int);
    	FUNC_POINTER f = my_func;
    	//如何调用?
    	(*f)(10, 20);
    	f(10, 20);
    }
    
    //3. 直接定义函数指针变量
    void test03(){
    	
    	int(*f)(int, int) = my_func;
    	//如何调用?
    	(*f)(10, 20);
    	f(10, 20);
    }
    

    函数指针数组

    函数指针数组,每个元素都是函数指针

    void func01(int a){
    	printf("func01:%d\n",a);
    }
    void func02(int a){
    	printf("func02:%d\n", a);
    }
    void func03(int a){
    	printf("func03:%d\n", a);
    }
    
    void test(){
    
    #if 0
    	//定义函数指针
    	void(*func_array[])(int) = { func01, func02, func03 };
    #else
    	void(*func_array[3])(int);
    	func_array[0] = func01;
    	func_array[1] = func02;
    	func_array[2] = func03;
    #endif
    
    	for (int i = 0; i < 3; i ++){
    		func_array[i](10 + i);
    		(*func_array[i])(10 + i);
    	}
    }
    

    函数指针和指针函数

    函数指针 指向了函数的指针

    指针函数 函数返回值是指针的函数

    函数指针做函数阐述(回调函数)

    函数参数除了是普通变量,还可以是指针变量

    //形参为普通变量
    void fun( int x ){}
    //形参为函数指针变量
    void fun( int(*p)(int a) ){}
    

    函数指针变量是把指针作为参数传递到其他函数,指向函数的指针也可以作为参数,实现函数地址传递

    int plus(int a, int b)
    {
    	return a + b;
    }
    int sub(int a, int b)
    {
    	return a - b;
    }
    int mul(int a, int b)
    {
    	return a * b;
    }
    int division(int a, int b)
    {
    	return a / b;
    }
    
    //函数指针 做函数的参数 --- 回调函数
    void Calculator(int(*myCalculate)(int, int), int a, int b)
    {
    	int ret = myCalculate(a, b); //dowork中不确定用户选择的内容,由后期来指定运算规则
    	printf("ret = %d\n", ret);
    }
    
    void test01()
    {
    	printf("请输入操作符\n");
    	printf("1、+ \n");
    	printf("2、- \n");
    	printf("3、* \n");
    	printf("4、/ \n");
    
    	int select = -1;
    	scanf("%d", &select);
    
    	int num1 = 0;
    	printf("请输入第一个操作数:\n");
    	scanf("%d", &num1);
    
    	int num2 = 0;
    	printf("请输入第二个操作数:\n");
    	scanf("%d", &num2);
    
    	switch (select)
    	{
    	case  1:
    		Calculator(plus, num1, num2);
    		break;
    	case  2:
    		Calculator(sub, num1, num2);
    		break;
    	case 3:
    		Calculator(mul, num1, num2);
    		break;
    	case 4:
    		Calculator(division, num1, num2);
    		break;
    	default:
    		break;
    	}
    
    }
    

    利用回调函数实现打印任意类型数据

    提供能够打印任意类型数组函数

    利用回调函数提供查找功能

    day07


    预处理

    c 语言对程序处理四步骤,预处理,编译,汇编,链接

    文件包含指令

    文件包含处理

    “文件包含处理”是指一个源文件可以将另外一个文件的全部内容包含进来。C语言提供了#include命令用来实现“文件包含”的操作。

    img

    # 与 “”

    “” 表示在file.c所在的当前目录找file.h,如果找不到,再按系统指定的目录检索

    <> 表示系统直接按系统指定目录检索

    include <> 常用于包含库函数的头文件

    include “” 常用于包含自定义头文件

    理论上#include可以包含任意格式的文件(.c .h等) ,但一般用于头文件的包含;

    宏常量

    • 宏名一般用大写,以便与变量区别

    • 宏定义可以是常数,表达式

    • 宏定义不做语法检查,只有在编译被宏展开后的源程序才会报错

    • 宏定义不是C语言,不在行末尾加分号

    • 宏名有效范围从定义到本源文件结束

    • 可以用#undef命令终止定义作用域

    • 在宏定义中,可以引用已经定义的宏名

    • 用括号括住每一个参数,并括住宏的整体定义。

      • 用大写字母表示宏的函数名。

      • 如果打算宏代替函数来加快程序运行速度。假如在程序中只使用一次宏对程序的运

      • 行时间没有太大提高。

    • 宏的名字中不能有空格,但是在替换的字符串中可以有空格。ANSI C允许在参数列表中使用空格;不重视作用域

    • 没有数据类型

    • 利用#undef卸载宏

    宏函数

    • 将频繁,短小函数写成宏函数
    • 优点,用空间换时间

    条件编译

    当满足一定条件时,对这部分源程序指定编译条件

    img

    防止头文件被重复包含使用

    ifdef #else #endif 测试存在

    ifndef #else #endif 测试不存在

    if #else #endif 自定义条件编译

    #ifndef _SOMEFILE_H
    #define _SOMEFILE_H
    
    //需要声明的变量、函数
    //宏定义
    //结构体
    
    #endif
    

    特殊宏

    //	__FILE__			宏所在文件的源文件名 
    //	__LINE__			宏所在行的行号
    //	__DATE__			代码编译的日期
    //	__TIME__			代码编译的时间
    
    void test()
    {
    	printf("%s\n", __FILE__);
    	printf("%d\n", __LINE__);
    	printf("%s\n", __DATE__);
    	printf("%s\n", __TIME__);
    }
    

    FILE 宏所在文件路径

    LINE 宏所在行

    DATE 宏编译日期

    TIME 宏编译时间

    静态库创建

    \1. 创建一个新项目,在已安装的模板中选择“常规”,在右边的类型下选择“空项目”,在名称和解决方案名称中输入staticlib。点击确定。

    2.在解决方案资源管理器的头文件中添加,mylib.h文件,在源文件添加mylib.c文件(即实现文件)。

    3.在mylib.h文件中添加如下代码:

    #ifndef TEST_H
    #define TEST_H
    
    int myadd(int a,int b);
    #endif
    

    4.在mylib.c文件中添加如下代码:

    #include"test.h"
    int myadd(int a, int b){
    	return a + b;
    }
    
    1. 配置项目属性。因为这是一个静态链接库,所以应在项目属性的“配置属性”下选择“常规”,在其下的配置类型中选择“静态库(.lib)。

    6.编译生成新的解决方案,在Debug文件夹下会得到mylib.lib (对象文件库),将该.lib文件和相应头文件给用户,用户就可以使用该库里的函数了。

    静态库使用

    *方法一:配置项目属性*

    A、添加工程的头文件目录:工程---属性---配置属性---c/c++---常规---附加包含目录:加上头文件存放目录。
    B、添加文件引用的lib静态库路径:工程---属性---配置属性---链接器---常规---附加库目录:加上lib文件存放目录。
    C  然后添加工程引用的lib文件名:工程---属性---配置属性---链接器---输入---附加依赖项:加上lib文件名。
    

    *方法二:使用编译语句*

    pragma comment(lib,"./mylib.lib")

    *方法三:添加工程中*

    就像你添加.h和.c文件一样,把lib文件添加到工程文件列表中去.切换到"解决方案视图",--->选中要添加lib的工程-->点击右键-->"添加"-->"现有项"-->选择lib文件-->确定.

    静态库优缺点

    静态库对函数库的链接是放在编译时期完成的,静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系;
    程序在运行时与函数库再无瓜葛,移植方便。
    浪费空间和资源,所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
    

    动态库创建

    \1. 创建一个新项目,在已安装的模板中选择“常规”,在右边的类型下选择“空项目”,在名称和解决方案名称中输入mydll。点击确定。

    2.在解决方案资源管理器的头文件中添加,mydll.h文件,在源文件添加mydll.c文件(即实现文件)。

    3.在test.h文件中添加如下代码:

    #ifndef TEST_H
    #define TEST_H
    
    __declspec(dllexport) int myminus(int a, int b);
    
    #endif
    

    4.在test.c文件中添加如下代码:

    #include"test.h"
    __declspec(dllexport) int myminus(int a, int b){
    	return a - b;
    }
    

    配置项目属性。因为这是一个动态链接库,所以应在项目属性的“配置属性”下选择“常规”,在其下的配置类型中选择“动态库(.dll)。

    6.编译生成新的解决方案,在Debug文件夹下会得到mydll.dll (对象文件库),将该.dll文件、.lib文件和相应头文件给用户,用户就可以使用该库里的函数了。

    *疑问一:*__declspec(dllexport)是什么意思?

    动态链接库中定义有两种函数:导出函数(export function)和内部函数(internal function)。 导出函数可以被其它模块调用,内部函数在定义它们的DLL程序内部使用。

    *疑问二:*动态库的lib文件和静态库的lib文件的区别?

    动态库使用

    隐式调用

    创建主程序TestDll,将mydll.h、mydll.dll和mydll.lib复制到源代码目录下。(P.S:头文件Func.h并不是必需的,只是C++中使用外部函数时,需要先进行声明)在程序中指定链接引用链接库 : #pragma comment(lib,"./mydll.lib")

    显式调用

    	HANDLE hDll; //声明一个dll实例文件句柄
    	hDll = LoadLibrary("mydll.dll"); //导入动态链接库
    	MYFUNC minus_test; //创建函数指针
    	//获取导入函数的函数指针
    	minus_test = (MYFUNC)GetProcAddress(hDll, "myminus");
    

    递归函数

    递归函数就是直接或间接调用自身的函数

    普通函数调用

    void funB(int b){
    	printf("b = %d\n", b);
    }
    
    void funA(int a){
    	funB(a - 1);
    	printf("a = %d\n", a);
    }
    
    int main(void){
    	funA(2);
        printf("main\n");
    	return 0;
    }
    

    img

    递归函数调用

    void fun(int a){
    	
    	if (a == 1){
    		printf("a = %d\n", a);
    		return; //中断函数很重要
    	}
    
    	fun(a - 1);
    	printf("a = %d\n", a);
    }
    
    int main(void){
    	
    	fun(2);
    	printf("main\n");
    
    	return 0;
    }
    

    img

    void recursion(int val){
    	if (val == 0){
    		return;
    	}
    	int ret = val / 10;
    	recursion(ret);
    	printf("%d ",val % 10);
    }
    

    递归实现字符串反转

    int reverse1(char *str){
    	if (str == NULL)
    	{
    		return -1;
    	}
    
    	if (*str == '\0') // 函数递归调用结束条件
    	{
    		return 0;
    	}
    	
    	reverse1(str + 1);
    	printf("%c", *str);
    
    	return 0;
    }
    
    char buf[1024] = { 0 };  //全局变量
    
    int reverse2(char *str){
    	if (str == NULL) 
    	{
    		return -1;
    	}
    
    	if ( *str == '\0' ) // 函数递归调用结束条件
    	{
    		return 0;
    	}
    
    	reverse2(str + 1);
    	strncat(buf, str, 1);
    
    	return 0;
    }
    
    int reverse3(char *str, char *dst){
    	if (str == NULL || dst == NULL) 
    	{
    		return -1;
    	}
    
    	if (*str == '\0') // 函数递归调用结束条件
    	{
    		return 0;
    	}
    
    	reverse3(str + 1);
    
    	strncat(dst, str, 1);
    
    	return 0;
    }
    
  • 相关阅读:
    高性能css动画
    关于thinkphp验证码的那些事
    DOM对象的属性
    关于data属性的一些常见的处理方式
    webstorm快捷键整理
    javascript模块化编程
    2016年5月30日上午(传智Bootstrap笔记六(图片样式))
    Bootstrap列排序
    2016年5月29日晚上(传智Bootstrap笔记五(表单2))
    2016年5月29日晚上(传智Bootstrap笔记四(栅格系统 ))
  • 原文地址:https://www.cnblogs.com/abldh12/p/16171973.html
Copyright © 2020-2023  润新知