4 控制语句、赋值、自增和自减运算符
1 算法
对任何可求解的计算问题来说,都能够以特定的顺序执行一系列动作来完成。解决问题的步骤(procedure)称为算法(algorithm),它包含两方面的含义:
- 执行的动作
- 这些动作执行的顺序
所谓程序控制,就是指定语句(动作)执行的顺序。
2 伪代码
伪代码(pseudocode)是一种人为的、非正式的语言,目的是帮助程序员不必受C++语法细节的束缚而开发算法。
伪代码语句通常只描述可执行语句(executable statement)。所谓可执行语句,是指程序员将程序从伪代码转换为C++代码并在计算机上编译、运行时,会引起特定动作发生的语句。没有涉及初始化和构造函数调用的声明并不是可执行语句。例如:
int counter;
这个声明只告诉编译器变量counter的类型,并提示编译器在内存中为这个变量保留内存空间。但是,当程序执行时,这个声明并不会导致任何动作,比如输入、输出或一次计算等的发生。
通常我们在伪代码中不包含变量声明。然而,有些程序员也会在伪代码程序的开始部分列出变量并提到它们的用途。
提示用户输入第一个整数
输入第一个整数
提示用户输入第二个整数
输入第二个整数
第一个整数和第二个整数相加
显示结果
如上,就是一个伪代码的例子。
3 控制结构
一般来说,程序中的语句是按照书写的顺序逐条执行的。这种程序执行的方式称为顺序执行(sequential execution)。可是,许多C++语句还允许程序员自己指定接下来要执行的语句,按照顺序该语句也许不是下一条语句。这种程序执行的方式称为控制转移(transfer of control)。
上世纪60年代,goto
语句的使用创建了“意大利面条式的代码”。结构化编程(structured programming)的概念几乎成了“取消goto
”的代名词。
编写所有的程序其实只需要三种控制结构(control structure),即顺序结构(sequence structure)、选择结构(selection structure)和循环结构(repetition structure)。
C++中的选择语句
C++提供了三种选择语句。
其中,if
选择语句是一种单路选择语句(single-selection statement),因此它要么选择,要么忽略一个动作(或者是专门的一组动作)。在某个条件为真(true)时执行(选择)一个动作;否则,跳过该动作。
if...else
选择语句是双路选择语句(double-selection statement),因为它在两个不同的动作(或动作组)之间做选择。在某个条件为真时执行一个动作;条件为假(false)时,选择另一个动作。
switch
选择语句是多路选择语句(multiple-selection statement),它从许多不同的动作(或动作组)中选择要执行的一个动作(或动作组)。则根据一个整型表达式的值执行许多不同动作中的一个动作。
C++中的循环语句
C++提供了三种循环语句,它们使程序在某个条件(称为循环继续条件)为真的情况下重复执行语句。这三种循环语句分别是while
语句、do...while
语句和for
语句。
在while
语句或for
语句循环体中的动作(或动作组)可以执行0次或多次:如果初始时循环继续条件为假,那么动作(或动作组)一次也不会执行。而do...while
语句的循环体中的语句则至少执行一次。
TIPS:
我们所构建的任何C++程序,都只需使用7种形式的控制语句(顺序、
if
、if...else
、switch
、while
、do...while
和for
),并以仅有的两种连接方式(控制语句堆叠和控制语句嵌套)组合而成。
4 if选择语句
如果学生的成绩大于或等于60分
打印“通过”
这段伪代码第二行进行了缩进处理,这种缩进格式并非必须,但建议这样做,因为强调了结构化程序固有的结构。
C++中,上述伪代码可以写成
if(grade >= 60)
cout<<"Passed";
实际上,在C++中,可以基于任何表达式做判断:如果表达式的值等于零,就把它当作假;若不等于零,就可以当作真。bool值true可以用任何非零值表示(编译器通常使用1),而false可以使用零值来表示。
5 if…else双路选择语句
if(grade >= 60)
cout<<"Passed";
else
cout<<"Failed";
else的体也进行了缩进。
如果有多级缩进,为了提高程序的可读性和可维护性,每级相对上级缩进的空格幅度应保持一致。
条件运算符(?:)
条件运算符是C++唯一的三元运算符(ternary operator)——它需要三个操作数。 这些操作数和条件运算符一起构成了条件表达式。
其中,第一个操作数是条件,第二个操作数是条件为真时整个条件表达式的值,第三个操作数是条件为假时整个条件表达式的值。
cout<<(grade>=60 ? "Passed" : "Failed");
这与前面的if…else语句功能相同。
嵌套的if…else语句
嵌套的if…else语句用于检测多路分支的情况,它将if...else
语句放置在其他if...else
语句内部。
if(grade >= 90)
cout<<"A";
else
if(grade >= 80)
cout<<"B";
else
if(grade >= 70)
cout<<"C";
else
if(grade >= 60)
cout<<"D";
else
cout<<"F";
大多数C++程序员都喜欢将上述if...else
语句写成如下形式:
if(grade >= 90)
cout<<"A";
else if(grade >= 80)
cout<<"B";
else if(grade >= 70)
cout<<"C";
else if(grade >= 60)
cout<<"D";
else
cout<<"F";
这种形式避免了代码过于向右缩进,提高了程序可读性。
TIPS:
与一系列单路选择
if
语句相比,嵌套的if...else
语句的执行速度要快很多,这是由于在遇到第一个满足的条件后,可以及早退出整个结构。在嵌套的
if...else
语句中,应首先检测最有可能为true的条件,这样程序会运行的更快退出的也更早。
else摇摆问题
C++编译器总是把else同它之前最近的if联系起来。除非放置花括号,否则会导致else摇摆问题(dangling-else problem)。
if(x > 5)
if(y > 5)
cout<<"x and y are > 5";
else
cout<<"x is <= 5";
这条嵌套的 if...else
语句并不按它表面的形式运行。实际上编译器将它解释为:
if(x > 5)
if(y > 5)
cout<<"x and y are > 5";
else
cout<<"x is <= 5";
为了迫使按它原有意图运行,我们改写为:
if(x > 5)
{
if(y > 5)
cout<<"x and y are > 5";
}
else
cout<<"x is <= 5";
语句块
if
选择语句通常认为它的体内只有一条语句。同样,if...else
语句的if
和else
部分也认为各自只有一条体语句。
要在if
或if...else
语句的任一部分的体中包括几条语句,就应将这些语句用一对花括号括起来。
包含在一对花括号中的一组语句成为一条“复合语句”(compound statement)或一个“语句块”(block)。
凡是可以放置单条语句的地方,都可以放置语句块。但是也不排除一种情况,即根本不放置任何语句——这些地方可以放置称为空语句的语句。
空语句的表示方式是:在本该是语句的地方,单用一个分号表示。
TIPS:
在单路选择语句的
if
语句条件后防止分号将导致逻辑错误,而在双路选择的if...else
语句(假设其if
部分包含了一条实际的体语句)条件后放置分号将导致语法错误。
6 while循环语句
如果在while
语句的循环体中,没有提供使while
条件最终变成假的动作,通常会导致一个称为无限循环的逻辑错误。
当判断条件为假时,while
语句退出(即达到其结束状态),并将控制权移交给程序中顺次的下一条语句。
TIPS:
对在循环中多次执行的代码而言,对它们微小的性能改进可能导致实质性的整体性能的提高。
7 算法详述:计数器控制的循环
常常将计数器控制的循环称为定数循环,因为在循环开始之前,循环的次数是已知的。
设置总数为0
设置成绩计数器为1
当成绩计数器小于或等于10时
提示用户输入下一个成绩
输入下一个成绩
将该成绩加到总数中
成绩计数器加1
用总数除以10的商设置全班平均成绩
打印全班所有学生成绩的总数
打印全班平均成绩
如上就是一个例子的伪代码。
下面将GradeBook类进行优化:
//GradeBook.h
//GradeBook class definition. This file presents GradeBook's public
//interface without revealing the implementations of GradeBook's member
#include<string>
//GradeBook class definition
class GradeBook
{
public:
explicit GradeBook(std::string);//constructor initialize courseName
void setCourseName(std::string);
std::string getCourseName() const;
void displayMessage() const;
void determineClassAverage() const;
private:
std::string courseName; //course name for this GradeBook
}; //end class GradeBook
//GradeBook.cpp
//GradeBook member-function definitions. This file contains
// implementations of the member functions prototyped in GradeBook.h
#include<iostream>
#include"GradeBook.h"
using namespace std;
//construtor initializes courseName with string supplied as argument
GradeBook::GradeBook(string name)
{
setCourseName(name); //validate and store courseName
} //end GradeBook constructor
void GradeBook::setCourseName(string name)
{
if (name.size() <= 25)
{
courseName = name; //store the course name in the object
}
else
{
//set courseName to first 25 characters of parameter name
courseName = name.substr(0, 25); //start at 0, length of 25
cerr << "Name \"" << name << "\"exceeds maximum length(25).\n" << "Limiting courseName to first 25 characters.\n" << endl;
}
}
string GradeBook::getCourseName() const
{
return courseName;
}
void GradeBook::displayMessage() const
{
std::cout << "Welcome to the Grade Book for\n" << getCourseName() << "!" << endl;
}
void GradeBook::determineClassAverage() const
{
int total = 0;
unsigned int gradeCounter = 1;
while (gradeCounter)
{
cout << "Enter grade: ";
int grade = 0;
cin >> grade;
total = total + grade;
gradeCounter = gradeCounter + 1;
}
int average = total / 10;
cout << "\nTotal of all 10 grades is " << total << endl;
cout << "Class average is " << average << endl;
}
#include"GradeBook.h"
//function main begins program execution
int main()
{
//creat GradeBook object myGradeBook and pass course name to constructor
GradeBook myGradeBook("CS101 C++ Programming");
myGradeBook.displayMessage();
myGradeBook.determineClassAverage();
}
在类的实现代码中,我们将gradeCounter变量的数据类型声明为unsigned int
。这种数据类型的变量只能存储非负的值。
计数器变量一般初始化为0或1。未初始化的变量中存放的是垃圾值(garbage value),也成为不确定的值,即为此变量分配的内存区域中原先存储的值。
关于整数除法和截尾的说明
两整数相除即为整数除法——计算结果中的小数部分都将被丢弃,即结尾(truncated)。
差1错误(off-by-one-error):一个逻辑错误,在计数器控制的循环中,由于循环计数器每次循环加1,所以当它的值比最大的合法值大1时(例如,从1计数到10时,它为11),循环终止。
关于算术溢出的说明
total = total + grade;
这个简单语句是个潜在问题,即整数相加可能导致和太大而不能存储到一个int
变量中,这就是算术溢出。算术溢出引起不确定的行为。
在int
变量中能保存的最大和最小值分别由符号常量INT_MAX
和INT_MIN
表示,它们定义在头文件 <climits>
中。且对于整数其他类型和浮点类型都有类似的常量,浮点类型的定义在头文件<cfloat>
中。
8 算法详述:标记控制的循环
利用标记值(sentinel value),也称为信号值、哑值或标志值的特殊值,指示“数据输入结束”。
标记控制的循环常常成为不定数循环(indefinite repetition),因为循环次数在循环开始执行前未知。
用0作除数引起不确定的行为,并且通常导致一个致命的逻辑错误。
下面在GradeBook类中实现标记控制的循环,并引入一种特殊的运算符(cast operator),迫使求平均数的计算产生一个浮点数的结果。
GradeBook的实现代码优化如下:
//GradeBook.cpp
//GradeBook member-function definitions. This file contains
// implementations of the member functions prototyped in GradeBook.h
#include<iostream>
#include<iomanip>
#include"GradeBook.h"
using namespace std;
//construtor initializes courseName with string supplied as argument
GradeBook::GradeBook(string name)
{
setCourseName(name); //validate and store courseName
} //end GradeBook constructor
void GradeBook::setCourseName(string name)
{
if (name.size() <= 25)
{
courseName = name; //store the course name in the object
}
else
{
//set courseName to first 25 characters of parameter name
courseName = name.substr(0, 25); //start at 0, length of 25
cerr << "Name \"" << name << "\"exceeds maximum length(25).\n" << "Limiting courseName to first 25 characters.\n" << endl;
}
}
string GradeBook::getCourseName() const
{
return courseName;
}
void GradeBook::displayMessage() const
{
std::cout << "Welcome to the Grade Book for\n" << getCourseName() << "!" << endl;
}
void GradeBook::determineClassAverage() const
{
int total = 0;
unsigned int gradeCounter = 0;
cout << "Enter grade or -1 to quit: ";
int grade = 0;
cin >> grade;
while (grade != -1)
{
total = total + grade;
gradeCounter = gradeCounter + 1;
cout << "Enter grade or -1 to quit: ";
cin >> grade;
}
if (gradeCounter != 0)
{
double average = static_cast<double>(total) / gradeCounter;
cout << "\nTotal of all " << gradeCounter << " grades entered is " << total << endl;
cout << setprecision(2) << fixed;
cout << "Class average is " << average << endl;
}
else
cout << "No grades were entered" << endl;
}
浮点数的精度和存储空间要求
C++将程序源代码中键入的所有浮点数默认为double
类型。在源代码中的这些值称为浮点数常量。
在常规计算中,浮点数通常是除法计算的结果。例如10除以3结果是3.333333…,是一个无限循环小数。可是计算机只为这样的值分配固定大小的空间,显然,存储的浮点值只能是一个近似值。把浮点数当精确值使用很可能导致错误结果。
基本数据类型之间显示的和隐式的转换
double average = total / gradeCounter;
这行代码计算结果中小数部分已经丢失。为使整数值进行浮点数运算,必须生成临时的浮点数值。
C++提供了static_cast
运算符完成强制类型转换这一任务。
double average = static_cast<double>(total) / gradeCounter;
这里生成一个临时的浮点数值,它是括号中操作数total的浮点数副本。像这种利用强制类型转化运算符进行的转换称为显示转换(explicit conversion)。存储在total中的值仍是一个整数。
C++编译器知道如何计算操作数数据类型相同的表达式。为保证操作数类型相同,编译器对所选的操作数进行了一种称为升级(promotion),也称为隐式转换(implicit conversion)的操作。例如,在同一个表达式中若同时含有int
和double
数据类型的值,C++将int
类型的操作数升级为double
类型。
所有的数据类型,包括类类型,都可以使用强制类型转换运算符。
static_cast
运算符的使用形式是:static_cast<数据类型>()
。强制类型转换符是一元运算符,其优先级只比圆括号的优先级低。
浮点数的格式化
setprecision()
是一个参数化的流操纵符(parameterized stream manipulator),圆括号里的参数表示输出精度,即小数点后显示的数字位数。使用这些流操纵符必须包含#include<iomanip>
这一预处理指令。
endl
是一个无参数的流操纵符(nonparameterized stream manipulator),因为其后没有含值或表达式的圆括号对,也不需要头文件<iomanip>
。若不显式地指定精度,通常情况下输出浮点数具有6位数字的精度。
流操纵符fixed
的作用是控制浮点数值以所谓的定点格式(fixed-point format)输出,这种格式是科学计数法相对而言的。内存中的值并未因此改变。
当程序中同时设置流操纵符fixed
和setprecision
时,显示的值是一个四舍五入到指定小数位置上的值。
利用流操纵符showpoint
也可以强制浮点数将小数点输出。如果只设置了showpoint
而没设置fixed
,则浮点数小数点后补足的0将不会输出。像endl
一样,流操纵符fixed
和showpoint
也是无参数的,不需要头文件iomanip
。它们的声明可以在头文件<iostream>
中找到。
关于无符号整数的声明
一般而言,计数器值存储非负的值,声明时应用unsigned
数据类型。
与有符号整数相比,unsigned
整数类型的变量可以表示0到近似前者两倍正数范围的值。可以用<climits>
中的符号常量UINT_MAX
确定自己所用平台的最大unsigned int
值。
9 算法详述:嵌套的控制语句
循环程序有时需要在每次循环开始处初始化。这种重新初始化的实现方式通常有两种:一是用赋值语句而非声明语句;二是将声明语句挪至循环体内。
C++11的列表初始化
列表初始化(List initialization),也称为统一初始化(uniform initialization),是程序员能够用一种语法来初始化任意类型的一个变量。
unsigned int studentCounter = 1;
这行代码在C++11中,可以书写为:
unsigned int studentCounter = {1};
或者
unsigned int studentCounter{1};
花括号表示列表初始化器。对于一个基本类型的变量,只放置一个值在列表初始化器中。对于一个对象,列表初始化器中可以是逗号分隔的值的列表,这些值传递给对象的构造函数。如:
Employee employee1{"Bob","Blue",1234.56};
对基本数据类型的变量来说,列表初始化的语法还可以阻止所谓的“缩小转换”(narrowing conversion),这种转换可能导致数据损失。如:
int x = 12.7;
这条语句试图将double
类型的值赋给int类型的变量,这个double
值的浮点部分会被截掉,从而转换为一个int
数据,但导致了信息缺损——这就是一次“缩小转换”,即实际赋值给x的是12。
许多编译器会对这样的语句发出一个警告信息,但仍允许它通过编译。
如果采用以下语句:
int x = {12.7};
或
int x{12.7};
那么将产生一个编译错误。
10 赋值运算符
任何如下形式的语句:
变量 = 变量 运算符 表达式;
其中相同的变量在赋值运算符的两边出现,而且若运算符是+
、-
、*
、/
、或%
二元运算符中的一个,都可以写成以下形式:
变量 运算符= 表达式;
赋值运算符 | 示例表达式 | 说明 |
---|---|---|
+= | c+=7; | c=c+7; |
-= | d-=4; | d=d-4; |
*= | e*=5; | e=e*5; |
/= | f/=3; | f=f/3; |
%= | g%=9; | g=g%9; |
11 自增和自减运算符
自增运算符(increment operator)和自减运算符(decrement operator)都是一元运算符。
运算符 | 名称 | 示例表达式 | 说明 |
---|---|---|---|
++ | 前置递增 | ++a; | a先增1,然后在a出现的表达式中使用a的这个新值 |
++ | 后置递增 | a++; | 在a出现的表达式中使用a的当前值,然后a增1 |
-- | 前置递减 | --b; | b先减1,然后在b出现的表达式中使用b的这个新值 |
-- | 后置递减 | b--; | 在b出现的表达式中使用b的当前值,然后b减1 |
TIPS:
与二元运算符不同,一元的自增和自减运算符应该紧邻其操作数,中间不能有任何空格。
在一条仅由单个变量的自增或自减构成的语句中,前置和后置的逻辑效果是一样的。
企图用表达式而不是一个可修改变量的名字,例如++(x+1)
作为自增或自减运算符的操作数是个语法错误。
12 运算符优先级总表
运算符 | 结合律 | 类型 |
---|---|---|
:: () | 从左向右 | 最高 |
++ -- static_cast<类型>() | 从左向右 | 后缀 |
++ -- + - | 从右向左 | 一元(前缀) |
* / % | 从左向右 | 乘 |
+ - | 从左向右 | 加 |
<< >> | 从左向右 | 插入/提取 |
< <= > >= | 从左向右 | 关系 |
== != | 从左向右 | 相等 |
?: | 从右向左 | 条件 |
= += -= *= /= %= | 从右向左 | 赋值 |