< Lập trình tân binh | 4.6. Tiến xa hơn với thư viện chuẩn

4.6. Tiến xa hơn với thư viện chuẩn

Ngay từ bài học đầu tiên về thư viện chuẩn, tôi đã thông báo trước là chủ đề này khá là rộng và nhiều khả năng là chúng ta không thể nói đến tất cả mọi thứ.

Đến bây giờ, chúng ta đã thảo luận về phần thư viện kế thừa từ C cũng như tập trung nói về thư viện lớp mẫu chuẩn và các luồng. Dịp này, chúng ta sẽ cùng tìm hiểu về những khả năng khác mà thư viện chuẩn có thể cung cấp cho chúng ta.

Bài học này đề cập tới 3 lĩnh vực mà chúng ta có thể sử dụng SL để cải thiện chương trình. Đầu tiên, hãy cùng nhìn lại về chuỗi kỹ tự và xem làm thể nào chúng ta có thể sử dụng các biến đếm với chúng. Tiếp đó, chúng ta tìm tới 1 người bạn cũ khác là mảng tĩnh, 1 người bạn mà chúng ta ít nhiều bỏ quên khi được nếm mật ngọt của việc sử dụng các lớp chứa. Và cuối cùng, để kết thúc, chúng ta sẽ cùng tìm hiểu về 1 mảng hoàn toàn mới : các công cụ chuyên biện cho tính toán khoa học. Các bạn cũng cần biết là C++ cũng được sử dụng rất nhiều bởi các nhà nghiên cứu khi muốn thực hiện các tính toán giả lập, mô phỏng tính toán.

Tiến xa hơn với chuỗi ký tự

Chuỗi ký tự ! Không thể nhớ được khái niệm này đã làm bạn với chúng ta từ lúc nào nữa ! Thế nhưng, có 1 sự thật là chúng ta còn lâu mới có thể hiểu hết được về chúng. Và đúng như một số bạn có thể đã đoán già đoán non được, vì chúng ta đang trong phần thảo luận về STL, vậy nên trong bài học này chúng ta sẽ thử kết hợp chúng với các biến đếm.

Các chuỗi ký tự có nhiều điểm tương đồng đến kỳ lạ với các vector. Chúng không chỉ giống nhau ở việc có thể sử dụng với phép toán [] như chúng ta đã thấy, mà còn ở nhiều điếm khác, chẳng hạn như việc chúng cũng cung cấp các phương thức begin()end() dùng để xác định điểm đầu và cuối của chuỗi.

string chuoiKyTu("Xin chào Tân binh !");    //1 chuoi ky tu
string::iterator it = chuoiKyTu.begin(); //Bien dem o dau chuoi

Toàn những kiến thức quen thuộc, không có gì để giải thích thêm.

Chúng ta từng nói qua về các hàm toupper()tolower() hồi mới nhắc đến thư viện chuẩn. Đây là các hàm cho phép biến chữ viết thường thành chữ viết hoa và ngược lại. Chúng ta có thể áp dụng chúng kết hợp với các thuật toán được tạo sẵn, trong trường hợp này là transform(). Chuỗi ký tự sẽ được duyệt toàn bộ và mỗi ký tự sẽ được thay đổi và cho ra kết quả mới.

#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
using namespace std;

class BienDoi {
public:
    char operator()(char c) const {
        return toupper(c);
    }
};
int main() {
    string chuoiKyTu("Xin chao Tan binh !");
    transform(chuoiKyTu.begin(),chuoiKyTu.end(),chuoiKyTu.begin(), BienDoi());
    cout << chuoiKyTu << endl;
    return 0;
}

Không có quá nhiều điều để nói về chủ đề này bới đến giờ thì các bạn cũng biết khá nhiều rồi. Tôi chỉ cho các bạn biết thêm là với các chuỗi ký tự, các hàm insert()erase() cũng tồn tại và có tác dụng tương tự như với các vector, nghĩa là dùng để chèn và xóa ký tự ở giữa chuỗi.

Bây giờ, hãy tới chỗ người bạn cũ mà chúng ta đã nói : mảng tĩnh.

Thao tác với mảng tĩnh

Bắt đầu với tóm tắt nhắc kiến thức nho nhỏ. 1 mảng tĩnh là 1 mảng mà kích thước của nó không thể thay đổi. Chúng được khai báo bằng cách sử dụng [] trong đó ghi kích thước mà chúng ta muốn. Ví dụ, nếu muốn tạo ra 1 mảng chứa 1 số nguyên :

int mang[10];

Và đương nhiên, chúng ta có thể tạo ra mảng để chứa bất cứ loại dữ liệu nào dù là double, string hay thậm chí là NhanVat (lớp do chúng ta tự tạo ra trong chương giáo trình về OOP). Yêu cầu duy nhất về kiểu dữ liệu ở trong mảng đó là nó phải sở hữu 1 phương thức khởi tạo mặc định.

Bởi vì các mảng không phải là các đối tượng như vector hay deque, vậy nên chúng chả có phương thức nào của riêng mình. Vậy nên có 1 số thứ  chúng ta không thể làm giống như đã làm với các kiểu dữ liệu khác, chẳng hạn như lấy ra kích thước của mảng. Trong trường hợp này, chúng ta bắt buộc phải luôn có 1 biến để lưu trữ kích thước của mảng.

Các biến đếm

Dù các mảng không phải là đối tượng nhưng chúng ta sẽ vẫn muốn có thể áp dụng các biến đếm với chúng bới đó là cách chúng ta có thể sử dụng đến các thuật toán sẵn có. Tuy vậy, chúng ta lại chẳng có biến đếm đặc biệt nào để sử dụng, chẳng lấy đâu ra begin() mà cũng chả có end().

Các bạn có còn nhớ chúng ta từng nói về việc các biến đếm là « phiên bản đối tượng » của con trỏ cũng như vector là « phiên bản đối tượng » của mảng. Vậy thì tại sao lại không thể sử dụng con trỏ trên mảng như cách chúng ta đã làm với biến đếm và vector ? Chẳng phải là con trỏ, cũng giống như biến đếm, có thể tiến lùi cũng như truy cập đến đối tượng nhờ phép toán * còn gì.

Trong những thao tác lúc trước, chúng ta cần 1 biến đếm trỏ tới đối tượng đầu tiên. Khi làm việc với mảng, yêu cầu này biến thành cần địa chỉ của ô nhớ đầu tiên.

int mang[10];  //Mang gom 10 so nguyen
int* it(&mang[0]); //Lay dia chi cua o nho dau tien

Phép toán & trả về địa chỉ của 1 biến, trong trường hợp của chúng ta là ô nhớ đầu tiên trong mảng, và chúng ta dùng kết quả này để khởi tạo con trỏ của chúng ta. Thế là có 1 biến đếm rồi.

Tôi thừa nhận là cách viết trên có chút hơi rườm rà. Thật tốt là chúng ta còn 1 cách khác để thực hiện khai báo này. Cần phải biết rằng tên của mảng, trong thực tế chính là 1 con trỏ trỏ tới điểm đầu của mảng, đúng ngay những gì chúng ta cần.

int mang[10];  //Mang gom 10 so nguyen
int* it(mang); //Con tro tren mang
Biến đếm kết thúc

Như mọi khi, chúng ta cần tổng cộng 2 biến đếm, biến đếm đầu tiên đã có, giờ cần tìm ra biến đếm kết thúc. Nếu đã nắm vững về con trỏ và mảng, chúng ta sẽ biết là từng ô trong mảng được trỏ tới bởi các con trỏ it, it+1, it+2, vv… Vậy nên, nếu chúng ta duyệt từ it tới trước it+10 thì chúng ta sẽ thực hiện duyệt hết mảng trong ví dụ trước. Đoạn mã dưới đây cho chúng ta 2 con trỏ mong muốn.

int const kichThuoc(10);
int mang[kichThuoc];
int* batDau(mang); //Con tro dau mang
int* ketThuc(mang+ kichThuoc); //Con tro cuoi mang

Vậy là chúng ta đã có công cụ tương ứng với begin()end(). Giờ là lúc áp dụng các thuật toán dựng sẵn yêu quý mà chúng ta đều đã thuần thục, ít nhất là tôi hy vọng thế. Thử ví dụ nhé :

#include <algorithm>
using namespace std;
int main() {
    int const kichThuoc(1000);
    double mang[kichThuoc];  
    //Dien gia tri vao mang
    double* batDau(mang);
    double* ketThuc(mang+kichThuoc);
    sort(batDau, ketThuc);           //Sap xep
    return 0;
}

! Chúng ta có thể truy cập trực tiếp tới bất cứ ô nào trong mảng nhờ kỹ thuật này do con trỏ của chúng ta mang các đặc điểm của biến đếm truy cập ngẫu nhiên.

Gì chứ mấy cái nhỏ nhặt này tôi cho rằng các bạn cũng tự đoán ra được :D.

Tính toán khoa học

Trong tất cả các chương trình tính toán khoa học, cái nhiều nhất hiển nhiên là… phép tính. Đây là các chương trình chuyên chơi đùa với các con số dưới mọi hình thức. Chúng ta đã biết đến kiểu int, double và đôi khi là phân số. Thế nhưng thế giới số học ngoài kia còn vô vàn các kiểu số khác, như số phức chẳng hạn.

Các số phức

Bởi vì đây là 1 kiểu số thuộc loại cơ bản nên thư viên chuẩn cũng hộ trợ nó cho chúng ta. Chúng ta cần phải thêm tệp tiêu đề complex để có thể làm việc với chúng.

Để khai báo số phức 2+3i chẳng hạn, chúng ta sẽ làm như sau :

#include <complex>
#include <iostream>
using namespace std;
int main() {
    complex<double> c(2,3);
    cout << c << endl;
    return 0;
}

Và kết quả chúng ta sẽ nhận được.

Khi thao tác với số phức, chúng ta cần chỉ ra kiểu số chúng ta muốn sử dụng cho phần thực và phần ảo. Trong hầu hết các trường hợp thì chúng ta đều dùng double nhưng nhắc nhở trên cũng không thừa.

Từ đây, chúng ta có thể sử dụng các phép toán thông thường như cộng trừ nhân chia lên các số này. Lai thêm 1 lần nữa việc ghi đè phép toán thể hiện ra ưu điểm của nó. Ngoài các phép toán cơ bản đó, chúng ta cũng có 1 vài phép toán nâng cao như phép lấy căn hay hàm lượng giác.

complex<double> a(1., 2.), b(-2, 4), c;
c = sqrt(a+b);
a = cos(c/b) + sin(b/c);

Đấy là tất cả những gì chúng ta cần biết để thực hiện các phép toán nâng cao. Ngoài ra các bạn cũng cần biết còn 1 số phép toán khác hữu dụng với số phức. Tất cả miêu tả của các phép toán này nằm trong tài liệu của thư viện, tùy các bạn tham khảo.

complex<double> a(3,4);
cout << norm(conj(a)) << endl; //Gia tri la 5

Tất cả các hàm này đều đặt tên theo các hàm thông dụng tương ứng trong toán học nên không khó để nhớ chúng. Chỉ cần các bạn ghi nhớ trong đầu là chúng tồn tại mà sử dụng.

Các valarray

1 thành phần khác chúng ta rất hay gặp trong các chương trình tính toán giả lập chính là các mảng số. Ngoài những loại mà chúng ta đã vô cùng quen thuộc, các mảng số còn tồn tại dưới 1 dạng đặc biệt thích hợp cho các tính toán, đó là valarray.

Loại mảng này có những hạn chế nhất định so với các vector do chúng không cho phép việc dễ dàng thêm vào mảng các đối tượng mới. Tuy nhiên đây thực sự không phải vấn đề lớn do trong các chương trình mà chúng ta đang nói đến, thao tác thêm đối tượng này không phải là quá thông dụng. Điểm mạnh của các valarray thể hiện ở việc chúng cho phép thực hiện các phép tính toán học trực tiếp trên toàn bộ mảng.

Ví dụ dưới đây tính tổng từng cặp giá trị xuất hiện ở vị trí tương ứng trong 2 mảng chỉ đơn giản với phép toán +.

#include<valarray>
using namespace std;
int main() {
    valarray<int> a(10, 5);  //5 so 10
    valarray<int> b(8, 5);   //5 so 8
    valarray<int> c = a + b;  //Moi thanh phan cua c deu bang 18
    return 0;
}

Chúng ta thậm chí không cần sử dụng đến vòng lặp khi thực hiện các phép tính cơ bản.

1 điểm cần hết sức chú ý đó là khi khởi tạo đối tượng valarray, các tham số truyền cho phương thức khởi tạo có chút ngược so với khi chúng ta khởi tạo vector : tham số đầu tiên để khởi tạo valarray là giá trị chúng ta muốn đưa vào các ô và tham số thứ 2 mới là số ô.

Tất cả các phép toán cơ bản đều được ghi đè để thao tác trên từng đối tượng riêng rẽ bị chứa trong mảng thay vì cả mảng. Ví dụ toán tử == sẽ thực hiện so sánh từng cặp số trong 2 mảng mà trả về kết quả là 1 mảng các giá trị boolean.

Cuối cùng, chúng ta cũng có thể sử dụng phương thức apply() để áp dụng các đối tượng hàm lên từng thành phần trong mảng. Chúng ta cũng tiết kiệm được nhiều công sức nếu biết sử dụng các thuật toán dựng sẵn kết hợp với các biến đếm. Dưới đây là đoạn mã ví dụ để tính hàm cos của từng số trong valarray.

#include<valarray>
#include<cmath>
using namespace std;
class Cosinus  { //Doi tuong ham de tinh cosinus
public:
    double operator()(double x) const {
        return cos(x);
    }
};

int main() {
    valarray<double> a(10);  //10 o chua
    //Dien gia tri vao tung o ...
    a.apply(Cosinus);
    //Moi o gio se chua gia tri cua ham cos ap dung len gia tri cu
    return 0;
}

Thêm 1 lần nữa, đừng ngại tham khảo tài liệu hướng dẫn để phát hiện ra những điều lý thú về các mảng.

Thư viện chuẩn không cung cấp tính năng tính toán ma trận dù đây cũng thuộc số các phép tính thường gặp. Trong trường hợp cần thực hiện nó, chúng ta đành phải nhờ đến các thư viện ngoài như lapack, MTL hay blas.

Tóm tắt bài học :

  • Các chuỗi ký tự string cũng cung cấp các biến đếm. Vậy nên chúng ta cũng có thể áp dụng các thuật toán dựng sẵn lên chúng.
  • Các mảng tĩnh không cung cấp biến đếm nhưng có thể sử dụng con trỏ để thay thế.
  • Thư viện chuẩn cung cấp 1 số công cụ tính toán khoa học như các thao tác với số phức và các mảng tối ưu hóa cho tính toán.