1.基础知识
linux环境c语言基础
1.登录树莓派
本项目采用基于linux的微型电脑——树莓派环境,其中温度值的获取是从树莓派中获取的,不是在树莓派上运行的uu可以修改温度采样文件路径。具体进行代码编写的平台在软件准备Secure CRT中有介绍。
2.结构体
结构体在本项目应用很广,包括在进行socket通信、封装温度、时间以及序列号上均有重要作用。这里也有一个重要的在面试中经常会问到的知识点可以补充:
typedef与#define的区别
typedef关键字用于为现有的数据类型取别名。struct student stu1相当于stu stu1。
#define是简单的替换,如
typedef struct Student
{
int num;
char name[10];
char sex;
int age;
}Stu;
#define INT int*
INT i,j;
//相当于 int* i; int j;
而typedef 相当于int* i;int* j;
在socket通信中的使用
这里将socket通信中用到的主机地址、文件描述符以及端口号进行了封装,在与客户端进行通信时只需要创建对象,然后便可通过对象直接修改相应的端口号、文件描述符以及主机地址,简化代码封装过程中遇到的需要重复传入参数而造成的代码冗余问题。
typedef struct socket_ctx_s
{
char host[HOST_NAME_LEN];
int fd;
int port;
}socket_ctx_t;
3.指针
指针是c语言中必须掌握知识点之一,主要用途如下
内存管理:通过指针可以动态分配和释放内存空间,实现灵活的内存管理。
数组和字符串的操作:指针可以被用来遍历数组和字符串,进行元素的访问和修改。
结构体和联合的操作:指针可以用来操作结构体和联合类型的成员,实现对结构体和联合的访问和修改。
函数指针:指针可以被用来传递函数作为参数,实现回调函数和动态函数调用。
动态数据结构:指针可以用来实现动态数据结构,如链表、树等。
传递大型数据对象:通过传递指针,可以减少数据的拷贝,提高程序的效率。
提高程序的灵活性和效率:指针可以用来操作底层的硬件和内存,实现对系统资源的直接访问,提高程序的灵活性和效率。
在这里主要对指针对于字符、结构体以及函数指针进行详解。
字符
在获取温度值时,先将指针设置为空指针,然后使用strstr来查找文件中字符匹配为"t="(t=后跟随着的就是温度值),并将指针指向于此,此时指针指向"t"的前一个字符,将指针+2即可获取“=”后的温度。ps:ptr指针为char类型,而这里的字符也为char类型,故使ptr+2就相当于指针向后偏移2个单位就可找到温度值。
int get_temperature(float *temp)
{
int rv=0;
int fd=-1;
char file_name[64];
char buf[1024];
char *ptr=NULL;
char des[16]="28-";
char temp_path[64]="/sys/bus/w1/devices/";
rv=find_file(temp_path, des, file_name);
if (rv < 0)
{
log_error("Find file failure: %s\n", strerror(errno));
return -1;
}
strncat(temp_path, file_name, sizeof(temp_path)-strlen(temp_path));
strncat(temp_path, "/w1_slave", sizeof(temp_path)-strlen(temp_path));
if ((fd = open(temp_path,O_RDONLY)) < 0)
{
log_error("Open directory: %s failure: %s\n", temp_path,strerror(errno));
return -2;
}
memset(buf,0,sizeof(buf));
read(fd,buf,sizeof(buf));
ptr=strstr(buf,"t=");
if (!ptr)
{
log_error("Can not find the temperature\n");
return -3;
}
ptr += 2;
*temp = atof(ptr) / 1000;
close(fd);
return rv;
}
结构体
这里给socket_init函数传参时用到了,这里使用socket_ctx_t类型的指针接收传入参数,使用指针的好处有:这里是一种浅拷贝,其属性与拷贝源对象的属性共享相同引用(指向相同的底层值)的副本,可以避免数据的拷贝,提升程序效率。
补充一下,函数传参有三种方式:按值传递、引用传参以及指针。
按值传递
这种方式在传参的时候,在内存中会直接把实参的值复制一份再把副本传递给形参,对于形参的修改并不会影响到实参。也就是一种深拷贝。
引用传递
引用类型的地址存放在栈中,对应的值存放在堆中。当传参发生的时候,值类型会直接将栈中的值进行复制,形参和实参此时实际上是两个完全不相干的变量。对于引用类型,传参发生时,会将实参变量位于栈中的地址进行复制,此时栈中会有两个指向同一个堆地址的指针,因此此时直接修改形参是会印象实参的值的
指针
常见的包括一级指针和二级指针。我们需要知道形参一旦离开作用域就立即销毁,我们需要明确修改的是什么,我们借助指针进行传址操作只能改变当前指针指向的内容,并不能改变同级的实参 的值,要想通过函数改变主函数中一级指针的指向,就必须借助二级指针的帮助。这篇文章对于指针传参讲的非常清楚:【详解】指针与函数传参——多图、多例子(c语言)_指针传参-CSDN博客
int socket_init(socket_ctx_t *sock, char *servip, int port)
{
if(!sock || !port || !servip)
{
return -1;
}
memset(sock, 0, sizeof(sock));
sock->fd = -1;
sock->port = port;
if( servip)
{
strncpy(sock->host, servip, HOST_NAME_LEN);
}
log_debug("Socket init successfully\n");
return 0;
}
函数指针
在C语言中,函数被视为存储在内存中的一段可执行代码,每个函数都有一个唯一的地址。函数指针是一个指针变量,它存储了一个函数的地址。你可以将函数指针用来调用函数,就像你可以使用普通指针来访问变量一样。可以用在实现回调函数、动态函数调用、函数表等多种应用场景。本项目主要应用于回调函数。
应用方法如下
#include <stdio.h>
// 声明一个函数原型
int add(int a, int b);
int main() {
// 声明一个函数指针,指向具有相同参数和返回类型的函数
int (*ptr)(int, int);
// 初始化函数指针,将其指向add函数
ptr = add;
// 使用函数指针调用函数
int result = ptr(10, 20);
printf("Result: %d\n", result);
return 0;
}
// 定义一个函数
int add(int a, int b) {
return a + b;
}
回调函数
回调函数是一种将函数指针作为参数传递给其他函数的常见用途。它允许你将某种操作或行为插入到另一个函数的执行中,从而使代码更加灵活和可重用。
应用场景:它可以在异步操作完成后调用一个预定义的函数来处理结果。回调函数通常用于处理事件、执行异步操作或响应用户输入等场景。
事件处理:回调函数可以用于处理各种事件,例如鼠标点击、键盘输入、网络请求等。
异步操作:回调函数可以用于异步操作,例如读取文件、发送邮件、下载文件等。
数据处理:回调函数可以用于处理数据,例如对数组进行排序、过滤、映射等。
插件开发:回调函数可以用于开发插件,例如 WordPress 插件、jQuery 插件等。
作用:将代码逻辑分离出来,使得代码更加模块化和可维护。使用回调函数可以避免阻塞程序的运行,提高程序的性能和效率。另外,回调函数还可以实现代码的复用,因为它们可以被多个地方调用。
4.封装
完成一个项目必不可少的就是将数据和代码用结构体或者函数封装起来,提高代码的可维护性、可重用性和安全性。既可以用上文提到的结构体封装数据结构,也可以用函数封装代码。这里不多家赘述
5.文件地打开与关闭
系统调用open()与close()
#include <fcntl.h> // 包含 open 的定义
#include <unistd.h> // 包含 close 的定义
#include <stdio.h>
int main() {
int fd = open("example.txt", O_RDONLY); // 以只读模式打开文件
if (fd == -1) {
printf("文件打开失败\n");
return 1;
}
// 文件操作...
close(fd); // 关闭文件描述符
return 0;
}
打开文件 (open): 使用 fopen() 或 open() 来打开文件,文件可以以不同模式(读、写、追加)进行操作。返回一个文件描述符。.
关闭文件 (close): 使用 fclose() 或 close() 来关闭文件,释放占用的系统资源。关闭文件描述符,释放文件资源。
6.socket通信
Socket 是网络通信中的一个抽象概念,它用于在不同主机之间建立通信连接。Socket 是应用层与传输层之间的编程接口,通过它,应用程序可以发送和接收数据。Socket 通常用于基于 TCP 或 UDP 协议的网络通信。这里讲基于TCP的网络协议。
Socket 通信的基本流程:
- 创建 Socket: 调用系统函数创建一个 socket,指定通信协议(TCP/UDP)。
- 绑定地址: 服务器端需要绑定一个 IP 地址和端口号,监听来自客户端的连接。
- 监听(服务器端): 服务器端开始监听指定端口,等待客户端连接。
- 连接(客户端): 客户端通过 socket 请求与服务器建立连接。
- 数据传输: 双方建立连接后,开始通过 socket 发送和接收数据。
- 关闭连接: 通信完成后,双方关闭 socket 释放资源。
下图展示了客户端与服务端通信过程
客户端可以不用绑定地址,操作系统会自动分配地址,当然如果你想自己绑定固定的地址也是可以的。在这里可以注意到TCP的三次握手和四次挥手。
三次握手:
客户端:在调用 connect() 函数时,客户端发起三次握手。
服务器:在服务器端调用 accept() 之前,服务器会进入监听状态并等待客户端的连接请求。当 accept() 被调用时,服务器完成三次握手的处理。
四次挥手:
客户端:当客户端调用 close() 或 shutdown() 函数时,发起四次挥手以关闭连接。
服务器:在服务器端调用 close() 或 shutdown() 后,响应客户端的关闭请求并参与四次挥手的过程。
树莓派ds18b20基础
1.温度获取
基础的系统调用write、read、open和close等需要掌握,如果不知道相应的用法的话,直接用man 2 xx指令即可,如man 2 write。
存储温度文件在“/sys/bus/w1/devices/28-0317320a8aff/w1_slave文件中,温度在此位置。
获取温度只需要获取t=后的数,然后再除1000.0就可以得到树莓派的温度值了。注意:这里需要除的是浮点数1000.0,因为除整数得到的也是整数。
代码如下,我封装了两个函数,get_temperature函数以及find_file函数。find_file获取目标的文件路径。
这里有几个主要注意的要点:
1.使用文件描述符打开后一定要关闭,如:使用open打开文件,fd就是这个open操作返回的文件描述符,当退出程序时,这个文件描述符一定要关闭。
2.strncat和memset等函数的使用,strncat指的是字符串的拼接,第一个和第二个参数都是拼接的字符串,第三个参数是字符大小。memset这里是将buf数组都清0,为什么要清0,因为0的ascii码是0x00,也就是printf等函数读取字符结尾的码,这样可以保证,在将温度传入buf后,温度后字符码为0x00,读取行为截至,直接将字符输出到屏幕。
3.关于指针,指针需要掌握的内容比较复杂,
//获取温度
int get_temperature(ReportTemp *report_temp)
{
int rv=0;
int fd=-1;
char file_name[64];
char buf[1024];
char *ptr=NULL;
char des[16]="28-";
char temp_path[64]="/sys/bus/w1/devices/";
rv=find_file(temp_path,des,file_name);
if(rv<0)
{
printf("Find file failure: %s\n",strerror(errno));
return -1;
}
strncat(temp_path,file_name,sizeof(temp_path)-strlen(temp_path));
strncat(temp_path,"/w1_slave",sizeof(temp_path)-strlen(temp_path));
if((fd=open(temp_path,O_RDONLY))<0)
{
printf("Open directory: %s failure: %s\n",temp_path,strerror(errno));
return -2;
}
memset(buf,0,sizeof(buf));
read(fd,buf,sizeof(buf));
ptr=strstr(buf,"t=");
if(!ptr)
{
printf("Can not find the temperature\n");
return -3;
}
ptr+=2;
printf("ptr:%s\n",ptr);
sprintf(report_temp->temperature,"%08.4f",atof(ptr)/1000);
close(fd);
return rv;
}
int find_file( char *path,char *des,char *file_name)
{
DIR *dirp=NULL;
struct dirent *direntp=NULL;
int found=0;
int rv=0;
dirp=opendir(path);
if(!dirp)
{
printf("Open the %s directory failure: %s",path,strerror(errno));
rv= -1;
}
printf("Open the directory success\n");
while(NULL!=(direntp=readdir(dirp)))
{
if(strstr(direntp->d_name,des))
{
strncpy(file_name,direntp->d_name,64);
found=1;
}
}
if(!found)
{
printf("Find temperature failure: %s\n",strerror(errno));
rv= -2;
}
closedir(dirp);
printf("%s() return\n", __func__);
return rv;
}
数据库
入门基础教程,重点关注SQLite c/c++接口
1.增
database_name 数据库名称
table_name 表的名称
datatype有这些
常用的类型有TEXT 、INTERGER和REAL。字符串类型一般用TEXT。整型用INTERGER、浮点型用REAL。
本文用BLOB,原因在可以一次录入序列号、温度等信息,不需要分开创建属性,缺点也在此,代码复用性没那么高。
pack是你所打包的温度信息相关数据,size是一次传输的数据大小。
/* description: push a blob packet into database
* input args:
* $pack: blob packet data address
* $size: blob packet data bytes
* return value: <0: failure 0:ok
*/
int database_push_packet(void *pack, int size)
{
char sql[SQL_COMMAND_LEN]={0};
int rv = 0;
sqlite3_stmt *stat = NULL;
if( !pack || size<=0 )
{
log_error("%s() Invalid input arguments\n", __func__);
return -1;
}
if( ! s_clidb )
{
log_error("sqlite database not opened\n");
return -2;
}
snprintf(sql, sizeof(sql), "INSERT INTO %s(packet) VALUES(?)", TABLE_NAME);
rv = sqlite3_prepare_v2(s_clidb, sql, -1, &stat, NULL);
if(SQLITE_OK!=rv || !stat)
{
log_error("blob add sqlite3_prepare_v2 failure\n");
rv = -2;
goto OUT;
}
if( SQLITE_OK != sqlite3_bind_blob(stat, 1, pack, size, NULL) )
{
log_error("blob add sqlite3_bind_blob failure\n");
rv = -3;
goto OUT;
}
rv = sqlite3_step(stat);
if( SQLITE_DONE!=rv && SQLITE_ROW!=rv )
{
log_error("blob add sqlite3_step failure\n");
rv = -4;
goto OUT;
}
OUT:
sqlite3_finalize(stat);
if( rv < 0 )
log_error("add new blob packet into database failure, rv=%d\n", rv);
else
log_info("add new blob packet into database ok\n");
return rv;
}
goto
语句在 C 语言中用于无条件地跳转到程序中的指定标签,常用于错误处理和复杂状态转化,对于编程能力有限的小白一定要谨慎使用,因为它可以使代码变得难以理解和维护。
2.删
这里取出数据库中的第一条记录,不需要传入任何数据。
/* description: remove the first blob packet from database
* input args: none
* return value: <0: failure 0:ok
*/
int database_del_packet(void)
{
char sql[SQL_COMMAND_LEN]={0};
char *errmsg = NULL;
if( ! s_clidb )
{
log_error("sqlite database not opened\n");
return -2;
}
/* remove packet from db */
memset(sql, 0, sizeof(sql));
snprintf(sql, sizeof(sql), "DELETE FROM %s WHERE rowid = (SELECT rowid FROM %s LIMIT 1);", TABLE_NAME, TABLE_NAME);
if( SQLITE_OK != sqlite3_exec(s_clidb, sql, NULL, 0, &errmsg) )
{
log_error("delete first blob packet from database failure: %s\n", errmsg);
sqlite3_free(errmsg);
return -2;
}
log_warn("delete first blob packet from database ok\n");
/* Vacuum the database */
sqlite3_exec(s_clidb, "VACUUM;", NULL, 0, NULL);
return 0;
}
3.取
同理,这里也给出从数据库中删除记录的函数实现,这里删除的是数据库中的第一条记录。这里bytes是从数据中取出一条数据的长度,这里需要与size进行区分。这里的重新加入一个bytes参数的主要作用是,看是否完整取出了一条数据。
/* description: pop the first blob packet from database
* input args:
* $pack: blob packet output buffer address
* $size: blob packet output buffer size
* $byte: blob packet bytes
* return value: <0: failure 0:ok
*/
int database_pop_packet(void *pack, int size, int *bytes)
{
char sql[SQL_COMMAND_LEN]={0};
int rv = 0;
sqlite3_stmt *stat = NULL;
const void *blob_ptr;
if( !pack || size<=0 )
{
log_error("%s() Invalid input arguments\n", __func__);
return -1;
}
if( ! s_clidb )
{
log_error("sqlite database not opened\n");
return -2;
}
/* Only query the first packet record */
snprintf(sql, sizeof(sql), "SELECT packet FROM %s WHERE rowid = (SELECT rowid FROM %s LIMIT 1);", TABLE_NAME, TABLE_NAME);
rv = sqlite3_prepare_v2(s_clidb, sql, -1, &stat, NULL);
if(SQLITE_OK!=rv || !stat)
{
log_error("firehost sqlite3_prepare_v2 failure\n");
rv = -3;
goto out;
}
rv = sqlite3_step(stat);
if( SQLITE_DONE!=rv && SQLITE_ROW!=rv )
{
log_error("firehost sqlite3_step failure\n");
rv = -5;
goto out;
}
/* 1rd argument<0> means first segement is packet */
blob_ptr = sqlite3_column_blob(stat, 0);
if( !blob_ptr )
{
rv = -6;
goto out;
}
*bytes = sqlite3_column_bytes(stat, 0);
if( *bytes > size )
{
log_error("blob packet bytes[%d] larger than bufsize[%d]\n", *bytes, size);
*bytes = 0;
rv = -1;
}
memcpy(pack, blob_ptr, *bytes);
rv = 0;
out:
sqlite3_finalize(stat);
return rv;
}
makefile编写
Makefile
是一个自动化构建工具 make
的配置文件,通常用于管理和简化编译过程,尤其是在包含多个源文件或模块的大型项目中。它定义了如何编译和链接程序,以及文件之间的依赖关系。在写makefile之前先了解一下编译过程。
编译过程
1.预处理
处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。删除所有注释 “//”和”/* */”.添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。保留所有的#pragma编译器指令,因为编译器需要使用它们命令如:gcc -E hello.c -o hello.i
2.编译
gcc -S hello.i > hello.s
3.汇编
4.链接
动态链接: gcc hello.o -o hello 生成的文件后缀名为.lib静态链接: gcc hello.o -o hello -static 生成的文件后缀名为.a
src中.c程序的makefile的编写
先了解相对路径和绝对路径,比如说我的桌面。C:\Users\yr\Desktop 这就是他的绝对路径,包括了从根目录到目前所在文件夹的完整路径。相对路径在C:\Users\yr\Desktop 这个路径下输入cd ..那么将移动到当前文件夹的父路径,C:\Users\yr。
这里我们需要明确我们的项目一般来说有四个文件夹。include文件夹一般存放后缀名为.h的头文件,src文件夹一般存放后缀名为.c的源文件,lib文件夹一般存放后缀名为.lib和.a的链接文件,.bin文件夹一般存放主程序链接.lib或.a文件后的可执行文件。
开始将src文件夹里面的makefile文件的编写
最简单也最常用的makefile结构如下
# 以 '#' 开头的行表示注释# 定义变量 VAR ,强制赋值为 appVAR=test# 在 VAR 之前定义的值后面再追加 app 这个值,这时该变量值扩展为 testappVAR+=app# 如果之前 VAR 没有被定义,则定义并使用 testapp ;否则使用之前的值。VAR?=testapp# 第一条目标为总的目标 ,# 依赖可以是文件 ( 目录 ) 或为其他目标# 动作可以是 Linux 命令,动作的那一行必须以 TAB 键开头target: depend1 depend2 depend3 ...[TAB] action1[TAB ] action2target1:[TAB] action1[TAB] action2
#这个makefile文件包括预处理、编译、汇编和链接过程,生成.a和.lib文件,放入lib文件夹中
#定义一个变量LIBNAME,后面接.a和.lib的文件名
LIBNAME=temp
#定义一个变量指定文件和安装路径
INSTPATH=`pwd`/../include
LIBPATH=`pwd`/../lib
#对于变量的引用使用 ${}即可获得变量的值。这里-I为指定源文件对应的头文件所在的位置。`pwd`也就是文#件当前所在位置,这里CFLAGS可以理解为CFLAGS=-I `pwd`/../include -I `pwd`/../sqlite/include/
#建议使用相对路径,因为不同用户的项目所在路径都不同。
CFLAGS+=-I ${INSTPATH}
CFLAGS+=-I `pwd`/../sqlite/include/
#指定编译器,这里是gcc
CC=gcc
AR=ar
#all:这个target,当用户在本makefile命令台输入make将会按照顺序执行相应target,先执行#dynamic_lib,然后执行make clear make install make clean
all: dynamic_lib
make clear
make install
make clean
#动态链接 *.c代表的是所有.c文件,*通配符就是匹配所有的意思。这句的意思就是把所有.c文件链接成.lib
#文件。翻译就是gcc -shared -fPIC *.c -o libtemp.so -I `pwd`/../include -I `pwd`/../sqlite/#include/
dynamic_lib:
${CC} -shared -fPIC *.c -o lib${LIBNAME}.so ${CFLAGS}
#静态链接
static_lib:
${CC} -c *.c ${CFLAGS}
${AR} -rcs lib${LIBNAME}.a *.o ${LDFLAGS}
#install是一个单独的目标,它用来将编译生成的库文件拷贝到相应的安装路径下
install:
cp -rf lib${LIBNAME}.* ${LIBPATH}
#clear将编译生成的object临时文件删除
clear:
rm -f *.o
#clean先clear临时文件,之后再删除编译产生的库文件 默认不执行。依赖于clear,也就是先执行输入make #clean,会自动先执行make clear 再执行rm -f lib${LIBNAME}.*
clean:clear
rm -f lib${LIBNAME}.*
2.软件准备
1.Secure CRT
推荐按照这个教程进行安装
SecureCRT安装、汉化、上传、美化_securecrt汉化-CSDN博客
2.sqlite
从官网下载sqlte的安装包解压安装
建议先新建一个文件夹放sqlite3下载相关的东西
mkdir sqlite3
进入sqlite3文件夹中
cd sqlite3
用curl或wget从官网下载sqlte的安装包到当前路径下
curl https://www.sqlite.org/2022/sqlite-autoconf-3390400.tar.gz
或
wget https://www.sqlite.org/2022/sqlite-autoconf-3390400.tar.gz
解压到当前路径下
wget https://www.sqlite.org/2022/sqlite-autoconf-3390400.tar.gz
在sqlite3目录下新建一个文件夹备用
mkdir install
进入到刚解压的文件夹中
cd sqlite-autoconf-3390400
检查一下发现没有makefile,所以这里./configure自动生成makefile
这里可以用./configure --help查看帮助文档,按需设置自己的参数
这里的--prefix是指定安装的路径
./configure --prefix=`pwd`/../install
//或按需自己设置./configure --prefix=../install --disable-static
完成后用ls命令检查sqlite-autoconf-3390400文件夹中是否有名字为Makefile的文件,有的话继续下一步,执行make命令
make
最后执行make install安装sqlite
make install
输入下方内容,验证是否安装成功
sqlite3
代码获取可私信我