Fork me on GitHub

C++:基本之前都深入问过豆包了,没啥新东西:类和对象、get()set()、公有私有保护、作用域解析运算符::、继承、函数重载、多态(从这开始不帖回答了,只放豆包回答链接)、虚函数

开始面向对象

菜鸟教程:C++ 类 & 对象  ~ C++ 多态

 

访问数据成员

精简代码

#include <iostream>
using namespace std;
class Box
{
   public:
      double length;   // 长度
      double breadth;  // 宽度
      double height;   // 高度
//      // 成员函数声明
      double get(void);
      void set( double len, double bre, double hei );
};
// 成员函数定义
double Box::get(void)
{
    return length * breadth * height;
}

void Box::set( double len, double bre, double hei)
{
    length = len;
    breadth = bre;
    height = hei;
}
int main()
{
   Box Box1;        // 声明 Box1,类型为 Box
   Box Box3;        // 声明 Box3,类型为 Box
   double volume = 0.0;     // 用于存储体积

   Box1.height = 5.0;
   Box1.length = 6.0;
   Box1.breadth = 7.0;

   volume = Box1.height * Box1.length * Box1.breadth;
   cout << volume <<endl;

   Box3.set(16.0, 8.0, 12.0);
   volume = Box3.get();
   cout << volume <<endl;
}

首先关于get()和set():

1、Box::get() 里的 get 不是固定的,可以叫 gett 等任意合法标识符,只要遵循命名规则;这里可以理解为一种虽然不是什么固定写法,可以随便起名字但为了方便导致大家都用get

2、其他成员函数也可以这么弄,即类内声明、类外使用作用域解析运算符 :: 定义

3、成员变量不能用这种方式,成员变量是在类内定义描述对象属性,而不是像成员函数那样类外通过 :: 定义。

 

成员函数定义 Box::get() 虽然不在类 Box 里直接写函数体,但通过作用域解析运算符 :: 表明它是类 Box 的成员函数,类里只是声明,函数体可在类外定义

 

Box1 和 Box3 本质都是 Box 类的对象,只是初始化方式不同,Box1 直接对成员变量赋值,Box3 通过调用 set 成员函数来设置成员变量的值。

 

注意到Box类里先对get成员函数声明了, 然后外面进行了定义:

内部声明后,定义写在内外都可以

而如果内部不声明,定义只能写在内部,不可以写在外部

 

注意到没写private还是public:

get 和 set 函数虽然没显式写 public 或 private ,但它们是公有的。原因如下:
  • 类成员访问控制规则:在 C++ 的类定义中,若不指定访问控制修饰符,成员的访问权限由上一个指定的访问控制修饰符决定。你代码里 public: 已经出现,后续未指定访问控制修饰符的成员函数(像 get 和 set 函数的声明)就都遵循此 public 权限,也就是这些函数能在类外被调用
class ExampleClass {
    int privateVar; // 未指定,默认 private
    void privateFunc() {} // 未指定,默认 private
public:
    int publicVar;
    int publichaha;  //未指定,由上一个public来定
};

由于类没显示指定访问权限,则默认是private的,再结合没指定也由上一个指定的访问权限规则,得知上述代码 

 

—————————————————— C++ 继承 ——————————————————

 

东西好多,他妈的

首先关于:::

:: 叫作用域解析运算符:主要用于指定要访问的标识符(如变量、函数、类等)所在的作用域,几个场景:

1. 类的成员函数定义

当在类外定义成员函数时,左侧是类名,右侧是成员函数名。
class MyClass {
public:
    void myFunction();
};

void MyClass::myFunction() {
    // 函数体
}

2. 命名空间

在使用命名空间中的标识符时,左侧是命名空间名,右侧是该命名空间内的标识符
namespace MyNamespace {
    int myVariable = 10;
}

int main() {
    int value = MyNamespace::myVariable;
    return 0;
}
MyNamespace::myVariable 表示访问 MyNamespace 命名空间里的 myVariable 变量

3. 全局作用域

使用 :: 前面不写内容,表示全局作用域。例如当局部变量和全局变量同名时,可通过这种方式访问全局变量
#include <iostream>
int globalVar = 20;
int main() {
    int globalVar = 10;
    std::cout << "Local variable: " << globalVar << std::endl;
    std::cout << "Global variable: " << ::globalVar << std::endl;
}

4. 派生类访问基类同名成员

当派生类和基类有同名成员时,使用 基类名::成员 访问基类的成员

class Animal {
public:
    void makeSound() {
        std::cout << "Some generic animal sound." << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() {
        std::cout << "Woof!" << std::endl;
    }

    void callBaseSound() {
        // 调用基类的 makeSound 函数
        Animal::makeSound();
    }
};
 
:是继承
// 基类
class Animal {
    // eat() 函数
    // sleep() 函数
};

// 派生类
class Dog : public Animal {
    // bark() 函数
};

 

初学者可以简单理解为

::是从左到右一次缩小范围

则是右边的是主(基类),派生是儿子(左边)

 

关于访问修饰符

菜鸟教程里的class Rectangle: public Shape继承了基类的全部成员函数和成员变量

问豆包更多:

一般情况下,若继承方式为公有(public)、保护(protected)继承,派生类会包含基类的全部成员(包括成员变量和成员函数)

但对于私有(private)继承也包含基类全部成员,只是访问权限有变化。不过,构造函数、析构函数和赋值运算符重载函数不会被继承,而是会根据自身情况重新定义。

不同继承方式下成员的访问权限

  • 公有继承(public:基类的公有成员和保护成员在派生类中的访问权限不变,私有成员在派生类中不可直接访问。
  • 保护继承(protected:基类的公有成员和保护成员在派生类中变为保护成员,私有成员在派生类中不可直接访问。
  • 私有继承(private:基类的公有成员和保护成员在派生类中变为私有成员,私有成员在派生类中不可直接访问。

但说了等于没说,看具体例子

#include <iostream>

class Base {
public:
    int publicMember;
protected:
    int protectedMember;
private:
    int privateMember;
};

// 公有继承
class PublicDerived : public Base {
public:
    void accessMembers() {
        publicMember = 1; // 可访问
        protectedMember = 2; // 可访问
        // privateMember = 3; // 不可访问
    }
};

// 保护继承
class ProtectedDerived : protected Base {
public:
    void accessMembers() {
        publicMember = 1; // 可访问
        protectedMember = 2; // 可访问
        // privateMember = 3; // 不可访问
    }
};

// 私有继承
class PrivateDerived : private Base {
public:
    void accessMembers() {
        publicMember = 1; // 可访问
        protectedMember = 2; // 可访问
        // privateMember = 3; // 不可访问
    }
};

int main() {
    PublicDerived publicDerived;
    publicDerived.publicMember = 1; // 可访问

    ProtectedDerived protectedDerived;
    // protectedDerived.publicMember = 1; // 不可访问,已变为保护成员

    PrivateDerived privateDerived;
    // privateDerived.publicMember = 1; // 不可访问,已变为私有成员
}
  1. 公有继承能在派生类里访问基类 protectedMember,在类外main不行,因 protected 成员只能类内或派生类用。
  2. 保护继承在派生类的成员函数里可访问 publicMember 和 protectedMember,示例中 accessMembers 函数就能访问,在保护继承中,基类的publicMemberprotectedMember在派生类中都变为了保护成员。而保护成员只能在类内或者其派生类内访问,在main函数(类外部)是无法直接访问的。不过,你可以在派生类中定义公有的访问函数来间接访问这些成员
    #include <iostream>
    
    class Base {
    public:
        int publicMember;
    protected:
        int protectedMember;
    private:
        int privateMember;
    };
    
    // 保护继承
    class ProtectedDerived : protected Base {
    public:
        // 提供公有接口来访问基类的 publicMember
        void setPublicMember(int value) {
            publicMember = value;
        }
        int getPublicMember() {
            return publicMember;
        }
    
        // 提供公有接口来访问基类的 protectedMember
        void setProtectedMember(int value) {
            protectedMember = value;
        }
        int getProtectedMember() {
            return protectedMember;
        }
    };
    
    int main() {
        ProtectedDerived protectedDerived;
        // 通过派生类提供的公有接口设置和获取成员值
        protectedDerived.setPublicMember(10);
        std::cout << "publicMember: " << protectedDerived.getPublicMember() << std::endl;
    
        protectedDerived.setProtectedMember(20);
        std::cout << "protectedMember: " << protectedDerived.getProtectedMember() << std::endl;
    
    }
  3. 私有继承同理,在派生类的成员函数里可访问 publicMember 和 protectedMember,像 accessMembers 函数。
  4. 基类 privateMember 只能在基类自身成员函数里用,派生类和类外都没法直接用。想在main里访问也要定义公有的访问函数来间接访问这些成员
    #include <iostream>
    
    class Base {
    public:
        int publicMember;
    protected:
        int protectedMember;
    };
    
    // 私有继承
    class PrivateDerived : private Base {
    public:
        // 定义公有接口来访问基类的 publicMember
        void setPublicMember(int val) {
            publicMember = val;
        }
        int getPublicMember() {
            return publicMember;
        }
        // 定义公有接口来访问基类的 protectedMember
        void setProtectedMember(int val) {
            protectedMember = val;
        }
        int getProtectedMember() {
            return protectedMember;
        }
    };
    int main() {
        PrivateDerived privateObj;
        privateObj.setPublicMember(10);
        std::cout << "publicMember: " << privateObj.getPublicMember() << std::endl;
        privateObj.setProtectedMember(20);
        std::cout << "protectedMember: " << privateObj.getProtectedMember() << std::endl;
    }

这种基本问题,没啥深度难度的问豆包就可以了

总结:

1、基类构造函数不继承,派生类一般要在构造函数里调用基类构造函数,可按需决定是否额外写逻辑;

2、公有继承可以在基类内和继承类内访问基类的公有和保护的成员变量和函数,私有成员变量和函数无法直接在派生类内和main内访问,想访问要弄个公有函数,而main里可以访问公有函数和变量,想访问保护类型变量和函数也要弄个共有函数 且公有继承,main 里能访问基类公有成员;保护和私有继承,若不写公有函数,main 里无法访问基类原公有和保护成员 。 保护继承在派生类写公有访问,然后可以在基类和派生类内访问公有和保护成员函数和变量,而main里只能访问公有成员变量和函数,对于保护和私有成员变量和函数想在main和派生类里访问都需要弄个函数 私有继承在派生类内要写个公有函数来访问基类里的公有和保护变量和函数,而访问基类里的私有变量和函数或者mian里访问都要弄个公有函数来访问公有、保护、私有成员变量和函数都要写个公有访问函数

3、

Q:

代码中派生类里为何还要写个public?然后void accessMembers?

A:

是因为外界(如 main 函数)就能调用它,若不写 public 的 accessMember 函数,main 里无法访问派生类非公有的、保护和私有类型成员。

4、

Q:

那我看派生类里写了个public然后直接可以用基类里的相应函数和变量了,为何我看你又写了一种写法是get和set呢?

A:

get 和 set 函数,是为了更好地封装和控制对成员变量的访问。直接用基类函数和变量可能会破坏封装性,而 get 和 set 能在访问时添加额外逻辑,如边界检查、数据验证等,增强代码的安全性和可维护性。

 

自己思考的很深,总结完得到豆包肯定后再回头看菜鸟教程里的“我们可以根据访问权限总结出不同的访问类型,如下所示”那个表就太小儿科了

 

自己问完豆包,再回来看多继承也明白了,感觉也没啥难的,就是太绕了,真他妈恶心

 

问了下豆包,找工作的东西,╮(╯▽╰)╭~~~~(>_<)~~~~ 之前的咨询

—————————————————— C++ 重载运算符和重载函数 ——————————————————

C++ 重载运算符和重载函数

重载函数

重载运算符

#include <iostream>

// 定义 Box 类
class Box {
private:
    double length;  // 盒子的长度
    double breadth; // 盒子的宽度
    double height;  // 盒子的高度

public:
    // 构造函数,用于初始化盒子的尺寸
    Box(double l = 0, double b = 0, double h = 0) : length(l), breadth(b), height(h) {}

    // 重载 + 运算符,用于把两个 Box 对象相加
    Box operator+(const Box& b) {
        Box box;
        box.length = this->length + b.length;
        box.breadth = this->breadth + b.breadth;
        box.height = this->height + b.height;
        return box;
    }

    // 成员函数,用于计算盒子的体积
    double getVolume() {
        return length * breadth * height;
    }
};

int main() {
    // 创建两个 Box 对象
    Box box1(1.0, 2.0, 3.0);
    Box box2(4.0, 5.0, 6.0);

    // 使用重载的 + 运算符将两个 Box 对象相加
    Box box3 = box1 + box2;

    // 输出每个盒子的体积
    std::cout << "Box 1 Volume: " << box1.getVolume() << std::endl;
    std::cout << "Box 2 Volume: " << box2.getVolume() << std::endl;
    std::cout << "Box 3 (box1 + box2) Volume: " << box3.getVolume() << std::endl;
}

关于未定义,我理解最终没初始化就是未定义:

不完全是。局部作用域的内置类型(如 intdouble)未初始化,其值是未定义的,使用会导致未预期结果;但全局或静态存储期的内置类型变量未显式初始化会被零初始化;类类型对象未显式初始化会调用默认构造函数,若默认构造函数完成初始化则并非未定义。
 
 
 

关于构造函数传参问题,上一个博客,说“对于这里的构造函数,可以不写”,但实际上虽然

#include <iostream>
#include <string>
using namespace std;
struct Books{
    string title;
    int book_id;
//    Books(string t, int id)
//        : title(t), book_id(id) {}
};
void printBookInfo(const Books& book) {
    cout << "书籍标题: " << book.title << endl;
    cout << "书籍 ID: " << book.book_id << endl;
}
int main(){
    Books Book1("C++ 教程",12345);
    Book1.title="C++ 教程";
    printBookInfo(Book1);
}

通过编译了,但说是可能版本还是编译器这些我最讨厌的问题,导致的

严格来说

没构造函数时,不能传参初始化对象

有带参数的构造函数,必须传参初始化对象,除非同时定义了无参数的构造函数

 

 

C++ 会根据操作数的类型来决定使用哪个 + 运算符:如果操作数是内置类型,就使用内置的加法规则;如果操作数是自定义类型且该类型重载了 + 运算符,就调用重载的运算符函数。
 
 
 
操作数是参与运算的对象,在 Box box3 = box1 + box2; 里 box1 和 box2 是操作数,在 int result = a + b; 中 a 和 b 是操作数。
 
 
当把加法运算符重载函数定义为非成员函数时,由于它不在类内部,没有隐含的当前对象,所以需要显式传入两个操作数,因此函数原型为 Box operator+(const Box&, const Box&); ,其中两个 const Box& 分别表示两个参与加法运算的 Box 对象的引用。

 

现在说下类成员函数

我一直因为是什么高大上的玩意,结果tmd根本不是类内函数本身有什么参数,而是main里传入的实参那,其实相当于省略了一个写法,无语

 

在 C++ 中,当调用类的成员函数时,编译器会自动将调用该函数的对象的地址作为一个隐含的参数传递给函数,这个隐含的参数就是 this 指针。对于运算符重载函数也是如此。

 

当在 main 函数中执行 box1 + box2 时,实际上等价于调用 box1.operator+(box2)。这里 box1 的地址通过 this 指针隐式传递给 operator+ 函数,而 box2 作为显式参数传递。

 

运算符重载实例

看到个有意思的

C++ 函数调用运算符 () 重载

函数调用运算符 () 可以被重载用于类的对象。当重载 () 时,您不是创造了一种新的调用函数的方式,相反地,这是创建一个可以传递任意数目参数的运算符函数。

 

C++ 下标运算符 [] 重载

safearay()Box(double l = 0, double b = 0, double h = 0) : length(l), breadth(b), height(h) {}都是构造函数,只是前者无参,在函数体中初始化数组,后者有默认参数,用成员初始化列表初始化成员变量。

 

1、safearay() 干啥用的?

safearay() 是 safearay 类的构造函数。在 C++ 中,构造函数是一种特殊的成员函数,其名称与类名相同,并且没有返回类型(包括 void)。构造函数的主要作用是在创建类的对象时对对象进行初始化操作。
在 safearay 类的构造函数中,它会遍历数组 arr 的每个元素,并将元素的下标值赋给该元素。具体代码如下:
safearay()
{
    int i;
    for(i = 0; i < SIZE; i++)
    {
        arr[i] = i;
    }
}
当你在 main 函数中创建 safearay 类的对象 A 时,safearay() 构造函数会被自动调用,从而将数组 arr 的元素初始化为 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

2、int& operator[](int i)

int& operator[](int i) 是对下标运算符 [] 的重载。在 C++ 中,运算符重载允许你为自定义类型(如类)重新定义运算符的行为。

具体解释如下:

  • operator[]:这是 C++ 中用于重载下标运算符 [] 的固定语法。当你使用 对象名[索引] 的形式时,实际上会调用这个重载的 operator[] 函数。
  • int&:返回值类型为 int&,表示返回一个 int 类型的引用。引用的作用是允许你直接修改数组中的元素。如果返回的是 int 而不是 int&,那么返回的只是数组元素的一个副本,而不是元素本身,这样就无法通过返回值修改数组元素。
  • (int i):这是函数的参数列表,i 表示要访问的数组元素的下标。
例如,在 main 函数中使用 A[2] 时,实际上会调用 A.operator[](2),该函数会返回数组 arr 中索引为 2 的元素的引用,从而可以直接访问和修改该元素。

3、这么麻烦有tm啥用啊?直接正常 int n[10] 这样赋值不更方便简单吗?

虽然使用普通的数组 int n[10] 可以很方便地定义和使用数组,但重载下标运算符在某些情况下具有重要的优势:
  • 边界检查:在实际应用中,普通数组不会进行边界检查,如果你访问的下标超出了数组的有效范围,会导致未定义行为,可能会引发程序崩溃或产生不可预测的结果。而通过重载下标运算符,你可以在 operator[] 函数中添加边界检查逻辑,确保访问的下标在有效范围内,从而提高程序的健壮性。例如:
 
int& operator[](int i)
{
    if (i < 0 || i >= SIZE) {
        // 处理越界情况,如抛出异常
        throw std::out_of_range("Index out of range");
    }
    return arr[i];
}
  • 封装性:使用类来封装数组可以隐藏数组的具体实现细节,只提供必要的接口给外部使用。这样可以避免外部代码直接访问和修改数组,提高代码的安全性和可维护性。
  • 自定义行为:重载下标运算符可以实现一些自定义的行为,例如对数组元素进行特殊的处理或转换。你可以根据具体需求在 operator[] 函数中添加额外的逻辑,而普通数组无法做到这一点。

 

A这个对象内部封装了一个数组。

 

C++ 类成员访问运算符 -> 重载

纯傻逼,先略过

 

———————————————————— C++ 多态 ————————————————————

 

先理清楚几个知识点(都是问豆包的)

1、继承机制产生了父子类的概念,

基类叫父(被继承的类通常被叫做基类或者父类)

派生类叫子(通过继承基类产生的新类被称作派生类或者子类)

 

2、当我们用子类去创建一个变量时,这个变量就代表了一个子类对象。

 

3、多态比继成功牛逼在哪啊?

牛逼一:更灵活的运行时绑定

  • 例子:有一个父类 “动物”,有 “叫” 的方法,子类 “猫” 和 “狗” 分别重写 “叫” 方法。用父类引用指向子类对象,运行时根据实际对象调用对应方法,如 “动物 a = new 猫 (); a. 叫 ();” 会输出猫叫。

  • 继承做不到的原因:继承在编译时就确定调用的方法,而多态能在运行时动态确定,更灵活适应不同情况。

  • 我的疑问:继承有猫和狗,分别写“叫”就好了,为啥会做不到啊

  • 解答:在继承里直接写猫和狗的 “叫” 方法,调用时只能明确指定是猫对象还是狗对象去调用。但多态可以用父类引用指向子类对象,程序运行到这里才根据实际对象确定调用哪个 “叫” 方法,比如有一个方法接收父类参数,传入猫对象就执行猫叫,传入狗对象就执行狗叫,继承难以在运行时这么灵活切换调用不同子类方法

  • Q:看不懂,代码说明

  • A:
    #include <iostream>
    
    // 定义父类 Animal
    class Animal {
    public:
        // 声明虚函数 call,为多态做准备
        virtual void call() {
            std::cout << "动物发出声音" << std::endl;
        }
    };
    
    // 定义子类 Cat,继承自 Animal
    class Cat : public Animal {
    public:
        // 重写父类的虚函数 call
        void call() override {
            std::cout << "喵喵喵" << std::endl;
        }
    };
    
    // 定义子类 Dog,继承自 Animal
    class Dog : public Animal {
    public:
        // 重写父类的虚函数 call
        void call() override {
            std::cout << "汪汪汪" << std::endl;
        }
    };
    
    // 继承方式调用
    void inheritanceCall() {
        Cat cat;
        Dog dog;
        cat.call();
        dog.call();
    }
    
    // 多态方式调用
    void polymorphismCall(Animal* animal) {
        animal->call();
    }
    
    int main() {
        std::cout << "继承方式调用:" << std::endl;
        inheritanceCall();
    
        std::cout << "\n多态方式调用:" << std::endl;
        Cat cat;
        Dog dog;
        Animal* animal1 = &cat;
        Animal* animal2 = &dog;
        polymorphismCall(animal1);
        polymorphismCall(animal2);
    }
  • 代码解释:

    1. 类的定义
      • Animal 类是父类,其中 call 方法被声明为 virtual 虚函数,这是实现多态的关键。虚函数允许在运行时根据实际对象类型来决定调用哪个版本的函数。
      • Cat 和 Dog 类继承自 Animal 类,并重写了 call 方法,分别输出不同的叫声。
    2. 继承方式调用
      • 在 inheritanceCall 函数中,我们直接创建 Cat 和 Dog 类的对象,然后调用它们的 call 方法。这种方式在编译时就确定了要调用的方法,缺乏灵活性。
    3. 多态方式调用
      • polymorphismCall 函数接受一个 Animal 类的指针作为参数。在 main 函数中,我们创建 Cat 和 Dog 对象,并使用 Animal 类型的指针指向它们。当调用 polymorphismCall 函数时,虽然传入的是 Animal 指针,但在运行时会根据指针实际指向的对象类型来调用相应的 call 方法,实现了运行时绑定。
  • Q:没感觉多灵活啊

  • A:

    灵活性体现

    1. 统一接口调用不同行为

    假如现在有个场景,需要管理一群动物,不管是新加入的动物还是已有的,都能统一处理。
    #include <iostream>
    
    class Animal {
    public:
        virtual void call() {
            std::cout << "动物发出声音" << std::endl;
        }
    };
    
    class Cat : public Animal {
    public:
        void call() override {
            std::cout << "喵喵喵" << std::endl;
        }
    };
    
    class Dog : public Animal {
    public:
        void call() override {
            std::cout << "汪汪汪" << std::endl;
        }
    };
    
    // 统一处理动物的函数
    void makeAnimalsCall(Animal* animals[], int size) {
        for (int i = 0; i < size; ++i) {
            animals[i]->call();
        }
    }
    
    int main() {
        Cat cat;
        Dog dog;
        Animal* animals[] = {&cat, &dog};
        makeAnimalsCall(animals, 2);
    }
  • 这里 makeAnimalsCall 函数,不管传入的是 Cat 还是 Dog 对象,甚至之后新增的 Bird 等子类对象,都可以直接调用 call 方法,无需为每个子类单独写处理逻辑。

 

牛逼二:更易扩展

  • 例子:已有 “动物” 父类和 “猫”“狗” 子类,现在新增 “鸟” 子类,使用多态的调用代码无需改动就能处理 “鸟” 对象。
  • 继承做不到的原因:继承在扩展新子类时,可能要修改大量调用代码来适配新子类,而多态调用处通用,不用改。

  • 上面代码如果后续要添加新的动物子类,比如 Bird 类

  • class Bird : public Animal {
    public:
        void call() override {
            std::cout << "叽叽叽" << std::endl;
        }
    };
  • 在 main 函数里创建 Bird 对象,加到 animals 数组,makeAnimalsCall 函数不用做任何修改就能处理新的 Bird 对象。
  • int main() {
        Cat cat;
        Dog dog;
        Bird bird;
        Animal* animals[] = {&cat, &dog, &bird};
        makeAnimalsCall(animals, 3);
    }
  • 要是没有多态,添加新子类后,就得大幅修改代码来处理新类型,而多态让代码扩展性和维护性大大提升,这就是其灵活性的重要体现。

 

牛逼三:增强可维护性

  • 例子:若要修改 “猫” 类 “叫” 的方法,使用多态时只改 “猫” 类,调用处不受影响。
  • 继承做不到的原因:继承里修改子类方法可能影响到调用该子类方法的所有地方,多态减少了这种影响范围。

牛逼四:统一处理逻辑

  • 例子:有多个 “动物” 子类对象,用多态可将它们放入一个 “动物” 数组,统一调用 “叫” 方法。
  • 继承做不到的原因:继承可能需要对不同子类对象分别编写不同处理代码,使用条件判断,代码复杂,多态则简化了这种处理

 

 

如果没多态你写个代码看看

#include <iostream>

// 定义枚举类型来区分动物种类
enum AnimalType {
    CAT,
    DOG
};

// 定义一个结构体来表示动物
struct Animal {
    AnimalType type;
};

// 定义猫叫的函数
void catCall() {
    std::cout << "喵喵喵" << std::endl;
}

// 定义狗叫的函数
void dogCall() {
    std::cout << "汪汪汪" << std::endl;
}

// 根据动物类型调用相应叫声的函数
void makeAnimalCall(Animal animal) {
    switch (animal.type) {
        case CAT:
            catCall();
            break;
        case DOG:
            dogCall();
            break;
        default:
            std::cout << "未知动物类型" << std::endl;
    }
}

int main() {
    // 创建猫和狗的动物实例
    Animal cat = {CAT};
    Animal dog = {DOG};

    // 调用函数让动物发出叫声
    makeAnimalCall(cat);
    makeAnimalCall(dog);
}
  • 扩展性差:如果要添加新的动物种类,比如鸟,需要做以下修改:
    • 在 AnimalType 枚举中添加新的类型,如 BIRD
    • 定义鸟叫的函数,如 birdCall
    • 在 makeAnimalCall 函数的 switch 语句中添加新的 case 分支来处理鸟的情况。
  • 代码复杂度高:随着动物种类的增加,makeAnimalCall 函数会变得越来越长,包含大量的条件判断,代码的可读性和可维护性都会降低。

 

呵呵,还是没感觉到有啥牛逼的好处

 

继续问豆包:

Q:关于你说的“牛逼三:增强可维护性”举例子

A:

多态情况

#include <iostream>

// 父类 Animal
class Animal {
public:
    virtual void call() {
        std::cout << "动物发出声音" << std::endl;
    }
};

// 子类 Cat
class Cat : public Animal {
public:
    void call() override {
        std::cout << "喵喵喵" << std::endl;
    }
};

// 子类 Dog
class Dog : public Animal {
public:
    void call() override {
        std::cout << "汪汪汪" << std::endl;
    }
};

// 调用函数
void makeAnimalCall(Animal* animal) {
    animal->call();
}

int main() {
    Cat cat;
    Dog dog;
    makeAnimalCall(&cat);
    makeAnimalCall(&dog);
}
如果要修改 Cat 类的 call 方法,比如把叫声改成 “喵呜喵呜” ,只需要在 Cat 类的 call 方法里修改,makeAnimalCall 函数以及 main 函数这些调用处不需要做任何改动,因为调用是基于父类指针,根据实际对象类型调用对应方法。 

单纯继承情况

#include <iostream>

// 父类 Animal
class Animal {
public:
    void call() {
        std::cout << "动物发出声音" << std::endl;
    }
};

// 子类 Cat
class Cat : public Animal {
public:
    void call() {
        std::cout << "喵喵喵" << std::endl;
    }
};

// 子类 Dog
class Dog : public Animal {
public:
    void call() {
        std::cout << "汪汪汪" << std::endl;
    }
};

int main() {
    Cat cat;
    Dog dog;
    cat.call();
    dog.call();
}

要是修改 Cat 类的 call 方法,由于调用是直接针对 Cat 对象,所有调用 cat.call() 的地方都要确认是否会受到影响。如果项目里有很多地方调用了 cat.call() ,修改时就很容易出错,维护成本高。

 

Q:不是吧,我想修改成“喵呜喵呜” ,直接把派生类输出换成 ”喵呜喵呜” 就好了啊,咋需要改那么多东西?

A:

你说得对,单纯修改派生类(子类)里的输出内容时,不管是多态还是普通继承,确实都只需要在子类的对应方法里改。但实际开发中,情况会更复杂,下面给你详细说说多态在增强可维护性上的优势。

实际开发中的复杂场景

假设除了输出叫声,call 方法还会调用其他辅助方法或者有一些额外逻辑,并且在不同调用处有不同的参数传递和后续操作。

普通继承场景

#include <iostream>

class Animal {
public:
    void commonFunction() {
        std::cout << "执行一些通用操作" << std::endl;
    }
};

class Cat : public Animal {
public:
    void call() {
        commonFunction(); 
        std::cout << "喵喵喵" << std::endl;
    }
};

void specificCall(Cat cat) {
    cat.call();
    // 这里有针对 Cat 对象 call 之后的特殊操作
    std::cout << "执行针对猫叫后的特殊操作" << std::endl;
}

int main() {
    Cat cat;
    specificCall(cat);
}

如果要修改 Cat 类的 call 方法,比如添加一些额外逻辑或者修改辅助方法的调用方式,由于 specificCall 函数是专门针对 Cat 对象编写的,可能需要检查并修改 specificCall 函数以及其他所有调用 Cat 对象 call 方法的地方,以确保修改不会引发错误

 

多态场景 

#include <iostream>

class Animal {
public:
    virtual void call() {
        std::cout << "执行一些通用操作" << std::endl;
    }
};

class Cat : public Animal {
public:
    void call() override {
        Animal::call(); 
        std::cout << "喵喵喵" << std::endl;
    }
};

void makeAnimalCall(Animal* animal) {
    animal->call();
    // 这里的操作不依赖具体是哪种动物
    std::cout << "执行通用的动物叫声后续操作" << std::endl;
}

int main() {
    Cat cat;
    makeAnimalCall(&cat);
}

在多态的情况下,makeAnimalCall 函数接受的是 Animal 类型的指针。当修改 Cat 类的 call 方法时,makeAnimalCall 函数不需要做任何修改,因为它只依赖于 Animal 类的接口,只要 Animal 类的接口不变,调用处就不受影响。这大大减少了修改代码时的影响范围,增强了代码的可维护性。

所以,虽然简单的输出修改在两种方式下看起来区别不大,但在实际复杂的项目中,多态能更好地隔离修改带来的影响,让代码更易于维护。

 

继续问豆包:

Q:关于你说的“牛逼四:统一处理逻辑”举例子

A:

多态情况

#include <iostream>

// 父类 Animal
class Animal {
public:
    virtual void call() {
        std::cout << "动物发出声音" << std::endl;
    }
};

// 子类 Cat
class Cat : public Animal {
public:
    void call() override {
        std::cout << "喵喵喵" << std::endl;
    }
};

// 子类 Dog
class Dog : public Animal {
public:
    void call() override {
        std::cout << "汪汪汪" << std::endl;
    }
};

int main() {
    Animal* animals[2];
    Cat cat;
    Dog dog;
    animals[0] = &cat;
    animals[1] = &dog;

    for (int i = 0; i < 2; ++i) {
        animals[i]->call();
    }
    return 0;
}

这里可以把 Cat 和 Dog 对象的指针存到 Animal 类型的数组里,然后通过循环统一调用 call 方法,不管有多少种动物子类,都可以用这种简洁统一的方式处理。

 

单纯继承情况 

#include <iostream>

// 父类 Animal
class Animal {
public:
    void call() {
        std::cout << "动物发出声音" << std::endl;
    }
};

// 子类 Cat
class Cat : public Animal {
public:
    void call() {
        std::cout << "喵喵喵" << std::endl;
    }
};

// 子类 Dog
class Dog : public Animal {
public:
    void call() {
        std::cout << "汪汪汪" << std::endl;
    }
};

int main() {
    Cat cat;
    Dog dog;
    if (/* 判断是否是 Cat 对象 */) {
        cat.call();
    }
    if (/* 判断是否是 Dog 对象 */) {
        dog.call();
    }
    return 0;
}

当需要处理多个子类对象时,只能通过条件判断区分不同子类对象,然后分别调用对应方法。随着子类数量增多,条件判断会越来越复杂,代码可读性和可维护性变差,而多态避免了这些问题,让代码更简洁。

 
 
有点懂了点,我tm跟写书一样

 

以上是我起手看菜鸟教程发现说跟继承有关系,故追问了许多

现在回头继续看菜鸟教程,直接把里面的实例1丢给又追问好久,发现懂了好多

不再记录了,太浪费时间了

不再强迫症了,以防失效,之前的 收获与成长

 

实例2,问完豆包后觉得太简单了,不过菜鸟教程里解释的也非常棒

以防失效

 

###:

妈逼的情人节0214图书馆坐位旁边一个身材一级棒,该肥的地方肥,该瘦的瘦,大奶子这tm又在磨练我心性好想一口下去
咳嗽一声太他妈销魂了,离得我还很近

可是经历使我立马冷静下来,做一个够狠够绝杀伐果断的人

女人只会磨灭我的斗志

一看是个考公的,呵呵,呵呵呵
View Code

###:学英文

class derived-class: access-specifier base-class

          衍生         接近-说明符

即:   派生                   访问说明符

 

polymorphism:多态性

###:豆包:

一句话回答我以下所有问题,且都是C++的,且每个问题换行,且如果需要写代码一律去掉最后的return 0

###:傻逼VS滚动代码跟他妈个僵尸一样,这么垃圾的残次品也这么受欢迎吗

posted @ 2025-02-13 23:26  GerJCS  阅读(58)  评论(0)    收藏  举报