1. Nhắc lại về việc sử dụng từ khóa static

Trong bài học về phạm vi của file và từ khóa static, bạn đã học được rằng các biến tĩnh sẽ giữ giá trị của chúng và không bị hủy ngay cả sau khi chúng đã nằm ngoài phạm vi đoạn code mà chương trình đang chạy (go out of scope). 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/
*/

#include <iostream>
 
int generateID()
{
    static int s_id{ 0 };
    return ++s_id;
}
 
int main()
{
    std::cout << generateID() << '\n';
    std::cout << generateID() << '\n';
    std::cout << generateID() << '\n';
 
    return 0;
}

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

1
2
3

Lưu ý rằng biến tĩnh s_id vẫn ghi nhớ được giá trị của nó qua nhiều lời gọi hàm.

Từ khóa static còn có một ý nghĩa khác khi được áp dụng cho các biến toàn cục – đó là, nó cung cấp cho chúng các mối liên kết nội bộ (giúp hạn chế chúng khỏi việc bị nhìn thấy hoặc bị sử dụng ở bên ngoài file mà chúng được định nghĩa). Bởi vì các biến toàn cục thường được hạn chế sử dụng, nên từ khóa static cũng không thường được sử dụng trong khả năng này.

2. Các biến thành viên tĩnh

C++ đã giới thiệu thêm hai cách sử dụng cho từ khóa static khi áp dụng cho các class: Các biến thành viên tĩnh, và các hàm thành viên tĩnh. May mắn thay, những cách sử dụng này khá đơn giản. Chúng ta sẽ nói về các biến thành viên tĩnh trong bài học này, và các hàm thành viên tĩnh trong bài học tiếp theo.

Trước khi đi vào tìm hiểu từ khóa static khi được áp dụng cho các biến thành viên, trước tiên hãy xét class sau:

class Something
{
public:
    int m_value{ 1 };
};
 
int main()
{
    Something first;
    Something second;
    
    first.m_value = 2;
 
    std::cout << first.m_value << '\n';
    std::cout << second.m_value << '\n';
 
    return 0;
}

Khi chúng ta khởi tạo các đối tượng của class, mỗi đối tượng sẽ có được bản sao riêng của tất cả các biến thành viên bình thường. Trong trường hợp này, bởi vì chúng ta đã khai báo hai đối tượng của class Something là first và second, nên cuối cùng ta sẽ có được hai bản sao của biến m_value là: first.m_value và second.m_value. Trong đó, first.m_value khác với second.m_value. Do đó, đoạn chương trình trên sẽ in ra:

2
1

Các biến thành viên của một class có thể được đặt thành tĩnh bằng cách sử dụng từ khóa static. Không giống như các biến thành viên bình thường, các biến thành viên tĩnh được chia sẻ bởi tất cả các đối tượng của class. Cùng xem đoạn chương trình ví dụ dưới đây, tương tự với ví dụ ở trên:

/**
* 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
{
public:
    static int s_value;
};
 
int Something::s_value{ 1 };
 
int main()
{
    Something first;
    Something second;
 
    first.s_value = 2;
 
    std::cout << first.s_value << '\n';
    std::cout << second.s_value << '\n';
    return 0;
}

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

2
2

Bởi vì s_value là một biến thành viên tĩnh, nên s_value sẽ được chia sẻ giữa tất cả các đối tượng của class . Do đó, first.s_value là cùng một biến với second.s_value. Đoạn chương trình trên cho thấy rằng giá trị mà chúng ta gán bằng cách sử dụng đối tượng first thì có thể được truy cập bằng cách sử dụng đối tượng second!

3. Các thành viên tĩnh không được liên kết với các đối tượng của class

Mặc dù bạn có thể truy cập đến các thành viên tĩnh (bao gồm cả biến thành viên tĩnh và hàm thành viên tĩnh) thông qua các đối tượng của class (như đã được trình bày trong đoạn code về first.s_value và second.s_value ở ví dụ phía trên), nhưng bạn chợt nhận ra rằng các thành viên tĩnh vẫn sẽ tồn tại ngay cả khi không có đối tượng nào của class được khởi tạo! Rất giống với các biến toàn cục, được tạo ra khi chương trình bắt đầu, và hủy đi khi chương trình kết thúc.

Do đó, tốt hơn hết là ta nên nghĩ và hiểu theo hướng: Các thành viên tĩnh (static members – 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) không thuộc về các đối tượng của class, mà chúng thuộc về chính class đó. Bởi vì s_value tồn tại độc lập với mọi đối tượng của class, nên nó có thể được truy cập trực tiếp bằng cách sử dụng tên của class và toán tử phân giải phạm vi (trong trường hợp này là Something::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
{
public:
    static int s_value; // declares the static member variable
};
 
int Something::s_value{ 1 }; // defines the static member variable (we'll discuss this section below)
 
int main()
{
    // note: we're not instantiating any objects of type Something
 
    Something::s_value = 2;
    std::cout << Something::s_value << '\n';
    return 0;
}

Trong đoạn code ví dụ trên, s_value được tham chiếu tới bằng tên của class, thay vì thông qua một đối tượng. Lưu ý rằng, mặc dù thậm chí còn chưa khởi tạo một đối tượng nào thuộc kiểu class Something, nhưng chúng ta vẫn có thể truy cập và sử dụng Something::s_value. Đây là phương pháp được ưa chuộng để truy cập đến các thành viên tĩnh.

4. Định nghĩa và khởi tạo các biến thành viên tĩnh

Khi khai báo một biến thành viên tĩnh bên trong một class, chúng ta chỉ đang nói cho trình biên dịch biết về sự tồn tại của một biến thành viên tĩnh cụ thể, chứ không thực sự định nghĩa nó (tức là ta chỉ đang làm một cái khai báo trước thôi). Bởi vì các biến thành viên tĩnh không phải là một phần của các đối tượng riêng biệt của class (các biến thành viên tĩnh được xử lý tương tự như các biến toàn cục, và được khởi tạo khi chương trình bắt đầu), nên bạn phải định nghĩa một cách rõ ràng các thành viên tĩnh ở trong phạm vi toàn cục, nằm bên ngoài class.

Trong ví dụ trên, chúng ta đã thực hiện điều này thông qua dòng code:

int Something::s_value{ 1 }; // defines the static member variable

Dòng code này phục vụ hai mục đích: Nó thể hiện (instantiates) biến thành viên tĩnh (giống như đối với biến toàn cục), và khởi tạo nó một cách tùy chọn. Trong trường hợp này, chúng ta đang cung cấp giá trị khởi tạo là 1. Nếu không có cú pháp khởi tạo (initializer) nào được cung cấp, C++ sẽ khởi tạo giá trị mặc định là 0.

Lưu ý rằng việc định nghĩa biến thành viên tĩnh này không bị chi phối bởi các kiểm soát truy cập: Bạn có thể định nghĩa và khởi tạo giá trị ngay cả khi biến thành viên tĩnh được khai báo là private (hoặc protected) trong class.

Nếu class được định nghĩa trong một file .h, thì phần code định nghĩa biến thành viên tĩnh sẽ thường được đặt trong một file code được liên kết dành cho class (ví dụ: Something.cpp). Nếu class được định nghĩa bên trong một file .cpp, phần code định nghĩa biến thành viên tĩnh sẽ thường được đặt trực tiếp bên dưới class. Đừng đặt phần code định nghĩa biến thành viên tĩnh vào trong một file header (cũng giống như đối với biến toàn cục,nếu file header đó được include nhiều hơn một lần, bạn sẽ có nhiều phần code định nghĩa giống nhau cùng nằm trong một file, gây ra lỗi biên dịch).

5. Khởi tạo nội tuyến các biến thành viên tĩnh

Đầu tiên, khi biến thành viên tĩnh có kiểu hằng số nguyên (bao gồm cả kiểu char và kiểu bool) hoặc là một kiểu hằng enum, thì biến thành viên tĩnh này có thể được khởi tạo bên trong phần code định nghĩa class:

class Whatever
{
public:
    static const int s_value{ 4 }; // a static const int can be declared and initialized directly
};

Trong ví dụ trên, bởi vì biến thành viên tĩnh là một giá trị hằng kiểu int, nên nó có thể được khai báo và khởi tạo trực tiếp.

Thứ hai, các biến thành viên constexpr tĩnh có thể được khởi tạo bên trong phần code định nghĩa class:

#include <array>
 
class Whatever
{
public:
    static constexpr double s_value{ 2.2 }; // ok
    static constexpr std::array<int, 3> s_array{ 1, 2, 3 }; // this even works for classes that support constexpr initialization
};

5. Một ví dụ về các biến thành viên tĩnh

Tại sao lại sử dụng các biến tĩnh bên trong class? Việc gán một ID độc nhất cho mỗi thể hiện của class có lẽ sẽ là một ví dụ tuyệt vời để minh họa việc ứng dụng biến tĩnh trong lập trình. Dưới đây là ví dụ về điều đó:

/**
* 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_idGenerator;
    int m_id;
 
public:
    Something() { m_id = s_idGenerator++; } // grab the next value from the id generator
 
    int getID() const { return m_id; }
};
 
// Note that we're defining and initializing s_idGenerator even though it is declared as private above.
// This is okay since the definition isn't subject to access controls.
int Something::s_idGenerator = 1; // start our ID generator with value 1
 
int main()
{
    Something first;
    Something second;
    Something third;
 
    std::cout << first.getID() << '\n';
    std::cout << second.getID() << '\n';
    std::cout << third.getID() << '\n';
    return 0;
}

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

1
2
3

Bởi vì s_idGenerator được chia sẻ bởi tất cả các đối tượng của class Something, nên khi một đối tượng Something mới được tạo ra, hàm constructor sẽ lấy giá trị hiện tại ra khỏi s_idGenerator và sau đó tăng giá trị này lên một đơn vị để dành cho lần gán ID của đối tượng tiếp theo. Điều này đảm bảo rằng mỗi đối tượng Something được thể hiện sẽ nhận được một id độc nhất (được tăng dần theo thứ tự tạo đối tượng). Điều này thực sự hữu ích khi phải gỡ lỗi nhiều mục dữ liệu bên trong một mảng, bởi vì nó cung cấp một cách để có thể phân biệt được nhiều đối tượng thuộc cùng kiểu class!

Các biến thành viên tĩnh cũng có thể hữu dụng khi class cần sử dụng tới một bảng tra cứu nội bộ – internal lookup table (ví dụ: Một mảng được sử dụng để chứa một tập hợp các giá trị đã được tính toán trước). Bằng cách làm cho bảng tra cứu này trở thành tĩnh, chỉ có một bản sao tồn tại cho tất cả các đối tượng, thay vì tạo ra một bản sao cho mỗi đối tượng được thể hiện. Điều này có thể tiết kiệm đáng kể bộ nhớ.

Tham khảo thêm:

Series tự học C++
Series tự học Java

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