封装与继承
封装
inline 关键字
| 场景 | 是否加 inline | 结果 |
|---|---|---|
实现写在 .cpp |
不加 | 安全 |
实现写在 .h |
必须加 | 否则链接报错 |
安全写法(实现放源文件)
// ---------- TDate.h ----------
class TDate {
public:
void SetDate(int y, int m, int d); // 仅声明
private:
int year{}, month{}, day{};
};
// ---------- TDate.cpp ----------
void TDate::SetDate(int y, int m, int d) {
year = y; month = m; day = d;
}
头文件写法(实现放头文件)
// ---------- TDate.h ----------
class TDate {
public:
inline void SetDate(int y, int m, int d) { // 必须 inline
year = y; month = m; day = d;
}
private:
int year{}, month{}, day{};
};
不加
inline且被多个.cpp包含 → multiple definition 错误。
谁被import谁就先被编译

如果不进行处理,类会有默认的函数,包含
class CO {
public:
/* 1. 默认构造 */
CO() noexcept = default; // 对内置成员不初始化;对类成员调其默认构造
/* 2. 析构 */
~CO() noexcept = default; // 对类成员调其析构函数;内置成员不管
/* 3. 拷贝构造 */
CO(const CO& rhs) noexcept = default;
// 逐成员拷贝(内置类型按位拷贝,类类型调其拷贝构造)
/* 4. 拷贝赋值 */
CO& operator=(const CO& rhs) noexcept = default;
// 逐成员赋值,返回 *this;类成员调其拷贝赋值
};
构造
=default 让编译器把构造函数“还给你”;
=delete 让编译器把构造函数“没收掉”。
class CO {
public:
CO(int x){} // 我写了带参构造
CO() = default; // 显式要回默认构造
};
- 一旦你自己写任何构造函数,编译器就不再自动生成
CO()。 - 在后面写
=default就等于对编译器说:“麻烦把那个无参默认构造再生成一份给我。”
这时这两个函数都生效,相当于函数重载
class CO {
public:
CO(const CO&) = delete; // 禁止拷贝
};
-
现在外部代码
CO b(a);会直接 编译报错。 -
典型用途:做单例、防止对象被意外复制
CO(const CO&)就无效了
委托构造(Delegating Constructor)= 一个构造函数“复用”另一个构造函数,避免重复写初始化代码。 在构造函数的初始化列表里把另一个构造函数当成初始化项:
构造函数(参数A) : 另一个构造函数(参数B) { /* 本构造额外逻辑 */ }
CO:C(1) 就是“当前类 CO 的某个构造函数,把活儿委托给 带参版本 C(int),实参传 1”。
class CO {
public:
CO(int x, int y) : x_(x), y_(y) {} // 真正干活的构造
CO() : CO(0, 0) {} // 委托构造——复用上面那个
CO(int x) : CO(x, 0) {} // 也是委托
private:
int x_, y_;
};
构造函数本来是 public,但你完全可以把它改成 private;这样一来“外部就不能直接创建对象”,类的创建权完全被你自己“接管”。
典型用途——单例(Singleton) 或 禁止栈对象:
class CO {
private:
CO() = default; // ① 构造 privatized
public:
static CO& getInstance() { // ② 统一入口
static CO ins;
return ins;
}
};
int main() {
// CO obj; // 编译错误:构造函数不可访问
CO& ref = CO::getInstance(); // 只能通过类自己提供的方法创建
}
这里的static是为了使ins的生命周期贯穿整个程序 否则ins在return 之前就会被析构
现在的效果是每次都返回相同的ins
“成员初始化表” 是在 构造函数体 真正运行前,给数据成员设初值 的“快车道”; 它必须出现的场景有三类: ① 常量成员 ② 引用成员 ③ 需要传参的对象成员; 且初始化顺序只认类里声明的先后顺序,不认你写的顺序。
类名(形参列表) : 成员1(初值1), 成员2(初值2), ... // 初始化表
{ /* 构造函数体 */ }
为什么“要先于构造体”
- 成员变量在进入构造函数体之前就已经存在;
- 如果在表外再赋值,等于先默认构造、再赋值,多一次开销,对
const/ 引用甚至直接编译失败。
三类“必须写初始化表”的成员
| 成员类型 | 原因 |
|---|---|
const int y;(const成员) |
常量只能初始化,不能赋值 |
int& z;(reference成员) |
引用必须绑定对象,不能先默认、再改绑 |
| 对象成员(默认构造被自己写的构造替换掉的) | 想传参给它的构造函数,否则只能默认构造 |
对象成员比如
class Inner {
public:
Inner() { std::cout << "default\n"; }
Inner(int v) { std::cout << "int\n"; }
};
class Outer {
Inner in;
public:
Outer() : in(42) {} // 只打印一次 "int"
// Outer() { in = Inner(42); } // 先 default 再赋值,打印两次
};
这样成员是对象且想调它的带参构造 → 参数只能写在成员初始化表;否则编译器只能调无参默认构造,后面再赋值既可能报错又一定慢
eg:
class A {
int x; // 普通变量
const int y; // 常量
int& z; // 引用
public:
A() : y(1), z(x), x(0) // 初始化表
{
x = 100; // 这里只是重新赋值,不是初始化
}
};
注意顺序:
类里声明次序是 x → y → z,所以真正执行次序是
x(0)y(1)z(x)
所以不如写的时候就按照声明顺序写,这样也方便自己看
统一初始化
类型 变量{初值}; // 统一初始化标志就是“花括号”
对比旧语法
| 对象 | 旧写法 | 统一初始化 | 说明 |
| —— | ——————————– | ——————— | —————- |
| 数组 | int x[3] = {1,2,3}; | int x[]{1,2,3}; | 一样能用,但更短 |
| 容器 | vector<int> v; v.push_back(1); | vector<int> v{1,2}; | 直接构造,省拷贝 |
| 聚合体 | S s = {1,"abc"}; | S s{1,"abc"}; | 花括号即可 |
| 类对象 | CO c(42); | CO c{42}; | 匹配构造函数 |
窄化检查
double d = 3.14;
int a(d); // 旧语法:静默截断,a 变成 3
int b = d; // 旧语法:同样截断
int c{d}; // ❌ 统一初始化:编译错误(narrowing)
窄化 = 数据可能丢失(小数变整数、大范围整数变小范围等)。 花括号版本一旦发现窄化就直接报错,避免“静默截断”的 bug。
析构
~<类名>()// 对象消亡时 系统自动调用
可以通过析构函数私有化 实现强制控制对象自主分配
class A {
public:
static A* create() { return new A; } // 工厂:返回堆对象
void destroy() { delete this; } // 自杀接口
private:
A() = default;
~A() {} // 析构私藏,外部无法 delete
};
int main() {
A* p = A::create(); // OK
/* delete p; // 编译错误:析构不可访问 */
p->destroy(); // 通过类自己提供的方法释放
}
或者换种写法
// A.h —— 完全控制对象生命周期的 Better Solution
class A {
public:
// 1. 统一工厂:只准动态创建
static A* create() { return new A(); }
// 2. 统一释放入口
static void free(A* p) {
if (p) {
p->destroy(); // 内部自杀
}
}
// 3. 禁止外部随意 delete 或栈对象
A(const A&) = delete;
A& operator=(const A&) = delete;
private:
A() = default; // 构造函数私有
~A() = default; // 析构函数私有
// 4. 自杀接口
void destroy() { delete this; }
};
/* ---------- 使用 ---------- */
#include <iostream>
int main() {
A* p = A::create(); // 唯一创建方式
/* delete p; // 编译错误:析构函数不可访问 */
A::free(p); // 唯一释放方式
return 0;
}
析构函数的典型用法与必要性——释放构造函数里用 new[] 申请的资源,对比“有析构” vs “无析构”带来的后果。
String(char *p) {
str = new char[strlen(p)+1]; // 构造函数里动态申请
strcpy(str, p);
}
~String() { delete[] str; } // 析构函数里对称释放
- 对象生命周期结束(离开作用域、被 delete)时自动调用。
- 如果忘记写
~String()→ 内存泄漏;写了就自动回收,无需程序员手动delete[]。
把资源封装成“对象生命期”——RAII 核心
- 申请资源 → 构造函数
- 释放资源 → 析构函数 资源生命周期 = 对象生命周期,离开作用域即自动清理,这就是 C++ 的 RAII 思想。
拷贝构造函数
拷贝构造函数的使命:“创建新对象时,用同类的旧对象给它做初始化。” 签名固定为:
ClassName(const ClassName& rhs);
四种自动调用场景
-
直接复制式定义
A b = a; // 或 A b(a); -
函数传值 —— 实参复制给形参
void f(A a); // 调用时产生临时副本 -
函数返回值 —— 返回对象给调用者
A f() { A tmp; return tmp; } // 返回时生成副本 -
异常抛对象
默认拷贝构造函数的行为
若程序员未自定义,编译器自动生成,规则:
- 逐成员初始化(member-wise copy)
- 对对象成员递归调用其拷贝构造函数
- 对内置类型按位复制
→ 对只含普通成员的类足够; → 对含资源管理(指针、文件句柄等)的类会触发浅拷贝灾难(双删、泄漏)。
= delete 的用法(右上角)
A(const A& a) = delete;
表示禁止复制,任何复制语法/传值/返回都会编译错误,常用于单例、不可拷贝对象。
什么时候必须自己写拷贝构造函数——只要类内部管理了资源(这里是堆内存),就要用深拷贝防止悬挂指针 / 双删崩溃
string(char* str) {
p = new char[strlen(str)+1]; // 构造函数动态申请
strcpy(p, str);
}
~string() { delete[] p; } // 析构释放
- 默认拷贝构造函数会逐成员按位复制 → 只复制指针值,不复制内容。
- 结果:
s2.p == s1.p,两个对象指向同一块堆内存。
悬挂指针 & 双删灾难
- 任意对象提前析构 → 另一对象的
p变成悬挂指针(野指针)。 - 程序结束再次
delete[] p→ 重复释放,直接崩溃。
解决方案:自己写深拷贝(Deep Copy)
string(const string& s) { // 拷贝构造函数
p = new char[strlen(s.p)+1]; // 重新申请同样大小
strcpy(p, s.p); // 把内容复制过来
}
此时,再
string s2 = s1; // s2.p 指向**新**内存,内容与 s1 相同但互不干扰
生命周期、释放动作各自独立,不再共享资源。
也就是说,拷贝构造函数本身做的是深拷贝 但是 他深拷贝时 会对每个成员进行浅拷贝
接下来是一个例子
class A {
public:
int x, y;
A() : x(0), y(0) {} // 默认构造
void inc() { ++x; ++y; } // 递增
};
class B {
public:
int z;
A a; // 成员对象
B() : z(0) {} // 默认构造
B(const B& b) : a(b.a){z = b.z} // 自定义拷贝构造
void inc() { ++z; a.inc(); } // 递增并转发给成员
};
int main() {
B b1; // b1.z=0, b1.a.x=0, b1.a.y=0
b1.inc(); // b1.z=1, b1.a.x=1, b1.a.y=1
B b2(b1); // 拷贝构造:b2.z=1, b2.a.x=1, b2.a.y=1
return 0;
}
但若B这么写
class B {
public:
int z;
A a; // 成员对象
B() : z(0) {} // 默认构造
B(const B& b){z = b.z} // 自定义拷贝构造
void inc() { ++z; a.inc(); } // 递增并转发给成员
};
int main() {
B b1; // b1.z=0, b1.a.x=0, b1.a.y=0
b1.inc(); // b1.z=1, b1.a.x=1, b1.a.y=1
B b2(b1); // 拷贝构造:b2.z=1, b2.a.x=0, b2.a.y=0
return 0;
}
b2.a.x=0, b2.a.y=0 因为自定义了拷贝构造函数 那么拷贝构造函数里没涵盖到的成员就会调用默认构造函数
移动构造函数
左值 vs 右值(l-value / r-value)
- 左值:有持久地址,可出现在赋值号左边(变量、返回引用的函数等)。
- 右值:临时、无持久地址,只能出现在赋值号右边(字面量、表达式结果、返回对象值的函数等)。
| 引用类型 | 能绑左值? | 能绑右值? | 示例 |
|---|---|---|---|
T&(非常量左值引用) |
✅ | ❌ | int& r = a; OK;int& r = 1; 编译错 |
const T&(常量左值引用) |
✅ | ✅ | const int& r = 1; OK,生命周期延长 |
右值引用 T&& 右值引用可以绑定右值
-
语法:
A&& aa = getA();// 绑定返回的临时对象 -
作用:
- 允许非常量方式绑定到右值(临时对象)。
- 成为移动构造/移动赋值的入口:函数内部可安全地“掠夺”临时对象的资源(指针、文件句柄等),避免深拷贝。
class A { /* 空类仅作示例 */ }; A getA() { return A(); } // 按值返回临时对象(右值) int main() { int a = 1; /* ---------- 左值引用 ---------- */ int& ra = a; // ✅ 绑定变量(左值) /* ---------- 常量左值引用 ---------- */ const A& ca = getA(); // ✅ 绑定右值(临时量),生命周期延长 /* ---------- 非常量左值引用 ---------- */ // A& aa = getA(); // ❌ 编译错误:非常量引用不能绑定右值 /* ---------- 右值引用引用 ---------- */ A&& ca = getA(); // }
下面把完整代码按“**现代 C++ 常规做法**”拆成 `.h` + `.cpp`:
- 头文件只放 **声明** + **inline 小函数**
- 源文件放 **非模板、非 inline 的大函数实现**
- 依旧保持 **Rule of Five / 工厂 / RAII / 禁止外部 new** 全部特性
------------------------------------------------
MyArray.h
```cpp
#ifndef MY_ARRAY_H
#define MY_ARRAY_H
#include <cstddef> // std::size_t
#include <cstring> // memcpy
#include <iostream>
class MyArray {
public:
/* ===== 工厂:唯一创建入口 ===== */
static MyArray* create(std::size_t sz = 0);
static MyArray* create(const MyArray& other); // 深拷贝
static MyArray* create(MyArray&& other) noexcept; // 移动
/* ===== 统一释放 ===== */
static void free(MyArray* p);
/* ===== 移动语义 ===== */
MyArray(MyArray&& other) noexcept;
MyArray& operator=(MyArray&& other) noexcept;
/* ===== 常规接口 ===== */
std::size_t size() const { return size_; }
int& operator[](std::size_t i);
const int& operator[](std::size_t i) const;
/* ===== 输出辅助 ===== */
friend std::ostream& operator<<(std::ostream& os, const MyArray& ma);
/* ===== 显式删除拷贝 + 外部 new/delete ===== */
MyArray(const MyArray&) = delete;
MyArray& operator=(const MyArray&) = delete;
void* operator new(std::size_t) = delete;
void* operator new[](std::size_t) = delete;
void operator delete(void*) = delete;
void operator delete[](void*) = delete;
private:
/* ===== 构造 + 析构:仅由工厂调用 ===== */
explicit MyArray(std::size_t sz);
explicit MyArray(const MyArray& other); // 深拷贝构造
~MyArray();
void swap(MyArray& rhs) noexcept;
void destroy(); // delete this;
std::size_t size_;
int* arr_;
};
#endif // MY_ARRAY_H
```
------------------------------------------------
MyArray.cpp
```cpp
#include "MyArray.h"
/* ========== 工厂实现 ========== */
MyArray* MyArray::create(std::size_t sz) {
return new MyArray(sz);
}
MyArray* MyArray::create(const MyArray& other) {
return new MyArray(other); // 深拷贝
}
MyArray* MyArray::create(MyArray&& other) noexcept {
return new MyArray(std::move(other)); // 移动
}
void MyArray::free(MyArray* p) {
if (p) p->destroy();
}
/* ========== 私有构造 / 析构 ========== */
MyArray::MyArray(std::size_t sz)
: size_(sz), arr_(sz ? new int[sz]{} : nullptr) {}
MyArray::MyArray(const MyArray& other)
: size_(other.size_)
, arr_(size_ ? new int[size_] : nullptr) {
std::memcpy(arr_, other.arr_, size_ * sizeof(int));
}
MyArray::~MyArray() { delete[] arr_; }
void MyArray::destroy() { delete this; }
/* ========== 移动构造 ========== */
MyArray::MyArray(MyArray&& other) noexcept
: size_(other.size_), arr_(other.arr_) {
other.size_ = 0;
other.arr_ = nullptr;
}
/* ========== 移动赋值 ========== */
MyArray& MyArray::operator=(MyArray&& other) noexcept {
if (this == &other) return *this;
this->swap(other); // 交换资源
return *this;
}
/* ========== 常规接口 ========== */
int& MyArray::operator[](std::size_t i) {
return arr_[i];
}
const int& MyArray::operator[](std::size_t i) const {
return arr_[i];
}
/* ========== 输出辅助 ========== */
std::ostream& operator<<(std::ostream& os, const MyArray& ma) {
os << '[';
for (std::size_t i = 0; i < ma.size_; ++i) {
os << ma.arr_[i] << (i + 1 == ma.size_ ? "" : ", ");
}
return os << ']';
}
/* ========== 工具:交换资源 ========== */
void MyArray::swap(MyArray& rhs) noexcept {
std::swap(size_, rhs.size_);
std::swap(arr_, rhs.arr_);
}
```
------------------------------------------------
main.cpp(使用示例)
```cpp
#include "MyArray.h"
#include <iostream>
int main() {
MyArray* a = MyArray::create(4);
for (std::size_t i = 0; i < a->size(); ++i) (*a)[i] = static_cast<int>(i * 10);
std::cout << "a = " << *a << '\n';
MyArray* b = MyArray::create(*a); // 深拷贝
MyArray tmp(3);
for (std::size_t i = 0; i < 3; ++i) tmp[i] = 100 + static_cast<int>(i);
MyArray* c = MyArray::create(std::move(tmp)); // 移动构造
MyArray* d = MyArray::create(2);
*d = std::move(*c); // 移动赋值
MyArray::free(a); MyArray::free(b);
MyArray::free(c); MyArray::free(d);
return 0;
}
```
编译 & 运行
```bash
g++ -std=c++11 -Wall -O2 MyArray.cpp main.cpp -o demo && ./demo
```
输出与之前完全一致,资源生命周期全程受控,**零内存泄漏**。
const 成员函数
void f(); // 普通成员:可以修改对象数据
void show() const; // const 成员:承诺**不修改对象数据**(编译器把 this 变成 const A*)
- 只有
const对象才能调用const成员函数。 const成员函数里如果改非 mutable 成员会编译报错。
mutable 关键字
mutable int xy; // 即使在 const 对象/const 成员函数里也能被修改
典型用途:缓存、引用计数、调试信息——逻辑上“不改变对象值”,但物理上需要改数据。
const A a(0,0); // 生成 const 对象
a.show(); // OK,调用 const 成员函数
a.f(); // 错误!const 对象不能调非 const 成员函数
void A::show() const
{
x = 1; // ❌ 编译错误:x 不是 mutable
xy = 1; // ✅ OK,xy 是 mutable
indirect_int++; // ✅ OK,mutable 引用指向的值也能改
}
“const 成员函数保证对象逻辑不变,mutable 让你在不破坏 const 语义的前提下修改指定成员。”
| 关键字 | 用途 | 规则 |
|---|---|---|
constexpr |
函数/变量可在编译期求值 | 调用处给常量 ⇒ 编译期算;给变量 ⇒ 运行期算 |
consteval(C++20) |
强制编译期求值 | 调用处不是常量 ⇒ 直接报错 |
enum Flags { GOOD = 0, FAIL = 1, BAD = 2, EOF = 3 };
/* 3. 正确:constexpr 版本 —— 编译期求值,可用于 case 标签 */
constexpr int operator|(Flags f1, Flags f2) {
return static_cast<int>(f1) | static_cast<int>(f2);
}
/* 1. 错误:普通函数 —— 运行期求值,不能用于 case 标签 case标签必须编译期就需要知道值 */
// int operator|(Flags f1, Flags f2) {
// return static_cast<int>(f1) | static_cast<int>(f2);
// }
void f(Flags x) {
switch (x) {
case BAD: break; // 枚举常量,OK
case EOF: break; // 枚举常量,OK
case BAD | EOF: break; // constexpr 表达式,OK(第 3 张图)
// case BAD | EOF: break; // 若用普通函数,这里报错:非常量表达式
default: break;
}
}
/* 简单测试 */
int main() {
f(static_cast<Flags>(BAD | EOF)); // 演示用
std::cout << "BAD | EOF = " << (BAD | EOF) << '\n';
return 0;
}
constexpr int sqr(int x) { return x * x; }
constexpr int A = sqr(10); // OK,编译期常量
int y = 3;
int B = sqr(y); // OK,但退为运行期求值
consteval int pow2(int n) { return 1 << n; }
constexpr int M = pow2(8); // OK,编译期常量
// int r = pow2(y); // ❌ 编译错误:y 不是常量
静态成员变量
- 类内声明,类外定义(可初始化)。
- 所有对象共享同一份数据。
- 生命周期:程序开始 → 程序结束。
- 访问控制遵循
public/protected/private。
class A {
int x, y;
static int shared; // 声明
};
int A::shared = 0; // 定义(必须)
C++17 起可用 inline 直接在类内初始化:
class A {
inline static int shared = 0;
};
使用方式:
A a, b;
a.shared++; // 通过对象
A::shared++; // 通过类名(推荐)
## 静态成员函数
- 无
this指针,只能访问静态成员。 - 可通过对象或类名调用。
- 常用来做工厂方法、工具函数、全局状态管理。
class A {
static int count;
public:
static int getCount() { return count; }
};
典型用法:对象计数器
class A {
static int objCount;
public:
A() { ++objCount; }
~A() { --objCount; }
static int getObjCount() { return objCount; }
};
int A::objCount = 0;
单例(Singleton)雏形
class Singleton {
static Singleton* inst;
Singleton() = default;
public:
static Singleton* instance() {
return inst ? inst : (inst = new Singleton);
}
static void destroy() { delete inst; inst = nullptr; }
};
Singleton* Singleton::inst = nullptr;
原则:谁创建,谁释放(RAII)。
友元(friend)
- 打破封装,让外部函数/类/成员函数直接访问 private/protected 成员。
- 提高灵活性,但降低封装性 → 慎用。
| 类别 | 写法 | 含义 |
|---|---|---|
| 友元函数 | friend void func(); |
全局函数 func 可访问 A 的私有/保护成员 |
| 友元类 | friend class B; |
整个类 B 的所有成员函数都是 A 的友元 |
| 友元类成员函数 | friend void C::f(); |
只有 C 的成员函数 f 是 A 的友元 |
示例:矩阵 × 向量
class Vector; // 前向声明
class Matrix {
int *data, rows, cols;
public:
Matrix(int r, int c) : rows(r), cols(c) {
data = new int[r * c];
}
~Matrix() { delete[] data; }
friend void multiply(Matrix& m, Vector& v, Vector& r); // 友元函数
};
class Vector {
int *data, len;
public:
Vector(int n) : len(n) { data = new int[n]; }
~Vector() { delete[] data; }
int& elem(int i) { return data[i]; }
friend void multiply(Matrix& m, Vector& v, Vector& r);
};
/* 友元实现:直接访问 private 数据 */
void multiply(Matrix& m, Vector& v, Vector& r) {
int lin = m.rows, col = m.cols;
for (int i = 0; i < lin; ++i) {
r.elem(i) = 0;
for (int j = 0; j < col; ++j)
r.elem(i) += m.data[i * col + j] * v.elem(j);
}
}
- 不具有传递性:A 是 B 的友元,≠ B 是 A 的友元。
- 不具有继承性:友元关系不会传给派生类。
- 避免滥用:优先用
public接口;友元仅用于性能关键或语义紧密的场景。
一句话总结
“静态成员让类拥有‘全局变量’能力;友元让外部代码直接触碰 private,二者都提高灵活性,但要牺牲封装,需谨慎。”
继承
基类 派生类
派生类“确实”在用基类的成员——只是这些成员被编译器隐藏地安插进派生类对象里,你平时写代码时看不见,但它们客观存在,而且必须被初始化。
class A { int x; }; // 4 字节
class B : public A { int y; }; // 8 字节
- 编译器把
A子对象(含x)放在B对象的开头。 - 因此
B对象里物理上就有一份x,只是你写b.x时语法被封装成 “通过基类接口访问”。
基类子对象构造 → 成员子对象构造 → 派生类构造函数体执行
如果允许你先构造派生类再“回头”构造基类,就会出现:
- 派生类构造函数体里访问
x时,x还是未初始化的垃圾值 → UB。 - 析构时顺序也会乱套。
class A { protected: int x; };
class B : public A {
public:
B(int v) { x = v; } // ← 这里就在用基类成员 x
};
即使你写的是 x = v;,编译器底层也会把它翻译成:
this->A::x = v; // 通过基类子对象赋值
若基类子对象还没构造,x 就是一块未初始化的内存。
派生类不要重新定义基类中同名的(除了多态函数)
外部调用函数时先在对应类中调用 若找到了相应名字的函数则去找参数是否相同 若没找到参数相同 则报错;若没找到相应名字的函数,则去基类找 以此类推
如果不显示声明的话,默认private继承,但实际上一般都是用public继承,记得写
| 基类原有 \ 继承方式 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public 成员 | 仍为 public | 降为 protected | 降为 private |
| protected 成员 | 仍为 protected | 仍为 protected | 降为 private |
| private 成员 | 始终隐藏(子孙不能用,外人更看不见) |
class Student {
public: void set_ID(int);
protected: int id;
private: char secret[8];
};
class Pub : public Student { /* id 在 Pub 里仍是 protected */ };
class Pro : protected Student { /* id 在 Pro 里仍是 protected */ };
class Pri : private Student { /* id 在 Pri 里变成 private */ };
Pub p; p.set_ID(1); // ✅ 合法,set_ID 在 Pub 里仍是 public
Pro pr; pr.set_ID(1);// ❌ 非法,set_ID 在 Pro 里已被降为 protected
Pri pi; pi.set_ID(1);// ❌ 非法,set_ID 在 Pri 里已被降为 private
protected的成员是派生类可以访问 外部类无法访问
“类型相容 / 赋值相容” 的核心规则:
“向上转型(派生→基)总是安全的;向下转型(基→派)必须显式且可能失败。”
对象赋值(值拷贝)
class B: public A
A a; B b;
a = b; // OK:只复制 b 的“A 部分”,b 的“B 部分”被**切片丢失**。
b = a; // ❌ 错误:a 根本没有 B 部分,无法赋值。
- 身份变化:
b被切片成A,原属B的成员不复存在
指针 / 引用(地址拷贝)
B b;
A* pa = &b; // OK:向上转型,安全
A& ra = b; // OK:向上转型,安全
// 反过来就会error
- 身份不变:
b仍是完整B对象,只是通过A的视角去使用。 - 通过
pa只能访问A的成员;要访问B特有成员需 向下转型(dynamic_cast/static_cast)
虚函数(Virtual Function)
- 实现动态绑定(运行时决定调用哪一份函数)。
- 让“基类指针/引用”能表现出派生类行为 → 真正做到“is-a”关系。
class A {
public:
virtual void f(){}; // 虚函数
};
class B : public A {
public:
void f() override; // 自动成为虚函数
};
如基类中被定义为虚成员函数,则派生类中对其重定义的成员函数均为虚函数
限制
- 只有成员函数可以是虚函数。
- 静态成员 / 构造函数 / 内联不能是虚函数。
- 析构函数可以且常常必须是虚函数。
| 绑定方式 | 时机 | 依据 | 关键字 |
|---|---|---|---|
| 前期绑定 | 编译期 | 静态类型 | 默认 |
| 后期绑定 | 运行期 | 实际对象类型 | virtual |
class Base {
public:
void foo() { cout << "Base::foo (non-virtual)\n"; } // 非 virtual —— 编译期绑定(默认)
virtual void bar() { cout << "Base::bar (virtual)\n"; } // virtual —— 运行期绑定
};
class Derived : public Base {
public:
void foo() { cout << "Derived::foo (hides Base::foo)\n"; }
void bar() override { cout << "Derived::bar (overrides Base::bar)\n"; }
};
int main() {
Base* p = new Derived();
p->foo(); // 调用 Base::foo —— 非 virtual,早绑定(编译期)
p->bar(); // 调用 Derived::bar —— virtual,晚绑定(运行期)
delete p;
return 0;
}
构造函数里的虚调用
class A {
public:
virtual void f();
void g() { f(); } // 非虚,静态绑定 A::g
};
class B : public A {
public:
void f() override; // 虚函数
B() { f(); } // 构造函数
};
B b; // 先调 A() → 再调 B()
↑
在 A() 体内若调用 f() → 永远是 A::f()
在 B() 体内若调用 f() → 永远是 B::f()
原因:对象尚未完全构造,C++ 保证不会跨阶段动态绑定,防止访问未初始化成员。
普通函数里的虚调用
A* p = &b;
p->f(); // 后期绑定 → B::f()(因为 b 是 B 对象)
p->g(); // 非虚,静态绑定 → A::g()
p->h(); // 若 h() 里再调 f() → 后期绑定 → B::f()
override & final
| 关键字 | 用途 | 效果 |
|---|---|---|
override |
显式标记重写 | 签名不匹配 → 编译错误 |
final |
禁止再被重写 | 派生类继续重写 → 编译错误 |
// 基类
struct B {
virtual void f1(int) const; // 虚函数
virtual void f2(){}; // 虚函数(无参)
void f3(){}; // 非虚函数
virtual void f5(int) final {}; // 虚函数且已 final
};
// 派生类:演示 override 与 final 的各种对错
struct D : B {
void f1(int) const override; // ✅ 正确:签名完全匹配
void f2(int) override; // ❌ 错误:B 没有 f2(int) 原型(应为无参)
void f3() override; // ❌ 错误:f3 不是虚函数 只有虚函数允许重写
void f4() override; // ❌ 错误:B 没有名为 f4 的函数
void f5(int) override; // ❌ 错误:B::f5 已标记 final,禁止再重写
};
访问控制 & 虚函数
class B {
protected:
virtual void f() {}
};
class D : public B {
public:
void f() override {} // 放宽权限
};
B* pb = &d;
pb->f(); // ❌ 编译错误:B::f 是 protected
结论:调用检查以静态类型的访问限定为准。
class Base {
protected:
int prot_mem; // protected 成员
};
class Sneaky : public Base {
friend void clobber(Sneaky&); // 友元函数
friend void clobber(Base&); // 友元函数
int j; // 默认 private
};
// 定义
void clobber(Sneaky &s) {
s.j = 0; // ✅ 正确:能访问 Sneaky 的 private & protected
s.prot_mem = 0; // ✅ 正确:能访问 Sneaky::prot_mem(继承而来)
}
void clobber(Base &b) {
b.prot_mem = 0; // ❌ 错误:不能访问 Base::prot_mem
}
纯虚函数 & 抽象类
class Abstract {
public:
virtual void display() = 0; // 纯虚函数
};
规则
- 含 ≥1 个纯虚函数 → 抽象类。
- 不能创建对象;只能作为基类。
- 派生类必须实现全部纯虚函数,否则仍是抽象类。
典型用法
- 接口/框架设计(Abstract Factory、Strategy 等)。
- 运行时多态数组/容器:
Figure* figures[100]; figures[0] = new Rectangle(); figures[1] = new Ellipse(); for (auto f : figures) f->display();
虚析构函数
问题
B* p = new D;
delete p; // 若 ~B 非 virtual → 只析构 B 部分(内存泄漏)
解决
class B {
public:
virtual ~B() = default;
};
class D : public B {
mystring* name = new mystring;
public:
~D() { delete name; } // 会被正确调用
};
结论:任何可能被继承的类,析构函数都写成 virtual。
“虚函数的缺省参数值”是静态绑定的——重写时千万别改它,否则调用结果和你想象的不一样!”
class A {
public:
virtual void f(int x = 0) = 0; // 默认实参 = 0
};
class B : public A {
public:
void f(int x = 1) override { cout << x; } // 重写时把默认值改成 1
};
class C : public A {
public:
void f(int x) override { cout << x; } // 不改默认值
};
A* p_a = new B;
A* p_a1 = new C;
p_a->f(); // 输出 0 ❌ 不是 1
p_a1->f(); // 输出 0 ✅ 符合预期
底层原因
- 虚函数入口 → 动态绑定(运行时按实际对象类型找函数)。
- 缺省实参 → 静态绑定(编译期按静态类型取默认值)。
因此:
- 静态类型是
A*→ 编译器永远取A里写的默认值(这里是 0)。 - 你写在
B或C里的新默认值不会被使用。
继承方式
| 继承方式 | 基类 public 成员 | 基类 protected 成员 | 基类 private 成员 | 对外是否 is-a | 常见用途 |
|---|---|---|---|---|---|
| public | 仍为 public | 仍为 protected | 不可见 | ✅ 是 | 接口复用 / 运行时多态 |
| protected | 变为 protected | 仍为 protected | 不可见 | ❌ 否 | 仅派生类内部使用 |
| private | 变为 private | 变为 private | 不可见 | ❌ 否 | 实现细节复用(Implemented-in-terms-of) |
private
使用场景
- 复用基类实现,但不暴露基类接口。
- 例如:
class CStudent : private CHumanBeing {};
class CHumanBeing { /*...*/ };
class CStudent : private CHumanBeing { /*...*/ };
void eat(const CHumanBeing& h);
CStudent s;
eat(s); // ❌ 错误:私有继承,外部不可转换 编译器拒绝把 CStudent 当成 CHumanBeing
因为 private 继承不暴露“is-a”关系,编译器不会执行向上转型。
protected场景
class Base {
protected: // 仅自身与派生类可访问
int data_;
public:
Base(int d) : data_(d) {}
};
class Derived : protected Base { // protected 继承
public:
Derived(int d) : Base(d) {}
void show() const {
std::cout << "data = " << data_ << '\n'; // ✅ 合法:派生类内访问 protected
}
};
int main() {
Derived d(42);
d.show(); // ✅ 合法:show() 是 public
// Base b(d); // ❌ 错误:外部不能把 Derived 当成 Base 用
// std::cout << d.data_; // ❌ 错误:data_ 在 Derived 里已是 protected
}
多继承(Multiple Inheritance)
class D : public B1, public B2 { /*...*/ };
- 所有基类成员都被继承下来。
- 构造函数调用顺序 = 类定义中 从左到右 依次调用。
- 名字冲突 → 用
基类名::成员显式限定。
菱形问题 & 虚基类
class A { int x; };
class B : virtual public A { };
class C : virtual public A { };
class D : public B, public C { }; // D 只有一份 A::x
关键规则:
- 虚基类构造函数由最底层派生类(D)负责调用,且优先于非虚基类。
- 非
virtual继承时,D 会包含 两份 A 子对象(B::x 和 C::x)。
抽象工厂模式(Multi-Inheritance 典型)
class AbstractFactory {
public:
virtual Button* CreateButton() = 0;
virtual Label* CreateLabel() = 0;
};
class WinFactory : public AbstractFactory {
public:
Button* CreateButton() override { return new WinButton; }
Label* CreateLabel() override { return new WinLabel; }
};
客户端:
AbstractFactory* fac = new WinFactory;
Button* btn = fac->CreateButton(); // 实际 WinButton
Label* lab = fac->CreateLabel(); // 实际 WinLabel
你好!作为软件学院的同学,我很乐意帮你整理这份关于 C++ 操作符重载(Operator Overloading)的深度笔记。
这份 PPT 的跨度很大,从基础的语法一直讲到了 C++11 的 Lambda 表达式和函数式编程范式。为了帮你“知其然,知其所以然”,我将内容分为了 核心机制、内存与资源管理、高级模式、函数式编程演进 四个模块进行深度解析。