🤖 作者:包瑞清(richie bao): lastmod: 2024-11-19T14:39:48+08:00

面向对象编程(Object-Oriented Programming,OOP)是一种编程范式,以对象为核心,通过模拟现实世界的对象及其交互,设计和实现程序。OOP 的主要目标是提高代码的重用性、可维护性和扩展性,其核心概念包括对象(Object)、类(Class)、封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)和抽象(Abstracton)等。

  1. 对象,是程序中的一个实体,表示现实中的某个具体事务。每个对象包含两部分,一个是属性(Attributes),用于描述对象的状态,用变量表示;一个是方法(Methods),定义对象的行为,用函数表示。
  2. 类,是对象的模板或蓝图,用于定义一组具有相同属性和方法的对象,将对象的结构和行为进行抽象。
  3. 封装,将数据(属性)和操作(方法)封装在类中,并通过访问权限(如公有、私有)保护数据,来隐藏实现细节,提高代码的安全性。
  4. 继承,子类可以继承父类的属性和方法,从而复用代码,并可以对其进行扩展或重写,以提高代码的复用性,实现层次化设计。
  5. 多态,同一方法在不同对象中可以表现出不同的行为,以提高代码的灵活性,且实现统一接口。
  6. 抽象,通过隐藏细节,只暴露必要的功能,提高代码的易用性和可读性,其可以通过抽象类或接口实现。

OOP 的优势体现在模块化,将代码划分为独立模块(类和对象),便于维护和调试;代码复用,通过继承和组合,减少重复代码;灵活性和扩展性,通过多态,能够在不修改原有代码的情况下扩展系统功能;贴近现实,模拟现实世界对象及其关系,使设计直观。除了 C 语言,Python、C++ 和 C# 均支持 OOP。

内容 Py C++ C#

类与对象和封装

(定义类,实例化对象,类的属性(字段)和方法,及访问修饰符)

在 Python 中,使用class关键字,后接类名,定义一个类。类名通常使用 PascalCase(每个单词首字母大写)的命名约定,例如MyClass__init__方法为构造函数,是初始化方法,在创建对象时自动执行,常用于传入参数和初始化对象属性。self表示实例本身,所有实例方法的第一个参数必须是self。实例化对象是指通过类生成对象。调用类名时会自定调用__init__方法。

类的属性和方法可以细分为实例属性和方法,及类属性和方法。实例属性和方法是每个对象都有自己独立的属性和方法,并通过self访问;类属性和方法,是所有对象共享的属性和方法,类属性和方法不仅可以通过实例访问,也可以直接通过类访问。类的方法是通过@classmethod装饰器定义,使用cls作为第一个参数。

  # A-定义类。使用 class 关键字定义类
class Dog:
    species = "Canis familiars" # 类属性

    # __init__ 为初始化方法,可以传入初始化参数,定义属性
    def __init__(self, name, breed):
        self.name = name # 通过 self 将属性绑定到对象。为实例属性
        self.breed = breed 

    # 实例方法。为定义的行为
    def bark(self): # 用 self 作为方法的第一个参数
        print(f"{self.name} says: Woof!")

    # 设置年龄
    def setAge(self, age):
        self.age = age

    # 获取年龄
    def getAge(self):
        return self.age

    # 通过装饰器定义类方法
    @classmethod
    def describe(cls):
        return cls.species

# B-创建对象。使用类创建对象
dog1 = Dog("Buddy", "Golden Retriever")
dog2 = Dog("Max", "Bulldog")

# 调用实例属性
print(dog1.name)
print(dog2.breed)

# 调用方法
dog1.bark()
dog2.bark()

# 调用类属性
print(Dog.species) # 通过类访问
print(dog1.species) # 通过实例访问
print(dog2.species)

# 调用类方法
print(Dog.describe()) # 通过类访问
print(dog1.describe()) # 通过实例访问
print(dog2.describe())
  
  🡮
Buddy
Bulldog
Buddy says: Woof!
Max says: Woof!
3
Canis familiars
Canis familiars
Canis familiars
Canis familiars
Canis familiars
Canis familiars
  

🤖 逐行解释代码

  • 类的定义和属性声明
  class Dog:
    species = "Canis familiars"
  
  1. 定义了一个名为Dog的类,表示为狗的模型。
  2. species是一个类属性,属于整个类Dog,而不是某个实例(对象)。其值为Canis familiars,表示狗的学名。
  • 初始化函数
      def __init__(self, name, breed):
        self.name = name
        self.breed = breed
  
  1. __init__是构造函数,在创建类的实例(对象)时自动调用。
  2. 参数namebreed分别表示狗的名字和品种,并通过self绑定到该对象,使每个实例拥有独立的namebreed属性;。
  • 实例方法:bark
      def bark(self):
        print(f"{self.name} says: Woof!")
  
  1. bark是一个实例方法,表示狗叫。
  2. 通过打印该实例的name,然后加上"says: Woof!"来表示。
  • 实例方法:setAgegetAge
      def setAge(self, age):
        self.age = age

    def getAge(self):
        return self.age
  
  1. setAge是用来给实例设置age(狗年龄)属性的函数。
  2. getAge是用来获取实例的age属性的函数。
  3. age属性在实例初始化时并未定义,而是通过调用setAge方法动态添加。
  • 类方法:describe
      @classmethod
    def describe(cls):
        return cls.species
  
  1. @classmethod装饰器表示这是一个类方法,而不是实例方法。
  2. 类方法的第一个参数是cls,代表类本身,而不是具体实例;
  3. describe返回类属性species的值。
  • 创建实例
  dog1 = Dog("Buddy", "Golden Retriever")
dog2 = Dog("Max", "Bulldog")
  
  1. 使用Dog类创建两个实例:dog1dog2
  2. dog1的名字是"Buddy",品种是"Golden Retriever"
  3. dog2的名字是"Max",品种是"Bulldog"
  • 访问实例属性
  print(dog1.name)
print(dog2.breed)
  
  1. 打印dog1name属性,输出"Buddy"
  2. 打印dog2breed属性,输出"Bulldog"
  • 调用实例方法:bark
  dog1.bark()
dog2.bark()
  
  1. 调用dog1bark方法,输出"Buddy says: Woof!"
  2. 调用dog2bark方法,输出"Max says: Woof!"
  • 设置和获取动态属性age
  dog1.setAge(3)
print(dog1.getAge())
  
  1. 使用setAge方法给dog1设置age属性为3
  2. 使用getAge方法获取dog1age属性,输出3
  • 访问类属性
  print(Dog.species)
print(dog1.species)
print(dog2.species)
  
  1. 直接通过类名Dog访问类属性species,输出"Canis familiars"
  2. 通过实例dog1dog2访问类属性species,输出结果仍然是"Canis familiars",因为类属性是共享的。
  • 调用类方法
  print(Dog.describe())
print(dog1.describe())
print(dog2.describe())
  
  1. 通过类名Dog调用类方法describe,返回species的值,输出"Canis familiars"
  2. 通过实例dog1dog2调用类方法,效果与通过类名调用相同,输出"Canis familiars"

类的私有属性和私有方法

在 Python 中,类的私有属性和私有方法是一种约定,通过前缀使用双下划线__来标识,命名规则如__attribute_name__method_name。如果希望某个属性是私有的,使用单下划线_更加常见,表示“受保护”。但这种方法不能够完全阻止外部访问。实际上,Python 没有真正意义上的私有成员概念,但提供了名称改写(name mangling),以避免外部代码直接访问。双下划线会触发名称改写机制,使得私有属性和私有方法的名称被修改为_ClassName__AttributeName的形式,从而减少了命名冲突的危险。因此可以通过名称改写机制访问,如obj._MyClass__private_attr。但并不推荐使用这种方法,而是使用gettersetter方法来访问或修改私有属性。然而,过多的私有成员可能使代码难以测试和维护,只有在明确需要保护实现细节时才使用私有成员。

  class MyClass:
    def __init__(self):
        self.__private_attr = 43 # 私有属性
        self._protected_attr = "This is a convention-prottected attribute" # 

    def get_private_attr(self):
        return self.__private_attr # 通过方法访问私有属性

    def set_private_attr(self, value):
        self.__private_attr = value

    # 私有方法
    def __private_method(self):
        print("This is a private mehotd.")

    def public_method(self):
        self.__private_method() # 类内调用私有方法


obj = MyClass()
print(obj._MyClass__private_attr) # 可以通过名称改写机制访问,但不推荐

obj.set_private_attr(97) # 设置私有属性
print(obj.get_private_attr()) # 通过方法访问私有属性
  
  🡮
43
97
  

C++ 使用 class关键字定义类,后接类名,类名通常使用 PascalCase(每个单词首字母大写)的命名约定,例如MyClass。类包括的访问修饰符有只能在类内访问的private;可以从类外部访问的public;及在继承中对子类可见的protected。构造函数的名称与类名相同,且没有返回值,用于传入参数和初始化对象的属性。定义类时包含属性(成员变量)和行为(成员函数),实例化对象后,即使用类创建对象后,可以通过对象访问类的公有成员。

关键字static定义类中的静态成员变量(Static Member Variable),为对所有对象共享。但,静态成员变量需要在类外进行定义和初始化;关键字static定义类中的静态成员函数,其可以通过类名直接调用,而不需要实例化对象。静态成员函数与具体的对象无关,因此无法使用this指针,不能是虚函数(virtual),也只能访问静态成员变量或其他静态成员函数,不能访问非静态成员。

  #include <iostream>
#include <format>

// 定义类
class Dog {
// 私有成员变量和成员函数
private:
	int age;
// 公有成员变量和成员函数
public:
    //成员变量
	static std::string species; // 静态成员变量
	std::string name;
	std::string breed;

    // 构造函数
	Dog(const std::string& name, const std::string& breed) {
		this->name = name;
		this->breed = breed;
		this->age = 0;
	}

    //成员函数
	void bark() const {
		std::cout << std::format("{} syas: Woof!\n", name);
	}

    //设置年龄
	void setAge(int age) {
		this->age = age;
	}

    // 获取年龄
	int getAge() const {
		return age;
	}

    // 静态成员函数
	static std::string describe() {
		return species;
	}

};

std::string Dog::species = "Canis familiaris";

int main() {
    // 实例化对象
	Dog dog1("Buddy", "Golden Retriever");
	Dog dog2("Max", "Bulldog");

	std::cout << dog1.name << std::endl;
	std::cout << dog2.name << std::endl;

	dog1.bark();
	dog2.bark();

    // 修改属性
	dog1.setAge(3);
	std::cout << dog1.getAge() << std::endl;

    // 访问静态成员变量
	std::cout << Dog::species << std::endl;
	std::cout << dog1.species << std::endl;
	std::cout << dog2.species << std::endl;

    // 调用静态成员函数
	std::cout << Dog::describe() << std::endl;
	std::cout << dog1.describe() << std::endl;
	std::cout << dog2.describe() << std::endl;

	return 0;
}
  
  🡮
Buddy
Max
Buddy syas: Woof!
Max syas: Woof!
3
Canis familiaris
Canis familiaris
Canis familiaris
Canis familiaris
Canis familiaris
Canis familiaris
  

🤖 逐行解释代码

类的定义

  class Dog {
private:
    ...
public:
    ...
};
  
  • 成员变量
  1. int age(私有成员):用于存储狗的年龄。
  2. static std::string species(静态成员):表示狗的物种名称,所有类实例共享此属性。
  3. std::string namestd::string breed(公有成员):分别表示狗的名字和品种。
  • 构造函数
  Dog(const std::string& name, const std::string& breed)
  
  1. 初始化Dog类的实例。
  2. 使用this->关键字将参数namebreed赋值给当前对象的成员变量。
  3. 初始化age为0.
  • 成员函数
  1. void bark() const:打印出狗的名字和它发出的叫声。const表示该函数不会修改类成员变量。
  2. void setAge(int age):设置狗的年龄。
  3. int getAge() const:返回狗的年龄。const表示该函数不会修改类成员变量。
  4. static std::string describe():静态成员函数,用于返回静态成员变量species的值。静态成员不依赖于具体对象,可以通过类名直接掉用。

静态成员变量初始化

  std::string Dog::species = "Canis familiaris";
  
  1. 定义并初始化species。必须在类的外部对静态成员变量进行初始化。

主函数

  int main() {
    Dog dog1("Buddy", "Golden Retriever");
    Dog dog2("Max", "Bulldog");
  
  1. 创建两个Dog对象:dog1名字为"Buddy",品种为"Golden Retriever"dog2名字为"Max",品种为"Bulldog"
  • 访问实例属性
      std::cout << dog1.name << std::endl;
    std::cout << dog2.name << std::endl;
  
  1. 打印dog1dog2的名字。
  • 调用成员函数
      dog1.bark();
    dog2.bark();
  
  1. 掉用dog1dog2bark方法。
  • 设置和获取年龄
      dog1.setAge(3);
    std::cout << dog1.getAge() << std::endl;
  
  1. 设置dog1的年龄为3
  2. 使用getAge方法获取并打印输出3.
  • 访问静态变量(属性)
      std::cout << Dog::species << std::endl;
    std::cout << dog1.species << std::endl;
    std::cout << dog2.species << std::endl;
  
  1. 直接通过类名Dog访问静态成员变量species,输出Canis familiaris
  2. 通过对象dog1dog2访问species,输出结果同。
  • 调用静态成员函数
      std::cout << Dog::describe() << std::endl;
    std::cout << dog1.describe() << std::endl;
    std::cout << dog2.describe() << std::endl;
  
  1. 通过类名Dog调用静态方法describe,输出species的值,输出为Canis familiaris
  2. 通过对象dog1dog2调用静态方法,输出结果同。

在 C# 中,用class关键字定义类,后接类名,类名通常使用 PascalCase(每个单词首字母大写)的命名约定,例如MyClass。构造函数可用于传入值(如果带参数)和初始化对象的字段,其名称与类名同,并没有返回值。如果不显示定义, C# 会自动提供一个默认的无参构造函数。C# 的访问修饰符用于控制类成员的访问权限,private为只能在类内部访问(默认);public则可以从类外部访问;protected为子类可访问;internal为同一程序集内可访问;protected internal为同一程序集或子类中可访问。

  using System;

// 定义类
class Dog
{
    // 静态变量
    public static string species = "Canis familiaris"; 

    // 实例成员变量
    public string name;
    public string breed;
    private int age;

    // 构造方法
    public Dog(string name, string breed)
    {
        this.name = name;
        this.breed = breed;
        this.age = 0; // 设置默认值
    }

    // 狗叫的实例方法
    public void Bark()
    {
        Console.WriteLine($"{this.name} says: Woof!");
    }

    // 设置狗的年龄
    public void SetAge(int age)
    {
        this.age = age;
    }

    // 访问狗的年龄
    public int GetAge()
    {
        return this.age;
    }

    // 描述物种的静态方法
    public static string Describe()
    {
        return species;
    }

}

class Program
{    
    static void Main()
    {
        // 创建 Dog 的实例
        Dog dog1 = new Dog("Buddy", "Golden Retriever");
        Dog dog2 = new Dog("Max", "Bulldog");

        // 访问狗的名字和品种
        Console.WriteLine(dog1.name);
        Console.WriteLine(dog2.breed);

        // 调用狗的实例方法(狗吠)
        dog1.Bark();
        dog2.Bark();    

        // 设置和打印狗的年龄
        dog1.SetAge(3);
        dog1.GetAge();

        // 打印静态变量
        Console.WriteLine(Dog.species);
        //Console.WriteLine(dog1.species); // 实例化对象不能访问静态变量

        // 调用静态方法
        Console.WriteLine(Dog.Describe()); 
       // Console.WriteLine(dog1.Describe());  // 实例化方法不能调用静态方法
    }
}
  
  🡮
Buddy
Bulldog
Buddy says: Woof!
Max says: Woof!
Canis familiaris
Canis familiaris
  

🤖 逐行解释代码

  • 引入命名空间
  using System;
  
  1. 引入System命名空间,其包含基础的类库,如Console类,用于处理控制台输入和输出。
  • 定义Dog
  class Dog
{
    public static string species = "Canis familiaris";
  
  1. 定义一个Dog类,为狗的属性和行为。
  2. species是一个由static关键字定义的静态成员变量,属于类本身,而不是类的实例。其初始值为"Canis familiaris",表示狗的物种名称。
  • 实例成员变量
      public string name;
    public string breed;
    private int age;
  
  1. namebreed是实例变量,分别表示狗的名字和品种。这些成员变量使用public关键字,使其对外部是公开的,为公有成员变量。
  2. age使用private关键字,是私有成员变量,表示狗的年龄,只有类的内部方法可以访问。
  • 构造函数
      public Dog(string name, string breed)
    {
        this.name = name;
        this.breed = breed;
        this.age = 0;
    }
  
  1. 构造函数Dog(string name, string breed)用来初始化Dog类的实例。
  2. this.namethis.breed是通过构造函数的参数namebreed来初始化实例变量。
  3. this.age = 0;age实例变量初始化为0。
  • Bark方法
      public void Bark()
    {
        Console.WriteLine($"{this.name} says: Woof!");
    }
  
  1. Bark是一个实例方法,描述狗叫的行为。
  2. 使用Console.WriteLine输出狗的名字和叫声"says: Woof!"
  • SetAge 方法
      public void SetAge(int age)
    {
        this.age = age;
    }
  
  1. SetAge是一个实例方法,用来设置狗的年龄。
  2. 输入参数为age,将其赋值给实例的age变量。
  • GetAge方法
      public int GetAge()
    {
        return this.age;
    }
  
  1. GetAge是一个实例方法,用于获取狗的年龄。
  2. 返回实例的age变量的值。
  • 静态 Describe 方法
      public static string Describe()
    {
        return species;
    }
  
  1. Describe是一个静态方法,可以通过类名直接调用,而不需要实例化对象。
  2. 该方法返回静态成员species,表示狗的物种名称。
  • Program 类
  class Program
{
    static void Main()
    {
  
  1. Program是主程序类,其中包含Main方法,作为程序的入口点。
  2. static关键字表示该方法是静态的,可以直接由 CLR(公共语言运行时)调用,而不需要创建Program类的实例。
  • 创建实例和输出
          Dog dog1 = new Dog("Buddy", "Golden Retriever");
        Dog dog2 = new Dog("Max", "Bulldog");

        Console.WriteLine(dog1.name);
        Console.WriteLine(dog2.breed);
  
  1. 创建了两个Dog对象dog1dog2,分别表示一只名为"Buddy"、品种为"Golden Retriever"的狗和一只名为"Max"、品种为"Bulldog"的狗。
  2. 使用Console.WriteLine输出dog1的名字(name)和dog2的品种(breed)。
  • 调用Bark方法
          dog1.Bark();
        dog2.Bark();
  
  1. 调用dog1dog2Bark方法。
  • 设置和获取年龄
          dog1.SetAge(3);
        dog1.GetAge();
  
  1. 调用dog1.SetAge(3),将dog1的年龄设置为3
  2. 调用dog1.GetAge(),获取dog1的年龄,为返回值。
  • 访问静态变量和方法
          Console.WriteLine(Dog.species);
        //Console.WriteLine(dog1.species);
  
  1. 使用Console.WriteLine(Dog.species);输出species静态变量的值,即"Canis familiaris"
  2. 注释掉了//Console.WriteLine(dog1.species);代码行,因为species属于静态成员,可以通过类名直接访问,而不是通过实例访问。
  • 调用静态 Describe 方法
          Console.WriteLine(Dog.Describe());
        // Console.WriteLine(dog1.Describe());
    }
}
  
  1. 使用Console.WriteLine(Dog.Describe());调用类方法Describe(),输出静态成员species的值。
  2. 注释掉了// Console.WriteLine(dog1.Describe());代码行,因为Describe是静态方法,不需要通过实例对象调用。

Static解释

static关键字可用于声明类的静态成员、静态类和静态构造方法。静态成员中的静态变量和静态方法属于类本身,而不是类的实例,因此不需要通过实例化对象来访问或调用,而是通过类本身来调用,如上述示例代码。

静态类只能包含静态成员,不能实例化。静态类通常用于封装与对象状态无关的功能。示例中MathUtilities是静态类,不能创建该类的实例,所以方法和变量都是静态的。

  using System;

public static class MathUtilities
{
    public static double Pi = 3.14;
    public static double Square(double x)
    {
        return x * x;   
    }
}

class Program
{    
    static void Main()
    {
        Console.WriteLine(MathUtilities.Pi);
        Console.WriteLine(MathUtilities.Square(5));
    }
}
  
  🡮
3.14
25
  

静态构造函数用于初始化静态成员,并且只会在类的第一次访问时执行一次。静态构造函数没有访问修饰符,不能接受参数,通常用于静态字段的初始化。

  using System;

public class Example
{
    public static int counter;

    // 静态构造函数
    static Example()
    {
        counter = 10;
        Console.WriteLine("Static constructor called.");
    }

    public static void ShowCounter()
    {
        Console.WriteLine($"Counter: {counter}");
    }
}

class Program
{    
    static void Main()
    {
        Example.ShowCounter();
    }
}
  
  🡮
Static constructor called.
Counter: 10
  

静态属性类似于静态变量,通常提供对静态字段的封装访问。示例代码中count是静态属性,可以通过类名来访问和修改。

  using System;

public class Counter
{
    private static int _count;

    public static int Count
    {
        get { return _count; }
        set { _count = value; }
    }
}

class Program
{    
    static void Main()
    {
            Counter.Count = 7;
            Console.WriteLine(Counter.Count);
    }
}
  
  🡮
7
  

🤖 逐行解释代码

  • 定义 Counter 类
  public class Counter
{
    private static int _count;
  
  1. 定义一个Counter类,使用public访问修饰符,表示该类对所有代码可见。
  2. 定义一个私有变量_count,用于存储计数值。static表示这个变量属于类本身,而不是属于类的某个实例,所有实例共享这一变量。使用下划线_作为私有字段的命名约定,表明它是类的内部实现细节。
  • 定义 Count 属性
      public static int Count
    {
        get { return _count; }
        set { _count = value; }
    }
  
  1. 定义一个公有的静态属性Count,对外部代码公开。
  2. get访问器(getter)用于返回_count的值。外部代码通过Counter.Count读取当前的计数值。
  3. set访问器(setter)用于修改_count的值。外部代码通过Counter.Count = value;设置新的计数器。

静态属性Count和静态字段_count的区别在于,_count是类内部使用的实际存储变量;Count是对外的接口,提供对_count的受控访问。

继承和多态

  • 继承

在面向对象编程(OOP)中,继承是一个核心概念,但不同编程语言在实现继承时存在语法和行为上的差异。在 Python 中,继承通过在类定义时使用括号指定父类来实现。子类可以重写父类的方法,并使用super()调用父类方法(如super().speak())。C++ 中的继承通过在类声明中使用:来指定基类。C++ 支持公有继承(public)、保护继承(protected)和私有继承(private)。方法可以被重写,并且使用virtual关键字启动运行时多态,配合override重写基类的虚函数。C# 的继承通过冒号:类指定基类,方法可以使用virtual关键字声明虚方法,子类可以使用override关键字重写虚方法,并支持多态。

特性 Python C++ C#
继承类型 单继承和多(重)继承 单继承和多(重)继承 单继承和接口实现多(重)继承
默认基类 object 无默认基类 System.Object
访问控制 没有明确访问修饰符 publicprotectedprivate publicprotectedprivate
多继承支持 支持(MRO 管理) 支持(但易出现菱形继承问题) 不支持,使用接口实现
内存管理 自动 手动 自动
多态支持 动态(通过 duck typing) 静态和动态(virtual/override 静态和动态(virtual/override
  • 多态

多态是面向对象编程的一个重要特性,指的是同一操作作用于不同类型对象时表现出不同的行为。通过多态,父类指针或引用可以指向子类对象,并调用被重写的方法。多态可以分为运行时多态(动态多态)和编译时多态(静态多态)。

特性 Python C++ C#
支持的多态类型 运行时多态(通过 duck typing) 运行时多态(虚函数/重写) + 编译时多态(函数重载/模板) 运行时多态(虚方法/重写)+ 编译时多态(方法重载/泛型)
动态绑定 自动 通过虚函数和动态绑定 通过虚方法和动态绑定
关键字 无显示关键字,动态解析 virtual(基类),override(派生类) virtual(基类),override(派生类)
多态的实现方式 方法重写、duck typing 通过虚函数和重写 通过虚方法和重写
编译时多态 不支持 支持(函数重载、模板) 支持(方法重载、泛型)

Python 中的多态被称为 duck typing(鸭子类型)是因为其基于对象的行为而非其类型。在 Python 中,如果一个对象像鸭子一样游泳、嘎嘎叫,就可以认为它是鸭子。这个概念用来形容 Python 中的动态类型和多态性,即只要对象实现了必要的方法或属性,就能够被当作某个类型来使用,而不关心其具体的类型是什么。这个术语来源于一句谚语“If it looks like a duck and quacks like a duck, it's a duck”。

Python 中,OOP 中的继承允许一个类从另一个类继承属性和方法。继承使得代码复用变得容易,并也支持多态等其他 OOP 特性。继承的基本概念包括父类(Base Class/Superclass),被继承的类;和子类(Drived Class/subclass),继承父类的类。

  • 单继承
  # 定义父类(基类)
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} makes a sound."

# 定义子类(派生类)
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name) # 调用父类的构造方法
        self.breed = breed

    def speak(self): # 子类重新父类的方法
        print(f"{self.name} barks.")


class Cat(Animal):
    def speak(self): # 子类重新父类的方法
        print(f"{self.name} meows.")


dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers")

dog.speak()
cat.speak()
  
  🡮
Buddy barks.
Whiskers meows.
  

🤖 要点释义

  1. super():用于调用父类的方法。例如在子类中的构造函数中,使用super().__init__(name)来调用父类的构造方法。
  2. 方法重写(Override):子类可以重写父类的方法,修改其行为,例如子类重新父类的speak()方法。
  3. __init__构造方法:子类可以重写父类的构造方法,以便根据需要初始化子类特有的属性。通过super()可以调用父类的__init__方法,确保父类的初始化逻辑被执行。
  • 多(重)继承

Python 支持多重继承,即一个子类可以继承多个父类。在多重继承中,类的继承顺序非常重要,其遵循方法解析顺序(Method Rsolution Order,MRO)。MRO 确定了在调用方法时,Python 会按照什么顺序去查找方法和属性,尤其当涉及多个父类时。可以使用类的mro()方法或__mro__属性查看类的 MRO 顺序。

  class Animal:
    def __init__(self, name):
        self.name = name


class Mammal:
    def give_birth(self):
        print(f"{self.name} gives birth.")


class Dog(Animal, Mammal):
    def __init__(self, name, breed):
        Animal.__init__(self, name)
        self.breed = breed

    def speak(self):
        print(f"{self.name} barks.")


dog = Dog("Buddy", "Golden Retriever")
dog.speak()
dog.give_birth()

print(Dog.mro())
  
  🡮
Buddy barks.
Buddy gives birth.
[<class '__main__.Dog'>, <class '__main__.Animal'>, <class '__main__.Mammal'>, <class 'object'>]
  

C++ 中,OOP 中的继承是将一个类的属性和方法,即成员变量和成员函数传递到另一个类的机制。继承允许在已有类基础上创建新类,增强代码复用性。继承的基本概念包括基类(Base Class),被继承的类和派生类(Derived Class),从基类继承的类。

  • 单继承
  #include <iostream>
#include <format>

// 基类
class Base {
public:
    int baseValue;

    Base(int val) : baseValue(val) {} // 基类构造函数

    virtual void display() { // 使用 virtual(虚函数),允许派生类重写该方法
        std::cout << std::format("Base class value: {}\n", baseValue);
    }
};

// 派生类
class Derived : public Base { // 公有继承
public:
    int derivedValue;

    Derived(int baseVal, int derivedValue) : Base(baseVal), derivedValue(derivedValue) {} // 派生类构造函数

    void display() override { // 使用 override,重写基类的虚函数 display
        std::cout << std::format("Derived class value: {}\n", derivedValue);
    }
};

int main() {
    Derived instance(10, 20); // 创建 Derived 类对象 instance

    // 调用 display 方法
    instance.display();

    return 0;
}
  
  🡮
Derived class value: 20
  

🤖 逐行解释代码

  • 定义基类,及成员变量
  1. class Base { ... };:定义一个名为Base的类,表示基类。
  2. public::公共访问修饰符,接下来的成员对外部代码可访问。
  3. int baseValue;:定义一个整型成员变量baseValue,表示基类的值。
  • 基类的构造函数
  1. Base(int val): baseValue(val) {}:与类名同来定义构造函数,接受一个参数val;用初始化列表: baseValue(val) {},直接初始化成员变量baseValue,提高效率。
  • 虚函数
  1. virtual void display():定义虚函数display,用于输出baseValue的值。由virtual关键字定义的虚函数允许派生类重写该方法,并支持运行时多态。
  2. std::format("Base class value: {}\n", baseValue):使用std::format格式化输出baseValuestd::cout将格式化后的字符打印到控制台。
  • 定义派生类,及成员变量
  1. class Derived : public Base { ... };:定义派生类Derived,继承自基类Base。使用public Base表示公开继承,基类的public成员和方法在派生类中仍然为public
  2. int derivedValue;:定义一个整型成员变量derivedValue,表示派生类的特有值。
  • 派生类构造函数
  1. Derived(int baseVal, int derivedValue):构造函数接受两个参数,baseVal(传递给基类的值)和derivedValue(派生类的值)。
  2. : Base(baseVal):调用基类的构造函数,初始化baseValue.
  3. , derivedValue(derivedValue):使用初始化列表初始化派生类的成员变量derivedValue
  • 重写虚函数
  1. void display() overrideoverride关键字明确声明该方法是对基类虚函数的重写,以增强代码的可读性和安全性。重写基类的display虚函数。
  • 实例化对象和调用方法
  1. Derived instance(10, 20);:创建Derived类的对象instance。调用Derived类的构造函数初始化Base::baseValuebaseVal = 10)和Derived::derivedValuederivedValue = 20)。
  2. instance.display();,调用instance对象的display方法。由于Derived类重写了display,运行时调用的是派生类的版本。

又例如,

  #include <iostream>
#include <format>

// 基类
class Animal {
public:
    Animal(const std::string& name):name(name){}

    virtual void speak() {
        std::cout << std::format("{} makes a sound.\n", name);
    }

protected:
    std::string name;
};

// 派生类
class Dog : public Animal {
public:
    Dog(const std::string& name,const std::string& breed):Animal(name),breed(breed){}

    void speak()override {
        std::cout << std::format("{}, barks!\n", name);
    }
private:
    std::string breed;
};



int main() {
    Dog dog("Buddy", "Golden Retriever");
    dog.speak();

    return 0;
}
  
  🡮
Buddy, barks!
  

🤖 要点释义

  1. 继承方式有公有继承(public)、保护继承(protected)和私有继承(private)。公有继承为派生类公开继承基类的公有成员,是最常见的继承类型;保护继承为派生类以受保护的方式继承基类的公有和保护成员;私有继承为派生类以私有的方式继承基类的公有和保护成员。
  2. 构造函数与初始化列表:在派生类构造函数中,必须显示地调用基类地构造函数来初始化基类部分。并通过初始化列表来初始化基类部分。
  3. 方法重写(Override):派生类可以重写(覆盖)父类的方法,例如speak()方法在Dog类中被重写。override关键字可以明确告诉编译器正在重写基类的虚函数(virtual)。基类的方法如果声明为virtual,则可以实现运行时多态,意味着通过基类指针或引用调用派生类的方法时,实际调用的是派生类的方法,而不是基类中的方法。
  • 多(重)继承

C++ 支持多重继承,一个派生类可以继承多个基类,之间使用逗号分隔。但多重继承可能会导致“菱形继承问题”(或“钻石问题”),为多个基类继承自同一祖先类,而派生类又同时继承这些基类,导致派生类中会有多个相同的基类成员副本(冗余继承);派生类无法明确选择哪一个基类成员(歧义问题)。为了解决菱形问题,C++ 提供了虚继承(virtual inheritance),使基类的成员在派生类中只有一份共享的副本,语法为class DerivedClass : virtual public BaseClass { };。多重继承实现了代码复用,也更贴近某些场景中一个类具有多基类的特性。但是也带来了复杂性,可维护性差和性能影响等问题。因此,尽量避免不必要的多重继承,优先选择单继承或组合;并使用虚继承来解决菱形继承问题;明确指定基类作用域,避免成员访问的歧义;并对复杂继承关系进行清晰的设计和注释,确保代码可读性。

  #include <iostream>
#include <format>

class A {
public:
    void displayA() {
        std::cout << "Class A" << std::endl;
    }
};

class B {
public:
    void displayB() {
        std::cout << "Class B" << std::endl;
    }
};

class C : public A, public B {
public:
    void displayC() {
        std::cout << "Class C" << std::endl;
    }
};


int main() {
    C obj;
    obj.displayA();
    obj.displayB();
    obj.displayC();

    return 0;
}
  
  🡮
Class A
Class B
Class C
  
  • 析构函数

析构函数(Destructor)是一个特殊的成员函数,用于在对象的生命周期结束时自动执行清理工作,用来释放对象占用的资源,例如内存、文件句柄或网络连接等。析构函数的名称与类名同,但是需要在前面加一个波浪号~,以区分构造函数,例如~Base();析构函数不能接受参数,也不能返回任何值;当对象的生命周期结束时,析构函数会被自动调用,无需手动;且析构函数不能被重载。

  #include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "Constructor called!" << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructor called!" << std::endl;
    }
};


int main() {
    MyClass obj; // 创建对象,调用构造函数
    std::cout << "Inside main function." << std::endl;
    // main 函数结束,obj 的析构函数被调用
    return 0;
}
  
  🡮
Constructor called!
Inside main function.
Destructor called!
  

需要注意,不要在析构函数中抛出异常,这可能会导致未定义的行为,尤其在栈展开(stack unwinding)过程中。当对象数组销毁时,析构函数会被依次调用。

  #include <iostream>
#include <format>

class MyClass {
public:
    MyClass() {
        std::cout << "Constructor called!" << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructor called!" << std::endl;
    }
};


int main() {
    MyClass arr[3];
    std::cout << "Inside main function." << std::endl;

    return 0;
}
  
  🡮
Constructor called!
Constructor called!
Constructor called!
Inside main function.
Destructor called!
Destructor called!
Destructor called!
  

如果一个类有多态行为(即通过基类指针或引用操作派生类对象),析构函数应声明为虚函数(virtual),成为虚析构函数,以确保销毁对象时调用正确的析构函数。

  #include <iostream>
#include <format>

class Base {
public:
    Base() {
        std::cout << "Constructor called!" << std::endl;
    }

    virtual ~Base() { std::cout << "Base destructor" << std::endl; }
};

class Derived : public Base {
    ~Derived() override { std::cout << "Derived destructor" << std::endl; }
};


int main() {
    Base* obj = new Derived();
    delete obj;

    return 0;
}
  
  🡮
Constructor called!
Derived destructor
Base destructor
  

在 C# 中,继承是面向对象编程(OOP)的核心特征之一,允许一个类继承另一个类的成员(属性和方法),从而实现代码的复用、扩展和多态性。OOP 的基本概念包括基类(Base Class/Parent Class)和派生来(Derived Class/Child Class)。C# 中的继承使用:来表示,派生类可以继承基类的公有和受保护成员。

  • 单继承
  using System;

class BaseClass
{
    public string Name { get; set; }

    public void Display()
    {
        Console.WriteLine($"Name: {Name}");
    }
}

class DerivedClass : BaseClass
{
    public int Age { get; set; }

    public void Show()
    {
        Console.WriteLine($"Name: {Name}; Age: {Age}");
    }
}


class Program
{    
    static void Main()
    {
        // 创建 BaseClass 类的实例,设置字段值,并调用 Show 和 Display 方法
        DerivedClass derivedObj=new DerivedClass();
        derivedObj.Name = "Bob";
        derivedObj.Age = 29;
        derivedObj.Show();

        derivedObj.Display();  
    }
}
  
  🡮
Name: Bob; Age: 29
Name: Bob
  

🤖 逐行解释代码

  • 定义基类BaseClass和属性Name及方法Display
  1. class BaseClass { ... }:定义一个名为BaseClass的类,这个类包含一个属性Name和一个方法Display
  2. public string Name { get; set; }:定义一个public的自动实现属性Name,类型为string。自动属性意味着 C# 会自动为该属性提供一个私有字段,并提供默认的getset访问器。get访问器返回字段的值;set访问器设置字段的值。
  3. public void Display():定义一个公有方法Display,没有返回值(void),仅使用Console.WriteLine($"Name: {Name}");输出格式化的字符串,包含Name字段的值。
  • 定义派生类DerivedClass和属性Age及方法Show
  1. class DerivedClass : BaseClass { ... }:定义一个名为DerivedClass的类,继承自基类BaseClass,意味着DerivedClass拥有BaseClass的所有公有(公共)成员(包括Name字段和Display方法)。DerivedClass还新增了一个Age属性和一个show方法。
  2. public int Age { get; set; }:定义一个public的自动实现属性Age,类型为int。这个属性用于存储派生类中的年龄数据。
  3. public void Show():定义了一个公共方法Show,没有返回值(void),仅用Console.WriteLine($"Name: {Name}; Age: {Age}");打印NameAge字段值。

又例如,

  using System;

// 基类
class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    public virtual void Speak()
    {
        Console.WriteLine($"{Name} makes a sound.");
    }
}

// 派生类
class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public override void Speak() // 重写基类方法
    {
        Console.WriteLine($"{Name} barks.");
    }
}

// 派生类
class Cat : Animal
{
    public Cat(string name) : base(name) { }

    public override void Speak() // 重写基类方法
    {
        base.Speak();
        Console.WriteLine($"{Name} meows.");
    }
}


class Program
{    
    static void Main()
    {
        Animal dog = new Dog("Budyy");
        Animal cat = new Cat("Whiskers");

        dog.Speak();
        cat.Speak();
    }
}
  
  🡮
Budyy barks.
Whiskers makes a sound.
Whiskers meows.
  

🤖 要点释义

  1. 构造函数与基类调用:派生类的构造函数通常需要调用基类的构造函数,用base关键字实现。
  2. 方法重写:在基类中用virtual关键字声明可以被重写的方法;在派生类中使用override关键字重写该方法;如果派生类不希望其方法在被重写,可以使用sealed关键字,如public sealed override void Speak() {}
  3. base关键字:用于访问和调用基类的成员(属性或方法),如base.Speak();
  4. 多态(Polymorphisum):多态通过基类引用调用派生类对象的方法。使用虚方法(virtual)和重写(override)实现运行时多态。
  5. 访问修饰符:基类的publicprotected成员可以被派生类访问;private成员不能被派生类直接访问;protected修饰符允许派生类访问,但外部类无法访问。
  • 接口(Interface)与继承

接口(Interface)和多重继承(Multiple Inheritance)是常见的面向对象编程概念。C# 不支持类的多重继承(即一个类直接继承多个类),但是支持实现多个接口。这种机制允许获得类似多重继承的功能,同时避免多重继承带来的复杂性和潜在问题。接口是定义一组方法签名(没有实现)的结构,任何类或结构体都可以实现一个或多个接口。接口可以包含方法、属性、事件和索引器等成员,但不能包含字段。接口的特点有,不能包含实现,只有方法签名;类必须实现接口中定义的所有方法(除非类是抽象类);一个类可以实现多个接口,相当于多重继承;接口本身可以继承多个其他接口。

  using System;

// 定义接口
public interface IDrivable
{
    void Drive(); // 接口方法没有实现
}

public interface IFlyable
{
    void Fly(); // 接口方法没有实现
}

// 实现多个接口
public class Vehicle : IDrivable, IFlyable
{
    public void Drive()
    {
        Console.WriteLine("Driving the vehicle");
    }

    public void Fly()
    {
        Console.WriteLine("Flying the vehicle");
    }
} 

class Program
{    
    static void Main()
    {
        Vehicle v = new Vehicle();
        v.Drive(); // 调用 Drive 方法
        v.Fly(); // 调用 Fly 方法
    }
}
  
  🡮
Driving the vehicle
Flying the vehicle
  
  1. IDriveableIFlyable是两个接口,分别定义了DriveFly方法。
  2. Vehicle类同时实现了IDriveableIFlyable这两个接口,并提供了具体实现。
  3. Main方法中,创建了Vehicle类的对象v,然后调用了Drive()Fly()方法。

接口可以继承其他接口。这样一个接口可以继承多个接口的方法签名。

  using System;

// 定义基础接口
public interface IShape
{
    void Draw();
}

public interface IColorable
{
    void Color();
}

// 定义一个继承多个接口的接口
public interface IColoredShape : IShape, IColorable
{
    void GetShapeDetails();
}

// 实现接口
public class Circle : IColoredShape
{
    public void Draw()
    {
        Console.WriteLine("Drawing Circle");
    }

    public void Color()
    {
        Console.WriteLine("Coloring Circle");
    }

    public void GetShapeDetails()
    {
        Console.WriteLine("This is a colored circle");
    }
}

class Program
{
    static void Main()
    {
        Circle c = new Circle();
        c.Draw(); // 绘制圆形
        c.Color(); // 给圆形上色
        c.GetShapeDetails(); // 获取圆形细节
    }
}
  
  🡮
Drawing Circle
Coloring Circle
This is a colored circle
  
  1. IShapeIColorable是两个基础接口。
  2. IColoredShape继承自这两个接口,要求实现它们的方法。
  3. Circle类实现了IColoredShape接口,并提供了相应的实现。

C# 中, 析构函数(Destructor)是一种特殊的成员方法,用于在对象销毁之前执行清理工作,通常用于释放非托管资源,例如文件句柄、数据库连接或其他系统资源等。C# 的垃圾回收机制会自动管理托管资源的内存,因此不需手动释放托管对象,但对于非托管资源,依然需要显示清理。如果类持有非托管资源,推荐实现 IDisposable 接口,并在 Dispose 方法中显示释放资源,避免依赖析构函数。

抽象

抽象是面向对象编程(OOP)的一个重要概念,指的是隐藏对象的具体实现。通过提供一个简洁的接口,让用户只关注功能的使用,而无需了解内部实现细节。抽象通常通过抽象类和接口来实现。

特性 Python C++ C#
定义抽象类 class Animal(ABC) class Animal {virtual void speak() = 0;}; abstract class Animal {}
抽象方法 @abstractmethod def speak(self): virtual void speak() = 0 public abstract void Speak();
抽象类实例化 不能实例化抽象类 不能实例化包含纯虚函数的类 不能实例化抽象类
继承与实现 子类必须实现抽象方法 子类必须实现纯虚函数 子类必须实现抽象方法
已实现方法 可以包含普通方法 可以包含已实现方法 可以包含已实现方法
关键字 @abstractmethodABC virtual= 0 abstractoverride

在 Python 中,抽象类是通过 abc(Abstract Base Class)模块来实现的。抽象类(Abstract Class)是不能实例化的类,其定义了一个接口(方法),但这些方法并没有具体实现,子类必须实现这些抽象方法。具体实现是使用ABC类(abc.ABC)和@abstractmethod装饰器来定义抽象类和抽象方法。抽象类可以包含非抽象方法(既有实现的方法)。

  from abc import ABC, abstractmethod


class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass


class Dog(Animal):
    def speak(self):
        return "Woof"


class Cat(Animal):
    def speak(self):
        return "Meow"


# animal = Animal() # 错误,不能实例化抽象类,返回错误信息为 TypeError: Can't instantiate abstract class Animal without an implementation for abstract method 'speak'
dog = Dog()
cat = Cat()

print(dog.speak())
print(cat.speak())
  
  🡮
Woof
Meow
  
  1. Animal类是抽象类,不能直接实例化。
  2. speak()方法是抽象方法,必须由子类实现。
  3. @abstractmethod装饰器标记抽象方法,确保子类实现这些方法。

在 C++ 中,抽象类通过纯虚函数(pure virtual function)来实现。一个包含至少一个纯虚函数的类就是一个抽象类,不能实例化。纯虚函数使用= 0语法类定义。派生类必须实现所有的纯虚函数,除非子类本身也是抽象类。

  #include <iostream>
#include <format>

class Animal {
public:
    virtual void speak() = 0; // 纯虚函数
    virtual ~Animal(){} // 虚析构函数
};

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

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


int main() {
    // Animal animal; // 错误:不能实例化抽象类,返回错误信息为,object of abstract class type "Animal" is not allowed
    Animal* dog = new Dog();
    Animal* cat = new Cat();

    dog->speak();
    cat->speak();

    delete dog;
    delete cat;

    return 0;
}
  
  🡮
Woof
Meow
  
  1. speak()是一个纯虚函数,Animal类是抽象类。
  2. DogCat必须实现speak()方法,才能实例化。
  3. 纯虚函数使用= 0来定义,表示该函数没有实现,必须由子类实现。

在 C# 中,抽象类使用abstract关键字定义。抽象类可以包含抽象方法和已实现的方法。抽象方法在类中声明,但没有实现,必须在子类中实现。使用abstract关键字定义抽象类和抽象方法。抽象类可以包含已经实现的方法,派生类可以选择重写这些方法。

  using System;

abstract class Animal
{
    public abstract void Speak(); // 抽象方法

    public void Move()
    {
        Console.WriteLine("Moving...");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Woof");
    }
}

class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Meow");
    }
}

class Program
{
    static void Main()
    {
        // Animal animal = new Animal(); // 错误:不能实例化抽象类,返回错误信息为,Cannot create an instance of the abstract type or interface 'Animal'
        Animal dog = new Dog(); 
        Animal cat = new Cat();

        dog.Speak();
        cat.Speak();

        dog.Move();
    }
}
  
  🡮
Woof
Meow
Moving...
  
  1. Speak()是抽象方法,必须在派生类中实现。
  2. Move()是已实现的方法,派生类可以选择是否重写。
  3. 抽象类不能实例化,必须通过派生类来实例化。