Trong bài này chúng ta sẽ học lớp và đối tượng trong C++: cách khai báo class với thuộc tính và phương thức, tạo đối tượng, phạm vi truy cập public và private, con trỏ this, và cách tách class ra file .h và .cpp. Đây là cú pháp nền tảng của toàn bộ chương lập trình hướng đối tượng, từ bài này trở đi chương trình thư viện sẽ được xây bằng class.
Bài 15 trong Serie Lập trình C++ từ A tới Z
Lớp và đối tượng trong C++ là gì?
- Lớp (class) là một kiểu dữ liệu do các bạn tự định nghĩa, gồm thuộc tính (dữ liệu) và phương thức (các hàm xử lý dữ liệu đó).
- Đối tượng (object) là một biến có kiểu là lớp đó.
Lớp giống như bản thiết kế một ngôi nhà, còn đối tượng là ngôi nhà xây theo bản thiết kế. Từ một bản thiết kế xây được nhiều ngôi nhà, mỗi nhà sơn màu khác nhau, có người ở khác nhau, nhưng đều cùng một kiểu.
Ở bài so sánh OOP và lập trình hướng cấu trúc chúng ta đã thấy ý tưởng. Giờ đi vào cú pháp cụ thể nhé.
Khai báo class và tạo đối tượng
#include <iostream>
#include <string>
class Sach
{
public:
std::string ten;
std::string tacGia;
int soLuong = 0;
void inThongTin()
{
std::cout << ten << " - " << tacGia << " (con " << soLuong << " cuon)\n";
}
void muon()
{
if (soLuong > 0)
soLuong--;
}
};
int main()
{
Sach s1;
s1.ten = "Lap trinh C++";
s1.tacGia = "Bjarne Stroustrup";
s1.soLuong = 3;
Sach s2;
s2.ten = "Clean Code";
s2.tacGia = "Robert Martin";
s2.soLuong = 1;
s1.muon();
s1.inThongTin();
s2.inThongTin();
std::cout << "sizeof(Sach) = " << sizeof(Sach) << " byte\n";
return 0;
}
Các thành phần của class:
class Sach { ... };: khai báo lớp tênSach. Chú ý dấu;sau ngoặc nhọn đóng, quên nó là lỗi biên dịch rất khó hiểu.ten,tacGia,soLuong: các thuộc tính (member variable).int soLuong = 0;là giá trị mặc định khi tạo đối tượng.inThongTin(),muon(): các phương thức (member function). Bên trong phương thức dùng thẳng tên thuộc tính, không cần truyền vào.Sach s1;: tạo đối tượngs1, giống khai báo biếnint x;.s1.ten,s1.muon(): dùng dấu chấm.để truy cập thuộc tính và gọi phương thức.
Gọi s1.muon() chỉ giảm số lượng của s1, không ảnh hưởng s2. Mỗi đối tượng có bộ thuộc tính riêng. Còn sizeof(Sach) là 72 byte: 2 chuỗi std::string (32 byte mỗi chuỗi trên g++ 64-bit), 1 số int 4 byte, và 4 byte đệm để căn lề. Phương thức không chiếm chỗ trong từng đối tượng.
public, private và class khác struct thế nào?
Ví dụ trên để mọi thứ là public cho dễ hiểu, nhưng như vậy bên ngoài vẫn gán được s1.soLuong = -5. C++ có 3 phạm vi truy cập (access specifier):
| Phạm vi | Ai truy cập được |
|---|---|
public |
Mọi nơi |
private |
Chỉ các phương thức của chính class đó |
protected |
Class đó và các class kế thừa nó (học ở bài kế thừa) |
Trong C++, class và struct gần như giống hệt nhau, chỉ khác phạm vi mặc định: thành viên của class mặc định là private, của struct mặc định là public.
#include <string>
class Sach
{
std::string ten; // khong ghi gi: mac dinh la private
int soLuong = 0;
};
struct TapChi
{
std::string ten; // struct: mac dinh la public
int soKy = 0;
};
int main()
{
TapChi t;
t.soKy = 12; // hop le
Sach s;
s.soLuong = 3; // loi
return 0;
}
Thói quen chung trong C++: dùng struct cho kiểu chỉ gom vài dữ liệu, không có ràng buộc gì (tọa độ x, y; một dòng log). Dùng class khi có dữ liệu cần bảo vệ và các phương thức đi kèm. Việc giấu dữ liệu và thiết kế getter, setter chúng ta sẽ học kỹ ở bài tính đóng gói trong C++.
Con trỏ tới đối tượng và toán tử ->
Có con trỏ tới đối tượng thì truy cập thành viên bằng toán tử mũi tên ->:
#include <iostream>
#include <string>
class Sach
{
public:
std::string ten;
int soLuong = 0;
void muon()
{
if (soLuong > 0)
soLuong--;
}
};
int main()
{
Sach s;
s.ten = "Lap trinh C++";
s.soLuong = 2;
Sach* p = &s; // con tro toi doi tuong
p->muon(); // goi ham qua con tro: dung ->
std::cout << p->ten << " con " << p->soLuong << " cuon\n";
std::cout << "(*p).soLuong = " << (*p).soLuong << '\n';
return 0;
}
p->soLuong chỉ là cách viết gọn của (*p).soLuong. Quy tắc dễ nhớ: biến đối tượng hoặc tham chiếu thì dùng dấu chấm, con trỏ thì dùng mũi tên. Phần con trỏ các bạn xem lại bài mảng và con trỏ trong C++ nhé.
Con trỏ this
Bên trong mỗi phương thức có sẵn một con trỏ ẩn tên this, trỏ tới chính đối tượng đang gọi phương thức. Khi gọi s1.muon() thì this trỏ tới s1, gọi s2.muon() thì this trỏ tới s2. Nhờ vậy cùng một hàm muon() biết phải giảm số lượng của đối tượng nào.
Thường thì không cần viết this ra, nhưng có một trường hợp bắt buộc: tham số trùng tên với thuộc tính.
#include <iostream>
#include <string>
class Sach
{
public:
void datTen(const std::string& ten)
{
this->ten = ten; // dung: this->ten la thuoc tinh
}
void datSoLuong(int soLuong)
{
soLuong = soLuong; // sai: gan tham so cho chinh no
}
void inThongTin()
{
std::cout << "Ten: '" << ten << "', so luong: " << soLuong << '\n';
}
private:
std::string ten;
int soLuong = 0;
};
int main()
{
Sach s;
s.datTen("Lap trinh C++");
s.datSoLuong(5);
s.inThongTin();
return 0;
}
Tên sách được gán đúng, còn số lượng vẫn là 0 dù đã gọi datSoLuong(5). Trong datSoLuong, tên soLuong chỉ tới tham số (biến gần nhất che biến xa hơn, như bài phạm vi biến), nên soLuong = soLuong là tự gán tham số cho chính nó, thuộc tính không hề đổi. g++ không báo gì cả.
Lưu ý: Để không bao giờ dính lỗi này, nhiều dự án đặt tên thuộc tính có hậu tố gạch dưới như soLuong_, hoặc tiền tố m_ như m_soLuong. Từ giờ trong serie mình dùng hậu tố _ cho thuộc tính private, nên tham số đặt tên thoải mái mà không cần this->.
Tách class ra file .h và .cpp
Như bài về hàm, class trong chương trình thật được tách làm 2 phần: khai báo trong file .h, định nghĩa phương thức trong file .cpp.
File sach.h:
#ifndef SACH_H
#define SACH_H
#include <string>
class Sach
{
public:
void datThongTin(const std::string& ten, int soLuong);
bool muon();
void inThongTin() const;
private:
std::string ten_;
int soLuong_ = 0;
};
#endif
File sach.cpp:
#include "sach.h"
#include <iostream>
void Sach::datThongTin(const std::string& ten, int soLuong)
{
ten_ = ten;
soLuong_ = (soLuong >= 0) ? soLuong : 0;
}
bool Sach::muon()
{
if (soLuong_ == 0)
return false;
soLuong_--;
return true;
}
void Sach::inThongTin() const
{
std::cout << ten_ << ": con " << soLuong_ << " cuon\n";
}
File main.cpp:
#include "sach.h"
#include <iostream>
int main()
{
Sach s;
s.datThongTin("Lap trinh C++", 1);
for (int lan = 1; lan <= 2; lan++)
{
if (s.muon())
std::cout << "Lan " << lan << ": muon thanh cong\n";
else
std::cout << "Lan " << lan << ": het sach\n";
}
s.inThongTin();
return 0;
}
Mấy điểm mới trong cách viết này:
Sach::muon(): dấu::gọi là toán tử phạm vi, báo rằngmuonlà phương thức của classSach. ThiếuSach::thì g++ hiểu đây là một hàm tự do tênmuon, không liên quan gì tới class.muon()giờ trả vềboolđể báo cho bên ngoài biết mượn được hay không, thay vì tự in ra màn hình. Class không nên tự quyết định việc hiển thị.inThongTin() const: chữconstsau tên hàm hứa rằng phương thức này không sửa thuộc tính nào. Hàm chỉ đọc thì nên luôn thêmconst, chi tiết ở bài tính đóng gói.datThongTinkiểm tra số lượng âm thì đổi thành 0. Dữ liệu sai bị chặn ngay từ cửa vào.
Cách gán thông tin bằng hàm datThongTin sau khi tạo đối tượng vẫn còn một điểm yếu: quên gọi là đối tượng có tên rỗng. Bài tiếp theo về hàm khởi tạo (constructor) sẽ giải quyết việc này: đối tượng nhận đủ thông tin ngay lúc được tạo ra.
Bài tập
- Viết class
HinhChuNhatcó 2 thuộc tính chiều dài, chiều rộng và 2 phương thức tính chu vi, diện tích. Tạo 2 đối tượng và in kết quả. - Viết class
DocGia(người mượn sách) gồm tên, mã thẻ, số sách đang mượn. Phương thứcmuonThem()không cho mượn quá 5 cuốn. - Đổi
class Sachở ví dụ đầu thànhstruct Sach, bỏ chữpublic:. Chương trình còn chạy không? Vì sao? - Tách class
DocGiaở bài 2 ra 3 filedoc_gia.h,doc_gia.cpp,main.cppvà biên dịch.
Lời giải sẽ có trong bài tổng hợp bài tập OOP C++ nhé.
Kết
Lớp và đối tượng trong C++ là nền móng của mọi thứ phía sau trong chương OOP: class gói dữ liệu với phương thức, đối tượng là từng thể hiện cụ thể, private bảo vệ dữ liệu, còn this giúp phương thức biết đang làm việc với đối tượng nào. Nhớ 2 thói quen từ bài này: thuộc tính để private với tên có hậu tố _, và phương thức chỉ đọc thì thêm const.
Muốn tra cứu chi tiết về class, các bạn xem thêm tài liệu chuẩn trên cppreference.
Nếu đã quen struct trong C, các bạn có thể coi class là struct có thêm hàm và có khóa bảo vệ. Ở bài tiếp theo chúng ta sẽ học constructor và destructor để tạo đối tượng đúng cách. Thấy bài viết hữu ích thì chia sẻ cho bạn bè nhé.







