实验信息
- 实验课程: C++程序设计实验
第一次实验
实验目的
通过本次实验,熟悉C++的基本编程环境,掌握程序调试方法,了解C++输入输出方式、函数重载的定义与使用、类与对象的基本概念和实现方法。
实验任务
任务1:熟悉编译环境
结合视频和不同编译器的教程,熟悉编译环境,掌握程序调试方法,并正确运行最简程序。
使用的编译器
使用的是 Visual Studio Code (VSCode) 作为编辑器,并且配置了 MinGW-w64 作为编译器和调试器。
编译器和调试器的配置详解
- 集成开发环境(IDE): VSCode
- 工具链 : MinGW-w64 -包含GCC编译器(
g++.exe
)和GDB调试器(gdb.exe
) 。
操作步骤
- 编写Hello World程序
#include <iostream>
int main() {
// 在标准输出中打印 "Hello, world!"
std::cout << "Hello, world!" << std::endl;
}
- 编译和运行
- 打开编译器
- 新建项目
- 编写代码
- 编译运行
任务2:计算整数因子的和
编写一个程序,函数func
的功能是计算并输出给定整数n的所有因子(不包括1与其自身)之和。
程序设计
- 功能描述 本程序的主要功能是: - 从用户输入一个整数
n
(1 < n <= 1000)。 - 计算并输出该整数的所有因子的和(不包括1和n本身)。 - 确保输入的整数在有效范围内,否则提示错误信息。 - 程序结构 程序主要包括以下几个部分:
- 设置区域和编码:使用
std::setlocale
和std::ios_base::sync_with_stdio
设置区域和同步机制,以确保在终端中正确显示和输入 UTF-8 编码的中文字符。 - 函数定义:定义一个函数
sumOfFactors
来计算给定整数n
的所有因子的和。 - 主函数:负责读取用户输入,调用
sumOfFactors
函数,输出结果,并进行输入范围验证。
- 设置区域和编码:使用
- 函数设计 函数
sumOfFactors
的设计步骤如下:- 初始化因子和
sum
为 0。 - 遍历从2到
n/2
之间的所有数,检查这些数是否是n
的因子(即n % i == 0
)。 - 如果是因子,则将该因子加到
sum
中。 - 最后返回因子和
sum
。
- 初始化因子和
- 主函数流程
- 设置区域和编码确保中文可以正常显示。
- 提示用户输入一个整数,并读取用户输入。
- 检查输入的整数是否在有效范围内(1 < n <= 1000)。如果不在范围内,输出错误信息并返回错误码。
- 调用
sumOfFactors
函数计算因子和。 - 输出计算结果。
代码实现
#include <iostream>
#include <string>
#include <locale>
#include <codecvt>
int sumOfFactors(int n) {
int sum = 0;
// 遍历2到n/2之间的所有数,找出n的因子
for (int i = 2; i <= n / 2; ++i) {
if (n % i == 0) {
sum += i;
}
}
return sum;
}
int main() {
// 设置区域,使得 std::wcout 支持 UTF-8
std::setlocale(LC_ALL, "");
std::ios_base::sync_with_stdio(false);
int n;
std::wcout << L"请输入一个整数 n(不大于1000): ";
std::wcin >> n;
if (n > 1000 || n <= 1) {
std::wcout << L"输入的值不在有效范围内" << std::endl;
return 1; // 返回错误码
}
int result = sumOfFactors(n);
std::wcout << L"sum=" << result << std::endl;
return 0; // 返回正常退出码
}
运行结果
![[Pasted image 20240605154403.png]]
任务3:重载函数与默认形参值函数的二义性
自举一例说明重载函数与带默认形参值函数在调用时可能产生的二义性问题,并总结避免二义性问题的注意事项。
二义性示例
#include <iostream>
// 重载函数
void print(int x) {
std::cout << "print(int x) called with " << x << std::endl;
}
// 带默认参数的函数
void print(int x, int y = 10) {
std::cout << "print(int x, int y) called with " << x << " and " << y << std::endl;
}
int main() {
print(5); // 这里会产生二义性
return 0;
}
说明
在上述代码中,我们有两个名为 print
的函数:
void print(int x)
:这是一个重载函数。void print(int x, int y = 10)
:这是一个带默认参数的函数。
当我们在main
函数中调用print(5)
时,编译器会遇到二义性问题,因为它无法确定应该调用哪一个print
函数:
print(int x)
可以接受单个int
参数。print(int x, int y = 10)
也可以接受单个int
参数,并使用默认值10
作为第二个参数。
修改后代码
#include <iostream>
// 重载函数
void printInt(int x) {
std::cout << "printInt(int x) called with " << x << std::endl;
}
// 带默认参数的函数
void print(int x, int y = 10) {
std::cout << "print(int x, int y) called with " << x << " and " << y << std::endl;
}
int main() {
printInt(5); // 调用重载函数
print(5); // 调用带默认参数的函数
return 0;
}
通过将重载函数的名称修改为 printInt
,消除了二义性问题。现在,编译器可以明确地知道应该调用哪个函数。
注意事项
- 避免重载函数和带默认参数函数的签名过于相似:
- 如果有一个带默认参数的函数,尽量不要再重载一个参数列表相似的函数。
- 明确调用函数时的参数:
- 尽量在调用带默认参数的函数时,明确提供所有参数,以避免编译器误解。
- 使用不同的函数名:
- 在设计 API 时,尽量使用不同的函数名来避免这种二义性问题。
- 使用命名空间:
- 如果必须重载,可以使用命名空间来区分不同的函数。
任务4:Factorial类
编写一个名为Factorial
的阶乘类,包含两个私有数据成员value
和fact
;包含两个函数成员:求阶乘和显示结果。
类设计
成员变量
value
:存储用户输入的整数值n
,用于计算阶乘。fact
:存储计算得到的阶乘结果。由于阶乘结果可能非常大,因此使用long long
类型来存储。
成员函数- 构造函数
Factorial(int n)
:
- 接受一个整数参数
n
,并将其存储在成员变量value
中。 - 调用
calculateFactorial
成员函数计算阶乘,并将结果存储在成员变量fact
中。
calculateFactorial
:
- 这是一个私有成员函数,用于计算
value
的阶乘。 - 使用一个循环从 1 乘到
value
,计算阶乘结果并返回。
displayResult
:
- 这是一个公共成员函数,用于显示阶乘结果。
- 输出格式为
value! = fact
,其中value
是用户输入的整数,fact
是计算得到的阶乘结果。
代码实现
#include <iostream>
class Factorial {
private:
int value;
unsigned long long fact;
void calculateFactorial() {
fact = 1;
for (int i = 1; i <= value; ++i) {
fact *= i;
}
}
public:
Factorial(int val) : value(val), fact(1) {
calculateFactorial();
}
void displayResult() const {
std::cout << "The factorial of " << value << " is " << fact << std::endl;
}
};
int main() {
int num;
std::cout << "Enter a number: ";
std::cin >> num;
Factorial factorial(num);
factorial.displayResult();
return 0;
}
运行结果
![[Pasted image 20240606174542.png]]
任务5:Rectangle类与Point类
编写一个名为Rectangle
的矩形类,包含两个私有数据成员:矩形的左下角和右上角两个点;包含两个函数成员:判断是否构成矩形,以及计算矩形的面积。
类设计
设计思路
在本次实验中,我设计了两个类:Point
类和Rectangle
类。Point
类用于表示二维平面上的一个点,而Rectangle
类则表示一个由两个点(左下角和右上角)定义的矩形。以下是具体的设计思路:
Point 类
Point
类的设计相对简单,主要用于表示一个点的坐标。它包含两个公有的成员变量x
和y
,分别表示点的横坐标和纵坐标。此外,Point
类还包含一个构造函数,用于初始化点的坐标。
- 成员变量:
int x
:表示点的横坐标。int y
:表示点的纵坐标。
- 构造函数:
Point(int xCoord, int yCoord)
:初始化点的坐标。
Rectangle 类
Rectangle
类通过聚合两个Point
类的对象来表示一个矩形。矩形的定义由左下角和右上角的两个Point
对象决定。为了确保矩形的有效性,Rectangle
类包含一个判断是否构成矩形的成员函数和一个计算矩形面积的成员函数。
- 成员变量:
Point bottomLeft
:表示矩形的左下角。Point topRight
:表示矩形的右上角。
- 构造函数:
Rectangle(Point bl, Point tr)
:通过两个Point
对象初始化矩形的左下角和右上角。
- 成员函数:
bool isRectangle()
:判断左下角和右上角的坐标是否满足矩形的要求,即左下角的横坐标小于右上角的横坐标,且左下角的纵坐标小于右上角的纵坐标。int area()
:计算矩形的面积。通过计算矩形的宽度和高度,然后相乘得到面积。
使用 Point 类的对象作为 Rectangle 类的数据成员
在Rectangle
类的设计中,我们使用了Point
类的对象作为数据成员。这种设计方式称为类的聚合,即一个类包含另一个类的对象作为其成员。这样设计的好处是:
- 代码重用:避免重复定义表示点的坐标的成员变量。
- 结构清晰:矩形的定义更加直观,左下角和右上角的点明确了矩形的边界。
- 易于扩展:如果需要对点的表示进行扩展或修改,只需修改
Point
类,而不需要修改Rectangle
类。
代码实现
#include <iostream>
// 定义 Point 类
class Point {
public:
int x;
int y;
// 构造函数
Point(int xCoord, int yCoord) : x(xCoord), y(yCoord) {}
};
// 定义 Rectangle 类
class Rectangle {
private:
Point bottomLeft; // 左下角
Point topRight; // 右上角
public:
// 构造函数
Rectangle(Point bl, Point tr) : bottomLeft(bl), topRight(tr) {}
// 判断是否构成矩形
bool isRectangle() {
// 判断左下角和右上角的坐标是否满足矩形的要求
return (bottomLeft.x < topRight.x && bottomLeft.y < topRight.y);
}
// 计算矩形的面积
int area() {
// 计算矩形的宽和高,并相乘得到面积
int width = topRight.x - bottomLeft.x;
int height = topRight.y - bottomLeft.y;
return width * height;
}
};
int main() {
// 创建 Point 类对象作为矩形的两个点
Point bottomLeft(1, 1);
Point topRight(4, 3);
// 创建 Rectangle 类对象
Rectangle rect(bottomLeft, topRight);
// 判断是否构成矩形
if (rect.isRectangle()) {
std::cout << "It forms a rectangle" << std::endl;
// 计算并输出矩形的面积
std::cout << "The area of the rectangle is: " << rect.area() << std::endl;
} else {
std::cout << "It does not form a rectangle" << std::endl;
}
return 0;
}
运行结果
![[Pasted image 20240606175137.png]]
思考题
能否将Point类的定义放在Rectangle类后面?为什么?如果可以,如何实现?
在C++中,可以将类的定义放在另一个类的定义之后。如果Rectangle
类需要使用Point
类作为成员变量的类型,那么在定义Rectangle
类之前需要先声明Point
类。前向声明告诉编译器有一个名为Point
的类,但不提供其具体定义。这允许编译器知道Point
类的存在,以便在Rectangle
类中使用它。
实现方法
- 前向声明:在定义
Rectangle
类之前,先声明Point
类。 - 定义顺序:在前向声明之后,定义
Rectangle
类,然后再定义Point
类。
#include <iostream>
// 前向声明 Point 类
class Point;
// 定义 Rectangle 类
class Rectangle {
private:
Point* bottomLeft; // 使用指针类型,以便前向声明有效
Point* topRight;
public:
// 构造函数
Rectangle(Point* bl, Point* tr) : bottomLeft(bl), topRight(tr) {}
// 判断是否构成矩形
bool isRectangle();
// 计算矩形的面积
int area();
};
// 定义 Point 类
class Point {
public:
int x;
int y;
// 构造函数
Point(int xCoord, int yCoord) : x(xCoord), y(yCoord) {}
};
// 实现 Rectangle 类的成员函数
bool Rectangle::isRectangle() {
// 判断左下角和右上角的坐标是否满足矩形的要求
return (bottomLeft->x < topRight->x && bottomLeft->y < topRight->y);
}
int Rectangle::area() {
// 计算矩形的宽和高,并相乘得到面积
int width = topRight->x - bottomLeft->x;
int height = topRight->y - bottomLeft->y;
return width * height;
}
int main() {
// 创建 Point 类对象作为矩形的两个点
Point bottomLeft(1, 1);
Point topRight(4, 3);
// 创建 Rectangle 类对象
Rectangle rect(&bottomLeft, &topRight);
// 判断是否构成矩形
if (rect.isRectangle()) {
std::cout << "It forms a rectangle" << std::endl;
// 计算并输出矩形的面积
std::cout << "The area of the rectangle is: " << rect.area() << std::endl;
} else {
std::cout << "It does not form a rectangle" << std::endl;
}
return 0;
}
- 前向声明:在定义
Rectangle
类之前,使用class Point;
进行前向声明。 - 使用指针:在
Rectangle
类中,使用指针类型Point
来声明成员变量。 - 定义顺序:先定义
Rectangle
类,然后定义Point
类。
任务6:Complex类
编写一个名为Complex
的复数类,以及实现两复数相加的函数add
。
类设计
在设计Complex
类时,我们需要考虑如何表示和操作复数。复数由实部和虚部组成,因此需要两个成员变量来存储这些信息。此外,我们需要提供基本的功能,如构造函数、访问器方法(getter)以及复数的加法操作。以下是具体的设计思路:
- 成员变量:
double real
: 存储复数的实部。double imag
: 存储复数的虚部。
- 构造函数:
Complex(double r, double i)
: 用于初始化复数的实部和虚部。
- 访问器方法(getter):
double getReal() const
: 返回复数的实部。double getImag() const
: 返回复数的虚部。
- 复数加法:
static Complex add(const Complex& c1, const Complex& c2)
: 静态方法,用于将两个复数相加并返回结果。
设计思路总结
- 封装性: 使用私有成员变量
real
和imag
,并提供公有的访问器方法getReal()
和getImag()
来获取这些变量的值。这确保了类的封装性。 - 构造函数: 提供一个构造函数来初始化复数的实部和虚部,确保在对象创建时就已经初始化。
- 静态方法: 使用静态方法
add
来实现复数的加法操作,这样可以不依赖于具体的对象实例进行操作。
代码实现
#include <iostream>
class Complex {
private:
double real; // 实部
double imag; // 虚部
public:
// 构造函数
Complex(double r, double i) : real(r), imag(i) {}
// 获取实部
double getReal() const {
return real;
}
// 获取虚部
double getImag() const {
return imag;
}
// 复数相加
static Complex add(const Complex& c1, const Complex& c2) {
double realSum = c1.real + c2.real;
double imagSum = c1.imag + c2.imag;
return Complex(realSum, imagSum);
}
};
int main() {
// 创建两个复数对象
Complex c1(3.5, 2.5);
Complex c2(2.5, 1.5);
// 调用复数相加函数
Complex sum = Complex::add(c1, c2);
// 输出结果
std::cout << "The result of adding two complex numbers is: " << sum.getReal() << " + " << sum.getImag() << "i" << std::endl;
return 0;
}
运行结果
![[Pasted image 20240606180618.png]]
实验小结
通过本次实验,我收获颇丰。首先,熟悉了C++编译环境和程序调试方法,成功运行了最简程序。其次,通过编写计算整数因子的程序,巩固了对C++输入输出方式和函数使用的理解。针对重载函数与默认形参值函数的二义性问题,通过实例掌握了避免二义性的方法。
在面向对象编程方面,设计了Factorial类和Rectangle类,深入理解了类的聚合和数据成员的使用。此外,还实现了Complex类,完成了复数相加功能。通过这些练习,增强了对C++面向对象编程的理解和实际操作能力。
第二次实验
实验目的
通过本次实验,进一步熟悉C++的基本编程环境和输入输出方式,掌握面向对象编程思想,学习类与对象、友元函数、静态成员与函数、对象数组和各类指针的用法。
实验任务
任务7:设计银行类
设计一个面向对象的程序,其中有3个类,CBank、IBank和ABank,分别代表中国银行类、中国工商银行类和中国农业银行类。每个类都包含一个私有数据balance
用于存放储户在该行的存款数。设计一个total
函数用于计算该储户在这3家银行的总存款数。
友元声明及常引用的目的
友元函数声明的目的
- 访问私有成员:友元函数可以访问类的私有成员,这在某些情况下非常有用。例如,在我们的例子中,
total
函数需要访问CBank
、IBank
和ABank
类的私有成员balance
,以便计算总存款数。如果没有友元声明,total
函数将无法直接访问这些私有成员。 - 简化代码:使用友元函数可以避免编写大量的公有访问器函数(getter函数),从而简化代码结构。
以常引用作为形参的目的
- 避免拷贝:使用引用可以避免对象的拷贝,从而提高程序的效率。拷贝大对象会消耗大量的时间和空间,而引用只需传递对象的地址。
- 保护数据:使用
const
关键字可以防止函数修改传入的参数,保证数据的安全性和完整性。这在设计函数时尤为重要,尤其是当函数不需要修改参数时。 - 提高可读性:常引用参数可以明确表达函数的意图,即函数不会修改传入的参数,从而提高代码的可读性和可维护性。
程序设计
设计思路
该程序的设计目标是计算三个银行的总存款数。为了实现这一目标,我设计了三个类:CBank
、IBank
和ABank
,每个类表示一个银行,并包含一个表示存款数的私有成员变量balance
。此外,我设计了一个友元函数total
,用于计算三个银行的总存款数。
具体设计步骤如下:
- 类的定义:
- 每个银行类(
CBank
、IBank
、ABank
)包含一个私有成员变量balance
,用于存储银行的存款数。 - 每个银行类包含一个构造函数,用于初始化
balance
变量。 - 每个银行类声明
total
函数为友元函数,以便total
函数可以访问类的私有成员balance
。
- 每个银行类(
- 友元函数的定义:
total
函数接收三个银行类对象的常引用作为参数,并返回三个银行的总存款数。- 在
total
函数中,直接访问每个银行对象的balance
成员,并计算总和。
- 主函数的实现:
- 在
main
函数中,创建三个银行类对象,并初始化它们的存款数。 - 调用
total
函数计算总存款数,并输出结果。
- 在
代码实现
#include <iostream>
// 前置声明,以便于在类中声明友元
class CBank;
class IBank;
class ABank;
// total函数声明为三个类的友元
double total(const CBank& cBank, const IBank& iBank, const ABank& aBank);
class CBank {
private:
double balance; // 存款数
public:
// 构造函数
CBank(double bal) : balance(bal) {}
// 友元函数声明
friend double total(const CBank&, const IBank&, const ABank&);
};
class IBank {
private:
double balance; // 存款数
public:
// 构造函数
IBank(double bal) : balance(bal) {}
// 友元函数声明
friend double total(const CBank&, const IBank&, const ABank&);
};
class ABank {
private:
double balance; // 存款数
public:
// 构造函数
ABank(double bal) : balance(bal) {}
// 友元函数声明
friend double total(const CBank&, const IBank&, const ABank&);
};
// total函数定义,计算三个银行的总存款数
double total(const CBank& cBank, const IBank& iBank, const ABank& aBank) {
return cBank.balance + iBank.balance + aBank.balance;
}
int main() {
// 创建三个银行对象
CBank cBank(1000.0);
IBank iBank(2000.0);
ABank aBank(3000.0);
// 调用total函数计算总存款数,并输出结果
std::cout << "Total balance is: " << total(cBank, iBank, aBank) << std::endl;
return 0;
}
运行结果
![[Pasted image 20240606184055.png]]
任务8:商品类
某商店销售某一商品,每天公布统一的折扣(discount),同时还允许销货员在销售时灵活掌握售价(price)。现已知当天3名销货员的销售情况如表2所列:
![[Pasted image 20240606185633.png]]
编写程序,声明一个名为Product的商品类,计算出当日此商品的总销售额sum以及平均售价。
静态数据成员和静态成员函数的使用
静态数据成员的使用目的
- 共享数据:静态数据成员在所有对象之间共享,适用于需要在所有对象之间共享的全局数据。例如,在本程序中,
totalSum
和totalSales
用于记录所有产品的总销售额和总销售数量。 - 节省内存:静态数据成员在类的所有实例中只有一个副本,可以节省内存空间,尤其是在类的实例数量较多时。
- 全局访问:静态数据成员可以通过类名直接访问,而不需要创建类的实例。这使得数据的访问更加方便。
静态成员函数的使用目的
- 操作静态数据成员:静态成员函数可以直接访问和操作静态数据成员。例如,在本程序中,
calculateSales
和getAveragePrice
函数用于计算和获取总销售额和平均价格。 - 无需对象实例:静态成员函数可以在没有对象实例的情况下调用,通过类名直接调用。这使得某些全局操作更加方便。
- 封装全局行为:静态成员函数可以封装与类相关的全局行为,使代码更加模块化和易于维护。
程序设计
设计思路
该程序的设计目标是计算和输出所有产品的总销售额和平均价格。为了实现这一目标,我设计了一个Product
类,该类包含产品的价格和折扣信息,并通过静态数据成员和静态成员函数来计算和管理总销售额和平均价格。
具体设计步骤如下:
- 类的定义:
Product
类包含两个静态数据成员:totalSum
和totalSales
,用于记录所有产品的总销售额和总销售数量。Product
类包含两个普通数据成员:price
和discount
,用于存储单个产品的价格和折扣。Product
类包含一个构造函数,用于初始化产品的价格和折扣。
- 静态成员函数的定义:
calculateSales
函数接受一个产品对象和销售数量作为参数,计算该产品的销售额并更新totalSum
和totalSales
。getAveragePrice
函数返回所有产品的平均价格,通过totalSum
除以totalSales
计算得出。
- 主函数的实现:
- 在
main
函数中,创建一个包含多个产品的vector
,并初始化它们的价格和折扣。 - 遍历产品列表,假设每个产品销售5次,调用
calculateSales
函数计算总销售额。 - 输出总销售额和平均价格。
- 在
代码实现
#include <iostream>
#include <vector>
using namespace std;
class Product {
public:
static double totalSum;
static int totalSales;
double price;
double discount;
Product(double p, double d) : price(p), discount(d) {}
static void calculateSales(const Product& p, int quantity) {
totalSum += p.price * quantity * (1 - p.discount);
totalSales += quantity;
}
static double getAveragePrice() {
return totalSales > 0 ? totalSum / totalSales : 0;
}
};
double Product::totalSum = 0.0;
int Product::totalSales = 0;
int main() {
vector<Product> products = {
{100, 0.1}, // price = 100, discount = 10%
{200, 0.2},
// ... add more products as needed
};
for (const auto& p : products) {
Product::calculateSales(p, 5); // assuming each product is sold 5 times
}
cout << "Total sales sum: " << Product::totalSum << endl;
cout << "Average price: " << Product::getAveragePrice() << endl;
return 0;
}
运行结果
![[Pasted image 20240606185708.png]]
任务9:学生成绩信息类
编写一个学生成绩信息类,包含的数据有学号、姓名、程序设计课程成绩、计算机网络课程成绩、数据库课程成绩。输入若干名学生的成绩信息,显示3门课程总分从高到低的排名和每门课程成绩均大于85分的学生名单。
使用对象数组
原因:
- 方便管理:使用对象数组可以方便地管理和操作一组相关的对象。例如,在学生成绩管理系统中,我们可以使用对象数组来存储所有学生的成绩信息。
- 统一操作:对象数组可以让我们对一组对象进行统一的操作,如排序、筛选、统计等,简化代码逻辑。
- 提高代码可读性:将相关的数据和操作封装在对象数组中,可以提高代码的可读性和可维护性。
方法:
- 定义类:首先定义一个类来表示单个对象。例如,在学生成绩管理系统中,我们定义一个
Student
类来表示单个学生的成绩信息。 - 创建对象数组:使用标准库中的
vector
容器来创建对象数组,并在其中存储多个对象。 - 操作对象数组:使用
vector
容器的成员函数和标准库算法来操作对象数组,如添加、删除、排序、筛选等。
程序设计
设计思路
该程序的设计目标是计算并输出所有学生的总分排名,以及每门课程成绩均大于85分的学生名单。为了实现这一目标,我们设计了一个Student
类和一个StudentInfo
类,使用对象数组来存储和管理学生的成绩信息。
具体设计步骤如下:
- 定义
Student
类:- 包含学生的学号、姓名、程序设计课程成绩、计算机网络课程成绩、数据库课程成绩。
- 提供一个构造函数,用于初始化学生的成绩信息。
- 定义
StudentInfo
类:- 包含一个
vector<Student>
对象数组,用于存储多个学生的成绩信息。 - 提供
addStudent
方法,用于向对象数组中添加学生。 - 提供
displayRanking
方法,用于按照总分从高到低排序并输出学生的成绩信息。 - 提供
displayHighScorers
方法,用于筛选并输出每门课程成绩均大于85分的学生。
- 包含一个
- 在
main
函数中使用StudentInfo
类:- 创建
StudentInfo
对象,并向其中添加多个学生的成绩信息。 - 调用
displayRanking
方法,输出学生的总分排名。 - 调用
displayHighScorers
方法,输出每门课程成绩均大于85分的学生名单。
- 创建
代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Student {
string id;
string name;
int score_prog;
int score_net;
int score_db;
Student(string i, string n, int prog, int net, int db)
: id(i), name(n), score_prog(prog), score_net(net), score_db(db) {}
};
class StudentInfo {
private:
vector<Student> students;
public:
void addStudent(const Student& s) {
students.push_back(s);
}
void displayRanking() {
sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
return a.score_prog + a.score_net + a.score_db > b.score_prog + b.score_net + b.score_db;
});
cout << "Ranking by total score:" << endl;
cout << "| ID | Name | Programming | Networking | Database | Total |\n";
cout << "|-------|------|-------------|------------|----------|-------|\n";
for (const auto& s : students) {
int total_score = s.score_prog + s.score_net + s.score_db;
cout << "| " << s.id << " | " << s.name << " | "
<< s.score_prog << " | " << s.score_net << " | "
<< s.score_db << " | " << total_score << " |\n";
}
}
void displayHighScorers() {
cout << "Students with all scores above 85:" << endl;
cout << "| ID | Name | Programming | Networking | Database | Total |\n";
cout << "|-------|------|-------------|------------|----------|-------|\n";
for (const auto& s : students) {
if (s.score_prog > 85 && s.score_net > 85 && s.score_db > 85) {
int total_score = s.score_prog + s.score_net + s.score_db;
cout << "| " << s.id << " | " << s.name << " | "
<< s.score_prog << " | " << s.score_net << " | "
<< s.score_db << " | " << total_score << " |\n";
}
}
}
};
int main() {
StudentInfo info;
info.addStudent({"99001", "Zhang Wei", 100, 95, 90});
info.addStudent({"99002", "Wang Wei", 70, 80, 90});
info.addStudent({"99003", "Li Wei", 50, 60, 70});
info.addStudent({"99004", "Zhao Wei", 70, 80, 75});
info.addStudent({"99005", "Zhang Fei", 100, 100, 100});
info.displayRanking();
info.displayHighScorers();
return 0;
}
运行结果
![[Pasted image 20240606215325.png]]
任务10:Circle类
实现一个名为Circle的圆类,并规定其数据成员float *radius
为指向半径值的指针。以Point类的对象指针作为另一个数据成员,表示圆心位置。定义一个判别两圆位置关系(相交/外离/内含/内切/外切)的函数。
各类指针的用法
在这段代码中,指针主要用于动态分配内存和访问类的数据成员。具体来说有以下几种用途:
-
指向基本数据类型的指针:
float *radius;
- 用途:指向一个浮点数,用于存储圆的半径。
- 方法:在构造函数中,动态分配内存并初始化值。析构函数中释放分配的内存。
-
指向对象的指针:
Point *center;
- 用途:指向一个
Point
对象,用于存储圆心的位置。 - 方法:在构造函数中,动态分配内存并初始化值。析构函数中释放分配的内存。
- 用途:指向一个
-
参数传递中的指针使用:
void determineRelationship(const Circle& other)
- 用途:通过引用传递参数,避免复制整个对象,提高效率。
- 方法:函数参数
const Circle& other
以常引用的方式传递另一个Circle
对象,确保函数内部不能修改传入的对象,同时避免了不必要的对象复制。
程序设计
设计思路
-
类的定义:
- 定义
Point
类表示二维平面上的点,包含x
和y
坐标。 - 定义
Circle
类表示圆,包含指向半径的指针radius
和指向圆心的指针center
。
- 定义
-
构造函数和析构函数:
Circle
类的构造函数:用于动态分配内存,并初始化半径和圆心。Circle
类的析构函数:用于释放动态分配的内存,防止内存泄漏。
-
关系判断函数:
determineRelationship
函数:用于判断两个圆之间的几何关系。- 计算圆心之间的距离
distance
,圆半径之和sumRadii
,以及半径之差diffRadii
。 - 根据距离和半径的关系,判断两个圆的几何关系并输出相应的结果。
实现步骤
-
定义类:
- 创建
Point
类,包含一个默认构造函数。 - 创建
Circle
类,包含指向半径和圆心的指针,构造函数和析构函数。
- 创建
-
实现关系判断函数:
- 计算两个圆心之间的距离。
- 根据距离和半径关系,判断并输出两个圆的位置关系。
-
主函数测试:
- 创建两个
Point
对象,表示两个圆的圆心。 - 创建两个
Circle
对象,表示两个圆。 - 调用
determineRelationship
函数,判断两个圆之间的关系并输出结果。
- 创建两个
代码实现
#include <iostream>
#include <cmath>
using namespace std;
class Point {
public:
float x, y;
Point(float x = 0, float y = 0) : x(x), y(y) {}
};
class Circle {
public:
float *radius;
Point *center;
Circle(float r, Point c) : radius(new float(r)), center(new Point(c)) {}
~Circle() {
delete radius;
delete center;
}
// Function to determine the relationship between two circles
void determineRelationship(const Circle& other) {
float distance = sqrt(pow(center->x - other.center->x, 2) + pow(center->y - other.center->y, 2));
float sumRadii = *radius + *other.radius;
float diffRadii = abs(*radius - *other.radius);
if (distance > sumRadii) {
cout << "The circles are separate." << endl;
} else if (distance == sumRadii) {
cout << "The circles are touching externally." << endl;
} else if (distance < sumRadii && distance > diffRadii) {
cout << "The circles are intersecting." << endl;
} else if (distance == diffRadii) {
cout << "The circles are touching internally." << endl;
} else if (distance < diffRadii) {
cout << "One circle is inside the other." << endl;
} else {
cout << "The circles are coincident." << endl;
}
}
};
int main() {
Point center1(0, 0);
Point center2(3, 4);
Circle circle1(5, center1);
Circle circle2(1, center2);
circle1.determineRelationship(circle2);
return 0;
}
运行结果
![[Pasted image 20240606221600.png]]
实验小结
在本次实验中,我设计并实现了四个C++程序,分别符合上述任务书中的四个要求。这四个项目让我深入实践了C++的一些重要特性,如面向对象的设计,引用,静态数据成员和函数,对象数组,以及利用函数指针调用函数。我在这次实验中得到了宝贵的经验和知识
- “银行”示例强调了C++面向对象编程的优点,尤其是封装性和可重用性的优点。运用友元函数可以直接访问对象的私有和保护成员,解决了不同类对象间的通信问题。常引用作为友元函数的参数可以保证对象在函数运行期间不会被修改,同时也可以优化性能。
- “商品”实例让我体验了静态成员函数和静态数据成员的重要性。静态成员数据用于存放所有对象共享的信息,具有全局性。而静态成员函数则可以直接访问静态数据成员,是一种封装和保护信息的有效方法。
- “成绩信息”示例教我使用对象数组来处理一组相关数据。这与使用传统的数组或结构体相比,有更好的灵活性和扩展性。
- “圆”程序通过使用指针实现了灵活的函数调用和内存管理。我使用函数指针调用函数,这是函数作为一级对象的一个强大示例。
第三次实验
实验目的
通过本次实验,进一步熟悉C++的面向对象编程技术,掌握类型转换函数、动态数组的实现、派生类的构造与析构函数等高级特性。
实验任务
任务11:为复数类增加类型转换函数
任务描述: 为第7题中的复数类增加一个类型转换函数,使得复数类的对象能够与基本数据类型如double类型的变量进行合法的四则运算。
类型转换函数的目的与实现
类型转换函数的主要目的是在不同类型之间提供隐式转换的能力。在Complex
类的上下文中,类型转换函数允许将Complex
对象隐式转换为double
类型。这种转换只涉及复数的实部,因为函数的实现返回了Complex
对象的实部。这种设计选择允许Complex
对象在需要double
值的表达式中直接使用,从而提高了代码的灵活性和可读性。 类型转换函数的实现方法是在类内部声明一个没有参数但返回目标类型(本例中为double
)的运算符函数。在Complex
类中,类型转换函数如下:
operator double() const { return real; }
此函数没有参数,并且以成员函数的形式存在,使得任何Complex
对象都可以隐式转换为其实部所表示的double
值。
程序设计
程序设计的核心是创建一个能够处理复数算术的Complex
类。复数由实部和虚部组成,因此类中定义了两个私有成员变量real
和imag
来分别存储这两部分。通过构造函数,可以创建具有特定实部和虚部的复数对象。 为了访问和操作这些复数对象,类提供了一系列成员函数和重载运算符。成员函数getReal
和getImag
允许外部代码获取复数的实部和虚部。重载的算术运算符(+
、-
、*
、/
)使得复数之间的加法、减法、乘法和除法运算变得直接且自然。 此外,通过重载+
运算符为非成员函数,程序允许Complex
对象与double
值进行加法运算,这进一步增强了类的灵活性。这种设计使得复数运算符更加直观,并且易于使用,同时保持了代码的简洁性和可维护性。
代码实现
#include <iostream>
class Complex {
private:
double real; // 实部
double imag; // 虚部
public:
// 构造函数
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
// 获取实部
double getReal() const {
return real;
}
// 获取虚部
double getImag() const {
return imag;
}
// 类型转换函数,将复数转换为double类型(返回实部)
operator double() const {
return real;
}
// 重载加法运算符,复数对象与复数对象之间的加法
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// 重载减法运算符,复数对象与复数对象之间的减法
Complex operator-(const Complex& other) const {
return Complex(real - other.real, imag - other.imag);
}
// 重载乘法运算符,复数对象与复数对象之间的乘法
Complex operator*(const Complex& other) const {
double newReal = real * other.real - imag * other.imag;
double newImag = real * other.imag + imag * other.real;
return Complex(newReal, newImag);
}
// 重载除法运算符,复数对象与复数对象之间的除法
Complex operator/(const Complex& other) const {
double divisor = other.real * other.real + other.imag * other.imag;
double newReal = (real * other.real + imag * other.imag) / divisor;
double newImag = (imag * other.real - real * other.imag) / divisor;
return Complex(newReal, newImag);
}
// 非成员函数重载加法运算符,复数对象与double类型之间的加法
friend Complex operator+(const Complex& c, double d) {
return Complex(c.real + d, c.imag);
}
friend Complex operator+(double d, const Complex& c) {
return Complex(d + c.real, c.imag);
}
};
int main() {
Complex c1(3.0, 2.0);
Complex c2(1.0, 4.0);
// 进行复数对象与double类型之间的加法运算
double result = c1 + 5.0;
std::cout << "Result of c1 + 5.0: " << result << std::endl;
return 0;
}
运行结果
![[Pasted image 20240606223836.png]]
任务12:动态数组类
任务描述: 编写一个动态数组类,包括基本功能:对数组的查找、添加/删除元素、逆序等功能(可自行添加额外功能)。
动态数组的设计与实现
设计思路
动态数组类 DynamicArray
是用于管理一个可以自动扩展的数组。它具有基本的数组操作功能,如查找、添加、删除、逆序等。为了实现这些功能,设计了以下数据成员和成员函数:
数据成员
int* arr
:指向数组首地址的指针,用于存储数组元素。int capacity
:数组的容量,即数组最多能容纳的元素个数。int size
:数组当前的元素个数。
构造函数和析构函数
-
构造函数:
DynamicArray(int initialCapacity = 10)
:初始化数组,默认容量为10,动态分配内存,并初始化capacity
和size
。
-
析构函数:
~DynamicArray()
:释放动态分配的内存,防止内存泄漏。
成员函数
-
查找元素:
int find(int element)
:在数组中查找指定元素,返回元素的索引,未找到则返回-1。
-
添加元素:
void append(int element)
:在数组末尾添加元素,如果数组已满,则扩展容量为原来的两倍。
-
插入元素:
void insert(int index, int element)
:在指定位置插入元素,如果数组已满,则扩展容量为原来的两倍。
-
删除元素:
void removeAt(int index)
:删除指定位置的元素,元素后移填补空缺。
-
逆序数组:
void reverse()
:将数组元素逆序。
-
获取数组大小:
int getSize() const
:返回数组当前的元素个数。
-
重载下标运算符:
int& operator[](int index)
:重载[]
运算符,用于访问数组元素,支持边界检查。
程序设计
设计思路
-
创建动态数组对象:
- 在
main
函数中创建DynamicArray
类的对象。
- 在
-
基本操作:
- 调用
append
函数向数组中添加元素。 - 调用
insert
函数在指定位置插入元素。 - 调用
removeAt
函数删除指定位置的元素。 - 调用
reverse
函数逆序数组。 - 调用
getSize
函数获取数组当前大小。 - 使用重载的
[]
运算符访问和打印数组元素。
- 调用
代码结构
-
类定义:
- 定义
DynamicArray
类,包括数据成员和成员函数。
- 定义
-
构造函数和析构函数实现:
- 实现
DynamicArray
类的构造函数和析构函数,负责内存分配和释放。
- 实现
-
成员函数实现:
- 实现
find
,append
,insert
,removeAt
,reverse
,getSize
, 和重载的operator[]
成员函数。
- 实现
-
主函数:
- 在
main
函数中创建DynamicArray
对象,进行基本操作测试,并输出结果。
- 在
代码实现
#include <iostream>
class DynamicArray {
private:
int* arr; // 指向数组首地址的指针
int capacity; // 数组容量
int size; // 数组当前元素个数
public:
// 构造函数,初始化数组
DynamicArray(int initialCapacity = 10) : arr(new int[initialCapacity]), capacity(initialCapacity), size(0) {}
// 析构函数,释放数组内存
~DynamicArray() {
delete[] arr;
}
// 查找元素,返回索引,若未找到则返回-1
int find(int element) {
for (int i = 0; i < size; ++i) {
if (arr[i] == element) {
return i;
}
}
return -1;
}
// 添加元素到数组末尾
void append(int element) {
if (size == capacity) {
// 如果数组已满,则扩容为原来的两倍
int* temp = new int[2 * capacity];
for (int i = 0; i < size; ++i) {
temp[i] = arr[i];
}
delete[] arr;
arr = temp;
capacity *= 2;
}
arr[size++] = element;
printArray("After append");
}
// 在指定位置插入元素
void insert(int index, int element) {
if (index < 0 || index > size) {
std::cerr << "Invalid index!" << std::endl;
return;
}
if (size == capacity) {
// 如果数组已满,则扩容为原来的两倍
int* temp = new int[2 * capacity];
for (int i = 0; i < size; ++i) {
temp[i] = arr[i];
}
delete[] arr;
arr = temp;
capacity *= 2;
}
for (int i = size; i > index; --i) {
arr[i] = arr[i - 1];
}
arr[index] = element;
size++;
printArray("After insert");
}
// 删除指定位置的元素
void removeAt(int index) {
if (index < 0 || index >= size) {
std::cerr << "Invalid index!" << std::endl;
return;
}
for (int i = index; i < size - 1; ++i) {
arr[i] = arr[i + 1];
}
size--;
printArray("After removeAt");
}
// 逆序数组
void reverse() {
int left = 0;
int right = size - 1;
while (left < right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
printArray("After reverse");
}
// 获取数组大小
int getSize() const {
return size;
}
// 重载下标运算符[],用于访问数组元素
int& operator[](int index) {
if (index < 0 || index >= size) {
std::cerr << "Index out of bounds!" << std::endl;
exit(1);
}
return arr[index];
}
// 打印数组元素
void printArray(const std::string& message = "Array elements") const {
std::cout << message << ": ";
for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
};
int main() {
// 创建动态数组对象
DynamicArray arr;
// 输出初始数组元素
arr.printArray("Initial array");
// 添加元素
arr.append(10);
arr.append(20);
arr.append(30);
// 插入元素
arr.insert(1, 15);
// 删除元素
arr.removeAt(2);
// 逆序数组
arr.reverse();
// 最终输出数组元素
arr.printArray("Final array");
return 0;
}
运行结果
![[Pasted image 20240606230240.png]]
任务13:长方体类Box
任务描述: 以第6题中的矩形类作为基类,派生出长方体类Box。新增计算长方体对象表面积和体积的功能。要求为派生类Box编写构造函数,复制构造函数以及析构函数。
派生类Box的设计与实现
派生类Box
继承自基类Rectangle
,在Rectangle
的基础上增加了处理三维空间的能力。设计思路是扩展二维矩形到三维空间中的长方体,这需要一个额外的数据成员来表示长方体的深度。
-
新增的数据成员:
depth
: 私有成员变量,用于存储长方体的深度。
-
成员函数:
volume()
: 计算并返回长方体的体积。它使用Rectangle
类的area()
函数计算底面积,并乘以深度得到体积。surfaceArea()
: 计算并返回长方体的表面积。它分别计算长方体六个面的面积并求和。
-
构造函数:
Box(double bl_x, double bl_y, double tr_x, double tr_y, double d)
: 接收左下角坐标、右上角坐标和深度,然后使用成员初始化列表初始化基类Rectangle
的坐标成员和派生类Box
的深度成员。
-
复制构造函数:
Box(const Box& other)
: 接收另一个Box
对象的引用,通过对每个成员变量进行复制来初始化新对象。
-
析构函数:
~Box()
: 在这个示例中,析构函数是默认的,因为Box
类没有动态分配的资源需要释放。如果类将来需要管理动态资源,析构函数将需要相应地实现以释放这些资源。
程序设计
整个程序的设计思路是建立一个类层次结构,其中Rectangle
类代表二维空间中的矩形,而Box
类作为派生类,扩展了Rectangle
的功能,使其能够表示和操作三维空间中的长方体。
- 在设计
Rectangle
基类时,考虑了矩形的基本属性,即左下角和右上角的坐标,以及计算面积和周长的基本方法。 - 在设计
Box
派生类时,保留了矩形的特性,并添加了深度属性及计算体积和表面积的方法。这样的设计允许Box
类专注于三维特性,同时重用Rectangle
类的逻辑。 - 构造函数和复制构造函数确保了对象的正确初始化和复制,而析构函数保持了默认的实现,因为当前的设计不要求特殊的资源管理。
代码实现
#include <iostream>
// 基类:矩形类
class Rectangle {
protected:
// 左下角和右上角两个点
struct Point {
double x;
double y;
};
Point bottomLeft; // 左下角
Point topRight; // 右上角
public:
// 构造函数
Rectangle(double bl_x, double bl_y, double tr_x, double tr_y) : bottomLeft({bl_x, bl_y}), topRight({tr_x, tr_y}) {}
// 计算矩形面积
double area() const {
double width = topRight.x - bottomLeft.x;
double height = topRight.y - bottomLeft.y;
return width * height;
}
// 计算矩形周长
double perimeter() const {
double width = topRight.x - bottomLeft.x;
double height = topRight.y - bottomLeft.y;
return 2 * (width + height);
}
};
// 派生类:长方体类
class Box : public Rectangle {
private:
double depth; // 长方体的深度
public:
// 构造函数
Box(double bl_x, double bl_y, double tr_x, double tr_y, double d) : Rectangle(bl_x, bl_y, tr_x, tr_y), depth(d) {}
// 复制构造函数
Box(const Box& other) : Rectangle(other.bottomLeft.x, other.bottomLeft.y, other.topRight.x, other.topRight.y), depth(other.depth) {}
// 析构函数
~Box() {}
// 计算长方体的体积
double volume() const {
double width = topRight.x - bottomLeft.x;
double height = topRight.y - bottomLeft.y;
return width * height * depth;
}
// 计算长方体的表面积
double surfaceArea() const {
double width = topRight.x - bottomLeft.x;
double height = topRight.y - bottomLeft.y;
double side1 = width * height;
double side2 = width * depth;
double side3 = height * depth;
return 2 * (side1 + side2 + side3);
}
};
int main() {
// 创建一个长方体对象
Box box(1.0, 1.0, 3.0, 4.0, 2.0);
// 计算并输出长方体的体积和表面积
std::cout << "The volume of the box is: " << box.volume() << std::endl;
std::cout << "The surface area of the box is: " << box.surfaceArea() << std::endl;
return 0;
}
运行结果
![[Pasted image 20240606233131.png]]
实验小结
我深入思考和实践面向对象编程(OOP)的概念,特别是类的设计、继承、类型转换、以及动态内存管理等高级特性
- 类型转换函数:给复数类增加一个能够实现与
double
类型合法四则运算的类型转换函数,我认识到了隐式类型转换在简化代码和提高可读性方面的作用。但同时也意识到,如果不加以控制,隐式转换可能导致预期之外的行为,因此在设计时需要仔细权衡其必要性和潜在的风险。 - 动态数组类:在编写动态数组类时,我不仅复习了数组的基本操作,还学习了如何使用指针和动态内存分配来管理自定义大小的数组。这个过程中,我强化了对内存管理的理解,特别是在添加/删除元素时对数组进行扩容和缩减的策略,以及如何防止内存泄漏和维护数据一致性。
- 类继承和多态:通过将矩形类作为基类来派生出长方体类,我练习了类的继承和多态的应用。我学会了如何在子类中扩展和重写基类的功能,以及如何通过构造函数、复制构造函数和析构函数来确保对象的正确初始化和资源的正确管理。
思考与扩展
快速排序算法
在原有的DynamicArray
类中引入快速排序算法,首先需要添加一个私有成员函数来执行快速排序,然后提供一个公共成员函数供外部调用以排序数组。
快速排序是一个分而治之的算法,它通过选定一个“轴心”元素将数组分割成两个子数组,子数组中的元素分别比轴心元素小和大,然后递归地在两个子数组上执行相同的操作。
代码实现
#include <iostream>
class DynamicArray {
private:
int* arr; // 指向数组首地址的指针
int capacity; // 数组容量
int size; // 数组当前元素个数
// 快速排序的辅助函数,用于划分数组
int partition(int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为轴心
int i = (low - 1); // 小于轴心元素的区域的索引
for (int j = low; j <= high - 1; j++) {
// 如果当前元素小于或等于轴心元素
if (arr[j] <= pivot) {
i++; // 扩大小于轴心的区域
std::swap(arr[i], arr[j]);
}
}
std::swap(arr[i + 1], arr[high]);
return (i + 1);
}
// 快速排序的主要递归函数
void quickSort(int low, int high) {
if (low < high) {
// pi 是 partitioning index,arr[pi] 现在在正确的位置
int pi = partition(low, high);
// 递归地分别对分割后的两个子数组进行排序
quickSort(low, pi - 1);
quickSort(pi + 1, high);
}
}
public:
// 构造函数,初始化数组
DynamicArray(int initialCapacity = 10) : arr(new int[initialCapacity]), capacity(initialCapacity), size(0) {}
// 析构函数,释放数组内存
~DynamicArray() {
delete[] arr;
}
// 公共接口,供外部调用排序
void sort() {
if (size > 1) { // 如果数组中有多于一个元素,则进行排序
quickSort(0, size - 1);
}
printArray("After sort");
}
void append(int element) {
if (size == capacity) {
// 如果数组已满,则扩容为原来的两倍
int* temp = new int[2 * capacity];
for (int i = 0; i < size; ++i) {
temp[i] = arr[i];
}
delete[] arr;
arr = temp;
capacity *= 2;
}
arr[size++] = element;
printArray("After append");
}
// 打印数组元素
void printArray(const std::string& message = "Array elements") const {
std::cout << message << ": ";
for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
};
int main() {
// 创建动态数组对象
DynamicArray arr;
// 添加元素
arr.append(10);
arr.append(5);
arr.append(2);
arr.append(20);
arr.append(7);
// 排序数组
arr.sort();
// 输出排序后的数组元素
arr.printArray("Sorted array");
return 0;
}
运行结果
![[Pasted image 20240607003827.png]]
第四次实验
实验目的
通过本次实验,熟悉C++的类与继承、多态、运算符重载等高级特性,掌握面向对象编程的设计与实现方法。
实验任务
任务14:电梯类的设计与实现
任务描述: 设有一个电梯类Elevator,包括私有数据成员:型号id和售价price。客梯类pElevator除了型号和售价外,还包括新增属性:最大载客数passenger_load。货梯类cElevator除了型号和售价外,还包括新增属性:最大载货重量cargo_load。客货两用类dElevator包含上述所有属性。编写一个程序,明确上述各类之间的继承关系,并能够求取单位载客/载重价格(数据自拟)。
类的设计与实现
在这个程序中,我设计和实现了几个类,以模拟电梯的不同功能和特性。设计思路围绕着面向对象编程的核心概念,如继承和多态性,以达到代码重用和扩展性的目的。
- Elevator 基类:这是一个抽象基类,代表电梯的通用特性和行为。它包含两个受保护的数据成员:
id
(型号)和price
(售价),以及一个计算单位载客/载重价格的成员函数unitPrice()
。构造函数初始化电梯的型号和价格。unitPrice()
函数在这个基类中提供了一个默认实现,但预期在派生类中被重写以反映不同类型的电梯的具体计算方式。 - pElevator 客梯类:这个类继承自
Elevator
基类,专门代表客梯。它新增了一个私有数据成员passenger_load
(最大载客数),并重写了unitPrice()
函数,以按照单位载客数来计算价格。 - cElevator 货梯类:与
pElevator
类似,这个类也继承自Elevator
基类,但专注于货梯的特性。它引入了cargo_load
(最大载货重量)作为私有数据成员,并相应地重写了unitPrice()
函数,以根据单位载重量计算价格。 - dElevator 客货两用类:这个类是一个多重继承的例子,它同时继承自
pElevator
和cElevator
类。由于客货两用电梯既能载客又能载货,该类的构造函数接收与两个基类相关的参数。它重写了unitPrice()
函数,计算时取两者的较小值,以反映出客货两用电梯的特性。
程序设计
通过继承机制,我能够定义一组通用的行为和属性(在Elevator
基类中),然后通过派生类(pElevator
、cElevator
和dElevator
)来提供特定类型电梯的具体实现。 多态性在这个设计中也起到了关键作用。通过虚函数unitPrice()
的使用,程序可以在运行时决定调用哪个类的unitPrice()
方法。
代码实现
#include <iostream>
// 基类:电梯类
class Elevator {
protected:
std::string id; // 型号
double price; // 售价
public:
// 构造函数
Elevator(const std::string& model, double cost) : id(model), price(cost) {}
// 计算单位载客/载重价格
virtual double unitPrice() const {
return price;
}
};
// 客梯类
class pElevator : public Elevator {
private:
int passenger_load; // 最大载客数
public:
// 构造函数
pElevator(const std::string& model, double cost, int maxPassenger) : Elevator(model, cost), passenger_load(maxPassenger) {}
// 重写基类的单位载客/载重价格计算函数
virtual double unitPrice() const override {
return price / passenger_load;
}
};
// 货梯类
class cElevator : public Elevator {
private:
double cargo_load; // 最大载货重量
public:
// 构造函数
cElevator(const std::string& model, double cost, double maxCargo) : Elevator(model, cost), cargo_load(maxCargo) {}
// 重写基类的单位载客/载重价格计算函数
virtual double unitPrice() const override {
return price / cargo_load;
}
};
// 客货两用类
class dElevator : public pElevator, public cElevator {
public:
// 构造函数
dElevator(const std::string& model, double cost, int maxPassenger, double maxCargo)
: pElevator(model, cost, maxPassenger), cElevator(model, cost, maxCargo) {}
// 重写基类的单位载客/载重价格计算函数
virtual double unitPrice() const override {
// 对于客货两用电梯,计算单位载客/载重价格时,取两者的较小值
return std::min(pElevator::unitPrice(), cElevator::unitPrice());
}
};
int main() {
// 创建客梯对象
pElevator passengerElevator("P123", 50000, 10);
// 创建货梯对象
cElevator cargoElevator("C456", 80000, 2000);
// 创建客货两用电梯对象
dElevator dualElevator("D789", 100000, 10, 2000);
// 输出单位载客/载重价格
std::cout << "Unit passenger price: " << passengerElevator.unitPrice() << std::endl;
std::cout << "Unit cargo price: " << cargoElevator.unitPrice() << std::endl;
std::cout << "Unit passenger/cargo price: " << dualElevator.unitPrice() << std::endl;
return 0;
}
运行结果
![[Pasted image 20240606235842.png]]
任务15:矩阵运算的运算符重载
任务描述: 通过运算符重载实现两矩阵的加、减、乘运算。
运算符重载的设计与实现
在这个程序中,我通过运算符重载为矩阵类添加了加法、减法和乘法操作。运算符重载允许我们以自然和直观的方式使用标准算术运算符来操作自定义的数据类型。以下是运算符重载的设计思路:
- 加法运算符
+
:重载加法运算符使得两个矩阵对象可以通过+
符号进行相加。首先检查两个矩阵的维度是否相同,如果不匹配则抛出异常。如果维度匹配,则逐元素相加并返回新的矩阵对象。 - 减法运算符
-
:减法运算符的重载与加法类似,只是在相应位置上对元素进行相减。同样地,如果两个矩阵的维度不匹配,则抛出异常。 - 乘法运算符
*
:乘法运算符的重载比加法和减法更为复杂。首先检查第一个矩阵的列数是否与第二个矩阵的行数相等,若不等则抛出异常。如果维度匹配,则执行矩阵乘法,并返回结果矩阵。矩阵乘法的实现涉及三层嵌套循环,计算结果矩阵的每个元素。
在所有情况下,我都创建了一个新的矩阵对象来存储和返回运算结果,这确保了原始矩阵对象的数据不会被修改。这是因为矩阵加法和减法是按元素操作,而乘法则涉及点积计算。通过这种方式,我们可以连续地使用这些运算符来构建复杂的矩阵表达式。
程序设计
旨在创建一个能够执行基本矩阵运算的类。设计重点包括:
- 封装:矩阵的数据被封装在类内部,通过公共接口方法进行访问和操作。这样的设计隐藏了实现细节,提供了一个清晰和简洁的接口给类的使用者。
- 异常处理:在执行矩阵运算时,可能会遇到不匹配维度的情况。程序通过抛出异常来处理这些错误情况,提醒用户进行相应的错误处理。
- 友元函数:为了输出矩阵,
operator<<
被定义为一个友元函数,允许它访问Matrix
类的私有成员。这样做是为了在不破坏封装性的前提下提供打印功能。 - 易用性:通过运算符重载,类的使用者可以轻松地像使用基本数据类型那样使用矩阵对象进行算术运算。
代码实现
#include <iostream>
#include <vector>
#include <stdexcept>
class Matrix {
private:
std::vector<std::vector<int>> data;
size_t rows;
size_t cols;
public:
Matrix(size_t numRows, size_t numCols) : rows(numRows), cols(numCols) {
data.resize(rows, std::vector<int>(cols, 0));
}
size_t numRows() const {
return rows;
}
size_t numCols() const {
return cols;
}
int& operator()(size_t row, size_t col) {
return data[row][col];
}
Matrix operator+(const Matrix& other) const {
if (rows != other.rows || cols != other.cols) {
throw std::invalid_argument("Matrices must have the same dimensions for addition.");
}
Matrix result(rows, cols);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
result(i, j) = data[i][j] + other.data[i][j];
}
}
return result;
}
Matrix operator-(const Matrix& other) const {
if (rows != other.rows || cols != other.cols) {
throw std::invalid_argument("Matrices must have the same dimensions for subtraction.");
}
Matrix result(rows, cols);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
result(i, j) = data[i][j] - other.data[i][j];
}
}
return result;
}
Matrix operator*(const Matrix& other) const {
if (cols != other.rows) {
throw std::invalid_argument("Number of columns in first matrix must match number of rows in second matrix for multiplication.");
}
Matrix result(rows, other.cols);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < other.cols; ++j) {
int sum = 0;
for (size_t k = 0; k < cols; ++k) {
sum += data[i][k] * other.data[k][j];
}
result(i, j) = sum;
}
}
return result;
}
friend std::ostream& operator<<(std::ostream& os, const Matrix& matrix) {
for (size_t i = 0; i < matrix.numRows(); ++i) {
for (size_t j = 0; j < matrix.numCols(); ++j) {
os << matrix.data[i][j] << " ";
}
os << std::endl;
}
return os;
}
};
int main() {
try {
Matrix A(2, 2);
A(0, 0) = 1; A(0, 1) = 2;
A(1, 0) = 3; A(1, 1) = 4;
Matrix B(2, 2);
B(0, 0) = 5; B(0, 1) = 6;
B(1, 0) = 7; B(1, 1) = 8;
Matrix C = A + B;
std::cout << "Matrix A + B:" << std::endl;
std::cout << C << std::endl;
Matrix D = A - B;
std::cout << "Matrix A - B:" << std::endl;
std::cout << D << std::endl;
Matrix E = A * B;
std::cout << "Matrix A * B:" << std::endl;
std::cout << E << std::endl;
} catch (const std::invalid_argument& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
运行结果
![[Pasted image 20240607000251.png]]
实验小结
在完成本次实验的过程中,我深入探索了C++中类的继承与多态性,以及运算符重载的概念。这次实验不仅令我对面向对象编程有了更深刻的理解,而且提高了我的编程技能,特别是在设计和实现复杂系统时的能力。
类的继承与多态性 通过设计和实现电梯系统中的不同类别——电梯基类(Elevator)、客梯类(pElevator)、货梯类(cElevator)和客货两用类(dElevator),我学会了如何有效地使用继承来重用和扩展代码。这个练习让我认识到,合理地利用继承可以大大减少代码重复,同时保持代码的清晰性和可维护性。我还学会了如何使用多态来处理不同类型的电梯对象,这在计算单位载客/载重价格时特别有用。这个实践加深了我对多态性的理解,即如何通过基类的指针或引用来操纵派生类的对象。
运算符重载 通过为矩阵类实现加、减、乘运算的重载,我学习了如何扩展C++中的运算符以用于自定义数据类型。这个过程不仅增强了我的C++语法知识,还提高了我解决问题的能力。实现这些运算符时,我遇到了几个挑战,如维度不匹配的错误处理和效率问题。通过这个实践,我了解到了异常处理在运算符重载中的重要性,以及如何创建高效、健壮的运算符重载函数。
思考与扩展
项目架构
项目的架构可以分为三个主要部分:矩阵类的定义和实现、矩阵运算符重载的实现,以及主函数(main)中矩阵运算的演示。
- 矩阵类的定义(Matrix.h):
- 数据成员:
std::vector<std::vector<int>> data
: 存储矩阵的二维向量。size_t rows
: 表示矩阵的行数。size_t cols
: 表示矩阵的列数。
- 成员函数:
Matrix(size_t numRows, size_t numCols)
: 构造函数,创建指定大小的零矩阵。size_t numRows() const
: 返回矩阵的行数。size_t numCols() const
: 返回矩阵的列数。int& operator()(size_t row, size_t col)
: 重载()
运算符以访问和修改矩阵中的特定元素。Matrix operator+(const Matrix& other) const
: 重载+
运算符,实现矩阵加法。 -Matrix operator-(const Matrix& other) const
: 重载-
运算符,实现矩阵减法。Matrix operator*(const Matrix& other) const
: 重载*
运算符,实现矩阵乘法。friend std::ostream& operator<<(std::ostream& os, const Matrix& matrix)
: 友元函数重载<<
运算符,实现矩阵的输出。
- 数据成员:
- 矩阵类的实现(Matrix.cpp):
- 实现了
Matrix.h
中声明的所有构造函数和成员函数。 - 包括运算符重载的具体逻辑,如检查维度匹配、执行逐元素加减或矩阵乘法等。
- 为输出运算符提供了友元函数的实现。
- 实现了
- 主函数(main.cpp):
- 创建了几个
Matrix
类的实例,分别用于演示矩阵加法、减法和乘法。 - 包括异常处理逻辑,以捕获和响应运算中可能出现的错误(如维度不匹配)。
- 输出运算结果,展示了运算符重载在工作中的样子。
项目的文件目录
MatrixProject/
│
├── include/
│ └── Matrix.h // 矩阵类的头文件
│
├── src/ │
├── Matrix.cpp // 矩阵类的实现文件
│
└── main.cpp // 包含main函数的文件,演示矩阵运算
│
└── Makefile // 用于编译项目的Makefile
代码实现
Matrix.h
#ifndef MATRIX_H
#define MATRIX_H
#include <iostream>
#include <vector>
#include <stdexcept>
class Matrix {
private:
std::vector<std::vector<int>> data; // 矩阵数据
size_t rows; // 行数
size_t cols; // 列数
public:
// 构造函数,创建指定大小的零矩阵
Matrix(size_t numRows, size_t numCols);
// 获取矩阵的行数
size_t numRows() const;
// 获取矩阵的列数
size_t numCols() const;
// 访问矩阵中指定位置的元素
int& operator()(size_t row, size_t col);
// 矩阵加法运算符重载
Matrix operator+(const Matrix& other) const;
// 矩阵减法运算符重载
Matrix operator-(const Matrix& other) const;
// 矩阵乘法运算符重载
Matrix operator*(const Matrix& other) const;
// 输出矩阵内容
friend std::ostream& operator<<(std::ostream& os, const Matrix& matrix);
};
#endif // MATRIX_H
Matrix.cpp
#include "Matrix.h"
// 构造函数,创建指定大小的零矩阵
Matrix::Matrix(size_t numRows, size_t numCols) : rows(numRows), cols(numCols) {
data.resize(rows, std::vector<int>(cols, 0));
}
// 获取矩阵的行数
size_t Matrix::numRows() const {
return rows;
}
// 获取矩阵的列数
size_t Matrix::numCols() const {
return cols;
}
// 访问矩阵中指定位置的元素
int& Matrix::operator()(size_t row, size_t col) {
return data[row][col];
}
// 矩阵加法运算符重载
Matrix Matrix::operator+(const Matrix& other) const {
if (rows != other.rows || cols != other.cols) {
throw std::invalid_argument("Matrices must have the same dimensions for addition.");
}
Matrix result(rows, cols);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
result(i, j) = data[i][j] + other.data[i][j];
}
}
return result;
}
// 矩阵减法运算符重载
Matrix Matrix::operator-(const Matrix& other) const {
if (rows != other.rows || cols != other.cols) {
throw std::invalid_argument("Matrices must have the same dimensions for subtraction.");
}
Matrix result(rows, cols);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
result(i, j) = data[i][j] - other.data[i][j];
}
}
return result;
}
// 矩阵乘法运算符重载
Matrix Matrix::operator*(const Matrix& other) const {
if (cols != other.rows) {
throw std::invalid_argument("Number of columns in first matrix must match number of rows in second matrix for multiplication.");
}
Matrix result(rows, other.cols);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < other.cols; ++j) {
int sum = 0;
for (size_t k = 0; k < cols; ++k) {
sum += data[i][k] * other.data[k][j];
}
result(i, j) = sum;
}
}
return result;
}
// 输出矩阵内容
std::ostream& operator<<(std::ostream& os, const Matrix& matrix) {
for (size_t i = 0; i < matrix.numRows(); ++i) {
for (size_t j = 0; j < matrix.numCols(); ++j) {
os << matrix.data[i][j] << " ";
}
os << std::endl;
}
return os;
}
main.cpp
#include "Matrix.h"
int main() {
try {
Matrix A(2, 2);
A(0, 0) = 1; A(0, 1) = 2;
A(1, 0) = 4; A(1, 1) = 5;
Matrix B(2, 2);
B(0, 0) = 7; B(0, 1) = 8;
B(1, 0) = 9; B(1, 1) = 10;
// 矩阵加法
Matrix C = A + B;
std::cout << "Matrix A + B:" << std::endl;
std::cout << C << std::endl;
// 矩阵减法
Matrix D = A - B;
std::cout << "Matrix A - B:" << std::endl;
std::cout << D << std::endl;
// 矩阵乘法
Matrix E = A * B;
std::cout << "Matrix A * B:" << std::endl;
std::cout << E << std::endl;
} catch (const std::invalid_argument& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
附录
(代码见附件)