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

117

我有:

uint8 buf[] = {0, 1, 10, 11};

我想将字节数组转换为字符串,以便我可以使用printf打印字符串:

printf("%s\n", str);

获取(冒号不必要):

"00:01:0A:0B"
任何帮助都将不胜感激。

buf[i] must be casted to unsigned char, or it will overflow if buf[i] > 127, that is:buf_ptr += sprintf(buf_ptr, "%02X", (unsigned char)buf[i]); - whatacold
19个回答

118
printf("%02X:%02X:%02X:%02X", buf[0], buf[1], buf[2], buf[3]);

更通用的方式:

int i;
for (i = 0; i < x; i++)
{
    if (i > 0) printf(":");
    printf("%02X", buf[i]);
}
printf("\n");

要将字符串连接起来,可以有几种方法。我可能会保留指向字符串末尾的指针并使用sprintf。你还应该跟踪数组的大小,以确保它不会超过分配的空间:

int i;
char* buf2 = stringbuf;
char* endofbuf = stringbuf + sizeof(stringbuf);
for (i = 0; i < x; i++)
{
    /* i use 5 here since we are going to add at most 
       3 chars, need a space for the end '\n' and need
       a null terminator */
    if (buf2 + 5 < endofbuf)
    {
        if (i > 0)
        {
            buf2 += sprintf(buf2, ":");
        }
        buf2 += sprintf(buf2, "%02X", buf[i]);
    }
}
buf2 += sprintf(buf2, "\n");

谢谢Mark - 我的问题有点复杂。实际上,我有一个长度为X字节的缓冲区。我希望找到一种通用的方法来处理X字节,并将其作为字符串结果返回。 - Steve Walsh
是的,这会变得有点困难。我更新了帖子,使用sprintf,并使用一个额外的指针来跟踪我想要写入的位置(即字符串的末尾)。 - Mark Synowiec
@Mark,我刚刚提交了一个答案,但是因为你先提交了,所以我只想指出你可能需要在末尾添加一个\0字节。 - Mr. Shickadance
7
应该使用printf("%02X", (unsigned char)buf[i]);,原始代码会导致无符号字符溢出。 - easytiger
3
为什么不使用 printf("%02hhX", buf[i]) - Hintron
显示剩余3条评论

43

为了完整起见,您也可以轻松地在不调用任何重型库函数(无snprintf,无strcat,甚至无memcpy)的情况下执行此操作。如果您正在编写一些微控制器或OS内核程序,并且没有可用的libc,则可能会很有用。

如果您搜索这个问题,您可以找到类似的代码,这并不是什么高级玩意儿。 它确实比调用snprintf简单得多,而且速度更快。

#include <stdio.h>

int main(){
    unsigned char buf[] = {0, 1, 10, 11};
    /* target buffer should be large enough */
    char str[12];

    unsigned char * pin = buf;
    const char * hex = "0123456789ABCDEF";
    char * pout = str;
    int i = 0;
    for(; i < sizeof(buf)-1; ++i){
        *pout++ = hex[(*pin>>4)&0xF];
        *pout++ = hex[(*pin++)&0xF];
        *pout++ = ':';
    }
    *pout++ = hex[(*pin>>4)&0xF];
    *pout++ = hex[(*pin)&0xF];
    *pout = 0;

    printf("%s\n", str);
}

这里有另一个稍微更短的版本。它仅仅避免了中间索引变量 i 并且去除了最后一个情况代码的重复(但是终止字符写了两次)。

#include <stdio.h>
int main(){
    unsigned char buf[] = {0, 1, 10, 11};
    /* target buffer should be large enough */
    char str[12];

    unsigned char * pin = buf;
    const char * hex = "0123456789ABCDEF";
    char * pout = str;
    for(; pin < buf+sizeof(buf); pout+=3, pin++){
        pout[0] = hex[(*pin>>4) & 0xF];
        pout[1] = hex[ *pin     & 0xF];
        pout[2] = ':';
    }
    pout[-1] = 0;

    printf("%s\n", str);
}

以下是另一种回答评论的版本,评论中说我使用了一个“技巧”来知道输入缓冲区的大小,但实际上这不是技巧,而是必要的输入知识(需要知道要转换的数据的大小)。通过将转换代码提取到单独的函数中,我使这一点更加清晰。我还为目标缓冲区添加了边界检查代码,如果我们知道我们在做什么,这并不是真正必要的。

#include <stdio.h>

void tohex(unsigned char * in, size_t insz, char * out, size_t outsz)
{
    unsigned char * pin = in;
    const char * hex = "0123456789ABCDEF";
    char * pout = out;
    for(; pin < in+insz; pout +=3, pin++){
        pout[0] = hex[(*pin>>4) & 0xF];
        pout[1] = hex[ *pin     & 0xF];
        pout[2] = ':';
        if (pout + 3 - out > outsz){
            /* Better to truncate output string than overflow buffer */
            /* it would be still better to either return a status */
            /* or ensure the target buffer is large enough and it never happen */
            break;
        }
    }
    pout[-1] = 0;
}

int main(){
    enum {insz = 4, outsz = 3*insz};
    unsigned char buf[] = {0, 1, 10, 11};
    char str[outsz];
    tohex(buf, insz, str, outsz);
    printf("%s\n", str);
}

1
这不是一个技巧,只是一个常数。在问题的上下文中,我们要转换为十六进制的源长度是众所周知的(我本可以放一些硬编码的4而不是sizeof)。在一般情况下,函数应该在已知长度的某些输入上调用,并且目标缓冲区有3倍+1个字节可用。这必须由调用者确保,没有理由让转换函数执行该任务。在某些情况下,调用strlen()可能是找到源大小的一种方法,但并非总是如此。如果要转换为十六进制的数字包含零怎么办? - kriss
受你的函数启发,我编写了一个版本,类似于snprintf等函数,也返回写入输出缓冲区的字节数。https://gist.github.com/cellularmitosis/0d8c0abf7f8aa6a2dff3 - Jason Pepas
我上面显式使用sizeof表达式的优点是,如果您将另一个值添加到初始化器列表中,它可以通过自动调整输出缓冲区大小来保护您。在您的更新版本中,您必须希望常量是正确/维护的。断言可能是另一种路线,在某些情况下更灵活。 - Cecil Ward
截断缓冲区的问题在于,在某些情况下,用户可能会遗漏一半的数据结构,因此可能会读取输出末尾之后的垃圾数据。它还会对零终止符的存在或不存在做出各种奇怪的假设,并通过在最后一个字节上写入零来破坏输出(即使用户不需要)。最好的方法是不做任何假设,也不添加任何零终止符。这是给调用者的建议。 - Cecil Ward
@Cecil Ward:什么都不做确实是一种选择。这就是我在我的代码注释中写的内容。 - kriss
显示剩余4条评论

32

以上已经有类似的答案了,我之所以添加这个答案是为了解释下面这行代码的具体工作原理:

ptr += sprintf(ptr, "%02X", buf[i])

这个问题有点棘手,理解起来并不容易。我将解释放在下面的注释中:

uint8 buf[] = {0, 1, 10, 11};

/* Allocate twice the number of bytes in the "buf" array because each byte would
 * be converted to two hex characters, also add an extra space for the terminating
 * null byte.
 * [size] is the size of the buf array */
char output[(size * 2) + 1];

/* pointer to the first item (0 index) of the output array */
char *ptr = &output[0];

int i;

for (i = 0; i < size; i++) {
    /* "sprintf" converts each byte in the "buf" array into a 2 hex string
     * characters appended with a null byte, for example 10 => "0A\0".
     *
     * This string would then be added to the output array starting from the
     * position pointed at by "ptr". For example if "ptr" is pointing at the 0
     * index then "0A\0" would be written as output[0] = '0', output[1] = 'A' and
     * output[2] = '\0'.
     *
     * "sprintf" returns the number of chars in its output excluding the null
     * byte, in our case this would be 2. So we move the "ptr" location two
     * steps ahead so that the next hex string would be written at the new
     * location, overriding the null byte from the previous hex string.
     *
     * We don't need to add a terminating null byte because it's been already 
     * added for us from the last hex string. */  
    ptr += sprintf(ptr, "%02X", buf[i]);
}

printf("%s\n", output);

绝妙的逻辑。我花了一个小时寻找一种优雅的非C++字符串解决方案来应对这个挑战! - Mark Terrill

15

这里有一种方法,速度要快得多:

#include <stdlib.h>
#include <stdio.h>

unsigned char *     bin_to_strhex(const unsigned char *bin, unsigned int binsz,
                                  unsigned char **result)
{
  unsigned char     hex_str[]= "0123456789abcdef";
  unsigned int      i;

  if (!(*result = (unsigned char *)malloc(binsz * 2 + 1)))
    return (NULL);

  (*result)[binsz * 2] = 0;

  if (!binsz)
    return (NULL);

  for (i = 0; i < binsz; i++)
    {
      (*result)[i * 2 + 0] = hex_str[(bin[i] >> 4) & 0x0F];
      (*result)[i * 2 + 1] = hex_str[(bin[i]     ) & 0x0F];
    }
  return (*result);
}

int                 main()
{
  //the calling
  unsigned char     buf[] = {0,1,10,11};
  unsigned char *   result;

  printf("result : %s\n", bin_to_strhex((unsigned char *)buf, sizeof(buf), &result));
  free(result);

  return 0
}

3
这段代码包含一个漏洞,只有在奇怪的不可打印输入时才会显现出来(还没有时间深入研究数学上发生了什么)。尝试对十六进制 ca9e3c972f1c5db40c0b4a66ab5bc1a20ca4457bdbe5e0f8925896d5ed37d726 进行二进制编码,你会得到 ÌaÌe3cÌ72f1c5dÌ40c0b4a66Ìb5bÌ1Ì20cÌ4457bÌbÌ5Ì0Ì8Ì258Ì6Ì5Ìd37Ì726 的输出。要修复这个问题,需要将循环的第一行中 hex_str 内部的位改为 (input[i] >> 4) & 0x0F,就像 @kriss 的答案一样。这样就可以正常工作了。 - niemiro
Bug - 没有检查 malloc() 失败的情况。 - Cecil Ward
在编程中,绝对到处使用无符号字符比使用有符号字符更好,因为没有人想冒有符号字符的风险(疯狂的DEC PDP11硬件特性),这样你就不会冒有符号比较出错或右移导致数值损坏的风险。在这种情况下,公平地说,代码确实在任何地方都进行了防御性地 & 0x0F 操作,以保护您。 - Cecil Ward
bin输入参数应该是const unsigned char const * bin,以便在此例程中将内存声明为只读。 - Cecil Ward
我更喜欢将结果缓冲区的地址作为函数返回值返回 - 这样做在许多方面都更有效率,更易于阅读。 - Cecil Ward
1
我已经整合了Cecil Ward的建议,感谢您的反馈。 - Yannuth

14

解决方案

btox函数将任意数据*bb转换为一个由n个十六进制数字组成的未终止字符串*xp

void btox(char *xp, const char *bb, int n) 
{
    const char xx[]= "0123456789ABCDEF";
    while (--n >= 0) xp[n] = xx[(bb[n>>1] >> ((1 - (n&1)) << 2)) & 0xF];
}

示例

#include <stdio.h>

typedef unsigned char uint8;

void main(void) 
{
    uint8 buf[] = {0, 1, 10, 11};
    int n = sizeof buf << 1;
    char hexstr[n + 1];

    btox(hexstr, buf, n);
    hexstr[n] = 0; /* Terminate! */
    printf("%s\n", hexstr);
}

结果:00010A0B

实时运行:Tio.run


命名可以更好。 - Mightywill

7
我想要补充一下,即使它有点离题(不是标准的C语言),但我经常需要寻找它,并在第一次搜索中发现这个问题。Linux内核打印函数printk也具有格式说明符,可以通过单个格式说明符“直接”输出数组/内存内容: https://www.kernel.org/doc/Documentation/printk-formats.txt
Raw buffer as a hex string:
    %*ph    00 01 02  ...  3f
    %*phC   00:01:02: ... :3f
    %*phD   00-01-02- ... -3f
    %*phN   000102 ... 3f

    For printing a small buffers (up to 64 bytes long) as a hex string with
    certain separator. For the larger buffers consider to use
    print_hex_dump(). 

然而,这些格式说明符似乎不存在于标准的用户空间(s)printf中。

1

基于 Yannuth 的 答案,但简化了。

这里,dest[]的长度暗示为len的两倍,并且其分配由调用者管理。

void create_hex_string_implied(const unsigned char *src, size_t len, unsigned char *dest)
{
    static const unsigned char table[] = "0123456789abcdef";

    for (; len > 0; --len)
    {
        unsigned char c = *src++;
        *dest++ = table[c >> 4];
        *dest++ = table[c & 0x0f];
    }
}

1
这是一种执行转换的方法:
#include<stdio.h>
#include<stdlib.h>

#define l_word 15
#define u_word 240

char *hex_str[]={"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"};

main(int argc,char *argv[]) {


     char *str = malloc(50);
     char *tmp;
     char *tmp2;

     int i=0;


     while( i < (argc-1)) {
          tmp = hex_str[*(argv[i]) & l_word];
          tmp2 = hex_str[*(argv[i]) & u_word];

          if(i == 0) { memcpy(str,tmp2,1); strcat(str,tmp);}
          else { strcat(str,tmp2); strcat(str,tmp);}
          i++;
    }

    printf("\n*********  %s  *************** \n", str);

}

1
稍作修改的Yannith版本。只是我喜欢将其作为返回值。

typedef struct {
   size_t len;
   uint8_t *bytes;
} vdata;

char* vdata_get_hex(const vdata data)
{
   char hex_str[]= "0123456789abcdef";

   char* out;
   out = (char *)malloc(data.len * 2 + 1);
   (out)[data.len * 2] = 0;
   
   if (!data.len) return NULL;
   
   for (size_t i = 0; i < data.len; i++) {
      (out)[i * 2 + 0] = hex_str[(data.bytes[i] >> 4) & 0x0F];
      (out)[i * 2 + 1] = hex_str[(data.bytes[i]     ) & 0x0F];
   }
   return out;
}


1
这个函数适用于用户/调用者想要将十六进制字符串放入字符数组/缓冲区的情况。有了字符缓冲区中的十六进制字符串,用户/调用者可以使用自己的宏/函数将其显示或记录到任何想要的地方(例如文件)。此函数还允许调用者控制每行放置的(十六进制)字节数量。
/**
 * @fn 
 * get_hex
 *
 * @brief 
 * Converts a char into bunary string 
 *
 * @param[in]   
 *     buf Value to be converted to hex string
 * @param[in]   
 *     buf_len Length of the buffer
 * @param[in]   
 *     hex_ Pointer to space to put Hex string into
 * @param[in]   
 *     hex_len Length of the hex string space
 * @param[in]   
 *     num_col Number of columns in display hex string
 * @param[out]   
 *     hex_ Contains the hex string
 * @return  void
 */
static inline void
get_hex(char *buf, int buf_len, char* hex_, int hex_len, int num_col)
{
    int i;
#define ONE_BYTE_HEX_STRING_SIZE   3
  unsigned int byte_no = 0;

  if (buf_len <= 0) {
      if (hex_len > 0) {
        hex_[0] = '\0';
      }
      return;
  }

  if(hex_len < ONE_BYTE_HEX_STRING_SIZE + 1)
  {
      return;
  }

  do {
         for (i = 0; ((i < num_col) && (buf_len > 0) && (hex_len > 0)); ++i )
         {
            snprintf(hex_, hex_len, "%02X ", buf[byte_no++] & 0xff);
            hex_ += ONE_BYTE_HEX_STRING_SIZE;
            hex_len -=ONE_BYTE_HEX_STRING_SIZE;
            buf_len--;
         }
         if (buf_len > 1)
         {
             snprintf(hex_, hex_len, "\n");
             hex_ += 1;
         }
  } while ((buf_len) > 0 && (hex_len > 0));

}

例子:

代码

#define DATA_HEX_STR_LEN 5000
    char      data_hex_str[DATA_HEX_STR_LEN];

    get_hex(pkt, pkt_len, data_hex_str, DATA_HEX_STR_LEN, 16);
    //      ^^^^^^^^^^^^                                  ^^
    //      Input byte array                              Number of (hex) byte
    //      to be converted to hex string                 columns in hex string

    printf("pkt:\n%s",data_hex_str) 

输出

pkt:
BB 31 32 00 00 00 00 00 FF FF FF FF FF FF DE E5 
A8 E2 8E C1 08 06 00 01 08 00 06 04 00 01 DE E5 
A8 E2 8E C1 67 1E 5A 02 00 00 00 00 00 00 67 1E 
5A 01 

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