实验2
实验任务1:
源码:#include "t.h"
include
include
include
// static成员数据: 类内声明、类外初始化
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 << ")";
}
include
include
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:普通构造函数,普通构造函数,移动构造函数,析构函数
问题2:
这三行是类的静态成员变量的定义(初始化),不是声明。
头文件会被多个.cpp文件#include,一旦放到头文件,多个编译单元都会包含这几条定义,发生多重定义(multiple definition)链接错误。
问题3:1. 作用域 & 生命周期(假设 t1~t4 在同一个 test 函数内定义)
对象t1,t2,t3,t4的作用域:从定义行开始,到所在{}代码块结束;生命周期:进入作用域时调用构造函数创建,离开作用域时调用析构函数销毁。
2. 能不能通过t1访问私有成员m1,m2?
能;在类的成员函数 / 友元函数内部可以访问;在 main、test 等外部普通函数里不能直接访问。
私有成员
private只允许:类自己的成员函数、友元访问。
- 体现的特性:封装(Encapsulation)
把数据(m1,m2)隐藏起来,对外只开放接口,控制访问权限,保护内部数据。
问题4:hrow 的作用
throw 用来主动抛出异常,中断当前正常执行流程,寻找匹配的catch捕获块;如果没有 catch 捕获,程序默认调用std::terminate,终止程序。
MAX_CNT 修改为 2 之后
最多允许创建 2 个 T 对象。
- 创建第 1 个对象:ok,current_cnt=1
- 创建第 2 个对象:ok,current_cnt=2
- 创建第 3 个对象时,
current_cnt == MAX_CNT,执行throw runtime_error
如果代码没有写 try-catch 捕获这个异常:程序直接终止,不是普通崩溃,是异常终止。
- 已经创建成功的 t1、t2 会不会正常销毁?
✅ 会正常调用析构函数销毁
原理:C++ 栈展开(stack unwinding)机制:
当抛出异常、栈回退的时候,**已经构造完成、还在栈上的局部对象,会自动调用析构函数释放,再继续向上找 catch;
只有正在构造过程中、还没构造完成的对象,不会调用析构。
实验任务2:
源代码:#include
include
include
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
std::complex
std::complex
std::complex
std::complex
std::complex
const std::complex
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:必须加的关键字:const
const 对象调用成员函数时,编译器把 &c6 作为隐式 this 实参传入,其类型是 const Complex;而非 const 成员函数的 this 是 Complex,const Complex* → Complex* 会"丢弃 const 限定"(discards qualifiers),直接编译失败。
这体现了const 数据保护(const-correctness):用 const 成员函数标记"只读接口",const 对象只能走只读接口,编译器在编译期就阻止任何修改(const 对象还必须在定义时初始化,const/引用成员只能用初始化列表)。
问题2:std::complex 更简洁
内在关联:C++ 的运算符本质上就是函数,运算符重载把运算符映射为函数调用。 c1 += c2 即 c1.operator+=(c2),c1 == c2 即 operator==(c1, c2),cout << c1 即 operator<<(cout, c1)。所以两者并非两套机制:标准库只是把同样的函数换了个"运算符"的语法外壳,语义完全一一对应——这也是先写函数版再对照运算符版的用意。
问题3:不是必须的。 关键在于类是否提供足够的公有接口
只要提供 real()/imag() 这类只读接口,output、abs、add(非成员版)完全可以写成普通非成员函数,不需要 friend
实验任务3:
源代码:#include "PlayerControl.h"
include
include
include
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;
// tolower 的参数必须是 unsigned char,否则遇到负值字符是未定义行为
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return static_cast
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;
}
截图:

问题1:const 修饰的是 *this(对象本身),而 total_cnt 是 static 成员,不属于任何对象。
const 成员函数只承诺“不修改本对象的非静态数据成员”,它约束的对象范围是 this 指向的实例。
static 数据成员属于类而非对象,不参与 this,访问它不需要对象实例,所以 const 成员函数可以照常修改它。
实验任务4:
源代码:#pragma once
include
// 分数类: 以分子/分母表示, 分母恒为正, 并自动约分
class Fraction {
public:
// 构造函数: 缺省 0/1; 分母为 0 时抛出 std::invalid_argument
Fraction(int up = 0, int down = 1);
// 复制构造函数
Fraction(const Fraction &f);
// 复制赋值运算符
Fraction &operator=(const Fraction &f);
Fraction negative() const; // 返回相反数
int get_up() const; // 分子
int get_down() const; // 分母
static const std::string doc; // 类Fraction的描述信息
private:
int m_up; // 分子
int m_down; // 分母, 恒为正
void normalize(); // 约分并规范符号
};
// 以 up/down 形式输出分数
void output(const Fraction &f);
// 分数的加、减、乘、除
Fraction add(const Fraction &a, const Fraction &b);
Fraction sub(const Fraction &a, const Fraction &b);
Fraction mul(const Fraction &a, const Fraction &b);
Fraction div(const Fraction &a, const Fraction &b); // 除数为 0 时抛出 std::domain_error
include "Fraction.h"
include
include
include
include
// static成员数据: 类内声明、类外初始化
const std::string Fraction::doc{"a simple fraction class"};
Fraction::Fraction(int up, int down) : m_up{up}, m_down{down} {
if (m_down == 0) {
throw std::invalid_argument("Fraction: denominator is zero");
}
normalize();
}
Fraction::Fraction(const Fraction &f) : m_up{f.m_up}, m_down{f.m_down} {}
Fraction &Fraction::operator=(const Fraction &f) {
if (this != &f) {
m_up = f.m_up;
m_down = f.m_down;
}
return *this;
}
Fraction Fraction::negative() const {
return Fraction(-m_up, m_down);
}
int Fraction::get_up() const {
return m_up;
}
int Fraction::get_down() const {
return m_down;
}
// 规范符号并约分: 分母恒为正, 且分子分母互质
void Fraction::normalize() {
if (m_down < 0) {
m_up = -m_up;
m_down = -m_down;
}
// 辗转相除法求 |m_up| 与 |m_down| 的最大公约数
int a = std::abs(m_up);
int b = std::abs(m_down);
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
if (a > 1) {
m_up /= a;
m_down /= a;
}
if (m_up == 0) {
m_down = 1; // 0 统一表示为 0/1
}
}
void output(const Fraction &f) {
std::cout << f.get_up() << "/" << f.get_down();
}
Fraction add(const Fraction &a, const Fraction &b) {
return Fraction(a.get_up() * b.get_down() + b.get_up() * a.get_down(),
a.get_down() * b.get_down());
}
Fraction sub(const Fraction &a, const Fraction &b) {
return Fraction(a.get_up() * b.get_down() - b.get_up() * a.get_down(),
a.get_down() * b.get_down());
}
Fraction mul(const Fraction &a, const Fraction &b) {
return Fraction(a.get_up() * b.get_up(),
a.get_down() * b.get_down());
}
Fraction div(const Fraction &a, const Fraction &b) {
if (b.get_up() == 0) {
throw std::domain_error("Fraction: division by zero");
}
return Fraction(a.get_up() * b.get_down(),
a.get_down() * b.get_up());
}
截图:

问题:我选方案2
理由四条:
分数运算不需要访问私有状态。 分子、分母已经完全决定了一个分数,add/sub/mul/div 只要有这两个只读值就能算,没有“必须看内部表示”的需求。friend 是用来“破封装”的,这里没有需要破的地方。
耦合更低、可替换性更好。 若以后把内部表示改掉(如改 long long、或存 double 比值的近似实现),运算函数一行不用动;用 friend 则要逐个检查所有友元函数,因为它们直接碰 m_up/m_down。
对称二元运算天然适合自由函数。 add(a,b)、mul(a,b) 两个操作数地位平等,写成成员函数会有“左右操作数不对称”的怪异感(a.add(b) 读起来像单目),写成自由函数最自然。
命名空间能隔离通用名字。 output、add、sub 太通用,本课程其他实验(如 t.cpp 里的 output(const T&))就有同名函数。同签名的全局函数会导致重定义/链接错误,而 namespace fraction { ... } 能彻底避免,还可用 using 按需引入。

浙公网安备 33010602011771号