1. Định nghĩa các hàm thành viên bên ngoài phần code khai báo của class

Tất cả các class mà chúng ta đã viết cho đến nay đều đủ đơn giản để chúng ta có thể cài đặt trực tiếp các hàm thành viên bên trong phần code khai báo của class. Ví dụ, dưới đây là một class Date điển hình:

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; }
};

Tuy nhiên, khi các class trở nên dài hơn và phức tạp hơn, việc để tất cả các phần code định nghĩa của các hàm thành viên vào bên trong class có thể làm cho class khó quản lý và khó sử dụng hơn. Để sử dụng một class đã được viết sẵn, lập trình viên chỉ cần hiểu được phần public interface (giao diện công khai) của nó (phần public interface này chính là các hàm thành viên ở chế độ public), chứ không cần phải hiểu chi tiết về cách thức hoạt động sâu bên trong của class.

May mắn thay, C++ có cung cấp một tính năng giúp một class có thể tách biệt phần “khai báo” khỏi phần “cài đặt” của nó. Điều này được thực hiện bằng cách định nghĩa các hàm thành viên của class ở bên ngoài phần code khai báo của class đó. Để làm vậy, ta chỉ cần định nghĩa các hàm thành viên của class như thể chúng là các hàm bình thường khác, nhưng đặt thêm tiền tố là tên class và toán tử phân giải phạm vi (::) (giống như đối với namespace) vào trước tên hàm.

Dưới đây là phiên bản cải tiến của ví dụ trên, trong đó hàm constructor Date và hàm setDate() được định nghĩa bên ngoài phần code khai báo của class Date. Lưu ý rằng những phần khuôn mẫu hàm (prototypes) của các hàm thành viên này vẫn tồn tại bên trong phần code khai báo của class, nhưng phần code triển khai thực sự của chúng đã được chuyển ra bên ngoài:

/**
* 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 Date
{
private:
    int m_year;
    int m_month;
    int m_day;
 
public:
    Date(int year, int month, int day);
 
    void SetDate(int year, int month, int day);
 
    int getYear() { return m_year; }
    int getMonth() { return m_month; }
    int getDay()  { return m_day; }
};
 
// Date constructor
Date::Date(int year, int month, int day)
{
    SetDate(year, month, day);
}
 
// Date member function
void Date::SetDate(int year, int month, int day)
{
    m_month = month;
    m_day = day;
    m_year = year;
}

Việc tách biệt các phần code này thật là đơn giản. Mặc dù các đoạn code chỉ định phạm vi truy cập của một hàm thành viên có thể được di chuyển ra bên ngoài, nhưng vì thường chỉ chiếm 1 dòng code, nên chúng vẫn được để lại ở bên trong phần khai báo của class.

Dưới đây là một ví dụ khác, bao gồm một hàm constructor được định nghĩa ở bên ngoài class, cùng với một member initialization líst – danh sách khởi tạo biến thành viên:

class Calc
{
private:
    int m_value = 0;
 
public:
    Calc(int value=0): m_value(value) {}
 
    Calc& add(int value) { m_value  += value;  return *this; }
    Calc& sub(int value) { m_value -= value;  return *this; }
    Calc& mult(int value) { m_value *= value;  return *this; }
 
    int getValue() { return m_value ; }
};

Trở thành:

class Calc
{
private:
    int m_value = 0;
 
public:
    Calc(int value=0);
 
    Calc& add(int value);
    Calc& sub(int value);
    Calc& mult(int value);
 
    int getValue() { return m_value; }
};
 
Calc::Calc(int value): m_value(value)
{
}
 
Calc& Calc::add(int value)
{
    m_value += value;
    return *this;
}
 
Calc& Calc::sub(int value) 
{
    m_value -= value;
    return *this;
}
 
Calc& Calc::mult(int value)
{
    m_value *= value;
    return *this;
}

2. Đặt phần code khai báo của class vào trong một file header

Trong bài học về các file header, bạn đã học được rằng, có thể đặt các phần code khai báo hàm vào trong các file header để sử dụng các hàm đó trong nhiều files hoặc thậm chí nhiều projects. Điều này cũng có thể được thực hiện đối với các class. Phần code định nghĩa của các class có thể được đặt vào trong các file header để tạo điều kiện tái sử dụng trong nhiều files hoặc nhiều projects. Thông thường, phần code định nghĩa của class sẽ được đặt vào trong một file header có cùng tên với class này, còn các hàm thành viên được định nghĩa bên ngoài class sẽ được đặt vào trong một file .cpp có cùng tên với class mà chúng thuộc về.

Dưới đây tiếp tục là ví dụ về class Date, nhưng đã được phân chia vào một file .cpp và một file .h:

Date.h:

/**
* 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/
*/

#ifndef DATE_H
#define DATE_H
 
class Date
{
private:
    int m_year;
    int m_month;
    int m_day;
 
public:
    Date(int year, int month, int day);
 
    void SetDate(int year, int month, int day);
 
    int getYear() { return m_year; }
    int getMonth() { return m_month; }
    int getDay()  { return m_day; }
};
 
#endif

Date.cpp:

#include "Date.h"
 
// Date constructor
Date::Date(int year, int month, int day)
{
    SetDate(year, month, day);
}
 
// Date member function
void Date::SetDate(int year, int month, int day)
{
    m_month = month;
    m_day = day;
    m_year = year;
}

Từ bây giờ, bất kỳ file header hoặc file code nào muốn sử dụng class Date, chỉ cần gõ lệnh #include “Date.h”. Lưu ý rằng Date.cpp cũng cần được biên dịch vào bất kỳ dự án nào mà sử dụng Date.h để linker (trình liên kết) có thể biết được class Date được cài đặt như thế nào.

Việc định nghĩa một class bên trong một file header có vi phạm quy tắc one-definition – một định nghĩa hay không?

Điều này là không nên. Nếu file header của bạn có các header guards phù hợp, thì không một ai có thể include phần code định nghĩa của class nào đó nhiều hơn một lần, vào trong cùng một file.

Các kiểu dữ liệu (bao gồm cả các kiểu class), được miễn trừ khỏi một phần của quy tắc one-definition (một định nghĩa), phần này phát biểu rằng: Bạn chỉ có thể có được một định nghĩa (vdu: định nghĩa về một class cụ thể nào đó) cho mỗi chương trình. Do đó, việc #including các phần định nghĩa class vào trong nhiều file code là không vấn đề gì (Nếu điều này xảy ra, các class sẽ không được sử dụng nhiều).

Việc định nghĩa các hàm thành viên bên trong một file header có vi phạm quy tắc one-definition không?

Điều này còn tùy. Các hàm thành viên được định nghĩa bên trong phần code định nghĩa của class được coi là các hàm nội tuyến một cách ngầm định. Các hàm nội tuyến (inline functions) được miễn trừ khỏi luật một định nghĩa cho mỗi chương trình (one definition per program), là một phần của quy tắc one-definition. Điều này có nghĩa là sẽ không có vấn đề gì khi định nghĩa các hàm thành viên thông thường (chẳng hạn như các hàm truy cập – access functions) bên trong phần code định nghĩa của chính class.

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 được coi là các hàm bình thường, và phải tuân theo luật một định nghĩa cho mỗi chương trình (one definition per program), là một phần của quy tắc one-definition. Do đó, các hàm này nên được định nghĩa bên trong một file code, chứ không phải bên trong file header. Có một ngoại lệ cho điều này, đó là đối với các hàm khuôn mẫu (template functions), mà chúng tôi sẽ trình bày trong một chương khác.

Vậy tôi nên định nghĩa cái gì bên trong file header và file cpp? Và nên định nghĩa cái gì bên trong và bên ngoài phần code định nghĩa của class?

Bạn có thể muốn đưa tất cả các phần code định nghĩa hàm thành viên vào trong file header , bên trong class. Mặc dù chương trình vẫn được biên dịch thành công, nhưng có một vài nhược điểm khi làm như vậy. Thứ nhất, như đã đề cập ở trên, điều này sẽ làm cho phần code định nghĩa class của bạn trở nên lộn xộn. Thứ hai, các hàm được định nghĩa bên trong class đều là các hàm nội tuyến ngầm định (implicitly inline functions). Đối với các hàm có khối lượng code lớn được gọi từ nhiều nơi, điều này có thể khiến cho code của bạn bị phình to ra. Thứ ba, nếu bạn thay đổi bất cứ điều gì liên quan đến code trong file header, thì bạn sẽ cần phải biên dịch lại mọi file mà include cái file header này. Điều này có thể gây ra những hiệu ứng lan tỏa, trong đó, một thay đổi nhỏ cũng sẽ khiến cho toàn bộ chương trình cần phải biên dịch lại (có thể làm chậm tiến độ công việc). Nếu bạn thay đổi phần code bên trong một file .cpp, thì chỉ có file .cpp đó cần được biên dịch lại.

Do đó, chúng tôi khuyên bạn nên:

  • Đối với các class chỉ được sử dụng trong một file mà không thường được tái sử dụng, hãy định nghĩa chúng trực tiếp bên trong một file .cpp mà chúng sẽ được sử dụng ở trong file .cpp này.
  • Đối với các class được sử dụng trong nhiều files, hoặc được dự định tái sử dụng nhiều lần, thì hãy định nghĩa chúng trong một file .h có cùng tên với tên của một class cụ thể.
  • Các hàm thành viên thông thường (các hàm constructor hoặc destructor thông thường, các hàm truy cập – access function, v.v…) có thể được định nghĩa bên trong class.
  • Các hàm thành viên không thông thường khác thì nên được định nghĩa bên trong một file .cpp có cùng tên với tên của class.

Trong các bài học sau, hầu hết các class sẽ được định nghĩa bên trong một file .cpp, với tất cả các hàm được cài đặt trực tiếp bên trong phần code định nghĩa của class. Điều này chỉ nhằm giúp cho bạn đọc thuận tiện hơn khi theo dõi, và giữ cho đoạn code ví dụ được ngắn gọn. Trong các projects thực tế, ta sẽ thường thấy các class được đặt trong phần code riêng của chúng và trong các file header, và bạn nên làm quen với việc bố trí các class như vậy.

3. Các tham số truyền vào mặc định

Các tham số truyền vào mặc định dành cho các hàm thành viên nên được kháo báo bên trong phần code định nghĩa của class (ở trong file header), nơi mà bất kỳ ai cũng có thể nhìn thấy chúng khi thực hiện #include cái file header đó.

4. Các thư viện

Việc tách biệt phần code khai báo/định nghĩa và phần code cài đặt của một class là rất phổ biến đối với các thư viện mà bạn có thể sử dụng để mở rộng chương trình của mình. Trong suốt các chương trình C++ của mình, bạn đã #include (thêm vào file code của mình) các file headers thuộc về thư viện standard của C++, chẳng hạn như iostream, string, vector, array, và các thư viện khác. Lưu ý rằng, bạn không cần phải thêm các file iostream.cpp, string.cpp, vector.cpp, hay array.cpp vào trong các projects của mình. Chương trình của bạn cần tới các đoạn code khai báo từ các file header để trình biên dịch có thể xác nhận rằng các chương trình mà bạn đang viết có cú pháp đúng. Tuy nhiên, những phần code cài đặt dành cho các class thuộc về thư viện standard của C++, đều được chứa trong một file đã được biên dịch trước (precompiled file), trong đó, file này sẽ được liên kết vào chương trình ở giai đoạn liên kết. Bạn sẽ không bao giờ nhìn thấy được những phần code cài đặt thực sự của các class thuộc những bộ thư viện mở rộng.

Ngoài một số phần mềm mã nguồn mở (trong đó cả các file .h và các file .cpp đều được cung cấp), thì hầu hết các bộ thư viện lập trình của bên thứ 3 đều chỉ cung cấp các file headers cùng với một file thư viện đã được biên dịch trước. Có một vài lý do cho việc này:

  • Thứ nhất, việc chỉ cần liên kết một file đã được biên dịch trước sẽ nhanh hơn so với việc phải biên dịch lại file này mỗi khi bạn cần nó.
  • Thứ hai, một bản sao của một thư viện đã được biên dịch trước có thể được chia sẻ bởi nhiều ứng dụng, trong khi đó một file code thông thường sẽ cần phải được biên dịch vào trong một file thực thi (executable) mà sẽ sử dụng nó (điều này làm tăng kích thước size).
  • Và thứ ba, để bảo vệ quyền sở hữu trí tuệ (bạn sẽ không muốn mọi người đánh cắp mã nguồn của mình).

Việc tách biệt các files thành phần khai báo (file header) và phần cài đặt (file code) không chỉ tốt về mặt hình thức, mà nó còn giúp cho việc tạo ra các thư viện tùy chỉnh riêng trở nên dễ dàng hơn. Việc tạo ra các thư viện riêng nằm ngoài phạm vi của bài hướng dẫn này, nhưng việc tách biệt phần khai báo và phần cài đặt của các class chính là một điều kiện tiên quyết để có thể tạo ra các thư viện riêng. 

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