Trong chương này, chúng ta sẽ cùng nói về lập trình hướng đối tượng (object oriented programmation – OOP). Như lúc trước tôi đã nói, đây là 1 phương thức lập trình khác so với lập trình truyền thống. Có thể ban đầu bạn thấy là mã nguồn mà bạn viết không thay đổi nhiều lắm và những kiến thức này có vẻ là vô dụng. Thế nhưng tôi tin rằng khi đã quen thuộc, bạn sẽ nhận ra chương trình được diễn đạt tự nhiên hơn và mã nguồn của bạn được sắp xếp hợp lý hơn.
Các bạn sẽ thấy, trong OOP có 2 dạng lập trình viên : những người tạo ra đối tượng (object) và những người sử dụng chúng. Chúng ta sẽ đề cập đến họ trong bài học này. Rồi tôi sẽ chỉ cho các bạn cách để sử dụng 1 đối tượng đã được tạo sẵn qua 1 số ví dụ về kiểu string
của C++.
Tôi dám cá là nếu có 1 nhóm những từ gì mà khi nghe bạn cảm thấy vô cùng mơ hồ, vô cùng khó hiểu và khó giải thích thì chắc chắn là « đối tượng » sẽ nằm trong nhóm đó.
Thuật ngữ của khái niệm này trong tiếng Anh là « object », dịch ra tiếng Việt thì có nghĩa là « vật » hay « vật thể ». Vậy chúng ta có thể kết luận là « đối tượng » là 1 « vật thể » và thế là chúng ta đã bước thêm được 1 bước dài…. chừng 1 cm trên con đường để hiểu về khái niệm này. Nói vậy là bởi vì « vật thể » thì cũng là khái niệm mơ hồ đến không thể mơ hồ hơn được, nhưng chúng ta có thể hiểu rõ hơn qua các ví dụ : cái ôtô là 1 vật thể, cái máy vi tính là 1 vật thể và con mèo Tobi nhà tôi cũng là 1 vật thể, vv…
Thực ra, tồn tại xung quanh chúng ta đều là các « vật thể ». Cả những kiến thức của chúng ta cũng có thể coi là 1 dạng « vật thể ». Vậy nên, mọi thứ đều là « vật thể », và cũng chính là các « đối tượng ».
Ý tưởng của lập trình hướng đối tượng chính là trong mã nguồn chương trình, chúng ta sẽ thao tác với các yếu tố thành phần được gọi là « đối tượng ».
1 vài ví dụ về các đối tượng thường gặp trong lập trình :
Như các bạn thấy, rất nhiều thứ có thể được coi là đối tượng.
Hoàn toàn không giống như các biến hay các hàm, đối tượng là 1 khái niệm mới trong lập trình. Nếu muốn nói cụ thể hơn thì tôi có thể cho các bạn biết là đối tượng là tập hợp của nhiều biến và nhiều hàm khác nhau.
Tuy nhiên, đừng quá mất thời gian dừng lại ở tiết lộ vừa rồi của tôi, hãy tiếp tục và bạn sẽ hiểu rõ ràng hơn.
Để tránh việc các bạn bị lạc giữa đống sương mù các khái niệm mơ hồ, chúng ta sẽ cùng hình dung 1 đối tượng thông qua các hình ảnh cụ thể.
Cùng tưởng tượng 1 lâp trình viên luốn viết 1 chương trình thao tác với các cửa số giao diện. Anh ta muốn hiển thị nó ra màn hình, thay đổi kích thước, di chuyển và cũng muốn xóa đi khi không cần sử dụng nữa. Đoạn mã đó khá phức tạp với nhiều hàm gọi lẫn nhau và vô số biến để lưu những dữ liệu con như màu nền hay giá trị kích thước cửa sổ, vv…
Anh ta mất rất nhiều thời gian, thế nhưng rồi 1 ngày anh ấy cũng làm xong. Mã nguồn của chương trình gồm 1 đống hàm và biến khác nhau mà chúng ta chả hiều gì mấy, giống như mấy bài thực hành hồi học cấp 3 vậy.
Lập trình viên này rất hài lòng với thành quả của mình và muốn chia sẻ nó cho mọi người qua Internet để không ai còn phải mất thười gian đi viết mã mỗi khi muốn tạo ra các cửa sổ nữa.
Mục đích rất tốt đẹp nhưng còn tồn tại 1 vấn đề đó là bạn không biết làm sao để sử dụng được đoạn mã đó. Hàng loạt câu hỏi như « Phải truyền thông số gì cho hàm nào để có thể thay đổi kích thước cửa số ? » hay « Gọi hàm nào để xóa cửa sổ ? » được đặt ra trong quá trình sử dụng.
Thật may là anh bạn lập trình viên của chúng ta đã tính trước tất cả và thiết kế đoạn mã của anh ta theo hướng đối tượng, nghĩa là đặt tất cả những quy trình phức tạp và đau đầu đó vào trong 1 cái hộp kín mà ta gọi là « đối tượng ».
Trong hình ảnh này, tôi cố tình để cho chiếc hộp trong suốt để các bạn thấy đống quy trình được đặt trong đấy. Trên thực tế thì chiếc hộp này hoàn toàn kín và người dùng sẽ không nhìn thấy gì ở bên trong cả.
Tất cả những xử lý phức tạp vẫn còn đó nhưng bị che lấp trước mắt người dùng.
Người tạo ra chiếc hộp không bắt buộc chúng ta phải hiểu hết cơ chế hoạt động bên trong. Thay vào đó, anh ta cung cấp cho chúng ta 1 số cái nút và cần gạt ở bên ngoài chiếc hộp để có thể tương tác với nó : có chiếc nút để thay đổi kích thước cửa số, có chiếc nút để xóa cửa sổ, vv… Người dùng sử dụng những chiếc nút với cách thức hoạt động khá đơn giản thay vì đau đầu vì đống cơ chế khó hiểu bên trong.
Lập trình hướng đối tượng chính là công việc tạo ra chiếc hộp đó. Chúng ta tạo ra những đoạn mã nguồn phức tạp để thực hiện 1 số xử lý nhất định nhưng che giấu chúng trước người dùng bằng cách cho vào trong 1 cái « hộp » (đối tượng). Về phần người dùng cần phải biết cách ấn nút và không cần quan tâm đến cái gì khác.
Bất cứ khi nào các bạn thấy mơ hồ, hãy nhớ lại hình ảnh của chiếc hộp này, hy vọng sẽ giúp bạn hiểu rõ thêm điều gì đó.
Trước tiên, chúng ta sẽ chưa học cách tạo ra các đối tượng mà bắt đầu với việc đơn giản hơn là cách sử dụng chúng.
Các ví dụ chúng ta sắp xem tới đây có liên quan tới kiểu dữ liệu mà các bạn tưởng như đã quen thuộc, kiểu string
.
Sự thật là không giống như những kiểu dữ liệu khác như int
, float
hay double
, kiểu string
bản thân nó chính là chiếc hộp bí mật. Đúng vậy, string
chính là 1 đối tượng. Từ trước cho tới giờ thì những gì chúng ta làm mới chỉ là ấn nút trên chiếc hộp này. Hôm nay chúng ta sẽ vén lên bức màn vẫn luôn che giấu bí mật kinh hoàng của kiểu dữ liệu này.
Nhờ sự thần kỳ của lập trình hướng đối tượng, các bạn có thể sử dụng kiểu string
khi viết những chương trình đầu tiên dù không hiểu nó hoạt động như thế nào. Hãy chuẩn bị tâm lý vì sau đây, tôi sẽ giới thiệu cho bạn cơ chế phức tạp ẩn bên trong chiếc hộp này.
Kiểu string mang 1 cơ chế vận hành phức tạp vì nó làm việc với các chữ cái.
? Có thể có chuyện gì phức tạp khi thao tác với các chữ cái chứ ?
Sự thật thì sự phức tạp nằm chính trong các ký tự mà bạn cho là đơn giản đó. Bởi vì, máy vi tính không hề biết đến sự tồn tại của các ký tự. Đúng như tên gọi, nó là 1 chiếc máy để tính toán và không biết gì khác ngoài các con số.
? Nếu máy tính chỉ biết đến các con số, làm thế nào nó in được các thông điệp ra màn hình ?
Đó là nhờ một mánh khóe nhỏ được sử dụng từ rất rất lâu về trước : bảng ASCII. Bảng này có công dụng để chuyển đổi những con số thành các ký tự.
Số | Ký tự | Số | Ký tự |
64 | @ | 96 | ' |
65 | A | 97 | a |
66 | B | 98 | b |
67 | C | 99 | c |
68 | D | 100 | d |
69 | E | 101 | e |
70 | F | 102 | f |
71 | G | 103 | g |
72 | H | 104 | h |
73 | I | 105 | i |
74 | J | 106 | j |
75 | K | 107 | k |
76 | L | 108 | l |
77 | M | 109 | m |
Như các bạn đã thấy, chữ A tương đương với số 65 trong khi đó chữ a thì là số 97, vv… Tất cả các chữ cái tiếng Anh đều có ở trong bảng này. Nguyên nhân mà những chữ có dấu trong tiếng Việt đều không dùng được trong C++ cũng nằm ở đây, bởi vì chúng không nằm trong bảng.
? Điều này nghĩa là mỗi khi máy tính thấy số 65, nó sẽ nghĩ đó là chữ A ?
Không phải, máy tính chỉ dịch từ số sang chữ nếu được yêu cầu. Trên thực tế, máy tính dựa vào kiểu dữ liệu của biến để xác định xem dữ liệu được lưu là chữ hay là số :
int
để lưu số 65, máy tính sẽ coi đây là 1 số.char
để lưu số 65, máy tính sẽ coi đây là chữ cái A do kiểu char
là kiểu dữ liệu dành cho các ký tự. Vậy nên nếu bạn dùng kiểu char
để lưu 1 số, nó sẽ tự động bị quy đổi.? 1 char
chỉ có thể lưu được 1 ký tự, vậy làm sao để lưu được 1 chuỗi ?
Việc này không hề đơn giản chút nào. Xin mời đọc tiếp…
Bởi vì mỗi char
chỉ có thể lưu được 1 ký tự, các lập trình viên đã nảy ra ý tưởng là tạo ra 1 mảng ký tự để có thể lưu được 1 câu chứa nhiều ký tự. Nhờ cấu trúc mảng, trong bộ nhớ, các ký tự này sẽ nằm cạnh nhau và tạo thành 1 chuỗi. Đó là nguồn gốc tại sao chúng ta gọi là chuỗi ký tự.
Vậy nên, chỉ cần khai báo 1 mảng kiểu char với kích thước 100 là đủ để lưu 1 đoạn văn dài 100 ký tự
char vanBan[100];
hoặc sử dụng vector
nếu các bạn muốn kích thước thay đổi
vector<char> vanBan;
Các văn bản trong bộ nhớ thực chất chỉ là các ký tự liên tiếp nhau.
Về lý thuyết thì chúng ta hoàn toàn có thể sử dụng các thao tác với mảng ký tự mỗi khi chúng ta muốn thao tác văn bản. Tuy nhiên việc này quá tốn công sức nên những người thiết kế ra ngôn ngữ đã quyết định sẽ tạo ra 1 đối tượng và sử dụng đối tượng đó để che giấu những xử lý phức tạp đằng sau.
Các bạn đã thấy rằng thao tác với các chuỗi ký tự hoàn toàn không đơn giản như trong tưởng tượng : cần tạo ra 1 mảng ký tự với kích thước đủ dài, vv… Vô vàn thứ phải tính đến.
Đây chính là lúc lập trình hướng đối tượng thể hiện ra mặt mạnh của nó. Lập trình viên giấu những phức tạp bên trong chiếc hộp đối tượng string
và chỉ bày ra trước mắt chúng ta những chiếc nút bấm.
Như các bạn đã biết, tạo ra 1 đối tượng không khác mấy với tạo ra các biến truyền thống như int
hay double
.
#include <iostream> #include <string> using namespace std; int main(){ string chuoiKyTu; //Tao ra doi tuong ‘chuoiKyTu’ kieu string return 0; }
Chắc là các bạn vẫn chưa quên là để có thể sử dụng các đối tượng string
, cần thêm gói thư viên string như tôi đã làm ở dòng thứ 2 trong đoạn mã.
Bây giờ, hãy chú ý đến dòng lệnh dùng để tạo ra đối tượng.
? Vậy cách để tạo ra 1 đối tượng giống với cách khai báo 1 biến ?
Thật ra có nhiều phương thức để tạo ra 1 đối tượng. Cách mà chúng ta vừa nhìn thấy ở trên chỉ là cách đơn giản nhất. Và đúng thế, nó y chang như cách để tạo ra 1 biến.
? Vậy làm thế nào để phân biệt 1 đối tượng với 1 biến bình thường ?
Rất tiếc là trong đoạn mã thì chúng hoàn toàn giống nhau, không thể nào phân biệt được. Thế nhưng nếu các bạn tuân thủ theo những quy tắc đặt tên mà chúng ta thống nhất với nhau thì cũng có thể dễ dàng tránh được nhầm lẫn. Quy tắc sau đây được tất cả các lập trình viên thống nhất sử dụng :
? Thế tại sao kiểu string là 1 kiểu đối tượng nhưng lại không bắt đầu bằng chữ in hoa ?
Đơn giản là quy tắc nêu trên là không bắt buộc và những người tạo nên kiểu string
thì không tuân theo quy tắc này. Tuy vậy, đa phần những lập trình viên hiện nay đều bắt đầu tên đối tượng của họ bằng 1 chữ cái in hoa.
Thêm vào đấy, các bạn sẽ nhanh chóng nhận ra rằng khi đã quen thuộc với các đối tượng thì nhu cầu phân biệt chúng sẽ không còn nữa vì sẽ có những cách khác để nhận ra 1 đối tượng giữa các biến, ví dụ như cách chúng ta sử dụng các đối tượng.
Chúng ta có rất nhiều cách để khởi tạo đối tượng khi khai báo và gán cho chúng giá trị. Cách thông dụng nhất là sử dụng dấu ()
như chúng ta vẫn làm từ trước đến giờ.
int main(){ string chuoiKyTu("Xin chao, Tan Binh !"); //Tao ra doi tuong 'chuoiKyTu' kieu string va khoi tao gia tri return 0; }
! Chúng ta cũng có thể sử dụng dấu =
giống như khi thao tác với các biến : string chuoiKyTu = “Xin chao, Tan Binh !”;
Chúng ta có thể hiển thị chuỗi ra màn hình như bình thường.
int main(){ string chuoiKyTu("Xin chao, Tan Binh !"); cout << chuoiKyTu << endl; return 0; }
Chúng ta cũng có thể thay đổi giá trị của chuỗi sau khi khai báo.
int main(){ string chuoiKyTu("Tan Binh!"); cout << chuoiKyTu << endl; chuoiKyTu = "Xin chao, Tan Binh !"; cout << chuoiKyTu << endl; return 0; }
! Để thay đổi giá trị của chuỗi sau khi khai báo, bắt buộc phải sử dụng dấu =.
Những kiến thức này các bạn đều đã biết cả rồi. Tôi nhắc lại chúng để các bạn thấy rằng nhờ sự thần kỳ của OOP, cuộc sống đã trở nên dễ dàng như thế nào. Ví dụ như vừa rồi, bạn chỉ dùng dấu =
để yêu cầu thay đổi giá trị của chuỗi. Trên thực tế thì rất nhiều xử lý đã diễn ra bên trong đối tượng. Đối tượng cần xác nhận xem mảng hiện thời có kích thước đủ chứa giá trị mới không. Trong trường hợp này do kích thước hiện tại không đủ, đối tượng phải tạo ra mảng mới đủ dài, xóa đi mảng ký tự cũ không còn cần thiết và lưu giá trị mới vào mảng mới.
Và người dùng chúng ta thì hoàn toàn chẳng quan tâm gì đến những xử lý rắc rối đó !
Lợi ích của OOP là ở đấy : người dùng thì chỉ cần phải biết đưa ra yêu cầu nào cho đối tượng để đạt được mục đích còn đối tượng đủ thông minh để thực hiện những xử lý cần thiết để thực hiện yêu cầu của chúng ta.
Bây giờ chúng ta muốn thực hiện ghép (concatenate) 2 chuỗi ký tự thành 1 chuỗi mới dài hơn. Trong lý thuyết thì xử lý này khá phức tạp vì cần phải nối 2 mảng lại với nhau. Thực tế thì chúng ta để cho OOP lo liệu những cơ chế lằng nhằng bên trong.
int main(){ string chuoiKyTu1("Xin chao"); string chuoiKyTu2 ("Tan Binh !"); string chuoiKyTu3; chuoiKyTu3 = chuoiKyTu1 + chuoiKyTu2; cout << chuoiKyTu3 << endl; return 0; }
Tôi nhận ra là thiếu dấu cách giữa 2 chuỗi. Không có gì khó khăn cả, hãy thay đổi câu lệnh.
chuoiKyTu3 = chuoiKyTu1 + " " + chuoiKyTu2;
Quá đơn giản cho người sử dụng trong khi đối tượng cần phải đánh vật để xử lý nối 2 mảng kiểu char
.
Không chỉ có thế, chúng ta còn có thể so sánh 2 chuỗi ký tự sử dụng dấu ==
hoặc !=
. Điều này rất hữu ích trong câu điều kiện.
int main(){ string chuoiKyTu1("Xin chao"); string chuoiKyTu2("Tan Binh"); if (chuoiKyTu1 == chuoiKyTu2){ cout << "2 chuoi giong nhau !" << endl; }else{ cout << "2 chuoi khac nhau !" << endl; } return 0; }
Bên trong đối tượng, 2 chuỗi được so sánh với nhau từng chữ 1 nhờ 1 vòng lặp. Sau khi thực hiện tất cả các phép tình toán xuôi ngược, đối tượng sẽ trả về kết quả của câu hỏi mà chúng ta quan tâm là 2 chuỗi có giống nhau không.
Những kiến thức trong phần tiếp theo của giáo trình này là các hướng dẫn cho lập trình viên để tạo ra các đối tượng và che giấu các xử lý phức tạp trong đó. Người dùng chỉ cần sử dụng mà không cần lo lắng cơ chế chức năng của từng quá trình cụ thể trong chuỗi xử lý.
Những xử lý mà string
có thể thực hiện không phải chỉ có thế. Kiểu string
còn cung cấp cho chúng ta rất nhiều tính năng khác cần để thực hiện những xử lý ta muốn.
Sau đây, chúng ta sẽ không nói về tất cả các tính năng của string mà chỉ điểm qua những tính năng quan trọng mà chúng ta cần dùng trong phần sau của giáo trình.
Tôi từng nói với các bạn là 1 đối tượng được tạo thành từ nhiều biến và hàm khác nhau. Trong thực tế, từ vựng sử dụng trong đối tượng có hơi khác so với những gì chúng ta đã biết. Các biến tồn tại bên trong đối tượng được gọi là thuộc tính (attribut) và các hàm thì được gọi là phương thức (method).
Mỗi phương thức (hàm) của đối tượng chính là 1 cái nút trên bề mặt chiếc hộp của chúng ta.
! 1 số người sử dụng thuật ngữ biến thành viên và hàm thành viên cho 2 khái niệm này.
Để sử dụng 1 phương thức của đối tượng, chúng ta sử dụng cú pháp mà các bạn đã có cơ hội thấy qua : tenDoiTuong.tenPhuongThuc()
.
Chúng ta viết tên đối tượng theo sau là dấu . rồi đến tên phương thức cần gọi.
! Trong lý thuyết thì chúng ta cũng có thể truy cập vào thuộc tính của đối tượng một cách tương tự như trên. Tuy nhiên, có 1 quy tắc rất quan trọng trong OOP, đấy là không được cho phép người dùng truy cập trực tiếp đến các thuộc tính mà chỉ được trực tiếp tới các phương thức của đối tượng. Chúng ta sẽ nói kỹ hơn về điểm này trong bài học sau.
Tôi đã nói ở bên trên rằng các bạn sẽ dễ dàng phân biệt 1 biến và 1 đối tượng. Lý do chính là ở cách sử dụng như vừa rồi. Cú pháp sử dụng dấu . là riêng chỉ có ở đối tượng nên các bạn không thể áp dụng lên các biến.
Phương thức size()
Các bạn đã được biết phương thức size()
cho phép chúng ta biến được độ dài chuỗi ký tự được lưu trong đối tượng string
.
Phương thức này không cần thông số và kết quả trả về là giá trị độ dài của chuỗi. Như cú pháp vừa được nêu bên trên, để gọi phương thức này, các bạn cần viết như sau.
chuoiKyTu.size();
Sau đây là ví dụ sử dụng phương thức này trong đoạn mã.
int main(){ string chuoiKyTu("Xin chao !"); cout << "Do dai cua chuoi : " << chuoiKyTu.size(); return 0; }
Phương thức erase()
Phương thức đơn giản này dùng để xóa nội dung của chuỗi.
int main(){ string chuoiKyTu("Xin chao !"); chuoiKyTu.erase(); cout << "Chuoi ky tu la : " << chuoiKyTu << endl; return 0; }
Trong chuỗi không còn chứa bất cứ ký tự nào.
! Lệnh bên trên tương đương với chuoiKyTu = "" ;
Phương thức substr()
1 phương thức cũng khá hữu dụng là substr()
. Phương thức này cho phép chúng ta cắt tách ra 1 chuỗi con từ chuỗi ban đầu.
Dưới đây là nguyên mẫu của phương thức này.
string substr( size_type index, size_type num = npos );
Giá trị trả về của phương thức này là 1 chuỗi con kết quả của phép cắt.
Phương thức này nhận vào 2 thông số, 1 là bắt buộc và cái còn lại thì không. Hãy cùng phân tích nguyên mẫu này.
index
cho phép chỉ ra vị trí mà bắt đầu từ đó chúng ta thực hiện phép cắt, đơn vị là số ký tự tính từ đầu chuỗi.num
chỉ định số ký tự mà chúng ta muốn cắt ra hay cũng là độ dài của chuỗi con. Giá trị mặc định của thông số này là npos
, có nghĩa là chuỗi con sẽ là phần còn lại của chuỗi ban đầu tính từ vị trí cắt.int main(){ string chuoiKyTu("Xin chao !"); cout << chuoiKyTu.substr(4) << endl; return 0; }
Chúng ta yêu cầu cắt từ vị trí thứ 4, là chữ « c » bởi vị các chỉ số được bắt đầu là 0. Vì chúng ta không cung cấp thông số thứ 2 nên mặc định là cắt lấy hết phần còn lại của chuỗi.
int main(){ string chuoiKyTu("Xin chao !"); cout << chuoiKyTu.substr(4, 4) << endl; return 0; }
Chúng ta chỉ muốn cắt lấy 4 ký tự nên kết quả hiện ra đương nhiên là « chao ».
Ngoài ra chúng ta cũng đã từng dùng 1 cách khác để truy cập tới 1 chữ cái của chuỗi là sử dụng dấu []
giống như thao tác mảng.
string chuoiKyTu("Xin chao !"); cout << chuoiKyTu[4] << endl; //Hien thu chu 'c'
Phương thức c_str()
Phương thức này có chút khác biệt nhưng đặc biệt hữu dụng trong 1 số trường hợp. Kết quả trả về cùa phương thức này là 1 con trỏ trỏ tới mảng của đối tượng string.
Trong C++ thì phương thức này không mang nhiều lợi ích sử dụng vì thường thường, chúng ta sẽ thích làm việc với 1 đối tượng hơn là với 1 mảng. Tuy nhiên trong 1 số trường hợp, 1 số hàm yêu cầu thông số truyền vào phải có dạng mảng thay vì là 1 đối tượng. Đấy là lúc chúng ta cần sử dụng c_str()
để lấy ra thành phần mảng chứa bên trong đối tượng. Ví dụ như khi chúng ta làm việc với tệp trong bài học trước.
string const tep("C:/Applis/lttb/files/score.txt "); ofstream luong(tep.c_str());
Dù vậy thì lợi ích của hàm này vẫn rất hạn chế.
Bài học giới thiệu OOP này đã cố đơn giản hóa các khái niệm và giải thích rõ ràng để không khiến các bạn thấy sợ hãi. Trên thực tế thì các bạn đã bắt đầu sử dụng các đối tượng từ đầu giáo trình mà không có bất cứ khó khăn nào. Đó chính là lợi ích của OOP : cung cấp 1 giao thức đơn giản cho các xử lý phức tạp.
Hy vọng là các bạn đã sẵn sàng để tự tạo ra các đối tượng của riêng mình !
string
. Kiểu dữ liệu này giúp chúng ta thao tác với các chuỗi mà không cần chú ý nhiều đến các xử lý bộ nhớ.