1. Các hàm thành viên tĩnh

Trong bài học trước về các biến thành viên tĩnh, bạn đã học được rằng, về bản chất thì các biến thành viên tĩnh là các biến thành viên mà thuộc về class, chứ không phải là thuộc về các đối tượng của class. Nếu các biến thành là public, chúng ta có thể truy cập chúng trực tiếp thông qua tên class và toán tử phân giải phạm vi (scope resolution operator). Nhưng sẽ thế nào nếu các biến thành viên tĩnh đều được thiết lập là private? Cùng xem xét ví dụ sau:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

class Something
{
private:
    static int s_value;
 
};
 
int Something::s_value{ 1 }; // initializer, this is okay even though s_value is private since it's a definition
 
int main()
{
    // how do we access Something::s_value since it is private?
}

Trong trường hợp này, chúng ta không thể truy cập trực tiếp đến Something::s_value từ hàm main(), bởi vì nó có chỉ định phạm vi truy cập là private. Thông thường, chúng ta sẽ truy cập đến các biến thành viên private thông qua các hàm thành viên public. Mặc dù chúng ta có thể tạo ra một hàm thành viên public bình thường để truy cập đến biến s_value, nhưng sau đó chúng ta sẽ cần phải thể hiện/khởi tạo một đối tượng của kiểu class đó để sử dụng được hàm vừa tạo! Tuy nhiên, có một cách tốt hơn đề làm điều này, đó là thiết lập cho các hàm thành viên trở thành tĩnh (static) luôn.

Giống như các biến thành viên tĩnh, các hàm thành viên tĩnh cũng không thuộc về/gắn liền với đối tượng của class. Dưới đây là đoạn code lấy từ ví dụ ở trên, nhưng khai báo thêm một một hàm thành viên tĩnh và sử dụng nó để truy cập tới giá trị của biến s_value.

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

class Something
{
private:
    static int s_value;
public:
    static int getValue() { return s_value; } // static member function
};
 
int Something::s_value{ 1 }; // initializer
 
int main()
{
    std::cout << Something::getValue() << '\n';
}

Các hàm thành viên tĩnh đều không được gắn với một đối tượng cụ thể nào, chúng có thể được gọi đến trực tiếp bằng cách sử dụng tên class và toán tử phân giải phạm vi. Giống như các biến thành viên tĩnh, cám hàm thành viên tĩnh cũng có thể được gọi thông qua các đối tượng của kiểu class cụ thể, mặc dù điều này không được khuyến khích.

2. Các hàm thành viên tĩnh không có con trỏ *this

Có hai điều thú vị đáng chú ý về các hàm thành viên tĩnh:

  • Thứ nhất, bởi vì các hàm thành viên tĩnh đều không được gắn với một đối tượng nào, nên chúng không có con trỏ this! Điều này là hợp lý vì con trỏ this luôn luôn trỏ tới đối tượng mà hàm thành viên hiện tại đang làm việc trên đối tượng đó. Do các hàm thành viên tĩnh không làm việc trên một đối tượng nào, vì vậy con trỏ this là không cần thiết.
  • Thứ hai, các hàm thành viên tĩnh có thể truy cập trực tiếp tới các thành viên tĩnh khác (bao gồm cả các biến thành viên tĩnh và các hàm thành viên tĩnh), nhưng không truy cập được tới các thành viên không tĩnh. Điều này là do các thành viên không tĩnh phải thuộc về một đối tượng của class, trong khi đó các hàm thành viên tĩnh thì không làm việc cùng với bất kỳ đối tượng nào của class!

3. Một ví dụ khác

Các hàm thành viên tĩnh cũng có thể được định nghĩa bên ngoài phần code khai báo của class. Điều này diễn ra tương tự như đối với các hàm thành viên bình thường.

Dưới đây là một ví dụ:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

class IDGenerator
{
private:
    static int s_nextID; // Here's the declaration for a static member
 
public:
     static int getNextID(); // Here's the declaration for a static function
};
 
// Here's the definition of the static member outside the class.  Note we don't use the static keyword here.
// We'll start generating IDs at 1
int IDGenerator::s_nextID{ 1 };
 
// Here's the definition of the static function outside of the class.  Note we don't use the static keyword here.
int IDGenerator::getNextID() { return s_nextID++; } 
 
int main()
{
    for (int count{ 0 }; count < 5; ++count)
        std::cout << "The next ID is: " << IDGenerator::getNextID() << '\n';
 
    return 0;
}

Đoạn chương trình trên sẽ in ra:

The next ID is: 1
The next ID is: 2
The next ID is: 3
The next ID is: 4
The next ID is: 5

Lưu ý rằng, bởi vì tất cả các dữ liệu và các hàm bên trong class này đều là tĩnh (static), nên chúng ta sẽ không cần phải thể hiện/khởi tạo đối tượng nào của class để có thể sử dụng được chúng. Class này sử dụng một biến thành viên tĩnh để giữ giá trị của ID tiếp theo sẽ được gán, và cung cấp một hàm thành viên tĩnh để trả về ID đó, rồi tăng nó lên một đợt vị.

4. Một lời cảnh báo về việc class có tất cả các thành viên đều là tĩnh

Hãy cẩn thận khi viết các class mà có tất cả các thành viên đều là tĩnh (static). Mặc dù những cái “class tĩnh hoàn toàn” như vậy có thể hữu ích, nhưng chúng cũng đi kèm với một số nhược điểm tiềm tàng.

Thứ nhất , bởi vì tất cả các thành viên tĩnh đều chỉ được thể hiện (instantiated) một lần, nên không có cách nào để có được nhiều bản sao của một class tĩnh hoàn toàn (mà không phải nhân bản class đó rồi đổi tên khác). Ví dụ, nếu bạn cần hai đối tượng IDGenerator độc lập, thì điều này sẽ không thể thực hiện được đối với  một class tĩnh hoàn toàn.

Thứ hai, trong bài học về các biến toàn cục, bạn đã học được rằng các biến toàn cục đều nguy hiểm, bởi vì việc bất kỳ đoạn code nào cũng có thể thay đổi giá trị của biến toàn cục sẽ tiềm tàng khả năng làm cho một đoạn code nào đó dường như không liên quan bị phá vỡ. Điều tương tự cũng sẽ xảy ra đối với các class tĩnh hoàn toàn. Bởi vì tất cả các thành viên đều thuộc về class (thay vì thuộc về đối tượng của class), và phần code khai báo class thường có phạm vi toàn cục, nên việc sử dụng một class tĩnh hoàn toàn, về cơ bản chính là tương đương với việc khai báo các hàm và các biến toàn cục bên trong một namespace có thể truy cập toàn cục, được kèm theo bởi tất cả những nhược điểm vốn có của các biến toàn cục.

5. C++ không hỗ trợ các hàm constructor tĩnh

Nếu bạn có thể khởi tạo các biến thành viên bình thường thông qua một hàm consuctor, vậy thì xét rộng ra, điều này có nghĩa là bạn cũng hoàn toàn có thể khởi tạo các biến thành viên tĩnh thông qua một hàm constructor tĩnh, phải không nào? Và trong khi một số ngôn ngữ lập trình hiện đại có hỗ trợ các hàm constructor tĩnh cho một số mục đích chính đáng, thì thật không may rằng C++ không nằm trong số đó.

Nếu biến tĩnh của bạn có thể được khởi tạo trực tiếp, khì không cần tới hàm constructor: Bạn có thể khởi tạo biến thành viên tĩnh tại thời điểm định nghĩa ra nó (ngay cả khi nó được thiết lập là private). Chúng ta đã làm điều này trong ví dụ về IDGenerator ở bên trên. Dưới đây là một ví dụ khác:

class MyClass
{
public:
	static std::vector<char> s_mychars;
};
 
std::vector<char> MyClass::s_mychars{ 'a', 'e', 'i', 'o', 'u' }; // initialize static variable at point of definition

Nếu việc khởi tạo biến thành viên tĩnh của bạn đòi hỏi việc thực thi code (ví dụ: Một vòng lặp), có nhiều cách khác nhau để làm được, mặc dù chúng khá là tù. Đoạn code sau đây sẽ trình bày một trong số những phương pháp tốt hơn. Tuy nhiên, nó hơi lắt léo một chút, và bạn có thể sẽ không bao giờ cần sử dụng đến nó, vì vậy, hãy bỏ qua phần còn lại của mục này nếu bạn muốn.

class MyClass
{
private:
    static std::vector<char> s_mychars;
};
 
std::vector<char> MyClass::s_mychars{
  []{ 
      // Inside the lambda we can declare another vector and use a loop.
      std::vector<char> v{};
      
      for (char ch{ 'a' }; ch <= 'z'; ++ch)
      {
          v.push_back(ch);
      }
      
      return v;
  }() // Call the lambda right away
};

Khi (biến đối tượng) thành viên tĩnh s_initializer được định nghĩa, hàm constructor mặc định _init() sẽ được gọi (bởi vì s_initializer có kiểu _init). Chúng ta có thể sử dụng hàm constructor này để khởi tạo bất kỳ biến thành viên tĩnh nào. Điều thú vị về giải pháp này là tất cả phần code khởi tạo sẽ được giữ kín bên trong class gốc, cùng với các thành viên tĩnh.

6. Tổng kết

Các hàm thành viên tĩnh có thể được sử dụng để làm việc với các biến thành viên tĩnh trong class. Có thể thông qua tên class gọi trực tiếp đến các hàm thành viên tĩnh, mà không cần phải sử dụng tới đối tượng của class.

Các class có thể được tạo ra với tất cả các biến thành viên và hàm thành viên đều là tĩnh. Tuy nhiên, việc tạo ra những class như vậy về cơ bản là tương đương với việc khai báo các hàm và các biến thành viên bên trong một namespace có thể truy cập toàn cục, và chúng ta nên tránh làm điều này, trừ khi bạn có một lý do đặc biệt và đủ hợp lý để sử dụng chúng.

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!