掌握C++面向对象的基石——类的定义、成员(数据/函数/const)、构造函数、访问控制、友元——能设计并实现自己的类

学习目标

  • 能定义一个完整的类——正确写出 class 声明、public/private 访问控制、数据成员和成员函数,并能解释 this 指针在幕后做了什么
  • 能设计并实现构造函数——正确使用成员初始化列表初始化成员,区分默认构造函数和带参构造函数,并在自己的类中写出构造链
  • 能回答:const Sales_data item; 定义的对象能调用 item.isbn() 但不能调用 item.combine(rhs)——为什么?如果把 combine 改成 combine(...) const 会有什么后果?

机制总览

类:机制路径

  1. 1

    直觉:你的代码需要一张「零件图纸」

    到目前为止,你一直在用 C++ 内置的类型—— int 、 double 、 string 、 vector 。但真实世界的软件里,数据从来不是孤立的——一本书有书名、销量和单价,一个学生有姓名、学号和成绩。这些数据天然应该捆在一起。

  2. 2

    类是什么:一张图纸 + 工厂

    看上面的内存布局图——一个对象在内存中只是一块连续的数据区域。成员函数不占对象空间——它们的代码在另一块区域,所有同类的对象共享。

  3. 3

    访问控制:谁可以碰我的数据?

    上一节看到数据存在对象里——那外部代码能不能直接读写这些数据?如果能,调用方就可能绕过你的规则乱改——比如把收入设成负数。C++ 用 访问控制(access control) 解决这个问题。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

类:机制与证据

切换《类》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 直觉:你的代码需要一张「零件图纸」

到目前为止,你一直在用 C++ 内置的类型—— int 、 double 、 string 、 vector 。但真实世界的软件里,数据从来不是孤立的——一本书有书名、销量和单价,一个学生有姓名、学号和成绩。这些数据天然应该捆在一起。

可核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:你的代码需要一张「零件图纸」」的契约。

学完《类》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

类:失效与核验

直觉:你的代码需要一张「零件图纸」

典型失效

若把「直觉:你的代码需要一张「零件图纸」」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。

核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:你的代码需要一张「零件图纸」」的契约。

类是什么:一张图纸 + 工厂

典型失效

若把「类是什么:一张图纸 + 工厂」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。

核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「类是什么:一张图纸 + 工厂」的契约。

访问控制:谁可以碰我的数据?

典型失效

若把「访问控制:谁可以碰我的数据?」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。

核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「访问控制:谁可以碰我的数据?」的契约。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

直觉:你的代码需要一张「零件图纸」

到目前为止,你一直在用 C++ 内置的类型——intdoublestringvector。但真实世界的软件里,数据从来不是孤立的——一本书有书名、销量和单价,一个学生有姓名、学号和成绩。这些数据天然应该捆在一起。

想象一个零件加工厂:你不会每次都手画一张新图纸从头切一块铝——你会先画一张「标准零件图纸」:标好尺寸、材料、公差。之后想要这个零件?对着图纸开工就行。C++ 里你要做的就是这样一张数据图纸——先画清楚这种东西长什么样(有哪些数据、能做哪些操作),之后照着这张图就能造出一个个具体的成品。

不这么做,描述一本书就得用三个零散变量 string bookNo; unsigned sold; double revenue;,每个操作都要手动一个个传进去,参数越列越长。本章教你画第一张自己的零件图——这是从「照着现成的零件图开工」迈向「自己动手画零件图」的关口。

类是什么:一张图纸 + 工厂

——蓝图,规定了这种类型里有什么数据、能做什么操作。用这张蓝图造出来的每一个实例叫做对象

Sales_data 对象的内存布局bookNostd::stringunits_soldunsigned(4 字节)revenuedouble(8 字节)成员函数不占对象空间this指向对象的首地址offset 0offset +24Boffset +28B对象 = 数据集合• 只存数据成员• 函数不占空间• 按声明顺序排列• 可能有对齐填充• this→成员访问• sizeof 返回总大小sizeof(Sales_data)成员函数(代码区,所有同类对象共享)avg_price() | isbn() | combine() — 通过隐藏的 this 参数知道在操作哪个对象🟪 紫色区域 = 对象占用的内存空间    this → 指向当前操作对象的首地址    虚线框 = 代码段
类对象在内存中只存储数据成员的值;成员函数的代码在另一块代码区,所有同类对象共享,通过隐藏的 this 指针区分在操作哪个对象。

看上面的内存布局图——一个对象在内存中只是一块连续的数据区域。成员函数不占对象空间——它们的代码在另一块区域,所有同类的对象共享。

那么成员函数怎么知道它在操作哪一个对象?秘密藏在 里——每个非静态成员函数都有一个隐藏的 this 形参:可以把 item.isbn() 在概念上理解为把 &item 交给 isbn,函数内部再通过 this->bookNo 访问数据成员;这不是可直接调用的真实函数签名。

定义在类内部或外部——在外部定义时用 类名::函数名

// 在类外部定义成员函数——用 Sales_data:: 标明它属于哪个类
double Sales_data::avg_price() const {
    if (units_sold)                    // units_sold 其实是 this->units_sold
        return revenue / units_sold;
    else
        return 0;
}

——你可能会问:之前用过 struct,和 class 有什么区别?唯一的区别是默认访问权限struct 默认 publicclass 默认 private。除此之外完全一样——struct 也能有构造函数和成员函数。惯例:纯数据聚合用 struct,需要封装行为的用 class

访问控制:谁可以碰我的数据?

上一节看到数据存在对象里——那外部代码能不能直接读写这些数据?如果能,调用方就可能绕过你的规则乱改——比如把收入设成负数。C++ 用 解决这个问题。

类的 public 接口 vs private 实现public:外部可见——调用方能使用的接口string isbn() const;✓ 外部可调用double avg_price() const;✓ 外部可调用Sales_data& combine(const Sales_data&);✓ 外部可调用—————— 封装边界:外部代码不能越过这条线 ——————private:隐藏实现——外部不可见、不可直接访问string bookNo;✗ 外部不可访问unsigned units_sold = 0;✗ 外部不可访问调用方.isbn().bookNo✗ error: private member使用原则构造函数 → public需要外部调用的 → public数据成员 → private内部辅助函数 → private
public 是类的"使用说明书"——外部代码只能通过 public 成员函数操作对象,不能直接碰 private 数据。这就是 C++ 封装的核心机制。

看上面的分界线图。public: 下面的成员是类的接口——外部调用方可以随意使用。private: 下面的成员是类的实现细节——外部代码碰不到,直接编译报错。

的设计意图:外部只能通过 public 函数间接操作数据,类作者可在函数内部加校验——比如检查 ISBN 是否匹配。改了内部实现(比如换一种存储方式),只要 public 接口不变,调用方一行代码不用改。

一个完整的类骨架:

class Sales_data {
public:   // ← 从这里往下都是公开接口
    std::string isbn() const { return bookNo; }
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
 
private:  // ← 从这里往下都是隐藏实现
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

构造函数:对象诞生的那一刻

定义了一个 Sales_data 变量之后,它的数据成员分别是什么值?谁来初始化它们?答案藏在 里。

写在参数列表右括号后、函数体左花括号前:

Sales_data(const std::string &s, unsigned n, double p)
    : bookNo(s), units_sold(n), revenue(p) { }
//  ↑ 成员初始化列表(在构造体之前执行)
构造函数执行顺序1① 分析类定义编译器看到类定义,确定各成员类型2② 分配内存在栈或堆上分配 sizeof(Sales_data) 字节3③ 成员初始化列表初始化各数据成员(默认值/指定值)4④ 构造体执行执行构造函数的 {} 内代码5⑤ 构造完成对象诞生,可正常使用class Sales_data {string bookNo;unsigned units_sold;double revenue;}; ;Sales_data item;// 触发构造过程 ↓[ 分配 sizeof=40B 栈空间 ]bookNo = "" (string())units_sold = 0revenue = 0.0← 默认初始化(合成默认构造){ 构造函数体 }← 空体或自定义逻辑对象bookNo: ""units: 0revenue: 0💡 关键:成员初始化在构造体之前——进入 {} 时所有成员已初始化完毕。
构造函数执行分两阶段:先跑成员初始化列表(各数据成员按声明顺序初始化),再跑构造体。进入 {} 时成员已有确定值。

看上面的构造链图——构造分两阶段:先跑成员初始化列表(各数据成员按声明顺序初始化),再跑构造体。进入 {} 时成员已经初始化好了。

——如果你一个构造函数都没写,编译器自动生成一个。但内置类型成员(如 unsigneddouble)的初始值未定义。

关键规则:一旦你写了任何一个构造函数,编译器就不再生成默认构造函数:

Sales_data(const std::string &s) : bookNo(s) {}
// 只定义了这一个构造函数
Sales_data item;  // ✗ 编译错误!没有默认构造函数

修法:手动加默认构造函数——Sales_data() = default; 或给全部参数提供默认值:

Sales_data(const std::string &s = "") : bookNo(s) {}
// 有了默认实参,这一个构造函数同时充当默认和带参两种角色

const 成员函数:承诺「我不改这个对象」

有时候你有一个 const 对象——只想读数据、不想改。但编译器怎么知道某个成员函数会不会偷偷改数据?答案:——在参数列表后面加一个 const

std::string isbn() const { return bookNo; }
//                     ↑ 这个 const——承诺不改 *this
const 对象 vs 非 const 对象:谁能调用谁Sales_data item;非 const 对象可调用所有函数:✓ isbn() const✓ avg_price() const✓ combine(rhs)✓ print()const 成员函数:承诺不改 this非 const 成员函数:允许改 thisitem.isbn() → this 类型: Sales_data*constconst Sales_data c;const 对象只能调 const 函数:✓ isbn() const✓ avg_price() const✗ combine(rhs)编译错误✗ print()编译错误只读承诺升级:const 对象 →this 指针变成 const Sales_data*非 const 函数期待 Sales_data* → 不匹配对比绿色 ✓ 可调用;红色 ✗ 编译错误;const 成员函数承诺不修改 *this
const 对象只能调用 const 成员函数。const 成员函数把 this 指针的类型从 T* 变成 const T*——不能修改 *this。非 const 对象两者都能调。

看上面的对比图——非 const 对象什么函数都能调;const 对象只能调标了 const 的函数,没标的被编译拒绝。

const 成员函数的底层机制:把 thisT* 变成 const T*——不能通过这个指针修改对象。设计原则:只读的成员函数一律标 const。这既是给编译器的承诺,也是给读代码的人一个信号。

友元:给特定函数开后门

private 成员外部不能访问——但有些函数确实需要碰 private 数据才算合理。比如把对象输出到屏幕的 print 函数——如果它只能通过 isbn()avg_price() 等公开接口间接凑信息,写出来啰嗦又低效。

——在类内部用 friend 声明外部函数,授权访问 private:

class Sales_data {
    // 友元声明——print/read/add 不是成员函数,但可以碰 private 数据
    friend std::ostream& print(std::ostream&, const Sales_data&);
    friend std::istream& read(std::istream&, Sales_data&);
    friend Sales_data add(const Sales_data&, const Sales_data&);
public:
    // ... public 成员
private:
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
 
// print 是普通函数,但因为 friend 声明可以直接访问 private 成员
std::ostream& print(std::ostream &os, const Sales_data &item) {
    os << item.isbn() << " " << item.units_sold << " "  // 直接读 units_sold
       << item.revenue << " " << item.avg_price();      // 直接读 revenue
    return os;
}

友元不是类的成员——声明在 public 区还是 private 区都一样。友元不具有传递性:A 是 B 的友元、B 是 C 的友元,不意味着 A 能访问 C 的 private。使用原则:只在操作符重载<<>>)和紧密关联的非成员函数(如上面的 print/read/add)中用友元——不要为了省事乱开 friend。

官方 Chapter 7 的完整类契约

前面的 Sales_data 已经说明了如何定义类,但原书这一章还要回答更深的问题:一个类怎样形成抽象数据类型,名字在类内怎样查找,构造函数怎样委托与控制转换,以及哪些状态属于整个类型而不是单个对象。

的重点不是把变量装进 class,而是建立稳定接口和表示不变量。以 Sales_data 为例,调用者只看见 isbncombinereadprintbookNounits_soldrevenue 如何存储由类自己负责。combine 还应检查 ISBN 是否一致,这种约束不该散落在每个调用点。

类作用域、类成员与不完整类型

使同名的类成员归属于类型。类外定义成员函数时,函数名后的形参列表和函数体已经处于类作用域;但返回类型写在限定名之前,所以类内类型通常还要显式限定:

class Screen {
public:
    using pos = std::string::size_type;
    pos size() const;
};
 
Screen::pos Screen::size() const { return contents.size(); }
// 返回类型在 Screen::size 之前,因此写成 Screen::pos

名字查找还受声明位置影响:类型名通常要先声明再使用;成员函数体则在整个类定义处理完后再解析成员名。只有前向声明的类是不完整类型,可以声明其指针或引用,却不能创建对象、访问成员或用 sizeof 求大小。类可包含指向自身的指针,却不能直接包含自身对象,否则大小无法确定。

构造委托、类类型转换与特殊类

用初始化列表选择目标构造函数,目标构造完整执行后才回到委托者的函数体:

class Sales_data {
public:
    Sales_data(std::string s, unsigned n, double p)
        : bookNo(std::move(s)), units_sold(n), revenue(n * p) {}
    Sales_data() : Sales_data("", 0, 0.0) {}
    explicit Sales_data(std::string s) : Sales_data(std::move(s), 0, 0.0) {}
};

可用一个实参调用的构造函数同时定义从实参类型到类类型的转换。explicit 会禁止这种隐式转换,但仍允许直接初始化:Sales_data direct("isbn") 合法;若已有 std::string id = "isbn",则 Sales_data copy = id 被拒绝。这里不用字符串字面量直接做拷贝初始化示例,因为从字面量到 std::string、再到 Sales_data 会要求连续两次用户定义转换,本来就不合法。explicit 只用于构造函数和转换运算符的声明,不能写在类外定义处。

适合简单数据记录;它没有封装边界,成员次序成为调用契约。更高语言版本调整过聚合判定条件,阅读现代代码时应按项目标准版本核对。 则服务于编译期计算。二者用途不同,不应把所有业务类都做成聚合。

静态类成员:状态属于类型

适合保存类型级配置或计数:

class Account {
public:
    static double rate() { return interestRate; }
    static void rate(double value) { interestRate = value; }
private:
    static double interestRate; // 类内声明
};
 
double Account::interestRate = 0.03; // 一个定义,分配存储
auto r = Account::rate();             // 不需要 Account 对象

静态成员函数没有 this,因此不能标 const,也不能直接读取某个对象的非静态数据。C++11 中普通静态数据成员通常仍需在类外提供一次定义;constexpr 静态成员的规则还取决于是否被取地址以及所用语言版本。

动手:一步步看一个对象是怎么构造出来的

构造一个对象分五个阶段:分析类定义 → 分配内存 → 成员初始化列表 → 构造体执行 → 构造完成。每一步下面用 ConstructorCallDiagram 高亮对应阶段——图上高亮哪一步,正文就在讲哪一步。

猜一猜:定义 Sales_data item; 之后立刻用 cout << item.avg_price()——会输出什么?如果类内部没写任何构造函数,编译器生成的默认构造会把 revenue 初始化成什么?通过下面 Stepper 一步步看。

分步1 / 5

① 定义对象——编译器看到类定义

构造函数执行顺序1① 分析类定义编译器看到类定义,确定各成员类型2② 分配内存在栈或堆上分配 sizeof(Sales_data) 字节3③ 成员初始化列表初始化各数据成员(默认值/指定值)4④ 构造体执行执行构造函数的 {} 内代码5⑤ 构造完成对象诞生,可正常使用class Sales_data {string bookNo;unsigned units_sold;double revenue;}; ;Sales_data item;// 触发构造过程 ↓[ 分配 sizeof=40B 栈空间 ]bookNo = "" (string())units_sold = 0revenue = 0.0← 默认初始化(合成默认构造){ 构造函数体 }← 空体或自定义逻辑对象bookNo: ""units: 0revenue: 0💡 关键:成员初始化在构造体之前——进入 {} 时所有成员已初始化完毕。
构造函数执行分两阶段:先跑成员初始化列表(各数据成员按声明顺序初始化),再跑构造体。进入 {} 时成员已有确定值。

编译器扫描类的定义——记录所有数据成员的类型和名字(string bookNounsigned units_solddouble revenue),以及所有 public 成员函数的签名(isbn()combine()avg_price())。此时还没分配任何内存——只是在编译器的符号表里注册了这个类型。类定义的作用就是告诉编译器一个概念上完整的数据类型长什么样。

代码逐段拆解:从零设计并实现一个完整的类

把本章类的定义、构造函数、const 成员函数、访问控制、友元串在一起——设计一个 Sales_data 类,实现所有关键成员。每段一个主题,读完你就能自己写类。

类定义骨架:public 接口 + private 数据

#include <iostream>
#include <string>
 
class Sales_data {
// 友元——普通函数,被授权访问 private 成员
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::ostream& print(std::ostream&, const Sales_data&);
friend std::istream& read(std::istream&, Sales_data&);
 
public:
    // 构造函数
    Sales_data() = default;                          // ① 默认构造
    Sales_data(const std::string &s) : bookNo(s) {}  // ② 只给书名
    Sales_data(const std::string &s, unsigned n, double p)
        : bookNo(s), units_sold(n), revenue(p) {}    // ③ 全参构造
    Sales_data(std::istream&);                       // ④ 从流构造
 
    // 成员函数——const 表示不修改 *this
    std::string isbn() const { return bookNo; }
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
 
private:
    std::string bookNo;
    unsigned units_sold = 0;      // 类内初始值——给默认构造用
    double revenue = 0.0;
};

设计要点:① 四个构造函数覆盖不同场景——空对象、只给书名、给定全部字段、从流读取。= default 告诉编译器「我要默认行为」——但这里必须显式写,因为后面定义了其他构造函数,编译器不会自动合成。② avg_price()isbn() 标了 const——不修改对象状态。③ combine() 没标 const——它会修改 units_soldrevenue。④ 友元声明在类的顶部——位置无所谓,但统一放开头更清晰。

combine:把两笔销售合并

Sales_data& Sales_data::combine(const Sales_data &rhs) {
    units_sold += rhs.units_sold;    // 累加销量
    revenue += rhs.revenue;          // 累加收入
    return *this;                     // 返回当前对象——支持链式调用
}

combine 是典型的需要修改 *this 的成员函数——所以不能标 const。返回 Sales_data&(自身引用)允许链式调用:a.combine(b).combine(c)rhsconst Sales_data& 传——只需要读它、不修改、不需要拷贝。

avg_price:只读、不修改

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

avg_price 是 const 成员函数的典型——只读、不改。revenue / units_sold 实际是 this->revenue / this->units_sold。销量为 0 时没有有意义的均价;在常见的 IEC 60559 浮点实现中,除零可能得到 NaN 或无穷并触发浮点异常状态,但不能把这种平台行为当作业务结果,所以这里显式返回 0。

read / print:友元函数读写对象

// read——从输入流读取一笔交易,填进 item
std::istream& read(std::istream &is, Sales_data &item) {
    double price = 0;       // 单价(用户输入的是单价,不存进对象)
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;  // 收入 = 单价 × 销量
    return is;
}
 
// print——把 item 的状态输出到流
std::ostream& print(std::ostream &os, const Sales_data &item) {
    os << item.isbn() << " " << item.units_sold << " "
       << item.revenue << " " << item.avg_price();
    return os;
}

两个关键设计决策:① read 的参数 Sales_data &item 不是 const——要修改 item。② print 的参数 const Sales_data &item——只读不拷贝。③ 用户输入的是单价 price 而非收入——price 只是 read 的局部变量,不存进对象。④ 返回 istream& / ostream& 是为了能链式调用。

add:相加两笔交易(ISBN 相同才合法)

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
    Sales_data sum = lhs;   // 拷贝 lhs 的数据
    sum.combine(rhs);       // 把 rhs 的数据合进来
    return sum;              // 返回结果;编译器可做 NRVO,否则可移动
}

两个参数都是 const 引用——不修改。以 lhs 为基础构造 sum,然后调 combine 合并 rhs。返回 Sales_data 按值时,编译器通常可做具名返回值优化(NRVO);若未消除返回对象,满足条件时会移动。这里不能笼统承诺“零拷贝”。

完整的使用示例

int main() {
    Sales_data total;                      // 默认构造——所有成员为默认值
    if (read(std::cin, total)) {           // 读第一笔——read 返回 cin 引用
        Sales_data trans;                  // 当前交易
        while (read(std::cin, trans)) {    // 逐笔读入
            if (total.isbn() == trans.isbn())
                total.combine(trans);       // 同书号——合并
            else {
                print(std::cout, total) << '\n';  // 不同书号——输出上一本
                total = trans;                     // 开始处理新书号
            }
        }
        print(std::cout, total) << '\n';   // 输出最后一本
    } else {
        std::cerr << "没有数据!\n";
    }
    return 0;
}

这段是前面的书店程序用类重写的版本——对比之前用裸变量和函数的版本:total.isbn() 代替了手动传字符串,total.combine(trans) 代替了手动 total_sold += trans_sold; total_revenue += trans_revenue;。数据和行为捆在一起后,代码干净了多少?

容易踩的坑

小结

  • 类由数据成员和成员函数组成——数据存对象里,函数代码所有对象共享,通过 this 指针区分操作哪个对象
  • 构造函数在对象创建时自动执行——先跑成员初始化列表(按声明顺序)、再跑构造体——一旦自定义构造,编译器不再合成默认构造
  • public 是接口(外部可调)、private 是实现(外部碰不到)——封装让修改内部实现不影响调用方
  • const 成员函数承诺不修改 *this——const 对象只能调 const 函数,非 const 对象两者都能调
  • 友元用 friend 声明授权外部函数或类访问 private——用于操作符重载和紧密关联的非成员函数,不滥用

练习

问题 1(改代码型) 下面这个 Person 类的设计有几个问题——找出并写出修正后的代码。

#include <string>
 
class Person {
    std::string name;
    std::string address;
 
public:
    std::string getName() { return name; }
    std::string getAddress() { return address; }
};

问题 2(独立实现题) 设计并实现一个 Student 类,包含:

  • 数据成员:姓名 name(string)、学号 id(int)、成绩 score(double)
  • 构造函数:默认构造(name=""、id=0、score=0.0)、全参构造(给定全部三个字段)
  • getName() const / getId() const / getScore() const
  • setScore(double s)——修改成绩
  • bool isPassed() const——成绩 ≥ 60 返回 true
  • 在 main 中用 vector<Student> 存 3 个学生,打印每个学生的姓名和是否及格

问题 3(问答型) 如果把 Sales_datacombine() 改成 Sales_data& combine(const Sales_data&) const;(末尾加 const),这个设计有什么问题?内部实现能通过编译吗?如果能通过,调用方调用后会发生什么?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

类(class)

C++ 中自定义类型的蓝图。用 class 或 struct 定义,包含数据成员(状态)和成员函数(操作)。class 是图纸,对象是按图纸造出的零件——你定义一个 class 之后,创建变量时用的就是这个名字。详见本章「类是什么」一节和 ClassLayoutDiagram 图示。

成员函数(member function)

在类内部定义的函数。可以访问该类的所有成员(包括 private)。内部通过隐藏的 this 指针知道在操作哪个对象——bookNo 实际是 this->bookNo 的简写。在类外部定义时用 类名::函数名 标明所属。详见本章「类是什么」一节。

this 指针

成员函数内部隐含的指针,指向调用该函数的对象——item.isbn() 时编译器把 &item 作为隐藏参数传给 isbn()。const 成员函数中 this 变成 const T*。详见本章「类是什么」一节和 ClassLayoutDiagram 图中 this 箭头的标注。

struct vs class

唯一区别是默认访问权限——struct 默认 public、class 默认 private。除此之外 struct 也能有构造函数、析构函数、成员函数。惯例:纯数据聚合用 struct,需要封装行为的用 class。详见本章「类是什么」一节。

访问控制(access control)

C++ 通过 public/private 控制谁能访问类成员。public 构成接口——外部可随意调用;private 是隐藏实现——只有自己人(成员函数+友元)能碰。class 默认 private。详见本章「访问控制」一节和 AccessControlDiagram 图示。

封装(encapsulation)

把数据和操作捆在类内部,用 private 隐藏实现、用 public 暴露接口。封装让调用方不依赖内部实现——改了 private 存储方式,只要接口不变外部代码无需修改。详见本章「访问控制」一节。

构造函数(constructor)

类的一种特殊函数——对象创建时自动调用。函数名=类名、无返回类型。可以有多个重载。负责初始化数据成员。一旦自定义任何构造,默认构造不再自动生成。详见本章「构造函数」一节和 ConstructorCallDiagram 图示。

成员初始化列表(member initializer list)

构造函数参数列表右括号后、函数体花括号前的初始化表达式:: bookNo(s), units_sold(n)。在构造体执行之前完成——进入 {} 时成员已有确定值。对 const 成员和引用成员是唯一初始化途径。执行顺序是声明顺序。详见本章「构造函数」一节。

合成默认构造函数

编译器在没有自定义任何构造时自动生成的默认构造。有类内初始值用它,类类型成员调自身默认构造,内置类型成员未定义。自定义任何构造后编译器不再生成。详见本章「构造函数」一节。

const 成员函数

参数列表后加 const 的成员函数:double avg_price() const。把 this 从 T*const T*——函数内不能修改数据成员。const 对象只能调 const 函数。只读不写的成员函数一律标 const。详见本章「const 成员函数」一节和 ConstMemberDiagram 图示。

友元(friend)

用 friend 关键字在类内部声明的外部函数或另一个类——授权访问本类 private。声明位置无关,不是成员,不传递。主要用于操作符重载(<< >>)和紧密关联的非成员函数。详见本章「友元」一节。

抽象数据类型(abstract data type)

只公开操作和行为语义、隐藏数据表示与实现细节的自定义类型。调用者依赖稳定接口,类负责维持内部不变量。详见「官方 Chapter 7 的完整类契约」。

类作用域(class scope)

类定义建立的名字查找区域。类成员都在其中,类外成员定义通过 ClassName::member 重新进入该作用域;返回类型若使用类内别名通常仍需限定。详见「类作用域、类成员与不完整类型」。

委托构造函数(delegating constructor)

先调用同类另一个目标构造函数,再执行自身函数体的构造函数。它能集中公共初始化逻辑,避免多个构造版本逐份复制代码。

聚合类(aggregate class)

满足聚合约束、可按成员顺序用花括号初始化的简单类。它适合数据记录,但公开成员次序会成为调用契约,不适合需要强封装与复杂不变量的对象。

字面值常量类(literal class)

满足字面值类型约束、其对象可参与常量表达式的类。constexpr 构造函数负责建立编译期可用的合法对象。

静态类成员(static class member)

属于类而不是单个对象的成员。静态数据由全部对象共享;静态成员函数没有 this,只能直接访问静态成员,可通过类名调用。

原版目录概念补充核对

以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。

静态类成员:机制、边界与证据

在《类》的官方单元 cppp-07 中,静态类成员连接本章第 5 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。

资料与写作方式声明

本章以C++ Primer, Fifth Edition, Chapter 7: Classes权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…