Bài giảng Lập trình hướng đối tượng - Template
Giới thiệu
Lập trình tổng quát (generic programming)
Lập trình tổng quát trong C
C++ template
Khuôn mẫu hàm
Khuôn mẫu lớp
Các tham số template khác
Template sử dụng template
trong khai báo/định nghĩa hàm swap() không phải thay thế text đơn giản và cũng không được thực hiện bởi trình tiền xử lý Việc chuyển phiên bản mẫu của swap() thành các cài đặt cụ thể cho int và float được thực hiện bởi trình biên dịch Khuôn mẫu hàm Hãy xem xét hoạt động của trình biên dịch khi gặp lời gọi swap() thứ nhất (với hai tham số int) Trước hết, trình biên dịch tìm xem có một hàm swap() được khai báo với 2 tham số kiểu int hay không Nó không tìm thấy một hàm thích hợp, nhưng tìm thấy một template có thể dùng được Tiếp theo, nó xem xét khai báo của template swap() để xem có thể khớp được với lời gọi hàm hay không Lời gọi hàm cung cấp hai tham số thuộc cùng một kiểu (int) Trình biên dịch thấy template chỉ ra hai tham số thuộc cùng kiểu T, nên nó kết luận rằng T phải là kiểu int Do đó, trình biên dịch kết luận rằng template khớp với lời gọi hàm Khuôn mẫu hàm Khi đã xác định được template khớp với lời gọi hàm, trình biên dịch kiểm tra xem đã có một phiên bản của swap() với hai tham số kiểu int được sinh ra từ template hay chưa Nếu đã có, lời gọi được liên kết (bind) với phiên bản đã được sinh (lưu ý: khái niệm liên kết này giống với khái niệm ta đã nói đến trong đa hình tĩnh) Nếu không, trình biên dịch sẽ sinh một cài đặt của swap() lấy hai tham số kiểu int (thực ra là viết đoạn mã mà ta sẽ tạo nếu ta tự mình viết) – và liên kết lời gọi hàm với phiên bản vừa sinh. Khuôn mẫu hàm Vậy, đến cuối quy trình biên dịch đoạn mã trong ví dụ, sẽ có hai phiên bản của swap() được tạo (một cho hai tham số kiểu int, một cho hai tham số kiểu float) với các lời gọi hàm của ta được liên kết với phiên bản thích hợp Vậy, ta có thể đoán rằng có chi phí phụ về thời gian biên dịch đối với việc sử dụng template Ngoài ra còn có chi phí phụ về không gian liên quan đến mỗi cài đặt của swap() được tạo trong khi biên dịch Tuy nhiên, tính hiệu quả của các cài đặt đó cũng không khác với khi ta tự cài đặt chúng. Khuôn mẫu hàm Cần ghi nhớ rằng tuy trình biên dịch đã tạo các phiên bản của swap() cho các tham số int và float, không tồn tại các hàm swap(int,int) hay swap(float, float) Thay vào đó, có một hàm swap() được dùng để tạo hai hàm swap() và swap() Khi được dùng với một cấu trúc template, cặp ngoặc nhọn được dùng để chỉ rõ kiểu dữ liệu cần đến Thực tế, ta có thể sửa đoạn mã trước để gọi các hàm trên một cách tường minh: int x = 1, y = 2; float a = 1.1, b = 2.2; ... swap(x, y); // Invokes int version of Swap() swap(a, b); // Invokes float version of Swap() Khuôn mẫu lớp Tương tự với khuôn mẫu hàm với tham số thuộc các kiểu tuỳ ý, ta cũng có thể định nghĩa khuôn mẫu lớp (class template) sử dụng các thể hiện của một hoặc nhiều kiểu dữ liệu tuỳ ý Ta cũng có thể định nghĩa template cho struct và union Khai báo một khuôn mẫu lớp cũng tương tự với khuôn mẫu hàm Khuôn mẫu lớp Ví dụ, ta sẽ tạo một cấu trúc cặp đôi giữ một cặp giá trị thuộc kiểu tuỳ ý Khuôn mẫu lớp Để tạo các thể hiện của template Pair, ta phải dùng ký hiệu cặp ngoặc nhọn Khác với khuôn mẫu hàm khi ta có thể bỏ qua kiểu dữ liệu cho các tham số, đối với khuôn mẫu class/struct/union, chúng phải được cung cấp tường minh Pair p; // Not permitted Pair q; // Creates a pair of ints Pair r; // Creates a pair with an int and a float Tại sao đòi hỏi kiểu tường minh? Các lệnh trên làm gì? - cấp phát bộ nhớ cho đối tượng Nếu không biết các kiểu dữ liệu được sử dụng, trình biên dịch làm thế nào để biết cần đến bao nhiêu bộ nhớ? Khuôn mẫu lớp Cũng như khuôn mẫu hàm, không có struct Pair mà chỉ có các struct có tên Pair, Pair, Pair,… Quy trình tạo các phiên bản struct Pair từ khuôn mẫu cũng giống như đối với khuôn mẫu hàm Khi trình biên dịch lần đầu gặp khai báo dùng Pair, nó kiểm tra xem struct đó đã tồn tại chưa, nếu chưa, nó sinh một khai báo tương ứng. Đối với các khuôn mẫu cho class, trình biên dịch sẽ sinh cả các định nghĩa phương thức cần thiết để khớp với khai báo class. Khuôn mẫu lớp Một khi đã tạo được một thể hiện của một khuôn mẫu class/struct/union, ta có thể tương tác với nó như thể nó là thể hiện của một class/struct/union thông thường. Tiếp theo, ta sẽ tạo một template cho lớp Stack đã được mô tả trong các slice trước Pair q; Pair r; q.first = 5; q.second = 10; r.first = 15; r.second = 2.5; Khuôn mẫu lớp Khi thiết kế khuôn mẫu (cho lớp hoặc hàm), thông thường, ta nên tạo một phiên bản cụ thể trước, sau đó mới chuyển nó thành một template Ví dụ, ta sẽ bắt đầu bằng việc cài đặt hoàn chỉnh Stack cho số nguyên Điều đó cho phép phát hiện các vấn đề về khái niệm trước khi chuyển thành phiên bản cho sử dụng tổng quát khi đó, ta có thể test tương đối đầy đủ lớp Stack cho số nguyên để tìm các lỗi tổng quát mà không phải quan tâm đến các vấn đề liên quan đến template Stack cho số nguyên Khai báo và định nghĩa lớp Stack cho kiểu int Bắt đầu bằng một ngăn xếp đơn giản class Stack { public: Stack(); ~Stack(); void push(const int& i) throw (logic_error); void pop(int& i) throw (logic_error); bool isEmpty() const; bool isFull() const; private: static const int max = 10; int contents[max]; int current; }; Stack::Stack() { this->current = 0; } Stack::~Stack() {} void Stack::push(const int& i) throw(logic_error) { if (this->current max) { this->contents[this->current++] = i; } else { throw logic_error(“Stack is full.”); } } void Stack::pop(int& i) throw(logic_error) { if (this->current > 0) { i = this->contents[--this->current]; } else { throw logic_error(“Stack is empty.”); } } bool Stack::isEmpty() const { return (this->current == 0;) } bool Stack::isFull() const { return (this->current == this->max); } Template Stack Chuyển khai báo và định nghĩa trước thành một phiên bản tổng quát: template class Stack { public: Stack(); ~Stack(); void push(const T& i) throw (logic_error); void pop(T& i) throw (logic_error); bool isEmpty() const; bool isFull() const; private: static const int max = 10; T contents[max]; int current; }; template Stack::Stack() { this->current = 0; } template Stack::~Stack() {} template void Stack::push(const T& i) { if (this->current max) { this->contents[this->current++] = i; } else { throw logic_error(“Stack is full.”); } } template void Stack::pop(T& i) { if (this->current > 0) { i = this->contents[--this->current]; } else { throw logic_error(“Stack is empty.”); } } template bool Stack::isEmpty() const { return (this->current == 0;) } template bool Stack::isFull() const { return (this->current == this->max); } Template Stack Sau đó, ta có thể tạo và sử dụng các thể hiện của các lớp được định nghĩa bởi template của ta: int x = 5, y; char c = 'a', d; Stack s; Stack t; s.push(x); t.push(c); s.pop(y); t.pop(d); Các tham số khuôn mẫu khác Ta mới nói đến các lệnh template với tham số thuộc "kiểu" typename Tuy nhiên, còn có hai "kiểu" tham số khác Kiểu thực sự (ví dụ: int) Các template Các tham số khuôn mẫu khác Nhớ lại rằng trong cài đặt Stack, ta có một hằng max quy định số lượng tối đa các đối tượng mà ngăn xếp có thể chứa Như vậy, mỗi thể hiện sẽ có cùng kích thước đối với mọi kiểu của đối tượng được chứa Nếu ta không muốn đòi hỏi mọi Stack đều có kích thước tối đa như nhau? Ta có thể thêm một tham số vào lệnh template chỉ ra một số int (giá trị này sẽ được dùng để xác định giá trị cho max) Lưu ý: ta khai báo tham số int giống như trong các khai báo khác template // Specifies that one arbitrary type T and one int I // will be parameters in the following statement Các tham số khuôn mẫu khác Sửa khai báo và định nghĩa trước để sử dụng tham số mới: template class Stack { public: Stack(); ~Stack(); void push(const T& i) throw (logic_error); void pop(T& i) throw (logic_error); bool isEmpty() const; bool isFull() const; private: static const int max = I; T contents[max]; int current; }; template Stack::Stack() { this->current = 0; } template Stack::~Stack() {} template void Stack::push(const T& i) { if (this->current max) { this->contents[this->current++] = i; } else { throw logic_error(“Stack is full.”); } } ... Các tham số khuôn mẫu khác Các tham số khuôn mẫu khác Giờ ta có thể tạo các thể hiện của các lớp Stack với các kiểu dữ liệu và kích thước đa dạng Lưu ý rằng các lệnh trên tạo thể hiện của 3 lớp khác nhau Stack s; // Creates an instance of a Stack // class of ints with max = 5 Stack t; // Creates an instance of a Stack // class of ints with max = 10 Stack u; // Creates an instance of a Stack // class of chars with max = 5 Các tham số khuôn mẫu khác Các ràng buộc khi sử dụng các kiểu thực sự làm tham số cho lệnh template: Chỉ có thể dùng các kiểu số nguyên, con trỏ, hoặc tham chiếu Không được gán trị cho tham số hoặc lấy địa chỉ của tham số Các tham số khuôn mẫu khác Loại tham số thứ ba cho lệnh template chính là một template Ví dụ, xét thiết kế khuôn mẫu cho một lớp Map (ánh xạ) ánh xạ các khoá tới các giá trị Lớp này cần lưu các ánh xạ từ khoá tới giá trị, nhưng ta không muốn chỉ ra kiểu của các đối tượng được lưu trữ ngay từ đầu Ta sẽ tạo Map là một khuôn mẫu sao cho có thể sử dụng các kiểu khác nhau cho khoá và giá trị Tuy nhiên, ta cần chỉ ra lớp chứa (container) là một template, để nó có thể lưu trữ các khoá và giá trị là các kiểu tuỳ ý Các tham số khuôn mẫu khác Ta có thể khai báo lớp Map: template Container> class Map { ... private: Container keys; Container values; ... }; Sau đó có thể tạo các thể hiện của Map như sau: Lệnh trên tạo một thể hiện của lớp Map chứa các thành viên là một tập các string và một tập các int (giả sử còn có các đoạn mã thực hiện ánh xạ mỗi từ tới một số int biểu diễn số lần xuất hiện của từ đó) Ta đã dùng template Stack để làm container lưu trữ các thông tin trên Map wordcount; Các tham số khuôn mẫu khác Như vậy, khi trình biên dịch sinh các khai báo và định nghĩa thực sự cho các lớp Map, nó sẽ đọc các tham số mô tả các thành viên dữ liệu Khi đó, nó sẽ sử dụng khuôn mẫu Stack để sinh mã cho hai lớp Stack và Stack Đến đây, ta phải hiểu rõ tại sao container phải là một khuôn mẫu, nếu không, làm thế nào để có thể dùng nó để tạo các loại stack khác nhau?
File đính kèm:
- Bài giảng Lập trình hướng đối tượng - Template.ppt