第10章 对象和类
类的核心概念
简单来说,C 语言关注的是步骤(先做什么,后做什么),而 C++ 的 OOP 关注的是谁来做(把程序看成一堆互相配合的“物体”)。
可以用开发一款“赛车游戏”的例子解释c++的最重要的OOP特性:
- 抽象 (Abstraction),大白话:抓住重点,忽略不重要的细节。
游戏例子:在游戏里写一辆“赛车”的代码时,我们只需要记录它的速度、颜色和位置。至于轮胎是什么牌子、发动机里有几个螺丝钉,这些细节对游戏没影响,我们就不用写进代码里。这就是抽象。
- 封装和数据隐藏 (Encapsulation & Data Hiding),大白话:把数据和外壳打包在一起,不让外面的人瞎动。
游戏例子:赛车的“当前速度”是一个敏感数据。我们用一个外壳(类)把速度保护起来,设置成私有(Private)。玩家不能直接在代码里把速度改成 9999,必须通过点击“踩油门”这个公有(Public)按钮,赛车才会自己慢慢加满速度。这保护了程序不崩溃。
- 继承 (Inheritance),大白话:儿子可以直接用爸爸的财产,不用从头开始赚。
游戏例子:你已经写好了一个基础的“赛车类”(有速度、能拐弯)。现在你想加一辆“导弹赛车”。你不需要重新写一遍速度和拐弯的代码,只需要让“导弹赛车”继承基础赛车。新赛车自动就有了基础赛车的所有功能,你只需要在它身上多写一个“发射导弹”的功能就行了。
- 多态 (Polymorphism),大白话:同一个命令,不同的对象收到后做出不同的反应。
游戏例子:游戏里有一个“按下空格键”的命令。如果当前玩家开的是“普通赛车”,按下空格是刹车;如果开的是“重型坦克”,按下空格是开炮。同一个“空格键”命令,表现出了多种不同的形态。
- 代码的可重用性 (Reusability),大白话:写好的代码可以像积木一样,到处重复使用,省时省力。
游戏例子:因为你把赛车、导弹、跑道都写成了独立的、封装好的“类(积木块)”。下一个月公司想开发《赛车游戏第二代》或者一款《卡丁车游戏》时,你直接把这套代码拿过去用就行了,不用重新搬砖。
C++ 引入的 “类 (Class)” 就像是一个模具或者设计图。它把上面的抽象和封装结合在一起。有了类,我们就可以轻而易举地在电脑里创造出各种各样的“对象”(比如成千上万辆赛车),让它们在游戏里自己跑起来。
OOP 设计程序的三步走
- 第一步:找对象与定接口(用户视角)
怎么做:先不写代码,而是拿张纸画出用户需要什么“物体”(对象),以及用户想怎么作弄它(交互操作)。
例子:要做一个“银行账户”程序。
数据:储户名字、账号、存款余额。
操作(接口):开户、存钱、取钱、查余额。
- 第二步:决定内部实现(程序员视角)
怎么做:有了外面的接口,再来想里面怎么存数据、怎么写算法。用户看不到这一步,这就是“封装和数据隐藏”。
例子:数据存储:名字用 std::string 存,余额用 double 存。接口实现:写“取钱”功能时,里面要加一行安全检查——“如果取钱金额大于余额,就报错拒绝”。
- 第三步:组装并创建程序(实现方案)
怎么做:把上面两步写成真正的 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 指针,因为它们属于类,不属于具体的对象。