Trong bài trước, để đơn giản, chúng ta đã khởi tạo các biến thành viên của class bằng toán tử gán, 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 Something
{
private:
    int m_value1;
    double m_value2;
    char m_value3;
 
public:
    Something()
    {
        // These are all assignments, not initializations
        m_value1 = 1;
        m_value2 = 2.2;
        m_value3 = 'c';
    }
};

Khi hàm constructor của class được thực thi, biến m_value1, m_value2, và m_value3 sẽ được tạo ra. Sau đó, phần thân hàm của hàm constructor sẽ được chạy, tại đây, các biến dữ liệu thành viên sẽ được gán cho các giá trị. Quá trình này tương tự với luồng hoạt động của đoạn code C++ không hướng đối tượng sau:

int m_value1;
double m_value2;
char m_value3;
 
m_value1 = 1;
m_value2 = 2.2;
m_value3 = 'c';

Mặc dù đoạn code trên là hợp lệ khi xét về khía cạnh cú pháp của ngôn ngữ C++, nhưng đây không phải là một phong cách viết code tốt (và có thể kém hiệu quả hơn so với khởi tạo bằng constructor).

Tuy nhiên, như bạn đã được học trong những bài trước, một số kiểu dữ liệu (ví dụ: hằng và các biến tham chiếu) phải được khởi tạo ngay khi khai báo. Chúng ta cùng xem ví dụ sau:

class Something
{
private:
    const int m_value;
 
public:
    Something()
    {
        m_value = 1; // error: const vars can not be assigned to
    } 
};

Đoạn code trên thì tương tự với:

const int m_value; // error: const vars must be initialized with a value
m_value = 5; //  error: const vars can not be assigned to

Do đó, ta có thể thấy rằng, việc gán các giá trị cho các biến thành viên thuộc kiểu tham chiếu hoặc hằng trong phần thân hàm của constructor rõ ràng là không đủ trong một số trường hợp.

1. Khởi tạo một danh sách các biến thành viên – Member initializer lists

Để giải quyết vấn đề này, C++ đã cung cấp một phương thức để khởi tạo các biến thành viên của class (thay vì gán các giá trị cho chúng sau khi chúng được tạo ra) thông qua một danh sách khởi tạo biến thành viên – member initializer list (thường được gọi là “member initialization list”). Đừng nhầm lẫn những cái tên này với initializer list mà chúng ta sử dụng để gán các giá chị cho mảng.

Chúng ta có thể khởi tạo các biến bằng 3 cách: Sao chép, trực tiếp, và thông qua cú pháp khởi tạo đồng đều – uniform initialization.

int value1 = 1; // copy initialization
double value2(2.2); // direct initialization
char value3 {'c'}; // uniform initialization

Initialization list có cách sử dụng gần như giống hệt với direct initialization (khởi tạo trực tiếp) hoặc uniform initialization (khởi tạo đồng đều).

Bởi vì cách tốt nhất để học những điều mới là thông qua các ví dụ, nên ta cùng xem lại đoạn code thực hiện các phép gán trong phần thân hàm của constructor:

class Something
{
private:
    int m_value1;
    double m_value2;
    char m_value3;
 
public:
    Something()
    {
        // These are all assignments, not initializations
        m_value1 = 1;
        m_value2 = 2.2;
        m_value3 = 'c';
    }
};

Bây giờ, hãy thử viết lại đoạn code trên nhưng sử dụng một initialization list:

/**
* 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:
    int m_value1;
    double m_value2;
    char m_value3;
 
public:
    Something() : m_value1{ 1 }, m_value2{ 2.2 }, m_value3{ 'c' } // directly initialize our member variables
    {
    // No need for assignment here
    }
 
    void print()
    {
         std::cout << "Something(" << m_value1 << ", " << m_value2 << ", " << m_value3 << ")\n";
    }
};
 
int main()
{
    Something something{};
    something.print();
    return 0;
}

Đoạn code trên sẽ in ra:

Something(1, 2.2, c)

Initialization list sẽ được chèn vào sau các tham số của hàm constructor, Nó bắt đầu bằng một dấu hai chấm (:), và sau đó liệt kê ra theo từng cặp biến để khởi tạo và giá trị dành cho biến đó, mỗi cặp được phân tách nhau bằng dấu phẩy.

Lưu ý rằng chúng ta đã không còn cần phải thực hiện các phép gán bên trong phần thân hàm của constructor, bởi vì initialization list đã đảm nhiệm việc này. Và cũng lưu ý rằng initialization list sẽ không kết thúc bằng một dấu chấm phẩy.

Tất nhiên, các hàm contructor sẽ trở nên hữu ích hơn nữa khi chúng ta cho phép đối tượng gọi hàm có thể truyền vào các giá trị khởi tạo:

/**
* 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>
 
class Something
{
private:
    int m_value1;
    double m_value2;
    char m_value3;
 
public:
    Something(int value1, double value2, char value3='c')
        : m_value1{ value1 }, m_value2{ value2 }, m_value3{ value3 } // directly initialize our member variables
    {
    // No need for assignment here
    }
 
    void print()
    {
         std::cout << "Something(" << m_value1 << ", " << m_value2 << ", " << m_value3 << ")\n";
    }
 
};
 
int main()
{
    Something something{ 1, 2.2 }; // value1 = 1, value2=2.2, value3 gets default value 'c'
    something.print();
    return 0;
}

Đoạn code trên sẽ in ra:

Something(1, 2.2, c)

Lưu ý rằng, bạn có thể sử dụng các tham số mặc định để cung cấp giá trị mặc định cho các tham số, trong trường hợp mà người dùng không truyền giá trị nào cho các tham số đó.

Dưới đây là ví dụ về class sở hữu biến thành viên kiểu hằng:

class Something
{
private:
    const int m_value;
 
public:
    Something(): m_value{ 5 } // directly initialize our const member variable
    {
    } 
};

Đoạn code trên sẽ hoạt động bởi vì chúng ta được phép khởi tạo các biến hằng (nhưng không được gán giá trị cho chúng!).

Quy tắc: Hãy sử dụng member initializer lists (danh sách khởi tạo biến thành viên) để khởi tạo các biến thành viên của class của bạn, thay vì thực hiện những phép gán.

2. Khởi tạo các biến thành viên thuộc kiểu dữ liệu mảng bằng member initializer lists

Đoạn code sau mô tả một class sở hữu một biến thành viên thuộc kiểu dữ liệu mảng:

class Something
{
private:
    const int m_array[5];
 
};

Trước C++ 11, bạn chỉ có thể gán 0 cho toàn bộ phần tử mảng của một mảng thành viên, thông qua một member initialization list – danh sách khởi tạo biến thành viên:

class Something
{
private:
    const int m_array[5];
 
public:
    Something(): m_array {} // zero the member array
    {
    }
 
};

Tuy nhiên, kể từ C++ 11, bạn hoàn toàn có thể khởi tạo một mảng thành viên bằng uniform initialization – khởi tạo đồng đều:

class Something
{
private:
    const int m_array[5];
 
public:
    Something(): m_array { 1, 2, 3, 4, 5 } // use uniform initialization to initialize our member array
    {
    }
 
};

3. Khởi tạo các biến thành viên thuộc kiểu dữ liệu class

Member initialization list (danh sách khởi tạo biến thành viên) cũng có thể được sử dụng để khởi tạo các biến thành viên thuộc kiểu class.

#include <iostream>
 
class A
{
public:
    A(int x) { std::cout << "A " << x << '\n'; }
};
 
class B
{
private:
    A m_a;
public:
    B(int y)
         : m_a{ y-1 } // call A(int) constructor to initialize member m_a
    {
        std::cout << "B " << y << '\n';
    }
};
 
int main()
{
    B b{ 5 };
    return 0;
}

Kết quả in ra là:

A 4
B 5

Khi biến b được khởi tạo, hàm constructor B(int) sẽ được gọi với đối số truyền vào là 5. Trước khi phần thân hàm của constructor này được thực thi, biến m_a sẽ được khởi tạo thông qua lời gọi hàm constructor A(int) với đối số truyền vào là 4, đồng thời chuỗi “A 4” cũng sẽ được in ra nhờ thực thi khối code trong constructor A(int) này. Sau đó điều khiển của chương trình sẽ quay trở lại hàm constructor B(int) để thực thi phần thân hàm của nó, và in ra chuỗi “B 5”.

4. Định dạng lại các initializer lists

C++ cung cấp cho bạn rất nhiều sự linh hoạt trong cách định dạng lại các initializer lists, và nó thực sự tùy thuộc vào cách mà bạn muốn tiến hành. Nhưng dưới đây là một số khuyến nghị dành cho bạn:

Nếu có thể, hãy đặt toàn bộ phần cú pháp của initializer lên cùng một dòng với tên hàm.

class Something
{
private:
    int m_value1;
    double m_value2;
    char m_value3;
 
public:
    Something() : m_value1{ 1 }, m_value2{ 2.2 }, m_value3{ 'c' } // everything on one line
    {
    }
};

Còn nếu phần nội dung của initializer list không thể nằm vừa vặn trên cùng một dòng với tên hàm, vậy thì hãy xuống dòng, thụt đầu dòng một đoạn, rồi đặt nó vào dòng mới này.

/**
* 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:
    int m_value1;
    double m_value2;
    char m_value3;
 
public:
    Something(int value1, double value2, char value3='c') // this line already has a lot of stuff on it
        : m_value1{ value1 }, m_value2{ value2 }, m_value3{ value3 } // so we can put everything indented on next line
    {
    }
 
};

 Nếu phần nội dung của initializer list không thể nằm vừa trên một dòng duy nhất (hoặc là các initializers này không ở dạng thông thường), thì bạn có thể đặt chúng trên từng dòng riêng biệt, phân tách nhau bằng dấu phẩy.

class Something
{
private:
    int m_value1;
    double m_value2;
    char m_value3;
    float m_value4;
 
public:
    Something(int value1, double value2, char value3='c', float value4=34.6f) // this line already has a lot of stuff on it
        : m_value1{ value1 }, // one per line, commas at end of each line
        m_value2{ value2 },
        m_value3{ value3 },
        m_value4{ value4 } 
    {
    }
 
};

5. Vấn đề thứ tự trong initializer list

Có lẽ bạn sẽ hơi bất ngờ, nhưng sự thật là các biến nằm trong initializer list không được khởi tạo theo thứ tự mà chúng được chỉ định trong initializer list. Thay vao đó, chúng được khởi tạo theo thứ tự mà chúng được khai báo trong class.

Để có được kết quả tốt nhất, bạn nên tuân thủ các khuyến nghị sau:

Đừng khởi tạo các biến thành viên theo cách mà chúng phải phụ thuộc vào việc các biến thành viên khác được khởi tạo trước (nói cách khác, bạn phải đảm bảo rằng các biến thành viên của mình sẽ được khởi tạo đúng, ngay cả với thứ tự khởi tạo khác).

Nên khởi tạo các biến trong initializer list theo cùng thứ tự mà chúng được khai báo trong class của bạn. Không cần thực hiện quá máy móc khuyến nghị này, miễn là khuyến nghị 1) đã được tuân thủ, nhưng trình biên dịch có thể sẽ đưa ra cảnh báo nếu bạn không làm vậy.

6. Tổng kết

Member initializer lists (khởi tạo danh sách các biến thành viên) cho phép chúng ta khởi tạo các biến thành viên, thay vì phải gán các giá trị cho chúng. Đây là cách duy nhất để khởi tạo các biến thành viên mà yêu cầu phải có ngay giá trị khi khởi tạo, chẳng hạn như các biến thành viên thuộc kiểu dữ liệu tham chiếu hoặc kiểu hằng, và việc sử dụng member initializer list có thể mang lại nhiều hiệu quả hơn so với việc gán các giá trị trong phần thân hàm của hàm constructor. Member initializer list có thể hoạt động với cả các kiểu dữ liệu cơ bản và cả các biến thành viên thuộc kiểu dữ liệu class.

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