实验2

四、实验结论

1.实验任务1

程序源代码

点击查看t.h源代码

复制代码
#pragma once

#include <string>

class T {
public:
    T(int x = 0, int y = 0);  
    T(const T &t); 
    T(T &&t);       
    ~T();         

    void adjust(int ratio);    

    static int get_cnt();     

    friend void output(const T &t); 

    static const std::string doc;    
    static const int MAX_CNT;          

private:
    int m1, m2;  

    static int current_cnt;   
};

点击查看t.cpp源代码

复制代码
#include "t.h"

#include <iostream>
#include <stdexcept>
#include <string>

const std::string T::doc{"a simple class sample"};
const int T::MAX_CNT = 999;
int T::current_cnt = 0;

T::T(int x, int y): m1{x}, m2{y} { 
    if(current_cnt == MAX_CNT) {
        throw std::runtime_error("T: maximum object count exceeded");
    }

    ++current_cnt; 
    std::cout << "T constructor called.\n";
} 

T::T(const T &t): m1{t.m1}, m2{t.m2} {
    if(current_cnt == MAX_CNT) {
        throw std::runtime_error("T: maximum object count exceeded");
    }

    ++current_cnt;
    std::cout << "T copy constructor called.\n";
}

T::T(T &&t): m1{t.m1}, m2{t.m2} {
    if(current_cnt == MAX_CNT) {
        throw std::runtime_error("T: maximum object count exceeded");
    }

    ++current_cnt;
    std::cout << "T move constructor called.\n";
}    

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

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

int T::get_cnt() {
   return current_cnt;
}

void output(const T &t) {
    std::cout << "(" << t.m1 << ", " << t.m2 << ")";
}

点击查看task1.cpp源代码

复制代码
#include <iostream>
#include <utility>

#include "t.h"

void test() {
    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 = "; output(t1); cout << endl;

        T t2(3, 4);
        cout << "t2 = "; output(t2); cout << endl;

        T t3(t2);
        t3.adjust(2);
        cout << "t3 = "; output(t3); cout << endl;

        T t4(std::move(t2));
        cout << "t4 = "; output(t4); cout << endl;

        cout << "test: T objects'current count: " << T::get_cnt() << endl;
    }
    cout << "test: T objects'current count: " << T::get_cnt() << endl;
}

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

测试截图

回答问题

1、t.h中,line7-10给出了各种构造函数、析构函数。用文字简洁说明它们各自的功能,调用时机。结合t1-t4,说明它们分别触发了哪个构造函数。

答:

(1)、普通构造函数T(int x=0,int y=0):用来新建对象,初始化成员m1,m2;直接定义对象,传参/不传参时调用,t1、t2触发。

(2)、复制构造函数T(const &t):用一个已经存在的同类型对象,复制一份新对象;用已有对象初始化新对象调用。t3(t2)触发。

(3)、移动构造函数T(t &&t):接管右值;用std::move把左值转换为右值,用来初始化新对象调用。t4(std::move(t2))触发。

(4)、析构函数~T():对象生命周期结束时自动调用,释放对象相关资源;对象离开作用域、销毁前自动调用。t1、t2、t3、t4在大括号结束时逆序调用析构。

2、t.cpp中,line8-10,剪切到t.h的末尾,重新编译,程序能否正确编译。如不能,以截图形式给出报错信息,分析原因。

答:

不能,报错信息如下

原因:doc,MAX_CNT,current_cnt是类的静态成员变量。t.h会被t.cpp和task1.cpp两个源文件同时包含。当把这三个静态成员的初始化定义语句写到头文件末尾时,每一个包含t.h的源文件编译后,都会在自己的.obj文件里生成这三个静态变量。同一个全局符号出现多份定义,报错。

3、类T的测试模块test中,说明对象t1-t4的作用域和生命期。并思考:在这个作用域内,能否通过t1访问其私有成员m1、m2?这体现了类的什么特性?

答:

作用域:test()内部的大括号,出了大括号就失效。

生存期:从对象定义语句创建开始,到大括号结束,自动调用析构函数销毁。

t1不能直接访问t1.m1和t1.m2,因为m1和m2是private私有成员。

体现了类的封装性。封装把数据隐藏在类内部,外部只能通过类的公开成员函数、友元访问私有数据,保护数据。

4、t.cpp中包含了头文件#include<stdexcept> ,构造函数中均使用了以下代码。使用搜索引擎/AI查询throw功能。把t.cpp中T::MAX_CNT值改成2,重新编译、运行程序。观察输出,程序是正常结束还是崩溃了?给出截图运行结果。如果崩溃了,已经创建的对象(如t1),会被正常销毁吗?给出答案,并说明理由。

答:

throw:主动抛出异常,抛出之后当前函数停止继续执行,程序沿着调用栈向上寻找能够捕获该异常的catch;如果全程没有匹配的catch,就是未处理异常,程序最终终止。

MAX_CNT改为2:程序崩溃。

运行现象截图:上方控制台输出到t2创建完成,随后在创建t3,进入拷贝构造时抛出异常,VS弹出未处理异常弹窗(两张截图为本次运行的结果)。

已经创建成功的t1,t2会正常销毁。

理由:当前作用域中已经构造完成的局部对象,自动调用析构函数。t1、t2 构造成功,正常析构;t3在构造中途抛出异常,对象构造未完成,不会执行析构。

2.实验任务2

程序源代码

点击查看Complex.h源代码

复制代码
#pragma once

#include <string>

class Complex {
public:
	static const std::string doc;
	Complex(double r = 0, double i = 0);
	Complex(const Complex& t);

	double real() const;
	double imag() const;

	void add(const Complex& c);

	friend Complex add(const Complex& c1, const Complex& c2);
	friend void output(const Complex& c);
	friend double abs(const Complex& c);
	friend bool is_equal(const Complex& c1, const Complex& c2);
	friend bool is_not_equal(const Complex& c1, const Complex& c2);

private:
	double m_real;
	double m_imag;
};

点击查看Complex.cpp源代码

复制代码
#include"Complex.h"
#include<iostream>
#include<cmath>

const std::string Complex::doc="a simplified complex class";

Complex::Complex(double r , double i):m_real(r),m_imag(i){
}

Complex::Complex(const Complex& t) {
	m_real = t.m_real;
	m_imag = t.m_imag;

}

double Complex::real() const {
	return m_real;
}
double Complex::imag() const {
	return m_imag;
}

void Complex::add(const Complex& c) {
	m_real += c.m_real;
	m_imag += c.m_imag;
}

Complex add(const Complex& c1, const Complex& c2) {
	return Complex(c1.m_real + c2.m_real, c1.m_imag + c2.m_imag);
}
void output(const Complex& c) {
	std::cout << c.m_real;
	if (c.m_imag >= 0)
		std::cout << "+" << c.m_imag << "i";
	else
		std::cout << "-" << -c.m_imag << "i";
}

double abs(const Complex& c) {
	return sqrt(c.m_imag * c.m_imag + c.m_real * c.m_real);
}

bool is_equal(const Complex& c1, const Complex& c2) {
	return bool(c1.m_imag == c2.m_imag && c1.m_real == c2.m_real);
}

bool is_not_equal(const Complex& c1, const Complex& c2) {
	return !is_equal(c1,c2);
}

点击查看task2.cpp源代码

复制代码
#include <iomanip>
#include <iostream>
#include <complex>

#include "Complex.h"

// 测试自定义类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);
    const Complex c6 = 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 = "; output(c5); cout << endl;
    cout << "c6.real = " << c6.real()
        << ", c6.imag = " << c6.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;
}

// 测试标准库类模板complex
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;
    std::complex<double> c5 = 3.5;
    const std::complex<double> c6(3.5);

    cout << "c1 = " << c1 << endl;
    cout << "c2 = " << c2 << endl;
    cout << "c3 = " << c3 << endl;
    cout << "c4 = " << c4 << endl;
    cout << "c5 = " << c5 << endl;

    cout << "c6.real = " << c6.real()
        << ", c6.imag = " << c6.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;
}

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

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

测试截图

回答问题

1、在测试代码中,c6是const对象。

① 为了让 c6.real() 和 c6.imag() 能编译通过,在设计 Complex 类的成员函数时,必须加上什么关键字?如果不加,编译器会报什么错?(请简述或截图报错信息)。

答:

必须加上const。如果不加,报错信息如下图:

② 作为const对象,c6在初始化后允许再被修改。在测试代码中添加执行c6.add(c2);,能编译通过吗?为什么?这体现了类的什么保护机制?

答:

不能,编译结果如下。const类对象只能调用const类成员函数,不能调用非const类成员函数。体现了类的const常量保护机制,保证const对象初始化完成之后,内部数据不能修改。

2、比较自定义类 Complex 和标准库模板类 complex 的用法,在使用形式上,哪种更简洁?据此推断函数和运算是否存在内在关联呢?

答:

我认为标准库类模板在使用形式上更简洁。

内在关联:自定义类和标准库模板类的底层逻辑都是函数来实现,二者其实是等价的,只是书写方式和语法上不同。

3、

①本练习中,output、abs、add(非成员版本)等被设为Complex的友元。这是必须的吗?如果将其声明为普通非成员函数(不用friend ),能否实现相同的功能?简述思路。

答:

不是必须的。如果不使用friend,就要使用类提供的real()和imag()对外接口来读取实部和虚部,不直接访问私有成员,以此完成output、abs、add等操作。

② 标准库 std::complex 是否把 abs 设为友元?(查阅 cppreference后回答,并提供页面具体链接地址)

答:abs不是友元函数。参考链接:https://zh.cppreference.com/cpp/numeric/complex/abs

③ 工业场景中,什么时候才考虑使用 friend ?和AI讨论后,简洁概括你的思考和结论。(限150字内)

答:

一是当函数函数逻辑不属于该类的成员行为,但是又要频繁访问私有成员:比如abs,output等,是外部工具对复数做操作,不像c.add()是对象自身做加法;二是函数需要同时访问多个不同类的私有成员时。但是friend会破坏类的封装性,所以不能滥用友元,在平时优先使用public成员接口。

3.实验任务3

程序源代码

点击查看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 <algorithm>  
#include <iostream>
#include<cctype>

int PlayerControl::total_cnt = 0;

PlayerControl::PlayerControl() {}

// 待补足
// 1. 将输入字符串统一成转为小写,实现大小写不敏感
// 2. 匹配"play"/"pause"/"next"/"prev"/"stop"并返回对应枚举;
//    未匹配的字符串返回ControlType::Unknown
ControlType PlayerControl::parse(const std::string& control_str) {
    //转换为小写
    std::string s = control_str;
    std::transform(s.begin(), s.end(), s.begin(), [](unsigned char ch) {
        return static_cast<unsigned char>(tolower(ch));
        });

    if (s == "play") return ControlType::Play;
    if (s == "pause") return ControlType::Pause;
    if (s == "next") return ControlType::Next;
    if (s == "prev") return ControlType::Prev;
    if (s == "stop") return ControlType::Stop;
    return ControlType::Unknown;

}

void PlayerControl::execute(ControlType cmd) const {
    // 每执行一次控制操作(含未知命令),total_cnt自增 1
    ++total_cnt;

    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 <iostream>

#include "PlayerControl.h"

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();
}

测试截图

回答问题

接口void execute(ControlType cmd) const;是const成员函数,为什么在其内部实现允许代码++total_cnt?

答:

const成员函数表示只能使用成员数据,不能修改;而total_cnt为static静态私有成员,被类的所有对象共享,不是某一个具体对象,所以不受const管控。

4.实验任务4

程序源代码

点击查看Fraction.h源代码

复制代码
#pragma once

#include<iostream>
#include<string>

class Fraction {
public:
	Fraction(int m_up=2, int m_down=1);
	Fraction(const Fraction& t);

	int get_up()const;
	int get_down()const;
	Fraction negative() const;

	static const std::string doc;

private:
	int up;
	int down;
	void normalize();
	static int gcd(int a, int b);
};

namespace frac {
	void output(const Fraction t);
	Fraction add(const Fraction t1, const Fraction t2);
	Fraction sub(const Fraction t1, const Fraction t2);
	Fraction mul(const Fraction t1, const Fraction t2);
	Fraction div(const Fraction t1, const Fraction t2);

}

点击查看Fraction.cpp源代码

复制代码
#include"Fraction.h"
#include<iostream>
#include<string>
#include<stdexcept>
#include<algorithm>

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

int Fraction::gcd(int a, int b) {
	a = std::abs(a);
	b = std::abs(b);
	while (b != 0) {
		int t = b;
		b = a % b;
		a = t;
	}
	return a;
}

void Fraction::normalize() {
	int t = gcd(up, down);
	up /= t;
	down /= t;
	if (down < 0) {
		up /= -1;
		down /= -1;
	}
}


Fraction::Fraction(int m_up , int m_down ) :up(m_up), down(m_down) {
	if (down == 0)
		throw std::invalid_argument("Fraction: 分母不能为0");
	normalize();
}

Fraction::Fraction(const Fraction& t) {
	down = t.down;
	up = t.up;
}

int Fraction::get_up()const {
	return up;
}

int Fraction::get_down()const {
	return down;
}

Fraction Fraction::negative() const {
	return Fraction(-up, down);
}

namespace frac {
	void output(const Fraction t) {
		if (t.get_down() == 1)
			std::cout << t.get_up() ;
		else
			std::cout << t.get_up() << '/'<<t.get_down ();
	}

	Fraction add(const Fraction t1, const Fraction t2) {
		int new_up = t1.get_up() * t2.get_down() + t2.get_up() * t1.get_down();
		int new_down = t1.get_down() * t2.get_down();
		return Fraction(new_up, new_down);
	}

	Fraction sub(const Fraction t1, const Fraction t2) {
		int new_up = t1.get_up() * t2.get_down()- t2.get_up() * t1.get_down();
		int new_down = t1.get_down() * t2.get_down();
		return Fraction(new_up, new_down);
	}

	Fraction mul(const Fraction t1, const Fraction t2) {
		return Fraction(t1.get_up() * t2.get_up(), t1.get_down() * t2.get_down());
	}

	Fraction div(const Fraction t1, const Fraction t2) {
		return Fraction(t1.get_up() * t2.get_down(), t1.get_down() * t2.get_up());
	}
}

点击查看task4.cpp源代码

复制代码
#include <iostream>

#include "Fraction.h"

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 = "; frac::output(f1); cout << endl;
    cout << "f2 = "; frac::output(f2); cout << endl;
    cout << "f3 = "; frac::output(f3); cout << endl;
    cout << "f4 = "; frac::output(f4); cout << endl;

    const Fraction f5(f4.negative());
    cout << "f5 = "; frac::output(f5); cout << endl;
    cout << "f5.get_up() = " << f5.get_up()
        << ", f5.get_down() = " << f5.get_down() << endl;

    cout << "f1 + f2 = "; frac::output(frac::add(f1, f2)); cout << endl;
    cout << "f1 - f2 = "; frac::output(frac::sub(f1, f2)); cout << endl;
    cout << "f1 * f2 = "; frac::output(frac::mul(f1, f2)); cout << endl;
    cout << "f1 / f2 = "; frac::output(frac::div(f1, f2)); cout << endl;
    cout << "f4 + f5 = "; frac::output(frac::add(f4, f5)); cout << endl;
}

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

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

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

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

测试截图

回答问题

分数运算(output/add/sub/mul/div),你选择的是哪种设计方案?你的决策理由?

方案1:分数类 + 友元函数
方案2:分数类 + 命名空间自由函数

答:

选择分数类+命名空间自由函数

理由:友元函数会破坏类的封装性,在这个设计中,运算函数需要频繁读取私有成员,会让这个问题更加突出。因此选择命名空间,通过public接口(get_down(),get_up())读取私有成员,在命名空间里实现加减乘除运算。这样避免了友元打破封装边界,方便维护代码。

五、实验总结

提炼本次实验的核心收获与思考

1、深拷贝和浅拷贝的本质差异:深拷贝需要重新开辟内存,保证每个对象拥有独立的数据空间;浅拷贝只是简单的复制指针。

2、throw抛出异常:主动告诉程序这里发生了非法错误,把错误信息抛出去。如果外面没有写try-catch接住这个异常,程序就直接终止,并且打印错误提示。

3、不同调试器对未捕获异常的拦截方式不一样:在VS2022调试运行时,调试器会直接捕获异常、中断程序(如下图),看不到控制台异常输出;而在Dev-C++运行时,程序正常打印异常信息,和预期样例结果一致。