当前位置:   article > 正文

python对字符编码的处理(源码篇)_repr <%s object at %p>

repr <%s object at %p>

示例代码如下

其中,a直接回车打印的是16进制编码,print a打印的是汉字,怎么做到的?

 

变量名+回车的方式

 

首先注意我们是在交互环境,输入的内容会立即解析,其源头就是将标准输入当成了读取文件一样:

  1. int
  2. Py_Main(int argc, char **argv)
  3. {
  4. ...
  5. sts = PyRun_AnyFileExFlags(
  6. fp,
  7. filename == NULL ? "<stdin>" : filename,
  8. filename != NULL, &cf) != 0;
  9. ...

然后就得到了opcode PRINT_EXPR

  1. PyObject *
  2. PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
  3. {
  4. ...
  5. case PRINT_EXPR:
  6. v = POP();
  7. w = PySys_GetObject("displayhook");
  8. if (w == NULL) {
  9. PyErr_SetString(PyExc_RuntimeError,
  10. "lost sys.displayhook");
  11. err = -1;
  12. x = NULL;
  13. }
  14. if (err == 0) {
  15. x = PyTuple_Pack(1, v);
  16. if (x == NULL)
  17. err = -1;
  18. }
  19. if (err == 0) {
  20. w = PyEval_CallObject(w, x);
  21. Py_XDECREF(w);
  22. if (w == NULL)
  23. err = -1;
  24. }
  25. Py_DECREF(v);
  26. Py_XDECREF(x);
  27. break;
  28. ...

可以看到它在sys模块中找到一个displayhook函数进行输出。

  1. static PyMethodDef sys_methods[] = {
  2. ...
  3. {"displayhook", sys_displayhook, METH_O, displayhook_doc},
  4. ...
  5. }
  6. static PyObject *
  7. sys_displayhook(PyObject *self, PyObject *o)
  8. {
  9. ...
  10. outf = PySys_GetObject("stdout");
  11. if (outf == NULL) {
  12. PyErr_SetString(PyExc_RuntimeError, "lost sys.stdout");
  13. return NULL;
  14. }
  15. if (PyFile_WriteObject(o, outf, 0) != 0)
  16. return NULL;
  17. ...

就像写文件一样,将变量o写入<stdout>(标准输出)文件中,注意这里的flags=0,而且会一直往下传递出去。

  • PyFile_WriteObject
    • file_PyObject_Print
      • PyObject_Print
        • internal_print

flags的意义就是,最终生成的字符串s,要不要去掉引号。即原生的字符串内容,不添加任何额外骚操作。

  1. /* Flag bits for printing: */
  2. #define Py_PRINT_RAW 1 /* No string quotes etc. */

对于str和unicode,前面的流程都是一样的,在 internal_print 开始走各自流程,原因在于 PyString_Type 的 tp_print 不为空。

 

str变量打印

 

最终走到 string_print,因为flags=0,意思就是“我不要原生字符串,你给我加工一下!”,于是一堆转义。

  1. static int
  2. string_print(PyStringObject *op, FILE *fp, int flags)
  3. {
  4. ...
  5. /* figure out which quote to use; single is preferred */
  6. quote = '\'';
  7. if (memchr(op->ob_sval, '\'', Py_SIZE(op)) &&
  8. !memchr(op->ob_sval, '"', Py_SIZE(op)))
  9. quote = '"';
  10. str_len = Py_SIZE(op);
  11. Py_BEGIN_ALLOW_THREADS
  12. fputc(quote, fp);
  13. for (i = 0; i < str_len; i++) {
  14. /* Since strings are immutable and the caller should have a
  15. reference, accessing the interal buffer should not be an issue
  16. with the GIL released. */
  17. c = op->ob_sval[i];
  18. if (c == quote || c == '\\')
  19. fprintf(fp, "\\%c", c);
  20. else if (c == '\t')
  21. fprintf(fp, "\\t");
  22. else if (c == '\n')
  23. fprintf(fp, "\\n");
  24. else if (c == '\r')
  25. fprintf(fp, "\\r");
  26. else if (c < ' ' || c >= 0x7f)
  27. fprintf(fp, "\\x%02x", c & 0xff);
  28. else
  29. fputc(c, fp);
  30. }
  31. fputc(quote, fp);
  32. Py_END_ALLOW_THREADS
  33. return 0;
  34. }

a的打印为什么能看到'\xba\xba'?核心代码就是 fprintf(fp, "\\x%02x", c & 0xff);

a因为是str的汉字,1个汉字占用2个字节,每个字节按两位的16进制输出%02x,再加个前缀\\x表示是16进制。

 

unicode变量打印

 

flags最终在这里起了作用,走了PyObject_Repr,将变量op变成字符串s,再通过 internal_print 打印到fp(也就是<stdout>)上。

  1. static int
  2. internal_print(PyObject *op, FILE *fp, int flags, int nesting)
  3. {
  4. ...
  5. else if (Py_TYPE(op)->tp_print == NULL) {
  6. PyObject *s;
  7. if (flags & Py_PRINT_RAW)
  8. s = PyObject_Str(op);
  9. else
  10. s = PyObject_Repr(op);
  11. if (s == NULL)
  12. ret = -1;
  13. else {
  14. ret = internal_print(s, fp, Py_PRINT_RAW,
  15. nesting+1);
  16. }
  17. Py_XDECREF(s);
  18. ...
  19. PyObject_Repr(PyObject *v)
  20. {
  21. ...
  22. if (v == NULL)
  23. return PyString_FromString("<NULL>");
  24. else if (Py_TYPE(v)->tp_repr == NULL)
  25. return PyString_FromFormat("<%s object at %p>",
  26. Py_TYPE(v)->tp_name, v);
  27. else {
  28. PyObject *res;
  29. res = (*Py_TYPE(v)->tp_repr)(v);
  30. ...
  • PyObject_Repr
    • unicode_repr
      • unicodeescape_string

关于unicode的骚操作都在这里了

  1. const Py_ssize_t expandsize = 6;
  2. ...
  3. repr = PyString_FromStringAndSize(NULL,
  4. 2
  5. + expandsize*size
  6. + 1);

我们看到返回值repr的空间是这么开辟的

前面的2:开头的 u'

后面的1:结尾的 '

中间:每6个字节(expandsize)显示1个编码,格式为 \uXXXX,X也就是4个比特位填充

  1. if (ch >= 256) {
  2. *p++ = '\\';
  3. *p++ = 'u';
  4. *p++ = hexdigit[(ch >> 12) & 0x000F];
  5. *p++ = hexdigit[(ch >> 8) & 0x000F];
  6. *p++ = hexdigit[(ch >> 4) & 0x000F];
  7. *p++ = hexdigit[ch & 0x000F];
  8. } // 可以发现,ch编码是大端存储的

最终我们有了 u'\u6c49'

 

print操作

 

首先要确定的是print对应的opcode是PRINT_ITEM

  1. >>> def foo():
  2. ... print a, b
  3. ...
  4. >>> import dis
  5. >>> dis.dis(foo)
  6. 2 0 LOAD_GLOBAL 0 (a)
  7. 3 PRINT_ITEM
  8. 4 LOAD_GLOBAL 1 (b)
  9. 7 PRINT_ITEM
  10. 8 PRINT_NEWLINE
  11. 9 LOAD_CONST 0 (None)
  12. 12 RETURN_VALUE

千万不要误解成从LOAD_GLOBAL找到print函数,然后CALL_FUNCTION之类的,除非你这么写

  1. >>> def foo():
  2. ... getattr(__builtins__,'print')('a')
  3. ...
  4. >>> dis.dis(foo)
  5. 2 0 LOAD_GLOBAL 0 (getattr)
  6. 3 LOAD_GLOBAL 1 (__builtins__)
  7. 6 LOAD_CONST 1 ('print')
  8. 9 CALL_FUNCTION 2
  9. 12 LOAD_CONST 2 ('a')
  10. 15 CALL_FUNCTION 1
  11. 18 POP_TOP
  12. 19 LOAD_CONST 0 (None)
  13. 22 RETURN_VALUE

言归正传,那我们看看PRINT_ITEM是怎么打印成汉字的?

  1. TARGET_NOARG(PRINT_ITEM)
  2. {
  3. v = POP();
  4. if (stream == NULL || stream == Py_None) {
  5. w = PySys_GetObject("stdout");
  6. if (w == NULL) {
  7. PyErr_SetString(PyExc_RuntimeError,
  8. "lost sys.stdout");
  9. err = -1;
  10. }
  11. }
  12. /* PyFile_SoftSpace() can exececute arbitrary code
  13. if sys.stdout is an instance with a __getattr__.
  14. If __getattr__ raises an exception, w will
  15. be freed, so we need to prevent that temporarily. */
  16. Py_XINCREF(w);
  17. // 如果前面都没问题的话,看看有没有必要插入一个空格
  18. if (w != NULL && PyFile_SoftSpace(w, 0))
  19. err = PyFile_WriteString(" ", w);
  20. if (err == 0)
  21. err = PyFile_WriteObject(v, w, Py_PRINT_RAW);
  22. if (err == 0) {
  23. /* XXX move into writeobject() ? */
  24. if (PyString_Check(v)) {
  25. // 如果打印的对象是str的空字符串
  26. // 调用一下 PyFile_SoftSpace(w, 1);
  27. ...
  28. }
  29. #ifdef Py_USING_UNICODE
  30. else if (PyUnicode_Check(v)) {
  31. // 如果打印的对象是unicode的空字符串
  32. // 调用一下 PyFile_SoftSpace(w, 1);
  33. ...
  34. }
  35. #endif
  36. else
  37. // 总之就是要调用一下啦
  38. PyFile_SoftSpace(w, 1);
  39. }
  40. Py_XDECREF(w);
  41. Py_DECREF(v);
  42. Py_XDECREF(stream);
  43. stream = NULL;
  44. if (err == 0) DISPATCH();
  45. break;
  46. }

后面的 PyFile_SoftSpace 是为了给print元素之间加空格,加空格的逻辑就是上面的 PyFile_WriteString(" ", w);

  1. /* Interface for the 'soft space' between print items. */
  2. int
  3. PyFile_SoftSpace(PyObject *f, int newflag)
  4. {
  5. ...
  6. else if (PyFile_Check(f)) {
  7. oldflag = ((PyFileObject *)f)->f_softspace;
  8. ((PyFileObject *)f)->f_softspace = newflag;
  9. }
  10. ...
  11. }

核心代码在 PyFile_WriteObject(v, w, Py_PRINT_RAW); 这里总算要求,打印不加工的原生字符串了,是不是这个原因导致打出汉字的呢?

 

print str

 

调用流程如下,我们又回到了string_print

  • PyFile_WriteObject
    • file_PyObject_Print
      • PyObject_Print
        • internal_print
          • string_print

这次是这段代码起了作用

  1. if (flags & Py_PRINT_RAW) {
  2. char *data = op->ob_sval;
  3. Py_ssize_t size = Py_SIZE(op);
  4. Py_BEGIN_ALLOW_THREADS
  5. while (size > INT_MAX) {
  6. // 对于很长的字符串,按14比特位为1个单位分批输出
  7. // 显然这也是有问题的:
  8. // 1. 可能会有内存对齐问题
  9. // 2. 字节都拆开了,输出会有误吧!(需验证)
  10. const int chunk_size = INT_MAX & ~0x3FFF;
  11. fwrite(data, 1, chunk_size, fp);
  12. data += chunk_size;
  13. size -= chunk_size;
  14. }
  15. fwrite(data, 1, (size_t)size, fp);
  16. Py_END_ALLOW_THREADS
  17. return 0;
  18. }

在这里fwrite代替了,非原生字符串输出的fputc和fprintf

 

print unicode

 

print unicode走了完全不一样的路子,在PyFile_WriteObject就将unicode对象value转换成str了

  1. #ifdef Py_USING_UNICODE
  2. if ((flags & Py_PRINT_RAW) &&
  3. PyUnicode_Check(v) && enc != Py_None) {
  4. char *cenc = PyString_AS_STRING(enc);
  5. char *errors = fobj->f_errors == Py_None ?
  6. "strict" : PyString_AS_STRING(fobj->f_errors);
  7. value = PyUnicode_AsEncodedString(v, cenc, errors);
  8. if (value == NULL)
  9. return -1;
  10. } else {
  11. ...
  12. }
  13. result = file_PyObject_Print(value, fobj, flags);
  14. Py_DECREF(value);
  15. return result;

cenc取的终端字符编码'cp936',errors=’strict',核心代码就在 PyUnicode_AsEncodedString(v, cenc, errors); 这句了

  • PyUnicode_AsEncodedString
    • _PyCodec_EncodeText(_PyCodec_TextEncoder,拿到encoding对应的encoder文件)
      • _PyCodec_EncodeInternal

_PyCodec_EncodeInternal的处理很简单,就是调用encoder函数,所以我们得聚焦到 _PyCodec_TextEncoder 怎么找到这个encoder的

其实核心代码就是调用了_PyCodec_Lookup(encoding),初始化了一个叫encodings的模块,其目录就在

{PythonDir}\Lib\encodings\

__init__.py初始化流程中,定义了search_function搜索函数赋值给codecs,又回到了C模块中

  1. [__init__.py]
  2. codecs.register(search_function)
  3. [_codecsmodule.c]
  4. static PyMethodDef _codecs_functions[] = {
  5. {"register", codec_register, METH_O,
  6. register__doc__},
  7. ...
  8. }
  9. static
  10. PyObject *codec_register(PyObject *self, PyObject *search_function)
  11. {
  12. if (PyCodec_Register(search_function))
  13. return NULL;
  14. Py_RETURN_NONE;
  15. }
  16. int PyCodec_Register(PyObject *search_function)
  17. {
  18. PyInterpreterState *interp = PyThreadState_GET()->interp;
  19. if (interp->codec_search_path == NULL && _PyCodecRegistry_Init())
  20. goto onError;
  21. if (search_function == NULL) {
  22. PyErr_BadArgument();
  23. goto onError;
  24. }
  25. if (!PyCallable_Check(search_function)) {
  26. PyErr_SetString(PyExc_TypeError, "argument must be callable");
  27. goto onError;
  28. }
  29. return PyList_Append(interp->codec_search_path, search_function);
  30. onError:
  31. return -1;
  32. }

search_function的逻辑,就是用了_aliases,找到encoding对应的文件名aliased_encoding,优先用文件名加载模块,否则就用encoding作为文件名。

我们在aliases.py中,可以确定的是cp936对应gbk.py文件。

search_function返回类型是codecs.CodecInfo,它是tuple的子类,详见{PythonDir}\Lib\codecs.py

要最终找到encoder函数也不容易,它其实是CPyFunction,而_codecs_cn和gbk函数都是宏定义的

  1. [gbk.py]
  2. codec = _codecs_cn.getcodec('gbk')
  3. class Codec(codecs.Codec):
  4. encode = codec.encode
  5. decode = codec.decode
  6. [_codecs_cn.c]
  7. BEGIN_CODECS_LIST
  8. CODEC_STATELESS(gb2312)
  9. CODEC_STATELESS(gbk)
  10. CODEC_STATELESS(gb18030)
  11. CODEC_STATEFUL(hz)
  12. END_CODECS_LIST
  13. I_AM_A_MODULE_FOR(cn)
  14. // 翻译过来就是这样,定义了几个map,还有encode/decode函数
  15. static const struct dbcs_map _mapping_list[] = {
  16. { "gb2312", NULL, (void*)gb2312_decmap },
  17. { "gbkext", NULL, (void*)gbkext_decmap },
  18. { "gbcommon", (void*)gbcommon_encmap, NULL },
  19. { "gb18030ext", (void*)gb18030ext_encmap, NULL },
  20. { "", NULL, NULL } };
  21. static const struct dbcs_map *mapping_list = (const struct dbcs_map *)_mapping_list;
  22. static const MultibyteCodec _codec_list[] = {
  23. { "gb2312", NULL, NULL, gb2312_encode, NULL, NULL, gb2312_decode, NULL, NULL },
  24. { "gbk", NULL, NULL, gbk_encode, NULL, NULL, gbk_decode, NULL, NULL },
  25. { "gb18030", NULL, NULL, gb18030_encode, NULL, NULL, gb18030_decode, NULL, NULL },
  26. { "hz", NULL, NULL, hz_encode, NULL, NULL, hz_decode, NULL, NULL },
  27. { "", NULL, } };
  28. static const MultibyteCodec *codec_list = (const MultibyteCodec *)_codec_list;
  29. void
  30. init_codecs_cn(void)
  31. {
  32. PyObject *m = Py_InitModule("_codecs_cn", __methods);
  33. if (m != NULL)
  34. (void)register_maps(m);
  35. }

[gbk.py]第一行代码基本上可以翻译成:

1. 在_codecs_cn模块的codec_list里,找到encoding=‘gbk’的MultibyteCodec对象codec

2. 创建PyCapsule对象codecobj,将这个codec包住(capsule->pointer = pointer;)

3. 调用_multibytecodec.__create_codec(codecobj),将MultibyteCodec对象codec取出来,用MultibyteCodecObject对象包住(self->codec = codec;)

4. 返回这个MultibyteCodecObject对象,就是gbk.py中的codec

5. 以encode = codec.encode为例,MultibyteCodecObject对象的函数就看MultibyteCodec_Type的定义了:

  1. static struct PyMethodDef multibytecodec_methods[] = {
  2. {"encode", (PyCFunction)MultibyteCodec_Encode,
  3. METH_VARARGS | METH_KEYWORDS,
  4. MultibyteCodec_Encode__doc__},
  5. {"decode", (PyCFunction)MultibyteCodec_Decode,
  6. METH_VARARGS | METH_KEYWORDS,
  7. MultibyteCodec_Decode__doc__},
  8. {NULL, NULL},
  9. };

为了确定encode逻辑是怎么把unicode变成str的,我们得关心这个gbk是怎么实现的了(把宏定义弄懂)。

看v2.7.15的代码还是有挑战性,v2.7.9的逻辑就很清晰,这样的改动,纯粹只是为了给python3的加个检查

  1. /* Text encoding/decoding API */
  2. PyObject * _PyCodec_LookupTextEncoding(const char *encoding,
  3. const char *alternate_command)
  4. {
  5. ...
  6. if (Py_Py3kWarningFlag && !PyTuple_CheckExact(codec)) {
  7. attr = PyObject_GetAttrString(codec, "_is_text_encoding");
  8. if (attr == NULL) {
  9. if (!PyErr_ExceptionMatches(PyExc_AttributeError))
  10. goto onError;
  11. PyErr_Clear();
  12. } else {
  13. is_text_codec = PyObject_IsTrue(attr);
  14. Py_DECREF(attr);
  15. if (is_text_codec < 0)
  16. goto onError;
  17. if (!is_text_codec) {
  18. PyObject *msg = PyString_FromFormat(
  19. "'%.400s' is not a text encoding; "
  20. "use %s to handle arbitrary codecs",
  21. encoding, alternate_command);
  22. if (msg == NULL)
  23. goto onError;
  24. if (PyErr_WarnPy3k(PyString_AS_STRING(msg), 1) < 0) {
  25. Py_DECREF(msg);
  26. goto onError;
  27. }
  28. Py_DECREF(msg);
  29. }
  30. }
  31. }
  32. ...

 

方便起见,你可以先了解这些函数:

[_codecsmodule.c]

ascii_encode/ascii_decode

ascii.py中的逻辑就写的很清晰,函数引用直指这两个!

[_codecs_iso2022.c]

 

所以我们有理由相信,encoder就是这个MultibyteCodecObject对象的成员函数,调用结果最终会进入

  1. static PyObject *
  2. multibytecodec_encode(MultibyteCodec *codec,
  3. MultibyteCodec_State *state,
  4. const Py_UNICODE **data, Py_ssize_t datalen,
  5. PyObject *errors, int flags)

它一手拿着和编译码协议相关的codec结构体({ "gbk", NULL, NULL, gbk_encode, NULL, NULL, gbk_decode, NULL, NULL }),一手拿着我们传入的unicode对象data
通过中间对象MultibyteEncodeBuffer buf;将输入信息buf.inbuf捣腾成buf.outobj。
说白了,就是把unicode的0x6c49变成gbk的0xbaba,然后fwrite到<stdout>。

  1. [multibytecodec.c]
  2. static PyObject *
  3. multibytecodec_encode(MultibyteCodec *codec,
  4. MultibyteCodec_State *state,
  5. const Py_UNICODE **data, Py_ssize_t datalen,
  6. PyObject *errors, int flags)
  7. {
  8. ...
  9. while (buf.inbuf < buf.inbuf_end) {
  10. Py_ssize_t inleft, outleft;
  11. /* we don't reuse inleft and outleft here.
  12. * error callbacks can relocate the cursor anywhere on buffer*/
  13. inleft = (Py_ssize_t)(buf.inbuf_end - buf.inbuf);
  14. outleft = (Py_ssize_t)(buf.outbuf_end - buf.outbuf);
  15. r = codec->encode(state, codec->config, &buf.inbuf, inleft,
  16. &buf.outbuf, outleft, flags);
  17. if ((r == 0) || (r == MBERR_TOOFEW && !(flags & MBENC_FLUSH)))
  18. break;
  19. else if (multibytecodec_encerror(codec, state, &buf, errors,r))
  20. goto errorexit;
  21. else if (r == MBERR_TOOFEW)
  22. break;
  23. }
  24. [_codecs_cn.c]
  25. ENCODER(gbk)
  26. {
  27. while (inleft > 0) {
  28. Py_UNICODE c = IN1;
  29. DBCHAR code;
  30. if (c < 0x80) {
  31. WRITE1((unsigned char)c)
  32. NEXT(1, 1)
  33. continue;
  34. }
  35. UCS4INVALID(c)
  36. REQUIRE_OUTBUF(2)
  37. GBK_ENCODE(c, code)
  38. else return 1;
  39. OUT1((code >> 8) | 0x80)
  40. if (code & 0x8000)
  41. OUT2((code & 0xFF)) /* MSB set: GBK */
  42. else
  43. OUT2((code & 0xFF) | 0x80) /* MSB unset: GB2312 */
  44. NEXT(1, 2)
  45. }
  46. return 0;
  47. }
  48. /* GBK and GB2312 map differently in few codepoints that are listed below:
  49. *
  50. * gb2312 gbk
  51. * A1A4 U+30FB KATAKANA MIDDLE DOT U+00B7 MIDDLE DOT
  52. * A1AA U+2015 HORIZONTAL BAR U+2014 EM DASH
  53. * A844 undefined U+2015 HORIZONTAL BAR
  54. */
  55. #define GBK_DECODE(dc1, dc2, assi) \
  56. if ((dc1) == 0xa1 && (dc2) == 0xaa) (assi) = 0x2014; \
  57. else if ((dc1) == 0xa8 && (dc2) == 0x44) (assi) = 0x2015; \
  58. else if ((dc1) == 0xa1 && (dc2) == 0xa4) (assi) = 0x00b7; \
  59. else TRYMAP_DEC(gb2312, assi, dc1 ^ 0x80, dc2 ^ 0x80); \
  60. else TRYMAP_DEC(gbkext, assi, dc1, dc2);
  61. #define GBK_ENCODE(code, assi) \
  62. if ((code) == 0x2014) (assi) = 0xa1aa; \
  63. else if ((code) == 0x2015) (assi) = 0xa844; \
  64. else if ((code) == 0x00b7) (assi) = 0xa1a4; \
  65. else if ((code) != 0x30fb && TRYMAP_ENC_COND(gbcommon, assi, code));
  66. #define _TRYMAP_ENC(m, assi, val) \
  67. ((m)->map != NULL && (val) >= (m)->bottom && \
  68. (val)<= (m)->top && ((assi) = (m)->map[(val) - \
  69. (m)->bottom]) != NOCHAR)
  70. #define TRYMAP_ENC_COND(charset, assi, uni) \
  71. _TRYMAP_ENC(&charset##_encmap[(uni) >> 8], assi, (uni) & 0xff)

都是一系列的查map、移位、计算,这里就不深入了。

有意思的是,查看控制台属性,能发现cp936和gbk的来源,感兴趣的同学可以试试修改这个值再开客户端,看看python又是怎么处理中文的。

 

总结:


1. fwrite和fputc、fprintf属于两组不同的输出接口
2. 变量名+回车的打印(repr方式)会要求字符串加工,print则打印原生字符串(str方式)
3. print unicode才需要转码(encode),print str则不需要
4. 转码,是通过encoding找到对应的py文件,再找到encoder和decoder的,核心逻辑都是查表和位运算。

 

遗留问题:


1. 字符编码的历史和规则
ascii cp936 gbk gb2312 utf8 utf16 这些都是怎么来的?

2. python的字符编码识别和转码
什么时候decode,什么时候encode?
怎么识别文件的字符编码?
怎么识别字符串的字符编码?

3. 文件读写接口的具体不同
fgetc和fputc
fgets和fputs
fread和fwrite
fscanf和fprinf

4. 文本分段fwrite,会不会乱码?

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/199229
推荐阅读
相关标签
  

闽ICP备14008679号