Kodewick
第 15 课,共 30 课目录

类和对象

第 15 课,共 30 课 · 第 4 部分 · 类 · 约 15 分钟

第 14 课用 struct 把几样东西放在一起。它很方便,可也有个问题:谁都能直接改里面的成员。一个银行账户,余额随手就能改成负数:

main.cpp
1#include <iostream>2#include <string>3 4struct Account {5    std::string owner;6    int balance;7};8 9int main() {10    Account acc{"小林", 100};11    acc.balance = -5000;12    std::cout << acc.owner << " 的余额:" << acc.balance << '\n';13    return 0;14}
输出
小林 的余额:-5000

编译器不拦着,程序也照常跑完。这一课讲 class:把数据藏起来,只让外面通过几个函数用它,规矩由这个类型自己守着。

成员函数

struct 和 class 里不光能放变量,还能放函数。写在里面的函数叫成员函数,它直接用得到同一个对象的成员变量:

main.cpp
1#include <iostream>2 3struct Counter {4    int count = 0;5 6    void add() {7        count++;8    }9 10    void show() {11        std::cout << "现在是 " << count << '\n';12    }13};14 15int main() {16    Counter people;17    Counter cars;18    people.add();19    people.add();20    cars.add();21    people.show();22    cars.show();23    return 0;24}
输出
现在是 2
现在是 1

int count = 0; 给成员一个起始值。Counter 是类型,people 和 cars 是这个类型的两个对象,各有各的 count。调用成员函数也用 .:people.add() 加的是 people 的 count,和 cars 没关系。点单步,看画面上两个对象各自的字段怎么变。

class 和 private

把 struct 换成 class,再用 public: 和 private: 分开:public: 下面的谁都能用,private: 下面的只有这个类自己的成员函数能用。

main.cpp
1#include <iostream>2 3class Account {4public:5    void deposit(int amount) {6        if (amount > 0) {7            balance_ += amount;8        }9    }10 11    int balance() {12        return balance_;13    }14 15private:16    int balance_ = 0;17};18 19int main() {20    Account acc;21    acc.deposit(300);22    acc.deposit(-50);23    std::cout << "余额:" << acc.balance() << '\n';24    return 0;25}
输出
余额:300

余额 balance_ 是私有的,外面只能通过 deposit 存钱,deposit 又不收负数,所以 -50 没存进去。这门课给私有的成员变量名后面加一个下划线(balance_),一眼就能看出哪些是成员;读余额的函数就可以叫 balance(),不会撞名。

外面硬要直接改,就是编译错误:

main.cpp
1#include <iostream>2 3class Account {4public:5    int balance() {6        return balance_;7    }8 9private:10    int balance_ = 0;11};12 13int main() {14    Account acc;15    acc.balance_ = -5000;16    std::cout << acc.balance() << '\n';17    return 0;18}
输出
main.cpp: In function 'int main()':
main.cpp:15:9: error: 'int Account::balance_' is private within this context
   15 |     acc.balance_ = -5000;
      |         ^~~~~~~~
main.cpp:10:9: note: declared private here
   10 |     int balance_ = 0;
      |         ^~~~~~~~

先看第一个 error::'int Account::balance_' is private within this context,balance_ 是私有的,在这里用不了。g++ 下面还会用 note: 指出它是在哪一行声明的。

为什么要 private

把数据藏起来,不是为了让人用着麻烦,而是让规矩只写在一个地方。账户的规矩是「余额不能变成负数」,那么取钱的检查就写在 withdraw 里,外面谁也绕不过去:

main.cpp
1#include <iostream>2 3class Account {4public:5    void deposit(int amount) {6        if (amount > 0) {7            balance_ += amount;8        }9    }10 11    bool withdraw(int amount) {12        if (amount <= 0 || amount > balance_) {13            return false;14        }15        balance_ -= amount;16        return true;17    }18 19    int balance() {20        return balance_;21    }22 23private:24    int balance_ = 0;25};26 27int main() {28    Account acc;29    acc.deposit(100);30    std::cout << acc.withdraw(30) << '\n';31    std::cout << acc.withdraw(500) << '\n';32    std::cout << "余额:" << acc.balance() << '\n';33    return 0;34}
输出
1
0
余额:70

取 30 成功(显示 1),取 500 不够(显示 0),余额还是 70。以后哪里出了负数,只要查这个类就行。

还有一个好处:外面只认得 deposit、withdraw、balance 这几个函数。哪天想把余额改成按「分」存,或者加一条流水记录,只改类里面,用它的代码一行都不用动。

先猜一猜

这段程序显示什么?

1#include <iostream>2 3class Light {4public:5    void toggle() {6        on_ = !on_;7        if (on_) {8            times_++;9        }10    }11 12    int times() {13        return times_;14    }15 16private:17    bool on_ = false;18    int times_ = 0;19};20 21int main() {22    Light lamp;23    lamp.toggle();24    lamp.toggle();25    lamp.toggle();26    std::cout << lamp.times() << '\n';27    return 0;28}

const 成员函数

第 10 课说过,大的东西只看不改时传 const 引用。可是把 Account 用 const Account& 传进函数,调用 balance() 却编译不过:

main.cpp
1#include <iostream>2 3class Account {4public:5    int balance() {6        return balance_;7    }8 9private:10    int balance_ = 70;11};12 13void report(const Account& acc) {14    std::cout << "余额:" << acc.balance() << '\n';15}16 17int main() {18    Account acc;19    report(acc);20    return 0;21}
输出
main.cpp: In function 'void report(const Account&)':
main.cpp:14:41: error: passing 'const Account' as 'this' argument discards qualifiers [-fpermissive]
   14 |     std::cout << "余额:" << acc.balance() << '\n';
      |                              ~~~~~~~~~~~^~

报错说 passing 'const Account' as 'this' argument discards qualifiers:acc 是 const 的,可 balance() 没有保证自己不改对象,编译器不敢调用它。(this 是指向对象自己的指针,第 19 课讲指针时再说。)

办法是在只看不改的成员函数的括号后面加 const:

main.cpp
1#include <iostream>2 3class Account {4public:5    int balance() const {6        return balance_;7    }8 9private:10    int balance_ = 70;11};12 13void report(const Account& acc) {14    std::cout << "余额:" << acc.balance() << '\n';15}16 17int main() {18    Account acc;19    report(acc);20    return 0;21}
输出
余额:70

int balance() const 说的是:这个函数不改对象。要是在里面改了 balance_,就是编译错误。只看不改的成员函数(读数、显示、计算)都加上 const,养成习惯。

一个温度类

再看一个完整的例子。温度不能低于绝对零度 −273.15 °C,set_celsius 守着这条规矩;华氏度不存,要的时候现算:

main.cpp
1#include <iostream>2 3class Temperature {4public:5    bool set_celsius(double c) {6        if (c < -273.15) {7            return false;8        }9        celsius_ = c;10        return true;11    }12 13    double celsius() const {14        return celsius_;15    }16 17    double fahrenheit() const {18        return celsius_ * 9 / 5 + 32;19    }20 21private:22    double celsius_ = 0;23};24 25int main() {26    Temperature t;27    t.set_celsius(25);28    t.set_celsius(-300);29    std::cout << t.celsius() << " °C = " << t.fahrenheit() << " °F\n";30    return 0;31}
输出
25 °C = 77 °F

-300 被拒绝了,温度还是 25 度。现在每个对象一开始都是 0 度,要另外调用 set_celsius 才有值;下一课的构造函数让对象一出生就带上该有的值。

struct 和 class 的区别

在 C++ 里,struct 和 class 几乎一样,struct 也能有成员函数、private:。只差一点:**没写 public: 或 private: 时**,struct 的成员默认是公开的,class 的默认是私有的。

先猜一猜

这段程序会怎样?

1#include <iostream>2 3class Point {4    int x = 3;5};6 7int main() {8    Point p;9    std::cout << p.x << '\n';10    return 0;11}

习惯上:只是把几样数据放在一起、没什么规矩要守的,用 struct(像第 14 课的 Item);有规矩要守、数据要藏起来的,用 class,先写 public: 部分,再写 private: 部分。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

修好它:只看不改要加 const

程序编译不过。只改 Account 类,让它显示 余额:250。

要显示
余额:250
exercise1.cpp
1#include <iostream>2 3class Account {4public:5    void deposit(int amount) {6        balance_ += amount;7    }8 9    int balance() {10        return balance_;11    }12 13private:14    int balance_ = 0;15};16 17void report(const Account& acc) {18    std::cout << "余额:" << acc.balance() << '\n';19}20 21int main() {22    Account acc;23    acc.deposit(250);24    report(acc);25    return 0;26}
输出
点「运行」或按 Ctrl+Enter
练习 2

计步器

写完 StepCounter:add(steps) 把步数加上去,但负数不算;total() 返回一共走了多少步。程序要显示 今天走了 9000 步。

要显示
今天走了 9000 步
exercise2.cpp
1#include <iostream>2 3class StepCounter {4public:5 6private:7    int total_ = 0;8};9 10int main() {11    StepCounter today;12    today.add(3500);13    today.add(-200);14    today.add(5500);15    std::cout << "今天走了 " << today.total() << " 步\n";16    return 0;17}
输出
点「运行」或按 Ctrl+Enter
练习 3

歌单有多长

Playlist 用 vector 存每首歌的秒数。给它加一个 const 成员函数 total_minutes(),返回一共多少分钟(整数除法,不足一分钟的不算)。程序要显示 3 首,11 分钟。

要显示
3 首,11 分钟
exercise3.cpp
1#include <iostream>2#include <vector>3 4class Playlist {5public:6    void add(int seconds) {7        songs_.push_back(seconds);8    }9 10    int count() const {11        return songs_.size();12    }13 14private:15    std::vector<int> songs_;16};17 18int main() {19    Playlist p;20    p.add(215);21    p.add(250);22    p.add(240);23    std::cout << p.count() << " 首," << p.total_minutes() << " 分钟\n";24    return 0;25}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 写在 struct / class 里的函数是成员函数,用 对象.函数() 调用,直接用得到这个对象的成员变量;每个对象有自己的一份成员变量。
  • public: 下面的谁都能用,private: 下面的只有类自己的成员函数能用;外面用了私有成员是编译错误(is private within this context)。
  • 把数据设成 private,改它只能通过成员函数,规矩(余额不为负、温度不低于绝对零度)就只写在一个地方。
  • 只看不改的成员函数在括号后面加 const;const 对象和 const 引用只能调用这样的函数。
  • struct 和 class 只差默认的访问权限:struct 默认公开,class 默认私有。
  • 这门课给私有成员变量名加下划线:balance_。