C++ Object-Oriented Programming Experiments: Class Design, Complex Numbers, Fractions, and Bank Account Management
1. Experiment Task 1
Verification experiment: Definition and testing of a simple class T. Practice, read the code, and answer questions.
This task covers:
- Class definition (encapsulation)
- Class usage: object creation, access
- Data sharing mechanisms
- Sharing data across operations on the same object — mechanism: encapsulation
- Sharing data across all objects of the same class — mechanism: static class members
- Sharing data across different modules (classes, functions) — mechanism: friends
- Data protection mechanisms
- const objects
- const references as parameters
- const member data/functions
- Code organization: multi-file structure
t.h: classTdeclaration, friend function declarationt.cpp: classTimplementation, friend function implementationtask1.cpp: test module,mainfunction
t.h
#pragma once
#include <string>
// Class T: declaration
class T {
public:
T(int x = 0, int y = 0); // Ordinary constructor
T(const T &t); // Copy constructor
T(T &&t); // Move constructor
~T(); // Destructor
void adjust(int ratio); // Scale data by a coefficient
void display() const; // Display T object info as (m1, m2)
private:
int m1, m2;
public:
static int get_cnt(); // Show current count of T objects
public:
static const std::string doc; // Descriptive info for class T
static const int max_cnt; // Upper limit for T objects
private:
static int cnt; // Current number of T objects
friend void func();
};
// Ordinary function declaration
void func();
t.cpp
#include "t.h"
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
// Static member data out-of-class initialization
const std::string T::doc{"a simple class sample"};
const int T::max_cnt = 999;
int T::cnt = 0;
T::T(int x, int y): m1{x}, m2{y} {
++cnt;
cout << "T constructor called.\n";
}
T::T(const T &t): m1{t.m1}, m2{t.m2} {
++cnt;
cout << "T copy constructor called.\n";
}
T::T(T &&t): m1{t.m1}, m2{t.m2} {
++cnt;
cout << "T move constructor called.\n";
}
T::~T() {
--cnt;
cout << "T destructor called.\n";
}
void T::adjust(int ratio) {
m1 *= ratio;
m2 *= ratio;
}
void T::display() const {
cout << "(" << m1 << ", " << m2 << ")";
}
int T::get_cnt() {
return cnt;
}
void func() {
T t5(42);
t5.m2 = 2049;
cout << "t5 = "; t5.display(); cout << endl;
}
task1.cpp
#include "t.h"
#include <iostream>
using std::cout;
using std::endl;
void test();
int main() {
test();
cout << "\nmain: \n";
cout << "T objects' current count: " << T::get_cnt() << endl;
}
void test() {
cout << "test class T: \n";
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 << "t3 = "; t4.display(); cout << endl;
cout << "T objects' current count: " << T::get_cnt() << endl;
func();
}
Execution result screenshot
(Screenshot placeholder)
Read the code, combine with the execution results, and understand the related knowledge points involved in using C++ encapsulation classes.
Question 1: In t.h, the ordinary function func is declared as a friend of class T inside the class. If you remove line 36 outside the class and recompile, can it run correctly? If it can, explain that line 36 can be removed. If not, provide a screenshot of the compilation error and analyze the possible reason.
Answer: It cannot run correctly.
After a full recompilation, the compiler reports an error stating that "func" is not found. This may be related to the declaration of the friend function.
Question 2: In t.h, lines 9-12 provide various constructors and the destructor. Summarize the functionality of each constructor and the timing of destructor calls.
Answer:
- Ordinary constructor: Used to create an object and initialize member variables via received parameters.
- Copy constructor: Used to create an object and initialize member variables from another object parameter.
- Move constructor: Initializes a new object of the same type using a temporary object. Unlike the copy constructor, which copies resources, the move constructor transfers ownership of resources from the temporary object, leaving it in a safely destructible state.
- Destructor call timing:
- For stack objects, when the object's scope ends (function return, block exit), its destructor is automatically called.
- For heap objects created with
new,deletemust be manually called to destroy the object; otherwise, memory leaks occur.
Question 3: In t.cpp, if lines 13-15 are moved into t.h and recompiled, can the program compile and run correctly?
Answer: It cannot.
(Screenshot placeholder)
2. Experiment Task 2
Without using the complex template class from the C++ standard library, design and implement a simplified complex number class Complex. Requirements:
- Class attribute:
docfor describing the customComplexclass, of typestring, constant, public.- Description: "a simplified complex class"
- Object attributes:
- Represent the real part
realand imaginary partimagof the complex number, both in decimal form, private.
- Represent the real part
- Object methods:
- Constructor:
- Support constructing complex objects in the following ways:
Complex(): real=0, imag=0Complex(r): real=r, imag=0Complex(r, i): real=r, imag=i
- Support constructing complex objects in the following ways:
- Interfaces:
get_real(): returns the real partget_imag(): returns the imaginary partadd(): adds another complex number to itself, e.g.,c1.add(c2)is equivalent toc1 += c2
- Constructor:
- Friend functions:
add(): adds two complex numbers, returns a complex number, e.g.,c3 = add(c1, c2)is_equal(): returnstrueif two complex numbers are equal, otherwisefalseis_not_equal(): returnstrueif two complex numbers are not equal, otherwisefalseoutput(): outputs a complex number in the forma+bi, e.g.,3 + 4iabs(): computes the modulus of a complex number, e.g., forComplex c(3, 4),abs(c)returns5.0
- Code requirements:
- Use a multi-file structure.
Complex.h: classComplexdeclaration, friend function declarationsComplex.cpp: classCompleximplementation, friend function implementationstask2.cpp: test code,main()function (provided)
- Use a multi-file structure.
- Design and coding style requirements:
- Reasonably utilize data sharing and protection mechanisms, achieving a balance between them.
- Pay attention to coding style, readability, and maintainability.
Based on the description and test code, the designed Complex UML class diagram is as follows.
(UML diagram placeholder)
Complex.h
#pragma once
#include<iostream>
#include<string>
#include<cmath>
using namespace std;
class Complex
{
public:
static const string doc;
Complex();
Complex(double r, double i = 0);
Complex(const Complex& other);
double get_real() const;
double get_imag() const;
void add(const Complex& other);
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);
friend void output(const Complex& c);
friend double abs(const Complex& c);
private:
double real;
double imag;
};
Complex.cpp
#include"Complex.h"
const string Complex::doc{ "a simplified Complex class" };
Complex::Complex() : real{ 0 }, imag{ 0 } {};
Complex::Complex(double r, double i) : real{ r }, imag{ i } {};
Complex::Complex(const Complex& other) : real(other.get_real()), imag(other.get_imag()) {};
double Complex::get_real() const { return real; }
double Complex::get_imag() const { return imag; }
void Complex::add(const Complex& other) {
real += other.get_real();
imag += other.get_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);
}
void output(const Complex& c)
{
cout << c.real;
if (c.imag >= 0) { cout << '+'; }
cout << c.imag << "i";
}
double abs(const Complex& c)
{
return sqrt(c.real * c.real + c.imag * c.imag);
}
main.cpp
#include"Complex.h"
void test() {
cout << "Class attribute test: " << endl;
cout << Complex::doc << endl;
cout << endl;
cout << "Complex object test: " << endl;
Complex c1;
Complex c2(3, -4);
const Complex c3(3.5);
Complex c4(c3);
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 << "c4.real = " << c4.get_real() << ", c4.imag = " << c4.get_imag() << endl;
cout << endl;
cout << "Complex operation test: " << 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 != c3 : " << is_not_equal(c1, c3) << endl;
c4 = add(c2, c3);
cout << "c4 = c2 + c3, c4 = "; output(c4); cout << endl;
}
int main() {
test();
}
(Execution result screenshot placeholder)
3. Experiment Task 3
Verification experiment. Use the standard library template class complex to implement complex number operations.
complex is a complex number template class provided in the C++ standard library, offering rich calculation interfaces for complex number types. For extensive usage, refer to: https://en.cppreference.com/w/cpp/numeric/complex
Enter the following code in a C++ coding environment.
task3.cpp
#include <iostream>
#include <complex>
using std::cout;
using std::endl;
using std::boolalpha;
using std::complex;
void test() {
cout << "Standard library template class complex test: " << endl;
complex<double> c1;
complex<double> c2(3, -4);
const complex<double> c3(3.5);
complex<double> c4(c3);
cout << "c1 = " << c1 << endl;
cout << "c2 = " << c2 << endl;
cout << "c3 = " << c3 << endl;
cout << "c4 = " << c4 << endl;
cout << "c4.real = " << c4.real() << ", c4.imag = " << c4.imag() << endl;
cout << endl;
cout << "Complex operation test: " << 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 != c3 : " << (c1 != c3) << endl;
c4 = c2 + c3;
cout << "c4 = c2 + c3, c4 = " << c4 << endl;
}
int main() {
test();
}
(Execution result screenshot placeholder)
Read the code, combine with the execution results, and analyze which interfaces provided by the standard library complex template class are used in the code.
Interfaces:
- Access real and imag parts via
real()andimag()member functions. - Compute modulus via
abs(const complex& other). - Direct arithmetic operations (
+,-,+=, etc.) and equality comparisons (==,!=) via overloaded operators.
Comparison with Task 2:
-
Differences and simplifications when using the standard library template class:
- Output format: standard library shows
(real, imag), while custom class usesreal+imagi. - Arithmetic: standard libray allows direct
+,-on objects; custom class requires external interfaces or friend functions. - Equality check: standard library uses overloaded
==and!=directly.
- Output format: standard library shows
-
Insights from the standard library design:
complexoverloads operators (+,-,*,/) to make complex arithmetic as natural as basic types.- Template parameter
<type>allows customization of real/imaginary component types.
4. Experiment Task 4
Design and implement a fraction class Fraction. Requirements:
- Class attribute:
docfor description, of typestring, constant, public.- Description: "Fraction class v 0.01. Currently supports construction, output, addition/subtraction/multiplication/division of fraction objects."
- Object attributes:
- Represent the numerator
upand denominatordown, both integers, private.
- Represent the numerator
- Object interfaces:
- Constructor:
Fraction(): 0/1Fraction(x): x/1Fraction(x, y): x/y
get_up(): returns numeratorget_down(): returns denominatornegative(): returns the negation of the fraction, e.g.,f2 = f1.negative();similar tof2 = -f1. The original objectf1remains unchanged.
- Constructor:
- Friend functions:
output(): outputs a fraction in simplified form like2/3or-2/5.add(): adds two fractions, returns a fraction, e.g.,f3 = add(f1, f2)sub(): subtracts two fractions, returns a fractionmul(): multiplies two fractions, returns a fractiondiv(): divides two fractions, returns a fraction
- Code requirements:
- Multi-file structure:
Fraction.h: class declaration, friend function declarationsFraction.cpp: class implementation, friend function implementationstask4.cpp: test code,main()function (provided)
- Multi-file structure:
- Design and coding style:
- Use necessary internal helper functions for fraction calculations; reasonably utilize data sharing and protection.
- Focus on coding style, readability, and maintainability.
Fraction.h
#pragma once
#include<iostream>
#include<string>
#include<cstdlib>
using namespace std;
class Fraction
{
public:
static const string doc;
Fraction(int _up = 0, int _down = 1);
Fraction(const Fraction& other);
int get_up() const;
int get_down() const;
Fraction negative();
friend void output(const Fraction& f);
friend Fraction add(const Fraction& f1, const Fraction& f2);
friend Fraction sub(const Fraction& f1, const Fraction& f2);
friend Fraction mul(const Fraction& f1, const Fraction& f2);
friend Fraction div(const Fraction& f1, const Fraction& f2);
private:
static int fac(int a, int b);
void adjust();
private:
int up;
int down;
};
Fraction.cpp
#include"Fraction.h"
const string Fraction::doc{ "Fraction class v 0.01. \nCurrently supports construction, output, add/sub/mul/div operations.\n" };
Fraction::Fraction(int _up, int _down) : up{ _up }, down{ _down } { adjust(); };
Fraction::Fraction(const Fraction& other) : up{ other.get_up() }, down{ other.get_down() } {};
int Fraction::get_up() const { return up; }
int Fraction::get_down() const { return down; }
Fraction Fraction::negative() { return Fraction(-up, down); }
int Fraction::fac(int a, int b)
{
if (!b) { return a; }
return fac(b, a % b);
}
void Fraction::adjust()
{
int factor = fac(abs(up), abs(down));
if (up * down > 0) { up = abs(up) / factor; down = abs(down) / factor; }
else if (up * down < 0) { up = -abs(up) / factor; down = abs(down) / factor; }
}
void output(const Fraction& f)
{
Fraction pf{ f };
if (pf.down == 0) { cout << "Denominator cannot be 0"; }
else
{
pf.adjust();
if (pf.down == 1) { cout << pf.up; }
else if (pf.up == 0) { cout << pf.up; }
else { cout << pf.up << "/" << pf.down; }
}
}
Fraction add(const Fraction& f1, const Fraction& f2)
{
Fraction pf1{ f1 }, pf2{ f2 };
int factor = Fraction::fac(pf1.down, pf2.down);
int lcm = (pf1.down * pf2.down / factor);
pf1.up *= (lcm / pf1.down);
pf2.up *= (lcm / pf2.down);
return Fraction(pf1.up + pf2.up, lcm);
}
Fraction sub(const Fraction& f1, const Fraction& f2)
{
Fraction pf1{ f1 }, pf2{ f2 };
int factor = Fraction::fac(pf1.down, pf2.down);
int lcm = (pf1.down * pf2.down / factor);
pf1.up *= (lcm / pf1.down);
pf2.up *= (lcm / pf2.down);
return Fraction(pf1.up - pf2.up, lcm);
}
Fraction mul(const Fraction& f1, const Fraction& f2)
{
return Fraction(f1.up * f2.up, f1.down * f2.down);
}
Fraction div(const Fraction& f1, const Fraction& f2)
{
return Fraction(f1.up * f2.down, f2.up * f1.down);
}
main.cpp
#include"Fraction.h"
void test1() {
cout << "Fraction class test: " << 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;
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() {
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;
}
int main() {
cout << "Test 1: Fraction class basic function test\n";
test1();
cout << "\nTest 2: Denominator zero test: \n";
test2();
}
Execution result screenshot:
(Screenshot placeholder)
5. Experiment Task 5
Enter the source code from Section 5.7, Example 5-11 of the textbook (Personal Bank Account Management Program) in a C++ coding environment. Compile, run, and test the program. Combine knowledge from Chapters 4-5, understand the code, experience thinking and designing business scenarios with an object-oriented mindset, and understand encapsulation, data sharing, and protection.
Note*: The personal bank account management program is a comprehensive example of object-oriented design and programming provided in the textbook. Section 4.8 of Chapter 4 presents the initial version, followed by iterative improvements in later chapters. It is recommended to start from Chapter 4's version, focusing on class abstraction and design in the banking business scenario, as well as iterative improvements in each subsequent chapter. Systematic and coherent reading and analysis of this example will help you better understand and experience object-oriented design and programming.
Based on your current experience using banking services (offline/bank app), analyze the current problems of this teaching-oriented example, and consider how you would abstract and design if you were to refactor this program.
account.h
#pragma once
class SavingAccount
{
private:
int id;
double balance;
double rate;
int lastDate;
double accumulation;
static double total;
void record(int date, double amount);
double accumulate(int date) const
{
return accumulation + balance * (date - lastDate);
}
public:
SavingAccount(int date, int id, double rate);
int getId() const { return id; }
double getBalance() const { return balance; }
double getRate() const { return rate; }
static double getTotal() { return total; }
void deposit(int date, double amount);
void withdraw(int date, double amount);
void settle(int date);
void show() const;
};
account.cpp
#include"account.h"
#include<cmath>
#include<iostream>
using namespace std;
double SavingAccount::total = 0;
SavingAccount::SavingAccount(int date, int id, double rate)
:id(id), balance(0), rate(rate), lastDate(date), accumulation(0)
{
cout << date << "\t#" << id << " is created" << endl;
}
void SavingAccount::record(int date, double amount)
{
accumulation = accumulate(date);
lastDate = date;
amount = floor(amount * 100 + 0.5) / 100;
balance += amount;
total += amount;
cout << date << "\t#" << id << "\t" << amount << "\t" << balance << endl;
}
void SavingAccount::deposit(int date, double amount)
{
record(date, amount);
}
void SavingAccount::withdraw(int date, double amount)
{
if (amount > getBalance()) { cout << "Error: not enough money" << endl; }
else
{
record(date, -amount);
}
}
void SavingAccount::settle(int date)
{
double interest = accumulate(date) * rate / 365;
if (interest != 0)
{
record(date, interest);
}
accumulation = 0;
}
void SavingAccount::show() const
{
cout << "#" << id << "\tBalance:" << balance;
}
main.cpp
#include"account.h"
#include<iostream>
using namespace std;
int main()
{
SavingAccount sa0(1, 21325302, 0.015);
SavingAccount sa1(1, 58320212, 0.015);
sa0.deposit(5, 5000);
sa1.deposit(25, 10000);
sa0.deposit(45, 5500);
sa1.withdraw(60, 4000);
sa0.settle(90);
sa1.settle(90);
sa0.show();
sa1.show();
cout << "Total: " << SavingAccount::getTotal() << endl;
}
Execution result screenshot:
(Screenshot placeholder)
Thoughts:
- Date handling could use a dedicated date/time library for more standardized processing.
- The
recordfunction rounds amounts to two decimal places, which is inflexible and has too few decimal digits.