Menu

Lớp trong C++: định nghĩa đối tượng, thành viên và phương thức

Tìm hiểu cách lớp trong C++ gom dữ liệu và hành vi thành các kiểu dùng lại được: khai báo biến thành viên và phương thức, tạo đối tượng, phân chia public/private, và những cái bẫy như thành viên chưa khởi tạo và con trỏ this.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Lớp là gì

Lớp (class) là bản thiết kế cho một kiểu tự định nghĩa, gom dữ liệu (biến thành viên) và hành vi (hàm thành viên) vào một khối. Các kiểu có sẵn như int và double mô tả những giá trị đơn lẻ, còn lớp cho phép bạn mô hình hóa cả một khái niệm, như một tài khoản ngân hàng, một điểm 2D hay một người chơi, thành một giá trị duy nhất mà bạn truyền đi được.

Bạn đã dùng lớp mà chưa cần tự định nghĩa: std::string và container vector đều là lớp trong thư viện chuẩn. Gọi name.length() hay v.push_back(3) chính là gọi một hàm thành viên trên một đối tượng. Giờ bạn sẽ xây dựng các kiểu của riêng mình theo cùng cách đó, rồi chuyển sang hàm khởi tạo để khởi tạo chúng.

Định nghĩa lớp và tạo đối tượng

Định nghĩa lớp liệt kê các thành viên trong cặp ngoặc nhọn và kết thúc bằng dấu chấm phẩy; quên dấu ; ở cuối là một trong những lỗi phổ biến nhất của người mới. Mỗi đối tượng bạn tạo từ lớp có bản sao biến thành viên của riêng nó.

rex và luna là hai đối tượng riêng biệt. Thay đổi rex.name không ảnh hưởng tới luna.name: mỗi đối tượng giữ dữ liệu của riêng mình. Bạn truy cập thành viên bằng toán tử chấm . trên một đối tượng (hoặc -> khi bạn có con trỏ tới nó).

public và private

Mặc định, mọi thứ trong class đều là private: chỉ các hàm thành viên của chính lớp đó mới chạm được vào. Nhãn public: mở các thành viên ra cho code bên ngoài. Sự phân chia này là cốt lõi của tính đóng gói: bạn để lộ một giao diện an toàn và giấu dữ liệu bên trong để nó không thể rơi vào trạng thái không hợp lệ.

Vì count là private, không nơi gọi nào có thể lén đưa vào một giá trị âm hay vô nghĩa: họ phải đi qua increment(). Từ const sau danh sách tham số của value() hứa rằng hàm sẽ không sửa đổi đối tượng, cho phép bạn gọi nó trên các đối tượng const và cho người đọc thấy rõ ý định.

Khai báo và định nghĩa phương thức

Với lớp nhỏ, định nghĩa phương thức ngay trong lớp (như trên) là ổn. Với lớp lớn hơn, người ta thường khai báo phương thức bên trong lớp và định nghĩa phần thân bên ngoài, dùng cú pháp phạm vi ClassName::method. Cách này giữ cho định nghĩa lớp dễ đọc như một bản tóm tắt giao diện.

Tiền tố Rectangle:: cho trình biên dịch biết hàm thuộc về lớp nào. Bên trong định nghĩa như vậy, bạn vẫn gọi thành viên bằng tên trần (width, area()): trình biên dịch biết chúng thuộc về đối tượng mà phương thức được gọi trên đó.

Con trỏ this

Bên trong mọi hàm thành viên không static, this là con trỏ trỏ tới đối tượng mà hàm được gọi trên đó. Thường bạn không cần nó, vì tên thành viên trần đã chỉ tới đối tượng hiện tại. Nó phát huy tác dụng khi một tham số che khuất một thành viên, tức hai cái trùng tên, và bạn phải phân biệt chúng.

Không có this->, viết x = x; bên trong setX sẽ gán tham số cho chính nó và để nguyên thành viên: một bug âm thầm. this->x làm mọi thứ rõ ràng. Nhiều codebase tránh hẳn vấn đề này bằng cách đặt tên tham số khác đi (ví dụ setX(int newX)), nhưng bạn vẫn sẽ thấy this-> liên tục trong code thực tế.

Lỗi thường gặp

Một vài cái bẫy về lớp khiến người học vấp đi vấp lại:

  • Thiếu dấu chấm phẩy ở cuối. Định nghĩa lớp kết thúc bằng };. Bỏ dấu ; và bạn sẽ nhận một loạt lỗi khó hiểu chỉ vào những gì nằm sau lớp, chứ không phải chính lớp đó.
  • Quên khởi tạo thành viên. Các thành viên kiểu có sẵn như int và double không tự động được gán bằng 0. Đọc một thành viên chưa khởi tạo là undefined behavior. Hãy cho thành viên giá trị mặc định (int count = 0;) hoặc khởi tạo chúng trong hàm khởi tạo.
  • Truy cập thành viên private từ bên ngoài. c.count = 5; trên một thành viên private là lỗi biên dịch; hãy đi qua một phương thức public. Đó là tính đóng gói đang hoạt động đúng như dự định.
  • Nhầm lớp với đối tượng. Dog.bark(); là sai, vì Dog là kiểu. Bạn gọi phương thức trên đối tượng: rex.bark();.
// Uninitialized member - reading 'age' is undefined behavior:
class Cat {
public:
    int age;            // no default value
};

Cat c;
std::cout << c.age;     // garbage value, not 0

Tiếp theo: hàm khởi tạo

Tự gán từng thành viên sau khi tạo đối tượng, như rex.name = ...; rex.age = ...;, vừa tẻ nhạt vừa dễ quên, và đó chính là cách bạn kết thúc với những thành viên chưa khởi tạo. Trang tiếp theo nói về hàm khởi tạo (constructor): những hàm đặc biệt tự chạy khi một đối tượng được tạo, giúp bạn đảm bảo mọi đối tượng đều bắt đầu ở trạng thái hợp lệ với cú pháp gọn gàng chỉ một dòng Dog rex("Rex", 4);.

Câu hỏi thường gặp

Lớp và đối tượng trong C++ khác nhau thế nào?

Lớp là bản thiết kế: nó mô tả một kiểu có dữ liệu (biến thành viên) và hành vi (hàm thành viên) gì. Đối tượng là một instance cụ thể được tạo từ bản thiết kế đó. class Dog { ... }; định nghĩa kiểu một lần; Dog rex; tạo ra một Dog thật mà bạn dùng được. Một lớp có thể tạo ra nhiều đối tượng độc lập.

class và struct trong C++ khác nhau thế nào?

Về mặt kỹ thuật chỉ khác ở mức truy cập mặc định: thành viên của class mặc định là private, còn thành viên của struct mặc định là public. Cả hai đều có thể có hàm thành viên, hàm khởi tạo và kế thừa. Theo quy ước, struct dùng cho những gói dữ liệu đơn giản còn class dùng cho các kiểu có hành vi và bất biến cần bảo vệ.

Con trỏ this trong lớp C++ dùng để làm gì?

Bên trong một hàm thành viên, this là con trỏ trỏ tới đối tượng mà hàm được gọi trên đó. Dùng nó để phân biệt tham số với thành viên (this->x = x;) hoặc để trả về chính đối tượng hiện tại. Bạn hiếm khi cần nó khi truy cập thành viên thông thường: viết x đã là nói tới x của đối tượng hiện tại.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu