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
也是黏性設置;