分类: C/C++
2012-11-15 17:52:57
auto:编译器在默认的缺省情况下,所有变量都是auto 的
register:register 变量必须是能被CPU 寄存器所接受的类型。意味着register 变量必须是一个单个的值,并且其长度应小于或等于整型的长度。而且register 变量可能不存放在内存中,所以不能用取址运算符“&”来获取register 变量的地址
static
修饰变量: 变量分为全局变量和局部变量,都存在于内存的静态区
静态全局变量,作用域仅限于变量被定义的文件中,其他文件即使用extern 声明也没法
使用他。准确地说作用域是从定义之处开始,到文件结尾处结束,在定义之处前面的那些
代码行也不能使用它。想要使用就得在前面再加extern ***获取register 变量的地址
静态局部变量,在函数体内定义的,就只能在这个函数里使用了,同一个文档的其他函数也使用不了。由于被static
修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值
修饰函数:函数前加static 使得函数成为静态函数。但此处“static”的含义
不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。使用内部函
数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件
中的函数同名
Sizeof
sizeof 是关键字不是函数
记住:sizeof 在计算变量所占空间大小时,括号可以省略,而计算类型(模子)大小时不能省略
上面的解释很容易理解,下面就考虑一下这个问题:
int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}
答案是255。
for 循环内,当i 的值为0 时,a[0]的值为-1。关键就是-1 在内存里面如何存储。
我们知道在计算机系统中,数值一律用补码来表示(存储)。主要原因是使用补码,可
以将符号位和其它位统一处理;同时,减法也可按加法来处理。另外,两个用补码表示的数
相加时,如果最高位(符号位)有进位,则进位被舍弃。正数的补码与其原码一致;负数的
补码:符号位为1,其余位为该数绝对值的原码按位取反,然后整个数加1。
按照负数补码的规则,可以知道-1 的补码为0xff,-2 的补码为0xfe……当i 的值为127
时,a[127]的值为-128,而-128 是char 类型数据能表示的最小的负数。当i 继续增加,a[128]
的值肯定不能是-129。因为这时候发生了溢出,-129 需要9 位才能存储下来,而char 类型
数据只有8 位,所以最高位被丢弃。剩下的8 位是原来9 位补码的低8 位的值,即0x7f。
当i 继续增加到255 的时候,-256 的补码的低8 位为0。然后当i 增加到256 时,-257 的补
码的低8 位全为1,即低八位的补码为0xff,如此又开始一轮新的循环……
按照上面的分析,a[0]到a[254]里面的值都不为0,而a[255]的值为0。strlen 函数是计
算字符串长度的,并不包含字符串最后的‘\0’。而判断一个字符串是否结束的标志就是看
是否遇到‘\0’。如果遇到‘\0’,则认为本字符串结束。
分析到这里,strlen(a)的值为255 应该完全能理解了。这个问题的关键就是要明白char
类型默认情况下是有符号的,其表示的值的范围为[-128,127],超出这个范围的值会产生溢
出。另外还要清楚的就是负数的补码怎么表示
留三个问题:
1),按照我们上面的解释,那-0 和+0 在内存里面分别怎么存储?
2),int i = -20;
unsigned j = 10;
i+j 的值为多少?为什么?
3), 下面的代码有什么问题?
unsigned i ;
for (i=9;i>=0;i--)
{
printf("%u\n",i);
}
bool 变量与“零值”进行比较
bool 变量与“零值”进行比较的if 语句怎么写?
bool bTestFlag = FALSE;//想想为什么一般初始化为FALSE 比较好?
A), if(bTestFlag == 0); if(bTestFlag == 1);
B), if(bTestFlag == TRUE); if(bTestFlag == FLASE);
C), if(bTestFlag); if(!bTestFlag);
哪一组或是那些组正确呢?我们来分析分析:
A)写法:bTestFlag 是什么?整型变量?如果要不是这个名字遵照了前面的命名规范,
肯怕很容易让人误会成整型变量。所以这种写法不好。
B)写法:FLASE 的值大家都知道,在编译器里被定义为0;但TRUE 的值呢?都是1
吗?很不幸,不都是1。Visual C++定义为1,而它的同胞兄弟Visual Basic 就把TRUE 定义
为-1.那很显然,这种写法也不好。
(C)大家都知道if 语句是靠其后面的括号里的表达式的值来进行分支跳转的。表达式如果
为真,则执行if 语句后面紧跟的代码;否则不执行。那显然,本组的写法很好,既不会引
起误会,也不会由于TRUE 或FLASE 的不同定义值而出错。记住:以后写代码就得这样写。
float 变量与“零值”进行比较
float 变量与“零值”进行比较的if 语句怎么写?
float fTestVal = 0.0;
A), if(fTestVal == 0.0); if(fTestVal != 0.0);
B), if((fTestVal >= -EPSINON) && (fTestVal <= EPSINON)); //EPSINON 为定义好的
精度。
哪一组或是那些组正确呢?我们来分析分析:
float 和double 类型的数据都是有精度限制的,这样直接拿来与0.0 比,能正确吗?明显
不能,看例子: p 的值四舍五入精确到小数点后10位为:3.1415926536,你拿它减去
0.00000000001 然后再四舍五入得到的结果是多少?你能说前后两个值一样吗?
EPSINON 为定义好的精度,如果一个数落在[0.0-EPSINON,0.0+EPSINON] 这个闭区间
内,我们认为在某个精度内它的值与零值相等;否则不相等。扩展一下,把0.0 替换为你想
比较的任何一个浮点数,那我们就可以比较任意两个浮点数的大小了,当然是在某个精度
内。
同样的也不要在很大的浮点数和很小的浮点数之间进行运算,比如:
10000000000.00 + 0.00000000001
这样计算后的结果可能会让你大吃一惊。
指针变量与“零值”进行比较
指针变量与“零值”进行比较的if 语句怎么写?
int* p = NULL;//定义指针一定要同时初始化,指针与数组那章会详细讲解。
A), if(p == 0); if(p != 0);
B), if(p); if(!p);
C) , if(NULL == p); if(NULL != p);
哪一组或是那些组正确呢?我们来分析分析:
A)写法:p 是整型变量?容易引起误会,不好。尽管NULL 的值和0 一样,但意义不同。
B)写法:p 是bool 型变量?容易引起误会,不好。
C)写法:这个写法才是正确的,但样子比较古怪。为什么要这么写呢?是怕漏写一个
“=”号:if(p = NULL),这个表达式编译器当然会认为是正确的,但却不是你要表达的意思。
所以,非常推荐这种写法
使用if 语句的其他注意事项
【规则1-17】先处理正常情况,再处理异常情况。
在编写代码是,要使得正常情况的执行代码清晰,确认那些不常发生的异常情况处理
代码不会遮掩正常的执行路径。这样对于代码的可读性和性能都很重要。因为,if 语句总是
需要做判断,而正常情况一般比异常情况发生的概率更大(否则就应该把异常正常调过来
了),如果把执行概率更大的代码放到后面,也就意味着if 语句将进行多次无谓的比较。另
外,非常重要的一点是,把正常情况的处理放在if 后面,而不要放在else 后面。当然这也
符合把正常情况的处理放在前面的要求
switch、case 组合
:case 后面只能是整型或字符型的常量或常量表达式(想想字符型数据在内存里
是怎么存的
【规则1-21】按字母或数字顺序排列各条case 语句。
如果所有的case 语句没有明显的重要性差别,那就按A-B-C 或1-2-3 等顺序排列case
语句。这样做的话,你可以很容易的找到某条case 语句
【规则1-22】把正常情况放在前面,而把异常情况放在后面。
如果有多个正常情况和异常情况,把正常情况放在前面,并做好注释;把异常情况放在
后面,同样要做注释
【规则1-24】简化每种情况对应的操作。
【规则1-25】不要为了使用case 语句而刻意制造一个变量。
void 关键字
如果指针p1 和p2 的类型相同,那么我们可以直接在p1 和p2 间互相赋值;
如果p1 和p2 指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的
指针类型转换为左边指针的类型例如:
float *p1;
int *p2;
p1 = p2;
其中p1 = p2 语句会编译出错,提示“'=' : cannot convert from 'int *' to 'float *'”,必须改为:
p1 = (float *)p2;
而void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换:
void *p1;
int *p2;
p1 = p2;
但这并不意味着,void *也可以无需强制类型转换地赋给其它类型的指针。因为“空类型”可
以包容“有类型”,而“有类型”则不能包容“空类型”。比如,我们可以说“男人和女人都是人”,
但不能说“人是男人”或者“人是女人”。下面的语句编译出错:
void *p1;
int *p2;
p2 = p1;
提示“'=' : cannot convert from 'void *' to 'int *'”。
【规则1-33】如果函数没有返回值,那么应声明为void 类型
在C 语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。
【规则1-34】如果函数无参数,那么应声明其参数为void
void 指针
【规则1-35】千万小心又小心使用void 指针类型。
按照ANSI(American National Standards Institute)标准,不能对void 指针进行算法操作,
即下列操作都是不合法的:
void * pvoid;
pvoid++; //ANSI:错误
pvoid += 1; //ANSI:错误
ANSI 标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指
向数据类型大小的。也就是说必须知道内存目的地址的确切值。
例如:
int *pint;
pint++; //ANSI:正确
但是大名鼎鼎的GNU(GNU's Not Unix 的递归缩写)则不这么认定,它指定void *的算法
操作与char *一致。因此下列语句在GNU 编译器中皆正确:
pvoid++; //GNU:正确
pvoid += 1; //GNU:正确
在实际的程序设计中,为符合ANSI 标准,并提高程序的可移植性,我们可以这样编写
实现同样功能的代码:
void * pvoid;
(char *)pvoid++; //ANSI:正确;GNU:正确
(char *)pvoid += 1; //ANSI:错误;GNU:正确
GNU 和ANSI 还有一些区别,总体而言,GNU 较ANSI 更“开放”,提供了对更多语法
的支持。但是我们在真实设计时,还是应该尽可能地符合ANSI 标准。
【规则1-36】如果函数的参数可以是任意类型指针,那么应声明其参数为void *。
典型的如内存操作函数memcpy 和memset 的函数原型分别为:
void * memcpy(void *dest, const void *src, size_t len);
void * memset ( void * buffer, int c, size_t num );
这样,任何类型的指针都可以传入memcpy 和memset 中,这也真实地体现了内存操作
函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型。如果memcpy
和memset 的参数类型不是void *,而是char *,那才叫真的奇怪了!这样的memcpy 和memset
明显不是一个“纯粹的,脱离低级趣味的”函数!
下面的代码执行正确:
例子:memset 接受任意类型指针
int IntArray_a[100];
memset (IntArray_a, 0, 100*sizeof(int) ); //将IntArray_a 清0
例子:memcpy 接受任意类型指针
int destIntArray_a[100], srcintarray_a[100];
//将srcintarray_a 拷贝给destIntArray_a
memcpy (destIntArray_a, srcintarray_a, 100*sizeof(int) );
有趣的是,memcpy 和memset 函数返回的也是void *类型
void 不能代表一个真实的变量
【规则1-37】void 不能代表一个真实的变量。
因为定义变量时必须分配内存空间,定义void 类型变量,编译器到底分配多大的内存呢。
下面代码都企图让void 代表一个真实的变量,因此都是错误的代码:
void a; //错误
function(void a); //错误
void 体现了一种抽象,这个世界上的变量都是“有类型”的,譬如一个人不是男人就是女
人(人妖不算)。
void 的出现只是为了一种抽象的需要,如果你正确地理解了面向对象中“抽象基类”的概
念,也很容易理解void 数据类型。正如不能给抽象基类定义一个实例,我们也不能定义一
个void(让我们类比的称void 为“抽象数据类型”)变量。
return 关键字
return 用来终止一个函数并返回其后面跟着的值。
return (Val);//此括号可以省略。但一般不省略,尤其在返回一个表达式的值时。
return 可以返回些什么东西呢?看下面例子:
char * Func(void)
{
char str[30];
…
return str;
}
str 属于局部变量,位于栈内存中,在Func 结束的时候被释放,所以返回str 将导致错误
【规则1-38】return 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时
被自动销毁。
这个语句有问题吗?如果没有问题,那返回的是什么?
const
精确的说应该是只读的变量,其值在编译时不能被使用,因为编译器在编译时不知道其存储的内容
const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点
const 修饰的只读变量
定义const 只读变量,具有不可变性。
例如:
const int Max=100;
int Array[Max];
这里请在Visual C++6.0 里分别创建.c 文件和.cpp 文件测试一下。你会发现在.c 文件中,
编译器会提示出错,而在.cpp 文件中则顺利运行。为什么呢?我们知道定义一个数组必须指
定其元素的个数。这也从侧面证实在C 语言中,const 修饰的Max 仍然是变量,只不过是只
读属性罢了;而在C++里,扩展了const 的含义,这里就不讨论了。
注意:const 修饰的只读变量必须在定义的同时初始化,想想为什么?
留一个问题:case 语句后面是否可以是const 修饰的只读变量呢?请动手测试一下
编译器通常不为普通const 只读变量分配存储空间,而是将它们保存在符号表中,这使
得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。
例如:
#define M 3 //宏常量
const int N=5; //此时并未将N 放入内存中
......
int i=N; //此时为N 分配内存,以后不再分配!
int I=M; //预编译期间进行宏替换,分配内存
int j=N; //没有内存分配
int J=M; //再进行宏替换,又一次分配内存!
const 定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define
一样给出的是立即数,所以,const 定义的只读变量在程序运行过程中只有一份拷贝(因为
它是全局的只读变量,存放在静态区),而#define 定义的宏常量在内存中有若干个拷贝。
#define 宏是在预编译阶段进行替换,而const 修饰的只读变量是在编译的时候确定其值。
#define 宏没有类型,而const 修饰的只读变量具有特定的类型。
const int *p; // p 可变,p 指向的对象不可变
int const *p; // p 可变,p 指向的对象不可变
int *const p; // p 不可变,p 指向的对象可变
const int *const p; //指针p 和p 指向的对象都不可变
先忽略类型名(编译器解析的时候也是忽略类型名),我们看const 离哪个近。“近水楼
台先得月”,离谁近就修饰谁。
const int *p; //const 修饰*p,p 是指针,*p 是指针指向的对象,不可变
int const *p; //const修饰*p,p 是指针,*p 是指针指向的对象,不可变
int *const p; //const修饰p,p 不可变,p 指向的对象可变
const int *const p; //前一个const 修饰*p,后一个const 修饰p,指针p 和p 指向的对象
都不可变
const 修饰符也可以修饰函数的参数,当不希望这个参数值被函数体内意外改变时使
用
const 修饰符也可以修饰函数的返回值,返回值不可被改变。例如:
const int Fun (void);
在另一连接文件中引用const 只读变量:
extern const int i; //正确的声明
extern const int j=10; //错误!只读变量的值不能改变。
我们详细讨论了const 这个关键字,我们知道const 修饰的数据是有类型的,而
define 宏定义的数据没有类型。为了安全,我建议你以后在定义一些宏常数的时候用const
代替,编译器会给const 修饰的只读变量做类型校验,减少错误的可能。但一定要注意const
修饰的不是常量而是readonly 的变量,const 修饰的只读变量不能用来作为定义数组的维数,
也不能放在case 关键字后面。
最易变的关键字----volatile
volatile 关键字和const 一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器
未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编
译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
先看看下面的例子:
int i=10;
int j = i;//(1)语句
int k = i;//(2)语句
这时候编译器对代码进行优化,因为在(1)、(2)两条语句中,i 没有被用作左值。这时候
编译器认为i 的值没有发生改变,所以在(1)语句时从内存中取出i 的值赋给j 之后,这个
值并没有被丢掉,而是在(2)语句时继续用这个值给k 赋值。编译器不会生成出汇编代码
重新从内存里取i 的值,这样提高了效率。但要注意:(1)、(2)语句之间i 没有被用作左
值才行。
再看另一个例子:
volatile int i=10;
int j = i;//(3)语句
int k = i;//(4)语句
volatile 关键字告诉编译器i 是随时可能发生变化的,每次使用它的时候必须从内存中取出i的值,因而编译器生成的汇编代码会重新从i 的地址处读取数据放在k 中。
这样看来,如果i 是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说volatile 可以保证对特殊地址的稳定访问。
但是注意:在VC++6.0 中,一般Debug 模式没有进行代码优化,所以这个关键字的作用有可能看不出来。你可以同时生成Debug 版和Release 版的程序做个测试。
问题:
const volatile int i=10;这行代码有没有问题?如果没有,那i 到底是什么属性?
extern
extern 可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,下面的代码用到的这些变量或函数是外来的,不是本文件定义的,提示编译器遇到此变量和函数时在其他模块中寻找其定义
struct 关键字
平时我们要求函数的参数尽量不多于4 个,如果函数的参数多于4 个使用起来非常容易出错(包括每个参数的意义和顺序都容易弄错),效率也会降低(与具体CPU 有关,ARM芯片对于超过4 个参数的处理就有讲究,具体请参考相关资料)。这个时候,可以用结构体压缩参数个数。
结构体所占的内存大小是其成员所占内存之和.但是下面的这种情况呢?
struct student
{
}stu;
sizeof(stu)的值是多少呢?在Visual C++ 6.0 上测试一下。
很遗憾,不是0,而是1。为什么呢?你想想,如果我们把struct student 看成一个模子的话,你能造出一个没有任何容积的模子吗?显然不行。编译器也是如此认为。编译器认
为任何一种数据类型都有其大小,用它来定义一个变量能够分配确定大小的空间。既然如此,编译器就理所当然的认为任何一个结构体都是有大小的,哪怕这个结构体为空。那万一结构体真的为空,它的大小为什么值比较合适呢?假设结构体内只有一个char 型的数据成员,那其大小为1byte(这里先不考虑内存对齐的情况).也就是说非空结构体类型数据最少需要占一个字节的空间,而空结构体类型数据总不能比最小的非空结构体类型数据所占的空间大吧。这就麻烦了,空结构体的大小既不能为0,也不能大于1,怎么办?定义为0.5个byte?但是内存地址的最小单位是1 个byte,0.5 个byte 怎么处理?解决这个问题的最好办法就是折中,编译器理所当然的认为你构造一个结构体数据类型是用来打包一些数据成员的,而最小的数据成员需要1 个byte,编译器为每个结构体类型数据至少预留1 个byte的空间。所以,空结构体的大小就定位1 个byte。
柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结
构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可
变的数组。sizeof 返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用
malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组
的预期大小。
柔性数组到底如何使用呢?看下面例子:
typedef struct st_type
{
int i;
int a[0];
}type_a;
有些编译器会报错无法编译可以改成:
typedef struct st_type
{
int i;
int a[];
}type_a;
这样我们就可以定义一个可变长的结构体, 用sizeof(type_a) 得到的只有4 , 就是
sizeof(i)=sizeof(int)。那个0 个元素的数组没有占用空间,而后我们可以进行变长操作了。通
过如下表达式给结构体分配内存:
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
这样我们为结构体指针p 分配了一块内存。用p->item[n]就能简单地访问可变长元素。
但是这时候我们再用sizeof(*p)测试结构体的大小,发现仍然为4。是不是很诡异?我们
不是给这个数组分配了空间么?
别急,先回忆一下我们前面讲过的“模子”。在定义这个结构体的时候,模子的大小就
已经确定不包含柔性数组的内存大小。柔性数组只是编外人员,不占结构体的编制。只是说
在使用柔性数组时需要把它当作结构体的一个成员,仅此而已。再说白点,柔性数组其实与
结构体没什么关系,只是“挂羊头卖狗肉”而已,算不得结构体的正式成员。
需要说明的是:C89 不支持这种东西,C99 把它作为一种特例加入了标准。但是,C99
所支持的是incomplete type,而不是zero array,形同int item[0];这种形式是非法的,C99 支
持的形式是形同int item[];只不过有些编译器把int item[0];作为非标准扩展来支持,而且在
C99 发布之前已经有了这种非标准扩展了,C99 发布之后,有些编译器把两者合而为一了。
当然,上面既然用malloc 函数分配了内存,肯定就需要用free 函数来释放内存:
free(p);
经过上面的讲解,相信你已经掌握了这个看起来似乎很神秘的东西。不过实在要是没
掌握也无所谓,这个东西实在很少用。
union 关键字
union 关键字的用法与struct 的用法非常类似。
union 维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间,在union 中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址。例子如下:
union StateMachine
{
char character;
int number;
char *str;
double exp;
};
一个union 只配置一个足够大的空间以来容纳最大长度的数据成员,以上例而言,最大长度是double 型态,所以StateMachine 的空间大小就是double 数据类型的大小。
在C++里,union 的成员默认属性页为public。union 主要用来压缩空间。如果一些数据不可能在同一时间同时被用到,则可以使用union。
大小端模式对union 类型数据的影响
下面再看一个例子:
union
{
int i;
char a[2];
}*p, u;
p =&u;
p->a[0] = 0x39;
p->a[1] = 0x38;
p.i 的值应该为多少呢?
这里需要考虑存储模式:大端模式和小端模式。
大端模式(Big_endian):字数据的高字节存储在低地址中,而字数据的低字节则存放
在高地址中。
小端模式(Little_endian):字数据的高字节存储在高地址中,而字数据的低字节则存放
在低地址中。
union 型数据所占的空间等于其最大的成员所占的空间。对union 型的成员的存取都是
相对于该联合体基地址的偏移量为0 处开始,也就是联合体的访问不论对哪个变量的存取都
是从union 的首地址位置开始。如此一解释,上面的问题是否已经有了答案呢?
枚举与#define 宏的区别
下面再看看枚举与#define 宏的区别:
1),#define 宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
2),一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
3),枚举可以一次定义大量相关的常量,而#define 宏一次只能定义一个
typedef 与#define 的区别
看如下例子:
E), #define INT32 int
unsigned INT32 i = 10;
F),typedef int int32;
unsigned int32 j = 10;
其中F)编译出错,为什么呢?E)不会出错,这很好理解,因为在预编译的时候INT32
被替换为int,而unsigned int i = 10;语句是正确的。但是,很可惜,用typedef 取的别
名不支持这种类型扩展。另外,想想typedef static int int32 行不行?为什么?
下面再看一个与#define 宏有关的例子:
G),#define PCHAR char *
PCHAR p3,p4;
H),typedef char* pchar;
pchar p1,p2;
两组代码编译都没有问题,但是,这里的p4 却不是指针,仅仅是一个char 类型的字符。
这种错误很容易被忽略,所以用#define 的时候要慎之又慎