一、计算机储存
1.1、计算机对数据类型的辨别
编译器在编译C程序时将其转变为汇编指令,其中指明了数据类型,此外,每种数据类型都有固定的存储长度,计算机运行程序时,会根据具体类型读出相应长度的数据进行计算
1.2、程序的储存
指令空间+静态数据空间+动态数据空间
1.3、字长
计算机进行一次运算所能处理的二进制最大位数,常用的有32位、16位、8位等。
二、数据类型和运算篇
2.1、C语言数据长度(机器字长32位):
int :4字节(=字长) long :4字节
float:4字节 double:8字节
short:2字节 char:1字节
一个字节代表8bit,就是8个2进制位
%p:以地址的形式打印
%x:打印16进制数字
%o:打印8进制数字
2.2、赋值运算中的类型自动转换
将数据长度短的转换为数据长度长的;
数据类型不同,则转换为相同类型
浮点运算总是转换为double类型
有符号与无符号混合运算时,总是转换为无符号
当赋值号右边式子计算完后,其结果类型自动转换为左边的数据类型
2.3、负数右移,在补码的右边补1,因此,多次右移后,补码每一位都变为1,即负数值为-1
2.4、自增自减运算的代码执行速度比赋值块【i++比i=i+1快】
2.5、复合赋值语句的代码执行速度比先运算再赋值块【直接一个等式出】
三、控制语句篇
3.1、除了二维数组,还可以定义更高维的数组,如a22[2],意义上表示空间,但是使用更高维的数组会使得计算机计算下标的工作量变大,影响效率
3.2、数组初始化特殊赋值方法
C99特性:
int a[40]={2,[10]=3,[30]=9}; //其他元素值都为0 int b[10][10]={[5][6]=2}; //其他元素都为0
3.3、动态分配数组
int *a; a=(int*)malloc(10,sizeof(int)); a[0]=1;a[1]=2; a=(int*)realloc(15,sizeof(int)); //数组扩展(原数据保留) free(a);
四、函数篇
4.1、可变函数创建
void func(int length,……) { int i; va_list vp; va_start(vp,length); for(i=0;i<length:i++) printf("%d",var_arg(v[,init])); va_end(vp); }
4.2、void函数
-
void代表无返回值,不需要return
-
void代表返回值的类型是无类型,return要写但后面不加变量。
五、特殊数据类型篇
5.1、联合
类似于结构体,但成员公用一段内存,该内存的大小为成员最大长度,当为一成员赋值时,其他成员的值就会被覆盖
union myunion { char a; int b; }; union myunion c;
联合器的各个成员公用内存,并应该只能有一个成员得到这块内存的使用权(即对内存的读写)
结构体是选取最大的内存为储存空间,联合体是累加为储存空间
5.2、位域
将一个字的每一位看成成员来操作,位域不能跨越两个字节,因此其长度不能超过8位,定义方法:
struct font{ unsigned char italic:1; unsigned char bold:1; unsigned char :4; unsigned char underline:2; } struct font font1; font1.italic=0; font1.bold=1; font1.underline=3;
该位域成员包括:占用字节bit0位的italic、占用bit1位的bold、占用bit2-bit5四位的保留位、占用bit6和bit7的underline
5.3、位域与联合的组合运用
union Byte { unsigned char byte; struct { bit0:1; bit1:1; bit2:1; bit3:1; bit4:1; bit5:1; bit6:1; bit7:1; }bit; };
通过上面的组合,既可以整体操作字节,也可以方便实现位操作
六、内存管理篇
6.1、内存组织形式:
静态存储分配:编译时确定的变量空间,像全局变量与静态变量采用这种方式分配
栈:在编译时不分配空间,但需要知道程序所需的空间大小,然后在程序运行时进行分配,像函数内部的局部变量就采用这种分配方式,栈的分配方向是高地址方向向低地址,并且分配时连续的,是先入后出的队列结构,栈由编译器分配与释放,它的空间小于堆,当申请的空间超过最大栈空间时,会提示:“堆栈溢出”
堆:堆的分配时不连续块为形式的,系统通过链表将这些块连接起来,例如malloc等函数就是在堆中进行分配。堆的空间一般比较大。
七、goto,void,extern,sizeof、define分析
7.1、Goto语句分析
高手潜规则:禁用goto
项目经验:程序质量与goto出现的次数成反比
尽量少用
7.2、void的意义
void修饰的函数返回值和函数。void修饰函数返回值和参数仅为了表示无,C语言中没有定义void究竟是多达内存的别名
void指针的意义:
C语言规定只有相同类型的指针才能相互赋值。void指针作为左值用于“接收”任意类型的指针,void指针作为右指针赋值给替他指针时需要强制类型转换。
7.3、extern中隐藏的意义
extern用于声明外部定义的变量和函数,extern用于告诉编译器C方式编译
7.4、sizeof关键字分析
sizeof是编译器的内置指示符,不是函数,sizeof的值在编译期就已经确定了,单位是字节,返回的都是类型的字节
C语言规定sizeof返回size_t类型的值,这是一个无符号整数类型,这是因为C头文件系统使用typedef把size_t作为unsigned int或unsigned long的别名
typedef unsigned int size_t int main(){ int a=10; printf("%d\n",sizeof(a)); printf("%d\n",sizeof(int)); //a的类型 printf("%d\n".sizeof a); //a两端的括号省略---->sizeof不是函数,函数必须有() int arr[10]={1,2,3,4,5,6}; printf("%d\n",sizeof(arr)); printf("%d\n",sizeof(int[10])); }
7.5、const修饰变量
在C语言中const修饰的变量是只读的,其本质还是变量。const不是真的变量,可以通过指针改变其值。本质上,const只对编译器有用,在运行时无用
const int* p; //p可变,p指向的内容不可变
int const* p; //p可变,p指向的内容不可变
int* const p; //p不可变,p指向的内容可变
const int const p; //p和p指向的内容都不可变
*口诀:左数右指*
当const出现在*号左边时指针指向的数据为常量。
当const出现在*后右边时指针本身为常量。
7.6、volatile
可以理解为“编译器警告指示字”,用于告诉编译器必须每次去内存中取变量值。主要修饰可能被多个线程访问的变量,也可以修饰被未知因数更改的变量
多线程,嵌入式。给编译器看,让CPU必须从内存中调取变量。
优化时,编译器会将变量没有变化的自动返回原来的值,而不会去内存中再去调用。
7.7、define
由调整器去处理,无类型安全检测,不会分配内存,储存在代码段。
可通过#undef取消
八、条件编译
类似于C语言中的if……else,条件编译是预编译指示命令,用于控制是否编译某段代码。#if……#else……#endif被预编译器处理
#pragma指令用于指示编译器完成一些特定的动作,是一个预处理指令,用于向编译器提供窗外信息的标准方法。
九、scanf和scanf_s的区别
9.1、使用区别
sanf()不会检查输入边界,可能会造成数据溢出
scanf_s()会进行边界检查
9.2、意思
scanf表示从键盘输入指定格式的数据,因为带“_s”后缀的函数是为了让原版函数更安全,传入一个和参数有关的大小值,避免用到不存的元素,防止hacker利用原版的不安全性(漏洞)黑掉系统
9.3、参数不同
例如scanf("%s",&name,n),整形n为name类型的大小,如果name是数组,那n就是该数组的大小
十、if的判断
0为假,非0都为真,
引入stdbool.h库可以使用true和false
十一、操作符篇
11.1、算法操作符
11.1.2、除法操作符
-
对于/ 操作符,如果两个操作符都为整数,执行整数除法,但是会截断计算结果的小数部分,不会四舍五入
-
对于负数来说,驱0截断
-
两个操作符只要有一个是浮点数执行的就是浮点数除法,计算的结果是浮点数(事实上,计算机不会真的用浮点数除以整数,编译器会把两个运算对象转换为相同的类型,)
11.1.3、求模运算符
-
操作符的两个操作数必须是整数;返回的是整除之后的余数
-
负数求模的结果取决于第一个操作数
11.2、移位操作符
“<<” 左移操作符:左边抛弃、右边补0
">>" 右移操作符:逻辑右移:补0(无符号必须逻辑右移)
算术右移:补最高位(基本对有符号数使用算术右移)用原该值的符号填充
注:移位操作符的操作数只能是整数
11.3、位操作符
-
按位与&:同1为1,有0则0
-
按位或|:有1为1,同0为0
-
按位异或^:相异为1;相同为0
注:他们的操作数必须是整数
int main(){ int a=3; // 00000000 00000000 00000000 00000011 —3的补码 int b=-5; // 11111111 11111111 11111111 11111011 -5的补码 int c=a&b; // == 3 int d= a | b; // == -5 int e= a^b; //按位异或 == -8 }
例题1:不能创建临时变量(第三个变量),实现两个数的交换
//法1:方法不太好,a+b时可能会导致正溢出 int main() { int a=3; int b=5; printf("交换前:a=%d b=%d",a,b); a = a+b; b = a -b; a = a -b; printf("交换后:a=%d b=%d",a,b); return 0; }
//异或法 a^a=0; 0^b=b; int main() { int a=3; int b=4; printf("交换前:a=%d b=%d\n", a, b); a = a ^ b; b = a ^ b;//a^b^b=a a = a ^ b;//a^b^a=b printf("交换后:a=%d b=%d\n", a, b); return 0; }
11.4、赋值操作符
11.4.1、左值与右值
左值:是可以放在等号左边的,一般是一块空间
右值:是可以放在等号右边的,一般是一个值,或者一块空间的内容
复合赋值符
a+=b <----> a=a+b a-=b <----> a=a-b a*=b <----> a=a*b a/=b <----> a=a/b a%=b <----> a=a%b a>>=b <----> a=a>>b a<<=b <----> a=a<<b a&=b <----> a=a&b a|=b <----> a=a|b a^=b <----> a=a^b
11.5、单目操作符
! 逻辑反操作 - 负值 + 正值 & 取地址 sizeof 操作数的类型长度(以字节为单位) ~ 对一个数的二进制按位取反 -- 前置、后置-- ++ * 间接访问操作符
按位取反(~),可以灵活地用较小的数表示极大数
注意事项
-
递增和递减操作符只能影响一个变量(只能影响一个可修改的左值)
-
x * y++理解为x *(y++)
-
-
递增和递减操作具有很高的结合优先级,只有圆括号的优先级比他们高
注意:不要对自增、自减操作符太过关注
如果一个变量出现在一个函数的多个参数中,不·要对该变量使用递增或递减操作符
如果一个变量多次出现在一个表达式中,不要对该变量使用递增或递减符。方法是记录
间接访问操作符(解引用操作符)(*)
int a=10; int * pa=&a; *pa=20; //*解引用操作符 printf("%d\n",a); int *px=&(*pa); //其实是对a取地址 *px=30; printf("%d\0n",a); return 0;
(类型)强制转换
1、在某个量面前放置用
11.5、关系操作符
11.6、逻辑操作符
&& 逻辑与
|| 逻辑或
逻辑操作符只关注真假
逻辑与------都得满足才行11
逻辑或------满足其中一个即可
面试题:
#include <stdio.h> int main(){ int i=0,a=0,b=2,c=3,d=4; i=a++&&++b&&d++; //第一个a为假,则后边被短路,不会被计算 printf("a=%d\n b=%d\n c=%d\n d=%d\n",a,b,c,d); //1.2.3.4; return 0; }
逻辑或与的短路原则:左操作数为真,右边不计算
#include<stdio.h> int main() { int i=0,a=1,b=2,c=3,d=4; i=a--||++b||d++; //此处为a--,先使用a的值,a为1,为真,则后边的短路 printf("a=%d\n b=%d\n c=%d\n d=%d\n",a,b,c,d); //0.2.3.4; return 0; }
11.7、条件操作符
也叫作三目操作符
表达式1?表达式2:表达式3
11.8、逗号表达式
用逗号隔开多个表达式,从左到右7依次执行、整个表达式的结果是最后一个表达式的结果
·2//代码1 int a = 1; int b = 2; int c = (a>b, a=b+10, a, b=a+1);-------关系操作符a>b不影响a,b的值,不用管 是多少? a=2+10 12 b=12+1=13 //代码2 if (a =b + 1, c=a / 2, d > 0) ----------d大于0 ,才执行if语句 //代码3 a = get_val(); count_val(a);//先处理一遍 while (a > 0) { //业务处理 a = get_val(); count_val(a); } 如果使用逗号表达式,改写: while (a = get_val(), count_val(a), a>0) { //业务处理 } 也可以使用do while来简化
11.9、下标引用、函数调用和结构成员
11.9.1、【】下标引用操作符
操作数:一个数组名+一个索引值
arr【2】:arr是首元素地址,arr【2】-------->编译器计算后:*(arr+2)----------->(7+arr)<---------->7[arr]
11.9.2、函数调用操作符
接收一个或者多个操作数:第一个操作数是函数名、剩余的操作数就是传递个函数的参数
11.9.3、结构成员
整型提升的意义:
-
表达式的整型运算要在CPU的相应运算器内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的想加、在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度
-
通用CPU是难以直接实现两个8比特字节字节相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须转换为int或unsigned int,然后才能送入CPU去执行运行计算
转换规则:向大的转换
一些问题表达式
1、计算路径不唯一
优先级可以保证先进行乘法运算然后再进行加法运算,不同的编译器选择的方案不同,因此产生了不唯一的计算路径
a*v+c*d+e*f 只能保证*的计算是比+早,但是优先级并不能决定第三个*比+早执行
2、操作数取值不唯一
c + --c;--c先算,但是+左边的c不知道是--c前取还是--c后取的
注释:操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们没有办法得知,+操作符的左操作符的获取在右操作符之前还是之后求值,所以结果是不可预测的
3、完全非法错误代码
int main() { int i=10; i=i-- - --i *(i=-3)*i++ + ++i; //错误代码 printf("i=%d\n",i); return 0; }
4、函数调用先后顺序不确定
int fun() { static int cont=1; return ++count; } int main() { int answer; answer=fun()-fun()*fun(); printf("%d\n",answer); return 0; }
上述代码answer=fun()-fun()*fun();中我们只能通过操作符的优先级得知,先算乘法,再算减法,但是函数调用先后顺序无法通过操作符的优先级确定。
5、操作数取值不唯一
#include <stdio.h> int main() { int i = 1; int ret = (++i) + (++i) + (++i); printf("%d\n", ret); printf("%d\n", i); return 0; }
十二、(Linux)多线程
12.1、线程
轻量级的进程,线程虽然不是进程,但却可以看做是Unix进程的表亲,在同一进程中的多条线程将共享该进程的全部系统资源,如虚拟地址空间,文件描述和信号处理等等。但统一进程的多个线程有各自的调用栈,自己的寄存器环境,自己的线程本地储存,一个进程可以有很多线程,每条线程并行执行不同的任务。
线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生阻塞的情况的表现性能。
使用了多线程,可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。
创建线程 pthread_creat
pthread_create (thread, attr, start_routine, arg)
参数 | 描述 |
---|---|
thread | 指向线程标识符指针 |
attr | 一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值NULL |
start_routine | 线程运行函数起始地址,一旦线程被创建就会执行 |
arg | 运行函数的参数,它必须通过把引用作为指针强制转换为void类型进行传递。如果没有传递参数,则使用NULL |
For example:-pthread_t thrd1; -pthread_arr_t attr; -void thread_function(void argument); -char *some_argument; phread_create(&thrd1,NULL,(void*)&thread_function,(void*)&some_argument)
结束线程
线程结束调用实例:phread_exit(void*retval); //retval用于存放线程结束的退出状态
用于显式地退出一个线程,通常情况下,pthread_exit()函数是在线程完成工作后无需继续存在时被调用
连接和分离线程
pthread_join(threadid,status);
pthread_detach(thread)
pthread_join() 子程序阻碍调用程序,直到指定的 threadid 线程终止为止。当创建一个线程时,它的某个属性会定义它是否是可连接的(joinable)或可分离的(detached)。只有创建时定义为可连接的线程才可以被连接。如果线程创建时被定义为可分离的,则它永远也不能被连。pthread_join() 函数来等待线程的完成。
包含两个参数
pthread_t th //th是要等待结束的线程标识 void **thread_return //指针thread_return指向的位置存放的是终止线程的返回状态
调用实例:pthread_join(thrd1,NULL);
简单例子:
十三、Windows的多线程
13.1、定义线程函数
线程就是描述进程的一条执行路径,进程内代码的一条执行路径。一个进程至少有一个主线程,且可以有多个线程,线程共享进程的所有资源。线程主要包括两个部分:
-
一个是线程的内核对象,操作系统用它来对线程实施管理,内核对象也是系统用来存放线程统计信息的地方
-
另一个是线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量。
实现多线程,就要启动线程函数,线程函数不同于常规函数,线程函数需要用到WINAPI来定义,
DWORD WINAPI welcomedonghuasub(void *p) { char str[50]; int i; for(i=0;i<43;i++) { memset(str,0,sizeof(char)*50); printf(str,"welcome".i+1) } ExitThread(0); }
一、CreateThread
函数功能:创建线程
函数原型:
HANDLE WINAPI CreateTtread (
LPSECURITY_ATTRIBUTESlpThreadAttributes,
SIZE_TdwStackSize,
LPTHREAD_START_ROUTINElpStartAddress,
LPVOIDlpParameter,
DWORDdwCreationFlags,
LPDWORDlpThreadId
);
函数说明:
第一个参数表示线程内核对象的安全属性,一般传入NULL表示使用默认设置
第二个参数表示线程栈空间大小,传入0表示使用默认大小
第三个参数表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址
第四个参数是传给线程函数的参数
第五个参数指定额外的标志来控制线程的创建,为0表示线程创建之后就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用Resume Thread()。
第六个参数将返回线程的ID号,传入NULL表示无需要返回该线程ID号
函数返回值:
成功返回新线程的句柄,失败返回NULL
HANDLE handle = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL);
二、WaitForSingleObject
函数功能:等待函数-使线程进入等待状态,直到指定的内核对象被触发·
函数原形:
DWORD WINAPI WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
函数说明:
第一个参数为要等待的内核对象
第二个参数为最长等待时间,以毫秒为单位,如传入5000就表示5秒,传入0就立即返回,传入INFINETE表示无线等待。
因为线程的句柄在线程运行时是未触发的,线程结束运行,句柄处于触发状态。所以可以用WaitForSingleObject()来等待一个线程结束运行。
函数返回值:
在指定的时间内对象被触发,函数返回WAIT_OBJECT_0. 超过最长等待时间对象任未触发返回WAIT_TIMEOUT。传入参数有错误将返回WAIT_FAILED
#include <stdio.h> #include<Windows.h> DWORD WINAPI ThreafFunc(LPVOID); int main() { HANDLE hThread; hThread = CreateThread(NULL,0,,ThreadFunc,0,0,NULL); printf("我是主线程,pid=%d\n",GetCurrentThreadId()); WaitForSingleObject(hThread,0); return 0; } DWORD WINAPI ThreadFunc(LPVOID p) { Sleep(10000); printf("我是子线程·,pid=%d\n",GetCurrentThreadId()); return 0; }
三、CreateThread和__beginthreadex()的区别
首先是从标准C运行库与多线程的矛盾说起,标准C运行库在1970年被实现了,由于当时没任何一个操作系统提供对多线程的支持。因此编写标准C运行库的程序员根本没有考虑多线程使用标准C库运行的情况。
多个线程访问修改导致的数据覆盖问题。
Windows操作系统提供了这样的一种解决方案——每个线程都将拥有自己专用的一块内存区域来供标准C运行库中所有有需要的函数使用。而且这块内存区域的创建就是由C/C++运行库函数_beginthreadex()来负责的
因此,如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()。
//创建多子个线程实例 #include <stdio.h> #include <process.h> #include <windows.h> //子线程函数 unsigned int __stdcall ThreadFun(PVOID pM) { printf("线程ID号为%4d的子线程说:Hello World\n", GetCurrentThreadId()); return 0; } //主函数,所谓主函数其实就是主线程执行的函数。 int main() { printf(" 创建多个子线程实例 \n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); const int THREAD_NUM = 5; HANDLE handle[THREAD_NUM]; for (int i = 0; i < THREAD_NUM; i++) handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL); WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); return 0; }
四、等待多个线程返回WaitForMulitpleObjects
函数原型:
DWORD WINAPI WaitForMultipleObjects( _In_ DWORD nCount, _In_ const HANDLE *lpHadnles, _In_ BOOL bWaitAll, _In_ DWORD dwMilliseconds )
参数说明:
-
第一个参数DWORD dwCount为等待的内核对象个数,可以是0到MAXIMUM_WAIT_OBJECTS(64)中的一个值
-
第一个参数CONST HANDLE*phObjects为一个存放被等待的内核对象句柄的数组。\
-
第一个参数BOOL bWaitAll是否等到所有内核对象为已通知状态后才返回,如果为TRUE,则只有当等待的所有内核对象为已通知状态函数才返回,如果为FALSE,则只要一个内核对象为已通知状态,则该函数返回
-
第一个参数DWORD dwMilliseconds为等待时间,和WaitForSingleObject中的dwMilliseconds参数类似
实例
#include<stdio.h> #include<windows.h> const unsigned int THREAD_NUM = 10;, DWORD WINAPI ThreadFunc(LPVOID); int main(){ printf("我是主线程,pid=%d\n",GetCurrentThreadId()); HANDLE hThread[THREAD_NUM]; for(int i=0;i<=THREAD_NUM;i++) { hThread[i]=CreateThread(NULL,0,ThreadFunc,&i,0,NULL); } WaitForMultipleObjects(THREAD_NUM,hThread,false,INFINITE); return 0; } DWORD WINAPI ThreadFunc(LPVOID p) { int n=*(int *)p; Sleep(1000*n); printf("我是,pid=%d的子线程\n",GetCurrentThreadId()); printf("pid =%d 的子线程退出\n",GetCurrentThreadId()); return 0; }
参数bWaitAll为true,等待所有线程返回
四、线程终止
void ExitThread(DWORD dwExitCode);
该函数将终止线程的进行,并导致损伤系统清除该线程所有操作系统资源。
即便要强制终止线程,也要使用_endThreadEx(不使用endThread_),因为它会兼顾多线程资源的安全
BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);
异步函数,即它告诉系统去终止指定线程,但是不能保证函数返回线程已经被终止了,因此调用者必须使用WairForSingleObject函数来确定线程是否终止。因此此函数调用终止后终止的线程堆栈资源不会释放,一般不建议使用该函数。
十四、乱码
一、之前的字符集
ASCLL字符 ,适用于128个字符
GB2312
CBK :大陆的
二、Unicode
Unicode:包含最广泛。
字符集只是字符和对应码点的集合,不代表字符一定会以对应码点被存储在计算机里
字符编码才是从字符到计算机存储内容的映射,32比特
UTF-8,针对不同字符,编码后的长度可以是32比特、24比特、16比特、8比特【主流 】
具体规则是:码点在0到127范围内的字符字节映射为1字节长度的二进制数,
码点在128到2047范围内的字符映射为2字节的二进制数。让二进制编码的第一个字节由110开头,第二个字节由10开头。
码点在2048到65535范围的字符。映射为3字节的二进制数。第一个字节由1110开头、第二个字节由10开头、第三个字节由10开头。分为三个部分
码点在65536到1114111范围的字符,映射为4字节的二进制数,第一个字节由11110开头,后面都有10开头
但储存效率会越来越低
优点:
-
兼容ASCLL
-
节约空间
三、乱码的诞生
一种是Unicode和中文编码转换时产生。无法识别的用特殊符号代替【锟斤拷】