< Lập trình tân binh | 4.3. Biến đếm và đối tượng hàm

4.3. Biến đếm và đối tượng hàm

Trong bài học trước, chúng ta đã làm quen với một số lớp mẫu chứa nằm trong STL.

Thông qua đó, chúng ta đã học được cách thêm các đối tượng vào trong đối tượng chứa nhưng cũng chỉ dừng lại ở đó. Chúng ta vẫn chưa biết cách làm thế nào để duyệt 1 lượt tất cả các đối tượng và thực hiện các xử lý lên chúng. Để làm điều này, chúng ta sẽ cần tìm hiểu 2 khái niệm : biến đếm (iterator) và đối tượng hàm (function object).

Các biến đếm là các đối tượng gần giống như các con trỏ. Chúng cho phép chúng ta duyệt 1 lượt các đối tượng bị chứa trong tập hợp. Điều hay ho ở chúng là cách sử dụng hoàn toàn giống nhau dù chúng làm việc với loại đối tượng chứa nào dù là list, map hay vector. Lợi hại 1 cách thần kỳ nhỉ !

Các đối tượng hàm lại là 1 khái niệm khác. Chúng là các đối tượng nhưng có thể sử dụng như 1 hàm. Chúng ta có thể sử dụng chúng để áp dụng các thao tác chung lên tất cả các đối tượng chẳng hạn.

Biến đếm : các con trỏ siêu cấp

Hồi chúng ta còn bỡ ngỡ ở đầu giáo trình này, tôi đã từng đưa cho các bạn hình ảnh về những con trỏ tương tự như những mũi tên chỉ đường tới các ô nhớ trong máy tính. Hãy ghi nhớ kỹ hình ảnh này vì nó sẽ giúp đỡ chúng ta rất nhiều trong việc hiểu những gì tôi sắp trình bày tới đây.

Hãy tưởng tượng các lớp chứa như những chiếc túi chứa đối tượng, cũng tương tự như bộ nhớ chứa các biến vậy. Nhìn thấy sự giống nhau đó, những người tạo ra STL đã có ý tưởng tạo ra các con trỏ đặc biệt cho phép dịch chuyển và trỏ tới từng đối tượng bị chứa, tương tự như cách con trỏ trỏ đến ô nhớ vậy. Chắc hẳn các bạn cũng đoán được, chúng ta gọi các con trỏ đặc biệt đó là các biến đếm.

!Trong thực tế, các biến đếm thường là các đối tượng khá phức tạp chứ không chỉ là 1 con trỏ đơn thuần.

Ưu điểm của cách thức tiến hành này là chúng ta có thể sử dụng những công cụ mà chúng ta đã quen thuộc, lấy ví dụ như các phép toán ++-- mà chúng ta có thể thực hiện trên các con trỏ. Không chỉ thế, chúng ta cũng có thể truy cập vào đối tượng được trỏ đến bằng * nữa. Thật là gợi lại nhiều hồi ức xa xôi nhỉ !

Khai báo biến đếm

Mỗi lớp chứa lại có kiểu biến đếm của riêng mình nhưng thật tốt là tất cả chúng lại được khai báo theo cùng 1 cách giống nhau. Vẫn như mọi khi, chúng ta cần 1 kiểu dữ liệu và 1 cái tên. Tôi không thể giúp gì được các bạn trong việc chọn ra tên của biến nhưng bù lại, tôi sẽ cố hết sức để giúp các bạn có thể dễ dàng chọn ra kiểu dữ liệu cần sử dụng. Thật ra không có gì khó khăn cả, tên kiểu dữ liệu này là tên kiểu của đối tượng chứa, thêm ::iterator. Thử nhé :

#include <vector>
using namespace std;

vector<int> mang(5,4);     //Mang gom 5 so 4
vector<int>::iterator bienDem;     //Bien dem tren mang so nguyen

map<string, int>::iterator bienDem1; //Bien dem trong map string-int
deque<char>::iterator bienDem2; //Bien dem trong deque chua cac ky tu
list<double>::iterator bienDem3; //Bien dem trong danh sach so thuc

Dễ quá phải không ? Tôi tin là các bạn đã nắm được mấu chốt rồi đấy.

...và sử dụng chúng

Sau khi khai báo, đương nhiên chúng ta phải sử dụng chúng chứ không để làm cảnh được. Tất cả các lớp chứa đều có 1 phương thức là begin() cho phép hướng biến đếm của chúng ta đến đối tượng đầu tiên bị chứa trong nó. Tiếp đó, chúng ta sẽ duyệt từng phần tử bằng cách tiến dần về phía trước trong tập hợp nhờ phép toán ++. Và cuối cùng, chúng ta cần biết khi nào tất cả các phần tử bị chứa đã được duyệt hết và biến đếm cần phải dừng lại. Dù sao thì cũng không thể cứ thế nhảy ra khỏi danh sách được phải không ? Để biết khi nào cần dừng lại, chúng ta lại có phương thức end() của lớp chứa để sử dụng để biết khi nào chúng ta đã đi đến cuối.

!Thực ra, end() trả về kết quả là 1 đối tượng nằm sát nhưng bên ngoài danh sách chứa. Vậy nên khi duyệt danh sách, chúng ta phải tránh không xử lý đối tượng được trỏ tới bởi end().

Vậy là chúng ta sẽ thực hiện duyệt từ begin() tới end() và tất cả các đối tượng bị chứa đều sẽ được xét qua.

#include<deque>
#include <iostream>
using namespace std;

int main() {
    deque<int> d(5,6);        //Hang gom 5 so 6
    deque<int>::iterator it;  //Bien dem cho hang
 
    //Duyet hang
    for(it = d.begin(); it!=d.end(); ++it) {
        cout << *it << endl;    //Truy cap toi cac thanh vien bi chua
    }
    return 0;
}

!Các biến đếm không quá tương thích để sử dụng các phép toán so sánh. Vậy nên hệ quả là sử dụng phép toán != trở nên hiệu quả hơn nhiều so với phép toán <.

Đơn giản không ? Nếu các bạn đã có thể mở lòng mình để đón nhận con trỏ, vậy sẽ không khó khăn gì để chấp nhận các biến đếm. Thật ra thì chúng có vẻ vô dụng với deque hay vector cho chúng ta có thể dễ dàng truy cập tới các đối tượng bị chứa nhờ [] nhưng với map hay list thì chúng lại trở nên vô giá do đấy là cách thức duy nhất để duyệt tập hợp.

Phương thức chung của các biến đếm

Thậm chí cả vectordeque cũng có khi cần sử dụng đến các biến đếm. Đấy là trường hợp của các phương thức insert()erase() cho phép thêm hoặc xóa 1 phần tử trong tập hợp. Cho tới lúc này chúng ta cũng chưa thể thêm các đối tượng vào giữa chuỗi mà chỉ bồi hồi ở 2 đầu. Lý do là khi thêm vào giữa, chúng ta cần phải chỉ rõ là thêm vào vị trí nào. Và đấy là lúc chúng ta dùng đến biến đếm.

#include <vector>
#include <string>
#include <iostream>
using namespace std;

int main() {
    vector<string> mang;    //Mang cac tu
    mang.push_back("Tân"); //Them cac tu vao mang
    mang.push_back("binh");
    mang.insert(tab.begin(), "Xin chào"); //Them ‘Xin chao’ vao dau cau

    //Hien thi "Xin chao Tan binh"
    for(vector<string>::iterator it= mang.begin(); it!= mang.end(); it++) {
        cout << *it << " ";
    }
    mang.erase(mang.begin()); //Xoa tu dau tien

    //Hien thi "Tan binh"
    for(vector<string>::iterator it= mang.begin(); it!= mang.end(); it++) {
        cout << *it << " ";
    }
    return 0;
}

Trong ví dụ, chúng ta có thể thay bằng bất cứ loại lớp chứa nào khác và đoạn mã vẫn sẽ hoạt động tốt. Chỉ cần có biến đếm, chúng ta có thể dễ dàng xóa đi 1 đối tượng bị chứa nhờ erase() hoặc thêm vào sau nó 1 đối tượng khác nhờ insert().

!Cần ghi nhớ là các vector không phải kiểu dữ liệu thích hợp nhất nếu các bạn muốn thêm các đối tượng bị chứa khác vào giữa. Khi lưỡng lự cần sử dụng lớp chứa nào, đừng ngại mở lại bảo bối mà chúng ta đã có trong bài học trước.

Bắt đầu thích rồi đúng không :D Tất cả mới chỉ là bắt đầu thôi.

Các biến đếm khác nhau

Chúng ta sẽ tạm kết phần này với vài khái niệm mang tính kỹ thuật hơn 1 chút. Trong thực tế, chúng ta có tổng cộng 5 loại biến đếm. Khi chúng ta khai báo vector::iterator hay map::iterator, thực ra chúng ta đang khai báo sử dụng 1 trong số chúng. Trong số 5 loại này, chỉ có 2 loại được sử dụng để duyệt các tập hợp, đó là biến đếm 2 chiều (bidirectional iterator) và biến đếm truy cập ngẫu nhiên (random access iterators).

Biến đếm 2 chiều

Đây là loại đơn giản hơn trong 2 loại được nhắc đến ở trên. Loại biến đếm này là loại cho phép chúng ta có thể tiến lên hay lùi lại khi duyệt tập hợp, nghĩa là chúng ta có thể thực hiện phép toán ++ hay -- đều được. Quan trọng là chúng ta chỉ có thể di chuyển từng bước 1. Vậy nên nếu muốn đi tới đối tượng bị chứa thứ 6 thì chúng ta sẽ phải bắt đầu từ begin() và thực hiện 5 lần phép toán ++.

Đây là kiểu biến đếm được sử dụng trong list, set, map do chúng ta không thể truy cập trực tiếp vào giữa của những tập hợp này.

Biến đếm truy cập ngẫu nhiên

Như tên gọi, biến đếm này cho phép chúng ta truy cập tới đối tượng bất kỳ trong tập hợp, nghĩa là chúng ta có thể đi trực tiếp tới đối tượng ở giữa danh sách.

Về mặt kỹ thuật, các biến đếm này không chỉ hỗ trợ phép toán ++-- mà còn cả +- cho phép chúng ta di chuyển nhiều bước một lúc.

Hãy quan sát đoạn mã sử dụng vector dưới đây.

vector<int> mang(100,2);  //Mang chua 100 so 2
vector<int>::iterator it = mang.begin() + 7; //Bien dem tren doi tuong thu 8

Loại biến đếm này được sử dụng trên vector và cả deque.

Cơ chế hoạt động thực của các biến đếm này khá là phức tạp, vậy nên tôi sẽ không trình bày quá nhiều do chúng ta cũng không cần sử dụng đến chúng trong phần sau. Cần ghi nhớ là có 1 số loại biến đếm thì có nhiều hạn chế hơn so với các loại khác. Ví dụ sắp tới chúng ta sẽ bắt gặp 1 số giải thuật chỉ có thể được thực hiện với các biến đếm truy cập ngẫu nhiên.

Quyền năng của listmap

Đến tận bây giờ, tôi vẫn chưa giới thiệu về list với các bạn. Đây là 1 lớp chứa hơi có chút khác biệt so với những gì mà chúng ta đã biết. Các đối tượng bị chứa trong nó không được xếp cạnh nhau ở bên trong bộ nhớ. Mỗi ô chứa đối tượng sẽ chứa 1 giá trị chính là đối tượng và 1 con trỏ trỏ tới đối tượng tiếp theo trong danh sách.

Ưu điếm của kiểu cấu trúc này là chúng ta có thể dễ dàng thêm vào 1 phần tử vào giữa danh sách mà không cần phải dịch chuyển tất cả các phần tử khác. Đương nhiên là ưu điểm cũng đi kèm với hạn chế, đó là chúng ta không thể truy cập trực tiếp tới các đối tượng bị chứa nằm ở giữa bởi sự thật là chúng ta không biết là nó nằm ở đâu trong bộ nhớ. Vậy nên để tới các đối tượng không ở đầu, chúng ta sẽ phải tiến từng bước một.

Công việc duyệt lớp chứa này vì thế trở nên vô cùng phù hợp cho các biến đếm. Thêm nữa, chúng ta cũng không có [] để sử dụng nên đây trở thành lựa chọn duy nhất.

Lần nữa chúng ta lại cảm thấy may mắn là biến đếm có cách sử dụng giống nhau với tất cả các loại dữ liệu nên chúng ta không cần quá hiểu cấu trúc của list để có thể sử dụng nó.

#include <list>
#include <iostream>
using namespace std;

int main() {
    list<int> danhSach;       //Danh sach so nguyen
    danhSach.push_back(5);    //Them sot hu 1 vao danh sach
    danhSach.push_back(8);    //Them so thu 2
    danhSach.push_back(7);    //Them tiep so thu 3

    //Duyet danh sach
    for(list<int>::iterator it = danhSach.begin(); it!= danhSach.end(); it++) {
        cout << *it << endl;
    }
    return 0;
}

Tuyệt quá phải không ?

Tương tự với các map

Cấu trúc của map thậm chí còn phức tạp hơn list. Chúng ta đang nhắc tới cấu trúc dạng cây nhị phân. Và tôi đảm bảo với các bạn là việc duyệt các thành phần của cấu trúc loại này rất dễ trở thành những thách thức khó khăn. Thật may là với ++-- thì chúng ta sẽ không phải quan tâm đến những sự phức tạp che dấu đằng sau đó.

Có một chút khác biệt với những gì chúng ta vừa thảo luận lúc trước với danh sách dạng chuỗi. Lý do là các nhóm liên kết được tạo từ cặp từ khóa – giá trị nhưng biến đếm lại chỉ có thể trỏ tới 1 đối tượng mỗi lúc. Chắc chắn sẽ có 1 hậu quả nào đấy nhưng đừng lo, nó không quá nghiêm trọng như các bạn nghĩ.

Trong thực tế, các biến đếm trỏ tới đối tượng kiểu pair, nghĩa là đối tượng chứa 2 thuộc tính firstsecond với quyền truy cập public. pair được định nghĩa trong tệp tiêu đề utility nhưng chúng ta khá ít khi phải sử dụng tệp này trực tiếp do hầu hết các gói khác đều bao gồm tệp này để sử dụng. Thê nhưng thử tạo 1 đối tượng kiểu này thì cũng chả mất gì.

#include <utility>
#include <iostream>
using namespace std;
int main() {
    pair<int, double> p(2, 3.14);    //Cap gia tri gom 1 so nguyen va 1 so thuc
    cout << "Cặp giá trị này là (" << p.first << ", " << p.second << ")" << endl;

    return 0;
}

Vậy đó, chúng ta chả thể làm thêm gì khác với kiểu dữ liệu này. Nó đơn giản là được dùng để chứa 1 cặp giá trị.

!2 thuộc tính đều có quyền truy cập public. Chắc hẳn là các bạn sẽ thấy kỳ lạ vì tôi luôn nhắc nhở việc phải khai báo các thuộc tính lớp với quyền truy cập private. pair thì khác, lớp này chỉ đơn thuần là để chứa cặp giá trị và không có thêm bất cứ thuộc tính hay phương thức nào khác. Đây là 1 công cụ quá cơ bản và đơn giản đến độ người ta không muốn rắc rối nó với các phương thức get()set(). Chính vì vậy mà thuộc tinh của nó đều có quyền truy cập public.

Vậy là trong map, các đối tượng thực tế bị chứa dưới dạng pair, trong đó thuộc tính first là từ khóa và second là giá trị.

Ở bài trước, chúng ta đã biết là map sắp xếp những đối tượng mà nó chứa theo thứ tự từ khóa. Hãy cùng kiểm nghiệm đặc tính này.

#include <iostream>
#include <string>
#include <map>
using namespace std;

int main() {
    map<string, double> canNang; //Bang chua gia tri can nang cua cac loai dong vat  
    
    //Them vao can nang cua 1 so loai vat
    canNang["voi"] = 10000;
    canNang["chuot"] = 0.05;
    canNang["meo"] = 3;
    canNang["ho"] = 200;

    //Va chung ta duyet mang
    for(map<string, double>::iterator it= canNang.begin(); it!= canNang.end(); it++) {
        cout << it->first << " nặng " << it->second << " kg." << endl;
    }
    return 0;
}

Vậy là chúng ta thấy các loài động vật đã được sắp xếp theo tên chúng chứ không phải thứ tự được đưa vào mảng.

!Mảng sử dụng phép toán < của lớp string để sắp xếp các đối tượng bị chứa. Trong phần tiếp, chúng ta sẽ xem làm sao để thay đổi thao tác mặc định này.

Các biến đếm cũng rất hữu dụng khi chúng ta muốn tìm kiếm 1 đối tượng trong nhóm liên kết. Toán tử [] chỉ cho phép chúng ta truy cập tới 1 đối tượng bị chứa. Nhưng bản thân nó tồn tại 1 vấn đề, đó là nếu đối tượng đó không tồn tại thì [] sẽ tự động tạo ra nó. Vậy nên chúng ta không thể dùng nó để biết xem đối tượng đó có đang bị chứa trong nhóm không.

Để giải quyết vấn đề này, chúng ta có phương thức find() của map. Kết quả trả về của nó là con trỏ trên đối tượng cần tìm kiếm hoặc end() nếu đối tượng này không tồn tại. Vậy nên việc tìm kiếm trở nên vô cùng đơn giản.

#include <iostream>
#include <string>
#include <map>
using namespace std;

int main() {
    map<string, double> canNang; //Bang chua gia tri can nang cua cac loai dong vat  

    //Them vao can nang cua 1 so loai vat
    canNang["voi"] = 10000;
    canNang["chuot"] = 0.05;
    canNang["meo"] = 3;
    canNang["ho"] = 200;
    map<string, double>::iterator ketQua = canNang.find("gau");
    if(ketQua == canNang.end()) {
        cout << "Không tìm thấy cân nặng của gấu." << endl;
    } else {
        cout << "Gấu nặng " << ketQua->second << " kg." << endl;
    }   
    return 0;
}

Mỹ mãn ngoài mong đợi !

Đối tượng hàm : khi các hàm trở thành đối tượng

Nếu các bạn theo học công nghệ thông tin trong chương trình đại học, người ta sẽ bảo với các bạn là biến đếm là khái niệm trừu tượng hóa của con trỏ còn đối tượng hàm là trừu tượng hóa của các hàm. Về cơ bản thì mọi thứ đúng như vậy và bài học của chúng ta có thể dừng lại ở đây. Tuy nhiên nếu tôi để các bạn lại tự xoay sở với đống bùi nhùi này thì chắc hẳn sẽ không ai trong chúng ta thấy thoải mái cả.

Quay trở lại với bài học, cái chúng ta muốn là bây giờ là áp dụng một thao tác xử lý nào đó lên tập hợp, ví dụ lấy 1 mảng ký tự và biến tất cả chúng thành ký tự in hoa chẳng hạn. Hoặc chúng ta cũng có thể lấy ra mảng số nguyên và nhân đôi tất cả các thành viên là số lẻ trong đó. Nói tóm lại, chúng ta muốn áp dụng 1 hàm lên các thành viên của tập hợp. Vấn đề là chúng ta phải biến hàm thành 1 tham số của cho 1 phương thức của lớp chứa. Và điều này thì chúng ta không biết làm vì đến giờ thì chúng ta chỉ biết truyền tham số là đối tượng chứ không phải hàm.

!Về mặt kỹ thuật thì điều này cũng không thật sự đúng. Tồn tại 1 khái niệm gọi là con trỏ hàm và chúng ta có thể sử dụng nó để giải quyết vấn đề này. Tuy nhiên, đối tượng hàm thì dễ sử dụng hơn và cũng cho phép chúng ta làm nhiều thứ hơn.

Các đối tượng hàm là các đối tượng mà phép toán () của chúng được nạp chồng. Chúng hoạt động như các hàm nhưng cho phép có thể được truyền như tham số cho phương thức hoặc hàm khác.

Tạo đối tượng hàm

Đối tượng hàm là 1 lớp có thể có các thuộc tính và phương thức của nó. Nhưng ngoài ra, nó bắt buộc phải có phép toán () thực hiện xử lý mà chúng ta muốn thực hiện.

Hãy lấy ví dụ đơn giản, đối tượng hàm mà chúng ta tạo ra để thực hiện phép cộng 2 số nguyên.

class Cong {
public:   
    int operator()(int a, int b) { // Nap chong phep toan ()
        return a+b;
    }
};

Lớp này không có thuộc tính nào và chỉ có 1 phương thức duy nhất là phương thức nạp chồng phép toán (). Do nó không có gì đặc biệt nên phương thức khởi tạo mặc định tạo bởi trình biên dịch hoàn toàn đủ cho chúng ta.

!Cú pháp nạp chồng phép toán không còn gì lạ lẫm với chúng ta nữa, bao gồm từ khóa operator kết hợp với phép toán chúng ta muốn nạp chồng. Điểm đặc biệt của phép toán lần này là phép () cho phép chúng ta truyền số lượng tham số không hạn định, khác với các phép toán khác chỉ nhận 1 lượng tham số nhất định.

Thế là chúng ta có thể dùng đối tượng hàm này để làm tính cộng.

#include <iostream>
using namespace std;
int main() {
    Cong doiTuongHam;
    int a(2), b(3);
    cout << a << " + " << b << " = " << doiTuongHam(a,b) << endl; //Su dung doi tuong nhu ham so
    return 0;
}

Đương nhiên là chúng ta được quyền làm tất cả mọi thứ chứ không phải chỉ là phép cộng trong đối tượng hàm, ví dụ nhảm nhí như thực hiện cộng 5 cho tất cả các tham số chẵn.

class XuLyNhamNhi{
public:   
    int operator()(int a) {
        if(a%2 == 0) {
            return a+5;
        } else {
            return a;
        }
    }
};
Đối tượng hàm linh động

Các đối tượng hàm trước hết vẫn là các đối tượng. Vậy nên chúng có thể có các thuộc tính như bất cứ đối tượng nào khác. Việc này giống như thể chúng ta có 1 hàm với 1 bộ nhớ nội tại của nó vậy. Nhờ điều này, hàm này có thể thực hiện các thao tác khác nhau mỗi lần được gọi sử dụng.Tôi cho rằng 1 ví dụ thì sẽ dễ hiểu hơn với chúng ta.

class CungCapGiaTri{
public:
    CungCapGiaTri(int i):m_giaTri(i) {}
    int operator()(){
        m_giaTri++;
        return m_giaTri;
    }

private:
    int m_giaTri;
};

Điều đầu tiên cần chú ý là đối tượng hàm của chúng ta có 1 phương thức khởi tạo. Nhiệm vụ của phương thức khởi tạo này đơn giản là khởi tạo giá trị cung cấp ban đầu nằm trong thuộc tính m_giaTri. Phép toán () đơn giản là trả về giá trị của thuộc tính này nhưng trước đó cần chú ý là nó đã tăng giá trị của thuộc tính lên 1 đơn vị. Vậy là hàm của chúng ta có thể trả về giá trị khác nhau mỗi lần được gọi!

Chúng ta có thể dùng hàm này để điền giá trị cho 1 mảng chẳng hạn.

int main() {
    vector<int> mang(100,0); //Mang chu 100 so 0
    CungCapGiaTri f(0);
    for(vector<int>::iterator it= mang.begin(); it!= mang.end(); it++) {
        *it = f(); //Goi ham de cung cap gia tri cho tung o cua mang
    }
    return 0;
}

Bên trên chỉ là 1 ví dụ đơn giản để minh họa nguyên lý của khái niệm này. Trong thực tế, đối tượng hàm của chúng ta hoàn toàn có thể chứa các thuộc tính và xử lý phức tạp hơn nhiều. Tóm lại, bất cứ những gì chúng ta có thể làm trên đối tượng đều có thể áp dụng trên các đối tượng hàm này.

!Ai đã từng làm việc với C thì chắc sẽ nghĩ đến từ khóa static cũng cho phép thực hiện xử lý tương tự với các hàm bình thường. Các đối tượng hàm chính là khái niệm tương đương của nó trong C++.

Các predicat

Trong toán học, predicate (dịch nôm na là phép thử) dùng để chỉ các hàm số mà kết quả trả về là giá trị đúng hoặc sai. Trong trường hợp của chúng ta cũng tương tự như thế, đây là các đối tượng hàm đặc biệt chỉ nhận 1 tham số duy nhất và trả về kết quả là 1 giá trị boolean. Chúng được dùng để kiểm tra 1 đặc tính nào đó của đối tượng được truyền làm tham số. Nói dễ hiểu, chúng thường được dùng để trả lời những câu hỏi kiểu như :

  • Số này lớn hơn 10 ?
  • Từ này có nguyên âm ?
  • Con mèo của Schrodinger còn sống hay chết ?

Các hàm này trở nên rất hữu dụng trong phần sau khi chúng ta sẽ thử xóa tất cả các đối tượng mang theo 1 đặc tính nào đó. Trước lúc đó, hãy cùng ngó qua 1 đoạn mã nguồn của hàm predicat kiểm tra xem trong từ có chữ ‘a’ không.

class PhepThu {
public:
    bool operator()(string const& chuoiKyTu) const {
        for(int i(0); i<chuoiKyTu.size(); ++i) {
            if((chuoiKyTu[i] == 'a') {  //Kiem tra tung ky tu          
                return true;  //Tra ve dung khi tim thay chu 'a'
            }
        }
        return false;   //Neu di toi cuoi tuc la khong tim thay chu 'a'
    }
};

!Rồi chúng ta sẽ biết cách làm sao để viết đoạn mã trên 1 cách ngắn gọn hơn.

Cuối cùng, hãy kết thúc phần này bằng 1 số đối tượng hàm được định nghĩa sẵn trong STL. Thật là tin tốt cho bọn lười biếng chúng ta.

Các hàm đối tượng dựng sẵn

Có 1 số quả sung đã rụng sẵn mà chúng ta chỉ cần nhặt và bỏ vào mồm là xong. Tất cả chúng được gói lại trong tệp tiêu đề functional. Ở đây, tôi sẽ không liệt kê hết chúng ra để tạo cho các bạn cơ hội có thể bắt đầu tập nghía qua tài liệu của thư viện chuẩn.

Tuy nhiên, chúng ta vẫn sẽ điểm qua 1 ví dụ, chính là hàm cho phép thực hiện phép cộng mà chúng ta nói đến lúc trước. Trong STL, chúng ta có đối tượng hàm plus thực hiện xử lý tương tự.

#include <iostream>
#include <functional>    //Dung quen!
using namespace std;
int main() {
    plus<int> doiTuongHam;    //Khai bao doi tuong ham thuc hien phep cong
    int a(2), b(3);
    cout << a << " + " << b << " = " << doiTuongHam(a,b) << endl; //Su dung nhu ham thong thuong
    return 0;
}
Kết hợp 2 khái niệm

Các đối tượng hàm cũng là 1 phần linh hồn quan trọng của STL. Chúng rất hữu dụng nếu kết hợp với các giải thuật mà chúng ta sẽ đề cập tới trong bài sau. Tạm thời, hãy cứ bàn về cách làm thế nào chúng giúp chúng ta sắp xếp thứ tự trong map theo ý muốn đã.

Thay đổi phản ứng của map

Trong thực tế, phương thức khởi tạo của map nhận vào 1 tham số, đó là đối tượng hàm cho phép so sánh các từ khóa của các đối tượng bị chứa. Nếu chúng ta không truyền tham số nào thì mặc định, phép toán < sẽ được sử dụng. Chúng ta sẽ thử thay đổi sang 1 xử lý tùy chọn. Xem nào, thay vì sử dụng phép so sánh giữa các chuỗi thông thường, sao không thực hiện so sánh giữa độ dài của chúng nhỉ.

#include <string>
using namespace std;
class SoSanhChieuDai {
public:
    bool operator()(const string& a, const string& b) {
        return a.length() < b.length();
    }
};

Mọi người ít nhiều cũng có đoạn mã tương tự thế chứ. Giờ chỉ còn việc yêu cầu map sử dụng đối tượng hàm mà chúng ta vừa tạo ra là được.

int main() {
  //Mang chua khoi luong cua cac loai dong vat dua theo ten
  map<string, double, SoSanhChieuDai> canNang;  //Su dung ham vua tao de so sanh tu khoa

  //Them cac doi tuong vao mang
  poids["chuot"] = 0.05;
  poids["ho"] = 200;
  poids["meo"] = 3;
  poids["ca voi"] = 10000;

  //Hien ten dong vat va can nang tuong ung
  for(map<string, double>::iterator it=canNang.begin(); it!=canNang.end(); it++) {
      cout << it->first << " nặng " << it->second << " kg." << endl;
  }
  return 0;
}

Và kết quả nhận được :

Các loài vật đã được sắp xếp theo độ dài của tên chúng.

Tóm tắt về các lớp chứa thông dụng

Trong bài học tới, có lẽ chúng ta sẽ động chạm nhiều đến các loại lớp chứa. Vậy nên xin mạn phép được nhắc lại 1 chút về đặc trưng của các lớp này để chúng ta cùng nắm được.

vector : ví dụ vector<int>

  • Các đối tượng bị chứa được xếp cạnh nhau
  • Tối ưu xử lý khi thêm đối tượng vào cuối danh sách
  • Đối tượng bị chứa bị đánh dấu bới các chỉ số nguyên

deque : ví dụ deque<int>

  • Các đối tượng bị chứa được xếp cạnh nhau
  • Tối ưu xử lý khi thêm đối tượng vào đầu hoặc cuối danh sách
  • Đối tượng bị chứa bị đánh dấu bới các chỉ số nguyên

list : ví dụ list<int>

  • Các đối tượng bị chứa được ngẫu nhiên trong bộ nhớ
  • Chỉ có thể duyệt nhờ biến đếm
  • Tối ưu xử lý cho việc thêm bớt đối tượng ở giữa danh sách

map : ví dụ map<string,int>

  • Đối tượng bị chứa được đánh dấu bằng chỉ số kiểu tùy chọn
  • Đối tượng bị chứa bị sắp xếp theo giá trị từ khóa
  • Chỉ có thể duyệt nhờ biến đếm

set : set<int>

  • Đối tượng bị chứa được sắp xếp.
  • Chỉ có thể duyệt nhờ biến đếm
Tóm tắt bài học :
  • Các biến đếm tương tự như các con trỏ nhưng bị giới hạn bên trong lớp chứa
  • Chúng ta sử dụng ++-- để di chuyển trong tập hợp và * để truy cập vào đối tượng bị chứa.
  • Các đối tượng hàm là các lớp có phương thức nạp chồng cho phép toán (). Chúng ta sử dụng chúng như các hàm thông thường.
  • STL sử dụng rất nhiều đối tượng hàm để thực hiện thay đổi xử lý mặc định của các lớp chứa.