----> Lời khuyên cho các bạn bắt đầu học lập trình: nếu có tham khảo code trên mạng thì các bạn nên xem nó và tự viết lại rồi tìm hiểu xem từng phần trong code ý nó có mục đích là gì chứ đừng nên copy và paste một cách máy móc mà mình chả hiểu được code ý nó viết gì.
Trong C++, class là nền tảng cho lập trình hướng đối tượng. Nó là sự mở rộng của khái niệm data structure: thay vì chỉ lưu trữ dữ liệu thì nó lưu trữ cả dữ liệu và hàm. Tuy nhiên nó chỉ là một khái niệm trừu tượng, nó không phải là một thực thể có thực khi chương trình đang chạy, mà chỉ là khuôn mẫu để “đúc” ra những object. Mộtobject là một thể hiện (instance) của class. Có thể coi class giống như kiểu dữ liệu còn object như các biến.
1. Định nghĩa một lớp
Các class được tạo ra bằng cách sử dụng từ khóa class. Chúng ta sẽ xem xét một ví dụ về định nghĩa lớp. Giả sử chúng ta lập một lớp Student trong đó lưu trữ các thông tin về sinh viên cũng như chứa các hàm để thao tác trên các dữ liệu này.
#include <string>
using namespace std;
// class definition
class Student{
private:
string name; // tên sinh viên
int age; // tuổi
string student_code; // mã số sinh viên
public:
// set information
void set_name(string); // nhập tên
void set_age(int); // nhập tuổi
void set_student_code(string); // nhập mã sinh viên
// get information
string get_name(); // lấy tên
int get_age(); // lấy tuổi
string get_student_code(); // lấy mã sinh viên
};
Ta sẽ phân tích định nghĩa trên của lớp Student. Đầu tiên là từ khóa class, sau đó là tên lớp mà người dùng muốn tạo (ở đây là Student). Phần còn lại nằm trong cặp ngoặc móc {} chứa những thành của lớp. Những dữ liệu như: name, age, student_code được gọi là các thành phần dữ liệu (data members), còn các hàm như: set_name(), get_name(), … được gọi là các hàm thành viên (member functions) hay phương thức (methods). Thông thường các data members được để ở chế độ private, còn các member functions thì ở chế độ public.
Từ khóa private và public là hai access-specifier quy định quyền truy nhập đối với các thành phần trong lớp, nó sẽ có hiệu lực cho các thành phần của lớp đứng sau nó cho đến khi gặp một access-specifier khác. Tất cả các thành phần được khai báo là public sẽ có thể được truy cập “thoải mái” bất cứ chỗ nào lớp có hiệu lực. Ví dụ nó có thể được truy cập bởi hàm thành viên của của một lớp khác, hoặc các hàm tự do trong chương trình. Các thành phần private thì được bảo mật cao hơn. Chỉ những thành phần của lớp mới có thể truy nhập đến chúng. Mọi cố gắng truy nhập bất hợp pháp từ bên ngoài đều sẽ gây lỗi. Do đó ta có thể mô tả cú pháp chung để định nghĩa một lớp như sau:
private:
// các thành phần private
public:
// các thành phần public
};
Chú ý sau mỗi định nghĩa lớp phải có dấu chấm phẩy (semicolon) vì định nghĩa lớp là tương đương với định nghĩa một kiểu dữ liệu mới. Cú khai báo một đối tượng của lớpStudent giống y như cú pháp khai báo một biến bình thường:
Student studentA, studentB; // khai báo hai đối tượng thuộc lớp Student là studentA và studentB
2. Định nghĩa các hàm thành viên cho lớp
Trong định nghĩa trên của lớp mới chỉ khai báo các nguyên mẫu hàm (function prototypes) chứ hoàn toàn chưa có thân hàm. Ta sẽ phải định nghĩa các hàm này. Có hai cách định nghĩa các hàm thành viên: định nghĩa hàm thành viên ngay trong định nghĩa lớphoặc khai báo nguyên mẫu trong lớp, còn định nghĩa bên ngoài lớp.
Định nghĩa hàm ngay trong định nghĩa lớp
Khi đó định nghĩa lớp được viết lại như sau:
private:
string name;
int age;
string student_code;
public:
// set information
void set_name(string str){ name=str; }
void set_age(int num){ age=num; }
void set_student_code(string str){ student_code=str; }
// get information
string get_name(){ return name; }
int get_age(){ return age; }
string get_student_code(){ return student_code; };
};
Ta nhận thấy các hàm đã được định nghĩa luôn trong nghĩa lớp. Tuy nhiên cách này không phải là cách tốt. Với bài này thì các hàm còn đơn giản, còn ngắn. Nhưng trong thực tế khi ta xây dựng các lớp những lớp phức tạp hơn thì số lượng hàm sẽ nhiều hơn và dài hơn. Nếu định nghĩa trong lớp sẽ làm “mất mĩ quan” và khó kiểm soát. Vì vậy trong định nghĩa lớp ta chỉ liệt kê các nguyên mẫu hàm, còn khi định nghĩa, ta sẽ định nghĩa ra bên ngoài.
Khai báo hàm trong lớp, còn định nghĩa ngoài lớp
class Student{
private:
string name;
int age;
string student_code;
public:
// set information
void set_name(string);
void set_age(int);
void set_student_code(string);
// get information
string get_name();
int get_age();
string get_student_code();
};
// member function definitions
// set name
void Student::set_name(string str){
name=str;
}
// set age
void Student::set_age(int num){
age=num;
}
// set student code
void Student::set_student_code(string str){
student_code=str;
}
// get name
string Student::get_name(){
return name;
}
// get age
int Student::get_age(){
return age;
}
// get student code
string Student::get_student_code(){
return student_code;
}
Cú pháp để định nghĩa một hàm thành viên bên ngoài lớp là:
// định nghĩa thân hàm ở đây
}
Ví dụ hàm set_name
void Student::set_name(string str){
name=str;
}
Cú pháp này chỉ rõ rằng hàm ta đang định nghĩa là hàm thành viên của lớp Student vì nó được chỉ định bởi toán tử phân giải phạm vi :: (trong trường hợp này là Student::), nghĩa là hàm này nằm trong phạm vi lớp Student.
Có một sự khác nhau giữa hai cách định nghĩa hàm này không phải chỉ ở góc độ “thẩm mỹ”. Theo cách thứ nhất: định nghĩa luôn trong định nghĩa lớp, thì hàm được coi là“hàm nội tuyến” hay inline. Thông thường khi muốn một hàm làm một việc gì đó thì ta phải làm một việc là “gọi hàm” (invoke). Việc gọi hàm sẽ phải tốn các chi phí về thời gian như gửi lời gọi hàm, truyền đối số, … điều này có thể làm chậm chương trình. C++ cung cấp một giải pháp đó là “hàm nội tuyến” bằng cách thêm vào từ khóa inline trước kiểu trả về của hàm như sau:
// định nghĩa thân hàm ở đây
}
Điều này “gợi ý” cho compiler sinh mã của hàm ở những nơi thích hợp để tránh phải gọi hàm. Như vậy sẽ tránh được những chi phí gọi hàm nhưng ngược lại nó làm tăng kích thước của chương trình. Vì cứ mỗi lời gọi hàm sẽ được thay thế bởi một đoạn mã tương ứng. Vì vậy chỉ khai báo một hàm là inline khi kích thước của nó không quá lớn và không chứa vòng lặp cũng như đệ quy. Hơn nữa không phải hàm nào khai báo inline đều được hiểu là inline, vì đó chỉ là “gợi ý” cho compiler. Nếu compiler nhận thấy hàm có kích thước khá lớn, xuất hiện nhiều lần trong chương trình, hoặc có chứa các cấu trúc lặp, đệ quy thì nó có thể “lờ đi” yêu cầu inline này. Hiện này các compiler đều được tối ưu rất tốt, vì vậy không cần thiết phải dùng inline. Đó là sự khác biệt của các hàm thành viên được định nghĩa ngay trong lớp so với các hàm thành viên được định nghĩa bên ngoài.
3. Truy cập đến những thành phần của lớp
Để truy cập đến các thành phần của lớp ta dùng toán tử chấm (selection dot operator) thông qua tên của đối tượng. Ví dụ đoạn chương trình sau gọi hàm set_name để nhập tên cho đối tượng studentA và gọi hàm get_name để lấy tên của đối tượng :
studentA.set_name(“Bill Gates”); // gán tên cho studentA là “Bill Gates”
cout << studentA.get_name(); // in ra tên đối tượng studentA
Kết quả thu được là màn hình hiển thị dòng văn bản “Bill Gates”. Để ý lại định nghĩa của hàm set_name và get_name:
void Student::set_name(string str){
name=str;
}
// get name
string Student::get_name(){
return name;
}
Ta nhận thấy name là thành phần dữ liệu được khai báo private. Điều đó nghĩa là chỉ có những hàm thành viên mới có quyền truy nhập đến nó (sau này ta sẽ biết thêm một trường hợp nữa, đó là hàm bạn – friend, cũng có khả năng truy nhập đến các thành phần private). Hàm set_name và get_name là hai hàm thành viên của lớp Student nên nó có thể truy nhập và thao tác được trên dữ liệu name. Nhưng nỗ lực truy nhập trực tiếp và các thành phần private mà không thông qua hàm thành viên như ví dụ sau sẽ gây lỗi biên dịch (compilation error):
studentA.name=”Bill Gate”; // error
4. Ưu điểm của việc đóng gói dữ liệu và phương thức trong một đơn vị thống nhất – lớp
Việc đóng gói dữ liệu kết hợp với quy định phạm vi truy nhập cho các thành phần của lớp có nhiều ưu điểm.
Thứ nhất: tạo ra sự gọn gàng dễ kiểm soát.
Việc đóng gói dữ liệu và các phương thức liên quan giúp chương trình gọn gàng hơn, lập trình viên dễ kiểm soát hơn vì tất cả đều được gói gọn trong phạm vi của lớp.
Thứ hai: trừu tượng hóa dữ liệu, thông qua “giao diện”, tạo thuận lợi cho người dùng
Việc cung cấp các hàm thành viên để thao tác trên các dữ liệu của đối tượng tạo sự “thân thiện” cho người dùng. Trong ví dụ lớp Student ở trên, để nhập tên cho một đối tượng ta chỉ cần gọi hàm set_name thông qua tên đối tượng mà không cần quan tâm đến cài đặt chi tiết như thế nào.
Thứ ba: tính bảo mật của dữ liêu được nâng cao
Để truy cập đến các dữ liệu private của một đối tượng bắt buộc phải thông qua hàm thành viên. Tức mọi “giao tiếp” với đối tượng đều phải thông qua “giao diện” mà ta đã quy định trước. Ví dụ: nhập tên cho studentA thì bắt buộc phải dùng hàm set_name, lấy tên thì dùng get_name. Do đó sẽ tránh được những truy cập và sửa đổi bất hợp pháp, đồng thời nếu phát sinh lỗi thì sẽ dễ khoanh vùng hơn. Ví dụ khi yêu cầu trả về mã số sinh viên của studentA thì phát hiện một số lỗi nào đó. Rõ ràng những lỗi đó chỉ có thể do các hàm có liên quan trực tiếp đến student_code như set_student_code hoặc get_student_code chứ không thể là set_name hay get_name được.
Thứ tư: tăng cường tính độc lập và ổn định hơn cho các thành phần sử dụng lớp trong chương trình
Giả sử vì một lý do nào đó mà thành phần name buộc phải đổi lại thành full_name thì chương trình sẽ phải chỉnh sửa lại một chút. Tuy nhiên chỉ những hàm thành viên nào liên quan trực tiếp đến name mới phải sửa đổi, tức là các hàm set_name và get_name sẽ phải sửa lại name thành full_name. Tuy nhiên, các hàm gọi đến hàm set_name vàget_name thì không hề phải sửa lại, bởi vì nó không biết cài đặt chi tiết bên trong set_name và get_name như thế nào mà chỉ biết “giao diện” của set_name và get_name vẫn thế, do đó chương trình không phải chỉnh sửa nhiều.