前端从零学C++(一):写一个书店程序

259 阅读15分钟

一、背景

我是一个 C/C++ 都没实战经验的前端开发,C 在几年前有了解过一些语法知识,下面我按照《C++ Primer第五版》第一章节,跟着一起实现一个书店程序。

注:本文要求至少有一门编程语言的基础。

我的电脑环境:

  • mac
  • vscode 1.88.1

二、编写一个简单的C++程序

C++ 必须通过调用一个 main 函数来运行,下面是一个最简单的 main 函数:

int main() {
    return 0;
}

1. vscode 编译、运行程序

  1. 检查是否有 g++、clang++ 编译器,没有请自行安装
g++ --version
clang++ --version

2. vscode 下载 c/c++ 插件和 c/c++ extension pack 插件、code runner 插件、vscode-icons-mac 插件 3. 创建后缀为 .cpp 的文件在 vscode 打开 4. vscode 调试运行,点击右侧的调试按钮点击运行和调试箭头,选择 run code

练习 1.1:在vscode运行图示c++程序。

image.png

练习 1.2:将练习1的返回值改成-1,看下c++如何处理错误返回

image.png

三、初识输入输出

c++ 使用 iostream 库提供标准输入输出对象有以下 4 个:

  1. cin (发音 see-in):标准输入
  2. cout (发音 see-out):标准输出
  3. cerr (发音 see-err):输出警告和错误日志
  4. clog (发音 see-log):输出日志

练习1.3:写一个输出 hello world 的程序:

image.png

std::cout << "Hello, World!" << std::endl; 这段代码是用于输出 “Hello, World!” 到标准输出(通常是屏幕),并在末尾添加换行符。让我来解释一下每个部分的含义:

  • std::cout: 是 C++ 标准库中用于输出的流对象,cout 是 “character output” 的缩写。
  • <<: 是 C++ 中的输出操作符,用于将数据插入到输出流中。
  • "Hello, World!": 是要输出的字符串常量,它将被插入到 cout 流中。
  • << std::endl;: 是另一个输出操作符,endl 是C++标准库提供的换行符和刷新缓冲区的方法。

因此,整个表达式 std::cout << "Hello, World!" << std::endl; 的作用是将 “Hello, World!” 这个字符串输出到屏幕,并换行,确保输出的内容立即显示在屏幕上。

练习1.4:使用加法运算符+将2个数相加

image.png

#include <iostream> // 使用 iostream 库
int main() {
    std::cout << "Enter two numbers:" << std::endl; // 输出"Enter two numbers:"
    int v1 = 0, v2 = 0; // 定义变量 v1、v2
    std::cin >> v1 >> v2; // 输入变量 v1、v2
    std::cout << "The sum of " << v1 << " and " << v2 << " is " << v1 + v2 << std::endl; // 打印 v1+v2 的值
    return 0; // 返回0
}

注意:如果 vscode 终端无法输入,请修改 vscode 配置:

image.png

练习1.5:重写练习 1.4,将每个运算对象的打印操作放在一个独立的语句中

image.png

#include <iostream>  // 引入输入输出流库

int main() {
    // 输出提示信息,要求用户输入两个数字
    std::cout << "Enter two numbers:" << std::endl;

    // 定义两个整数变量 v1 和 v2,并初始化为 0
    int v1 = 0, v2 = 0;

    // 从用户输入读取两个整数值
    std::cin >> v1 >> v2;

    // 输出提示信息
    std::cout << "The product of ";
    std::cout << v1;
    std::cout << " and ";
    std::cout << v2;
    std::cout << " is ";

    // 计算并输出两个数字的和(应该是乘积的计算错误)
    std::cout << v1 + v2;

    // 输出换行符
    std::cout << std::endl;

    return 0;  // 返回 0 表示程序正常结束
} 

四、注释简介

// 单行注释
/* 
 * 多行注释
 * 多行注释
 */

练习1.6:解释下面程序片段是否合法。

std::cout << "The sum of " << v1;
          << " and " << v2;
          << " is " << v1 + v2 << std::endl;

解:程序不合法,第二行有多余的分号,修改如下:

std::cout << "The sum of " << v1
          << " and " << v2
          << " is " << v1 + v2 << std::endl;

练习1.7:编译一个包含不正确的嵌套注释的程序,观察编译器返回的错误信息。

/* 正常注释 /* 嵌套注释 */ 正常注释*/

输出:

1-7.cpp:1:17: warning: '/*' within block comment [-Wcomment]
/* 正常注释 /* 嵌套注释 */ 正常注释*/
            ^
1-7.cpp:1:36: error: unknown type name '正常注释'
/* 正常注释 /* 嵌套注释 */ 正常注释*/
                           ^
1-7.cpp:1:49: error: expected unqualified-id
/* 正常注释 /* 嵌套注释 */ 正常注释*/
                                    ^
1 warning and 2 errors generated.

练习1.8:指出下列哪些输出语句是合法的(如果有的话):

std::cout << "/*";
std::cout << "*/";
std::cout << /* "*/" */;
std::cout << /* "*/" /* "/*" */;

第三行不合法,应该改成 std::cout << /* "*/ " */";

五、控制流

1. while 循环

while 语句反复执行一段代码,直到给定条件为假为止。

求 1 到 10 这 10 个数之和:

#include <iostream>
int main() {
    int sum=0,val=1;
    // 只要val的值小于等于10,while循环就会持续进行
    while (val <= 10) {
        sum += val; // 将sum + val赋予sum
        ++val; // 将val加1
    }
    std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
    return 0;
}

练习1.9:将 50-100 的整数相加

#include <iostream>
int main() {
    int sum=0,val=50;
    // 只要val的值小于等于10,while循环就会持续进行
    while (val <= 100) {
        sum += val; // 将sum + val赋予sum
        ++val; // 将val加1
    }
    std::cout << "Sum of 50 to 100 inclusive is " << sum << std::endl;
    return 0;
}

练习1.10:使用递减运算符在循环中按递减顺序打印出 10 到 0 之间的整数。

#include <iostream>

int main(){
    int val = 10;
    while (val >= 0)
    {
        std::cout << val << " "; // 输出当前值和一个空格
        --val;
    }
    std::cout << std::endl; // 输出一个换行符 
} 

练习1.11:提示用户输入两个整数,打印出这两个整数所指定的范围内的所有整数。

#include <iostream>  // 引入C++标准库中的输入输出流

int main () {
    // 输出提示信息,要求用户输入两个数字
    std::cout << "Enter two numbers:" << std::endl;
    
    // 定义两个整数变量v1和v2,并初始化为0
    int v1 = 0, v2 = 0;
    
    // 从标准输入读取用户输入的两个整数值
    std::cin >> v1 >> v2;
    
    // 当v1小于等于v2时,执行循环体内的语句
    while(v1 <= v2) {
        // 输出当前的v1值
        std::cout << v1 << " ";
        
        // 将v1的值增加1
        ++v1;
    }
    
    // 输出换行符,使输出结果更加整洁
    std::cout << std::endl;
    
    // 程序正常结束,返回0
    return 0;
}

2. for 循环

练习1.12:下面的 for 循环完成了什么功能?sum 的终值是多少?

int sum = 0;
for (int i = -100; i <= 100; ++i)
	sum += i;

解:从 -100 加到 100,sum 的终值是 0。

练习1.13:使用 for 循环重做练习 1.10

#include <iostream>  // 引入C++标准库中的输入输出流

int main () {
    // 提示用户输入两个整数
    std::cout << "Please input two integers: ";
    
    // 定义两个整数变量a和b
    int a, b;
    
    // 从标准输入读取用户输入的两个整数值
    std::cin >> a >> b;
    
    // 使用for循环从a开始迭代到b(包括b),每次循环输出一个整数
    for (int i = a; i <= b; ++i) {
        std::cout << i << " ";  // 输出当前整数值,后跟一个空格
    }
    
    // 输出换行符,使输出结果更加整洁
    std::cout << std::endl;
    
    // 程序正常结束,返回0
    return 0;
}

练习1.14:对比 for 循环和 while 循环,两种形式的优缺点各是什么?

while 循环适用于条件判断,for 循环更加通用。

练习1.15:编译器可以检查出的错误有哪些?

编译器可以检查出的错误有:

  • 语法错误
  • 类型错误
  • 声明错误

3. 读取数量不定的输入数据

练习1.16:编写程序,从 cin 读取一组数,输出其和。

#include <iostream>  // 引入C++标准库中的输入输出流

int main()
{
    // 定义一个整数变量sum,用于存储输入数字的总和,初始化为0
    int sum = 0;

    // 使用for循环读取输入的整数值,并累加到sum中
    // 循环的条件是std::cin >> value的结果,表示只要有有效输入,循环就会继续
    for (int value = 0; std::cin >> value; )
        sum += value;  // 将读取的值累加到sum中

    // 输出累加结果
    std::cout << sum << std::endl;

    // 程序正常结束,返回0
    return 0;
}

4. if 语句

用 if 语句写 1 个程序,统计在输入中每个值连续出现了多少次。

#include <iostream>
int main () {
    // currVal 是我们正在统计的数,val 是当前输入的数
    int currVal = 0, val = 0;
    if (std::cin >> currVal) {
        // 如果输入了第一个数,那么就开始统计
        int cnt = 1;
        // 读取剩余的数
        while (std::cin >> val) {
            // 如果当前数和我们正在统计的数相同,那么计数器加一
            if (val == currVal) {
                ++cnt;
            } else {
                // 否则输出当前数的统计结果,然后更新 currVal 和 cnt
                std::cout << currVal << " occurs " << cnt << " times" << std::endl;
                currVal = val;
                cnt = 1;
            }
        }
        // 输出最后一个数的统计结果
        std::cout << currVal << " occurs " << cnt << " times" << std::endl;
    }
    return 0;
}

练习1.17:如果输入的所有值都是相等的,本节的程序会输出什么?如果没有重复值,输出又会是怎样的?

解:

  • 所有值相等:程序会输出一个行,显示所有输入的值及其总出现次数。
  • 没有重复值:程序会为每个输入值输出一行,显示该值及其出现次数为 1。

练习1.18:编译并运行本节的程序,给它输入全都相等的值。再次运行程序,输入没有重复的值。

解:

全部重复:

1 1 1 1 1 
1 occurs 5 times 

没有重复:

1 2 3 4 5
1 occurs 1 times 
2 occurs 1 times 
3 occurs 1 times 
4 occurs 1 times 
5 occurs 1 times 

练习1.19:修改练习 1.11 所编写的程序(打印一个范围内的数),使其能处理用户输入的第一个数比第二个数小的情况。

解:

#include <iostream>  // 引入C++标准库中的输入输出流

int main()
{
    // 定义两个整数变量 start 和 end,用于存储输入的范围
    int start = 0, end = 0;
    
    // 提示用户输入两个整数
    std::cout << "Please input two num: ";
    
    // 从标准输入读取两个整数,分别赋值给 start 和 end
    std::cin >> start >> end;
    
    // 检查 start 是否小于或等于 end
    if (start <= end) {
        // 如果 start 小于或等于 end,使用 while 循环输出从 start 到 end 的所有整数
        while (start <= end) {
            std::cout << start << " ";  // 输出当前整数值,后跟一个空格
            ++start;  // 将 start 递增 1
        }
        // 输出换行符,以确保输出结果整洁
        std::cout << std::endl;
    }
    else {
        // 如果 start 大于 end,输出错误信息
        std::cout << "start should be smaller than end !!!";
    }

    return 0;  // 程序正常结束,返回0
}

六、书店程序

在 C++ 中,通过定义一个类来定义自己的数据结构。 我们需要使用头文件来访问类,通常使用 .h 作为头文件的后缀。

书店程序就用 Sales_item 类实现,Sales_item 类表示一本书的总销售额、售出册数和平均售价。

Sales_item 类应该有以下功能:

  • 调用一个名为 isbn 的函数从一个 sales_item 对象中提取 ISBN 书号
  • 用输入运算符 (>>) 和输出还算符 (<<) 读、写 sales_item 类型的对象算符
  • 将一个 Sales_item 对象的值赋子另一个 sales_item 对象。
  • 用加法运算符 (+) 将两个 Sales_item 对象相加。两个对象必须表示同一本书(相 同的 ISBN)。加法结果是一个新的 sales_item 对象,其 ISBN 与两个运算对象相同,而其总销售额和售出册数则是两个运算对象的对应值之和。
  • 使用复合赋值运算符 (+=) 将一个 Sales_item 对象加到另一个对象上

1. Sales_item 类

本例子已经提供了一个书店头文件 Sales_item.h头文件,让 Sales_item 类有了以上功能:

#ifndef SALESITEM_H
// 我们在这里只有当 SALESITEM_H 尚未被定义时
#define SALESITEM_H

// Sales_item 类及相关函数的定义在这里
#include <iostream>
#include <string>

class Sales_item {
    // 这些声明在第7.2.1节第270页和第14章第557, 558, 561页中解释
    friend std::istream& operator>>(std::istream&, Sales_item&);
    friend std::ostream& operator<<(std::ostream&, const Sales_item&);
    friend bool operator<(const Sales_item&, const Sales_item&);
    friend bool operator==(const Sales_item&, const Sales_item&);
public:
    // 构造函数在第7.1.4节第262 - 265页中解释
    // 默认构造函数用于初始化内置类型的成员
    Sales_item(): units_sold(0), revenue(0.0) { }
    Sales_item(const std::string &book): 
                  bookNo(book), units_sold(0), revenue(0.0) { }
    Sales_item(std::istream &is) { is >> *this; }
public:
    // 对 Sales_item 对象的操作
    // 成员二元运算符:左操作数绑定到隐式 this 指针
    Sales_item& operator+=(const Sales_item&);
    
    // 对 Sales_item 对象的操作
    std::string isbn() const { return bookNo; }
    double avg_price() const; // 计算平均价格的函数
private:
    std::string bookNo;      // 隐式初始化为空字符串
    unsigned units_sold;
    double revenue;
};

// 用于第10章
inline
bool compareIsbn(const Sales_item &lhs, const Sales_item &rhs) 
{ return lhs.isbn() == rhs.isbn(); }

// 非成员二元运算符:必须声明每个操作数的参数
Sales_item operator+(const Sales_item&, const Sales_item&);

inline bool 
operator==(const Sales_item &lhs, const Sales_item &rhs)
{
    // 必须成为 Sales_item 的友元
    return lhs.units_sold == rhs.units_sold &&
           lhs.revenue == rhs.revenue &&
           lhs.isbn() == rhs.isbn();
}

inline bool 
operator!=(const Sales_item &lhs, const Sales_item &rhs)
{
    return !(lhs == rhs); // != 通过 == 定义
}

// 假设两个对象引用相同的 ISBN
Sales_item& Sales_item::operator+=(const Sales_item& rhs) 
{
    units_sold += rhs.units_sold; 
    revenue += rhs.revenue; 
    return *this;
}

// 假设两个对象引用相同的 ISBN
Sales_item 
operator+(const Sales_item& lhs, const Sales_item& rhs) 
{
    Sales_item ret(lhs);  // 将 (lhs) 复制到一个本地对象中
    ret += rhs;           // 将 (rhs) 的内容添加到 (ret) 中
    return ret;           // 以值的形式返回 (ret)
}

std::istream& 
operator>>(std::istream& in, Sales_item& s)
{
    double price;
    in >> s.bookNo >> s.units_sold >> price;
    // 检查输入是否成功
    if (in)
        s.revenue = s.units_sold * price;
    else 
        s = Sales_item();  // 输入失败:将对象重置为默认状态
    return in;
}

std::ostream& 
operator<<(std::ostream& out, const Sales_item& s)
{
    out << s.isbn() << " " << s.units_sold << " "
        << s.revenue << " " << s.avg_price();
    return out;
}

double Sales_item::avg_price() const
{
    if (units_sold) 
        return revenue/units_sold; 
    else 
        return 0;
}
#endif

2. 读写类

一个简单的读写类的例子:

#include <iostream>
#include "Sales_item.h"

int main () {
    // 定义一个Sales_item对象1
    Sales_item book;
    // 读取一个ISBN号、售出的册数以及销售价格
    std::cin >> book;
    // 输出ISBN号、售出的册数、总销售额和平均价格
    std::cout << book << std::endl;
    return 0;
}

练习1.20:读取一组书籍销售记录,将每条记录打印到标准输出上

#include <iostream>
#include "Sales_item.h"

int main()
{
	for (Sales_item item; std::cin >> item; std::cout << item << std::endl);
	return 0;
}

3. Sales_item 对象加法

练习1.21

编写程序,读取两个 ISBN 相同的 Sales_item 对象,输出他们的和。

#include <iostream> 
#include "Sales_item.h"

int main()
{
    Sales_item item_1;
    Sales_item item_2;
    std::cin >> item_1;  // 从标准输入读取第一个 Sales_item 对象的数据
    std::cin >> item_2;  // 从标准输入读取第二个 Sales_item 对象的数据
    // 输出两个 Sales_item 对象的总销售情况
    std::cout << "Sum of sale items: " << item_1 + item_2 << std::endl;
    return 0;  // 程序正常结束
}

练习1.22

编写程序,读取多个具有相同 ISBN 的销售记录,输出所有记录的和。

#include <iostream>
#include "Sales_item.h"

int main()
{
    Sales_item sum_item;  // 创建一个 Sales_item 对象,用于存储所有输入的销售项的总和
    
    // 从标准输入读取一个 Sales_item 对象的数据,并存储在 sum_item 中
    std::cin >> sum_item;

    // 输出第一个 Sales_item 对象的数据
    std::cout << "First sale item: " << sum_item << std::endl;
    
    // 处理输入的其他 Sales_item 对象
    // 使用循环来读取并累加销售项,直到输入流结束
    for (Sales_item item; std::cin >> item; /* 空循环体 */)
    {
        // 输出当前读取的 Sales_item 对象的数据
        std::cout << item << std::endl;
        
        // 将当前 Sales_item 对象的值加到 sum_item 中
        sum_item += item;
    }

    // 输出所有销售项的总和
    std::cout << "Sum of sale items: " << sum_item << std::endl;
    
    return 0;  // 程序正常结束
}

练习1.23:编写程序,读取多条销售记录,并统计每个 ISBN(每本书)有几条销售记录。

练习1.24:输入表示多个 ISBN 的多条销售记录来测试上一个程序,每个 ISBN 的记录应该聚在一起。

#include <iostream>
#include "Sales_item.h"
int main()
{
    Sales_item total;  // 创建一个 Sales_item 对象 `total` 用于存储累加后的销售项数据

    // 尝试从标准输入读取第一个 Sales_item 对象的数据
    if (std::cin >> total) {
        Sales_item trans;  // 创建一个 Sales_item 对象 `trans` 用于临时存储从输入中读取的销售项

        // 使用 while 循环继续读取后续的 Sales_item 对象数据,直到输入流结束
        while (std::cin >> trans) {
            // 如果当前读取的销售项与 `total` 的 ISBN 相同,则累加 `trans` 到 `total`
            if (total.isbn() == trans.isbn()) {
                total += trans;  // 累加当前销售项的数据到 `total`
            }
            else {
                // 如果 ISBN 不同,则输出当前的 `total`,然后将 `total` 更新为新的销售项
                std::cout << total << std::endl;  // 输出当前累加结果
                total = trans;  // 将 `total` 更新为当前销售项 `trans`
            }
        }
        // 循环结束后,输出最后一个累加的销售项数据
        std::cout << total << std::endl;
    }
    else {
        // 如果从输入流中无法读取数据,输出错误信息
        std::cerr << "No data?!" << std::endl;  // 使用 std::cerr 输出错误信息到标准错误流
        return -1;  // 返回 -1 表示程序执行失败
    }

    return 0;  // 返回 0 表示程序正常结束
}

七、小结

这章内容足够让你掌握 C++ 的基础,能编译和运行一些简单的程序。

我们学会了怎么定义 main 函数,它就是操作系统运行你程序的入口。

接着,我们还了解了怎么定义变量,如何进行输入和输出操作,还学习了如何使用 iffor 和 while 这些控制语句。

之后,我们还介绍了C++的一个核心特性——类。你会学到怎么创建和使用别人定义的类的对象。简单来说,就是怎么用这些现成的类来做各种事情。

接下来的章节,我们会深入讲解怎么自己定义类,让你能够根据自己的需求创建各种功能。

参考资料