第 4 章 编程基础
文章目录
4.1 循环
4.1.1 基于范围的 for
循环(C++11)
C++11 新增了一种循环:基于范围的 for
循环。这简化了一种常见的循环任务:对数组(或容器类,如 vector
)的每个元素执行相同的操作,如:
double prices[5] = {4.99, 10.99, 6.87, 7.99, 8.49};
for(double x: prices)
cout<<x<<endl;
该循环将打印 prices
数组中所有的元素。
要修改数组的元素,需要使用不同的循环变量语法:
for (double &x:prices)
x*=0.8;
符号 &
表明 x
是一个引用变量,当修改引用变量时,原始变量(此处为数组元素)也会被修改。
还可以结合使用基于范围的 for
循环和初始化列表:
for (int x: {1,2,3,4,5})
cout<<x<<endl;
4.1.2 循环和文本输入
对于 C 语言中常见的 while((ch = getchar()) != EOF)
,在 C++ 中,应该用什么来替代呢?考虑下面程序:
#include <iostream>
int main()
{
using namespace std;
char ch;
int count = 0;
cin.get(ch);
while(cin.fail() == false)
{
cout << ch;
++count;
cin.get(ch);
}
cout << endl << count << " characters read\n";
return 0;
}
-
上述程序使用了
cin.get(ch)
函数,至此我们已经学过三个版本的cin.get()
:istream & cin.get(char*, int); char cin.get(void); istream & cin.get(char&);
-
当
cin
读取到EOF
时,cin
会将两位eofbit、failbit
都设置为 1 1 1,此时可以通过以下两个函数进行检测:-
eof()
成员函数
eof()
可以查看eofbit
是否被设置;如果检测到EOF
,则cin.eof()
将返回bool
类型值true
,否则返回false
。 -
fail()
(用途较前者更广)如果
eofbit
或failbit
被设置为1
,则fail()
成员函数将返回true
,否则返回false
。
-
注意,这两个函数都报告的是最近读取的结果,也就是说,它们是在事后报告,而不是预先报告。因此应将 cin.eof()
或 cin.fail()
测试放在读取后。
当 cin
方法检测到 EOF
后,cin
将不再读取输入,再次调用 cin
也不管用。在这种情况下,cin.clear()
方法将清除 EOF
标记,使输入继续进行。
有没有更简短的检测 EOF
的方法呢?istream
类提供了一个可以将 istream
对象转换为 bool
值的函数。当最后一次读取成功时,则转换得到的 bool
值为 true
;否则为 false
。这意味着可以将 while(cin.fail() == false)
简写成 while(cin)
。这比 while(!cin.eof())
或 while(!cin.fail())
更加通用。
最后,由于 cin.get(ch)
返回 cin
对象,所以可以将循环精简成以下格式:while(cin.get(ch))
。
-
C++ 中的
getchar()
和putchar()
熟悉 C 的用户可能更喜欢
getchar()
和putchar()
,它们在 C++ 中仍然适用。也可以使用 C++ 中istream
和ostream
类中类似功能的成员函数:char ch; ch = cin.get(); //ch=getchar() cout.put(ch); //putchar(ch)
所以
while((ch=getchar())!=EOF)
也可以写成while((ch=cin.get())!=EOF)
。值得一提的是,cin.get()
读到EOF
后也会终止cin
的任何读入,如:#include <iostream> int main() { using namespace std; char ch; while((ch=cin.get())!=EOF) cout << ch << endl; cin.get(); cin.get(); char s[10]; cin >> s; cout << s; }
下面代码中,当
cin.get()
读取到EOF
后将退出while
循环,并且终止后面所有的cin
读入,所以字符串s
将不会有任何输入。
4.2 分支语句和逻辑运算符
4.2.1 逻辑运算符的另一种表示方式
标识符 and、or
和 not
都是 C++ 关键字,可以分别代替 &&、||
和 !
。
4.2.2 读取数字的循环
考虑下面一种情形:
int n;
cin>>n;
如果用户输入一个单词,而不是一个数字,会发生什么呢?发生这种类型不匹配的情况时,将发生 4 4 4 种情况:
n
的值保持不变,本次输入将被跳过- 不匹配的输入将被留在输入队列中
cin
对象中的一个错误标记将被设置- 此时
(bool) cin
等于false
要继续输入,需要使用 clear()
方法重置错误输入标记。
#include <iostream>
const int Max = 5;
int main()
{
using namespace std;
double fish[Max];
cout << "Please enter the weights of your fish.\n";
cout << "You may enter up to " << Max << " fish <q to terminate>.\n";
cout << "fish #1: ";
int i=0;
while(i<Max&&cin>>fish[i])
{
if(++i<Max)
cout << "fish #" << i + 1 << ": ";
}
cout << "There are " << i << " fishes.\n";
cin.clear();
char s[10];
cin >> s;
cout << "s: " << s;
}
考虑上面的程序,当用户输入 q
时,cin>>fish[i]
转换为 bool
类型为假,且将跳过本次输入(下次输入将从 cin>>s
开始),退出 while
循环。随后,clear()
方法将重置错误标记(不重置错误标记无法再读入数据),然后 s
将读取输入的 q
并输出。注意,此处 clear()
并不会清空输入队列。而 cin
在读取到 Ctrl + Z
时会清空输入队列。
4.2.3 简单文件输入/输出
-
cin
处理输入的方式假设有以下输入行:
38.5 19.2
来看一下使用不同数据类型的变量来存储时,
cin
处理输入行的方式:-
char
char ch; cin >> ch;
cin
会将输入行中的第一个字符3
赋给ch
,输入队列中的下一个字符为 8 8 8,下一次处理输入将从 8 8 8 开始。 -
int
int n; cin >> n;
在这种情况下,
cin
将不断读取,直到遇到非数字字符。也就是说,n
将等于 38 38 38,下一次输入将从.
开始。 -
double
double x; cin >> x;
在这种情况下,
cin
将不断读取,直到遇到第一个不属于浮点数的字符。也就是说x
将等于38.5
,下一次输入将从 -
char
数组char word[50]; cin >> word; //case 1 cin.getline(word, 50); //case 2
在第一种情况下,
cin
将不断读取,直到遇到空白字符。也就是说,cin
会将38.5
这 4 4 4 个字符的编码存储到word
中,并在末尾加上一个空字符(\0
)。下一次输入将从 开始。在第二种情况下,
cin
将不断读取,直到遇到换行符,随后换行符会被丢弃,下一次输入将从下一行开始。
-
-
使用文件输入/输出
在 C 语言中,常见的文件操作为
FILE* fp = fopen()
等,在 C++ 中,更强调用流的概念。下面分文件输入、输出讨论 C++ 的这种特性。-
文件输出
文件输出需要按照以下几个操作:
- 必须包含头文件
#include <fstream>
,该头文件定义了用于输出的ofstream
类 - 需要声明
ofstream
对象 - 需要将
ofstream
对象与文件关联起来。方法之一是使用open()
方法 - 像使用
cout
那样使用该ofstream
对象 - 使用完文件之后,应使用
close()
方法将其关闭
例如:
#include <iostream> #include <fstream> using namespace std; int main() { ofstream fout; fout.open("test.txt"); fout << "This is a test."; fout.close(); return 0; }
- 必须包含头文件
-
文件输入
文件输入的基本操作和文件输出类似:
- 必须包含头文件
#include <fstream>
,该头文件定义了用于输入的ifstream
类 - 需要声明
ifstream
对象 - 需要将
ifstream
对象与文件关联起来。方法之一是使用open()
方法 - 像使用
cin
那样使用该ifstream
对象 - 使用完文件之后,应使用
close()
方法将其关闭
例如:
#include <iostream> #include <fstream> using namespace std; int main() { ifstream fin; fin.open("test.txt"); char s[15]; fin >> s; cout << s; fin.close(); return 0; }
如果试图打开一个不存在的文件,将导致后面使用
ifstream
对象进行输入时失败。检查文件是否被成功打开的方法之一是使用is_open()
,当文件成功打开时,该方法返回true
。可以参考以下代码:fin.open("test.txt"); if(!fin.is_open()) exit(EXIT_FAILURE); //终止程序运行
方法
is_open()
是 C++ 中相对较新的内容。C++ 较老的方法是good()
,下面程序是一个示例:#include <iostream> #include <fstream> #include <cstdlib> using namespace std; const int SIZE = 60; int main() { char filename[SIZE]; ifstream inFile; cout << "Enter name of data file: "; cin.getline(filename, SIZE); inFile.open(filename); if(!inFile.is_open()) { cout << "Could not open the file " << filename << endl; cout << "Program terminating.\n"; exit(EXIT_FAILURE); } double value; double sum = 0.0; int count = 0; inFile >> value; while(inFile.good()) { sum += value; count++; inFile >> value; } //上述循环可以简化成 // while(inFile >> value) //{ // sum += value; // count++; //} if(inFile.eof()) cout << "End of file reached.\n"; else if(inFile.fail()) cout << "Input terminated by data mismatch.\n"; else cout << "Input terminated for unknown reason.\n"; if(count==0 cout << "No data processed.\n"; else { cout << "Items read: " << count << endl; cout << "Sum: " << sum << endl; cout << "Average: " << sum / count << endl; } inFile.close(); return 0; }
读取文件时,有几点需要检查。首先,程序读取文件时不应超过
EOF
。如果最后一次读取数据时遇到EOF
,方法eof()
将返回true
。其次,程序可能遇到类型不匹配的情况(如给int
类型变量输入字符),在这种情况下,方法fail()
将返回true
(如果遇到了EOF
,该方法也将返回true
)。最后,可能出现意外问题,如文件受损或硬件故障。如果最后一次读取文件发生了这样的问题,方法bad()
将返回true
。不要分别检查这些情况,一种更简单的方法是使用
good()
方法,该方法在没有发生任何错误时返回true
。然后,当循环终止时,再分别检查原因,正如程序逻辑展示的那样。 - 必须包含头文件
-
4.3 函数
4.3.1 函数与 string
对象
可以像字符串一样将 string
对象作为函数的参数。但与字符串传的是地址不同,string
对象是按值传递,也就是说在函数内部修改将不会改变 string
对象,例如:
#include <iostream>
#include <string>
using namespace std;
void f(string s1, char* s2)
{
s1[0] = '1';
s2[0] = '1';
}
int main()
{
string s1 = "abc";
char s2[] = "abc";
f(s1, s2);
cout << "s1: " << s1 << endl;
cout << "s2: " << s2 << endl;
}
在上面的程序中,字符串 s1
将不会被修改,而字符串 s2
将被修改为 1bc
。
4.3.2 函数与 array
对象
传递 array
作为函数参数也和传递数组不一样,在函数内部修改前者将不起作用。如果想要在函数内部修改 array
对象,需要将该对象的地址传递给函数。例如:
std::array<double, 4> expenses;
//展示数组
void show(std::array<double,4> da);
//修改数组
void fill(std::array<double,4> *pa);
上述传递方法都有缺点。函数 show()
按值传递数组,效率太低;函数 fill()
按址传递数组,操作不便。下一章讨论的按引用传递可以解决这两个问题。
4.3.3 函数指针
-
函数的地址
获取函数的地址很简单:函数名就是函数的地址。例如,
think()
函数的地址就是think
。 -
声明函数指针
通常要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用
(*pf)
替换函数名。这样,pf
就是这类函数的指针。如:double pam (int); //指针如下 double (*pf) (int) = pam;
-
使用指针来调用参数
使用指针参数有两种方式:
//case 1 double x = (*pf)(1); //case 2 double x = pf(2);
解引用函数指针或者直接使用函数指针都可以作为调用函数的方式。
4.3.4 函数指针数组
考虑下面三个函数:
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);
这三个函数的形参列表看似不同,实则相同。接下来,假设要声明一个指针 p1
,指向这三个函数之一,则只需要将目标函数的函数名换成 (*p1)
:
const double * (*p1)(const double *, int) = f1;
可以使用 auto
自动类型推断,简化代码:
auto p2 = f2;
还可以使用 typedef
进行简化:
typedef const double *(*p_fun)(const double*, int);
p_fun = f1;
如果需要使用这三个函数,则用函数指针数组将更方便:
const double * (*pa[3])(const double *, int) = {f1,f2,f3};
p_fun pa[3] = {f1,f2,f3};
这里不能使用 auto
。自动类型推断只能用于单值初始化,而不能用于初始化列表。
如果想要声明一个指向函数指针数组的指针,应该如何创建呢?显然,应该在某个地方添加一个 *
。具体地说,如果这个指针名为 pd
,则需要指出它是一个指针,而不是一个数组。这意味着声明的核心部分应该为:(*pd)
,指向的类型是包含三个函数指针的数组,最终声明如下:
//pd指针指向的类型如下
const double * (*()[3])(const double *, int);
//最终声明
const double * (*(*pd)[3])(const double *, int) = &pa;
//typedef
p_fun (*pd)[3] = &pa;