Chuyển tới nội dung chính

Giới thiệu

15 bàiNgôn ngữ C++Xem code chạy từng bước

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.

15
bài học
C++
ngôn ngữ
Từng bước
xem code chạy
4
trụ cột OOP

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

OOP
Lớp và đối tượng
Khuôn mẫu và thể hiện, tạo đối tượng đầu tiên.
OOP
Đóng gói
Gói dữ liệu và hành vi, che phần bên trong.
OOP
Khởi tạo và hủy
Hàm dựng, hàm hủy và vòng đời đối tượng.
OOP
Con trỏ this và truyền đối tượng
Đối tượng tự tham chiếu, truyền theo trị và tham chiếu.
OOP
Nạp chồng toán tử
Cho phép dùng dấu toán tử với kiểu do ta định nghĩa.
OOP
Nạp chồng nhập xuất và chuyển kiểu
Toán tử luồng và chuyển đổi kiểu tự nhiên.
OOP
Thành viên tĩnh
Dữ liệu và hành vi dùng chung cho cả lớp.
OOP
Quan hệ có một: Composition
Ghép đối tượng từ các đối tượng thành phần.
OOP
Kế thừa và đa hình
Lớp con mở rộng lớp cha, một lời gọi nhiều đáp ứng.
OOP
Quyền truy cập
public, private, protected và ranh giới đóng gói.
OOP
Lớp trừu tượng và interface
Hàm ảo thuần túy và hợp đồng giữa các lớp.
OOP
Đa thừa kế và vấn đề kim cương
Nhiều lớp cha, thừa kế ảo gỡ nhập nhằng.
OOP
Sao chép đối tượng
Sao chép sâu và sao chép nông, tránh bẫy con trỏ.
OOP
Cấp phát động và con trỏ thông minh
Cấp phát trên vùng nhớ động, quản lý tài nguyên an toàn.
OOP
Template: lập trình tổng quát
Viết một lần, chạy với nhiều kiểu dữ liệu.

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.
Vì sao OOP ra đời

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ínhCó cấu trúc / thủ tụcHướng đối tượng
Đơn vị tổ chứcdãy lệnhhàm, thủ tụcđối tượng (dữ liệu + hành vi)
Quan hệ dữ liệu và hàmrời rạc, biến toàn cụcdữ liệu và hàm tách rờigói chung một chỗ
Khoanh vùng thay đổirất kémtrung bìnhtốt, gói trong đối tượng
Tái sử dụng mãhầu như khôngqua hàmqua lớp và kế thừa
Mô phỏng thế giới thựckhông tự nhiênmột phầnsát cách con người nghĩ
Quy mô phù hợpchương trình rất nhỏvừa và nhỏvừa và lớn
Ba phương pháp không loại trừ nhau

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.

Thông điệp và hành vi

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ácVí 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 trongmá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 quanmô 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ượnglệnh "kêu đi": mèo kêu "meo meo", chó kêu "gau gau"
Bốn trụ cột bổ trợ nhau

Đó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.

Lưu ý về kế thừa

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ô động cơ chứ ô tô không độ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().

Tạo một đối tượng và gọi hành vi của nó C++
1class SinhVien {
2public:
3 int maSo;
4 double diemTB;
5 bool coHocBong() { return diemTB >= 8.0; }
6};
7
8int main() {
9 SinhVien an;
10 an.maSo = 22001;
11 an.diemTB = 8.5;
12 if (an.coHocBong())
13 cout << "Co hoc bong";
14}
Ngăn xếp stack
main()
#1SinhVien
maSo?
diemTB?
Bộ nhớ động heap
(trống)
Khai báo SinhVien an. Một đối tượng được tạo trên NGĂN XẾP theo khuôn mẫu của lớp SinhVien. Hai thuộc tính chưa gán nên còn là giá trị rác.
1/5
Vì sao C++

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ểmNhược điểm
Độ phức tạpkhoanh vùng thay đổi gọn trong từng đối tượnggiai đoạn thiết kế công phu, cần kinh nghiệm chia lớp
Tái sử dụngqua 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
Khi nào nên dùng

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:

  1. 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.
  2. Lớp HinhVuong được xây dựng dựa trên lớp HinhChuNhat đã có.
  3. 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.
  4. 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
  1. Đó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).
  2. 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).
  3. Đ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.
  4. 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.

Ghi nhớ

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.