背景知识
类和面向对象
C++ 是一种多范式编程语言,支持过程编程和面向对象编程,它将数据和操作数据的方法组织为类和对象。
这里的“对象”实际上是对现实世界中所存在的事物的一种抽象,举个例子,你在计算机世界中怎么表示“人”这个概念呢?
我们可以将之抽象为一个类Person, 并具有一些“属性”和“方法”。
“属性”表示Person类所具有的特征,比如姓名、年龄、性别,通过这些特征,我们可以描述一个“人”的基本状态。
“方法”表示Person类的行为和功能,比如吃饭、睡觉、行走,通过这些动作,我们可以描述一个“人”的动态行为。
简而言之,“类”是现实世界中的实体在计算机世界中的抽象概念,类可以看作是对象的模板,它定义了对象的结构和行为方式,可以用来创建具有相同属性和行为的多个对象,而对象是“类”的实现。
类的基本写法:
class 类名{
访问修饰符:
// 成员变量,表示类的属性, 定义方式和变量的定义一样
// 成员方法,表示类的行为, 定义方式和方法的定义一样
}; // 分号结束一个类
C++使用class定义一个类,并在类中定义成员变量和成员方法。
访问修饰符指定了成员变量和成员方法的可见性和访问权限。常用的修饰符包括 private(私有)、public(公有)和protected(受保护),
- public: 被修饰的成员在类的内部、派生类(子类)的内部和类的对象外部都可以访问。
- private: 被修饰的成员只能在定义该成员的类的内部访问。
- protected: 被修饰的成员只能在定义该成员的类的内部以及派生类汇总访问。
比如下面的示例,public定义成员变量和成员方法的权限,后面定义了成员变量myAttribute和成员方法myMethod。
class MyClass {
public:
// 成员变量
int myAttribute;
// 成员方法
void myMethod() {
// 方法实现
}
};
int main() {
// 创建对象
MyClass obj;
// 访问属性
obj.myAttribute = 42;
// 调用方法
obj.myMethod();
return 0;
}
1、封装
封装是面向对象的三大特性之一,封装的主要目的是为了保证数据的安全性,我们假设有一个Circle类,它具有半径这个属性。
class Circle {
public:
// 成员变量
int radius;
};
然后我们创建一个圆对象
Circle circe; // 创建一个对象
创建对象之后,外部代码可以直接访问和修改半径,甚至将其设置为负数,这样的设计显然是不合理的。
// 外部代码没有经过验证可以直接访问和修改半径
circle.radius = -1;
为了防止这些问题的发生,我们可以通过封装隐藏对象中一些不希望被外部所访问到的属性或方法
- 将对象的属性名,设置为private,只能被类所访问
- 提供公共的get和set方法来获取和设置对象的属性
class Circle {
// 私有属性和方法
private:
int radius; // 将圆的半径设置为私有的
// 公有属性和方法
public:
// setXX方法设置属性
void setRadius(int r) {
radius = r;
}
// getXXX方法获取属性
int getRadius() {
return radius;
}
};
使用封装,我们隐藏了类的一些属性,具体的做法是使用get方法获取属性,使用set方法设置属性,如果希望属性是只读的,则可以直接去掉set方法,如果希望属性不能被外部访问,则可以直接去掉get方法。此外我们还可以在读取属性和修改属性的同时做一些其他的处理,比如如下的操作:
void setRadius(int r) {
// 对输入的半径进行验证,只有半径大于0,才进行处理
if (r >= 0) {
radius = r;
} else {
cout << "半径不能为负数" << endl;
}
}
2、构造函数
类的构造函数和之前结构体的构造函数类似,用于初始化对象的成员变量,构造函数与类同名,没有返回类型,并且在对象创建时自动调用。其基本语法包括:
1. 函数名:与类名相同
2. 参数列表:可以有零个或多个参数,用于在创建对象
3. 时传递初始化信息。
函数体: 用于执行构造函数的初始化逻辑。
下面我们还以Person类作为示例,包含一个默认构造函数和带参数的构造函数。
// const string& personName表示对string类型对常量引用,你可以传递字符串参数,但是不能在函数中修改这个参数的值。
class Person {
private:
int age;
string name;
public:
// 默认构造函数
Person() {
age = 20;
name = "Tom"
}
// 带参数的构造函数
Person(int personAge, const string& personName) {
age = personAge;
name = personName
}
}
int main() {
// 使用默认构造函数创建对象
Person person1;
// 使用带参数的构造函数创建对象
Person person2(20, "Jerry");
return 0;
}
此外,还有构造函数的成员初始化列表写法,这种写法允许在进入构造函数主体之前对类成员进行初始化,比如下面的示例。
Person(int personAge, const string& personName) : age(personAge), name(personName) {
}
在上面的代码中, Person 类的构造函数接受一个 string 类型的参数 persnName和一个int类型的参数personAge,并通过成员初始化列表初始化了 成员变量。在这里,: age(personAge), name(personName) 表示将 personAge 的值赋给 age 成员变量, 将 personName 的值赋给 name , 从而完成了成员变量初始化。
3、继承
在对象中,总有一些操作是重复的,比如说Person类具有姓名、身高、年龄等特征,并具有一些行走、吃饭、睡觉的方法,而我们要实现一个Teacher类,Teacher首先也是一个人,他也具备人的特征和方法,那我们是不是也应该用代码去实现这些特征和方法呢,这就势必会产生一些重复的代码。
因此,我们可以采用“继承”的方式使得一个类获取到其他类中的属性和方法。在定义类时,可以在类名后指定当前类的父类(超类), 子类可以直接继承父类中的所有属性和方法,从而避免编写重复性的代码,此外我们还可以对子类进行扩展。
假设,我们有一个图形类Shape, 它具有一个属性和一个方法,属性为类型,方法为求图形的面积
class Shape {
protected:
string type; // 形状类型
public:
// 构造函数
Shape(const string& shapeType) : type(shapeType) {}
// 求面积的函数
double getArea() const {
return 0.0;
}
// 获取形状类型
string getType() const {
return type;
}
};
在上面的代码中,getArea函数使用const用来修饰,是用来表示该函数不会修改对象的状态,使用const能保证对对象的访问是安全的。
double getArea() const {
// 不会修改对象的状态
return 0.0;
}
要想实现继承,我们还需要一个关于圆的类Circle,它继承自Shape类
// Circle 类,继承自 Shape
class Circle : public Shape {
private:
int radius; // 圆的半径
public:
// 构造函数, 调用Shape的构造函数,初始化了类型为"circle"
Circle(int circleRadius) : Shape("Circle"), radius(circleRadius) {}
// 重写基类的方法
double calculateArea() const override{
return 3.14 * radius * radius; // 圆的面积公式
}
// 获取半径
int getRadius() const {
return radius;
}
};
在上面的示例代码中,图形类拥有shape属性和getArea、getType方法,而子类在父类这些属性和方法的基础上新增了radius属性和getRadius方法,并且在子类和父类中都有getArea这个方法,这被称为方法的重写,方法的重写需要override关键字,其意思是子类重写父类的方法,并提供自己的实现。
4、多态
多态常常和继承紧密相连,它允许不同的对象使用相同的接口进行操作,但在运行时表现出不同的行为。多态性使得可以使用基类类型的指针或引用来引用派生类的对象,从而在运行时选择调用相应的派生类方法。
C++中实现多态性的方法是通过virtual虚函数,比如下面的示例:
class Shape {
public:
virtual double calculateArea() const = 0;
};
class Circle : public Shape {
private:
int radius;
public:
double calculateArea() const override {
return 3.14 * radius * radius;
}
};
class Rectangle : public Shape {
private:
int width;
int height;
public:
// 构造函数,用于初始化 width 和 height
Rectangle(int w, int h) : width(w), height(h) {}
// width * height 的结果是整数,但 calculateArea 方法的返回类型是 double
// 为了确保结果是一个浮点数,使用 static_cast<double> 将其显式转换为 double 类型
double calculateArea() const override {
return static_cast<double>(width * height);
}
};
这里使用virtual在父类中定义了一个虚函数,而= 0表示这是一个纯虚函数,即定义的函数在基类中没有实现,但是要求它的派生类都必须提供这个函数的实现,这种抽象的方法使得 Shape 类成为一个抽象基类,不能被实例化,只能被用作派生其他类的基类。
然后两个派生类 Circle 和 Rectangle则是重写了 calculateArea 方法,它们提供了各自的实现,有着不同的计算逻辑。
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Rectangle(4, 5));
shapes.push_back(new Circle(3));
for (const Shape* shape : shapes) {
std::cout << "Area: " << shape->calculateArea() << std::endl;
}
return 0;
}
之后我们创建了一个容器shapes,包含不同类型的图形对象,然后循环遍历该容器并为每一个shape对象调用 calculateArea 方法,尽管方法名称相同,但实际调用的方法是根据对象的类型动态确定的,这其实就是多态的概念。
5、固定小数点格式输出
要求输出小数点后两位,可以引入iomanip库文件中的内容,当使用 fixed 时,浮点数会以固定小数点格式输出,setprecision()函数用于设置输出浮点数的精度,即小数点后的位数, fixed << setprecision(2)表示输出小数点后两位。
// 引入iomanip库文件,用于控制输出格式
#include <iomanip>
// 输出结果,控制小数位数为两位
for (const Shape* shape : shapes) {
cout << shape->GetType() << " area: " << fixed << setprecision(2) << shape->CalculateArea() << endl;
}
练习题目
图形的面积
题目描述:
考虑一个简单的图形类层次结构,包括基类 Shape 和两个派生类 Rectangle 和 Circle。每个类都有一个用于计算面积的方法。你的任务是编写一个程序,根据输入数据创建一个图形对象,然后计算并输出其面积。
输入描述:
输入包括多行,每行包含一个图形的描述。 描述的第一个单词是图形类型(“rectangle"或"circle”),然后是与该图形相关的参数。 对于矩形,参数是宽度和高度,对于圆形,参数是半径。输入以单词"end"结束。
输出描述:
对于每个图形描述,输出其类型和面积。使用两位小数点精度输出面积。
输入示例
rectangle 5 3
circle 2
end
输出示例
Rectangle area: 15.00
Circle area: 12.56
提示信息
长方形面积的计算 = 长 * 宽
圆形面积的计算 = 3.14 * 半径 * 半径
具体代码实现
#include <iostream>
#include <vector>
#include <string>
// 引入iomanip库文件,用于控制输出格式
#include <iomanip>
using namespace std;
class Shape {
public:
// 定义虚函数
// const = 0,代表纯虚函数,该类不能被实例化,需要派生类(子类)必须实现该类的函数
virtual double CalculateArea() const = 0;
virtual string GetType() const = 0;
}; // 类的结束括号后,必须加上”;分号
class Rectangle : public Shape {
public:
// 通过构造函数,初始化参数列表
Rectangle(int width, int height) : width(width), height(height) {}
// 重写父类虚函数
double CalculateArea() const override {
return static_cast<double>(width * height);
}
// 重写父类获取类型虚函数
string GetType() const override {
return "Rectangle";
}
private:
int width;
int height;
};
class Circle : public Shape {
public:
// 通过构造函数,初始化参数列表
Circle(int radius) : radius(radius){}
// 重写父类计算面积的虚函数
double CalculateArea() const override {
return 3.14 * radius * radius;
}
// 重写父类获取类型虚函数
string GetType() const override {
return "Circle";
}
private:
int radius;
}; // 类结束后,记得加分号
int main()
{
// 创建一个容器,用来处理Shape类型数据
vector<Shape *> shapes;
while (1) {
// 获取输入类型
string type;
cin >> type;
// 判断输入类型是否为“end
if (type == "end") {
break;
}
// 如果输入的是rectangle
if (type == "rectangle") {
int width, height;
cin >> width >> height; // 输入宽和高
// 创建对象,并放入容器中
Shape *temp = new Rectangle(width, height);
shapes.push_back(temp);
}
if (type == "circle") {
int radius;
cin >> radius;
Shape *temp = new Circle(radius);
shapes.push_back(temp);
}
}
for (const Shape *sh :shapes) {
cout << sh->GetType() << " area: " << fixed << setprecision(2) << sh->CalculateArea() << endl;
}
return 0;
}
// 简易版
/*
#include <iostream>
#include <string>
#include <cstdio>
using namespace std;
int main()
{
string type;
while (1) {
cin >> type;
if (type == "end") {
break;
}
if (type == "rectangle") {
int a, b;
cin >> a >> b;
float result = a * b;
printf ("Rectangle area: %.2f\n", result);
}
if (type == "circle") {
int a;
cin >> a;
float result = 3.14 * a * a;
printf ("Circle area: %.2f\n", result);
}
}
return 0;
}
*/
小白寄语
扎根,做好手头的事,精进。
诗词共勉
人生若只如初见,何事秋风悲画扇。
等闲变却故人心,却道故人心易变。
-----------纳兰性德