面相对象的程序设计

2 面相对象的基本特性

2.1 类

2.1.1 构造函数

构造函数主要有如下几种类别:

  1. 默认构造函数
  2. 含参构造函数
  3. 拷贝构造函数
    • 注:拷贝构造函数并不是赋值构造函数,赋值构造函数的本质是赋值运算符重载(即赋值构造并不是构造函数的一种)
  4. 移动构造函数
  5. 委托构造函数
  6. 显式构造函数
2.1.1.1 默认构造函数

默认构造函数是指不需要指定参数时的构造函数,其在部分语言中有特殊功能和特殊用法,因此单独成一类。

特殊功能有:

  • C++在使用一些标准库容器vector 、 std::list时,要求其所存储的对象必须提供一个默认构造函数。
  • C++或者Java等语言使用第三方库时可能会要求其使用的类包含默认构造函数。
  • 延迟初始化。
  • 在C++中,使用该对象构造数组时,该对象必须提供一个默认构造函数。
    特殊用法:
  1. 在C++中,调用默认构造函数的方法并不是 ClassName obj();而是 ClassName obj;前者是声明了一个返回值类型为ClassName的函数
// 调用默认构造函数
ClassName obj;

// 定义一个返回值为ClassName的函数,函数名为obj
ClassName obj();
2.1.1.1.1 C++的默认构造函数及其调用

在C++中,有以下两种方式可以定义默认构造函数:

  1. 如果开发者没有显式地声明任何构造函数,编译器会自动生成一个默认构造函数(但是并不会初始化类中的变量)。
class Cat {
public:
	void printInfo(void);

private:
	// 品种
	std::string breed;
	float weight;
	float age;
};

void Cat::printInfo(void)
{
	using namespace std;
	cout << "breed: " << breed << endl;
	cout << "weight: " << weight << endl;
	cout << "age: " << age << endl;
}

int main()
{
	// 无参构造,默认生成的默认构造函数并不会初始化类中变量
	Cat cat1;
	// 行为未定义
	cat1.printInfo();
	return 0;
}
  • 注意:
    • 在上述代码中,编译器自动生成的构造函数所生成的对象中的breed等成员变量均未被初始化(当然在C++11之后,允许在类的定义中直接定义成员的默认值)。
    • 若类中已经提供了构造函数,则编译器不会生成默认构造函数,上述代码第23行就会报错。
  1. 开发者可以显式定义默认构造函数。在自定义的默认构造函数中可以进行填装初始值等操作。
2.1.1.2 含参构造函数

含参构造函数即在创建类时需要指定若干参数。最为常用的方法。

2.1.1.3 拷贝构造函数

拷贝构造函数用于创建一个对象作为另一个对象的副本

2.1.1.3.1 C++的拷贝构造函数

C++的拷贝构造函数形式为:ClassName(const ClassName &obj)
需要注意的是在C++中,拷贝构造函数会被默认生成,其实现方式为浅拷贝。浅拷贝的规则如下:

  • 对于类的每一个非静态成员变量,使用其对应的赋值操作进行拷贝。
  • 对于内置类型(如 intdouble ),会进行直接的值复制。
  • 对于指针类型,默认的拷贝构造函数只会复制指针的地址(这就是浅拷贝),而不会复制指针所指向的内容。
    自定义的拷贝构造函数中可以实现自定义的逻辑。
class Cat {

public:
	void printInfo(void);
	
	// 默认构造函数
	Cat() : breed("default"), weight(0), age(0) {
		std::cout << "Called the default constructor." << std::endl;
	};

	// 含参构造函数
	Cat(std::string breed, float weight, float age) : breed(breed), weight(weight), age(age) {
		std::cout << "Called a constructor with parameters." << std::endl;
	};

	// 拷贝构造函数
	Cat(const Cat &cat) : breed(cat.breed), weight(cat.weight), age(cat.age) {
		std::cout << "Called the copy constructor." << std::endl;
	};

	// 重载operator=运算符
	Cat& operator=(const Cat& cat) {
		std::cout << "Called the operator=." << std::endl;
		this->breed = cat.breed;
		this->weight = cat.weight;
		this->age = cat.age;
		return *this;
	};

private:
	// 品种
	std::string breed;
	float weight;
	float age;
};

void Cat::printInfo(void)
{
	using namespace std;
	cout << "breed: " << breed << endl;
	cout << "weight: " << weight << endl;
	cout << "age: " << age << endl;
}

int main()
{
	// 含参构造
	Cat cat1 = Cat("British shorthair cat", 1.0, 0.75);
	cat1.printInfo();

	// 拷贝构造
	Cat cat2 = Cat(cat1);
	cat2.printInfo();

	// 同样是拷贝构造
	Cat cat3 = cat1;
	cat3.printInfo();

	// 调用赋值运算函数
	cat3 = cat1;
	return 0;
}

则上述的:

  • Cat cat2 = Cat(cat1);
  • Cat cat3 = cat1;
    会调用拷贝构造函数。
2.1.1.4 移动构造函数
2.1.1.5 委托构造函数
2.1.1.6 显式构造函数
2.1.1.7 语言限定特性
2.1.1.7.1 C++初始化列表特性

2.1.2 析构函数

2.2 接口

在面相对象的程序设计中,接口可以理解为一个规范或约定。

在C++中并未提供类似于Java中直接给出的 interface 关键字,因此在C++中的接口通常使用抽象类实现。具体的接口可以如下定义:

#include <iostream>
#include <string>

class Animal {
public:
    // 纯虚函数,使 Animal 成为抽象类
    virtual std::string getName() const = 0; // 获取动物的名字
    virtual void setName(const std::string& name) = 0; // 设置动物的名字
    virtual void makeSound() = 0; // 发出声音

    // 虚析构函数,确保派生类的析构函数被调用
    virtual ~Animal() {}
};

在Java中,接口可通过如下方式定义:

public interface Animal {
	String getName();
	void setName(String name);
	void makeSound();
}

在上述的若干语言中的代码均定义了一个最基础的 Animal "规范"(即接口),均定义有如下方法:

  • 获取名字
  • 设置名字
  • 制造叫声
    随后即可在下一章节中讲述如何实现这些接口。

2.3 继承

而在Java中,继承有两种方式:

  1. 类继承类 或者 接口继承接口:此时应当使用 extends 特性。该特性主要用来扩展类的功能。子类继承父类后,除了可以获得父类的属性和方法外,还可以添加新的属性和方法,或者覆盖(Override)父类的方法。
  2. 类实现接口:此时应当使用 implements 特性。当使用该特性时该类必须实现对应接口的所有方法(除非该类被声明为抽象类)。该特性确保了使用implements的非抽象类能满足对应接口所要求的所有行为规范

2.4 多态

2.4.1 基本概念

多态 (Polymorphism)是一个希腊词汇,从字面上理解为多种形态。
该特性允许开发者使用相同的接口调用不同的基类方法
其最重要的两个要素为:

  1. 接口的一致性:通过相同的接口或抽象基类调用方法。
  2. 行为的多样性:在派生类中实现相同的接口方法,但具有不同的行为。

在上述的定义下,从语言理论上,多态通常有如下的分类:

  1. 类型多态
    1. 参数多态:例如使用泛型,允许函数或数据类型在不同数据类型上操作而不必对每种数据类型编写单独的代码。
    2. 包含多态:又称为子类型多态。这种多态性依赖于继承机制,允许子类类型的对象被视为其父类类型的对象使用,同时保持自己的方法实现。它是动态多态的一种实现方式,主要通过虚函数来实现。
  2. 接口多态:接口多态指通过接口的实现,不同的类可以以相同的方式被处理。这是面向对象编程中的核心概念之一,它强调实现特定接口的类必须提供接口声明的所有方法的实现。
  3. 广义多态
    1. 函数重载
    2. 运算符重载
  4. 协变与逆变
    1. 协变:允许一个方法返回比它在父类中声明的方法更具体的类型
    2. 逆变:允许一个方法接收比它在父类中声明的方法更一般的类型作为参数
    而从多态解析的时间上进行分类,则有如下的类型:
  5. 静态多态:在编译期间进行多态的解析。
  6. 动态多态:在运行时进行多态的解析(通常依赖类中的指针)。

接下来将根据如下的C++代码对如上的若干分类进行讨论:

#include <iostream>

// 基类
class Animal {
public:
    // 虚函数
    virtual void makeSound() {
        std::cout << "Some animal sound" << std::endl;
    }
    virtual ~Animal() {}  // 虚析构函数,确保删除派生类对象时,能正确调用派生类的析构函数
};

// 派生类 Dog
class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof" << std::endl;
    }
};

// 派生类 Cat
class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Meow" << std::endl;
    }
};

// 函数,接受 Animal 类型的引用
void perform(Animal &animal) {
    animal.makeSound();
}

int main() {
    Dog myDog;
    Cat myCat;

	myDog.makeSound();
	myCat.makeSound();

    perform(myDog);  // 输出 Woof
    perform(myCat);  // 输出 Meow

    return 0;
}

在上述代码中:

  1. DogCat 类继承了 Animal 类,并重写了 makeSound 方法,是类型多态中的包含多态
  2. myDog.makeSound();myCat.makeSound();
    1. 在多态的语言理论上的分类来看是接口多态
    2. 其多态的解析是在编译时进行的,从解析的时间上进行分类是静态多态
  3. void perform(Animal &animal);perform(myDog); 以及 perform(myCat);
    1. perform 方法允许接收比它在父类中声明的方法更一般的类型作为参数(即允许接收 DogCat 类型),属于逆变
    2. 在其方法中,多态的解析是在运行时进行的,从解析的时间上进行分类是动态多态

2.4.2 类型转换(子类转父类与父类转子类)

正如类的继承关系,通常父类是子类的子集。因此子类转父类通常是自动转换的该过程是多态的一种特性。这个过程叫做向上转型
而当父类转子类时,该方法通常需要强制类型转换。而子类通常含有父类所不拥有的特性和方法,因此并不是所有的父类转子类都会成功。通常来说,该转换成功的前提是该父类事实上是子类隐式转换过来的,也就是说前提是这个父类本身就是这个子类这个过程叫做向下转型

2.4.3 重载&重写

基本概念:

  • 重载(Overloading):
    重载是指在同一个类中可以有多个同名的方法,但是这些方法的参数列表不同。这些方法通常被设计为相似的功能。
  • 重写(Overriding):
    重写的基本释义为子类覆盖父类中的一个实现。
    重写允许子类重新定义继承自父类的一个方法,重写的方法在子类中必须具有相同的方法名参数列表和相同或协变的返回类型

2.5 组合