The C++ Programming Language 第六章 笔记
james chen
050301
6.1至6.1.8等章节因较繁杂难懂暂略过。
*********************
6.2 运算符概览
*********************
顺序从上至下,从左至右
class_name 类名
namespace_name 命名空间名
member 成员
object 对象
pointer 指针
type 类型
expr 表达式
lvalue 左值
dynamic 动态
static 静态
作用域解析 class_name::member
作用域解析 namespace_name::member
全局 ::name
全局 ::qualified-name
成员选择 object.member
成员选择 pointer->member
下标 pointer[expr]
函数调用 expr(expr_list)
值构造 type(expr_list)
后增量 lvalue++
后减量 lvalue--
类型识别 typeid(type)
运行时类型识别 typeid(expr)
运行时检查的转换 dynamic_cast<type>(expr)
编译时检查的转换 static_cast<type>(expr)
不检查的转换 reinterpret_cast<type>(expr)
const转换 const_cast<type>(expr)
对象大小 sizeof(expr)
类型大小 sizeor(type)
前增量 ++lvalue
前减量 --lvalue
补 ~expr
非 !expr
一元负号 -expr
一元正号 +expr
地址 &lvalue
间接 *expr
建立 分配 new type
建立 分配并初始化 new type(expr_list)
建立 放置 new (expr_list)type
建立 放置并初始化 new (expr_list)type(expr_list)
销毁 释放 delete pointer
销毁 数组 delete [] pointer
强制类型转换 (type)expr
成员选择 object.*pointer-to-member
成员选择 object->*pointer-to-member
乘 expr*expr
除 expr/expr
取模(求余数) expr%xpr
加 expr+expr
减 expr-expr
左移 expr<<expr
右移 expr>>expr
小于 expr<expr
小于等于 expr<=expr
大于 expr>expr
大于等于 expr>=expr
等于 expr==expr
不等于 expr!=expr
按位与 expr&expr
按位或 expr|expr
按位异或 expr^expr
逻辑与 expr&&expr
逻辑或 expr||expr
条件表达式 expr?expr:expr;
简单赋值 expr=expr
乘并赋值 expr*=expr
除并赋值 expr/=expr
取模并赋值 expr&=expr
加并赋值 expr+=expr
减并赋值 expr-=expr
左移并赋值 expr<<=expr
右移并赋值 expr>>=expr
与并赋值 expr&=expr
或并赋值 expr|=expr
异或并赋值 expr^=expr
抛出异常 throw expr
逗号表达式 expr,expr
每个间隔里的运算符具有机同优先级。位于上面的间隔里的运算符比下面间隔里的运算符优先级更高。
例如:
a+b*c的意思是a+(b*c),而不是(a+b)*c,因为*比+的优先级更高。
*p++的意思是*(p++),而不是(*p)++。 //不过我觉得应该是先*p,再p++才对。
一元运算符和赋值运算符是右结合的,其他运算符都是左结合的。
例如:
a=b=c的意思是a=(b=c),而a+b+c的意思则是(a+b)+c。
6.2.1 结果
算术运算符的结果类型是由一级称做"普通算术转换"的规则确定。这里的整体目的就是让产生的结果具有“最大的”运算对象类型。如果二元运算符的的一个对象是浮点数,那么计算就是通过浮点运算来完成的,结果类型也是浮点数值。。如果有一个对象是long,那计算就用长整数算术,结果也是long。比int小的对象,在运算符作用之前都将被转换到int。
只要逻辑上可行,一个以左值作为运算对象的运算符的结果,仍然是指这个左值对象的左值。。
int x=10,y=100;
int z=x=y; //x=y后的值是x的值
cout<<z<<endl; //100
cout<<x<<endl; //100
cout<<y<<endl; //100
int *p=&++x; //先++x,然后将&x送给p
//int *q=&x++; //错,此时x++并非左值,x++!=x,它没有将结果放到x里
6.2.2 求值顺序
在一个表达式里,子表达式的求值顺序是没有定义的。特别是,你不能假定表达式从左到右求值。
int x=f(2)+g(3); //没有定义f()和g()谁先调用
不对表达式的求值顺序加以限定,使得在具体的实现中有可能生成更好的代码,但有时也会导致无定义的结果。
int i=1;
int a[3]={11,22,33};
a[i]=i++;
cout<<i<<endl;
for(int j=0;j<3;j++)cout<<a[j]<<endl;
在vc中,编译通过,没有警告,a[i]=i++等于a[1]=1,但也可能为a[2]=1,也有可能还有更奇怪的行为。
运算符,逗号,&&逻辑与,||逻辑或保证了位于它们左边对象的值一定要在右边运算对象之前求值。
如有逗号表达式,则最右边表达式的值为整个表达式的值
b=(a=2,a+1); //a+1为(a=2,a+1)整个表达式的值,b=3
如是逻辑与,则左边表达式为真时才运算右边表达式
int a=2,b=4;
cout<<(a>b&&++b>a)<<endl;
cout<<b<<endl; //b=4,因为a>b为假,帮++b未运行
如是逻辑或,则左边表达式为假时才运算右边表达式
6.2.3 运算符优先级
if(i<=0 || max<i)的意思是如果i大于等于零,或是max小于i,它等价于:if((i<=0)||(max<i))。
而不是下面这个看起来合法但却一派胡言的:
if(i<=(0||max)<i)/// //乱七八糟
如果程序员对某些规则不清楚,他就应该使用刮号。在子表达式变得更加复杂时,使用括号就变得比较普遍,但复杂的子表达式往往是错误根源。因此,如果你开始感到需要括号的时候,你或许就应该考虑通过额外的变量将表达式分解开。
确实存在一些情况,其中运算符的优先级造成合“最明显的”解释,如:
if(i&mask==0)
这并不是将mask作用于i而后检测结是否为0。因为==具有比&更高的优先级。他将会被解释为:i&(mask==0),不过大多数编译器都会给出警告,对于这种情况,加上括号是至关重要的:
if((i&mask)==0) //
请注意下面这个表达式:
if(0<=x<=99) //
这个表达式的本意是“如果x大于等于0或x小于等于99”,
但实际上它却相当于:if((0<=x)<=99)....因为子表达式的值或0或1,整个表达式的无论如何都为真。。
这肯定不是我们想要的结果,正确的表示方法:if(x>=0 && x<=99)....
另外,千万不要将=(赋值)当成==(相等),这种大错绝不能犯,会笑死人的。。
6.2.4 按位逻辑运算符(暂略)
6.2.5 增量和减量
++运算符用于直接表示增加,--则相减。
++lvalue的意思是lvalue+=1,也就相当于lvalue=lvalue+1
--lvalue同理。
++和--可以被用作前缀或后缀运算符。
用作前缀时,表达式的值为增量之后的值,作用对象的值亦为增量之后的值。
用作后缀时,表达式的值为对象的值,然后对象的值再作增量。
如:
int i=3,a,b;
a=++i; //a=4,i=4,相当于i++,a=i;
b=i++; //b=4,i=5,相当于b=i,i++;
++,--既可以操作指针,亦可操作数组.
运算符++,--对于在循环里增加或减少变量特别有用,以下是复制一个以0结果的字符串。。
void strcpy(char* p,const char* q)
{
while(*p++=*q++); //一行OK,简洁高效
}
下面来解释是如何做的。
先看一个传统的字符串复制:
int length=strlen(q);
for(int i=0;i<=length;i++)p[i]=q[i];
求length的那一行很浪费,完全没有必要,因为字符串是以0结尾的,所以根本不用条件i<=length。
所以我们来尝试一下另外一种方式:
for(int i=0;q[i]!=0;i++)p[i]=q[i];
p[i]=0; //以零结尾
用做下标的i可以去掉,因为p和q本身就是指针,可以通过++或--来操作数组。
while(*q!=0)
{
*p=*q;
*p++;
*q++;
}
*p=0;
后增量运算符可以让我们先用它的值再作增量,所以:
while(*q!=0)
{
*p++=*q++;
}
*p=0;
由于表达款*p++=*q++的值也等于*q++,所以:
while((*p++=*q++)!=0);
注意,此处已无*p=0,在这种情况下,先做的是这个表达式*p++=*q++,在最后一位时,即把0赋值给*p,然后再看整个表达式的值,而*p++=*q++的值就是赋值后*p的值,所以不用单独来加结束符。
既然*p++=*q++到了结束符时值为0,则可省略!=0的判断:
while(*p++=*q++);
这就是我们想要的最简洁最快速的写法,由此可看出有循环中合理使用++,--可以使程序更精简更高效。
6.2.6自由存储
命名对象的生存时间由它的作用域所决定。然而,能够建立起生存时间不依赖于建立它的作用域的对象,这是很有用的。有时我希望在函数中建一些对象,在函数结束后仍然能使用该对象,运算符new专干这事情,而delete则负责销毁它们,由new分配的对象被说成是在“自由存储里的”,也有说是“堆对象”或“动态存储中分配的”。如:
int *aa()
{
int *a=new int(123); //动态申请一int,并初始化为123
return a;
}
int *bb=aa();
cout<<*bb<<endl; //123
delete bb; //销毁new生成的对象
6.2.6.1数组
也可以用new来建立对象的数组,如:
char *a="i love bb!!";
char *b=new char[strlen(a)+1]; //动态申请一char数组
strcpy(b,a);
cout<<b<<endl;
delete [] b; //销毁b
6.2.6.2内存耗尽
当内存空间用尽,new无法分配到空间时会发生什么情况呢??按照默认方式,将抛出一个bad_alloc异常,如:
void f()
{
try
{
for(;;)new char[10000];
}
catch(bad_alloc)
{
cout<<"靠,内存用完了!!"<<endl;
}
}
晕,在winxp+vc6下未成功,系统内存在用得差不多的时候,系统就出面干涉这个小程序,不准它乱来,可能还是要在dos下测才行。。。
以后再来看这节,哎,有些东西还不大明白,看起来太吃力。。
6.2.7/6.2.8暂略过
*********************
6.3 语句概览
*********************
6.3.1声明作为语句
一个声明也是一个语句。除非一个变量被声明为static,否则它的初始式的执行就将控制线程经过这个声明的时候进行。允许在所有写语句的地方写声明,是为了使程序员能够最大限度的减少由于未初始化的变量而导致错误,并使代码得到更好的局部化,如:
void f()
{
for(int i=0;i<10;i++) //使用时才声明
....
}
6.3.2选择语句
一个值可以被if或switch语句来检测:
if(expr)a
switch(expr)a
比较运算符:==,!=,>=,<=,>,<
比较结果为真是返回true,为假时返回false...
逻辑运算符:&&,||,!
运算符&&和||除了在必要的时候,是不会对第二个运算对象求值的。
if(p&&i<5) //若p为假,则不会运算i<5...
某些if语句可以方便的用条件表达式代替,如:
if(a>b)
max=a;
else
max=b;
写成下面这样将会更好些:
max=a>b?a:b;
switch等价与一组if语句:
if(a=1)
a();
else if(a=2)
b();
else if(a=3)
c();
else
d();
等价于:
switch(a)
{
case 1:a();break;
case 2:b();break;
case 3:c();break;
default:d();break;
}
两者意思相同,但用switch的版本更好一些,因为都是检测一个值。对一些复杂的语句而言,switch更容易阅读。
6.3.2.1在条件中声明
为了避免意外地错误使用变量,在最小的作用域里引进变量是一个很好的想法,最好把局部变量的定义一直推迟到可以给它初始值时再去做,采用这种方式,就不会出现因为使用未初始化的变量而造成的麻烦了。
这两条原则的一个最优雅的应用就是在条件中声明变量:
if(double d=aa)
{aa=d;}
在这里,d被声明和初始化,初始化后的d值又被作为条件的值。d的作用域是从它的声明点一直延伸到这个条件所控制的语句结束,如果这个if语句有else分支,则else分支亦属d的作用域。
在条件中声明变量,除了逻辑方面的优点之外,还能产生更紧凑的源代码。
在条件中的声明只能声明和单个的变量或者const。
6.3.3迭代语句
while,for,do..while(expr)
for语句是为了表述最规范的循环形式。循环变量、结束条件以及更新循环变量的表达式都可以在“最前面”的一行里描述。这样可以极大地提高可读性,也会减少出错的频率。如果不需要初始化,初始化语句可以为空,如果忽略条件部分,这个for语句可能永远循环下去,除非用户明确地通过break,return,goto,throw或某些不那么明显的方式,如调用一个exit(),从循环中退出来。
如果一个循环不是简单的“引进一个循环变量、检测条件、更新循环变量”类型的,那么最好是用while语句来描述。
while语句简单地执行受其控制的语句,直到它的条件变成false。对于那些没有明确的循环变量,或者对循环变量的更新自然地出现在循环体的中间时,最好使用while语句而不是for语句。输入循环就是一种没有明确的循环变量的循环:
while(cin>>a)
do语句是错误和混乱的一个根源。究其原因,缘于它在条件求值之前总要执行循环体一次。然而,要使循环体正确工作,一定会有一些很像条件的东西必须在第一次到达时就成立,而这个条件并不一定会像期望的那样成立。
尽量避免使用do循环。
6.3.3.1for语句里的声明
for(int i=0;i<4;i++)
i的作用域就是for循环。
6.3.4goto
C++拥有臭名昭著的goto语句,呵呵,bs都说臭名昭著,看来还是少用为妙。
*********************
6.4 注释和缩进编排
*********************
明智地使用注释,一致性地使用缩进编排形式,可以使阅读和理解一个程序的工作变得更愉快些。
注可能被人们以许多方式误用,并因此严重地影响程序的可读性。编译器当然不会理解注释的内容,因此它无法保证一个注释:
1,是有意义的。
2,描述了这个程序。
3,是符合目前情况的。
许多程序里都包含着不易理解的,歧义的,或者根本就是错误的注释。糟糕的注释还不如没有注释。
如果某些东西已经由语言本身说明白,那么不应当再作为注释中提及的内容。如:
//变量a必须初始化
//变量a只能由f函数调用
这种注释太不必要,程序本身就已经很清楚。
一旦某些东西已经在语言里说清楚了,就不应该在注释里第二次提到它:
a=b+c; //a等于b加c //晕,这肯定不要注释啊
count++; //count加1 //超级废话啊
这种注释比多余还要糟,因它们增加了阅读量,也使程序结构变得模糊,而且有可能还是错的。
BS关于注释的偏爱:
1,为每个源文件写一个注释,一般性地陈述在它里面有哪些声明,对有关手册的引用,为维护而提供的一般性提示等。
2,对每个类、模板和名字空间写一个注释。
3,对每个非平凡的函数写一个注释,陈述其用途,所用的算法,以及可能有的关于它对环境所做的假设。
4,对每个全局的和名字空间的变量和常量写一个注释。
5,在非明显或不可移植的代码处的少量注释。
6,极少的其他东西。
一组经过良好选择和良好书写的注释是好程序中最具本质性的一个组成部分。写出好注释可能就像写出好程序一样困难,但这是一种值得去好好修养的艺术。
*********************
6.5 忠告
*********************
1,应尽可能使用标准库,而不是其他的库和“手工打造的代码”。
2,避免过于复杂的表达式。
3,如果对运算符的优先级有疑问,请加括号。
4,避免显式类型转换(强制)。
5,若必须做显式类型转换提倡使用特殊强制运算符,而不是C风格的强制。
6,只对定义良好的构造使用T(e)记法。
7,避免带有无定义求值顺序的表达式。
8,避免goto。
9,避免do语句。
10,在你已经有了去初始化某个变量的值之前,不要去声明它。
11,使注释简洁、清晰、有意义。
12,保持一致的缩进编排风格。
13,倾向于去定义一个成员函数operator new()去取代全局的operator new();
14,在读输入的时候,总应考虑病态形式的输入。
The C++ Programming Language 第六章 笔记
最新推荐文章于 2021-07-19 23:21:28 发布