C++学习-入门到精通-【3】控制语句、赋值、自增和自减运算符
控制语句、赋值、自增和自减运算符
- C++学习-入门到精通-【3】控制语句、赋值、自增和自减运算符
- 一、什么是算法
- 二、伪代码
- 三、控制结构
- 顺序结构
- 选择结构
- if语句
- if...else语句
- switch语句
- 循环结构
- while语句
- 四、算法详述:计数器控制的循环
- 五、算法详述:标记控制的循环
- 六、算法详述:嵌套的控制语句
- 七、关于变量的定义和声明两个概念
- 八、for循环的应用
- 九、使用switch语句继续优化GradeBook类
- 十、流操纵符boolalpha
一、什么是算法
对任何可求解的问题来说,都能够以一种特定的顺序执行一系列动作来完成。解决问题的步骤称为算法,它包含两方面的含义:
- 执行的动作
- 这些动作的执行顺序
举个例子来说明动作的执行顺序是非常重要的。
一个人早上会按照下面的算法:①起床;②脱掉睡衣;③沐浴;④更衣;⑤吃早饭;
如果上面动作的执行顺序发生改变:①起床;②脱掉睡衣;③更衣;④沐浴;⑤吃早饭;
这个从如果按照改变之后的顺序执行,那么他将会湿瀌瀌的吃早饭。
所以在程序执行过程中,语句的执行顺序也是非常重要的,所谓的程序控制,就是指定语句(动作)的执行顺序。
下面我们会讨论如何使用控制语句来进行程序控制。
二、伪代码
伪代码语句通常只描述可执行语句。所谓的可执行语句,是指转换为C++程序的代码之后会引起特定动作的语句。注意,没有涉及初始化和构造函数的声明并不是可执行语句。例如:int number;
该语句仅是告诉编译器有一个类型为int的变量,需要你为它预留空间,但并没有导致任何动作的发生。
一个伪代码例子:
三、控制结构
C++程序中语句的执行顺序一般都是按照书写顺序,这种执行方式也被称为是顺序执行。但是在实际问题中只有顺序执行是不够,比如当只有满足条件A时,才执行语句A,否则跳过它,所以在最初的C++中使用goto
语句来控制程序的语句执行顺序,这种由程序员自己指定程序下一句要执行的语句的方式被称为控制转移。但是使用goto
语句程序代码,因为条理复杂,非常容易出错,所以这种使用goto
控制语句执行顺序的方式被淘汰了,取而代之的是三种控制结构:顺序结构
、选择结构
、循环结构
。只使用这三种结构,也可以实现所有的代码需求,且使用这种方式的代码条理清晰、易于调试,使得程序员的工作效率变高。
顺序结构
正常书写(非选择、非循环语句)的程序代码都是顺序结构。
选择结构
C++中提供3种选择语句:if
语句(单路选择语句),if...else
语句(双路选择语句),switch
语句(多路选择语句)。严格来说,还可以使用if...else if...else
语句作为多路选择语句。
if语句
判断一个人是否成年,我们需要根据他的年龄来进行判断,超过18岁就是成年
下面给出该选择语句的伪代码:
如果年龄大于等于18岁输出已成年
下面给出该选择语句的UML活动图。
其中实心圆点表示活动的起始位置,空心圆圈包围的实心圆点表示结束位置。菱形(判断符号)、圆角矩形(动作状态符号)、和小圆圈表示活动。这些符号通过箭头连接,箭头方向表示活动的流向。
if…else语句
继续上面的例子,超过18岁就是成年,不满18岁为未成年;
伪代码:
如果年龄大于等于18岁输出已成年
否则输出未成年
UML活动图:
else摇摆问题
如果没有圆括号强制指定if和else的匹配关系,else会和上一个距离最近的if进行匹配。
语句块
包含在花括号{}
内部的一组语句称为一个语句块。
switch语句
UML活动图:
循环结构
C++中提供3种循环语句,while
循环,for
循环,do...while
循环。在需要重复多次的执行某些语句时使用这种语句。
while语句
当变量i小于10时i增加1
UML活动图
上图中有两个菱形符号,第一个是合并符号,它的作用是将两个工作流(活动)合并成一个工作流。有多个流入,单个流出。第二个是判断符号,作用是判断条件是否成立,单个流入,多个流出。
注意,在switch的case标签中只能使用整型常量值(单个字符或整型数)
上面提到的if,else,switch,while,for,do都是C++中的关键字。在程序中不可以使用关键字作为标识符(例如:变量的名字)。所以大家需要了解到底有哪些关键字。
控制结构的小结
C++中只有三种控制结构,分别是顺序语句、选择语句、循环语句。每个C++程序都是由这些控制语句组建起来的。可以将每个控制语句用一张活动图表示。这样每张图都包含一个初始状态和结束状态,分别代表控制语句的入口点和出口点。使用这些单入口/单出口的控制语句可以很方便的构建程序,将一个语句的出口点与下一个语句的入口点连接在一起即可,因此称控制语句的这种连接方式为控制语句的堆叠。还会有另一种控制语句的连接方式——控制语句的嵌套,这种方式中,一个控制语句位于另一个控制语句的体内。
四、算法详述:计数器控制的循环
现在为了解决一个实际问题:10个学生,,打印他们的总成绩,计算并打印他们的平均成绩。
伪代码:
设置总数为0
设置成绩计数器为1当成绩计数器小于等于10时提示输入下一个成绩输入下一个成绩将该成绩加到总数中成绩计数器加1用总数除以10得到全班的平均成绩
打印总数
打印平均成绩
在之前的GradeBook类中增加上述功能:
// GradeBook.h
#include <iostream>
#include <string>class GradeBook
{
public:explicit GradeBook(std::string);void setCourseName(std::string);std::string getCourseName() const;void display() const;void determineClassAverage() const;
private:std::string courseName;
};
// GradeBook.cpp
#include "GradeBook.h"
using namespace std;GradeBook::GradeBook(string name)
{setCourseName(name);
}
void GradeBook::setCourseName(string name)
{// size()是string类的一个成员函数,返回调用它的对象的长度,// 还有一个相同作用的函数length(),这两个函数在这里的作用完全相同,你喜欢哪个就用哪个// 字符串的长度不超过25,该字符串有效if (name.size() <= 25){courseName = name;}else // 长度不合法{courseName = name.substr(0, 25); // 将name的前25个字符截断,赋值给数据成员cerr << "Name \"" << name << "\" exceeds maximum length(25).\n"<< "limiting courseName to firse 25 characters.\n" << endl;}
}
string GradeBook::getCourseName() const
{return courseName;
}
void GradeBook::display() const
{cout << "Welcome to the Grade Book for " << getCourseName() << " !" << endl;
}void GradeBook::determineClassAverage() const
{// 设置总数int sum = 0;// 设置计数器int count = 1;while (count <= 10){cout << "Enter Grade:>";int grade = 0;cin >> grade;sum += grade;count++;}// 输出总成绩cout << "Total of all grades is " << sum << endl;// 输出平均成绩cout << "Class Average is " << sum / 10 << endl;
}
五、算法详述:标记控制的循环
上面的代码是存在缺陷的,不可能所有的班级的人数都是10人。那么我们应该如何修改,使得该程序可以处理任何个数的学生数量呢?
解决该问题的一种方法是利用一种被称为标记值(又称哑值、信号值、或标志值)的特殊值,来指示循环结束。用户在输入完所有的合法成绩之后,输入标记值来表示数据输入结束。标记控制的循环往往被称为不定数循环,因为循环的执行次数在执行之前都是未知的。显然这个标记值不可以与可接受的值相混淆,合法的成绩都是非负数,所以可以使用一个负值来作为标记值,通常使用-1
作为标记值。
采用自顶向下、逐步求精的方法开发伪代码算法:顶层和第一层求精
为了开发出良好结构化的程序,自顶向下逐步求精的方法是一种非常有用的方法。我们最开始给出的是顶层的伪代码表示,即一条概括了程序总体功能的单语句:
计算全班的平均成绩
实际顶层伪代码是一个程序的总体描述。但是仅有顶层的伪代码的远远不够的,它无法表达出可以作为程序编写依据的细节。因为需要对其进行细化:将上层伪代码分解成一系列小的任务,并将它们按照要执行的顺序列出。这就是所谓的第一级求精。它的结果如下:
初始化变量
输入、求和和计算成绩的个数
计算并打印全班的总成绩和平均成绩
提示:许多程序在逻辑上可以分为三个阶段:初始化程序变量的初始化阶段,输入数据的值和程序变量做相应调整的处理阶段,计算和打印最终结果的收尾阶段
继续进行第二级求精
首先指定特定的变量。我们需要一个变量来保存输入的成绩的累加和,还需要一个变量来保存输入的成绩的数量。
所以伪代码初始化变量
可以细化为:
初始化total为0
初始化count为0
伪代码输入、求和和计算成绩的个数
需要一个循环语句来接收连续输入的每个成绩,由于此时不知道会输入多少个成绩,所以使用标记控制的循环。用户在输入完所有的成绩之后输入一个标记值来终止循环。
所以这句伪代码细化为:
提示用户输入第一个成绩
输入第一个成绩,可能为标记值
当用户输入的值不是标记值时将该成绩累加入total中count增1提示用户输入下一个成绩输入下一个成绩,可能为标记值
对于伪代码计算并打印全班的总成绩和平均成绩
可以细化为
如果count不等于0用total除以count的值设置平均成绩打印全班的总成绩打印全班的平均成绩
否则打印“未输入任何成绩”
当伪代码算法足以转换成C++代码时,这种自顶向下逐步求精的过程便可以结束。
在GradeBook类中实现标记控制的循环
仅有下面的函数发生变化
#include <iomanip>void GradeBook::determineClassAverage() const
{// 设置总数int sum = 0;// 设置计数器int count = 0;int grade = 0; // 接收输入的成绩cout << "Enter Grade or -1 to quit:>";cin >> grade;// -1作为标记值while (grade != -1){sum += grade;count++;cout << "Enter Grade or -1 to quit:>";cin >> grade;}if (count != 0){double average = static_cast<double>(sum) / count;// 输出总成绩cout << "Total of all grades is " << sum << endl;// 设置浮点数格式cout << setprecision(2) << fixed;// 输出平均成绩cout << "Class Average is " << average << endl;}else{cout << "There are no valid grades" << endl;}
}
注意在最后的if…else语句中,使用了一个double类型的变量来接收平均值,这是因为整数除法结果出现小数会直接舍去。导致结果不准确。所以在这段代码中使用double类型的的变量来接收计算得到的平均成绩。并使用static_cast<>
将int类型的sum强制转换为double类型。这里如果不使用强制类型转换将两个操作数之一转换为浮点数,那么变量average仍然会被一个int类型的值赋值(之后隐式转换为double类型)。
使用static_cast
操作符称为显式转换。使用语法为static_cast<type>()
;
提示:浮点数是一个近似值,将一个浮点数当作精确值来使用可能会导致错误,比如:比较两个浮点数的大小
在上面的函数输出平均值之前,使用一个语句cout << setprecision(2) << fixed
来设置浮点数的输出格式。这里的setprecision
是一个参数化的流操纵符,它的参数为2,所以这里输出的浮点数保留小数点后两位数字。要使用该流操纵符必须使用预处理指令包含一个头文件#define <iomanip>
。如果没有使用这个语句手动设置浮点数的精度,那么输出的浮点数一般保留小数点后面6位有效数字(默认精度为6)。
流操纵符fixed
的作用是控制浮点数值以所谓的定点格式输出。类似于科学计数法,定点格式强制浮点数显示特定数量的位数,而且一旦采用了定点格式,那么就一定要显示小数点及为补足小数点后的位数会补0,即使这个浮点数是一个整数量,例如,66.00。如果没有设置定点格式,那么就会输出66。当程序中同时设置了流操纵符fixed和setprecision时,显示的值是一个四舍五入到指定小数位置的数,这个位置是由setprecision的参数决定的(仅输出发生变化,内存中的值不变)。例如:87.638输出为87.64,91.773输出为91.77。
除了使用fixed之外,还可以使用showpoint这个流操纵符强制输出小数点。如果只设置了showpoint没有设置fixed,则小数点后面的不会补0。showpoint也是一个无参的流操纵符。这些无参的流操纵符是不需要头文件iomanip
的,在iostream
中就有它们的定义。
六、算法详述:嵌套的控制语句
问题:
对上面的问题进行分析:
- 程序需要处理10个数据,所以可以使用计数器控制的循环;
- 每个考试结果都是一个数字,要么是1,要么是2;每次读入一个考试结果,程序都需要判断输入的数据是1还是2;
- 使用两个计数器来跟踪考试结果,一个记录输入的考试结果数量,另一个记录不合格的人数;
- 程序处理完所有的考试结果之后,需要判断是否有8个以上的学生通过考试;
下面我们使用自顶向下逐步求精的方法来分析上面的问题。
顶层
分析考试结果,并决定是否给予奖金;
这是该程序总体功能的描述。需要对其进行多次细化才能得到可以顺利转换为C++代码的伪代码。
第一层求精
该层将顶层分成三个部分:初始化,数据处理,输出结果;
所以该层的伪代码为:
初始化变量
输入10个考试结果,并对通过和未通过的人数进行计数
输出考试结果,并决定是否给予奖金
第二层求精
初始化变量
的细化:
初始化变量student count为0
初始化变量failures为0
输入10个考试结果,并对通过和未通过的人数进行计数
的细化:
当(while)student count小于10时提示输入一个考试结果输入一个考试结果student count增加1如果(if)考试结果为2failures增加1
输出考试结果,并决定是否给予奖金
的细化:
显示通过人数的值(student count - failures)
显示failures的值
如果failures小于等于2输出"Bonus to instructor"
上面的伪代码的详细程序已经达到转换为C++代码的程度了,细化结束。
C++代码如下:
#include <iostream>
using namespace std;int main()
{int student_count = 0;int failures = 0;while (student_count < 10){cout << "Enter result:>";int result = 0;cin >> result;student_count++;if(result == 2)failures++;}cout << "Passes is " << (student_count - failures) << endl;cout << "failures is " << failures << endl;if (failures <= 2){cout << "Bonus to instructor" << endl;}
}
运行结果:
C++11的列表初始化
C++11中引入了一种新的变量初始化语法。列表初始化又称统一初始化,使得程序员可以使用同一种语法来初始化任何类型的变量。
例如:
int number = 1;
// 可以写成
int number = { 1 };
// 或
int number{1};
{}
表示列表初始化器。对于一个基本数据类型的变量,只放置一个值在列表初始化器中。对于一个对象,列表初始化器中可以是逗号分隔的值的列表,这些值传递给对象的构造函数。
比如一个雇员类,它有三个数据成员,雇员的名字,雇员的性别,雇员的薪水。
// 使用列表初始化器初始化一个雇员类的对象
Employee enployee = { "zhangsan", "male", 12345 };
// 或
Employee enployee{ "zhangsan", "male", 12345 };
使用列表初始化器来进行初始化还可以阻止“缩小转换”的发生,这种转换可能会导致数据的损失。例如,之前使用int i = 3.14
试图将一个浮点数赋值给int类型的变量i,这个浮点数的浮点部分(.14)会被截掉,保存在变量i中的值其实是3,这导致了数据的损失——这就是一次缩小转换。这样的语句,编译器会发生警告,但是仍可以编译通过。
如果是使用列表初始化器进行这样的初始化,就无法编译通过,可以帮助程序员避免这种可能发生的非常微妙的逻辑错误。
比如:int i = { 3.14 }
,error。
七、关于变量的定义和声明两个概念
声明是对一个变量命名,如上面35行声明了i是一个int类型的变量,为其在内存中预留了空间,并设置它的值为1。
在C++中同时预留内存空间的变量声明,应该换个更确切的说法,变量定义
。
由于定义也是声明,所以除非这种声明特别重要,否则一般情况下还是使用术语“声明”
八、for循环的应用
问题:
#include <iostream>
#include <iomanip>
#include <cmath>using namespace std;int main()
{// 年利率double rate = { 0.05 };// 年数double n = { 10 };// 本金double principal = { 1000.0 };// 总金额double amount{ 0 };// 打印表头// setw操作符设置输出域的宽度cout << "Year" << setw(22) << "Amount on deposit" << endl;// 初始化计数器为0int i{ 0 };for (i = 1; i <= n; i++){amount = principal * pow((1 + rate), i);cout << setw(4) << i << setw(22) << amount << endl;}
}
运行结果:
可以看到这样输出的浮点数格式整齐,设置其格式cout << setprecision(2) << fixed;
,再次输出:
代码中使用到的两个新函数/操作符:
计算幂:
该函数要求两个double类型的实参。返回一个double类型的值。需要包含cmath
这个头文件。
流操纵符格式化数值的输出
该流操纵符的作用是设置下一个输出值应占用的域宽,代码中的setw(4)表示下一个输出值会占用4个字符的空间。
代码中使用两个有参数的流操纵符setprecision
和setw
,所以必须包含头文件<iomanip>
。如果输出的值小于规定字符位置的宽度,在默认情况下,输出值在域宽范围内右对齐;输出的值大于规定字符位置的宽度,则扩展到整个输出值的宽度。可以使用无参流操纵符left
设置左对齐,这是一种黏性设置(没有手动修改的情况下,之后的输出都是左对齐)。可以使用无参流操纵符right
恢复右对齐。
测试代码:
#include <iostream>
#include <iomanip>using namespace std;int main()
{// 验证setw流操纵符只影响下一个输出值cout << setw(5) << "Year" << 'a' << endl;// 记忆性的左对齐cout << setw(5) << left << "Year" << setw(4) << 'a' << endl;// 使用right恢复右对齐cout << setw(5) << left << "Year" << setw(4) << right << 'a' << endl;
}
运行结果:
性能提示
:
在循环中1+rate
这个表达式是不会发生变化的,所以它应该放在循环外面以避免不必要的开销。虽然计算一个表达式的开销很小,但放在循环中时,当循环次数很多时会导致这种开销被放大,使得性能下降。
九、使用switch语句继续优化GradeBook类
现在需要在GradeBook类中增加一些功能,包括接收用户输入的用字母表示(A、B、C、D、E)的成绩,输出每级成绩对应的学生人数的总结。
GradeBook.h
// GradeBook.h
#include <string>class GradeBook
{
public:explicit GradeBook(std::string); // 初始化课程名void setCourseName(std::string); // 设置课程名std::string getCourseName() const; // 获取课程名void display() const; // 打印欢迎信息void inputGrades(); // 输入任意个学生成绩void displayGradeReport() const; // 打印输入的成绩报告
private:std::string courseName;unsigned int aCount; // 成绩为A的学生人数unsigned int bCount; // 成绩为B的学生人数unsigned int cCount; // 成绩为C的学生人数unsigned int dCount; // 成绩为D的学生人数unsigned int eCount; // 成绩为E的学生人数
};
GradeBook.cpp
#include <iostream>
#include "GradeBook.h"using namespace std;GradeBook::GradeBook(string name):aCount(0),bCount(0), cCount(0), dCount(0), eCount(0)
{// 调用set成员函数初始化课程名// 需要进行有效性检验setCourseName(name);
}void GradeBook::setCourseName(string name)
{// 课程名长度小于等于25时,合法if (name.size() <= 25){courseName = name;}else{// 为了程序的健壮性,就算课程名长度不合法// 也会截取前25个字符存入数据成员courseName = name.substr(0, 25);// 进行异常处理cerr << "Name \"" << name << "\" is exceeds miximum length(25)\n" << "Limits courseName to first 25 Characters.\n" << endl;}
}string GradeBook::getCourseName() const
{// 返回数据成员courseName的值return courseName;
}void GradeBook::display() const
{// 打印欢迎信息cout << "Welcome to the GradeBook for " << getCourseName() << "!" << endl;
}void GradeBook::inputGrades()
{// 定义变量int grade{0}; // 暂存输入的成绩// 打印提示信息cout << "Enter the letter grades(A、B、C、D、E)." << endl<< "Enter the EOF character to end input." << endl;// 使用标记控制的循环来接收输入的成绩while ((grade = cin.get())/*读取一个字符*/ != EOF){switch (grade){case 'A': case 'a':aCount++;break;case 'B':case 'b':bCount++;break;case 'C':case 'c':cCount++;break;case 'D':case 'd':dCount++;break;case 'E':case 'e':eCount++;break;case '\n': // 忽略换行符case '\t': // 忽略制表符case ' ': // 忽略空格break;default:cerr << "Incorrect letter grade entered." << "Enter a new grade." << endl;break;}}
}void GradeBook::displayGradeReport() const
{// 输入结果cout << "Number of students who received each letter grade:" << "\nA:" << aCount << "\nB:" << bCount<< "\nC:" << cCount<< "\nD:" << dCount<< "\nE:" << eCount<< endl;
}
test.cpp
#include <iostream>
#include "GradeBook.h"using namespace std;int main()
{GradeBook myGradeBook("CS1201 C++ programming");myGradeBook.display();myGradeBook.inputGrades();myGradeBook.displayGradeReport();
}
运行结果:
读入输入的字符
上面代码中使用cin.get从键盘读入一个字符,并把他保存在变量grade中。可能有些刚接触编程的同学有疑惑,字符不是应该由char类型的变量来保存吗,为什么这里使用一个整型类型的变量来保存呢?这是因为在内存中字符是以二进制的形式储存的,它们是通过ASCII编码转换成字符的。本质上整型类型和字符类型在内存中都是以二进制的形式保存,且整型类型的长度大于字符类型,所以可以使用整型类型的变量来保存字符。
一般情况下,赋值表达式的值就是赋值运算符左边的值。因此,赋值表达式grade = cin.get()
的值等于cin.get()赋给grade的值。
输入EOF指示符
头文件中定义了EOF,它的值是-1
,在程序执行中我们怎么输入一个EOF呢?直接输入-1
?还是输入EOF
?
都不是,“文件结束”的输入是通过在一行上输入组合键来实现的,在Linux/UNIX/OS X这些系统中是使用<ctrl> d
,同时按下ctrl键和d键;在Windows系统中是使用<ctrl> z
来实现的。
忽略输入的换行符、制表符和空格
往程序输入字符时,为了让程序读入字符,必须通过按键盘的回车键的方式,将它们送入计算机。每次输入一个字符,计算机其实读入了两个字符,一个有效成绩字符,一个换行符。当使用cin.get()
读取一个字符时,会出现读入换行符的情况,为了避免每次读入这些符号都由default子句打印打印一条错误信息,所以在switch语句中包含了处理这些情况的子句。
C++11的类内初始化器
C++11允许程序员在类声明中的声明数据成员的同时,为它们提供默许值。
例如:
// 使用类内初始化器,将下面的数据成员的值初始化为0unsigned int aCount = 0; // 成绩为A的学生人数unsigned int bCount = 0; // 成绩为B的学生人数unsigned int cCount = 0; // 成绩为C的学生人数unsigned int dCount = 0; // 成绩为D的学生人数unsigned int eCount = 0; // 成绩为E的学生人数
这不同于上面例子中的在类的构造函数对数据成员进行初始化。在之后的章节中我们会对该初始化器进行更详细的介绍。
十、流操纵符boolalpha
在C++程序中使用流插入运算符输出布尔值true
和false
时,程序输出会是1
和0
。如果想要让程序以单词形式输出,可以使用流操纵符boolalpha
。
示例代码:
#include <iostream>using namespace std;int main()
{cout << boolalpha << "Logical AND (&&)"<< "\nfalse && false: " << (false && false) << "\nfalse && true: " << (false && true)<< "\ntrue && false: " << (true && false)<< "\ntrue && true: " << (true && true)<< endl;cout << "\nLogical OR (||)"<< "\nfalse || false: " << (false || false)<< "\nfalse || true: " << (false || true)<< "\ntrue || false: " << (true || false)<< "\ntrue || true: " << (true || true)<< endl;cout << "\nLogical NOT (!)"<< "\n!false: " << (!false)<< "\n!true: " << (!true)<< endl;
}
运行结果:
从结果中可以看出流操纵符boolalpha
也是黏性设置;