深入解析:C# 串口通信全解析:从基础到复杂协议的设计思路

news/2025/9/28 21:25:37/文章来源:https://www.cnblogs.com/ljbguanli/p/19117640

深入解析:C# 串口通信全解析:从基础到复杂协议的设计思路

做C#上位机开发时,很多人第一次接触串口通信都会陷入“迷茫循环”:按教程写的代码能打开端口,却收不到数据;偶尔收到数据又全是乱码;好不容易能通信,换个设备又出现“半包”——前半段数据是温度,后半段却混着湿度;更头疼的是,现场运行时突然断连,排查半天找不到原因。

串口通信看似简单,实则是“硬件交互+数据解析+异常处理”的综合考验。本文从C# SerialPort类的基础用法入手,一步步拆解“端口操作→数据收发→协议设计→复杂场景处理”的核心逻辑,每个环节都附实战代码和避坑技巧,帮你从“能通信”进阶到“稳定可靠通信”,哪怕是和STM32、PLC等工业设备对接也能游刃有余。

一、基础篇:搞懂SerialPort类,打通串口通信“第一公里”

C# 提供的 System.IO.Ports.SerialPort 类是串口通信的核心工具,但新手常因“属性配置不当”“方法调用顺序错”导致通信失败。先掌握基础用法,才能避免低级错误。

1.1 必懂的SerialPort核心属性(配置错了必失败)

串口通信的“底层约定”全靠这些属性定义,必须和下位机(如STM32、Arduino)完全一致,否则会出现“发得出去,收不到”或“收到乱码”:

属性名作用说明常见取值与注意事项
PortName串口名称(如COM3、COM10)需通过 SerialPort.GetPortNames() 获取系统可用端口,避免用户输入不存在的端口
BaudRate波特率(数据传输速率)常用9600、115200bps,必须和下位机一致;波特率越高,对线路要求越高(易丢包)
Parity校验位(检测数据传输错误)工业场景常用Parity.None(无校验),若用其他校验(如Odd/Even),下位机需同步配置
DataBits数据位(每帧数据的位数)固定为8(工业标准),极少用7或9位
StopBits停止位(标识一帧数据结束)常用StopBits.One(1位停止位),复杂场景可用1.5位(仅低速通信)
ReadTimeout读取超时时间(毫秒)设为500-1000ms,避免读取操作无限阻塞;设为0则永不超时(不推荐)
WriteTimeout写入超时时间(毫秒)同ReadTimeout,防止写入时因端口异常卡住
ReceivedBytesThreshold触发DataReceived事件的字节数设为1(收到1字节就触发),适合实时性要求高的场景;设为协议包长度(如10)可减少事件触发次数

1.2 基础通信流程:打开→发送→接收→关闭(附完整代码)

以“C#上位机与STM32通信”为例,实现“发送指令→接收设备反馈”的基础流程,关键是“安全打开端口”和“正确处理接收事件”。

步骤1:安全打开串口(避免端口占用、不存在等错误)
using System;
using System.IO.Ports;
using System.Linq;
using System.Windows.Forms;
namespace SerialCommDemo
{
public partial class MainForm : Form
{
// 全局SerialPort对象,避免频繁创建销毁
private SerialPort _serialPort;
public MainForm()
{
InitializeComponent();
// 初始化时加载系统可用串口
LoadAvailablePorts();
}
// 加载系统可用串口到下拉框
private void LoadAvailablePorts()
{
cboPortName.Items.Clear();
// 获取所有可用串口(注意:部分虚拟串口可能不显示,需检查驱动)
string[] availablePorts = SerialPort.GetPortNames();
if (availablePorts.Length == 0)
{
cboPortName.Items.Add("无可用串口");
btnOpenPort.Enabled = false;
}
else
{
cboPortName.Items.AddRange(availablePorts);
cboPortName.SelectedIndex = 0; // 默认选中第一个
btnOpenPort.Enabled = true;
}
// 默认配置(可根据需求修改)
cboBaudRate.Text = "9600";
cboParity.Text = "None";
cboDataBits.Text = "8";
cboStopBits.Text = "One";
}
// 打开/关闭串口按钮点击事件
private void btnOpenPort_Click(object sender, EventArgs e)
{
if (_serialPort == null || !_serialPort.IsOpen)
{
// 1. 解析配置参数
string portName = cboPortName.Text;
if (!int.TryParse(cboBaudRate.Text, out int baudRate))
{
MessageBox.Show("波特率必须是整数!");
return;
}
Parity parity = (Parity)Enum.Parse(typeof(Parity), cboParity.Text);
if (!int.TryParse(cboDataBits.Text, out int dataBits))
{
MessageBox.Show("数据位必须是整数!");
return;
}
StopBits stopBits = (StopBits)Enum.Parse(typeof(StopBits), cboStopBits.Text);
// 2. 创建SerialPort对象并配置
_serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits)
{
ReadTimeout = 1000,
WriteTimeout = 1000,
ReceivedBytesThreshold = 1 // 收到1字节就触发接收事件
};
// 3. 订阅接收事件(关键:数据到达时触发)
_serialPort.DataReceived += SerialPort_DataReceived;
try
{
_serialPort.Open();
btnOpenPort.Text = "关闭串口";
// 打开后禁用配置修改
cboPortName.Enabled = false;
cboBaudRate.Enabled = false;
btnSendData.Enabled = true;
MessageBox.Show($"串口 {portName} 打开成功!");
}
catch (UnauthorizedAccessException)
{
MessageBox.Show($"串口 {portName} 已被占用,请关闭其他程序!");
}
catch (System.IO.IOException)
{
MessageBox.Show($"串口 {portName} 无法访问,可能是驱动异常!");
}
catch (Exception ex)
{
MessageBox.Show($"打开串口失败:{ex.Message}");
}
}
else
{
// 关闭串口(必须释放资源)
try
{
_serialPort.DataReceived -= SerialPort_DataReceived; // 取消事件订阅(避免内存泄漏)
_serialPort.Close();
_serialPort.Dispose(); // 释放资源
_serialPort = null;
btnOpenPort.Text = "打开串口";
// 关闭后启用配置修改
cboPortName.Enabled = true;
cboBaudRate.Enabled = true;
btnSendData.Enabled = false;
}
catch (Exception ex)
{
MessageBox.Show($"关闭串口失败:{ex.Message}");
}
}
}
}
}
步骤2:发送数据(文本/字节数组,满足不同场景)

串口通信支持“文本发送”和“字节数组发送”,文本适合简单指令(如“GET_TEMP”),字节数组适合工业控制(如二进制指令)。

// 发送数据按钮点击事件
private void btnSendData_Click(object sender, EventArgs e)
{
if (_serialPort == null || !_serialPort.IsOpen)
{
MessageBox.Show("请先打开串口!");
return;
}
string sendContent = txtSendData.Text.Trim();
if (string.IsNullOrEmpty(sendContent))
{
MessageBox.Show("发送内容不能为空!");
return;
}
try
{
// 场景1:发送文本(适合ASCII指令,如"GET_TEMP\r\n")
_serialPort.WriteLine(sendContent); // 自带换行符,下位机可按换行符判断帧结束
// 记录发送日志
AddLog($"发送(文本):{sendContent}");
// 场景2:发送字节数组(适合二进制指令,如0xAA 0x03 0x01 0x00 0x02 0x55)
// byte[] sendBytes = new byte[] { 0xAA, 0x03, 0x01, 0x00, 0x02, 0x55 };
// _serialPort.Write(sendBytes, 0, sendBytes.Length);
// AddLog($"发送(字节):{BitConverter.ToString(sendBytes).Replace("-", " ")}");
}
catch (TimeoutException)
{
MessageBox.Show("发送超时,端口可能已断开!");
}
catch (Exception ex)
{
MessageBox.Show($"发送失败:{ex.Message}");
}
}
// 添加日志到文本框(跨线程安全)
private void AddLog(string log)
{
if (txtLog.InvokeRequired)
{
// 跨线程时切换到UI线程
txtLog.Invoke(new Action<string>(AddLog), log);return;}// 带时间戳的日志txtLog.AppendText($"[{DateTime.Now:HH:mm:ss.fff}] {log}\r\n");// 自动滚动到最新行txtLog.SelectionStart = txtLog.TextLength;txtLog.ScrollToCaret();}
步骤3:接收数据(处理文本/字节,避免乱码)

DataReceived 事件在后台线程触发,不能直接更新UI,需用 Invoke 切换到UI线程;同时要注意“编码格式”——默认是ASCII,若下位机用UTF8或GBK,需手动设置 SerialPort.Encoding

// 串口数据接收事件(后台线程执行)
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (_serialPort == null || !_serialPort.IsOpen) return;
try
{
// 场景1:接收文本(对应发送的文本指令,按换行符分割)
string receivedText = _serialPort.ReadLine(); // 读取到换行符为止
AddLog($"接收(文本):{receivedText}");
// 场景2:接收字节数组(对应二进制指令,先读字节数再读内容)
// int bytesToRead = _serialPort.BytesToRead; // 获取当前缓冲区字节数
// byte[] receivedBytes = new byte[bytesToRead];
// _serialPort.Read(receivedBytes, 0, bytesToRead);
// // 转16进制字符串显示(避免乱码)
// string receivedHex = BitConverter.ToString(receivedBytes).Replace("-", " ");
// AddLog($"接收(字节):{receivedHex}");
}
catch (TimeoutException)
{
AddLog("接收超时,未收到完整数据!");
}
catch (System.IO.IOException)
{
AddLog("接收失败,串口已断开!");
// 自动关闭串口
if (_serialPort.IsOpen)
{
_serialPort.Close();
}
}
catch (Exception ex)
{
AddLog($"接收异常:{ex.Message}");
}
}

1.3 基础避坑:3个新手必踩的“低级错误”

  1. 端口占用后无法重试:关闭串口时必须 Dispose(),且取消 DataReceived 事件订阅,否则资源泄漏,下次打开提示“访问被拒绝”;
  2. 接收乱码:若下位机发送中文或特殊字符,需设置 _serialPort.Encoding = Encoding.UTF8(或GBK),确保和下位机编码一致;
  3. UI线程阻塞:不要在 DataReceived 事件中做耗时操作(如复杂计算),也不要用 ReadLine() 读取无换行符的数据(会阻塞后台线程)。

二、进阶篇:解决“半包/粘包”,设计可靠的自定义协议

基础通信能实现“收发数据”,但工业场景中,数据常因“传输延迟”“缓冲区堆积”出现“半包”(数据不完整)或“粘包”(多帧数据混在一起)——比如下位机发“0xAA 0x03 0x01 0x23 0x45 0x55”(温度23.45℃),上位机可能只收到“0xAA 0x03”(半包),或收到“0xAA 0x03 0x01 0x23 0x45 0x55 0xAA 0x03 0x02 0x67 0x89 0x55”(粘包)。

解决这个问题的核心是:设计自定义通信协议,让上位机能“准确识别一帧数据的开始和结束”。

2.1 工业级自定义协议设计原则

一个可靠的协议需包含“帧标识+数据长度+有效数据+校验+帧结束”,确保“能识别帧边界”“能检测数据错误”。以“STM32向上位机发送温湿度数据”为例,设计协议如下:

字段字节数作用说明示例值(十六进制)
包头(SOF)1标识一帧数据开始,固定值(避免与数据冲突)0xAA
数据长度(LEN)1后续“有效数据”的字节数(不含包头、长度、校验、包尾)0x04(表示有效数据4字节)
有效数据(DATA)N实际业务数据(如温度2字节+湿度2字节)0x00 0x64 0x00 0x32(温度100℃,湿度50%)
校验和(CHK)1包头+长度+有效数据的累加和(低8位),检测数据错误0xAA+0x04+0x00+0x64+0x00+0x32=0xFB → 0xFB
包尾(EOF)1标识一帧数据结束,固定值0x55

协议设计关键点

  • 包头/包尾用“特殊字节”(如0xAA、0x55),避免和有效数据重复(若数据可能包含0xAA,需加“转义机制”,如0xAA→0xAA 0x00);
  • 数据长度字段(LEN)是核心:上位机先读包头和长度,再按长度读后续数据,避免半包;
  • 校验和(CHK)是“数据正确性的最后一道防线”,防止传输过程中数据被篡改或丢失。

2.2 协议解析实战:用“缓冲区+状态机”处理半包/粘包

上位机接收数据时,先将字节存入“全局缓冲区”,再从缓冲区中按协议解析完整帧——这种方式能应对半包(缓冲区数据不足时等待下次接收)和粘包(解析完一帧后继续解析剩余数据)。

步骤1:定义全局缓冲区和解析状态
// 全局接收缓冲区(存未解析的字节)
private List<byte> _receiveBuffer = new List<byte>();// 协议字段常量(和下位机一致)private const byte PROTOCOL_SOF = 0xAA;   // 包头private const byte PROTOCOL_EOF = 0x55;   // 包尾private const int PROTOCOL_MIN_LENGTH = 5; // 最小帧长度:SOF(1)+LEN(1)+DATA(1)+CHK(1)+EOF(1) = 5字节
步骤2:修改接收逻辑,将数据存入缓冲区
// 串口数据接收事件(修改为字节接收,存入缓冲区)
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (_serialPort == null || !_serialPort.IsOpen) return;
try
{
int bytesToRead = _serialPort.BytesToRead;
if (bytesToRead <= 0) return;
// 读取当前缓冲区所有字节,存入全局缓冲区
byte[] tempBytes = new byte[bytesToRead];
_serialPort.Read(tempBytes, 0, bytesToRead);
_receiveBuffer.AddRange(tempBytes);
// 记录原始接收日志
AddLog($"接收(原始字节):{BitConverter.ToString(tempBytes).Replace("-", " ")}");
// 解析缓冲区中的完整帧
ParseProtocolFrame();
}
catch (Exception ex)
{
AddLog($"接收异常:{ex.Message}");
}
}
步骤3:按协议解析缓冲区数据(核心逻辑)
// 解析协议帧(从缓冲区中提取完整帧)
private void ParseProtocolFrame()
{
// 循环解析:直到缓冲区中没有完整帧
while (_receiveBuffer.Count >= PROTOCOL_MIN_LENGTH)
{
// 1. 找包头(从缓冲区开头找PROTOCOL_SOF)
int sofIndex = _receiveBuffer.IndexOf(PROTOCOL_SOF);
if (sofIndex == -1)
{
// 没有找到包头,清空缓冲区(避免脏数据堆积)
_receiveBuffer.Clear();
AddLog("缓冲区无有效包头,清空脏数据");
return;
}
if (sofIndex > 0)
{
// 包头前有脏数据,删除(如缓冲区开头是0x00 0xAA...,删除0x00)
byte[] dirtyBytes = _receiveBuffer.Take(sofIndex).ToArray();
AddLog($"删除包头前脏数据:{BitConverter.ToString(dirtyBytes).Replace("-", " ")}");
_receiveBuffer.RemoveRange(0, sofIndex);
continue;
}
// 2. 读取数据长度(包头后第1字节)
byte dataLength = _receiveBuffer[1];
// 计算完整帧的总长度:SOF(1) + LEN(1) + DATA(dataLength) + CHK(1) + EOF(1)
int totalFrameLength = 1 + 1 + dataLength + 1 + 1;
// 检查缓冲区数据是否足够(避免半包)
if (_receiveBuffer.Count < totalFrameLength)
{
AddLog($"缓冲区数据不足(当前{_receiveBuffer.Count}字节,需{totalFrameLength}字节),等待后续数据");
return;
}
// 3. 读取完整帧数据
byte[] fullFrame = _receiveBuffer.Take(totalFrameLength).ToArray();
// 检查包尾是否正确
if (fullFrame.Last() != PROTOCOL_EOF)
{
AddLog($"包尾错误(实际{fullFrame.Last():X2},期望{PROTOCOL_EOF:X2}),丢弃当前帧");
// 删除当前错误帧,继续解析下一个
_receiveBuffer.RemoveRange(0, totalFrameLength);
continue;
}
// 4. 校验和验证
byte calculatedChk = CalculateCheckSum(fullFrame.Take(totalFrameLength - 2).ToArray()); // 取SOF到DATA的字节(不含CHK和EOF)
byte receivedChk = fullFrame[totalFrameLength - 2]; // CHK是倒数第2字节(EOF是最后1字节)
if (calculatedChk != receivedChk)
{
AddLog($"校验和错误(计算{calculatedChk:X2},接收{receivedChk:X2}),丢弃当前帧");
_receiveBuffer.RemoveRange(0, totalFrameLength);
continue;
}
// 5. 解析有效数据(DATA字段)
byte[] validData = fullFrame.Skip(2).Take(dataLength).ToArray(); // 跳过SOF和LEN,取dataLength字节
AnalyzeValidData(validData);
// 6. 删除已解析的帧,继续解析剩余数据(处理粘包)
_receiveBuffer.RemoveRange(0, totalFrameLength);
}
}
// 计算校验和(累加和,低8位)
private byte CalculateCheckSum(byte[] data)
{
int sum = 0;
foreach (byte b in data)
{
sum += b;
}
return (byte)(sum & 0xFF); // 取低8位,避免溢出
}
// 解析有效数据(根据业务需求处理)
private void AnalyzeValidData(byte[] validData)
{
// 示例:有效数据为“温度(2字节,小端)+湿度(2字节,小端)”
if (validData.Length != 4)
{
AddLog($"有效数据长度错误(实际{validData.Length}字节,期望4字节)");
return;
}
// 小端字节转int(如0x00 0x64 → 100 → 温度100℃)
int temperature = BitConverter.ToInt16(validData, 0);
int humidity = BitConverter.ToInt16(validData, 2);
// 边界判断(过滤异常值,如温度不可能超过125℃)
if (temperature < -40 || temperature > 125){AddLog($"温度数据异常:{temperature}℃,忽略");return;}if (humidity < 0 || humidity > 100){AddLog($"湿度数据异常:{humidity}%,忽略");return;}// 更新UI显示(温湿度)UpdateSensorData(temperature, humidity);AddLog($"解析成功:温度{temperature}℃,湿度{humidity}%");}// 更新温湿度显示(跨线程安全)private void UpdateSensorData(int temp, int humi){if (InvokeRequired){Invoke(new Action<int, int>(UpdateSensorData), temp, humi);return;}lblTemperature.Text = $"当前温度:{temp}℃";lblHumidity.Text = $"当前湿度:{humi}%";}

2.3 协议优化:应对复杂场景的3个技巧

  1. 支持多指令类型:在有效数据中加“指令码”字段(如0x01表示温湿度,0x02表示设备状态),实现“一协议多用途”;
  2. 超时重发机制:发送指令后若超时未收到响应,自动重发(最多3次),避免因偶然丢包导致通信失败;
  3. 转义机制:若有效数据中可能包含包头(0xAA),约定“0xAA→0xAA 0x00”,接收时再还原,防止误识别包头。

三、高级篇:复杂场景处理,实现工业级稳定通信

工业现场的串口通信环境远比实验室复杂:多设备同时通信、线路干扰导致断连、高频数据采集要求低延迟……这需要在基础和进阶的基础上,进一步优化“并发控制”“异常恢复”“性能”。

3.1 多设备并发通信:用“字典+锁”管理多个SerialPort

若上位机需同时对接多个设备(如2个STM32、1个PLC),需为每个设备创建独立的SerialPort对象,并加锁防止并发冲突。

// 多设备管理:Key=设备ID(如"Device1"),Value=SerialPort对象
private Dictionary<string, SerialPort> _deviceSerialPorts = new Dictionary<string, SerialPort>();// 线程锁:避免多线程操作字典时出现异常private object _serialPortLock = new object();// 添加设备并打开串口public bool AddDeviceAndOpen(string deviceId, string portName, int baudRate){lock (_serialPortLock){if (_deviceSerialPorts.ContainsKey(deviceId)){AddLog($"设备{deviceId}已存在,无需重复添加");return false;}SerialPort serialPort = new SerialPort(portName, baudRate){ReadTimeout = 1000,WriteTimeout = 1000,ReceivedBytesThreshold = 1};serialPort.DataReceived += (s, e) =>{// 为每个设备单独处理接收(通过sender区分设备)SerialPort devPort = s as SerialPort;if (devPort == null) return;// 读取数据并解析(逻辑类似前面,需标记设备ID)};try{serialPort.Open();_deviceSerialPorts.Add(deviceId, serialPort);AddLog($"设备{deviceId}{portName})打开成功");return true;}catch (Exception ex){AddLog($"设备{deviceId}打开失败:{ex.Message}");return false;}}}

3.2 异常恢复:串口断连后自动重连

工业现场可能因线路松动、设备重启导致串口断连,需定期检测端口状态,断连后自动重试。

// 定时器:定期检测串口状态(1000ms一次)
private System.Timers.Timer _portCheckTimer;
// 初始化定时器
private void InitPortCheckTimer()
{
_portCheckTimer = new System.Timers.Timer(1000);
_portCheckTimer.Elapsed += (s, e) =>
{
lock (_serialPortLock)
{
foreach (var kvp in _deviceSerialPorts.ToList()) // ToList避免遍历中修改字典
{
string deviceId = kvp.Key;
SerialPort serialPort = kvp.Value;
if (serialPort.IsOpen)
{
// 检测端口是否正常(可发送心跳指令,如"HEART\r\n")
try
{
serialPort.Write(new byte[] { 0xAA, 0x01, 0x00, 0xAA, 0x55 }, 0, 5); // 心跳指令
}
catch
{
// 发送失败,标记为断连
serialPort.Close();
}
}
else
{
// 断连后自动重连(最多重试5次)
int retryCount = 0;
while (retryCount < 5 && !serialPort.IsOpen)
{
try
{
serialPort.Open();
AddLog($"设备{deviceId}自动重连成功");
}
catch
{
retryCount++;
Thread.Sleep(1000); // 重试间隔1秒
}
}
if (!serialPort.IsOpen)
{
AddLog($"设备{deviceId}重连5次失败,请检查线路");
}
}
}
}
};
_portCheckTimer.Start();
}

3.3 性能优化:高频采集场景的3个关键

当需要每秒采集100次以上数据时,需优化“事件触发频率”和“数据处理速度”:

  1. 提高 ReceivedBytesThreshold(如设为协议帧长度),减少 DataReceived 事件触发次数;
  2. Read() 代替 ReadLine(),避免文本解析的耗时;
  3. 数据解析逻辑放在后台线程池(ThreadPool.QueueUserWorkItem),不阻塞接收线程。

四、讨论:你遇到的串口通信“疑难杂症”?

串口通信看似“老技术”,但在工业物联网中仍占据核心地位——从智能传感器到PLC,从医疗设备到汽车电子,都离不开它。本文从基础的SerialPort用法,到复杂协议设计,再到工业场景优化,覆盖了大部分实战需求,但实际开发中,你可能还会遇到更棘手的问题:

欢迎在评论区分享你的串口通信“踩坑”经历,或提出你的疑问——无论是基础用法还是复杂协议设计,我们一起探讨解决方案,让串口通信从“头疼问题”变成“拿手好戏”!

------------伴代码深耕技术、连万物探索物联,我聚焦计算机、物联网与上位机领域,盼同频的你关注,一起交流成长~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/921141.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

完整教程:Python 高效实现 PDF 转 Word:告别手动复制粘贴

完整教程:Python 高效实现 PDF 转 Word:告别手动复制粘贴pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consol…

生物科技网站建设 中企动力北京网站建设要钱么

原创作者&#xff1a;田超凡&#xff08;程序员田宝宝&#xff09; 版权所有&#xff0c;引用请注明原作者&#xff0c;严禁复制转载

asp.net 网站提速百度应用市场app下载安装

Rust 是一门以系统编程为主要应用场景的编程语言&#xff0c;它提供了无需垃圾回收机制就能安全实现内存管理的特性。其中&#xff0c;所有权&#xff08;Ownership&#xff09;系统是 Rust 语言最核心的特点之一&#xff0c;它能够保证内存安全&#xff0c;同时避免数据竞争。…

P6652 「SWTR-5」String

首先有个结论是,倒着做,每次左边右边一定会扩展到最大的. 然后这个最大的会有单调性,可以用 hash 快速求出来. 然后就变成了一个 DP 问题,你把它看成自动机,在上面跑最短路即可.

Java 包(package)

Java 包(package)在 Java 中,包(Package)是组织类和接口的核心机制,它如同文件系统中的文件夹,将相关的类和接口归类存放,解决了命名冲突、代码管理和访问控制等关键问题。本文从基础概念到实战应用,全面解析 J…

网站突然被降权检察机门户网站建设自查报告

源码安装Python2.7.18 。是目前2020年后发布的新Linux发行版的唯一途径。并且安装好了之后只能运行基本的python2功能。不能作为共享库使用&#xff0c;即 ./configure --enable-shared 会导致编译通不过。也不能优化&#xff0c;即 ./configure --enable-optimizations 会导致…

数学解题中常见的“漏解”情况分析

一、概念不清,导致漏解 对所学知识概念不清,领会不够深刻,导致答题不完整。 例:已知(a-3)x>6,求x的取值范围。分析:根据不等式的性质“不等式的两边同乘或同除以不为零的负数,不等号的方向要改变”,而此题中(…

企业网站建设开发四个阶段餐饮行业管理系统

不知不觉差不多一个月就过去了 刚到培训班那时候的心情&#xff0c;现在也还能有所感觉 作为今年6月份的毕业生&#xff0c;刚从大学的实习期出来&#xff0c;辞掉了上一份工作&#xff0c;本来是打算找一份更加与专业挂钩的工作做的 也许是90后对网络的情有独钟&#xff0c;亦…

陕西住房和城乡建设部网站首页西安最新防疫信息

目录 构造函数 输出字符串 修改和清空字符串 利用 stringstream 去除字符串空格 利用stringstream去除指定的字符 stringstream 数据库 <sstream> 构造函数 创建一个对象&#xff0c;向对象输入字符串&#xff1a; string x"abcdefg";stringstream s…

模拟退火 - 学习笔记

前置知识:爬山算法 从爬山算法的局限到模拟退火 对于爬山算法所求解问题:计算一个函数的最大/小值。 我们知道它的核心目标是求解函数的最大值或最小值 —— 就像人沿着山坡向上爬,始终朝着 “更高”(求最大值)或…

做长尾词优化去哪些网站asp.net网站改版 旧网站链接

VSCode安装Go 1.点击Go官网&#xff0c;根据自己环境下载go安装包&#xff0c;我这里为Windows 2.双击安装包&#xff0c;一直点击【Next】即可 VSCode配置Go基础环境 1.创建Go的工作目录: C:\Code\GoCode 2.创建Go的环境变量: GOPATH (1)右键【此电脑】&#xff0c;点击…

图册

为了节省一些博客的篇幅并方便查看,将一些高清大图放在这里以供查阅。 有一些图是摘自别处,我对此致歉。 p1

实用指南:FreeRTOS 事件组详解

实用指南:FreeRTOS 事件组详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco&quo…

大型网站开发方案网站设计面试问题

论文题目&#xff1a;Vision Transformers Need Registers 论文链接&#xff1a;https://arxiv.org/abs/2309.16588 视觉Transformer&#xff08;ViT&#xff09;目前已替代CNN成为研究者们首选的视觉表示backbone&#xff0c;尤其是一些基于监督学习或自监督学习预训练的ViT&a…

宁夏考试教育网站oa电子办公系统

根据上面一篇随笔所介绍的PC购买流程的项目&#xff0c;在项目中&#xff0c;需要有一个生成订单的功能&#xff0c;能够使得Admin很方便的在获得批准的申请中选取一些来生成订单&#xff0c;要求界面操作简单明了&#xff0c;大概的效果图如下&#xff1a; 点击checkbox&#…

[ABC425C] Rotate and Sum Query 题解

思路 对于操作二,多次求 $ \displaystyle \sum_{i=l}^r A_i $,不难想到前缀和。所以先记录 \(A\) 的前缀和。 对于操作一,我们不用按题意模拟,直接记录一共偏移了多少。以后出现操作二时直接根据偏移量输出偏移前 …

C语言网站开发pdf音乐网页设计材料加字加图片

本次由快手刘建刚老师分享&#xff0c;内容主要分为三部分。首先介绍流式计算的基本概念&#xff0c; 然后介绍 Flink 的关键技术&#xff0c;最后讲讲 Flink 在快手生产实践中的一些应用&#xff0c;包括实时指标计算和快速 failover。 一、流式计算的介绍 流式计算主要针对 u…

用名字做壁纸网站哪里有创建网站的

String 在编程中被广泛使用&#xff0c;所以掌握 String 和 int 的相互转换方法是极其重要的。 String转换为int String 字符串转整型 int 有以下两种方式&#xff1a; Integer.parseInt(str)Integer.valueOf(str).intValue()注意&#xff1a;Integer 是一个类&#xff0c;是…

制作高端网站公司排名网络营销推广seo

关于单引号和双引号 当输出的字符串内部没有单引号的时候&#xff0c;外面可以用单引号&#xff0c; 但是如果内部有了单引号&#xff0c;那么外部只能用双引号。 dict {Name: Zara, Age: 7, Class: First} print(dict) print (dict[Name]: , dict[Name]) print ("dic…

线程--基本使用、线程常用方法

2.2 继承Thread vs 实现 Runnable 的区别从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了Runnable接口 实现Runnable接口方式更加适合…