南京大学cpp复习(c10——多态、操作符重载)

多态

含义

  • 同一论域中一个元素可有多种解释
    • 在同一个上下文中,一个元素可以有多种不同的表现形式或行为方式。
    • 允许程序以一种统一的方式处理不同类型的对象,从而提高代码的灵活性和可扩展性。

好处

  • 提高语言灵活性

多态的实现方式

  • 一名多用
    • 函数重载
  • 类属(泛型)
    • 模板
  • OO程序设计
    • 继承和虚函数

重载

函数重载

cpp 复制代码
void print(int x);     // 一个版本
void print(double x);  // 另一个版本
  • 函数名同、参数的类型或数量不同
  • 静态绑定
    • 在编译时根据参数类型决定调用哪个函数
  • 歧义控制
    • 当多个重载函数可能匹配同一个调用时,编译器必须做出选择
    • 按照如下顺序进行选择:
      1. 完美匹配
      2. 提升 (不会造成精度损失,如 int -> longfloat -> double
      3. 标准转换(可能造成精度损失)
      4. 用户自定义转换(A 的构造函数的参数为 B)
    • 如果无法确定唯一一个函数(比如存在两个同一等级),则不通过编译。

操作符重载

动机

  • 让用户自定义的数据类型也能支持熟悉的运算符语义
  • 提高代码可读性、直观性、可扩充性
  • 提供结合性(例如 a + b + c,只要 + 重载返回一个类即可实现链式调用)

如何实现

  • built_in 类型:由编译器预定义行为
  • 自定义数据类型:由程序员自己定义

歧义控制

  • 和函数重载的歧义控制一致,操作符重载本质上是特殊的函数重载
  • 成员操作符重载和全局操作符重载会放在一起比较(C++ 经过 Cfront 编译后 C 语言后,不会有类的概念)

两个例子

重载 +

成员函数重载(可以省略第一个参数)
cpp 复制代码
class Complex {
public:
    Complex operator+(Complex& x);  // 成员函数
};

Complex a(1,2), b(3,4);
Complex c = a + b;        // OK
Complex d = b + a;        // OK(对称)
全局函数重载(不能省略第一个参数)
cpp 复制代码
class Complex {
    double real, imag;
public:
    Complex() { ... }
    Complex(double r, double i) { ... }
    friend Complex operator+(Complex& c1, Complex& c2);  // 声明为友元
};

Complex operator+(Complex& c1, Complex& c2) {
    Complex temp;
    temp.real = c1.real + c2.real;
    temp.imag = c1.imag + c2.imag;
    return temp;
}

Complex a(1,2), b(3,4);
Complex c = a + b;        // OK
Complex d = b + a;        // OK(对称)

注意:全局操作符重载必须包含一个自定义类型!!!


绝对不能被重载的操作符

  • .
  • .*
  • ::
  • ?:

它们是语言的基础,一旦重载,整个语言的可靠性将崩溃。


重载的基本原则

  • 重载方式
    • 类成员函数重载
    • 带有类参数的全局函数重载
  • 遵循原有语义(否则语法错误)
    • 单目/双目
    • 优先级
    • 结合性(左结合或右结合,比如 a = b = c 为右结合,从右到左计算,等价于 a = (b = c)

双目操作符重载

类成员函数重载

  • 格式

    cpp 复制代码
    <ret type> operator # (<arg>)
    • 返回类型可以是 T 或者 T&
    • 参数个数为 1 个,因为隐含了 this
    • # 是你想要重载的操作符
  • 调用方式

    cpp 复制代码
    a # b;                    // 等价于:
    a.operator#(b);           // 显式调用

全局函数形式重载

  • 格式

    cpp 复制代码
    friend <ret type> operator # (<arg1>, <arg2>); // 必须声明为友元
    
    <ret type> operator # (<arg1>, <arg2>) // 两个参数都需显式输入
  • 示例

    cpp 复制代码
    Complex a(1,2), b(3,4);
    Complex c = a + b;        // 调用 operator+(a, b)
  • 注意

    • =, (), [], -> 不能作为全局函数重载 这些操作符的语义与对象内部状态紧密绑定,要求左操作数必须是该类类型的可修改对象,作为全局函数重载破坏了对象的完整性。
    • 全局函数重载两次即可实现交换律
    cpp 复制代码
    // 全局函数 1:支持 int + CL
    CL operator+(int i, CL& a) {
        return CL(i + a.count);  // 假设 CL 构造函数接受 int
    }
    
    // 全局函数 2:支持 CL + int
    CL operator+(CL& a, int i) {
        return CL(a.count + i);
    }
  • 警告

    • 不要重载 &&||,因为这会破坏短路机制,可能引发严重 bug。

单目操作符

类成员函数重载

cpp 复制代码
<ret type> operator # ()

全局函数重载

cpp 复制代码
<ret type> operator # (<arg>)

前置自增和后置自增

  • 两者的区别:
    • ++a:先加 1,再返回修改后的对象
    • a++:加 1,但是返回旧值的副本
  • 实现示例:
    • 后置自增,加上一个哑元用以区分(没有其他任何用处)
cpp 复制代码
class Counter {
    int value;
public:
    Counter() { value = 0; }
    Counter& operator ++() { // ++a
        value++;
        return *this;
    }
    Counter operator ++(int) { // a++
        Counter temp = *this;
        value++;
        return temp;
        // 能不能返回 Counter&?绝对不行,temp 为栈上对象,一旦退出函数就会析构
    }
};

特殊操作符重载

= 的重载

  • 默认赋值操作符(编译器自动生成)

    • 逐个成员赋值(类似默认拷贝函数)
    • 数据成员中有对象则递归
  • 赋值操作符不能被继承

    • = 的行为与类的内存布局高度相关,子类可能有额外成员变量,继承父类的 =,会导致额外成员未被赋值
  • 返回值------return *this

    • 以支持链式赋值(a = b = c
  • 如果一个类中有额外申请的资源,必须重载赋值操作符(深拷贝和浅拷贝)

    • 其实更广的应该遵从三法则五法则
  • 三法则和五法则

    • 三法则 :如果你显式定义了以下三个特殊成员函数中的任意一个,那么你很有可能需要重新定义全部三个
      • 析构函数
      • 拷贝构造函数
      • 拷贝赋值操作符
    • 五法则:加入移动构造函数和移动赋值操作符
    • 为什么需要这样? 主要还是额外资源可能需要深拷贝,移动时将被移动的对象的额外资源置为空。
  • 避免自我赋值

    cpp 复制代码
    class A {
        int* ptr;
    public:
        A& operator=(const A& other) {
            delete ptr;           // 1. 删除旧内存
            ptr = new int(*other.ptr);  // 2. 分配新内存
            return *this;
        }
    };
    // 考虑 a = a 的情况

    解决方法:加入证同测试(对象身份 object identity 是否相同,对象身份由对象内存地址决定)。

    cpp 复制代码
    A& operator=(const A& other) {
        if (this != &other) {  // 证同测试,查看地址是否相同
            delete ptr;
            ptr = new int(*other.ptr);
        }
        return *this;
    }

[] 的重载

  • 读写控制(返回非 const 引用还是 const 引用)

    • 例子
    cpp 复制代码
    class string {
        char* p;  // 动态分配的字符数组
    public:
        string(char* p1) {
            p = new char[strlen(p1)+1];
            strcpy(p, p1);
        }
    
        virtual ~string() { delete[] p; }
    
        // 1. 非常量版本:允许修改
        char& operator[](int i) { return p[i]; }
    
        // 2. 常量版本:只读访问
        const char& operator[](int i) const { return p[i]; }
    };
    
    string s("aacd");           // 创建可变字符串
    s[2] = 'b';                 // ✅ 合法!调用非 const 版本
    cout << s[0];               // ✅ 输出 'a'
    
    const string cs("const");   // 创建常量字符串
    cout << cs[0];              // ✅ 合法!调用 const 版本
    cs[0] = 'D';                // ❌ 编译错误!不能修改 const 对象
    • 关键点
      • 如果没有常量版本,那么 const string cs("const"); cout << cs[0]; 会报错,因为非 const 引用不能绑定 const 变量。
      • 所以 const string 则会自动调用常量版本,多个重载函数只需要有一个符合即可。
  • 如何实现多维数组访问

    • C++ 中用一维数组标识多维数组,类似如下的方式无法解决多维数组访问的问题
    cpp 复制代码
    class Array2D {
        int n1, n2;
        int* p;  // 指向整个二维数组的连续内存
    public:
        Array2D(int i, int j) : n1(i), n2(j) {
            p = new int[n1*n2];
        }
    
        virtual ~Array2D() { delete[] p; }
    
        int& operator[](int i) { return p[i]; }  // ❌ 只能返回 int&
    };
    • 解决方案:使用 Array1D 代理类
    cpp 复制代码
    class Array1D {
        int* q;  // 指向某一行的数据
    public:
        int& operator[](int j) {
            return q[j];
        }
    };
    
    class Array2D {
        int n1, n2;
        int* p;  // 指向整个二维数组的连续内存
    public:
        Array2D(int i, int j) : n1(i), n2(j) {
            p = new int[n1*n2];
        }
    
        virtual ~Array2D() { delete[] p; }
    
        // 关键:返回 Array1D 对象!
        Array1D operator[](int i) {
            Array1D temp;
            temp.q = p + i * n2;  // 指向第 i 行的起始地址
            return temp;
        }
    };

() 的重载

  • 让类可以像函数一样被调用,从而实现函数对象。
cpp 复制代码
class Func {
    double para;
    int lowerBound, upperBound;
public:
    double operator()(double x, int i, int j) {
        // 实现某种计算逻辑
        return para * x + i * j;
    }
};

Func f;  // 创建一个 Func 对象
f(2.4, 0, 8);  // ✅ 调用 f.operator()(2.4, 0, 8)
  • sort 就用到了函数对象
cpp 复制代码
template <class RandomAccessIterator, class Compare>
void sort(RandomAccessIterator first,
          RandomAccessIterator last,
          Compare comp);

它要求 comp 必须实现了 () 的重载


三种可调用对象

  • 普通函数
  • 成员函数
  • 函数指针:即一般的函数
cpp 复制代码
int multiply_by_2(int x) { return x * 2; }

// 声明一个指向该函数类型的指针
int (*func_ptr)(int);  // func_ptr 是一个函数指针

// 赋值:让指针指向 multiply_by_2 函数
func_ptr = &multiply_by_2;   // & 可省略
  • 函数对象 :重载了 operator() 的类示例
    • 和函数指针的最大区别是它可以携带状态
cpp 复制代码
class Multiply {
    int factor;  // 成员变量:保存状态
public:
    Multiply(int f) : factor(f) {}  // 构造函数初始化状态
    int operator()(int x) const {  // 重载 () 操作符
        return x * factor;
    }
};

// 函数对象:可以参数化
Multiply m(3);
m(4);  // 返回 12
  • Lambda 函数
    • 这是创建匿名函数对象的语法糖,并没有实现新的功能,只是让你不用自己创建一个函数对象,编译器会自动为你生成一个匿名类。
cpp 复制代码
int add5 = [base = 5](int x) { return x + base; };
cout << add5(10);  // 输出 15
  • 语法结构
cpp 复制代码
[capture-list] (params) -> return-type { body }
  • ->return-type 可省略,自动推断。
  • [] 为捕获列表,即捕获作用域中的变量(类似于初始化)
捕获方式 含义
[] 不捕获任何变量
[&] 以引用方式捕获所有在作用域内被引用的变量
[=] 以值拷贝方式捕获所有在作用域内被引用的变量
[=, &foo] 以值拷贝方式捕获所有被引用的变量,但 foo 变量以引用方式捕获
[&, foo] 以引用方式捕获所有被引用的变量,但 foo 以值拷贝方式捕获
[bar] 仅以值拷贝方式捕获 bar;不捕获任何其他变量

function------通用函数封装器

  • 可以存储并调用任何可调用对象,实现类型统一和接口抽象,将所有能调用的对象都当作同一类处理。
cpp 复制代码
void doSomething(std::function<void(int)> callback) {
    int result = 42;
    callback(result);  // 调用用户提供的逻辑
}

function 的语法

cpp 复制代码
bool cmpInt(int a, int b) { return a < b; }

class CmpInt {
    bool operator()(const int a, const int b) const {
        return a < b;
    }
};

std::function<bool(int, int)> f1(cmpInt);
std::function<bool(int, int)> f2(CmpInt);
std::function<bool(int, int)> f3([](int a, int b) { return a < b; });

function 的实现(个人认为了解即可)

  • 模板参数不同,则实例化出的对象是完全不同的类,所以 function 包裹的函数参数不同就会调用不同的 call,传入不同的参数。
cpp 复制代码
struct _Base {
    virtual void call() = 0;         // 纯虚函数:多态调用点
    virtual ~_Base() {}              // 虚析构函数
};

template<class F>
struct _Model : _Base {
    F f;                             // 存储实际的可调用对象
    _Model(F func) : f(func) {}
    
    void call() override { f(); }    // 多态调用点!
};
相关推荐
网安_秋刀鱼5 小时前
【java安全】shiro反序列化1(shiro550)
java·开发语言·安全·web安全·网络安全·1024程序员节
降临-max5 小时前
JavaWeb企业级开发---快速入门、请求响应、分层解耦
java·开发语言·笔记·学习
lsx2024065 小时前
MongoDB 删除文档
开发语言
西西学代码5 小时前
【道德经】1-5
学习
Lucky高5 小时前
Pandas库实践3_索引
开发语言·python·pandas
Sheep Shaun5 小时前
STL:string和vector
开发语言·数据结构·c++·算法·leetcode
小徐Chao努力5 小时前
Go语言核心知识点底层原理教程【Slice的底层实现】
开发语言·算法·golang
EchoL、5 小时前
【论文阅读】HiDDeN:Hiding Data With Deep Networks
论文阅读·笔记·机器学习
知识分享小能手5 小时前
Ubuntu入门学习教程,从入门到精通,Ubuntu 22.04的远程登录(6)
linux·学习·ubuntu