类
掌握C++面向对象的基石——类的定义、成员(数据/函数/const)、构造函数、访问控制、友元——能设计并实现自己的类
学习目标
- 能定义一个完整的类——正确写出 class 声明、public/private 访问控制、数据成员和成员函数,并能解释 this 指针在幕后做了什么
- 能设计并实现构造函数——正确使用成员初始化列表初始化成员,区分默认构造函数和带参构造函数,并在自己的类中写出构造链
- 能回答:
const Sales_data item;定义的对象能调用item.isbn()但不能调用item.combine(rhs)——为什么?如果把combine改成combine(...) const会有什么后果?
机制总览
类:机制路径
- 1
直觉:你的代码需要一张「零件图纸」
到目前为止,你一直在用 C++ 内置的类型—— int 、 double 、 string 、 vector 。但真实世界的软件里,数据从来不是孤立的——一本书有书名、销量和单价,一个学生有姓名、学号和成绩。这些数据天然应该捆在一起。
- 2
类是什么:一张图纸 + 工厂
看上面的内存布局图——一个对象在内存中只是一块连续的数据区域。成员函数不占对象空间——它们的代码在另一块区域,所有同类的对象共享。
- 3
访问控制:谁可以碰我的数据?
上一节看到数据存在对象里——那外部代码能不能直接读写这些数据?如果能,调用方就可能绕过你的规则乱改——比如把收入设成负数。C++ 用 访问控制(access control) 解决这个问题。
章级决策实验
类:机制与证据
切换《类》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 直觉:你的代码需要一张「零件图纸」
到目前为止,你一直在用 C++ 内置的类型—— int 、 double 、 string 、 vector 。但真实世界的软件里,数据从来不是孤立的——一本书有书名、销量和单价,一个学生有姓名、学号和成绩。这些数据天然应该捆在一起。
可核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:你的代码需要一张「零件图纸」」的契约。
学完《类》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
类:失效与核验
直觉:你的代码需要一张「零件图纸」
典型失效
若把「直觉:你的代码需要一张「零件图纸」」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:你的代码需要一张「零件图纸」」的契约。
类是什么:一张图纸 + 工厂
典型失效
若把「类是什么:一张图纸 + 工厂」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「类是什么:一张图纸 + 工厂」的契约。
访问控制:谁可以碰我的数据?
典型失效
若把「访问控制:谁可以碰我的数据?」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「访问控制:谁可以碰我的数据?」的契约。
直觉:你的代码需要一张「零件图纸」
到目前为止,你一直在用 C++ 内置的类型——int、double、string、vector。但真实世界的软件里,数据从来不是孤立的——一本书有书名、销量和单价,一个学生有姓名、学号和成绩。这些数据天然应该捆在一起。
想象一个零件加工厂:你不会每次都手画一张新图纸从头切一块铝——你会先画一张「标准零件图纸」:标好尺寸、材料、公差。之后想要这个零件?对着图纸开工就行。C++ 里你要做的就是这样一张数据图纸——先画清楚这种东西长什么样(有哪些数据、能做哪些操作),之后照着这张图就能造出一个个具体的成品。
不这么做,描述一本书就得用三个零散变量 string bookNo; unsigned sold; double revenue;,每个操作都要手动一个个传进去,参数越列越长。本章教你画第一张自己的零件图——这是从「照着现成的零件图开工」迈向「自己动手画零件图」的关口。
类是什么:一张图纸 + 工厂
↡C++ 中自定义类型的蓝图。用 class 或 struct 关键字定义,包含数据成员(描述状态)和成员函数(描述操作)。class 是图纸,对象是按图纸造出的具体实例。——蓝图,规定了这种类型里有什么数据、能做什么操作。用这张蓝图造出来的每一个实例叫做对象。
看上面的内存布局图——一个对象在内存中只是一块连续的数据区域。成员函数不占对象空间——它们的代码在另一块区域,所有同类的对象共享。
那么成员函数怎么知道它在操作哪一个对象?秘密藏在 ↡成员函数内部隐含的指针,指向调用该函数的对象。函数内对数据成员的访问实际经由 this 完成。this 的类型是 T* 或 const T*,取决于函数是否为 const。 里——每个非静态成员函数都有一个隐藏的 this 形参:可以把 item.isbn() 在概念上理解为把 &item 交给 isbn,函数内部再通过 this->bookNo 访问数据成员;这不是可直接调用的真实函数签名。
↡在类内部声明的函数。可以访问该类的所有成员,包括 private。内部对数据成员的直接引用实际经由 this 完成。在类外部定义时用作用域运算符标明所属。 定义在类内部或外部——在外部定义时用 类名::函数名:
// 在类外部定义成员函数——用 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 默认 public,class 默认 private。除此之外 struct 也能有构造函数、成员函数、析构函数。业界惯例:纯数据聚合用 struct,需要封装行为的用 class。——你可能会问:之前用过 struct,和 class 有什么区别?唯一的区别是默认访问权限:struct 默认 public,class 默认 private。除此之外完全一样——struct 也能有构造函数和成员函数。惯例:纯数据聚合用 struct,需要封装行为的用 class。
访问控制:谁可以碰我的数据?
上一节看到数据存在对象里——那外部代码能不能直接读写这些数据?如果能,调用方就可能绕过你的规则乱改——比如把收入设成负数。C++ 用 ↡C++ 通过 public 和 private 关键字控制类成员的访问权限。public 成员构成接口——任何代码可访问;private 成员构成实现细节——只有类的成员函数和友元可访问。这是封装的核心机制。 解决这个问题。
看上面的分界线图。public: 下面的成员是类的接口——外部调用方可以随意使用。private: 下面的成员是类的实现细节——外部代码碰不到,直接编译报错。
↡把数据和操作数据的函数捆在类内部,用 private 隐藏实现、用 public 暴露接口。封装让调用方不必关心内部实现——改了 private 数据存储方式,只要 public 接口不变,外部代码无需修改。的设计意图:外部只能通过 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 变量之后,它的数据成员分别是什么值?谁来初始化它们?答案藏在 ↡类的一种特殊成员函数——对象创建时自动调用。函数名=类名、无返回类型(连 void 都不写)。可以有多个重载。负责初始化对象的数据成员。未显式初始化则执行默认初始化。 里。
↡构造函数参数列表右括号后、函数体左花括号前的初始化表达式。用冒号开头:: member(value), ...。初始化在构造体执行前完成——进入 {} 时成员已有确定值。比在函数体内赋值更高效,对 const 成员和引用成员是唯一初始化途径。 写在参数列表右括号后、函数体左花括号前:
Sales_data(const std::string &s, unsigned n, double p)
: bookNo(s), units_sold(n), revenue(p) { }
// ↑ 成员初始化列表(在构造体之前执行)看上面的构造链图——构造分两阶段:先跑成员初始化列表(各数据成员按声明顺序初始化),再跑构造体。进入 {} 时成员已经初始化好了。
↡编译器在没有自定义任何构造函数时自动生成的无参构造。有类内初始值的用它,类类型成员调自身默认构造,内置类型成员未定义。一旦自定义了任何构造函数,编译器就不再合成——此时 Sales_data item; 会编译报错。——如果你一个构造函数都没写,编译器自动生成一个。但内置类型成员(如 unsigned、double)的初始值未定义。
关键规则:一旦你写了任何一个构造函数,编译器就不再生成默认构造函数:
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 关键字的成员函数。把 this 从 T* 变 const T*——承诺不修改 *this。const 对象只能调 const 成员函数;非 const 对象两者都能调。只读不写的成员函数一律标 const。——在参数列表后面加一个 const:
std::string isbn() const { return bookNo; }
// ↑ 这个 const——承诺不改 *this看上面的对比图——非 const 对象什么函数都能调;const 对象只能调标了 const 的函数,没标的被编译拒绝。
const 成员函数的底层机制:把 this 从 T* 变成 const T*——不能通过这个指针修改对象。设计原则:只读的成员函数一律标 const。这既是给编译器的承诺,也是给读代码的人一个信号。
友元:给特定函数开后门
private 成员外部不能访问——但有些函数确实需要碰 private 数据才算合理。比如把对象输出到屏幕的 print 函数——如果它只能通过 isbn()、avg_price() 等公开接口间接凑信息,写出来啰嗦又低效。
↡用 friend 关键字在类内部声明的外部函数或另一个类,授权访问本类的 private 成员。friend 声明不是成员函数,位置在 public 还是 private 区无关。友元关系不传递。——在类内部用 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 为例,调用者只看见 isbn、combine、read 和 print;bookNo、units_sold、revenue 如何存储由类自己负责。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 只用于构造函数和转换运算符的声明,不能写在类外定义处。
↡按 C++11 规则,没有用户提供的构造函数、没有类内成员初始值、没有 private 或 protected 的非静态数据成员、没有基类和虚函数的类。可用花括号按成员顺序初始化。 适合简单数据记录;它没有封装边界,成员次序成为调用契约。更高语言版本调整过聚合判定条件,阅读现代代码时应按项目标准版本核对。↡可用于常量表达式的类类型。其数据成员必须满足字面值类型约束,constexpr 构造函数建立可在编译期使用的对象。 则服务于编译期计算。二者用途不同,不应把所有业务类都做成聚合。
静态类成员:状态属于类型
↡用 static 声明、属于类本身而不是某个对象的成员。静态数据成员在所有对象间共享,静态成员函数没有 this 指针,只能直接访问静态成员。 适合保存类型级配置或计数:
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 一步步看。
① 定义对象——编译器看到类定义
编译器扫描类的定义——记录所有数据成员的类型和名字(string bookNo、unsigned units_sold、double 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_sold 和 revenue。④ 友元声明在类的顶部——位置无所谓,但统一放开头更清晰。
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)。rhs 用 const 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()constsetScore(double s)——修改成绩bool isPassed() const——成绩 ≥ 60 返回 true- 在 main 中用
vector<Student>存 3 个学生,打印每个学生的姓名和是否及格
问题 3(问答型) 如果把 Sales_data 的 combine() 改成 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 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。