C++学习——多态

1、什么是多态

多态允许一个类的对象被当作另一个类的对象来使用,即同一个接口,使用不同的实例而执行不同操作。

多态分为两种:编译时多态和运行时多态。

2、静态多态

2.1 函数重载

允许在同一作用域内定义多个同名函数,只要它们的参数列表不同即可。

基本条件:

  • 函数名称必须相同

  • 参数列表必须不同(参数类型、参数个数或参数顺序)

  • 返回类型可以相同也可以不同(但仅返回类型不同不足以构成重载)

void print(int a)
{
    cout << "void print(int a)" << endl;
}

void print(int a, int b)
{
    cout << "void print(int a, int b)" << endl;
}

void print(double a)
{
        cout << "void print(double a)"  << endl;
}

int print(int a, double b)
{
    cout << "int print(int a, double b)" << endl;
}

float print(double a, int b)
{
    cout << "float print(double a, int b)" << endl;
}

int main()
{
    print(10 ,10);
    print(3.14f);
    print(10);
    print(3.14f, 10);
    print(10,3.14f);
}

2.1.1 函数重载调用规则

优先级从高到低:

  1. 精确匹配
    当实参类型与形参类型完全一致时。

    void func(int);
    void func(double);
    
    func(10);    // 调用 func(int) - 精确匹配
    func(3.14);  // 调用 func(double) - 精确匹配
    
  2. 类型提升

    • char, short 提升为 int

    • float 提升为 double

    • bool 提升为 int

    void func(int);
    void func(double);
    
    char c = 'A';
    func(c);    
    // 调用 func(int) - char 提升为 int
    
    float f = 3.14f;
    func(f);    
    // 调用 func(double) - float 提升为 double
    
  3. 标准转换(隐式类型转换)

    • 算术类型转换

      int→long, int→double, double→long double

    • 指针转换(如派生类指针转基类指针)

    • bool 转换

    实例1:整数间类型转换

    void func(long x) 
    {
        cout << "long: " << x << endl;
    }
    
    void func(double x) 
    {
        cout << "double: " << x << endl;
    }
    
    int main() 
    {
        int i = 42;
        func(i);  
        // 调用 func(long) , int 转 long 是标准转换
        // 注意:不是类型提升,因为 int→long 不属于整数提升范围
    }
    

    实例2:浮点类型间的转换

    void func(long double x) 
    {
        cout << "long double: " << x << endl;
    }
    
    int main() 
    {
        double d = 3.14;
        func(d);  
        // 调用 func(long double)  
        // double 转 long double 是标准转换
    }
    

    实例3:整数和浮点数的类型转换

    void func(double x) 
    {
        cout << "double: " << x << endl;
    }
    
    int main() 
    {
        int i = 10;
        func(i);  
        // 调用 func(double) 
        // int 转 double 是标准转换
    }
    

    实例4:派生类指针转基类指针

    class Base 
    {
    public:
        virtual void show() { cout << "Base" << endl; }
    };
    
    class Derived : public Base 
    {
    public:
        void show() override { cout << "Derived" << endl; }
    };
    
    void func(Base* ptr) 
    {
        ptr->show();
    }
    
    int main() 
    {
        Derived d;
        func(&d);  // 派生类指针转基类指针是标准转换
        // 输出: Derived (多态行为)
    }
    

    实例5:数组转指针

    void func(int* arr) 
    {
        cout << arr[0] << endl;
    }
    
    int main() 
    {
        int arr[5] = {1, 2, 3, 4, 5};
        func(arr);  // 数组转指针是标准转换
    }
    

    实例6:类类型转bool类型

    #include<iostream>
    using namespace std;
    
    class MyClass
    {
    public:
        explicit operator bool() const 
        { 
            return isValid; 
        }
        bool isValid = true;
    };
    
    void func(bool b) 
    {
        cout << (b ? "true" : "false") << endl;
    }
    
    int main() 
    {
        MyClass obj;
        func(obj);  // 调用 operator bool() 是用户定义转换
                    // 如果去掉 explicit,可以直接转换
                    // 有 explicit 需要 static_cast<bool>(obj)
    }
    

    实例7:数值类型转bool类型

    void func(bool b)
    {
        cout << (b ? "true" : "false") << endl;
    }
    
    int main() 
    {
        int i = 10;
        func(i);  // int 转 bool (非零→true)
        
        double d = 0.0;
        func(d);  // double 转 bool (0.0→false)
    }
    
  4. 用户定义转换

    class MyClass 
    {
    public:
        operator int() const { return 42; }
    };
    
    void func(int);
    void func(double);
    
    MyClass obj;
    func(obj);    // 调用 func(int) - 使用用户定义的转换
    

    这里为什么转int类型不转double类型呢?

    对于 func(obj),编译器会考虑以下两种可能的转换路径:

    路径一(选择 func(int)):

     MyClass → int(通过 operator int())
    
     然后精确匹配 func(int)
    

    路径二(选择 func(double)):

     MyClass → int(通过 operator int())
    
     然后 int → double(标准转换)
    
     最后匹配 func(double)
    

    为什么选择第一个呢?

    用户定义转换序列的优先级:当存在多个可行的用户定义转换序列时,编译器会选择最短的转换序列

2.2 运算符重载

2.2.1 基本概念

定义:赋予已有运算符新的功能,使其能用于自定义类型的操作

本质:运算符重载是特殊的函数重载

限制:

  • 不能创建新运算符

  • 不能改变运算符的优先级和结合性:编译期间已经确定。

  • 不能改变运算符的操作数个数

  • 部分运算符不能被重载

  • 至少一个操作数必须是用户自定义类型(不能对两个内置类型进行运算符重载)

2.2.2 运算符重载的两种形式

1. 成员函数形式:左操作数一定是当前对象this

class ClassName
{
public:
    ClassName operator+(const ClassName& other) const 
    { 
        // 实现加法操作
    }
};

ClassName a, b, c;
c = a + b;  // 等价于 a.operator+(b)

2. 友元函数形式

对于两个不同类型的对象进行运算,或者左操作数不是当前类的对象,使用非成员函数方式。为了访问类的私有成员,需要将函数声明为类的友元。

class Complex 
{
    friend Complex operator+(const Complex& a, const Complex& b);
};

Complex operator+(const Complex& a, const Complex& b) 
{
    return Complex(a.real + b.real, a.imag + b.imag);
}
a、基本友元运算符重载
class Complex
{
private:
    double _real;
    double _imag;

public:
    Complex(){}
    Complex(double real, double imag):_real(real), _imag(imag){}
    void Print()
    {
        cout << _real << "+" << _imag << endl; 
    }
    friend Complex operator+(const Complex &a, const Complex &b);
};

Complex operator+(const Complex &a, const Complex &b) 
{
    Complex tmp;
    tmp._imag = a._imag + b._imag;
    tmp._real = a._real + b._real;
    return tmp;
}

关键点:

  • friend 声明在类内部,但函数实现仍在类外部

  • 友元函数可以访问类的所有私有成员

  • 调用时和使用成员函数形式完全一样

b、混合类型运算(必须用友元)
class Complex
{
private:
    double _real;
    double _imag;

public:
    Complex():_real(0), _imag(0){}
    Complex(double real, double imag):_real(real), _imag(imag){}
    void Print()
    {
        cout << _real << "+" << _imag << endl; 
    }
    friend Complex operator+(double real, const Complex& a);
};

Complex operator+(double real, const Complex& a)
{
    Complex tmp;
    tmp._real = real + a._real;
    tmp._imag = a._imag;
    return tmp;
}

int main()
{

    Complex c1(10.0f, 3.14f);
    Complex c2 = 3.14f + c1;
    c2.Print();
    return 0;
}

为什么必须用友元:

  • 左操作数不是类对象,无法使用成员函数形式。
  • 需要访问私有成员,普通非成员函数无法做到
c、输入输出运算符重载

class Complex
{
private:
    double _real;
    double _imag;

public:
    Complex():_real(0), _imag(0){}
    Complex(double real, double imag):_real(real), _imag(imag){}
    friend istream& operator>>(istream &is, Complex &c);
    friend ostream& operator<<(ostream &os, const Complex &c);
};

istream& operator>>(istream &is, Complex &c)
{
    is >> c._real;
    is >> c._imag;
    return is;
}

ostream& operator<<(ostream &os, const Complex &c)
{
    os << "real = " << c._real << " imag = " << c._imag;
    return os;
}

问题:

  • 返回值必须是流引用类型,否则无法实现链式调用。

    istream& operator>>(istream& is, ...)  // 不是 istream
    ostream& operator<<(ostream& os, ...)  // 不是 ostream
    

    这样才能支持链式调用:cin >> c1 >> c2; 和 cout << c1 << c2;

  • 为什么输出可以使用 os << "real = " << c._real << " imag = " << c._imag; 无视了第一个参数必须是流引用类型,第二个参数必须是自定义常引用类型?

    // 自己的重载
    ostream &operator<<(ostream &os, const Complex &c) 
    {
        os << "real = " << c._real << " imag = " << c._imag;  // 这里发生了什么?
        return os;
    }
    
    // 标准库的重载(简化版)
    // 简化版的标准库实现(非完整代码)
    namespace std 
    {
        class ostream 
        {
        public:
            // 基本类型重载
            ostream& operator<<(int val);
            ostream& operator<<(double val);
            // ... 其他基本类型(bool, long, 等)
    
            // 字符串重载
            ostream& operator<<(const char* str);  // 关键重载
        };
    

    标准库的 const char* 重载始终存在,与你是否定义 Complex 的重载无关。

    自己写的重载和标准库的重载是互补的:

    自写的 operator<<(ostream&, const Complex&) 只负责处理 Complex 类型。

    标准库的 operator<<(ostream&, const char*) 只负责处理字符串。

    编译器会智能组合它们:当你的函数内使用 os << "text" 时,自动选择标准库版本。

2.2.3 常用运算符重载示例

  1. 算术运算符

    此处注意:operator+需要返回一个新对象而不是修改现有对象,数学本质中加法是产生一个新值,而不是修改原有值。

    class Complex
    {
    private:
        double _real;
        double _imag;
    
    public:
        Complex(){}
        Complex(double real, double imag):_real(real), _imag(imag){}
        void Print()
        {
            cout << _real << "+" << _imag << endl; 
        }
        Complex operator+(const Complex &other) const
        {
            Complex tmp;
            tmp._real = this->_real + other._real;
            tmp._imag = this->_imag + other._imag;
            return tmp; 
        }
        // 更高效的方法
        // Complex operator+(const Complex& other) const 
        // { 
        //     return Complex(_real + other._real, _imag + other._imag);
        // }
    };
    
    int main()
    {
        Complex c1(10.0f, 3.14f);
        Complex c2(5.0f, 3.14f);
        Complex c3 = c1 + c2;
        c3.Print();
        return 0;
    }
    
  2. 关系运算符

    bool operator==(const Complex &c) const
    {
        if(this->_imag == c._imag && this->_real == c._real)
            return true;
        return false;
    }
    
  3. 下标运算符 []

    需要实现两个,const和非const版本,当对象被声明为const时,只能调用其常量成员函数。如果没有const版本,代码将无法编译。

    class Arr
    {
    public:
        Arr(int n):size(n), arr(new int[n]){}
    
        int& operator[](int index) 
        {
            if (index < 0 || index >= size) 
            {
                cout << "index error !" << endl;
            }
            return arr[index];
        }
    
        // 重载[]运算符(常量版本,只读)
        const int& operator[](int index) const 
        {
            if (index < 0 || index >= size) 
            {
                cout << "index error !" << endl;
            }
            return arr[index];
        }
        // 获取数组大小
        int getSize() const 
        {
            return size;
        }
    private:
        int *arr;
        int size;
    };
    
    int main() 
    {
        Arr myArr(5);
        
        // 写入数据
        for (int i = 0; i < myArr.getSize(); ++i) 
        {
            myArr[i] = i * 10; // 使用非常量版本
        }
    
        // 读取数据
        for (int i = 0; i < myArr.getSize(); ++i) 
        {
            cout << myArr[i] << " "; // 使用非常量版本
        }
    
        
    const Arr constArr(5);
    // 使用const版本读取数据
    cout << constArr[0];  
    // 如果没有const版本,这里会编译失败!
    }
    
  4. 自增/自减运算符
    前置需要返回引用,后置需要返回临时对象。

    后置版本必须带一个int类型参数(实际调用时不传值,仅用于语法区分),编译器会自动传递一个int类型的参数0。

    class Num
    {
    public:
        Num(){}
        Num(int num):_num(num){}
        void Print()
        {
            cout << _num << endl;
        }
    
        // 前置++
        Num& operator++()
        {
            _num++;
            return *this;
        }
    
        // 后置++
        Num operator++(int)
        {
            Num old = *this;  // 保存原值
            _num++;        
            return old;       // 返回旧值
        }
    
        // 前置--
        Num& operator--()
        {
            _num--;
            return *this;
        }
    
        // 后置--
        Num operator--(int)
        {
            Num temp = *this;
            _num--;
            return temp;
        }
    private:
        int _num;
    };
    

2.2.4 特殊运算符重载

  1. 函数调用运算符 ()

    class Adder 
    {
    public:
        int operator()(int a, int b) { return a + b; }
    };
    Adder add;
    int sum = add(3, 4);  // 使用方式如同函数
    
  2. 类型转换运算符

    class Rational 
    {
    public:
        operator double() const { return numerator/(double)denominator; }
    private:
        int numerator, denominator;
    };
    Rational r(1, 2);
    double d = r;  // 自动转换为0.5
    
  3. new/delete 运算符

    class ClassName 
    {
    public:
        void* operator new(size_t size) { /* 自定义内存分配 */ }
        void operator delete(void* p) { /* 自定义内存释放 */ }
    };
    

2.2.5 运算符重载的注意事项

  1. 保持语义一致性:重载的运算符行为应与内置类型类似

  2. 谨慎重载 &&、|| 和 ,:这些运算符通常有短路求值特性,重载后会失去这一特性

  3. 考虑对称性:如 a + b 和 b + a 应该结果相同

  4. 优先选择成员函数形式:特别是需要访问私有成员的运算符

  5. 必要时使用友元:当第一个操作数不是类对象时(如 <<、>>)

  6. 不能重载的运算符:

     作用域解析 ::
    
     成员访问 .
    
     成员指针访问 .*
    
     条件运算符 ?:
    
     sizeof 和 typeid
    
  7. 重载运算符的参数限制:

    至少有一个参数是类类型或枚举类型

    不能改变运算符的优先级和结合性

  8. 赋值运算符的特殊性:

    如果没有定义,编译器会生成默认的赋值运算符

    通常需要处理自赋值问题

3、动态多态

3.1 虚函数

3.2 继承对象之间的指针和引用

posted @ 2025-06-20 00:01  baobaobashi  阅读(29)  评论(0)    收藏  举报