如何将十六进制字符串转换为无符号字符数组?

20
例如,我有一个cstring "E8 48 D8 FF FF 8B 0D" (包括空格),需要转换为相应的无符号char数组 {0xE8,0x48,0xD8,0xFF,0xFF,0x8B,0x0D}。如何高效地完成此操作?谢谢!

编辑:我不能使用std库...所以这是个C语言问题。非常抱歉!

7个回答

37

这个回答解决了原问题,该问题要求提供一个C++解决方案。

您可以使用带有hex操作符的istringstream

std::string hex_chars("E8 48 D8 FF FF 8B 0D");

std::istringstream hex_chars_stream(hex_chars);
std::vector<unsigned char> bytes;

unsigned int c;
while (hex_chars_stream >> std::hex >> c)
{
    bytes.push_back(c);
}

请注意,变量 c 必须是 int (或者 long,或其他整数类型),而不是 char;如果是 char (或 unsigned char),将会调用错误的 >> 重载函数,并从字符串中提取单个字符,而不是十六进制整数字符串。

为了确保提取的值适合一个 char,建议进行额外的错误检查。


3
因为我不能给出两个正确答案,所以我选择支持并赞同这个答案,因为它绝对是C++用户的一个很好的解决方案! - Gbps

14

你永远无法说服我这个操作是个性能瓶颈。 高效的方法是通过使用标准的C库来充分利用时间:

static unsigned char gethex(const char *s, char **endptr) {
  assert(s);
  while (isspace(*s)) s++;
  assert(*s);
  return strtoul(s, endptr, 16);
}

unsigned char *convert(const char *s, int *length) {
  unsigned char *answer = malloc((strlen(s) + 1) / 3);
  unsigned char *p;
  for (p = answer; *s; p++)
    *p = gethex(s, (char **)&s);
  *length = p - answer;
  return answer;
}

已编译并测试。在您的示例中可以使用。


我选择这个作为答案,因为它提供了一个可行的示例。谢谢! - Gbps
3
另一方面,“A B C D E F 1 2 3 4 5 6 7 8 9”存在缓冲区溢出。 - Ben Voigt
5
更简洁的代码:for (i=0; i<max && isxdigit(*s); i++) a[i]=strtol(s, &s, 16); 关键在于,你的 gethex 函数是完全不必要的。strtol 函数会自行跳过前导空格。如果你想更严格地拒绝不符合模式的字符串,可以使用 sscanf 来控制字段宽度并测量匹配长度。 - R.. GitHub STOP HELPING ICE
@R: 对strtoul的提点很好——我没有仔细阅读man页。随意编辑吧。 - Norman Ramsey
只有在每两个数字之间存在空格时,这才能正常工作。在我看来,这使得这种方法很糟糕。 - Marek R

8
  • 遍历所有字符。
    • 如果您有一个十六进制数字,则数字为 (ch >= 'A')? (ch - 'A' + 10): (ch - '0')
      • 将累加器左移四位并添加(或OR)新数字。
    • 如果您有一个空格,并且上一个字符不是空格,则将当前累加器值附加到数组中并将累加器重置为零。

+1:这可能是最直接和简单的方法。 - James McNellis
这基本上就是我所做的,只不过使用了 switch 而不是三元测试。根据编译器和处理器架构,其中一个可能会更快。但你也应该测试每个字符是否在 0-9A-F 的范围内,并且这会使得相同的测试被重复两次。 - kriss
1
@kriss:一切都在假设中。你假设每个值之间必须有两个十六进制数字和一个空格,而我的方法允许省略前导零或多个空格,但假设字符串中没有其他类别的字符。如果不能这样假设,我可能会选择单独进行验证,通过测试 if (s[strspn(s, " 0123456789ABCDEF")]) /* error */;。当然,这又是对字符串的另一次遍历,但更加简洁。或者通过对每个字符使用 isspaceisxdigit 来避免第二次遍历字符串,这将使用查找表来提高速度。 - Ben Voigt
循环开关并不是真正的问题,我并不认为它有什么区别。我选择假设输入中恰好有两个十六进制字符,因为如果你允许更多的话,你也应该检查值的范围。那么如果允许负数呢?我们还需要处理符号等问题。开关一种查找表...(另一种快速转换方法是真正使用一个作为数组实现的查找表)。 - kriss
问题指定所有输入都是无符号的。问题没有指定总会有零填充到恰好两个数字(例如,所有这些都适合于char0xA0x0A0x000A)或只有一个空格,尽管这些假设在示例输入中是正确的。 - Ben Voigt
你应该先使用isxdigit。或者查看R上面的评论。 - Mark

5

如果您事先知道要解析的字符串的长度(例如,您正在从/proc读取某些内容),则可以使用sscanf和“hh”类型修饰符。该修饰符指定下一次转换是diouxX之一,并且用于存储它的指针将是signed char或unsigned char。

// example: ipv6 address as seen in /proc/net/if_inet6:
char myString[] = "fe80000000000000020c29fffe01bafb";
unsigned char addressBytes[16];
sscanf(myString, "%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx
%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx", &addressBytes[0],
&addressBytes[1], &addressBytes[2], &addressBytes[3], &addressBytes[4], 
&addressBytes[5], &addressBytes[6], &addressBytes[7], &addressBytes[8], 
&addressBytes[9], &addressBytes[10], addressBytes[11],&addressBytes[12],
&addressBytes[13], &addressBytes[14], &addressBytes[15]);

int i;
for (i = 0; i < 16; i++){
    printf("addressBytes[%d] = %02x\n", i, addressBytes[i]);
}

输出:

addressBytes[0] = fe
addressBytes[1] = 80
addressBytes[2] = 00
addressBytes[3] = 00
addressBytes[4] = 00
addressBytes[5] = 00
addressBytes[6] = 00
addressBytes[7] = 00
addressBytes[8] = 02
addressBytes[9] = 0c
addressBytes[10] = 29
addressBytes[11] = ff
addressBytes[12] = fe
addressBytes[13] = 01
addressBytes[14] = ba
addressBytes[15] = fb

5

使用“旧”的sscanf()函数:

string s_hex = "E8 48 D8 FF FF 8B 0D"; // source string
char *a_Char = new char( s_hex.length()/3 +1 ); // output char array

for( unsigned i = 0, uchr ; i < s_hex.length() ; i += 3 ) {
    sscanf( s_hex.c_str()+ i, "%2x", &uchr ); // conversion
    a_Char[i/3] = uchr; // save as char
  }
delete a_Char;

0

如果你需要一个纯C实现,我认为你可以说服 sscanf(3) 去做你想要的事情。我相信只要你的输入字符串只包含两个字符的十六进制值,这应该是可移植的(包括稍微有点危险的类型强制转换以取悦编译器)。

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


char hex[] = "E8 48 D8 FF FF 8B 0D";
char *p;
int cnt = (strlen(hex) + 1) / 3; // Whether or not there's a trailing space
unsigned char *result = (unsigned char *)malloc(cnt), *r;
unsigned char c;

for (p = hex, r = result; *p; p += 3) {
    if (sscanf(p, "%02X", (unsigned int *)&c) != 1) {
        break; // Didn't parse as expected
    }
    *r++ = c;
}

声明 cunsigned int,否则你可能会覆盖其他本地变量(或更糟糕的是,你的返回地址)。 - Ben Voigt
但通常情况下,scanf 要花费更长的时间来确定格式代码,而不是我的整个答案,而且问题确实要求一种高效的方法。 - Ben Voigt
@Ben Voigt。是的,但“高效”是指运行时间还是程序员时间呢?-)无论如何,感谢您指出我应该将c设置为unsigned int并将其强制转换为result数组。 - bjg
1
UB。由于在预期的结尾处,p 指向终止零字节之后的一个字节。 - Marek R
@MarekR 很好的发现。显然我在写这个(6年前)时有些犹豫,声明了一个 cnt 变量,但后来没有使用它。 - bjg

-1
老式的C语言方式,手动完成;-)(有许多更短的方法,但我不是在打高尔夫球,我要考虑运行时间)。
enum { NBBYTES = 7 };
char res[NBBYTES+1];
const char * c = "E8 48 D8 FF FF 8B 0D";
const char * p = c;
int i = 0;

for (i = 0; i < NBBYTES; i++){
    switch (*p){
    case '0': case '1': case '2': case '3': case '4':
    case '5': case '6': case '7': case '8': case '9':
      res[i] = *p - '0';
    break;
    case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
      res[i] = *p - 'A' + 10;
    break;
   default:
     // parse error, throw exception
     ;
   }
   p++;
   switch (*p){
   case '0': case '1': case '2': case '3': case '4':
   case '5': case '6': case '7': case '8': case '9':
      res[i] = res[i]*16 + *p - '0';
   break;
   case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
      res[i] = res[i]*16 + *p - 'A' + 10;
   break;
   default:
      // parse error, throw exception
      ;
   }
   p++;
   if (*p == 0) { continue; }
   if (*p == ' ') { p++; continue; }
   // parse error, throw exception
}

// let's show the result, C style IO, just cout if you want C++
for (i = 0 ; i < 7; i++){
   printf("%2.2x ", 0xFF & res[i]);
}
printf("\n");

现在又来了一个允许数字之间有任意数量的数字、任意数量的空格来分隔它们,包括前导或尾随空格(Ben的规格):

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

int main(){
    enum { NBBYTES = 7 };
    char res[NBBYTES];
    const char * c = "E8 48 D8 FF FF 8B 0D";
    const char * p = c;
    int i = -1;

    res[i] = 0;
    char ch = ' ';
    while (ch && i < NBBYTES){
       switch (ch){
       case '0': case '1': case '2': case '3': case '4':
       case '5': case '6': case '7': case '8': case '9':
          ch -= '0' + 10 - 'A';
       case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
          ch -= 'A' - 10;
          res[i] = res[i]*16 + ch;
          break;
       case ' ':
         if (*p != ' ') {
             if (i == NBBYTES-1){
                 printf("parse error, throw exception\n");
                 exit(-1);
            }
            res[++i] = 0;
         }
         break;
       case 0:
         break;
       default:
         printf("parse error, throw exception\n");
         exit(-1);
       }
       ch = *(p++);
    }
    if (i != NBBYTES-1){
        printf("parse error, throw exception\n");
        exit(-1);
    }

   for (i = 0 ; i < 7; i++){
      printf("%2.2x ", 0xFF & res[i]);
   }
   printf("\n");
}

不,它并不是真正的混淆代码...但是看起来像。


3
我们可以说“呸”吗?(仅仅因为代码在最后一个循环中会“抛出异常”,因为字符串中只有6个空格,而不是代码所需的7个。) - Jonathan Leffler
@Jonathan:不再是这样了...我也可以添加一个输入空格。旧的分隔符与终止符之争。 - kriss
你的小修改没有帮助......在终止NUL上 *p != ' ',无论你用什么逻辑或它都没有关系。 - Ben Voigt
糟糕,我又出错了。你应该会喜欢这个新的修复 :-) - kriss
有效性检查仍然不稳定。 - Ben Voigt

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