bÀi 1. sƠ lƯỢc vỀ lẬp trÌnh hƯỚng ĐỐi tƯỢng – object...

72
1 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ORIENTED PROGRAMMING __ム少___ Tất cả các ngôn ngữ lập trình đều sinh ra để hỗ trợ một hoặc một số phong cách lập trình hay một mô hình lập trình nào đó (programming paradigm). Vì vậy trước khi bắt tay vào học ngôn ngữ ta nên tìm hiểu sơ lược về mô hình lập trình được ngôn ngữ hỗ trợ mà ta dđịnh viết chương trình theo mô hình đó. Cụ thể, nói “lập trình hướng đối tượng với C++” thì ta phải biết sơ sơ về hướng đối tượng trước khi “ngâm cứu” C++. Vì vậy bài đầu tiên này mình muốn dành để nói về lập trình hướng đối tượng là gì, và quan điểm của giới lập trình về nó như thế nào, tại sao nó lại là một mô hình tiên tiến và bạn sẽ không phải hối hận khi bỏ thời gian và công sức ra để học nó. Ngày xửa ngày xưa, khoảng ba chục năm về trước, quy mô các của các dự án phần mềm còn nhỏ, các lập trình viên gần như có thể viết ngay được chương trình mà không cần suy nghĩ nhiều (giả sử rằng không có lập trình viên nào bị thiểu năng về trí tuệ. Thời đó lập trình cấu trúc (structured programming) hay còn gọi lập trình thủ tục (procedural programing) là kỹ thuật lập trình chủ yếu. Tớ sẽ nói sơ qua một chút về kỹ thuật này (trong phạm vi hiểu biết). Theo quan điểm của lập trình cấu trúc, người ta xem chương trình là một “công việc lớn” cần phải xử lý. Để giải quyết “công việc lớn” này, người ta tìm cách chia thành các phần công việc nhỏ hơn và mỗi phần này sẽ được quẳng cho một hàm đảm nhiệm. Chương trình chính sẽ gọi đến mỗi hàm vào những thời điểm cần thiết. Trong mỗi hàm, nếu như phần công việc vẫn còn lớn, thì ta lại chia nhỏ tiếp cho tới khi vấn đề trở nên đủ đơn giản. Và dĩ nhiên để giải quyết những phần con đó ta cũng phải quẳng chúng cho các hàm tương ứng. Quá trình này được gọi là “làm mịn” hay “tinh chế từng bước” (stepwise refinement). Việc trao đổi dữ liệu giữa các hàm được thực hiện thông qua việc truyền đối số hoặc các biến, mảng toàn cục. Như vậy có thể coi chương trình là một tập hợp các hàm được thiết kế để xử lý các phần công việc được giao. Các ngôn ngữ lập trình hướng thủ tục thường gặp là C, Pascal, FORTRAN … và cả C++. Tuy nhiên C++ còn được thiết kế để hỗ trợ cả lập trình hướng đối tượng nữa. Một chương trình viết theo hướng cấu trúc sẽ tập trung vào quá trình xử lý. Nghĩa là mỗi câu lệnh chỉ dẫn cho máy tính làm một việc gì đó, kiểu như: nhận 2 số nguyên từ bàn phím, cộng chúng lại với nhau, rồi đem chia đôi, hiển thị kết quả lên màn hình. Một chương trình là một tập các chỉ dẫn. Lập trình cấu trúc tỏ ra khá hiệu quả khi quy mô chương trình còn nhỏ, nhưng khi quy mô chương trình lớn dần lên và phức tạp hơn thì nó bộc lộ nhiều khiếm khuyết. Có thể nêu ra một số vấn đề sau: 1. Trọng tâm vào “hành động” hơn là “dữ liệu”: thực tế dữ liệu là cái tối thượng mà chúng ta quan tâm. Mọi chương trình đều nhằm mục đích nhét dữ liệu vào input rồi chờ đợi kết quả ở output. Rõ ràng mục đích của ta là dữ liệu đầu ra, mặc kệ chương trình nó muốn xử lý cái gì thì xử lý, ta chỉ quan tâm đến kết quả đầu ra có đạt yêu cầu hay không. Tuy nhiên lập trình cấu trúc quá chú trọng đến việc thiết kế các hàm (hành động) mà xem nhẹ dữ liệu, đây là hạn chế thứ nhất. 2. Tính bảo mật của dữ liệu không cao: (nếu như không muốn nói là không có). Dliệu trong chương trình gần như là của chung, và có thể dễ dàng truy cập hay sửa đổi một cách vô tội vạ. Những hàm không phận sự cũng có thể tọc mạch vào vùng dữ liệu mà nó “chằng liên quan” và sửa đổi nó . Điều này làm chương trình rất dễ phát sinh lỗi đặc biệt là những “lỗi tinh vi” hoặc “lỗi logic”. Và khi có lỗi thì rất khó debug vì phạm vi khoanh vùng là rất rộng (vì ai cũng có thể tọc mạch vào dữ liệu nên không biết nghi cho thằng nào). Đây là hạn chế thứ hai.

Upload: others

Post on 05-Sep-2019

15 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

1 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ORIENTED PROGRAMMING

__ ム 少혱___

Tất cả các ngôn ngữ lập trình đều sinh ra để hỗ trợ một hoặc một số phong cách lập trình hay một mô hình lập trình nào đó (programming paradigm). Vì vậy trước khi bắt tay vào học ngôn ngữ ta nên tìm hiểu sơ lược về mô hình lập trình được ngôn ngữ hỗ trợ mà ta dự định viết chương trình theo mô hình đó. Cụ thể, nói “lập trình hướng đối tượng với C++” thì ta phải biết sơ sơ về hướng đối tượng trước khi “ngâm cứu” C++. Vì vậy bài đầu tiên này mình muốn dành để nói về lập trình hướng đối tượng là gì, và quan điểm của giới lập trình về nó như thế nào, tại sao nó lại là một mô hình tiên tiến và bạn sẽ không phải hối hận khi bỏ thời gian và công sức ra để học nó. Ngày xửa ngày xưa, khoảng ba chục năm về trước, quy mô các của các dự án phần mềm còn nhỏ, các lập trình viên gần như có thể viết ngay được chương trình mà không cần suy nghĩ nhiều (giả sử rằng không có lập trình viên nào bị thiểu năng về trí tuệ. Thời đó lập trình cấu trúc (structured programming) hay còn gọi lập trình thủ tục (procedural programing) là kỹ thuật lập trình chủ yếu. Tớ sẽ nói sơ qua một chút về kỹ thuật này (trong phạm vi hiểu biết). Theo quan điểm của lập trình cấu trúc, người ta xem chương trình là một “công việc lớn” cần phải xử lý. Để giải quyết “công việc lớn” này, người ta tìm cách chia thành các phần công việc nhỏ hơn và mỗi phần này sẽ được quẳng cho một hàm đảm nhiệm. Chương trình chính sẽ gọi đến mỗi hàm vào những thời điểm cần thiết. Trong mỗi hàm, nếu như phần công việc vẫn còn lớn, thì ta lại chia nhỏ tiếp cho tới khi vấn đề trở nên đủ đơn giản. Và dĩ nhiên để giải quyết những phần con đó ta cũng phải quẳng chúng cho các hàm tương ứng. Quá trình này được gọi là “làm mịn” hay “tinh chế từng bước” (stepwise refinement). Việc trao đổi dữ liệu giữa các hàm được thực hiện thông qua việc truyền đối số hoặc các biến, mảng toàn cục. Như vậy có thể coi chương trình là một tập hợp các hàm được thiết kế để xử lý các phần công việc được giao. Các ngôn ngữ lập trình hướng thủ tục thường gặp là C, Pascal, FORTRAN … và cả C++. Tuy nhiên C++ còn được thiết kế để hỗ trợ cả lập trình hướng đối tượng nữa. Một chương trình viết theo hướng cấu trúc sẽ tập trung vào quá trình xử lý. Nghĩa là mỗi câu lệnh chỉ dẫn cho máy tính làm một việc gì đó, kiểu như: nhận 2 số nguyên từ bàn phím, cộng chúng lại với nhau, rồi đem chia đôi, hiển thị kết quả lên màn hình. Một chương trình là một tập các chỉ dẫn. Lập trình cấu trúc tỏ ra khá hiệu quả khi quy mô chương trình còn nhỏ, nhưng khi quy mô chương trình lớn dần lên và phức tạp hơn thì nó bộc lộ nhiều khiếm khuyết. Có thể nêu ra một số vấn đề sau:

1. Trọng tâm vào “hành động” hơn là “dữ liệu”: thực tế dữ liệu là cái tối thượng mà chúng ta quan tâm. Mọi chương trình đều nhằm mục đích nhét dữ liệu vào input rồi chờ đợi kết quả ở output. Rõ ràng mục đích của ta là dữ liệu đầu ra, mặc kệ chương trình nó muốn xử lý cái gì thì xử lý, ta chỉ quan tâm đến kết quả đầu ra có đạt yêu cầu hay không. Tuy nhiên lập trình cấu trúc quá chú trọng đến việc thiết kế các hàm (hành động) mà xem nhẹ dữ liệu, đây là hạn chế thứ nhất.

2. Tính bảo mật của dữ liệu không cao: (nếu như không muốn nói là không có). Dữ liệu trong chương trình gần như là của chung, và có thể dễ dàng truy cập hay sửa đổi một cách vô tội vạ. Những hàm không phận sự cũng có thể tọc mạch vào vùng dữ liệu mà nó “chằng liên quan” và sửa đổi nó . Điều này làm chương trình rất dễ phát sinh lỗi đặc biệt là những “lỗi tinh vi” hoặc “lỗi logic”. Và khi có lỗi thì rất khó debug vì phạm vi khoanh vùng là rất rộng (vì ai cũng có thể tọc mạch vào dữ liệu nên không biết nghi cho thằng nào). Đây là hạn chế thứ hai.

Page 2: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

2 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

3. Tách rời dữ liệu với “hành động” liên quan: không phải tất cả các hàm được viết ra để dùng cho tất cả dữ liệu, và ngược lại. Mỗi nhóm dữ liệu chỉ sử dụng một nhóm các hàm “dành riêng cho chúng”. Trong lập trình, việc “đóng gói” dữ liệu và hàm liên quan được gọi là “mô-đun hóa” (modularization). Điều này có hai cái lợi. Thứ nhất, các hàm và dữ liệu được nhóm lại với nhau nên “gọn gàng” hơn và dễ kiểm soát hơn. Thứ hai, thông thường chỉ những hàm trong khối mới có thể truy nhập vào dữ liệu của khối. Do đó hạn chế sự tọc mạch từ bên ngoài, tính bảo mật dữ liệu cao hơn, hạn chế lỗi và phạm vi khoanh vùng lỗi sẽ được thu hẹp. Tuy nhiên, lập trình cấu trúc không làm được điều này. Đây là hạn chế thứ ba.

4. Phụ thuộc nặng nề vào cấu trúc dữ liệu và thuật toán: minh chứng cho điều này là câu nói nổi tiếng của bác Niklaus Wirth (creator of Pascal): Algorithms + Data Structures = Programs. Cũng xin nói thêm mô hình lập trình hướng cấu trúc được dựa trên mô hình toán học của Bohm và Guiseppe (nói thật là mình không biết hai bác này, theo đó, một chương trình máy tính đều có thể viết dựa trên ba cấu trúc là: tuần tự (sequence), lựa chọn hay rẽ nhánh (selection) và lặp (repetition). Vì vậy một chương trình được xem là một chuỗi các hành động liên tiếp để đi đến kết quả cuối cùng. Và việc thiết kế chương trình phụ thuộc nặng nề vào việc dùng giải thuật gì và tổ chức dữ liệu như thế nào. Điều này làm cho việc thiết kế là rất “không tự nhiên” vì nó làm cho quá trình thiết kế phụ thuộc vào cài đặt và khi quy mô chương trình lớn dần lên sẽ rất khó triển khai. Đồng thời khi có thay đổi về cấu trúc dữ liệu hoặc nâng cấp chương trình gần như ta phải viết lại hầu hết các hàm liên quan và sửa đổi lại thuật toán vì mỗi cấu trúc dữ liệu chỉ phù hợp với một số thuật toán nhất định. Đây là hạn chế thứ tư.

5. Không tận dụng được mã nguồn: mặc dù hàm là một phát minh quan trọng để tăng cường khả năng sử dụng lại mã nguồn, tuy nhiên trong lập trình cấu trúc điều này không triệt để. Ta vẫn phải viết lại những đoạn code hao hao giống nhau để thực hiện những công việc tương tự nhau. Ví dụ: trong C, hàm hàm int min(int x, int y) có nhiệm vụ tính toán và trả về min trong hai số nguyên được truyền vào, còn hàm float min(float x, float y) cũng làm nhiệm vụ tương tự nhưng là với số thực. Rõ ràng nội dung hai hàm này là giống nhau đến 99%, có khác thì chỉ khác mỗi kiểu int và float, thế nhưng trong C ta vẫn phải viết hai hàm khác nhau. Trong C++, với định hướng đối tượng ta có thể viết một hàm dùng để dùng cho mọi kiểu int, float, double. Ngoài ra còn nhiều điểm mạnh khác mà OOP mang lại để tận dụng tối đa khả năng sử dụng lại mã nguồn như tính kế thừa (inheritance), đa hình (polymorphism). Đây là hạn chế thứ năm của lập trình cấu trúc.

Nói chung mình chỉ mới bới ra được có thế thôi, ai biết thêm cái nào thì bổ sung nhé. Rõ ràng với nhiều hạn chế như vậy thì lập trình cấu trúc không phải là giải phải pháp tốt. Và những nỗ lực để vá những lỗ hổng này dẫn đến sự ra đời của một kỹ thuật lập trình mới lập trình hướng đối tượng (object oriented programming – OOP). Mình cũng nói sơ qua một chút về OOP. Khác với lập trình cấu trúc, OOP coi chương trình là tập hợp của các đối tượng có quan hệ nào đó với nhau. Mỗi đối tượng có dữ liệu và phương thức của riêng mình. Ví dụ một đối tượng Human sẽ có các dữ liệu như: tên, ngày sinh, tuổi, số chứng minh nhân dân, nghề nghiệp, … blah blah … và được đóng gói cùng các phương thức đi kèm ví dụ phương thức set_name() sẽ cho phép nhập tên , get_name() sẽ cho phép lấy tên của đối tượng, tương tự ta cũng cho các phương thức như set_ID(), get_ID() cho chứng minh nhân dân … Các đối tượng sử dụng những phương thức này để giao tiếp với bên ngoài. Việc này trước

Page 3: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

3 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

giúp dữ liệu được quan tâm đúng mức, và an toàn hơn. Mọi truy cập đến dữ liệu đều được kiểm soát thông qua các phương thức được cung cấp sẵn nên hạn chế được những truy cập bất hợp pháp. Tức là đã giải quyết được ba hạn chế đầu tiên của lập trình cấu trúc. Thứ hai, những thay đổi nào đó về dữ liệu chỉ ảnh hưởng đến một số lượng hàm nhất định và thay vì phải viết lại hầu hết các hàm thì ta chỉ phải viết lại một số hàm có liên quan trực tiếp đến sự thay đổi đó. Ví dụ thành phần dữ liệu name biểu thị tên của một đối tượng Human vì một lý do nào đó được đổi thành full_name thì những hàm liên quan trực tiếp đến name như set_name() hay get_name() mới phải viết lại, còn những hàm như set_ID(), get_ID() hay thậm chí những hàm gọi hàm set_name() và get_name() thì chẳng việc gì cả. Điều này thuận lợi cho việc nâng cấp và bảo trì. Tức là hạn chế thứ tư đã được giải quyết. OOP cũng cung cấp những khái niệm về kế thừa và đa hình giúp tận dụng tối đa khả năng sử dụng lại mã nguồn để giảm bớt vất vả cho lập trình viên cũng như tăng chất lượng phần mềm. Ví dụ chúng ta có thể tạo ra một lớp (class) mới là Girl, kế thừa từ lớp Human. Khi đó, một đối tượng thuộc lớp Girl sẽ có đầy đủ các thuộc tính và phương thức của Human, và ta chỉ cần bổ sung thêm những phần khác như số đo ba vòng: round_1, round_2, round_3 … Vì thể không phải viết lại toàn bộ code cho lớp Girl. Cụ thể như thế nào thì mình sẽ đề cập trong những bài post sau. Đây chỉ là bài mở đầu để giúp mọi người so sánh giữa kỹ thuật OOP với kỹ thuật lập trình cấu trúc truyền thống và có những hình dung cơ bản về OOP, những ưu điểm mà nó mang lại, và vì sao nó lại là một kỹ thuật được ưa chuộng nhất hiện nay. Trong những năm gần đây, lập trình đã dịch chuyển từ hướng cấu trúc sang hướng đối tượng vì những ưu điểm và khả năng mạnh mẽ của nó. Thực tế hiện nay OOP được sử dụng rộng rãi trong các dự án phần mềm, còn lập trình cấu trúc chỉ chiếm một phần rất nhỏ thường là giải quyết những vấn đề có quy mô nhỏ hoặc dùng trong giảng dạy để giúp người học bước đầu làm quen với lập trình. Đấy là mình cũng chỉ nghe thiên hạ nói thế thôi chứ cũng mới học OOP nên cũng không biết là thực tế doanh nghiệp bây giờ nó viết phần mềm bằng ngôn ngữ gì cả. Nhưng có điều mình cảm nhận được đúng là OOP lập trình sướng hơn hơn lập trình cấu trúc nhiều, ít ra là cái khoản thiết kế nó trực quan hơn, rõ ràng hơn, thật hơn. Còn nếu để ý kỹ thì những cài đặt chi tiết trong hướng đối tượng suy cho cùng vẫn là lập trình cấu trúc, có điều chúng được tổ chức tốt hơn và được phủ lên một giao diện mang tính hướng đối tượng mà thôi.

BÀI 2. NHỮNG ĐẶC TRƯNG CƠ BẢN CỦA OOP Chúng ta sẽ xem xét sơ qua một số khái niệm và thành phần chính của OOP nói chung và của C++ nói riêng 1. Đối tượng (Objects) Khi thiết kế một chương trình theo tư duy hướng đối tượng người ta sẽ không hỏi “vấn đề này sẽ được chia thành những hàm nào” mà là “vấn đề này có thể giải quyết bằng cách chia thành những đối tượng nào”. Tư duy theo hướng đối tượng làm cho việc thiết kế được “tự nhiên” hơn và trực quan hơn. Điều này xuất phát từ việc các lập trình viên cố gắng tạo ra một phong cách lập trình càng giống đời thực càng tốt. Nếu ngoài đời có cái công nông thì

Page 4: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

4 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

khi thiết kế ta cũng bê nguyên cả cái công nông vào trong chương trình, và như vậy chương trình là tập hợp tất cả các đối tượng có liên quan với nhau. Tất cả mọi thứ đều có thể trở thành đối tượng trong OOP, nếu có giới hạn thì đó chính là trí tưởng của bạn. Đối tượng là một thực thể tồn tại trong khi chương trình chạy. Nó có các thuộc tính (attributes) và phương trức (methods) của riêng mình. 2. Lớp (Classes) Trong khi đối tượng là một thực thể xác định thì lớp lại là một khái nhiệm trừu tượng. Có thể so sánh lớp như “kiểu dữ liệu còn” đối tượng là “biến” có kiểu của lớp. Ví dụ: lớp Công_nông có thể được mô tả như sau: Lớp Công_nông Thuộc tính:

Nhãn hiệu (ví dụ Lamborghini) Màu xe Giá xe Vận tốc tối đa (ví dụ 300 km/h)

Phương thức:

Khởi động Chạy thẳng Rẽ trái / phải Dừng Tắt máy

Một khai báo: C++ Code:

1. Công_nông công_nông_của_tôi;

Hoàn toàn tương tự như khai báo: C++ Code:

1. int my_integer;

Tạo một lớp mới tương tự như tạo ra một kiểu dữ liệu mới – kiểu người dùng tự định nghĩa (user-defined type) Lớp là “khuôn” để đúc ra các đối tượng.Một đối tượng thuộc lớp Công_nông sẽ có đầy đủ những thuộc tính và phương thức như được mô tả ở trên, trong trường hợp này công_nông_của_tôi được đúc ra từ “khuôn” Công_nông. Có một sự tương ứng giữa lớp và đối tượng nhưng bản chất thì lại khác nhau. Lớp là sự trừu tượng hóa của đối tượng, còn đối tượng là một sự thể hiện (instance) của lớp. Đối tượng là một thực thể có thực, tồn tại trong hệ thống, còn lớp là khái niệm trừu tượng chỉ tồn tại ở dạng khái niệm để mô tả đặc tính chung cho đối tượng. Tất cả những đối tượng của một lớp sẽ có thuộc tính và phương thức giống nhau. 3. Sự đóng gói và trừu tượng hóa dữ liệu (Encapsulation & Data Abstraction) Nhìn lại thí dụ trên thì mỗi đối tượng thuộc lớp Công_nông sẽ có cả các thuộc tính và phương thức được “đóng gói” chung lại. Muốn truy cập vào các thành phần dữ liệu bắt buộc phải thông qua phương thức, và các phương thức này tạo ra một giao diện để đối tượng

Page 5: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

5 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

giao tiếp với bên ngoài. Giao diện này giúp cho dữ liệu được bảo vệ và ngăn chặn những truy cập bất hợp pháp, đồng thời tạo ra sự thân thiện cho người dùng. Ví dụ: nếu như trong C, một xâu được lưu trữ trong một mảng str nào đó, muốn biết độ dài của xâu ta phải gọi hàm strlen() trong thư viện <string.h> thì trong C++, nếu str là một đối tượng thuộc lớp string thì tự nó “biết” kích thước của mình, và chỉ cần gọi str.size() hoặc str.length() là nó sẽ trả về độ dài của xâustr. Người dùng hoàn toàn không cần biết cài đặt chi tiết bên trong lớp string như thế nào mà chỉ cần biết “giao diện” để có thể giao tiếp với một đối tượng thuộc lớp string là ok. Điều này dẫn đến sự trừu tượng hóa dữ liệu. Nghĩa là bỏ qua mọi cài đặt chi tiết và chỉ quan tâm vào đặc tả dữ liệu và các phương thức thao tác trên dữ liệu. Đặc tả về lớp Công_nông ở trên cũng là một sự trừu tượng hóa dữ liệu. 4. Sự kế thừa (Inheritance) Những ý tưởng về lớp dẫn đến những ý tưởng về kế thừa. Trong cuộc sống hàng ngày chúng ta thấy rất nhiều ví dụ về sự kế thừa (tất nhiên là không phải thừa kế vê tài sản. Ví dụ:lớp động vật có thể phân chia thành nhiều lớp nhỏ hơn như lớp côn trùng, lớp chim, lớp động vật có vú, không có vú … blah blah … hay lớp phương tiện có thể chia thành các lớp nhỏ hơn như xe đạp, xe thồ, xe tăng, xích lô, … Các lớp nhỏ hơn được gọi là lớp con (subclass) hay lớp dẫn xuất (derived class) còn các lớp phía trên gọi là lớp cha (super class) hay lớp cơ sở (base class). Một nguyên tắc chung là các lớp con sẽ có các đặc điểm chung được thừa hưởng từ các lớp cha mà nó kế thừa. Ví dụ lớp côn trùng và động vật có vú đều sẽ có những đặc điểm chung của lớp động vật. Và do đó ta chỉ cần bổ sung những đặc điểu cần thiết thay vì viết lại tòan bộ code. Điều này giảm gánh nặng cho các lập trình viên và do đó góp phần giảm chi phí sản xuất cũng như bảo trì, nâng cấp phần mềm. 5. Tính đa hình và sự quá tải (Polymorphism & Overloading) Giả sử ta xây dựng một lớp String để “đúc” ra các đối tượng lưu trữ xâu ký tự, ví dụ ta có 3 đối tượng s1, s2, s3 thuộc lớp String. Ta muốn thiết kế lớp String sao cho câu lệnh C++ Code:

1. s3 = s1 + s2 ;

sẽ thực hiện việc nối xâu s2 vào đuôi xâu s1 rồi gán kết quả cho xâu s3. Nếu như vậy công việc lập trình trông sẽ “tự nhiên” hơn. Nhưng thật không may ngôn ngữ lập trình không cung cấp sẵn điều này. Sử dụng các toán tử (operators) + và = như trên sẽ gây lỗi. Tuy nhiên C++ cung cấp một cơ chế cho phép lập trình viên “định nghĩa lại” các toán tử này để dùng trong các mục đích khác nhau. Việc định nghĩa lại cách sử dụng toán tử được gọi là “quá tải toán tử” (operator overloading). Một số người gọi nó là “nạp chồng toán tử” nhưng mình thích dùng từ quá tải hơn vì nghe nó có vẻ “cơ khí” . C++ cho phép quá tải hầu hết các toán tử thông dụng như +, -, *, /, [], <<, >>, … Ngoài việc cho phép quá tải toán

tử, C++ còn cho phép “quá tải hàm” (function overloading), cái này mình sẽ nói kỹ hơn ở bài khác. Nói chung overloading là một cách cho phép ta sử dụng một toán tử hoặc hàm bằng những cách khác nhau tùy theo ngữ cảnh, và đó một trường hợp của “tính đa hình” (polymorphism), một tính năng rất quan trọng của OOP.

Page 6: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

6 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

BÀI 3. MỘT CHƯƠNG TRÌNH C++ ĐƠN GIẢN Bây giờ chúng ta sẽ xem xét một chương trình C++ đơn giản sau C++ Code:

1. // my first program in C++

2. #include <iostream> 3. using namespace std; 4. 5. int main(){ 6. cout << “Hello, Girl” << endl; 7. return 0;

8. }

Dòng đầu tiên là một chú thích (comment). Tất cả những gì từ sau ký hiệu // đến hết dòng được hiểu là chú thích và bị trình biên dịch bỏ qua, hoàn toàn không gây ảnh hưởng gì đến hoạt động của chương trình. Mục đích duy nhất của chú thích là làm tăng tính sáng sủa của chương trình, ta dùng chú thích để giải thích ngắn gọn mục đích của đoạn code hay của chương trình là gì. Trong ví dụ này, chú thích cho biết đây là chương trình đầu tiên bằng C++ của tôi. Ta có thể chú thích trên nhiều dòng bằng cặp ký hiệu /* Here are your comments */. Tuy nhiên tớ nghĩ dùng những chú thích ngắn gọn trên một dòng sẽ tốt hơn. Thêm nữa, chúng ta nên hạn chế sử dụng chú thích bừa bãi. Chỉ dùng khi thực sự cần thiết, và nên ngắn gon súc tích. Như vậy giúp ta trọng tâm hơn vào những phần chính và giúp chương trình không bị rối. Hãy để các đoạn code tự nói lên ý nghĩa của chúng. Dòng thứ hai là một chỉ thị tiền xử lý (preprocessor directive). Tất cả những gì bắt đầu bằng # đều là chỉ thị tiền xử lý và được xử lý bởi bộ tiền xử lý trước khi chương trình được dịch . Nó không phải là một câu lệnh (lưu ý mọi câu lệnh đều phải kết thúc bởi dấu chấm phẩy – semicolon )mà là một chỉ thị hướng dẫn preprocessor nạp nội dung của tệp <iostream> vào. Việc này giống như ta copy toàn bộ nội dung của tệp <iostream> rồi paste vào đúng vị trí của chỉ thị #include <iostream>. <iostream> là một header file liên quan đến những thao tác nhập/ xuất cơ bản. Nó chứa những khai báo (declarations) cần thiết cho nhập/ xuất, ví dụ trong trường hợp này sẽ được dùng bởi cout và toán tử <<. Thiếu những khai báo này trình biên dịch sẽ không nhận ra cout và sẽ báo lỗi. Vì vậy cần thiết phải include <iostream>. Chú ý: đôi khi ta thấy một số chương trình viết C++ Code:

1. #include <iostream>

Trong khi một số thì lại viết C++ Code:

1. #include <iostream.h>

Hai cách viết này là khác nhau. Những file có phần mở rộng .h là những file “cũ” có từ thời kỳ sơ khai của C++ và phần lớn trong số đó kế thừa và phát triển dựa trên các file của ngôn ngữ C. Khi ANSI và ISO công bố chuẩn cho C++ thì các standard header file mới đều không có phần mở rộng. Nói chung thì New Standard Header File so với Classic Standard Header File không khác nhau nhiều lắm, cái sau cải tiến và hoàn thiện một số khiếm khuyết của cái

Page 7: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

7 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

trước. Tất nhiên là những cái gì theo chuẩn mới thì thông thường sẽ tốt hơn. Tớ sẽ nói rõ hơn về phần này trong phần I/O stream. Dòng thứ ba đề cập đến một khái niệm đó là “namespace” (đôi khi còn được gọi là name scope). Thông thường một chương trình có chứa nhiều định danh (identifiers) thuộc nhiều phạm vi (scope) khác nhau. Đôi khi một đối tượng trong phạm vi này bị trùng tên với một đối tượng khác trong một phạm vi khác. Điều này dẫn đến xung đột và gây lỗi biên dịch. Sự chồng chéo tên (identifier overlapping ) có thể xảy ra ở nhiều cấp độ khác nhau, đặc biệt là trong các thư viện cung cấp bởi bên thứ ba. C++ standard nỗ lực giải quyết vấn đề này bằng cách sử dụng namespace. Mỗi namespace xác định một phạm vi mà trong đó các định danh được nhận biết, ngoài phạm vi này chúng sẽ không được nhận biết. Để sử dụng một thành phần trong namespace ta có thể dùng câu lệnh như sau C++ Code:

1. my_namespace::member;

Câu lệnh trên sử dụng một identifier có tên là member trong namespace có tên là my_namespace. Rõ ràng khi một namespace khác (ví dụ: your_namespace) cũng có một thành phần tên là member thì việc dùng hai tên này không sợ bị chồng chéo lên nhau. Toán tử :: là toán tử “phân giải phạm vi” (binary scope resolution operator). Trong câu lệnh trên toán tử :: cho biết rằng định danh member được sử dụng nằm trong phạm vi của namespace tên là my_namespace chứ không phải your_namespace. Quay trở lại chương trình của ta, nhận thấy trong hàm main, dòng thứ 5 có sử dụng cout và endl. Đây là hai định danh được khai báo trong namespace std. Để chương trình “nhận biết” được cout và endl thì ta có thể dùng cú pháp như vừa nói ở trên tức dòng lệnh thứ 5 được viết lại là: C++ Code:

1. std::cout << “Hello, Girl” << std::endl;

Tuy nhiên, rõ ràng cách viết trên là dài dòng. Nếu ta sử dụng nhiều hơn các đinh danh trong namespace std thì mỗi lần dùng ta lại phải viết thêm std::, vì vậy để có thể sử dụng được toàn bộ các định danh trong namespace std ta dùng câu lệnh như dòng thứ 3: C++ Code:

1. using namespace std;

Những dòng còn lại là định nghĩa hàm main(). Đây là hàm quan trọng nhất trong chương trình và có nhiệm vụ điều phối và kiểm soát toàn bộ chương trình, nó gọi những hàm khác khi cần thiết. Tuy nhiên mình muốn nói một điều hơi bất cập một tý. Khi mình đọc các tài liệu về C++ thì tất cả đều nóihàm main được gọi và xử lý trước mọi hàm khác trong chương trình. Điều này có luôn luôn đúng? Phần này mình nói hơi ngoài lề một tý, nó liên quan đến constructor của class nên nếu bạn nào chưa học đến phần này thì có thể bỏ qua. Xét một chương trình sau: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. // định nghĩa lớp My_class 5. class My_class{ 6. private: 7. int number;

Page 8: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

8 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

8. public: 9. My_class(){ number = 0; } // constructor

10. } 11. 12. My_class global_var; // khai báo một biến toàn cục

13. 14. // hàm main 15. int main(){ 16. cout << “Is main always called first ?” << endl; 17. return 0;

18. }

Biến global_var được khai báo toàn cục bên ngoài tất cả mọi hàm. Khi khai báo biến global_var thì theo nguyên tắc phải gọi đến constructor của lớpMy_class để khởi tạo number = 0. Vì vậy thực tế trong chương trình trên constructor My_class() được gọi trước main. Bây giờ trở lại vấn đề chính, ta sẽ vẫn tiếp tục phân tích nốt mấy câu lệnh còn lại. Chúng ta để ý đến dòng thứ 5. C++ Code:

1. cout << “Hello, Girl” << endl;

Dòng này có tác dụng in dòng text nằm giữa hai dấy nháy kép, cụ thể là “Hello, Girl” lên màn hình. Chúng ta sẽ phân tích kỹ hơn một chút về nguyên tắc hoạt động của nó, tuy nhiên chỉ là một sự mô tả rất thô sơ. Để hiểu biết kỹ hơn chúng ta cần biết những kiến thức về đối tượng, quá tải toán tử, và nhiều vấn đề khác nữa. Trong C, để in một đoạn văn bản lên màn hình ta có thể dùng hàm printf(). Điều này dễ làm cho ta lầm tưởng cout cũng là một hàm, nhưng không phải thế. C là ngôn ngữ hướng thủ tục, còn C++ là ngôn ngữ hướng đối tượng. Và cout là một đối tượng (object). Nó được định nghĩa sẵn trong C++ tương ứng với dòng xuất chuẩn (standard output stream). Stream là một khái niệm trừu tượng được hiểu như luồng dữ liệu (data flow). standard output stream thông thường được “kết nối” (connected to) hay “chảy” (flows to) tới màn hình. Toán tử << được gọi là toán tử chèn dòng xuất (insertion output stream operator). Nó ra lệnh chuyển những nội dung của đối tượng bên tay phải sang đối tượng bên tay trái (giống như chiều mũi tên của toán tử << luôn). Ở đây endl (đối tượng này được khai báo trong namespace std như đã nói ở trên và tác dụng của nó là kết thúc một dòng, chuyển sang dòng mới) được chuyển sang bên trái cho xâu ký tự nằm trong dấu nháy kép. Sau đó toàn bộ dữ liệu này được chuyển sang cho cout, mà cout lại kết nối tới màn hình nên kết quả là trên màn hình in ra dòng text: Hello, Girl và con trỏ chuyển xuống dòng mới. Có thể mô tả bởi hình vẽ sau: Câu lệnh cuối cùng là: C++ Code:

1. return 0;

Câu lệnh này là một cách thông thường để kết thúc hàm main. Nó báo cho trình biên dịch biết là chương trình kết thúc thành công, không có lỗi.

Page 9: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

9 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Chương trình trên mặc dù rất đơn giản nhưng nó trình bày được cấu trúc chung của của một chương trình C++. Những bài sau mình sẽ giới thiệu những tiện ích thông dụng của C++ và cách sử dụng chúng.

BÀI 4. NHẬP / XUẤT CƠ BẢN VỚI C++ Trong bài này mình sẽ trình bày những thao tác nhập/ xuất cơ bản (basic input/output) để các bạn có thể viết ngay được chương trình với C++. Học C++ cũng giống như học một ngoại ngữ như tiếng Ý hay Italia. Để học được và học tốt ngôn ngữ việc trước tiên không phải là lao ngay vào ngâm cứu ngữ pháp hay văn phạm hoặc các luật lệ của ngôn ngữ, mà là phải biết một số từ mới, một số câu xã giao đơn giản kiểu như : Hey, Girl ! You’re so beautiful … Dần dần khi vốn từ vựng đã đủ dùng và ta ta đã quen dần với ngôn ngữ thì lúc đó mới ngâm cứu những vấn đề sâu hơn, mới tìm hiểu được bản chất của vấn đề. Vì vậy bài này giúp các bạn mới học C++ làm quen với những thao nhập xuất tác cơ bản trong C++ như học các từ vựng đầu tiên của ngôn ngữ, còn muốn hiểu rõ bản chất và nguyên tắc hoạt động của chúng ra sao thì cần phải cần có những kiến thức sâu hơn một chút, mình sẽ trình bày trong bài nói về input/ output stream và file. 1. Xuất dữ liệu ra màn hình Để in dữ liệu của một biểu thức nào đó ra màn hình (standard output device) ta dùng câu lệnh sau: C++ Code:

1. cout << exp ; // output content of expression

Hoặc cho nhiều biểu thức: C++ Code:

1. cout << exp_1 << exp_2 << … << exp_n ; // output content of expression 1, 2, … , n

Ta cũng có thể viết câu lệnh trên trên nhiều dòng: C++ Code:

1. cout << exp_1 2. << exp_2 3. << exp_3 4. … 5. << exp_n;

Kết quả thu được là hoàn toàn tương tự. cout cùng với toán tử << có thể xuất được nhiều kiểu dữ liệu khác nhau mà không cần sự can thiệp của lập trình viên. C++ tự động nhận biết và xuất ra dưới định dạng phù hợp. Xét đoạn chương trình sau: C++ Code:

1. #include <iostream> 2. #include <string> 3. using namespace std; 4.

Page 10: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

10 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

5. int main(){ 6. int n=3; 7. float x=12.08; 8. string str="Be aware ! I have a gun"; 9. 10. cout << n << endl; 11. cout << x << endl; 12. cout << str << endl; 13. 14. return 0;

15. }

Kết quả thu được trên màn hình sẽ như sau 3 12.08 Be aware ! I have a gun

2. Nhập dữ liệu vào từ bàn phím Bàn phím là thiết bị nhập chuẩn (standard input device). Để vào dữ liệu từ bàn phím cho các biến ta có thể dùng cin vùng toán tử >>. Cú pháp sẽ như sau: C++ Code:

1. cin >> var; // read data into variable

Hoặc cho nhiều biến: C++ Code:

1. cin >> var_1 >> var_2 >> … >> var_n;

Khi gặp những câu lệnh như thế này chương trình sẽ “pause” lại để chờ chúng ta nhập dữ liệu vào từ bàn phím. Câu lệnh cin >> var_1 >> var_2 >> … >> var_n; coi các ký tự trắng là ký tự phân cách các lần nhập dữ liệu. Các ký tự trắng (white space characters) bao gồm: dấu cách, dấu tab, và ký tự xuống dòng (new line). Ví dụ a, b là hai biến kiểu int, thì câu lệnh: C++ Code:

1. cin >> a >> b;

sẽ đợi người dùng nhập dữ liệu hai lần, cách nhau bởi ít nhất một ký tự trắng. Ví dụ ta nhập vào bàn phím như sau : 1989 2011 ↵ thì biến a sẽ nhận giá trị 1989, còn biến b sẽ nhận giá trị 2011. Chúng ta cũng không cần quan tâm đến kiểu của các biến, C++ cũng tự động nhận biết điều này. Xem xét đoạn chương trình sau: C++ Code:

1. int num; 2. cin >> num;

nếu ta nhập vào giá trị 12.08 thì biến num chỉ nhận được giá trị 12. Còn phần thập phân sẽ không được ghi nhận, do num là một biến kiểu int nên C++ tự động nhận biết và “chặt cụt” đi phần này. 3. Những lưu ý trong nhập/ xuất xâu ký tự.

Page 11: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

11 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Trong tất cả các kiểu dữ liệu, theo mình thì nhập xuất với xâu (string) là thể loại củ chuối và rắc rối nhất. Trước hết mình nói qua một chút về thư viện chuẩn <string> của C++. Nếu bạn nào đã học qua ngôn ngữ C rồi thì đều biết xâu ký tự là một mảng chứa các ký tự (kiểu char) và phần tử cuối cùng của mảng là NULL (hay \0) để đánh dấu sự kết thúc xâu. Tuy nhiên, sử dụng mảng để lưu trữ xâu có phần phức tạp vì mảng là một cấu trúc tĩnh phải biết rõ kích thước ngay khi khai báo. Điều này làm cho chương trình “cứng nhắc” và không “kinh tế”. Ví dụ nếu khai báo ít quá thì khi muốn chứa thêm nhiều ký tự hơn sẽ không được, mà nếu khai báo nhiều quá, không dùng hết sẽ lãng phí bộ nhớ. Để khắc phục người ta dùng biện pháp quản lý mảng bằng con trỏ và cấp bộ nhớ phát động cho mảng (dynamic memory allocation). Tuy nhiên nếu việc này diễn ra thường xuyên thì chương trình sẽ rất rối rắm, gây mệt mỏi cho lập trình viên và dễ dẫn đến lỗi. C++ giải quyết tốt vấn đề này bằng cách xây dựng lớp string. Một đối tượng của lớp string có thể lưu trữ các ký tự “tốt hơn” mảng trong C rất nhiều. Muốn sử dụng nó ta phải include header file <string>. Trong các bài viết, nếu không có lý do gì đặc biệt, thì mình sẽ dùng string để lưu trữ xâu. Bây giờ trở lại vấn đề của chúng ta, đó là nhập/ xuất xâu ký tự. Xuất thì ok, không có vấn đề gì phải bàn luận nhiều, nhưng nhập thì lại có nhiều điều để nói: Thứ nhất, cin >> không cho phép nhập khoảng trắng. Xét đoạn chương trình sau: C++ Code: #include <iostream>

1. #include <string> 2. using namespace std; 3. 4. int main(){ 5. string str; 6. 7. cin>> str; 8. cout << str << endl; 9. 10. return 0;

11. }

Nếu ta nhập vào đoạn văn bản sau: “Osama Binladen” thì xâu str chỉ ghi nhận đoạn đầu “Osama” vì dấu cách là một khoảng trắng mà cin coi các dấu trắng là ký tự báo hiệu kết thúc việc nhập dữ liệu. Để khắc phục điều này C++ cung cấp một số cách thức để nhập toàn bộ xâu ký tự. Nhưng trước hết ta phải xem xét một vấn đề về bộ đệm. Bộ đệm và các vấn đề liên quan đến nhập dữ liệu từ bàn phím Khi ta nhập dữ liệu vào bàn phím thì dữ liệu không được đọc ngay vào biến mà được đẩy lên trên bộ đệm (buffer). Dữ liệu sẽ tồn tại trong bộ đệm cho tới khi một lệnh nào đó gửi yêu cầu đến bộ đệm “xin phép” được load dữ liệu về. Ví dụ khi chương trình gặp câu lệnh: C++ Code:

1. cin >> x; // với x là một biến kiểu int

thì nó sẽ mò lên buffer để load dữ liệu về, nếu như trên bộ đệm có số 100 thì nó sẽ đọc 100 vào biến x mà không đợi ta nhập gì cả, còn nếu bộ đệm trống hoặc có những dữ liệu không phải số nguyên (ví dụ mã phím Enter của lần nhập trước) thì nó mới dừng lại đợi ta nhập dữ

Page 12: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

12 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

liệu vào. Như vậy ta hoàn toàn không cần để ý nhiều việc sử dụng cin >> để nhập các dữ liệu số (nguyên hoặc dấu chấm động – floating point), nhưng để nhập dữ liệu ký tự thì lại hoàn phải hết sức chú ý. Trong đoạn chương trình test nhập xâu ký tự bên trên ta nhận thấy biến str chỉ lưu trữ phần “Osama”, phần còn lại thì vẫn nằm trên buffer để cho lần nhập sau. C++ cung cấp getline cho phép nhập toàn bộ xâu ký tự kể cả khoảng trắng. Cú pháp sử dụng hàm getline như sau. C++ Code:

1. getline(cin, str, delimiter);

Câu lệnh trên sẽ thực hiện đọc toàn bộ xâu nhập từ bàn phím vào biến str, cho tới khi bắt gặp ký tự kết thúc (delimiter) hoặc EOF (end-of-file). Nếu không viết delimiter thì mặc định là ký tự xuống dòng – ‘\n’. Xét chương trình sau: C++ Code:

1. #include <iostream> 2. #include <string> 3. using namespace std; 4. 5. int main(){ 6. string str; 7. getline(cin,str); 8. cout << str << endl; 9. 10. return 0;

11. }

Nếu ta nhập vào bàn phím xâu “Osama Binladen” rồi nhấn Enter thì kết quả thu được trên màn hình sẽ là trọn vẹn xâu “Osama Binladen”. Điều gì xảy ra với ký tự Enter, nó có nằm lại trên bộ đệm không? Câu trả lời là getline đã đọc mã của ký tự Enter nhưng không gắn nó vào trong xâu str và cũng không để lại nó trên bộ đệm mà hủy nó đi. Bây giờ ta xét đến hiện tượng trôi lệnh getline. Xét chương trình sau: C++ Code:

1. #include <iostream> 2. #include <string> 3. using namespace std; 4. 5. int main(){ 6. int num; 7. string str; 8. 9. cout << "Input an integer a= "; 10. cin >> num; 11. cout << num << endl; 12. cout << "Input a string str= ";

Page 13: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

13 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

13. getline(cin,str); 14. cout << str << endl; 15. cout << "End program" << endl; 16. 17. return 0;

18. }

Bạn chạy thử chương trình trên sẽ thấy ngay. Sau khi nhập dữ liệu cho biến num, chương trình không dừng lại cho ta nhập dữ liệu cho str. Mà in ngay ra thông báo “End program”. Nguyên nhân là do sau khi nhập dữ liệu cho biến num ta gõ phím Enter. Mã của Enter được lưu trong bộ đệm (chính là ký tự xuống dòng ‘\n’) và do đó khi chương trình gặp câu lệnh C++ Code:

1. getline(cin,str);

nó sẽ đọc ngay ký tự này và nhận thấy đây là ký tự kết thúc (delimiter) nên nó sẽ loại bỏ ký tự này ra khỏi bộ đệm mà không đọc vào xâu str. Sau đó nó sẽ chạy thẳng đến câu lệnh tiếp theo là: C++ Code:

1. cout << "End program" << endl;

Người ta thường gọi đó là hiện tượng “trôi lệnh”. Để khắc phục hiện tượng trôi lệnh này thì trước mỗi lệnh getline ta nên đặt câu lệnh: C++ Code:

1. fflush(stdin);

Câu lệnh này có tác dụng xóa bộ đệm và do đó ta có thể yên tâm là sẽ không bị trôi lệnh nữa

BÀI 5a. CLASSES & OBJECTS (PART 1) Trong C++, class là nền tảng cho lập trình hướng đối tượng. Nó là sự mở rộng của khái niệm data structure: thay vì chỉ lưu trữ dữ liệu thì nó lưu trữ cả dữ liệu và hàm. Tuy nhiên nó chỉ là một khái niệm trừu tượng, nó không phải là một thực thể có thực khi chương trình đang chạy, mà chỉ là khuôn mẫu để “đúc” ra những object. Một object là một thể hiện (instance) của class. Có thể coi class giống như kiểu dữ liệu còn object như các biến. 1. Định nghĩa một lớp Các class được tạo ra bằng cách sử dụng từ khóa class. Chúng ta sẽ xem xét một ví dụ về định nghĩa lớp. Giả sử chúng ta lập một lớp Student trong đó lưu trữ các thông tin về sinh viên cũng như chứa các hàm để thao tác trên các dữ liệu này. C++ Code:

1. #include <iostream> 2. #include <string> 3. using namespace std;

Page 14: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

14 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

4. 5. // class definition 6. class Student{ 7. private: 8. string name; // tên sinh viên 9. int age; // tuổi 10. string student_code; // mã số sinh viên 11. public: 12. // set information 13. void set_name(string); // nhập tên 14. void set_age(int); // nhập tuổi 15. void set_student_code(string); // nhập mã sinh viên

16. 17. // get information 18. string get_name(); // lấy tên 19. int get_age(); // lấy tuổi 20. string get_student_code(); // lấy mã sinh viên 21. };

Ta sẽ phân tích định nghĩa trên của lớp Student. Đầu tiên là từ khóa class, sau đó là tên lớp mà người dùng muốn tạo (ở đây là Student). Phần còn lại nằm trong cặp ngoặc móc {} chứa những thành của lớp. Những dữ liệu như: name, age, student_code được gọi là các thành phần dữ liệu (data members), còn các hàm như: set_name(), get_name(), … được gọi là các hàm thành viên (member functions) hay phương thức (methods). Thông thường các data members được để ở chế độ private, còn các member functions thì ở chế độ public. Từ khóa private và public là hai access-specifier quy định quyền truy nhập đối với các thành phần trong lớp, nó sẽ có hiệu lực cho các thành phần của lớp đứng sau nó cho đến khi gặp một access-specifier khác. Tất cả các thành phần được khai báo là public sẽ có thể được truy cập “thoải mái” bất cứ chỗ nào lớp có hiệu lực. Ví dụ nó có thể được truy cập bởi hàm thành viên của của một lớp khác, hoặc các hàm tự do trong chương trình. Các thành phần private thì được bảo mật cao hơn. Chỉ những thành phần của lớp mới có thể truy nhập đến chúng. Mọi cố gắng truy nhập bất hợp pháp từ bên ngoài đều sẽ gây lỗi. Do đó ta có thể mô tả cú pháp chung để định nghĩa một lớp như sau: C++ Code:

1. class Class_name{ 2. private: 3. // các thành phần private 4. public: 5. // các thành phần public 6. };

Page 15: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

15 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Chú ý sau mỗi định nghĩa lớp phải có dấu chấm phẩy (semicolon) vì định nghĩa lớp là tương đương với định nghĩa một kiểu dữ liệu mới. Cú khai báo một đối tượng của lớp Student giống y như cú pháp khai báo một biến bình thường: C++ Code:

1. int num1, num2; // khai báo hai biến nguyên num1, num2 2. Student studentA, studentB; // khai báo hai đối tượng thuộc lớp Student là

studentA và studentB

2. Định nghĩa các hàm thành viên cho lớp Trong định nghĩa trên của lớp mới chỉ khai báo các nguyên mẫu hàm (function prototypes) chứ hoàn toàn chưa có thân hàm. Ta sẽ phải định nghĩa các hàm này. Có hai cách định nghĩa các hàm thành viên: định nghĩa hàm thành viên ngay trong định nghĩa lớp hoặc khai báo nguyên mẫu trong lớp, còn định nghĩa bên ngoài lớp. Định nghĩa hàm ngay trong định nghĩa lớp Khi đó định nghĩa lớp được viết lại như sau: C++ Code:

1. class Student{ 2. private: 3. string name; 4. int age; 5. string student_code; 6. public: 7. // set information 8. void set_name(string str){ name=str; } 9. void set_age(int num){ age=num; } 10. void set_student_code(string str){ student_code=str; } 11. 12. // get information 13. string get_name(){ return name; } 14. int get_age(){ return age; } 15. string get_student_code(){ return student_code; }; 16. };

Ta nhận thấy các hàm đã được định nghĩa luôn trong nghĩa lớp. Tuy nhiên cách này không phải là cách tốt. Với bài này thì các hàm còn đơn giản, còn ngắn. Nhưng trong thực tế khi ta xây dựng các lớp những lớp phức tạp hơn thì số lượng hàm sẽ nhiều hơn và dài hơn. Nếu định nghĩa trong lớp sẽ làm “mất mĩ quan” và khó kiểm soát. Vì vậy trong định nghĩa lớp ta chỉ liệt kê các nguyên mẫu hàm, còn khi định nghĩa, ta sẽ định nghĩa ra bên ngoài. Khai báo hàm trong lớp, còn định nghĩa ngoài lớp C++ Code:

1. // class definition 2. class Student{

Page 16: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

16 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

3. private: 4. string name; 5. int age; 6. string student_code; 7. public: 8. // set information 9. void set_name(string); 10. void set_age(int); 11. void set_student_code(string); 12. 13. // get information 14. string get_name(); 15. int get_age(); 16. string get_student_code(); 17. }; 18. 19. // member function definitions 20. // set name 21. void Student::set_name(string str){ 22. name=str; 23. } 24. 25. // set age 26. void Student::set_age(int num){ 27. age=num; 28. } 29. 30. // set student code 31. void Student::set_student_code(string str){ 32. student_code=str; 33. } 34. 35. // get name 36. string Student::get_name(){ 37. return name; 38. } 39. 40. // get age 41. int Student::get_age(){ 42. return age; 43. }

Page 17: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

17 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

44. 45. // get student code 46. string Student::get_student_code(){ 47. return student_code;

48. }

Cú pháp để định nghĩa một hàm thành viên bên ngoài lớp là: C++ Code:

1. <kiểu_trả_về> <tên_lớp>::<tên_hàm_thành_viên>(danh_sách_tham_số){ 2. // định nghĩa thân hàm ở đây

3. } 4. 5. Ví dụ hàm set_name 6. 7. void Student::set_name(string str){ 8. name=str;

9. }

Cú pháp này chỉ rõ rằng hàm ta đang định nghĩa là hàm thành viên của lớp Student vì nó được chỉ định bởi toán tử phân giải phạm vi :: (trong trường hợp này là Student:: ), nghĩa là hàm này nằm trong phạm vi lớp Student. Có một sự khác nhau giữa hai cách định nghĩa hàm này không phải chỉ ở góc độ “thẩm mỹ”. Theo cách thứ nhất: định nghĩa luôn trong định nghĩa lớp, thì hàm được coi là “hàm nội tuyến” hay inline. Thông thường khi muốn một hàm làm một việc gì đó thì ta phải làm một việc là “gọi hàm” (invoke). Việc gọi hàm sẽ phải tốn các chi phí về thời gian như gửi lời gọi hàm, truyền đối số, … điều này có thể làm chậm chương trình. C++ cung cấp một giải pháp đó là “hàm nội tuyến” bằng cách thêm vào từ khóa inline trước kiểu trả về của hàm như sau: C++ Code:

1. inline <kiểu_trả_về> <tên_lớp>::<tên_hàm_thành_viên>(danh_sách_tham_số){ 2. // định nghĩa thân hàm ở đây

3. }

Điều này “gợi ý” cho compiler sinh mã của hàm ở những nơi thích hợp để tránh phải gọi hàm. Như vậy sẽ tránh được những chi phí gọi hàm nhưng ngược lại nó làm tăng kích thước của chương trình. Vì cứ mỗi lời gọi hàm sẽ được thay thế bởi một đoạn mã tương ứng. Vì vậy chỉ khai báo một hàm là inline khi kích thước của nó không quá lớn và không chứa vòng lặp cũng như đệ quy. Hơn nữa không phải hàm nào khai báo inline đều được hiểu là inline, vì đó chỉ là “gợi ý” cho compiler. Nếu compiler nhận thấy hàm có kích thước khá lớn, xuất hiện nhiều lần trong chương trình, hoặc có chứa các cấu trúc lặp, đệ quy thì nó có thể “lờ đi” yêu cầu inline này. Hiện này các compiler đều được tối ưu rất tốt, vì vậy không cần thiết phải dùng inline. Đó là sự khác biệt của các hàm thành viên được định nghĩa ngay trong lớp so với các hàm thành viên được định nghĩa bên ngoài.

Page 18: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

18 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

BÀI 5b. CLASSES & OBJECTS (PART 2) 3. Truy cập đến những thành phần của lớp Để truy cập đến các thành phần của lớp ta dùng toán tử chấm (selection dot operator) thông qua tên của đối tượng. Ví dụ đoạn chương trình sau gọi hàm set_name để nhập tên cho đối tượng studentA và gọi hàm get_name để lấy tên của đối tượng : C++ Code:

1. Student studentA; // khai báo đối tượng studentA thuộc lớp Student 2. studentA.set_name(“Bill Gates”); // gán tên cho studentA là “Bill Gates” 3. cout << studentA.get_name(); // in ra tên đối tượng studentA

Kết quả thu được là màn hình hiển thị dòng văn bản “Bill Gates”. Để ý lại định nghĩa của hàm set_name và get_name: C++ Code:

1. // set name 2. void Student::set_name(string str){ 3. name=str; 4. } 5. // get name 6. string Student::get_name(){ 7. return name;

8. }

Ta nhận thấy name là thành phần dữ liệu được khai báo private. Điều đó nghĩa là chỉ có những hàm thành viên mới có quyền truy nhập đến nó (sau này ta sẽ biết thêm một trường hợp nữa, đó là hàm bạn – friend, cũng có khả năng truy nhập đến các thành phần private). Hàm set_name và get_name là hai hàm thành viên của lớp Student nên nó có thể truy nhập và thao tác được trên dữ liệu name. Nhưng nỗ lực truy nhập trực tiếp và các thành phầnprivate mà không thông qua hàm thành viên như ví dụ sau sẽ gây lỗi biên dịch (compilation error): C++ Code:

1. Student studentA; // khai báo đối tượng studentA thuộc lớp Student 2. studentA.name=”Bill Gate”; // error

4. Ưu điểm của việc đóng gói dữ liệu và phương thức trong một đơn vị thống nhất – lớp Việc đóng gói dữ liệu kết hợp với quy định phạm vi truy nhập cho các thành phần của lớp có nhiều ưu điểm. Thứ nhất: tạo ra sự gọn gàng dễ kiểm soát. Việc đóng gói dữ liệu và các phương thức liên quan giúp chương trình gọn gàng hơn, lập trình viên dễ kiểm soát hơn vì tất cả đều được gói gọn trong phạm vi của lớp. Thứ hai: trừu tượng hóa dữ liệu, thông qua “giao diện”, tạo thuận lợi cho người

Page 19: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

19 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

dùng Việc cung cấp các hàm thành viên để thao tác trên các dữ liệu của đối tượng tạo sự “thân thiện” cho người dùng. Trong ví dụ lớp Student ở trên, để nhập tên cho một đối tượng ta chỉ cần gọi hàm set_name thông qua tên đối tượng mà không cần quan tâm đến cài đặt chi

tiết như thế nào. Thứ ba: tính bảo mật của dữ liêu được nâng cao Để truy cập đến các dữ liệu private của một đối tượng bắt buộc phải thông qua hàm thành viên. Tức mọi “giao tiếp” với đối tượng đều phải thông qua “giao diện” mà ta đã quy định trước. Ví dụ: nhập tên cho studentA thì bắt buộc phải dùng hàm set_name, lấy tên thì dùng get_name. Do đó sẽ tránh được những truy cập và sửa đổi bất hợp pháp, đồng thời

nếu phát sinh lỗi thì sẽ dễ khoanh vùng hơn. Ví dụ khi yêu cầu trả về mã số sinh viên củastudentA thì phát hiện một số lỗi nào đó. Rõ ràng những lỗi đó chỉ có thể do các hàm có liên quan trực tiếp đến student_code như set_student_codehoặc get_student_code chứ không thể là set_name hay get_name được.

Thứ tư: tăng cường tính độc lập và ổn định hơn cho các thành phần sử dụng lớp trong chương trình Giả sử vì một lý do nào đó mà thành phần name buộc phải đổi lại thành full_name thì

chương trình sẽ phải chỉnh sửa lại một chút. Tuy nhiên chỉ những hàm thành viên nào liên quan trực tiếp đến name mới phải sửa đổi, tức là các hàm set_name và get_name sẽ phải sửa lại name thành full_name. Tuy nhiên, các hàm gọi đến hàm set_name và get_name thì

không hề phải sửa lại, bởi vì nó không biết cài đặt chi tiết bên trong set_name và get_name như thế nào mà chỉ biết “giao diện” của set_name và get_name vẫn thế, do đó chương trình không phải chỉnh sửa nhiều.

BÀI 6. HÀM TẠO (CONSTRUCTOR) Bài này mình sẽ dành để viết về constructor trong C++. Tại sao phải dùng constructor, dùng nó như thế nào, và những vấn đề cần lưu ý khi sử dụngconstructor sẽ là những nội dung chính được đưa ra. 1. Vấn đề đặt ra Giả sử ta tạo ra một lớp Rectangle (hình chữ nhật) như sau: C++ Code:

1. #include <iostream> 2. #include <string> 3. using namespace std; 4. 5. // class definition 6. class Rectangle{ 7. private: 8. int width; // chiều rộng

Page 20: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

20 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

9. int height; // chiều cao 10. public: 11. // set width & height 12. void set_width(int); // nhập chiều rộng 13. void set_height(int); // nhập chiều cao

14. 15. // get width & height 16. int get_width(); // lấy chiều rộng 17. int get_height(); // lấy chiều cao

18. 19. // calculate area 20. int area(); // tính diện tích 21. }; 22. 23. // member function definitions 24. // set width 25. void Rectangle::set_width(int a){ 26. width=a; 27. } 28. 29. // set height 30. void Rectangle::set_height(int b){ 31. height=b; 32. } 33. 34. // get width 35. int Rectangle::get_width(){ 36. return width; 37. } 38. 39. // get height 40. int Rectangle::get_height(){ 41. return height; 42. } 43. 44. // calculate area 45. int Rectangle::area(){ 46. return height*width;

47. }

Điều gì sẽ xảy ra khi ta gọi hàm tính diện tích area trước khi thiết lập chiều rộng và chiều cao cho hình chữ nhật như trong đoạn chương trình sau:

Page 21: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

21 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

C++ Code: 1. Rectangle my_rectangle; // khai báo đối tượng my_rectangle thuộc lớp Rectangle 2. cout << my_rectangle.area() << endl; // in ra màn hình diện tích của

my_rectangle

Giá trị thu được trên màn hình có thể là một số âm ! Câu lệnh thứ nhất khai báo đối tượng my_rectangle, chương trình sẽ cấp phát bộ nhớ cho các thành phần dữ liệu width và height, giả sử width rơi vào ô nhớ mà trước đó có lưu trữ giá trị 20, còn height rơi vào ô nhớ trước đó có lưu trữ giá trị -3. Ngay sau đó, câu lệnh thứ hai yêu cầu tính diện tích của my_rectangle rồi hiển thị ra màn hình, và kết quả ta thu được là diện tích my_rectanglebằng -60 ! Để đảm bảo mọi đối tượng đều được khởi tạo hợp lệ trước khi nó được sử dụng trong chương trình, C++ cung cấp một giải pháp đó là hàm tạo (constructor). 2. Hàm tạo (constructor) Constructor là một hàm thành viên đặc biệt có nhiệm vụ thiết lập những giá trị khởi đầu cho các thành phần dữ liệu khi đối tượng được khởi tạo. Nó có tên giống hệt tên lớp để compiler có thể nhận biết được nó là constructor chứ không phải là một hàm thành viên giống như các hàm thành viên khác. Trong constructor ta có thể gọi đến các hàm thành viên khác. Một điều đặc biệt nữa là constructor không có giá trị trả về, vì vậy không được định kiểu trả về nó, thậm chí là void. Constructor phải được khai báo public. Constructor được gọi duy nhất một lần khi đối tượng được khởi tạo. Những lớp không khai báo tường minh constructor trong định nghĩa lớp, như lớp Rectangle ở trên của chúng ta, trình biên dịch sẽ tự động cung cấp một“constructor mặc định" (default constructor). Construtor mặc định này không có tham số, và cũng không làm gì cả. Nhiệm vụ của nó chỉ là để lấp chỗ trống. Nếu lớp đã khai báo constructor tường minh rồi thì default constructor sẽ không được gọi. Bây giờ ta sẽ trang bị constructor cho lớpRectangle: C++ Code:

1. class Rectangle{ 2. private: 3. int width; 4. int height; 5. public: 6. // constructor 7. Rectangle(); 8. 9. /* các hàm khác khai báo ở chỗ này */ 10. }; 11. 12. // member function definitions 13. // constructor 14. Rectangle::Rectangle(){ 15. width=0; 16. height=0; 17. }

Page 22: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

22 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

18. 19. /* các hàm khác định nghĩa ở đây */

Khi đó câu lệnh C++ Code:

1. Rectangle my_rectangle;

sẽ tạo ra một đối tượng my_rectangle có width=0 và height=0. 3. Thiết lập giá trị bất kỳ cho các thành phần dữ liệu khi khởi tạo đối tượng Một vấn đề được đặt ra là có thể khởi tạo những giá trị nhau khác cho các đối tượng ngay lúc khai báo không? Giống như với kiểu int: C++ Code:

1. int a=10; 2. int b=100; 3. int c=1000;

C++ hoàn toàn cho phép chúng ta làm điều này. Có một số cách để thiết lập những giá trị khác nhau cho các thành phần dữ liệu trong khi khai báo. Cách thứ nhất: viết thêm một hàm tạo nữa có tham số. C++ hoàn toàn không giới hạn số lượng constructor. Chúng ta thích viết bao nhiêu constructor cũng ok. Đây chính là khả năng cho phép quá tải hàmcủa C++ (function overloading), trong trường hợp của ta là quá tải hàm tạo. Tức là cùng một tên hàm nhưng có thể định nghĩa theo nhiều cách khác nhau để dùng cho những mục đích khác nhau. Để quá tải một hàm (bất kỳ) ta chỉ cần cho các hàm khác nhau về số lượng tham số , kiểu tham số còn giữ nguyên tên hàm. Tạm thời cứ thế đã, tớ sẽ đề cập rõ hơn trong một bài riêng cho functions. Bây giờ ta sẽ bổ sung thêm một constructor nữa vào định nghĩa lớp Rectangle: C++ Code:

1. class Rectangle{ 2. private: 3. int width; 4. int height; 5. public: 6. // constructor 7. Rectangle(); // hàm tạo không có tham số 8. Rectangle(int, int); // hàm tạo với hai tham số

9. 10. /* các hàm khác khai báo ở chỗ này */ 11. }; 12. 13. // member function definitions 14. // constructor with no parameters 15. Rectangle::Rectangle(){ 16. width=0;

Page 23: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

23 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

17. height=0; 18. } 19. 20. // constructor with two parameters 21. Rectangle::Rectangle(int a, int b){ 22. width=a; 23. height=b; 24. } 25. 26. /* các hàm khác định nghĩa ở đây */

Bây giờ ta sẽ test bằng chương trình sau: C++ Code:

1. Rectangle rectA; // gọi hàm tạo không tham số 2. Rectangle rectB(3,4); // gọi hàm tạo có tham số

3. 4. cout << rectA.area() << endl; // kết quả là 0 5. cout << rectB.area() << endl; // kết quả là 12

C++ sẽ tự nhận biết để gọi constructor phù hợp. Trong đoạn chương trình trên, câu lệnh thứ nhất khởi tạo đối tượng rectA nhưng không kèm theo truyền tham số vào, nên compiler sẽ gọi tới hàm tạo thứ nhất, tức hàm tạo không có tham số. Sau câu lệnh này rectA đều có width và height đều bằng 0. Câu lệnh thứ hai khởi tạo đối tượng rectB, nhưng đồng thời truyền vào hai đối số là 3 và 4. Do đó compiler sẽ gọi đến hàm tạo thứ hai. Sau câu lệnh này rectB có width=3 còn height=4. Và kết quả ta được diện tích thằng rectA là 0, còn rectB là 12. Cách thứ hai: dùng đối số mặc định (default arguments) Chúng ta vẫn làm việc với lớp Rectangle ở trên và sẽ chỉ dùng một hàm tạo nhưng “chế biến” nó một chút: C++ Code:

1. class Rectangle{ 2. private: 3. int width; 4. int height; 5. public: 6. // constructor 7. Rectangle(int =0, int =0); // hàm tạo với đối số mặc định

8. 9. /* các hàm khác khai báo ở chỗ này */ 10. }; 11. 12. // member function definitions 13. // constructor with default arguments

Page 24: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

24 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

14. Rectangle::Rectangle(int a, int b){ 15. width=a; 16. height=b; 17. } 18. 19. /* các hàm khác định nghĩa ở đây */

Chúng ta chú ý đến khai báo của hàm tạo: C++ Code:

1. Rectangle(int =0, int =0);

Khai báo này cho biết, khi khai báo đối tượng, nếu đối số nào bị khuyết (tức không được truyền vào) thì sẽ được mặc định là 0. Và để đảm bảo không xảy ra sự nhập nhằng, C++ yêu cầu tất cả những đối số mặc định đều phải tống sang bên phải nhất (rightmost), tức ngoài cùng bên phải. Vì vậy: C++ Code:

1. Rectangle rectA; // sẽ gán width=0, height=0 2. Rectangle rectB(4); // sẽ gán width=4, height=0 3. Rectangle rectC(2,6); // sẽ gán width=2, height=6

Chú ý: giá trị mặc định (ví dụ int =0) chỉ được viết lúc khai báo hàm, chứ không phải lúc định nghĩa hàm. Nếu ta viết lại những giá trị mặc định này trong danh sách tham số lúc định nghĩa hàm sẽ gây lỗi biên dịch. C++ Code:

1. // lỗi đặt đối số mặc định khi định nghĩa hàm 2. Rectangle::Rectangle(int a=0, int b=0){ // error 3. width=a; 4. height=b;

5. }

4. Hàm tạo mặc định Như đã nói ở trên, nếu ta không cung cấp hàm tạo cho lớp thì compiler sẽ làm điều đó thay chúng ta. Nó sẽ cung cấp một hàm tạo không tham số và không làm gì cả ngoài việc lấp chỗ trống. Đôi khi hàm tạo không có tham số do người dùng định nghĩa cũng được gọi là hàm tạo mặc định (hay ngầm định). Chúng ta xem xét chuyện gì sẽ xảy ra nếu như không có hàm tạo ngầm định khi khai báo một mảng các đối tượng. Ví dụ vẫn là lớp Rectangle với hàm tạo hai tham số: C++ Code:

1. class Rectangle{ 2. private: 3. int width; 4. int height; 5. public: 6. // constructor 7. Rectangle(int, int); // hàm tạo với hai tham số

Page 25: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

25 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

8. 9. /* các hàm khác khai báo ở chỗ này */ 10. }; 11. 12. // member function definitions 13. // constructor with 2 parameters 14. Rectangle::Rectangle(int a, int b){ 15. width=a; 16. height=b; 17. } 18. 19. /* các hàm khác định nghĩa ở đây */

Nếu như ta khai báo một mảng tầm chục thằng Rectangle thì chuyện gì sẽ xảy ra? C++ Code:

1. Rectangle my_rectangle(1,2); // 1 thằng thì ok 2. Rectangle rect_array[10]; // chục thằng thì có vấn đề - error

Điều này là do ta cần khai báo 10 thằng Rectangle nhưng lại không cung cấp đủ tham số

cho chúng, vì hàm tạo yêu cầu hai tham số cần phải được truyền vào. Giải quyết chuyện này bằng cách bổ sung thêm một hàm tạo không có tham số hoặc chỉnh lại tất cả các tham số của hàm tạo hai tham số bên trên thành dạng đối số mặc định là được

BÀI 7a. FUNCTIONS (PART 1)

- from alpha to omega - Bài này mình sẽ nói về một số vấn đề nâng cao về hàm trong C++. Vì vậy các bạn cần phải có một số kiến thức nhất định về hàm. Nói là nâng cao cho nó oách chứ thực ra nếu học C++ thì trước sau gì cũng phải biết đến mấy thứ này. Mình sẽ cố gắng trình bày thật đầy đủ dễ hiểu. Dưới đây là liệt kê những phần sẽ được đề cập trong bài:

Tại sao phải dùng hàm? Khai báo và định nghĩa hàm (function declarations & function definitions) Truyền đối số cho hàm (passing arguments to functions) Trả về giá trị của hàm (returning value from functions) Đối số mặc định (default argument) Quá tải hàm (function overloading) Hàm nội tuyến (inline function) Phạm vi và lớp lưu trữ (scope and storage classes) Vai trò của biến toàn cục (role of global variable) Đối hằng và hàm hằng (const arguments & const functions)

Page 26: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

26 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

1. Tại sao phải dùng hàm – why, why, why? Hàm là một tập các câu lệnh được nhóm lại dưới một cái tên, gọi là tên hàm, dùng để thực hiện một công việc xác định nào đó. Những vấn đề thực tế thường rất lớn và phức tạp. Không thể giải quyết kiểu “một phát xong ngay”. Kinh nghiệm của các bậc tiền bối trong lập trình cho thấy rằng, cách tốt nhất để phát triển cũng như bảo trì một phần mềm là phân chia và tổ chức nó thành những khối nhỏ hơn, đơn giản hơn. Kỹ thuật này được biết với tên gọi quen thuộc là “chia-để-trị” (devide-and-conquer). Tư tưởng chia-để-trị là một trong những nguyên lý quan trọng của lập trình cấu trúc, tuy nhiên lập trình hướng đối tượng cung cấp những cách thức phụ trợ mạnh mẽ hơn để tổ chức chương trình. Như mình đã nói trong bài 1, khi giải quyết một “công việc lớn” ta phải chia nhỏ công việc đó ra, mỗi phần sẽ quẳng cho một hàm đảm nhiệm. Nếu từng phần công việc vẫn còn lớn thì lại chia nhỏ tiếp cho tới khi đủ đơn giản, và tương tự cũng có các hàm tương ứng với những phần này. Đó là nguyên nhân thứ nhất dẫn đến việc sử dụng hàm. Một nguyên nhân nữa thúc đẩy việc sử dụng hàm là khả năng tận dụng lại mã nguồn. Một hàm khi đã được viết ra có thể được sử dụng lại nhiều lần. Ví dụ: hàm strlen trong thư viện <string.h> của C được viết để tính chiều dài của một xâu bất kỳ, vì vậy khi muốn tính độ dài của một xâu nào đó ta chỉ việc gọi hàm này là ok, thay vì lại phải viết một đoạn chương trình loằng ngoằng để đếm từng ký tự trong xâu. Nói túm lại, nếu bạn không muốn viết chương trình theo kiểu

“trâu bò” và “cục súc” thì bạn phải dùng hàm 2. Khai báo và định nghĩa một hàm (function declarations & function definitions) Một nguyên tắc muôn thủa của C và C++ là mọi thứ cần phải được khai báo trước lần sử dụng đầu tiên. Bạn không thể sử dụng một biến hay hàm nếu như không nói trước cho trình biên dịch biết điều đó (chắc compiler cho rằng hành động dùng mà không xin phép của

bạn là một sự "xúc phạm" với nó nên nó bực, nó không dịch cho ). Vì vậy trước khi sử dụng hàm ta phải khai báo. Nếu ta chỉ khai báo tên hàm còn viết định nghĩa thân hàm ở chỗ khác thì đó là sự khai báo bình thường (declaration) hay khai báo nguyên mẫu hàm (prototype). Còn nếu ta viết luôn cả thân hàm thì đó là một sự định nghĩa hàm (definition). Khai báo nguyên mẫu hàm (function prototype declaration) C++ Code:

1. <kiểu_trả_về> <tên_hàm>(danh_sách_tham_số); 2. Ví dụ: 3. int square(int); // tính bình phương của một số nguyên

Khai báo này giống như việc bạn nói với trình biên dịch: “này chú compiler, sẽ có một hàm kiểu như thế xuất hiện trong chương trình, vì vậy nếu chú nhìn thấy chỗ nào gọi cái hàm này thì đừng có xoắn, anh sẽ viết định nghĩa nó ở một xó nào đấy trong chương trình. Yên

tâm đi, anh không lừa chú đâu” Định nghĩa hàm (function definition) Bây giờ giả sử thằng compiler nó tạm thời “tin” theo lời chúng ta, rằng sẽ có định nghĩa đầy đủ cho cái nguyên mẫu được khai báo trên kia, và nó bắt đầu dịch tiếp. Giả sử nó gặp một câu lệnh như sau: C++ Code:

Page 27: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

27 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

1. x=square(y); // giả thiết x, y đã được khai báo trước

Vì đã được thông báo từ trước nên nó sẽ “không xoắn”, mà bắt đầu tìm định nghĩa cho hàm này, vì nó vẫn tin vào “lời hứa” của chúng ta. Nếu nó tìm mà không thấy, nghĩa là chúng ta đã “lừa” nó, nó sẽ báo lỗi. Vì vậy ta phải cung cấp định nghĩa cho hàm như đã cam kết. Dưới đây là định nghĩa cho hàmsquare: C++ Code:

1. int square(int n){ 2. return n*n;

3. }

Định nghĩa này bao gồm phần header (hay còn gọi là declarator) và theo sau nó là phần thân hàm (body). Phần header phải tương thích với nguyên mẫu hàm, nghĩa là phải có cùng kiểu trả về, cùng tên, cùng số lượng tham số và cùng kiểu tham số ở những vị trí tương ứng. Một số chú ý nhỏ

Tham số (parameters) khác với đối số. Tham số (hay còn gọi là tham số hình thức) là những biến tượng trưng ở trong danh sách tham số, xuất hiện lúc khai báo nguyên mẫu hoặc định nghĩa hàm, còn đối số là dữ liệu truyền vào cho hàm khi hàm được gọi. Ví dụ:

C++ Code:

1. int min(int a, int b); // a và b là tham số 2. minimum=min(x,y); // x, y đối số được truyền vào cho hàm

Trong danh sách tham số ở khai báo nguyên mẫu có thể chỉ cần nêu kiểu dữ liệu của của tham số mà không cần nêu tham số, lúc định nghĩa mới cần. Ví dụ

C++ Code:

1. int min(int, int); // khai báo nguyên mẫu không có tham số hình thức mà chỉ có kiểu

2. … 3. int min(int a, int b){ // bây giờ mới cần tham số hình thức 4. // thân hàm ở đây

5. }

3. Truyền đối số cho hàm (passing arguments to functions) Đối số (argument) là một mẩu dữ liệu nào đó như một giá trị nguyên, một ký tự thậm chí là cả một cấu trúc dữ liệu hết sức rối rắm như một mảng các đối tượng chẳng hạn, được truyền vào cho hàm. Có nhiều cách truyền đối số cho hàm, ta sẽ xem xét các cách này và phân tích ưu nhược điểm của chúng. Let’s go! Truyền hằng (passing constants) Xét hàm square ở trên, câu lệnh: C++ Code:

Page 28: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

28 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

1. x=square(10);

sẽ thực hiện tính bình phương của 10, rồi gán kết quả thu được cho biến x. Sau câu lệnh này x có giá trị là 100. Ta thấy đối truyền vào cho hàm squareở đây là một hằng số kiểu int. điều này hoàn toàn hợp lệ miễn là hằng truyền vào có kiểu tương thích với kiểu của tham số hình thức. Ta cũng có thể truyền cho hàm một hằng ký tự, hoặc hằng xâu ký tự. Ví dụ cho việc này là hàm printf của C. Truyền biến (passing variables) Đây là cách truyền đối số phổ biến nhất cho hàm. xét đoạn chương trình sau: C++ Code:

1. n=10; 2. x=square(n);

Kết quả thu được sau khi kết thúc đoạn chương trình trên vẫn là x=100. Tuy nhiên truyền biến cho hàm có một số điều “thú vị”. Ta có thể truyền biến cho hàm dưới hai hình thức là truyền bằng tham trị (pass-by-value) và truyền bằng tham chiếu (pass-by-reference). Mỗi cách có một ưu, nhược điểm riêng và ta sẽ phân tích chúng để đưa ra cách tối ưu nhất. a. Truyền bằng tham trị (pass-by-value) Xét đoạn chương trình sau: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int min(int a, int b){ 5. return (a<b?a:b); // trả về số nhỏ nhất trong hai số nguyên

6. } 7. 8. int main(){ 9. int x=5; 10. int y=10; 11. int z=min(x,y); // z là giá trị nhỏ nhất trong hai giá trị x, y

12. 13. cout << "min= " << z << endl; // hiển thị giá trị nhỏ nhất

14. 15. return 0;

16. }

Chúng ta đều đoán được kết quả là màn hình hiển thị min= 5, nhưng thực sự thì chương trình trên hoạt động như thế nào? Ta để ý vào câu lệnh: C++ Code:

1. int z=min(x,y);

Page 29: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

29 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Khi gặp câu lệnh này, compiler sẽ gọi đến hàm min và thực hiện truyền x và y làm đối số. Tuy nhiên, đây là truyền theo tham trị. Tức là x, y không được truyền trực tiếp vào trong hàm min mà compiler thực hiện một công đoạn như sau: đầu tiên nó tạo ra hai biến tạm a, b có kiểu int, rồi copy giá trị của x, y vào hai biến đó. Sau đó hai biến tạm đó được tống vào trong hàm min và thực tế hàm min đang thao tác trên “bản sao” của x và y chứ không phải trực tiếp trên x, y. Điều này có cái lợi mà cũng có cái hại. Cái lợi là do không bị thao tác trực tiếp nên các biến ban đầu (ở đây là x và y) sẽ không có khả năng bị "dính" những sửa đổi không mong muốn do hàm min gây ra. Còn cái hại là nếu như ta muốn sửa đổi giá trị của biến ban đầu thì lại không được (ví dụ muốn hoán đổi nội dung của hai biến x, y cho nhau) vì mọi thao tác là trên bản sao của x, y chứ không phải trên x, y. Thêm nữa, khi tạo bản sao cần phải tạo ra những biến tạm copy dữ liệu từ biến gốc sang biến tạm. Điều này gây ra những chi phí về bộ nhớ cũng như về thời gian, đặc biệt khi kích thước của các đối số lớn hoặc được truyền nhiều lần. b. Truyền theo tham chiếu (pass-by-reference) Như đã nói ở trên truyền theo tham trị không truyền bản thân biến vào mà chỉ truyền bản sao cho hàm. Do đó có những hạn chế nhất định của nó. Bây giờ mời bà con và cô bác ngâm cứu cách truyền thứ hai, truyền theo tham chiếu (passing-by-reference). Có hai cách để truyền theo tham chiếu làtruyền tham chiếu thông qua tham chiếu (pass-by-reference-with-references), và truyền tham chiếu thông qua con trỏ (pass-by-reference-with-pointers). Nghe có vẻ hơi lằng nhằng nhưng mình sẽ giải thích ngay bây giờ. Truyền tham chiếu thông qua con trỏ Chắc chắn các bạn đã quen thuộc với con trỏ rồi nên mình sẽ không nói nhiều về phần này. Tuy nhiên có thể mình sẽ dành ra một bài để viết riêng về mục con trỏ nếu thấy cần thiết để đảm bảo tính hệ thống. Nhắc lại, con trỏ là một biến đặc biệt lưu trữ địa chỉ của một biến mà nó trỏ tới. Cú pháp khai báo con trỏ cũng như cách sử dụng nó được mình họa trong chương trình sau: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int x; // khai báo một biến nguyên 6. int *ptr; // khai báo một con trỏ kiểu nguyên 7. ptr=&x; // ptr trỏ tới x hay gán địa chỉ của x cho ptr

8. 9. *ptr=10; // gán giá trị 10 cho vùng nhớ mà ptr trỏ tới, cụ thể ở đây

là x

10. cout << x << endl; // in giá trị của x, bây giờ là 10

11. 12. return 0;

13. }

Page 30: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

30 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Chương trình trên nhắc lại những kiến thức hết sức cơ bản về con trỏ. Bây giờ ta sẽ xem xét cách truyền đối số cho hàm thông qua con trỏ như thế nào. Ví dụ chương trình sau thực hiện việc hoán đổi nội dung hai biến cho nhau, một chương trình hết sức cổ điển gần như lúc nào cũng được lôi ra làm ví dụ khi nói về truyền đối số bằng con trỏ: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. void swap(int* a, int* b){ // hoán đổi nội dung hai biến cho nhau 5. int temp; 6. temp=*a; 7. *a=*b; 8. *b=temp; 9. } 10. 11. int main(){ 12. int x=5; 13. int y=7; 14. 15. // trước khi gọi swap 16. cout << "Before calling swap" << endl; 17. cout << "x= " << x << endl; 18. cout << "y= " << y << endl; 19. 20. // gọi swap 21. swap(&x, &y); 22. 23. // sau khi gọi swap 24. cout << "After calling swap" << endl; 25. cout << "x= " << x << endl; 26. cout << "y= " << y << endl; 27. 28. return 0;

29. }

Nhận thấy kết quả sẽ là Before calling swap x= 5 y= 7 After calling swap x=7 y=5

Mình sẽ giải thích về bản chất của cách truyền này. Để ý câu lệnh:

Page 31: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

31 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

C++ Code: 1. swap(&x, &y);

Câu lệnh này truyền địa chỉ của x và y chi hàm swap, và hàm swap cứ thế mò thẳng đến vùng nhớ của x và y mà thao tác. Điều này nghĩa mọi mọi thao tác trong hàm swap có thể làm thay đổi biến ban đầu, và do đó nó cho phép hoán đổi nội dung của x, y cho nhau. Truyền tham chiếu thông qua con trỏ cũng có cái lợi và cái hại. Cái lợi thứ nhất là nó cho phép thao tác trực tiếp trên biến ban đầu nên có thể cho phép sửa đổi nội dung của biến nếu cần thiết (như ví dụ hàm swap trên). Thứ hai, cũng do thao tác trực tiếp trên biến gốc nên ta không phải tốn chi phí cho việc tạo biến phụ hay copy các giá trị sang biến phụ. Cái hại là làm giảm đi tính bảo mật của dữ liệu. Ví dụ trong trường hợp hàm min ở trên ta hoàn toàn không mong muốn thay đổi dữ liệu của biến gốc mà chỉ muốn biết thằng nào bé hơn. Nhưng nếu truyền theo kiểu con trỏ như thế này có khả năng ta “lỡ” sửa đổi biến gốc và do đó gây ra lỗi (sợ nhất vẫn là những lỗi logic, nó không chạy thì còn đỡ, nó chạy sai mới đểu). Truyền tham chiếu thông qua tham chiếu Tham chiếu (reference) là một khái niệm mới của C++ so với C. Nói nôm na nó là một biệt danh hay nickname của một biến. Chương trình sau minh họa đơn giản cách sử dụng tham chiếu trong C++ C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int x; // khai báo biến nguyên x 6. int &ref=x; // tham chiếu ref là nickname của x

7. 8. ref=10; // gán ref=10, nghĩa là x cũng bằng 10 9. cout << x << endl; // in giá trị của x, tức là 10, lên màn hình 10. return 0;

11. }

Một lưu ý về tham chiếu là nó phải được khởi tạo ngay khi khai báo. Câu lệnh như sau sẽ báo lỗi: C++ Code:

1. int &ref; // lỗi không khởi tạo ngay khi khai báo

Mọi thay đổi về trên tham chiếu cũng gây ra những thay đổi tương tự trên biến vì bản chất nó là hai cái tên cho cùng một biến (giống như thằng Bờm với con của bố thằng Bờm là một thằng, giả thiết bố thằng Bờm chỉ đẻ được một thằng. Vì vậy ta cũng có thể dùng tham chiếu để truyền đối số cho hàm với tác dụng giống hệt con trỏ. Bây giờ ta sẽ cải tiến lại hàm swap bên trên bằng cách dùng tham chiếu. C++ Code:

1. #include <iostream> 2. using namespace std; 3.

Page 32: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

32 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

4. // hàm swap 5. void swap(int& a, int& b){ 6. int temp; 7. temp=a; 8. a=b; 9. b=temp; 10. } 11. 12. int main(){ 13. … 14. // gọi hàm swap 15. swap(x,y); 16. … 17. }

Nhận xét: về cơ bản tác dụng của việc truyền theo tham chiếu và truyền theo con trỏ là hòan toàn như nhau, tuy nhiên dùng tham chiếu sẽ tốt hơn vì nó làm cho “giao diện” của hàm thân thiện hơn. Hãy so sánh việc truyền tham số của hai cách: C++ Code:

1. // theo con trỏ 2. swap(&x, &y); 3. // theo tham chiếu 4. swap(x, y);

Rõ ràng thằng dưới nhìn “thân thiện” hơn thằng trên (tự dưng để cái dấu & ở trước trông nó chướng mắt .Hơn nữa tham chiếu đã gắn với biến nào rồi thì cố định luôn, không thay đổi được, còn con trỏ không thích trỏ biến này nữa thì có thể trỏ sang biến khác, nên nếu lỡ tay mà ta cho nó “trỏ lung tung” thì không biết đằng nào mà lần. Lợi ích của việc truyền tham chiếu hằng (const references) Bây giờ ta lại đặt ra vấn đề: liệu có cách nào tận dụng được tính an toàn bảo mật của truyền theo tham trị nhưng lại tận dụng được lợi thế về chi phí bộ nhớ và thời gian như truyền theo tham chiếu không? Câu trả lời đói là dùng tham chiếu hằng. Chúng ta sẽ xem chương trình sau: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int min(const int& a, const int& b){ 5. return (a<b?a:b); // trả về giá trị nhỏ hơn

6. } 7. 8. int main(){ 9. int x=5;

Page 33: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

33 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

10. int y=7; 11. int minimum=min(x,y); // gọi hàm min tính giá trị nhỏ nhất rồi gán cho

minimum

12. 13. cout << "minimum= " << minimum << endl; 14. 15. return 0;

16. }

Chú ý vào header của hàm: C++ Code:

1. int min(const int& a, const int& b)

Việc đặt từ khóa const trước kiểu của tham số a và b như trên được gọi là truyền theo tham chiếu hằng. Với từ khóa const này, ta vẫn truyền trực tiếp biến x, y vào cho hàm min nhưng hàm min không có quyền “sửa đổi” giá trị của x, y mà chỉ được dùng những thao tác không làm ảnh hưởng đến x, y như so sánh, lấy giá trị của x, y để tính toán, … Nếu cố tình sửa đổi x, y sẽ gây lỗi. Xét một ví dụ như sau: C++ Code:

1. int example(const int& a){ 2. a=20; // lỗi vì cố tình sủa đổi tham chiếu hằng 3. return a;

4. }

Việc sử dụng tham chiếu hằng như trên là một ví dụ về nguyên tắc “quyền ưu tiên tối thiểu” (the principle of least privilege), một nguyên tắc nền tảng trong lập trình. Trong trường hợp này nghĩa là chỉ trao cho hàm min những quyền ưu tiên tối thiểu thao tác trên dữ liệu để nó đủ thực hiện nhiệm vụ, không hơn. Rõ ràng hàm min chỉ cần so sánh hai đối số truyền vào để xem thằng nào nhỏ hơn rồi trả về giá trị. Vì vậy truyền theo tham chiếu hằng là phương án đảm bảo nguyên tắc trên. Truyền cấu trúc dữ liệu (passing data structures) Tạm thời mình chỉ giới thiệu cấu trúc đơn giản nhất là mảng (arrays). Còn những cấu trúc dữ liệu phức tạp hơn, nếu có điều kiện mình sẽ nói trong dịp khác. Như ta biết tên mảng là một con trỏ hằng, trỏ đến phần tử đầu tiên của mảng. Vì vậy truyền mảng giống như truyền con trỏ vậy. Chương trình sau gọi hàm input để nhập các phần tử vào một mảng, và output để xuất các phần tử của mảng: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. void input(int*, int); // nguyên mẫu hàm input 5. void output(int*, int); // nguyên mẫu hàm output

6. 7. int main(){

Page 34: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

34 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

8. int num; // biến lưu số lượng phần tử mảng 9. int *ptr; // con trỏ quản lý mảng

10. 11. cout << "Enter number of elements: " << endl; 12. cin >> num; // nhập số lượng phần tử mảng 13. ptr=new int[num]; // cấp phát bộ nhớ động cho con trỏ ptr

14. 15. cout << "Enter elements: " << endl; 16. input(ptr, num); //nhập mảng

17. 18. cout << "Here are elements of the array: " << endl; 19. output(ptr, num); // xuất mảng

20. 21. return 0; 22. } 23. 24. // định nghĩa hàm input 25. void input(int* a, int n){ 26. for(int i=0; i<n; i++){ 27. cout << "element "<< i+1 << "= "; 28. cin >> a[i]; 29. } 30. } 31. 32. // định nghĩa hàm output 33. void output(int* a, int n){ 34. for(int i=0; i<n; i++){ 35. cout << a[i] << " "; 36. } 37. }

nếu test thử kết quả sẽ như sau Enter number of elements: 4 Enter elements: element 1= 1 element 2= 2 element 3= 0 element 4= 8 Here are elements of the array: 1 2 0 8

Lưu ý, do mảng tương tự con trỏ nên truyền mảng bao giờ cũng là truyền theo tham chiếu, không phải theo tham trị.

Page 35: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

35 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

BÀI 7b. FUNCTIONS (PART 2)

- from alpha to omega - 4. Trả về giá trị của hàm (returning value from functions) Khi hoàn tất nhiệm vụ, hàm có thể trả về một giá trị nào đó cho tên hàm. Ví dụ hàm square trả về giá trị là bình phương của đối số truyền vào. Kiểu trả về của hàm quyết định kiểu của giá trị được trả về. Nó có thể là bất cứ kiểu built-in nào (như char, in, long, double, … ) hoặc các kiểu người dùng định nghĩa như Rectangle hay Student mà ta đã xây dựng ở những bài trước. Hàm cũng có thể trả về giá trị là một con trỏ hoặc một tham chiếu. Phần này mình sẽ tập trung vào những vấn đề cần lưu ý khi trả về một con trỏ hay hoặc một tham chiếu cho tên hàm. Trả về một con trỏ (returning a pointer) Khi nào ta dùng hàm để trả về con trỏ? Có rất nhiều trường hợp bạn trả lại con trỏ cho lời gọi hàm. Nhắc lại, xâu ký tự là một con trỏ hằng. Bây giờ mình sẽ viết một chương trình convert một xâu ký tự thành chữ hoa. Trong chương trình có hàm to_upper nhận vào một xâu ký tự ASCII-8 bit, đổi hết các ký tự thành ký tự hoa, rồi trả về xâu viết hoa: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. // hàm đổi sang chữ hoa 5. char* to_upper(char* str){ 6. int length=strlen(str); 7. 8. for(int i=0; i<length; i++){ // duyệt hết xâu 9. if(str[i]>=97 && str[i]<=122){ // nếu là chữ thường 10. str[i]-=32; // đổi thành chữ hoa

11. } 12. } 13. return str; // trả về xâu (là một con trỏ)

14. } 15. 16. // hàm main 17. int main(){ 18. char s1[]="Hey, baby ! you are crazy"; 19. cout << s1 << endl; 20. char* s2; 21. s2=new char[strlen(s1)]; // cấp phát bộ nhớ động cho con trỏ s2

Page 36: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

36 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

22. s2=strcpy(s2,to_upper(s1)); // copy kết quả đổi xâu s1 thành chữ hoa cho xâu s2

23. cout << s2 << endl; 24. 25. delete [] s2; 26. 27. return 0;

28. }

Kết quả sẽ là: Hey, baby ! you are crazy HEY, BABY ! YOU ARE CRARY

Đây là một ví dụ về việc trả về con trỏ cho hàm, tránh nhầm lẫn hàm trả về con trỏ với con trỏ hàm (function pointer). Bởi vì con trỏ hàm là một vấn đề tương đối phức tạp nên mình sẽ viết riêng một bài. Trả về tham chiếu (returning a reference) Hàm trả về tham chiếu tức là giá trị của hàm trả về là một tham chiếu đến một biến nào đó. Hàm trả về tham chiếu có dạng: C++ Code:

1. <kiểu_trả_về>& <tên_hàm>(danh_sách_tham_số){ 2. // thân hàm 3. return var; // trả về tham chiếu đến biến var

4. }

Như đã phân tích trong bài trên (7a) mục so sánh ưu nhược điểm giữa truyền theo tham chiếu và tham trị, thì dùng tham chiếu có lợi hơn về mặt hiệu suất (performance) vì nó cho phép thao tác trực tiếp trên các biến. Nói chung, trong mọi trường hợp dùng tham chiếu sẽ có lợi hơn tham trị, nếu cần đảm bảo an toàn cho dữ liệu thì dùng tham chiếu hằng. Vì vậy, chỗ nào có thể dùng được tham chiếu thì nên dùng. Nhưng lưu ý rằng nếu không cẩn thận sẽ rất dễ mắc lỗi, đó là hiện tượng “tham chiếu treo” (dangling reference), nghĩa là tham chiếu tới một đối tượng "không tồn tại", và gây là một lỗi logic. Chúng ta không thể dự đoán được hành vi của chương trình. Xét chương trình sau: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int& dangling_square(int n){ // hàm tính bình phương trả về một tham chiếu treo 5. int sqr=n*n; // sqr là biến cụ bộ 6. return sqr; // trả về tham chiếu tới sqr

7. } 8. 9. // hàm main 10. int main(){ 11. int x=5;

Page 37: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

37 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

12. int y=dangling_square(x); // tính bình phương của x rồi gán cho y

13. 14. cout << y << endl; 15. 16. return 0;

17. }

Mình đã test chương trình trên, kết quả nó vẫn chạy ngon lành, cho kết quả đúng. Thực sự mình cũng không hiểu thế này là thế nào? Về nguyên tắc thì việc trả về tham chiếu tới biến sqr như trong hàm dangling_square ở trên là “không ổn” , nhưng có thể là mấy cái compiler bây giờ nó được tối ưu tinh vi nên nhận biết được "ý định" của chúng ta. Nó issue một cái warning như sau (IDE mình dùng là Dev C++ 4.9.9.2) [Warning] reference to local variable `sqr' returned

Rõ ràng thằng compiler cũng nhận ra điều gì đó “không ổn”. Tại sao nó lại “không ổn”? Để ý hàm dangling_square ta thấy biến sqr được khai báo trong hàm, do đó nó là một biến cục bộ (local variable). Nó chỉ “sống” khi hàm dangling_square thực thi. Khi hàm kết thúc nó cũng “die” theo hàm luôn. Ở chương trình trên của ta, trước khi chết nó kịp return một cái tham chiếu. Trong hàm main câu lệnh: C++ Code:

1. int y=dangling_square(x);

sẽ gán giá trị của dangling_square(x) cho y, nhưng giá trị này là một tham chiếu tới một thằng đã "chết", vì vậy ta hoàn toàn không thể dự đoán được hành vi của chương trình. Khi nào ta có thể sử dụng tham chiếu mà không sợ bị dính tham chiếu treo? Câu trả lời là tất cả những trường hợp mà biến trả về vẫn “sống” sau khi hàm kết thúc. Ví dụ các biến trả về có phạm vi rộng hơn phạm vi của hàm (như các biến toàn cục), hàm thành viên trả về tham chiếu tới các thành phần dữ liệu. Tuy nhiên chẳng ai làm điều này cả bởi vì nó phá vỡ tính bảo mật của dữ liệu. Thông qua tham chiếu này dữ liệu có thể bị sửa đổi, điều này giống như “đục một cái lỗ qua bức tường private” vậy. 5. Đối số mặc định (default arguments) Cái này mình đã nói qua trong bài nói về constructor, bây giờ mình sẽ nói rõ hơn. Khi khai báo một hàm ta có thể chỉ định những giá trị mặc định cho các tham số. Nếu như khi gọi hàm, những đối số tương ứng với những vị trí này bị khuyết (không được truyền) thì những giá trị mặc định sẽ được thay thế vào đó. Do đó chúng được gọi là đối mặc định (default arguments). Xét chương trình sau: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int default_arg(int , int =1, int =2); // nguyên mẫu hàm với hai đối mặc định

5. 6. int default_arg(int a, int b, int c){ // định nghĩa hàm 7. return a*b*c; 8. };

Page 38: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

38 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

9. 10. int main(){ 11. 12. // truyền đủ đối, các giá trị mặc dịnh không được dùng 13. cout << default_arg(2, 3, 4) << endl; 14. 15. // khuyết đối ở vị trí thứ 3, giá trị mặc định c=2 được dùng 16. cout << default_arg(2, 3) << endl; 17. 18. // khuyết đối ở vị trí thứ 2 và thư 3, giá trị mặc định b=1, c=2 được dùng 19. cout << default_arg(2) << endl; 20. 21. return 0;

22. }

Kết quả là 24 12 4

Để tránh sự nhập nhằng, C++ yêu cầu các đối mặc định phải được đặt sang vị trí bên phải nhất và thứ tự ưu tiên sử dụng giá trị mặc định sẽ từ phải sang trái. Ví dụ trong chương trình trên, câu lệnh: C++ Code:

1. cout << default_arg(2, 3) << endl;

chỉ truyền vào hai đối là 2 và 3, trong khi hàm yêu cầu ba đối, vì vậy vị trí thứ 3 là vị trí khuyết, và được sử dụng mặc định c=2. 6. Quá tải hàm (function overloading) C++ cho phép nhiều hàm trùng tên nhau trong cùng một phạm vi, miễn là danh sách tham số của chúng khác nhau (khác về số lượng tham số hoặcnếu cùng số lượng thì các tham số ở những vị trí tương ứng phải khác kiểu). Khả năng này được gọi là “quá tải hàm” (function overloading). Giả sử một hàm có tên overloaded_func được quá tải thành tầm chục cái hàm cùng tên thì khi bắt gặp lời gọi hàm overloaded_func, compiler sẽ xem xét qua chục hàm này để tìm ra hàm phù hợp nhất dựa vào việc so sánh các đối số truyền vào với danh sách tham số ở header hàm (về số lượng cũng như kiểu ở các vị trí tương ứng). Ví dụ như chương trình sau: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. // khai báo ba hàm cùng tên 5. int min(int, int); 6. int min(int, int, int); 7. double min(double, double);

Page 39: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

39 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

8. 9. // hàm tính min 2 số nguyên 10. int min(int a, int b){ 11. cout << "Call function 1: int min(int a, int b)" << endl; 12. int minimum=a; 13. if(b<minimum){ 14. return b; 15. } 16. return minimum; 17. } 18. 19. // hàm tính min 3 số nguyên 20. int min(int a, int b, int c){ 21. cout << "Call function 2: int min(int a, int b, int c)" << endl; 22. int minimum=a; 23. if(b<minimum){ 24. minimum=b; 25. } 26. if(c<minimum){ 27. minimum=c; 28. } 29. return minimum; 30. } 31. 32. // hàm tính min hai số double 33. double min(double a, double b){ 34. cout << "Call function 3: double min(double a, double b)" << endl; 35. float minimum=a; 36. if(b<minimum){ 37. return b; 38. } 39. return minimum; 40. } 41. 42. // hàm main 43. int main(){ 44. // gọi hàm thứ nhất 45. int x=min(2,5); 46. cout << x << endl << endl; 47. // gọi hàm thứ hai 48. int y=min(2, 3, -1);

Page 40: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

40 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

49. cout << y << endl << endl; 50. 51. // gọi hàm thứ ba 52. double z=min(2.4, 5.2); 53. cout << z << endl << endl; 54. 55. return 0;

56. }

Kết quả như sau: Call function 1: int min(int a, int b) 2 Call function 2: int min(int a, int b, int c) -1 Call function 3: double min(double a, double b) 2.4

Khi quá tải hàm có tham số mặc định thì cần phải hết sức chú ý bởi vì rất dễ dẫn đến sự nhập nhằng. Ví dụ ta có hàm min được quá tải thành hai hàm như sau: C++ Code:

1. int min(int a, int b); 2. int min(int a, int b, int c=0);

nếu bắt gặp câu lệnh C++ Code:

1. x=min(4, 6);

thì compiler không biết phải gọi hàm nào vì xét cả hai hàm đều hòan toàn hợp lệ. Nếu gọi hàm thứ nhất, min sẽ được truyền đủ 2 đối số và kết quả z=4. Còn nếu gọi hàm thứ hai, đối thứ 3 bị khuyết và được mặc định là 0, kết quả z=0. Điều này gây ra lỗi biên dịch.

BÀI 7c. FUNCTIONS (PART 3) - from alpha to omega -

7. Hàm nội tuyến (inline function) Khi tổ chức chương trình thành các hàm chương trình sẽ sáng sủa hơn, thuận tiện hơn cho debug và bảo trì. Tuy nhiên trong lúc thực thi ta phải “gọi hàm”. Việc gọi hàm tốn những chi phí về không gian và thời gian nhất định. Vì vậy C++ cung cấp khái niệm hàm nội tuyến (inline) để giảm bớt chi phí gọi hàm, đặc biệt là với những hàm có kích thước nhỏ. Để khai báo một hàm là nội tuyến, ta chỉ cần đặt từ khóa inline phía trước kiểu trả về của hàm. Ví dụ C++ Code:

1. inline int sqr(int x); // khai báo hàm sqr là inline

Page 41: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

41 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Khai báo một hàm inline gợi ý cho compiler sinh mã của hàm vào những nơi tương ứng mà nó được gọi. Điều này giúp tránh những lời gọi hàm, nhưng ngược lại nó làm tăng kích thước chương trình vì mỗi nơi có lời gọi hàm sẽ được chèn vào một bản copy mã đã được dịch của hàm. Compiler có thể lờ đinhững yêu cầu inline nếu nhận thấy kích thước hàm quá lớn, có chứa các cấu trúc lặp hoặc đệ quy. 7. Phạm vi (scope) Phạm vi của một đối tượng quyết định hai điểm: thứ nhất, nó quy định những đoạn code nào có thể tác động được lên đối tượng, thứ hai, nó quy định thời gian tồn tại của đối tượng (lifetime). Trong C++, nếu phân loại theo phạm vi thì sẽ có 3 loại biến: cục bộ, tham số hình thức, và biến toàn cục. a. Biến cục bộ (local variables) Biến cục bộ là những biến được khai báo bên trong một khối lệnh. Một khối lệnh (code block) là một nhóm các câu lệnh được nhóm lại bên trong cặp ngoặc móc {}. Ví dụ: C++ Code:

1. { 2. int local_var; // biến cục bộ của khối lệnh 3. // các câu lệnh ở đây

4. }

Một biến cục bộ chỉ được biết đến bên trong khối lệnh mà nó được khai báo, bên ngoài khối lệnh đó, nó không được biết đến. Nghĩa là chỉ những code bên trong khối lệnh mới có thể thao tác trên biến cục bộ, còn những code bên ngoài phạm vi khối lệnh thì không thể. Biến cục bộ được sinh ra khi câu lệnh khai báo nó được thực thi, và nó sẽ bị hủy khi chương trình thoát ra khỏi khối lệnh, và như vậy giá trị của nó cũng biến mất. Vì vậy biến cục bộ không lưu trữ giá trị giữa các lần chương trình bước vào khối lệnh. Hàm là một khối lệnh thông dụng nhất của C++. Mọi biến được khai báo trong hàm đều là cục bộ. Nghĩa là dữ liệu và code của hàm là của riêng hàm, các code bên ngoài (của hàm khác) không thể truy cập trực tiếp đến chúng mà phải thông qua lời gọi hàm. Lý do rất đơn giản, chúng có phạm vi khác nhau. Thông thường ta khai báo tất các biến được sử dụng trong hàm lên đầu tiên để dễ kiểm soát, tuy nhiên đó là vấn đề style. C++ cho phép khai báo ở bất kỳ chỗ nào, miễn là trước lần sử dụng đầu tiên, không nhất thiết là cứ phải ở đầu. b. Tham số hình thức (formal parameters) Tham số hình thức là những biến được khai báo trong danh sách tham số ở header của hàm. Nó hoàn toàn giống các biến cục bộ được khai báo bên trong hàm, ngọai trừ việc nó nhận (copy) giá trị của các đối số truyền vào cho hàm. c. Biến toàn cục (global variables) Biến toàn cục là những biến có phạm vi hoạt động toàn chương trình, có thời gian sống từ lúc chương trình bắt đầu đến khi chương trình kết thúc. Chúng được khai báo toàn cục, nghĩa là bên ngoài mọi hàm, kể cả main. Biến toàn cục có thể khai báo ở bất kỳ chỗ nào miễn là bên ngoài mọi hàm, và trước lần sử dụng đầu tiên. Theo quy ước biến toàn cục được khai báo ở đầu chương trình, dưới các chỉ thị tiền xử lý.

Page 42: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

42 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Một biến toàn cục có thể được truy cập từ bất kỳ đâu trong chương trình. Nó là dữ liệu chung của chương trình.Khi một biến toàn cục trùng tên với một biến cục bộ thì biến toàn cục bị biến cục bộ “che” mất. Xem xét chương trình sau: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int num=100; // biến toàn cục tên num

5. 6. int func(){ 7. int num=20; // biến cục bộ tên num 8. return num; 9. } 10. 11. int main(){ 12. 13. cout << "Global: " << num << endl; 14. cout << "Local: " << func() << endl; 15. 16. return 0;

17. }

Để thông báo cho chương trình biết ta đang làm việc với biến toàn cục ta có thể dùng toán tử phân giải phạm vi (scope resolution operator) :: như câu lệnh sau: C++ Code:

1. cout << ::num << endl; // làm việc với biến toàn cục num

Thông thường ta nên hạn chế sử dụng biến toàn cục đến mức tối đa có thể vì những lý do sau:

Thứ nhất: biến toàn cục chiếm dụng bộ nhớ trong toàn bộ thời gian chương trình thực thi, ngay cả khi chúng không cần thiết.

Thứ hai: khi sử dụng biến cục bộ là đủ, thì việc sử dụng biến toàn cục làm cho các hàm mất đi tính độc lập, vì phải phụ thuộc vào những yếu tố bên ngoài hàm.

Thứ ba: sử dụng nhiều biến toàn cục sẽ rất dễ dẫn đến lỗi vì các hàm có thể lạm dụng biến toàn cục, làm sai lệch các giá trị khiến các hàm khác sử dụng sau nó nhận giá trị sai, và nhiều hiệu ứng phụ khác nữa.

Page 43: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

43 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

BÀI 8. KIỂU DỮ LIỆU CƠ SỞ

1. Tổng quan về kiểu dữ liệu cơ sở (basic data types) Trong C++ có 7 kiểu cơ sở: char, wchar_t, int, float, double, bool, và void. Giả sử môi trường của chúng ta là một môi trường 32-bit thông thường:

char: các biến kiểu char có dung lượng 1 byte, được dùng để chứa các ký tự ASCII 8-bit (characters) hay bất kỳ một lượng 8-bit nào khác.

wchar_t: các biến kiểu wchar_t có dung lượng 2 bytes, được dùng để lưu giữ các ký tự Unicode (wide characters). Lý do là số lượng các ký tự ASCII là quá ít, không đủ để biểu diễn hết tất cả các ký tự của các ngôn ngữ và các ký hiệu khoa học. Vì vậy bảng mã Unicode được đề xuất để giải quyết vấn đề trên.

int: các biến kiểu int có dung lượng 4 bytes, được dùng để lưu trữ các giá nguyên (integers). Các biến này thường được dùng để điều khiển vòng lặp hoặc dùng trong các biểu thức điều kiện. Về bản chất, kiểu char cũng là một kiểu số nguyên.

float: các biến float có dung lượng 4 bytes, được dùng để lưu trữ các giá trị thực, dấu chấm động, độ chính xác đơn (floating-point number).

double: các biến double có dung lượng 8 bytes, được dùng để lưu trữ các giá trị thực, dấu chấm động, độ chính xác kép (double floating-point numbers).

bool: các biến kiểu bool (boolean) chỉ nhận một trong hai giá trị: true hoặc false. Theo quy ước của C/C++ thì giá trị zero sẽ ứng với false, còn những giá trị non-zero sẽ tương ứng với true. Thông thường khi convert từ bool sang int thì true bằng 1 còn false bằng 0.

void: void là một kiểu khá đặc biệt trong C/C++. Nó được gọi là kiểu không có giá trị (valueless). Kiểu void thường được dùng để khai báo các hàm không trả về giá trị, ép kiểu con trỏ để in ra địa chỉ, …

2. Khai báo biến Cú pháp chung của khai báo biến là: C++ Code:

1. <kiểu_dữ_liệu> <danh_sách_các_biến>; 2. Ví dụ: 3. int a, b, c; 4. char ch;

Ta cũng có thể khởi tạo giá trị cho biến ngay khi khai báo: C++ Code:

1. int a=10, b=50; 2. char ch=’T’; // ký tự phải được đặt trong dấu nháy đơn

3. Một số modifier cho kiểu dữ liệu C++ cho phép “chế biến” các kiểu char, int, double bằng cách thêm modifier vào trước những kiểu dữ liệu này. Có 4 modifiers là: signed, unsigned, short, long. Với kiểu int: có thể sử dụng được với cả 4 modifiers trên.

Page 44: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

44 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

C++ Code: 1. signed int x; // x được khai báo là kiểu nguyên có dấu. 2. unsigned int x; // x được khai báo là kiểu nguyên không dấu. 3. short int x; // x được khai báo là kiểu nguyên ngắn 4. long int x; // x được khai báo là kiểu nguyên dài

Bây giờ ta sẽ xem xét chi tiết. Kiểu int mặc định là số nguyên có dấu, vì vậy khai báo signed int là không cần thiết, chỉ int là đủ. Khi đó một biến kiểu int sẽ chứa được những giá trị nằm trong miền -32,768 đến 32,767. Còn khi khai báo unsigned int, số nguyên sẽ được hiểu là không dấu. Do đó miền giá trị nó sẽ mở rộng gấp đôi vì không phải tốn một bit làm bit dấu. Về vấn đề này các bạn cần đọc lại cách biểu diễn số nguyên dưới dạng nhị phân, mã bù một, bù hai … Mình sẽ không nói sâu về vấn đề này vì nó nằm ngoài phạm vi topic. Còn về modifier short và long thì theo quy ước thông thường của các compiler C++: kiểu long int sẽ có dung lượng tối thiểu bằng int, còn kiểu int sẽ có dung lượng tối thiểu bằng short int. Các bạn có thể kiểm tra điều này bằng toán tử sizeof. Đây là chương trình mình test thử trên IDE – Dev C++ 4.9.9.2 C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. 6. cout << "short int: " << sizeof(short int) << endl; 7. cout << "int: " << sizeof(int) << endl; 8. cout << "long int: " << sizeof(long int) << endl; 9. 10. return 0;

11. }

Kết quả: short int: 2 int: 4 long int: 4

Đối với kiểu char: ta có thể sử dụng signed và unsigned. Thông thường mặc định char là signed. Vì vậy không cần thiết phải thêm modifier signed, miền biểu diễn của char là từ -128 đến 127. Nếu sử dụng unsigned thì miền biểu diễn sẽ được mở rộng gấp đôi, từ 0 đến 255. Đối với double: ta có thể dử dụng long. Một biến kiểu long double có dung lượng 12 bytes, nghĩa là được mở rộng thêm 4 bytes so với double (8 bytes). Ta cũng có thể sử dụng nhiều modifier cho một kiểu dữ liệu nếu cần thiết và hợp lệ. Ví dụ: =c++ Code:

1. unsigned long int; 2. unsigned short int;

Page 45: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

45 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Dựa vào miền biểu diễn của các kiểu dữ liệu các bạn có thể giải thích được một số hiện tượng như trong chương trình sau: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. 6. char x=126; 7. x++; // bây giờ x=127 8. cout << (int)x << endl; // in ra số 127 9. x++; // ta hy vọng x bây giờ là 128 10. cout << (int)x << endl; // x có bằng 128 ???

11. 12. return 0;

13. }

Đây là kết quả (lưu ý rằng mình đã nói từ đầu, bài viết này được viết trong môi trường 32-bit điển hình. Hiện tại mình đang dùng Windows 7 _ Ultimate _ Copyright by Microsoft _ 15,000 VNĐ mua ở đại lý phân phối chính thức trên đường Lê Thanh Nghị). 127 -128

Hiện tượng này gọi là “tràn số” (overflow), vì giá trị của biến vượt ngoài miền biểu diễn mà kiểu char quy định. Vì vậy, giá trị của biến bị “quay vòng” lại. Nói chung các bạn không cần confused quá nhiều về vấn đề dung lượng chính xác cho một kiểu dữ liệu cơ bản như in, float hay double là bao nhiêu. Điều này tùy thuộc vào trình biên dịch và platform của bạn. 4. Các toán tử (operators) Kiểu dữ liệu không chỉ quy định miền biểu diễn của các biến thuộc kiểu đó mà nó con quy định các phép toán (hay toán tử - operators) được phép thao tác trên những biến đó. Ví dụ ta có thể lấy phần dư của một số kiểu int bằng toán tử % nhưng không thể làm điều này với kiểu double. Các toán tử của C++ có thể phân thành ba loại: toán tử số học (arithmetic), toán tử quan hệ và logic (relational and logical), và các toán tử thao tác trên bit(bitwise). Toán tử số học (arithmetic operator): bao gồm

Addition (+): toán tử cộng Substraction (-): toán tử trừ Multiplication (*): toán tử nhân Division (/): toán tử chia Modulus (%): toán tử chia dư Increment (++): toán tử tăng một đơn vị Decrement (--): toán tử giảm một đơn vị Negative (-): toán tử phủ định một ngôi

Page 46: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

46 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Chú ý rằng toán tử modulus (%) chỉ áp dụng được với giá trị nguyên (char, int, long, bool), không áp dụng được với float hay double. Nó trả về kết quả là phần dư của phép chia hai số nguyên. Ví dụ C++ Code:

1. int x=5, y=3; 2. cout << x%y << endl; // in ra kết quả là 2, vì 5 chia 3 dư 2

Toán tử division (/) khi áp dụng với số nguyên thì sẽ là phép chia nguyên. Nghĩa là kết quả trả về sẽ là phần nguyên của phép chia, muốn thu được kết quả chính xác ta phải “ép kiểu”. Ví dụ chương trình sau: C++ Code:

1. int x=5, y=2; 2. cout << x/y << endl; // in ra 2 3. cout << (double)x/y << endl; // in ra 2.5

Toán tử ++ và -- là hai toán tử chỉ có trong C++. Nó cho phép viết những câu lệnh hết sức súc tích. Nó có thể dùng cả ở dạng tiền tố (prefix) lẫn hậu tố (postfix). C++ Code:

1. ++x hoặc x++ : tương đương với x=x+1; 2. --x hoặc x-- : tương đương với x=x-1;

Hầu hết các compiler C++ đều sinh mã máy rất nhanh và hiệu quả cho cho các toán tử ++ và --. Vì vậy câu lệnh x++; hoặc ++x; sẽ nâng cao hiệu suất chương trình hơn là sử dụng câu lệnh gán x=x+1. Nếu các câu lệnh tăng/giảm giá trị của biến đứng độc lập, nghĩa là không được sử dụng trong các biểu thức tính toán, thì cách viết tiền tố và hậu tố là hoàn toàn như nhau. Tuy nhiên, nếu câu lệnh tăng/giảm giá trị này nằm trong biểu thức tính toán thì sẽ có sự khác biệt. Dạng tiền tố sẽ tăng giá trị của biến lên 1 đơn vị rồi mới sử dụng, còn dạng hậu tố sử dụng giá trị của biến xong rồi mới tăng giá trị biến lên 1. Xem xét chương trình sau. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int a=100; 6. int x=5, y=5; 7. 8. cout << a*(x++) << endl; // in ra 500 vì dùng x=5 trước rồi mới tăng x

thành 6

9. cout << a*(++y) << endl; // in ra 600 vì tăng y lên thành 6 rồi mới sử dụng

10. 11. return 0;

12. }

Page 47: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

47 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Toán tử quan hệ và logic (relational and logic operators) Các toán tử quan hệ thể hiện mối quan hệ giữa giá trị này với giá trị khác. Ví dụ: x>=5; y!=z; … Các toán tử logic thể hiện cách mà các giá trị logic (true/false) liên kết với nhau. Ví dụ (x>=3) && (x<=10), … Vì các toán tử quan hệ sinh ra các giá trị true/false nên nó thường đi kèm với các toán tử logic. Các toán tử quan hệ của C++ bao gồm: >, >=, <, <=, ==, !=. Các toán tử logic bao gồm: && (and), || (or), ! (not). Ý nghĩa của chúng hoàn toàn như trong đại số.

BÀI 9a. CÁC CẤU TRÚC ĐIỀU KHIỂN (PART 1) CONTROL FLOWS - Cấu trúc rẽ nhánh -

Như mình đã nói trong bài 1, lập trình cấu trúc (structured programming) được xây dựng dựa theo mô hình toán học của Bohm và Guiseppe. Theo đó một chương trình máy tính có thể được viết dựa trên ba cấu trúc là: tuần tự, rẽ nhánh và lặp. C++ được thiết kế không chỉ hỗ trợ lập trình hướng đối tượng mà còn cả cho lập trình cấu trúc vì vậy nó cung cấp những cấu trúc điều khiển (control flows) để thực hiện việc cài đặt các cấu trúc trên. Bài này mình sẽ giới thiệu chi tiết cách sử dụng cũng như những đánh giá sơ lược về các cấu trúc lựa chọn (selection), lặp (iteration) và các lệnh nhảy (jump statements). 1. Các lệnh lựa chọn (selection statements) Trong cuộc sống chúng ta luôn luôn phải ra những quyết định. Nếu bạn gái bạn phải lòng một gã nào đó bạn sẽ làm gì? Có rất nhiều phương án cho bạn lựa chọn:

A: tao sẽ giết chết thằng khốn đó ngay lập tức (hic, sư phụ thật dũng cảm và … dã man )

B: kệ chúng nó đi, nó đã muốn thế thì tao cũng chẳng giữ làm gì (bạn thật là độ

lượng ) C: tao sẽ chứng minh cho bạn gái của tao thấy rằng thằng khốn đó không đáng để

yêu. Tuy tao không giàu bằng nó, không đẹp trai bằng nó, không khéo ăn nói bằng nó, … blah, blah … nhưng chắc chắn một điều tao hơn nó. Đó là tao … biết lập trình và biết ít nhất 6 ngôn ngữ: C, C++, C#, Java, PHP, và tiếng Việt. (hự, cái khoản này

thì .. hic hic ) Trong lập trình cũng vậy, bạn sẽ phải đưa ra những lựa chọn trong những hoàn cảnh cụ thể. C++ cung cấp 2 lệnh lựa chọn là if (mà dạng đầy đủ là if … else) và switch. a. Câu lệnh if (if statement) Cú pháp cho câu lệnh if là: C++ Code:

1. // dạng đơn 2. if(condition){

Page 48: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

48 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

3. // câu lệnh ở đây

4. } 5. 6. // dạng đầy đủ 7. if(condition){ 8. // câu lệnh ở đây

9. } 10. else{ 11. // câu lệnh ở đây

12. }

Hoạt động của câu lệnh if (dạng đầy đủ) như sau: nếu điều kiện đúng (true) thì chương trình sẽ thực hiện câu lệnh ngay sau if nếu sai thì nó sẽ thực hiện câu lệnh ngay sau else. Chương trình sau đây minh họa cho hoạt động của lệnh if. Nó nhận một số nguyên nhập và từ bàn phím và xác định xem số đó là chẵn (even) hay lẻ (odd). C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int n; 6. 7. cout << "Enter an integer: "; 8. cin >> n; 9. 10. if(n%2){ 11. cout << n << " is ODD" << endl; 12. } 13. else{ 14. cout << n << " is EVEN" << endl; 15. } 16. 17. return 0;

18. }

Các câu lệnh if có thể lồng nhau (nested if) rất hay gặp. Một điều cần chú ý là else sẽ tương ứng với if gần nó nhất. Chương trình sau mô tả lệnh if lồng nhau. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int n;

Page 49: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

49 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

6. 7. cout << "Enter an integer in range from 0 to 2: "; // gợi ý nhập vào 1 số

nguyên từ 0 đến 2

8. cin >> n; 9. 10. if(n==0){ // nếu là số 0 11. cout << "Zero" << endl; 12. } 13. else{ // nếu không là số 0 14. if(n==1){ // nếu là số 1 15. cout << "One" << endl; 16. } 17. else{ // nếu cũng không là số 1 18. if(n==2){ // nếu là số 2 19. cout << "Two" << endl; 20. } 21. else{ // nếu cũng không là số 2 22. cout << "Ivalid" << endl; 23. } 24. } 25. } 26. 27. return 0;

28. }

Tuy nhiên, nhìn chương trình trên thật “rối rắm” và đau mắt. Một phong cách lập trình biểu thị cấu trúc if-else-if lồng nhau hay được sử dụng là “thang if-else-if” (if-else-if-ladder). Chương trình trên sẽ được viết lại như sau. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int n; 6. 7. cout << "Enter an integer in range from 0 to 2: "; 8. cin >> n; 9. 10. if(n==0){ 11. cout << "Zero" << endl; 12. } 13. else if(n==1){

Page 50: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

50 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

14. cout << "One" << endl; 15. } 16. else if(n==2){ 17. cout << "Two" << endl; 18. } 19. else{ 20. cout << "Invalid" << endl; 21. } 22. 23. return 0;

24. }

b. Câu lệnh switch Khi phải đối mặt với nhiều hơn hai lựa chọn thì ta có thể dùng thang if-else-if như trên. Tuy nhiên C++ cung cấp một cách khác, trong một số trường hợp hiệu quả hơn, đó là lệnh switch. Cú pháp cho câu lệnh switch như sau. C++ Code:

1. switch(expression){ 2. case constant1: 3. // các câu lệnh ở đây 4. break; 5. case constant2: 6. // các câu lệnh ở đây 7. break; 8. case constant3: 9. // các câu lệnh ở đây 10. break; 11. ... 12. default: 13. // các câu lệnh ở đây

14. }

Biểu thức expression của switch phải có giá trị nguyên (có thể là char, integer hoặc bool). Hoạt động câu lệnh switch như sau: giá trị của expression sẽ được so sánh lần lượt với giá trị của các case (tức là các constant1, constant2, …). Nếu phát hiện ra trường hợp nào “khớp” thì những câu lệnh sau case đó được thực hiện. Chú ý rằng nếu như không có lệnh nào “ngắt” (ví dụ break; chẳng hạn) thì nó cứ thế chạy tuột đến cuối thậm chí những case sau đó không khớp. Các câu lệnh sau nhãn default sẽ được thực hiện nếu như chạy qua hết các case mà không có trường hợp nào khớp. Chúng ta sẽ xem xét hai chương trình sử dụng switch sau đây. C++ Code:

1. // version 1 – without break

2. #include <iostream>

Page 51: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

51 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

3. using namespace std; 4. 5. int main(){ 6. int n; 7. 8. cout << "Enter an integer in range 0-5: "; 9. cin >>n; 10. 11. switch(n){ 12. case 0: 13. cout << "Zero" << endl; 14. case 1: 15. cout << "One" << endl; 16. case 2: 17. cout << "Two" << endl; 18. case 3: 19. cout << "Three" << endl; 20. case 4: 21. cout << "Four" << endl; 22. case 5: 23. cout << "Five" << endl; 24. default: 25. cout << "Invalid" << endl; 26. } 27. 28. return 0;

29. }

Điều gì sẽ xảy ra khi bạn nhập vào số 3. Kết quả sẽ như sau: Enter an integer in range 0-5: 3 Three Four Five Invalid

Như mình đã nói ở trên, một khi đã khớp với một trường hợp nào đó thì “tất cả những câu lệnh phía sau case đó sẽ được thực thi”. Vì vậy ta cần phải có cái gì đó để “ngắt” tại những thời điểm phù hợp và giải pháp là sử dụng lệnh break. Hãy xem xét phiên bản thứ hai của chương trình trên. C++ Code:

1. #include <iostream> 2. using namespace std; 3.

Page 52: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

52 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

4. int main(){ 5. int n; 6. 7. cout << "Enter an integer in range 0-5: "; 8. cin >>n; 9. 10. switch(n){ 11. case 0: 12. cout << "Zero" << endl; 13. break; 14. case 1: 15. cout << "One" << endl; 16. break; 17. case 2: 18. cout << "Two" << endl; 19. break; 20. case 3: 21. cout << "Three" << endl; 22. break; 23. case 4: 24. cout << "Four" << endl; 25. break; 26. case 5: 27. cout << "Five" << endl; 28. break; 29. default: 30. cout << "Invalid" << endl; 31. } 32. 33. return 0;

34. }

Bằng cách sử dụng lệnh break; chương trình sẽ hoạt động theo ý muốn của chúng ta. Khi gặp lệnh break; chương trình sẽ nhảy ra khỏi lệnh switch mà không thực thi những câu lệnh còn lại trong khối. Bây giờ ta sẽ so sánh một chút giữa lệnh if và switch:

Lệnh switch chỉ có thể kiểm tra tính bằng nhau để ra quyết định (khớp giữa giá trị của expression với giá trị của case), còn if có thể sử dụng mọi toán tử quan hệ và logic.

Biểu thức trong switch chỉ có thể nhận giá trị nguyên, còn biểu thức trong if có thể là bất kỳ kiểu hợp lệ nào, những giá trị khác 0 sẽ là true, bằng 0 sẽ là false.

Page 53: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

53 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Dùng switch (nếu có thể) sẽ hiệu quả hơn if lồng nhau. Chương trình trông sẽ sáng sủa hơn, kể cả là if-else-if ladder.

BÀI 9a. CÁC CẤU TRÚC ĐIỀU KHIỂN (PART 2) CONTROL FLOWS

- Cấu trúc lặp - 2. Các câu lệnh lặp (iteration statements) C++ cung cấp 3 câu lệnh lặp là for, while, và do while. Một đoạn chương trình viết được bởi một trong ba câu lệnh trên thì cũng có thể viết được qua hai câu lệnh còn lại. Chúng là tương đương nhau. Điều này thuộc về vấn đề programming style. Trong một số trường hợp, vòng lặp này có thể thích hợp hơn những cái còn lại. a. Vòng lặp for Cú pháp chung cho vòng lặp for như sau: C++ Code:

1. for(initialization, condition, increment){ 2. // câu lệnh ở đây

3. }

Initialization (biểu thức khởi tạo): thông thường đó là câu lệnh gán (assignment) khởi tạo giá trị cho biến điều kiển vòng lặp (control variable)

Condition (điều kiện tiếp tục vòng lặp): thông thường trước mỗi lần lặp, biến điều khiển phải so sánh giá trị của mình với một giá trị nào đó để quyết định xem vòng lặp có được lặp lại nữa hay không. Đây chính là biểu thức điều kiện tiếp tục vòng lặp.

Increment (thay đổi biến điều khiển): sau mỗi lần lặp biến điều khiển phải thay đổi theo một cách thức nào đó, ví dụ như tăng lên 1 đơn vị, hoặc giảm xuống 5 đơn vị chẳng hạn. Và điều này được thực hiện trong phần Increment.

Vòng for sẽ thực hiện việc lặp cho đến khi biểu thức điều kiện tiếp tục vòng lặp không còn đúng nữa. Chương trình sau mô tả sự họa động của vòng lặp for. Nó in ra bảng chữ cái (hoa) từ A-Z. C++ Code:

1. // chương trình in ra bảng chữ cái (chữ hoa)

2. #include <iostream> 3. using namespace std; 4. 5. int main(){ 6. 7. for(char i='A'; i<='Z'; i++){

Page 54: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

54 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

8. cout << i << endl; 9. } 10. 11. return 0;

12. }

Bây giờ ta sẽ chế biến vòng for một chút để biết thêm một số cách sử dụng vòng for. Hãy xem xét chương trình sau: C++ Code:

1. // in ra các cặp số tự nhiên có tổng bằng 9

2. #include <iostream> 3. using namespace std; 4. 5. int main(){ 6. 7. int x, y; 8. for(x=0, y=9; (x<=9) && (y>=0); x++, y--){ 9. cout << "x= " << x << " " << "y= " << y << endl; 10. } 11. 12. system("pause"); 13. return 0;

14. }

Để ý header của vòng for C++ Code:

1. for(x=0, y=9; (x<=9) && (y>=0); x++, y--)

Nhận thấy biểu thức khởi tạo có tới hai câu lệnh gán x=0 và y=9 được phân cách nhau bởi dấu phẩy (,). C++ cho phép chúng ta khởi tạo bao nhiêu biến cũng được miễn là các câu lệnh gán này phân cách nhau bởi dấu phẩy. Dấu phẩy này thực ra là một toán tử (comma operator), nó quy định các toán hạng ở hai bên của nó được đánh giá từ trái sang phải, có thể phát biểu như sau: “hãy thực hiện việc này, rồi việc này, rồi việc này …. “, trong trường hợp của ta thì nó có nghĩa là: “hãy gán x=0 rồi gán y=9”. Tương tự biểu thức thay đổi biến điều khiển cũng vậy: “hãy tăng x lên 1 đơn vị rồi giảm y đi một đơn vị”. Header của vòng for có thể khuyết đi một số phần, hoặc khuyết hết cũng chả sao. Miễn là dấu chấm phẩy ngăn cách giữa các phần vẫn đủ là được. Ví dụ chương trình sau. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5.

Page 55: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

55 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

6. int i=0; 7. for( ; ; ){ // header vòng for rỗng 8. int n; 9. cout << "\nIf you enter a negative number, program will exit"; 10. cout << "\nYour number: "; 11. cin >> n; 12. 13. if(n<0) 14. break; 15. else 16. cout << "Number is: " << n << endl; 17. } 18. return 0;

19. }

b. Vòng lặp while Cú pháp cho vòng lặp while như sau. C++ Code:

1. while(condition){ 2. // câu lệnh ở đây

3. }

Hoạt động của vòng while rất đơn giản. Nếu điều kiện vẫn còn đúng thì vòng while vẫn còn được lặp lại, tức là các câu lệnh bên trong vòng while vẫn được thực hiện, nó sẽ lặp cho tới khi điều kiện sai mới thôi. Chương trình sau sẽ sử dụng vòng while để thực hiện việc in ra số đảo ngược của một số nhập vào từ bàn phím. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int n; 6. 7. cout<< "Enter an integer: "; 8. cin >> n; 9. 10. while(n){ 11. cout << n%10; 12. n/=10; 13. } 14. cout << endl; 15.

Page 56: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

56 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

16. return 0;

17. }

c. Vòng lặp do while Vòng lặp for và while đều kiểm tra điều kiện ở đầu vòng lặp, vì vậy có thể xảy ra khả năng ngay ở lần kiểm tra đầu tiên điều kiện tiếp tục vòng lặp đã không thỏa mãn. Và do đó các câu lệnh trong thân vòng for và do while sẽ không được thực hiện lần nào. Khác với for và while, vòng lặp đo while thực hiện câu lệnh trước rồi mới kiểm tra điều kiện ở cuối vòng lặp. Vì vậy các câu lệnh bên trong vòng do while luôn được thực hiện ít nhất là một lần. Ngoài khác biệt duy nhất đó ra thì while và do while là hoàn toàn giống nhau. Cú pháp của vòng do while là. C++ Code:

1. do{ 2. // câu lệnh ở đây 3. } while(condition);

3. Các câu lệnh nhảy (jump statements) Khi chương trình thực thi một vòng lặp, nếu thỏa mãn một số điều kiện nào đó nó có thể thoát ra ngoài vòng lặp mà không cần phải thực hiện nốt các câu lệnh còn lại trong thân vòng lặp. Để làm được được ddieuf này C++ hỗ trợ 4 câu lệnh nhảy (jump statements) là: break, continue, goto và return. Tuy nhiên return là một trường hợp đặc biệt được sử dụng để trả lại giá trị cho hàm nên mình sẽ không nói ở đây. a. Sử dụng continue Chúng ta dùng continue khi muốn kết thúc sớm lần lặp hiện tại để chuyển qua lần lặp tiếp theo. Khi gặp câu lệnh continue; chương trình sẽ bỏ qua mọi câu lệnh còn lại bên trong vòng lặp để chuyển qua lần lặp mới. Chương trình sau mô tả hoạt động của continue. C++ Code:

1. // in ra các số chia hết cho 4

2. #include <iostream> 3. using namespace std; 4. 5. int main(){ 6. 7. for(int i=0; i<=100; i++){ 8. if(i%4) continue; // nếu không chia hết cho 4 9. cout << i << endl; 10. } 11. 12. system("pause"); 13. return 0;

14. }

Page 57: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

57 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

b. Sử dụng break continue chỉ dùng để kết thúc sớm một lần lặp, rồi lại chuyển ngay qua lần lặp tiếp theo. Để thoát hoàn toàn khỏi vòng lặp thì ta phải sử dụng lệnh break. Khi chương trình gặp câu lệnh break, nó sẽ thoát luôn khỏi vòng lặp và tiếp tục thực thi câu lệnh tiếp theo ngay sau vòng lặp. Điều này hoàn toàn giống như trong ví dụ về lệnh switch mà mình đã đề cập ở trên. Chương trình sau mô tả cách hoạt động của lệnh break cho vòng lặp while. C++ Code:

1. // chương trình tìm số tự nhiên có 3 chữ số nhỏ nhất chia hết cho 13

2. #include <iostream> 3. using namespace std; 4. 5. int main(){ 6. int i=100; // 100 là số tự nhiên nhỏ nhất có 3 chữ số

7. 8. while(i<=999){ // 999 là số tự nhiên lớn nhất có 3 chữ số 9. if(i%13==0){ // nếu i chia hết cho 13 10. cout << i << endl; 11. break; 12. } 13. i++; // nếu không duyệt tiếp

14. } 15. 16. return 0;

17. }

Chú ý: khi có nhiều vòng lặp lồng nhau thì lệnh break chỉ thoát ra khỏi vòng lặp hiện tại, chứ không phải thoát hết ra khỏi mọi vòng lặp. c. Sử dụng goto Câu lệnh goto là lệnh rẽ nhánh vô điều kiện (unconditional branch statement). Kinh nghiệm của các tiền bối đi trước cho thấy sử dụng lệnh goto sẽ là chương trình trở nên rối rắm và khó kiểm soát. Người ta thường gọi những đoạn code tối nghĩa như vậy là “spaghetti code”. Tuy nhiên có một vài trường hợp nó tỏ ra khá hữu dụng. Ví dụ khi ta muốn thoát từ vòng lặp trong cùng của một khối các vòng lặp lồng nhau, ra bên ngoài thì dùng goto là một phương án tốt, hoặc khi tốc độ xử lý chương trình là mối quan tâm hàng đầu thì goto có thể được chọn. Để sử dụng goto ta chỉ cần chỉ định một nhãn (label) để nó “nhảy tới”. Một nhãn là một định danh hợp lệ theo sau bởi dấu hai chấm. Chương trình sau in ra 100 số nguyên dương đầu tiên sử dụng lệnh goto. C++ Code:

1. // chương trình in ra 100 số nguyên dương đầu tiên

2. #include <iostream> 3. using namespace std; 4.

Page 58: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

58 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

5. int main(){ 6. int i=1; 7. 8. begin: // nhãn 9. cout << i << endl; 10. i++; 11. if(i<=100) 12. goto begin; // nhảy tới nhãn

13. 14. return 0;

15. }

Chú ý: goto chỉ nhảy từ bên trong khối lệnh ra bên ngoài khối lệnh chứ không thể nhảy từ bên ngoài vào bên trong khối lệnh được.

BÀI 10a. CON TRỎ (PART1) Con trỏ (pointer) là một đặc điểm vô cùng mạnh mẽ của C/C++. Nó cho phép chúng ta giải quyết nhiều vấn đề phức tạp một cách hiệu quả và linh hoạt. Có thể kể ra một vài ứng dụng cơ bản của con trỏ như: cho phép truyền đối số theo tham chiếu, tạo và thao tác trên các cấu trúc dữ liệu động (dynamic data structure) như danh sách liên kết (linked list), ngăn xếp (stack), hàng đợi (queue), cài đặt các cơ sở dữ liệu, con trỏ hàm … Bài này mình sẽ giới thiệu những đặc điểm và cách sử dụng cơ bản của con trỏ 1. Con trỏ là gì? Con trỏ hiểu nôm na là một biến lưu trữ địa chỉ của một vùng nhớ. Thông thường nó lưu trữ địa chỉ của các biến khác. Khi một con trỏ ptr lưu trữ địa chỉ của biến x, ta nói “ptr trỏ tới x” (ptr points to x). Do biến có thể thuộc nhiều kiểu dữ liệu khác nhau nên sẽ có nhiều kiểu con trỏ tương ứng. Đối với một kiểu T, thì kiểu con trỏ tương ứng sẽ là T*, và được gọi là “con trỏ đến kiểu T” (pointer to T). Điều này có nghĩa là, con trỏ là một biến (như một biến bình thường khác) có kiểu T* và có thể lưu trữ địa chỉ của một đối tượng kiểu T. Ví dụ sau minh họa cách khai báo và gán giá trị cho con trỏ. C++ Code:

1. int x=5; // khai báo và khởi tạo một biến nguyên 2. int* pi; // khai báo một con trỏ kiểu int* 3. pi=&x; // gán địa chỉ của x cho pi

Đoạn code ngắn trên có một số điều cần lưu ý: Thứ nhất: khi khai báo con trỏ chúng viết là int* pi, hay int *pi ? (dấu * viết gần kiểu int hay gần biến pi?). Xin trả lời là viết kiểu nào cũng được, compiler nó hiểu hai cách viết là như nhau. Vấn đề ở đây chỉ là style. Tuy nhiên, tại sao C++ không thống nhất cách viết mà bày vẽ ra những hai cách làm gì cho nó mệt? Để hiểu rõ vấn đề này cần phải nhìn lại lịch sử C++ một chút. C++ được Bjarne Stroustrup phát minh vào cuối những năm 70 của thế kỷ trước (khoảng năm 1979), nhưng mãi đến 1994 ANSI/ ISO mới dự thảo

Page 59: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

59 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

chuẩn hóa cho C++. Và quá trình chuẩn hóa này diễn ra trong tới 4 năm trời, chỉ vì một gã điên khùng có tên là Alexander Stepanove tự dưng nghĩ ra STL (Standard Template Library) và việc phải xem xét thêm một thư viện khổng lồ này làm chậm lại đáng kể quy trình chuẩn hóa. Nghĩa là từ lúc C++ ra đời đến phiên bản chuẩn hóa đầu tiên của nó mất tới 30 năm! Trong 30 năm đó, người ta đã dùng C++ như một ngôn ngữ chuyên nghiệp để lập trình và có tới hàng tá phiên bản cài đặt khác nhau của C++ được tung ra. Rõ ràng với một lượng code lớn như vậy hiện hữu trong các hệ thống máy tính đang vận hành thì chuẩn mới của C++ không thể đặt ra những cú pháp mới tinh được, mà phải dựa trên những cái có sẵn, nếu hợp lý thì cho làm chuẩn. Trong bản phác thảo đầu tiên của Stroustrup thì dấu * được viết gần với kiểu dữ liệu (int* pi thay vì int *pi) điều này phản ánh * là một phần của kiểu dữ liệu (tức kiểu int*) chứ không phải là một ký hiệu lạ, hay một toán tử mới. Và mình cũng thích viết theo cách này hơn. Trong tất cả các đoạn code của mình khi khai báo con trỏ hay tham chiếu thì dấu * hoặc & luôn được viết cùng với kiểu dữ liệu vì điều này phản ánh bản chất thực sự của con trỏ và tham chiếu. Tuy nhiên một số người lại thích viết theo kiểu: int *pi; hơn. Cách viết này không phải là không có cái lý của nó. Hãy xem xét đoạn code sau. C++ Code:

1. int* pi, a; // pi là một con trỏ int*, còn a là một biến int 2. int *pi, *a; // cả pi và a đều là con trỏ int

Câu lệnh thứ nhất rất dễ làm cho người ta lầm tưởng pi và a đều là hai con trỏ kiểu int*, nhưng thực tế chỉ có pi là con trỏ, con a lại là một biến nguyên. Vì vậy người ta nghĩ viết theo cách thứ hai sẽ hạn chế được những hiểu lầm như vậy. Tuy nhiên khi bạn đã hiểu rõ bản chất của con trỏ rồi thì vấn đề chỉ còn là phong cách. Bạn muốn viết theo style nào cũng được. Riêng mình vẫn thích cách viết int* hơn. Thêm nữa, mỗi khai báo nên đặt trên một dòng, để còn tiện comment khi cần thiết. Nhà giàu tiếc gì con lợn con, hờ hờ . Như vậy sẽ không còn sự nhập nhằng nữa. C++ Code:

1. int* p1; // comment vào chỗ này 2. int* p2; // comment vào chỗ này

Thứ hai: để ý đến câu lệnh C++ Code:

1. pi=&x;

Câu lệnh gán này thực hiện gán địa chỉ của biến x cho con trỏ pi, sau câu lệnh này ta nói pi trỏ đến x. Toán tử & là toán tử lấy địa chỉ, nó là toán tử một ngôi (unary operator) trả về địa chỉ của toán hạng (operand) bên phải nó (ở đây là x). Chúng ta sẽ xem xét thêm một số cú pháp khai báo các kiểu con trỏ hay gặp bao gồm con trỏ đa cấp, hàm trả về con trỏ, con trỏ hàm, mảng con trỏ, ... C++ Code:

1. int* pi; // con trỏ int* 2. char** ppc; // con trỏ hai cấp – con trỏ trỏ tới con trỏ char* 3. int* ap[100]; // mảng 100 con trỏ kiểu int* 4. int* func(char*); // hàm func nhận đối đầu vào là con trỏ char* và trả về con

trỏ int*

Page 60: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

60 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

5. int (*funcp)(char*); // con trỏ tới hàm funcp (hàm funcp nhận một đối là con trỏ char* và trả về kiểu int)

2. Thao tác trên con trỏ Hai toán tử * và & là hai toán tử quan trọng nhất thao tác trên con trỏ. Toán tử &, như đã nói ở trên, là toán tử một ngôi, trả về địa chỉ của toán hạng bên phải nó. Để hình dung khái niệm địa chỉ ta coi bộ nhớ như một khu phố. Bộ nhớ được chia thành các thành các vùng nhớ, giống như khu phố được phân chia thành các hộ gia đình. Mỗi một vùng nhớ được đánh số thứ tự giống như mỗi hộ gia đình có một số nhà. Số thứ tự này gọi là địa chỉ của vùng nhớ đó. Ta cũng cần phân biệt giữa địa chỉ vùng nhớ và nội dung chứa trong vùng nhớ. Địa chỉ giống như số nhà còn nội dung hay giá trị (value) chứa trong vùng nhớ giống như tài sản trong nhà vậy. Câu lệnh: C++ Code:

1. pi=&x;

đơn thuần chỉ cung cấp địa chỉ của x cho pi, nó không ảnh hưởng gì đến giá trị của x cả. Giả sử ban đầu x=5 thì sau câu lệnh trên x vẫn bằng 5. Khi con trỏ pi đã có trong tay địa chỉ của x, nó có thể “đột nhập” đến biến x để xem nội dung của x là gì, thậm chí thay đổi nó. Để làm điều này ta dùng toán tử truy xuất giá trị * (dereference). Toán tử * là toán tử một ngôi, trả về nội dung chứa trong vùng nhớ mà con toán hạng bên phải nó trỏ tới. Xem xét đoạn chương trình sau: C++ Code:

1. x=5; 2. pi=&x; 3. cout << *pi << endl; // in ra giá trị nằm trong vùng nhớ mà pi trỏ tới, tức in

ra 5

4. *pi=100; // thay đổi nội dung của x thông qua con trỏ pi 5. cout << x << endl; // in ra nội dung x, tức 100

Sử dụng con trỏ như trên được gọi là gián tiếp (indirection) vì nó thay đổi nội dung của biến thông qua một biến khác (biến trỏ). 3. Chú ý về kiểu và ép kiểu con trỏ Một câu hỏi được đặt ra là: thông qua con trỏ ta có thể gián tiếp thao tác trên biến. Vậy giả sử ta muốn copy giá trị của một biến a kiểu int, sang cho biến b kiểu int thông qua một con trỏ thì làm thế nào trình biên dịch biết được số lượng byte cần copy là bao nhiêu? (giả thiết môi trường của ta là 32 bit). Ví dụ đoạn chương trình sau. C++ Code:

1. a=5; 2. pa=&a; // pa trỏ đến a 3. b=*pa; // gán nội dung của biến a cho b thông qua con trỏ pa, tức b=5

Khi muốn copy giá trị của biến a cho biến p thông qua con trỏ pa trỏ tới a thì pa phải có kiểu tương thích với kiểu của a, tức pa có kiểu int*. Khi ta dùng pa để thao tác trên một vùng nhớ nào đó, pa luôn tự nhủ rằng mình đang trỏ đến một vùng nhớ kiểu int và mình chỉ được tọc mạch vào 4 bytes thôi, đo đó nó nó sẽ copy đúng 4 bytes dữ liệu tương ứng với địa chỉ được chỉ định. Chú ý rằng địa chỉ của a mà pa trỏ tới không phải là địa chỉ của cả 4 bytes dữ

Page 61: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

61 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

liệu của a, mà là địa chỉ byte đầu tiên của vùng nhớ đó. Còn việc đếm 3 byte còn lại như thế nào thì là việc của compiler. Nếu pa là kiểu int* còn a, b là kiểu double, muốn thực hiện đoạn code trên ta phải “ép kiểu con trỏ”. Xem xét chương trình sau. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. double a=12.34, b; 6. int* pa; 7. 8. pa=(int*)&a; // ép kiểu double* thành int* 9. b=*pa; 10. 11. cout << b << endl; 12. 13. return 0;

14. }

Bạn hãy chạy thử chương trình trên và xem kết quả ! Tại sao kết quả lại sai? Rất đơn giản khi câu lệnh C++ Code:

1. pa=(int*)&a;

được thực hiện thì pa vẫn nhận đúng địa chỉ của vùng nhớ của a, nhưng khi nó mò tới vùng nhớ này, thay vì nó copy đủ 8 bytes dữ liệu của a cho b, thì nó chỉ copy 4 bytes ! (vì nó vẫn tự nhủ với mình rằng nó là con trỏ kiểu int* và nó chỉ được tọc mạch vào 4 bytes chứ không phải 8 bytes) Đây là một lỗi logic hết sức tinh vi, nếu không nắm rõ bản chất của con trỏ bạn sẽ rất dễ mắc lỗi này khi thực hiện “ép kiểu” linh tinh. 4. Con trỏ NULL Sau khi một con trỏ được khai báo, nếu như ta không gán giá trị cho nó, nó sẽ chứa một giá trị tùy ý (tức trỏ lung tung tới một vùng nhớ nào đó). Nếu ta sử dụng nó trước khi gán cho nó một giá trị hợp lệ, thì không chỉ chương trình của ta có thể bị “toi” mà có khi cả hệ điều hành của ta cũng bị “đi” luôn. Bởi vì rất có thể khi khởi tạo, con trỏ trỏ tới một vùng nhớ có chứa dữ liệu quan trọng của HĐH. Và khi ta gián tiếp thay đổi nội dung vùng nhớ đó thông qua con trỏ thì hậu quả không biết đằng nào mà lần. Theo quy ước, nếu một con trỏ chứa giá trị là 0 thì nó được hiểu là không trỏ tới cái gì cả. Bởi vì một lý do hết sức đơn giản: không một đối tượng nào được phép tồn tại trong khu vực địa chỉ là 0. Bất cứ con trỏ nào đều có thể được khởi tạo là NULL khi khai báo bằng cách gán nó bằng 0. Ví dụ: C++ Code:

1. int* pi=0; 2. double* pd=0; 3. char* pc=0;

Page 62: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

62 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

BÀI 10b. CON TRỎ (PART 2) 5. Phép toán trên con trỏ Các phép toán hợp lệ được sử dụng trên con trỏ bao gồm

Cộng/trừ con trỏ với một số nguyên: pa=pb+5; Trừ hai con trỏ cùng kiểu cho nhau: x=pa-pb; Tăng giảm con trỏ lên một đơn vị: pa++; pb-- So sánh hai con trỏ: ==, !=, >, <, >=, <=

a. Các phép toán số học trên con trỏ: +, -, ++, -- Khi cộng/trừ một con trỏ với một số nguyên, thì con trỏ sẽ “dịch” đi một đoạn “tương ứng” (không có nghĩa là bằng nhé, chỉ “tương ứng” thôi) với số nguyên đó. Xét đoạn chương trình sau. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int a[10]; 6. int* pa=a; 7. 8. cout << "Before: " << (void*)pa << endl; // in địa chỉ pa trước khi dịch

chuyển

9. pa=pa+5; // dịch pa đi 5 đơn vị 10. cout << "After: " << (void*)pa << endl; // địa chi pa sau khi dịch chuyển

11. 12. return 0;

13. }

Kết quả mình test là: Before: 0x22ff10 After: 0x22ff24

Nghĩa là nó pa đã dịch đi một đoạn là 14 ứng với số hexa hay 20 với decimal, tức là dịch qua 5 vùng nhớ kiểu int (vì mỗi biến int rộng 4 bytes). Từ ví dụ trên ta thấy khoảng dịch của con trỏ phụ thuộc vào kiểu mà nó trỏ tới. pa=pa+5 không có nghĩa là nó dịch đi 5 bytes mà nó dịch đi một đoạn ứng với 5 đối tượng có kiểu int (tức 20 bytes). Tương tự cho phép toán ++ và --. Các bạn có thể viết chương trình và test thử. C++ không cho phép cộng hai con trỏ cho nhau, nhưng trừ hai con trỏ cho nhau thì hoàn toàn hợp lệ. Tuy nhiên phép trừ hai con trỏ “chẳng liên quan gì đến nhau” là một phép trừ

Page 63: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

63 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

vô nghĩa, mặc dù C++ không cấm điều này. Thông thường ta chỉ trừ những con trỏ liên quan đến nhau, ví dụ các con trỏ trỏ đến các phần tử trong một mảng, kết quả trả về là “khoảng cách: giữa hai con trỏ. Xét chương trình sau. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int a[10]; 6. int* p1; 7. int* p2; 8. 9. p1=&a[0]; // p1 trỏ đến phần tử đầu mảng 10. p2=&a[9]; // p2 trỏ đến phần tử cuối mảng

11. 12. cout << p2-p1 << endl; // in ra khoảng cách giữa hai con trỏ

13. 14. return 0;

15. }

Khi chạy chương trình trên bạn sẽ thấy kết quả là 9 ! Nghĩa là p1 và p2 cách nhau 9 đối tượng kiểu int, tức 36 bytes (chứ không phải 9 bytes). b. Các phép toán quan hệ trên con trỏ: ==, !=, >=, <=, >, < Ta có thể so sánh hai con trỏ xem nội dung của chúng (địa chỉ của vùng nhớ mà chúng trỏ tới) có bằng nhau hay không, vùng nhớ nào cao hơn, thấp hơn, … Xem xét chương trình sau. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int a[10]; // mảng 10 số nguyên 6. int* p=&a[9]; // con trỏ p trỏ đến phần tử cuối mảng

7. 8. cout << "Input 10 integers: \n"; // nhập vào 10 số nguyên từ cuối đến đầu

mảng

9. while(p>=a){ // khi p còn chưa trỏ về đầu mảng 10. cin >> *p; // nhập dữ liệu vào các phần tử tương ứng 11. p--; // dịch con trỏ về đầu mảng

12. } 13. 14. cout << "Reverse: \n"; // in ra thứ tự đảo ngược

Page 64: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

64 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

15. p=a; // cho con trỏ p trỏ về đầu mảng 16. while(p<=&a[9]){ // chừng nào p chưa trôi về cuối mảng 17. cout << *p << endl; // in ra các phần tử mảng tương ứng 18. p++; // dịch con trỏ p về cuối mảng

19. } 20. 21. return 0;

22. }

Ngoài các phép toán trên, đối với những con trỏ trỏ đến một mảng thì ta cũng có thể dùng phép toán lấy chỉ số như thao tác với tên mảng. Ví dụ chương trình sau: C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int a[5]; 6. int* pa=a; // con trỏ pa trỏ đến đầu mảng

7. 8. cout << "Input array:\n"; 9. for(int i=0; i<5; i++){ 10. cin >> pa[i]; // thao tác qua p, với chỉ số

11. } 12. 13. cout << "Ouput array:\n"; 14. for(int i=0; i<5; i++){ 15. cout << pa[i] << " "; // thao tác qua p, với chỉ số

16. } 17. 18. return 0;

19. }

6. Con trỏ và hằng (pointers and constants) C++ cung cấp khái niệm hằng (const) để thể hiện rằng đối tượng nào đó là cố định, không thể thay đổi được. Điều này có những lợi ích nhất định tùy thuộc vào ngữ cảnh. Ví dụ: bằng việc khai báo hằng tượng trưng (symbolic constant) sẽ giúp việc viết chương trình được thuận lợi và dễ bảo trì hơn là việc “nhúng” hẳn các hằng trực tiếp vào trong code. Ví dụ: C++ Code:

1. const int PI=3.14; // như thế này tốt hơn là nhúng hẳn số 3.14 và từng đoạn code

Hoặc ví dụ trong một số trường hợp, khi ta truyền biến bằng tham chiếu cho hàm nhưng chỉ cho phép hàm đọc dữ liệu mà không cho phép chỉnh sửa dữ liệu thì tham số hình thức của

Page 65: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

65 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

hàm sẽ được khai báo là tham chiếu hằng hoặc con trỏ hằng. Sau đây là một số ví dụ khai báo hằng: C++ Code:

1. const int x=12; // x là hằng nguyên, giá trị 12 2. const char ch[]={‘I’, ‘H’, ‘A’, ‘T’, ‘E’, ‘U’}; // ch là con trỏ hằng (const

pointer) trỏ tới phần tử đầu mảng

3. const int number; // Error: phải khởi tạo hằng ngay khi khai báo

Khi sử dụng con trỏ có hai đối tượng cần quan tâm: thứ nhất là bản thân con trỏ và thứ hai là đối tượng mà con trỏ trỏ tới. Ta phân biệt hai khái niệm: con trỏ hằng (const pointer, một số người thì gọi là hằng con trỏ, cái này tùy thuộc vào cách dịch) và con trỏ tới hằng (pointer to const). Con trỏ hằng (const pointer): là con trỏ luôn luôn trỏ tới một địa chỉ cố định, và không thể thay đổi được trong suốt thời gian thực thi chương trình. Mọi nỗ lực sử đổi như gán địa chỉ mới cho con trỏ hằng hay tăng giảm con trỏ hằng đều gây lỗi biên dịch. Ví dụ quen thuộc nhất về con trỏ hằng là tên mảng: C++ Code:

1. // tên mảng là một const pointer 2. char str[]=”I am first_pace, from congdongCViet”; // str là con trỏ hằng, nhưng

các str[i] không phải hằng

3. char* p1; 4. char* p2; 5. p1=str; // ok: vì p1 không phải con trỏ hằng nên được phép trỏ đi chỗ khác 6. str=p2; // error: vì str là con trỏ hằng nên không thể trỏ đi chỗ khác được

Con trỏ tới hằng (pointer to const): là con trỏ trỏ tới một đối tượng hằng, tức là với con trỏ đó, đối tượng được "hiểu" là hằng. Con trỏ này có thể đọc (read) dữ liệu của đối tượng nhưng không được sửa đổi (write) nội dung của đối tượng. Mọi nỗ lực sửa đổi nội dung của đối tượng sẽ gây ra lỗi biên dịch.Con trỏ tới hằng khác con trỏ hằng ở hai điểm: thứ nhất, khi không thích trỏ tới đối tượng này nó có thể trỏ tới đối tượng khác, con trỏ hằng không được phép làm điều này. Thứ hai, con trỏ tới đối tượng hằng (pointer to constant) thì nó hiểu đối tượng là hằng, và không được phép “ghi” dữ liệu lên đối tượng, còn con trỏ hằng nó vẫn cho phép thao tác trên dữ liệu, nó hoàn toàn giống con trỏ bình thường ngoài việc nó chỉ trỏ tới một địa chỉ cố định. Khi ta đặt từ khóa const trước một khai báo con trỏ thông thường thì điều đó khiến con trỏ “hiểu” đối tượng là hằng chứ không phải con trỏ là hằng. Nghĩa là nó chỉ có thể “đọc” dữ liệu từ đối tượng nhưng không thể “ghi” dữ liệu lên đối tượng. Trong trường hợp này nó là con trỏ đến đối tượng hằng (pointer to const). Tuy nhiên đối tượng không phải là trở thành hằng, mà nó chỉ được con trỏ đó “hiểu là hằng” (chỉ là là hằng đối với con trỏ đó), đối với con trỏ khác nó có thể là biến. Để hiểu rõ hơn ta xem xét ví dụ sau: C++ Code:

1. char str[]="Hello, Ajinomoto"; // str là con trỏ hằng, nhưng str[i] thì không 2. const char* p=str; // con trỏ p (pointer to const) trỏ đến đầu mảng str, và p

“hiểu” str[i] là các hằng

Page 66: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

66 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

3. p[3]='X'; // Error: không được, vì p chỉ được đọc dữ liệu chứ không được ghi dữ liệu lên str[i]

4. str[3]='X'; // Ok: vì str[i] chỉ được coi là hằng theo “cách hiểu” của p, với str thì các str[i] vẫn là biến

Để khai báo một con trỏ hằng ta phải sử dụng *const chứ không phải const. Xem xét ví dụ sau: C++ Code:

1. int x, y; 2. int *const px=&x; // bây giờ px luôn luôn trỏ đến x, không thể thay đổi được 3. px=&y; // Error: px là con trỏ hằng, không được phép trỏ đi chỗ khác

Có thể tóm lại như trong ví dụ sau: C++ Code:

1. const int* p1=&x; // con trỏ tới hằng int (pointer to const char) 2. int const* p2=&x; // hoàn toàn giống p1, con trỏ tới hằng int (pointer to const

char)

3. int *const p3=&x; // con trỏ hằng tới biến char (pointer to const char)

Để dễ nhớ các bạn có thể đọc từ trái sang phải như sau: “p3 là con trỏ hằng tới kiểu int”. Chú ý: một đối tượng hằng là cố định, vì vậy gán địa chỉ của nó cho một “con trỏ tự do” sẽ gây lỗi biên dịch. Bởi vì thông qua con trỏ ta có thể thay đổi giá trị của hằng, và điều này là không được phép. C++ chỉ cho phép gán địa chỉ của một đối tượng hằng cho một con trỏ trỏ tới đối tượng hằng (pointer to const). Ví dụ chương trình sau: C++ Code:

1. const int x=100; // khai báo hằng nguyên x=100 2. const int* p1=&x; // Ok: con trỏ p1 trỏ tới hằng x 3. int* p2=&x; // Error: con trỏ p2 là “con trỏ tự do”, không được phép trỏ tới

hằng x

Câu hỏi mở rộng nhé: việc gán địa chỉ của một đối tượng hằng cho con trỏ hằng (const pointer) là có được phép hay không? Tại sao? Nhưng từ từ hãy post vội, đợi mình viết nốt loạt tut này đã

BÀI 11. MẢNG - ARRAY Trong C++, mảng là một nhóm các vùng nhớ liên tiếp có cùng kiểu dữ liệu. Thông thường mảng chứa những phần tử dữ liệu có liên quan đến nhau. Mảng có thể là một chiều, hai chiều, hoặc nhiều hơn hai chiều, trong đó mảng 1 chiều là thông dụng nhất. 1. Mảng một chiều (one-dimension array)

Page 67: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

67 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

a. Khai báo mảng một chiều Cú pháp khai báo cho mảng một chiều như sau C++ Code:

1. <kiểu_dữ_liệu> <tên_mảng>[kích_thước]; 2. Ví dụ: 3. int num[100]; // khai báo mảng 100 số nguyên 4. char ch[12]; // khai báo mảng 12 ký tự

Chúng ta cũng có thể khởi tạo giá trị cho mảng ngay lúc khai báo. Ví dụ: C++ Code:

1. int num[100]={1,2,3,4,8,11}; // gán giá trị cho 6 phần tử đầu, các phần tử còn lại tự động set bằng 0

2. int ch[]={‘a’, ‘b’, ‘c’, ‘d’}; // không cần chỉ rõ kích cỡ mảng, compiler sẽ tự động tính toán đủ chỗ

b. Truy xuất đến các phần tử mảng Để truy xuất đến các phần tử của mảng ta có thể dùng con trỏ hoặc chỉ số mảng. Sau đây là các cách truy xuất các phần tử của mảng thông dụng Thông qua chỉ số mảng: xem xét chương trình sau. Chương trình nhận 10 chữ số nguyên từ bàn phím và in ra các số chẵn đã được nhập vào. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int const size=10; 6. int n; 7. int a[size]; 8. 9. cout << "Enter " << size << " integers\n"; 10. for(int i=0; i<size; i++){ // nhập 10 số nguyên 11. cout << "a[" << i << "]= "; 12. cin >> a[i]; // nhập dữ liệu cho phần tử a[i]

13. } 14. 15. cout << "Even numbers are: \n"; 16. for(int i=0; i<size; i++){ // duyệt hết mảng 17. if(a[i]%2==0){ // nếu là số chẵn 18. cout << a[i] << " "; // in ra số chẵn

19. } 20. }

Page 68: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

68 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

21. 22. return 0;

23. }

Thông qua con trỏ: tên mảng thực chất là một con trỏ hằng, trỏ đến phần tử đầu tiên của mảng. Vì vậy ta có thể truy xuất tới các phần tử của mảng thông qua con trỏ này. Chương trình trên được viết lại theo phong cách con trỏ. C++ Code:

1. #include <iostream> 2. using namespace std; 3. 4. int main(){ 5. int const size=5; 6. int n; 7. int a[size]; 8. 9. cout << "Enter " << size << " integers\n"; 10. for(int i=0; i<size; i++){ 11. cout << "a[" << i << "]= "; 12. cin >> *(a+i); // đọc dữ liệu vào a[i]

13. } 14. 15. cout << "Even numbers are: \n"; 16. for(int i=0; i<size; i++){ 17. if(*(a+i)%2==0){ // nếu a[i] chẵn 18. cout << *(a+i) << " "; // in ra a[i]

19. } 20. } 21. 22. cout << 23. system("pause"); 24. return 0;

25. }

Ta có thể chỉ dùng con trỏ mà không cần dùng biến chạy i, đặc biệt là trong thao tác với chuỗi (string). Mọi tăng giảm đều thực hiện trên con trỏ, tuy nhiên tên mảng là một con trỏ hằng, không thể thay đổi được. Vì vậy ta phải thực hiện việc này thông qua một con trỏ khác. Chương trình sau thực hiện việc đổi tất cả các dấu cách (space) thành dấu chấm (dot). Lưu ý rằng, C++ không có kiểu built-in là string, string được cài đặt thông qua mảng các ký tự (kiểu char). Dấu hiệu kết thúc của một string là ký tự NULL (hay ký tự ‘\0’), đây chính là cơ sở để làm điều kiện kết thúc vòng lặp. Xem xét chương trình. C++ Code:

1. #include <iostream>

Page 69: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

69 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

2. #include <cstdio> 3. using namespace std; 4. int main(){ 5. char str[]="I am a superman, ha ha ha"; 6. char* ptr=str; 7. 8. while(*ptr){ // nếu còn chưa hết xâu 9. if(*ptr==' '){ // nếu là dấu cách 10. *ptr='.'; // đổi thành dấu chấm

11. } 12. ptr++; // dịch đến ký tự tiếp theo

13. } 14. 15. cout << str << endl; // in ra xâu đã được chỉnh sửa

16. 17. return 0;

18. }

2. Mảng hai chiều (two-dimension array) Mảng hai chiều thực chất là “mảng một chiều của các mảng một chiều”. Để khai báo mảng một chiều ta dùng cú pháp sau. C++ Code:

1. <kiểu_sữ_liệu> <tên_mảng>[chiều_1][chiều_2]; 2. Ví dụ: 3. int x[10][8]; // khai báo một mảng nguyên 10 phần tử, mỗi phần tử là một mảng

nguyên tám phần tử

Ta có thể coi mảng hai chiều như một bảng (table), chiều thứ nhất là số hàng (row), chiều thứ hai là số cột (column), như mô tả trong hình vẽ sau. Thực tế trong máy tính mảng trên được tổ chức như một mảng một chiều gồm 4 phần tử. Mỗi phần tử lại là một mảng một chiều gồm 5 phần tử như hình vẽ sau. Ta cũng có thể khởi tạo mảng hai chiều khi khai báo. Chiều thứ nhất có thể bỏ trống, trình biên dịch sẽ tự động tính toán vừa đủ, nhưng chiều thứ hai phải được chỉ định rõ. Ví dụ. C++ Code:

1. int a[][2]={ 2. {1,2}, 3. {3,4}, 4. {2,6} 5. };

Cách viết trên để cho dễ nhìn, bạn cũng có thể viết như sau mà kết quả vẫn tương đương. C++ Code:

1. a[][2]={1,2,3,4,2,6};

Page 70: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

70 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

BÀI 12. LỚP LƯU TRỮ (STORAGE CLASS) C++ có 5 định danh lớp lưu trữ (storage class specifiers), đó là: auto, extern, static, register và mutable. Các specifiers này quy định cách thức mà biến được lưu trữ. Specifier mutable chỉ áp dụng trong các đối tượng của lớp (class objects) nên mình sẽ đề cập đến nó trong những phần sau. Bây giờ ta chỉ khảo sát 4 specifiers đầu tiên. 1. auto auto dùng để khai báo các biến cục bộ. Cú pháp khai báo một biến là auto như sau: C++ Code:

1. auto <kiểu_dữ_liệu> <tên_biến>;

Tuy nhiên khai báo auto hiếm khi được sử dụng vì mặc định các biến cục bộ là auto. Vì vậy những biến cục bộ như thế nào thì các biến auto như thế. 2. extern Trong thực tế các chương trình thường có quy mô lớn. Vì vậy người ta thường phải chia chương trình thành các files riêng rẽ , nhỏ hơn. Khi có một thay đổi nhỏ nào đó xảy ra thì ta không phải compile lại cả chương trình mà chỉ cần compile lại những file có thay đổi. Trong một chương trình lớn bao gồm nhiều files, nếu chương trình có sử dụng biến toàn cục thì các files phải “biết” các biến toàn cục này. Tuy nhiên ta không thể khai báo lại các biến toàn cục trong từng file. Vì như vậy trình liên kết (linker) không biết link biến nào vì có nhiều bản sao của biến toàn cục và sẽ báo lỗi. C++ cung cấp giải pháp đó là khai báo biến toàn cục trong một file và sử dụng từ khóa extern. Khi khai báo một biến là extern thì nó báo cho trình biên dịch biết kiểu (type) và tên (name) của biến toàn cục, nhưng thực sự cấp phát bộ nhớ cho biến, tức là không thực sự tạo ra biến. Ta sẽ khai báo nó ở đâu đó trrong chương trình và mỗi khi gặp biến này, linker tự động tìm đến biến “thực sự” để lấy dữ liệu. Ví dụ chương trình của chúng ta gồm hai files là fileA, fileB và được khai báo như sau: C++ Code:

1. File A: 2. double global_var; // biến toàn cục thực sự 3. int main(){ 4. // thân hàm main

5. } 6. 7. File B: 8. extern double global_var; // thông báo global_var đã được khai báo trong một

file nào đó

9. demo_func(){ 10. // thân hàm demo_func

Page 71: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

71 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

11. }

Một ứng dụng khác của extern đó là giúp ta có thể sử dụng biến toàn cục trước khi định nghĩa chúng. Thông thường ta thường khai báo các biến toàn cục ở đầu chương trình, nhưng điều này là không cần thiết. Khi một hàm nào đó cần sử dụng biến toàn cục, ta có thể khai báo biến toàn cục trong hàm với từ khóa extern, rồi sau đó thích đặt biến toàn cục ở đâu cũng được. Ví dụ chương trình sau: C++ Code:

1. int main(){ 2. // thân hàm main

3. } 4. void func(){ 5. extern int x; // khai báo extern 6. cout << “ x is global variable and x= ” << x << endl; 7. } 8. // định nghĩa biến toàn cục cuối chương trình 9. int x=100; 10. Nói túm lại có hai cách sử dụng của extern: thứ nhất, dùng để thông báo biến toàn

cục đã được định nghĩa trong một file nào đó của một chương trình nhiều files. Thứ hai, cho phép sử dụng biến trước khi định nghĩa chính thức cho biến. Tuy nhiên có một điều cần chú ý. Nếu trong khai báo extern ta khởi tạo giá trị cho biến thì khai báo (declaration) đó sẽ trở thành định nghĩa (definition). Khai báo extern thì có thể có nhiều, nhưng định nghĩa thì chỉ có một nên sẽ có thể dẫn đến lỗi. Vì vậy khi sử dụng cần phải chú ý. 3. static Các biến có kiểu static là các biến “tĩnh” hay “cố định” (permanent) bên trong phạm vi khai báo của nó. Nó giống biến toàn cục ở chỗ nó được cấp phát bộ nhớ ngay khi chương trình bắt đầu, và tồn tại cho đến khi chương trình kết thúc, và nó duy trì giá trị trong suốt thời gian thực thi chương trình. Tuy nhiên điểm khác biệt là nó là biến cục bộ (local). Nó chỉ được biết đến trong phạm vi hàm, hoặc file mà nó được khai báo trong khi biến toàn cục có phạm vi toàn bộ chương trình. Có hai loại biến static là static cục bộ và static toàn cục, ta sẽ xem xét cả hai loại biến này. a. static cục bộ (static local variables) Khi một biến cục bộ được khai báo static thì nó sẽ được cấp phát một vùng nhớ cố định. Điều nay cho phép biến cục bộ duy trì giá trị của nó trong suốt thời gian thực thi chương trình. Nếu biến cục bộ của hàm thì giá trị của nó vẫn được duy trì giữa các lần gọi hàm. Điểm khác biệt chính giữa biến static cục bộ với biến toàn cục là “phạm vi” của nó. Biến static cục bộ chỉ được biết đến trong phạm vi khối lệnh mà chúng được khai báo, nó bị giới hạn bởi phạm vi. Để khai báo một biến là static ta chỉ cần thêm từ khóa static vào trước kiểu dữ liệu. Ví dụ:

C++ Code: 1. static int x; 2. static int count=0;

Page 72: BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ...dulieu.tailieuhoctap.vn/books/cong-nghe-thong-tin/the-loai-khac/file... · bác này, theo đó, một

72 Hướng dẫn lập trình hướng đối tượng với C++_ first_pace

Chú ý là biến static cục bộ chỉ được khởi tạo một lần, khi chương trình bắt đầu, chứ không phải mỗi lần khai báo của nó được bắt gặp. Việc cho phép duy trì giá trị biến cục bộ sau khi thoát khỏi hàm giúp hàm trở nên độc lập hơn. Vì một số hàm cần lưu giữ thông tin sau mỗi lần gọi, nếu biến cục bộ không được phép lưu giữ giá trị khi thoát khỏi hàm thì ta phải sử dụng các biến toàn cục. Và như mình đã nói ở bài trước, việc sử dụng biến toàn cục nên hạn chế, vì nó làm mất đi tính độc lập của các hàm đồng thời gây ra những hiệu ứng phụ không mong muốn. b. static toàn cục (static global variables) Các biến được khai báo static toàn cục chỉ “toàn cục” trong phạm vi file mà nó được khai báo. Nghĩa là mặc dù nó là global nhưng nó chỉ được biết đến trong phạm vi file mà nó được khai báo, các đoạn code trong những files khác không thể thao tác được trên nó. Túm lại cho cả hai loại biến static: từ khóa static cho phép một biến được phép duy trì giá trị của mình trong suốt thời gian chương trình thực thi. Tuy nhiên giới hạn phạm vi của nó vẫn được tuân thủ. Một static cục bộ chỉ được biết đến trong khối lệnh, hoặc hàm mà nó được khai báo, một static toàn cục cũng chỉ được biết đến trong file mà nó được khai báo. Điều này giúp giảm bớt những hiệu ứng phụ không mong muốn. Một chú ý nữa cần phải nhắc đến là: mặc dù biến static toàn cục (static global) vẫn hợp lệ và được sử dụng rộng rãi trong lập trình C++ nhưng C++ standard thì lại phản đối sử dụng nó. Thay vào đó người ta khuyến khích sử dụng các phương án khác để kiểm soát truy nhập đến biến toàn cục, trong đó có sử dụng namespace. Mình sẽ nói đến vấn đề namespace trong phần sau. 4. register register là specifier được sử dụng thường xuyên nhất. Khi một biến được khai báo là register, nó sẽ thông báo cho trình biên dịch biết để lưu trữ biến theo một cách nào đó mà biến có thể được truy cập một cách nhanh nhất có thể. Thông thương biến sẽ được lưu trữ trong thanh ghi (register) hoặc bộ nhớ đệm (cache memory). Truy cập dữ liệu từ register hoặc cache sẽ nhanh hơn truy cập từ bộ nhớ chính (main memory – RAM). Vì vậy biến lưu trữ trong register sẽ được truy cập nhanh hơn nhiều so với khi nó được lưu trữ trong RAM. Tuy nhiên đây chỉ là “gợi ý” yêu cầu compiler lưu trữ biến trong register, còn việc có đáp ứng yêu cầu này hay không thì đó là vấn đề khác. Compiler hoàn toàn có quyền từ chối. Lý do là trong CPU thì lượng thanh ghi là có hạn, và dung lượng của chúng là rất thấp. Vì vậy khi compiler nhận thấy các thanh ghi đã hết, nó sẽ lờ đi yêu cầu register này và chuyển biến tới lưu trữ ở khu vực khác (RAM). Ta phải lựa chọn hết sức cẩn thận các biến để khai báo register thì mới có thể khai thác tối đa lợi ích về hiệu suất, vì compiler có quyền lờ đi các yêu cầu yêu cầu một lượng quá lớn registers. Thông thường các biến điều khiển vòng lặp hoặc các biến được thao tác bên trong vòng lặp, với tần suất sử dụng nhiều lần, thì nên được khai báo register.