Chapter 6
表达式和语句
1.
一个桌面计算器,允许用户自己输入语句的,类似一个小编译器
写得蛮优雅。。。
完整代码在这里
2.
有关命令行参数
int main(int argc,char* argv[]){}
程序的名字会传进来
一个简单的测试程序,顺便巩固一下数组和指针知识
#include<iostream>
using namespace std;
int main(int argc,char* argv[])
{
cout<<&argv<<'/n';
cout<<argv<<'/n';
cout<<*argv<<'/n';
cout<<&argv[0]<<'/n';
cout<<argv[0]<<'/n';
return 0;
}
结果:
0013FF8C
00480E70
C:/Program Files/Microsoft Visual Studio/MyProjects/copy/Debug/copy.exe
00480E70
C:/Program Files/Microsoft Visual Studio/MyProjects/copy/Debug/copy.exe
Press any key to continue
3.
输入流
用一个指向istream类的指针来选择输入
istream *input;
....
input=&cin;
....
input=new istringstream(argv[1]);//istringstream()是以string为输入流
....
4.
运算符的优先级,结合性
一元运算符和赋值符=是右结合的
*p++是*(p++)而不是(*p)++
a=b=c;是a=(b=c)
a+b+c;则是(a+b)+c
5.
results结果
结果的类型是由“普通算术转换”的规则确定,产生的是“最大的”操作数的类型
如果一个二元运算符binary operator,有个一个foating-point型的操作数,那么计算就通过浮点数进行,结果也是floating-point类型的
值,如果是一个long型的操作数,那么运算就通过long integer的类型进行,结果也是long型的值,比int型小的操作对象(例如bool,char),在运算符作用之前将被转换成int。
关系运算符,==,>=,etc.产生bool型的结果
用户定义的运算符的结果类型和意义由它们的声明决定
只要逻辑上可行,一个以lvalue为操作对象的运算符的结果仍然是以这个lvalue为标称的lvalue
void f(x,y)
{
int j=y=x;//
int *p=&++x;//ok
int *q=&(x++);//error,x++不是一个左值(不是存在x中的值)
int* pp=&(x>y?x:y);//x,y中较大者的地址
}
++x和x++
++x是自增之后立即更新,而x++则是在语句结束后才更新x中的值
如果?:操作符的第二个和第三个操作数都是lvalue并且是相同类型的,那么结果是这种类型的并且是一个左值
sizeof()的结果类型是一个unsigned int型的size_t,这也说明了sizeof()是一个运算符,而不是一个函数
typedef unsigned int size_t;
指针相减的结果类型是一个signed int类型的,叫做ptrdiff_t;
typedef int ptrdiff_t;
实现不会检查算术溢出
int main()
{
int x=1;
while(0<x) x++;
std::cout<<x<<'/n';
}
像这样的指令会引起程序终止或者其他的什么后果,但是下溢上溢被0除都不会抛出标准异常
6.
求值顺序
一个表达式里子表达式的求值顺序是undefined,无定义的。你不能假定他是从左到右求值的,例如:
int x=f(2)+g(3);
f(),g()哪个先被调用是undefined
不对表达式进行求值顺序的限制会生产更好的代码,但是有可能会导致undefined results
int i=1;
v[i]=i++;
在我的VC6里是v[1]=1,而在其他编译器里有可能产生其他结果或者其他奇怪的行为
逗号","操作符和||还有&&运算符能够保证第一个表达式先被求值,而且对于内部类型||只有在第一个值是false的时候会求第二个表达式的值,而&&只有在第一个为true的时候才会去对第二个表达式求值。例如
b=(a=2,a+1);
b得到的值将是3
顺序操作符,和函数中分隔参数的,逻辑意义是不同的,在函数参数中的,不具备顺序操作的意义,函数参数的求值顺序是undefined,不能依赖于这种顺序。
(v[i],i++)这个式子的值是i++
()可以强制group,a*b/c,这样的顺序是(a*b)/c,如果想要a*(b/c)就必须加上括号,这两种方法的值,当a,b,c是floatting-point的时候实际上是不同的,所以编译器将严格按照顺序执行
7.
运算符优先级
优先级别和结合规则影响到最平常的使用
如果对优先级有所怀疑的话,那么就应该考虑使用圆括号,子表达式越复杂,那么圆括号使用的就越频繁,但是复杂的子表达式往往是错误的根源。
这里有一些情况,优先级不像”明显“那样的作用
if(i & mask==0)
这个不是将mask(掩码)作用于i之后判断是否=0,因为==比&有更高的优先级。表达式将被解释成i & (mask==0)
if(0<=x<=99)
不管第一个比较的结果是什么,0<=x的值不外乎0和1,与99比较,结果必然是真
8.
按位逻辑运算符
按位逻辑运算符用在整形的对象中,也就是,int,bool,char,short,long以及他们的unsigned类型,结果也是整形的
人们采用无符号数的位作为member来表示一个位集合,位向量,二元操作符 & 是求交集, | 是求并集,^是求对称差,~为求补。利用枚举类型可以作为这样一个集合的成员命名。一个例子
enum ios_base::iostate{
goobit=0,eofbit=1,failbit=2,badbit=4};
state=goodbit;
//........
if(state&(badbit | failbit))
额外的括号是必须,&的优先级比|更高。
state |= eofbit;
|=操作符用于向state里添加东西,如果只是简单赋值state=eofbit将清除其他的二进制位丢失信息。
位操作最好保留在底层中,位操作也可以用于掩码和移位。
注意别和&& ||和!搞混了,逻辑运算符总是返回true或者false
例如!0是真,而~0则是对0求补,补码表示的值是-1
9.
增量和减量
操作符是++和--
可以用作前缀和后缀,++i的值是i的新值,y=++i就是y=(++i),而i++的值是i的旧值,例如y=i++,是,y=(t=x,x+=1,t),t和x是一样的类型,同时,这也解释了为什么x++不是一个lvalue,不能作为&的操作数
对指向数组元素的指针的++和--同指针加法和减法类似
void cpy(char* p,const char* q)
{
while(*p++=*q++);
}
因为这样的写法使得C或者C++遭到喜爱或者厌恶
首先考虑一个更传统的写法
int length=strlen(q);
for(int i=0;i<=length;i++) p[i]=q[i];
这确实很浪费,因为我们读了两次字符串,一次获取长度,一次复制,试试另外一种方式
for(int i=0;q[i]!=0;i++) p[i]=q[i];
p[i]=0;
用作下标的i可以去掉,因为p和q本身就是指针,
注意:这里是说p和q是指针,可没说数组名是指针,数组名作为参数传进来的时候退化成指针
while(*q!=0){
*p=*q;
p++;
q++;}
*p=0;
后增量运算符可以让我们先用他的值,然后再增加,再去掉一些不必要的
while(*p++=*q++)
至于效率问题,优先使用库
10.
自由存储
Free store
一个命名的对象有他的生命周期,通过作用域来决定。然而,我们经常需要创建一个独立于作用域而存在的对象。特别是,创建一个在创建他的函数调用返回之后仍然可以用的对象是非常平常的。运算符new创建这样的对象,而操作符delete可以用来销毁他们。用new分配的对象被称做,“自由存储里的”或者说堆对象(heap object)或者动态存储里分配的(allocated in dynamic memory)。
一个用new创建的对象,必须用delete显式得销毁。C++实现不保证有一个垃圾回收器。
Delete操作符只能作用于new返回的指针或者0,作用于0没反应。
11.
数组
New也可以用于创建对象的数组,例子
char* save_string(const char* p)
{
char* s=new char[strlen(p)+1];
strcpy(s,p);
return s;
}
int main(int argc,char* argv[])
{
if(argc<2) exit(1);
char* p=save_string(argv[1]);
//………………………..
delete [] p;
}
用delete操作符释放空间的时候必须知道对象被分配的大小,因此用new标准实现分配的对象将比一个静态对象占用的空间多一点。通常一个机器字,用来存储对象的长度信息。
注意vector是一个合适的对象,因此可以用new和delete来分配和回收,而不是new[]和delete[]。
11.
内存耗尽
自由存储操作符new,delete,new[],delete[]通过函数来实现。
new和new[]并不对返回的存储做初始化。
如果new找不到存储空间来分配的时候,会抛出一个bad_alloc的异常
void f()
{
try{
for(;;) new char[1000];
}
catch(bad_alloc){
cerr<<”Memory exhausted!/n”;
}
}
我们可以手动指定new失败的时候的动作,new失败的时候,如果有,首先会调用一个由调用set_new_handler()这个函数指定的函数。
void out_of_store()
{
cerr<<”operator new failed:out of store/n”;
throw bad_alloc();
}
int main()
{
set_new_handler(out_of_store);
for(;;) new char[10000];
cout<<”done/n”;
}
当然这个程序永远不会输出done,会输出
operator new failed:out of store
12.
显示类型转换
有的时候我们需要处理“生内存”,也就是存储或者将要存储一个编译器不知道其类型的对象。
例如,一个内存分配器可能返回一个void*指向新分配的内存或者我们想要将一个整型值当作一个I/O设备的地址。
void* malloc(size_t);
void f()
{
int* p=static_cast<int*>(malloc(100));
IO_device* d1=reinterpret_cast<IO_device*>(0Xff00);
//……..
}
explicit type conversion又被叫做cast强制,有些时候是必要的
static_cast操作符在两个相关类型之间转换,例如一种指针类型到另一种,一个enum到整型,一个floating-point到整型。reinterpret_cast处理不相关的类型转换,例如整型到指针。
还有一种运行时的类型转换dynamic_cast,一种去掉const约束的类型转换const_cast。
(T)e这种记法危险。。。。。。
13.
构造函数
从一个值e构造出一个类型T的值可以被表述成T(e),例如
void f(double d)
{
int i=int(d);
complex z=complex(d);
}
这种通常被称为函数风格的强制,对于一个内部类型T,T(e)就等于static_cast<T> e,这说明T(e)的使用不总是安全的,指针类型的这种风格转换是非法的,char*(a)是非法的,而typedef char*之后就可以用了
T()将产生T类型的默认值,一般是0.例如int();
cout<<int()<<endl;
VC6的输出是
0
14.
语句概览
赋值和函数调用不是语句,是表达式
声明是语句,除非被声明为static,否则它的初始化就将在控制线程经过这个声明的时候执行。
将变量的声明推迟到初始化式可用之时,还可以导致更好的性能。
string s;//……..//s=”abcd”;
将比
string s=”abcd”;
慢得多
15.
条件语句
||,&&只有在必要的时候才会去计算第二个表达式的值
有些条件语句可以更好得用条件表达式表示
if(a>b)
max=a;
else
max=b;
可以用条件语句更好得表达成
max=a>b?a:b;
switch语句也可以用if语句替换
switch语句更好一些,easier to read & generate better code
注意case必须被终结,除非你想接着进行,例如
switch(val){
case 1:
cout<<”case1”<<endl;
case 2:
cout<<”case2”<<endl;
default:
cout<<”default”<<endl;
}
这样的话,如果val=1,那么输出将是
case1
case2
default
常见的结束方式是break,return。
在最小的作用域引进变量是很好的做法
if(double d=term(true))
{
left/=d;
break;
}
d的作用域是条件语句,从声明处开始,如果有else分支,将延续到else分支结束
这样做不仅有逻辑上的好处,而且能生成更紧凑的源代码
在条件语句中只能声明一个变量或者const
16.
循环语句
for(;;)
一直循环,直到break,return,throw,goto或者exit()等
如果没有一个明显的循环变量或者循环变量的更新很自然得在循环体内,那么最好用while(),一个输入循环就是一个没有明显的循环变量的例子
while(cin>>ch);
BJ不推荐用do-while语句
for语句中的声明
书里面说for语句里声明的变量的作用域是for语句,而在我的vc6中,出了for依然有作用域。
#include<iostream>
using namespace std;
int main()
{
for(int i=0;i<9;i++)
cout<<i<<endl;
cout<<"end is "<<i<<endl;
}
goto语句,某些情况下还是有用的,例如一些性能至上的实时程序。
17.
注释与缩进编排
就那么回事。。。
保持良好的风格
18.
建议
用库!!
避免过于复杂的表达式
用特殊强制运算符来强制转换,避免用(T)e
只对构造良好的类型用T(e)
避免带有无定义 求值顺序 的表达式
尽量在有初始化式的时候去声明一个变量
用一个成员operator new()代替全局的operator new()