2.9. Thành phần tĩnh và thành phần bạn

Các bạn vẫn ổn chứ ? Tôi biết là 2 bài học trước khá là khó tiêu hóa. Không cần vội vàng, cố gắng của các bạn sẽ được đền đáp xứng đáng sớm thôi.

Để chúng ta cùng xả hơi, bài học tiếp theo này sẽ đơn giản hơn nhiều. Chúng ta sẽ nói về 1 số khái niệm khá đặc biệt trong C++ : các thành phần tĩnh và thành phần bạn. Đừng quá lo lắng, chúng ta sẽ không đi sâu vào những khái niệm này, chỉ cần biết đến sự tồn tại của chúng là đủ rồi.

Những cái chúng ta học dưới đây, các bạn chắc chắn sẽ dùng đến, thậm chí dùng nhiều. Và dần dần theo thực tiễn, các bạn sẽ hiểu lợi ích của việc tại sao các lập trình viên lại tốn công nghĩ ra những thứ lằng nhằng như vậy. Tôi cam đoan là không phải để làm khó chúng ta rồi.

Các phương thức tĩnh

Các phương thức tĩnh (static method) có đôi chút khác biệt so với những phương thức mà chúng ta đa nói qua. Chúng không thuộc về 1 đối tượng thực thể của lớp mà thuộc về bản thân lớp đó. Thật ra, chúng ta có thể coi đây là những hàm bình thường được xếp vào trong khai báo lớp nhưng lại không tác động gì đến các thuộc tính của lớp. Cách mà chúng được sử dụng cũng khá đặc biệt.

Tôi nghĩ rằng không gì tốt hơn là 1 ví dụ cụ thể

Tạo 1 phương thức tĩnh

Dưới đây là 1 nguyên mẫu của phương thức tĩnh trong tệp .h.

class TenLop{
   public:
    TenLop();
    static void phuongThuc();
};

Trong mã xử lý của phương thức trong tệp .cpp, chúng ta không cần đến từ khóa static nữa.

void TenLop::phuongThuc(){ //Khong co tu khoa 'static'
   cout << "Xin chao !" << endl;
}

Và đây mà cách chúng ta sử dụng phương thức trong hàm main().

int main(){
   TenLop::phuongThuc();
   return 0;
}

Chắc các bạn cũng đã nhận ra được điểm đặc biệt của phương thức tĩnh rồi phải không ?

Đúng thế, chúng ta không hề cần tạo ra đối tượng thực thể của lớp nhưng vẫn sử dụng được phương thức. Nếu chúng ta muốn sử dụng nó, đơn giản là viết tên lớp, tiếp theo là dấu :: rồi tới tên của phương thức là đủ. Trong ví dụ ở đây sẽ là TenLop::phuongThuc(); .

Như tôi đã nói, phương thức này không được phép truy cập tới các thuộc tính của lớp. Nó hoàn toàn chỉ đơn giản là 1 hàm được đưa vào trong khai báo lớp. Lợi ích của nó là nhóm các hàm có đặc điểm chung cũng như để tránh xung đột khi trùng tên hàm.

Nghe qua có vẻ ngu ngốc vì nếu cho phép mọi người nhét tất cả các hàm vào trong 1 lớp thì còn cần đến khái niệm đối tượng làm gì.

Câu trả lời là chúng ta luôn cần đến các hàm bình thường đó ngay cả khi thiết kế theo hướng đối tượng. Thực chất, đây là những hàm mà việc tạo ra 1 đối tượng chỉ để gọi nó là hoàn toàn không cần thiết.

Lấy ví dụ nhé!

  • Trong thư viện Qt có lớp QDate dùng để mô tả thời gian theo ngày tháng. Chúng ta có thể thao tác như cộng, so sánh, vv… giữa các đối tượng thời gian. Thế nhưng cũng có những hàm như hàm có giá trị trả về là thời điểm hiện tại. Chúng ta hoàn toàn không cần có 1 đối tượng để có thể lấy được thông tin này. Vậy nên QDate có 1 hàm currentDate(), là 1 phương thức tĩnh, để làm việc đó.
  • Vẫn là Qt có 1 lớp tên là QDir dùng để thao tác với các thư mục. Nó có 1 phương thức tĩnh là QDir::drives() trả về kết quả là tên các phân vùng đĩa trên ổ cứng : C:\, D:\, vv… Chúng ta không cần thiết phải tạo ra 1 đối tượng chỉ để có được những thông tin chung của ổ đĩa như vậy.

Các bạn thấy Qt có đáng để mong chờ không?

Các thuộc tính tĩnh

Chúng ta có các phương thức tĩnh, đương nhiên cũng có các thuộc tính tĩnh (static attribute). Giống phương thức tình, đây cũng là những thuộc tính thuộc về bản thân lớp chứ không thuộc về bất cứ thực thể nào của nó.

Khai báo thuộc tính tĩnh

Rất đơn giản, chúng ta cũng chỉ cần thêm từ khóa static vào đầu dòng khai báo thuộc tính. 1 thuộc tính tĩnh, dù có thể được truy cập trừ bên ngoài lớp, vẫn có thể có quyền truy cập là private hay protected. Đây là 1 ngoại lệ các bạn nên nhớ.

class TenLop{
   public:
     TenLop ();

   private:
     static int thuocTinh;
};

Thế nhưng chúng ta không thể khởi tạo giá trị của nó tại đây mà phải thực hiện trong 1 không gian chung, nghĩa là nằm ngoài tất cả các khai báo lớp cũng như nằm ngoài tất cả các xử lý hàm.

//Nam ngoai khai bao lop va ham
int TenLop::thuocTinh = 5;

! Thường thì chúng ta thực hiện khởi tạo giá trị trong tệp .cpp của lớp.

1 thuộc tính tĩnh sẽ giống như 1 biến toàn thể, nghĩa là có thể được truy cập từ bất cứ đâu trong đoạn mã.

! Những người mới lập trình có xu hướng khai báo nhiều thuộc tính tĩnh để có thể truy cập mọi lúc tới biến mà không cần thông qua việc truyền tham số cho hàm. Nói chung thì đây là 1 thói quen xấu vì sẽ gây trở ngại lớn cho việc bảo dưỡng mã nguồn. Thử tưởng tượng 1 thuộc tính có thể được truy cập từ khắp mọi nơi, làm sao để chúng ta xác định chính xác được lúc nào thì nó bị thay đổi ? Khi mã nguồn lên tới hàng chục đến hàng trăm tệp, làm cách nào mà chúng ta có thể tìm lại được đoạn xử lý đã thay đối biến ? Gần như là không thể.

Vậy nên, chỉ khai báo các thuộc tính tĩnh khi thật sự cần.

1 trong những ứng dụng thường gặp nhất của thuộc tính tĩnh là biến đếm số thực thể của lớp bởi đôi khi chúng ta muốn biết là bao nhiêu đối tượng của lớp đã được tạo ra.

Để làm thế, chúng ta tạo ra 1 thuộc tính tĩnh và khởi tạo giá trị là 0.Chúng ta sẽ tăng giá nó lên 1 đơn vị trong phương thức tạo và bớt đi 1 trong phương thức hủy. Ngoài ra đừng quên tính đóng gói, chúng ta cần cho thuộc tính này quyền truy cập private và truy cập tới nó thông qua phương thức lấy. Đương nhiên, phương thức lấy tương ứng này cũng sẽ là 1 phương thức tĩnh vì chúng ta không cần tạo ra 1 đối tượng để có thể sử dụng nó.

class NhanVat{
   public:
     NhanVat(string ten);
     //Cac phuong thuc khac …
     ~NhanVat();
     static int soThucThe();   //Tra ve so doi tuong thuc the da duoc tao ra

   private:
     string m_ten;
     static int soLuong;
}

Và tất cả xử lý diễn ra trong tệp .cpp.

int NhanVat::soLuong = 0; //Khoi tao bien dem so luong bang 0
NhanVat::NhanVat(string ten) : m_ten(ten){
   soLuong++; //Khi tao them 1 NhanVat, bien dem tang them 1
}

NhanVat::~NhanVat(){
    soLuong--; //Khi xoa 1 NhanVat, bien dem giam di 1
}

int NhanVat::soThucThe(){
   return soLuong;   //Tra ve gia trị cua bien dem
}

Như vậy, bất kỳ lúc nào chúng ta cũng có thể xác định được số nhân vật trong trò chơi thông qua thuộc tính NhanVat::soLuong bằng cách gọi phương thức tĩnh soThucThe().

int main(){
   //Tao ra 2 nhan vat
   NhanVat goliath("Goliath");
   NhanVat david("David");
   cout << "Trong tro choi co tat ca " << NhanVat::soThucThe() << " nhan vat." << endl;
   return 0;
}

Quá đơn giản ! Các bạn dần dần sẽ thấy thêm nhiều ví dụ khác của thuộc tính tĩnh trong C++.

Quan hệ bạn

Chúng ta đã từng tạo ra các lớp mẹ, lớp con, lớp cháu, vv… 1 kiểu quan hệ gia đình. Thế nhưng trong C++ không chỉ có vậy. Chúng ta còn có quan hệ bạn bè.

Thế nào là quan hệ bạn ?

Trong OOP, khái niệm quan hệ bạn ( friend ) nghĩa là trao quyền truy cập hoàn toàn tới tất cả các thành viên của 1 lớp cho 1 hàm.

Điều đó nghĩa là, nếu chúng ta khai báo hàm f là bạn của lớp A, hàm f sẽ có quyền thay đổi tất cả các thuộc tính của A dù cho các thuộc tính này có quyền truy cập là private hay protected. Tương tự, hàm f cũng có thể sử dụng tất cả các phương thức của A bất kể quyền truy cập là gì.

Khi khai báo 1 hàm bạn bè đồng nghĩa với việc chúng ta hủy hoàn toàn tính đóng gói của lớp bởi lẽ 1 đoạn mã bên ngoài lớp có thể dễ dàng thay đổi nội dung của lớp. Vì thế, chúng ta cũng không nên quá lạm dụng khái niệm này.

Ngay từ đầu tôi đã giải thích với các bạn là tính đóng gói chính là thứ làm nên sức mạnh của lập trình hướng đối tượng. Vậy mà mới vừa đây, tôi đã chỉ cho các bạn 1 cách để phá vỡ tính chất này. Tôi thừa nhận là thật là nghịch lý. Tuy nhiên nếu chúng ta lợi dụng tốt quan hệ bạn thì nó trái lại chỉ giúp củng cố thêm tính đóng gói mà thôi. Sau đây chính là dẫn chứng.

Ví dụ lớp ThoiGian

Chúng ta trở lại với 1 ví dụ cũ mà chúng ta từng dùng trong bài học về ghi đè các phép toán : lớp ThoiGian dùng để mô tả 1 khoảng thời gian. Dưới đây là khai báo của lớp này.

class ThoiGian{
   public:
       ThoiGian(int gio = 0, int phut = 0, int giay = 0);
       ThoiGian& operator+=(ThoiGian const& thoiGian);
       void hienThi(std::ostream &out) const;

   private:
       int m_gio;
       int m_phut;
       int m_giay;
};

std::ostream& operator<<(std::ostream& out, ThoiGian const& ThoiGian);

Trong thức tế thì lớp mà chúng ta đã viết đầy đủ hơn nhiều. Ở đây, tôi chỉ viết lại những thành phần mà tôi cần để giải thích cho ví dụ. Cái chúng ta quan tâm là phép toán ghi đè phép ghi vào luồng.

ostream& operator<<(ostream& out, ThoiGian const& ThoiGian){
   ThoiGian.hienThi(out) ;
   return out;
}

Tôi đã từng nói rằng đây là 1 giải pháp tốt, thế nhưng không phải lúc nào cũng là tốt nhất. Trong thực tế, chúng ta đã bắt buộc phải thêm phương thức hienThi() trong trường hợp này. Điều đó đồng nghĩa với việc lớp ThoiGian của chúng ta cung cấp thêm 1 chức năng cho người ngoài sử dụng. Nói cách khác, chúng ta cung cấp thêm 1 cái nút bên ngoài chiếc hộp kín của chúng ta.

Vấn đề là chiếc cần gạt này chỉ mang lại lợi ích thực sự cho phép toán << mà hầu như không mang lại lợi ích sử dụng khác. Với người dùng thì chiếc cần gạt này hoàn toàn vô dụng.

Trong trường hợp phương thức hienThi() của chúng ta thì không phải vấn đề gì lớn. Thế nhưng nếu là 1 phương thức khác mà chúng ta nhất thiết không thể cho người dùng sử dụng thì cách tốt nhất là đừng tạo ra cái nút nào cả. Bởi vì nếu có nút thì dù bạn có dán nhãn là "không nên bấm" thì một ngày nào đó cũng sẽ người nhầm lẫn mà sử dụng đến nó, không thể tránh được. Vậy nên, phương thức hienThi() của chúng ta tốt nhất là nên có mức truy cập private.

class ThoiGian{
   public:
       ThoiGian(int gio = 0, int phut = 0, int giay = 0);
       ThoiGian& operator+=(ThoiGian const& thoiGian);

   private:
       void hienThi(std::ostream &out) const;
       int m_gio;
       int m_phut;
       int m_giay;
};

Thế là chúng ta chắc rằng người dùng sẽ không có khả năng sử dụng phương thức này nữa. Đáng buồn là hàm ghi đè của chúng ta cũng không thể truy cập được nó luôn. Và đây là lúc chúng ta cần sử dụng tới quan hệ bạn bè. Nếu phép toán << là bạn của lớp ThoiGian, nó sẽ có quyền truy cập tới các thành viên private, và đương nhiên là truy cập được đến phương thức hienThi().

Khai báo hàm bạn của lớp

Không tốn nhiều công sức suy nghĩ, những người viết nên C++ chọn từ khóa friend, dịch ra tiếng Việt nghĩa là bạn, để sử dụng cho khai báo hàm bạn.

Cú pháp là từ khóa friend, theo sau là nguyên mẫu của hàm. Và tất cả được đặt bên trong khai báo của lớp bạn mà chúng ta muốn.

class ThoiGian{
   public:
       ThoiGian(int gio = 0, int phut = 0, int giay = 0);
       ThoiGian& operator+=(ThoiGian const& thoiGian);

   private:
       void hienThi(std::ostream &out) const;
       int m_gio;
       int m_phut;
       int m_giay;

       friend std::ostream& operator<<(std::ostream& out, ThoiGian const& ThoiGian);

};

! Các bạn có thể đặt nguyên mẫu này ở bất cứ đâu trong khai báo lớp.

Vậy là phép toán << đã có thể truy cập tới tất cả các thành viên của lớp ThoiGian, trong đó có cả phương thức hiển thị. Cách sử dụng không khác lúc trước trừ việc hàm này là thành phần bên ngoài duy nhất truy cập được tới phương thức này.

Chúng ta cũng có thể làm tương tự với phép toán ==<. Khi chúng trở thành bạn của lớp, chúng có thể thao tác trực tiếp với các thuộc tính và giúp chúng ta bớt đi 1 số phương thức không cần thiết như bang() hay nhoHon(). Tôi dành phần thực hiện nó cho các bạn.

Trách nhiệm khi sử dụng quan hệ bạn

Quyền lợi lớn đương nhiên sẽ đi cùng với trách nhiệm lớn (With great power comes great responsibility). Trong C++, quan hệ bạn đi kèm với điều kiện là hàm bạn sẽ không xóa đối tượng hay hủy hoại các thuộc tính của lớp. Nếu các bạn muốn có 1 hàm mà có thể tác động thay đổi lớn tới nội dung của lớp, tốt nhất là biến hàm đấy thành phương thức của lớp thì hơn.

Tóm lại, chương trình của bạn phải thỏa mãn những điều kiện sau :

  • 1 hàm bạn sẽ không thể thay đổi đối tượng của lớp.
  • Chỉ sử dụng các hàm bạn khi nào không có lựa chọn nào khác.

Điều kiện thứ 2 là rất quan trọng. Nếu bạn không tuân theo thì cũng không cần tiếp tục học OOP nữa vì thiếu điều kiện này thì khái niệm lớp đã hoàn toàn mất đi ý nghĩa của nó.

Tóm tắt bài học :
  • 1 phương thức tĩnh là 1 phương thức có thể được gọi sử dụng mà không cần thông qua việc tạo đối tượng. Nó tương tự như 1 hàm cơ bản.
  • 1 thuộc tính tĩnh là thuộc tính được chia sẻ giữa nhiều đối tượng của lớp.
  • 1 hàm bạn của 1 lớp có thể truy cập tới tất cả thành viên của lớp kể cả thành viên private.
  • Nên hạn chế sử dụng quan hệ bạn trong C++, trừ khi thật sự cần thiết và không còn giải pháp khác.