实验二

一、实验目的

  • 加深对OOP概念(类、对象)和特性(封装)的理解
  • 会用C++正确定义、实现、测试类;会创建对象,并基于对象编程
  • 加深对C++内存资源管理技术的理解,能够解释构造函数、析构函数的用途,分析它们何时会被调用
  • 会用多文件方式组织代码
  • 针对具体问题场景,练习运用面向对象思维设计,合理利用C++语言特性(封装与访问权限控制, static, friend,
  • const),在数据共享和保护之间达到平衡

二、实验准备

系统浏览教材第4-5章,熟悉以下内容:

  • 类的抽象、设计
  • 使用C++定义类、使用类创建对象的语法;构造/析构函数语法、用途及调用时机
  • 类的共享与保护机制:访问权限控制、静态成员(static)、友元(friend)、const
  • 多文件组织代码

三、实验内容

1. 实验任务一

说明

验证性实验:简单类T的定义和测试。实践、阅读代码,回答问题。

这个简单任务覆盖以下内容:

  • 类的定义(封装)

  • 类的使用:对象的创建、访问

  • 数据共享

    • 在同一个对象的所有操作之间共享 —— 实现基础:封装

    • 在同一个类的所有对象之间共享 —— 实现机制:类的static成员

    • 在不同模块(类、函数)之间共享 —— 实现机制:友元

  • 数据保护机制

    • const对象

    • const引用作为形参

    • const成员数据/const成员函数

  • 代码组织方式:多文件结构

代码组织:

  • T.h 内容:类T的声明、友元函数声明
  • T.cpp 内容:类T的实现、友元函数实现
  • task1.cpp 内容:测试模块、main函数

代码

T.h
#pragma once

#include <string>

// 类T: 声明
class T {
// 对象属性、方法
public:
    T(int x = 0, int y = 0);   // 普通构造函数
    T(const T &t);  // 复制构造函数
    T(T &&t);       // 移动构造函数
    ~T();           // 析构函数

    void adjust(int ratio);      // 按系数成倍调整数据
    void display() const;           // 以(m1, m2)形式显示T类对象信息

private:
    int m1, m2;

// 类属性、方法
public:
    static int get_cnt();          // 显示当前T类对象总数

public:
    static const std::string doc;       // 类T的描述信息
    static const int max_cnt;           // 类T对象上限

private:
    static int cnt;         // 当前T类对象数目

// 类T友元函数声明
    friend void func();
};

// 普通函数声明
void func();
T.cpp
#include "T.h"
#include <iostream>
#include <string>

// 类T实现

// static成员数据类外初始化
const std::string T::doc{"a simple class sample"};
const int T::max_cnt = 999;
int T::cnt = 0;

// 类方法
int T::get_cnt() {
   return cnt;
}

// 对象方法
T::T(int x, int y): m1{x}, m2{y} { 
    ++cnt; 
    std::cout << "T constructor called.\n";
} 

T::T(const T &t): m1{t.m1}, m2{t.m2} {
    ++cnt;
    std::cout << "T copy constructor called.\n";
}

T::T(T &&t): m1{t.m1}, m2{t.m2} {
    ++cnt;
    std::cout << "T move constructor called.\n";
}    

T::~T() {
    --cnt;
    std::cout << "T destructor called.\n";
}           

void T::adjust(int ratio) {
    m1 *= ratio;
    m2 *= ratio;
}    

void T::display() const {
    std::cout << "(" << m1 << ", " << m2 << ")" ;
}     

// 普通函数实现
void func() {
    T t5(42);
    t5.m2 = 2049;
    std::cout << "t5 = "; t5.display(); std::cout << '\n';
}
task1.cpp
#include "T.h"
#include <iostream>

void test_T();

int main() {
    std::cout << "test Class T: \n";
    test_T();

    std::cout << "\ntest friend func: \n";
    func();

}

void test_T() {
    using std::cout;
    using std::endl;

    cout << "T info: " << T::doc << endl;
    cout << "T objects'max count: " << T::max_cnt << endl;
    cout << "T objects'current count: " << T::get_cnt() << endl << endl;
    
    T t1;
    cout << "t1 = "; t1.display(); cout << endl;
    
    T t2(3, 4);
    cout << "t2 = "; t2.display(); cout << endl;
    
    T t3(t2);
    t3.adjust(2);
    cout << "t3 = "; t3.display(); cout << endl;
    
    T t4(std::move(t2));
    cout << "t4 = "; t4.display(); cout << endl;
    
    cout << "test: T objects'current count: " << T::get_cnt() << endl;

}

结果

实验一

回答问题

问题1:T.h中,在类T内部,已声明 func 是T的友元函数。在类外部,去掉line36,重新编译,程序能否正常运行。

如果能,回答YES;如果不能,以截图形式提供编译报错信息,说明原因。

答:不能。因为类内的 friend 声明只授予访问权限,不会引入函数名到外部作用域,所以,外部调用前仍需独立声明该函数。

实验一回答一报错


问题2:T.h中,line9-12给出了各种构造函数、析构函数。总结它们各自的功能、调用时机。

答:T(int x = 0, int y = 0)是一个普通构造函数,接收最多两个变量,默认都是0,直接初始化T t(a, b)即可。T(const T &t)是复制构造函数,将另一个同样的类变量中的数据拷贝到当前的新类中,当使用T t(ot)时会调用(ot为另一个T类)。T(T &&t)是移动构造函数,用于从一个临时对象(右值)中构造一个新对象,而无需进行深拷贝,当使用T t = ot时会调用。~T()是析构函数,在销毁类的时候自动调用,可以释放资源。


问题3:T.cpp中,line8-10,剪切到T.h的末尾,重新编译,程序能否正确编译。

如不能,以截图形式给出报错信息,分析原因。

答:不能正确编译。定义在头文件里之后,如果多个 .cpp 文件包含了这个头文件,每个翻译单元都会生成一个定义,链接器就会报“重复定义”。而#pragma once 只能防止同一个源文件在一次编译过程中被重复包含,它不会防止多个不同的源文件各自独立地包含头文件。因此,将此处代码剪切到头文件后不能正确编译,会报重复定义的错误。

实验一问题三报错


2.实验任务二

说明

不使用C++标准库提供的复数模板类,设计并实现一个简化版复数类Complex。要求如下:

  • 类属性

    doc:用于类说明, std::string 类型,常量,公有

    说明信息为: a simplified complex class

  • 对象属性

    用于表示复数的实部real和虚部imag, 均为小数形式,私有

  • 对象方法

    • 构造函数

      要求支持以下方式构造复数对象

      Complex c1; // 构造的复数对象,对应数学中的0+0i
      
      Complex c2(3.5); // 构造的复数对象,对应数学中的3.5+0i
      
      Complex c3(3,-4); // 构造的复数对象,对应数学中的3-4i
      
      Complex c4(c2); // 用已经存在的复数对象c2构造c4
      
      Complex c5 = c2; // 用已经存在的复数对象c2构造c5
      
    • 接口

      • get_real() 返回复数实部

      • get_imag() 返回复数虚部

      • add() 用于把一个复数加到自身,用法: c1.add(c2) , 相当于 c1 += c2

  • 友元函数

    • output() 用于输出一个复数,以a+bi的形式,如3 + 4i

    • abs() 用于对复数取模。比如, Complex c(3, 4); 调用 abs(c) 结果为5.0

    • add() 用于实现两个复数相加,返回复数。比如 c3 = add(c1, c2)

    • is_equal() 用于判断两个复数是否相等,相等返回 true , 否则,返回 false

    • is_not_equal() 用于判断两个复数是否相等,不相等返回 true , 否则,返回 false

    • 代码要求

      • 采用多文件组织代码:

        Complex.h 类Complex声明、友元函数声明

        Complex.cpp 类Complex实现、友元函数实现

        task2.cpp 测试代码、main()函数 (测试代码已经在task2.cpp给出)

  • 类设计及编码风格要求

    • 设计并实现类时,合理利用数据的共享和保护机制,尽可能在数据共享和保护之间达到平衡
    • 关注编码风格、可读性、易维护性

代码

task2.cpp
#include "Complex.h"
#include <iostream>
#include <iomanip>
#include <complex>

void test_Complex();
void test_std_complex();

int main() {
    std::cout << "*******测试1: 自定义类Complex*******\n";
    test_Complex();

    std::cout << "\n*******测试2: 标准库模板类complex*******\n";
    test_std_complex();

}

void test_Complex() {
    using std::cout;
    using std::endl;
    using std::boolalpha;

    cout << "类成员测试: " << endl;
    cout << Complex::doc << endl << endl;
    
    cout << "Complex对象测试: " << endl;
    Complex c1;
    Complex c2(3, -4);
    Complex c3(c2);
    Complex c4 = c2;
    const Complex c5(3.5);
    
    cout << "c1 = "; output(c1); cout << endl;
    cout << "c2 = "; output(c2); cout << endl;
    cout << "c3 = "; output(c3); cout << endl;
    cout << "c4 = "; output(c4); cout << endl;
    cout << "c5.real = " << c5.get_real() 
         << ", c5.imag = " << c5.get_imag() << endl << endl;
    
    cout << "复数运算测试: " << endl;
    cout << "abs(c2) = " << abs(c2) << endl;
    c1.add(c2);
    cout << "c1 += c2, c1 = "; output(c1); cout << endl;
    cout << boolalpha;
    cout << "c1 == c2 : " << is_equal(c1, c2) << endl;
    cout << "c1 != c2 : " << is_not_equal(c1, c2) << endl;
    c4 = add(c2, c3);
    cout << "c4 = c2 + c3, c4 = "; output(c4); cout << endl;

}

void test_std_complex() {
    using std::cout;
    using std::endl;
    using std::boolalpha;

    cout << "std::complex<double>对象测试: " << endl;
    std::complex<double> c1;
    std::complex<double> c2(3, -4);
    std::complex<double> c3(c2);
    std::complex<double> c4 = c2;
    const std::complex<double> c5(3.5);
    
    cout << "c1 = " << c1 << endl;
    cout << "c2 = " << c2 << endl;
    cout << "c3 = " << c3 << endl;
    cout << "c4 = " << c4 << endl;
    
    cout << "c5.real = " << c5.real() 
         << ", c5.imag = " << c5.imag() << endl << endl;
    
    cout << "复数运算测试: " << endl;
    cout << "abs(c2) = " << abs(c2) << endl;
    c1 += c2;
    cout << "c1 += c2, c1 = " << c1 << endl;
    cout << boolalpha;
    cout << "c1 == c2 : " << (c1 == c2)<< endl;
    cout << "c1 != c2 : " << (c1 != c2) << endl;
    c4 = c2 + c3;
    cout << "c4 = c2 + c3, c4 = " << c4 << endl;

}

设计的Complex类如下:

Complex.h
#ifndef MY_COMPLEX_H
#define MY_COMPLEX_H
#include <string>

class Complex{
private:
	float real, imag;
	
public:
	static const std::string doc;
	
public:
	Complex()
	:real(0.f), imag(0.f){}
	Complex(float re)
	:real(re), imag(0.f){}
	Complex(float re, float im)
	:real(re), imag(im){ }
	Complex(const Complex& obj)
	:real(obj.real), imag(obj.imag){ }
	
public:
	float get_real() const;
	float get_imag() const;
	void add(const Complex& c);
	
public:
	friend void output(const Complex& c);
	friend float abs(const Complex& c);
	friend Complex add(const Complex& c1,
	const Complex& c2);
	friend bool is_equal(const Complex& c1,
	const Complex& c2);
	friend bool is_not_equal(const Complex& c1,
	const Complex& c2);
};
#endif
Complex.cpp
#include "Complex.h"
#include <iostream>
#include <cmath>
const std::string Complex::doc = "a simplified complex class";

float Complex::get_real() const{
	return this->real;
}

float Complex::get_imag() const{
	return this->imag;
}

void Complex::add(const Complex& c){
	this->real += c.real;
	this->imag += c.imag;
}

void output(const Complex& c){
	std::cout << c.real;
	if (c.imag < 0) std::cout << " - ";
	else std::cout << " + ";
	std::cout << std::abs(c.imag) << "i\n";
}

float abs(const Complex& c){
	return sqrt(c.real * c.real + c.imag * c.imag);
}

Complex add(const Complex& c1,
	const Complex& c2){
		return Complex(c1.real+c2.real,c1.imag+c2.imag);
	}
	
bool is_equal(const Complex& c1,
	const Complex& c2){
		return c1.real == c2.real && c1.imag == c2.imag;
	}
	
bool is_not_equal(const Complex& c1,
	const Complex& c2){
		return !(c1.real == c2.real && c1.imag == c2.imag);
	}

结果

屏幕截图 2025-10-23 195414

回答问题

问题1:比较自定义类 Complex 和标准库模板类 complex 的用法,在使用形式上,哪一种更简洁?函数和运算内在有关联吗?

自定义类 Complex 标准库模板类 complex
c1.add(c2) c1 += c2
c4 = add(c2, c3) c4 = c2 + c3
is_equal(c1, c2) c1 == c2
is_not_equal(c1, c2) c1 != c2
output(c1) cout << c1
abs(c2) abc(c2)

答:自定义类没有对运算符进行重载,而标准库模板类做了这件事,因此标准库模板类可以直接使用类似于普通运算的方式直接对类进行操作从而更加简洁。这里的标准库的运算功能与自定义类的方法功能基本一致,但形式上更加简洁明了。


问题2:

2-1:自定义 Complex 中, output/abs/add/ 等均设为友元,它们真的需要访问 私有数据 吗?(回答“是/否”并给出理由)

2-2:标准库 std::complex 是否把 abs 设为友元?(查阅 cppreference后回答)

2-3:什么时候才考虑使用 friend?总结你的思考。

答:(2-1)否,实际上对于outputabs这些方法来说可以直接调用成员函数get_real()get_imag()来访问私有变量,而不需要直接使用成员属性;而像add这种涉及到修改成员属性的方法一般为了保证封装性,通常考虑设置方法如set_xxx_value()方法来通过成员函数间接修改私有变量。

(2-2)没有。std::complexabs 不是友元,而是完全普通的非成员函数,它通过 public 的 real()/imag() 接口 完成计算。

(2-3)一般来说能不用friend就不用,结合其他语言如C#,Java等面向对象语言的代码经验来看,基本都是通过对成员变量设置两个方法(get/set)来间接获取和修改,这样既保护了封装性,也更好维护。但以下情况也可考虑使用友元:

1.运算符重载需要“对称隐式转换”

例:operator+ 希望左、右操作数都能隐式转换。

class BigInt {
    std::vector<std::uint32_t> digits;   // 私有
    friend BigInt operator+(const BigInt&, const BigInt&);
};

若写成成员函数,则 1 + big 无法通过隐式构造调用;非成员又需访问 digits,于是友元是最简且唯一方案。

2.两个紧密耦合的类,职责上“天生互访”
例:迭代器 ↔ 容器、句柄 ↔ 内部节点。

template<class T>
class List {
    struct Node { T val; Node *next; };
    friend class ListIterator<T>;   // 迭代器要跳节点
};

3.非成员工具函数只读或只写原始表示,且暴露 getter 会泄露抽象
例:矩阵序列化、哈希、高性能 SIMD 运算。
这些函数对对象只做一次性“原始位搬运”,加一堆 getBlock()/getRaw() 反而把内部布局公开得更彻底;此时集中成少数友元,封装破口反而最小。

它们逻辑上是一体的,但出于实现/接口分离不得不拆成两个类。


问题3:如果构造对象时禁用=形式,即遇到 Complex c4 = c2; 编译报错,类Complex的设计应如何调整?

答:Complex c4 = c2这种构造方法就是复制构造,其会调用复制构造函数Complex(const Complex& obj)来创建新对象。需要禁用这种形式可以直接修改复制构造函数为Complex(const Complex& obj) = delete;修改后只要是复制构造类型就会报错(Complex c4(c2)一样是复制构造,所以也会报错)。


Tip : 移动构造函数单独声明之后,会导致 ‘ = ’ 运算符隐式重载被删除,这会导致如 c4 = add(c2, c3) 这样的移动构造无法实现。

3.实验任务三

说明

设计并实现播放器控制类 PlayerControl ,模拟音频播放器中控制操作:播放、暂停、下一首、上一首、结束、退出

(仅模拟播放控制器行为,暂不实现真实音频播放控制)

  • 播放控制,用枚举类型表示,限定以下枚举值:

    enum class ControlType {Play, Pause, Next, Prev, Stop, Unknown};

  • 类属性

    total_cntint类型,私有,用于记录播放控制操作总次数

  • 类方法

    int get_cnt() , 公有,用于获取当前播放控制操作总数

  • 接口

    ControlType parse(const std::string& control_str); 负责将用户输入的控制命令串转换成枚举值

    解析转换时,忽略大小写。例如,用户输入字符串"play", "Play", "PLAY",均会被解析成枚举值Play

    void execute(ControlType cmd) const; 负责模拟执行控制命令

    例如,当cmd是枚举值Play时,屏幕上输出"播放"(暂不实现实际播放行为)

  • 代码组织

    多文件方式

    PlayerControl.h 播放控制类PlayerControl声明

    PlayerControl.cpp 播放控制类PlayerControl实现

    task3.cpp 测试模块 + main

代码

PlayerControl.h
#pragma once
#include <string>

enum class ControlType {Play, Pause, Next, Prev, Stop, Unknown};

class PlayerControl {
public:
    PlayerControl();

    ControlType parse(const std::string& control_str);   // 实现std::string --> ControlType转换
    void execute(ControlType cmd) const;   // 执行控制操作(以打印输出模拟)       
    
    static int get_cnt();

private:
    static int total_cnt;   
};
PlayerControl.cpp
#include "PlayerControl.h"
#include <iostream>
#include <algorithm>   
#include <cctype>
int PlayerControl::total_cnt = 0;

PlayerControl::PlayerControl() {}

ControlType PlayerControl::parse(const std::string& control_str) {
	std::string str = control_str;  //由于无法直接对const类型进行修改,固用临时变量来代替
    std::transform(str.begin(), str.end(), str.begin(),
                   [](unsigned char c){ return std::tolower(c); });//此处实现转小写
    //以下只考虑全小写
    int cmd = -1;   
    if(str == "play"){
    	cmd = 0;
	} 
	else if(str == "pause"){
		cmd = 1;
	}
	else if(str == "next"){
		cmd = 2;
	}
	else if(str == "prev"){
		cmd = 3;
	}
	else if(str == "stop"){
		cmd = 4;
	}
    if (cmd != -1) total_cnt++;
	return static_cast<ControlType>(cmd);               
}

void PlayerControl::execute(ControlType cmd) const {
    switch (cmd) {
    case ControlType::Play:  std::cout << "[play] Playing music...\n"; break;
    case ControlType::Pause: std::cout << "[Pause] Music paused\n";    break;
    case ControlType::Next:  std::cout << "[Next] Skipping to next track\n"; break;
    case ControlType::Prev:  std::cout << "[Prev] Back to previous track\n"; break;
    case ControlType::Stop:  std::cout << "[Stop] Music stopped\n"; break;
    default:                 std::cout << "[Error] unknown control\n"; break;
    }
}

int PlayerControl::get_cnt() {
    return total_cnt;
}
task3.cpp
#include "PlayerControl.h"
#include <iostream>

void test() {
    PlayerControl controller;
    std::string control_str;
    std::cout << "Enter Control: (play/pause/next/prev/stop/quit):\n";

    while(std::cin >> control_str) {
        if(control_str == "quit")
            break;
        
        ControlType cmd = controller.parse(control_str);
        controller.execute(cmd);
        std::cout << "Current Player control: " << PlayerControl::get_cnt() << "\n\n";
    }

}

int main() {
    test();
}

结果

实验三

Plus

要实现模拟播放控制时,输出控制更现代(如使用emoji)(此处主要是Windows系统下),首先需要修改保存格式为UTF - 8(无签名),然后在主函数中添加 SetConsoleOutputCP(CP_UTF8)SetConsoleCP(CP_UTF8)这两个语句(需要Windows.h)。之后在输出语句的字符串前加u8,类似于std::cout << u8"🎵 Playing music...\n";这种形式,确保按照utf-8的格式输出。最终实现效果如下:

实验三plus

4.实验任务四

说明

设计并实现一个分数类Fraction. 要求如下:

  • 类属性

    doc:用于类说明, std::string 类型,常量,公有

    说明信息为:

    Fraction类 v 0.01版.

    目前仅支持分数对象的构造、输出、加/减/乘/除运算

  • 对象属性

    用于表示分数的分子up和分母down, 均为整数形式,私有

  • 对象方法

    • 构造函数

      要求支持以下方式构造分数对象

      Fraction f1(2); // 构造的分数对象,对应数学中的分数2/1,分母为1
      
      Fraction f2(2, -3); // 构造的分数对象,对应数学中的分数-2/3
      
      Fraction f3(f2); // 用已经存在的分数对象,构造新的分数对象
      
    • 接口

      • get_up() 返回分子

      • get_down() 返回分母

      • negative() 用于求负,支持 Fraction f2 = f1.negative() ,形如 f2 = -f1

        求负运算,分数对象f1本身不变,返回值是求负后的分数对象

  • 工具函数

  • 这些工具函数以何种方案实现(友元/命名空间+自由函数/类+static函数),请自行设计。

    • output() 用于输出一个分数,以形如2/3或-2/3这样的形式

      (输出化简后的形式,比如-4/10,输出时化简后的-2/5)

    • add() 用于实现两个分数相加,返回分数。比如 f3 = add(f1, f2)

    • sub() 用于实现两个分数相减,返回分数。比如 f3 = sub(f1, f2)

    • mul() 用于实现两个分数相乘,返回分数。比如 f3 = mul(f1, f2)

    • div() 用于实现两个分数相除法,返回分数。比如 f3 = div(f1, f2)

  • 代码组织

    要求采用多文件方式

    • Fraction.h 类Fraction声明、其他声明

    • Fraction.cpp 类Fraction实现、其他实现

    • task4.cpp 测试代码、main()函数 (测试代码已经在task4.cpp给出)

(如确定方案时,需新增文件或调整task4.cpp内运算调用方式,请根据方案做微调)

  • 类设计及编码风格要求
    • 设计并实现类时,可设计必要的内部工具函数辅助分数计算;合理利用数据的共享和保护机制
    • 关注编码风格、可读性、易维护性

代码

task4.cpp
#include "Fraction.h"
#include <iostream>

void test1();
void test2();
using namespace FracCal;	//使用命名空间,以下即可省略


int main() {
    std::cout << "测试1: Fraction类基础功能测试\n";
    test1();

    std::cout << "\n测试2: 分母为0测试: \n";
    test2();

}

void test1() {
    using std::cout;
    using std::endl;   

    cout << "Fraction类测试: " << endl;
    cout << Fraction::doc << endl << endl;
    
    Fraction f1(5);
    Fraction f2(3, -4), f3(-18, 12);
    Fraction f4(f3);
    cout << "f1 = "; output(f1); cout << endl;
    cout << "f2 = "; output(f2); cout << endl;
    cout << "f3 = "; output(f3); cout << endl;
    cout << "f4 = "; output(f4); cout << endl;
    
    const Fraction f5(f4.negative());
    cout << "f5 = "; output(f5); cout << endl;
    cout << "f5.get_up() = " << f5.get_up() 
        << ", f5.get_down() = " << f5.get_down() << endl;
    
    cout << "f1 + f2 = "; output(add(f1, f2)); cout << endl;
    cout << "f1 - f2 = "; output(sub(f1, f2)); cout << endl;
    cout << "f1 * f2 = "; output(mul(f1, f2)); cout << endl;
    cout << "f1 / f2 = "; output(div(f1, f2)); cout << endl;
    cout << "f4 + f5 = "; output(add(f4, f5)); cout << endl;

}

void test2() {
    using std::cout;
    using std::endl;

    Fraction f6(42, 55), f7(0, 3);
    cout << "f6 = "; output(f6); cout << endl;
    cout << "f7 = "; output(f7); cout << endl;
    cout << "f6 / f7 = "; output(div(f6, f7)); cout << endl;

}
Fraction.h
#ifndef FRACTION_H	//防止重复的一种形式
#define FRACTION_H
#include <string>
#include <iostream>

class Fraction {
private:
	int up, down;	//up表示分子,down表示分母

public:
	static const std::string doc;	//说明文档

public:
	Fraction(int u);
	Fraction(int u, int d);
	Fraction(const Fraction& o);
	//移动构造函数按默认
public:
	int get_up() const;	//获取分子
	int get_down() const;	//获取分母
	Fraction negative();	//取反
};



//由于所有方法都不需要改变原类的属性,且不需要直接访问私有属性,所以不需要使用友元
namespace FracCal {	//专用于Fraction类的方法空间
	namespace FracCav {	//子命名空间
		int gcd(int a, int b);	//用于计算最大公约数
	}


	void output(const Fraction& o);	//输出
	
	Fraction add(const Fraction& f1, const Fraction& f2);//加
	
	Fraction sub(const Fraction& f1, const Fraction& f2);//减
	
	Fraction mul(const Fraction& f1, const Fraction& f2);//乘
	
	Fraction div(const Fraction& f1, const Fraction& f2);//除

}
#endif
Fraction.cpp
#include "Fraction.h"

const std::string Fraction::doc{ "Fraction类 v 0.01版. "
"目前仅支持分数对象的构造、输出、加 / 减 / 乘 / 除运算." };


Fraction::Fraction(int u)
	:up(u), down(1)
{ }
//对象初始化的时候就自动处理好了约分,后续无序再进行
Fraction::Fraction(int u, int d)
	:up(u), down(d)
{
	if (d == 0) {
		return;
	}
	int gc = FracCal::FracCav::gcd(u, d);

	up /= gc;
	down /= gc;

}

Fraction::Fraction(const Fraction& o)
	:up(o.up), down(o.down)
{ }

int Fraction::get_up() const {
	return up;
}
int Fraction::get_down() const {
	return down;
}
Fraction Fraction::negative() {
	return Fraction(-this->up, this->down);
}

namespace FracCal {
	namespace FracCav {
		int gcd(int a, int b) {
			while (b) {
				int temp = a % b;
				a = b;
				b = temp;
			}

			return a;
		}
	}

	//分情况输出结果
	void output(const Fraction& o) {
        //分子为0,且分母不为0的情况下直接输出0,而不是0/x
		if (o.get_up() == 0 && o.get_down() != 0) {
			std::cout << o.get_up() << std::endl;
			return;
		}
        //一旦分母为0,输出错误信息
		else if (o.get_down() == 0) {
			std::cout << "不合法的分母!" << std::endl;
			return;
		}
        //正常情况正常输出
		else {
			std::cout << o.get_up() << "/" << o.get_down() << std::endl;
		}
	}
	
	Fraction add(const Fraction& f1, const Fraction& f2)
	{
		int up, down;
		int gc = FracCal::FracCav::gcd(f1.get_down(), f2.get_down());
		down = f1.get_down() * f2.get_down() /
			gc;
		//计算分母扩大倍数,然后给予分子,之后的sub等方法内通分方法均如此
		int k1 = down / f1.get_down();
		int k2 = down / f2.get_down();
		up = f1.get_up() * k1 + f2.get_up() * k2;
	
		return Fraction(up, down);
	}
	//整体逻辑与add相同,只是加变成减
	Fraction sub(const Fraction& f1, const Fraction& f2)
	{
		int up, down;
		int gc = FracCal::FracCav::gcd(f1.get_down(), f2.get_down());
		down = f1.get_down() * f2.get_down() /
			gc;
		
		int k1 = down / f1.get_down();
		int k2 = down / f2.get_down();
		up = f1.get_up() * k1 - f2.get_up() * k2;
	
		return Fraction(up, down);
	}
	
	Fraction mul(const Fraction& f1, const Fraction& f2)
	{	//构造函数自动完成约分,无须在此约分
		return Fraction(f1.get_up() * f2.get_up(),
			f1.get_down() * f2.get_down());
	}
	
	Fraction div(const Fraction& f1, const Fraction& f2)
	{	//只需考虑f2的分子是否是0
		if (f2.get_up() == 0) {
			return Fraction(0, 0);
		}
	
		return Fraction(f1.get_up() * f2.get_down(),
			f1.get_down() * f2.get_up());
	}

}

结果

实验四

回答问题

分数的输出和计算, output/add/sub/mul/div ,你选择的是哪一种设计方案?(友元/自由函数/命名空间+自由函数/类+static)你的决策理由?如友元方案的优缺点、静态成员函数方案的适用场景、命名空间方案的考虑因素等。

答:我选择自由函数和命名空间的方案。虽然友元函数能直接访问对象的私有属性,也能在一定程度上加速程序运行,但此处因为没必要直接访问和修改对象的私有属性,而且运算也并不复杂,没必要使用友元这种会破坏封装性的方案;静态成员函数在需要对所有同类型的对象进行操作时非常有用,此处使用静态成员函数也可以。命名空间方案在此处也可起到类似静态成员函数的作用,而且不必塞到类中,一定程度上便于维护,这个命名空间中的所有方法都是围绕着Fraction类进行的,非常直观,因此选择该方案。


四、实验结论

核心收获与思考

通过本次实验,我深入理解了 C++ 面向对象编程的核心思想,掌握了类的封装、构造与析构函数、静态成员、友元函数等关键语法特性,并在实际编码中体会到“数据共享与保护”的设计平衡。例如:

  • 在任务1中,通过 static int cnt 统计对象数量,让我直观感受到静态成员在“类级别”数据共享中的作用;
  • 在任务2实现复数类时,对比标准库 std::complex,我意识到运算符重载对使用简洁性的重要性;
  • 任务3的播放器控制类让我练习了枚举与字符串解析的结合,理解了控制逻辑抽象的基本方法;
  • 任务4中设计分数类时,我尝试了友元函数与命名空间自由函数两种方案,最终选择了“命名空间+自由函数”,因为它在不破坏封装的前提下,提供了更好的扩展性和可读性。

感受

  • 多文件组织代码 看似麻烦,但在调试和阅读时极具优势,尤其是类声明与实现分离后,接口更清晰;
  • const 成员函数 的使用不仅是语法要求,更是设计思维的体现;
  • 友元函数虽强大,但应慎用。避免过度暴露内部数据;
  • 现代输出体验(如 emoji)让我知道了控制台不止可以输出普通的文字。

总结

本次实验不仅锻炼了我对 C++ 语法细节的掌握,更让我体会到“设计优先”的编程理念。良好的类设计不是堆砌语法,而是根据实际需求,在封装性、扩展性、可读性之间找到最优解。未来我将继续在实践中打磨面向对象思维,逐步向“写出优雅且高效的 C++ 代码”目标迈进。

posted @ 2025-10-23 19:37  cuupe  阅读(27)  评论(0)    收藏  举报