封装与继承

封装

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谁就先被编译

image-20251118185546735

如果不进行处理,类会有默认的函数,包含

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,所以真正执行次序是

  1. x(0)
  2. y(1)
  3. 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);

四种自动调用场景

  1. 直接复制式定义

    A b = a;   // 或 A b(a);
    
  2. 函数传值 —— 实参复制给形参

    void f(A a);   // 调用时产生临时副本
    
  3. 函数返回值 —— 返回对象给调用者

    A f() { A tmp; return tmp; }   // 返回时生成副本
    
  4. 异常抛对象

默认拷贝构造函数的行为

若程序员未自定义,编译器自动生成,规则:

  • 逐成员初始化(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(); // 绑定返回的临时对象

  • 作用:

    1. 允许非常量方式绑定到右值(临时对象)。
    2. 成为移动构造/移动赋值的入口:函数内部可安全地“掠夺”临时对象的资源(指针、文件句柄等),避免深拷贝。
    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 的成员函数 fA 的友元

示例:矩阵 × 向量

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)。
  • 你写在 BC 里的新默认值不会被使用

继承方式

继承方式 基类 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 表达式和函数式编程范式。为了帮你“知其然,知其所以然”,我将内容分为了 核心机制内存与资源管理高级模式函数式编程演进 四个模块进行深度解析。