[C++学习笔记] 第 4 章 编程基础

第 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;
}
  1. 上述程序使用了 cin.get(ch) 函数,至此我们已经学过三个版本的 cin.get()

    istream & cin.get(char*, int);
    char cin.get(void);
    istream & cin.get(char&);
    
  2. cin 读取到 EOF 时,cin 会将两位 eofbit、failbit 都设置为 1 1 1,此时可以通过以下两个函数进行检测:

    1. eof()

      成员函数 eof() 可以查看 eofbit 是否被设置;如果检测到 EOF,则 cin.eof() 将返回 bool 类型值 true,否则返回 false

    2. fail()(用途较前者更广)

      如果 eofbitfailbit 被设置为 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))

  1. C++ 中的 getchar()putchar()

    熟悉 C 的用户可能更喜欢 getchar()putchar(),它们在 C++ 中仍然适用。也可以使用 C++ 中 istreamostream 类中类似功能的成员函数:

    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、ornot 都是 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 简单文件输入/输出
  1. 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 将不断读取,直到遇到换行符,随后换行符会被丢弃,下一次输入将从下一行开始。

  2. 使用文件输入/输出

    在 C 语言中,常见的文件操作为 FILE* fp = fopen() 等,在 C++ 中,更强调用的概念。下面分文件输入、输出讨论 C++ 的这种特性。

    • 文件输出

      文件输出需要按照以下几个操作:

      1. 必须包含头文件 #include <fstream>,该头文件定义了用于输出的 ofstream
      2. 需要声明 ofstream 对象
      3. 需要将 ofstream 对象与文件关联起来。方法之一是使用 open() 方法
      4. 像使用 cout 那样使用该 ofstream 对象
      5. 使用完文件之后,应使用 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;
      }
      
    • 文件输入

      文件输入的基本操作和文件输出类似:

      1. 必须包含头文件 #include <fstream>,该头文件定义了用于输入的 ifstream
      2. 需要声明 ifstream 对象
      3. 需要将 ifstream 对象与文件关联起来。方法之一是使用 open() 方法
      4. 像使用 cin 那样使用该 ifstream 对象
      5. 使用完文件之后,应使用 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 函数指针
  1. 函数的地址

    获取函数的地址很简单:函数名就是函数的地址。例如,think() 函数的地址就是 think

  2. 声明函数指针

    通常要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用 (*pf) 替换函数名。这样,pf 就是这类函数的指针。如:

    double pam (int);
    //指针如下
    double (*pf) (int) = pam;
    
  3. 使用指针来调用参数

    使用指针参数有两种方式:

    //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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值