Google C++每周贴士 #165: 带初始化器的`if`和`switch`语句

(原文链接:https://abseil.io/tips/165 译者:clangpp@gmail.com)

每周贴士 #165: 带初始化器的ifswitch语句

如果你用不到条件控制流程,那可以到此为止了。

一个新语法

C++17 允许 ifswitch语句包含初始化器:

if (init; cond) { /* ... */ }
switch (init; cond) { /* ... */ }

这种语法可以让你的变量作用域尽量地小:

if (auto it = m.find("key"); it != m.end()) {
  return it->second;
} else {
  return absl::NotFoundError("Entry not found");
}

这里初始化器的语义和for语句里的完全一样;下面会详细说明。

什么时候用

管理复杂度最重要的方式之一,就是把复杂系统拆解成为互不影响的,本地化的组件,每个组件都可以被单独理解,并且作为整体被忽略。在C++中,变量的存在增加了复杂度,而 作用域 允许我们限制由此带来的复杂度:变量所在的作用域越小,读者需要记住的变量就越少。

当需要读者关注的时候,将变量作用域限制在其被使用的地方就变得重要了。这个新语法为此提供了新工具。对比这个新语法和C++17以前的代码:要么我们保持作用域尽量小,为此需要多写一对大括号:

{
  auto it = m.find("key");
  if (it != m.end()) {
    return it->second;
  } else {
    return absl::NotFoundError("Entry not found");
  }
}

要么,正如看起来更典型的方案,我们 并不 保持作用域尽量小,就“泄露”变量:

auto it = m.find("key");
if (it != m.end()) {
  return it->second;
} else {
  return absl::NotFoundError("Entry not found");
}

这种复杂的考量催生了常见的程序员黑话:变量名长度应该与作用域大小相匹配;也就是说,作用域越大,其中的变量名就该越长(为了让读者走出二里地还能记着它)。反之,作用域越小,其中的变量名就可以越短。当变量名“泄露”的时候(如上所示),我们经常看见坑爹的模式涌现出来,例如:多个变量it1it2,…变得必要以防止命名冲突;变量被重新赋值(auto it = m1.find(/* ... */); it = m2.find(/* ... */));或变量有个侵入式的长名字(auto database_index_iter = m.find(/* ... */))。

细节,作用域,声明区

这个新的,可选的ifswitch语句中的初始化器,其工作方式与for语句里的初始化器完全一致。(后者基本是一个带初始化器的while语句。)也就是说,这个含有初始化器的语法基本就是如下写法的语法糖:

语法糖格式(译者注:自动)改写成为
if (init; cond) BODY{ init; if (cond) BODY }
switch (init; cond) BODY{ init; switch (cond) BODY }
for (init; cond; incr) BODY{ init; while (cond) { BODY; incr; }

重点提一句,在初始化器中声明的变量名,在if语句可能存在的else分支中仍然有效。

当然,还是有一点不同:在语法糖格式里,初始化器跟条件(condition)或主体(body)(既包含if分支,又包含else分支)在同一作用域里,而不是在另一个,更大的作用域。这意味着变量名在这一堆组件里必须保持唯一,当然它们可以遮蔽掉更早的声明。下面的例子阐释了各种禁止的重定义和允许的遮蔽声明:

int w;

if (int x, y, z; int y = g()) {   // 错误:y重定义了,初始化器中定义过了
  int x;                          // 错误:x重定义了,初始化器中定义过了
  int w;                          // 可以,遮蔽掉外层变量
  {
    int x, y;                     // 可以,在嵌套作用域中遮蔽变量是允许的
  }
} else {
  int z;                          // 错误:z重定义了,初始化器中定义过了
}

if (int w; int q = g()) {         // w的声明可以,遮蔽掉外层变量
  int q;                          // 错误:q重定义了,在条件语句中定义过了
  int w;                          // 错误:w重定义了,在初始化器中定义过了
}

与结构化绑定(structured bindings)的交互

C++17也引入了 结构化绑定,这种机制可以用来为“可解构的”值(例如元组、数组或简单的结构体)其中的元素命名:auto [iter, ins] = m.insert(/* ... */);

这个特性和if语句里的初始化器相得益彰:

if (auto [iter, ins] = m.try_emplace(key, data); ins) {
  use(iter->second);
} else {
  std::cerr << "Key '" << key << "' already exists.";
}

另一个例子来自C++17新引入的 节点句柄node handles),可以用来在map和set之间移动元素(而不是拷贝)。这个特性定义了一个可解构的 插入返回值insert-return-type),作为插入一个节点句柄的返回值。

if (auto [iter, ins, node] = m2.insert(m1.extract(k)); ins) {
  std::cout << "Element with key '" << k << "' transferred successfully";
} else if (!node) {
  std::cerr << "Key '" << k << "' does not exist in first map.";
} else {
  std::cerr << "Key '" << k << "' already in m2; m2 unchanged; m1 changed.";
}

结论

在以下情况下使用新的if (init; cond)switch (init; cond)语法:当你需要一个新变量,在ifswitch语句内部用到,且在此之外不被用到的时候。这可以简化周围的代码。另外,变量的作用域小了,变量名也可以起得更短。

帮我写c++代码内容如下 Description 表达式有三种表示方法,分别为: 前缀表示式:把运算符写在操作数之前,即运算符+操作数1+操作数2 中缀表示式:把运算符写在操作数之间,即操作数1+运算符+操作数2 后缀表示式:把运算符写在操作数之后,即操作数1+操作数2+运算符 例如:a+b*(c-d)-e/f 前缀表示式:-+a*b-cd/ef 中缀表示式:a+b*(c-d)-e/f 后缀表示式:abcd-*+ef/- 前缀表达式后缀表达式与相应的中缀表达式的操作次序相同,但是没有括号,计算顺序非常明确,不需要考虑运算符的优先级,是计算机最容易处理的表达式。 根据教材给出的算法思路参考代码,将输入的中缀表达式转换为后缀表达式。 主要参考代码见教材第92至95页,Stack类继承了Vector类,且为了实现栈的不同功能,添加或修改了部分函数。Vector类可直接使用实验二的预习代码,新添加的栈相关的函数见教材第72至74页。 OJ平台不支持多文件代码,请将Vector类、Stack类的实现代码与主程序写在同一个文件中。 Input 只有一行,中缀表达式,以&#39;#&#39;开始,以&#39;#&#39;结束。 Output 第一行,输出转换的后缀表达式; 第二行,输出表达式的计算结果; 具体格式见样例。 Sample Input #1*2-4/(5-2*1.5)# Output 转换的后缀表达式为:1 2 * 4 5 2 1.5 * - / - 计算结果为:0 Hint 避坑小贴士: 1、不要打印提示用户输入的信息,避免平台错判;所有输出的冒号为中文冒号。 2、继承的Vector类需包含remove函数(教材第28页),但该函数的序号从1开始,在本题中使用应修改为: public: // 删除指定位置的元素 ElemType remove(int i) { ElemType e = elem[i-1]; for(int j = i-1; j < length; j++) elem[j] = elem[j+1]; --length; return e; } 3、本实验平台的编译器不支持gets()方法,请用cin.get(str,n)代替,其中n为str数组的长度。 4、G++Visual C++处理流插入操作符<<的顺序不同,在G++中,它从左到右处理,所以下面的代码它先输出计算结果为:,再输出evaluate(str)中的内容,而在Visual C++中,它先调用evaluate(str),然后再输出计算结果为:。 cout << "\n计算结果为:" << evaluate(str) << endl; 我们的实验平台使用的是G++编译器,同学们如果使用Visual Studio则使用的是Visual C++,书上的代码会输出不同的结果,为了避免这个问题,同学们可以将最后的输出语句修改为下面的代码: float ans = evaluate(str); cout << "\n计算结果为:" << ans << endl;
10-29
<think>我们需要完成以下任务: 1. 实现一个Vector类(基于教材,但注意OJ要求)。 2. 实现一个Stack类,继承Vector类,并添加栈的相关操作(push, pop, top等)。 3. 编写将中缀表达式转换为后缀表达式的函数。 4. 编写计算后缀表达式的函数。 5. 主函数:读取输入(以#开始结束),转换为后缀表达式并输出,然后计算并输出结果。 注意: - 输入样例:以&#39;#&#39;开始结束,例如"#1*2-4/(5-2*1.5)#" - 输出:第一行输出转换的后缀表达式(用空格分隔各个操作数运算符),第二行输出计算结果。 步骤: 1. 实现Vector类(注意:教材中的Vector类可能包含多个函数,但本题中我们只需要实现部分,并且注意remove函数的实现按照提示修改为从1开始索引,但是我们在栈操作中可能不会直接用到remove,而是使用栈的pop等操作。不过为了完整,我们按照要求实现Vector)。 2. 实现Stack类:继承Vector,并实现栈的基本操作(push, pop, top, empty等)。注意教材中Stack类的实现(第72-74页)可能有所不同,我们按照要求实现。 转换中缀表达式为后缀表达式的算法(教材92-95页): - 初始化一个运算符栈。 - 从左到右扫描中缀表达式: 1. 如果是操作数,则直接输出(注意:操作数可能不止一位,而且可能有小数,所以需要连续读取数字小数点)。 2. 如果是左括号&#39;(&#39;,则压入栈中。 3. 如果是右括号&#39;)&#39;,则依次弹出栈顶运算符并输出,直到遇到左括号(左括号出栈但不输出)。 4. 如果是运算符: - 若栈为空或者栈顶为左括号,则直接压栈。 - 否则,比较当前运算符与栈顶运算符的优先级: 如果当前运算符优先级高于栈顶,则压栈; 否则,弹出栈顶运算符并输出,直到栈顶运算符优先级低于当前运算符或栈为空,然后将当前运算符压栈。 5. 扫描完毕后,将栈中剩余运算符依次弹出并输出。 但是,题目要求输入以&#39;#&#39;开始结束,所以我们可以在表达式开始前压入一个&#39;#&#39;作为栈底,并且在表达式最后也加一个&#39;#&#39;来弹出所有运算符。 注意:教材中使用了两个&#39;#&#39;作为边界。我们也可以这样处理。 然而,在教材代码中,输入表达式是以&#39;#&#39;结束的,并且栈初始化时也压入一个&#39;#&#39;。所以我们按照教材的思路。 但是,注意输入样例:输入是"#1*2-4/(5-2*1.5)#",所以两个&#39;#&#39;分别位于开始结束。 具体步骤: - 初始化栈,将&#39;#&#39;压入栈中。 - 读取第一个字符(应该是&#39;#&#39;,然后开始处理后续字符,直到遇到结束的&#39;#&#39;)。 在转换过程中,我们需要考虑操作数的提取(包括整数小数),所以不能一个字符一个字符地处理操作数,而需要连续读取直到遇到非数字非小数点的字符。 因此,我们可以这样处理操作数: - 如果当前字符是数字或者小数点,则继续读取后续字符,直到遇到非数字非小数点的字符,然后将这个完整的操作数输出(并加上空格分隔)。 运算符的优先级:我们需要定义一个函数来返回运算符的优先级。假设只有加减乘除括号。 优先级:&#39;(&#39; > &#39;乘除&#39; > &#39;加减&#39; > &#39;#&#39;,但是注意在栈内时,左括号在栈内时优先级较低(直到遇到右括号才弹出),而栈外左括号优先级最高。 实际上,教材中使用了一个二维数组precedence来比较栈顶运算符当前运算符的优先级(见教材94页表3-2)。我们也可以采用类似方法。 定义优先级比较表(7种运算符): 运算符:&#39;+&#39;, &#39;-&#39;, &#39;*&#39;, &#39;/&#39;, &#39;(&#39;, &#39;)&#39;, &#39;#&#39; 优先级:我们定义两个优先级:栈内优先级(isp)栈外优先级(icp) 运算符 isp icp # 0 0 ( 1 7 ) 7 1 # 注意:右括号在栈内实际上不会出现,因为遇到右括号会立即弹出直到左括号 * 5 4 / 5 4 + 3 2 - 3 2 但是教材中使用的优先级表(表3-2)如下: 运算符 栈内优先级 栈外优先级 # 0 0 ( 1 10 ) 10 1 * 5 5 / 5 5 % 5 5 + 3 3 - 3 3 我们这里采用教材中的方法(但是题目中只有加减乘除括号,没有取模,所以去掉%),并注意右括号的栈外优先级为1,栈内优先级为10;左括号栈内优先级为1,栈外优先级为10。 注意:当栈顶运算符的isp大于当前运算符的icp时,则弹出栈顶运算符;如果相等,则弹出栈顶运算符(这种情况只有左右括号或#,但注意遇到结束符#时,栈底也有#,这时弹出栈顶的#,然后结束)。 步骤: - 初始化栈,压入&#39;#&#39;。 - 读入第一个字符ch(应该是开始#,我们跳过这个#,然后开始读取下一个字符) - 然后进入循环,读取下一个字符,直到表达式结束。 - 但是为了方便,我们也可以将整个字符串一次读入,然后遍历。 具体转换算法: while(未到字符串末尾) if ch是数字或者小数点,则连续读取直到遇到非数字非小数点,将这部分作为操作数输出,并加上空格。 else 根据当前字符ch(运算符)栈顶运算符的优先级比较: 如果栈顶的isp < ch的icp,则将ch压栈,读取下一个字符。 如果栈顶的isp > ch的icp,则弹出栈顶运算符并输出(注意输出运算符也要加空格),然后不读取下一个字符(继续用当前ch新的栈顶比较)。 如果栈顶的isp == ch的icp,则弹出栈顶(但不输出,因为这种情况是左右括号匹配或者两个#匹配),然后读取下一个字符。 注意:当遇到右括号时,我们将其icp设为1,然后栈顶比较,栈顶可能是运算符或者左括号。当栈顶是左括号(isp=10)时,因为10>1,所以会弹出栈顶直到遇到左括号(这里弹出左括号,然后读取下一个字符)。注意右括号本身不压栈,遇到右括号时,我们处理完直到左括号,然后跳过左括号。 但是,教材中的表3-2实际上是这样的: 运算符 isp icp # 0 0 ( 1 10 ) 10 1 * 5 5 / 5 5 + 3 3 - 3 3 注意:在栈内,左括号的isp=1,但是当栈顶是左括号时,当前运算符的icp如果大于1,则压栈。而当前运算符如果是右括号(icp=1),则栈顶左括号的isp=1,此时等于,所以弹出左括号并丢弃,然后继续读取下一个字符。 所以,我们可以按照教材的方法实现。 但是,注意我们读取操作数时,由于操作数可能是多字符(包括小数点),所以我们需要一个临时数组来保存操作数,然后一起输出。 另外,在输出后缀表达式时,每个操作数运算符之间用空格分隔。 计算后缀表达式算法: - 初始化一个操作数栈。 - 从左到右扫描后缀表达式(每个操作数运算符用空格分隔,我们可以利用这个空格来分割token): 1. 如果是操作数,则压栈。 2. 如果是运算符,则从栈中弹出两个操作数(注意顺序:先弹出的是右操作数,后弹出的是左操作数),进行运算,将结果压栈。 - 最后栈中剩下的就是结果。 但是,注意后缀表达式字符串中可能有空格,我们可以使用字符串流(stringstream)来分割,或者自己遍历字符串分割。 然而,我们在转换中缀表达式为后缀表达式时,已经用空格分隔了每个token,所以计算时可以直接按空格分割。 具体步骤: - 将后缀表达式字符串按空格分割成多个token(操作数运算符)。 - 遍历每个token: if token是运算符,则弹出两个操作数,计算结果压栈。 else 将token转换为浮点数压栈。 注意:操作数可能是整数或小数,所以用float或double。 由于题目要求输出计算结果,我们使用float即可。 实现细节: 1. Vector类:我们按照实验二预习的代码实现(教材28页),但注意remove函数按照提示修改(索引从1开始)。 2. Stack类:继承Vector,然后实现: void push(ElemType e) { insert(length+1, e); } // 在末尾插入 ElemType pop() { return remove(length); } // 移除最后一个元素 ElemType top() { return elem[length-1]; } // 注意:这里我们直接访问元素,需要确保栈非空 bool empty() { return length==0; } 但是注意:教材中Stack的pop函数是删除并返回栈顶元素,而我们的Vector中remove(i)是删除第i个元素(i从1开始)。所以栈顶元素在位置length(即最后一个元素),所以pop应该删除第length个元素。 另外,top函数返回栈顶元素,即最后一个元素(下标为length-1)。 但是,注意:我们使用Vector类,其内部使用数组存储,下标从0开始。所以: push操作:在位置length+1插入(即在当前最后一个元素后面插入,下标为length),这样插入后length增加1。 pop操作:删除最后一个元素(即第length个元素),所以用remove(length)(因为remove的参数i表示第i个元素,最后一个元素就是第length个元素)。 但是,教材中Stack类的实现(第74页): void push(const T& item) { insert(size(), item); } T pop() { return remove(size()-1); } 注意这里size()等于length,而insert(size(), item)就是在尾部追加。pop是remove(size()-1),即删除最后一个元素(索引为size()-1,也就是第size()个元素?)不对,因为remove(i)中i从1开始,所以应该是remove(size())?因为最后一个元素的位置是size()(第size()个元素)。 这里容易混淆。根据提示,remove函数删除指定位置的元素,位置从1开始。那么数组下标0对应位置1,下标1对应位置2...下标n-1对应位置n(即length)。因此,最后一个元素的位置就是length(即size())。 所以,Stack类的实现: void push(ElemType e) { insert(length+1, e); } // 在位置length+1插入,相当于在尾部插入 ElemType pop() { return remove(length); } // 删除最后一个元素(位置为length) ElemType top() { if (length==0) throw "stack is empty"; return elem[length-1]; // 因为位置length对应的下标是length-1 } bool empty() { return length==0; } 但是,注意insert函数的实现:在位置i插入,意味着新元素将位于第i个位置(即下标i-1),所以insert(length+1, e)就是在当前最后一个元素后面插入,下标为length(因为位置length+1的下标是length)。 另外,Vector类需要实现的函数: Vector(int capacity=10); // 构造函数 ~Vector(); // 析构函数 int size(); // 返回元素个数 void insert(int i, ElemType e); // 在第i个位置插入元素 // 以及remove函数(按照提示) 我们还需要重载[]运算符来获取元素?但是Stack类只需要用到top(直接使用elem数组访问最后一个元素),所以可以不用重载[],但是为了Vector的完整性,我们也可以实现。 然而,在Stack类中,我们只需要使用Vector的insertremove函数,以及length成员变量elem数组。所以我们可以将Vector的成员变量设为protected,这样Stack可以访问。 按照题目要求,我们将所有代码写在一个文件中。 步骤总结: 1. 定义Vector类(模板类,但本题表达式中的操作数是浮点数,运算符是字符,所以我们可以分别处理。但是为了通用性,我们使用模板。不过题目要求输出计算结果为浮点数,操作数可能是整数或小数,所以操作数用double,运算符用char。) 2. 实际上,在表达式转换中,栈中存储的是运算符(char),而在计算后缀表达式时,栈中存储的是操作数(double)。所以我们定义两个栈:一个用于转换的运算符栈(char),一个用于计算后缀表达式的操作数栈(double)。因此,我们需要两个不同的栈。但是,我们的Vector类Stack类应该是模板类。 因此,我们实现模板类Vector模板类Stack(继承Vector)。 但是,题目要求将代码写在一个文件中,所以我们使用模板类。 具体实现如下: 注意:由于我们使用模板,我们将所有实现写在头文件风格中(即全部在类定义中实现,或者类外实现但写在同一个文件中,且不使用分离编译)。 由于代码较长,我们逐步实现。 注意:在转换表达式中,我们需要一个函数来返回运算符的栈内优先级(isp)栈外优先级(icp)。我们可以用一个函数实现: int isp(char c); // 返回栈内优先级 int icp(char c); // 返回栈外优先级 优先级表: 运算符 isp icp # 0 0 ( 1 10 ) 10 1 // 注意:实际上右括号不会出现在栈内,所以isp可以不用定义,但为了统一,我们这样定义。但是当遇到右括号时,我们不会将其压栈,而是使用icp=1进行比较。另外,在栈内我们也不可能出现右括号,所以isp函数在遇到右括号时我们可以返回一个很大的值(如10)?但是实际上,在栈内只有左括号,没有右括号。所以当遇到右括号时,我们只使用icp=1,而在栈内比较时,栈顶如果出现右括号,那是不可能的,所以我们在isp中对于右括号返回10?但是为了避免错误,我们按照教材,对于右括号的isp设为10,这样当栈顶是右括号(但实际不会出现)时,优先级很高。 但是,我们不会将右括号压栈,所以栈内不可能有右括号。因此,isp函数中,我们可以不考虑右括号,但是为了统一,我们还是按照教材表3-2返回。 定义: int isp(char c) { switch(c) { case &#39;#&#39;: return 0; case &#39;(&#39;: return 1; case &#39;)&#39;: return 10; // 实际上不会用到,因为不会压右括号 case &#39;*&#39;: case &#39;/&#39;: return 5; case &#39;+&#39;: case &#39;-&#39;: return 3; default: return -1; // 错误 } } int icp(char c) { switch(c) { case &#39;#&#39;: return 0; case &#39;(&#39;: return 10; case &#39;)&#39;: return 1; case &#39;*&#39;: case &#39;/&#39;: return 4; // 教材中是5,但根据教材表3-2,栈外优先级栈内优先级相同,除了括号。但是这里我们按照教材表3-2的实现(上面已经列出的表)? // 注意:教材表3-2中栈外优先级栈内优先级对于*、/、+、-都是一样的(分别为5,53,3)?但是教材中栈顶运算符当前运算符比较时,用的是二维数组,其中*、/、+、-的栈外优先级栈内优先级相同(表3-2中同一行)。但是教材代码中(94页)的precede表却不同:例如,对于栈顶运算符为+,当前运算符为*,那么当前运算符*的优先级高于栈顶+,所以压栈。因此,我们定义: // 实际上,我们可以这样定义:栈外优先级(icp)栈内优先级(isp)对于*/都是5,对于+-都是3。 // 但是教材94页表3-2是一个二维数组,它直接给出了两个运算符的优先级关系。我们也可以直接实现一个二维数组来比较两个运算符的优先级。 // 为了简单,我们采用ispicp分开的方法,并且让*/的icp=4(这样当栈顶是+(isp=3)时,当前*(icp=4)>3,所以压栈;当栈顶是*(isp=5)时,当前+(icp=2)<5,所以弹出栈顶*,然后再比较)。 // 而教材表3-2中,栈外优先级栈内优先级相同(除了括号#),所以实际上我们可以让icpisp返回相同的值(除了括号#)。 // 但是,在教材算法中,当遇到运算符时,比较的是栈顶运算符的isp当前运算符的icp。所以如果我们让*/的isp=5,icp=4,那么就不符合教材表3-2(表3-2中栈外优先级栈内优先级相同)。 // 这里我们按照教材94页的precede数组的思路:该数组给出了栈顶运算符当前运算符的优先级关系(&#39;>&#39;, &#39;<&#39;, &#39;=&#39;)。 // 为了避免混淆,我们按照教材94-95页的代码实现,使用一个二维数组precede[7][7](但是教材中使用了枚举,这里我们用字符直接映射到数组下标)。 // 由于时间关系,我们采用教材94页的二维数组方法(使用一个二维字符数组,precede[i][j]表示栈顶运算符为i,当前运算符为j,它们的优先级关系(&#39;>&#39;表示栈顶优先级高,弹出栈顶;&#39;<&#39;表示当前优先级高,压栈;&#39;=&#39;表示相等,通常用于括号#))。 // 因此,我们改变策略:不单独定义ispicp,而是定义一个二维数组(7x7)来表示运算符之间的优先级关系。 考虑到时间,我们按照教材95页的代码实现优先级比较: // 定义运算符的顺序:按照教材95页的OPT定义: char OPT[7] = {&#39;+&#39;, &#39;-&#39;, &#39;*&#39;, &#39;/&#39;, &#39;(&#39;, &#39;)&#39;, &#39;#&#39;}; // 然后定义一个7x7的二维数组precede,表示栈顶运算符(行)当前运算符(列)的关系: // 行号:根据栈顶运算符在OPT中的下标 // 列号:根据当前运算符在OPT中的下标 // 关系:&#39;>&#39;表示栈顶优先级高,需要弹出栈顶;&#39;<&#39;表示当前优先级高,压栈;&#39;=&#39;表示匹配(如左右括号或两个#) // 这个数组的值按照教材94页表3-2填写。 // 表3-2的行:栈顶运算符(横着看),列:当前运算符(竖着看) // 我们按行:+ - * / ( ) # // 但是OPT数组的顺序是:&#39;+&#39;, &#39;-&#39;, &#39;*&#39;, &#39;/&#39;, &#39;(&#39;, &#39;)&#39;, &#39;#&#39;,所以共7行7列。 // 教材表3-2: // 当前运算符 // + - * / ( ) # // + > > < < < > > // - > > < < < > > // * > > > > < > > // / > > > > < > > // ( < < < < < = ? // ) ? ? ? ? ? ? ? // # < < < < < ? = // 注意:表中有些位置是?,表示不可能出现(比如栈顶不可能出现右括号)。但我们为了安全,可以都设置为>(弹出)?但是教材95页代码中,对于&#39;)&#39;&#39;#&#39;,在栈顶时遇到某些当前运算符会报错。实际上,我们保证不会出现这些情况。 // 按照教材95页的代码,我们这样定义precede数组: char precede[7][7] = { /*栈顶\当前 + - * / ( ) # */ /* + */ &#39;>&#39;, &#39;>&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;>&#39;, &#39;>&#39;, /* - */ &#39;>&#39;, &#39;>&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;>&#39;, &#39;>&#39;, /* * */ &#39;>&#39;, &#39;>&#39;, &#39;>&#39;, &#39;>&#39;, &#39;<&#39;, &#39;>&#39;, &#39;>&#39;, /* / */ &#39;>&#39;, &#39;>&#39;, &#39;>&#39;, &#39;>&#39;, &#39;<&#39;, &#39;>&#39;, &#39;>&#39;, /* ( */ &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;=&#39;, &#39; &#39;, /* ) */ &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, /* # */ &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39; &#39;, &#39;=&#39; }; // 注意:教材95页代码中,对于栈顶是&#39;)&#39;的情况,直接返回&#39; &#39;(错误),同样当前运算符是&#39;#&#39;时,栈顶是&#39;)&#39;也是错误。不过我们保证输入合法,所以不会出现。 // 然后,我们需要一个函数,给定两个运算符(栈顶当前),返回优先级关系(&#39;>&#39;, &#39;<&#39;, &#39;=&#39;): char getPrecede(char top_op, char cur_op) { int i, j; // 找到top_op在OPT中的下标 for (i=0; i<7; i++) { if (OPT[i] == top_op) break; } for (j=0; j<7; j++) { if (OPT[j] == cur_op) break; } if (i>=7 || j>=7) return &#39; &#39;; // 错误 return precede[i][j]; } 但是,这样有点麻烦。由于运算符不多,我们可以用一个map来映射,但这里为了简单,直接循环查找下标。 因此,我们使用二维数组precede来比较优先级。 算法步骤(转换函数): string infixToPostfix(string infix) { // 去掉输入字符串的第一个最后一个#,但为了统一,我们直接处理整个字符串(包含两个#) // 但是,注意输入字符串第一个字符是&#39;#&#39;,最后一个字符也是&#39;#&#39;,所以我们可以: Stack<char> opStack; // 运算符栈 opStack.push(&#39;#&#39;); // 栈底放一个# string postfix; // 存放后缀表达式 int i = 1; // 跳过第一个&#39;#&#39;,即从下标1开始,直到倒数第二个(因为最后一个是#,但我们希望遇到最后的#时结束循环) // 另一种:遍历整个字符串,包括最后的#,因为我们希望最后的#能弹出所有运算符。 int n = infix.length(); i = 1; // 跳过第一个# while (i < n-1 || opStack.top()!=&#39;#&#39;) { // 注意:教材中是在循环内读取字符,这里我们使用i遍历字符串。循环条件:表达式未处理完或栈顶不是#(即还有运算符) // 但是,我们可以在每次处理一个字符后i++,所以循环条件可以设为i < n-1(即处理到倒数第二个字符,但最后一个是#,我们希望用这个#来弹出栈中所有运算符)。实际上,当遇到最后的#时,我们需要处理直到栈顶也为#。 // 因此,我们修改:循环条件为 i < n,处理整个字符串(包括最后的#),因为最后的#是结束符,需要参与比较。 // 但题目要求输入以#开始结束,所以字符串infix的第一个字符是#,最后一个字符也是#,长度为len,那么有效表达式在[1, len-2]?不对,例如"#a#",那么a的位置是1,结束的#在位置2(下标从0开始:str[0]=&#39;#&#39;, str[1]=&#39;a&#39;, str[2]=&#39;#&#39;)。我们遍历i从1到len-1(即最后一个#的位置)?但实际上,当i指向最后一个#时,我们希望结束。 // 所以我们遍历整个字符串,包括最后的#。 // 但是,注意:当i指向第一个字符(下标0)时是开始#,我们已经压栈了一个#,所以从下标1开始读取。 } 具体循环: for (int i=1; i<infix.length(); i++) { char ch = infix[i]; if (isdigit(ch) || ch==&#39;.&#39;) { // 操作数,读取整个操作数 // 注意:操作数可能是整数或小数,所以连续读取数字小数点 string num; while (i<infix.length() && (isdigit(infix[i]) || infix[i]==&#39;.&#39;)) { num += infix[i]; i++; } i--; // 因为for循环还会i++,所以这里后退一步 postfix += num; postfix += &#39; &#39;; // 加空格 } else if (ch == &#39; &#39;) continue; // 忽略空格(题目输入中没有空格,但可以加上) else { // 运算符或括号或# char top_op = opStack.top(); char relation = getPrecede(top_op, ch); while (relation == &#39;>&#39;) { // 栈顶优先级高,弹出栈顶并输出 postfix += top_op; postfix += &#39; &#39;; opStack.pop(); top_op = opStack.top(); relation = getPrecede(top_op, ch); } if (relation == &#39;<&#39;) { opStack.push(ch); } else if (relation == &#39;=&#39;) { opStack.pop(); // 弹出栈顶,但不输出(用于括号匹配或两个#匹配) } else { // 错误情况 cerr << "表达式错误!" << endl; break; } } } // 循环结束后,栈中应该只剩下# // 然后返回postfix return postfix; } 但是,上面的循环中,我们遍历到最后一个字符(结束#)时,会将其与栈顶比较,然后弹出栈顶的#(遇到=关系),然后栈为空(因为弹出后栈顶变为下面的元素,但实际上我们一开始压入一个#,然后弹出后栈空?不对,我们一开始压入一个#,最后弹出后栈空,所以循环结束后栈空)。 但是,注意:在最后,我们可能会在postfix的末尾多一个空格,可以去掉,但题目输出要求用空格分隔,所以末尾有空格也没关系(样例输出有空格)。 然而,教材中的算法是逐个字符读取,而且操作数是在遇到运算符或括号时才结束。我们这里在读取操作数时,已经将整个操作数提取出来,然后i指向操作数后的第一个非操作数字符,然后for循环会i++,所以我们需要i--来抵消一次。 但是,上面的代码中,我们用while循环读取操作数,然后i指向操作数后的第一个字符,然后i--,然后for循环的i++会指向操作数后的字符。所以没有问题。 但是,有一种情况:操作数刚好是表达式最后一个操作数(后面紧跟着结束#),那么我们在while循环中i会指向#,然后退出while,然后i--,然后for循环的i++又指向了#,然后下一次循环处理#。 注意:操作数提取时,如果后面是运算符,那么当前字符(运算符)不会被读入操作数,所以i--后,下次循环会再次处理这个运算符。所以正确。 但是,我们需要注意:如果操作数提取后,i可能越界,所以while循环中要判断i<infix.length()。 计算后缀表达式: float evaluate(string postfix) { // 按空格分割字符串 // 使用stringstream Stack<float> numStack; stringstream ss(postfix); string token; while (ss >> token) { if (token=="+" || token=="-" || token=="*" || token=="/") { // 弹出两个操作数 float b = numStack.pop(); // 右操作数 float a = numStack.pop(); // 左操作数 float res; if (token=="+") res = a+b; else if (token=="-") res = a-b; else if (token=="*") res = a*b; else if (token=="/") res = a/b; numStack.push(res); } else { // 操作数,转换为浮点数 float num = stof(token); // string to float numStack.push(num); } } return numStack.pop(); } 但是,注意:后缀表达式字符串中每个token之间用空格分隔,所以分割正确。 主函数: 读入一行,注意题目说输入以#开始结束,并且要求不要打印提示信息。 输入:使用cin.getline或者getline(cin, str) 避坑小贴士: 本实验平台的编译器不支持gets(),用cin.get(str, n)代替,但是我们可以使用: string str; getline(cin, str); // 因为一行,可以这样读 但是,题目输入只有一行,以\n结束,所以getline可以读取一行(包括空格),而且不会包含换行符。 步骤: string infix; getline(cin, infix); // 读入中缀表达式,例如"#1*2-4/(5-2*1.5)#" // 调用infixToPostfix,传入infix,得到后缀表达式 string postfix = infixToPostfix(infix); // 输出第一行:转换的后缀表达式为:xxx cout << "转换的后缀表达式为:" << postfix << endl; // 计算后缀表达式 float ans = evaluate(postfix); // 注意:evaluate函数需要处理后缀表达式字符串(空格) cout << "计算结果为:" << ans << endl; 但是,问题:转换的后缀表达式末尾可能有多余空格?题目没有要求去掉,所以没关系。 另外,注意:在转换函数中,我们忽略空格,但是输入中没有空格。操作数之间运算符之间用空格分隔,所以后缀表达式格式正确。 但是,有一个问题:在提取操作数时,如果操作数包含多个字符(比如1.5),我们将其作为一个整体输出,然后加空格。所以正确。 但是,注意:在转换函数中,我们处理到最后一个字符(结束#)时,会与栈顶的#匹配,然后弹出栈顶的#,然后循环结束。栈中应该空了。 但是,注意:表达式转换中,遇到结束#时,栈中可能还有运算符,我们会不断弹出直到遇到#(按照优先级比较)。 另一个注意点:二维数组precede中,对于栈顶为&#39;(&#39;,当前为&#39;)&#39;,返回&#39;=&#39;,然后弹出栈顶(即左括号),然后继续读取下一个字符。 但是,在转换函数中,当关系为&#39;=&#39;时,我们弹出栈顶(左括号),但不输出,然后继续。 另外,对于栈顶为&#39;#&#39;,当前为&#39;#&#39;,返回&#39;=&#39;,弹出栈顶(#),然后栈空。 所以,我们的算法正确。 但是,我们还没有实现VectorStack类。 开始实现: 注意:模板类的定义实现通常放在一起。 由于代码较长,我们按顺序写: 1. 定义Vector模板类 2. 定义Stack模板类(继承Vector) 3. 定义运算符优先级数组getPrecede函数 4. 实现infixToPostfix函数 5. 实现evaluate函数 6. 主函数 注意:教材92-95页的代码中,Stack类有is_empty函数,但我们这里用empty()(返回bool)。 另外,题目要求Vector类包含remove函数(按照提示修改为索引从1开始)。 由于操作数可能是负数?题目没有提到负数,所以假设输入都是正数。 但是,表达式可能有负数?题目没有说明,所以按没有负数处理。 开始写代码: </think>以下是根据您的要求实现的C++代码,包括Vector类、Stack类以及中缀表达式转后缀表达式并计算的功能。代码严格遵循题目要求,包括输入输出格式特殊处理。 ```cpp #include <iostream> #include <cctype> #include <sstream> #include <cstring> #include <vector> using namespace std; const int MAXN = 1000; // 最大表达式长度 // 运算符优先级表 const char OPT[7] = {&#39;+&#39;, &#39;-&#39;, &#39;*&#39;, &#39;/&#39;, &#39;(&#39;, &#39;)&#39;, &#39;#&#39;}; const char PRECEDE[7][7] = { // + - * / ( ) # /* 栈顶运算符 */ {&#39;>&#39;, &#39;>&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;>&#39;, &#39;>&#39;}, // + {&#39;>&#39;, &#39;>&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;>&#39;, &#39;>&#39;}, // - {&#39;>&#39;, &#39;>&#39;, &#39;>&#39;, &#39;>&#39;, &#39;<&#39;, &#39;>&#39;, &#39;>&#39;}, // * {&#39;>&#39;, &#39;>&#39;, &#39;>&#39;, &#39;>&#39;, &#39;<&#39;, &#39;>&#39;, &#39;>&#39;}, // / {&#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;=&#39;, &#39; &#39;}, // ( {&#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;}, // ) - 不会出现在栈内 {&#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39;<&#39;, &#39; &#39;, &#39;=&#39;} // # }; // 获取运算符优先级关系 char getPrecede(char stack_op, char cur_op) { int i = 0, j = 0; while (i < 7 && OPT[i] != stack_op) i++; while (j < 7 && OPT[j] != cur_op) j++; if (i < 7 && j < 7) return PRECEDE[i][j]; return &#39; &#39;; // 错误情况 } // 模板Vector类 template <typename ElemType> class Vector { protected: ElemType *elem; // 存储数组 int length; // 当前长度 int capacity; // 总容量 public: // 构造函数 Vector(int cap = 10) : capacity(cap), length(0) { elem = new ElemType[capacity]; } // 析构函数 ~Vector() { delete[] elem; } // 获取元素个数 int size() const { return length; } // 在位置i插入元素 void insert(int i, ElemType e) { if (length == capacity) { // 扩容 ElemType *newElem = new ElemType[capacity * 2]; for (int j = 0; j < length; j++) newElem[j] = elem[j]; delete[] elem; elem = newElem; capacity *= 2; } // 将i-1及之后的元素后移 for (int j = length; j >= i; j--) { elem[j] = elem[j - 1]; } elem[i - 1] = e; length++; } // 删除指定位置元素(索引从1开始) ElemType remove(int i) { ElemType e = elem[i - 1]; for (int j = i - 1; j < length - 1; j++) { elem[j] = elem[j + 1]; } length--; return e; } // 访问元素(索引从0开始) ElemType& operator[](int index) { return elem[index]; } // 获取元素(索引从0开始) ElemType get(int index) const { return elem[index]; } }; // 模板Stack类继承Vector template <typename ElemType> class Stack : public Vector<ElemType> { public: // 构造函数 Stack() : Vector<ElemType>() {} // 入栈 void push(ElemType e) { this->insert(this->size() + 1, e); } // 出栈 ElemType pop() { return this->remove(this->length); } // 获取栈顶元素 ElemType top() { if (this->length == 0) throw "Stack is empty"; return this->elem[this->length - 1]; } // 判断栈是否为空 bool empty() const { return this->length == 0; } }; // 中缀转后缀表达式 string infixToPostfix(const string& infix) { Stack<char> opStack; opStack.push(&#39;#&#39;); string postfix; int i = 1; // 跳过起始&#39;#&#39; int n = infix.length(); while (i < n - 1) { // 跳过结束&#39;#&#39; char ch = infix[i]; if (isdigit(ch) || ch == &#39;.&#39;) { // 处理操作数(整数或小数) while (i < n - 1 && (isdigit(infix[i]) || infix[i] == &#39;.&#39;)) { postfix += infix[i]; i++; } postfix += &#39; &#39;; // 操作数后加空格 continue; } else if (ch == &#39; &#39;) { i++; // 跳过空格 continue; } else { // 运算符处理 while (true) { char stack_top = opStack.top(); char relation = getPrecede(stack_top, ch); if (relation == &#39;<&#39;) { opStack.push(ch); break; } else if (relation == &#39;>&#39;) { postfix += opStack.pop(); postfix += &#39; &#39;; } else if (relation == &#39;=&#39;) { opStack.pop(); break; } } i++; } } // 弹出栈中剩余运算符 while (!opStack.empty() && opStack.top() != &#39;#&#39;) { postfix += opStack.pop(); postfix += &#39; &#39;; } return postfix; } // 计算后缀表达式 float evaluate(const string& postfix) { Stack<float> numStack; stringstream ss(postfix); string token; while (ss >> token) { if (token == "+" || token == "-" || token == "*" || token == "/") { float b = numStack.pop(); float a = numStack.pop(); if (token == "+") numStack.push(a + b); else if (token == "-") numStack.push(a - b); else if (token == "*") numStack.push(a * b); else if (token == "/") numStack.push(a / b); } else { numStack.push(stof(token)); } } return numStack.pop(); } int main() { string infix; getline(cin, infix); // 读取整行输入 // 转换为后缀表达式 string postfix = infixToPostfix(infix); cout << "转换的后缀表达式为:" << postfix << endl; // 计算结果 float ans = evaluate(postfix); cout << "计算结果为:" << ans << endl; return 0; } ``` ### 代码解释: 1. **Vector类模板**: - 实现动态数组,支持插入(`insert`)、删除(`remove`)、获取大小(`size`)等操作 - 内部数组动态扩容(容量不足时自动加倍) - 删除操作索引从1开始(如要求) 2. **Stack类模板**: - 继承Vector类实现栈功能 - 提供`push`(入栈)、`pop`(出栈)、`top`(获取栈顶)、`empty`(判空)等操作 3. **运算符优先级处理**: - 使用预定义的`PRECEDE`二维数组存储运算符优先级关系 - `getPrecede`函数获取栈顶运算符当前运算符的优先级关系(&#39;<&#39;, &#39;>&#39;, &#39;=&#39;) 4. **中缀转后缀**: - 跳过起始结束的&#39;#&#39;符号 - 操作数直接输出(整数小数点连续读取) - 运算符根据优先级决定入栈/出栈 - 输出后缀表达式时用空格分隔各元素 5. **后缀表达式计算**: - 使用字符串流分割token - 操作数压栈,运算符弹出两个操作数计算后压回 - 最终栈中剩余元素即为结果 6. **主函数**: - 读取完整行输入(包含首尾&#39;#&#39;) - 转换并输出后缀表达式 - 计算并输出结果(格式符合要求) ### 注意事项: - 严格遵循题目要求的输入输出格式 - 处理整数小数操作数 - 使用动态数组避免固定大小限制 - 符合教材算法要求(运算符优先级表) - 处理边界情况(空栈、表达式结束等)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值