实验4

Task1

源代码

GradeCalc.h
#pragma once

#include <vector>
#include <array>
#include <string>

class GradeCalc
{
public:
	GradeCalc(const std::string& cname);
	void input(int n); // 录入n个成绩
	void output() const; // 输出成绩
	void sort(bool ascending = false); // 排序 (默认降序)
	int min() const; // 返回最低分(如成绩未录入,返回-1)
	int max() const; // 返回最高分 (如成绩未录入,返回-1)
	double average() const; // 返回平均分 (如成绩未录入,返回0.0)
	void info(); // 输出课程成绩信息

private:
	void compute(); // 成绩统计

private:
	std::string course_name; // 课程名
	std::vector<int> grades; // 课程成绩
	std::array<int, 5> counts; // 保存各分数段人数([0, 60), [60, 70), [70, 80), [80,90), [90, 100]
	std::array<double, 5> rates; // 保存各分数段人数占比
	bool is_dirty; // 脏标记,记录是否成绩信息有变更
};

GradeCalc.cpp
#include "GradeCalc.h"

#include <algorithm>
#include <array>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>

GradeCalc::GradeCalc(const std::string& cname) 
	:course_name{ cname }, is_dirty{ true }
{
	counts.fill(0);
	rates.fill(0);
}

void GradeCalc::input(int n) {
	if (n < 0) {
		std::cerr << "无效输入! 人数不能为负数\n";
		std::exit(1);
	}
	grades.reserve(n);
	int grade;
	for (int i = 0; i < n;) {
		std::cin >> grade;
		if (grade < 0 || grade > 100) {
			std::cerr << "无效输入! 分数须在[0,100]\n";
			continue;
		}
		grades.push_back(grade);
		++i;
	}
	is_dirty = true; // 设置脏标记:成绩信息有变更
}

void GradeCalc::output() const {
	for (auto grade : grades)
		std::cout << grade << ' ';
	std::cout << std::endl;
}

void GradeCalc::sort(bool ascending) {
	if (ascending)
		std::sort(grades.begin(), grades.end());
	else
		std::sort(grades.begin(), grades.end(), std::greater<int>());
}

int GradeCalc::min() const {
	if (grades.empty())
		return -1;
	auto it = std::min_element(grades.begin(), grades.end());
	return *it;
}

int GradeCalc::max() const {
	if (grades.empty())
		return -1;
	auto it = std::max_element(grades.begin(), grades.end());
	return *it;
}

double GradeCalc::average() const {
	if (grades.empty())
		return 0.0;
	double avg = std::accumulate(grades.begin(), grades.end(), 0.0) / grades.size();
	return avg;
}

void GradeCalc::info() {
	if (is_dirty)
		compute();
	std::cout << "课程名称:\t" << course_name << std::endl;
	std::cout << "平均分:\t" << std::fixed << std::setprecision(2) << average() << std::endl;
	std::cout << "最高分:\t" << max() << std::endl;
	std::cout << "最低分:\t" << min() << std::endl;
	const std::array<std::string, 5> grade_range{ "[0, 60) ","[60, 70)","[70, 80)","[80, 90)","[90, 100]" };
	for (int i = grade_range.size() - 1; i >= 0; --i)
		std::cout << grade_range[i] << "\t: " << counts[i] << "人\t"
		<< std::fixed << std::setprecision(2) << rates[i] * 100 << "%\n";
}

void GradeCalc::compute() {
	if (grades.empty())
		return;
	counts.fill(0);
	rates.fill(0.0);
	// 统计各分数段人数
	for (auto grade : grades) {
		if (grade < 60)
			++counts[0]; // [0, 60)
		else if (grade < 70)
			++counts[1]; // [60, 70)
		else if (grade < 80)
			++counts[2]; // [70, 80)
		else if (grade < 90)
			++counts[3]; // [80, 90)
		else
			++counts[4]; // [90, 100]
	}
	// 统计各分数段比例
	for (int i = 0; i < rates.size(); ++i)
		rates[i] = counts[i] * 1.0 / grades.size();
	is_dirty = false; // 更新脏标记
}
demo1.cpp
#include <iostream>
#include <string>
#include "GradeCalc.h"
void test() {
	GradeCalc c1("OOP");
	std::cout << "录入成绩:\n";
	c1.input(5);
	std::cout << "输出成绩:\n";
	c1.output();
	std::cout << "排序后成绩:\n";
	c1.sort(); c1.output();
	std::cout << "*************成绩统计信息*************\n";
	c1.info();

	//GradeCalc c("OOP");
	//c.input(5);
	//c.push_back(97);
}
int main() {
	test();
}

测试截图

Task1

问题

问题1:组合关系识别
GradeCalc 类声明中,逐行写出所有体现"组合"关系的成员声明,并用一句话说明每个被组合对象的功能。

std::string course_name;	存储课程名称,用于标识课程
std::vector<int> grades;	动态存储多个课程成绩,支持成绩的增删改查等操作
std::array<int, 5> counts;	固定大小存储5个分数段的人数统计结果
std::array<double, 5> rates;	固定大小存储5个分数段的人数占比
bool is_dirty;	标记成绩数据是否变更,用于控制是否重新计算统计信息

问题2:接口暴露理解
如在 test 模块中这样使用,是否合法?如不合法,解释原因。

不合法
原因:push_back()函数不是c的成员函数,无法进行正常调用

问题3:架构设计分析
当前设计方案中, compute 在 info 模块中调用:
(1)连续打印3次统计信息, compute 会被调用几次?标记 is_dirty 起到什么作用?
(2)如新增 update_grade(index, new_grade) ,这种设计需要更改 compute 调用位置吗?简洁说明理由。

(1)1次 ; 当且仅当数据被修改时,is_dirty值为true,此时会调用compute()函数进行数据更新
(2)不需要 ; 只需要在update_grade(index, new_grade)函数中修改完成数据后,将is_dirty设置为true。后续在调用info()函数时会再次调用compute()函数进行重新统计

问题4:功能扩展设计
要增加"中位数"统计,不新增数据成员怎么做?在哪个函数里加?写出伪代码。

(1)不新增数据成员的要求下,可以在排序后根据索引直接进行中位数获取的操作
(2)可以在info()函数中添加
(3)在info()函数的最后添加:
if (!grades.empty()) {
    std::vector<int> temp = grades; // 复制一个成绩容器
    std::sort(temp.begin(), temp.end());
    double a;
    int size = temp.size();
    if (size % 2 == 1) {
        a = temp[size / 2]; // 奇数个取中间值
    } else {
        a = (temp[size/2 - 1] + temp[size/2]) / 2.0; // 偶数个取平均
    }
    std::cout << "中位数:\t" << a << std::endl;
}

问题5:数据状态管理
GradeCalc 和 compute 中都包含代码: counts.fill(0); rates.fill(0); 。
compute 中能否去掉这两行?如去掉,在哪种使用场景下会引发统计错误?

不能去掉。
当多次调用compute()函数时,counts和rates会保留上一次统计的旧数据,导致新统计结果与旧数据叠加,造成统计错误

问题6:内存管理理解
input 模块中代码 grades.reserve(n); 如果去掉:
(1)对程序功能有影响吗?(去掉重新编译、运行,观察功能是否受影响)
(2)对性能有影响吗?如有影响,用一句话陈述具体影响。

(1)无影响
(2)有。当n较大时,grades会多次进行扩容操作,这导致内存重新分配和数据拷贝的额外开销

Task2

源代码

GradeCalc.h
#pragma once

#include <array>
#include <string>
#include <vector>

class GradeCalc : private std::vector<int> {
public:
	GradeCalc(const std::string& cname);
	void input(int n); // 录入n个成绩
	void output() const; // 输出成绩
	void sort(bool ascending = false); // 排序 (默认降序)
	int min() const; // 返回最低分
	int max() const; // 返回最高分
	double average() const; // 返回平均分
	void info(); // 输出成绩统计信息

private:
	void compute(); // 计算成绩统计信息

private:
	std::string course_name; // 课程名
	std::array<int, 5> counts; // 保存各分数段人数([0, 60), [60, 70), [70, 80), [80,90), [90, 100]
	std::array<double, 5> rates; // 保存各分数段占比
	bool is_dirty; // 脏标记,记录是否成绩信息有变更
};
GradeClac.cpp
#include <algorithm>
#include <array>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>

#include "GradeCalc.h"

GradeCalc::GradeCalc(const std::string& cname) : course_name{ cname }, is_dirty{ true } {
	counts.fill(0);
	rates.fill(0);
}
void GradeCalc::input(int n) {
	if (n < 0) {
		std::cerr << "无效输入! 人数不能为负数\n";
		return;
	}
	this->reserve(n);
	int grade;
	for (int i = 0; i < n;) {
		std::cin >> grade;
		if (grade < 0 || grade > 100) {
			std::cerr << "无效输入! 分数须在[0,100]\n";
			continue;
		}
		this->push_back(grade);
		++i;
	}
	is_dirty = true;
}
void GradeCalc::output() const {
	for (auto grade : *this)
		std::cout << grade << ' ';
	std::cout << std::endl;
}
void GradeCalc::sort(bool ascending) {
	if (ascending)
		std::sort(this->begin(), this->end());
	else
		std::sort(this->begin(), this->end(), std::greater<int>());
}
int GradeCalc::min() const {
	if (this->empty())
		return -1;
	return *std::min_element(this->begin(), this->end());
}
int GradeCalc::max() const {
	if (this->empty())
		return -1;
	return *std::max_element(this->begin(), this->end());
}
double GradeCalc::average() const {
	if (this->empty())
		return 0.0;
	double avg = std::accumulate(this->begin(), this->end(), 0.0) / this->size();
	return avg;
}
void GradeCalc::info() {
	if (is_dirty)
		compute();
	std::cout << "课程名称:\t" << course_name << std::endl;
	std::cout << "平均分:\t" << std::fixed << std::setprecision(2) << average() <<
		std::endl;
	std::cout << "最高分:\t" << max() << std::endl;
	std::cout << "最低分:\t" << min() << std::endl;
	const std::array<std::string, 5> grade_range{ "[0, 60) ",
	"[60, 70)",
	"[70, 80)",
	"[80, 90)",
	"[90, 100]" };
	for (int i = grade_range.size() - 1; i >= 0; --i)
		std::cout << grade_range[i] << "\t: " << counts[i] << "人\t"
		<< std::fixed << std::setprecision(2) << rates[i] * 100 << "%\n";
}
void GradeCalc::compute() {
	if (this->empty())
		return;
	counts.fill(0);
	rates.fill(0);
	// 统计各分数段人数
	for (int grade : *this) {
		if (grade < 60)
			++counts[0]; // [0, 60)
		else if (grade < 70)
			++counts[1]; // [60, 70)
		else if (grade < 80)
			++counts[2]; // [70, 80)
		else if (grade < 90)
			++counts[3]; // [80, 90)
		else
			++counts[4]; // [90, 100]
	}
	// 统计各分数段比例
	for (int i = 0; i < rates.size(); ++i)
		rates[i] = counts[i] * 1.0 / this->size();
	is_dirty = false;
}
demo2
#include <iostream>
#include <string>
#include "GradeCalc.h"
void test() {
	GradeCalc c1("OOP");
	std::cout << "录入成绩:\n";
	c1.input(5);
	std::cout << "输出成绩:\n";
	c1.output();
	std::cout << "排序后成绩:\n";
	c1.sort(); c1.output();
	std::cout << "*************成绩统计信息*************\n";
	c1.info();
}
int main() {
	test();
}

测试截图

Task2

问题

问题1:继承关系识别
写出 GradeCalc 类声明体现"继承"关系的完整代码行。

class GradeCalc : private std::vector<int>

问题2:接口暴露理解
当前继承方式下,基类 vector 的接口会自动成为 GradeCalc 的接口吗?
如在 test 模块中这样用,能否编译通过?用一句话解释原因。

(1)不会
(2)不能。GradeCalc采用private继承

问题3:数据访问差异
对比继承方式与组合方式内部实现数据访问的一行典型代码。说明两种方式下的封装差异带来的数据访问接口差异。

继承:直接复用基类的接口
组合:通过成员变量调用其接口

问题4:组合 vs. 继承方案选择
你认为组合方案和继承方案,哪个更适合成绩计算这个问题场景?简洁陈述你的结论和理由。

组合
GradeCalc类与vector是“has-a”的关系,而不是一个“is-a”的关系

Task3

源代码

Graph.h
#pragma once

#include <string>
#include <vector>

enum class GraphType { circle, triangle, rectangle };

class Graph
{
public:
	virtual void draw() {}
	virtual ~Graph() = default;
};

// Circle类声明
class Circle : public Graph {
public:
	void draw();
};
// Triangle类声明
class Triangle : public Graph {
public:
	void draw();
};
// Rectangle类声明
class Rectangle : public Graph {
public:
	void draw();
};


// Canvas类声明
class Canvas {
public:
	void add(const std::string& type); // 根据字符串添加图形
	void paint() const; // 使用统一接口绘制所有图形
	~Canvas(); // 手动释放资源
private:
	std::vector<Graph*> graphs;
};

// 4. 工具函数
GraphType str_to_GraphType(const std::string& s); // 字符串转枚举类型
Graph* make_graph(const std::string& type); // 创建图形,返回堆对象指针
Graph.cpp
#include <algorithm>
#include <cctype>
#include <iostream>
#include <string>

#include "Graph.h"

// Circle类实现
void Circle::draw() { std::cout << "draw a circle...\n"; }
// Triangle类实现
void Triangle::draw() { std::cout << "draw a triangle...\n"; }
// Rectangle类实现
void Rectangle::draw() { std::cout << "draw a rectangle...\n"; }

// Canvas类实现
void Canvas::add(const std::string& type) {
	Graph* g = make_graph(type);
	if (g)
		graphs.push_back(g);
}

void Canvas::paint() const {
	for (Graph* g : graphs)
		g->draw();
}
Canvas::~Canvas() {
	for (Graph* g : graphs)
		delete g;
}

// 工具函数实现
// 字符串 → 枚举转换
GraphType str_to_GraphType(const std::string& s) {
	std::string t = s;
	std::transform(s.begin(), s.end(), t.begin(),
		[](unsigned char c) { return std::tolower(c); });
	if (t == "circle")
		return GraphType::circle;
	if (t == "triangle")
		return GraphType::triangle;
	if (t == "rectangle")
		return GraphType::rectangle;
	return GraphType::circle; // 缺省返回
}
// 创建图形,返回堆对象指针
Graph* make_graph(const std::string& type) {
	switch (str_to_GraphType(type)) {
	case GraphType::circle: return new Circle;
	case GraphType::triangle: return new Triangle;
	case GraphType::rectangle: return new Rectangle;
	default: return nullptr;
	}
}
demo3.cpp
#include <string>
#include "Graph.h"

void test() {
	Canvas canvas;
	canvas.add("circle");
	canvas.add("triangle");
	canvas.add("rectangle");
	canvas.paint();
}

int main() {
	test();
}

测试截图

Task3

问题

问题1:对象关系识别
(1)写出Graph.hpp中体现"组合"关系的成员声明代码行,并用一句话说明被组合对象的功能。
(2)写出Graph.hpp中体现"继承"关系的类声明代码行。

(1)组合:std::vector<Graph*> graphs;
(2)继承:
class Circle : public Graph
class Triangle : public Graph
class Rectangle : public Graph

问题2:多态机制观察
(1) Graph 中的 draw 若未声明成虚函数, Canvas::paint() 中 g->draw() 运行结果会有何不同?
(2)若 Canvas 类 std::vector<Graph*> 改成 std::vector ,会出现什么问题?
(3)若 ~Graph() 未声明成虚函数,会带来什么问题?

(1)无论g指向哪个派生类对象,都会调用基类Graph中的draw()
(2)始终调用Graph::draw(),无法调用派生类中的draw()函数
(3)只会调用基类的析构函数,而不会去调用派生类的析构函数。在通过基类指针删除派生类对象时,因此会导致内存泄露或者资源未释放

问题3:扩展性思考
若要新增星形 Star ,需在哪些文件做哪些改动?逐一列出。

(1)Graph.h文件:
修改GraphType枚举:
enum class GraphType { circle, triangle, rectangle, star };

新增Star类声明:
class Star : public Graph {
public:
    void draw();
};


(2)Graph.cpp文件:
实现Star::draw():
void Star::draw() {
    std::cout << "draw a circle...\n" << std::endl; 
}

扩展str_to_GraphType()函数:
GraphType str_to_GraphType(const std::string& s) {
    if (s == "circle") return GraphType::circle;
    else if (s == "triangle") return GraphType::triangle;
    else if (s == "rectangle") return GraphType::rectangle;
    else if (s == "star") return GraphType::star; // 新增
    else throw std::invalid_argument("Unknown graph type");
}

扩展make_graph()函数:
Graph* make_graph(const std::string& type) {
    GraphType gt = str_to_GraphType(type);
    switch (gt) {
        case GraphType::circle: return new Circle();
        case GraphType::triangle: return new Triangle();
        case GraphType::rectangle: return new Rectangle();
        case GraphType::star: return new Star(); // 新增
        default: throw std::invalid_argument("Unsupported graph type");
    }
}

问题4:资源管理
观察 make_graph 函数和 Canvas 析构函数:
(1) make_graph 返回的对象在什么地方被释放?
(2)使用原始指针管理内存有何利弊?

(1)在Canvas的析构函数中,遍历graphs向量,通过delete g释放每个Graph*指向的对象(delete g会调用正确的派生类的析构函数)
(2)优点:灵活性更高;无需额外的内存
缺点:必须手动释放内存,否则会导致内存泄露

Task4

源代码

Toy.h
#pragma once

#include <string>
#include <iostream>
#include <vector>
#include <memory>

class SpecialAbility {
public:
	virtual ~SpecialAbility() = default;
	virtual std::string performAbility() const = 0;
};

class Toy : public SpecialAbility {
protected:
    std::string name;
    std::string type;
    double money;

public:
    Toy(const std::string& name, const std::string& type, double money);
    virtual ~Toy() = default;

    std::string getName() const;
    std::string getType() const;
    double getMoney() const;

    virtual std::string performAbility() const = 0;  // 纯虚函数
};

//类Plush的实现
class Plush : public Toy {
public:
    Plush(const std::string& name, double money);  
    std::string performAbility() const override; 
};
//类Robot的实现
class Robot : public Toy {
public:
    Robot(const std::string& name, double money);
    std::string performAbility() const override;
};


class ToyFactory {
private:
    std::vector<std::unique_ptr<Toy>> toys;

public:
    void addToy(std::unique_ptr<Toy> toy);
    void displayToys() const;
};
Toy.cpp
#include "Toy.h"

//Toy
Toy::Toy(const std::string& name, const std::string& type, double money)
    : name(name), type(type), money(money) {
}

std::string Toy::getName() const {
    return name;
}

std::string Toy::getType() const {
    return type;
}

double Toy::getMoney() const {
    return money;
}

std::string Toy::performAbility() const {
    return "This toy has a special ability!";
}

//Plush
Plush::Plush(const std::string& name, double money)
    : Toy(name, "Plush", money) {
}

std::string Plush::performAbility() const {
    return getName() + " is a soft plush toy! ";
}

//Robot
Robot::Robot(const std::string& name, double money)
    : Toy(name, "Robot", money) {
}
std::string Robot::performAbility() const {
    return "The robot is moving!";
}

//ToyFactory
void ToyFactory::addToy(std::unique_ptr<Toy> toy) {
    toys.push_back(std::move(toy));
}

void ToyFactory::displayToys() const {
    for (const auto& toy : toys) {
        std::cout << "Name: " << toy->getName()
            << ", Type: " << toy->getType()
            << ", Price: $" << toy->getMoney()
            << ", Ability: " << toy->performAbility()
            << std::endl;
    }
}
demo4.cpp
#include "Toy.h"
#include <memory>

int main() {
    ToyFactory factory;

    // 使用 std::make_unique 创建 Plush 和 Robot 对象
    factory.addToy(std::make_unique<Plush>("Cat", 19.99));
    factory.addToy(std::make_unique<Robot>("Dog", 49.99));

    factory.displayToys();

    return 0;
}

测试截图

Task4

场景描述

根据玩具的名称和金额,输出玩具的类别和特异功能

各类的关系

Toy继承SpecialAbility,Plush和Robot继承Toy
SpecialAbility和Toy都是抽象类。纯虚函数在Plush和Robot类中实现
ToyFactory与Toy是组合关系,是has-a的关系
posted @ 2025-12-02 17:02  永和九年2  阅读(6)  评论(0)    收藏  举报