在C语言中将十六进制字符串转换为字节数组

68

有没有标准的C函数可以将十六进制字符串转换为字节数组
我不想自己编写函数。


1
你的意思是一个包含表示十六进制数字的字符的字符串吗? - Nate
1
是的,我有用户输入字符串,例如“abCD12ff34”,长度大于等于0,我想将其转换为字节数组,如0xaa、0xcd、0x12等。 - Szere Dyeri
22个回答

80
据我所知,没有标准函数可以实现这个功能,但是可以通过以下方式轻松实现:
#include <stdio.h>

int main(int argc, char **argv) {
    const char hexstring[] = "DEadbeef10203040b00b1e50", *pos = hexstring;
    unsigned char val[12];

     /* WARNING: no sanitization or error-checking whatsoever */
    for (size_t count = 0; count < sizeof val/sizeof *val; count++) {
        sscanf(pos, "%2hhx", &val[count]);
        pos += 2;
    }

    printf("0x");
    for(size_t count = 0; count < sizeof val/sizeof *val; count++)
        printf("%02x", val[count]);
    printf("\n");

    return 0;
}

编辑

正如 Al 指出的那样,在字符串中有奇数个十六进制数字时,你必须确保在其前面添加一个起始的 0。例如,字符串 "f00f5" 将被以上示例错误地解释为 {0xf0, 0x0f, 0x05},而不是正确的 {0x0f, 0x00, 0xf5}

稍微更改了示例以回应 @MassimoCallegari 的评论。


6
这是一个不错的方法,但请注意,如果十六进制字符串中的数字个数为奇数,则会得到错误的结果(隐式零将作为前缀添加到最后一位数字而不是第一位,因此“5ab5c”将被打印为0x5ab50c而不是0x05ab5c)。 - DrAl
这里字节序是否重要呢? - Geremia
1
@Geremia 不是的。字节序只在将字节数组重新解释为整数类型或反之时才起作用。http://commandcenter.blogspot.com/2012/04/byte-order-fallacy.html ED:除非你很奇怪,会以小端顺序编写十六进制文字。无论如何,这将每对数字转换为一个字节;结果的字节序与原始字节序相同。 - josaphatv
如果源字符串是大写的,这个代码能正常工作吗?例如:"DEADBEEF..." - Massimo Callegari
1
@MassimoCallegari,是的,因为scanf的x转换说明符适用于像strtoul这样的字符串,它期望按照C11 6.4.4.1中定义的输入进行。 - Michael Foukarakis
请检查我的方法...我的方法允许使用空格作为分隔符并且包含大小写字母:https://dev59.com/HHA75IYBdhLWcg3wOWVd#56247335 - Zibri

21

我通过谷歌搜索找到了这个问题。我不喜欢调用sscanf()或strtol(),因为感觉有点过度。我写了一个快速函数,它不验证文本是否确实是字节流的十六进制表示形式,但可以处理奇数个十六进制数字:

uint8_t tallymarker_hextobin(const char * str, uint8_t * bytes, size_t blen)
{
   uint8_t  pos;
   uint8_t  idx0;
   uint8_t  idx1;

   // mapping of ASCII characters to hex values
   const uint8_t hashmap[] =
   {
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //  !"#$%&'
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ()*+,-./
     0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567
     0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>?
     0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // @ABCDEFG
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // HIJKLMNO
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // PQRSTUVW
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // XYZ[\]^_
     0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // `abcdefg
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hijklmno
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pqrstuvw
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // xyz{|}~.
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // ........
   };

   bzero(bytes, blen);
   for (pos = 0; ((pos < (blen*2)) && (pos < strlen(str))); pos += 2)
   {
      idx0 = (uint8_t)str[pos+0];
      idx1 = (uint8_t)str[pos+1];
      bytes[pos/2] = (uint8_t)(hashmap[idx0] << 4) | hashmap[idx1];
   };

   return(0);
}

7
通过添加4条逻辑指令,你可以将地图大小缩小到32字节,这是一个很好的权衡:idx0 = (uint8_t)str[pos+0] & 0x1F ^ 0x10;对于 idx1 同样如此。然后你可以移除 01234567 行之前和 HIJKLMNO 行之后的所有字节。 - 5andr0
@5andr0,发布为Gist:https://gist.github.com/vi/dd3b5569af8a26b97c8e20ae06e804cb。尚未检查。 - Vi.
2
sscanfstrtol可能有些过度,但是一个不必要的32行十六进制表却不是? - Jakob
1
对于字符串长度超过255的情况,此行代码将失败,因为pos是uint8_t类型,因此循环条件将始终为真。代码如下:for (pos = 0; ((pos < (blen*2)) && (pos < strlen(str))); pos += 2) - pbn
3
在运行100万次迭代时,我的代码平均完成时间为0.011秒,而你的代码平均完成时间为0.113秒。我的关于过度设计的评论是指运行时效率。回顾评论,有一些很好的建议可以改进我的代码,包括检查循环的pos变量。另外,当编写输出函数时应始终包含边界检查,因为如果输入的十六进制数长度超过了输出缓冲区的大小,你的函数可能会导致缓冲区溢出。 - David M. Syzdek
显示剩余4条评论

13

除了上面优秀的回答,我想写一个不使用任何库并且对错误字符串做了一些防护的C函数。

uint8_t* datahex(char* string) {

    if(string == NULL) 
       return NULL;

    size_t slength = strlen(string);
    if((slength % 2) != 0) // must be even
       return NULL;

    size_t dlength = slength / 2;

    uint8_t* data = malloc(dlength);
    memset(data, 0, dlength);

    size_t index = 0;
    while (index < slength) {
        char c = string[index];
        int value = 0;
        if(c >= '0' && c <= '9')
          value = (c - '0');
        else if (c >= 'A' && c <= 'F') 
          value = (10 + (c - 'A'));
        else if (c >= 'a' && c <= 'f')
          value = (10 + (c - 'a'));
        else {
          free(data);
          return NULL;
        }

        data[(index/2)] += value << (((index + 1) % 2) * 4);

        index++;
    }

    return data;
}

说明:

a. index / 2 | 整数相除会向下取整,所以0/2=0,1/2=0,2/2=1,3/2=1,4/2=2,5/2=2等。因此,对于每2个字符串字符,我们将该值添加到1个数据字节中。

b. (index + 1) % 2 | 我们希望奇数结果为1,偶数为0,因为十六进制字符串的第一个数字是最重要的数字,并且需要乘以16。所以对于索引0 => 0 + 1 % 2 = 1,索引1 => 1 + 1 % 2 = 0等。

c. << 4 | 左移4位相当于乘以16。例如:b00000001 << 4 = b00010000


2
它在偶数时失败。if(slength % 2 == 0) 应该是 if(slength % 2 != 0)。否则似乎工作正常。 - sudo
1
谢谢!刚刚修复了那个问题。 - Mike M
1
不错。非常适合嵌入式系统。 - ataraxic
@MikeM 注意不要在你的程序中返回指向动态分配缓冲区(data)的指针!这不是 C 风格,让用户提供缓冲区。 - IMAN4K
1
似乎存在内存泄漏。如果遇到非十六进制字符,则返回“NULL”,但分配的“data”缓冲区未被释放。 - Howard Yeh
@HowardYeh 谢谢。已修复! - Mike M

9

对于短字符串,strtolstrtollstrtoimax可以正常工作(请注意第三个参数是处理字符串时使用的进制...将其设置为16)。 如果您的输入超过 最长整数类型字位数/4,则需要使用其他答案建议的更灵活的方法之一。


5
迈克尔·福卡拉基斯的帖子的详细版本:
#include <stdio.h>
#include <string.h>

void print(unsigned char *byte_array, int byte_array_size)
{
    int i = 0;
    printf("0x");
    for(; i < byte_array_size; i++)
    {
        printf("%02x", byte_array[i]);
    }
    printf("\n");
}

int convert(const char *hex_str, unsigned char *byte_array, int byte_array_max)
{
    int hex_str_len = strlen(hex_str);
    int i = 0, j = 0;

    // The output array size is half the hex_str length (rounded up)
    int byte_array_size = (hex_str_len+1)/2;

    if (byte_array_size > byte_array_max)
    {
        // Too big for the output array
        return -1;
    }

    if (hex_str_len % 2 == 1)
    {
        // hex_str is an odd length, so assume an implicit "0" prefix
        if (sscanf(&(hex_str[0]), "%1hhx", &(byte_array[0])) != 1)
        {
            return -1;
        }

        i = j = 1;
    }

    for (; i < hex_str_len; i+=2, j++)
    {
        if (sscanf(&(hex_str[i]), "%2hhx", &(byte_array[j])) != 1)
        {
            return -1;
        }
    }

    return byte_array_size;
}

void main()
{
    char *examples[] = { "", "5", "D", "5D", "5Df", "deadbeef10203040b00b1e50", "02invalid55" };
    unsigned char byte_array[128];
    int i = 0;

    for (; i < sizeof(examples)/sizeof(char *); i++)
    {
        int size = convert(examples[i], byte_array, 128);
        if (size < 0)
        {
            printf("Failed to convert '%s'\n", examples[i]);
        }
        else if (size == 0)
        {
            printf("Nothing to convert for '%s'\n", examples[i]);
        }
        else
        {
            print(byte_array, size);
        }
    }
}

5

通过对用户411313代码的一些修改,以下内容适用于我:

#include <stdio.h>
#include <stdint.h> 
#include <string.h>

int main ()
{
    char *hexstring = "deadbeef10203040b00b1e50";
    int i;
    unsigned int bytearray[12];
    uint8_t str_len = strlen(hexstring);

    for (i = 0; i < (str_len / 2); i++) {
        sscanf(hexstring + 2*i, "%02x", &bytearray[i]);
        printf("bytearray %d: %02x\n", i, bytearray[i]);
    }

    return 0;
}

1
这也会写入缓冲区的末尾。 - mafu
int i; ... uint8_t str_len 都应该改为 size_t - chux - Reinstate Monica
这段代码还允许十六进制字节之间有空格,例如 char *hexstring = "de ad be ef 10 20 30 40 b0 0b 1e 50";,它可以正常工作。太好了!编辑:但我们应该调整循环。 - ollydbg23
12 可以被 str_len 替换,也许只需要使用字符数组来节省空间? - LinconFive

3

你可以使用这个函数,它是为了性能而设计的(适用于嵌入式处理器),没有scanfstrtol动态内存分配

  1. 对输出缓冲区溢出和奇怪的输入字符串长度有防护措施
/// in: valid chars are 0-9 + A-F + a-f
/// out_len_max==0: convert until the end of input string, out_len_max>0 only convert this many numbers
/// returns actual out size
int hexStr2Arr(unsigned char* out, const char* in, size_t out_len_max = 0)
{
    if (!out_len_max)
        out_len_max = 2147483647; // INT_MAX

    const int in_len = strnlen(in, out_len_max * 2);
    if (in_len % 2 != 0)
        return -1; // error, in str len should be even

    // calc actual out len
    const int out_len = out_len_max < (in_len / 2) ? out_len_max : (in_len / 2);

    for (int i = 0; i < out_len; i++) {
        char ch0 = in[2 * i];
        char ch1 = in[2 * i + 1];
        uint8_t nib0 = (ch0 & 0xF) + (ch0 >> 6) | ((ch0 >> 3) & 0x8);
        uint8_t nib1 = (ch1 & 0xF) + (ch1 >> 6) | ((ch1 >> 3) & 0x8);
        out[i] = (nib0 << 4) | nib1;
    }
    return out_len;
}

使用说明:

unsigned char result[128];
memset(result, 0, 128); // optional
printf("result len=%d\n", hexStr2Arr(result, "0a0B10"));  // result = [0A 0B 10 00 00 ...]

memset(result, 0, 128); // optional
// only convert single number
printf("result len=%d\n", hexStr2Arr(result, "0a0B10", 1)); // result = [0A 00 00 00 00 ...]

3

这里有相对清晰易读的十六进制转二进制和二进制转十六进制代码。(请注意,原本通过错误日志系统返回的是枚举错误代码,而不是简单的-1或-2.)

typedef unsigned char ByteData;
ByteData HexChar (char c)
{
    if ('0' <= c && c <= '9') return (ByteData)(c - '0');
    if ('A' <= c && c <= 'F') return (ByteData)(c - 'A' + 10);
    if ('a' <= c && c <= 'f') return (ByteData)(c - 'a' + 10);
    return (ByteData)(-1);
}

ssize_t HexToBin (const char* s, ByteData * buff, ssize_t length)
{
    ssize_t result = 0;
    if (!s || !buff || length <= 0) return -2;

    while (*s)
    {
        ByteData nib1 = HexChar(*s++);
        if ((signed)nib1 < 0) return -3;
        ByteData nib2 = HexChar(*s++);
        if ((signed)nib2 < 0) return -4;

        ByteData bin = (nib1 << 4) + nib2;

        if (length-- <= 0) return -5;
        *buff++ = bin;
        ++result;
    }
    return result;
}

void BinToHex (const ByteData * buff, ssize_t length, char * output, ssize_t outLength)
{
    char binHex[] = "0123456789ABCDEF";

    if (!output || outLength < 4) return (void)(-6);
    *output = '\0';

    if (!buff || length <= 0 || outLength <= 2 * length)
    {
        memcpy(output, "ERR", 4);
        return (void)(-7);
    }

    for (; length > 0; --length, outLength -= 2)
    {
        ByteData byte = *buff++;

        *output++ = binHex[(byte >> 4) & 0x0F];
        *output++ = binHex[byte & 0x0F];
    }
    if (outLength-- <= 0) return (void)(-8);
    *output++ = '\0';
}

3

以下是我为了性能而编写的解决方案:

void hex2bin(const char* in, size_t len, unsigned char* out) {

  static const unsigned char TBL[] = {
     0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  58,  59,  60,  61,
    62,  63,  64,  10,  11,  12,  13,  14,  15,  71,  72,  73,  74,  75,
    76,  77,  78,  79,  80,  81,  82,  83,  84,  85,  86,  87,  88,  89,
    90,  91,  92,  93,  94,  95,  96,  10,  11,  12,  13,  14,  15
  };

  static const unsigned char *LOOKUP = TBL - 48;

  const char* end = in + len;

  while(in < end) *(out++) = LOOKUP[*(in++)] << 4 | LOOKUP[*(in++)];

}

例子:

unsigned char seckey[32];

hex2bin("351aaaec0070d13d350afae2bc43b68c7e590268889869dde489f2f7988f3fee", 64, seckey);

/*
  seckey = {
     53,  26, 170, 236,   0, 112, 209,  61,  53,  10, 250, 226, 188,  67, 182, 140, 
    126,  89,   2, 104, 136, 152, 105, 221, 228, 137, 242, 247, 152, 143,  63, 238
  };
*/

如果您不需要支持小写字母:
static const unsigned char TBL[] = {
     0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  58,  59,
    60,  61,  62,  63,  64,  10,  11,  12,  13,  14,  15
};

1
警告:'in'存在多个未排序的修改[-Wunsequenced] - evandrix
你正在使用双重“in ++”调用未定义的行为。 - Astrinus

2

hextools.h

#ifndef HEX_TOOLS_H
#define HEX_TOOLS_H

char *bin2hex(unsigned char*, int);

unsigned char *hex2bin(const char*);

#endif // HEX_TOOLS_H

hextools.c

#include <stdlib.h>

char *bin2hex(unsigned char *p, int len)
{
    char *hex = malloc(((2*len) + 1));
    char *r = hex;

    while(len && p)
    {
        (*r) = ((*p) & 0xF0) >> 4;
        (*r) = ((*r) <= 9 ? '0' + (*r) : 'A' - 10 + (*r));
        r++;
        (*r) = ((*p) & 0x0F);
        (*r) = ((*r) <= 9 ? '0' + (*r) : 'A' - 10 + (*r));
        r++;
        p++;
        len--;
    }
    *r = '\0';

    return hex;
}

unsigned char *hex2bin(const char *str)
{
    int len, h;
    unsigned char *result, *err, *p, c;

    err = malloc(1);
    *err = 0;

    if (!str)
        return err;

    if (!*str)
        return err;

    len = 0;
    p = (unsigned char*) str;
    while (*p++)
        len++;

    result = malloc((len/2)+1);
    h = !(len%2) * 4;
    p = result;
    *p = 0;

    c = *str;
    while(c)
    {
        if(('0' <= c) && (c <= '9'))
            *p += (c - '0') << h;
        else if(('A' <= c) && (c <= 'F'))
            *p += (c - 'A' + 10) << h;
        else if(('a' <= c) && (c <= 'f'))
            *p += (c - 'a' + 10) << h;
        else
            return err;

        str++;
        c = *str;

        if (h)
            h = 0;
        else
        {
            h = 4;
            p++;
            *p = 0;
        }
    }

    return result;
}

main.c

#include <stdio.h>
#include "hextools.h"

int main(void)
{
    unsigned char s[] = { 0xa0, 0xf9, 0xc3, 0xde, 0x44 };

    char *hex = bin2hex(s, sizeof s);
    puts(hex);

    unsigned char *bin;
    bin = hex2bin(hex);

    puts(bin2hex(bin, 5));

    size_t k;
    for(k=0; k<5; k++)
        printf("%02X", bin[k]);

    putchar('\n');

    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接