Thắc mắc: Class và hướng đối tượng trong C++

Em đang tự học C++, hiện tại đang làm bài tập viết class cộng hai phân số ạ. em viết thì đã hiển thị đúng kết quả, nhưng code thì em nghĩ là mình dùng sai cách hoặc chưa thực hiện đúng tiêu chí hướng đối tượng. Vì thế, em đăng lên đây nhờ các anh các chị chỉ giáo giúp em tìm khuyết điểm trong code của mình ạ. Em xin chân thành cảm ơn :smiley:

#include <iostream>
using namespace std;

class PhanSo{
private:
	int _ts;
	int _ms;
public:
	void nhap();
	void xuat();
	void rutGon(PhanSo &ps);
	void cong(PhanSo ps1, PhanSo ps2);
};

void PhanSo::nhap(){
	cout << "Nhap tu so: ";
	cin >> this->_ts;
	cout << "Nhap mau so: ";
	cin >> this->_ms;
}

void PhanSo::xuat(){
	cout << this->_ts << "/" << this->_ms;
}

void PhanSo::rutGon(PhanSo &ps){
	int a = ps._ts;
	int b = ps._ms;
	while (a != b){
		if (a > b) a -= b;
		else b -= a;
	}
	ps._ts = ps._ts / a;
	ps._ms = ps._ms / a;
}

void PhanSo::cong(PhanSo ps1, PhanSo ps2){
	PhanSo tong;
	tong._ts = ps1._ts*ps2._ms + ps2._ts*ps1._ms;
	tong._ms = ps1._ms * ps2._ms;
	cout << tong._ts << "/" << tong._ms;
}

int main(){

	PhanSo ps1, ps2;
	ps1.nhap();
	ps2.nhap();
	ps1.rutGon(ps1);
	ps2.rutGon(ps2);
	ps1.cong(ps1, ps2);
	system("pause");
	return 0;
}

@ltd anh Đạt giúp em với ạ :blush:

  1. Kiểm soát dữ liệu không có.
  2. Trong rutgon nên dùng % thay vì -= (tất nhiên là phải sửa :v)
  3. Hàm cong này không kiểm tra tràn số và không rút gọn.
1 Like

Vâng, đúng là hàm cộng không thể rút gọn được phân số ạ :frowning:
Mà kiểm soát dữ liệu không có là như nào anh, thuộc mảng kiến thức nào ạ? Anh có thể chỉ rõ hơn cho em được ko?

Tại sao không được?

Mẫu phải khác 0 và nếu mẫu là số âm thì phải chỉnh lại.

1 Like

Cộng thì nên khai báo là operator để có thể viết được phép tính như sau :

phansotong = phanso1 + phanso2
1 Like

Mình đoán là cái hàm rút gọn kia không chạy đúng với số âm

1 Like

Ngoài ra phép cộng không tính mẫu nhỏ nhất có thể, nên một số phép cộng sẽ khó khăn hơn. Nên tách phần ucln thành helper.

1 Like

Theo như mình thấy, bạn làm thế cũng đúng yêu cầu đề bài, tuy nhiên tư duy thì hơi ngắn, hoặc là do em mới chỉ biết 1 hình thức khai báo của phương thức/hàm.

  • Đã là class, em nên xây dựng cho nó phương thức khởi tạo thay vì hàm tạo mặc định.
  • Phương thức rutGon là phương thức của đối tượng. Giả sử em có 2 đối tượng phân số ps1ps2, em gọi phương thức ps1.rutGon(ps2) thì nó sẽ rút gọn ps2, em thấy có buồn cười không :joy:
    Cách giải quyết là viết phương thức tác động trực tiếp lên đối tượng this (anh viết rút gọn, không có this nhé), đồng thời thay đổi thuật toán để rút gọn cả phân số âm:
void PhanSo::rutGon()
{
	if (_ms < 0)
	{
		_ms = -_ms;
		_ts = -_ts;
	}
	for (int i = 2; i <= abs(abs(_ts) - abs(_ms)); i += 1)
		while (_ts % i == 0 && _ms % i == 0)
		{
			_ts /= i;
			_ms /= i;
		}
}

Như vậy, khi muốn rút gọn phân số ps69, ta chỉ việc ps69.rutGon(); thậm chí nếu gọi phương thức bằng các phương thức của chính đối tượng đó thì chỉ cần rutGon();

  • Phương thức nhap của em nó rất ư là có vấn đề vì không kiểm tra điều kiện. Cái này là tối kỵ nhé.
  • Phương thức xuat của em nên viết riêng cho trường hợp giá trị của phân số là số nguyên.
  • Phương thức cong của em chưa rút gọn phân số.
  • Phương thức cong, em phải tư duy khác một chút: Bởi vì phép cộng của 2 phân số sẽ cho kết quả là 1 phân số, thế nên em nên trả về kiểu dữ liệu PhanSo thay vì kiểu dữ liệu void

Nếu là anh, anh sẽ viết chương trình phức tạp hơn 1 chút như sau (Vì anh viết phương thức cong trả về kiểu dữ liệu PhanSo nên anh viết hàm đó nằm ở ngoài class. Vì hàm nằm ở ngoài class thì không truy cập được vào các thuộc tính private nên anh viết thêm phương thức Get):

#include <iostream>
#include <string>
using namespace std;
class PhanSo
{
private:
	int TuSo = 0;
	int MauSo = 1;
public:
	PhanSo()//Nạp chồng phương thức khởi tạo mặc định
	{
		TuSo = 0;
		MauSo = 1;
	}
	PhanSo(int ts, int ms)//Xây dựng phương thức khởi tạo có tham số
	{
		if (ms == 0)
		{
			cout << "Loi: Mau so bang 0. Khoi tao phan so mac dinh" << endl;
			TuSo = 0;
			MauSo = 1;
		}
		else
		{
			TuSo = ts;
			MauSo = ms;
			RutGon();
		}
	}
	void RutGon()
	{
		if (TuSo == 0)
			MauSo = 1;
		if (MauSo < 0)
		{
			MauSo = -MauSo;
			TuSo = -TuSo;
		}
		for (int i = 2; i <= abs(abs(TuSo) - abs(MauSo)); i += 1)
			while (TuSo % i == 0 && MauSo % i == 0)
			{
				TuSo /= i;
				MauSo /= i;
			}
	}
	string ToString()
	{
		if (MauSo == 1)
			return to_string(TuSo);
		return to_string(TuSo) + "/" + to_string(MauSo);
	}
	void Nhap()
	{
		cout << "Nhap tu so: ";
		cin >> TuSo;
		cout << "Nhap mau so: ";
		cin >> MauSo;
		if (MauSo == 0)
		{
			cout << "Loi: Mau so bang 0. Khoi tao phan so mac dinh" << endl;
			TuSo = 0;
			MauSo = 1;
		}
		else
			RutGon();
	}
	void Xuat()
	{
		cout << "Gia tri phan so: " << ToString() << endl;
	}
	int GetTuSo()
	{
		return TuSo;
	}
	int GetMauSo()
	{
		return MauSo;
	}
};
PhanSo Cong(PhanSo PS1, PhanSo PS2)
{
	int ts1 = PS1.GetTuSo();
	int ts2 = PS2.GetTuSo();
	int ms1 = PS1.GetMauSo();
	int ms2 = PS2.GetMauSo();
	return PhanSo(ts1*ms2 + ts2*ms1, ms1*ms2);
}
void main()
{
	PhanSo ps1, ps2;
	ps1.Nhap();
	ps1.Xuat();
	ps2.Nhap();
	ps2.Xuat();
	ps1.RutGon();//Thực ra không cần thiết vì đã rút gọn ở phương thức khởi tạo
	ps2.RutGon();//Thực ra không cần thiết vì đã rút gọn ở phương thức khởi tạo
	cout << "Tong 2 phan so: " << Cong(ps1, ps2).ToString() << endl;
	system("pause");
}

1 Like

À lộn, trong C++ thì this là con trỏ, không phải đối tượng :joy:

1 Like

Hi Nguyen Dinh Dung.

  1. Bạn đọc về nạp chồng toán tử trong C++.
  2. Không nên đưa hàm nhập xuất vào trong class này.
  3. Phân số thì mẫu số không thể bằng 0 nên bạn viết một hàm khởi tạo tử số mẫu số và ném ngoại lệ khi mẫu bằng 0.
  4. Hàm rút gọn thì viết thành hàm bạn hoặc phương thức thì tùy bạn,

Góp ý thêm.
VIết hết các toán tử mà phân số có thể dùng.
Thêm phương thức lấy giá trị tử số mẫu số.
Toán tử ép kiểu về float hoặc doble.

1 Like

Đổi dấu trước khi chạy thuật toán Euclid là xong rồi @@ đâu cần phải chia làm gì.

Kèm theo khống chế mẫu luôn dương thì chỉ thêm 1 dòng.

1 Like

Theo mình thì dấu của phân số không phải là dấu của tử số hay mẫu số. Vậy nên kiểu dữ liệu của tử số và mẫu số là số nguyên không đấu mẫu khác 0 và thêm một thuộc tính dấu cho phân số nữa.

1 Like

Việc tách dấu lưu riêng không thật sự cần thiết, vì dung lượng sẽ đội lên mà hiệu quả không có.

1 Like

Em cảm ơn các anh, em sẽ nghiền ngẫm lại comment của các anh và học thêm phần kiến thức còn thiếu ạ. Thanks!!! :grin:

Vấn đề là mục đích của mình không phải là tìm UCLN mà là rút gọn phân số, kiểu gì cũng phải dùng thêm một vài biến trung gian để lưu dấu phép tính, các biến tạm để tìm UCLN, tốc độ cải thiện không đáng kể nhưng code sẽ phức tạp hơn 1 chút, trong khi thuật toán của mình nó tường minh hơn. Nếu muốn code bằng Euclid mà sáng sủa thì phải xây riêng 1 hàm tính UCLN.
Còn nếu bạn nào thấy vụ abs... phức tạp thì cho i ≤ min(abs(TuSo), abs(MauSo)) cũng không vấn đề gì.

Hi rogp10.

  1. Thực sự là đội dung lương ? Nếu không đưa trường dấu ra thì rõ rang tử số và mẫu số là kiểu số có dấu -> tốn dung lượng so với việc dùng kiểu không dấu. (phạm vi tăng gấp đôi).
  2. Rõ ràng trong quy tắc sử dụng. Nếu không thuộc tính dấu thì dấu của phân số được lưu ở đâu ? Tử số hay mẫu số hay cả hai ? Tình tranh hai phân số rút gọn bằng nhau có tử số trái dấu có thể sảy ra -> phức tập cho việc so sánh 2 phân số. (-2/3 và 2/-3).
  3. Việc quy ước dấu của phân số là dấu của tử số cũng là một giải pháp nhưng dẫn đến việc dùng số nguyên có dấu cho mẫu số là thừa nhưng nếu để tử số và mẫu số khác kiểu dữ liệu thì lại phức tập trong tính toán.

Thực ra kiểu dữ liệu không dấu và kiểu dữ liệu có dấu tốn dung lượng như nhau, 1 đằng thì từ 0 - 2 ^ n đến 2 ^ n - 1, một đằng thì từ 0 đến 2 ^ 2n - 1, dùng bao nhiều thì xài bấy nhiêu thôi. Chưa kể bây giờ nếu muốn làm triệt để thì người ta có BigInteger rồi, số càng to càng tốn bộ nhớ.

Nhưng mà nếu thêm 1 biến dấu thì đúng là nó phức tạp hơn vì phải có thêm các thao tác kiểm dấu trong bất kỳ phép tính nào, thiếu đi sự tự nhiên của phép tính.

Vấn đề (-2/3 và 2/-3) hay (1/2 và 2/4) thì giải quyết như em ở trên, phương thức rút gọn được đưa vào constructor. Và như vậy thì ngay cả việc xét dấu cũng chỉ cần xét tử số.

Thuật toán bạn đưa ra là O(n), với mẫu số đủ lớn thì chả biết chừng nào xong (gcd cỡ tỉ -> 5-6 tỉ bước, khoảng 2s), trong khi Euclid nháy mắt xong. (hàng log(n))

Vậy bạn tính lưu dấu ở đâu? Có phải là đội dung lượng mà không ích lợi gì không?

1 Like

Hi rogp10.
VD. Mình dùng 3 biếnt int không dấu đề lưu một phân số nhưng có miền giá trị là lớn gấp đôi bạn dùng 2 biến int.

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?