C++ Primer Plus(第6版)读书笔记【4】

第10章 对象和类

类的核心概念

简单来说,C 语言关注的是步骤(先做什么,后做什么),而 C++ 的 OOP 关注的是谁来做(把程序看成一堆互相配合的“物体”)。

可以用开发一款“赛车游戏”的例子解释c++的最重要的OOP特性:

  1. 抽象 (Abstraction),大白话:抓住重点,忽略不重要的细节。

游戏例子:在游戏里写一辆“赛车”的代码时,我们只需要记录它的速度、颜色和位置。至于轮胎是什么牌子、发动机里有几个螺丝钉,这些细节对游戏没影响,我们就不用写进代码里。这就是抽象。

  1. 封装和数据隐藏 (Encapsulation & Data Hiding),大白话:把数据和外壳打包在一起,不让外面的人瞎动。

游戏例子:赛车的“当前速度”是一个敏感数据。我们用一个外壳(类)把速度保护起来,设置成私有(Private)。玩家不能直接在代码里把速度改成 9999,必须通过点击“踩油门”这个公有(Public)按钮,赛车才会自己慢慢加满速度。这保护了程序不崩溃。

  1. 继承 (Inheritance),大白话:儿子可以直接用爸爸的财产,不用从头开始赚。

游戏例子:你已经写好了一个基础的“赛车类”(有速度、能拐弯)。现在你想加一辆“导弹赛车”。你不需要重新写一遍速度和拐弯的代码,只需要让“导弹赛车”继承基础赛车。新赛车自动就有了基础赛车的所有功能,你只需要在它身上多写一个“发射导弹”的功能就行了。

  1. 多态 (Polymorphism),大白话:同一个命令,不同的对象收到后做出不同的反应。

游戏例子:游戏里有一个“按下空格键”的命令。如果当前玩家开的是“普通赛车”,按下空格是刹车;如果开的是“重型坦克”,按下空格是开炮。同一个“空格键”命令,表现出了多种不同的形态。

  1. 代码的可重用性 (Reusability),大白话:写好的代码可以像积木一样,到处重复使用,省时省力。

游戏例子:因为你把赛车、导弹、跑道都写成了独立的、封装好的“类(积木块)”。下一个月公司想开发《赛车游戏第二代》或者一款《卡丁车游戏》时,你直接把这套代码拿过去用就行了,不用重新搬砖。

C++ 引入的 “类 (Class)” 就像是一个模具或者设计图。它把上面的抽象和封装结合在一起。有了类,我们就可以轻而易举地在电脑里创造出各种各样的“对象”(比如成千上万辆赛车),让它们在游戏里自己跑起来。

OOP 设计程序的三步走

  1. 第一步:找对象与定接口(用户视角)

怎么做:先不写代码,而是拿张纸画出用户需要什么“物体”(对象),以及用户想怎么作弄它(交互操作)。

例子:要做一个“银行账户”程序。

数据:储户名字、账号、存款余额。

操作(接口):开户、存钱、取钱、查余额。

  1. 第二步:决定内部实现(程序员视角)

怎么做:有了外面的接口,再来想里面怎么存数据、怎么写算法。用户看不到这一步,这就是“封装和数据隐藏”。

例子:数据存储:名字用 std::string 存,余额用 double 存。接口实现:写“取钱”功能时,里面要加一行安全检查——“如果取钱金额大于余额,就报错拒绝”。

  1. 第三步:组装并创建程序(实现方案)

怎么做:把上面两步写成真正的 C++ 类,然后在 main 函数里创建成千上万个对象让它们跑起来。

在 C++ 中,这三步刚好对应了不同的代码位置:

// 1 & 2. 接口描述与数据存储(通常写在头文件 .h 中)
class BankAccount {
private:
    // 决定如何存储数据
    std::string owner;
    double balance;

public:
    // 描述用户如何交互(接口声明)
    void deposit(double money);  // 存钱
    void withdraw(double money); // 取钱
};

// 3. 使用新方案创建程序(在源文件 .cpp 中使用它)
int main() {
    BankAccount my_account; // 创建对象
    my_account.deposit(100.0); // 用户与对象交互
    return 0;
}

“抽象”与“类”的核心概念

  • 生活中减负: 面对复杂系统(如人体、思想),我们忽略底层微观细节(原子、主体),将其视为一个整体(人)。
  • 编程中建模: 抽象就是找出问题的本质特征。通过定义一个明确的接口,规定用户如何与数据进行交互(如:初始化、更新、显示),而隐藏复杂的实现细节。
  • C++中的落地: 类(Class)就是抽象的编程实现。它是一种用户自定义的数据类型,负责把这些抽象出来的特征和接口打包成一个可用的代码实体。

内置类型与用户自定义类型(类)的本质区别

使用 C++ 的 class 定义新类型时,需要自己接管这三项编译器原本自动处理的工作:

  • 内存数量: 由类中的成员变量(Data Members)决定。编译器会计算所有成员变量的总大小(加上内存对齐)来决定该类对象占用多少字节。
  • 解释内存中的位: 由成员变量的数据类型决定。类通过组合不同的基本类型,来告诉编译器如何读取和翻译这块内存。
  • 执行的操作或方法: 由类中的成员函数(Member Functions)和运算符重载(Operator Overloading)决定。必须显式写出哪些函数可以操作这个对象。
核心工作 内置类型 (如 int, double) 用户自定义类型 (如 SoccerTeam 类)
内存大小 编译器硬编码(如 4 或 8 字节) 由类内所有成员变量的大小决定
位解释 编译器自带的整数/浮点数解码规则 依序调用类中各成员变量的解码规则
可用操作 编译器内置的 +, -, *, / 等 你在类中定义的公有函数和重载运算符

这就是 C++ 核心思想的体现:让用户自定义的类,在语法和行为上尽可能接近内置类型。虽然设计时付出了编写代码的劳动,但换来了极强的定制能力。

C++ 工程开发中的经典规范:接口与实现分离

在实际开发中,把类声明(接口)和类方法(实现)分离开来,是管理大型项目、提高编译效率的基础。

C++ 的类声明(通常在 .h 头文件中)结构非常像 struct 结构体,但它引入了面向对象的核心机制:

  • 防重复包含(Header Guards): 使用 #ifndef、#define 和 #endif 确保头文件在同一个编译单元中只被解析一次,避免重定义错误。
  • 访问控制(Access Specifiers): 明确划分了 public(公有部分,即接口)和 private(私有部分,即实现细节)。成员函数(Member Functions): 声明中不仅有数据,还包含了操作这些数据的函数原型。
  • 命名约定: 首字母大写(如 Stock)有助于在代码中快速区分“类名”与“变量/函数名”。

类和结构

class 和 struct 在语法上几乎完全相同,但在使用习惯(语义)上有着巨大的分歧。唯一的语法区别:默认访问权限在 C++ 中,class 和 struct 唯一的硬性区别就是默认的访问控制级别:

  • struct: 默认是 public(公有)。如果你在结构体中定义成员而不写任何标签,外部代码可以直接访问它们。
  • class: 默认是 private(私有)。如果你在类中定义成员而不写任何标签,外部代码无法直接访问,必须通过公有接口。

这同样适用于继承:struct 默认是公有继承(public),而 class 默认是私有继承(private)。

这种划分有助于提高代码的可读性。当你看到 struct 时,你就知道可以直接读写它的字段;当你看到 class 时,你就知道应该去寻找它的公有方法。

类的成员函数

当我们要为类声明中的函数原型编写具体代码时,必须使用作用域解析运算符(::)

  • 成员函数的身份认领: Stock::update 明确告诉编译器,这个 update() 函数不是一个全局函数,而是属于 Stock 类的成员。
  • 避免命名冲突(多态性基础): 不同的类可以拥有同名的成员函数。例如 Stock::update(double price) 和 Buffoon::update() 互不干扰,因为它们处于不同的类作用域中。
  • 特权:访问私有组件(private): 普通的全局函数不能访问 Stock 类的私有成员变量(比如股票价格、持股数量),但只要函数头加上了 Stock::,该函数体内就可以自由、直接地读写该类的所有私有数据。

stock.h

#ifndef CPPCONSOLEBASE_STOCK_H
#define CPPCONSOLEBASE_STOCK_H

#include <string>

class Stock // 类声明开始
{
private:
    std::string company;
    long shares = -1; //持仓数量
    double price_average_cost = -1;//平均持仓成本价
    double price_latest = -1;//市场最新价格
    double total_cost = -1;//持仓总成本
    double total_value_latest = -1;//持仓最新总价值

    void set_tot() { total_value_latest = shares * price_latest; } // 内联成员函数:计算总价值

public:
    // 成员函数原型(接口)
    void acquire(const std::string &co,long num,double price);
    void buy(long num, double price);

    void sell(long num, double price);

    void update_price(double price);; // 用于更新当前市场最新价

    void show();
};

#endif //CPPCONSOLEBASE_STOCK_H

stock.cpp

#include "stock.h"
#include "iostream"

void Stock::acquire(const std::string &co, long num, double price) {
    //首次购买股票
    company = co;
    if (num < 0) {
        std::cout << "股票数量不能为负值:" << company << "," << num << std::endl;
    } else {
        if (shares < 0) {    // 首次购买股票
            shares = num;
            price_latest = price;
            total_cost = num * price;
            price_average_cost = price;
            set_tot();
        } else {
            shares += num;
            price_latest = price;
            total_cost += num * price;
            price_average_cost = total_cost / shares;
            set_tot();
        }

    }
}

void Stock::buy(long num, double price) {
    //首次购买股票
    if (num < 0) {
        std::cout << "股票数量不能为负值:" << company << "," << num << std::endl;
    } else {
        if (shares < 0) {
            shares = num;
            price_latest = price;
            price_average_cost = price;
            total_cost = price * shares;
            set_tot();
        } else {
            shares += num;
            price_latest = price;
            total_cost += num * price;
            price_average_cost = total_cost / shares;
            set_tot();
        }

    }
}

void Stock::sell(long num, double price) {
    if (num < 0) {
        std::cout << "股票售出数量不能为负值:" << company << ", 交易取消." << std::endl;
    } else if (num > shares) {
        std::cout << "售出的股票数量不能大于拥有的股票数量:" << company << ", 交易取消." << std::endl;
    } else {
        shares -= num;
        price_latest = price;
        total_cost -= num * price;
        price_average_cost = total_cost / shares;
        set_tot();
    }
}

void Stock::update_price(double price) {
    price_latest = price;
    set_tot();
}

void Stock::show() {
    std::cout << "公司:" << company << std::endl;
    std::cout << "持股数量:" << shares << std::endl;
    std::cout << "持股成本价格:$" << price_average_cost << std::endl;
    std::cout << "持股最新价值:$" << total_value_latest << std::endl;
    std::cout << std::endl;
}

main.cpp

#include "stock.h" //只包含头文件

int main() {
    Stock s;
    s.show();

    s.acquire("浦发银行", 9, 10);
    s.show();

    s.buy(10, 10);
    s.show();

    s.sell(15, 9);
    s.show();

    return 0;
}

输出:

公司:
持股数量:-1
持股成本价格:$-1
持股最新价值:$-1

公司:浦发银行
持股数量:9
持股成本价格:$10
持股最新价值:$90

公司:浦发银行
持股数量:19
持股成本价格:$10
持股最新价值:$190

公司:浦发银行
持股数量:4
持股成本价格:$13.75
持股最新价值:$36

客户/服务器模型

OOP程序员常依照客户/服务器模型来讨论程序设计。在这个概念中,客户是使用类的程序。类声明(包括类方法)构成了服务器,它是程序可以使用的资源。客户只能通过以公有方式定义的接口使用服务器,这意味着客户(客户程序员)唯一的责任是了解该接口。服务器(服务器设计人员)的责任是确保服务器根据该接口可靠并准确地执行。服务器设计人员只能修改类设计的实现细节,而不能修改接口。这样程序员独立地对客户和服务器进行改进,对服务器的修改不会客户的行为造成意外的影响。

  • 客户(Client):调用、使用某个类的外部代码或程序段。
  • 服务器(Server):提供功能的类(包含其属性和方法)。
  • 接口(Interface):类中公开(public)的方法声明。这是客户与服务器沟通的唯一桥梁

这种模式带来的核心优势:

  • 解耦性(Decoupling):客户和服务器之间没有强依赖,双方可以独立升级。
  • 安全修改(Safe Maintenance):只要不改动公开接口,服务器内部的代码重构(如优化算法、修复Bug)完全不会破坏客户的原有功能。

构造函数:无返回值

主要是因为它的核心职责是初始化已分配的内存,而不是计算并返回一个数据。编译器正是通过“没有返回值类型(连 void 都没有)”这一独有特征,来精确定位并识别这是一个构造函数的。

  • 普通函数:用于执行某项计算或操作,并通过返回值告诉调用者结果。
  • 构造函数:是一种特殊的成员函数]。它的唯一任务是将一块已经分配好的原始内存空间,初始化为一个符合逻辑的类对象状态。它不负责创建内存,也不负责销毁内存,它只负责“装修”内存。因此,从语义上讲,它不需要返回任何运行结果。

stock.h

#ifndef CPPCONSOLEBASE_STOCK_H
#define CPPCONSOLEBASE_STOCK_H

#include <string>

class Stock // 类声明开始
{
private:
    std::string company="";
    long shares = -1; //持仓数量
    double price_average_cost = -1;//平均持仓成本价
    double price_latest = -1;//市场最新价格
    double total_cost = -1;//持仓总成本
    double total_value_latest = -1;//持仓最新总价值

    void set_tot() { total_value_latest = shares * price_latest; } // 内联成员函数:计算总价值

public:
    Stock(const std::string& co,long num,double price);

    void acquire(const std::string& co,long num,double price);
    void buy(long num, double price);

    void sell(long num, double price);

    void update_price(double price);; // 用于更新当前市场最新价

    void show();
};

#endif //CPPCONSOLEBASE_STOCK_H

stock.cpp

#include "stock.h"
#include "iostream"

Stock::Stock(const std::string &co, long num, double price) {
    //首次购买股票
    company = co;
    if (num < 0) {
        std::cout << "股票数量不能为负值:" << company << "," << num << std::endl;
    } else {
        if (shares < 0) {    // 首次购买股票
            shares = num;
            price_latest = price;
            total_cost = num * price;
            price_average_cost = price;
            set_tot();
        } else {
            shares += num;
            price_latest = price;
            total_cost += num * price;
            price_average_cost = total_cost / shares;
            set_tot();
        }

    }
}

void Stock::acquire(const std::string &co, long num, double price) {
    //首次购买股票
    company = co;
    if (num < 0) {
        std::cout << "股票数量不能为负值:" << company << "," << num << std::endl;
    } else {
        if (shares < 0) {    // 首次购买股票
            shares = num;
            price_latest = price;
            total_cost = num * price;
            price_average_cost = price;
            set_tot();
        } else {
            shares += num;
            price_latest = price;
            total_cost += num * price;
            price_average_cost = total_cost / shares;
            set_tot();
        }

    }
}

void Stock::buy(long num, double price) {
    //首次购买股票
    if (num < 0) {
        std::cout << "股票数量不能为负值:" << company << "," << num << std::endl;
    } else {
        if (shares < 0) {
            shares = num;
            price_latest = price;
            price_average_cost = price;
            total_cost = price * shares;
            set_tot();
        } else {
            shares += num;
            price_latest = price;
            total_cost += num * price;
            price_average_cost = total_cost / shares;
            set_tot();
        }

    }
}

void Stock::sell(long num, double price) {
    if (num < 0) {
        std::cout << "股票售出数量不能为负值:" << company << ", 交易取消." << std::endl;
    } else if (num > shares) {
        std::cout << "售出的股票数量不能大于拥有的股票数量:" << company << ", 交易取消." << std::endl;
    } else {
        shares -= num;
        price_latest = price;
        total_cost -= num * price;
        price_average_cost = total_cost / shares;
        set_tot();
    }
}

void Stock::update_price(double price) {
    price_latest = price;
    set_tot();
}

void Stock::show() {
    std::cout << "公司:" << company << std::endl;
    std::cout << "持股数量:" << shares << std::endl;
    std::cout << "持股成本价格:$" << price_average_cost << std::endl;
    std::cout << "持股最新价值:$" << total_value_latest << std::endl;
    std::cout << std::endl;
}

main.cpp

#include "stock.h" //只包含头文件

int main() {
    Stock s("浦发银行", 9, 10);
    //另一种形式: Stock s=Stock("浦发银行", 9, 10);
    s.show();

    //s.acquire("浦发银行", 9, 10);
    s.show();

    s.buy(10, 10);
    s.show();

    s.sell(15, 9);
    s.show();

    return 0;
}

构造函数Stock()的代码和函数acquire()相同。区别在于,程序声明对象时,将自动调用构造函数

析构函数

析构函数是一种特殊的成员函数,在对象生命周期结束时自动执行,用于释放内存和清理资源。其名称与类名相同,前缀加波浪号(~)。

  • 自动调用:当对象离开作用域或被 delete 时自动触发。
  • 无返回值与参数:没有返回值,不能重载(每个类只有一个析构函数)。
  • 清理资源:主要用于释放 new 分配的动态内存、关闭文件句柄或解锁互斥锁。

stock.h

#ifndef CPPCONSOLEBASE_STOCK_H
#define CPPCONSOLEBASE_STOCK_H

#include <string>

class Stock // 类声明开始
{
private:
    std::string company="";
    long shares = -1; //持仓数量
    double price_average_cost = -1;//平均持仓成本价
    double price_latest = -1;//市场最新价格
    double total_cost = -1;//持仓总成本
    double total_value_latest = -1;//持仓最新总价值

    void set_tot() { total_value_latest = shares * price_latest; } // 内联成员函数:计算总价值

public:
    Stock();//默认构造函数
    Stock(const std::string& co,long num,double price);
    ~Stock();//析构函数
    
    void buy(long num, double price);

    void sell(long num, double price);

    void update_price(double price);; // 用于更新当前市场最新价

    void show();
};

#endif //CPPCONSOLEBASE_STOCK_H

stock.cpp

#include "stock.h"
#include "iostream"

Stock::Stock() {
    //调用默认构造函数
    std::cout << "调用默认构造函数\n";
    company = "无公司名称";
    shares = 0;
    price_latest = 0;
    total_cost = 0;
    price_average_cost = 0;
    set_tot();
}

Stock::Stock(const std::string &co, long num, double price) {
    company = co;
    if (num < 0) {
        std::cout << "股票数量不能为负值:" << company << "," << num << std::endl;
    } else {
        if (shares < 0) {    // 首次购买股票
            shares = num;
            price_latest = price;
            total_cost = num * price;
            price_average_cost = price;
            set_tot();
        } else {
            shares += num;
            price_latest = price;
            total_cost += num * price;
            price_average_cost = total_cost / shares;
            set_tot();
        }

    }
}

Stock::~Stock(){
    std::cout<<"调用析构函数,stock name:"<<company<<",bye!\n";
}

void Stock::buy(long num, double price) {
    //首次购买股票
    if (num < 0) {
        std::cout << "股票数量不能为负值:" << company << "," << num << std::endl;
    } else {
        if (shares < 0) {
            shares = num;
            price_latest = price;
            price_average_cost = price;
            total_cost = price * shares;
            set_tot();
        } else {
            shares += num;
            price_latest = price;
            total_cost += num * price;
            price_average_cost = total_cost / shares;
            set_tot();
        }

    }
}

void Stock::sell(long num, double price) {
    if (num < 0) {
        std::cout << "股票售出数量不能为负值:" << company << ", 交易取消." << std::endl;
    } else if (num > shares) {
        std::cout << "售出的股票数量不能大于拥有的股票数量:" << company << ", 交易取消." << std::endl;
    } else {
        shares -= num;
        price_latest = price;
        total_cost -= num * price;
        price_average_cost = total_cost / shares;
        set_tot();
    }
}

void Stock::update_price(double price) {
    price_latest = price;
    set_tot();
}

void Stock::show() {
    std::cout << "公司:" << company << std::endl;
    std::cout << "持股数量:" << shares << std::endl;
    std::cout << "持股成本价格:$" << price_average_cost << std::endl;
    std::cout << "持股最新价值:$" << total_value_latest << std::endl;
    std::cout << std::endl;
}

main.cpp

#include "stock.h" //只包含头文件

int main() {
    Stock s("浦发银行", 9, 10);
    s.show();

    //s.acquire("浦发银行", 9, 10);
    s.show();

    s.buy(10, 10);
    s.show();

    s.sell(15, 9);
    s.show();

    Stock s2;
    s2.show();

    return 0;
}

输出:

公司:浦发银行
持股数量:9
持股成本价格:$10
持股最新价值:$90

公司:浦发银行
持股数量:9
持股成本价格:$10
持股最新价值:$90

公司:浦发银行
持股数量:19
持股成本价格:$10
持股最新价值:$190

公司:浦发银行
持股数量:4
持股成本价格:$13.75
持股最新价值:$36

调用默认构造函数
公司:无公司名称
持股数量:0
持股成本价格:$0
持股最新价值:$0

调用析构函数,stock name:无公司名称bye!
调用析构函数,stock name:浦发银行bye!

进程已结束,退出代码为 0

this指针

this指针是一个指向当前对象实例的隐式指针。它在非静态成员函数中自动可用,用于访问调用该函数的对象本身。

  • 隐式传递:调用成员函数时,编译器自动将对象地址作为 this 指针传入。
  • 指针类型:在 ClassName 的成员函数中,this 的类型通常是 ClassName* const(指针本身不可变)。
  • 非静态专用:静态成员函数没有 this 指针,因为它们属于类,不属于具体的对象。

正文完
 0
评论(没有评论)