c_advance

2023-12-24

C 语言进阶学习笔记

一、数据类型变量和内存四区指针

0、体系介绍

0.1、概述

  • 学习要求
  • 学习标准

0.2、数据类型和变量

0.3、内存四区

  • 全局
  • 代码区

0.4、指针强化

0.5、字符串处理

  • 字符串基本操作
  • 字符串一级指针内存模型
  • 字符串做函数参数

0.6、二级指针与多级指针

  • 二级指针的输入输出模型
  • 二级指针输入的三种内存模型
  • 多级指针的使用

0.7、数组

  • 一维数组、二维数组
  • 多维数组、数组指针类型、数组指针类型变量
  • 多维数组

0.8、结构体

  • 结构体的使用

0.9、文件操作

  • 文件读写操作
  • 配置文件的读写案例(自定义接口)
  • 文件的加密和解密案例(使用别人写好的接口)

0.10、链表

  • 单向链表的使用
  • 函数指针

0.11、辅助性内容

  • 预处理
  • 如何使用动态库
  • 动态库的封装和设计
  • 内存泄漏的检测

0.12、扯白

1)程序员进阶之路

  • 基础:C/C++语言、数据结构、系统编程 / 游戏编程 $\rightarrow$ 接口的封装和设计
    • 接口:函数(API,方法)
  • 提高:日志库、配置文件读写库、windows/linux IPC库、socket、cocos2d-Unity3D、数据库-统一访问库 $\rightarrow$ 经验
  • 跳跃:win 下项目、Linux 下项目、Android 项目、IOS 项目

2)要求

  • 资料整理
  • 经验记录和积累
  • 临界点
  • 当堂运行
  • 动手

3)学习重点

  • 接口的封装和设计(功能的抽象和封装)
    • 接口 api 的使用能力
    • 接口 api 的查找能力
    • 接口 api 的实现能力
  • 建立正确的程序运行内存布居图
    • 内存四区模型
    • 函数调用模型
  • 在写程序的时候,涉及到指针地方,可以通过绘制内存四区图,来理清程序中的变量传递和变量的寿命,从而更好地控制内存

4)学习标准

  • 在头文件中声明兼容 C++ 编译器

  • 项目打桩:接口写好,具体的功能实现先空着,保证项目可以跑起来,功能以后实现

    • socketclient.h

    ```c //头文件函数声明

//防止头文件重复包含 #pragma once

//兼容 C++ 编译器 //如果是C++编译器,按C标准编译 #ifdef __cpluscplus extern “c” {

#endif //第一套接口 //初始化环境 int socketclient_init(void** handle);

//发送信息
int socketclient_send(void* handle,void* buf,int len);

//接受信息
int socketclient_recv(void* handle,void* buf,int len);

//释放资源
int sockenclient_destroy(void *handle);

#ifdef __cpluscplus }

#endif


  * socketclient.c

  ```c
//函数的实现
//初始化环境
int socketclient_init(void** handle)
{
    return 0;
}
    
//发送信息
int socketclient_send(void* handle,void* buf,int len)
{
    return 0;
}
    
//接受信息
int socketclient_recv(void* handle,void* buf,int len)
{
    return 0;
}
    
//释放资源
int sockenclient_destroy(void *handle)
{
    return 0;
}
  • socketclient_test.c
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include "socketclient.h"

int main()
{
    
    
    printf("\n");
    system("pause");
    return 0;
}

0.13、选择法排序

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

int main()
{
    int a[]={10,7,1,9,4,6,7,3,2,0};
    int n;
    int i=0;
    
    n=sizeof(a)/sizeof(a[0]);	//元素个数
    
    printf("排序前:")
    for(i=0;i<n;i++)
    {
        printf("%d ",a[i]);
        
    }
    printf("\n");
    
    //选择法排序:每一次把当前序列中最小的数放到当前序列最前面
    for(i=0;i<n-1;i++)
    {
        for(int j=i+1;j<n;j++)
        {
            if(a[i]>a[j])
            {
                int temp=a[i];
                a[i]=a[j];
                a[j]=temp;
            }
        }
    }
    
    printf("排序后:")
    for(i=0;i<n;i++)
    {
        printf("%d ",a[i]);
        
    }
    
    printf("\n");
    system("pause");
    return 0;
}
  • 函数封装和数组形参退化为指针:把数组输出、数组排序等功能封装到函数里面。当数组作为函数的形参的时候,数组会退化为指针,丢失了长度信息。
  • 数组形参退化为指针:
void print_array(int a[10],int n)		//在这里,数组形参使用 int a[10], int a[2], int a[] 或者 int* a都是一样的,传入的都是数组的首地址
{
	return;
}

1、内存四区

1.1、数据类型的本质

1)数据类型基本概念

  • 类型是对数据的抽象
  • 类型相同的数据具有相同的表示形式、存储格式、相关的操作
  • 程序中使用的数据必定属于某种数据类型
  • 数据类型和内存 有关系
  • C/C++ 引入数据类型,可以更方便地管理数据

2)数据类型的本质

  • 数据类型可以理解为创建变量的模具:固定内存大小的别名
  • 数据类型的作用:编译器预算对象(变量)分配的内存空间大小
  • 数据类型只是模具,编译器不为类型分配空间,只有根据类型创建的变量才会分配空间
int main()
{
    int a;			//告诉编译器分配 4 个字节
    int b[10];		//告诉编译器分配 4*10 个字节
    //类型的本质是固定大小内存的别名
    
    printf("sizeof(a)=%d\nsizeof(b)=%d\n",sizeof(a),sizeof(b));		//4 40
    
    //打印地址
    printf("b:%d	&b:%d\n",b,&b);		//数组名字就是数组首元素地址,数组首地址		两个输出一样的
    
    
    //b 和 &b 的数据类型不一样
    //b,数组首元素地址,一个元素 4 字节,+1 -> +4
    //&b,整个数组的首地址,一个数组 4*10=40字节,+1 -> +40
    printf("b+1:%d	&b+1:%d\n",b+1,&b+1);		//14678444 14678480
    
    //指针类型长度,32位为4, 64位为8
    char *****************p=NULL;
    int *q=NULL;
    printf("%d %d\n",sizeof(p),sizeof(q));		//32位:4 4		64位:8 8
    
    return 0;
}

3)数据类型的别名

  • typedef 可以给类型取别名,且 typedef 只能给类型取别名
typedef unsigned int u32;

//typedef 通常和结构体一起使用
typedef struct	myStruct		//这里的 myStruct 可写可不写
{
    int a;
    int b;
}TMP;
int mian()
{
    u32 t;				//unsigned int t;
    return 0;
}

4)void 类型(空类型、无类型)

  • 函数参数为空,在定义函数的时候,可以使用 void 来修饰:int func(void); 在 C++ 中 void 写不写是一样的,但是在 C 中是存在区别的
  • 函数没有返回值,使用 void 来修饰:void func(void);
  • 不能定义 void 类型的普通变量:void a; //err
  • 可以定义 void* 类型的指针:void* p;
    • 主要是因为 void 类型的普通变量不同类型的内存大小不一样,在编译器分配内存空间的时候无法确定分配的内存大小,而在相同的操作系统下,不同类型的指针变量内存大小相同,不影响编译器的内存分配
  • void* p 万能指针,常用作函数返回值,或者函数的参数
    • 这样可以很灵活,只要是指针就可以使用,例如 molloc 函数的定义:void* molloc(size_t size)
    • memcpy 函数,拷贝内存的内容,可以拷贝各种类型的数组,而 strcpy,只能拷贝 char 类型的数组

1.2、变量的使用

  • C 语言中,一维数组、二维数组其实也是有数据类型的
  • C 语言中,函数也是具有数据类型的,可以通过函数指针进行重定义

1)变量的本质

  • 变量:既能度又能写的内存对象。一旦初始化后不能修改的称为称量
  • 变量定义形式:
    • 类型 标识符1,标识符2,…,标识符n
  • 变量的本质是:一段连续内存空间的别名
int main()
{
    int a;
    
    //变量相当于门牌号,内存相当于房间
    //直接赋值
    a=10;
    
    pringf("a=%d\n",a);		//10
    
    //间接赋值
    pringf("&a=%p\n",&a);		// a的地址
    p=&a;
    pringf("p=%p\n",p);		// a的地址
    
    *p=22;
    pringf("a=%d\n",a);		//22
    pringf("*p=%d\n",*p);		//22
    
    return 0;
}

1.3、内存四区模型

  • 四区:栈区、堆区、全局区、代码区(不用管)

1)全局区(静态区):全局变量、静态变量、文字常量

  • 全局变量和静态变量,初始化的全局变量和静态变量存储在一起,未初始化的全局变量在另一块
  • 全局区相同的常量只存在一份
char *get_str1()
{
    char *p="abcdef";		//文字常量区
    return p;
}

char *get_str1()
{
    char *q="abcdef";		//文字常量区
    return q;
}

int main()
{
    char *p=NULL;
    char *q=NULL;
    
    p=get_str1();
    
    //%s,打印指针指向的内存区域的内容
    //%d,打印 p 本身的值
    printf("p=%s, p %d\n",p,p);			//p=abcdef,p=一串地址
    
    q=get_str2();
    printf("q=%s, q %d\n",q,q);			//q=abcdef,q=一串地址,且这个地址和p的地址一样
    
    //主函数里面的p和 get_str1里面的p对应不同的内存
    //全局区相同的常量只存在一份,因此主函数里面的 p 和 q 指向同一块内存
    
    return 0;
}

2)栈区

char* get_str()
{								//字符串 "sdskcnckjana" 是存放在全局区
    char str[]="sdskcnckjana";		//栈区,函数结束,内存销毁,主函数中复制内存的内容,因此复制到的内容是不确定的,可能是原本的内容,也可能是乱码
    							
    							//这里在 char str[]="sdskcnckjana" 之后,会拷贝一份字符串到栈区
    return str;
}

int main()
{
    char* buffer[128]={0};
    
    strcpy(buffer,get_str());
    printf("%s\n",buffer);		//打印的结果:不确定,即乱码,这里还有可能输出 sdskcnckjana,是因为这里拷贝的时候可能 get_char 还没有销毁
    return 0;
}
char* get_str()
{								//字符串 "sdskcnckjana" 是存放在全局区
    char str[]="sdskcnckjana";		//栈区,函数结束,内存销毁,主函数中复制内存的内容,因此复制到的内容是不确定的,可能是原本的内容,也可能是乱码
    printf("%s\n",buffer);		//打印的结果:sdskcnckjana
    							
    							//这里在 char str[]="sdskcnckjana" 之后,会拷贝一份字符串到栈区
    return str;
}

int main()
{
    char* buffer[128]={0};
    
    char* p=NULL;
    p=get_str();
    printf("%s\n",buffer);		//打印的结果:不确定,即乱码
    
    return 0;
}

3)堆区

char* get_str()
{								//字符串 "sdskcnckjana" 是存放在全局区
    char str[]="sdskcnckjana";		//栈区,函数结束,内存销毁,主函数中复制内存的内容,因此复制到的内容是不确定的,可能是原本的内容,也可能是乱码
    printf("%s\n",buffer);		//打印的结果:sdskcnckjana
    							
    							//这里在 char str[]="sdskcnckjana" 之后,会拷贝一份字符串到栈区
    return str;
}

char* strget2()
{
    char *temp=(char*)malloc(100);		//堆区分配空间
    
    if(temp==NULL)
        return NULL;
    strcpy(tmp,"snjcscsdmkcs");
    //在这里,字符串"snjcscsdmkcs"存放在全局区,temp存放在栈区,指向一块堆区的内存,strcpy之后,会拷贝一份字符串"snjcscsdmkcs"到temp指向的堆区内存
    //get_str2函数运行完毕之后,p也会指向temp指向的堆区内存,并且会释放指针temp,但是temp指向的内存不会释放,需要手动释放
    return temp;
}
int main()
{
    char* buffer[128]={0};
    
    char* p=NULL;
    p=get_str2();
    if(!p)
    {
        printf("%s\n",buffer);		//打印的结果:snjcscsdmkcs
        free(p);		//释放p之前,这块堆内存使用权归p,释放之后,使用权归操作系统,但是内部的内容依然存在,直到下次被写才会改变,并且释放p之后,p依然指向这块堆区域,只是p指向的堆内存可以由系统支配了,所以一般指针释放之后,会将其指向空指针
        p=NULL;
    }
    	
    
    return 0;
}

1.4、函数调用模型

  • 关注重点在于调用的流程和变量的生命周期
  • 调用模型是一个栈模型:先调用,后返回

image-20220531161154720

1.5、函数调用变量传递分析

  • main 函数调用子函数1,子函数1调用子函数2,那么 main 函数在栈区开辟的内存,子函数1和子函数2都可以使用
  • main 函数在堆区开辟的内存,没有释放的时候,子函数1和子函数2都可以使用
  • 子函数1在栈区开辟的内存,子函数1和子函数2都可以使用,但是 main 函数无法使用
  • 子函数在堆区开辟的内存,没有释放的时候,main 函数、子函数1和子函数2都可以使用
  • 全局区存放的变量,生命周期和程序一致,因此无论哪个函数在全局区开辟的内存,所有函数都可以使用

1.6、静态局部变量的使用

int *getA()
{
    static int a=10;	//a是一个局部的静态变量,函数结束,内存不释放,因此只要把地址传出去,就可以通过地址使用这个内存了
    return &a;
}

int main()
{
    int *p=getA();		//通过地址使用局部静态变量
    
    return 0;
}
  • 在变量的生命周期之外,只要内存没有释放,就能够通过一定的手段使用对应的内存

1.7、栈的生长方向和内存释放方向

  • 栈底,高地址;栈顶,低地址。栈的生长方向:栈底到栈顶,即栈的高地址到低地址,一般描述为从上到下
  • 堆的生长方向与栈相反,从低地址到高地址,一般描述为从下到上
  • 栈内的数组内部,是从低地址向高地址的,即栈内数组内部,也是从下到上

2、指针强化

2.1、指针也是一种数据类型

  • 指针变量也是一种变量,占有内存空间,用来保存内存地址
  • 通过星号操作内存
    • 在指针声明的时候,星号表示所声明的是指针变量
    • 在指针使用的时候,星号表示操作指针所指向的内存空间中的值
    • *p 相当于通过地址(p 变量的值)找到一块内存,然后操作内存
int main()
{
    int a=100;
    int *p=NULL;
    int p1=NULL;
    char *********q=0x1111;
    
    //指针指向谁,就把谁的地址赋值给指针
    p1=&a;
    //通过星号可以找到指针指向的内存区域,操作的还是内存
    //星号放在等号的左边,给内存赋值,写内存
    //星号放在等号的右边,取内存赋值,读内存
    *p1=22;
    
    printf("%d %d\n",sizeof(p),sizeof(q));		//32位系统:4 4
    return 0;
}

2.2、指针间接赋值

  • void* 类型的指针在使用的时候需要转换成实际的类型
int main()
{
    void* p;
    char buf[1024]="aancnciwce";
    p=buf;
    //void* 类型的指针在使用的时候需要转换成实际的类型
    printf("%s\n",(char*)p);
    
    int a[100]={1,2,3,4};
    p=a;
    int i=0;
    //void* 类型的指针在使用的时候需要转换成实际的类型
    for(i=0;i<4;i++)
    {
        printf("%d ",*((int*)p+i));
    }
    
    int b[3]={1,2,3};
    int c[3];
    memcpy(c,b,sizeof(b));		//void* 类型的指针转换成了 int*
    for(i=0;i<3;i++)
    {
        printf("%d ",c[i]);
    }
    
    char *q=NULL;			//#define NULL ((void*)0),这里实际上q没有具体的指向,所以给q指向的内存赋值就会报错
    
    /*
    //加上这样两句,就可以给q 一个具体的指向,再给其指向的内存赋值,就没问题了
    char str2[100]={0};
    q=str2;
    */
    
    //给 q 指向的内存区域赋值
    strcpy(q,"1234");		//err
    
    return 0;
}
  • 分文件编程说明
//防止头文件重复包含
#pragma once

//例如,有两个头文件 a.h 和 b.h,且在 a.h 中包含了 b.h,在 b.h 中包含了 a.h,那么会出现头文件包含的死循环,导致文件包含过多的错误

//不添加兼容 C++,使用 C++ 语言的程序调用 C 语言的程序的语句,编译的时候不会有问题,但是使用的时候会有问题。添加了之后,可以不做任何改动,直接使用
  • 复习:
    • 数据类型本质是固定内存大小的别名
    • typedef,给数据类型起别名
    • 栈和堆,栈是为了效率,堆为了内存分配更加灵活
    • 栈的分配和回收由系统进行,堆的分配和回收由程序员进行
    • 数组做形参,退化为指针。数组作为形参,丢失长度信息,使用 sizeif(a)/sizeof(a[0]) 无法计算出数组长度
    • strcpy(p,”abcdefg”); 实际上不是给指针赋值,而是把字符串 “abcdefg” 拷贝到指针 p 指向的内存空间*
  • 字符串,通过首地址可以用 printf 打印出来,而数组不可以,是因为字符串末尾有字符串结束符,而数组没有
  • 指针变量和指针指向的内存是两个不同的概念
    • 改变指针变量的值,会改变指针的指向,但是不会改变指针指向的内存的内容
    • 改变指针指向的内存的内容,不会改变影响到指针变量的值
  • 使用指针写内存的时候,一定要确保内存可写
char *buf="nscnscnwsicw";	//指针直接指向文字常量区
buf2[2]='l';			//err,因为这个字符串存放在文字常量区,内容不可改

char str[]="nsijacnicwc";	//字符串常量本身存放在文字常量区,但是由于字符数组赋值,会复制一份存放在栈区
str[2]='l';				//OK,由于 str 是存放在栈区的字符数组,因此是可修改的

  • 指针是一种数据类型,是指它指向的内存空间的数据类型

    • 指针步长 (p++),根据指针所指向的内存空间的数据类型来确定
      p++ 等价于 (unsigned char)p+sizeof(a);
    
  • 不允许向 NULL 和未知非法地址拷贝内存

char *p3=NULL;
strcpy(p3,"lll");		//err,如果给 p3 赋值为某一个具体的非法地址,如 0x0001,也会出错,因为这个内存不允许使用
//给 p3 指向的内存区域拷贝内存,但是 p3 为空,没有指向任何有效的内存,因此内存拷贝会出错

2.3、通过指针间接赋值

  • 步骤:
    • 一般变量和指针变量
    • 建立关系
    • 通过 * 操作内存
int main()
{
    int a=100;
    int *p=NULL;
    
    //建立关系,指针指向谁,就把谁的地址赋给指针了
    p=&a;
    
    //通过 *  操作内存;
    *p=32
        
    return 0;
}
  • 如果想通过形参改变实参的值,必须地址传递
int get_a()
{
    int a=10;
    return a;
}

void get_a2(int a)
{
    a=22;
}

void get_a3(int *a)
{
    *a=33;		//通过星号操作内存
}

void get_a4(int *a1,int *a2,int *a3,int *a4)
{
    *a1=33;		//通过星号操作内存
    *a2=44;
    *a3=55;
    *a4=66;
}

int main()
{

    int a=get_a();
    printf("%d\n",a);		//输出为:10

    get_a2(a);
    printf("%d\n",a);		//输出为:10
    
    //如果想通过形参改变实参的值,必须地址传递
    //实参,形参
    get_a2(a);				//在函数调用时,建立关系
    printf("%d\n",a);		//输出为:33
    
    int a1,a2,a3,a4;
    get_a4(&a1,&a2,&a3,&a4);
    printf("%d %d %d %d\n",a1,a2,a3,a4);
    
    
    
    return 0;
}
  • 间接赋值是指针的最大意义,尤其是配合函数使用的时候
  • 二级指针间接赋值
void func1(int *p)
{
    p=0xaabb;
    printf("%p\n",p);		//0000aabb
}

void func2(int **p)
{
    *p=0xeeff;				//需要深入理解
    printf("%p\n",p);		//0000eeff
}

int main()
{
    /*
    //一个变量应该定义一个什么类型的指针保存它的地址
    //在原来的基础上再多加一个*
    int a=10;
    int *p=&a;
    int **q=&p;
    
    int *********t=NULL;
    int **********tp=&t;
    */
    int *p=0x1122;
    printf("%p\n",p);		//00001122
    
    func1(p);				//值传递,传递的是指针变量的值
    printf("%p\n",p);		//000011222
    
    func2(&p);				//地址传递,传递的是指针变量的地址
    printf("%p\n",p);		//0000eeff
    
    return 0;
}

2.3、指针作为函数参数的输入输出特性

  • 主调函数可以把堆区、栈区、全局数据内存地址传给被调函数
  • 被调函数只能返回堆区、全局数据
  • 指针作为函数参数具有输入输出特性:
    • 输入:主调函数分配内存
    • 输出:被调函数分配内存
void func(char* p)
{
    //给p指向的内存区域拷贝,实际上就是main中的buf
    strcpy(p,"ssvcscac");
}

void func1(char **p,int *len)
{
    if(p==NULL)
        return;
    
    char* tmp=(char*)malloc(100);
    if(tmp==NULL)
        return;
    strcpy(tmp,"cscnsncscna");
    
    //间接赋值
    *p=tmp;
    *len=strlen(tmp);    
}

int main()
{
    //输入:主调函数分配内存
    char buf[100]={0};
    func(buf);
    printf("%s\n",buf);		//ssvcscac
    
    char *p=NULL;
    func(p);		//err,不能给空或者非法未知内存拷贝
    
    //输出:被调用函数分配内存,要想进行内存修改,必须进行地址传递
    char *p1=NULL;
    int len=0;
    func1(&p1,&len);
    if(p)
    	printf("%s	%d\n",p,len);		//cscnsncscna 11
    
    
    return 0;
}

二、字符串与指针

1、字符串的基本操作

1)字符串初始化

  • C 语言没有字符串类型,通过字符数组模拟
  • C 语言字符串,以字符 \0 或者 数字 0 结尾
int main()
{
    //不指定长度,且没有结束符
    char buf[]={'a','b','c'};
    printf("%s\n",buf);				//输出完 abc 之后,会输出一串乱码,是因为没有字符数组没有指定长度,且末尾没有字符串结束符
    //指定长度,后面没有赋值的元素会自动补零
    char buf1[100]={'a','b','c'};
    printf("%s\n",buf1);			//输出:abc
    
    //所有元素都赋为零
    char buf3[100]={0};
    
    char buf4[2]={'a','b','c'};		//err,数组越界
    
    char buf5[50]={'1','a','b',0,'7'};		//{'1','a','b','\0','7'} 这样赋值也是一样的结果
    printf("%s\n",buf6);			//输出:1ab
    
    //常用的字符串初始化
    char buf6[]="sdnsjkcnsjcs";
    //strlen 计算字符串长度,不包括字符串结束符
    //sizeof 计算的是数组长度
    printf("strlen=%d\n,sizeof=%d\n",strlen(buf6),sizeof(buf6));		//12 13
    
    char buf7[100]="sdnsjkcnsjcs";
    printf("strlen=%d\n,sizeof=%d\n",strlen(buf7),sizeof(buf7));		//12 100
    
    return 0;
}

2)转义字符说明

  • 当使用 \0 的时候,尽量不要在其后面根其他数字,容易使得 \0 和其他数字在一起组合成其他的转义字符,例如: \012 就是换行符 \n

3)字符串操作

int mian()
{
	char buf[]="sdncscwckamcaskc";
    
    //数组方式访问字符串元素
    int i=0;
    for(i=0;i<strlen(buf);i++)
    {
        printf("%c",buf[i]);
    }
    printf("\n");
    
    //指针方式访问字符串元素
    //数组名就是数组首地址
    char *p=buf;
    for(i=0;i<strlen(buf);i++)
    {
        printf("%s",*(p+i));		//这种写法也可以:printf("%s",p[i]);
    }
    printf("\n");
    
    for(i=0;i<strlen(buf);i++)
    {
        printf("%s",*(buf+i));
    }
    printf("\n");
    
    //p和buf完全等价吗?
    //并不是,buf是一个指针常量,不能修改,而p可以修改,这是为了保证数组的内存可以回收(编译器实现就这么规定的)
    p++;		//OK
    buf++		//err
        
    
    return 0;
}

4)字符串拷贝

void my_strcpy(char *dst,char *src)
{
    int i=0;
    
    for(i=0;isrc[i]!=0;i++)
    {
        dst[i]=src[i]
    }
    dst[i]=0;
}

void my_strcpy1(char *dst,char *src)
{
    while(*src)
    {
        *dst=*src;
        src++;
        dst++
    }
    *dst=0;
}

void my_strcpy2(char *dst,char *src)
{
    while(*src)
    {
        *dst=*src;
        src++;
        dst++
    }
    *dst=0;
}

void my_strcpy5(char *dst,char *src)
{
    while(*dst++=*src++);		//先用了再加,写代码不能盲目追求代码简洁,而是应该让代码有足够好的性能和可读性
    *dst=0;
}

//完善字符串拷贝函数
//成功返回0,失败返回非零
//1、判断形参指针是否为空
//2、最好不要直接使用形参
int my_strcpy(char *dst,char *src)
{
    if(!dst||!src)
        return -1;
    
    //赋值变量,把形参备份
    char *to=dst;
    char *from=src;
     while(*from)
    {
        *to=*from;
        from++;
        to++
    }
    *to=0;
   return 0; 
}

int main()
{
    char src[]="ncscnsjicdjv";
    char dst[100];
    
    int ret=my_srtcpy(dst,src);
    if(ret)
    {
        printf("my_strcpy5 err: %d\n",ret);
        return ret;
    }
        
    return 0;
}

2、项目开发中常用字符串应用模型

1)while 和 do-while 模型

  • 子串查找
int main()
{
    char *p="ncsjcscszmnascnscuicakcnsucn";
    int cnt=0;
    /*
    do
    {
        p=strstr(p."xnasx");
        if(p!=NULL)
        {
            n++;				//累计个数
            //重新设计查找的起点
            p=p+strlen("xnasx");
        }
        else		//如果没有匹配的字符串,跳出循环
        {
            break;
        }
    }while(*p!=0);
    */
    char *p;
    while((p=strstr(p,"xnasx")!=NULL))
    {
        p=p+strlen("xnasx");
        cnt++;
        if(*p==0)
            break;
    }
    printf("%d\n",cnt);
    return 0;
}

2)两头堵模型

  • 查找字符串非空格字符个数

    • 实际就是数组中常用的双指针思想
  • 复习:

    • 指针也是一种数据类型,指针也是一种变量

    • 指针和指针指向的内存是两个不同的概念

    • 改变指针变量的值,会改变指针的指向,不影响指向内存的内容

    • 改变指针指向内存的内容,不影响指针的值,即指针的指向不变

    • 写内存时,一定要保证 内存可写

    • 只有地址传递,才能通过形参的修改,来影响实参的值

    • 不允许向 NULL 或者位置非法地址拷贝内存

    • void* 型的指针,建议使用的时候使用强制类型转换转化为目标类型的指针

    • 栈返回变量的值和变量的地址的区别

    • 可执行程序的生成过程:

      • 预处理:宏定义展开、头文件展开、条件编译、此时不进行语法检查
      • 编译:检查语法、将预编译处理后的文件编译生成汇编文件
      • 汇编:将汇编文件生成目标文件(二进制文件)
      • 链接:将目标文件链接为可执行程序

      程序只有在运行时才加载到内存(由系统完成),但是某个变量具体分配多大内存,是在编译阶段就已经确定了。即,在比那一阶段做完处理之后,程序运行时系统才知道分配多大内存空间,因此,变量的空间在编译时就确定了,只是在运行时系统才知道具体分配多大内存,并分配相应的内存空间。

    • 变量内存的值和变量的地址

    • C 语言中没有字符串类型,使用字符数组进行模拟,数组的各种操作

    • 代码编写原则:在保证性能的前提下,尽可能保证好的阅读性

3、const 的使用

1)const 修饰变量

  • const:修饰一个变量为只读
typedef struct
{
    int a;
    int b;
}myStruct;
void func(myStruct *p)
{
    //指针能变
    //指针指向的内存的内容也可变
    p->a=10;		//OK
    
}
void func2( const myStruct* p)
{
    p=NULL;		//OK
    p->a=10;		//err
}
void func2(myStruct*   const p)
{
    p=NULL;		//err
    p->a=10;		//OK
}
void func2(const myStruct*   const p)
{
    myStruct tmp;
    p=NULL;		//err
    p->a=10;		//err
    tmp.a=p->a;		//OK
    
}
int main()
{
    //const:修饰一个变量为只读
    const int a=10;
    a=100;			//err
    
    //指针变量,指针指向的内存,2个不同的概念
    char buf[]="nsjcksic sdjhc";
    //从左往右看,跳过类型关键字,如果 const 后面是 *,说明指针指向的内存内容不可以修改;如果 const 后面是指针变量,说明指针变量的值不能修改
    const char* p=buf;		//这个和上面的 char buf[]="nsjcksic sdjhc"; 写法是一致的,都是指针变量可以修改,但是指针指向的内存的内容不可以修改
    //char const *p=buf; 这种写法和上面的是一致的
    p[1]='2';		//err
    p="ncjndscslkcm";		//OK
        
    char* const p2=buf;
    p2[1]="c";			//OK
    p2="cmsdcmsdics";		//err
    
    //p3为只读,指向不能变,指向的内容也不能变
    const char* const p3=buf;
    
    return 0;
}
  • 引用另一个 .c 文件中的 const 变量
  • const 修饰的变量在定义的时候必须初始化,否则后面就无法进行赋值和初始化了
//如何引用另一个 .c 文件中的 const 变量
extern const int a;		//只能声明,不能再赋值
printf("%d\n",a);

2)C 语言的 const 是假的 const

  • C 语言中,const 修饰的变量,无法通过变量名进行修改,但是可以通过指针修改
int main()
{
    //无法通过a来修改值,但是可以通过指针来修改
    const int a=10;
    a=100;		//err
    int *p=&b;
    *p=22;
    printf("%d %d\n",a,*p);		//22 22
    
    return 0;
}

4、二级指针

4.1、二级指针作为输出,函数传参

int getNUm(char* p)
{
    p=(char*)malloc(sizeof(char)*100);
    if(p==NULL)
        return-1;
    strcpy(p,"cnscnc");
    printf("p=%s\n",p);				//输出:cnscnc
    return 0;						//运行完毕,虽然堆空间没有释放,但是函数中指向堆空间的指针已经销毁,而主函数中的实参并没有指向堆空间,因此在主函数中输出的字符串仍旧为空
}
int getNUm(char** p)		//形参是指向 main 函数中的p的指针,
    //tmp 指向堆空间中的内存,并将文字常量区的字符串复制到 tmp 指向的堆空间,最后使 p 也指向这块堆空间,因此在 main 函数中可以输出这个字符串
{
    if(p==NULL)
        return-1;
    char *tmp=NULL;
    tmp=(char*)malloc(sizeof(char)*100);
    if(p==NULL)
        return-2;
    strcpy(tmp,"cnscnc");
    *p=tmp;
    printf("p=%s\n",p);				//输出:cnscnc
    return 0;						
}

int main()
{
    char *p=NULL;
    int ret=0;
    /*
    ret=getNUm(p);				//值传递,传递的是指针变量
    //值传递,形参的任何改变都不会影响到实参
    */
    
    ret=getNum2(&p);			//地址传递,传递的是指针变量的地址
    //形参修改会影响到实参
    if(ret!=0)
    {
        printf("getNum err:%d\n",ret);
        return ret;
    }
    printf("p=%s\n",p);				//输出:cnscnc
    if(p!=NULL)
    {
        free(p);
        p=NULL;
    }
    return 0;
}

4.2、二级指针作为输入

1)指针数组

  • 指针数组,指针的数组,是一个数组,每一个元素都是指针
int main()
{
    //指针数组,指针的数组,是一个数组,每一个元素都是指针
    //每个元素都是相同类型的指针
    char *p[]={"11111111","22222222","333333333","44444444"};		//数组 p 有四个元素,每个元素四个字节大小
    char **q={"11111111","22222222","333333333","44444444"};		//err,因为q是一个指针,不能同时指向多个内存,q可以作为形参,但是不可以这么定义赋值
    int n=sizeof(p)/sizeof(p[0]);		// 16/4=4
    printf("%d\n",n);		// 4
    int i=0;
    for(i=0;i<4;i++)
    {
        printf("%s\n",p[i]);		//四个字符串逐一输出
    }
    
    char *q[10]={"11111111","22222222","333333333","44444444"};	
    int m=n=sizeof(q)/sizeof(q[0]);		// 40/4=4
    printf("%d\n",m);		// 10
    char *tmp;
    //选择法对p进行排序
    for(i=0;i<n-1;i++)
    {
        for(int j=i+1;j<n;j++)
        {
            if(strcmp(p[i],p[j])>0)
            {
                tmp=p[i];
                p[i]=p[j];
                p[j]=tmp;
            }
        }
    }
    
    return 0;
}

2)第一种内存模型:指针数组

int test(int a[],int n);
//等价于:
int test(itn* a,int n);

int func(int *p[],int n);
//等价于:
int func(int **p,int n);

3)第二种内存模型:二维数组

  • 二维数组,多个等长的一维数组,从存储角度看,依然是一维线性的
void printf_array(char **a,inr n)		//二维数组作为参数,这么写不对
{
    for(int i=0;i<n;i++)
    {
        printf("%s\n",a[i]);		//err
    }
}
void printf_arry1(char a[][30],int n)	//二维数组作为参数,可以这么写
{
    for(int i=0;i<n;i++)
    {
        printf("%s\n",a[i]);		//OK
    }
}

void sort_arrry(char a[][30],int n)
{
    itn i=0,j=0;
    char tmp[30];
    for(i=0;i<n-1;i++)
    {
        for(j=i+1;j<n;j++)
        {
            if(strcmp(a[i],a[j])>0)
            {
                //交换的是内存块
                strcpy(tmp,a[i]);
                strcpy(a[i],a[j]);
                strcpy(a[j],tmp);
            }
        }
    }
}

char a[4][30]={"111111111","222222222","333333333","444444444"};
//a,首行地址;a+i,第 i 行地址
//a代表首行地址,首行首元素地址有区别,但是值是一样的,区别是地址的步长不一样
//首行地址 +1,移动30 个字节,首行首元素地址 +1,移动 1 个字节
//首行地址和首元素地址是一样的,注意 *(a+i),a[i],a+i的区别

int n=sizeof(a)/sizeof(a[0]);		//n=4

//二位是数组的第一个维度可以不写,但是必须满足一定的条件:必须在二维数组定义的时候初始化
char b[][30]={"111111111","222222222","333333333","444444444"};		//OK
char c[][30];			//err

printf_array(a,n);

4)第三种内存模型:动态生成二维数组,指针数组

int main()
{
    char *p=NULL;
    p=(char*)malloc(100);
    strcpy(p,"nsjkcnsics");
    
    //10 个 char*,每个都是NULL
    char *p1[10]={0};
    for(int i=0;i<10;i++)
    {
        p[i]=(char*)malloc(100);
        strcpy(p[i],"cnijcns");
    }
    int a[10];
    int *q=(int*) malloc(10*sizeof(int));		//相当于是 q[10]
    
    //动态分配一个数组,数组每个元素都是 char*
    //char* ch[10]
    int n=3;
    char **buf=(char**)malloc(sizeod(char*)*n);		//相当于 char* buf[3];
    if(buf==NULL)
        return -1;
    for(i=0;i<n;i++)
    {
        buf[i]=(char*)malloc(30*sizeof(char));
        char str[30];
        strcpy(buf[i],"fcnijcnjwi");	
        //strcpy(buf[i],str);	
    }
    for(i=0;i<n;i++)
    {
        printf("%s\n",buf[i]);
    }
    
    
    //内存释放,先释放内层,再释放外层
    for(i=0;i<n;i++)
    {
        free(buf[i]);
        buf[i]=NULL;
    }
    if(buf!=NULL)
    {
        free(buf);
        buf=NULL;
    }
    
    return 0;
}
char **getMem(int n)
{
    int i=0;
    char **buf=(char**)malloc(sizeod(char*)*n);		//相当于 char* buf[3];
    if(buf==NULL)
        return NULL;
    for(i=0;i<n;i++)
    {
        buf[i]=(char*)malloc(30*sizeof(char));
        char str[30];
        strcpy(buf[i],"fcnijcnjwi");	
        //strcpy(buf[i],str);	
    }
    for(i=0;i<n;i++)
    {
        printf("%s\n",buf[i]);
    }
    return buf;
}

void print_buf(char **buf,int n)
{
    int i=0;
    for(i=0;i<n;i++)
    {
        printf("%s\n",buf[i]);
    }
}

void free_buf(char **buf,int n)
{
    int i;
    for(i=0;i<n;i++)
    {
        free(buf[i]);
        buf[i]=NULL;
    }
    if(buf!=NULL)
    {
        free(buf);
        buf=NULL;
    }
}
int main()
{
    char **buf=NULL;
    int n=3;
    buf=getMem(n);
    if(buf==NULL)
    {
        printf("getMem err\n");
        return -1;
    }
    print_buf(buf,n);		//值传递
    free_buf(buf,n);		//值传递
    buf=NULL;
    return 0;
}

5、一维数组的使用

5.1、一维数组的赋值

int a[]={1,2,3,4,5,6,7,8};
//int b[];	//err,不指定长度,在定义的时候必须初始化
int c[100]={1,2,3,4};		//没有赋值的元素都为 0 

int n=0;
//sizeof,计算变量的类型所占的空间
//sizeof(a)=4*8=32	// 数组类型的大小,由元素个数、元素类型决定
n=sizeof(a)/sizeof(a[0]);		//n=8

//元素访问:a[i], *(a+i), 两种写法等价
//a+i 代表第 i 个元素的地址

5.2、数组类型的定义

int a[]={1,2,3,4,5,6,7,8};
//a 代表首元素地址,&a 代表整个数组的首地址,和首元素地址一样,但是他们的步长不一样
//a+1 跳一个元素,&(a+1) 跳整个数组长度
pritnf("%d %d\n",a,a+1);			// +4
pritnf("%d %d\n",&a,&(a+1));		// +32

//通过 typedef 定义一个数组类型
//有 typedef 是类型,没有 typedef 是变量
typedef int A[8];			//代表是一个数组类型,这里的 A 是一个数据类型,不是变量
//等价写法:typedef int (A)[8];

A b;		//等价于 int b[8]; 关于 b 的各种操作,与 int b[8] 的各种操作是一致的
//取了 typedef,b 替换到 A 的位置,就是他的含义

for(int i=0;i<8;i++)
{
    b[i]=i;
}

6、数组指针与指针数组

6.1、数组指针:指向一个数组的指针

  • 数组指针:指向一维数组的整个数组,而不是首元素

1)先定义数组类型,根据类型定义指针

int main()
{
    int a[10]={0};
    //定义数组指针变量
    //1、先定义数组类型,根据类型定义指针
    typedef int A[10];		//[10]代表步长
    A *p=NULL;				//p 是一个数组指针变量
    p=&a;		//OK, &a 代表整个数组的首地址
    //p=a;		//不会报错,但是会有警告,这么写不准确,p是指针数组,而a 是数组首元素的地址
    //但是 a 和 &a 一样,a 最终也会被当做 &a,但是这么写不好
    pritnf("%d %d\n",p,p+1);		//输出的两个地址相差 40 ,步长为整个数组的长度 
    
    for(int i=0;i<10;i++)
    {
        //a[]
        //p=&a;		*p=*&a -> a
        (*p)[i]=i+1;
    }
    for(int i=0;i<10;i++)
    {
        printf("%d ",(*p)[i]);			//输出:1 2 3 4 5 6 7 8 9 10
    }
    printf("%d\n",sizeof(p));			// 4   
    return 0;
}

2)先定义数组指针类型,再定义变量

int main()
{
    int a[10]={0};
    //和指针数组写法很类似,但是多了 ()
    //() 和 [] 优先级一样,从左往右
    //() 内有指针,说明他是一个指针,[]说明是一个数组,因此是一个数组指针,前面有 typedef,说明是类型 -> 数组指针类型
    typedef int (*P)[10];			
    P q;				//数组指针类型变量
    q=&a;
    for(int i=0;i<10;i++)
    {
        //a[]
        //p=&a;		*p=*&a -> a
        (*p)[i]=i+1;
    }
    for(int i=0;i<10;i++)
    {
        printf("%d ",(*p)[i]);			//输出:1 2 3 4 5 6 7 8 9 10
    }
    printf("%d\n",sizeof(p));			// 4  
    return 0;
}

3)直接定义数组指针变量

int main()
{
    int a[10]={0};
	int (*q)[10];		//数组指针变量	
    q=&a;				//指向 a 数组
    for(int i=0;i<10;i++)
    {
        //a[]
        //p=&a;		*p=*&a -> a
        (*p)[i]=i+1;
    }
    for(int i=0;i<10;i++)
    {
        printf("%d ",(*p)[i]);			//输出:1 2 3 4 5 6 7 8 9 10
    }
    printf("%d\n",sizeof(p));			// 4  
    return 0;
}

6.2、指针数组:数组,每个元素都是指针

int main(int argc,char* argv[])		//argv,也是有个指针数组
    //argc 传参的个数,包括执行的可执行程序本身
    //argv 传入的参数字符串数组
{
    //[] 比 *  的优先级高,a 是一个指针数组
	char* a[]={"aaaaaaaa","bbbbbbbb","cccccccc"};
    
    for(int i=0;i<argc;i++)
    {
        printf("%s\n",argv[i]);		//命令行中运行:xxx.exe a b c dddd
        //输出:xxx.exe a b c dddd 共五个字符串
    }
    
    
    return 0;
}

6.3、数组越界问题说明

int main()
{
    int a[10]={0};
    a[11]=0;			//可能不会报错,只是因为刚好这个空间没有被使用,但是一旦工程项目大了,这个空间被使用了,就会报错
    return 0;
}

6.4、数组指针与二维数组

1)二维数组

  • 二维数组名 a:第 0 行首地址
  • a+i:第 i 行首地址;等价于&a[i]
  • 要想把该行首地址转换为该行的首元素地址,加 *, *(a+i),等价于 a[i]
  • 要想得到某个元素地址,加偏移量:*(a+i)+j;等价于 a[i][i]
  • 要想得到某个元素的值,在该元素的地址前加 *, *(*(a+i)+j),等价于a[i][j]
int main()
{
    int a1[3][4]=
    {
        {1,2,3,4},
        {5,6,7,8},
        {9,10,11,12}
    };		//等价于:int a1[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
    printf("%d %d\n",a1,a1=1);		//相差 16
    //二维数组名,代表的是第 0 行的首地址(区别于第 0 行首元素地址,虽然他们的值一样,但是他们的步长不一样)
    printf("%d %d\n",*(a1+0)*(a1+1));		//第 0 行首元素地址,第 1 行首元素地址,相差 16	等价于:printf("%d %d\n",a1[0],a1[1]);
    printf("%d\n",*(a1+0)+1);		//第 0 行 第一个元素地址		等价于:printf("%d\n",a[0]+1);
    //a,代表第 0 行首地址
    //a+i,代表第 i 行首地址
    //*(a+i),等价于 a[i],代表第 i 行首元素地址
    //*(a+i)+j,等价于 &a[i][j],代表第 i 行第 j 列元素的地址
    //*(*(a+i)+j),等价于 a[i][j],代表第 i 行第 j 列的元素

    return 0;
}

2)二维数组(多维数组)的存储结构

  • 不存在多维数据,线性存储的(可以通过打印上一行最后一个元素的地址和下一行第一个元素的地址,会发现他们是相邻存储的)
  • 二维数组元素个数:n=sizeof(a)/sizeof(a[0][0]])
  • 二维数组的行数:sizeof(a)/sizeof(a[0])
  • 二维数组的列数:sizeof(a[0])/sizeof(a[0][0])
int main()
{
    int a1[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
    int n=sizeof(a)/sizeof(a[0][0]]);
    for(int i=0;i<n;i++)
    {
        printf("%d ",a[i]);		//输出结果:1 2 3 4 5 6 7 8 9 10 11 12
    }
    return 0;
}

3)数组指针和二维数组

int main()
{
    //3 个 a[4] 的一维数组
    int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
    
    //定义数组指针
    int (*p)[4];
    p=a;		//指向首行首地址,步长为二维数组的一行的长度,p 的等价于数组名
    //p=&a;		//指向整个二维数组的首地址,步长为整个数组的长度
    printf("%d %d\n",p,p+1);		//相差 16,步长为二维数组的一行的长度
    for(int i=0;i<3;i++)
    {
        for(int j=0;j<4;j++)
        {
            printf("%d ",*(*(p+i)+j));		//等价于:printf("%d ",p[i][j]);
        }
        printf("\n");
    }
    
    return 0;
}

4)首行首元素地址和首行首地址的区别

  • 计算一维数组的长度:sizeof(首行首元素地址)
int t[]={1,2,3,4,5,6,7,8,9,10};
primtf("%d %d\n",sizeof(t),sizeof(&t));		// 40 4	,分别是首行首元素地址和首行首地址

int a[2][10];
pritnf("%d %d\n",sizeof(a[0]),sizeof(&a[0]));		// 40 4	,分别是首行首元素地址和首行首地址

5)二维数组做形参

void printArray(int a[3][4])
{
    for(int i=0;i<3;i++)
    {
        for(int j=0;j<4;j++)
        {
            printf("%d ",a[i][j]);
        }
    }
}

//数组指针
typedef int(*P)[]
void printArray1(int a[][4])
{
    for(int i=0;i<3;i++)
    {
        for(int j=0;j<4;j++)
        {
            printf("%d ",a[i][j]);
        }
    }
}

//数组做形参,会退化为指针
void printArray2(int (*a)[4])
{
    for(int i=0;i<3;i++)
    {
        for(int j=0;j<4;j++)
        {
            printf("%d ",a[i][j]);
        }
    }
}


int main()
{
    int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
    printArray(a);
    printArray1(a);
    printArray2(a);
    
    return 0;
}

三、结构体

1、结构体的基本操作

1)结构体类型的定义

  • struct 关键字,struct Teacher 合在一起才是类型
  • {} 后面有分号

2)结构体变量的定义

  • 先定义类型,再定义变量
  • 定义结构体的同时定义结构体变量
  • 无名称的结构体类型

3)结构体变量的初始化

  • 定义变量是直接初始化,通过 {}

4)typedef 改类型名

typedef struct Teacher
{
    char name[50];
    int age;
}Teacher1;

5)点运算符和指针法操作结构体

6)结构体也是一种数据类型,复合类型(自定义类型)

//结构体类型定义
//struct 关键字,struct Teacher 合在一起才是类型
//{} 后面有分号
struct Teacher
{
    char name[50];
    int age;
};
//结构体变量定义,全局变量
struct Teacher t;
int main()
{
    //结构体变量定义,局部变量
    struct Teacher t2={"lili",18};
    printf("%s %d\n",t2.name,t2.age);
    
    strcpy(t2.name,"xiaoming");
    t2.age=22;
    
    struct Teacher *p=NULL;		//指针指向空,无法对其成员进行操作
    p=&t2;					
    strcpy(p->name,"xiaoming");
    p->age=22;			//等价于:(*p).age=22;
    printf("%s %d\n",p->name,p->age);
    
    return 0;
}

1.1、结构体内存四区

  • 与普通变量的内存四区一致

1.2、结构体变量相互赋值

//定义结构体类型时。不要直接给成员赋值
//结构体只是一个类型,还没有分配空间
//只有根据类型定义变量时,才分配空间,有空间后才能赋值
typedef struct Teacher
{
    char name[50];
    //int age=50;		//err
    int age;
}Teacher;

void copyTeacher(Teacher to,Teacher from)
{
    to=from;
}

void copyTeacher1(Teacher *to,Teacher *from)
{
    *to=*from;
}

int main()
{
    Teacher t1={"lily","22"};
    
    //相同类型的两个结构体变量,可以相互赋值
    //把 t1 成员变量内存的值拷贝给 t2 成员变量的内存
    //t1 和 t2 在内存上没有关系,类似于:int a=10;   int b=a;	
    Teacher t2=t1;
    printf("%s %d\n",t2.name,t2.age);		//lily 22
    
    Teacher t3;
    memset(&t3,0,sizeof(t3));
    copyTeacher(t3,t1);			//t1 拷贝给 t3
    printf("%s %d\n",t3.name,t3.age);		// 0		值传递
    
    copyTeacher1(&t3,&t1);			//t1 拷贝给 t3
    printf("%s %d\n",t3.name,t3.age);		// lily 22		地址传递
    
    return 0;
}

1.3、结构体数组

1)结构体静态数组

typedef struct Teacher
{
    char name[50];
    int age;
}Teacher;
int main()
{
    //结构体数组初始化方法1:
    Teacher a[3]=
    {
        {"a",18},  
        {"a",18},  
        {"a",18}
    };
    
    //结构体数组初始化方法2:
    //这种属于是静态结构体数组
    Teacher b[3]={"a",18,"a",18};		//剩下没有赋值的部分会自动初始化为 0 
    for(int i=0;i<3;i++)
    {
        printf("%s %d\n",b[i].name,b[i].age);
    }
    
    return 0;
}

2)结构体动态数组

typedef struct Teacher
{
    char name[50];
    int age;
}Teacher;
int main()
{
    //类似于:Teacher p[3];
    Teacher* p=(Teacher*)malloc(3*sizeof(Teacher));
    if(p==NULL)
        return -1;
    char buf[50];
    for(int i=0;i<3;i++)
    {
        sprintf(buf,"name%d%d%d\n",i,i,i);
        strcpy(p[i].name,buf);
        p[i].age=20+i;
    }
    for(int i=0;i<3;i++)
    {
        printf("%s %d\n",p[i].name,p[i].age);
    }
    if(p!=NULL)
    {
        free(p);
        p=NULL;
    }
    
    return 0;
}

1.4、结构体一级指针

1)结构体套一级指针

typedef struct Teacher
{
    char *name;
    int age;
}Teacher;
int main()
{
    char* name=NULL;
    //strcpy(name,"lily");		//err,因为 name 指向空,无法对其进行拷贝
    name=(char*)malloc(sizeof(char)*30);
    strcpy(name,"lily");
    if(name!=NULL)
    {
        free(name);
        name=NULL;
    }
    
    //1.
    Teacher t;
    t.name=(char*)malloc(sizeof(char)*30);
    strcpy(t.name,"lily");
    t.age=22;
    printf("%s %d\n",t.name,t.age);		//lily 22
    if(t.name!=NULL)
    {
        free(t.name);
        t.name=NULL;
    }
    
    //2.
    Tearcher *p=NULL;
    p=(Teacher*)malloc(sizeof(Teacher)*1);
    p->name=(char*)malloc(sizeof(char)*30);
    strcpy(p->name,"lily");
    p->age=22;
    printf("%s %d\n",p->name,p->age);		//lily 22
    if(p->name!=NULL)
    {
        free(p->name);
        p->name=NULL;
    }
    if(p!=NULL)
    {
        free(p);
        p=NULL;
    }
    
    //3.
    Teacher *q=NULL;
    q=(Teacher*)malloc(sizeof(Teacher)*3);		//Teacher q[3];
    char buf[30];
    for(int i=0;i<3;i++)
    {
        q[i].name=(char*)malloc(sizeof(char)*30);
        sprintf(buf,"name%d%d%d",i,i,i);
        strcpy(q[i].name,buf);
        q[i].age=20+i;
    }
    for(int i=0;i<3;i++)
    {
        printf("%s %d\n",q[i].name,q[i].age);		
    }
    for(int i=0;i<3;i++)
    {
        if(q[i].name!=NULL)
        {
            free(q[i].name);
            q[i].name=NULL;
        }
    }
    if(q!=NULL)
    {
        free(q);
        q=NULL;
    }
    
    
    return 0;
}

1.5、结构体做函数参数

typedef struct Teacher
{
    char *name;
    int age;
}Teacher;
void showTeacher(Taecher *q,int n)
{
    int i=0;
    for(i=0;i<n;i++)
    {
        printf("%s %d\n",q[i].name,q[i].age);		
    }
}

void freeTeacher(Teacher *q,int n)
{
    int i=0;
    for(int i=0;i<3;i++)
    {
        if(q[i].name!=NULL)
        {
            free(q[i].name);
            q[i].name=NULL;
        }
    }
    if(q!=NULL)
    {
        free(q);
        q=NULL;
    }
}

Teacher* getMem(int n)
{
    Teacher *q;
    q=(Teacher*)malloc(sizeof(Teacher)*n);		//Teacher q[3];
    char buf[30];
    for(int i=0;i<n;i++)
    {
        q[i].name=(char*)malloc(sizeof(char)*30);
        sprintf(buf,"name%d%d%d",i,i,i);
        strcpy(q[i].name,buf);
        q[i].age=20+i;
    }
    return q;
}

int getMem1(Teacher **tmp,int n)
{
    if(tmp==NULL)
        return -1;
    Teacher *q;
    q=(Teacher*)malloc(sizeof(Teacher)*n);		//Teacher q[3];
    char buf[30];
    for(int i=0;i<n;i++)
    {
        q[i].name=(char*)malloc(sizeof(char)*30);
        sprintf(buf,"name%d%d%d",i,i,i);
        strcpy(q[i].name,buf);
        q[i].age=20+i;
    }
    *tmp=q;
    
    return 0;
}
int main()
{
    Teacher *q=NULL;
    //q=getMem(3);		//值传递,返回值
    
    int ret=0;
    ret=getMem1(&q,3);		//地址传递
    if(ret!=0)
        return ret;
    
    showTeacher(q,3);
    
    freeTeacher(q,3);
    q=NULL;
    
    
    return 0;
}

1.6、结构体套二级指针

typedef struct Teacher
{
    char **stu;		//一个老师有多个学生
}Teacher;

int main()
{
    //1.
    Teacher t;		
    //t.stu[3]
    //char *t.stu[3]
    int n=3;
    int i=0;
    t.stu=(char**)malloc(sizeof(char*)*n);
    for(i=0;i<n;i++)
    {
        t.stu[i]=(char*)malloc(30);
        strcpy(t.stu[i],"lily");
    }
    for(i=0;i<n;i++)
    {
        printf("%s\n",t.stu[i]);
    }
    for(i=0;i<n;i++)
    {
        if(t.stu[i]!=NULL)
        {
            free(t.stu[i]);
            t.stu[i]=NULL;
        }
    }
    if(t.stu!=NULL)
    {
        free(t.stu);
        t.stu=NULL;
    }
        
    
    //2.
    Teacher *p=NULL;
    //p->stu[3]
    p=(Teacher*)malloc(sizeof(Teacher));
    p->stu=(char**)malloc(sizeof(char*)*n);
    for(i=0;i<n;i++)
    {
        p->stu[i]=(char*)malloc(30);
        strcpy(p->stu[i],"lily");
    }
    for(i=0;i<n;i++)
    {
        printf("%s\n",p->stu[i]);
    }
    for(i=0;i<n;i++)
    {
        if(p->stu[i]!=NULL)
        {
            free(p->stu[i]);
            p->stu[i]=NULL;
        }
    }
    if(p->stu!=NULL)
    {
        free(t.stu);
        p->stu=NULL;
    }
    if(p!=NULL)
    {
        free(p);
        p=NULL;
    }
    
    
    //3.
    Teacher *q=NULL;
    //Teacher q[3]
    //q[i].stu[3]
    q=(Teacher*)malloc(sizeof(Teacher)*n);		//Teacher q[3]
    for(i=0;i<n;i++)
    {
        (q+i)->stu=(char**)malloc(n*sizeof(char*));  //char *stu[3];
        for(int j=0;j<3;j++)
        {
            (q+i)->stu[j]=(char*)malloc(30);
            char buf[30];
            sprintf(buf,"name%d%d%d",i,i,i);
            strcpy((q+i)->stu[j],buf);
        }
    }
    for(i=0;i<3;i++)
    {
        for(int j=0;j<3;j++)
        {
            printf("%s\n",(q+i)->stu[j]);
        }
    }
    //free
    for(i=0;i<3;i++)
    {
        for(int j=0;j<3;j++)
        {
            if((q+i)->stu[j]!=NULL)
            {
                free((q+i)->stu[j]);
                (q+i)->stu[j]=NULL;
            }
        }
        if(q[i].stu!=NULL)
        {
            free(q[i].stu);
            q[i].stu=NULL;
        }
    }
    if(q!=NULL)
    {
        free(q);
        q=NULL;
    }
    
    
    return 0;
}
  • 函数封装
typedef struct Teacher
{
    char **stu;		//一个老师有多个学生
}Teacher;

//n1 老师数量,n2 每个老师的学生数量
int creatTeacher(Teacher **tmp,int n1,int n2)
{
    int i=0;
    if(tmp==NULL)
        return -1;
    
    Teacher *q=(Teacher*)malloc(sizeof(Teacher)*n1);		//Teacher q[3]
    for(i=0;i<n1;i++)
    {
        (q+i)->stu=(char**)malloc(n2*sizeof(char*));  //char *stu[3];
        for(int j=0;j<n2;j++)
        {
            (q+i)->stu[j]=(char*)malloc(30);
            char buf[30];
            sprintf(buf,"name%d%d%d",i,i,i);
            strcpy((q+i)->stu[j],buf);
        }
    }
    //间接赋值
    *tmp=q;
    
    return 0;
}

void showTeacher(Teacher *q,int n1,int n2)
{
    if(q==NULL)
    {
        return;
    }
    for(int i=0;i<n1;i++)
    {
        for(int j=0;j<n2;j++)
        {
            printf("%s\n",(q+i)->stu[j]);
        }
    }
}

void freeTeacher(Teacher **tmp,int n1,int n2)
{
    if(tmp==NULL)
        return;
    Teacher *q=*tmp;
    //free
    for(int i=0;i<n1;i++)
    {
        for(int j=0;j<n2;j++)
        {
            if((q+i)->stu[j]!=NULL)
            {
                free((q+i)->stu[j]);
                (q+i)->stu[j]=NULL;
            }
        }
        if(q[i].stu!=NULL)
        {
            free(q[i].stu);
            q[i].stu=NULL;
        }
    }
    if(q!=NULL)
    {
        free(q);
        q=NULL;
        *tmp=NULL;
    }
}

int main()
{   
    //3.
    Teacher *q=NULL;
    int ret=0;
    ret=creatTeacher(&q,3,3);
    if(ret!=0)
        return -1;
    //Teacher q[3]
    //q[i].stu[3]
    showTeacher(q,3,3);
    
    freeTeacher(&q,3,3);
    
    
    return 0;
}

1.7、结构体数组排序

typedef struct Teacher
{
    int age;
    char **stu;		//一个老师有多个学生
}Teacher;

void sortTeacher(Teacher *q,int n)
{
    if(q==NULL)
        return -1;
    int i=0;
    int j=0;
    Teacher tmp;
    for(i=0;i<n-1;i++)
    {
        for(j=i+1;j<n;j++)
        {
            if(p[i].age<p[j].age)
            {
                tmp=p[i];
                p[i]=p[j];
                p[j]=tmp;
            }
        }
    }
}

1.8、结构体深拷贝和浅拷贝

1)浅拷贝

  • 结构体中嵌套指针,而且动态分配空间
  • 同类型结构体变量赋值
  • 不同的结构体成员指针指向同一块内存
typedef struct Teacher
{
    char *name;
    int age;
}Teacher;

int main()
{
    Teacher t1;
    t1.name=(char*)malloc(30);
    strcpy(t1.name,"lily");
    t1.age=22;
    
    Teacher t2;
    t2=t1;				//浅拷贝,t1.name 和 t2.name 都指向堆区的同一块内存
    printf("%s %d\n",t2.name,t2.age);		//lily 22
    
    //释放
    if(t1.name!=NULL)
    {
        free(t1.name);
        t1.name=NULL;
    }
    
    if(t2.name!=NULL)		//err,由于t1.name 和 t2.name 都指向堆区的同一块内存,前面已经是放过了,这里释放的是与前面同一块内存,同一块内存释放两次,就会报错
    {
        free(t2.name);
        t2.name=NULL;
    }
    
    return 0;
}

2)深拷贝

typedef struct Teacher
{
    char *name;
    int age;
}Teacher;

int main()
{
    Teacher t1;
    t1.name=(char*)malloc(30);
    strcpy(t1.name,"lily");
    t1.age=22;
    
    Teacher t2;    
    //深拷贝,人为地分配内存
    t2.name=(char*)malloc(30);
    t2=t1;				//深拷贝,此时t1.name 和 t2.name 指向堆区的不同内存
    
    printf("%s %d\n",t2.name,t2.age);		//lily 22
    
    //释放
    if(t1.name!=NULL)
    {
        free(t1.name);
        t1.name=NULL;
    }
    
    if(t2.name!=NULL)		//OK
    {
        free(t2.name);
        t2.name=NULL;
    }
    
    return 0;
}

1.9、结构体偏移量

  • 结构体定义下来,内部成员变量的内存布居已经确定
typedef struct Teacher
{
    char name[64];			//64
    int age;				//4
    int id;					//4
}Teacher;

int main()
{
    Teacher t1;
    Teacher *p=NULL;
    p=&t1;
    
    int n1=(int)(&p->age)-(int)p;		//64		相对于结构体首地址
    int n2=&((Teacher*)0)->age;			//决定 0 地址的偏移量
    printf("%d\n",n2);					//64
    
    
    return 0;
}

1.10、结构体内存对齐

1)对齐规则

  • 数据成员的对齐规则(以最大的类型的字节大小为单位):结构体的数据成员,第一个数据成员放在偏移量为 0 的位置,以后每个数据成员放在偏移量为该数据成员大小的整数倍的地方
  • 结构体作为成员的对其规则:如果一个结构体 B 里嵌套结构体 A,则结构体 A 应该从偏移量为 A 内部最大成员的整数倍的地方开始存储。例如,结构体 A 中有 int、char、double 等类型的成员,那么 A 应该从偏移量为 8 的整数倍的位置开始存储。结构体 A 站的内存为该结构体成员内部最大元素的整数值,不足补齐。
  • 收尾工作:结构体的总大小,即 sizeof 计算的大小,是其内部最大成员的整数倍,不足的部分要补齐。
  • char 占一个字节,char 型数组,不管定义的数组有多大,都是只有一个字节
struct stru1
{
    int a;
    short b;
    double c;
}A;			//对齐单位为 8 个字节,a 放在 0-7 字节内,b 放在 8-15 字节内,放不满的补齐,c 放在 16-23 字节
// A 的大小为 24 字节,sizeof(A)=24

struct stru2
{
    int a;						//0-7
    short b;					//8-15
    double c;					//16-23
    struct stru1 B;				//B 内部的每个成员也都按照以 8 字节进行对齐存储,B.a:24-31, B.b:32-39, B.c:40-48
}C;		//对齐单位为 8 个字节
//结构体嵌套:以最大的成员的内存大小为单位进行对齐,

2)指定对齐单位

  • 指定的对齐单位小于最大成员,则以指定的对齐单位进行对齐
  • 指定的对齐单位大于最大成员,则以最大成员的大小为单位进行对齐
#pragma pack(2)			//指定对其单位为 2 个字节
struct stru1
{
    int a;				//以两个单位来对齐
    short b;			//以一个单位来对齐
    double c;			//以四个单位来对齐
}A;	

3)不完整类型的字节对齐:位域

  • 一个位域不许存储在同一个字节中,不可以跨字节。例如,一个字节所剩空间不够存另一位域时,应从下一单元存放该位域
  • 如果相邻位域字段的类型相同,且其位宽之和小于类型的 sizeof 大小时,则后面的字段将紧邻前一个字段存储,直到不能容纳为止
  • 如果相邻位域字段的类型相同,但其位宽之和大于类型的 sizeof 大小时,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍
  • 如果相邻位域字段的类型不相同,则各编译器的具体实现不同,VC6 采取不压缩的方式,Dev-C++ 和 gcc 采取压缩的方式
  • 整个结构体的总大小为最宽基本类型成员大小的整数倍
struct A
{
   	int a1:5;		//a1 指定位域宽度为 5 位
   	int a2:			//a2 指定位域宽度为 9 位
    char c;
    int b:4;
    short s;
}B;		//sizeof(b)=16
//a1+a2=14 位,少于32 位,占据32位,即 4 字节
//c,也需要 4 字节
// b,占 4 位,少于32 位,占据32位,即 4 字节
// s,也需要 4 字节
//共 16 字节

四、文件api

1、文件基本操作

1.1、文件操作的步骤

  • 引入头文件(stdio.h)
  • 打开文件
  • 使用文件指针
  • 关闭文件

1.2、文件相关的概念

  • 按文件的逻辑结构:记录文件、流式文件
  • 按存储介质:普通文件、设备文件
  • 按数据的组织形式:文本文件、二进制文件

1.3、文件缓冲区

  • 在程序的数据区和磁盘之间进行数据交换的时候,要经过缓冲区的传递
  • 不同平台的缓冲区大小不同
  • 刷新缓冲区:fflush(fp)
  • 在文件指针关闭、程序结束、文件缓冲区满的三种情况下,缓冲区的内容都会写入文件,否则不会
  • 对于 ACSI C 标准采用文件缓冲区,系统调用不会使用文件缓冲区

1.4、输入输出流

  • 流表示了信息从源到目的端的流动
  • 输入操作时:数据从文件流向计算机内存(文件读取)
  • 输出操作时:数据从计算机六流向文件(文件的写入)
  • 文件句柄:实际是一个结构体,标志着文件的各种状态

1.5、文件操作 API

  • fgets、fputc:按照字符读写文件
  • fputs、fgets:按照行读写文件(读写配置文件)
  • fread、fwrite:按照块读写文件(大数据块迁移)
  • fprintf、fscanf:按照格式化进行读写文件

2、标准的文件读写

2.1、文件的顺序读写

typedef struct Stu
{
    char name{50};
    int id;
}Stu;

int main()
{
    fputc('c',stdio);		//字符输出到标准输出,此时没有缓冲区,缓冲区是针对普通文件的
    char ch=fgetc(stdin);	
    fputc(stderr,"%c",ch);	//标准错误输出,//stderr 通常也是指向屏幕的
    
    FILE *fp=NULL;
    //绝对路径、相对路径(./test.txt、../test.txt)
    //直接运行可执行程序,相对路径是相对于可执行程序的
    //字符串续行符:
    /*
    char *p="aacscscscsdscaxasca"\
    "cscnsjcnkcskc";
    */ //表示一个字符串
    
    // "w+",写读的方式打开。如果文件不存在,则创建文件;如果文件存在,则清空文件内容,再写
    fp=fopen("文件路径/test.txt","w+");
    if(fp==NULL)
    {
        perror("fopen");		//提示错误信息,字符串的内容会在错误信息前面输出,作为错误信息的标识
        return -1;
    }
    
    //按字符写文件
    char buf="cnsakcsccmdmaasx";
    int i=0;
    int n=strlen(buf);
    for(i=0;i<n;i++)
    {
        int res=fputc(buf[i],fp);			//返回值是成功写入文件的字符的 ASCII 码
        //写完之后,会自动添加添加文件结束符
    }
    //文件关闭
    if(fp!=NULL)
    {
        fclose(fp);
        fp=NULL;
    }
    
    
    
    //按字符读文件
    FILE *fp=NULL;
    //"r+",文件以读写方式打开,如果文件不存在,打开失败
    fp=fopen("./test.txt","r+");
    if(fp==NULL)
    {
        perror("fopen");		//提示错误信息,字符串的内容会在错误信息前面输出,作为错误信息的标识
        return -1;
    }
    char ch;
    while(ch=fgetc(fp)!=EOF)		//等价于:while(!feof(fp)){ch=fgetc(fp);printf("%c",ch)}
    {
        printf("%c",ch);
    }
    printf("\n");
    if(fp!=NULL)
    {
        fclose(fp);
        fp=NULL;
    }
    
    
    //按照行读写文件
    FILE *fp=NULL;
    fp=fopen("文件路径/test.txt","w+");
    if(fp==NULL)
    {
        perror("fopen");		//提示错误信息,字符串的内容会在错误信息前面输出,作为错误信息的标识
        return -1;
    }
    char *buf[]={"aaaaaaaaaa\n","ssssssssss\n","dddddddddd\n","ffffffffff\n"};
    for(int i=0;i<4;i++)
    {
        int res=fputs(buff[i],fp);		//返回值:0代表成功,非零代表失败
    }
    
    char rbuf[30]={0};
    while(!feof(fp))
    {
        //sizeof(buf),表示最大读取的字节数。如果文件当行字节数大于sizeof(buf),则只读取前sizeof(buf);若文件件当行字节数小于sizeof(buf),则按照当行实际长度全部读取
        //返回值是成功读取的文件内容
        //读取的过程中,以 "\n"作为一行结束的标识
        //fgets 读取完毕后,还自动在读取的字符串末尾添加字符串结束符
        char *p=fgets(rbuf,sizeof(buf),fp);		
        printf("%s\n",rbuf);
    }
    if(fp!=NULL)
    {
        fclose(fp);
        fp=NULL;
    }
    
    
    
    //按照块写文件
    FILE *fp=NULL;
    fp=fopen("文件路径/test.txt","w+");
    if(fp==NULL)
    {
        perror("fopen");		//提示错误信息,字符串的内容会在错误信息前面输出,作为错误信息的标识
        return -1;
    }
    Stu s[3];
    char buf[50];
    for(int i=0;i<3;i++)
    {
        sprintf(buf,"stu%d%d%d",i,i,i);
        strcpy(s[i].name,buf);
        s[i].id=i+1;
    }
    //按块写文件
    //s,写入文件的内容的首地址
    //sizeof(Stu),按块写文件的单块的大小
    //3,块数,写文件数据的大小:sizeof(Stu)*3
    //fp,文件指针
    //返回值:成功写入文件的块数
    //fwrite 写入默认是按照二进制进行
    int res=fwrite(s,sizeof(Stu),3,fp);		//如果三块都写入成功,res=3
    if(fp!=NULL)
    {
        fclose(fp);
        fp=NULL;
    }
    
    
    
    
    //按照块读文件
    FILE *fp=NULL;
    fp=fopen("文件路径/test.txt","r+");
    if(fp==NULL)
    {
        perror("fopen");		//提示错误信息,字符串的内容会在错误信息前面输出,作为错误信息的标识
        return -1;
    }
    Stu s[3];
    char buf[50];
    //按块读文件
    //s,放文件内容的首地址
    //sizeof(Stu),按块读文件的单块的大小
    //3,块数,读文件数据的大小:sizeof(Stu)*3
    //fp,文件指针
    //返回值:成功读取文件的块数
    //fread 读取默认是按照二进制进行
    int ret=fread(s,sizeof(Stu),3,fp);		//如果三块都读取成功,ret=3
    for(int i=0;i<3;i++)
    {
        printf("%s %d\n",s[i].name,s[i].age);
    }
    if(fp!=NULL)
    {
        fclose(fp);
        fp=NULL;
    }
    
    
    
    
    // 按照格式化写文件
    FILE *fp=NULL;
    fp=fopen("文件路径/test.txt","w+");
    if(fp==NULL)
    {
        perror("fopen");		//提示错误信息,字符串的内容会在错误信息前面输出,作为错误信息的标识
        return -1;
    }
    printf("hello\n");	//等价于:fprintf(stdout,"hello");
    fprintf(fp,"hello,	I am pig,mike=%d\n",250);	//文件中的内容:hello,	I am pig,mike=250
    if(fp!=NULL)
    {
        fclose(fp);
        fp=NULL;
    }
    
    
    
    //格式化读取文件
    FILE *fp=NULL;
    fp=fopen("文件路径/test.txt","w+");
    if(fp==NULL)
    {
        perror("fopen");		//提示错误信息,字符串的内容会在错误信息前面输出,作为错误信息的标识
        return -1;
    }
    //printf("hello\n");	//等价于:fprintf(stdout,"hello");
    //fprintf(fp,"hello,I am pig,mike=%d\n",250);	//文件中的内容:hello,	I am pig,mike=250
    int a=0;
    fscanf(fp,"hello,I am pig,mike=%d\n",&a);
    printf("%d\n",a);		//250
    if(fp!=NULL)
    {
        fclose(fp);
        fp=NULL;
    }
    
    
    
    return 0;
}

2.2、文件的随机读写

#include<stdio.h>

int fseek(FILE *stream,long offset,int whence);
//返回值:成功返回0;错返回-1,并设置errno
//offset,偏移量,可正可负可为零
//whence,光标开始移动的起点

long ftell(FILE *stream);
//返回值:成功返回当前读写位置;出错返回-1,并设置errno

void rewind(FILE *stream);
//该函数是把光标移动文件开头
int main()
{
    //随机位置读文件    
    FILE *fp=NULL;
    fp=fopen("文件路径/test.txt","r+");			//文件中存放了前面那个结构体数组的三个元素
    if(fp==NULL)
    {
        perror("fopen");		//提示错误信息,字符串的内容会在错误信息前面输出,作为错误信息的标识
        return -1;
    }
    Stu s[3];
    Stu tmp;
    //读第三个结构体
    fseek(fp,sizeof(Stu)*2,SEEK_SET);		//等价于:fseek(fp,sizeof(Stu)*(-1),SEEK_END);
    int ret=fread(&tmp,sizeof(Stu),1,fp);
    if(ret==1)
    {
        printf("%s %d\n",tmp.name,tmp.id);
    }
    //把光标移动到最开始的地方,读取前两个
    fseek(fp,0,SEEK_SET);			//等价于:rewind(fp);
    ret=fread(s,sizeof(Stu),2,fp);
    for(int i=0;i<2;i++)
    {
        printf("%s %d\n",s[i].name,s[i].id);
    }
    if(fp!=NULL)
    {
        fclose(fp);
        fp=NULL;
    }
    return 0;
}

五、链表和函数指针

1、链表相关概念

1.1、链表和数组的区别

  • 链表是一种常用的数据结构,通过指针将一系列数据结点,连接成一个数据链
  • 相对于数组,链表有更好的动态性(数组顺序存储,链表非顺序存储)
  • 数据域用于存储数据,指针域用来建立与下一个结点的联系
  • 数组一次性分配一块连续的存储区域
    • 优点:随机访问效率高
    • 缺点:
      • 如果需要分配的区域非常大,可能会分配失败
      • 删除和插入某个元素的效率低
  • 链表:
    • 优点:
      • 不需要一块连续的存储区域
      • 删除和插入某个元素效率高
    • 缺点:
      • 随机访问效率低

1.2、链表概念和分类

  • 数据域、指针域
  • 动态链表、静态链表
    • 动态链表:动态分配内存(常用)
  • 带头链表、不带头链表
    • 带头链表:头结点不存储数据,只是标志位,用来指向第一个数据结点。头结点固定,第二个是有效结点
    • 不带头链表:头结点不固定
  • 单向链表、双向链表、循环链表

1.3、结构体套结构体

  • 结构体可以嵌套另外一个结构体的任意类型变量
  • 结构体不可以嵌套本结构体的普通变量(因为此时本结构体还未定义完成,大小还不确定,而类型的本质是固定大小内存块别名)
  • 结构体可以嵌套本结构体的指针变量(因为指针变量的空间是确定的,此时结构体的大小是可以确定的)
typedef struct A
{
    int a;
    int b;
    char *p;
}A;
//结构体可以嵌套另外一个结构体的任意类型变量
//结构体不可以嵌套本结构体的普通变量(因为此时本结构体还未定义完成,大小还不确定,而类型的本质是固定大小内存块别名)
//结构体可以嵌套本结构体的指针变量(因为指针变量的空间是确定的,此时结构体的大小是可以确定的)
typedef struct B
{
    int a;
    A tmp1;		//OK
    A *p1;		//OK
    B b;		//err
}B;

2、静态链表的使用

typedef struct Stu
{
    int id;		//数据域
    char name[100];
    struct Stu *next;	//指针域
}Stu;

int main()
{
    //初始化三个结构体变量
    Stu s1={1,"mkle",NULL};
    Stu s2={2,"lily",NULL};
    Stu s2={3,"lilei",NULL};
    
    s1.next=&s2;
    s2.next=&s3;
    s3.next=NULL;		//尾结点
    
    Stu *p=&s1;
    while(p!=NULL)
    {
        printf("%d %s\n",p->id,p->name);
        
        // p 移动到下一个结点
        p=p->next;
    }
    
    return 0;
}

3、动态链表

3.1、单向链表的基本操作

typedef struct Node
{
    int id;
    struct Node *next;
}Node;

//创建头结点,链表创建
//链表的头结点地址由函数返回
Node *SListCreat()
{
    Node *head=NULL;
    //头结点作为标志,不存储数据
    head=(Node*)malloc(sizeof(Node));
    if(head==NULL)
        return NULL;
    //给head的成员变量赋值
    head->id=-1;
    head->next=NULL;
    
    Node *pCur=head;
    Node *pNew=NULL;
    
    int data;
    while(1)
    {
        printf("请输入数据:");
        scanf("%d",&data);
        if(data==-1)		//输入-1,退出输入
            break;
        //新节点动态分配空间
        pNew=(Node*)malloc(sizeof(Node));
        if(pNew==NULL)
            continue;
        //给pNew成员变量赋值
        pNew->id=data;
        pNew->next=NULL;
        
        //链表建立关系
        //当前节点的next指向pNew
        pCur->next=pNew;
        //pNew下一个结点为空
        pNew->next=NULL;
        
        //把pCur移动到pNew
        pCur=pNew;
        
    }
    return head;
}


//链表的遍历
int SListPrint(Node* head)
{
    if(head==NULL)
        return -1;
    //取出第一个有效结点,即头结点的next
    Node *pCur=head->next;
    
    while(pCur!=NULL)
    {
        printf("%d->",pCur->id);
        
        //当前节点移动到下一个结点
        pCur=pCur->next;
    }
    printf("NULL\n");
    return 0;
}


//链表插入结点
//在值为 x 的结点前,插入值为 y 的结点,若值为 x 的结点不存在,则插在链表尾部
int SListNodeInsert(Node* head,int x,int y)
{
    if(head==NULL)
        return -1;
    Node *pPre=head;
    NOde *pCur=head->next;
    
    while(pCur!=NULL)
    {
        if(pCur->id==x)
            break;
        
        //不相等,两个指针同时后移一个结点
        pPre=pCur;
        pCur=pCur->next;
    }
    //给新结点动态分配空间
    Node *pNew=(Node*)malloc(sizeof(Node));
    if(pNew==NULL)
        return -2;
    
    //给pNew的成员变量赋值
    pNew->id=y;
    pNew->next=NULL;
    
    //插入到指定位置
    pPre->next=pNew;
    pNew->next=pCur;
    
    return 0;
}



//删除指定的结点
//删除第一个值为x的结点
int SListNodeDelete(Node *head,int x)
{
    if(head==NULL)
        return -1;
    Node *pPre=head;
    NOde *pCur=head->next;
    int flag=0;		//0表示没有找到,1 表示找到了
    
    while(pCur!=NULL)
    {
        if(pCur->id==x)
        {
            //删除结点操作
            pPre->next=pCur->next;		
            free(pCur);
            pCur=NULL;
            flag=1;
            break;
        }

        //不相等,两个指针同时后移一个结点
        pPre=pCur;
        pCur=pCur->next;
    }
    if(flag==0)
    {
        printf("没有 id 为 %d 的结点\n",x);
        return -2;
    }
    return 0;    
}



//清空链表,释放所有结点
int SlistNodeDestroy(Node *head)
{
    if(head==NULL)
        return -1;
    Node *tmp=NULL;
    while(head!=NULL)
    {
        tmp=head->next;
        free(head);
        head=NULL;
        
        head=tmp;
    }
    return 0;
}

int main()
{
	Node *head=NULL;
    head=SListCreat();		//创建头结点
    int res=SListPrint(head);
    
    //在5的前面插入4
    SListNodeInsert(head,5,4);
    SListPrint(head);
    
    SListNodeDelete(head,5);		//删除第一个值为x的结点
    SListPrint(head);
    
    //链表清空
    SlistNodeDestroy(head);
    head=NULL;
    
    return  0;
}

4、函数指针

4.1、指针函数:返回指针类型的函数

//指针函数
//() 的优先级比 * 高,因此 int* func() 是一个函数,返回值是 int 型的指针
int* func()
{
    int *p=(int*)malloc(sizeof(int));
    return p;
}

int main()
{
    int *p=func();
    return 0;
}

4.2、函数指针:是指针,指向函数的指针

1)函数指针的定义方式

  • 定义函数类型,根据类型定义指针变量(有 typedef 是类型,没有是变量)
  • 先定义函数指针类型,根据类型定义指针变量(常用)
  • 直接定义函数指针变量(常用)
int func(int a)
{
    printf("a=========%d\n",a);
    return 0;
}

int main()
{
    
    //1、定义函数类型,根据类型定义指针变量(有 typedef 是类型,没有是变量),这种方式不常用
    typedef int FUNC(int a);		//FUNC 就是函数类型
    FUNC *p1=NULL;					//函数指针变量,且有要求,要求 p1 指向的函数返回值是 int 类型,且只有一个参数,参数类型是 int
    p1=func;		//等价于:p1=&func;		p1 指向 func 函数
    func(5);		//传统的函数调用
    p1(6);			//函数指针变量调用方式
    
    
    
    //2、先定义函数指针类型,根据类型定义指针变量(常用)
    typedef int (*FUNC)(int a);			//FUNC,就是函数指针类型
    FUNC p2=func;		//p2 指向函数 func
    p2(7);
    
    
    
    //3、直接定义函数指针变量(常用)
    int (*p3)(int a)=func;		//p3 指向函数 func
    p3(8);
    
    int (*p4)(int a);
    p4=func;					//p4 指向函数 func
    p4(9);
    
    
    return 0;
}

2)函数指针的应用

int add(int x,int y)
{
    return x+y;
}

int sub(int x,int y)
{
    return x-y;
}

int multi(int x,int y)
{
    return x*y;
}

int divide(int x,int y)
{
    if(y==0)
    {
        printf("除数不可以是0\n");
        return 0;
    }
    return x/y;
}

void myExit()
{
    exit(0);
}

int main()
{
    char cmd[100];
    while(1)
    {
        printf("请输入指令:");
        scanf("%s",cmd);
        if(!strcmp(cmd,"add"))
            add(1,2);
        else if(!strcmp(cmd,"sub"))
            sub(1,2);
        else if(!strcmp(cmd,"multi"))
            multi(1,2);
        else if(!strcmp(cmd,"divide"))
            divide(1,2);
        else if(!strcmp(cmd,"myExit"))
            myExit();
        else 
            pritnf("Wrong input!\n")
    }
    
    
    //使用函数指针数组调用函数
    int (*func[4])(int a,int b)={add,sub,multi,divide};
    char *buf[]={"add","sub","multi","divide"};
    while(1)
    {
        printf("请输入指令:");
        scanf("%s",cmd);
        if(!strcmp(cmd,"myExit"))
            myExit();
        else 
            pritnf("Wrong input!\n")
        for(int i=0;i<4;i++)
        {
            if(strcmp(cmd,buf[i])==0)
            {
                func[i](1,2);
                break;				// 跳出for
            }
                
        }
    }
    
    return 0;
}

3)回调函数的使用

int add(int x,int y)
{
    return x+y;
}

//在17:11,添加减法功能,则可以直接使用之前的框架,调用减法功能
int sub(int x,int y)
{
    return x-y;
}


//函数的参数是变量,可以使函数指针变量吗?
//框架,固定不变,完成coding时间为 17:10
//C++ 的多态便是如此,调用相同的接口,执行不同的功能
void func(int x,int y,int(*p)(int a,int b))
{
    printf("func111111111\n");
    int res=p(x,y);		//回调函数
    printf("%d\n",a);
}


//上面的等价写法:
typedef int(*Q)(int a,int b);		//函数指针类型
void func2(int x,int y,Q p)
{
    printf("func111111111\n");
    int res=p(x,y);		//回调函数
    printf("%d\n",a);
}



int main()
{
    func(1,2,add);		//输出结果:func111111111\n 3		//1+2=3
    func(1,2,sub);		//输出结果:func111111111\n -1		//1-2=-1
    return 0;
}

4.3、链表的内存四区

  • 链表结点交换有两种方法:
    • 第一种方法:分别交换两个链表的数据域和指针域
    • 第二种方法:只交换两个链表的数据,将数据封装为结构体,这样能够将数据封装为一个,简化交换的过程

4.4、删除指定的所有结点

//删除值为 x 的所有结点
int SListNodeDeletePro(Node* head,int x)
{
    if(head==NULL)
        return -1;
    Node *pPre=head;
    NOde *pCur=head->next;
    int flag=0;		//0表示没有找到,1 表示找到了
    
    while(pCur!=NULL)
    {
        if(pCur->id==x)
        {
            //删除结点操作
            pPre->next=pCur->next;		
            free(pCur);
            pCur=NULL;
            flag=1;
            pCur=pPre->next;
            continue;		//如果相等,删除结点,并特跳出本次循环,避免后续再次两指针同时后移一个结点
        }

        //不相等,两个指针同时后移一个结点
        pPre=pCur;
        pCur=pCur->next;
    }
    if(flag==0)
    {
        printf("没有 id 为 %d 的结点\n",x);
        return -2;
    }
    return 0;   
}

4.5、链表排序

int SListNodeSort(Node* head)
{
    if(head==NULL||head->next==NULL)
    {
        return -1;
    }
    Node *pPre=NULL;
    Node *pCur=NULL;
    Node tmp;
    for(pPre=head->next;pPre->next!=NULL;pPer=pPre->nxt)
    {
        for(pCur=pPre->next;pCur!=NUll;pCur=pCur->next)
        {
            if(pPre->id>pCur->id)
            {
                /*
                //交换数据域
                tmp=*pPre;
                *pPre=*pCur;
                *pCur=tmp;
                
                //交换指针域
                tmp.next=pCur->next;
                pCur->next=pPre->next;
                pPre->next=tmp.next;
                */
                
                //节点只有一个数据,可以只交换数据域,比较容易实现
                tmp.id=pCur->id;
                pCur->id=pPre->id;
                pPre->id=tmp.id;
            }
        }
    }
    return 0;
}

4.6、升序插入链表结点

int SListNodeInsertPro(Node *head,int x)
{
    //保证插入前就是有序的
    itn retr=SListNodeSort(head);
    if(ret!=0)
        return ret;
    
    //插入结点
    Node *pPre=head;
    NOde *pCur=head->next;
    
    //1 2 3 5 6,插入4
    //pre:3,	cur:5
    while(pCur!=NULL)
    {
        if(pCur->id>x)		//找到了插入点
            break;
        
        //不相等,两个指针同时后移一个结点
        pPre=pCur;
        pCur=pCur->next;
    }
    //给新结点动态分配空间
    Node *pNew=(Node*)malloc(sizeof(Node));
    if(pNew==NULL)
        return -2;
    
    //给pNew的成员变量赋值
    pNew->id=y;
    pNew->next=NULL;
    
    //插入到指定位置
    pPre->next=pNew;
    pNew->next=pCur;
    
    return 0;
}

4.7、链表的翻转

int SListNodeReverse(Node* head)
{
    if(head==NULL||head->next==NULL||head->next->next==NULL)
        return -1;
    Node *pPer=head->next;
    Node *pCur=pPre->next;
    Node *tmp=NULL;
    
    while(pCur!=NULL)
    {
        tmp=pCur->next;
        pCur->next=pPre;
        
        pPre=pCur;
        pCur=tmp;        
    }
    
    //头结点和之前的第一个节点的指针域的处理
    head->next->next=NULL;
    head->next=pPre;
    
    return 0;
}

5、预处理

5.1、预处理

  • C 语言 对源程序处理的四个步骤:预处理、编译、汇编、链接
  • 预处理是在程序源代码被编译之前,由预处理器对程序源代码进行的处理。这个过程不对程序的源代码进行语法解析,但会把源代码分割或处理为特定的符号为下一步的编译做准备。

5.2、预编译命令

  • C 编译器提供的预处理功能主要有四种:
    • 文件包含:#include
    • 宏定义:#define
    • 条件编译:#if #endif
    • 一些特殊作用的预定义宏
  • #include<> 和 #include”” 的区别
    • <> 表示系统直接按照系统指定的目录检索
    • ”” 表示系统现在 file.c 所在的当前目录找 file.h,如果找不到,再按照系统指定的目录检索
    • 注意:
      • #include<> 常用于包含库函数的头文件
      • #include”” 常用于包含自定义的头文件
      • 理论上 #include 可以包含任何格式的文件(.c .h 等),但一般用于头文件的包含

5.3、宏定义

  • 在源程序中,允许一个标识符(宏名)来表示一个语言符号字符串,用指定的符号代替指定的信息

  • 在 C 语言中,宏分为无参数的宏和有参数的宏

  • 宏的作用域:宏可以写在程序的任何地方,但是都是类似于是全局的。只要定义了宏,宏定义后面的代码都可以使用

  • 取消宏定义:在定义过宏之后,可以取消宏定义,取消之后面的代码都不能使用这个宏了

      //宏定义
      #define PI 3.14
      int r=10;
      double area=PI*r*r;
        
      //取消宏定义
      #undef PI 
        
    

1)无参数的宏

#define 宏名 字符串
#include<stdio.h>

#define PI 3.14

int main()
{
    int r=10;
    double area=PI*r*r;
    
    return 0;
}

2)有参数的宏

#define TEST(a,b) a*b
int main()
{
    int a=TEST(1,2);		//相当于: a=1*2;
    //但是宏只进行简单的替换,因此,这个宏最好这么写:#define TEST(a,b) (a)*(b)
        
    return 0;
}

3)宏定义函数

//宏定义比较两个数的大小,返回较大的数
#define MAX2(a,b) (a)>(b)?(a):(b)

//宏定义比较三个数的大小,返回最大的数
#define MAX3(a,b,c) (a)>(MAX2(b,c))?(a):(MAX2(b,c))		//#define MAX3(a,b,c) (a)>MAX2(b,c)?(a):MAX2(b,c) 这样写是不对的,宏展开的时候直接替换,MAX2(b,c) 也是直接替换,结果不对

5.4、条件编译

1)条件编译

  • 一般情况下,源程序中所有的行都参加编译,但是有时候希望部分程序行只在满足一定条件时才编译,即对这部分源程序航指定编译条件:

    • 测试存在:一般用于调试,裁剪
      # ifdef 标识符
      	程序段1
      # else
          程序段2
      # endif    
    
      #define D1
        
      #ifdef D1
      	printf("D111111111111\n");
      #else
      	printf("others\n");
      #endif
    
    • 测试不存在:一般用于头文件,防止重复包含
      # ifndef 标识符
      	程序段1
      # else
          程序段2
      # endif
    
      //#pragma once				//比较新的编译器支持,老的编译器不支持
        
        
      //老的编译器支持的写法
      //__SOMEHEAD_H__ 是自定义宏,每个头文件的宏都不一样
      //一般都这么写:
      //test.h -> _TEST_H_
      //fun.h -> _FUN_H_
      #ifndef __SOMEHEAD_H__
      #define __SOMEHEAD_H__
        
      //函数声明
        
        
      #endif      		//!__SOMEHEAD_H__
    
    • 根据表达式定义
      # if 表达式
      	程序段1
      # else
      	程序段2
      # endif
    
      #define TEST 1
        
      #if TEST
      	printf("1111111111111\n");
      #else
      	pritnf("22222222222222\n");
      #endif
    

6、递归

  • 递归:函数可以调用函数本身(不要用 main 函数调用 main 函数,不是不可以,只是这样做往往得不到想要的结果)

  • 普通函数调用:栈结构,先进后出,先调用后结束

  • 函数递归调用:调用流程与普通函数的调用是一致的

  • 递归函数一定要注意递归结束条件的设置。

       int add(int n)
       {
           if(n==100)
               return n;
           return n+add(n+1);
       }
        
      int main()
      {
          int n=100;
          int sum=0;
          sum=add(1);
            
          return 0;
      }
    

1)递归实现字符串翻转

int reverseStr(char *str)		//递归的结果放在全局变量内
{
    if(str==NULL)
        return -1;
    
    if(*str=='\0')		//递归结束条件
        return 0;
    
    if(inverseStr(str+1)<0)
        return -1;
    strcat(g_buf,str,1);
    
    return 0;
}

int main()
{
	    
    return 0;
}

7、函数动态库封装

  • 封装的时候,不需要主函数,只需要实现的功能的 .c 文件和 .h 文件即可

  • 动态库不可以有中文路径

  • 在 Windows 下,VS 里面,新建项目(路径不能有中文):testdll(win32 控制台应用程序)(放在 文件夹 testdll 中) $\rightarrow$ 下一步 $\rightarrow$ DLL、空项目 $\rightarrow$ 完成

    • 要生成动态库的代码在 socketclient.c 和 socketclient.h 中,不需要主函数,将这两个文件放在前面新建的 testdll 所在的文件夹 testdll 中,在 testdll 文件夹中会自动生成一个 testdll 子文件夹,将 socketclient.c 和 socketclient.h 放在这个子文件夹中
    • 添加现有项,选中 socketclient.c 和 socketclient.h 并添加进来
    • 对于 socketclient.c 和 socketclient.h 中的每一个函数,需要在函数定义钱脉添加:__declspet(dllexport) 。如果某个函数前面没有放,生成的动态库里面就不包含该函数,调用该函数会失败
    • 编译,会提示没有主函数,可忽略该提示即可
    • 找到前面的 testdll 子文件夹,进入其中的 Debug 文件夹,里面有两个很重要的文件:testdll.dll 和 testdll.lib。前者是程序运行的时候使用,后者是编译程序的时候使用
    • 使用的时候,单独拷贝出 testdll.dll 和 testdll.lib,以及 socketclient.h, socketclient.h,的作用是为用户提供一个函数的声明说明
    • 在其他项目中,需要使用该动态库的时候,将 testdll.lib,以及 socketclient.h 拷贝到项目所在文件夹,并把头文件添加到 头文件 文件夹中 $\rightarrow$ 在 VS 中右键项目文件夹(此时无法编译通过) $\rightarrow$ 属性(或者:项目 $\rightarrow$ 属性)$\rightarrow$ 配置属性 $\rightarrow$ 链接器 $\rightarrow$ 输入 $\rightarrow$ 附加依赖项 $\rightarrow$ testdll.lib $\rightarrow$ 确定 $\rightarrow$ 应用(此时可以编译通过了,但是无法运行) $\rightarrow$ testdll.dll 拷贝到项目所在文件夹 $\rightarrow$ 此时就可以编译运行通过了
  • 对于安装的程序,在安装路径中,会有很多的 .dll 文件,也都是动态库

      // 加法
      __declspet(dllexport) 			//表示这个函数导出为动态库
      int addAB(int a,int b)
      {
          return a+b;
      }
    

8、内存泄漏检测

8.1、日志打印

  • C 语言中的一些常用的与日志打印相关的预定义的宏
#define _CRT_SECURE_NO_WARNINGS

// __FILE__:打印这个语句所在的文件的绝对路径
// __LINE__:打印这个语句所在的行号
printf("%s, %d\n",__FILE__,__LINE__);
  • 实际开发中,会有专门的日志打印相关的工具

8.2、内存泄漏检查

  • 实际开发中会有专门的内存泄漏检测相关的工具
  • 有很多好用的开源框架可以使用