• [技术干货] 8月嵌入式项目开发专题总汇
    一、前言本文介绍基于嵌入式系统和C语言开发的系列项目。这些项目涵盖了多个领域,从自动化控制到游戏开发,从计算机网络到物联网应用。通过这些项目的开发过程,将深入探讨各种技术和解决方案,并分享相关经验和知识。在本文中,基于STM32设计的自动刹车灯和出租车计费系统。这两个项目将展示如何利用STM32单片机实现车辆安全和智能交通管理。详细解释硬件和软件的设计原理,并提供详细的代码示例和电路图。通过51单片机实现直流电机调速和基于STM32+华为云IOT设计的智能窗帘控制系统。这些项目深入了解嵌入式系统的电机控制和物联网应用。介绍调速原理、控制策略以及与外部云平台的通信方式。除了嵌入式系统开发,还涵盖了一些有趣的C语言实例,如贪吃蛇游戏、推箱子和校验算法。这些实例项目将展示C语言的应用,帮助提升编程技巧和思维能力。最后,介绍Linux下C语言调用libcurl库下载文件到本地和获取天气预报信息的方法。这些项目帮助了解在Linux环境下进行网络编程的基本原理,并提供实际可行的代码示例。通过这些项目的学习,可以获得丰富的项目开发经验和实用的技术知识。二、项目目录【1】基于STM32设计的自动刹车灯cid:link_2随着科技的发展,人们对低碳环保的认知和需求不断提高。骑自行车既能够低碳环保,又能够锻炼身体,成为了很多人出行的首选。然而,由于自行车本身没有带指示灯,比如刹车指示灯等,所以自行车的安全性并不是很好,如果人们在骑自行车时紧急刹车,后车无法及时判断前方自行车的行为,容易造成交通事故。因此,设计一款自动刹车灯系统具有十分重要的意义。本项目实现了通过安装ADXL345陀螺仪和四枚LED灯还有STM32F103C8T6主控芯片来实现自行车自动刹车灯的功能。当自行车上安装了该设备后,ADXL345通过IIC通信协议将X,Y,Z三轴的加速度实时值发送给SMT32F103C8T6主控芯片,并结合STM32高级定时器的PWM功能,输出不同占空比的脉冲,控制不同的LED灯输出多种亮度等级,从而控制不同的LED的开关以及明暗,并且通过不同亮度的红光和绿光混合,能够得到黄色的LED灯光。这样,在自行车急刹或者加速时,实时地控制LED灯的亮度和颜色,让后方车辆能够更清楚地了解前方自行车的行为,从而做出快速的反应,保障骑行者以及后车的安全。同时,该系统也能够提高自行车的可见性,并且对于追求低碳环保的人群来说,让自行车既能低碳环保,又能够锻炼身体。【2】通过51单片机实现直流电机调速cid:link_3随着各种工业生产设备和机械设备的广泛使用,直流电机调速技术的研究和应用越来越受到人们的重视,具有广泛的应用前景。本项目通过51单片机实现直流电机调速功能,为实际工程应用提供一个可靠和有效的调速方案。【3】通过C语言设计的贪吃蛇游戏(控制台终端)cid:link_4当前通过控制台终端实现一个贪吃蛇小游戏,实现游戏的绘制、更新、控制等功能。【4】通过C语言设计的推箱子(控制台终端)cid:link_5 推箱子游戏是一款经典的益智小游戏,玩家需要控制主角角色将几个木箱按照要求推到指定位置。在控制台终端中,可以使用字符来表示不同的游戏元素,例如 '#' 表示墙壁, ' ' 表示空地, '$' 表示木箱, '@' 表示主角角色, '+' 表示完成任务的目标位置。【5】基于STM32设计的出租车计费系统cid:link_6 在城市交通中,出租车是一种常见的交通工具。为了方便乘客和司机之间的交易,出租车计费系统被广泛应用于出租车行业。系统能够自动计算乘客的费用,提供准确、方便的计费服务,并且能够记录乘客的行驶数据,方便后续查询和管理。传统的出租车计费方式是基于人工计算,司机根据里程和时间进行估算并告知乘客费用。然而,这种计费方式容易存在误差和争议,并且对司机和乘客都不够方便和透明。因此,出租车行业迫切需要一种更加准确、高效和可靠的计费系统。基于此背景,本项目设计和开发一种基于STM32微控制器的出租车计费系统,以替代传统的人工计费方式。该系统将利用STM32微控制器的强大处理能力和丰富的外设接口,集成各种功能模块,实现自动计算乘客费用、显示计费信息等功能。通过该出租车计费系统,乘客只需在上车时按下对应按钮,系统将自动开始计费,并在显示屏上实时显示行驶时间、里程和费用等信息。乘客还可以通过按键输入特殊情况,如堵车或夜间行驶,以便系统进行相应的额外计费。当乘客下车时,系统将自动停止计费,并显示最终费用。同时,系统还将记录乘客的行驶数据以备查询和管理。【6】Linux下C语言调用libcurl库下载文件到本地cid:link_7 当前文章介绍如何使用C语言调用libcurl库在Linux(Ubuntu)操作系统下实现网络文件下载功能。libcurl是一个开源的跨平台网络传输库,用于在C和C++等编程语言中实现各种网络通信协议的客户端功能。它支持多种协议,包括HTTP、HTTPS、FTP、SMTP、POP3等,可以方便地进行数据的上传和下载操作。【7】Linux下C语言调用libcurl库获取天气预报信息cid:link_8 当前文章介绍如何在Linux(Ubuntu)下使用C语言调用libcurl库获取天气预报的方法。通过HTTP GET请求访问百度天气API,并解析返回的JSON数据,可以获取指定城市未来7天的天气预报信息。【8】C语言实例_CRC校验算法cid:link_9CRC(Cyclic Redundancy Check,循环冗余校验)是一种常用的错误检测技术,用于验证数据在传输或存储过程中是否发生了错误。它通过对数据进行一系列计算和比较,生成一个校验值,并将其附加到数据中。接收方可以使用相同的算法对接收到的数据进行校验,然后与接收到的校验值进行比较,从而确定数据是否存在错误。CRC校验通常用于以下方面:(1)数据传输的可靠性:在数据通过媒体或网络进行传输时,可能会发生噪声、干扰或其他传输错误。通过在数据中添加CRC校验值,接收方可以检测到传输过程中是否发生了错误,并采取相应措施,如请求重新发送数据。(2)存储介质的完整性检测:在存储介质上读取或写入数据时,可能会发生位翻转、介质故障等错误。通过在数据存储时使用CRC校验,可以在读取数据时检测到这些错误,并提供数据的完整性保证。(3)网络通信协议:许多网络通信协议(如Ethernet、WiFi、USB等)使用CRC校验作为数据帧的一部分,以确保传输的数据准确无误。接收方在接收到数据帧后,使用CRC校验来验证数据的完整性。在项目中,CRC校验广泛应用于各种通信系统、存储系统和数据传输系统中。通过使用CRC校验,可以提高数据的可靠性,并减少传输或存储过程中的错误。它可以检测到数据位级别的错误,并提供一定程度的数据完整性保证。CRC校验在保障数据可靠性和完整性方面具有重要作用,特别是在对数据完整性有较高要求的应用场景中。【9】C语言实例_调用SQLITE数据库完成数据增删改查cid:link_10SQLite是一种轻量级的关系型数据库管理系统(RDBMS),它是一个开源的、零配置的、服务器端的、自包含的、零管理的、事务性的SQL数据库引擎。它被广泛应用于嵌入式设备、移动设备和桌面应用程序等领域。SQLite的特点包括:(1)轻量级:SQLite的核心库只有几百KB,非常适合在嵌入式设备、移动设备等资源受限的环境中使用。(2)零配置:SQLite不需要任何配置,只需要将库文件嵌入到应用程序中即可。(3)服务器端:SQLite不需要运行在服务器上,所有的数据都存储在本地文件中。(4)自包含:SQLite的所有功能都包含在一个单独的库文件中,不需要依赖其他库文件。(5)零管理:SQLite不需要维护数据库的连接、事务等状态,所有的操作都是自动的。(6)事务性:SQLite支持ACID事务,保证数据的一致性和可靠性。SQLite支持标准的SQL语句,包括SELECT、INSERT、UPDATE、DELETE等操作,同时还支持多种数据类型,包括整数、浮点数、字符串、日期等。SQLite还支持多种编程语言,包括C、C++、Python、Java等,可以方便地集成到各种应用程序中。【10】基于STM32+华为云IOT设计的智能窗帘控制系统cid:link_11随着智能家居技术的不断发展,人们对于家居生活的需求也越来越高。智能窗帘作为智能家居领域的重要组成部分,为用户提供了更便捷、舒适的生活体验。本项目基于STM32主控芯片和华为云物联网平台,设计一款智能窗帘控制系统,以满足家庭和商业场所的需求。在本项目中,选择了STM32F103ZET6作为主控芯片具有强大的处理能力和丰富的外设接口,适合用于物联网设备的控制和通信。通过与ESP8266-WIFI模块的连接,可以实现智能窗帘与华为云物联网平台的互联互通,实现远程控制和监测。为了方便用户的操作和控制,使用Qt开发了Android手机APP和Windows上位机软件,用户可以通过这些应用程序进行窗帘的远程控制。同时,本地窗帘也支持手动控制,用户可以通过物理按钮或开关来操作窗帘的开关、升降等功能。在智能化方面,引入了语音识别技术(LD3320模块),用户可以通过语音指令来控制窗帘的运行。这为用户提供了更加便捷、智能的控制方式,使得窗帘的操作更加自然和智能化。除了远程控制和智能化功能,还引入了自动模式。在自动模式下,系统会根据环境条件进行智能判断和控制。例如,当检测到阳光强度超过设定阈值时,系统会自动关闭窗帘,以避免阳光直射室内;在晚上时,系统也会自动拉上窗帘,提供更好的隐私和安全性。本智能窗帘控制系统基于STM32主控芯片和华为云物联网平台,结合语音识别、智能家居控制等功能,为家庭和1商业场所提供便捷、舒适的智能化服务。通过远程控制、自动模式和智能化功能,用户可以实现对窗帘的灵活、智能的控制,提升生活质量和用户体验。【11】基于STM32设计的智能门锁2(采用华为云IOT平台)cid:link_0随着智能家居的快速发展,智能门锁作为家庭安全的重要组成部分,受到了越来越多用户的关注和需求。为了满足用户对安全和便捷的需求,决定设计一款基于STM32的智能门锁,并将其与华为云IOT平台相结合。传统的门锁存在一些弊端,比如使用钥匙容易丢失、开锁过程繁琐等。而智能门锁的出现,有效地解决了这些问题。我选择使用STM32作为智能门锁的核心控制器,因为STM32系列具有低功耗、高性能和丰富的外设接口等优点,非常适合嵌入式应用。华为云IOT平台作为一个强大的云服务平台,提供了丰富的物联网解决方案和强大的数据处理能力。将智能门锁与华为云IOT平台相结合,可以实现远程控制、数据监测和智能化的功能,为用户带来更加便捷和安全的居家体验。智能门锁设计具有以下主要特点和功能:安全可靠:采用先进的加密算法和身份验证机制,确保门锁的安全性。用户可以通过手机APP、指纹识别或密码等方式进行开锁,有效防止非法入侵。 远程控制:通过与华为云IOT平台的连接,用户可以通过手机APP在任何地方实现对门锁的远程控制。比如,可以远程开关门锁、查看开锁记录等。 多种开锁方式:除了传统的钥匙开锁方式外,我们的智能门锁还支持多种开锁方式,如指纹识别、密码输入、手机APP控制等。用户可以根据自己的需求选择最方便的开锁方式。 实时监测:智能门锁可以实时监测门锁状态、开锁记录等信息,并将这些数据上传到华为云IOT平台进行存储和分析。用户可以通过手机APP查看相关数据,了解家庭安全状况。 智能化功能:基于华为云IOT平台的数据处理能力,我们的智能门锁还可以实现一些智能化的功能。比如,可以设置自动开锁时间、远程授权开锁等。【12】C语言实例_和校验算法cid:link_12 和校验(Checksum)是一种简单的纠错算法,用于检测或验证数据传输或存储过程中的错误。它通过对数据进行计算并生成校验和,然后将校验和附加到数据中,在接收端再次计算校验和并进行比较,以确定数据是否完整和正确。和校验算法通常使用位运算来计算校验和。常见的和校验算法有如下几种:(1)简单累加校验和(Simple Sum Checksum):将数据中的所有字节相加,并将结果与一个预定义的校验和进行比较。如果两者相等,则数据没有发生错误。(2)CRC(Cyclic Redundancy Check):使用除法来计算校验和,具有更高的错误检测能力。CRC算法使用一个固定的生成多项式对数据进行除法运算,生成一个余数作为校验和。和校验算法可以用于各种不同的应用场景:(1)数据传输:在数据通过网络传输、串口通信或其他通信渠道传递时,和校验可以检测出传输过程中发生的位错误或传输错误,确保数据的完整性和准确性。(2)存储校验:在数据存储介质上写入数据或从存储介质中读取数据时,和校验可以帮助检测到媒体故障或数据损坏。(3)文件校验:在下载文件、备份文件或转移文件等场景中,和校验可用于验证文件完整性,确保文件没有被篡改或损坏。(4)数据库校验:在数据库系统中,和校验可用于检测数据完整性,防止数据在存储或传输过程中发生错误或损坏。和校验算法是一种简单但实用的纠错算法,用于检测数据传输或存储过程中的错误,并在很多应用中得到了广泛的应用,以确保数据的完整性和准确性。【13】C语言实例_奇偶校验算法cid:link_13 奇偶校验算法(Parity Check Algorithm)是一种简单的错误检测方法,用于验证数据传输中是否发生了位错误。通过在数据中添加一个附加的奇偶位(即校验位),来实现错误的检测和纠正。在奇偶校验算法中,假设每个字节由8个比特(位)组成。奇偶校验位的值取决于数据字节中的1的个数。如果数据字节中1的个数是偶数个,奇偶校验位被设置为0;如果1的个数是奇数个,奇偶校验位被设置为1。这样,在接收端,通过统计接收到的数据字节中1的个数,就可以检测出位错误。具体的奇偶校验算法包括以下几个步骤:(1)发送端:在发送数据字节之前,统计数据字节中1的个数,根据个数设置奇偶校验位的值,并将数据字节和奇偶校验位一起发送。(2)接收端:在接收数据字节后,再次统计接收到的数据字节中1的个数,与接收到的奇偶校验位进行比较。如果两者不一致,说明数据传输中发生了位错误。奇偶校验算法在以下场景中常被使用:(1)串行通信:在串行通信中,奇偶校验算法可以用于检测数据传输过程中发生的位错误。发送端计算奇偶校验位并附加到发送的数据字节上,接收端通过验证奇偶校验位来判断接收到的数据是否正确。(2)存储介质:在一些存储介质上,如磁盘驱动器或闪存存储器,奇偶校验算法可以用于检测数据读取或写入过程中发生的位错误。在存储数据时,计算奇偶校验位并与数据一起存储;在读取数据时,再次计算校验位并与存储的校验位进行比较,以确保数据的完整性和准确性。(3)错误检测:奇偶校验算法也可以用于其他需要简单错误检测的场景。例如,在计算机内存或寄存器中,奇偶校验位可以用于检测存储数据过程中的位错误,以避免数据的错误使用或传输。奇偶校验算法只能检测到位错误,而不能纠正错误。如果检测到错误,则需要采取其他纠错措施或请求重新传输数据。【14】C语言实例_获取文件MD5值cid:link_1 MD5(Message Digest Algorithm 5)是一种常用的哈希函数算法。将任意长度的数据作为输入,并生成一个唯一的、固定长度(通常是128位)的哈希值,称为MD5值。MD5算法以其高度可靠性和广泛应用而闻名。image.pngMD5算法主要具备以下特点:(1)不可逆性:给定MD5值无法通过逆运算得到原始数据。(2)唯一性:不同的输入数据会生成不同的MD5值。(3)高效性:对于给定的数据,计算其MD5值是非常快速的。MD5值的应用场景包括:(1)数据完整性验证:MD5值可以用于验证文件是否在传输过程中被篡改。发送方计算文件的MD5值并发送给接收方,接收方在接收到文件后重新计算MD5值,然后与发送方的MD5值进行比较,如果一致,则说明文件未被篡改。(2)密码存储:在许多系统中,用户密码通常不会以明文形式存储,而是将其转换为MD5值后存储。当用户登录时,系统会将用户输入的密码转换为MD5值,然后与存储的MD5值进行比较,以验证密码的正确性。(3)安全认证:MD5值也可用于数字证书等安全认证中,用于验证文件的完整性和认证信息的真实性。(4)数据指纹:MD5值可以作为数据的唯一标识符,用于快速比对和查找重复数据。
  • [技术干货] C语言实例_获取文件MD5值
    一、MD5介绍MD5(Message Digest Algorithm 5)是一种常用的哈希函数算法。将任意长度的数据作为输入,并生成一个唯一的、固定长度(通常是128位)的哈希值,称为MD5值。MD5算法以其高度可靠性和广泛应用而闻名。MD5算法主要具备以下特点:(1)不可逆性:给定MD5值无法通过逆运算得到原始数据。(2)唯一性:不同的输入数据会生成不同的MD5值。(3)高效性:对于给定的数据,计算其MD5值是非常快速的。MD5值的应用场景包括:(1)数据完整性验证:MD5值可以用于验证文件是否在传输过程中被篡改。发送方计算文件的MD5值并发送给接收方,接收方在接收到文件后重新计算MD5值,然后与发送方的MD5值进行比较,如果一致,则说明文件未被篡改。(2)密码存储:在许多系统中,用户密码通常不会以明文形式存储,而是将其转换为MD5值后存储。当用户登录时,系统会将用户输入的密码转换为MD5值,然后与存储的MD5值进行比较,以验证密码的正确性。(3)安全认证:MD5值也可用于数字证书等安全认证中,用于验证文件的完整性和认证信息的真实性。(4)数据指纹:MD5值可以作为数据的唯一标识符,用于快速比对和查找重复数据。二、示例代码2.1 获取数据MD5值(openssl库)在C语言中获取一段数据的MD5值,可以使用现有的第三方库实现。以下是一个使用 OpenSSL 库计算数据的MD5值的示例代码:(1)需要安装 OpenSSL 库(如果尚未安装)并包含相关头文件:#include <stdio.h>#include <stdlib.h>#include <openssl/md5.h>(2)创建一个子函数来计算数据的MD5值:void calculate_md5(const unsigned char* data, size_t length, unsigned char* md5_hash) { MD5_CTX ctx; MD5_Init(&ctx); MD5_Update(&ctx, data, length); MD5_Final(md5_hash, &ctx);}该函数接受三个参数:data 为待计算的数据指针,length 为数据长度,md5_hash 为存储MD5值的数组。下面是一个完整的程序,展示如何调用以上子函数并打印MD5值:#include <stdio.h>#include <stdlib.h>#include <openssl/md5.h>​void calculate_md5(const unsigned char* data, size_t length, unsigned char* md5_hash) { MD5_CTX ctx; MD5_Init(&ctx); MD5_Update(&ctx, data, length); MD5_Final(md5_hash, &ctx);}​void print_md5(const unsigned char* md5_hash) { for (int i = 0; i < MD5_DIGEST_LENGTH; i++) { printf("%02x", md5_hash[i]); } printf("\n");}​int main() { const unsigned char data[] = "Hello, World!"; size_t length = sizeof(data) - 1; // 减去字符串末尾的空字符 unsigned char md5_hash[MD5_DIGEST_LENGTH];​ calculate_md5(data, length, md5_hash); printf("MD5: "); print_md5(md5_hash);​ return 0;}这个示例程序将输出一段数据的MD5值。可以将待计算的数据存储在 data 数组中,并根据需要调整数据长度。这里使用的是 OpenSSL 提供的 MD5 函数。在编译时,需要链接 OpenSSL 库。在 Linux 系统上,可以使用 -lssl -lcrypto 参数进行链接。在 Windows 系统上,需要下载并安装 OpenSSL 库,并配置正确的链接路径和库文件名称。2.2 获取文件的MD5值(openssl库)以下是使用 OpenSSL 库计算文件的MD5值的示例代码:(1)需要安装 OpenSSL 库(如果尚未安装)并包含相关头文件:#include <stdio.h>#include <stdlib.h>#include <openssl/md5.h>(2)创建一个子函数来计算文件的MD5值:void calculate_file_md5(const char* filename, unsigned char* md5_hash) { FILE* file = fopen(filename, "rb"); if (file == NULL) { printf("Failed to open file: %s\n", filename); return; }​ MD5_CTX ctx; MD5_Init(&ctx);​ unsigned char buffer[1024]; size_t read; while ((read = fread(buffer, 1, sizeof(buffer), file)) != 0) { MD5_Update(&ctx, buffer, read); }​ fclose(file);​ MD5_Final(md5_hash, &ctx);}该函数接受两个参数:filename 为待计算的文件名,md5_hash 为存储MD5值的数组。下面是一个完整的示例程序,展示如何调用以上子函数并打印文件的MD5值:#include <stdio.h>#include <stdlib.h>#include <openssl/md5.h>​void calculate_file_md5(const char* filename, unsigned char* md5_hash) { // ... 函数实现见上文 ...​void print_md5(const unsigned char* md5_hash) { for (int i = 0; i < MD5_DIGEST_LENGTH; i++) { printf("%02x", md5_hash[i]); } printf("\n");}​int main() { const char* filename = "path/to/file"; unsigned char md5_hash[MD5_DIGEST_LENGTH];​ calculate_file_md5(filename, md5_hash); printf("MD5: "); print_md5(md5_hash);​ return 0;}这个示例程序将打开指定文件并计算其MD5值。需要将文件路径存储在 filename 字符串中,并根据需要调整该字符串。请这里使用的是 OpenSSL 提供的 MD5 函数。在编译时,需要链接 OpenSSL 库。在 Linux 系统上,可以使用 -lssl -lcrypto 参数进行链接。在 Windows 系统上,需要下载并安装 OpenSSL 库,并配置正确的链接路径和库文件名称。2.3 自己写算法获取MD5值实现MD5算法比较复杂,涉及位操作、逻辑运算、位移等。以下是一个简化版本的纯C语言MD5算法实现:#include <stdio.h>#include <stdlib.h>#include <string.h>​typedef unsigned char uint8;typedef unsigned int uint32;​// MD5常量定义const uint32 MD5_CONSTANTS[] = { 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391};​// 循环左移#define LEFT_ROTATE(x, n) (((x) << (n)) | ((x) >> (32 - (n))))​// 转换为大端字节序void to_big_endian(uint32 value, uint8* buffer) { buffer[0] = (uint8)(value & 0xff); buffer[1] = (uint8)((value >> 8) & 0xff); buffer[2] = (uint8)((value >> 16) & 0xff); buffer[3] = (uint8)((value >> 24) & 0xff);}​// 处理消息块void process_block(const uint8* block, uint32* state) { uint32 a = state[0]; uint32 b = state[1]; uint32 c = state[2]; uint32 d = state[3]; uint32 m[16];​ // 将消息块划分为16个32位字,并进行字节序转换 for (int i = 0; i < 16; i++) { m[i] = (((uint32)block[i * 4 + 0]) << 0) | (((uint32)block[i * 4 + 1]) << 8) | (((uint32)block[i * 4 + 2]) << 16) | (((uint32)block[i * 4 + 3]) << 24); }​ // MD5循环运算 for (int i = 0; i < 64; i++) { uint32 f, g;​ if (i < 16) { f = (b & c) | ((~b) & d); g = i; } else if (i < 32) { f = (d & b) | ((~d) & c); g = (5 * i + 1) % 16; } else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) % 16; } else { f = c ^ (b | (~d)); g = (7 * i) % 16; }​ uint32 temp = d; d = c; c = b; b = b + LEFT_ROTATE((a + f + MD5_CONSTANTS[i] + m[g]), 7); a = temp; }​ // 更新状态 state[0] += a; state[1] += b; state[2] += c; state[3] += d;}​// 计算MD5值void calculate_md5(const uint8* message, size_t length, uint8* digest) { // 初始化状态 uint32 state[4] = { 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476 };​ // 填充消息 size_t padded_length = ((length + 8) / 64 + 1) * 64; uint8* padded_message = (uint8*)calloc(padded_length, 1); memcpy(padded_message, message, length); padded_message[length] = 0x80; // 添加一个1 to_big_endian((uint32)(length * 8), padded_message + padded_length - 8); // 添加长度(以位为单位)​ // 处理消息块 for (size_t i = 0; i < padded_length; i += 64) { process_block(padded_message + i, state); }​ // 生成摘要 for (int i = 0; i < 4; i++) { to_big_endian(state[i], digest + i * 4); } free(padded_message);}​// 打印MD5值void print_md5(const uint8* digest) { for (int i = 0; i < 16; i++) { printf("%02x", digest[i]); } printf("\n");}​int main() { const char* message = "Hello, World!"; size_t length = strlen(message); uint8 digest[16];​ calculate_md5((const uint8*)message, length, digest); printf("MD5: "); print_md5(digest);​ return 0;}这个程序可以计算给定字符串的MD5值。将待计算的数据存储在 message 字符串中,根据需要调整数据长度。
  • [技术干货] C语言实例_奇偶校验算法
    一、奇偶校验算法奇偶校验算法(Parity Check Algorithm)是一种简单的错误检测方法,用于验证数据传输中是否发生了位错误。通过在数据中添加一个附加的奇偶位(即校验位),来实现错误的检测和纠正。在奇偶校验算法中,假设每个字节由8个比特(位)组成。奇偶校验位的值取决于数据字节中的1的个数。如果数据字节中1的个数是偶数个,奇偶校验位被设置为0;如果1的个数是奇数个,奇偶校验位被设置为1。这样,在接收端,通过统计接收到的数据字节中1的个数,就可以检测出位错误。具体的奇偶校验算法包括以下几个步骤:(1)发送端:在发送数据字节之前,统计数据字节中1的个数,根据个数设置奇偶校验位的值,并将数据字节和奇偶校验位一起发送。(2)接收端:在接收数据字节后,再次统计接收到的数据字节中1的个数,与接收到的奇偶校验位进行比较。如果两者不一致,说明数据传输中发生了位错误。奇偶校验算法在以下场景中常被使用:(1)串行通信:在串行通信中,奇偶校验算法可以用于检测数据传输过程中发生的位错误。发送端计算奇偶校验位并附加到发送的数据字节上,接收端通过验证奇偶校验位来判断接收到的数据是否正确。(2)存储介质:在一些存储介质上,如磁盘驱动器或闪存存储器,奇偶校验算法可以用于检测数据读取或写入过程中发生的位错误。在存储数据时,计算奇偶校验位并与数据一起存储;在读取数据时,再次计算校验位并与存储的校验位进行比较,以确保数据的完整性和准确性。(3)错误检测:奇偶校验算法也可以用于其他需要简单错误检测的场景。例如,在计算机内存或寄存器中,奇偶校验位可以用于检测存储数据过程中的位错误,以避免数据的错误使用或传输。奇偶校验算法只能检测到位错误,而不能纠正错误。如果检测到错误,则需要采取其他纠错措施或请求重新传输数据。二、代码实现场景:在单片机通信里,单片机需要向上位机发送数据。 下面代码演示两个函数,针对发送方和接收方使用,使用奇偶校验算法对数据进行验证。2.1 发送方函数void sender_send_data_with_parity(unsigned char* data, int length) { // 统计数据字节中1的个数 int count = 0; for (int i = 0; i < length; i++) { unsigned char byte = data[i]; for (int j = 0; j < 8; j++) { if ((byte >> j) & 1) { count++; } } }​ // 计算奇偶校验位,如果1的个数是偶数,则校验位为0,否则为1 unsigned char parity_bit = (count % 2 == 0) ? 0 : 1;​ // 发送数据字节和奇偶校验位 for (int i = 0; i < length; i++) { send_byte(data[i]); } send_byte(parity_bit);}2.2 接收方函数void receiver_receive_data_with_parity() { // 接收数据 unsigned char received_data[MAX_LENGTH]; int length = receive_data(received_data);​ // 统计接收到的数据字节中1的个数 int count = 0; for (int i = 0; i < length - 1; i++) { unsigned char byte = received_data[i]; for (int j = 0; j < 8; j++) { if ((byte >> j) & 1) { count++; } } }​ // 比较接收到的奇偶校验位与数据字节中1的个数是否一致 unsigned char expected_parity_bit = (count % 2 == 0) ? 0 : 1; unsigned char received_parity_bit = received_data[length - 1];​ if (expected_parity_bit != received_parity_bit) { // 发生了位错误 handle_error(); } else { // 数据传输正常 process_data(received_data, length - 1); }}
  • [技术干货] C语言实例_和校验算法
    一、算法介绍和校验(Checksum)是一种简单的纠错算法,用于检测或验证数据传输或存储过程中的错误。它通过对数据进行计算并生成校验和,然后将校验和附加到数据中,在接收端再次计算校验和并进行比较,以确定数据是否完整和正确。和校验算法通常使用位运算来计算校验和。常见的和校验算法有如下几种:(1)简单累加校验和(Simple Sum Checksum):将数据中的所有字节相加,并将结果与一个预定义的校验和进行比较。如果两者相等,则数据没有发生错误。(2)CRC(Cyclic Redundancy Check):使用除法来计算校验和,具有更高的错误检测能力。CRC算法使用一个固定的生成多项式对数据进行除法运算,生成一个余数作为校验和。和校验算法可以用于各种不同的应用场景:(1)数据传输:在数据通过网络传输、串口通信或其他通信渠道传递时,和校验可以检测出传输过程中发生的位错误或传输错误,确保数据的完整性和准确性。(2)存储校验:在数据存储介质上写入数据或从存储介质中读取数据时,和校验可以帮助检测到媒体故障或数据损坏。(3)文件校验:在下载文件、备份文件或转移文件等场景中,和校验可用于验证文件完整性,确保文件没有被篡改或损坏。(4)数据库校验:在数据库系统中,和校验可用于检测数据完整性,防止数据在存储或传输过程中发生错误或损坏。和校验算法是一种简单但实用的纠错算法,用于检测数据传输或存储过程中的错误,并在很多应用中得到了广泛的应用,以确保数据的完整性和准确性。二、代码实现场景:在单片机通信里,单片机需要向上位机发送一段数据。比如,存放在char buff[1024];这个数组里。 需要封装两个函数,单片机端调用函数对这段数据进行和校验,封装校验值,然后上位机收到数据之后验证 校验和,校验数据是否传输正确。2.1 单片机端封装函数(发送数据并计算和校验)// 计算校验和unsigned char calculateChecksum(const char* data, int length) { unsigned char checksum = 0; for (int i = 0; i < length; i++) { checksum += data[i]; } return checksum;}​// 发送数据并附加校验和void sendDataWithChecksum(const char* data, int length) { // 发送数据... unsigned char checksum = calculateChecksum(data, length); // 发送校验和 // 若使用UART通信,可以使用以下代码发送校验和,并确保上位机端能够解析它 // sendByte(checksum); // 发送校验和}以上代码中,calculateChecksum 函数用于计算数据的校验和,将每个数据字节相加并返回校验和值。sendDataWithChecksum 函数用于在发送数据之前计算校验和,并将校验和发送给上位机。2.2 上位机端封装函数(接收数据并验证校验和)e// 验证校验和bool verifyChecksum(const char* data, int length, unsigned char receivedChecksum) { unsigned char calculatedChecksum = calculateChecksum(data, length); return calculatedChecksum == receivedChecksum;}​// 接收数据并验证校验和void receiveDataWithChecksum(const char* data, int length) { // 接收数据... unsigned char receivedChecksum = receiveChecksum(); // 假设从上位机接收到校验和的值 if (verifyChecksum(data, length, receivedChecksum)) { // 校验通过,数据传输正确 // 处理数据... } else { // 校验失败,数据传输错误 // 进行相应的处理... }}在上位机端,verifyChecksum 函数用于验证校验和是否与接收到的校验和相匹配。receiveDataWithChecksum 函数用于接收数据及校验和,并调用 verifyChecksum 函数进行验证。如果校验通过,则数据传输正确;否则,数据传输错误。
  • [问题求助] ffmpeg命令行录制桌面视频能指定区域录制吗?
    ffmpeg命令行录制桌面视频能指定区域录制吗?   比如,只是录制我设置的区域这种,或者录制全屏。
  • [技术干货] C语言实例_调用SQLITE数据库完成数据增删改查
    一、SQLite介绍SQLite是一种轻量级的关系型数据库管理系统(RDBMS),它是一个开源的、零配置的、服务器端的、自包含的、零管理的、事务性的SQL数据库引擎。它被广泛应用于嵌入式设备、移动设备和桌面应用程序等领域。SQLite的特点包括:(1)轻量级:SQLite的核心库只有几百KB,非常适合在嵌入式设备、移动设备等资源受限的环境中使用。(2)零配置:SQLite不需要任何配置,只需要将库文件嵌入到应用程序中即可。(3)服务器端:SQLite不需要运行在服务器上,所有的数据都存储在本地文件中。(4)自包含:SQLite的所有功能都包含在一个单独的库文件中,不需要依赖其他库文件。(5)零管理:SQLite不需要维护数据库的连接、事务等状态,所有的操作都是自动的。(6)事务性:SQLite支持ACID事务,保证数据的一致性和可靠性。SQLite支持标准的SQL语句,包括SELECT、INSERT、UPDATE、DELETE等操作,同时还支持多种数据类型,包括整数、浮点数、字符串、日期等。SQLite还支持多种编程语言,包括C、C++、Python、Java等,可以方便地集成到各种应用程序中。在 Windows 系统下,可以按照以下步骤下载和安装 SQLite 数据库:(1)打开 SQLite 官方网站:cid:link_0(2)在网站顶部的菜单栏中,点击 "Downloads" 进入下载页面。(3)在下载页面中,你会看到一些可用的预编译二进制文件。根据你的 Windows 操作系统位数(32位或64位)和需求选择适当的版本。(4)对于 32 位系统,可以在 "Precompiled Binaries for Windows" 部分直接下载 sqlite-dll-win32-x86-*.zip 和 sqlite-tools-win32-x86-*.zip 文件。(5)对于 64 位系统,可以在 "Precompiled Binaries for Windows" 部分直接下载 sqlite-dll-win64-x64-*.zip 和 sqlite-tools-win64-x64-*.zip 文件。(6)下载完成后,解压缩 sqlite-dll-win*.zip 文件得到 sqlite3.dll 文件。(7)创建一个文件夹(命名为 SQLite 或其他喜欢的名称),将 sqlite3.dll 文件拷贝到该文件夹中。(8)接下来,解压缩 sqlite-tools-win*.zip 文件得到一组工具文件,包括 sqlite3.exe,sqlite3_analyzer.exe 和 sqlite3.def 文件。(8)将这些工具文件也拷贝到之前创建的 SQLite 文件夹中。二、SQLite语法SQLite是一个轻量级的嵌入式数据库,支持标准SQL语法。下面逐一介绍SQLite数据库的创建表、插入数据、删除数据、更新数据和查询数据的语法:(1)创建表要在SQLite数据库中创建表,可以使用CREATE TABLE语句。语法如下:CREATE TABLE table_name ( column1 datatype constraint, column2 datatype constraint, ...);table_name 是所要创建的表的名称。column1, column2等是表的列名。datatype 是列的数据类型。constraint 是可选项,用于定义列约束条件,比如主键、唯一约束、外键等。例如,创建一个名为 "students" 的表,包含id、name和age三个列:CREATE TABLE students ( id INTEGER PRIMARY KEY, name TEXT, age INTEGER);(2)插入数据要向SQLite数据库中的表插入数据,可以使用INSERT INTO语句。语法如下:INSERT INTO table_name (column1, column2, ...)VALUES (value1, value2, ...);table_name 是要插入数据的表的名称。column1, column2等是表的列名。value1, value2等是要插入的具体数值。例如,向 "students" 表中插入一条数据:INSERT INTO students (id, name, age)VALUES (1, '张三', 20);(3)删除数据要从SQLite数据库中的表删除数据,可以使用DELETE FROM语句。语法如下:DELETE FROM table_nameWHERE condition;table_name 是要删除数据的表的名称。condition 是可选项,用于指定删除数据的条件。例如,从 "students" 表中删除 id 为 1 的数据:DELETE FROM studentsWHERE id = 1;(4)更新数据要更新SQLite数据库表中的数据,可以使用UPDATE语句。语法如下:UPDATE table_nameSET column1 = value1, column2 = value2, ...WHERE condition;table_name 是要更新数据的表的名称。column1, column2等是要更新的列名。value1, value2等是要更新的具体数值。condition 是可选项,用于指定更新数据的条件。例如,将 "students" 表中 id 为 1 的数据的 name 更新为 '李四',age 更新为 22:UPDATE studentsSET name = '李四', age = 22WHERE id = 1;(5)查询数据要从SQLite数据库表中查询数据,可以使用SELECT语句。语法如下:SELECT column1, column2, ...FROM table_nameWHERE condition;column1, column2等是要查询的列名,如果要查询所有列,可以使用 * 符号代替。table_name 是要查询数据的表的名称。condition 是可选项,用于指定查询数据的条件。例如,从 "students" 表中查询所有数据:SELECT * FROM students;三、完整代码下面C语言代码,演示如何使用SQLite数据库:#include <stdio.h>#include <stdlib.h>#include <sqlite3.h>// 数据库文件名const char* DB_FILE = "example.db";// 回调函数,用于执行查询sql语句后的结果处理int selectCallback(void* data, int argc, char** argv, char** azColName){ int i; for (i = 0; i < argc; i++) { printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL"); } printf("\n"); return 0;}// 初始化数据库连接sqlite3* initDatabase(){ sqlite3* db; int rc = sqlite3_open(DB_FILE, &db); if (rc != SQLITE_OK) { fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return NULL; } return db;}// 关闭数据库连接void closeDatabase(sqlite3* db){ if (db) { sqlite3_close(db); }}// 创建表void createTable(sqlite3* db){ char* errMsg; const char* createSql = "CREATE TABLE IF NOT EXISTS students (id INT PRIMARY KEY, name TEXT, age INT);"; int rc = sqlite3_exec(db, createSql, NULL, 0, &errMsg); if (rc != SQLITE_OK) { fprintf(stderr, "无法创建表: %s\n", errMsg); sqlite3_free(errMsg); } else { printf("表创建成功\n"); }}// 插入数据void insertData(sqlite3* db, int id, const char* name, int age){ char insertSql[100]; snprintf(insertSql, sizeof(insertSql), "INSERT INTO students (id, name, age) VALUES (%d, '%s', %d);", id, name, age); char* errMsg; int rc = sqlite3_exec(db, insertSql, NULL, 0, &errMsg); if (rc != SQLITE_OK) { fprintf(stderr, "无法插入数据: %s\n", errMsg); sqlite3_free(errMsg); } else { printf("数据插入成功\n"); }}// 更新数据void updateData(sqlite3* db, int id, const char* name, int age){ char updateSql[100]; snprintf(updateSql, sizeof(updateSql), "UPDATE students SET name = '%s', age = %d WHERE id = %d;", name, age, id); char* errMsg; int rc = sqlite3_exec(db, updateSql, NULL, 0, &errMsg); if (rc != SQLITE_OK) { fprintf(stderr, "无法更新数据: %s\n", errMsg); sqlite3_free(errMsg); } else { printf("数据更新成功\n"); }}// 删除数据void deleteData(sqlite3* db, int id){ char deleteSql[100]; snprintf(deleteSql, sizeof(deleteSql), "DELETE FROM students WHERE id = %d;", id); char* errMsg; int rc = sqlite3_exec(db, deleteSql, NULL, 0, &errMsg); if (rc != SQLITE_OK) { fprintf(stderr, "无法删除数据: %s\n", errMsg); sqlite3_free(errMsg); } else { printf("数据删除成功\n"); }}// 查询数据void selectData(sqlite3* db){ char* errMsg; const char* selectSql = "SELECT * FROM students;"; int rc = sqlite3_exec(db, selectSql, selectCallback, 0, &errMsg); if (rc != SQLITE_OK) { fprintf(stderr, "无法查询数据: %s\n", errMsg); sqlite3_free(errMsg); }}int main(){ sqlite3* db = initDatabase(); if (db) { createTable(db); insertData(db, 1, "张三", 20); insertData(db, 2, "李四", 22); insertData(db, 3, "王五", 25); selectData(db); updateData(db, 1, "赵六", 23); selectData(db); deleteData(db, 3); selectData(db); closeDatabase(db); } return 0;}在这个示例中,通过 initDatabase 函数初始化数据库连接,通过 createTable 函数创建一个名为 "students" 的表。然后使用 insertData 函数插入数据,updateData 函数更新数据,deleteData 函数删除数据,selectData 函数查询数据,并通过 selectCallback 回调函数处理查询结果。
  • [问题求助] ATK-HC05蓝牙模块与手机连接问题
    蓝牙模块第一次与手机配对时输入密码后配对成功 但是上电重启模块后,手机蓝牙设置中虽然显示之前配对过hc05,但模块本身并未进入配对状态。
  • [常见FAQ] C++编译异常
    为什么之前练习赛的时候上传到服务器是正常的,现在正式赛上传后显示编译错误呢,请问两者有些什么区别
  • [新手课堂] struct 结构详解——转载
    简介C 语言内置的数据类型,除了最基本的几种原始类型,只有数组属于复合类型,可以同时包含多个值,但是只能包含相同类型的数据,实际使用中并不够用。实际使用中,主要有下面两种情况,需要更灵活强大的复合类型。复杂的物体需要使用多个变量描述,这些变量都是相关的,最好有某种机制将它们联系起来。某些函数需要传入多个参数,如果一个个按照顺序传入,非常麻烦,最好能组合成一个复合结构传入。为了解决这些问题,C 语言提供了struct关键字,允许自定义复合数据类型,将不同类型的值组合在一起。这样不仅为编程提供方便,也有利于增强代码的可读性。C 语言没有其他语言的对象(object)和类(class)的概念,struct 结构很大程度上提供了对象和类的功能。下面是struct自定义数据类型的一个例子。struct fraction { int numerator; int denominator; };上面示例定义了一个分数的数据类型struct fraction,包含两个属性numerator和denominator。注意,作为一个自定义的数据类型,它的类型名要包括struct关键字,比如上例是struct fraction,单独的fraction没有任何意义,甚至脚本还可以另外定义名为fraction的变量,虽然这样很容易造成混淆。另外,struct语句结尾的分号不能省略,否则很容易产生错误。定义了新的数据类型以后,就可以声明该类型的变量,这与声明其他类型变量的写法是一样的。struct fraction f1; f1.numerator = 22; f1.denominator = 7;上面示例中,先声明了一个struct fraction类型的变量f1,这时编译器就会为f1分配内存,接着就可以为f1的不同属性赋值。可以看到,struct 结构的属性通过点(.)来表示,比如numerator属性要写成f1.numerator。再提醒一下,声明自定义类型的变量时,类型名前面,不要忘记加上struct关键字。也就是说,必须使用struct fraction f1声明变量,不能写成fraction f1。除了逐一对属性赋值,也可以使用大括号,一次性对 struct 结构的所有属性赋值。struct car { char* name; float price; int speed; }; struct car saturn = {"Saturn SL/2", 16000.99, 175};上面示例中,变量saturn是struct car类型,大括号里面同时对它的三个属性赋值。如果大括号里面的值的数量,少于属性的数量,那么缺失的属性自动初始化为0。注意,大括号里面的值的顺序,必须与 struct 类型声明时属性的顺序一致。否则,必须为每个值指定属性名。struct car saturn = {.speed=172, .name="Saturn SL/2"};上面示例中,初始化的属性少于声明时的属性,这时剩下的那些属性都会初始化为0。声明变量以后,可以修改某个属性的值。struct car saturn = {.speed=172, .name="Saturn SL/2"}; saturn.speed = 168;上面示例将speed属性的值改成168。struct 的数据类型声明语句与变量的声明语句,可以合并为一个语句。struct book { char title[500]; char author[100]; float value; } b1;上面的语句同时声明了数据类型book和该类型的变量b1。如果类型标识符book只用在这一个地方,后面不再用到,这里可以将类型名省略。struct { char title[500]; char author[100]; float value; } b1;上面示例中,struct声明了一个匿名数据类型,然后又声明了这个类型的变量b1。与其他变量声明语句一样,可以在声明变量的同时,对变量赋值。struct { char title[500]; char author[100]; float value; } b1 = {"Harry Potter", "J. K. Rowling", 10.0}, b2 = {"Cancer Ward", "Aleksandr Solzhenitsyn", 7.85};上面示例中,在声明变量b1和b2的同时,为它们赋值。下一章介绍的typedef命令可以为 struct 结构指定一个别名,这样使用起来更简洁。typedef struct cell_phone { int cell_no; float minutes_of_charge; } phone; phone p = {5551234, 5};上面示例中,phone就是struct cell_phone的别名。指针变量也可以指向struct结构。struct book { char title[500]; char author[100]; float value; }* b1; // 或者写成两个语句 struct book { char title[500]; char author[100]; float value; }; struct book* b1;上面示例中,变量b1是一个指针,指向的数据是struct book类型的实例。struct 结构也可以作为数组成员。struct fraction numbers[1000]; numbers[0].numerator = 22; numbers[0].denominator = 7;上面示例声明了一个有1000个成员的数组numbers,每个成员都是自定义类型fraction的实例。struct 结构占用的存储空间,不是各个属性存储空间的总和,而是最大内存占用属性的存储空间的倍数,其他属性会添加空位与之对齐。这样可以提高读写效率。struct foo { int a; char* b; char c; }; printf("Ö628fd57-6971-4990-b3f3-d9a220cda10en", sizeof(struct foo)); // 24上面示例中,struct foo有三个属性,在64位计算机上占用的存储空间分别是:int a占4个字节,指针char* b占8个字节,char c占1个字节。它们加起来,一共是13个字节(4 + 8 + 1)。但是实际上,struct foo会占用24个字节,原因是它最大的内存占用属性是char* b的8个字节,导致其他属性的存储空间也是8个字节,这样才可以对齐,导致整个struct foo就是24个字节(8 * 3)。多出来的存储空间,都采用空位填充,所以上面的struct foo真实的结构其实是下面这样。struct foo { int a; // 4 char pad1[4]; // 填充4字节 char *b; // 8 char c; // 1 char pad2[7]; // 填充7字节 }; printf("Ö628fd57-6971-4990-b3f3-d9a220cda10en", sizeof(struct foo)); // 24为什么浪费这么多空间进行内存对齐呢?这是为了加快读写速度,把内存占用划分成等长的区块,就可以快速在 Struct 结构体中定位到每个属性的起始地址。由于这个特性,在有必要的情况下,定义 Struct 结构体时,可以采用存储空间递增的顺序,定义每个属性,这样就能节省一些空间。struct foo { char c; int a; char* b; }; printf("Ö628fd57-6971-4990-b3f3-d9a220cda10en", sizeof(struct foo)); // 16上面示例中,占用空间最小的char c排在第一位,其次是int a,占用空间最大的char* b排在最后。整个strct foo的内存占用就从24字节下降到16字节。struct 的复制struct 变量可以使用赋值运算符(=),复制给另一个变量,这时会生成一个全新的副本。系统会分配一块新的内存空间,大小与原来的变量相同,把每个属性都复制过去,即原样生成了一份数据。这一点跟数组的复制不一样,务必小心。struct cat { char name[30]; short age; } a, b; strcpy(a.name, "Hula"); a.age = 3; b = a; b.name[0] = 'M'; printf("%s\n", a.name); // Hula printf("%s\n", b.name); // Mula上面示例中,变量b是变量a的副本,两个变量的值是各自独立的,修改掉b.name不影响a.name。上面这个示例是有前提的,就是 struct 结构的属性必须定义成字符数组,才能复制数据。如果稍作修改,属性定义成字符指针,结果就不一样。struct cat { char* name; short age; } a, b; a.name = "Hula"; a.age = 3; b = a;上面示例中,name属性变成了一个字符指针,这时a赋值给b,导致b.name也是同样的字符指针,指向同一个地址,也就是说两个属性共享同一个地址。因为这时,struct 结构内部保存的是一个指针,而不是上一个例子的数组,这时复制的就不是字符串本身,而是它的指针。并且,这个时候也没法修改字符串,因为字符指针指向的字符串是不能修改的。总结一下,赋值运算符(=)可以将 struct 结构每个属性的值,一模一样复制一份,拷贝给另一个 struct 变量。这一点跟数组完全不同,使用赋值运算符复制数组,不会复制数据,只会共享地址。注意,这种赋值要求两个变量是同一个类型,不同类型的 struct 变量无法互相赋值。另外,C 语言没有提供比较两个自定义数据结构是否相等的方法,无法用比较运算符(比如==和!=)比较两个数据结构是否相等或不等。struct 指针如果将 struct 变量传入函数,函数内部得到的是一个原始值的副本。#include struct turtle { char* name; char* species; int age; }; void happy(struct turtle t) { t.age = t.age + 1; } int main() { struct turtle myTurtle = {"MyTurtle", "sea turtle", 99}; happy(myTurtle); printf("Age is %i\n", myTurtle.age); // 输出 99 return 0; }上面示例中,函数happy()传入的是一个 struct 变量myTurtle,函数内部有一个自增操作。但是,执行完happy()以后,函数外部的age属性值根本没变。原因就是函数内部得到的是 struct 变量的副本,改变副本影响不到函数外部的原始数据。通常情况下,开发者希望传入函数的是同一份数据,函数内部修改数据以后,会反映在函数外部。而且,传入的是同一份数据,也有利于提高程序性能。这时就需要将 struct 变量的指针传入函数,通过指针来修改 struct 属性,就可以影响到函数外部。struct 指针传入函数的写法如下。void happy(struct turtle* t) { } happy(&myTurtle);上面代码中,t是 struct 结构的指针,调用函数时传入的是指针。struct 类型跟数组不一样,类型标识符本身并不是指针,所以传入时,指针必须写成&myTurtle。函数内部也必须使用(*t).age的写法,从指针拿到 struct 结构本身。void happy(struct turtle* t) { (*t).age = (*t).age + 1; }上面示例中,(*t).age不能写成*t.age,因为点运算符.的优先级高于*。*t.age这种写法会将t.age看成一个指针,然后取它对应的值,会出现无法预料的结果。现在,重新编译执行上面的整个示例,happy()内部对 struct 结构的操作,就会反映到函数外部。(*t).age这样的写法很麻烦。C 语言就引入了一个新的箭头运算符(->),可以从 struct 指针上直接获取属性,大大增强了代码的可读性。void happy(struct turtle* t) { t->age = t->age + 1; }总结一下,对于 struct 变量名,使用点运算符(.)获取属性;对于 struct 变量指针,使用箭头运算符(->)获取属性。以变量myStruct为例,假设ptr是它的指针,那么下面三种写法是同一回事。// ptr == &myStruct myStruct.prop == (*ptr).prop == ptr->propstruct 的嵌套struct 结构的成员可以是另一个 struct 结构。struct species { char* name; int kinds; }; struct fish { char* name; int age; struct species breed; };上面示例中,fish的属性breed是另一个 struct 结构species。赋值的时候有多种写法。// 写法一 struct fish shark = {"shark", 9, {"Selachimorpha", 500}}; // 写法二 struct species myBreed = {"Selachimorpha", 500}; struct fish shark = {"shark", 9, myBreed}; // 写法三 struct fish shark = { .name="shark", .age=9, .breed={"Selachimorpha", 500} }; // 写法四 struct fish shark = { .name="shark", .age=9, .breed.name="Selachimorpha", .breed.kinds=500 }; printf("Shark's species is %s", shark.breed.name);上面示例展示了嵌套 Struct 结构的四种赋值写法。另外,引用breed属性的内部属性,要使用两次点运算符(shark.breed.name)。下面是另一个嵌套 struct 的例子。struct name { char first[50]; char last[50]; }; struct student { struct name name; short age; char sex; } student1; strcpy(student1.name.first, "Harry"); strcpy(student1.name.last, "Potter"); // or struct name myname = {"Harry", "Potter"}; student1.name = myname;上面示例中,自定义类型student的name属性是另一个自定义类型,如果要引用后者的属性,就必须使用两个.运算符,比如student1.name.first。另外,对字符数组属性赋值,要使用strcpy()函数,不能直接赋值,因为直接改掉字符数组名的地址会报错。struct 结构内部不仅可以引用其他结构,还可以自我引用,即结构内部引用当前结构。比如,链表结构的节点就可以写成下面这样。struct node { int data; struct node* next; };上面示例中,node结构的next属性,就是指向另一个node实例的指针。下面,使用这个结构自定义一个数据链表。struct node { int data; struct node* next; }; struct node* head; // 生成一个三个节点的列表 (11)->(22)->(33) head = malloc(sizeof(struct node)); head->data = 11; head->next = malloc(sizeof(struct node)); head->next->data = 22; head->next->next = malloc(sizeof(struct node)); head->next->next->data = 33; head->next->next->next = NULL; // 遍历这个列表 for (struct node *cur = head; cur != NULL; cur = cur->next) { printf("Ö628fd57-6971-4990-b3f3-d9a220cda10en", cur->data); }上面示例是链表结构的最简单实现,通过for循环可以对其进行遍历。位字段struct 还可以用来定义二进制位组成的数据结构,称为“位字段”(bit field),这对于操作底层的二进制数据非常有用。struct { unsigned int ab:1; unsigned int cd:1; unsigned int ef:1; unsigned int gh:1; } synth; synth.ab = 0; synth.cd = 1;上面示例中,每个属性后面的:1,表示指定这些属性只占用一个二进制位,所以这个数据结构一共是4个二进制位。注意,定义二进制位时,结构内部的各个属性只能是整数类型。实际存储的时候,C 语言会按照int类型占用的字节数,存储一个位字段结构。如果有剩余的二进制位,可以使用未命名属性,填满那些位。也可以使用宽度为0的属性,表示占满当前字节剩余的二进制位,迫使下一个属性存储在下一个字节。struct { unsigned int field1 : 1; unsigned int : 2; unsigned int field2 : 1; unsigned int : 0; unsigned int field3 : 1; } stuff;上面示例中,stuff.field1与stuff.field2之间,有一个宽度为两个二进制位的未命名属性。stuff.field3将存储在下一个字节。弹性数组成员很多时候,不能事先确定数组到底有多少个成员。如果声明数组的时候,事先给出一个很大的成员数,就会很浪费空间。C 语言提供了一个解决方法,叫做弹性数组成员(flexible array member)。如果不能事先确定数组成员的数量时,可以定义一个 struct 结构。struct vstring { int len; char chars[]; };上面示例中,struct vstring结构有两个属性。len属性用来记录数组chars的长度,chars属性是一个数组,但是没有给出成员数量。chars数组到底有多少个成员,可以在为vstring分配内存时确定。struct vstring* str = malloc(sizeof(struct vstring) + n * sizeof(char)); str->len = n;上面示例中,假定chars数组的成员数量是n,只有在运行时才能知道n到底是多少。然后,就为struct vstring分配它需要的内存:它本身占用的内存长度,再加上n个数组成员占用的内存长度。最后,len属性记录一下n是多少。这样就可以让数组chars有n个成员,不用事先确定,可以跟运行时的需要保持一致。弹性数组成员有一些专门的规则。首先,弹性成员的数组,必须是 struct 结构的最后一个属性。另外,除了弹性数组成员,struct 结构必须至少还有一个其他属性。原文链接:https://wangdoc.com/clang/struct.html
  • [C/C++] C语言I/O 函数——转载
    缓存和字节流严格地说,输入输出函数并不是直接与外部设备通信,而是通过缓存(buffer)进行间接通信。这个小节介绍缓存是什么。普通文件一般都保存在磁盘上面,跟 CPU 相比,磁盘读取或写入数据是一个很慢的操作。所以,程序直接读写磁盘是不可行的,可能每执行一行命令,都必须等半天。C 语言的解决方案,就是只要打开一个文件,就在内存里面为这个文件设置一个缓存区。程序向文件写入数据时,程序先把数据放入缓存,等到缓存满了,再把里面的数据会一次性写入磁盘文件。这时,缓存区就空了,程序再把新的数据放入缓存,重复整个过程。程序从文件读取数据时,文件先把一部分数据放到缓存里面,然后程序从缓存获取数据,等到缓存空了,磁盘文件再把新的数据放入缓存,重复整个过程。内存的读写速度比磁盘快得多,缓存的设计减少了读写磁盘的次数,大大提高了程序的执行效率。另外,一次性移动大块数据,要比多次移动小块数据快得多。这种读写模式,对于程序来说,就有点像水流(stream),不是一次性读取或写入所有数据,而是一个持续不断的过程。先操作一部分数据,等到缓存吞吐完这部分数据,再操作下一部分数据。这个过程就叫做字节流操作。由于缓存读完就空了,所以字节流读取都是只能读一次,第二次就读不到了。这跟读取文件很不一样。C 语言的输入输出函数,凡是涉及读写文件,都是属于字节流操作。输入函数从文件获取数据,操作的是输入流;输出函数向文件写入数据,操作的是输出流。printf()printf()是最常用的输出函数,用于屏幕输出,原型定义在头文件stdio.h,详见《基本语法》一章。scanf()基本用法scanf()函数用于读取用户的键盘输入。程序运行到这个语句时,会停下来,等待用户从键盘输入。用户输入数据、按下回车键后,scanf()就会处理用户的输入,将其存入变量。它的原型定义在头文件stdio.h。scanf()的语法跟printf()类似。scanf("%d", &i);它的第一个参数是一个格式字符串,里面会放置占位符(与printf()的占位符基本一致),告诉编译器如何解读用户的输入,需要提取的数据是什么类型。这是因为 C 语言的数据都是有类型的,scanf()必须提前知道用户输入的数据类型,才能处理数据。它的其余参数就是存放用户输入的变量,格式字符串里面有多少个占位符,就有多少个变量。上面示例中,scanf()的第一个参数%d,表示用户输入的应该是一个整数。%d就是一个占位符,%是占位符的标志,d表示整数。第二个参数&i表示,将用户从键盘输入的整数存入变量i。注意,变量前面必须加上&运算符(指针变量除外),因为scanf()传递的不是值,而是地址,即将变量i的地址指向用户输入的值。如果这里的变量是指针变量(比如字符串变量),那就不用加&运算符。下面是一次将键盘输入读入多个变量的例子。scanf("%d%d%f%f", &i, &j, &x, &y);上面示例中,格式字符串%d%d%f%f,表示用户输入的前两个是整数,后两个是浮点数,比如1 -20 3.4 -4.0e3。这四个值依次放入i、j、x、y四个变量。scanf()处理数值占位符时,会自动过滤空白字符,包括空格、制表符、换行符等。所以,用户输入的数据之间,有一个或多个空格不影响scanf()解读数据。另外,用户使用回车键,将输入分成几行,也不影响解读。1 -20 3.4 -4.0e3上面示例中,用户分成四行输入,得到的结果与一行输入是完全一样的。每次按下回车键以后,scanf()就会开始解读,如果第一行匹配第一个占位符,那么下次按下回车键时,就会从第二个占位符开始解读。scanf()处理用户输入的原理是,用户的输入先放入缓存,等到按下回车键后,按照占位符对缓存进行解读。解读用户输入时,会从上一次解读遗留的第一个字符开始,直到读完缓存,或者遇到第一个不符合条件的字符为止。int x; float y; // 用户输入 " -13.45e12# 0" scanf("%d", &x); scanf("%f", &y);上面示例中,scanf()读取用户输入时,%d占位符会忽略起首的空格,从-处开始获取数据,读取到-13停下来,因为后面的.不属于整数的有效字符。这就是说,占位符%d会读到-13。第二次调用scanf()时,就会从上一次停止解读的地方,继续往下读取。这一次读取的首字符是.,由于对应的占位符是%f,会读取到.45e12,这是采用科学计数法的浮点数格式。后面的#不属于浮点数的有效字符,所以会停在这里。由于scanf()可以连续处理多个占位符,所以上面的例子也可以写成下面这样。scanf("%d%f", &x, &y);scanf()的返回值是一个整数,表示成功读取的变量个数。如果没有读取任何项,或者匹配失败,则返回0。如果读取到文件结尾,则返回常量 EOF。占位符scanf()常用的占位符如下,与printf()的占位符基本一致。%c:字符。%d:整数。%f:float类型浮点数。%lf:double类型浮点数。%Lf:long double类型浮点数。%s:字符串。%[]:在方括号中指定一组匹配的字符(比如%[0-9]),遇到不在集合之中的字符,匹配将会停止。上面所有占位符之中,除了%c以外,都会自动忽略起首的空白字符。%c不忽略空白字符,总是返回当前第一个字符,无论该字符是否为空格。如果要强制跳过字符前的空白字符,可以写成scanf(" %c", &ch),即%c前加上一个空格,表示跳过零个或多个空白字符。下面要特别说一下占位符%s,它其实不能简单地等同于字符串。它的规则是,从当前第一个非空白字符开始读起,直到遇到空白字符(即空格、换行符、制表符等)为止。因为%s不会包含空白字符,所以无法用来读取多个单词,除非多个%s一起使用。这也意味着,scanf()不适合读取可能包含空格的字符串,比如书名或歌曲名。另外,scanf()遇到%s占位符,会在字符串变量末尾存储一个空字符\0。scanf()将字符串读入字符数组时,不会检测字符串是否超过了数组长度。所以,储存字符串时,很可能会超过数组的边界,导致预想不到的结果。为了防止这种情况,使用%s占位符时,应该指定读入字符串的最长长度,即写成%[m]s,其中的[m]是一个整数,表示读取字符串的最大长度,后面的字符将被丢弃。char name[11]; scanf("s", name);上面示例中,name是一个长度为11的字符数组,scanf()的占位符s表示最多读取用户输入的10个字符,后面的字符将被丢弃,这样就不会有数组溢出的风险了。赋值忽略符有时,用户的输入可能不符合预定的格式。scanf("%d-%d-%d", &year, &month, &day);上面示例中,如果用户输入2020-01-01,就会正确解读出年、月、日。问题是用户可能输入其他格式,比如2020/01/01,这种情况下,scanf()解析数据就会失败。为了避免这种情况,scanf()提供了一个赋值忽略符(assignment suppression character)*。只要把*加在任何占位符的百分号后面,该占位符就不会返回值,解析后将被丢弃。scanf("%d%*c%d%*c%d", &year, &month, &day);上面示例中,%*c就是在占位符的百分号后面,加入了赋值忽略符*,表示这个占位符没有对应的变量,解读后不必返回。sscanf()sscanf()函数与scanf()很类似,不同之处是sscanf()从字符串里面,而不是从用户输入获取数据。它的原型定义在头文件stdio.h里面。int sscanf(const char* s, const char* format, ...);sscanf()的第一个参数是一个字符串指针,用来从其中获取数据。其他参数都与scanf()相同。sscanf()主要用来处理其他输入函数读入的字符串,从其中提取数据。fgets(str, sizeof(str), stdin); sscanf(str, "%d%d", &i, &j);上面示例中,fgets()先从标准输入获取了一行数据(fgets()的介绍详见下一章),存入字符数组str。然后,sscanf()再从字符串str里面提取两个整数,放入变量i和j。sscanf()的一个好处是,它的数据来源不是流数据,所以可以反复使用,不像scanf()的数据来源是流数据,只能读取一次。sscanf()的返回值是成功赋值的变量的数量,如果提取失败,返回常量 EOF。getchar(),putchar()(1)getchar()getchar()函数返回用户从键盘输入的一个字符,使用时不带有任何参数。程序运行到这个命令就会暂停,等待用户从键盘输入,等同于使用scanf()方法读取一个字符。它的原型定义在头文件stdio.h。char ch; ch = getchar(); // 等同于 scanf("%c", &ch);getchar()不会忽略起首的空白字符,总是返回当前读取的第一个字符,无论是否为空格。如果读取失败,返回常量 EOF,由于 EOF 通常是-1,所以返回值的类型要设为 int,而不是 char。由于getchar()返回读取的字符,所以可以用在循环条件之中。while (getchar() != '\n') ;上面示例中,只有读到的字符等于换行符(\n),才会退出循环,常用来跳过某行。while循环的循环体没有任何语句,表示对该行不执行任何操作。下面的例子是计算某一行的字符长度。int len = 0; while(getchar() != '\n') len++;上面示例中,getchar()每读取一个字符,长度变量len就会加1,直到读取到换行符为止,这时len就是该行的字符长度。下面的例子是跳过空格字符。while ((ch = getchar()) == ' ') ;上面示例中,结束循环后,变量ch等于第一个非空格字符。(2)putchar()putchar()函数将它的参数字符输出到屏幕,等同于使用printf()输出一个字符。它的原型定义在头文件stdio.h。putchar(ch); // 等同于 printf("%c", ch);操作成功时,putchar()返回输出的字符,否则返回常量 EOF。(3)小结由于getchar()和putchar()这两个函数的用法,要比scanf()和printf()更简单,而且通常是用宏来实现,所以要比scanf()和printf()更快。如果操作单个字符,建议优先使用这两个函数。puts()puts()函数用于将参数字符串显示在屏幕(stdout)上,并且自动在字符串末尾添加换行符。它的原型定义在头文件stdio.h。puts("Here are some messages:"); puts("Hello World");上面示例中,puts()在屏幕上输出两行内容。写入成功时,puts()返回一个非负整数,否则返回常量 EOF。gets()gets()函数以前用于从stdin读取整行输入,现在已经被废除了,仍然放在这里介绍一下。该函数读取用户的一行输入,不会跳过起始处的空白字符,直到遇到换行符为止。这个函数会丢弃换行符,将其余字符放入参数变量,并在这些字符的末尾添加一个空字符\0,使其成为一个字符串。它经常与puts()配合使用。char words[81]; puts("Enter a string, please"); gets(words);上面示例使用puts()在屏幕上输出提示,然后使用gets()获取用户的输入。由于gets()获取的字符串,可能超过字符数组变量的最大长度,有安全风险,建议不要使用,改为使用fgets()。原文链接:https://wangdoc.com/clang/io.html
  • c语言多字节字符详解——转载
    Unicode 简介C 语言诞生时,只考虑了英语字符,使用7位的 ASCII 码表示所有字符。ASCII 码的范围是0到127,也就是最多只能表示100多个字符,用一个字节就可以表示,所以char类型只占用一个字节。但是,如果处理非英语字符,一个字节就不够了,单单是中文,就至少有几万个字符,字符集就势必使用多个字节表示。最初,不同国家有自己的字符编码方式,这样不便于多种字符的混用。因此,后来就逐渐统一到 Unicode 编码,将所有字符放入一个字符集。Unicode 为每个字符提供一个号码,称为码点(code point),其中0到127的部分,跟 ASCII 码是重合的。通常使用“U+十六进制码点”表示一个字符,比如U+0041表示字母A。Unicode 编码目前一共包含了100多万个字符,码点范围是 U+0000 到 U+10FFFF。完整表达整个 Unicode 字符集,至少需要三个字节。但是,并不是所有文档都需要那么多字符,比如对于 ASCII 码就够用的英语文档,如果每个字符使用三个字节表示,就会比单字节表示的文件体积大出三倍。为了适应不同的使用需求,Unicode 标准委员会提供了三种不同的表示方法,表示 Unicode 码点。UTF-8:使用1个到4个字节,表示一个码点。不同的字符占用的字节数不一样。UTF-16:对于U+0000 到 U+FFFF 的字符(称为基本平面),使用2个字节表示一个码点。其他字符使用4个字节。UTF-32:统一使用4个字节,表示一个码点。其中,UTF-8 的使用最为广泛,因为对于 ASCII 字符(U+0000 到 U+007F),它只使用一个字节表示,这就跟 ASCII 的编码方式完全一样。C 语言提供了两个宏,表示当前系统支持的编码字节长度。这两个宏都定义在头文件limits.h。MB_LEN_MAX:任意支持地区的最大字节长度,定义在limits.h。MB_CUR_MAX:当前语言的最大字节长度,总是小于或等于MB_LEN_MAX,定义在stdlib.h。字符的表示方法字符表示法的本质,是将每个字符映射为一个整数,然后从编码表获得该整数对应的字符。C 语言提供了不同的写法,用来表示字符的整数号码。\123:以八进制值表示一个字符,斜杠后面需要三个数字。\x4D:以十六进制表示一个字符,\x后面是十六进制整数。\u2620:以 Unicode 码点表示一个字符(不适用于 ASCII 字符),码点以十六进制表示,\u后面需要4个字符。\U0001243F:以 Unicode 码点表示一个字符(不适用于 ASCII 字符),码点以十六进制表示,\U后面需要8个字符。printf("ABC\n"); printf("\101\102\103\n"); printf("\x41\x42\x43\n");上面三行都会输出“ABC”。printf("\u2022 Bullet 1\n"); printf("\U00002022 Bullet 1\n");上面两行都会输出“• Bullet 1”。多字节字符的表示C 语言预设只有基本字符,才能使用字面量表示,其它字符都应该使用码点表示,并且当前系统还必须支持该码点的编码方法。所谓基本字符,指的是所有可打印的 ASCII 字符,但是有三个字符除外:@、$、`。因此,遇到非英语字符,应该将其写成 Unicode 码点形式。char* s = "\u6625\u5929"; printf("%s\n", s); // 春天上面代码会输出中文“春天”。如果当前系统是 UTF-8 编码,可以直接用字面量表示多字节字符。char* s = "春天"; printf("%s\n", s);注意,\u + 码点和\U + 码点的写法,不能用来表示 ASCII 码字符(码点小于0xA0的字符),只有三个字符除外:0x24($),0x40(@)和0x60(`)。char* s = "\u0024\u0040\u0060"; printf("%s\n", s); // @$`上面代码会输出三个 Unicode 字符“@$`”,但是其它 ASCII 字符都不能用这种表示法表示。为了保证程序执行时,字符能够正确解读,最好将程序环境切换到本地化环境。setlocale(LC_ALL, "");上面代码中,使用setlocale()切换执行环境到系统的本地化语言。setlocale()的原型定义在头文件locale.h,详见标准库部分的《locale.h》章节。像下面这样,指定编码语言也可以。setlocale(LC_ALL, "zh_CN.UTF-8");上面代码将程序执行环境,切换到中文环境的 UTF-8 编码。C 语言允许使用u8前缀,对多字节字符串指定编码方式为 UTF-8。char* s = u8"春天"; printf("%s\n", s);一旦字符串里面包含多字节字符,就意味着字符串的字节数与字符数不再一一对应了。比如,字符串的长度为10字节,就不再是包含10个字符,而可能只包含7个字符、5个字符等等。setlocale(LC_ALL, ""); char* s = "春天"; printf("Ù375c3e5-77f2-429d-8f59-8e0aeb205b23n", strlen(s)); // 6上面示例中,字符串s只包含两个字符,但是strlen()返回的结果却是6,表示这两个字符一共占据了6个字节。C 语言的字符串函数只针对单字节字符有效,对于多字节字符都会失效,比如strtok()、strchr()、strspn()、toupper()、tolower()、isalpha()等不会得到正确结果。宽字符上一小节的多字节字符串,每个字符的字节宽度是可变的。这种编码方式虽然使用起来方便,但是很不利于字符串处理,因此必须逐一检查每个字符占用的字节数。所以除了这种方式,C 语言还提供了确定宽度的多字节字符存储方式,称为宽字符(wide character)。所谓“宽字符”,就是每个字符占用的字节数是固定的,要么是2个字节,要么是4个字节。这样的话,就很容易快速处理。宽字符有一个单独的数据类型 wchar_t,每个宽字符都是这个类型。它属于整数类型的别名,可能是有符号的,也可能是无符号的,由当前实现决定。该类型的长度为16位(2个字节)或32位(4个字节),足以容纳当前系统的所有字符。它定义在头文件wchar.h里面。宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。setlocale(LC_ALL, ""); wchar_t c = L'牛'; printf("%lc\n", c); wchar_t* s = L"春天"; printf("%ls\n", s);上面示例中,前缀“L”在单引号前面,表示宽字符,对应printf()的占位符为%lc;在双引号前面,表示宽字符串,对应printf()的占位符为%ls。宽字符串的结尾也有一个空字符,不过是宽空字符,占用多个字节。处理宽字符,需要使用宽字符专用的函数,绝大部分都定义在头文件wchar.h。多字节字符处理函数mblen()mblen()函数返回一个多字节字符占用的字节数。它的原型定义在头文件stdlib.h。int mblen(const char* mbstr, size_t n);它接受两个参数,第一个参数是多字节字符串指针,一般会检查该字符串的第一个字符;第二个参数是需要检查的字节数,这个数字不能大于当前系统单个字符占用的最大字节,一般使用MB_CUR_MAX。它的返回值是该字符占用的字节数。如果当前字符是空的宽字符,则返回0;如果当前字符不是有效的多字节字符,则返回-1。setlocale(LC_ALL, ""); char* mbs1 = "春天"; printf("Ù375c3e5-77f2-429d-8f59-8e0aeb205b23n", mblen(mbs1, MB_CUR_MAX)); // 3 char* mbs2 = "abc"; printf("Ù375c3e5-77f2-429d-8f59-8e0aeb205b23n", mblen(mbs2, MB_CUR_MAX)); // 1上面示例中,字符串“春天”的第一个字符“春”,占用3个字节;字符串“abc”的第一个字符“a”,占用1个字节。wctomb()wctomb()函数(wide character to multibyte)用于将宽字符转为多字节字符。它的原型定义在头文件stdlib.h。int wctomb(char* s, wchar_t wc);wctomb()接受两个参数,第一个参数是作为目标的多字节字符数组,第二个参数是需要转换的一个宽字符。它的返回值是多字节字符存储占用的字节数量,如果无法转换,则返回-1。setlocale(LC_ALL, ""); wchar_t wc = L'牛'; char mbStr[10] = ""; int nBytes = 0; nBytes = wctomb(mbStr, wc); printf("%s\n", mbStr); // 牛 printf("Ù375c3e5-77f2-429d-8f59-8e0aeb205b23n", nBytes); // 3上面示例中,wctomb()将宽字符“牛”转为多字节字符,wctomb()的返回值表示转换后的多字节字符占用3个字节。mbtowc()mbtowc()用于将多字节字符转为宽字符。它的原型定义在头文件stdlib.h。int mbtowc( wchar_t* wchar, const char* mbchar, size_t count );它接受3个参数,第一个参数是作为目标的宽字符指针,第二个参数是待转换的多字节字符指针,第三个参数是多字节字符的字节数。它的返回值是多字节字符的字节数,如果转换失败,则返回-1。setlocale(LC_ALL, ""); char* mbchar = "牛"; wchar_t wc; wchar_t* pwc = &wc; int nBytes = 0; nBytes = mbtowc(pwc, mbchar, 3); printf("Ù375c3e5-77f2-429d-8f59-8e0aeb205b23n", nBytes); // 3 printf("%lc\n", *pwc); // 牛上面示例中,mbtowc()将多字节字符“牛”转为宽字符wc,返回值是mbchar占用的字节数(占用3个字节)。wcstombs()wcstombs()用来将宽字符串转换为多字节字符串。它的原型定义在头文件stdlib.h。size_t wcstombs( char* mbstr, const wchar_t* wcstr, size_t count );它接受三个参数,第一个参数mbstr是目标的多字节字符串指针,第二个参数wcstr是待转换的宽字符串指针,第三个参数count是用来存储多字节字符串的最大字节数。如果转换成功,它的返回值是成功转换后的多字节字符串的字节数,不包括尾部的字符串终止符;如果转换失败,则返回-1。下面是一个例子。setlocale(LC_ALL, ""); char mbs[20]; wchar_t* wcs = L"春天"; int nBytes = 0; nBytes = wcstombs(mbs, wcs, 20); printf("%s\n", mbs); // 春天 printf("Ù375c3e5-77f2-429d-8f59-8e0aeb205b23n", nBytes); // 6上面示例中,wcstombs()将宽字符串wcs转为多字节字符串mbs,返回值6表示写入mbs的字符串占用6个字节,不包括尾部的字符串终止符。如果wcstombs()的第一个参数是 NULL,则返回转换成功所需要的目标字符串的字节数。mbstowcs()mbstowcs()用来将多字节字符串转换为宽字符串。它的原型定义在头文件stdlib.h。size_t mbstowcs( wchar_t* wcstr, const char* mbstr, size_t count );它接受三个参数,第一个参数wcstr是目标宽字符串,第二个参数mbstr是待转换的多字节字符串,第三个参数是待转换的多字节字符串的最大字符数。转换成功时,它的返回值是成功转换的多字节字符的数量;转换失败时,返回-1。如果返回值与第三个参数相同,那么转换后的宽字符串不是以 NULL 结尾的。下面是一个例子。setlocale(LC_ALL, ""); char* mbs = "天气不错"; wchar_t wcs[20]; int nBytes = 0; nBytes = mbstowcs(wcs, mbs, 20); printf("%ls\n", wcs); // 天气不错 printf("Ù375c3e5-77f2-429d-8f59-8e0aeb205b23n", nBytes); // 4上面示例中,多字节字符串mbs被mbstowcs()转为宽字符串,成功转换了4个字符,所以该函数的返回值为4。如果mbstowcs()的第一个参数为NULL,则返回目标宽字符串会包含的字符数量。原文链接:https://wangdoc.com/clang/multibyte.html
  • [C/C++] c语言预处理器介绍——转载
    简介C 语言编译器在编译程序之前,会先使用预处理器(preprocessor)处理代码。预处理器首先会清理代码,进行删除注释、多行语句合成一个逻辑行等工作。然后,执行#开头的预处理指令。本章介绍 C 语言的预处理指令。预处理指令可以出现在程序的任何地方,但是习惯上,往往放在代码的开头部分。每个预处理指令都以#开头,放在一行的行首,指令前面可以有空白字符(比如空格或制表符)。#和指令的其余部分之间也可以有空格,但是为了兼容老的编译器,一般不留空格。所有预处理指令都是一行的,除非在行尾使用反斜杠,将其折行。指令结尾处不需要分号。#define#define是最常见的预处理指令,用来将指定的词替换成另一个词。它的参数分成两个部分,第一个参数就是要被替换的部分,其余参数是替换后的内容。每条替换规则,称为一个宏(macro)。#define MAX 100上面示例中,#define指定将源码里面的MAX,全部替换成100。MAX就称为一个宏。宏的名称不允许有空格,而且必须遵守 C 语言的变量命名规则,只能使用字母、数字与下划线(_),且首字符不能是数字。宏是原样替换,指定什么内容,就一模一样替换成什么内容。#define HELLO "Hello, world" // 相当于 printf("%s", "Hello, world"); printf("%s", HELLO);上面示例中,宏HELLO会被原样替换成"Hello, world"。#define指令可以出现在源码文件的任何地方,从指令出现的地方到文件末尾都有效。习惯上,会将#define放在源码文件的头部。它的主要好处是,会使得程序的可读性更好,也更容易修改。#define指令从#开始,一直到换行符为止。如果整条指令过长,可以在折行处使用反斜杠,延续到下一行。#define OW "C programming language is invented \ in 1970s."上面示例中,第一行结尾的反斜杠将#define指令拆成两行。#define允许多重替换,即一个宏可以包含另一个宏。#define TWO 2 #define FOUR TWO*TWO上面示例中,FOUR会被替换成2*2。注意,如果宏出现在字符串里面(即出现在双引号中),或者是其他标识符的一部分,就会失效,并不会发生替换。#define TWO 2 // 输出 TWO printf("TWO\n"); // 输出 22 const TWOs = 22; printf("Õ9fd7ca2-dc9d-412c-8fb7-7e23f4c29b69n", TWOs);上面示例中,双引号里面的TWO,以及标识符TWOs,都不会被替换。同名的宏可以重复定义,只要定义是相同的,就没有问题。如果定义不同,就会报错。// 正确 #define FOO hello #define FOO hello // 报错 #define BAR hello #define BAR world上面示例中,宏FOO没有变化,所以可以重复定义,宏BAR发生了变化,就报错了。带参数的宏基本用法宏的强大之处在于,它的名称后面可以使用括号,指定接受一个或多个参数。#define SQUARE(X) X*X上面示例中,宏SQUARE可以接受一个参数X,替换成X*X。注意,宏的名称与左边圆括号之间,不能有空格。这个宏的用法如下。// 替换成 z = 2*2; z = SQUARE(2);这种写法很像函数,但又不是函数,而是完全原样的替换,会跟函数有不一样的行为。#define SQUARE(X) X*X // 输出19 printf("Õ9fd7ca2-dc9d-412c-8fb7-7e23f4c29b69n", SQUARE(3 + 4));上面示例中,SQUARE(3 + 4)如果是函数,输出的应该是49(7*7);宏是原样替换,所以替换成3 + 4*3 + 4,最后输出19。可以看到,原样替换可能导致意料之外的行为。解决办法就是在定义宏的时候,尽量多使用圆括号,这样可以避免很多意外。#define SQUARE(X) ((X) * (X))上面示例中,SQUARE(X)替换后的形式,有两层圆括号,就可以避免很多错误的发生。宏的参数也可以是空的。#define getchar() getc(stdin)上面示例中,宏getchar()的参数就是空的。这种情况其实可以省略圆括号,但是加上了,会让它看上去更像函数。一般来说,带参数的宏都是一行的。下面是两个例子。#define MAX(x, y) ((x)>(y)?(x):(y)) #define IS_EVEN(n) ((n)%2==0)如果宏的长度过长,可以使用反斜杠(\)折行,将宏写成多行。#define PRINT_NUMS_TO_PRODUCT(a, b) { \ int product = (a) * (b); \ for (int i = 0; i < product; i++) { \ printf("Õ9fd7ca2-dc9d-412c-8fb7-7e23f4c29b69n", i); \ } \ }上面示例中,替换文本放在大括号里面,这是为了创造一个块作用域,避免宏内部的变量污染外部。带参数的宏也可以嵌套,一个宏里面包含另一个宏。#define QUADP(a, b, c) ((-(b) + sqrt((b) * (b) - 4 * (a) * (c))) / (2 * (a))) #define QUADM(a, b, c) ((-(b) - sqrt((b) * (b) - 4 * (a) * (c))) / (2 * (a))) #define QUAD(a, b, c) QUADP(a, b, c), QUADM(a, b, c)上面示例是一元二次方程组求解的宏,由于存在正负两个解,所以宏QUAD先替换成另外两个宏QUADP和QUADM,后者再各自替换成一个解。那么,什么时候使用带参数的宏,什么时候使用函数呢?一般来说,应该首先使用函数,它的功能更强、更容易理解。宏有时候会产生意想不到的替换结果,而且往往只能写成一行,除非对换行符进行转义,但是可读性就变得很差。宏的优点是相对简单,本质上是字符串替换,不涉及数据类型,不像函数必须定义数据类型。而且,宏将每一处都替换成实际的代码,省掉了函数调用的开销,所以性能会好一些。另外,以前的代码大量使用宏,尤其是简单的数学运算,为了读懂前人的代码,需要对它有所了解。#运算符,##运算符由于宏不涉及数据类型,所以替换以后可能为各种类型的值。如果希望替换后的值为字符串,可以在替换文本的参数前面加上#。#define STR(x) #x // 等同于 printf("%s\n", "3.14159"); printf("%s\n", STR(3.14159));上面示例中,STR(3.14159)会被替换成3.14159。如果x前面没有#,这会被解释成一个浮点数,有了#以后,就会被转换成字符串。下面是另一个例子。#define XNAME(n) "x"#n // 输出 x4 printf("%s\n", XNAME(4));上面示例中,#n指定参数输出为字符串,再跟前面的字符串结合,最终输出为"x4"。如果不加#,这里实现起来就很麻烦了。如果替换后的文本里面,参数需要跟其他标识符连在一起,组成一个新的标识符,可以使用##运算符。它起到粘合作用,将参数“嵌入”一个标识符之中。#define MK_ID(n) i##n上面示例中,n是宏MK_ID的参数,这个参数需要跟标识符i粘合在一起,这时i和n之间就要使用##运算符。下面是这个宏的用法示例。int MK_ID(1), MK_ID(2), MK_ID(3); // 替换成 int i1, i2, i3;上面示例中,替换后的文本i1、i2、i3是三个标识符,参数n是标识符的一部分。从这个例子可以看到,##运算符的一个主要用途是批量生成变量名和标识符。不定参数的宏宏的参数还可以是不定数量的(即不确定有多少个参数),...表示剩余的参数。#define X(a, b, ...) (10*(a) + 20*(b)), __VA_ARGS__上面示例中,X(a, b, ...)表示X()至少有两个参数,多余的参数使用...表示。在替换文本中,__VA_ARGS__代表多余的参数(每个参数之间使用逗号分隔)。下面是用法示例。X(5, 4, 3.14, "Hi!", 12) // 替换成 (10*(5) + 20*(4)), 3.14, "Hi!", 12注意,...只能替代宏的尾部参数,不能写成下面这样。// 报错 #define WRONG(X, ..., Y) #X #__VA_ARGS__ #Y上面示例中,...替代中间部分的参数,这是不允许的,会报错。__VA_ARGS__前面加上一个#号,可以让输出变成一个字符串。#define X(...) #__VA_ARGS__ printf("%s\n", X(1,2,3)); // Prints "1, 2, 3"#undef#undef指令用来取消已经使用#define定义的宏。#define LIMIT 400 #undef LIMIT上面示例的undef指令取消已经定义的宏LIMIT,后面就可以重新用 LIMIT 定义一个宏。有时候想重新定义一个宏,但不确定是否以前定义过,就可以先用#undef取消,然后再定义。因为同名的宏如果两次定义不一样,会报错,而#undef的参数如果是不存在的宏,并不会报错。GCC 的-U选项可以在命令行取消宏的定义,相当于#undef。$ gcc -ULIMIT foo.c上面示例中的-U参数,取消了宏LIMIT,相当于源文件里面的#undef LIMIT。#include#include指令用于编译时将其他源码文件,加载进入当前文件。它有两种形式。// 形式一 #include // 加载系统提供的文件 // 形式二 #include "foo.h" // 加载用户提供的文件形式一,文件名写在尖括号里面,表示该文件是系统提供的,通常是标准库的库文件,不需要写路径。因为编译器会到系统指定的安装目录里面,去寻找这些文件。形式二,文件名写在双引号里面,表示该文件由用户提供,具体的路径取决于编译器的设置,可能是当前目录,也可能是项目的工作目录。如果所要包含的文件在其他位置,就需要指定路径,下面是一个例子。#include "/usr/local/lib/foo.h"GCC 编译器的-I参数,也可以用来指定include命令中用户文件的加载路径。$ gcc -Iinclude/ -o code code.c上面命令中,-Iinclude/指定从当前目录的include子目录里面,加载用户自己的文件。#include最常见的用途,就是用来加载包含函数原型的头文件(后缀名为.h),参见《多文件编译》一章。多个#include指令的顺序无关紧要,多次包含同一个头文件也是合法的。#if...#endif#if...#endif指令用于预处理器的条件判断,满足条件时,内部的行会被编译,否则就被编译器忽略。#if 0 const double pi = 3.1415; // 不会执行 #endif上面示例中,#if后面的0,表示判断条件不成立。所以,内部的变量定义语句会被编译器忽略。#if 0这种写法常用来当作注释使用,不需要的代码就放在#if 0里面。#if后面的判断条件,通常是一个表达式。如果表达式的值不等于0,就表示判断条件为真,编译内部的语句;如果表达式的值等于0,表示判断条件为伪,则忽略内部的语句。#if...#endif之间还可以加入#else指令,用于指定判断条件不成立时,需要编译的语句。#define FOO 1 #if FOO printf("defined\n"); #else printf("not defined\n"); #endif上面示例中,宏FOO如果定义过,会被替换成1,从而输出defined,否则输出not defined。如果有多个判断条件,还可以加入#elif命令。#if HAPPY_FACTOR == 0 printf("I'm not happy!\n"); #elif HAPPY_FACTOR == 1 printf("I'm just regular\n"); #else printf("I'm extra happy!\n"); #endif上面示例中,通过#elif指定了第二重判断。注意,#elif的位置必须在#else之前。如果多个判断条件皆不满足,则执行#else的部分。没有定义过的宏,等同于0。因此如果UNDEFINED是一个没有定义过的宏,那么#if UNDEFINED为伪,而#if !UNDEFINED为真。#if的常见应用就是打开(或关闭)调试模式。#define DEBUG 1 #if DEBUG printf("value of i : Õ9fd7ca2-dc9d-412c-8fb7-7e23f4c29b69n", i); printf("value of j : Õ9fd7ca2-dc9d-412c-8fb7-7e23f4c29b69n", j); #endif上面示例中,通过将DEBUG设为1,就打开了调试模式,可以输出调试信息。GCC 的-D参数可以在编译时指定宏的值,因此可以很方便地打开调试开关。$ gcc -DDEBUG=1 foo.c上面示例中,-D参数指定宏DEBUG为1,相当于在代码中指定#define DEBUG 1。#ifdef...#endif#ifdef...#endif指令用于判断某个宏是否定义过。有时源码文件可能会重复加载某个库,为了避免这种情况,可以在库文件里使用#define定义一个空的宏。通过这个宏,判断库文件是否被加载了。#define EXTRA_HAPPY上面示例中,EXTRA_HAPPY就是一个空的宏。然后,源码文件使用#ifdef...#endif检查这个宏是否定义过。#ifdef EXTRA_HAPPY printf("I'm extra happy!\n"); #endif上面示例中,#ifdef检查宏EXTRA_HAPPY是否定义过。如果已经存在,表示加载过库文件,就会打印一行提示。#ifdef可以与#else指令配合使用。#ifdef EXTRA_HAPPY printf("I'm extra happy!\n"); #else printf("I'm just regular\n"); #endif上面示例中,如果宏EXTRA_HAPPY没有定义过,就会执行#else的部分。#ifdef...#else...#endif可以用来实现条件加载。#ifdef MAVIS #include "foo.h" #define STABLES 1 #else #include "bar.h" #define STABLES 2 #endif上面示例中,通过判断宏MAVIS是否定义过,实现加载不同的头文件。defined 运算符上一节的#ifdef指令,等同于#if defined。#ifdef FOO // 等同于 #if defined FOO上面示例中,defined是一个预处理运算符,如果它的参数是一个定义过的宏,就会返回1,否则返回0。使用这种语法,可以完成多重判断。#if defined FOO x = 2; #elif defined BAR x = 3; #endif这个运算符的一个应用,就是对于不同架构的系统,加载不同的头文件。#if defined IBMPC #include "ibmpc.h" #elif defined MAC #include "mac.h" #else #include "general.h" #endif上面示例中,不同架构的系统需要定义对应的宏。代码根据不同的宏,加载对应的头文件。#ifndef...#endif#ifndef...#endif指令跟#ifdef...#endif正好相反。它用来判断,如果某个宏没有被定义过,则执行指定的操作。#ifdef EXTRA_HAPPY printf("I'm extra happy!\n"); #endif #ifndef EXTRA_HAPPY printf("I'm just regular\n"); #endif上面示例中,针对宏EXTRA_HAPPY是否被定义过,#ifdef和#ifndef分别指定了两种情况各自需要编译的代码。#ifndef常用于防止重复加载。举例来说,为了防止头文件myheader.h被重复加载,可以把它放在#ifndef...#endif里面加载。#ifndef MYHEADER_H #define MYHEADER_H #include "myheader.h" #endif上面示例中,宏MYHEADER_H对应文件名myheader.h的大写。只要#ifndef发现这个宏没有被定义过,就说明该头文件没有加载过,从而加载内部的代码,并会定义宏MYHEADER_H,防止被再次加载。#ifndef等同于#if !defined。#ifndef FOO // 等同于 #if !defined FOO预定义宏C 语言提供一些预定义的宏,可以直接使用。__DATE__:编译日期,格式为“Mmm dd yyyy”的字符串(比如 Nov 23 2021)。__TIME__:编译时间,格式为“hh:mm:ss”。__FILE__:当前文件名。__LINE__:当前行号。__func__:当前正在执行的函数名。该预定义宏必须在函数作用域使用。__STDC__:如果被设为1,表示当前编译器遵循 C 标准。__STDC_HOSTED__:如果被设为1,表示当前编译器可以提供完整的标准库;否则被设为0(嵌入式系统的标准库常常是不完整的)。__STDC_VERSION__:编译所使用的 C 语言版本,是一个格式为yyyymmL的长整数,C99 版本为“199901L”,C11 版本为“201112L”,C17 版本为“201710L”。下面示例打印这些预定义宏的值。#include int main(void) { printf("This function: %s\n", __func__); printf("This file: %s\n", __FILE__); printf("This line: Õ9fd7ca2-dc9d-412c-8fb7-7e23f4c29b69n", __LINE__); printf("Compiled on: %s %s\n", __DATE__, __TIME__); printf("C Version: %ld\n", __STDC_VERSION__); } /* 输出如下 This function: main This file: test.c This line: 7 Compiled on: Mar 29 2021 19:19:37 C Version: 201710 */#line#line指令用于覆盖预定义宏__LINE__,将其改为自定义的行号。后面的行将从__LINE__的新值开始计数。// 将下一行的行号重置为 300 #line 300上面示例中,紧跟在#line 300后面一行的行号,将被改成300,其后的行会在300的基础上递增编号。#line还可以改掉预定义宏__FILE__,将其改为自定义的文件名。#line 300 "newfilename"上面示例中,下一行的行号重置为300,文件名重置为newfilename。#error#error指令用于让预处理器抛出一个错误,终止编译。#if __STDC_VERSION__ != 201112L #error Not C11 #endif上面示例指定,如果编译器不使用 C11 标准,就中止编译。GCC 编译器会像下面这样报错。$ gcc -std=c99 newish.c newish.c:14:2: error: #error Not C11上面示例中,GCC 使用 C99 标准编译,就报错了。#if INT_MAX < 100000 #error int type is too small #endif上面示例中,编译器一旦发现INT类型的最大值小于100,000,就会停止编译。#error指令也可以用在#if...#elif...#else的部分。#if defined WIN32 // ... #elif defined MAC_OS // ... #elif defined LINUX // ... #else #error NOT support the operating system #endif#pragma#pragma指令用来修改编译器属性。// 使用 C99 标准 #pragma c9x on上面示例让编译器以 C99 标准进行编译。原文链接:https://wangdoc.com/clang/preprocessor.html
  • [技术干货] 标准库stdlib.h讲解——转载
    类型别名和宏stdlib.h 定义了下面的类型别名。size_t:sizeof 的返回类型。wchar_t:宽字符类型。stdlib.h 定义了下面的宏。NULL:空指针。EXIT_SUCCESS:函数运行成功时的退出状态。EXIT_FAILURE:函数运行错误时的退出状态。RAND_MAX:rand() 函数可以返回的最大值。MB_CUR_MAX:当前语言环境中,多字节字符占用的最大字节数。abs(),labs(),llabs()这三个函数用于计算整数的绝对值。abs()用于 int 类型,labs()用于 long int 类型,llabs()用于 long long int 类型。int abs(int j); long int labs(long int j); long long int llabs(long long int j);下面是用法示例。// 输出 |-2| = 2 printf("|-2| = Þb90b4ab-7762-4b3e-babc-c84153e209cen", abs(-2)); // 输出 |4| = 4 printf("|4| = Þb90b4ab-7762-4b3e-babc-c84153e209cen", abs(4));div(),ldiv(),lldiv()这三个函数用来计算两个参数的商和余数。div()用于 int 类型的相除,ldiv()用于 long int 类型的相除,lldiv()用于 long long int 类型的相除。div_t div(int numer, int denom); ldiv_t ldiv(long int numer, long int denom); lldiv_t lldiv(long long int numer, long long int denom);这些函数把第2个参数(分母)除以第1个参数(分子),产生商和余数。这两个值通过一个数据结构返回,div()返回 div_t 结构,ldiv()返回 ldiv_t 结构,lldiv()返回 lldiv_t 结构。这些结构都包含下面两个字段,int quot;  // 商 int rem;  // 余数它们完整的定义如下。typedef struct { int quot, rem; } div_t; typedef struct { long int quot, rem; } ldiv_t; typedef struct { long long int quot, rem; } lldiv_t;下面是一个例子。div_t d = div(64, -7); // 输出 64 / -7 = -9 printf("64 / -7 = Þb90b4ab-7762-4b3e-babc-c84153e209cen", d.quot); // 输出 64 % -7 = 1 printf("64 %% -7 = Þb90b4ab-7762-4b3e-babc-c84153e209cen", d.rem);字符串转成数值a 系列函数stdlib.h定义了一系列函数,可以将字符串转为数字。atoi():字符串转成 int 类型。atof():字符串转成 double 类型。atol():字符串转成 long int 类型。atoll():字符串转成 long long int 类型。它们的原型如下。int atoi(const char* nptr); double atof(const char* nptr); long int atol(const char* nptr); long long int atoll(const char* nptr);上面函数的参数都是一个字符串指针,字符串开头的空格会被忽略,转换到第一个无效字符处停止。函数名称里面的a代表 ASCII,所以atoi()的意思是“ASCII to int”。它们返回转换后的数值,如果字符串无法转换,则返回0。下面是用法示例。atoi("3490") // 3490 atof("3.141593") // 3.141593如果参数是数字开头的字符串,atoi()会只转换数字部分,比如atoi("42regular")会返回整数42。如果首字符不是数字,比如“hello world”,则会返回0。str 系列函数(浮点数转换)stdlib.h还定义了一些更强功能的浮点数转换函数。strtof():字符串转成 float 类型。strtod():字符串转成 double 类型。strtold():字符串转成 long double 类型。它们的原型如下。float strtof( const char* restrict nptr, char** restrict endptr ); double strtod( const char* restrict nptr, char** restrict endptr ); long double strtold( const char* restrict nptr, char** restrict endptr );它们都接受两个参数,第一个参数是需要转换的字符串,第二个参数是一个指针,指向原始字符串里面无法转换的部分。nptr:待转换的字符串(起首的空白字符会被忽略)。endprt:一个指针,指向不能转换部分的第一个字符。如果字符串可以完全转成数值,该指针指向字符串末尾的终止符\0。这个参数如果设为 NULL,就表示不需要处理字符串剩余部分。它们的返回值是已经转换后的数值。如果字符串无法转换,则返回0。如果转换结果发生溢出,errno 会被设置为 ERANGE。如果值太大(无论是正数还是负数),函数返回HUGE_VAL;如果值太小,函数返回零。char *inp = " 123.4567abdc"; char *badchar; double val = strtod(inp, &badchar); printf("þb90b4ab-7762-4b3e-babc-c84153e209cen", val); // 123.456700 printf("%s\n", badchar); // abdc字符串可以完全转换的情况下,第二个参数指向\0,因此可以用下面的写法判断是否完全转换。if (*endptr == '\0') { // 完全转换 } else { // 存在无法转换的字符 }如果不关心没有转换的部分,则可以将 endptr 设置为 NULL。这些函数还可以将字符串转换为特殊值 Infinity 和 NaN。如果字符串包含 INF 或 INFINITY(大写或小写皆可),则将转换为 Infinity;如果字符串包含 NAN,则将返回 NaN。str 系列函数(整数转换)str 系列函数也有整数转换的对应函数。strtol():字符串转成 long int 类型。strtoll():字符串转成 long long int 类型。strtoul():字符串转成 unsigned long int 类型。strtoull():字符串转成 unsigned long long int 类型。它们的原型如下。long int strtol( const char* restrict nptr, char** restrict endptr, int base ); long long int strtoll( const char* restrict nptr, char** restrict endptr, int base ); unsigned long int strtoul( const char* restrict nptr, char** restrict endptr, int base ); unsigned long long int strtoull( const char* restrict nptr, char** restrict endptr, int base );它们接受三个参数。(1)nPtr:待转换的字符串(起首的空白字符会被忽略)。(2)endPrt:一个指针,指向不能转换部分的第一个字符。如果字符串可以完全转成数值,该指针指向字符串末尾的终止符\0。这个参数如果设为 NULL,就表示不需要处理字符串剩余部分。(3)base:待转换整数的进制。这个值应该是2到36之间的整数,代表相应的进制,如果是特殊值0,表示让函数根据数值的前缀,自己确定进制,即如果数字有前缀0,则为八进制,如果数字有前缀0x或0X,则为十六进制。它们的返回值是转换后的数值,如果转换不成功,返回0。下面是转换十进制整数的例子。char* s = "3490"; unsigned long int x = strtoul(u, NULL, 10); printf("%lu\n", x); // 3490下面是转换十六进制整数的例子。char* end; long value = strtol("0xff", &end, 16); printf("%ld\n", value); // 255 printf("%s\n", end); // 无内容 value = strtol("0xffxx", &end, 16); printf("%ld\n", value); // 255 printf("%s\n", end); // xx上面示例中,strtol()可以指定字符串包含的是16进制整数。不能转换的部分,可以使用指针end进行访问。下面是转换二进制整数的例子。char* s = "101010"; unsigned long int x = strtoul(s, NULL, 2); printf("%lu\n", x); // 42下面是让函数自行判断整数进制的例子。#include #include int main(void) { const char* string = "-1234567abc"; char* remainderPtr; long x = strtol(string, &remainderPtr, 0); printf("%s\"%s\"\n%s%ld\n%s\"%s\"\n", "The original string is ", string, "The converted value is ", x, "The remainder of the original string is ", remainderPtr ); }上面代码的输出结果如下。The original string is "-1234567abc" The converted value is -1234567 The remainder of the original string is "abc"如果被转换的值太大,strtol()函数在errno中存储ERANGE这个值,并返回LONG_MIN(原值为负数)或LONG_MAX(原值为正数),strtoul()则返回ULONG_MAX。rand()rand()函数用来生成 0~RAND_MAX 之间的随机整数。RAND_MAX是一个定义在stdlib.h里面的宏,通常等于 INT_MAX。// 原型 int rand(void); // 示例 int x = rand();如果希望获得整数 N 到 M 之间的随机数(包括 N 和 M 两个端点值),可以使用下面的写法。int x = rand() % (M - N + 1) + N;比如,1 到 6 之间的随机数,写法如下。int x = rand() % 6 + 1;获得浮点数的随机值,可以使用下面的写法。// 0 到 0.999999 之间的随机数 printf("0 to 0.99999: þb90b4ab-7762-4b3e-babc-c84153e209cen", rand() / ((float)RAND_MAX + 1)); // n 到 m 之间的随机数: // n + m * (rand() / (float)RAND_MAX) printf("10.5 to 15.7: þb90b4ab-7762-4b3e-babc-c84153e209cen", 10.5 + 5.2 * rand() / (float)RAND_MAX);上面示例中,由于rand()和RAND_MAX都是 int 类型,要用显示的类型转换转为浮点数。srand()rand()是伪随机数函数,为了增加随机性,必须在调用它之前,使用srand()函数重置一下种子值。srand()函数接受一个无符号整数(unsigned int)作为种子值,没有返回值。void srand(unsigned int seed);通常使用time(NULL)函数返回当前距离时间纪元的秒数,作为srand()的参数。#include srand((unsigned int) time(NULL));上面代码中,time()的原型定义在头文件time.h里面,返回值的类型是类型别名time_t,具体的类型与系统有关,所以要强制转换一下类型。time()的参数是一个指针,指向一个具体的 time_t 类型的时间值,这里传入空指针NULL作为参数,由于 NULL 一般是0,所以也可以写成time(0)。abort()abort()用于不正常地终止一个正在执行的程序。使用这个函数的目的,主要是它会触发 SIGABRT 信号,开发者可以在程序中为这个信号设置一个处理函数。void abort(void);该函数没有参数。exit(),quick_exit(),_Exit()这三个函数都用来退出当前正在执行的程序。void exit(int status); void quick_exit(int status); void _Exit(int status);它们都接受一个整数,表示程序的退出状态,0是正常退出,非零值表示发生错误,可以使用宏EXIT_SUCCESS和EXIT_FAILURE当作参数。它们本身没有返回值。它们的区别是,退出时所做的清理工作不同。exit()是正常退出,系统会做完整的清理,比如更新所有文件流,并且删除临时文件。quick_exit()是快速退出,系统的清理工作稍微少一点。_Exit()是立即退出,不做任何清理工作。下面是一些用法示例。exit(EXIT_SUCCESS); quick_exit(EXIT_FAILURE); _Exit(2);atexit(),at_quick_exit()atexit()用来登记当前程序退出时(调用exit()或main()正常退出),所要执行的其他函数。at_quick_exit()则是登记使用quick_exit()方法退出当前程序时,所要执行的其他函数。exit()只能触发atexit()登记的函数,quick_exit()只能触发at_quick_exit()登记的函数。int atexit(void (*func)(void)); int at_quick_exit(void (*func)(void));它们的参数是要执行的函数地址,即函数名。它们的返回值都是调用成功时返回0,调用失败时返回非零值。下面是一个例子。void sign_off(void); void too_bad(void); int main(void) { int n; atexit(sign_off);   /* 注册 sign_off()函数 */ puts("Enter an integer:"); if (scanf("%d", &n) != 1) { puts("That's no integer!"); atexit(too_bad); /* 注册 too_bad()函数 */ exit(EXIT_FAILURE); } printf("%d is %s.\n", n, (n % 2 == 0) ? "even" : "odd"); return 0; } void sign_off(void) { puts("sign_off"); } void too_bad(void) { puts("too bad"); }上面示例中,用户输入失败时,会调用sign_off()和too_bad()函数;但是输入成功时只会调用sign_off()。因为只有输入失败时,才会进入if语句登记too_bad()。另外,如果有多条atexit()语句,函数退出时最先调用的,是最后一个登记的函数。atexit()登记的函数(如上例的sign_off和too_bad)应该不带任何参数且返回类型为void。通常,这些函数会执行一些清理任务,例如删除临时文件或重置环境变量。at_quick_exit()也是同样的规则,下面是一个例子。void exit_handler_1(void) { printf("1\n"); } void exit_handler_2(void) { printf("2\n"); } int main(void) { at_quick_exit(exit_handler_1); at_quick_exit(exit_handler_2); quick_exit(0); }执行上面的示例,命令行会先输出2,再输出1。getenv()getenv()用于获取环境变量的值。环境变量是操作系统提供的程序之外的一些环境参数。char* getenv(const char* name);它的参数是一个字符串,表示环境变量名。返回值也是一个字符串,表示环境变量的值。如果指定的环境变量不存在,则返回 NULL。下面是输出环境变量$PATH的值的例子。printf("PATH is %s\n", getenv("PATH"));system()system()函数用于执行外部程序。它会把它的参数字符串传递给操作系统,让操作系统的命令处理器来执行。void system( char const * command );这个函数的返回值因编译器而异。但是标准规定,如果 NULL 作为参数,表示询问操作系统,是否有可用的命令处理器,如果有的话,返回一个非零值,否则返回零。下面是执行ls命令的例子。system("ls -l"); 内存管理函数stdlib.h 提供了一些内存操作函数,下面几个函数详见《内存管理》一章,其余在本节介绍。malloc():分配内存区域calloc():分配内存区域。realloc():调节内存区域大小。free():释放内存区域。aligned_alloc()很多系统有内存对齐的要求,即内存块的大小必须是某个值(比如64字节)的倍数,这样有利于提高处理速度。aligned_alloc()就用于分配满足内存对齐要求的内存块,它的原型如下。void* aligned_alloc(size_t alignment, size_t size);它接受两个参数。alignment:整数,表示内存对齐的单位大小,一般是2的整数次幂(2、4、8、16……)。size:整数,表示内存块的大小。分配成功时,它返回一个无类型指针,指向新分配的内存块。分配失败时,返回 NULL。char* p = aligned_alloc(64, 256);上面示例中,aligned_alloc()分配的内存块,单位大小是64字节,要分配的字节数是256字节。qsort()qsort()用来快速排序一个数组。它对数组成员的类型没有要求,任何类型数组都可以用这个函数排序。void qsort( void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *) );该函数接受四个参数。base:指向要排序的数组开始位置的指针。nmemb:数组成员的数量。size:数组每个成员占用的字节长度。compar:一个函数指针,指向一个比较两个成员的函数。比较函数compar将指向数组两个成员的指针作为参数,并比较两个成员。如果第一个参数小于第二个参数,该函数应该返回一个负值;如果两个函数相等,返回0;如果第一个参数大于第二个参数,应该返回一个正数。下面是一个用法示例。#include #include int compar(const void* elem0, const void* elem1) { const int* x = elem0; const int* y = elem1; return *x - *y; } int main(void) { int a[9] = {14, 2, 3, 17, 10, 8, 6, 1, 13}; qsort(a, 9, sizeof(int), compar); for (int i = 0; i < 9; i++) printf("%d ", a[i]); putchar('\n'); }执行上面示例,会输出排序好的数组“1 2 3 6 8 10 13 14 17”。bsearch()bsearch()使用二分法搜索,在数组中搜索一个值。它对数组成员的类型没有要求,任何类型数组都可以用这个函数搜索值。注意,该方法只对已经排序好的数组有效。void *bsearch( const void* key, const void* base, size_t nmemb, size_t size, int (*compar)(const void *, const void *) );这个函数接受5个参数。key:指向要查找的值的指针。base:指向数组开始位置的指针,数组必须已经排序。nmemb:数组成员的数量。size:数组每个成员占用的字节长度。compar:指向一个将待查找值与其他值进行比较的函数的指针。比较函数compar将待查找的值作为第一个参数,将要比较的值作为第二个参数。如果第一个参数小于第二个参数,该函数应该返回一个负值;如果两个参数相等,返回0;如果第一个参数大于第二个参数,返回一个正值。如果找到待查找的值,bsearch()返回指向该值的指针,如果找不到,返回 NULL。下面是一个用法示例。#include #include int compar(const void *key, const void *value) { const int* k = key; const int* v = value; return *k - *v; } int main(void) { int a[9] = {2, 6, 9, 12, 13, 18, 20, 32, 47}; int* r; int key; key = 12; // 包括在数组中 r = bsearch(&key, a, 9, sizeof(int), compar); printf("Found Þb90b4ab-7762-4b3e-babc-c84153e209cen", *r); key = 30; // 不包括在数组中 r = bsearch(&key, a, 9, sizeof(int), compar); if (r == NULL) printf("Didn't find 30\n"); return 0; }执行上面的示例,会输出下面的结果。Found 12 Didn't find 30多字节字符函数stdlib.h 提供了下面的函数,用来操作多字节字符,详见《多字节字符》一章。mblen():多字节字符的字节长度。mbtowc():将多字节字符转换为宽字符。wctomb():将宽字符转换为多字节字符。mbstowcs():将多字节字符串转换为宽字符串。wcstombs():将宽字符串转换为多字节字符串。原文链接:https://wangdoc.com/clang/lib/stdlib.h.html
  • [C/C++] 浮点数计算误差分析——转载
    为什么在程序中浮点计算存在误差?一.什么是浮点数计算机定义了两种小数,分别为定点数和浮点数。其中,定点数的小数点位置是固定的,在确定字长的系统中 一旦指定小数点的位置后,它的整数部分和小数部分也随之确定。二者之间独立表示,互不干扰。由于小数点位置是固定的,所以定点数能够表示的范围非常有限。应用不广泛,目前应用更广、更加复杂的是浮点数。它是采用科学计数法来表示的,由符号位、有效数字、指数三部分组成。使用浮点数存储和计算的场景无处不在,若使用不当则容易造成计算值与理论值不一致。二.科学计数法数学中的科学计数法,大家都很熟悉,就是如下图中所展示的概念:但是浮点数是如何用二进制数表示符号、指数和有效数字?当前业界流行的浮点数标准是 IEEE754 该标准规定了浮点数类型单精度、双精度、延伸单精度、延伸双精度。前两种类型是最常用的,它们的取值范围如下图所示:因为浮点数无法表示零值,所以取值范围分为两个区间:正数区间和负数区间。 下面将着重分析单精度浮点数,而双精度浮点数与其相比只是位数不同而已,完全可以触类旁通 本节不再展开。以单精度类型为例,它被分配了4个字节,总共 32 位, 具体格式见下图:从数学世界的科学计数法映射到计算机世界的浮点数时,数制从十进制改为二进制,还要考虑内存硬件设备的实现方式。在规格化表示上存在差异,称谓上也有所改变,指数称为 “阶码”,有效数字称为“尾数”。1. 符号位 在最高二进制位上分配1位表示浮点数的符号, 0表示正数, 1表示负数;2. 阶码位(指数) 在符号位右侧分配8位用来存储指数;3. 尾数位(有效数字) 最右侧分配连续的 23 位用来存储有效数字。三.单精度数的有效位数float的有效位数是6-7位;为什么呢?由单精度数在计算机中的存储可知,位数部分(有效数字)只有23位,所以最大的表示数为1*2^23=8388608,一共7位, 但能绝对保证精确的为6位(最后1位受计算方式的影响,比如四舍五入)四.浮点计算误差产生的原因1.将十进制数转为二进制,在计算机运行中本就存在误差来看一个例子:将十进制的0.2转化为二进制,按照乘二取整法0.2*2 = 0.4 00.4*2 = 0.8 00.8*2 = 1.6 10.6*2 = 1.2 1… … …0.2*2 = 0.4 0很明显,已经进入了无限循环,受有效位数23位的影响,也就是说,存入计算机中的十进制数不是精准的。2.计算机中的计算法则产生误差两浮点数X,Y进行加减运算时,必须按以下几步执行:(1)对阶,使两数的小数点位置对齐,小的阶码向大的阶码看齐。(2)尾数求和,将对阶后的两尾数按定点加减运算规则求和(差)。(3)规格化,为增加有效数字的位数,提高运算精度,必须将求和(差)后的尾数规格化。(4)舍入,为提高精度,要考虑尾数右移时丢失的数值位。(5)判断结果,即判断结果是否溢出。关键就在与对阶这一步骤,由于float的有效位数只有7位有效数字,如果一个大数和一个小数相加时,会产生很大的误差,因为尾数得截掉好多位。例如:123 + 0.00023456= 1.23*10^2 + 0.000002 * 10^2 = 123.0002 = 1.230002*10^2在运算过程之中,对阶会造成误差那么此时就会产生0.00003456的误差,如果累加多次,则误差就会进一步加大。五.总结:浮点数计算存在误差是由两种原因造成的:1.计算机中存储的小数,本身就是近似值;2.在运算过程之中,对阶会造成误差。六.一些可采取的措施在要求绝对精确表示的业务场景下,比如金融行业的货币表示,推荐使用整型存储其最小单位的值,展示时可以转换成该货币的常用单位,比如人民币使用分存储,美元使用美分存储。在要求精确表示小数点位的业务场景下,比如圆周率要求存储小数点后 1000 位数字,使用单精度和双精度浮点数类型保存是难以做到的,这时推荐采用数组保存小数部分的数据。在比较浮点数时,由于存在误差,往往会出现意料之外的结果,所以禁止通过判断两个浮点数是否相等来控制某些业务流程。在数据库中保存小数时,推荐使用 decimal 类型,禁止使用 float 类型和 double 类型。原文链接:https://bbs.csdn.net/topics/392570902
  • [技术干货] 植物大战 模板——C++-转载
     一、泛型编程 概念: 编写与类型无关的通用代码,达成代码复用,模板是泛型编程的基础。  模板就是把工作交给编译器去做。让编译器去生成多个函数,省的我们再去写函数模板。比如Add加法函数。  平时经常用的是函数模板和类模板  二、函数模板 函数模板格式:  template 1 这里需要有个感性的认知:  1.一个模板参数只能定义一个函数。模板参数可以有缺省参数。  2.模板参数是类型。函数参数是对象。 模板参数传递的是类型,函数参数传递的是对象值。  3.普通函数是有地址的,而模板函数没有地址。 但是模板会推算,会通过实参传递给形参,推算他的实际类型。  template void Swap(T& left, T& right) {     T temp = left;     left = right;     right = temp; } 1.函数模板的实例化 实例化:用不同类型的参数使用函数模板时,称为函数模板的实例化 但是有时候也有例外。需要显式实例化。 代码如下  template T* func(int n) {     return new T[n]; } int main() {     int* p = func(10);     return 0; } typename是用来定义模板参数的关键字。也可以用class.但是不能用struct。  三、类模板 1.类模板和函数模板不同。函数模板一般可以显式传参推出实际类型。  2.而类模板不能传参推断类型,所以类模板需要在类名的 后面加尖括号<>里面加类型,这叫做类模板的显式实例化  例如:  vector v1;//int类型 vector v2;//元素是double类型1.类模板的定义格式 注意: cao不是具体的一个类,而是编译器根据被实例化的类型生成具体类的模具。  template class 类模板名 {     //类内成员定义 }; //类模板 template class cao {  }; //类模板的实例化 cao c; //cao类名,cao才是类型 四、非类型模板参数 概念:非类型形参。就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用  1.非类型的模板参数无法修改 2.浮点数,类对象以及字符串无法做非类型模板参数 (非类型模板参数一般是整形) 3.非类型的模板参数必须在编译期间就能确认结果。  N就是非类型形参。  template 1 五、模板的特化 使用模板可以实现与类型无关的代码。但有时候还需要做一些特殊处理。  1.函数模板的特化 特化步骤 1.必须要先有一个基础的函数模板 2.关键字template后面接一对空的尖括号<> 3.函数名后跟一对尖括号,尖括号中指定需要特化的类型。 4.函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪错误  struct Date {     int _year = 1;     int _month = 1;     int _day = 1; }; //基础的函数模板 template bool IsEqual(T left, T right) {     return left == right; } //关键字template后面接一对空的尖括号<> template<> //函数名后跟一对尖括号,尖括号中指定需要特化的类型 //函数形参表:必须要和模板函数的基础参数类型完全相同 bool IsEqual(Date* left, Date* right) {     return left->_year == right->_year         && left->_month == right->_month         && left->_day == right->_day; }  int main() {     cout << IsEqual(1, 2) << endl;      Date* p1 = new Date;     Date* p2 = new Date;     cout << IsEqual(p1, p2) << endl;     return 0; }  2.类模板的特化 全特化 全特化就指的是模板参数列表中所有的参数都确定化。  //全特化 template class Data { public:     Data()     {         cout << "Data" << endl;     } private:     T1 _d1;     T2 _d2; };  //跟一对尖括号<> template<> class Data { public:     Data()     {         cout << "Data" << endl;     } private:     int _d1;     char _d2; };  int main() {     Data d1;     //模板的全特化     Data d2; }  偏特化 偏特化是针对模板参数进一步进行条件限制设计的特化版本。  //类的基础模板 template class Data { public:     Data()     {         cout << "Data" << endl;     } private:     T1 _d1;     T2 _d2; };  //1.部分特化 //将模板参数列表中的一部分参数特化。  template  class Data { public:     Data()     {         cout << "Data" << endl;     } private:     T1 _d1;     int _d2; };  //两个参数偏特化为引用类型 template  class Data  { public:     Data() { cout << "Data" << endl; } private:     T1 _d1;     T2 _d2; };  //两个参数偏特化为引用类型 template  class Data  { public:     Data(const T1& d1, const T2& d2)         :_d1(d1)         , _d2(d2)     {         cout << "Data" << endl;     } private:     const T1& _d1;     const T2& _d2; };  int main() {     Data d1;     Data d2;     Data d3;     Data d4(1,2); } 六、模板分离编译(重点) 1.什么是分离编译 ? 一个程序由若干个源文件组成。而每个源文件单独编译成目标文件,然后目标文件链接起来形成可执行文件的过程称为分离编译。  模板一般不支持分离编译。但普通函数是可以的。  C/C++程序要运行,基本步骤. 预处理 -> 编译->汇编->链接  2.模板的声明和定义 函数模板的声明和定义也有一些讲就。 声明:  template void Swap(T& left, T& right); 定义:  template void Swap(T& left, T& right) {     T temp = left;     left = right;     right = temp; }  注意:类模板的定义还需要加上类域。  3.模板的分离编译原理 a.cpp  a.h  main.cpp  a.h头文件不参与编译。 a.cpp中不会生成模板函数的实例化, main.cpp中调用函数链接时找地址。main.cpp包含的头文件只有a.h的。但是a.cpp中没有实例化所以没有地址。  解决方法: 1.将声明和定义放到一个文件.hpp的文件中。 2.模板定义的位置显式实例化。(不推荐使用).  模板的优点:  1.模板复用了代码,节省了资源。STL标准模板库因此而产生。 2.增强了代码的灵活性。  缺点: 1.模板会导致代码膨胀,也会导致编译时间变长。 2.出现模板编译错误时,错误信息非常凌乱,不易定位错误。  为什么分离就链接不上? 符号表找不到。 和实例化有关系。  a.cpp 从预处理到 a.i经过了头文件的展开。 a.i经编译到a.s再经过汇编到a.o什么都没干,因为模板的类型没有确定,所以没法实例化。a.s和a.o都是空的,空壳子。  解决办法: 1.显示实例化,太矬了,几乎不用这个办法。 2.不分离到两个文件中,放到同一个文件中。这样为什么就可以了呢? 为什么就不存在链接错误了?**原因是因为在main.cpp中头文件展开后,有了函数模板的声明和定义。**在链接的时候就不用找他的地址了。  建群初衷为交流学习,切勿互吹 ———————————————— 版权声明:本文为CSDN博主「乔 巴」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq2466200050/article/details/127015025