Giới thiệu
Lập trình hướng đối tượng với C++
Học tư duy hướng đối tượng bằng C++, nhìn thấy từng đối tượng sinh ra trên bộ nhớ và tự trả lời câu hỏi về chính mình.
Lập trình không chỉ là gõ ra những dòng lệnh để máy chạy. Ở mức sâu hơn, lập trình là cách ta tổ chức suy nghĩ về một bài toán rồi diễn đạt nó thành cấu trúc mà máy hiểu được. Cách tổ chức ấy gọi là phương pháp lập trình (programming paradigm). Bài này nói về tư duy hướng đối tượng, chưa chạm tới cú pháp chi tiết của C++; cú pháp sẽ bắt đầu từ những bài sau.
Lộ trình học phần
Các phương pháp lập trình
Lịch sử lập trình là một chuỗi nỗ lực giúp con người khống chế độ phức tạp ngày một lớn của phần mềm. Mỗi khi chương trình phình to quá ngưỡng mà cách cũ không kiểm soát nổi, một phương pháp mới lại ra đời. Ba mốc lớn của hành trình đó là:
- Lập trình tuyến tính: một dãy lệnh chạy tuần tự từ trên xuống, nhảy qua lại bằng
goto. Hợp với chương trình rất nhỏ, nhưng khi lớn lên thì luồng điều khiển rối thành "mã mì spaghetti" (spaghetti code), sửa một chỗ hỏng một chỗ khác. - Lập trình có cấu trúc và thủ tục: chia bài toán thành các hàm, mỗi hàm làm một
việc và gọi lại được nhiều lần. Mọi luồng điều khiển diễn đạt bằng tuần tự, rẽ nhánh,
lặp, nên bỏ được
goto. Đây là lối mà nhập môn lập trình thường dạy. Điểm yếu cốt tử: nó tách rời dữ liệu khỏi hàm xử lý dữ liệu, ai cũng chạm được vào dữ liệu, nên khi dữ liệu đổi thì phải đi sửa rất nhiều nơi. - Lập trình hướng đối tượng (object-oriented programming, OOP): gói dữ liệu và hành vi tác động lên dữ liệu đó vào chung một đối tượng. Dữ liệu không còn phơi ra cho mọi hàm mà được đối tượng giữ kín, chỉ lộ ra qua hành vi công khai.
Cuối thập niên 1960, khi người ta viết những hệ thống lớn như hệ điều hành, các dự án liên tục trễ hạn, đầy lỗi, khó bảo trì. Giới nghiên cứu gọi đó là khủng hoảng phần mềm (software crisis). Gốc rễ không nằm ở tốc độ máy mà ở chỗ con người thiếu công cụ tư duy để khống chế độ phức tạp. OOP là một câu trả lời: thay vì hỏi "chương trình cần làm những thao tác gì", người viết hỏi "bài toán gồm những thực thể nào, mỗi thực thể biết gì và làm được gì". Ngôn ngữ Simula (thập niên 1960) đã có lớp và đối tượng; Smalltalk (thập niên 1970) đẩy thành triết lý; Bjarne Stroustrup đưa các ý tưởng này vào C để tạo ra C++ đầu thập niên 1980.
So sánh ba phương pháp
| Tiêu chí | Tuyến tính | Có cấu trúc / thủ tục | Hướng đối tượng |
|---|---|---|---|
| Đơn vị tổ chức | dãy lệnh | hàm, thủ tục | đối tượng (dữ liệu + hành vi) |
| Quan hệ dữ liệu và hàm | rời rạc, biến toàn cục | dữ liệu và hàm tách rời | gói chung một chỗ |
| Khoanh vùng thay đổi | rất kém | trung bình | tốt, gói trong đối tượng |
| Tái sử dụng mã | hầu như không | qua hàm | qua lớp và kế thừa |
| Mô phỏng thế giới thực | không tự nhiên | một phần | sát cách con người nghĩ |
| Quy mô phù hợp | chương trình rất nhỏ | vừa và nhỏ | vừa và lớn |
Hướng đối tượng kế thừa toàn bộ thành quả của lập trình có cấu trúc: bên trong mỗi hành vi của đối tượng, ta vẫn dùng tuần tự, rẽ nhánh và lặp. OOP chỉ thêm một tầng tổ chức ở mức cao hơn để gom dữ liệu và hành vi lại với nhau.
Lớp và đối tượng
Hai khái niệm trung tâm của cả học phần là lớp và đối tượng. Người mới rất hay nhầm, nên cần làm rõ ngay bằng trực giác.
- Lớp (class) là một khuôn mẫu: nó quy định một loại thực thể có những dữ liệu nào và làm được những hành vi nào. Lớp là bản thiết kế, bản thân nó chưa phải thực thể cụ thể.
- Đối tượng (object) là một thực thể cụ thể tạo ra theo khuôn mẫu của lớp. Mỗi đối tượng có giá trị dữ liệu riêng nhưng dùng chung tập hành vi do lớp định nghĩa. Việc tạo một đối tượng từ lớp gọi là tạo thể hiện (instance).
Một phép so sánh quen thuộc: lớp giống bản vẽ thiết kế của một mẫu xe, còn mỗi chiếc xe lăn ra khỏi nhà máy là một đối tượng. Bản vẽ chỉ có một, nhưng số xe sản xuất theo nó thì rất nhiều. Mọi chiếc xe cùng cấu trúc và cách vận hành, song mỗi chiếc mang biển số riêng, màu sơn riêng. Bản vẽ ứng với lớp, giá trị riêng của từng chiếc ứng với dữ liệu của từng đối tượng.
Chương trình hướng đối tượng chạy không phải bằng một hàm trung tâm điều khiển tất cả, mà bằng việc các đối tượng gửi thông điệp cho nhau để nhờ thực hiện một hành vi (còn gọi là phương thức). Bên gửi không cần biết bên nhận làm thế nào, chỉ cần biết nó làm được gì. Giống như khi nhờ ai pha cà phê, ta không quan tâm họ thao tác ra sao, miễn nhận được cốc cà phê.
Bốn trụ cột của hướng đối tượng
Hướng đối tượng được nâng đỡ bởi bốn ý tưởng nền tảng. Ở đây ta tiếp cận ở mức trực giác; các bài sau sẽ làm rõ từng trụ cột bằng cú pháp C++ cụ thể.
| Trụ cột | Ý niệm trực giác | Ví dụ đời thường |
|---|---|---|
| Đóng gói (encapsulation) | gói dữ liệu và hành vi vào một chỗ, che phần bên trong | máy ATM giấu tiền và sổ số dư sau lớp vỏ, chỉ chừa vài nút bấm |
| Trừu tượng (abstraction) | chỉ phơi cái cốt yếu, bỏ qua chi tiết không liên quan | mô hình sinh viên chỉ giữ tên, mã số, điểm; bỏ chiều cao, sở thích |
| Kế thừa (inheritance) | lớp con tái dùng và mở rộng lớp cha (quan hệ "là một") | GiangVien là một NhanVien, nhận sẵn họ tên và cách tính lương |
| Đa hình (polymorphism) | một lời gọi, nhiều cách đáp ứng tuỳ đối tượng | lệnh "kêu đi": mèo kêu "meo meo", chó kêu "gau gau" |
Đóng gói và trừu tượng giúp khống chế độ phức tạp bằng cách che giấu và chọn lọc. Kế thừa và đa hình giúp mở rộng linh hoạt bằng cách tái dùng và cho phép cùng một lời gọi thích ứng với nhiều loại đối tượng.
Chỉ dùng kế thừa khi quan hệ "là một" đúng thật. Nếu một lớp chỉ có chứa lớp khác, ví dụ ô tô có động cơ chứ ô tô không là động cơ, thì đó là quan hệ kết hợp, không phải kế thừa.
Nếm thử tư duy đối tượng
Lý thuyết là đủ rồi, hãy "nếm" thử một ví dụ tối giản. Ta mô tả khái niệm sinh viên bằng
một lớp SinhVien với hai thuộc tính (maSo, diemTB) và một hành vi coHocBong() trả
lời câu hỏi: sinh viên này có đủ điểm nhận học bổng không (lấy mốc 8.0).
#include <iostream>
using namespace std;
class SinhVien {
public:
int maSo; // student id
double diemTB; // grade point average
// behavior: does this student qualify for a scholarship?
bool coHocBong() { return diemTB >= 8.0; }
};
int main() {
SinhVien an; // create one object from the class
an.maSo = 22001;
an.diemTB = 8.5;
if (an.coHocBong()) cout << "Co hoc bong";
else cout << "Khong";
}
Điểm cốt lõi: dữ liệu (maSo, diemTB) và hành vi (coHocBong()) sống chung trong
lớp SinhVien. Khi gọi an.coHocBong(), hành vi đó đọc thẳng diemTB của chính đối
tượng an, không cần ta truyền gì vào.
Xem chạy từng bước
Bấm ▶ Chạy (hoặc kéo thanh trượt) để theo dõi: đối tượng an nằm trên ngăn xếp
với trạng thái riêng, rồi lời gọi an.coHocBong() được phân giải về SinhVien::coHocBong().
1class SinhVien {2public:3 int maSo;4 double diemTB;5 bool coHocBong() { return diemTB >= 8.0; }6};78int main() {9 SinhVien an;10 an.maSo = 22001;11 an.diemTB = 8.5;12 if (an.coHocBong())13 cout << "Co hoc bong";14}
Hướng đối tượng là một mô thức tư duy, không gắn chết với ngôn ngữ nào. Java bắt buộc mọi thứ nằm trong lớp và tự quản lý bộ nhớ; Python hỗ trợ OOP rất linh hoạt và cú pháp gọn. Học phần này chọn C++ vì nó buộc người học hiểu cả những thứ ngôn ngữ khác giấu đi, như cách đối tượng nằm trong bộ nhớ và vòng đời của nó. Khi đã vững OOP qua C++, chuyển sang Java hay Python sẽ rất nhẹ, vì bốn trụ cột là chung, chỉ khác cú pháp.
Ưu, nhược điểm và khi nào nên dùng
Không phương pháp nào hoàn hảo cho mọi bài toán. Hiểu đúng mặt mạnh và mặt yếu giúp ta dùng OOP đúng chỗ thay vì áp dụng máy móc.
| Ưu điểm | Nhược điểm | |
|---|---|---|
| Độ phức tạp | khoanh vùng thay đổi gọn trong từng đối tượng | giai đoạn thiết kế công phu, cần kinh nghiệm chia lớp |
| Tái sử dụng | qua kế thừa và đa hình, ít sửa mã cũ | với bài toán rất nhỏ, bộ khung lớp làm mã dài dòng |
| Mô hình hoá | sát cách con người tư duy về thế giới thực | đường học dốc, nhiều khái niệm trừu tượng phải nắm trước |
OOP phát huy giá trị rõ nhất ở hệ thống vừa và lớn, sống lâu, nhiều người cùng phát triển và phải đổi liên tục: phần mềm quản lý, ứng dụng nghiệp vụ, trò chơi, giao diện đồ hoạ. Ngược lại, với một script nhỏ chạy một lần hay một công thức toán thuần tuý, lối thủ tục gọn gàng có khi phù hợp hơn. Người lập trình giỏi không dùng OOP ở mọi nơi, mà biết chọn đúng mức tổ chức cho từng bài toán.
Bài tập thực hành
Bài tập 1: phân biệt lớp và đối tượng
Giải thích bằng lời của bạn sự khác nhau giữa lớp và đối tượng. Cho một ví dụ trong đời sống mà bạn tự nghĩ ra, chỉ rõ đâu là lớp, đâu là các đối tượng.
Gợi ý lời giải
Lớp là khuôn mẫu (bản thiết kế) mô tả một loại thực thể có những dữ liệu và hành vi nào; đối tượng là thể hiện cụ thể tạo ra từ khuôn mẫu đó, mang giá trị dữ liệu riêng.
Ví dụ: lớp DienThoai quy định mọi điện thoại có số IMEI, dung lượng pin, và hành vi
goiDien(), chupAnh(). Chiếc điện thoại trên bàn bạn và chiếc trong túi người bạn là
hai đối tượng khác nhau của lớp này: cùng cấu trúc và hành vi, nhưng IMEI và mức pin
riêng. Khuôn mẫu viết một lần, đối tượng sinh ra bao nhiêu tuỳ nhu cầu.
Bài tập 2: nhận diện bốn trụ cột
Với mỗi tình huống dưới đây, cho biết nó minh hoạ trụ cột nào và giải thích ngắn gọn lý do:
- Người dùng điều khiển máy giặt qua vài nút bấm mà không biết cơ chế bên trong.
- Lớp
HinhVuongđược xây dựng dựa trên lớpHinhChuNhatđã có. - Cùng một lệnh "vẽ" cho ra hình tròn hay hình tam giác tuỳ đối tượng nhận lệnh.
- Khi mô hình hoá cuốn sách trong thư viện, ta chỉ giữ tên, tác giả, mã số mà bỏ qua màu bìa.
Gợi ý lời giải
- Đóng gói: cơ chế bên trong bị che kín, người dùng chỉ tương tác qua giao diện công khai (vài nút bấm).
- Kế thừa: lớp mới tái dùng và mở rộng lớp đã có, quan hệ "là một" (một hình vuông là một hình chữ nhật có hai cạnh bằng nhau).
- Đa hình: cùng một lời gọi "vẽ" cho ra cách đáp ứng khác nhau tuỳ đối tượng thực sự đứng sau.
- Trừu tượng: ta chỉ giữ lại đặc điểm cốt yếu cho bài toán (tên, tác giả, mã số) và bỏ qua chi tiết không liên quan (màu bìa).
Bài tập 3: thiết kế một lớp ở mức ý tưởng
Chọn một thực thể trong miền trường học (môn học, giảng viên, hoặc phòng học). Thiết kế ở mức ý tưởng một lớp cho thực thể đó: liệt kê các dữ liệu và các hành vi mà bạn cho là cốt yếu, rồi mô tả hai đối tượng cụ thể được tạo từ lớp này. Chưa cần viết mã C++, chỉ mô tả bằng lời.
Gợi ý lời giải
Ví dụ lớp MonHoc:
- Dữ liệu cốt yếu: mã môn, tên môn, số tín chỉ.
- Hành vi cốt yếu:
tongSoTiet()(suy ra từ số tín chỉ),laMonTienQuyet(mon)(kiểm tra một môn khác có phải tiên quyết không).
Hai đối tượng cụ thể:
oop: mãOOP301, tên "Lập trình hướng đối tượng", 3 tín chỉ.csdl: mãDB202, tên "Cơ sở dữ liệu", 2 tín chỉ.
Cả hai dùng chung cấu trúc và hành vi do lớp MonHoc định nghĩa, nhưng mỗi đối tượng giữ
giá trị riêng. Đây chính là quan hệ một lớp sinh ra nhiều đối tượng, mỗi đối tượng một
trạng thái.
Hướng đối tượng gói dữ liệu và hành vi vào đối tượng, sinh ra từ một lớp khuôn mẫu. Bốn trụ cột là đóng gói, trừu tượng, kế thừa, đa hình. Mục tiêu cuối cùng là khống chế độ phức tạp và giúp phần mềm dễ thay đổi an toàn về sau.