C++ Programming – Constructors


Khi tất cả các member của một class (hay struct) là public, thì chúng ta có thể khởi tạo giá trị cho các member theo cách sau (intitialization list hay uniform initialization trong C++11)

class Foo
{
public:
    int m_x;
    int m_y;
};
 
int main()
{
    Foo foo1 = { 4, 5 }; // initialization list
    Foo foo2 { 6, 7 }; // uniform initialization (C++11)
 
    return 0;
}

Tuy nhiên, khi các member trong class (hay struct) được private, thì 2 cách thiết lập giá trị trên hoàn toàn không sử dụng được. Điều này là lý do contructor đươc sinh ra.

Constructors

Constructor là một function (hay còn gọi là method) đặc biệt trong một class, và constructor sẽ được gọi tự động khi một đối tượng (object) của một class được trình bày. Các constructor được sử dụng để thiết lập các biến member của mọt class với các giá trị mặc định thích hợp, hay là các giá trị do người dùng cung cấp, hay là để thiết lập các bước cần thiết cho một class để có thể sử dụng (ví dụ mở một file hay một database).

Khác với các hàm member thông thường trong một class, constructor có 2 luật bắt buộc như sau:

  1. Constructor luôn luôn có tên giống với tên của class.
  2. Constructor không bao giờ return giá trị.

Chý ý rằng, chúng ta sử dụng constructor để thiết lập giá trị, vì vậy chúng ta không nên dùng constructor để thiết lập lại giá trị của một đối tượng. Đương nhiên chươngt trình có thể chạy nhưng mà sẽ chạy sai giá trị chúng ta thiết lập lại (vì bản chất là nó không hề thiết lập lại, mà sẽ tạo ra một đối tượng tạm thời và sau đó phát hủy nó).

Default constructors

Một constructor mà không hề có một tham số nào (hoặc có tham số nhưng tất cả các tham số đó là mặc định) thì sẽ được gọi là constructor mặc định (defaut constructor). Chúng ta xem ví dụ sau:

#include <iostream>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    Fraction() // default constructor
    {
         m_numerator = 0;
         m_denominator = 1;
    }
 
    int getNumerator() { return m_numerator; }
    int getDenominator() { return m_denominator; }
    double getValue() { return static_cast<double>(m_numerator) / m_denominator; }
};
 
int main()
{
    Fraction frac; // Since no arguments, calls Fraction() default constructor
    std::cout << frac.getNumerator() << "/" << frac.getDenominator() << '\n';
 
    return 0;
}

Class này được thiết kế để tạo các đối tượng phân số (mẫu số và tử số là số integer). Như các bạn cũng biết, trong phân số, mẫu số không được bằng 0.
Chúng ta có một default constructor là Fraction (giống với tên class).

Trong chương trình trên, chúng ta có một object tên là frac được khai báo ở dòng 23. Ngay sau khi máy tính cấp bộ nhớ cho object này, default constructor sẽ được gọi ngay lập tức, và phân số frac sẽ được thiết lập giá trị.
Kết quả chương trình như sau:
0/1

Khi mà một class không có defaut constructor như trên, thì chương trình sẽ tự động sinh ra một default constructor nhưng các biến member (ở đây là m_numeratorm_denominator) trong class sẽ được thiết lập một giá trị rác.

Direct and uniform initialization using constructors with parameters

Chúng ta có thể thấy default constructor tuyệt vời thế nào, nhưng C++ còn cung cấp cho chúng ta điều tuyệt vời hơn thế nữa, constructor có tham số. Điều này giúp chúng ta rất linh hoạt trong các bài toán, và có thể thiết lập các giá trị đầu có lý cho các biến member trong một class. Như trong ví dụ trên, bây giờ chúng ta muốn thiết lập tử số bằng 1 ngay lúc đầu, thì chỉ có một cách duy nhất là chúng ta sửa code lại. Và constructor có tham số sẽ giúp chúng ta không phải sửa code lại, và cực kì linh hoạt.
Chúng ta sẽ xem xét ví dụ sau:

#include <cassert>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    Fraction() // default constructor
    {
         m_numerator = 0;
         m_denominator = 1;
    }
 
    // Constructor with two parameters, one parameter having a default value
    Fraction(int numerator, int denominator=1)
    {
        assert(denominator != 0);
        m_numerator = numerator;
        m_denominator = denominator;
    }
 
    int getNumerator() { return m_numerator; }
    int getDenominator() { return m_denominator; }
    double getValue() { return static_cast<double>(m_numerator) / m_denominator; }
};

Chúng ta có 2 constructor trong class trên: một constructor mặc định sẽ được gọi trong trường hợp mặc định, và thứ 2 là constructor có 2 tham số. Chúng ta có một khái niệm chính là function overloading (nạp chồng hàm), vì chúng ta có 2 hàm (constructor) giống nhau. Trong thực tế các bạn có thể define nhiểu constructor hơn nữa.

Bây giờ là cách chúng ta sử dụng constructor:

int x(5); // Direct initialize an integer
Fraction fiveThirds(5, 3); // Direct initialize a Fraction,  
                           // calls Fraction(int, int) constructor

Kết quả ta được một phân số: 5/3
Vì chúng ta truyền vào 2 tham số khi chúng ta khởi tạo đối tượng fiveThirds.
Trong C++ 11, chúng ta có thể sử dụng uniform initialization:

int x { 5 }; // Uniform initialization of an integer
Fraction fiveThirds {5, 3}; // Uniform initialization of a Fraction,  
                            // calls Fraction(int, int) constructor

Trong constructor có tham số phía trên, tham số thứ 2 chúng ta gán nó bằng 1, nghĩa là khi chúng ta khai báo một object mà không chỉ truyền vào một tham số thứ nhất, thì tham số thứ 2 mặc định sẽ là 1. Và nếu chúng ta truyền cả 2 tham số thì chúng ta đã làm ở trên.

Fraction six(6); // calls Fraction(int, int) constructor, second parameter 
                 // uses default value

Trong trường hợp này chúng ta sẽ có kết quả là: 6/1

Rule: Sử dụng direct hay uniform với class của bạn.

Copy initialization using equals with classes

Giống như các bạn tạo các biến bằng cách copy, thì chúng ta có thể tạo một đối tượng bằng cách copy:

int x = 6; // Copy initialize an integer
Fraction six = Fraction(6); // Copy initialize a Fraction, will call Fraction(6, 1)
Fraction seven = 7; // Copy initialize a Fraction.  
                    // The compiler will try to find a way to convert 7 to a Fraction,
                    // which will invoke the Fraction(7, 1) constructor.

Tuy nhiên, chúng tôi khuyên bạn nên tránh việc này để tao một object, bởi vì nó ít hiệu quả hơn.
Mặc dù tất cả các kiểu khởi tạo đều cho ra kết quả giống nhau, nhưng việc khởi tạo bằng cách sao chép nó sẽ hoạt động khác với kiểu khởi tạo bằng cách direct initialization hay uniform initialization. Chúng ta sẽ khám phá điều này trong các bài học sau.

Rule: Không được khởi tạo object bằng cách sao chép

Reducing your constructors

Trong ví dụ trên, chúng ta có 2 constructor và default constructor là hơi dư thừa, và chúng ta có thể đơn giản hóa như sau:

#include <cassert>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1)
    {
        assert(denominator != 0);
        m_numerator = numerator;
        m_denominator = denominator;
    }
 
    int getNumerator() { return m_numerator; }
    int getDenominator() { return m_denominator; }
    double getValue() { return static_cast<double>(m_numerator) / m_denominator; }
};

Mặc dù đây cũng vẫn là default constructor, nhưng nó có chức năng ngang với 2 constructor ở trên gộp lại:

Fraction zero; // will call Fraction(0, 1)
Fraction six(6); // will call Fraction(6, 1)
Fraction fiveThirds(5,3); // will call Fraction(5, 3)

Constructor như vậy rất là hữu dụng và thông minh.

Classes without default constructors

Nếu một class mà không có constructor này, C++ sẽ tự động tạo một default constructor rỗng, và constructor này sẽ không thiết lập giá trị cho các biến member trong class.
Ví dụ:

class Date
{
private:
    int m_year;
    int m_month;
    int m_day;
 
// No default constructor provided, so C++ creates an empty one for us
// Because no other constructors exist, this provided constructor will be public
};
 
int main()
{
    Date date; // calls default constructor that does nothing
    // date's member variables are uninitialized
    // Who knows what date we'll get?
 
    return 0;
}

Trong ví dụ trên, chúng ta đã định nghĩa một đối tượng Date, nhưng không hề có một constructor nào, m_month, m_day, và m_year sẽ không bao giờ được thiết lập giá trị. Vì vậy nó sẽ có một giá trị rác.

Tuy nhiên, nếu bạn có một constructor (nhưng không phải default constructor) trong class, C++ sẽ không tạo ra một default constructor rỗng nữa. Trong trường hợp này, chương trình sẽ báo lỗi nếu bạn khởi tạo một object mà không có tham số. Ví dụ sau giúp bạn hiểu rõ.

class Date
{
private:
    int m_year;
    int m_month;
    int m_day;
 
public:
    Date(int year, int month, int day) // not a default constructor
    {
        m_year = year;
        m_month = month;
        m_day = day;
    }
 
    // No default constructor provided
};
 
int main()
{
    Date date; // error: Can't instantiate object because default constructor doesn't exist
    Date today(2020, 10, 14); // today is initialized to Oct 14th, 2020
 
    return 0;
}

Chương trình sẽ báo lỗi ngay dòng 21.

Ý tưởng tốt là chúng ta hãy luôn luôn cung cấp ít nhất một constructor trong một class. Nó sẽ ngăn cản việc C++ tạo thêm một default constructor rỗng, và đưa cho các biến member giá trị rác.

Rule: Cung cấp ít nhất một constructor cho class, cho dù nó là default constructor rỗng.

Classes containing classes

Một class có thể chứa các class khác như là một member. Chúng ta hãy xem chuyện gì xảy ra trong ví dụ sau:

#include <iostream>
 
class A
{
public:
    A() { std::cout << "A\n"; }
};
 
class B
{
private:
    A m_a; // B contains A as a member variable
 
public:
    B() { std::cout << "B\n"; }
};
 
int main()
{
    B b;
    return 0;
}

Kết quả:
A
B

Trong ví dụ này, khi đối tượng b được tạo ra, constructor B() sẽ được gọi ngay lập tức, và trước khi B() constructor được thực hiện, biến m_a sẽ được thiết lập giá trị, và sẽ gọi constructor A(). Và nó in ra “A“, sau đó quay lại constructor B() và in ra “B“.
Một cách giải thích khác là khi constructor B() được gọi, thì nó muốn sử dụng biến m_a, vì vậy, m_a sẽ được thiết lập trước.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s