糕工的C语言开发日志 半月刊(日更版) 第一期2024/6下

一、程序存储与占用

1.编译后内存分配

编译后的Code(代码),RO-data(只读,譬如const),RW-data(读写,初始化非0的全局变量), 存储在ROM(flash)中,

ZI-data(初始化为0或者未初始化的变量),

运行时ROM占用是Code+RO-data+RW-data

运行时RAM占用是RW-data+ZI-data;RW和ZI会被搬运到RAM使用。

二、动态内存erro常见事故

1.free非动态内存1

        错误的free一个并非malloc/calloc来的内存会导致错误行为,譬如对一个字符串进行了free,下次malloc可能会错误的申请到该内存,而后由于字符串的非可变性导致该区域失效。

2.free非动态内存2

        同样的错误free一个非动态内存也会可能导致下次calloc卡死在calloc里。

3.calloc申请内存大小

        Calloc尽量申请len+1的内存,或者需要手动给末尾加\0,否则可能因为内存申请到有数据的位置而使得下次调用该内存长度变得更长,被错误的添加了无关的数据。

4.strlen与sizeof的区别

        strlen的搜索是以‘\0’作为结束条件的,函数的形参是以数值传递的,当一个数组作为形参传入,在函数内部使用sizeof,会错误的得到4byte,因为此时数组名会退化为指针。

三、AT指令交互与数据处理

1.交互过快

        在使用合宙/移远的4G模块时发现,当MCU与模块交互过快可能导致连包,无法接受到正确的数据,本次开发采用阻塞式,systick(1ms)作为时基,while等待。

        经过测试1500-2000ms可以保证数据有效性,无连包.

2.重复连接

        在AT交互初始化Socket,Https,MQTT的过程中发现,假如状态机switch重复执行了某个申请连接节点,会导致URC回传ERRO 或者重复连接,这时应当先查询连接状态,或者在申请连接之前先查询再处理。

        处理方法有两种:1.判断已连接,则直接进入下一个状态节点   2.判断已连接,先断开,再重新连接。

        处理方法取决于你的状态机switch查询流程设计。

四、非阻塞模拟异步操作

1.计时器if

        使用计时器,在第一次进入时获取一个当前时间(一般配合状态机使用,在结构体中进行时间保存),而后进行if判断下次进入时间是否超过设定时间间隔,超过则更新时间记录,并切换至下一状态,否则直接break,等待下次进入判断。

2.switch case异步 多状态

/*这是一个case分支多状态处理的案例,通过一个计数变量和一个发送标志变量,对case分支的不同流程作出了控制 , 以下是部分case代码*/
bool First_Used_Init(void)
{
	if(socketflag == true)
	{

		switch(indx1.NextCOM_ID)
		{

            			case AT_SAPBR_:
				if(cansendflag2 == true)
				{
					SEND_COMMAND(indx2.COMMAND);
					indx2.TIMECNT = GetSysRunTime();
					cansendflag2 = false;
				}
				
				if(GetSysRunTime()-indx2.TIMECNT > DelayX)
				{
					indx2.TIMECNT = GetSysRunTime();
					indx2.ACK = Get_COM_AT_SAPBR_ACK();

					static size_t sapbrFlag=0;
					if(sapbrFlag == 0)
					{
						if(AnalyRec(indx2.ACK) == true)
						{
							sapbrFlag=1;
							indx2.COMMAND = Get_COM_AT_SAPBR_(1);
						}
						cansendflag2 = true;
						break;
					}
					
					 if(sapbrFlag == 1)
					{	

						if(AnalyRec(indx2.ACK) == true)
						{
							sapbrFlag=2;
							indx2.COMMAND = Get_COM_AT_SAPBR_(2);
						}
						else
						{
							sapbrFlag=4;
							#ifdef GPRS_HZ
							indx2.COMMAND = "AT+SAPBR=0,1\r\n";
							#else
							indx2.COMMAND = "AT\r\n";
							#endif	
						}
						cansendflag2 = true;
						break;
					}
				
					if(sapbrFlag == 2)
					{	

						if(AnalyRec(indx2.ACK) == true)
						{
							sapbrFlag=0;
							indx2.NextCOM_ID = AT_HTTPINIT_;
							indx2.COMMAND = Get_COM_AT_HTTPINIT_();
						}
						else
						{
							sapbrFlag=3;
							#ifdef GPRS_HZ
							indx2.COMMAND = "AT+SAPBR=2,1\r\n";
							#else
							indx2.COMMAND = "AT\r\n";
							#endif
						}
						cansendflag2 = true;
						break;
					}
				
					if(sapbrFlag == 3)
					{

						#ifdef GPRS_HZ
						indx2.ACK = "+SAPBR: 1,1,";
						#else
						indx2.ACK = "OK";
						#endif

						if(AnalyRec(indx2.ACK) == true)
						{
							sapbrFlag=0;
							
							indx2.NextCOM_ID = AT_HTTPINIT_;
							indx2.COMMAND = Get_COM_AT_HTTPINIT_();
							
						}
						else	
						{
							sapbrFlag=4;
							#ifdef GPRS_HZ
							indx2.COMMAND = "AT+SAPBR=0,1\r\n";
							#else
							indx2.COMMAND = "AT\r\n";
							#endif
						}
						cansendflag2 = true;
						break;
					}
				
					if(sapbrFlag==4)
					{

						if(AnalyRec(indx2.ACK) == true)
						{
							sapbrFlag=0;
							indx2.COMMAND = Get_COM_AT_SAPBR_(0);
						}
						cansendflag2 = true;
						break;
					}
				
				break;
}

在case分支的多状态处理时,要注意给每个if都break;避免将流程计数器置位后,未正确进入下一次状态处理。

五、union,struct,类

1.union联合体

        union的使用需要编译器打开GNU标准,在联合体中数据是顺序排列的,在定义时可以显式的定义类型,也可以只定义联合体变量名用以访问。

        以下是一个实例:

#pragma pack(1)//数据以1byte对齐
typedef struct
{
    uint8_t head;
    uint8_t opcode;
    uint8_t length;
    union 
    {
       uint8_t payload0[2];
       struct 
       {
          uint8_t bit0:1;
          uint8_t bit1:1;
          uint8_t blecanbefindstate:1;
          uint8_t bit3:1;
          uint8_t bit4:1;
          uint8_t bit5:1;
          uint8_t bit6:1;
          uint8_t bit7:1;
		  uint8_t bit8_15:8;
       }bit;
		uint16_t notify_handle;	   
    };
	uint8_t payload1[];	
} BLE_CMD_PACKET;
#pragma pack()//结束强制对齐

             这是一个蓝牙模块的CMD命令数据帧格式定义,为了应对不同的数据内容,通过联合体,位域结构体和动态数组来实现兼容。同时利用#gragma pack()对结构体的数据对齐格式进行设置,保证了数据帧内部的连续性,若不进行强制对齐,则由于union的大小为2byte,会导致数据帧在转换为uint8_t* 指针的时候产生空白间隔。

        动态数组作为结构体成员是允许的,但是在实例化结构体之前需要先对结构体的大小初始化

        

static BLE_CMD_PACKET* cmd_create_union_hex(uint32_t datalenth)
{

	uint32_t len = datalenth+4;
	BLE_CMD_PACKET* cmd=calloc(len,sizeof(uint8_t));
	
	cmd->head = CMD_HEAD;
	cmd->length = datalenth;
	
	return cmd;
}

        数据总能正确的复制到正确的位置,通过以下方法实现

bool cmd_send_ble_data(uint8_t* data, uint32_t lenth)
{
	BLE_CMD_PACKET* cmd=cmd_create_union_hex(lenth);
	if(cmd == NULL)
	{
		return false;
	}
    cmd->opcode = CMD_SEND_BLE_DATA;
	cmd->notify_handle = NOTIFY_HANDLE;//假如是不带handle的数据帧则不需要赋值
	memcpy(cmd->payload1,data,cmd->length);//同时将这里的payload1改为payload0
    //这里用到了一个小知识,数组名代表首地址指针,同时由于内容并非字符串所以也不采用strcpy    

	cmd->length = cmd->length + 2; 
    BLE_UART_Send(0,(uint8_t*)cmd);
	free(cmd);
    return true;

}

六、指针与NULL

1.string.h系列

        string系列的函数是不允许传入NULL的,当传入一个NULL,可能会导致程序卡死在此处。

所以对数据合法性的判断至关重要。

/*
strtol/strtoul
这两个函数可以传入一个ascll的字符串数字譬如“123456”
那么他们会输出一个数值型的 123456
还可以在第三个形参传入一个数字,代表进制,譬如10就是10进制
*/
#include <stdio.h>
#include <stdlib.h> // 包含 strtol 的声明

int main() {
    const char *strNumber = "1234567890";
    char *endptr; // 用于存储结束扫描的位置
    long num;

    // 尝试将字符串转换为长整型
    num = strtol(strNumber, &endptr, 10); // 10 表示基数为10(十进制)

    if (endptr == strNumber) {
        printf("No digits were found!\n");
    } else {
        printf("The number is: %ld\n", num);
        // 检查是否有非数字字符
        if (*endptr != '\0') {
            printf("There were characters after the number: %s\n", endptr);
        }
    }

    return 0;
}

2.指针运算

        假设有两个指针,需要求取他们的偏移量时,做差的结果是整形类型变量。假设是同类型的指针则差值表示N个步长,步长由指针类型决定,char*就是1步长1字节,int*就是1步长4字节;

        非同类型指针做差无意义,需要先转化为char*或者void*再求取,可以得到相差的字节数。

同类型指针非char*类型也可以用这种方式来正确获取偏移字节数。

3.stdlib.h

        Keil的Mirco lib 和标准的stdlib不是同一个库,譬如exit(1)退出函数,就是标准库的,而微库没有的,不过编译后微库比标准库少大概1kb的code占用。

4.二维数组与动态分配

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

int main() 
{
    int rows = 3, cols = 4; // 假设是3行4列的二维数组

    // 首先,分配行指针数组
    int **matrix = (int **)malloc(rows * sizeof(int *));

    if (matrix == NULL) 
    {
        perror("Memory allocation failed for row pointers");
        exit(EXIT_FAILURE);
    }

    // 分配每一行的内存
    for (int i = 0; i < rows; ++i)
     {
        matrix[i] = (int *)malloc(cols * sizeof(int));
        if (matrix[i] == NULL) 
        {
            perror("Memory allocation failed for a row");
            exit(EXIT_FAILURE); // 如果任何一行分配失败,都需要释放已分配的内存并退出
        }
    }



    // 释放内存
    for (int i = 0; i < rows; ++i) 
    {
        free(matrix[i]); // 释放每一行的内存
    }
    free(matrix); // 最后释放行指针数组

    return 0;
}

七、数据结构之链表

1.通用双向链表与业务耦合

//定义节点
typedef struct Node
{
    struct Node* prev;
    struct Node* next;
}Node,*p_Node;

typedef struct Data
{
    int ID;
    int element1;
    int element2;
//.....很多业务参数
}Data,*p_Data;

typedef struct User
{
    Node node;
    Data data;
}User,*p_User;

static p_User UserHead;
/*
此时可以通过Node的指针遍历User,并在需要的时候
直接将Node*转化为(User*) 便可以直接访问业务数据
*/
p_User UserInit(void)
{
    p_User head = (p_User)malloc(sizeof(User));
    
    if(head != NULL)
    {
        head->node.prev = head;
        head->node.next = head;
        head->ID = 0;
        UserHead = head;    
    }
    
    return head;
}


p_User UserCreate(void)
{
    static int IDcnt = 0;
    p_User newUser = (p_User)malloc(sizeof(User));

    if(newUser != NULL)
    {
        newUser ->node.prev = newUser ;
        newUser ->node.next = newUser ;
        newUser ->ID = ++IDcnt;
    }

    return newUser;
}

void UserDestroy(p_User p)
{
    if(p == NULL)
    {
        exit(1);//return; 也行
    }

    if(((p_Node)p)->next == p)
    {
        free(p);
        return;
    }

    p_User current = p;
    p_User next;
    
    do
    {
        next = ((p_Node)current)->next;
        free(current);
        current = next;        
    }while(current != p);
    return;

}

void UserAdd(p_Node currentNode,p_Node newNode)
{
    newNode->prev = currentNode;
    newNode->next = currentNode->next;

    currentNode->next->prev = newNode;
    currentNode->next = newNode;
    
    return;
}

void UserDelete(p_Node DelNode)
{
    DelNode->prev->next = DelNode->next;
    DelNode->next->prev = DelNode->prev;

    free(DelNode);
}

p_User UserFind(p_Node p,int ID)
{
    p_Node findstart;
    p_Node findend;
    
    if(NULL ==  p)
    {
        findStart = UserHead;
        findend   = UserHead;
    }
    else
    {
        findStart = p;
        findend   = p;
    }

    while(((p_User)findStart)->ID != ID)
    {
        findStart = findStart->next; 
        if(findStart == findend)
        {
            return NULL;
        }
    }
    
    return findStart;
}

2.队列与链表

typedef struct Node
{
    struct Node* prev;
    struct Node* next; 
}Node,*p_Node;



typedef struct Data
{
    int     ID;
    int     element;
//很多数据
}Data,*p_Data;

//不必关心User存的什么  malloc来的
typedef struct User
{
    Node    Index;
    Data    userdata;
//很多数据
}User,*p_User;

typedef struct Queue
{
    p_Node front;
    p_Node rear;
    size_t size;
}Queue,*p_Queue;


static p_Queue my_queue = NULL;

p_Queue QueueCreate(void)
{   
    my_queue = (p_Queue)malloc(sizeof(Queue ));
    my_queue->front = NULL;
    my_queue->rear  = NULL;
    my_queue->size  = 0;
    
    return my_queue;
}

void QueueDestroy(void)
{
    if((my_queue == NULL)||(my_queue->front == NULL))
    {    
        return;    
    }
    
    Node* current = my_queue->front;
    Node* to_free;
    
    do
    {
        to_free = current;
        if(current->next != my_queue->front)
        {
              current = current->next;
        }
        else
        {
            break;
        }

        free(to_free);
      
    }while(current != my_queue->front);
    
    free(to_free);

    my_queue->front = NULL;
    my_queue->rear  = NULL;
    my_queue->size = 0;
    
    return;
}


void QueuePush(void* dataPtr)
{
    Node* newNode = (Node*)malloc(sizeof(User));//因为是共用的结构体头指针 按照大的申请
    if((newNode != NULL)&&(dataPtr != NULL))
    {
        p_User newUser = (p_User)newNode;
    
        newUser->userdata = *((p_Data)dataPtr);
        newNode->prev = my_queue->rear;
        newNode->next = my_queue->front;
        
        my_queue->front->prev = newNode;
        my_queue->rear->next = newNode;
        my_queue->rear = newNode;

        my_queue->size++;
    }
    
    return;
}


void QueuePop(void* dataptr)
{
    if((dataptr == NULL)||(my_queue->front == NULL))
    {    
        return;
    }

    Node* current = my_queue->front;

    (*(p_Data)dataPtr)  = ((p_User)current)->userdata;
    
    my_queue->front = current->next;
    my_queue->rear->next = current->next;
    my_queue->size--;
    
    current->next->prev = current->prev;
    current->prev->next  = current->next;

    free(current);
    return;

}

3.C语言实现OOP(结构体模拟类)

//C没有类 但是结构体和类的性质是相似的

typedef void(*pfunc)(void*);

typedef struct PfuncVtable
{
    pfunc sleep;

}PfuncVtable,*p_PfuncVtable;

typedef struct Base
{
    p_PfuncVtable vtable;//虚函数映射表
    int data1;
    int data2;
//data1 2 代表有有其他类型的业务数据
}Base,*p_Base;

//子类1
typedef struct son1
{
    Base base;
    int  x;//特有属性
}son1,*p_son1;

//子类2
typedef struct son2
{
    Base base;
    int  y;//特有属性
}son2,*p_son2;

//实现一个通用接口

void BaseSleep(void* p)
{
    p_Base ptr = (p_Base )p;
    ptr->vtable->sleep(ptr);//通过虚函数表映射来实现多态
    return;
}


void Son1_Sleep(void* p)
{
    p_son1 ptr = (p_son1) p; 
    p->x = 0;
    return;
}

void Son2_Sleep(void* p)
{
    p_son2 ptr = (p_son2) p; 
    p->y = 0;
    return;
}

p_son1 son1Create(void)
{
    p_son1 newSon1 = (p_son1)malloc(sizeof(son1));
    p_PfuncVtable newVtable = (p_PfuncVtable )malloc(sizeof(PfuncVtable));
    
    if((newSon1 != NULL)&&(newVtable != NULL))
    {
    
    newVtable->sleep = Son1_Sleep;
    newSon1->base.vtable = newVtable;
    newSon1->base.data1 = 0;
    newSon1->base.data2 = 0;
    newSon1->x = 0;
    }

    return newSon1; 
    
}

p_son2 son2Create(void)
{
    p_son2 newSon2 = (p_son2)malloc(sizeof(son2));
    p_PfuncVtable newVtable = (p_PfuncVtable )malloc(sizeof(PfuncVtable));

    if((newSon2 != NULL)&&(newVtable != NULL))
    {
    
        newVtable->sleep = Son2_Sleep;
        newSon2->base.vtable = newVtable;
        newSon2->base.data1 = 0;
        newSon2->base.data2 = 0;
        newSon2->y = 0;
    }

    return newSon2; 
    
}

4.栈与可变参数函数 

        栈是逆向生长的,先入栈的被放在栈底也就是高地址区,后入栈的放在低地址区,栈顶就是最低地址区,这使得访问取出的时候可以更方便,同时可变参数函数也与这有关。

        形参的压入实际上就是入栈的过程,ARM单片机是从后压栈的,也就是最右一个形参是最先入栈,最左个形参最后入栈在栈顶,所以stdarg.h里的可变参数函数的实现是依赖于形参压栈规则实现的,总能正确的找到栈顶,并通过偏移来找到后面的每个参数。

八、宏定义与预编译命令

1.一些常用预编译命令

        

//头文件包含 标准库用<> 自建用""
#include <stdio.h>
#include "sleep.h"

//宏定义
#define SLEEP     ( 5 )
#define RUNING    ( 4 )
#define DOING     ( 3 )

#undef DOING     //取消宏定义DOING,和#define一样 一次只能操作一个宏定义     

//预编译检查
#ifdef SLEEP  
#endif

//#ifdef 和 #if defined 等价
#if defined(SLEEP)||defined(RUNING)
#elif defined(DOING)
#endif

#ifndef SLEEP
#endif

#if !defined(SLEEP) 
#endif

//#pragma

#pragma pack(1)    //强制一字节对齐
//一堆结构体定义
#pragma pack()    //恢复默认对齐


#error //让编译器报错
#warning //让编译器警告


#pragma pack()的一些用法

#pragma pack(show) //显示当前内存对齐的字节数,编辑器默认8字节对齐

#pragma pack(n) //设置编辑器按照n个字节对齐,n可以取值1,2,4,8,16

#pragma pack(push) //将当前的对齐字节数压入栈顶,不改变对齐字节数

#pragma pack(push,n) //将当前的对齐字节数压入栈顶,并按照n字节对齐

#pragma pack(pop) //弹出栈顶对齐字节数,不改变对齐字节数

#pragma pack(pop,n) //弹出栈顶并直接丢弃,按照n字节对齐

2.宏定义中##的用法

//拼接字符串,必要时也可以用来实现类似的方式创建变量或者函数名
#define    CAST(A,B)    A##B

//假设参数也有宏定义需要额外定义一个宏定义才能正确展开
#define         X         i_
#define         Y         have
#define   _CAST(A,B)      A##B
#define    CAST(A,B)    _CAST(A,B)

//宏定义每次只能展开一级,所以有嵌套的情况下要定义一个中间宏

九、可变参数函数 

1.printf的奇怪知识

/*输出是AB 但是中间加了,就会错误,也就是说无论有多少个""组合 
没逗号之前都会输出*/
printf(“A”"B");

2.sprintf与sscanf

/*sprintf实际上只是比printf多了一个形参,用来设置接收数组
并且他实际是以字符串形式输出的,所以输入一个num为100,传出的是“100”*/
sprintf(rec,"%d",num);

#include <stdint.h>

int main(void)
{
    int number = 12345; // 需要转换的整数
    char str[10]; // 用于存放转换后的字符串,长度需足够大以容纳整数的最大位数加上'\0'

    // 使用sprintf将整数转换为字符串
    sprintf(str, "%d", number); //可以直接实现数字到字符串
    printf("%s",str);
    
 }

//输出结果是12345

//同理 sscanf比scanf多了一个字符来源数组,可以获取目标格式的数据
#include <stdio.h>

int main() {
    char logEntry[] = "2023-04-01 15:30:00 Application started";
    int hour;

    // 使用 sscanf 提取小时部分
    int result = sscanf(logEntry, "%*d-%*d-%*d %d:%*d:%*d", &hour);

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


    return 0;
}

//输出结果是15,前面的%*d是跳过这个格式的符号,相当于只定位到了hour这里

3.可变参数函数的实现 

//实现一个可变参数的函数需要 stdarg.h

#include <stdarg.h>

void sleep(int n,...)
{
        va_list arg; //定义指针
      
    //从n处后第一字节开始获取,这里的n不是指第一个固定参数,而是第一个可变参数之前的形参
        va_start(arg,n);

        //获取某个类型的变量,实际上用的是typeof,每次指针都会后移
        type1 receive1 = va_arg(arg,type1);
        type2 receive2 = va_arg(arg,type2);

        va_end(arg);//用完结束
        return;
}


typedef struct
{
    uint8_t a;
    uint16_t b;
    uint8_t c;
}Node;

//内存对其规则,每个变量都会从自身占用大小的整数倍开始,
//结构体本身占用是最大占用元素的整数倍
/*
默认对齐是sizeof(Node)占用6字节,分别是
1字节占用    1字节空缺    2字节占用    1字节占用    1字节空缺

强制#pragma pack(1)对齐是sizeof(Node)占用4字节,分别是
1字节占用    2字节占用    1字节占用
*/

/*但是这与可变参数函数无关,因为实际上内部是按照内存偏移来实现的,
函数实现不关注变量的类型;只关心内存偏移规则是几个字节*/

十、基本概念

1.const

const int *ptr,//const 修饰的是int 也就是指向内容不可变,
int* const ptr,//修饰的是ptr,指针不可变

const int n = 15;
int arr[n];
//这是不可以的,const修饰的变量是不可以作为数组的初始化元素个数的

/*同时很多函数的接收形参被设置为了const char*
这是因为const char*可以接收char* 也可以接收const char* ,但是反过来不行
const char*的形参在接收char*时 会被隐式的的转化为const char*
同时返回值定义为char*而非const char*则是因为 char*更灵活,当需要时候可以const
直接返回const char* 然后修改可能会导致未定义行为
*/

//其实返回值和形参的const 更多是出于安全考虑,尤其是传入字符串不可被改变的时候

2.++i与i++,*p++与*++p和++*p

i++和++i对于i来说是相同的行为
但是他们这个整体却是不一样的结果,i++结果为原值,++i结果为自增后的值
所以++i比i++具有更好的性能,当然在现代编译器的优化下其实是差不多的

*p++,*++p,++*p
*和++优先级是同一个等级的,同时遵循右优先性
所以如下

*p++ 先p++ 再*,结论是先自增指针,再访问原来的位置
*++p 先++p 再*,结论是先自增指针,再访问指针后移以后位置
++*p 先*p 再++,结论是先访问指向变量,再自增指向变量

3.exit 

        exit( )是标准stdlib.h的,mirco lib是没有的,同时exit( )是退出整个线程,所以在裸机或者RTOS上是不可以的,这种不带操作系统的只有单线程,exit()就会错误退出。

4.qsort排序函数

/*
ptr是存储类型数组的指针,cnt是待排序元素个数,
size是单个元素大小,pfunc是规则
*/

qsort(ptr,cnt,size,pfunc);

/*
pfunc是一个函数指针类型
自己实现一个pfunc,若返回1则交换,否则不交换
原型如下
*/

int pfunc(const void*,const void*);

//实际上qsort不止可以排序数组,也可以排序结构体,只要内部实现好就可以
/*
但是pfunc只实现比较,是不需要改变数据位置的,所以传入形参是带const的
*/
typedef struct
{
    int x;
    int y;
}data,*p_data;

int pfunc(const void* ptr1,const void* ptr2)
{
    p_data p1 = ptr1;
    p_data p2 = ptr2;
    
    if(p1->x > p2->x)
    {
        return 1;
    }
   
    else if(p1->x < p2->x)
    {
        return -1;
    }

     else if(p1->x == p2->x)
    {
        return 0;
    }
}

5.*与[ ] 

        

()和[]是同等优先级的,并且是左结合性,先执行左边
()和[]是比*的优先级高的

那么
int *arr[5]    代表一个元素为int*的arr数组,元素数量5
int (*arr)[5]  代表一个指针,其指向一个含有5个int的数组

int (*arr)[2][3] 代表一个指针arr指向了一个元素为int的二维数组

int (*arr[2])[3] 代表一个指针数组,里面的每个指针都指向了一个元素为int[3]的数组

十一、文件操作

1.打开与关闭

        

//FILE 是文件类型,fopen打开,fclsoe关闭
// r 写入 w读取 a追加模式 ,r+读写,w+读写 a+读写
// w系列是从从0开始,a系列是从尾开始,w和a都是没有就创建

//EOF -1
//打开一个文件读取并返回一个指针,失败返回NULL
FILE* fp = fopen("hello.txt","r");

//用完记得关闭,成功返回0,不成功返回 EOF -1
fclose(fp);


 2.输入输出

        

//fprintf和sprintf类似不过传入的是文件指针fp
//fscanf和sscanf类似

fprintf(fp,"%s",str);
fscanf(stdin,"%s",str);

//
fgets(buf,len,fp); //fgets最后会在末尾额外加一个'\0',正常返回NULL ,EOF返回EOF处位置
fputs(buf,fp);//把输出缓冲区的内容压到fp,不会额外添加'\0'

//
//从此处偏移num(可正可负)个字节读取,SEEK_SET 头 SEEK_CUR 当前 SEEK_END 尾
fseek(fp,num,SEEK_SET);//正常返回0 错误返回EOF -1
last = ftell(fp);//返回文件当前读写到头多少个字节数

十二、代码风格

1.unix风格

/*
Unix风格是以字母小写和下划线来命名函数和变量的,
以下是一组例子
*/

/*
不论是变量类型,还是函数名,又或者结构体变量
*/

// 结构体定义
typedef struct 
{
//具体实现
} student_record_t;

// 函数声明

void print_student_record(const student_record_t *record);
void free_student_record(student_record_t *record);

//但是宏定义和枚举依旧是大写和下划线

#define STUDENT_NUM   15

typedef enum
{
    STUDENT_TYPE_A = 1;
    STUDENT_TYPE_B = 2;
}e_student_type_t;


 2.命名技巧


函数命名常常以如下格式进行:类型头(譬如系列或者模块),动词,名词
void ble_connect_net()

变量类型命名则以:数据类型(p指针)/枚举类型(e)/func函数指针类型,名词,_t
p_student_t;
e_student_status_t;
void (*func_doing)(void);

变量本身命名以:静态s/全局g/计数器n,加上本身变量名
g_time_cnt;
s_time_loc;
n_student_total;

3.常用命名单词 

        

register 注册
callback 回调

interval 间隔
version  版本

config   配置
status   状态
device   装置
driver   驱动

bulid    构建
param    参数

event    事件
file     文件
info     信息
func     函数
bill     账单

N、附录,更新日志

2024/6/19

        开始了该记录,主要内容是在调试合宙和移远的4G模块时发现的。

        新增了1-1,        2-1~2-3,        3-1~3-2,        4-1等内容,针对AT交互和动态内存相关作出记录。

2024/6/20

        新增了2-4,        5-1,涉及到一些结构体和联合体以及位域的应用

2024/6/21

        新增了4-2,对switch case多状态异步的实现进行了更新

2024/6/25

        新增了6-1 6-2 7-1 7-2 主要实现了一个链表与业务耦合的结构体的一系列API,实现了一个双向循环链表队列

2024/6/26

        新增了7-3 通过C的结构体来模拟类实现OOP,实现了OOP封装,继承,多态的功能

        新增了8-1 8-2关于#预编译命令和##宏定义的用法

2024/6/27

        新增了6-3 6-4 7-4 补充了8-1和8-2 新开章节九、十,主要涉及了可变参数函数的实现,字符串和数字的转化,二维数组的动态分配,和一些基础概念的补充

2024/6/28

        更新了10-1  10-3  10-4 新开十一、 十二章节 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值