桌面计算器程序
我们的桌面计算器程序提供了四种标准算术操作,它们以中缀运算符的形式出现,可以作用于浮点数。此外,用户还可以定义变量。例如,给定输入:
r=2.5
area=pi*r*r
(预先定义了 pi)计算器程序将会输出:
2.5
19.635
其中,2.5
是第一行输入的结果,19.635
是第二行的结果。
计算器包含四个部分:分析器、输人函数、符号表和驱动。实际上,它的功能有点类似于一个微型编译器:
- 分析器负责分析语法
- 输入函数负责处理输入及词法分析
- 符号表存放永久信息
- 驱动处理初始化、输出和错误
计算器程序遵循的一套语法:
program:
END // END表示输入结束
expr_list END
expr_list:
expression PRINT // PRINT是分号
expression PRINT expr_list
expression:
expression + term
expression - term
term
term:
term / primary
term * primary
primary
primary:
NUMBER
NAME
NAME = expression
- primary
(expression)
源代码
#include <iostream>
#include <string>
#include <unordered_map>
enum class Kind : char {
NAME,NUMBER,END,
PLUS='+',MINUS='-',TIMES='*',DIVIDE='/',
PRINT=';',ASSIGN='=',LP='(',RP=')'
};
struct Token {
Kind kind;
std::string string_value;
double number_value;
};
int no_of_errors;
double error(const std::string& s) {
no_of_errors++;
std::cerr << "error:" << s << std::endl;
return 1;
}
class TokenStream {
public:
TokenStream(std::istream& s): ip(&s), owns(false) {}
TokenStream(std::istream* p): ip(p), owns(true) {}
~TokenStream() { close(); }
void set_input(std::istream& s) {
close();
ip = &s;
owns = false;
}
void set_input(std::istream* p) {
close();
ip = p;
owns = true;
}
Token get() {
char ch = 0;
do { // 跳过除 '\n' 之外的空白字符
if (!ip->get(ch)) return current_token = { Kind::END };
} while (ch != '\n' && isspace(ch));
// 默认情况下,>>运算符会跳过空白(即,空格、制表符、换行等),并当输入操作失败时不更改ch的值
switch (ch) {
case 0: // 因此,ch == 0 代表输入过程结束
return current_token = { Kind::END };
case '(':
return current_token = { Kind::LP };
case ')':
return current_token = { Kind::RP };
case '+':
case '-':
case '*':
case '/':
case '=':
return current_token = { static_cast<Kind>(ch) };
case ';':
case '\n':
return current_token = { Kind::PRINT };
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '.':
ip->putback(ch); // 把第一个数字或者.输入流中
*ip >> current_token.number_value;
current_token.kind = Kind::NUMBER;
return current_token;
default:
if (isalpha(ch)) { // 检查是否是字母,包括大写字母和小写字母
current_token.kind = Kind::NAME;
current_token.string_value = ch;
while (ip->get(ch) && isalnum(ch))
current_token.string_value += ch;
ip->putback(ch);
return current_token;
}
error("bad token");
return current_token = { Kind::PRINT };
}
}
const Token& current() {
return current_token;
}
private:
std::istream* ip;
bool owns;
Token current_token {Kind::END};
void close() {
if (owns) delete ip;
}
};
TokenStream ts{std::cin};
std::unordered_map<std::string, double> table;
double expr(bool get);
double term(bool get);
double prim(bool get);
void calculate() {
for (;;) {
ts.get();
if (ts.current().kind == Kind::END) break;
if (ts.current().kind == Kind::PRINT) continue;
std::cout << expr(false) << std::endl;
}
}
int main() {
table["pi"] = 3.141592653589793;
table["e"] = 2.718281828459045;
calculate();
return no_of_errors;
}
// get 只是是否需要调用 TokenStream::get() 以获取下一个单词
// 每个分析函数求值“它的”表达式,并返回相应值
double expr(bool get) {
double left = term(get);
for (;;) {
switch (ts.current().kind)
{
case Kind::PLUS:
left += term(true);
break;
case Kind::MINUS:
left -= term(true);
break;
default:
return left;
}
}
}
double term(bool get) {
double left = prim(get);
for (;;) {
switch (ts.current().kind) {
case Kind::TIMES:
left *= prim(true);
break;
case Kind::DIVIDE:
if (auto d = prim(true)) {
left /= d;
break;
}
return error("divide by zero");
default:
return left;
}
}
}
double prim(bool get) {
if (get) ts.get();
switch (ts.current().kind) {
case Kind::NUMBER: // 浮点数常量
{
double v = ts.current().number_value;
ts.get();
return v;
}
case Kind::NAME:
{
double& v = table[ts.current().string_value];
if (ts.get().kind == Kind::ASSIGN) { // 变量赋值
v = expr(true);
}
return v;
}
case Kind::MINUS: // 一元减法
return -prim(true);
case Kind::LP:
{
double e = expr(true);
if (ts.current().kind != Kind::RP) return error("')' expected");
ts.get(); // 吃掉了 ')'
return e;
}
default:
return error("primary expected");
}
}
运算符概述
每个间隔里的运算符具有相同优先级。位于上面的间隔里的运算符比下面间隔里的运算符优先级更高。
一元运算符和赋值运算符是右结合的,其他运算符都是左结合的。例如,a=b=c 的意思是 a=(b=c),a+b+c 是(a+b)+c。
结果
- 算术运算符的结果类型:
- 算术运算符的结果类型由“普通算术转换”规则确定,目的是使结果具有“最大的”运算对象类型。
- 如果运算对象中有浮点数,计算将通过浮点算术完成,结果为浮点数。
- 如果运算对象中有长整数(long),计算将通过长整数算术完成,结果为长整数。
- 比 int 小的运算对象(如 bool 和 char)在运算前会被转换为 int。
- 关系运算符的结果类型:
- 关系运算符(如==、<=等)产生布尔值(true 或 false)。
- 用户定义运算符:
- 用户定义运算符的意义及其结果类型取决于它们的声明。
求值顺序
C++ 没有明确规定表达式中子表达式的求值顺序,尤其请注意,你不能假定表达式是按照从左到右的顺序求值的。例如:
int x = f(2) + g(3); // 没定义f()或g()哪个先调用
v[i] = i++; // 未定义结果,没定义先执行i++,还是先执行v[i]
一定要避免在同一条表达式中同时读写一个对象,除非你只用到了一个运算符(比如 ++
和 +=
),或者显式地表达出了序列的含义(比如使用了逗号、&&
或者 ||
)。
逗号运算符(,)、逻辑与运算符(&&
)和逻辑或运算符(||
)规定它们的左侧运算对象先被求值,然后才是右侧运算对象。例如,b = (a = 2, a + 1)
的意思是把 3 赋给 b
。
运算符优先级
复杂表达式的处理办法:
- 当表达式复杂时,使用括号可以更清晰地表达意图。
- 复杂的子表达式可能导致错误的几率增加,因此使用额外的变量将长表达式分割成较短的表达式是一个好方法。
优先级与直觉不符的情况
if(i&mask == 0)
实际上意味着i&(mask == 0)
,因为==
的优先级高于&
。这种情况下,括号的使用变得非常重要,正确的写法是
if((i&mask) == 0)
。- 表达式
if(0 <= x <= 99)
在语法上是正确的,但它的含义是(0 <= x) <= 99
,这是一个常见的错误。正确的写法应该是 if(0 <= x && x <= 99),使用 && 来确保两个条件都被检查。
临时对象
通常情况下,编译器必须引人一个对象,用以保存表达式的中间结果。
除非我们将临时对象绑定到引用上或用它初始化一个命名对象,否则大多数时候在临时对象所在的完整表达式末尾,它就被销毁了。完整表达式(full expression)不是任何其他表达式的子表达式。
std::string s1 = "Hello";
std::string s2 = "World";
const std::string& s3 = s1 + s2; // OK
std::string s4 = s1 + s2; // OK
std::string& s5 = s1 + s2; // 不OK,不允许把一个临时变量绑定到非const左值引用
切记试图返回局部变量的引用会造成程序错误,并且也不允许把一个临时变量绑定到非 const
左值引用上。
常量表达式
C++ 提供了两种与“常量”有关的概念:
-
constexpr
:编译时求值 const
:在作用域内不改变其值
基本上,constexpr
的作用是启用并确保编译时求值,而 const
的主要作用是在接口中
规定某些成分不可修改。本节主要关注第一个问题:编译时求值。
常量表达式(constant expression)是指由编译器求值的表达式。它不能包含任何编译时
未知的值,也不能具有其他副作用。
constexpr
的函数既可以被当作常量表达式函数,也不可以像正常函数一样使用。
符号化常量
常量(constexpr
或者 const
值)最重要的一个用处是为值提供符号化的名字。我们应该在代码中有意识地使用符号化常量以避免出现“魔法数字”。散布在代码中的字面值给维护工作带来极大的困难。
constexpr int MAX_SIZE = 100;
const double PI = 3.14159;
常量表达式中的 const
以常量表达式初始化的 const
可以用在常量表达式中。与 constexpr
不同的是,const
可以用非常量表达式初始化,但是此时该 const
将不能用作常量表达式。例如:
#include <iostream>
#include <cmath>
#include <string>
int main() {
const int x = 7;
const std::string s = "asdf";
const int y = std::sqrt(x);
constexpr int xx = x;
constexpr std::string ss = s; // 错误,std::string 不是字面值常量类型
constexpr int yy = y; // 错误,sqrt(x) 不是常量表达式
}
字面值常量类型
在常量表达式中可以使用简单的用户自定义类型。
class MyClass {
public:
constexpr MyClass(int value) : data(value) {}
private:
int data;
};
含有 constexpr
构造函数的类称为字面值常量类型(literal type)。构造函数必须足够简单才能声明成 constexpr
,其所有成员都是用潜在的常量表达式初始化的。
class MyClass {
public:
constexpr MyClass(int value) : data(value) {
value += 1;
return;
}
private:
int data;
};
constexpr MyClass c(5); // OK
constexpr MyClass d{5}; // OK
constexpr MyClass e{std::sqrt(25)}; // 错误,std::sqrt 不是常量表达式
对于成员函数来说,constexpr
隐含了 const
的意思,所以下面的写法没有必要:
class MyClass {
public:
constexpr MyClass(int value) : data(value) {
value += 1;
return;
}
constexpr int getData() const {
return data;
}
private:
int data;
};
引用参数
当你使用 constexpr
时,谨记 constexpr
是一个关于值的概念。此时,任何对象都无法改变值或者造成其他什么影响:constexpr
实际上提供了一种微型的编译时函数式程序设计语言。
const
引用也能作为 constexpr
函数的参数。
地址常量表达式
全局变量等静态分配的对象的地址是一个常量。然而,该地址值是由链接器赋值的,而非编译器。因此,编译器并不知道这类地址常量的值到底是多少。这就限制了指针或者引用类型的常量表达式的使用范围。例如:
#include <iostream>
int main() {
constexpr const char* p1 = "asdf";
constexpr const char* p2 = p1; // OK
constexpr const char* p3 = p1 + 2; // 错误:编译器不知道p1本身值是多少
constexpr char c = p1[2]; // OK,c=='d',编译器知道p1所指的值
std::cout << p1 << std::endl;
std::cout << p2 << std::endl;
std::cout << p3 << std::endl;
std::cout << c << std::endl;
}
注意:在我的编译器和在线编译器都没有出现报错,可能这个功能已被优化???或者我写的方式不对???
隐式类型转换
如果我们转换了某个值的类型,然后能够把它再转回原类型并且保持初始值不变,则称该转换是值保护的。相反,如果某个转换做不到这一点,那么它被称为窄化类型转换(narrowing conversion)。
使用 {}
列表能防止窄化计算的发生。
提升
保护值不被改变的隐式类型转换通常称为提升(promotion)。在执行算术运算之前,通常先把较短的整数类型通过整型提升(integral promotion)成 int
。提升的结果一般不会是 long
(除非运算对象的类型是 char16_t
、char32_t
、wchar_t
或者本身比 int
大的一个普通枚举类型)或 long double
。这反映了 C
语言中类型提升的本质:把运算对象变得符合算术运算的“自然”尺寸。
整型提升的规则是:
- 如果
int
能表示类型为char
、signed char
、unsigned char
、short int
或者unsigned short int
的数据的值,则将该数据转换为int
类型;否则,将它转换为unsigned int
类型。 -
char16_t
、char32_t
、wchar_t
或者普通枚举类型的数据转换成下列类型中第一个能够表示其全部值的类型:int
、unsigned int
、long
、unsigned long
或者unsigned long long
。 - 如果位域的全部值都能用
int
表示,则它转换为int
;否则,如果全部值能用unsigned int
表示,则它转换为unsigned int
;如果int
和unsigned int
都不行,则不执行任何整型提升。 -
bool
值转换成int
,其中,false
变为0
而true
变为1
。
提升是常规算术类型转换的一部分。