이번에 Overlapped I/O 모델에 대해 정리도 할겸 C++ 시리얼 통신 클래스를 구현해 보았다. 지난 글에 올린 링버퍼 클래스를 활용한다.
먼저 Overlapped I/O 모델의 개념과 동작 방식, 그리고 왜 시리얼 통신에서 Overlapped I/O를 사용하는지 정리하고 시작하자.
Overlapped I/O
Overlapped I/O는 윈도우에서 제공하는 완전한 비동기(Asynchronous) I/O 모델이다. 고성능 네트워크 서버 구축에 사용되는 IOCP(I/O Completion Port) 역시 이 Overlapped I/O를 기반으로 동작한다.
Overlapped I/O의 핵심은 I/O 요청을 커널에 맡긴 뒤 I/O 완료를 기다리지 않고 바로 반환된다는 점이다. 실제 I/O가 끝났을 때는 커널로부터 완료 통지를 받는다.
select와의 차이
이벤트 통지를 받는 점에서 I/O Multiplexing 모델인 select와 비슷해보일 수 있지만 근본적인 차이가 있다.
- select는 커널이 "지금 이 fd를 읽거나 쓰면 블로킹되지 않을 수 있다"는 정보만 알려줄 뿐 실제 read/write 호출은 그 후에 사용자가 직접 수행해야한다. 그리고 호출 순간 부터는 커널이 다시 제어권을 잡고 I/O 완료까지 반환되지 않는다.
- Overlapped I/O는 I/O 요청 자체를 커널에 제출하고 제어권은 즉시 프로세스로 반환된다. 커널이 I/O 완료한 뒤 통지하고 사용자는 완료된 상태를 사후 확인하는 구조이다.
I/O 모델 이름이 Overlapped인 이유
Overlapped I/O라는 이름은 모델의 비동기 특성 때문이다. 동기 I/O는 하나의 I/O 요청이 완료되야 다음 요청을 보낼 수 있는 반면, Overlapped I/O에서는 먼저 보낸 요청이 끝나기도 전에 바로 다음 I/O 요청을 던질 수 있다. 이 과정에서 여러 개의 I/O 작업이 시간상으로 서로 겹쳐서(Overlapped) 진행되기 때문에 이런 이름이 붙은것이다.
Overlapped I/O를 적용하는 흐름
Overlapped I/O를 적용하는 기본 흐름은 다음과 같다.
- 핸들(또는 소켓)을 OVERLAPPED FLAG를 적용하여 생성
- ReadFile/WriteFile 등 I/O 함수 호출(소켓인 경우 WSARead, WSAWrite)
- "I/O 완료 통지를 기다리고, 완료 시 결과 처리"
이때 3번 단계(완료 통지 방식)에는 여러 구현 방법이 존재한다.
시리얼 통신에서 Overlapped I/O의 완료 통지 방식
시리얼 통신에서 구현 가능한 Overlapped I/O의 완료 통지 방식은 크게 4가지로 나눠볼 수 있다. 참고로 MSDN 공식 예제에서는 1번 방식으로 안내하고 있다.
1. 핸들의 이벤트 대기 방식 (WaitCommEvent + WaitForSingleObject / WaitForMultipleObjects)
이 방식의 핵심인 WaitCommEvent 함수는 SetCommMask로 등록해 둔 이벤트가 발생할 때까지 대기하는 함수인데, 보통 데이터 수신(EV_RXCHAR) 감시 용도로 사용한다.
WaitCommEvent 함수는 시리얼 핸들을 Overlapped 모드로 열었다고해서 무조건 Non-Blocking이 아니라 Blocking / Non-Blocking을 선택해서 쓸 수 있는 API이다. 세 번째 인자로 NULL을 넘기면 이벤트가 발생하지 않는 한 영원히 차단되기 때문에, 나는 세 번째 인자로 overlapped 구조체를 넘기고 WaitForMultipleObjects를 사용해 Overlapped 이벤트를 감지하는 방식을 선택했다.
2. GetOverlappedResult 폴링 방식
ReadFile/WriteFile 호출 이후 GetOverlappedResult를 반복 호출해 I/O 완료 여부를 확인하는 방식이다.
WriteFile의 경우 드라이버 송신 큐가 비어 있다면 즉시 전송될 것을 전제하며, ReadFile은 ClearCommError를 통해 이미 드라이버 입력 큐(cbInQue)에 데이터가 존재함을 확인한 상태에서 호출한다. 비록, 폴링 방식 자체는 성능 저하 요소이지만 Overlapped I/O 요청 즉시 혹은 매우 짧은 시간 내 완료 통지가 올 것으로 예상하고 쓰는 방식이다.
3. APC 기반 방식 (ReadFileEx / WriteFileEx)
APC(Asynchronous Procedure Call)는 비동기 I/O 요청 시 완료 시점에 실행될 콜백 함수를 커널에 등록하는 방식이다.
1. ReadFileEx나 WriteFileEx 같은 함수를 사용하면서 마지막 인자에 콜백 함수의 주소를 넘겨준다.
2. 커널이 I/O 작업을 백그라운드에서 완료하면, 해당 스레드의 APC Queue라는 곳에 콜백 함수 호출 정보를 집어넣는다.
3. 스레드가 Alertable wait가 되면, APC Queue에 쌓인 콜백 함수들이 차례대로 실행된다.
이 방식은 SleepEx, WaitForSingleObjectEx 등 함수로 스레드를 alertable wait 상태로 만들 때만 콜백이 호출되기 때문에 사용 조건이 까다로운 편이다.
4. IOCP 방식 (CreateIoCompletionPort + GetQueuedCompletionStatus)
1. CreateIoCompletionPort를 통해 Overlapped 모드의 핸들(또는 소켓)을 IOCP 객체에 연결한 뒤 최초 I/O 요청을 시작한다.
2. 백그라운드에서 I/O 작업이 완료되면 커널이 Completion Queue에 해당 작업의 결과(성공 여부, 전송 바이트 수 등)를 넣어준다. 3. GetQueuedCompletionStatus를 통해 대기중이던 워커 스레드가 큐에서 완료된 결과를 꺼내 처리한다.
4. 워커스레드는 즉시 다음 비동기 I/O 요청을 다시 등록함으로써, 끊김 없이 송수신이 이어지는 루프 구조를 형성한다.
이 방식은 고성능 네트워크 서버 구현에는 적합하지만, 시리얼 통신에 적용하기에는 다소 오버인 방식이다.
시리얼 통신에서 Overlapped I/O를 쓰는 이유?
소켓의 경우 내부적으로 송수신(RX/TX) 경로가 분리되어 있어 독립적인 처리가 가능하지만, 시리얼 포트는 그렇지가 않다. 시리얼 핸들을 블로킹 모드로 사용할 경우 한 스레드가 ReadFile에서 데이터 수신을 대기하면, 다른 스레드의 WriteFile도 함께 블로킹될 수 있다.
MSDN 문서에서도 다음에 대해 설명하고 있다. (https://learn.microsoft.com/en-us/previous-versions/ff802693(v=msdn.10))
If one thread were waiting for a ReadFile function to return, any other thread that issued a WriteFile function would be blocked.
하지만 Overlapped I/O를 사용하면 Read/Write 요청을 각각 비동기 작업으로 커널에 맡기고, 완료는 이벤트 기반으로 통지받기 때문에 이러한 구조적 제약에서 벗어날 수 있다.
Serial 클래스 주요 코드 및 사용예시
Serial.h - 클래스 인터페이스
#pragma once
#include <Windows.h>
#include <string>
#include <atomic>
#include <mutex>
#include "RingBuffer.h"
#define SERIAL_OK (0)
#define SERIAL_ERR_CREATEFILE (-1) // device not found
#define SERIAL_ERR_GETCOMMSTATE (-2)
#define SERIAL_ERR_SETCOMMSTATE (-3)
#define SERIAL_ERR_SETCOMMTIMEOUTS (-4)
#define SERIAL_ERR_SETCOMMMASK (-5)
#define SERIAL_ERR_CREATEEVENT (-6)
class Serial
{
public:
Serial();
~Serial();
Serial(const Serial&) = delete;
Serial& operator=(const Serial&) = delete;
Serial(Serial&&) = delete;
Serial& operator=(Serial&&) = delete;
int open(const std::string& port, int baudrate, int dataBits = 8, char parity = 'N', int stopBits = 1);
void close();
bool isOpen() const;
int available() const;
char read();
int readBytes(char* buf, int size);
int readBytesUntil(char delim, char* buf, int size);
std::string readStringUntil(char delim);
std::string readLine();
bool writeBytes(const char* data, int size);
bool write(char data);
bool writeString(const std::string& s);
bool writeLine(const std::string& s);
void setTimeout(int timeout_ms); // 0: non-block, <0: infinite
int getTimeout() const;
void setEndOfLine(const std::string& eol);
std::string getEndOfLine() const;
private:
void rxThread();
std::wstring toWideString(const std::string& s);
int readImpl(char* out, int maxSize, const std::string& terminator);
void trimTerminator(std::string& s, const std::string& term);
private:
HANDLE m_handle = INVALID_HANDLE_VALUE;
HANDLE m_event_stop = nullptr;
mutable std::mutex m_mutex;
OVERLAPPED m_ov_write = {}; // WriteFile용 overlapped 구조체
std::thread m_rxThread;
std::atomic<bool> m_running = false;
RingBuffer<char> m_rxBuffer{ 32768 }; // 링버퍼 사이즈 32768 bytes
int m_timeout_ms = -1;
std::string m_endOfLine = "\r\n";
};
개인적으로 아두이노의 Serial 라이브러리가 사용자 입장에서 단순하고 직관적으로 잘 짜여져있다고 생각해 API 구성 시 참고했다.
Com port 열기
int Serial::open(const std::string& port, int baudrate, int dataBits, char parity, int stopBits) {
std::lock_guard<std::mutex> lock(m_mutex);
// 1. 기존에 포트가 열려있는 경우 정리
close();
// 2. 포트 경로 처리
// COM10 이상은 앞에 \\.\를 붙여야 함
std::string path = port;
if (port.rfind("COM", 0) == 0 && port.size() > 4) {
path = "\\\\.\\" + port;
}
// wstring으로 변환
std::wstring wpath = toWideString(path);
// 3. 포트 열기 (with OVERLAPPED FLAG)
m_handle = CreateFile(
wpath.c_str(),
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
nullptr
);
if (m_handle == INVALID_HANDLE_VALUE)
return SERIAL_ERR_CREATEFILE;
// 4. 시리얼 포트 설정
// 드라이버 Rx/Tx 버퍼 크기 4096 bytes로 설정
SetupComm(m_handle, 4096, 4096);
// 드라이버 Rx/Tx 버퍼 비우기
PurgeComm(m_handle, PURGE_RXCLEAR | PURGE_TXCLEAR);
// 5. fail 시 호출할 람다함수 정의
auto failed = [&](int err) -> int {
if (m_event_stop) {
CloseHandle(m_event_stop);
m_event_stop = nullptr;
}
if (m_handle != INVALID_HANDLE_VALUE) {
CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
}
return err;
};
// 5. DCB(Device Control Block) 시리얼 포트 설정
DCB dcb = { 0, };
dcb.DCBlength = sizeof(dcb);
if (!GetCommState(m_handle, &dcb))
return failed(SERIAL_ERR_GETCOMMSTATE);
BYTE _parity;
switch (parity) {
case 'O': _parity = ODDPARITY; break;
case 'E': _parity = EVENPARITY; break;
case 'M': _parity = MARKPARITY; break;
case 'S': _parity = SPACEPARITY; break;
default: _parity = NOPARITY; break;
}
dcb.BaudRate = baudrate;
dcb.ByteSize = dataBits;
dcb.Parity = _parity;
dcb.StopBits = (stopBits == 2) ? TWOSTOPBITS : ONESTOPBIT;
dcb.fBinary = TRUE;
dcb.fOutxCtsFlow = FALSE; // no flow control
dcb.fOutxDsrFlow = FALSE; // no flow control
dcb.fDtrControl = DTR_CONTROL_ENABLE;
dcb.fRtsControl = RTS_CONTROL_ENABLE;
if (!SetCommState(m_handle, &dcb))
return failed(SERIAL_ERR_SETCOMMSTATE);
// 6. 시리얼 포트 타임아웃 정책 설정
// 비동기 처리를 위해 즉시 리턴하도록 설정
COMMTIMEOUTS to;
ZeroMemory(&to, sizeof(to));
to.ReadIntervalTimeout = MAXDWORD; // -1
to.ReadTotalTimeoutMultiplier = 0;
to.ReadTotalTimeoutConstant = 0;
to.WriteTotalTimeoutMultiplier = 0;
to.WriteTotalTimeoutConstant = 0;
if (!SetCommTimeouts(m_handle, &to))
return failed(SERIAL_ERR_SETCOMMTIMEOUTS);
// 7. 수신 이벤트를 WaitCommEvent로 감지하도록 설정
if (!SetCommMask(m_handle, EV_RXCHAR))
return failed(SERIAL_ERR_SETCOMMMASK);
// 8. 종료 이벤트 생성
m_event_stop = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!m_event_stop)
return failed(SERIAL_ERR_CREATEEVENT);
// 9. WriteFiel용 재사용 overlapped 구조체 초기화
m_ov_write = { 0, };
m_ov_write.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if(!m_ov_write.hEvent)
return failed(SERIAL_ERR_CREATEEVENT);
// 10. 수신 스레드 시작
m_running.store(true);
m_rxThread = std::thread(&Serial::rxThread, this);
return true;
}
시리얼 포트를 여는 과정은 CreateFile 함수를 통해 이루어진다. 이때 가장 중요한 점은 FILE_FLAG_OVERLAPPED 옵션을 사용해 핸들을 생성한다는 것이다.
m_handle = CreateFile(.., .., .., .., .., FILE_FLAG_OVERLAPPED, ..);
이 플래그를 지정함으로써 해당 핸들은 이후 WaitCommEvent, ReadFile, WriteFile 호출 시 Overlapped I/O 방식으로 동작할 수 있게 된다.
포트를 연 뒤에는 필요한 초기 설정 작업을 수행하고 실제 수신 처리를 담당할 RX 전용 스레드를 시작한다.
수신 - rxTrhead
void Serial::rxThread() {
// WaitCommEvent를 비동기로 처리하기 위한 overlapped 구조체
OVERLAPPED ov_wait{};
ov_wait.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
// WaitForMultipleObjects 대기 대상, 0: 시리얼 데이터 수신 이벤트, 1: 스레드 종료 이벤트
HANDLE waits[2] = { ov_wait.hEvent, m_event_stop };
// ReadFile을 비동기로 처리하기 위한 overlapped 구조체
OVERLAPPED ov_read{};
ov_read.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
// 임시 수신 버퍼
std::vector<char> tmp(1024);
while (m_running) {
DWORD mask = 0;
// 이전 이벤트 상태 초기화
ResetEvent(ov_wait.hEvent);
// 시리얼 데이터 수신 이벤트 대기 요청, 3번째 인자로 overlapped 구조체를 넘겼으니 즉시 반환됨
if (!WaitCommEvent(m_handle, &mask, &ov_wait)) {
// ERROR_IO_PENDING이면 정상, 실제 오류면 스레드 종료
if (GetLastError() != ERROR_IO_PENDING)
break;
}
// 시리얼 데이터 수신 이벤트 및 종료 이벤트 중 어느 하나가 발생할 때까지 대기
DWORD w = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
// 종료 이벤트(m_event_stop) 발생 시 스레드 종료
if (w == WAIT_OBJECT_0 + 1)
break;
// 현재 드라이버 상태 조회, 에러 발생 시 continue;
COMSTAT st{};
DWORD err;
if (!ClearCommError(m_handle, &err, &st))
continue;
// 수신 큐(cbInQue)에 데이터가 남아 있는 동안 반복 처리
while (st.cbInQue && m_running) {
// 한번에 읽을 최대 크기 결정
DWORD toRead = min((DWORD)tmp.size(), st.cbInQue);
// 이전 이벤트 상태 초기화
ResetEvent(ov_read.hEvent);
DWORD rd = 0;
// 데이터 Read 요청, overlapped 모드로 즉시 반환됨
if (!ReadFile(m_handle, tmp.data(), toRead, &rd, &ov_read)) {
// 비동기 처리 중이면 I/O 완료 이벤트를 대기
if (GetLastError() == ERROR_IO_PENDING)
WaitForSingleObject(ov_read.hEvent, INFINITE);
// Read 완료 결과 회수(rd: Read 요청으로 실제 수신한 바이트 수)
GetOverlappedResult(m_handle, &ov_read, &rd, FALSE);
}
// 수신된 데이터를 내부 링버퍼로 복사
for (DWORD i = 0; i < rd; ++i)
m_rxBuffer.push(tmp[i]);
// 다시 드라이버 상태를 조회에서 수신 큐에 남은 데이터 확인
ClearCommError(m_handle, &err, &st);
}
}
// OVERLAPPED 구조체 안의 이벤트 핸들 정리
CloseHandle(ov_read.hEvent);
CloseHandle(ov_wait.hEvent);
}
이 스레드는 커널 수신 버퍼에 있는 데이터를 사용자 영역의 내부 링버퍼로 복사하는 일만 담당한다.
실제 데이터 소비는 read(), readBytes(), readBytesUnitil(), readLine() 같은 사용자 API 호출 시점에 링버퍼에서 꺼내는 방식으로 처리된다.
RX 스레드의 처리 흐름은 다음과 같다.
- WaitCommEvent를 Overlapped 방식으로 요청한다.
- WaitForMultipleObjects로 "시리얼 수신 이벤트"와 "스레드 종료 이벤트"를 동시에 대기한다.
(종료 이벤트를 함께 대기하도록 구성한 이유는, 수신 대기 상태에서도 안전하게 스레드를 종료할 수 있도록 하기 위함이다.) - 수신 이벤트가 발생하면 ClearCommError를 통해 드라이버 수신 큐(cbInQue)에 실제로 남아 있는 바이트 수를 확인한다.
- 해당 크기만큼 ReadFile을 Overlapped 방식으로 호출한다.
- 비동기 읽기 요청이 즉시 완료되지 않은 경우에는 I/O 완료 통지가 올 때까지 대기한다.
- 읽기 작업이 완료되면 임시버퍼에 저장된 데이터를 내부 링버퍼로 복사한다.
송신 - Write
bool Serial::writeBytes(const char* data, int size) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_handle == INVALID_HANDLE_VALUE)
return false;
// 이전 이벤트 상태 초기화
ResetEvent(m_ov_write.hEvent);
DWORD wr = 0;
// Write 요청, overlapped 모드이므로 즉시 반환
if (!WriteFile(m_handle, data, size, &wr, &m_ov_write)) {
// 비동기 처리 중이면 I/O 완료 이벤트를 대기
if (GetLastError() == ERROR_IO_PENDING)
WaitForSingleObject(m_ov_write.hEvent, INFINITE);
// Write 완료 결과 회수(wr: Write 요청으로 실제 수신한 바이트 수)
GetOverlappedResult(m_handle, &m_ov_write, &wr, FALSE);
}
return wr == (DWORD)size;
}
송신의 경우 언제 데이터가 들어올지 알 수 없어 계속 감시해야하는 수신과 달리, 사용자가 명시적으로 요청한 시점에 처리하고 끝내면 되므로 구조가 훨씬 단순하다.
흐름은 WriteFile을 Overlapped 방식으로 호출하고, ERROR_IO_PENDING이 반환되면 I/O 완료 통지가 올 때까지 대기한 뒤 GetOverlappedResult를 통해 실제 송신 완료 결과를 확인한다.
그리고 write 요청에 쓰이는 overlapped 구조체는 지역변수로 만들면 매번 CreateEvent 호출 비용이 발생하므로 클래스 멤버변수로 두고 재사용하게끔 했다.
사용 예시
집에서 com0com 가상 시리얼 에뮬레이터를 사용해 COM3 - COM4, COM5 - COM6 포트가 서로 연결되도록 구성한 환경에서 테스트를 진행했다.

예시 1.
다음 예시는 두 개의 시리얼 포트를 동시에 열어, 각 포트로 수신되는 데이터를 콘솔에 출력하고 콘솔에서 입력되는 데이터를 양쪽 포트로 송신하는 간단한 프로그램이다.
#include <iostream>
#include <csignal>
#include <atomic>
#include "Serial.h"
#include <conio.h>
static std::atomic<bool> g_running{ true };
void signalHandler(int) {
g_running = false;
}
int main(int argc, char* argv[]) {
std::signal(SIGINT, signalHandler);
Serial serialA;
Serial serialB;
if (!serialA.open("COM3", 115200)) {
std::cerr << "Failed to open port: " << "\n";
return 1;
}
if (!serialB.open("COM5", 115200)) {
std::cerr << "Failed to open port: " << "\n";
return 1;
}
std::cout << "Serial test program running... (press Ctrl+C or ESC to quit)" << std::endl;
while (g_running) {
while (serialA.available() > 0) {
char c = serialA.read();
std::cout << c << std::flush;
}
while (serialB.available() > 0) {
char c = serialB.read();
std::cout << c << std::flush;
}
if (_kbhit()) {
char c = _getch();
if (c == 27) break;
serialA.write(c);
serialB.write(c);
std::cout << c << std::flush;
if (c == '\r') std::cout << "\n" << std::flush;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
serialA.close();
serialB.close();
return 0;
}

예시 2.
다음은 클래스에서 제공하는 API를 대부분 한번 씩 사용해보는 테스트 코드이다.
#include <iostream>
#include "Serial.h"
int main(int argc, char* argv[]) {
Serial serial;
if (!serial.open("COM3", 115200)) {
std::cerr << "Failed to open port: " << "\n";
return 1;
}
serial.writeBytes("ABC", 3);
serial.write('D');
serial.writeString("hello");
serial.setEndOfLine("\r");
serial.writeLine(" world!");
serial.setTimeout(5000);
char rx[256];
int len;
if (char c = serial.read(); c >= 0)
std::cout << "read: " << c << std::endl;
else
std::cout << "read: timeout" << std::endl;
if (len = serial.readBytes(rx, 2); len >= 0) {
rx[len] = '\0';
std::cout << "readLine: " << rx << std::endl;
}
else {
std::cout << "readBytes: timeout" << std::endl;
}
if (len = serial.readBytesUntil('!', rx, 256); len >= 0) {
rx[len] = '\0';
std::cout << "readBytesUntil: " << rx << std::endl;
}
else {
std::cout << "readBytesUntil: timeout" << std::endl;
}
if (auto line = serial.readLine(); !line.empty())
std::cout << "readLine: " << line << std::endl;
else
std::cout << "readLine: timeout" << std::endl;
serial.close();
return 0;
}
API 네이밍과 조합은 보면 알겠지만 아두이노 시리얼 라이브러리를 참고해 유사하게 지었다.
Write 계열 API
- bool write(char data);
- bool writeBytes(const char* data, int size);
- bool writeString(const std::string& s);
- bool writeLine(const std::string& s);
write 계열 함수의 핵심은 writeBytes 함수이고 나머지는 다 이 함수를 감싼 wrapper 형태로 구성했다.
Read 계열 API
- char read();
- int readBytes(char* buf, int size);
- int readBytesUntil(char delim, char* buf, int size);
- std::string readLine();
read 계열 함수의 핵심은 private 함수인 readImpl 함수이고 나머지는 다 이 함수를 감싼 wrapper 형태로 구성했다.

그리고 setTimeout() 함수를 제공해서 read 계열 함수에 공통적으로 적용되는 타임아웃 정책을 사용자가 직접 설정할 수 있도록 하였는데 이것도 역시 아두이노 스타일이다. 물론 ReadFile 자체는 Overlapped 방식으로 즉시 반환되도록 구성되어 있기 때문에, 사용자가 설정한 타임아웃 값은 read 계열 함수 내부에서 링버퍼 데이터를 대기할 때 적용된다.
void setTimeout(int timeout_ms); // 0: non-block, <0: infinite
int getTimeout() const;
또 하나 특이하다고 할법한 점은 setEndOfLine() 함수를 통해 개행 문자를 설정할 수 있게끔 한 부분이다. 회사에서 계측장비와 시리얼 통신을 구현할 경우가 많은데, SCPI 프로토콜을 사용하는 장비들은 명령의 끝을 개행 문자로 구분한다.
다만 장비에 따라 그 개행 문자가 '\r', \n', "\r\n" 등 제각각이기 때문에 셋 중 어떤 개행문자가 들어와도 자동으로 처리하는 방식이 아니라, readLine() / wrtieLine()에서는 미리 설정된 기준에 따라서만 처리하도록 했다.
void setEndOfLine(const std::string& eol);
std::string getEndOfLine() const;
'프로그래밍 > C++' 카테고리의 다른 글
| C++ ] 템플릿을 활용한 thread-safe 링버퍼 클래스 (0) | 2026.02.05 |
|---|---|
| C++ ] 함수 템플릿, 클래스 템플릿 (0) | 2025.12.30 |
| C++ ] 싱글톤(Singleton) 클래스 (0) | 2025.12.15 |
| C++ ] constexpr (1) | 2025.11.30 |
| C++] fstream의 open mode (0) | 2025.07.24 |