[Tự học C++] Hằng đối với các đối tượng và hàm thành viên của class » Cafedev.vn

Như bạn đã biết, các kiểu dữ liệu cơ bản (như int, double, char, v.v…) có thể được thiết lập thành giá trị hằng thông qua từ khóa const, và các biến hằng phải được khởi tạo tại thời điểm chúng được tạo ra.

Trong trường hợp các kiểu dữ liệu cơ bản là hằng, việc khởi tạo có thể được thực hiện thông qua sao chép, khởi tạo trực tiếp, hoặc khởi tạo đồng đều (uniform initialization):

const int value1 = 5; // copy initialization
const int value2(7); // direct initialization
const int value3 { 9 }; // uniform initialization (C++11)

Hằng với Class

Tương tự, các đối tượng được thể hiện của class cũng có thể được thiết lập thành giá trị hằng (const) bằng cách sử dụng từ khóa const. Việc khởi tạo các đối tượng này được thực hiện thông qua hàm constructor của class chúng thuộc về:

const Date date1; // initialize using default constructor
const Date date2(2020, 10, 16); // initialize using parameterized constructor
const Date date3 { 2020, 10, 16 }; // initialize using parameterized constructor (C++11)

Khi một đối tượng hằng của class được khởi tạo thông qua hàm constructor, thì mọi nỗ lực sửa đổi các biến thành viên của đối tượng này sẽ không được cho phép, bởi vì việc sửa đổi này sẽ vi phạm tính chất hằng (const-ness) của đối tượng. Điều này bao gồm cả việc thay đổi trực tiếp các biến thành viên (nếu chúng là public), hoặc gọi các hàm thành viên để đặt lại giá trị cho các biến thành viên. Xét class ví dụ sau:

class Something
{
public:
    int m_value;
 
    Something(): m_value(0) { }
 
    void setValue(int value) { m_value = value; }
    int getValue() { return m_value ; }
};
 
int main()
{
    const Something something; // calls default constructor
 
    something.m_value = 5; // compiler error: violates const
    something.setValue(5); // compiler error: violates const
 
    return 0;
}

Cả hai dòng code liên quan đến biến đối tượng something ở ví dụ trên đều không hợp lệ vì chúng đã vi phạm tính hằng/tính không thể thay đổi (constness) của biến đối tượng something thông qua việc cố gắng thay đổi trực tiếp giá trị của một biến thành viên, và gọi đến một hàm thành viên nhằm thay đổi giá trị của một biến thành viên.

Giống như với các biến bình thường, bạn sẽ thường muốn thiết lập các đối tượng của class của mình trở thành hằng khi bạn cần đảm bảo rằng chúng không bị sửa đổi sau khi được tạo ra.

Hàm thành viên là hằng

Xét dòng code ví dụ sau:

 std::cout << something.getValue();

Bạn có thể sẽ ngạc nhiên, nhưng câu lệnh trên cũng sẽ gây ra lỗi biên dịch, mặc dù hàm getValue() không làm bất cứ điều gì nhằm thay đổi biến thành viên nào! Từ câu lệnh ví dụ trên, ta có thể nhận ra rằng, các đối tượng hằng của class chỉ có thể gọi được tới các hàm thành viên cũng là hằng của class, và hàm getValue() ở ví dụ này thì vẫn chưa được đánh dấu là một hàm thành viên hằng.

Một hàm thành viên hằng (const member function) là một hàm thành viên được đảm bảo rằng nó sẽ không sửa đổi đối tượng hoặc gọi đến bất kỳ hàm thành viên không phải hằng (non-conts member functions) nào (bởi vì các hàm thành viên không phải hằng này có thể sẽ sửa đổi đối tượng).

Để làm cho hàm getValue() trở thành một hàm thành viên hằng, ta chỉ cần thêm từ khóa const vào phần nguyên mẫu hàm (function prototype), tức là đặt từ khóa const vào vị trí mà nằm sau danh sách tham số truyền vào, nhưng nằm trước phần thân hàm:

class Something
{
public:
    int m_value;
 
    Something(): m_value(0) { }
 
    void resetValue() { m_value = 0; }
    void setValue(int value) { m_value = value; }
 
    int getValue() const { return m_value; } // note addition of const keyword after parameter list, but before function body
};

Bây giờ, hàm getValue() đã trở thành một hàm thành viên hằng, điều này nghĩa là chúng ta đã có thể gọi đến nó từ bất kỳ đối tượng hằng nào.

Đối với các hàm thành viên được định nghĩa bên ngoài phần code định nghĩa của class, từ khóa const phải được sử dụng cả trên phần nguyên mẫu hàm (function prototype) nằm trong phần code định nghĩa của class, và cả trên phần code định nghĩa của hàm:

class Something
{
public:
    int m_value;
 
    Something(): m_value(0) { }
 
    void resetValue() { m_value = 0; }
    void setValue(int value) { m_value = value; }
 
    int getValue() const; // note addition of const keyword here
};
 
int Something::getValue() const // and here
{
    return m_value;
}

Hơn nữa, việc bất kỳ hàm thành viên hằng nào cố gắng thay đổi biến thành viên hoặc gọi đến một hàm thành viên không phải hằng khác, đều sẽ gây lỗi biên dịch. Ví dụ:

class Something
{
public:
    int m_value ;
 
    void resetValue() const { m_value = 0; } // compile error, const functions can't change member variables.
};

Trong ví dụ này, hàm resetValue() đã được thiết lập là một hàm thành viên hằng, nhưng nó lại cố gắng thay đổi giá trị của biến m_value. Điều này sẽ gây lỗi biên dịch.

Lưu ý rằng các hàm constructors không thể được thiết lập thành hằng. Điều này là do các hàm constructors cần có khả năng khởi tạo được các biến thành viên của chúng, và một hàm constructor hằng thì không thể thực hiện được điều này. Do đó, ngôn ngữ C++ không cho phép tạo ra các hàm constructor hằng.

Quy tắc: Nếu trong class của bạn có tồn tại bất kỳ các hàm hàm thành viên nào mà không sửa đổi trạng thái của đối tượng của class, thì hãy thiết lập nó trở thành hàm thành viên hằng, để nó có thể được gọi bởi các đối tượng hằng.

Hằng Tham chiếu

Mặc dù việc khởi tạo các đối tượng hằng của class là một cách để tạo ra các đối tượng hằng, tuy nhiên vẫn còn một cách khác phổ biến hơn, đó là truyền đi một đối tượng cho một hàm bằng tham chiếu hằng.

Trong bài học về truyền các đối số cho hàm bằng tham chiếu, chúng tôi đã đề cập đến giá trị của việc truyền đi các đối số thuộc kiểu class bằng tham chiếu hằng, thay vì bằng giá trị. Có thể tóm tắt lại một chút, việc truyền đi một đối số kiểu class bằng giá trị sẽ dẫn đến việc một bản sao của class được tạo ra (làm chậm chương trình) – Trong hầu hết các trường hợp, chúng ta đều không cần tới bản sao của class, ta chỉ quan tâm đến tham chiếu tới đối số gốc được truyền vào, làm vậy, hiệu năng của chương trình sẽ được cải thiện nhờ tránh được việc tạo ra các bản sao không cần thiết. Chúng tôi thường đánh dấu/khai báo cho các tham chiếu trở thành hằng để đảm bảo rằng một hàm cụ thể sẽ không vô tình thay đổi đối số truyền vào, và để cho phép hàm này hoạt động được với các R-values (ví dụ: các giá trị kiểu số, hoặc kiểu chuỗi ký tự, v.v…), là những giá trị có thể được truyền dưới dạng các tham chiếu hằng (không truyền được dưới dạng tham chiếu không hằng).

Xét đoạn code chứa một số điểm không đúng sau:

#include <iostream>
 
class Date
{
private:
    int m_year;
    int m_month;
    int m_day;
 
public:
    Date(int year, int month, int day)
    {
        setDate(year, month, day);
    }
 
    void setDate(int year, int month, int day)
    {
        m_year = year;
        m_month = month;
        m_day = day;
    }
 
    int getYear() { return m_year; }
    int getMonth() { return m_month; }
    int getDay() { return m_day; }
};
 
// note: We're passing date by const reference here to avoid making a copy of date
void printDate(const Date &date)
{
    std::cout << date.getYear() << "/" << date.getMonth() << "/" << date.getDay() << '\n';
}
 
int main()
{
    Date date(2016, 10, 16);
    printDate(date);
 
    return 0;
}

Bên trong hàm printDate thì biến date được coi là một đối tượng hằng. Và với đối tượng hằng date này, chúng ta đang gọi tới các hàm getYear(), getMonth(), và getDay(), đều là các hàm không phải là hàm hằng. Bởi vì chúng ta không thể sử dụng các đối tượng hằng để gọi tới những hàm thành viên không phải là hàm hằng, nên những lời gọi hàm trên sẽ gây lỗi biên dịch.

Để sửa lỗi này rất đơn giản, ta chỉ cần làm cho các hàm getYear(), getMonth() và getDay() trở thành hàm hằng là được:

class Date
{
private:
    int m_year;
    int m_month;
    int m_day;
 
public:
    Date(int year, int month, int day)
    {
        setDate(year, month, day);
    }
 
    // setDate() cannot be const, modifies member variables
    void setDate(int year, int month, int day)
    {
        m_year = year;
        m_month = month;
        m_day = day;
    }
 
    // The following getters can all be made const
    int getYear() const { return m_year; }
    int getMonth() const { return m_month; }
    int getDay() const { return m_day; }
};

Lúc này, bên trong hàm printDate(), đối tượng hằng date đã có thể gọi thành công tới các hàm getYear(), getMonth(), và getDay().

Việc overloading – nạp chồng các hàm hằng và các hàm không hằng

Cuối cùng, mặc dù không được thực hiện thường xuyên trong thực tế, nhưng việc nạp chồng một hàm để tạo ra các phiên bản hằng và không hằng của hàm này là hoàn toàn có thể thực hiện được:

#include <string>
 
class Something
{
private:
    std::string m_value;
 
public:
    Something(const std::string &value="") { m_value= value; }
 
    const std::string& getValue() const { return m_value; } // getValue() for const objects
    std::string& getValue() { return m_value; } // getValue() for non-const objects
};

Phiên bản hằng của hàm này sẽ có thể được gọi trên bất kỳ đối tượng hằng nào, và phiên bản không hằng của nó sẽ được gọi trên bất kỳ đối tượng không hằng nào:

int main()
{
	Something something;
	something.getValue() = "Hi"; // calls non-const getValue();
 
	const Something something2;
	something2.getValue(); // calls const getValue();
 
	return 0;
}

Việc nạp chồng một hàm với một phiên bản hằng và một phiên bản không hằng thường được thực hiện khi giá trị trả về cần phải khác nhau về tính chất hằng. Trong ví dụ trên, phiên bản không hằng của hàm getValue() sẽ chỉ làm việc với các đối tượng không hằng, nhưng nó linh hoạt hơn ở chỗ chúng ta có thể sử dụng nó để đọc và ghi lại giá trị của biến m_value (chúng ta đã làm điều này bằng cách gán cho nó chuỗi “Hi”).

Phiên bản hằng của hàm getValue() sẽ làm việc với hoặc là các đối tượng hằng, hoặc là các đối tượng không hằng, nhưng luôn trả về một tham chiếu hằng, để đảm bảo rằng chúng ta không thể sửa đổi các dữ liệu của đối tượng hằng. Có thể thực hiện được điều này là bởi vì tính chất hằng của một hàm được coi là một phần của signature (chữ ký) của hàm này, do đó, một hàm hằng và một hàm không hằng chỉ khác nhau về tính chất hằng, được coi là riêng biệt.

Tổng kết

Do sự phổ biến của việc truyền đi các đối tượng bằng tham chiếu hằng, nên các class của bạn cũng phải có tính chất hằng. Điều đó có nghĩa là cần thiếp lập hằng bằng từ khóa const cho mọi hàm thành viên không sửa đổi trạng thái của đối tượng của class!

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