HOWTO · C++

C++에서 문자열을 16 진수로 변환

이 기사에서는 C++에서 문자열을 16 진수 값으로 변환하는 방법을 소개합니다.

이 페이지의 내용

C++에서 std::string를 16진수로 변환한다는 것은 문자열의 바이트를 텍스트 표현으로 만든다는 뜻입니다. 각 char는 두 자리 16진수 값이 되므로 "Hello"는 48656c6c6f가 됩니다. 이 변환은 바이너리 데이터를 디버깅하거나, 체크섬이나 핑거프린트를 생성하거나, 16진수로 인코딩된 페이로드를 요구하는 프로토콜에 데이터를 전달할 때 필요합니다.

이 기사에서는 16진수 출력을 직접 출력하는 방법, 재사용 가능한 16진수 std::string를 만드는 방법, 모든 바이트를 두 자리로 유지하는 방법, 그리고 그 결과가 텍스트 인코딩과 어떻게 관련되는지를 보여 준 다음, 각 접근 방식을 비교해 적합한 방법을 선택할 수 있도록 합니다.

문자열을 16진수 바이트로 변환

가장 직접적인 방법은 입력을 한 바이트씩 순회하면서 각 값을 std::cout에 16진수로 출력하는 것입니다. 스트림 조작자 std::hex는 정수 출력을 16진수로 전환하고, std::setw(2)와 std::setfill('0')은 각 바이트를 두 자리로 유지합니다. unsigned char로 순회하면 모든 바이트가 0–255 범위에 머물므로, 일반 char가 부호 있는 타입인 플랫폼에서도 0x7f를 초과하는 바이트가 올바르게 포맷됩니다. 다음 예제를 실행해 보세요:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

#include <iomanip>
#include <iostream>
#include <string>

int main() {
    std::string input = "Hello";

    std::cout << std::hex << std::setfill('0');
    for (unsigned char c : input) {
        std::cout << std::setw(2) << static_cast<int>(c);
    }
    std::cout << '\n';
}
48656c6c6f

std::hex는 변경하기 전까지 계속 적용되므로 루프 전에 한 번만 설정하면 됩니다. std::setw(2)는 다음에 삽입되는 값에만 적용되므로 루프 안에 두어야 하고, std::setfill('0')은 계속 유지됩니다. std::cout으로 출력하는 방식은 빠르게 확인하기에는 좋지만, 변환된 텍스트는 화면에서 지나가는 순간 사라집니다. 다음 섹션에서는 이를 보관하는 방법을 다룹니다.

16진수 문자열 만들어 저장하기

변환된 텍스트가 나중에 필요할 때 — 함수에서 반환하거나, 로그 메시지에 추가하거나, 파일에 써야 할 때 — std::cout 대신 std::stringstream에 누적합니다. 동일한 포맷 조작자가 적용되며, str()은 결과를 담은 std::string를 생성합니다:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>

std::string toHex(const std::string& input) {
    std::stringstream ss;
    ss << std::hex << std::setfill('0');
    for (unsigned char c : input) {
        ss << std::setw(2) << static_cast<int>(c);
    }
    return ss.str();
}

int main() {
    std::string text = "C++ Programming";
    std::string hexText = toHex(text);
    std::cout << "string: " << text << '\n';
    std::cout << "hexval: " << hexText << '\n';
}
string: C++ Programming
hexval: 432b2b2050726f6772616d6d696e67

위 출력의 string:와 hexval: 레이블은 예제 코드와 바이트 단위로 동일하게 유지되므로 영어로 표시됩니다. toHex가 std::string를 반환하므로 호출자가 이를 어떻게 사용할지 결정합니다: 출력하거나, 저장하거나, 파일 스트림에 전달할 수 있습니다. 이 예제는 값을 출력하기만 합니다. 파일에 쓰는 것은 동일한 16진수 텍스트를 받아들이는 std::ofstream을 통한 별도 단계입니다.

각 바이트를 두 자리 16진수로 포맷

바이트는 0에서 255까지의 값(0x00–0xff)을 담으므로 두 자리면 항상 충분합니다. 패딩이 없으면 0x10 미만의 값은 한 자리로 출력되어 결과가 모호해집니다. 바이트 쌍 0x01, 0x0a와 단일 바이트 0x1a가 모두 1a를 만들기 때문입니다. std::setw(2)는 최소 필드 폭을 설정하고 std::setfill('0')은 패딩 문자를 지정합니다:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

#include <iomanip>
#include <iostream>
#include <string>

int main() {
    std::string input = "A\nB";

    std::cout << std::hex << std::setfill('0');
    for (unsigned char c : input) {
        std::cout << std::setw(2) << static_cast<int>(c);
    }
    std::cout << '\n';
}
410a42

가운데 바이트는 0x0a이지만 여전히 두 자리로 출력됩니다. std::setw와 std::setfill을 제거하면 같은 입력이 41a42로 출력되어 한 바이트가 어디서 끝나고 다음 바이트가 어디서 시작하는지 더 이상 알 수 없게 됩니다. std::setw는 포맷된 값마다 초기화되므로 루프 안에 두고, std::setfill은 한 번만 설정하면 됩니다.

바이트 값과 텍스트 인코딩 이해하기

지금까지의 모든 예제는 문자가 아니라 바이트를 변환합니다. std::string는 char의 시퀀스를 저장하고, 루프는 각각의 숫자 값을 포맷합니다. ASCII 입력에서는 이 구분이 드러나지 않습니다 — H는 단일 바이트 0x48입니다 — 하지만 다른 텍스트에서는 중요합니다. UTF-8에서 é(코드 포인트 U+00E9)는 두 바이트 0xc3 0xa9로 저장되므로, 같은 루프는 e9가 아니라 c3a9를 생성합니다:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

#include <iomanip>
#include <iostream>
#include <string>

int main() {
    std::string input = "\xc3\xa9";  // UTF-8 bytes for U+00E9

    std::cout << std::hex << std::setfill('0');
    for (unsigned char c : input) {
        std::cout << std::setw(2) << static_cast<int>(c);
    }
    std::cout << '\n';
}
c3a9

따라서 이 기사의 함수들은 문자열이 담고 있는 것이 무엇이든 그 인코딩된 바이트를 설명합니다. 예제 코드의 영어 주석은 원문과 바이트 단위로 동일하게 유지되며, 입력이 U+00E9의 UTF-8 바이트임을 나타냅니다. 대신 16진수 코드 포인트가 필요하다면 먼저 텍스트를 디코딩한 다음 디코딩된 값을 포맷하세요. 부호 없는 바이트 처리가 중요한 이유도 같습니다: 일반 char가 부호 있는 곳에서는 0xc3 같은 바이트가 0 미만의 값으로 변환되어 ffffffc3으로 출력됩니다. unsigned char로 순회하거나 static_cast<int> 전에 이를 거쳐 캐스팅하면 값이 0xc3으로 유지됩니다.

변환 접근 방식 선택하기

데이터를 한 번만 확인하면 되는 경우에는 std::cout 루프를 직접 사용하세요. 결과가 호출 이후에도 유지되어야 한다면 std::stringstream로 만든 std::string를 반환하세요. 이것이 가장 읽기 쉬운 범용적인 선택이며 std::hex, std::setw, std::setfill 포맷을 한 곳에 모아 둡니다.

세 번째 방법은 각 바이트를 두 개의 4비트 절반(니블)으로 나누고 각각을 룩업 테이블을 통해 매핑하는 것입니다:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

#include <iostream>
#include <string>

std::string toHexLookup(const std::string& input) {
    static const char digits[] = "0123456789abcdef";
    std::string result;
    result.reserve(input.size() * 2);
    for (unsigned char c : input) {
        result += digits[c >> 4];
        result += digits[c & 0x0f];
    }
    return result;
}

int main() {
    std::cout << toHexLookup("World") << '\n';
}
576f726c64

룩업 버전은 바이트당 항상 정확히 두 자리를 출력하고 스트림 포맷을 전혀 수행하지 않으므로, 일관성을 위해 이 방식을 선호하는 코드베이스도 있습니다. 더 빠르게 실행되는지는 표준 라이브러리 구현에 따라 다르므로, 그것이 중요하다면 실제 데이터로 직접 측정하세요. 어떤 접근 방식을 선택하든 결과는 바이트를 설명합니다. ASCII 텍스트에서는 문자당 한 쌍으로 읽히고, UTF-8이나 바이너리 데이터에서는 인코딩된 바이트당 한 쌍으로 읽힙니다.