面向对象的概念
面向对象(Object-Oriented)是一种软件开发的编程范式或思想方式。在面向对象编程(OOP)中,程序的设计和实现是围绕着对象的概念展开的。
对象是一个具体的实例,它具有唯一的标识、状态和行为。面向对象编程将数据和对数据的操作封装在一个对象中,通过定义类来创建这些对象。类是对象的模板或蓝图,它描述了对象具有的属性和方法。
面向对象编程的三个主要原则是封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。
- 封装:封装将数据和基于数据的操作封装在一个对象中,隐藏了对象的内部细节,只暴露必要的接口供外部使用。这提高了代码的可维护性和安全性。
- 继承:继承允许一个类(子类)继承另一个类(父类)的属性和方法,使子类具有父类的特性。通过继承,可以实现代码的重用和层次化的组织。
- 多态:多态允许使用相同的接口来处理不同类型的对象,具有不同的实现。这使得可以通过统一的方式调用不同类的方法,增加了代码的灵活性和可扩展性。
面向对象编程的优点包括代码的模块化、可重用性、可维护性、灵活性和易于理解。它提供了一种更直观、自然的方法来组织和处理复杂的软件系统。许多编程语言,如Java、C++和Python,都支持面向对象编程。
C++ 中的对象
在C++中,对象是类的实例化。类是一种自定义的数据类型,描述了对象具有的属性和行为。通过创建类的对象,可以在内存中分配对应的存储空间,并使用这些对象来访问类中定义的成员。
在C++中,对象由类定义的一组属性和方法构成。属性(成员变量)表示对象的状态或数据,而方法(成员函数)表示对象的行为或操作。通过对象可以访问和操作这些属性和方法,以实现所需的功能。
以下是在C++中创建对象的基本步骤:
- 定义类:使用类关键字和类名定义一个类。在类中,可以声明和定义需要的成员变量和成员函数。
- 创建对象:使用类名和变量名声明一个对象。声明时会自动调用类的构造函数来初始化对象。
- 访问成员:通过使用对象名和成员运算符(点号 .)来访问对象的属性和方法。可以使用对象名访问成员变量并修改其值,也可以调用对象的成员函数执行相应的操作。
- 销毁对象:在不再需要对象时,可以使用 delete 关键字来销毁对象,并释放相关的内存空间。销毁对象时会自动调用类的析构函数。
这是一个简单的C++代码示例,演示了定义类、创建对象、访问成员属性和成员方法以及销毁对象的过程:
#include <iostream> using namespace std; // 定义一个类 class MyClass { public: // 成员变量 int myNumber; // 构造函数 MyClass() { cout << "对象已创建" << endl; } // 成员函数 void setNumber(int number) { myNumber = number; } void printNumber() { cout << "My number is: " << myNumber << endl; } // 析构函数 // 析构函数中包含了删除对象的方法,调用析构函数就是删除对象。 ~MyClass() { cout << "对象已销毁" << endl; } }; int main() { // 创建对象 MyClass obj; // 访问成员属性 obj.myNumber = 42; // 访问成员方法 obj.printNumber(); // 设置成员属性并打印 obj.setNumber(88); obj.printNumber(); return 0; }
在上面的示例中,我们首先定义了一个名为MyClass
的类,该类具有一个整数类型的成员变量myNumber
和三个成员函数:一个构造函数用于对象的初始化,一个setNumber()
函数用于设置成员属性的值,一个printNumber()
函数用于打印成员属性的值。同时,我们还定义了一个析构函数,在对象被销毁时自动调用。
在主函数中,我们先创建一个MyClass
类的对象obj
,在对象创建时会调用构造函数。然后,通过对象名和成员运算符访问成员变量myNumber
并进行赋值。之后,我们调用成员方法printNumber()
来打印该数字。
接下来,我们使用成员方法setNumber()
设置成员属性的值为88,并再次调用printNumber()
打印更新后的值。
最后,在程序结束时,对象obj
会被自动销毁,析构函数会被调用。
C++ 面向对象:封装
下面是一个用C++演示封装的代码示例:
#include <iostream> #include <string> using namespace std; // 定义一个类 class Person { private: string name; int age; public: // 设置姓名 void setName(string personName) { name = personName; } // 获取姓名 string getName() { return name; } // 设置年龄 void setAge(int personAge) { if (personAge >= 0) { age = personAge; } } // 获取年龄 int getAge() { return age; } }; int main() { // 创建对象 Person person; // 使用公共接口设置和获取私有属性 person.setName("Alice"); person.setAge(25); // 打印私有属性的值 cout << "Name: " << person.getName() << endl; cout << "Age: " << person.getAge() << endl; return 0; }
在上述代码中,定义了一个Person
类,并使用private
访问修饰符将成员变量name
和age
声明为私有。这意味着只有类内部可以直接访问这些成员。
然后,在公共区域(public
)定义了用于设置和获取这些私有成员的公共成员函数。通过调用setName()
和setAge()
方法,可以设置私有成员变量的值。而使用getName()
和getAge()
方法,可以获取私有成员的值】】
在main()
函数中,创建一个Person
对象,并使用公共接口设置和获取私有属性。通过这种方式,实现了封装的概念,隐藏了类内部的实现细节,使外部代码只能通过公共接口来访问和操作私有属性。
最终输出结果为:
Name: Alice Age: 25
C++ 面向对象:继承
下面是一个用C++演示继承的代码示例:
#include <iostream> #include <string> using namespace std; // 基类 class Animal { protected: string name; public: // 构造函数 Animal(string animalName) : name(animalName) {} // 成员函数 void eat() { cout << name << " is eating." << endl; } }; // 派生类 class Cat : public Animal { private: string color; public: // 构造函数 Cat(string catName, string catColor) : Animal(catName), color(catColor) {} // 成员函数 void meow() { cout << name << " is meowing." << endl; } void displayColor() { cout << "The color of " << name << " is " << color << "." << endl; } }; int main() { // 创建派生类对象 Cat cat("Tom", "white"); // 调用基类成员函数 cat.eat(); // 调用派生类成员函数 cat.meow(); cat.displayColor(); return 0; }
在上述代码中,定义了一个基类Animal
和一个派生类Cat
。基类有一个受保护的成员变量name
,并且包含了一个构造函数和一个eat()
成员函数。
派生类Cat
继承自基类Animal
,并添加了一个私有成员变量color
,还定义了两个成员函数meow()
和displayColor()
。
在main()
函数中,创建一个Cat
对象,并分别调用基类的成员函数和派生类的成员函数。由于继承关系,派生类可以直接使用基类的成员函数。此外,派生类还可以拥有自己新增的成员变量和成员函数。
最终的输出结果为:
Tom is eating. Tom is meowing. The color of Tom is white.
C++ 面向对象:多态
以下是一个使用C++演示多态的代码示例:
#include <iostream> #include <string> using namespace std; // 基类 class Shape { protected: string name; public: // 构造函数 Shape(string shapeName) : name(shapeName) {} // 纯虚函数 virtual void draw() = 0; }; // 圆形派生类 class Circle : public Shape { private: double radius; public: // 构造函数 Circle(string circleName, double circleRadius) : Shape(circleName), radius(circleRadius) {} // 实现纯虚函数 void draw() override { cout << "Drawing a circle: " << name << ", radius: " << radius << endl; } }; // 三角形派生类 class Triangle : public Shape { private: double base; double height; public: // 构造函数 Triangle(string triangleName, double triangleBase, double triangleHeight) : Shape(triangleName), base(triangleBase), height(triangleHeight) {} // 实现纯虚函数 void draw() override { cout << "Drawing a triangle: " << name << ", base: " << base << ", height: " << height << endl; } }; int main() { // 创建基类指针数组 Shape* shapes[2]; // 创建圆形对象和三角形对象 Circle circle("Circle", 5.0); Triangle triangle("Triangle", 4.0, 3.0); // 将对象指针赋给数组中的元素 shapes[0] = &circle; shapes[1] = ▵ // 循环调用虚函数 for (int i = 0; i < 2; i++) { shapes[i]->draw(); } return 0; }
在上面的代码示例中,我们定义了一个基类Shape
,它包含一个纯虚函数draw()
。然后我们派生出两个具体的形状类:Circle
和Triangle
,并分别实现了draw()
函数。
在main()
函数中,我们首先创建了一个基类指针数组shapes
,然后通过创建不同的派生类对象给数组元素赋值。
接下来,我们使用循环遍历数组,并通过基类指针调用虚函数draw()
。由于虚函数是动态绑定的,它会根据对象的真实类型调用相应的成员函数,从而实现多态。
运行代码,输出结果为:
Drawing a circle: Circle, radius: 5 Drawing a triangle: Triangle, base: 4, height: 3
可以看到,虽然使用的是基类的指针数组,但通过调用虚函数draw()
时,会根据对象的类型来执行相应的成员函数,实现了多态的效果。
对象的属性是对象
在面向对象的程序设计中,有时候会将一个对象的属性设计为另一个对象,这种情况通常发生在以下两种情况下:
- 对象之间存在"has-a"关系:当一个对象在概念上包含或组成另一个对象时,适合将该对象的属性设计为另一个对象。例如,在汽车和轮胎之间存在"has-a"关系,因为汽车包含四个轮胎;在学生和课程之间存在"has-a"关系,因为学生选修多门课程。
class Tire { // 轮胎类的定义 }; class Car { private: Tire tire; // 将轮胎作为汽车的属性 public: // 汽车类的其他成员函数和成员变量 }; class Course { // 课程类的定义 }; class Student { private: Course courses[5]; // 将课程数组作为学生的属性 public: // 学生类的其他成员函数和成员变量 };
- 需要在不同对象之间共享数据:当多个对象需要共享一组数据时,适合将这组数据封装为一个对象,并将其作为其他对象的属性。这样可以确保对象之间共享的数据一致性。
class Location { private: double longitude; double latitude; public: // Location类的其他成员函数和构造函数 }; class Person { private: string name; Location location; // 将位置信息作为人的属性 public: // Person类的其他成员函数和构造函数 };
通过将对象作为另一个对象的属性,可以提高代码的可读性和可维护性。同时,这也符合面向对象的封装和组合原则,使得程序更加模块化和灵活。
C++ 对象序列化和反序列化
对象序列化和反序列化是将对象转换为字节流(序列化)或将字节流转换回对象(反序列化)的过程。在C++中,可以使用标准库提供的序列化和反序列化功能来实现。
对象序列化
要将对象序列化为字节流,可以使用std::ofstream
来将对象数据写入文件。需要注意的是,被写入文件的对象的类必须实现序列化操作符<<
。
以下是一个示例:
#include <iostream> #include <fstream> class Foo { private: int data; public: // 序列化操作符 friend std::ostream& operator<<(std::ostream& os, const Foo& obj) { return os.write(reinterpret_cast<const char*>(&obj.data), sizeof(obj.data)); } // 构造函数 Foo(int d) : data(d) {} }; int main() { Foo foo(42); std::ofstream file("data.bin", std::ios::binary); if (file) { file << foo; // 序列化对象到文件 file.close(); } return 0; }
在上述示例中,定义了一个类Foo
,其中包含一个data
成员变量。通过重载<<
操作符,将data
写入指定的输出流。在main()
函数中,创建了一个Foo
对象,并将其序列化到名为"data.bin"的二进制文件中。
对象反序列化
要从字节流中反序列化对象,可以使用std::ifstream
从文件中读取数据。被读取的对象的类必须实现反序列化操作符>>
。
以下是一个示例:
#include <iostream> #include <fstream> class Foo { private: int data; public: // 反序列化操作符 friend std::istream& operator>>(std::istream& is, Foo& obj) { return is.read(reinterpret_cast<char*>(&obj.data), sizeof(obj.data)); } // 构造函数 Foo() = default; // 打印数据 void printData() { std::cout << "Data: " << data << std::endl; } }; int main() { Foo foo; std::ifstream file("data.bin", std::ios::binary); if (file) { file >> foo; // 从文件反序列化对象 file.close(); } foo.printData(); // 打印反序列化后的数据 return 0; }
在上述示例中,定义了一个与序列化示例相同的类Foo
。通过重载>>
操作符,将数据从输入流中读取到data
成员变量中。在main()
函数中,创建了一个Foo
对象,并从名为"data.bin"的二进制文件中反序列化它。最后,调用printData()
函数打印反序列化后的数据。
注意 ,在序列化和反序列化过程中,使用的输入流和输出流都应以二进制模式打开。这可以通过将std::ios::binary
选项传递给std::ofstream
和std::ifstream
的构造函数来实现。