SlideShare a Scribd company logo
HỌC VIỆN CÔNG NGHỆ BƯU CHÍNH VIỄN THÔNG




NGÔN NGỮ LẬP TRÌNH C++

    (Dùng cho sinh viên hệ đào tạo đại học từ xa)

                  Lưu hành nội bộ




                    HÀ NỘI - 2006
NGÔN NGỮ LẬP TRÌNH C++
                                   PGS.TS. Trần Đình Quế
                                   KS. Nguyễn Mạnh Hùng




          Lập trình nâng cao với C++
       Lập trình hướng đối tượng với C++
2
GIỚI THIỆU

C++ là ngôn ngữ lập trình hướng đối tượng được mở rộng từ ngôn ngữ C. Do vậy, C++ có ưu
điểm là kế thừa được các điểm mạnh truyền thống của ngôn ngữ C như uyển chuyển, tương thích
với các thiết bị phần cứng. Hiện nay, C++ là một ngôn ngữ lập trình phổ biến, được giảng dạy tại
các trường đại học trong nước và trên thế giới và đặc biệt được sử dụng rộng rãi cho nhu cầu phát
triển của công nghiệp phần mềm hiện nay. Tài liệu này không những nhằm giới thiệu cho sinh
viên ngôn ngữ lập trình C++, mà còn mong muốn qua đó sinh viên có thể hiểu được tư tưởng của
phương pháp lập trình hướng đối tượng nói chung. Nội dung của tài liệu bao gồm hai phần chính:
   •   Phần thứ nhất là lập trình nâng cao với C++, bao gồm lập trình C++ với con trỏ và mảng,
       các kiểu dữ liệu có cấu trúc cùng các thao tác vào ra trên tệp.
   •   Phần thứ hai là lập trình hướng đối tượng với C++, bao gồm các định nghĩa và các thao
       tác trên lớp đối tượng, tính kế thừa và tương ứng bội trong C++, cách sử dụng một số lớp
       cơ bản trong thư viện C++.
Nội dung tài liệu được tổ chức thành 7 chương:
       Chương 1: Giới thiệu tổng quan về các phương pháp lập trình
       Trình bày các phương pháp lập trình tuyến tính, lập trình cấu trúc và đặc biệt, làm quen
       với các khái niệm trong lập trình hướng đối tượng.
       Chương 2: Con trỏ và mảng
       Trình bày cách khai báo và sử dụng các kiểu con trỏ và mảng trong ngôn ngữ C++.
       Chương 3: Kiểu dữ liệu có cấu trúc
       Trình bày cách biểu diễn và cài đặt một số kiểu cấu trúc dữ liệu trừu tượng trong C++. Sau
       đó, trình bày cách áp dụng các kiểu dữ liệu này trong các ứng dụng cụ thể.
       Chương 4: Vào ra trên tệp
       Trình bày các thao tác đọc, ghi dữ liệu trên các tệp tin khác nhau: tệp tin văn bản và tệp tin
       nhị phân. Trình bày các cách truy nhập tệp tin trực tiếp.
       Chương 5: Lớp đối tượng
       Trình bày các khái niệm mở đầu cho lập trình hướng đối tượng trong C++, bao gồm cách
       khai báo và sử dụng lớp, các thuộc tính của lớp; cách khởi tạo và huỷ bỏ đối tượng, các
       quy tắc truy nhập đến các thành phần của lớp.
       Chương 6: Tính kế thừa và tương ứng bội
       Trình bày cách thức kế thừa giữa các lớp trong C++, các nguyên tắc truy nhập trong kế
       thừa, định nghĩa nạp chồng các phương thức và tính đa hình trong lập trình hướng đối
       tương với C++.
       Chương 7: Một số lớp quan trọng
       Trình bày cách sử dụng một số lớp có sẵn trong thư viện chuẩn của C++, bao gồm các lớp:
       lớp tập hợp, lớp chuỗi, lớp ngăn xếp, lớp hàng đợi và lớp danh sách liên kết.
Để đọc được cuốn sách này, sinh viên phải quen biết các khái niệm cơ bản về lập trình, có một số
kỹ năng lập trình với ngôn ngữ C hoặc C++. Cuốn sách này cũng có thể dùng tài liệu tham khảo
cho những sinh viên muốn tìm hiểu các kỹ thuật lập trình nâng cao và lập trình hướng đối tượng


                                                                                                   3
với C++. Cuốn sách này có kèm theo một đĩa chương trình chứa toàn bộ các chương trình được
lấy làm minh hoạ và các bài tập trong cuốn sách.
Mặc dù các tác giả đã có nhiều cố gắng trong việc biên soạn tài liệu này, song không thể tránh
khỏi những thiếu sót. Rất mong nhận được những ý kiến đóng góp quý báu từ các sinh viên và các
bạn đồng nghiệp.




4
Chương 1: Giới thiệu về các phương pháp lập trình


                                         CHƯƠNG 1
         GIỚI THIỆU VỀ CÁC PHƯƠNG PHÁP LẬP TRÌNH

Nội dung của chương này tập trung trình bày các phương pháp lập trình:
   •   Phương pháp lập trình tuyến tính
   •   Phương pháp lập trình hướng cấu trúc
   •   Phương pháp lập trình hướng đối tượng.


1.1 LẬP TRÌNH TUYẾN TÍNH
Đặc trưng cơ bản của lập trình tuyến tính là tư duy theo lối tuần tự. Chương trình sẽ được thực
hiện theo thứ tự từ đầu đến cuối, lệnh này kế tiếp lệnh kia cho đến khi kết thúc chương trình.

Đặc trưng
Lập trình tuyến tính có hai đặc trưng:
   •   Đơn giản: chương trình được tiến hành đơn giản theo lối tuần tự, không phức tạp.
   •   Đơn luồng: chỉ có một luồng công việc duy nhất, và các công việc được thực hiện tuần tự
       trong luồng đó.

Tính chất

   •   Ưu điểm: Do tính đơn giản, lập trình tuyến tính được ứng dụng cho các chương trình đơn
       giản và có ưu điểm dễ hiểu.
   •   Nhược điểm: Với các ứng dụng phức tạp, người ta không thể dùng lập trình tuyến tính để
       giải quyết.
Ngày nay, lập trình tuyến tính chỉ tồn tại trong phạm vi các modul nhỏ nhất của các phương pháp
lập trình khác. Ví dụ trong một chương trình con của lập trình cấu trúc, các lệnh cũng được thực
hiện theo tuần tự từ đầu đến cuối chương trình con.


1.2 LẬP TRÌNH HƯỚNG CẤU TRÚC

1.2.1 Đặc trưng của lập trình hướng cấu trúc
Trong lập trình hướng cấu trúc, chương trình chính được chia nhỏ thành các chương trình con và
mỗi chương trình con thực hiện một công việc xác định. Chương trình chính sẽ gọi đến chương
trình con theo một giải thuật, hoặc một cấu trúc được xác định trong chương trình chính.
Các ngôn ngữ lập trình cấu trúc phổ biến là Pascal, C và C++. Riêng C++ ngoài việc có đặc trưng
của lập trình cấu trúc do kế thừa từ C, còn có đặc trưng của lập trình hướng đối tượng. Cho nên
C++ còn được gọi là ngôn ngữ lập trình nửa cấu trúc, nửa hướng đối tượng.

Đặc trưng
Đặc trưng cơ bản nhất của lập trình cấu trúc thể hiện ở mối quan hệ:
                                                                                              5
Chương 1: Giới thiệu về các phương pháp lập trình

         Chương trình = Cấu trúc dữ liệu + Giải thuật
Trong đó:
   •     Cấu trúc dữ liệu là cách tổ chức dữ liệu cho việc xử lý bởi một hay nhiều chương trình
         nào đó.
   •     Giải thuật là một quy trình để thực hiện một công việc xác định
Trong chương trình, giải thuật có quan hệ phụ thuộc vào cấu trúc dữ liệu:
   •     Một cấu trúc dữ liệu chỉ phù hợp với một số hạn chế các giải thuật.
   •     Nếu thay đổi cấu trúc dữ liệu thì phải thay đổi giải thuật cho phù hợp.
   •     Một giải thuật thường phải đi kèm với một cấu trúc dữ liệu nhất định.

Tính chất

   •     Mỗi chương trình con có thể được gọi thực hiện nhiều lần trong một chương trình chính.
   •     Các chương trình con có thể được gọi đến để thực hiện theo một thứ tự bất kì, tuỳ thuộc
         vào giải thuật trong chương trình chính mà không phụ thuộc vào thứ tự khai báo của các
         chương trình con.
   •     Các ngôn ngữ lập trình cấu trúc cung cấp một số cấu trúc lệnh điều khiển chương trình.

Ưu điểm
   •     Chương trình sáng sủa, dễ hiểu, dễ theo dõi.
   •     Tư duy giải thuật rõ ràng.

Nhược điểm

   •     Lập trình cấu trúc không hỗ trợ mạnh việc sử dụng lại mã nguồn: Giải thuật luôn phụ
         thuộc chặt chẽ vào cấu trúc dữ liệu, do đó, khi thay đổi cấu trúc dữ liệu, phải thay đổi giải
         thuật, nghĩa là phải viết lại chương trình.
   •     Không phù hợp với các phần mềm lớn: tư duy cấu trúc với các giải thuật chỉ phù hợp với
         các bài toán nhỏ, nằm trong phạm vi một modul của chương trình. Với dự án phần mềm
         lớn, lập trình cấu trúc tỏ ra không hiệu quả trong việc giải quyết mối quan hệ vĩ mô giữa
         các modul của phần mềm.

Vấn đề
Vấn đề cơ bản của lập trình cấu trúc là bằng cách nào để phân chia chương trình chính thành các
chương trình con cho phù hợp với yêu cầu, chức năng và mục đích của mỗi bài toán. Thông
thường, để phân rã bài toán trong lập trình cấu trúc, người ta sử dụng phương pháp thiết kế trên
xuống (top-down).

1.2.2 Phương pháp thiết kế trên xuống (top-down)
Phương pháp thiết kế top-down tiếp cận bài toán theo hướng từ trên xuống dưới, từ tổng qúat đến
chi tiết. Theo đó, một bài toán được chia thành các bài toán con nhỏ hơn. Mỗi bài toán con lại
được chia nhỏ tiếp, nếu có thể, thành các bài toán con nhỏ hơn nữa. Quá trình này còn được gọi là
quá trình làm mịn dần. Quá trình này sẽ dừng lại khi các bài toán con không cần chia nhỏ thêm
6
Chương 1: Giới thiệu về các phương pháp lập trình

nữa. Nghĩa là khi mỗi bài toán con đều có thể giải quyết bằng một chương trình con với một giải
thuật đơn giản.
Ví dụ, sử dụng phương pháp top-down để giải quyết bài toán xây một căn nhà mới. Chúng ta có
thể phân rã bài toán theo các bước như sau:
   •     Ở mức thứ nhất, chia bài toán xây nhà thành các bài toán nhỏ hơn như làm móng, đổ cột,
         đổ trần, xây tường, lợp mái.
   •     Ở mức thứ hai, phân rã các công việc ở mức thứ nhất như việc làm móng nhà có thể phân
         rã tiếp thành các công việc đào móng, gia cố nền, làm khung sắt, đổ bê tong; công việc đổ
         cột được phần rã thành …
   •     Ở mức thứ ba, phân rã các công việc của mức thứ hai như việc đào móng có thể phân chia
         tiếp thành các công việc như đo đạc, cắm mốc, chăng dây, đào và kiểm tra móng. Việc gia
         cố nền được phân rã thành …
Quá trình phân rã có thể dừng ở mức này, bởi vì các công việc con thu được như đo đạc, cắm
mốc, chăng dây, đào… có thể thực hiện được ngay, không cần chia nhỏ thêm nữa.
Lưu ý:
   •     Cùng sử dụng phương pháp top-down với cùng một bài toán, nhưng có thể cho ra nhiều
         kết quả khác nhau. Nguyên nhân là do sự khác nhau trong tiêu chí để phân rã một bài toán
         thành các bài toán con.
Ví dụ, vẫn áp dụng phương pháp top-down để giải quyết bài toán xây nhà, nhưng nếu sử dụng
một cách khác để phân chia bài toán, ta có thể thu được kết quả khác biệt so với phương pháp ban
đầu:
   •     Ở mức thứ nhất, chia bài toán xây nhà thành các bài toán nhỏ hơn như làm phần gỗ, làm
         phần sắt, làm phần bê tông và làm phần gạch.
   •     Ở mức thứ hai, phân rã các công việc ở mức thứ nhất là làm phần gỗ có thể chia thành các
         công việc như xẻ gỗ, gia công gỗ, tạo khung, lắp vào nhà. Việc làm sắt có thể chia nhỏ
         thành…
Rõ ràng, với cách làm mịn thế này, ta sẽ thu được một kết quả khác hẳn với cách thức đã thực
hiện ở phần trên.


1.3 LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG

1.3.1 Lập trình hướng đối tượng
Trong lập trình hướng đối tượng:
   •     Người ta coi các thực thể trong chương trình là các đối tượng và sau đó trừu tượng hoá đối
         tượng thành lớp đối tượng.
   •     Dữ liệu được tổ chức thành các thuộc tính của lớp. Nguời ta ngăn chặn việc thay đổi tuỳ
         tiện dữ liệu trong chương trình bằng các cách giới hạn truy nhập như chỉ cho phép truy
         nhập dữ liệu thông qua đối tượng, thông qua các phương thức mà đối tượng được cung
         cấp…
   •     Quan hệ giữa các đối tượng là quan hệ ngang hàng hoặc quan hệ kế thừa: Nếu lớp B kế
         thừa từ lớp A thì A được gọi là lớp cơ sở và B được gọi là lớp dẫn xuất.

                                                                                                 7
Chương 1: Giới thiệu về các phương pháp lập trình

Ngôn ngữ lập trình hướng đối tượng phổ biến hiện nay là Java, C++, C#...Mặc dù C++ cũng có
những đặc trưng cơ bản của lập trình hướng đối tượng nhưng vẫn không phải là ngôn ngữ lập
trình thuần hướng đối tượng.

Đặc trưng
Lập trình hướng đối tượng có hai đặc trưng cơ bản:
    •   Đóng gói dữ liệu: dữ liệu luôn được tổ chức thành các thuộc tính của lớp đối tượng. Việc
        truy nhập đến dữ liệu phải thông qua các phương thức của đối tượng lớp.
    •   Sử dụng lại mã nguồn: việc sử dụng lại mã nguồn được thể hiện thông qua cơ chế kế
        thừa. Cơ chế này cho phép các lớp đối tượng có thể kế thừa từ các lớp đối tượng khác. Khi
        đó, trong các lớp dẫn xuất, có thể sử dụng các phương thức (mã nguồn) của các lớp cơ sở
        mà không cần phải định nghĩa lại.

Ưu điểm
Lập trình hướng đối tượng có một số ưu điểm nổi bật:
    •   Không còn nguy cơ dữ liệu bị thay đổi tự do trong chương trình. Vì dữ liệu đã được đóng
        gói vào các đối tượng. Nếu muốn truy nhập vào dữ liệu phải thông qua các phương thức
        được cho phép của đối tượng.
    •   Khi thay đổi cấu trúc dữ liệu của một đối tượng, không cần thay đổi mã nguồn của các đối
        tượng khác, mà chỉ cần thay đổi một số thành phần của đối tượng dẫn xuất. Điều này hạn
        chế sự ảnh hưởng xấu của việc thay đổi dữ liệu đến các đối tượng khác trong chương
        trình.
    •   Có thể sử dụng lại mã nguồn, tiết kiệm tài nguyên, chi phí thời gian. Vì nguyên tắc kế
        thừa cho phép các lớp dẫn xuất sử dụng các phương thức từ lớp cơ sở như những phương
        thức của chính nó, mà không cần thiết phải định nghĩa lại.
    •   Phù hợp với các dự án phần mềm lớn, phức tạp.

1.3.2 Một số khái niệm cơ bản
Trong mục này, chúng ta sẽ làm quen với một số khái niệm cơ bản trong lập trình hướng đối
tượng. Bao gồm:
    •   Khái niệm đối tượng (object)
    •   Khái niệm đóng gói dữ liệu (encapsulation)
    •   Khái niệm kế thừa (inheritance)
    •   Khái niệm đa hình (polymorphism)

Đối tượng (Object)
Trong lập trình hướng đối tượng, đối tượng được coi là đơn vị cơ bản nhỏ nhất. Các dữ diệu và
cách xử lí chỉ là thành phần của đối tượng mà không được coi là thực thể. Một đối tượng chứa các
dữ liệu của riêng nó, đồng thời có các phương thức (hành động) thao tác trên các dữ liệu đó:
        Đối tượng = dữ liệu + phương thức


8
Chương 1: Giới thiệu về các phương pháp lập trình

Lớp (Class)
Khi có nhiều đối tượng giống nhau về mặt dữ liệu và phương thức, chúng được nhóm lại với nhau
và gọi chung là lớp:
   •   Lớp là sự trừu tượng hoá của đối tượng
   •   Đối tượng là một thể hiện của lớp.

Đóng gói dữ liệu (Encapsulation)

   •   Các dữ liệu được đóng gói vào trong đối tượng. Mỗi dữ liệu có một phạm vi truy nhập
       riêng.
   •   Không thể truy nhập đến dữ liệu một cách tự do như lập trình cấu trúc
   •   Muốn truy nhập đến các dữ liệu đã được bảo vệ, phải thông qua các đối tượng, nghĩa là
       phải sử dụng các phương thức mà đối tượng cung cấp mới có thể truy nhập đến dữ liệu
       của đối tượng đó.
Tuy nhiên, vì C++ chỉ là ngôn ngữ lập trình nửa đối tượng, cho nên C++ vẫn cho phép định nghĩa
các biến dữ liệu và các hàm tự do, đây là kết quả kế thừa từ ngôn ngữ C, một ngôn ngữ lập trình
thuần cấu trúc.

Kế thừa (Inheritance)
Tính kế thừa của lập trình hướng đối tượng cho phép một lớp có thể kế thừa từ một số lớp đã tồn
tại. Khi đó, lớp mới có thể sử dụng dữ liệu và phương thức của các lớp cơ sở như là của mình.
Ngoài ra, lớp dẫn xuất còn có thể bổ sung thêm một số dữ liệu và phương thức. Ưu điểm của kế
thừa là khi thay đổi dữ liệu của một lớp, chỉ cần thay đổi các phương thức trong phạm vi lớp cơ sở
mà không cần thay đổi trong các lớp dẫn xuất.

Đa hình (Polymorphsim)
Đa hình là khái niệm luôn đi kèm với kế thừa. Do tính kế thừa, một lớp có thể sử dụng lại các
phương thức của lớp khác. Tuy nhiên, nếu cần thiết, lớp dẫn xuất cũng có thể định nghĩa lại một
số phương thức của lớp cơ sở. Đó là sự nạp chồng phương thức trong kế thừa. Nhờ sự nạp chồng
phương thức này, ta chỉ cần gọi tên phương thức bị nạp chồng từ đối tượng mà không cần quan
tâm đó là đối tượng của lớp nào. Chương trình sẽ tự động kiểm tra xem đối tượng là thuộc kiểu
lớp cơ sở hay thuộc lớp dẫn xuất, sau đó sẽ gọi phương thức tương ứng với lớp đó. Đó là tính đa
hình.

1.3.3 Lập trình hướng đối tượng trong C++
Vì C++ là một ngôn ngữ lập trình được mở rộng từ một ngôn ngữ lập trình cấu trúc C nên C++
được xem là ngôn ngữ lập trình nửa hướng đối tượng, nửa hướng cấu trúc.

Những đặc trưng hướng đối tượng của C++

   •   Cho phép định nghĩa lớp đối tượng.
   •   Cho phép đóng gói dữ liệu vào các lớp đối tượng. Cho phép định nghĩa phạm vi truy nhập
       dữ liệu của lớp bằng các từ khoá phạm vi: public, protected, private.

                                                                                                9
Chương 1: Giới thiệu về các phương pháp lập trình

     •   Cho phép kế thừa lớp với các kiểu kế thừa khác nhau tuỳ vào từ khoá dẫn xuất.
     •   Cho phép lớp dẫn xuất sử dụng các phương thức của lớp cơ sở (trong phạm vi quy định).
     •   Cho phép định nghĩa chồng phương thức trong lớp dẫn xuất.

Những hạn chế hướng đối tượng của C++
Những hạn chế này là do C++ được phát triển từ một ngôn ngữ lập trình thuần cấu trúc C.
     •   Cho phép định nghĩa và sử dụng các biến dữ liệu tự do.
     •   Cho phép định nghĩa và sử dụng các hàm tự do.
     •   Ngay cả khi dữ liệu được đóng gói vào lớp, dữ liệu vẫn có thể truy nhập trực tiếp như dữ
         liệu tự do bởi các hàm bạn, lớp bạn (friend) trong C++.


TỔNG KẾT CHƯƠNG 1
Chương 1 đã trình bày tổng quan về các phương pháp lập trình hiện nay. Nội dung tập trung vào
ba phương pháp lập trình có liên quan trực tiếp đến ngôn ngữ lập trình C++:
     •   Lập trình tuyến tính
     •   Lập trình hướng cấu trúc
     •   Lập trình hướng đối tượng.
C++ là ngôn ngữ lập trình được mở rộng từ ngôn ngữ lập trình cấu trúc C. Do đó, C++ vừa có
những đặc trưng của lập trình cấu trúc, vừa có những đặc trưng của lập trình hướng đối tượng.




10
Chương 2: Con trỏ và mảng


                                         CHƯƠNG 2
                                 CON TRỎ VÀ MẢNG

Nội dung của chương này tập trung trình bày các vấn đề cơ bản liên quan đến các thao tác trên
kiểu dữ liệu con trỏ và mảng trong C++:
    •   Khái niệm con trỏ, cách khai báo và sử dụng con trỏ.
    •   Mối quan hệ giữa con trỏ và mảng
    •   Con trỏ hàm
    •   Cấp phát bộ nhớ cho con trỏ


2.1 KHÁI NIỆM CON TRỎ

2.1.1 Khai báo con trỏ
Con trỏ là một biến đặc biệt chứa địa chỉ của một biến khác. Con trỏ có cùng kiểu dữ liệu với kiểu
dữ liệu của biến mà nó trỏ tới. Cú pháp khai báo một con trỏ như sau:
        <Kiểu dữ liệu> *<Tên con trỏ>;
Trong đó:
    •   Kiểu dữ liệu: Có thể là các kiểu dữ liệu cơ bản của C++, hoặc là kiểu dữ liệu có cấu trúc,
        hoặc là kiểu đối tượng do người dùng tự định nghĩa.
    •   Tên con trỏ: Tuân theo qui tắc đặt tên biến của C++:
        -   Chỉ được bắt đầu bằng một kí tự (chữ), hoặc dấu gạch dưới “_”.
        -   Bắt đầu từ kí tự thứ hai, có thể có kiểu kí tự số.
        -   Không có dấu trống (space bar) trong tên biến.
        -   Có phân biệt chữ hoa và chữ thường.
        -   Không giới hạn độ dài tên biến.
Ví dụ, để khai báo một biến con trỏ có kiểu là int và tên là pointerInt, ta viết như sau:
        int *pointerInt;
Lưu ý
    •   Có thể viết dấu con trỏ “*” ngay sau kiểu dữ liệu, nghĩa là hai cách khai báo sau là tương
        đương:
                int *pointerInt;
                int* pointerInt;
    •   Các cách khai báo con trỏ như sau là sai cú pháp:
                *int pointerInt;        // Khai báo sai con trỏ
                int pointerInt*;        // Khai báo sai con trỏ


2.1.2 Sử dụng con trỏ
Con trỏ được sử dụng theo hai cách:
                                                                                               11
Chương 2: Con trỏ và mảng

     •   Dùng con trỏ để lưu địa chỉ của biến để thao tác
     •   Lấy giá trị của biến do con trỏ trỏ đến để thao tác

Dùng con trỏ để lưu địa chỉ của biến
Bản thân con trỏ sẽ được trỏ vào địa chỉ của một biến có cùng kiểu dữ liệu với nó. Cú pháp của
phép gán như sau:
         <Tên con trỏ> = &<tên biến>;
Lưu ý
     •   Trong phép toán này, tên con trỏ không có dấu “*”.
Ví dụ:
         int x, *px;
         px = &x;
sẽ cho con trỏ px có kiểu int trỏ vào địa chỉ của biến x có kiểu nguyên. Phép toán &<Tên biến>
sẽ cho địa chỉ của biến tương ứng.

Lấy giá trị của biến do con trỏ trỏ đến
Phép lấy giá trị của biến do con trỏ trỏ đến được thực hiện bằng cách gọi tên:
         *<Tên con trỏ>;
Lưu ý
     •   Trong phép toán này, phải có dấu con trỏ “*”. Nếu không có dấu con trỏ, sẽ trở thành
         phép lấy địa chỉ của biến do con trỏ trỏ tới.
Ví dụ:
         int x = 12, y, *px;
         px = &y;
         *px = x;
Quá trình diễn ra như sau:
              int x = 12, y, *px;   x = 12      y=0            px       null


              px = &y;              x = 12      y=0            px


              px = x;               x = 12     y=x =12         px

con trỏ px vẫn trỏ tới địa chỉ biến y và giá trị của biến y sẽ là 12.

Phép gán giữa các con trỏ
Các con trỏ cùng kiểu có thể gán cho nhau thông qua phép gán và lấy địa chỉ con trỏ:
         <Tên con trỏ 1> = <Tên con trỏ 2>;
Lưu ý
     •   Trong phép gán giữa các con trỏ, bắt buộc phải dùng phép lấy địa chỉ của biến do con trỏ
         trỏ tới (không có dấu “*” trong tên con trỏ) mà không được dùng phép lấy giá trị của
         biến do con trỏ trỏ tới.


12
Chương 2: Con trỏ và mảng

     •   Hai con trỏ phải cùng kiểu. Trong trường hợp hai con trỏ khác kiểu, phải sử dụng các
         phương thức ép kiểu tương tự như trong phép gán các biến thông thường có kiểu khác
         nhau.
Ví dụ:
         int x = 12, *px, *py;
         px = &x;
         py = px;


             int x = 12, *px, *py;    x = 12   px         null       py          null


             px = &x;                 x = 12   px                    py          null


             py = px;                 x = 12   px                    py




con trỏ py cũng trỏ vào địa chỉ của biến x như con trỏ px. Khi đó *py cũng có giá trị 12 giống như
*px và là giá trị của biến x.
Chương trình 2.1 minh hoạ việc dùng con trỏ giữa các biến của một chương trình C++.


 Chương trình 2.1
 #include <stdio.h>
 #include <conio.h>
 void main(void){
          int x = 12, *px, *py;
          cout << ”x = ” << x << endl;


          px = &x;                   // Con trỏ px trỏ tới địa chỉ của x
          cout << ”px = &x, *px = ” << *px << endl;


          *px = *px + 20;            // Nội dung của px là 32
          cout << ”*px = *px+20, x = ” << x << endl;


          py = px;                   // Cho py trỏ tới chỗ mà px trỏ: địa chỉ của x
          *py += 15;                 // Nội dung của py là 47
          cout << ”py = px, *py +=15, x =            ” << x << endl;
 }


Trong chương trình 2.1, ban đầu biến x có giá trị 12. Sau đó, con trỏ px trỏ vào địa chỉ của biến x
nên con trỏ px cũng có giá trị 12. Tiếp theo, ta tăng giá trị của con trỏ px thêm 20, giá trị của con
trỏ px là 32. Vì px đang trỏ đến địa chỉ của x nên x cũng có giá trị là 32. Sau đó, ta cho con trỏ py
trỏ đến vị trí mà px đang trỏ tới (địa chỉ của biến x) nên py cũng có giá trị 32. Cuối cùng, ta tăng
giá trị của con trỏ py thêm 15, py sẽ có giá trị 37. Vì py cũng đang trỏ đến địa chỉ của x nên x
cũng có giá trị 37. Do đó, ví dụ 2.1 sẽ in ra kết quả như sau:

                                                                                                  13
Chương 2: Con trỏ và mảng

        x = 12
        px = &x, *px = 12
        *px = *px + 20, x = 32
        py = px, *py += 15, x = 37


2.2 CON TRỎ VÀ MẢNG

2.2.1 Con trỏ và mảng một chiều

Mảng một chiều
Trong C++, tên một mảng được coi là một kiểu con trỏ hằng, được định vị tại một vùng nhớ xác
định và địa chỉ của tên mảng trùng với địa chỉ của phần tử đầu tiên của mảng.
Ví dụ khai báo:
        int A[5];
thì địa chỉ của mảng A (cũng viết là A) sẽ trùng với địa chỉ phần tử đầu tiên của mảng A (là
&A[0]) nghĩa là:
        A = &A[0];

Quan hệ giữa con trỏ và mảng
Vì tên của mảng được coi như một con trỏ hằng, nên nó có thể được gán cho một con trỏ có cùng
kiểu.
Ví dụ khai báo:
        int A[5] = {5, 10, 15, 20, 25};
        int *pa = A;


        int A[5] = {5, 10, 15, 20, 25};   A        A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


        int *pa = A;                      A        A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


                                          pa


thì con trỏ pa sẽ trỏ đến mảng A, tức là trỏ đến địa chỉ của phần tử A[0], cho nên hai khai báo sau
là tương đương:
        pa = A;
        pa = &A[0];
Với khai báo này, thì địa chỉ trỏ tới của con trỏ pa là địa chỉ của phần tử A[0] và giá trị của con trỏ
pa là giá trị của phần tử A[0], tức là *pa = 5;

Phép toán trên con trỏ và mảng
Khi một con trỏ trỏ đến mảng, thì các phép toán tăng hay giảm trên con trỏ sẽ tương ứng với phép
dịch chuyển trên mảng.
Ví dụ khai báo:
        int A[5] = {5, 10, 15, 20, 25};


14
Chương 2: Con trỏ và mảng

         int *pa = &A[2];
thì con trỏ pa sẽ trỏ đến địa chỉ của phần tử A[2] và giá trị của pa là: *pa = A[2] = 15.
Khi đó, phép toán:
         pa = pa + 1;
sẽ đưa con trỏ pa trỏ đến địa chỉ của phần tử tiếp theo của mảng A, đó là địa chỉ của A[3]. Sau đó,
phép toán:
         pa = pa – 2;
sẽ đưa con trỏ pa trỏ đến địa chỉ của phần tử A[1].

         int A[5] = {5, 10, 15, 20, 25};   A      A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


         int *pa = &A[2];                  A      A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


                                           pa


         pa = pa + 1;                      A      A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


                                           pa


         pa = pa - 2;                      A      A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


                                           pa


Lưu ý:
   •     Hai phép toán pa++ và *pa++ có tác dụng hoàn toàn khác nhau trên mảng, pa++ là thao
         tác trên con trỏ, tức là trên bộ nhớ, nó sẽ đưa con trỏ pa trỏ đến địa chỉ của phần tử tiếp
         theo của mảng. *pa++ là phép toán trên giá trị, nó tăng giá trị hiện tại của phần tử mảng
         lên một đơn vị.
         Ví dụ:
                    int A[5] = {5, 10, 15, 20, 25};
                    int *pa = &A[2];
         thì pa++ là tương đương với pa = &A[3] và *pa = 20.
         nhưng *pa++ lại tương đương với pa = &A[2] và *pa = 15+1 = 16, A[2] = 16.




                                                                                                 15
Chương 2: Con trỏ và mảng


         int A[5] = {5, 10, 15, 20, 25};      A    A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


         int *pa = &A[2];                     A    A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


                                              pa


         pa ++;                               A    A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


                                              pa


         Nhưng *pa ++;                        A    A[0]=5   A[1]=10 A[2]=16 A[3]=20 A[4]= 25


                                              pa


     •   Trong trường hợp:
                    int A[5] = {5, 10, 15, 20, 25};
                    int *pa = &A[4];
         thì phép toán pa++ sẽ đưa con trỏ pa trỏ đến một địa chỉ không xác định. Lí do là A[4] là
         phần tử cuối của mảng A, nên pa++ sẽ trỏ đến địa chỉ ngay sau địa chỉ của A[4], địa chỉ
         này nằm ngoài vùng chỉ số của mảng A nên không xác định. Tương tự với trường hợp
         pa=&A[0], phép toán pa-- cũng đưa pa trỏ đến một địa chỉ không xác định.



         int A[5] = {5, 10, 15, 20, 25};      A    A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


         int *pa = &A[2];                     A    A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


                                              pa


         pa ++;                               A    A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25   null


                                              pa


         pa = &A[0];                          A    A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


                                              pa


         pa --;                        null   A    A[0]=5   A[1]=10 A[2]=15 A[3]=20 A[4]= 25


                                              pa



     •   Vì mảng A là con trỏ hằng, cho nên không thể thực hiện các phép toán trên A mà chỉ có
         thể thực hiện trên các con trỏ trỏ đến A: các phép toán pa++ hoặc pa--là hợp lệ, nhưng các
         phép toán A++ hoặc A--là không hợp lệ.
Chương trình 2.2a minh hoạ việc cài đặt một thủ tục sắp xếp các phần tử của một mảng theo cách
thông thường.



16
Chương 2: Con trỏ và mảng

 Chương trình 2.2a
 void SortArray(int A[], int n){
         int temp;
         for(int i=0; i<n-1; i++)
         for(int j=i+1; j<n; j++)
                 if(A[i] > A[j]){
                        temp = A[i];
                        A[i] = A[j];
                        A[j] = temp;
                 }
 }


Chương trình 2.2b cài đặt một thủ tục tương tự bằng con trỏ. Hai thủ tục này có chức năng hoàn
toàn giống nhau.


 Chương trình 2.2b
 void SortArray(int *A, int n){
         int temp;
         for(int i=0; i<n-1; i++)
         for(int j=i+1; j<n; j++)
                 if(*(A+i) > *(A+j)){
                        temp = *(A+i);
                        *(A+i) = *(A+j);
                        *(A+j) = temp;
                 }
 }


Trong chương trình 2.2b, thay vì dùng một mảng, ta dùng một con trỏ để trỏ đến mảng cần sắp
xếp. Khi đó, ta có thể dùng các thao tác trên con trỏ thay vì các thao tác trên các phần tử mảng.

2.2.2 Con trỏ và mảng nhiều chiều

Con trỏ và mảng nhiều chiều
Một câu hỏi đặt ra là nếu một ma trận một chiều thì tương đương với một con trỏ, vậy một mảng
nhiều chiều thì tương đương với con trỏ như thế nào?
Xét ví dụ:
       int A[3][3] = {
                       {5, 10, 15},
                       {20, 25, 30},
                       {35, 40, 45}
               };


                                                                                              17
Chương 2: Con trỏ và mảng

Khi đó, địa chỉ của ma trận A chính là địa chỉ của hàng đầu tiên của ma trận A, và cũng là địa chỉ
của phần tử đầu tiên của hàng đầu tiên của ma trận A:
     •   Địa chỉ của ma trận A:         A = A[0] = *(A+0) = &A[0][0];

     •   Địa chỉ của hàng thứ nhất:     A[1] = *(A+1) = &A[1][0];

     •   Địa chỉ của hàng thứ i:        A[i] = *(A+i) = &A[i][0];

     •   Địa chỉ phần tử                &A[i][j] = (*(A+i)) + j;

     •   Giá trị phần tử                A[i][j] = *((*(A+i)) + j);
Như vậy, một mảng hai chiều có thể thay thế bằng một mảng một chiều các con trỏ cùng kiểu:
         int A[3][3];
có thể thay thế bằng:
         int (*A)[3];

Con trỏ trỏ tới con trỏ
Vì một mảng hai chiều int A[3][3] có thể thay thế bằng một mảng các con trỏ int (*A)[3]. Hơn
nữa, một mảng int A[3] lại có thể thay thế bằng một con trỏ int *A. Do vậy, một mảng hai chiều
có thể thay thế bằng một mảng các con trỏ, hoặc một con trỏ trỏ đến con trỏ. Nghĩa là các cách
viết sau là tương đương:
         int A[3][3];
         int (*A)[3];
         int **A;


2.3 CON TRỎ HÀM
Mặc dù hàm không phải là một biến cụ thể nên không có một địa chỉ xác định. Nhưng trong khi
chạy, mỗi một hàm trong C++ cũng có một vùng nhớ xác định, do vậy, C++ cho phép dùng con
trỏ để trỏ đến hàm. Con trỏ hàm được dùng để truyền tham số có dạng hàm.

Khai báo con trỏ hàm
Con trỏ hàm được khai báo tương tự như khai báo nguyên mẫu hàm thông thường trong C++,
ngoại trừ việc có thêm kí hiệu con trỏ “*” trước tên hàm. Cú pháp khai báo con trỏ hàm như sau:
         <Kiểu dữ liệu trả về> (*<Tên hàm>)([<Các tham số>]);
Trong đó:
     •   Kiểu dữ liệu trả về: là các kiểu dữ liệu thông thường của C++ hoặc kiểu do người dùng
         tự định nghĩa.
     •   Tên hàm: tên do người dùng tự định nghĩa, tuân thủ theo quy tắc đặt tên biến trong C++.
     •   Các tham số: có thể có hoặc không (phần trong dấu “[]” là tuỳ chọn). Nếu có nhiều tham
         số, mỗi tham số được phân cách nhau bởi dấu phẩy.
Ví dụ khai báo:
         int (*Calcul)(int a, int b);
là khai báo một con trỏ hàm, tên là Calcul, có kiểu int và có hai tham số cũng là kiểu int.
Lưu ý:

18
Chương 2: Con trỏ và mảng

     •   Dấu “()” bao bọc tên hàm là cần thiết để chỉ ra rằng ta đang khai báo một con trỏ hàm.
         Nếu không có dấu ngoặc đơn này, trình biên dịch sẽ hiểu rằng ta đang khai báo một hàm
         thông thường và có giá trị trả về là một con trỏ.
         Ví dụ, hai khai báo sau là khác nhau hoàn toàn:
                // Khai báo một con trỏ hàm
                int (*Calcul)(int a, int b);
                // Khai báo một hàm trả về kiểu con trỏ
                int *Calcul(int a, int b);

Sử dụng con trỏ hàm
Con trỏ hàm được dùng khi cần gọi một hàm như là tham số của một hàm khác. Khi đó, một hàm
được gọi phải có khuôn mẫu giống với con trỏ hàm đã được khai báo.
Ví dụ, với khai báo:
         int (*Calcul)(int a, int b);
thì có thể gọi các hàm có hai tham số kiểu int và trả về cũng kiểu int như sau:
         int add(int a, int b);
         int sub(int a, int b);
nhưng không được gọi các hàm khác kiểu tham số hoặc kiểu trả về như sau:
         int add(float a, int b);
         int add(int a);
         char* sub(char* a, char* b);
Chương trình 2.3 minh hoạ việc khai báo và sử dụng con trỏ hàm.


 Chương trình 2.3
 #include <ctype.h>
 #include <string>


 // Hàm có sử dụng con trỏ hàm như tham số
 void Display(char[] str, int (*Xtype)(int c)){
          int index = 0;
          while(str[index] != ‘0’){
                 cout << (*Xtype)(str[index]); // Sử dụng con trỏ hàm
                 index ++;
          }
          return;
 }


 // Hàm main, dùng lời gọi hàm đến con trỏ hàm
 void main(){
          char input[500];
          cout << “Enter the string: ”;
          cin >> input;

                                                                                            19
Chương 2: Con trỏ và mảng


          char reply;
          cout << “Display the string in uppercase or lowercase (u,l): ”;
          cin >> reply;
          if(reply == ‘l’)       // Hiển thị theo dạng lowercase
                  Display(str, tolower);
          else                   // Hiển thị theo dạng uppercase
                  Display(str, toupper);
          return;
 }


Chương trình 2.3 khai báo hàm Display() có sử dụng con trỏ hàm có khuôn mẫu
         int (*Xtype)(int c);
Trong hàm main, con trỏ hàm này được gọi bởi hai thể hiện là các hàm tolower() và hàm
toupper(). Hai hàm này được khai báo trong thư viện ctype.h với mẫu như sau:
         int tolower(int c);
         int toupper(int c);
Hai khuôn mẫu này phù hợp với con trỏ hàm Xtype trong hàm Display() nên lời gọi hàm
Display() trong hàm main là hợp lệ.



2.4 CẤP PHÁT BỘ NHỚ ĐỘNG
Xét hai trường hợp sau đây:
     •   Trường hợp 1, khai báo một con trỏ và gán giá trị cho nó:
                 int *pa = 12;
     •   Trường hợp 2, khai báo con trỏ đến phần tử cuối cùng của mảng rồi tăng thêm một đơn vị
         cho nó:
                 int A[5] = {5, 10, 15, 20, 25};
                 int *pa = &A[4];
                 pa++;
Trong cả hai trường hợp, ta đều không biết thực sự con trỏ pa đang trỏ đến địa chỉ nào trong bộ
nhớ: trường hợp 1 chỉ ra rằng con trỏ pa đang trỏ tới một địa chỉ không xác định, nhưng lại chứa
giá trị là 12 do được gán vào. Trường hợp 2, con trỏ pa đã trỏ đến địa chỉ ngay sau địa chỉ phần tử
cuối cùng của mảng A, đó cũng là một địa chỉ không xác định. Các địa chỉ không xác định này là
các địa chỉ nằm ở vùng nhớ tự do còn thừa của bộ nhớ. Vùng nhớ này có thể bị chiếm dụng bởi
bất kì một chương trình nào đang chạy.
Do đó, rất có thể các chương trình khác sẽ chiếm mất các địa chỉ mà con trỏ pa đang trỏ tới. Khi
đó, nếu các chương trình thay đổi giá trị của địa chỉ đó, giá trị pa cũng bị thay đổi theo mà ta
không thể kiểm soát được. Để tránh các rủi ro có thể gặp phải, C++ yêu cầu phải cấp phát bộ nhớ
một cách tường minh cho con trỏ trước khi sử dụng chúng.



20
Chương 2: Con trỏ và mảng

2.4.1 Cấp phát bộ nhớ động cho biến

Cấp phát bộ nhớ động
Thao tác cấp phát bộ nhớ cho con trỏ thực chất là gán cho con trỏ một địa chỉ xác định và đưa địa
chỉ đó vào vùng đã bị chiếm dụng, các chương trình khác không thể sử dụng địa chỉ đó. Cú pháp
cấp phát bộ nhớ cho con trỏ như sau:
         <tên con trỏ> = new <kiểu con trỏ>;
Ví dụ, khai báo:
         int *pa;
         pa = new int;
sẽ cấp phát bộ nhớ hợp lệ cho con trỏ pa.
Lưu ý:
   •     Ta có thể vừa cấp phát bộ nhớ, vừa khởi tạo giá trị cho con trỏ theo cú pháp sau:
                int *pa;
                pa = new int(12);
         sẽ cấp phát cho con trỏ pa một địa chỉ xác định, đồng thời gán giá trị của con trỏ *pa = 12.

Giải phóng bộ nhớ động
Địa chỉ của con trỏ sau khi được cấp phát bởi thao tác new sẽ trở thành vùng nhớ đã bị chiếm
dụng, các chương trình khác không thể sử dụng vùng nhớ đó ngay cả khi ta không dùng con trỏ
nữa. Để tiết kiệm bộ nhớ, ta phải huỷ bỏ vùng nhớ của con trỏ ngay sau khi không dùng đến con
trỏ nữa. Cú pháp huỷ bỏ vùng nhớ của con trỏ như sau:
         delete <tên con trỏ>;
Ví dụ:
         int *pa = new int(12);         // Khai báo con trỏ pa, cấp phát bộ nhớ
                                        // và gán giá trị ban đầu cho pa là 12.
         delete pa;                     // Giải phóng vùng nhớ vừa cấp cho pa.
Lưu ý:
   •     Một con trỏ, sau khi bị giải phóng địa chỉ, vẫn có thể được cấp phát một vùng nhớ mới
         hoặc trỏ đến một địa chỉ mới:
                int *pa = new int(12);          // Khai báo con trỏ pa, cấp phát bộ nhớ
                                                // và gán giá trị ban đầu cho pa là 12.
                delete pa;                      // Giải phóng vùng nhớ vừa cấp cho pa.
                int A[5] = {5, 10, 15, 20, 25};
                pa = A;                         // Cho pa trỏ đến địa chỉ của mảng A
   •     Nếu có nhiều con trỏ cùng trỏ vào một địa chỉ, thì chỉ cần giải phóng bộ nhớ của một con
         trỏ, tất cả các con trỏ còn lại cũng bị giải phóng bộ nhớ:
                int *pa = new int(12);          // *pa = 12
                int *pb = pa;                   // pb trỏ đến cùng địa chỉ pa.
                *pb += 5;                       // *pa = *pb = 17
                delete pa;                      // Giải phóng cả pa lẫn pb


                                                                                                  21
Chương 2: Con trỏ và mảng

     •   Một con trỏ sau khi cấp phát bộ nhớ động bằng thao tác new, cần phải phóng bộ nhớ trước
         khi trỏ đến một địa chỉ mới hoặc cấp phát bộ nhớ mới:
                int *pa = new int(12);         // pa được cấp bộ nhớ và *pa = 12
                *pa = new int(15);             // pa trỏ đến địa chỉ khác và *pa = 15.
                                               // địa chỉ cũ của pa vẫn bị coi là bận


2.4.2 Cấp phát bộ nhớ cho mảng động một chiều

Cấp phát bộ nhớ cho mảng động một chiều
Mảng một chiều được coi là tương ứng với một con trỏ cùng kiểu. Tuy nhiên, cú pháp cấp phát bộ
nhớ cho mảng động một chiều là khác với cú pháp cấp phát bộ nhớ cho con trỏ thông thường:
         <Tên con trỏ> = new <Kiểu con trỏ>[<Độ dài mảng>];
Trong đó:
     •   Tên con trỏ: tên do người dùng đặt, tuân thủ theo quy tắc đặt tên biến của C++.
     •   Kiểu con trỏ: Kiểu dữ liệu cơ bản của C++ hoặc là kiểu do người dùng tự định nghĩa.
     •   Độ dài mảng: số lượng các phần tử cần cấp phát bộ nhớ của mảng.
Ví dụ:
         int *A = new int[5];
sẽ khai báo một mảng A có 5 phần tử kiểu int được cấp phát bộ nhớ động.
Lưu ý:
     •   Khi cấp phát bộ nhớ cho con trỏ có khởi tạo thông thường, ta dùng dấu “()”, khi cấp phát
         bộ nhớ cho mảng, ta dùng dấu “[]”. Hai lệnh cấp phát sau là hoàn toàn khác nhau:
                // Cấp phát bộ nhớ và khởi tạo cho một con trỏ int
                int *A = new int(5);
                // Cấp phát bộ nhớ cho một mảng 5 phần tử kiểu int
                int *A = new int[5];

Giải phóng bộ nhớ của mảng động một chiều
Để giải phóng vùng nhớ đã được cấp phát cho một mảng động, ta dùng cú pháp sau:
         delete [] <tên con trỏ>;
Ví dụ:
         // Cấp phát bộ nhớ cho một mảng có 5 phần tử kiểu int
         int *A = new int[5];
         // Giải phóng vùng nhớ do mảng A đang chiếm giữ.
         delete [] A;
Chương trình 2.4 minh hoạ hai thủ tục khởi tạo và giải phóng một mảng động một chiều.


 Chương trình 2.4
 void InitArray(int *A, int length){
          A = new int[length];
          for(int i=0; i<length; i++)

22
Chương 2: Con trỏ và mảng

                 A[i] = 0;
          return;
 }


 void DeleteArray(int *A){
          delete [] A;
          return;
 }


2.4.3 Cấp phát bộ nhớ cho mảng động nhiều chiều

Cấp phát bộ nhớ cho mảng động nhiều chiều
Một mảng hai chiều là một con trỏ đến một con trỏ. Do vậy, ta phải cấp phát bộ nhớ theo từng
chiều theo cú pháp cấp phát bộ nhớ cho mảng động một chiều.
Ví dụ:
         int **A;
         const int length = 10;
         A = new int*[length];         // Cấp phát bộ nhớ cho số dòng của ma trận A
         for(int i=0; i<length; i++)
                // Cấp phát bộ nhớ cho các phần tử của mỗi dòng
                A[i] = new int[length];
sẽ cấp phát bộ nhớ cho một mảng động hai chiều, tương đương với một ma trận có kích thước
10*10.


Lưu ý:
     •   Trong lệnh cấp phát A = new int*[length], cần phải có dấu “*” để chỉ ra rằng cần cấp
         phát bộ nhớ cho một mảng các phần tử có kiểu là con trỏ int (int*), khác với kiểu
         int bình thường.

Giải phóng bộ nhớ của mảng động nhiều chiều
Ngược lại với khi cấp phát, ta phải giải phóng lần lượt bộ nhớ cho con trỏ tương ứng với cột và
hàng của mảng động.
Ví dụ:
         int **A;
         …;                          // cấp phát bộ nhớ
         …
         for(int i=0; i<length; i++)
                delete [] A[i];      // Giải phóng bộ nhớ cho mỗi dòng
         delete [] A;                // Giải phóng bộ nhớ cho mảng các dòng
sẽ giải phóng bộ nhớ cho một mảng động hai chiều.
Chương trình 2.5 minh hoạ việc dùng mảng động hai chiều để tính tổng của hai ma trận.


                                                                                            23
Chương 2: Con trỏ và mảng

 Chương trình 2.5
 #include<stdio.h>
 #include<conio.h>


 /* Khai báo nguyên mẫu hàm */
 void InitArray(int **A, int row, int colum);
 void AddArray(int **A, int **B, int row, int colum);
 void DisplayArray(int **A, int row, int colum);
 void DeleteArray(int **A, int row);


 void InitArray(int **A, int row, int colum){
        A = new int*[row];
        for(int i=0; i<row; i++){
               A[i] = new int[colum];
               for(int j=0; j<colum; j++){
                      cout << “Phan tu [” << i << “,” << j << “] = ”;
                      cin >> A[i][j];
               }
        return;
 }


 void AddArray(int **A, int **B, int row, int colum){
        for(int i=0; i<row; i++)
        for(int j=0; j<colum; j++)
               A[i][j] += B[i][j];
        return;
 }


 void DisplayArray(int **A, int row, int colum){
        for(int i=0; i<row; i++){
               for(int j=0; j<colum; j++)
                      cout << A[i][j] << “ ”;
               cout << endl; // Xuống dòng
        return;
 }


 void DeleteArray(int **A, int row){
        for(int i=0; i<row; i++)
               delete [] A[i];
        delete [] A;
        return;
 }


24
Chương 2: Con trỏ và mảng

 void main(){
          clrscr();
          int **A, **B, row, colum;
          cout << “So dong: ”;
          cin >> row;
          cout << “So cot: ”;
          cin >> colum;


          /* Khởi tạo các ma trận */
          cout << “Khoi tao mang A:” << endl;
          InitArray(A, row, colum);


          cout << “Khoi tao mang B:” << endl;
          InitArray(B, row, colum);


          // Cộng hai ma trận
          AddArray(A, B, row, colum);


          // Hiển thị ma trận kết quả
          cout << “Tong hai mang A va mang B:” << endl;
          DisplayArray(A, row, colum);


          // Giải phóng bộ nhớ
          DeleteArray(A, row);
          DeleteArray(B, row);
          return;
 }


TỔNG KẾT CHƯƠNG 2
Nội dung chương 2 đã trình bày các vấn đề liên quan đến việc khai báo và sử dụng con trỏ và
mảng trong ngôn ngữ C++:
     •   Con trỏ là một kiểu biến đặc biệt, nó trỏ đến địa chỉ của một biến khác. Có hai cách truy
         nhập đến con trỏ là truy nhập đến địa chỉ hoặc truy nhập đến giá trị của địa chỉ mà con trỏ
         trỏ đến.
     •   Con trỏ có thể tham gia vào các phép toán như các biến thông thường bằng phép lấy giá
         trị.
     •   Một con trỏ có sự tương ứng với một mảng một chiều có cùng kiểu.
     •   Một ma trận hai chiều có thể thay thế bằng một mảng các con trỏ hoặc một con trỏ trỏ đến
         con trỏ.
     •   Một con trỏ có thể trỏ đến một hàm, khi đó, nó được dùng để gọi một hàm như là một
         tham số cho hàm khác.

                                                                                                 25
Chương 2: Con trỏ và mảng

     •   Một con trỏ cần phải trỏ vào một địa chỉ xác định hoặc phải được cấp phát bộ nhớ qua
         phép toán new và giải phóng bộ nhớ sau khi dùng bằng thao tác delete.


CÂU HỎI VÀ BÀI TẬP CHƯƠNG 2
     1. Trong các khai báo con trỏ sau, những khai báo nào là đúng:
                a. int A*;
                b. *int A;
                c. int* A, B;
                d. int* A, *B;
                e. int *A, *B;
     2. Với khai báo:
                int a = 12;
                int *pa;
         Các phép gán nào sau đây là hợp lệ:
                a. pa = &a;
                b. pa = a;
                c. *pa = &a;
                d. *pa = a;
     3. Với khai báo:
                int A[5] = {10, 20, 30, 40, 50};
                int *pa = A+2;
         Khi đó, *pa = ?
                a. 10
                b. 20
                c. 30
                d. 40
                e. 50
     4. Với đoạn chương trình:
                int A[5] = {10, 20, 30, 40, 50};
                int *pa = A;
                *pa += 2;
         Khi đó, *pa = ?
                a. 10
                b. 12
                c. 30
                d. 32
     5. Với đoạn chương trình:
                int A[5] = {10, 20, 30, 40, 50};
                int *pa = A;
26
Chương 2: Con trỏ và mảng

              pa += 2;
       Khi đó, *pa = ?
              a. 10
              b. 12
              c. 30
              d. 32
   6. Với đoạn chương trình:
              int A[5] = {10, 20, 30, 40, 50};
              int *pa = A;
              pa += 2;
       Khi đó, pa = ?
              a. &A[0]
              b. A[2]
              c. &A[2]
              d. Không xác định
   7. Với đoạn chương trình:
              int A[5] = {10, 20, 30, 40, 50};
              int *pa = A;
              pa -= 2;
       Khi đó, pa = ?
              a. &A[0]
              b. &A[2]
              c. &A[4]
              d. Không xác định
   8. Với đoạn chương trình:
              int A[3][3] = {
                                     {10, 20, 30},
                                     {40, 50, 60},
                                     {70, 80, 90}
                                     };
              int *pa;
       Khi đó, để có được kết quả *pa = 50, các lệnh nào sau đây là đúng?
              a. pa = A + 4;
              b. pa = (*(A+1)) + 1;
              c. pa = &A[1][1];
              d. pa = *((*(A+1)) + 1);
   9. Giả sử ta khai báo một hàm có sử dụng con trỏ hàm với khuôn mẫu như sau:
              int Calcul(int a, int b, int (*Xcalcul)(int x, int y)){}
       Và ta có cài đặt một số hàm như sau:

                                                                                 27
Chương 2: Con trỏ và mảng

                int add(int a, int b);
                void cal(int a, int b);
                int squere(int a);
        Khi đó, lời gọi hàm nào sau đây là đúng:
                a. Calcul(5, 10, add);
                b. Calcul(5, 10, add(2, 3));
                c. Calcul(5, 10, cal);
                d. Calcul(5, 10, squere);
     10. Ta muốn cấp phát bộ nhớ cho một con trỏ kiểu int và khởi đầu giá trị cho nó là 20. Lệnh
         nào sau đây là đúng:
                a. int *pa = 20;
                b. int *pa = new int{20};
                c. int *pa = new int(20);
                d. int *pa = new int[20];
     11. Ta muốn cấp phát bộ nhớ cho một mảng động kiểu int có chiều dài là 20. Lệnh nào sau
         đây là đúng:
                a. int *pa = 20;
                b. int *pa = new int{20};
                c. int *pa = new int(20);
                d. int *pa = new int[20];
     12. Xét đoạn chương trình sau:
                int A[5] = {10, 20, 30, 40, 50};
                int *pa = A;
                pa = new int(2);
        Khi đó, *pa = ?
                a. 10
                b. 30
                c. 2
                d. Không xác định
     13. Xét đoạn chương trình sau:
                1>        int A[5] = {10, 20, 30, 40, 50};
                2>        int *pa = A;
                3>        pa += 15;
                4>        delete pa;
        Đoạn chương trình trên có lỗi ở dòng nào?
                a. 1
                b. 2
                c. 3
                d. 4

28
Chương 2: Con trỏ và mảng

   14. Viết chương trình thực hiện các phép toán cộng, trừ, nhân, chia trên đa thức. Các đa thức
       được biểu diễn bằng mảng động một chiều. Bậc của đa thức và các hệ số tương ứng được
       nhập từ bàn phím.
   15. Viết chương trình thực hiện các phép toán cộng, trừ, nhân hai ma trận kích thước m*n.
       Các ma trận được biểu diễn bằng mảng động hai chiều. Giá trị kích cỡ ma trận (m, n) và
       giá trị các phần tử của ma trận được nhập từ bàn phím.




                                                                                             29
Chương 3: Kiểu dữ liệu cấu trúc


                                         CHƯƠNG 3
                             KIỂU DỮ LIỆU CẤU TRÚC

Nội dung chương này tập trung trình bày các vấn đề liên quan đến kiểu dữ liệu có cấu trúc trong
C++:
     •   Định nghĩa một cấu trúc
     •   Sử dụng một cấu trúc bằng các phép toán cơ bản trên cấu trúc
     •   Con trỏ cấu trúc, khai báo và sử dụng con trỏ cấu trúc
     •   Mảng các cấu trúc, khai báo và sử dụng mảng các cấu trúc
     •   Một số kiểu dữ liệu trừu tượng khác như ngăn xếp, hàng đợi, danh sách liên kết.


3.1 ĐỊNH NGHĨA CẤU TRÚC
Kiểu dữ liệu có cấu trúc được dùng khi ta cần nhóm một số biến dữ liệu luôn đi kèm với nhau.
Khi đó, việc xử lí trên một nhóm các biến được thực hiện như trên các biến cơ bản thông thường.

3.1.1 Khai báo cấu trúc
Trong C++, một cấu trúc do người dùng tự định nghĩa được khai báo thông qua từ khoá struct:
         struct <Tên cấu trúc>{
                 <Kiểu dữ liệu 1> <Tên thuộc tính 1>;
                 <Kiểu dữ liệu 2> <Tên thuộc tính 2>;
                 …
                 <Kiểu dữ liệu n> <Tên thuộc tính n>;
         };
Trong đó:
     •   struct: là tên từ khoá để khai báo một cấu trúc, bắt buộc phải có khi định nghĩa cấu trúc.
     •   Tên cấu trúc: là tên do người dùng tự định nghĩa, tuân thủ theo quy tắc đặt tên biến trong
         C++. Tên này sẽ trở thành tên của kiểu dữ liệu có cấu trúc tương ứng.
     •   Thuộc tính: mỗi thuộc tính của cấu trúc được khai báo như khai báo một biến thuộc kiểu
         dữ liệu thông thường, gồm có kiểu dữ liệu và tên biến tương ứng. Mỗi khai báo thuộc tính
         phải kết thúc bằng dấu chấm phẩy “;” như một câu lệnh C++ thông thường.
Ví dụ, để quản lí nhân viên của một công ty, khi xử lí thông tin về mỗi nhân viên, ta luôn phải xử
lí các thông tin liên quan như:
     •   Tên
     •   Tuổi
     •   Chức vụ
     •   Lương
Do đó, ta sẽ dùng cấu trúc để lưu giữ thông tin về mỗi nhân viên bằng cách định nghĩa một cấu
trúc có tên là Employeee với các thuộc tính như sau:
30
Chương 3: Kiểu dữ liệu cấu trúc

         struct Employeee{
                char name[20];         // Tên nhân viên
                int age;               // Tuổi nhân viên
                char role[20];         // Chức vụ của nhân viên
                float salary;          // Lương của nhân viên
         };
Lưu ý:
   •     Cấu trúc chỉ cần định nghĩa một lần trong chương trình và có thể được khai báo biến cấu
         trúc nhiều lần. Khi cấu trúc đã được định nghĩa, việc khai báo biến ở lần khác trong
         chương trình được thực hiện như khai báo biến thông thường:
                <Tên cấu trúc> <tên biến 1>, <tên biến 2>;
         Ví dụ, sau khi đã định nghĩa cấu trúc Employeee, muốn có biến myEmployeee, ta khai
         báo như sau:
                Employee myEmployeee;


3.1.2 Cấu trúc lồng nhau
Các cấu trúc có thể được định nghĩa lồng nhau khi một thuộc tính của một cấu trúc cũng cần có
kiểu là một cấu trúc khác. Khi đó, việc định nghĩa cấu trúc cha được thực hiện như một cấu trúc
bình thường, với khai báo về thuộc tính đó là một cấu trúc con:
         struct <Tên cấu trúc cha>{
                <Kiểu dữ liệu 1> <Tên thuộc tính 1>;
                // Có kiểu cấu trúc
                <Kiểu cấu trúc con> <Tên thuộc tính 2>;
                …
                <Kiểu dữ liệu n> <Tên thuộc tính n>;
         };
Ví dụ, với kiểu cấu trúc Employee, ta không quan tâm đến tuổi nhân viên nữa, mà quan tâm đến
ngày sinh của nhân viên. Vì ngày sinh cần có các thông tin luôn đi với nhau là ngày sinh, tháng
sinh, năm sinh. Do đó, ta định nghĩa một kiểu cấu trúc con cho kiểu ngày sinh:
         struct Date{
                int day;
                int month;
                int year;
         };
khi đó, cấu trúc Employee trở thành:
         struct Employee{
                char name[20];                // Tên nhân viên
                Date birthDay;                // Ngày sinh của nhân viên
                char role[20];                // Chức vụ của nhân viên
                float salary;                 // Lương của nhân viên
         };



                                                                                             31
Chương 3: Kiểu dữ liệu cấu trúc



Lưu ý:
     •   Trong định nghĩa các cấu trúc lồng nhau, cấu trúc con phải được định nghĩa trước cấu trúc
         cha để đảm bảo các kiểu dữ liệu của các thuộc tính của cấu trúc cha là tường minh tại thời
         điểm nó được định nghĩa.

3.1.3 Định nghĩa cấu trúc với từ khoá typedef
Để tránh phải dùng từ khoá struct mỗi khi khai báo biến cấu trúc, ta có thể dùng từ khóa typedef
khi định nghĩa cấu trúc:
         typedef struct {
                <Kiểu dữ liệu 1> <Tên thuộc tính 1>;
                <Kiểu dữ liệu 2> <Tên thuộc tính 2>;
                …
                <Kiểu dữ liệu n> <Tên thuộc tính n>;
         } <Tên kiểu dữ liệu cấu trúc>;
Trong đó:
     •   Tên kiểu dữ liệu cấu trúc: là tên kiểu dữ liệu của cấu trúc vừa định nghĩa. Tên này sẽ
         được dùng như một kiểu dữ liệu thông thường khi khai báo biến cấu trúc.
Ví dụ, muốn có kiểu dữ liệu có cấu trúc nhân viên, có tên là Employee, ta dùng từ khoá typedef
để định nghĩa cấu trúc như sau:
         typedef struct {
                char name[20];          // Tên nhân viên
                int age;                // Tuổi nhân viên
                char role[20];          // Chức vụ của nhân viên
                float salary;           // Lương của nhân viên
         } Employee;
Khi đó, muốn có hai biến là myEmployee1 và myEmployee2 có kiểu cấu trúc Employee, ta chỉ
cần khai báo như sau mà không cần từ khoá struct:
         Employee myEmployee1, myEmployee2;
Trong ví dụ khai báo lồng cấu trúc Employee, dùng từ khoá typedef cho kiểu Date:
         typedef struct {
                int day;
                int month;
                int year;
         } Date;
cấu trúc Employee trở thành:
         typedef struct {
                char name[20];          // Tên nhân viên
                Date birthDay;          // Ngày sinh của nhân viên
                char role[20];          // Chức vụ của nhân viên
                float salary;           // Lương của nhân viên
         } Employee;
32
Chương 3: Kiểu dữ liệu cấu trúc



Lưu ý:
   •     Khi không dùng từ khoá typedef, tên cấu trúc (nằm sau từ khoá struct) được dùng để khai
         báo biến. Trong khi đó, khi có từ khoá typedef, tên kiểu dữ liệu cấu trúc (dòng cuối cùng
         trong định nghĩa) mới được dùng để khai báo biến.
   •     Khi dùng từ khoá typedef thì không thể khai báo biến đồng thời với định nghĩa cấu trúc.


3.2 THAO TÁC TRÊN CẤU TRÚC
Các thao tác trên cấu trúc bao gồm:
   •     Khai báo và khởi tạo giá trị ban đầu cho biến cấu trúc
   •     Truy nhập đến các thuộc tính của cấu trúc

3.2.1 Khởi tạo giá trị ban đầu cho cấu trúc

Khởi tạo biến có cấu trúc đơn
Biến cấu trúc được khai báo theo các cách sau:
         <Tên kiểu dữ liệu cấu trúc> <tên biến>;
Ngoài ra, ta có thể khởi tạo các giá trị cho các thuộc tính của cấu trúc ngay khi khai báo bằng các
cú pháp sau:
         <Tên kiểu dữ liệu cấu trúc> <tên biến> = {
                <giá trị thuộc tính 1>,
                <giá trị thuộc tính 2>,
                …
                <giá trị thuộc tính n>
         };
Trong đó:
   •     Giá trị thuộc tính: là giá trị khởi đầu cho mỗi thuộc tính, có kiểu phù hợp với kiểu dữ
         liệu của thuộc tính. Mỗi giá trị của thuộc tính được phân cách bằng dấu phẩy “,”.
Ví dụ, với định nghĩa cấu trúc:
         typedef struct {
                char name[20];          // Tên nhân viên
                int age;                // Tuổi nhân viên
                char role[20];          // Chức vụ của nhân viên
                float salary;           // Lương của nhân viên
         } Employee;
thì có thể khai báo và khởi tạo cho một biến như sau:
         Employee myEmployee1 = {
                “Nguyen Van A”,
                27,
                “Nhan vien”,
                300f

                                                                                                33
Chương 3: Kiểu dữ liệu cấu trúc

        };

Khởi tạo các biến có cấu trúc lồng nhau
Trong trường hợp các cấu trúc lồng nhau, phép khởi tạo cũng thực hiện như thông thường với
phép khởi tạo cho tất cả các cấu trúc con.
Ví dụ với khai báo cấu trúc như sau:
        typedef struct {
                int day;
                int month;
                int year;
        } Date;
và:
        typedef struct {
                char name[20];         // Tên nhân viên
                Date birthDay;         // Ngày sinh của nhân viên
                char role[20];         // Chức vụ của nhân viên
                float salary;          // Lương của nhân viên
        } Employee;
Thì khai báo và khởi tạo một biến có kiểu Employee có thể thực hiện như sau:
        Employee myEmployee1 = {
                “Nguyen Van A”,
                {15, 05, 1980},        // Khởi tạo cấu trúc con
                “Nhan vien”,
                300f
        };


3.2.2 Truy nhập đến thuộc tính của cấu trúc
Việc truy nhập đến thuộc tính của cấu trúc được thực hiện bằng cú pháp:
        <Tên biến cấu trúc>.<tên thuộc tính>
Ví dụ, với một biến cấu trúc kiểu Employee đơn:
        Employee myEmployee1 = {
                “Nguyen Van A”,
                27,
                “Nhan vien”,
                300f
        };
ta có thể truy xuất như sau:
        cout << myEmployee1.name;             // hiển thị ra “Nguyen Van A”
        myEmployee1.age += 1;                 // Tăng số tuổi lên 1
Đối với kiểu cấu trúc lồng nhau, phép truy nhập đến thuộc tính được thực hiện lần lượt từ cấu trúc
cha đến cấu trúc con.
Ví dụ, với một biến cấu trúc kiểu Employee lồng nhau:
34
Chương 3: Kiểu dữ liệu cấu trúc




        Employee myEmployee1 = {
                “Nguyen Van A”,
                {15, 05, 1980},
                “Nhan vien”,
                300f
        };
ta có thể truy xuất như sau:
        cout << myEmployee1.name;                     // hiển thị ra “Nguyen Van A”
        myEmployee1.birthDay.day = 16;                // Sửa lại ngày sinh thành 16
        myEmployee1.birthDay.month = 07;              // Sửa lại tháng sinh thành 07
Chương trình 3.1a minh hoạ việc tạo lập và sử dụng cấu trúc Employee đơn, không dùng từ khoá
typedef.


 Chương trình 3.1a
 #include<stdio.h>
 #include<conio.h>
 #include<string.h>


 struct Employee{
         char name[20];           // Tên nhân viên
         int age;                 // Tuổi nhân viên
         char role[20];           // Chức vụ của nhân viên
         float salary;            // Lương của nhân viên
 };


 /* Khai báo khuôn mẫu hàm */
 void Display(Employee myEmployee);


 void Display(Employee myEmployee){
         cout << “Name: ” << myEmployee.name << endl;
         cout << “Age: ” << myEmployee.age << endl;
         cout << “Role: ” << myEmployee.role << endl;
         cout << “Salary: ” << myEmployee.salary << endl;
         return;
 }


 void main(){
         clrscr();
         // Hiển thị giá trị mặc định
         Employee myEmployee =

                                                                                         35
Chương 3: Kiểu dữ liệu cấu trúc

                {“Nguyen Van A”, 27, “Nhan vien”, 300f};
        cout << “Thông tin mặc định:” << endl;
        Display(myEmployee);


        // Thay đổi giá trị cho các thuộc tính
        cout << “Name: ”;
        cin >> myEmployee.name;
        cout << “Age: ”;
        cin >> myEmployee.age;
        cout << “Role: ”;
        cin >> myEmployee.role;
        cout << “Salary: ”;
        cin >> myEmployee.salary;


        cout << “Thông tin sau khi thay đổi:” << endl;
        Display(myEmployee);
        return;
 }


Chương trình 3.1b minh hoạ việc tạo lập và sử dụng cấu trúc Employee lồng nhau, có dùng từ
khoá typedef.


 Chương trình 3.1b
 #include<stdio.h>
 #include<conio.h>
 #include<string.h>


 typedef struct {
        int day;
        int month;
        int year;
 } Date;


 typedef struct {
        char name[20];            // Tên nhân viên
        Date birthDay;            // Ngày sinh của nhân viên
        char role[20];            // Chức vụ của nhân viên
        float salary;             // Lương của nhân viên
 } Employee;


 /* Khai báo khuôn mẫu hàm */
 void Display(Employee myEmployee);

36
Chương 3: Kiểu dữ liệu cấu trúc


 void Display(Employee myEmployee){
        cout << “Name: ” << myEmployee.name << endl;
        cout << “Birth day: ” << myEmployee.birthDay.day << “/”
                << myEmployee.birthDay.month << “/”
                << myEmployee.birthDay.year << endl;
        cout << “Role: ” << myEmployee.role << endl;
        cout << “Salary: ” << myEmployee.salary << endl;
        return;
 }


 void main(){
        clrscr();
        // Hiển thị giá trị mặc định
        Employee myEmployee =
                {“Nguyen Van A”, {15, 5, 1980}, “Nhan vien”, 300f};
        cout << “Thông tin mặc định:” << endl;
        Display(myEmployee);


        // Thay đổi giá trị cho các thuộc tính
        cout << “Name: ”;
        cin >> myEmployee.name;
        cout << “Day of birth: ”;
        cin >> myEmployee.birthDay.day;
        cout << “Month of birth: ”;
        cin >> myEmployee.birthDay.month;
        cout << “Year of birth: ”;
        cin >> myEmployee.birthDay.year;
        cout << “Role: ”;
        cin >> myEmployee.role;
        cout << “Salary: ”;
        cin >> myEmployee.salary;


        cout << “Thông tin sau khi thay đổi:” << endl;
        Display(myEmployee);
        return;
 }




                                                                      37
Chương 3: Kiểu dữ liệu cấu trúc

3.3 CON TRỎ CẤU TRÚC VÀ MẢNG CẤU TRÚC

3.3.1 Con trỏ cấu trúc
Con trỏ cấu trúc là một con trỏ trỏ đến địa chỉ của một biến có kiểu cấu trúc. Cách khai báo và sử
dụng con trỏ cấu trúc được thực hiện như con trỏ thông thường.

Khai báo con trỏ cấu trúc
Con trỏ cấu trúc được khai báo theo cú pháp:
          <Tên kiểu cấu trúc> *<Tên biến>;
Ví dụ, với kiểu khai báo cấu trúc:
          typedef struct {
                 int day;
                 int month;
                 int year;
          } Date;
và:
          typedef struct {
                 char name[20];         // Tên nhân viên
                 Date birthDay;         // Ngày sinh của nhân viên
                 char role[20];         // Chức vụ của nhân viên
                 float salary;          // Lương của nhân viên
          } Employee;
thì ta có thể khai báo một con trỏ cấu trúc như sau:
          Employee *ptrEmployee;
Lưu ý:
      •   Cũng như khai báo con trỏ thông thường, dấu con trỏ “*” có thể nằm ngay trước tên biến
          hoặc nằm ngay sau tên kiểu cấu trúc.
Cũng giống con trỏ thông thường, con trỏ cấu trúc được sử dụng khi:
      •   Cho nó trỏ đến địa chỉ của một biến cấu trúc
      •   Cấp phát cho nó một vùng nhớ xác định.

Gán địa chỉ cho con trỏ cấu trúc
Một con trỏ cấu trúc có thể trỏ đến địa chỉ của một biến cấu trúc có cùng kiểu thông qua phép gán:
          <Tên biến con trỏ> = &<Tên biến thường>;
Ví dụ, khai báo và phép gán:
          Employee *ptrEmployee, myEmployee;
          ptrEmployee = &myEmployee;
sẽ đưa con trỏ ptrEmployee trỏ đến địa chỉ của biến cấu trúc myEmployee.




38
Chương 3: Kiểu dữ liệu cấu trúc

Cấp phát bộ nhớ động cho con trỏ cấu trúc
Trong trường hợp ta muốn tạo ra một con trỏ cấu trúc mới, không trỏ vào một biến cấu trúc có sẵn
nào, để sử dụng con trỏ mới này, ta phải cấp phát vùng nhớ cho nó. Cú pháp cấp phát vùng nhớ
cho con trỏ cấu trúc:
          <Tên biến con trỏ> = new <Kiểu cấu trúc>;
Ví dụ, cấu trúc Employee được khai báo bằng từ khoá typedef, ta có thể cấp phát vùng nhớ cho
con trỏ cấu trúc như sau:
          Employee *ptrEmployee;
          ptrEmployee = new Employee;
hoặc cấp phát ngay khi khai báo:
          Employee *ptrEmployee = new Employee;
Sau khi cấp phát vùng nhớ cho con trỏ bằng thao tác new, khi con trỏ không được dùng nữa, hoặc
cần trỏ sang một địa chỉ khác, ta phải giải phóng vùng nhớ vừa được cấp phát cho con trỏ bằng
thao tác:
          delete <Tên biến con trỏ>;
Ví dụ:
          Employee *ptrEmployee = new Employee;
          …
          // Thực hiện các thao tác trên con trỏ
          …
          delete ptrEmployee;
Lưu ý:
   •      Thao tác delete chỉ được thực hiện đối với con trỏ mà trước đó, nó được cấp phát bộ nhớ
          động thông qua thao tác new:
                 Employee *ptrEmployee = new Employee;
                 delete ptrEmployee;             //đúng
          mà không thể thực hiện với con trỏ chỉ trỏ đến địa chỉ của một biến cấu trúc khác:
                 Employee *ptrEmployee, myEmployee;
                 ptrEmployee = &myEmployee;
                 delete ptrEmployee;             //lỗi

Truy nhập thuộc tính của con trỏ cấu trúc
Thuộc tính của con trỏ cấu trúc có thể được truy nhập thông qua hai cách:
Cách 1:
          <Tên biến con trỏ> -> <Tên thuộc tính>;
Cách 2:
          (*<Tên biến con trỏ>).<Tên thuộc tính>;
Ví dụ, thuộc tính tên nhân viên của cấu trúc Employee có thể được truy nhập thông qua hai cách:
          Employee *ptrEmployee = new Employee;
          cin >> ptrEmployee -> name;
hoặc:

                                                                                               39
Chương 3: Kiểu dữ liệu cấu trúc

         cin >> (*ptrEmployee).name;
Lưu ý:
     •   Trong cách truy nhập thứ hai, phải có dấu ngoặc đơn “()” quanh tên con trỏ vì phép toán
         truy nhập thuộc tính “.” có độ ưu tiên cao hơn phép toán lấy giá trị con trỏ “*”.
     •   Thông thường, ta dùng cách thứ nhất cho đơn giản và thuận tiện.
Chương trình 3.2 cài đặt việc khởi tạo và hiển thị nội dung của một con trỏ cấu trúc.


 Chương trình 3.2
 #include<stdio.h>
 #include<conio.h>
 #include<string.h>


 typedef struct {
          int day;
          int month;
          int year;
 } Date;


 typedef struct {
          char name[20];          // Tên nhân viên
          Date birthDay;          // Ngày sinh của nhân viên
          char role[20];          // Chức vụ của nhân viên
          float salary;           // Lương của nhân viên
 } Employee;


 /* Khai báo khuôn mẫu hàm */
 void InitStruct(Employee *myEmployee);
 void Display(Employee *myEmployee);


 void InitStruct(Employee *myEmployee){
          myEmployee = new Employee;
          cout << “Name: ”;
          cin >> myEmployee->name;
          cout << “Day of birth: ”;
          cin >> myEmployee->birthDay.day;
          cout << “Month of birth: ”;
          cin >> myEmployee->birthDay.month;
          cout << “Year of birth: ”;
          cin >> myEmployee->birthDay.year;
          cout << “Role: ”;
          cin >> myEmployee->role;
          cout << “Salary: ”;

40
Chương 3: Kiểu dữ liệu cấu trúc

         cin >> myEmployee->salary;
 }


 void Display(Employee myEmployee){
         cout << “Name: ” << myEmployee->name << endl;
         cout << “Birth day: ” << myEmployee->birthDay.day << “/”
                   << myEmployee->birthDay.month << “/”
                   << myEmployee->birthDay.year << endl;
         cout << “Role: ” << myEmployee->role << endl;
         cout << “Salary: ” << myEmployee->salary << endl;
         return;
 }


 void main(){
         clrscr();
         Employee *myEmployee;
         InitStruct(myEmployee);
         Display(myEmployee);
         return;
 }


3.3.2 Mảng cấu trúc
Khi cần xử lí nhiều đối tượng có dùng kiểu dữ liệu cấu trúc, ta có thể sử dụng mảng các cấu trúc.
Vì một mảng một chiều là tương đương với một con trỏ có cùng kiểu. Do đó, có thể khai báo
mảng theo hai cách: Khai báo mảng tĩnh như thông thường hoặc khai báo mảng động thông qua
con trỏ.

Khai báo mảng tĩnh các cấu trúc
Khai báo mảng tĩnh các cấu trúc theo cú pháp:
         <Tên kiểu cấu trúc> <Tên biến mảng>[<Số phần tử mảng>];
Ví dụ:
         Employee employees[10];
là khai báo một mảng tên là employees gồm 10 phần tử có kiểu là cấu trúc Employee.

Khai báo mảng động các cấu trúc
Khai báo một mảng động các cấu trúc hoàn toàn tương tự khai báo một con trỏ cấu trúc cùng kiểu:
         <Tên kiểu cấu trúc> *<Tên biến>;
Ví dụ, khai báo:
         Employee *employees;
vừa có thể coi là khai báo một con trỏ thông thường có cấu trúc Employee, vừa có thể coi là khai
báo một mảng động các cấu trúc có kiểu cấu trúc Employee.


                                                                                              41
Chương 3: Kiểu dữ liệu cấu trúc

Tuy nhiên, cách cấp phát bộ nhớ động cho mảng các cấu trúc khác với một con trỏ. Đây là cách
để chương trình nhận biết ta đang dùng một con trỏ cấu trúc hay một mảng động có cấu trúc. Cú
pháp cấp phát bộ nhớ cho mảng động như sau:
       <Tên biến mảng> = new <Kiểu cấu trúc>[<Số lượng phần tử>];
Ví dụ, khai báo:
       Employee *employees = new Employee[10];
sẽ cấp phát bộ nhớ cho một mảng động employees có 10 phần tử kiểu cấu trúc Employee.

Truy nhập đến phần tử của mảng cấu trúc
Việc truy nhập đến các phần tử của mảng cấu trúc được thực hiện như truy cập đến phần tử của
mảng thông thường. Ví dụ muốn truy nhập đến thuộc tính tên nhân viên phần tử nhân viên thứ i
trong mảng cấu trúc, ta viết như sau:
       Employee *employees = new Employee[10];
       employees[i].name;
Chương trình 3.3 cài đặt việc khởi tạo một mảng các nhân viên của một phòng trong một công ty.
Sau đó, chương trình sẽ tìm và in ra thông tin về nhân viên có lương cao nhất và nhân viên có
lương thấp nhất trong phòng.


 Chương trình 3.3
 #include<stdio.h>
 #include<conio.h>
 #include<string.h>


 typedef struct {
        int day;
        int month;
        int year;
 } Date;


 typedef struct {
        char name[20];            // Tên nhân viên
        Date birthDay;            // Ngày sinh của nhân viên
        char role[20];            // Chức vụ của nhân viên
        float salary;             // Lương của nhân viên
 } Employee;


 /* Khai báo khuôn mẫu hàm */
 void InitArray(Employee *myEmployee, int length);
 Employee searchSalaryMax(Employee *myEmployee, int length);
 Employee searchSalaryMin(Employee *myEmployee, int length);
 void Display(Employee myEmployee);



42
Chương 3: Kiểu dữ liệu cấu trúc

 void InitArray(Employee *myEmployee, int length){
        myEmployee = new Employee[length];
        for(int i=0; i<length; i++){
                cout << “Nhan vien thu ” << i << endl;
                cout << “Name: ”;
                cin >> myEmployee[i].name;
                cout << “Day of birth: ”;
                cin >> myEmployee[i].birthDay.day;
                cout << “Month of birth: ”;
                cin >> myEmployee[i].birthDay.month;
                cout << “Year of birth: ”;
                cin >> myEmployee[i].birthDay.year;
                cout << “Role: ”;
                cin >> myEmployee[i].role;
                cout << “Salary: ”;
                cin >> myEmployee[i].salary;
        }
        return;
 }


 Employee searchSalaryMax(Employee *myEmployee, int length){
        int index = 0;
        int maxSalary = myEmployee[0].salary;
        for(int i=1; i<length; i++)
                if(myEmployee[i].salary > maxSalary){
                        maxSalary = myEmployee[i].salary;
                        index = i;
                }
        return myEmployee[index];
 }


 Employee searchSalaryMin(Employee *myEmployee, int length){
        int index = 0;
        int minSalary = myEmployee[0].salary;
        for(int i=1; i<length; i++)
                if(myEmployee[i].salary < minSalary){
                        minSalary = myEmployee[i].salary;
                        index = i;
                }
        return myEmployee[index];
 }


 void Display(Employee myEmployee){

                                                               43
Chương 3: Kiểu dữ liệu cấu trúc

          cout << “Name: ” << myEmployee.name << endl;
          cout << “Birth day: ” << myEmployee.birthDay.day << “/”
                  << myEmployee.birthDay.month << “/”
                  << myEmployee.birthDay.year << endl;
          cout << “Role: ” << myEmployee.role << endl;
          cout << “Salary: ” << myEmployee.salary << endl;
          return;
 }


 void main(){
          clrscr();
          Employee *myEmployee, tmpEmployee;
          int length = 0;
          cout << “So luong nhan vien: ”;
          cin >> length;


          // Khởi tạo danh sách nhân viên
          InitArray(myEmployee);


          // Nhân viên có lương cao nhất
          tmpEmployee = searchSalaryMax(myEmployee, length);
          Display(tmpEmployee);


          // Nhân viên có lương thấp nhất
          tmpEmployee = searchSalaryMin(myEmployee, length);
          Display(tmpEmployee);


          // Giải phóng vùng nhớ
          delete [] myEmployee;
          return;
 }


3.4 MỘT SỐ KIỂU DỮ LIỆU TRỪU TƯỢNG
Nội dung phần này tập trung trình bày việc cài đặt một số cấu trúc dữ liệu trừu tượng, bao gồm:
     •   Ngăn xếp (stack)
     •   Hàng đợi (queue)
     •   Danh sách liên kết (list)




44
Chương 3: Kiểu dữ liệu cấu trúc

3.4.1 Ngăn xếp
Ngăn xếp (stack) là một kiểu danh sách cho phép thêm và bớt các phần tử ở một đầu danh sách,
gọi là đỉnh của ngăn xếp. Ngăn xếp hoạt động theo nguyên lí: phần tử nào được đưa vào sau, sẽ
được lấy ra trước.

Định nghĩa cấu trúc ngăn xếp
Vì ta chỉ cần quan tâm đến hai thuộc tính của ngăn xếp là:
   •   Danh sách các phần tử của ngăn xếp
   •   Vị trí đỉnh của ngăn xếp
nên ta có thể định nghĩa cấu trúc ngăn xếp như sau (các phần tử của ngăn xếp có kiểu int):
       typedef SIZE 100;
       typedef struct {
               int top;                // Vị trí của đỉnh
               int nodes[SIZE];        // Danh sách các phần tử
       } Stack;
Tuy nhiên, định nghĩa này tồn tại một vấn đề, đó là kích thước (SIZE) của danh sách chứa các
phần tử là tĩnh. Do đó:
   •   Nếu ta chọn SIZE lớn, nhưng khi gặp ứng dụng chỉ cần một số ít phần tử cho ngăn xếp thì
       rất tốn bộ nhớ.
   •   Nếu ta khai báo SIZE nhỏ, thì khi gặp bài toán cần ngăn xếp có nhiều phần tử, ta sẽ không
       thêm được các phần tử mới vào, chương trình sẽ có lỗi.
Để khắc phục hạn chế này, ta có thể sử dụng bộ nhớ động (mảng động thông qua con trỏ) để lưu
danh sách các phần tử của ngăn xếp. Khi đó, định nghĩa cấu trúc ngăn xếp sẽ có dạng như sau:
       typedef struct {
               int top;                // Vị trí của đỉnh
               int *nodes;             // Danh sách các phần tử
       } Stack;
Ta sẽ sử dụng định nghĩa này trong các chương trình ứng dụng ngăn xếp.

Các thao tác trên ngăn xếp
Đối với các thao tác trên ngăn xếp, ta quan tâm đến hai thao tác cơ bản:
   •   Thêm một phần tử mới vào đỉnh ngăn xếp, gọi là push.
   •   Lấy ra một phần tử từ đỉnh ngăn xếp, gọi là pop.
Khi thêm một phần tử mới vào ngăn xếp, ta làm các bước như sau:
   1. Số phần tử trong ngăn xếp cũ là (top+1). Do đó, ta cấp phát một vùng nhớ mới để lưu
      được (top+1+1) = (top+2) phần tử.
   2. Sao chép (top+1) phần tử cũ sang vùng mới. Nếu danh sách ban đầu rỗng (top = -1) thì
      không cần thực hiện bước này.
   3. Thêm phần tử mới vào cuối vùng nhớ mới
   4. Giải phóng vùng nhớ của danh sách cũ

                                                                                             45
Chương 3: Kiểu dữ liệu cấu trúc

     5. Cho danh sách nodes trỏ vào vùng nhớ mới.
Chương trình 3.4a cài đặt thủ tục thêm một phần tử mới vào ngăn xếp.


 Chương trình 3.4a
 void push(Stack *stack, int node){
          int *tmpNodes = new int[stack->top + 2];// Cấp phát vùng nhớ mới
          stack->top ++;                               // Tăng chỉ số của node đỉnh
          for(int i=0; i<stack->top; i++)              // Sao chép sang vùng nhớ mới
                  tmpNodes[i] = stack->nodes[i];
          tmpNodes[stack->top] = node;                 // Thêm node mới vào đỉnh
          delete [] stack->nodes;                      // Giải phóng vùng nhớ cũ
          stack->nodes = tmpNodes;                     // Trỏ vào vùng nhớ mới
          return;
 }


Khi lấy ra một phần tử của ngăn xếp, ta làm các bước như sau:
     •   Kiểm tra xem ngăn xếp có rỗng (top = -1) hay không. Nếu không rỗng thì thực hiện các
         bước tiếp theo.
     •   Lấy phần tử ở đỉnh ngăn xếp ra
     •   Cấp phát một vùng nhớ mới có (top+1) -1 = top phần tử
     •   Sao chép top phần tử từ danh sách cũ sang vùng nhớ mới (trừ phần tử ở đỉnh).
     •   Giải phóng vùng nhớ cũ
     •   Cho con trỏ danh sách trỏ vào vùng nhớ mới.
     •   Trả về giá trị phần tử ở đỉnh đã lấy ra.
Chương trình 3.4b cài đặt thủ tục lấy một phần tử từ ngăn xếp.


 Chương trình 3.4b
 int pop(Stack *stack){
          if(stack->top < 0){                          // Kiểm tra ngăn xếp rỗng
                  cout << “Stack is empty!” << endl;
                  return 0;
          }
          int result = stack->nodes[stack->top];// Lưu giữ giá trị đỉnh
          int *tmpNodes = new int[stack->top];// Cấp phát vùng nhớ mới
          for(int i=0; i<stack->top; i++)              // Sao chép sang vùng nhớ mới
                  tmpNodes[i] = stack->nodes[i];
          stack->top --;                               // Giảm chỉ số của node đỉnh
          delete [] stack->nodes;                      // Giải phóng vùng nhớ cũ
          stack->nodes = tmpNodes;                     // Trỏ vào vùng nhớ mới
          return result;                               // Trả về giá trị node đỉnh


46
Chương 3: Kiểu dữ liệu cấu trúc

 }



Áp dụng
Ngăn xếp được sử dụng trong các ứng dụng thoả mãn nguyên tắc: cái nào đặt vào trước sẽ được
lấy ra sau. Chương trính 3.4c minh hoạ việc dùng ngăn xếp để đảo ngược một xâu kí tự được nhập
vào từ bàn phím.


 Chương trình 3.4c
 #include<stdio.h>
 #include<stdlib.h>
 #include<string.h>


 typedef struct {
        int top;                             // Vị trí node đỉnh
        int *nodes;                          // Danh sách phần tử
 } Stack;


 /* Khai báo nguyên mẫu hàm */
 void init(Stack *stack);
 void push(Stack *stack, int node);
 int pop(Stack *stack);
 void release(Stack *stack);


 void init(Stack *stack){
        stack = new Stack;                   // Cấp phát vùng nhớ cho con trỏ
        stack->top = -1;                     // Khởi tạo ngăn xếp rỗng
 }


 void push(Stack *stack, int node){
        int *tmpNodes = new int[stack->top + 2];// Cấp phát vùng nhớ mới
        stack->top ++;                               // Tăng chỉ số của node đỉnh
        for(int i=0; i<stack->top; i++)              // Sao chép sang vùng nhớ mới
                tmpNodes[i] = stack->nodes[i];
        tmpNodes[stack->top] = node;                 // Thêm node mới vào đỉnh
        delete [] stack->nodes;                      // Giải phóng vùng nhớ cũ
        stack->nodes = tmpNodes;                     // Trỏ vào vùng nhớ mới
        return;
 }


 int pop(Stack *stack){
        if(stack->top < 0){                          // Kiểm tra ngăn xếp rỗng

                                                                                           47
Chương 3: Kiểu dữ liệu cấu trúc

                 cout << “Stack is empty!” << endl;
                 return 0;
        }
        int result = stack->nodes[stack->top];// Lưu giữ giá trị đỉnh
        int *tmpNodes = new int[stack->top];// Cấp phát vùng nhớ mới
        for(int i=0; i<stack->top; i++)              // Sao chép sang vùng nhớ mới
                 tmpNodes[i] = stack->nodes[i];
        stack->top --;                               // Giảm chỉ số của node đỉnh
        delete [] stack->nodes;                      // Giải phóng vùng nhớ cũ
        stack->nodes = tmpNodes;                     // Trỏ vào vùng nhớ mới
        return result;                               // Trả về giá trị node đỉnh
 }


 void release(Stack *stack){
        delete [] stack->nodes;                      // Giải phóng vùng danh sách
        delete stack;                                // Giải phóng con trỏ
        return;
 }


 void main(){
        clrscr();
        Stack *stack;
        init(stack);                                 // Khởi tạo ngăn xếp
        char strIn[250];
        // Nhập chuỗi kí tự từ bàn phím
        cout << “Nhap chuoi: ”;
        cin >> strIn;
        for(int i=0; i<strlen(strIn); i++)           // Đặt vào ngăn xếp
                 push(stack, strIn[i]);
        while(stack->top > -1)                       // Lấy ra từ ngăn xếp
                 cout << pop(stack);
        release(stack);                              // Giải phóng bộ nhớ
        return;
 }


3.4.2 Hàng đợi
Hàng đợi (queue) cũng là một cấu trúc tuyến tính các phần tử. Trong đó, các phần tử luôn được
thêm vào ở một đầu, gọi là đầu cuối hàng đợi, và việc lấy ra các phần tử luôn được thực hiện ở
đầu còn lại, gọi là đầu mặt của hàng đợi. Hàng đợi hoạt động theo nguyên lí: phần tử nào được
đưa vào trước, sẽ được lấy ra trước.




48
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++

More Related Content

What's hot

Hdth01 ltudql2-lap tinhduongdoituong-vb
Hdth01 ltudql2-lap tinhduongdoituong-vbHdth01 ltudql2-lap tinhduongdoituong-vb
Hdth01 ltudql2-lap tinhduongdoituong-vbDũng Đinh
 
13690151 slide-phan-tich-thiet-ke-he-thong-huong-doi-tuong-dai-hoc-bach-khoa-...
13690151 slide-phan-tich-thiet-ke-he-thong-huong-doi-tuong-dai-hoc-bach-khoa-...13690151 slide-phan-tich-thiet-ke-he-thong-huong-doi-tuong-dai-hoc-bach-khoa-...
13690151 slide-phan-tich-thiet-ke-he-thong-huong-doi-tuong-dai-hoc-bach-khoa-...
leethinh
 
@Nmcntt2 do an#2-khmt-cntt-tgmt-ver0.4
@Nmcntt2 do an#2-khmt-cntt-tgmt-ver0.4@Nmcntt2 do an#2-khmt-cntt-tgmt-ver0.4
@Nmcntt2 do an#2-khmt-cntt-tgmt-ver0.4
khoa khoa
 
Project plan
Project planProject plan
Project plan
khoa khoa
 
Bài 2: Lập trình hướng đối tượng (OOP) - Giáo trình FPT
Bài 2: Lập trình hướng đối tượng (OOP) - Giáo trình FPTBài 2: Lập trình hướng đối tượng (OOP) - Giáo trình FPT
Bài 2: Lập trình hướng đối tượng (OOP) - Giáo trình FPT
MasterCode.vn
 
K3410301 tran le duy- bai 9-bao cao va ket xuat bao cao
K3410301  tran le duy- bai 9-bao cao va ket xuat bao caoK3410301  tran le duy- bai 9-bao cao va ket xuat bao cao
K3410301 tran le duy- bai 9-bao cao va ket xuat bao caoSP Tin K34
 
OOP Review - Ôn tập Hướng Đối Tượng
OOP Review - Ôn tập Hướng Đối TượngOOP Review - Ôn tập Hướng Đối Tượng
OOP Review - Ôn tập Hướng Đối Tượng
Nguyễn Quang Thiện
 

What's hot (7)

Hdth01 ltudql2-lap tinhduongdoituong-vb
Hdth01 ltudql2-lap tinhduongdoituong-vbHdth01 ltudql2-lap tinhduongdoituong-vb
Hdth01 ltudql2-lap tinhduongdoituong-vb
 
13690151 slide-phan-tich-thiet-ke-he-thong-huong-doi-tuong-dai-hoc-bach-khoa-...
13690151 slide-phan-tich-thiet-ke-he-thong-huong-doi-tuong-dai-hoc-bach-khoa-...13690151 slide-phan-tich-thiet-ke-he-thong-huong-doi-tuong-dai-hoc-bach-khoa-...
13690151 slide-phan-tich-thiet-ke-he-thong-huong-doi-tuong-dai-hoc-bach-khoa-...
 
@Nmcntt2 do an#2-khmt-cntt-tgmt-ver0.4
@Nmcntt2 do an#2-khmt-cntt-tgmt-ver0.4@Nmcntt2 do an#2-khmt-cntt-tgmt-ver0.4
@Nmcntt2 do an#2-khmt-cntt-tgmt-ver0.4
 
Project plan
Project planProject plan
Project plan
 
Bài 2: Lập trình hướng đối tượng (OOP) - Giáo trình FPT
Bài 2: Lập trình hướng đối tượng (OOP) - Giáo trình FPTBài 2: Lập trình hướng đối tượng (OOP) - Giáo trình FPT
Bài 2: Lập trình hướng đối tượng (OOP) - Giáo trình FPT
 
K3410301 tran le duy- bai 9-bao cao va ket xuat bao cao
K3410301  tran le duy- bai 9-bao cao va ket xuat bao caoK3410301  tran le duy- bai 9-bao cao va ket xuat bao cao
K3410301 tran le duy- bai 9-bao cao va ket xuat bao cao
 
OOP Review - Ôn tập Hướng Đối Tượng
OOP Review - Ôn tập Hướng Đối TượngOOP Review - Ôn tập Hướng Đối Tượng
OOP Review - Ôn tập Hướng Đối Tượng
 

Similar to Ngon ngu lap_trinh_c++

Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
Thang DV
 
C++ bưu chính viễn thông
C++ bưu chính viễn thôngC++ bưu chính viễn thông
C++ bưu chính viễn thông
Tiên Lý Rau Rút
 
Bai giang c++
Bai giang c++Bai giang c++
Bai giang c++
Nhân Tâm
 
Bai giang c++
Bai giang c++Bai giang c++
Bai giang c++
Thang DV
 
Lập trình hướng đối tượng với java
Lập trình hướng đối tượng với javaLập trình hướng đối tượng với java
Lập trình hướng đối tượng với java
Ngô Đăng Tân
 
OOP
OOPOOP
Lập trình hướng đối tượng với Java - Trần Đình Quế
Lập trình hướng đối tượng với Java  - Trần Đình QuếLập trình hướng đối tượng với Java  - Trần Đình Quế
Lập trình hướng đối tượng với Java - Trần Đình Quế
f3vthd
 
Lthdt
LthdtLthdt
Lthdt
tammao123
 
lap-trinh-huong-doi-tuong_nguyen-manh-son_lthdt_ptit - [cuuduongthancong.com]...
lap-trinh-huong-doi-tuong_nguyen-manh-son_lthdt_ptit - [cuuduongthancong.com]...lap-trinh-huong-doi-tuong_nguyen-manh-son_lthdt_ptit - [cuuduongthancong.com]...
lap-trinh-huong-doi-tuong_nguyen-manh-son_lthdt_ptit - [cuuduongthancong.com]...
tPhan78
 
Windows Programming Tác giả: Bộ môn Công nghệ phần mềm; Người hướng dẫn: -; ...
Windows Programming Tác giả: Bộ môn Công nghệ phần mềm;  Người hướng dẫn: -; ...Windows Programming Tác giả: Bộ môn Công nghệ phần mềm;  Người hướng dẫn: -; ...
Windows Programming Tác giả: Bộ môn Công nghệ phần mềm; Người hướng dẫn: -; ...
VTrung46
 
Lap trinh huong_doi_tuong
Lap trinh huong_doi_tuongLap trinh huong_doi_tuong
Lap trinh huong_doi_tuong
vovantrjnh
 
45128de03569526f74ab41afe7186aef
45128de03569526f74ab41afe7186aef45128de03569526f74ab41afe7186aef
45128de03569526f74ab41afe7186aef
Phi Phi
 
Ky thuat lap trinh
Ky thuat lap trinhKy thuat lap trinh
Ky thuat lap trinh
QaPhy-Duong MTE
 
Baigiangkythuatlaptrinh hvbcvt
Baigiangkythuatlaptrinh hvbcvtBaigiangkythuatlaptrinh hvbcvt
Baigiangkythuatlaptrinh hvbcvtAricent Bug
 
Ky thuat-lap-trinh 13754342619
Ky thuat-lap-trinh 13754342619Ky thuat-lap-trinh 13754342619
Ky thuat-lap-trinh 13754342619Ky Nguyen Ad
 

Similar to Ngon ngu lap_trinh_c++ (20)

Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++Ngon ngu lap_trinh_c++
Ngon ngu lap_trinh_c++
 
C++ bưu chính viễn thông
C++ bưu chính viễn thôngC++ bưu chính viễn thông
C++ bưu chính viễn thông
 
Ngon ngu lap_trinh_c__
Ngon ngu lap_trinh_c__Ngon ngu lap_trinh_c__
Ngon ngu lap_trinh_c__
 
Bai giang c++
Bai giang c++Bai giang c++
Bai giang c++
 
Bai giang c++
Bai giang c++Bai giang c++
Bai giang c++
 
C++ PTIT
C++ PTITC++ PTIT
C++ PTIT
 
Lập trình hướng đối tượng với java
Lập trình hướng đối tượng với javaLập trình hướng đối tượng với java
Lập trình hướng đối tượng với java
 
OOP
OOPOOP
OOP
 
Lập trình hướng đối tượng với Java - Trần Đình Quế
Lập trình hướng đối tượng với Java  - Trần Đình QuếLập trình hướng đối tượng với Java  - Trần Đình Quế
Lập trình hướng đối tượng với Java - Trần Đình Quế
 
Lthdt
LthdtLthdt
Lthdt
 
lap-trinh-huong-doi-tuong_nguyen-manh-son_lthdt_ptit - [cuuduongthancong.com]...
lap-trinh-huong-doi-tuong_nguyen-manh-son_lthdt_ptit - [cuuduongthancong.com]...lap-trinh-huong-doi-tuong_nguyen-manh-son_lthdt_ptit - [cuuduongthancong.com]...
lap-trinh-huong-doi-tuong_nguyen-manh-son_lthdt_ptit - [cuuduongthancong.com]...
 
Phạm văn ất
Phạm văn ấtPhạm văn ất
Phạm văn ất
 
Windows Programming Tác giả: Bộ môn Công nghệ phần mềm; Người hướng dẫn: -; ...
Windows Programming Tác giả: Bộ môn Công nghệ phần mềm;  Người hướng dẫn: -; ...Windows Programming Tác giả: Bộ môn Công nghệ phần mềm;  Người hướng dẫn: -; ...
Windows Programming Tác giả: Bộ môn Công nghệ phần mềm; Người hướng dẫn: -; ...
 
Huong doi tuong
Huong doi tuongHuong doi tuong
Huong doi tuong
 
Lap trinh huong_doi_tuong
Lap trinh huong_doi_tuongLap trinh huong_doi_tuong
Lap trinh huong_doi_tuong
 
45128de03569526f74ab41afe7186aef
45128de03569526f74ab41afe7186aef45128de03569526f74ab41afe7186aef
45128de03569526f74ab41afe7186aef
 
Ky thuat lap trinh
Ky thuat lap trinhKy thuat lap trinh
Ky thuat lap trinh
 
Ktlt
KtltKtlt
Ktlt
 
Baigiangkythuatlaptrinh hvbcvt
Baigiangkythuatlaptrinh hvbcvtBaigiangkythuatlaptrinh hvbcvt
Baigiangkythuatlaptrinh hvbcvt
 
Ky thuat-lap-trinh 13754342619
Ky thuat-lap-trinh 13754342619Ky thuat-lap-trinh 13754342619
Ky thuat-lap-trinh 13754342619
 

Ngon ngu lap_trinh_c++

  • 1. HỌC VIỆN CÔNG NGHỆ BƯU CHÍNH VIỄN THÔNG NGÔN NGỮ LẬP TRÌNH C++ (Dùng cho sinh viên hệ đào tạo đại học từ xa) Lưu hành nội bộ HÀ NỘI - 2006
  • 2. NGÔN NGỮ LẬP TRÌNH C++ PGS.TS. Trần Đình Quế KS. Nguyễn Mạnh Hùng Lập trình nâng cao với C++ Lập trình hướng đối tượng với C++
  • 3. 2
  • 4. GIỚI THIỆU C++ là ngôn ngữ lập trình hướng đối tượng được mở rộng từ ngôn ngữ C. Do vậy, C++ có ưu điểm là kế thừa được các điểm mạnh truyền thống của ngôn ngữ C như uyển chuyển, tương thích với các thiết bị phần cứng. Hiện nay, C++ là một ngôn ngữ lập trình phổ biến, được giảng dạy tại các trường đại học trong nước và trên thế giới và đặc biệt được sử dụng rộng rãi cho nhu cầu phát triển của công nghiệp phần mềm hiện nay. Tài liệu này không những nhằm giới thiệu cho sinh viên ngôn ngữ lập trình C++, mà còn mong muốn qua đó sinh viên có thể hiểu được tư tưởng của phương pháp lập trình hướng đối tượng nói chung. Nội dung của tài liệu bao gồm hai phần chính: • Phần thứ nhất là lập trình nâng cao với C++, bao gồm lập trình C++ với con trỏ và mảng, các kiểu dữ liệu có cấu trúc cùng các thao tác vào ra trên tệp. • Phần thứ hai là lập trình hướng đối tượng với C++, bao gồm các định nghĩa và các thao tác trên lớp đối tượng, tính kế thừa và tương ứng bội trong C++, cách sử dụng một số lớp cơ bản trong thư viện C++. Nội dung tài liệu được tổ chức thành 7 chương: Chương 1: Giới thiệu tổng quan về các phương pháp lập trình Trình bày các phương pháp lập trình tuyến tính, lập trình cấu trúc và đặc biệt, làm quen với các khái niệm trong lập trình hướng đối tượng. Chương 2: Con trỏ và mảng Trình bày cách khai báo và sử dụng các kiểu con trỏ và mảng trong ngôn ngữ C++. Chương 3: Kiểu dữ liệu có cấu trúc Trình bày cách biểu diễn và cài đặt một số kiểu cấu trúc dữ liệu trừu tượng trong C++. Sau đó, trình bày cách áp dụng các kiểu dữ liệu này trong các ứng dụng cụ thể. Chương 4: Vào ra trên tệp Trình bày các thao tác đọc, ghi dữ liệu trên các tệp tin khác nhau: tệp tin văn bản và tệp tin nhị phân. Trình bày các cách truy nhập tệp tin trực tiếp. Chương 5: Lớp đối tượng Trình bày các khái niệm mở đầu cho lập trình hướng đối tượng trong C++, bao gồm cách khai báo và sử dụng lớp, các thuộc tính của lớp; cách khởi tạo và huỷ bỏ đối tượng, các quy tắc truy nhập đến các thành phần của lớp. Chương 6: Tính kế thừa và tương ứng bội Trình bày cách thức kế thừa giữa các lớp trong C++, các nguyên tắc truy nhập trong kế thừa, định nghĩa nạp chồng các phương thức và tính đa hình trong lập trình hướng đối tương với C++. Chương 7: Một số lớp quan trọng Trình bày cách sử dụng một số lớp có sẵn trong thư viện chuẩn của C++, bao gồm các lớp: lớp tập hợp, lớp chuỗi, lớp ngăn xếp, lớp hàng đợi và lớp danh sách liên kết. Để đọc được cuốn sách này, sinh viên phải quen biết các khái niệm cơ bản về lập trình, có một số kỹ năng lập trình với ngôn ngữ C hoặc C++. Cuốn sách này cũng có thể dùng tài liệu tham khảo cho những sinh viên muốn tìm hiểu các kỹ thuật lập trình nâng cao và lập trình hướng đối tượng 3
  • 5. với C++. Cuốn sách này có kèm theo một đĩa chương trình chứa toàn bộ các chương trình được lấy làm minh hoạ và các bài tập trong cuốn sách. Mặc dù các tác giả đã có nhiều cố gắng trong việc biên soạn tài liệu này, song không thể tránh khỏi những thiếu sót. Rất mong nhận được những ý kiến đóng góp quý báu từ các sinh viên và các bạn đồng nghiệp. 4
  • 6. Chương 1: Giới thiệu về các phương pháp lập trình CHƯƠNG 1 GIỚI THIỆU VỀ CÁC PHƯƠNG PHÁP LẬP TRÌNH Nội dung của chương này tập trung trình bày các phương pháp lập trình: • Phương pháp lập trình tuyến tính • Phương pháp lập trình hướng cấu trúc • Phương pháp lập trình hướng đối tượng. 1.1 LẬP TRÌNH TUYẾN TÍNH Đặc trưng cơ bản của lập trình tuyến tính là tư duy theo lối tuần tự. Chương trình sẽ được thực hiện theo thứ tự từ đầu đến cuối, lệnh này kế tiếp lệnh kia cho đến khi kết thúc chương trình. Đặc trưng Lập trình tuyến tính có hai đặc trưng: • Đơn giản: chương trình được tiến hành đơn giản theo lối tuần tự, không phức tạp. • Đơn luồng: chỉ có một luồng công việc duy nhất, và các công việc được thực hiện tuần tự trong luồng đó. Tính chất • Ưu điểm: Do tính đơn giản, lập trình tuyến tính được ứng dụng cho các chương trình đơn giản và có ưu điểm dễ hiểu. • Nhược điểm: Với các ứng dụng phức tạp, người ta không thể dùng lập trình tuyến tính để giải quyết. Ngày nay, lập trình tuyến tính chỉ tồn tại trong phạm vi các modul nhỏ nhất của các phương pháp lập trình khác. Ví dụ trong một chương trình con của lập trình cấu trúc, các lệnh cũng được thực hiện theo tuần tự từ đầu đến cuối chương trình con. 1.2 LẬP TRÌNH HƯỚNG CẤU TRÚC 1.2.1 Đặc trưng của lập trình hướng cấu trúc Trong lập trình hướng cấu trúc, chương trình chính được chia nhỏ thành các chương trình con và mỗi chương trình con thực hiện một công việc xác định. Chương trình chính sẽ gọi đến chương trình con theo một giải thuật, hoặc một cấu trúc được xác định trong chương trình chính. Các ngôn ngữ lập trình cấu trúc phổ biến là Pascal, C và C++. Riêng C++ ngoài việc có đặc trưng của lập trình cấu trúc do kế thừa từ C, còn có đặc trưng của lập trình hướng đối tượng. Cho nên C++ còn được gọi là ngôn ngữ lập trình nửa cấu trúc, nửa hướng đối tượng. Đặc trưng Đặc trưng cơ bản nhất của lập trình cấu trúc thể hiện ở mối quan hệ: 5
  • 7. Chương 1: Giới thiệu về các phương pháp lập trình Chương trình = Cấu trúc dữ liệu + Giải thuật Trong đó: • Cấu trúc dữ liệu là cách tổ chức dữ liệu cho việc xử lý bởi một hay nhiều chương trình nào đó. • Giải thuật là một quy trình để thực hiện một công việc xác định Trong chương trình, giải thuật có quan hệ phụ thuộc vào cấu trúc dữ liệu: • Một cấu trúc dữ liệu chỉ phù hợp với một số hạn chế các giải thuật. • Nếu thay đổi cấu trúc dữ liệu thì phải thay đổi giải thuật cho phù hợp. • Một giải thuật thường phải đi kèm với một cấu trúc dữ liệu nhất định. Tính chất • Mỗi chương trình con có thể được gọi thực hiện nhiều lần trong một chương trình chính. • Các chương trình con có thể được gọi đến để thực hiện theo một thứ tự bất kì, tuỳ thuộc vào giải thuật trong chương trình chính mà không phụ thuộc vào thứ tự khai báo của các chương trình con. • Các ngôn ngữ lập trình cấu trúc cung cấp một số cấu trúc lệnh điều khiển chương trình. Ưu điểm • Chương trình sáng sủa, dễ hiểu, dễ theo dõi. • Tư duy giải thuật rõ ràng. Nhược điểm • Lập trình cấu trúc không hỗ trợ mạnh việc sử dụng lại mã nguồn: Giải thuật luôn phụ thuộc chặt chẽ vào cấu trúc dữ liệu, do đó, khi thay đổi cấu trúc dữ liệu, phải thay đổi giải thuật, nghĩa là phải viết lại chương trình. • Không phù hợp với các phần mềm lớn: tư duy cấu trúc với các giải thuật chỉ phù hợp với các bài toán nhỏ, nằm trong phạm vi một modul của chương trình. Với dự án phần mềm lớn, lập trình cấu trúc tỏ ra không hiệu quả trong việc giải quyết mối quan hệ vĩ mô giữa các modul của phần mềm. Vấn đề Vấn đề cơ bản của lập trình cấu trúc là bằng cách nào để phân chia chương trình chính thành các chương trình con cho phù hợp với yêu cầu, chức năng và mục đích của mỗi bài toán. Thông thường, để phân rã bài toán trong lập trình cấu trúc, người ta sử dụng phương pháp thiết kế trên xuống (top-down). 1.2.2 Phương pháp thiết kế trên xuống (top-down) Phương pháp thiết kế top-down tiếp cận bài toán theo hướng từ trên xuống dưới, từ tổng qúat đến chi tiết. Theo đó, một bài toán được chia thành các bài toán con nhỏ hơn. Mỗi bài toán con lại được chia nhỏ tiếp, nếu có thể, thành các bài toán con nhỏ hơn nữa. Quá trình này còn được gọi là quá trình làm mịn dần. Quá trình này sẽ dừng lại khi các bài toán con không cần chia nhỏ thêm 6
  • 8. Chương 1: Giới thiệu về các phương pháp lập trình nữa. Nghĩa là khi mỗi bài toán con đều có thể giải quyết bằng một chương trình con với một giải thuật đơn giản. Ví dụ, sử dụng phương pháp top-down để giải quyết bài toán xây một căn nhà mới. Chúng ta có thể phân rã bài toán theo các bước như sau: • Ở mức thứ nhất, chia bài toán xây nhà thành các bài toán nhỏ hơn như làm móng, đổ cột, đổ trần, xây tường, lợp mái. • Ở mức thứ hai, phân rã các công việc ở mức thứ nhất như việc làm móng nhà có thể phân rã tiếp thành các công việc đào móng, gia cố nền, làm khung sắt, đổ bê tong; công việc đổ cột được phần rã thành … • Ở mức thứ ba, phân rã các công việc của mức thứ hai như việc đào móng có thể phân chia tiếp thành các công việc như đo đạc, cắm mốc, chăng dây, đào và kiểm tra móng. Việc gia cố nền được phân rã thành … Quá trình phân rã có thể dừng ở mức này, bởi vì các công việc con thu được như đo đạc, cắm mốc, chăng dây, đào… có thể thực hiện được ngay, không cần chia nhỏ thêm nữa. Lưu ý: • Cùng sử dụng phương pháp top-down với cùng một bài toán, nhưng có thể cho ra nhiều kết quả khác nhau. Nguyên nhân là do sự khác nhau trong tiêu chí để phân rã một bài toán thành các bài toán con. Ví dụ, vẫn áp dụng phương pháp top-down để giải quyết bài toán xây nhà, nhưng nếu sử dụng một cách khác để phân chia bài toán, ta có thể thu được kết quả khác biệt so với phương pháp ban đầu: • Ở mức thứ nhất, chia bài toán xây nhà thành các bài toán nhỏ hơn như làm phần gỗ, làm phần sắt, làm phần bê tông và làm phần gạch. • Ở mức thứ hai, phân rã các công việc ở mức thứ nhất là làm phần gỗ có thể chia thành các công việc như xẻ gỗ, gia công gỗ, tạo khung, lắp vào nhà. Việc làm sắt có thể chia nhỏ thành… Rõ ràng, với cách làm mịn thế này, ta sẽ thu được một kết quả khác hẳn với cách thức đã thực hiện ở phần trên. 1.3 LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG 1.3.1 Lập trình hướng đối tượng Trong lập trình hướng đối tượng: • Người ta coi các thực thể trong chương trình là các đối tượng và sau đó trừu tượng hoá đối tượng thành lớp đối tượng. • Dữ liệu được tổ chức thành các thuộc tính của lớp. Nguời ta ngăn chặn việc thay đổi tuỳ tiện dữ liệu trong chương trình bằng các cách giới hạn truy nhập như chỉ cho phép truy nhập dữ liệu thông qua đối tượng, thông qua các phương thức mà đối tượng được cung cấp… • Quan hệ giữa các đối tượng là quan hệ ngang hàng hoặc quan hệ kế thừa: Nếu lớp B kế thừa từ lớp A thì A được gọi là lớp cơ sở và B được gọi là lớp dẫn xuất. 7
  • 9. Chương 1: Giới thiệu về các phương pháp lập trình Ngôn ngữ lập trình hướng đối tượng phổ biến hiện nay là Java, C++, C#...Mặc dù C++ cũng có những đặc trưng cơ bản của lập trình hướng đối tượng nhưng vẫn không phải là ngôn ngữ lập trình thuần hướng đối tượng. Đặc trưng Lập trình hướng đối tượng có hai đặc trưng cơ bản: • Đóng gói dữ liệu: dữ liệu luôn được tổ chức thành các thuộc tính của lớp đối tượng. Việc truy nhập đến dữ liệu phải thông qua các phương thức của đối tượng lớp. • Sử dụng lại mã nguồn: việc sử dụng lại mã nguồn được thể hiện thông qua cơ chế kế thừa. Cơ chế này cho phép các lớp đối tượng có thể kế thừa từ các lớp đối tượng khác. Khi đó, trong các lớp dẫn xuất, có thể sử dụng các phương thức (mã nguồn) của các lớp cơ sở mà không cần phải định nghĩa lại. Ưu điểm Lập trình hướng đối tượng có một số ưu điểm nổi bật: • Không còn nguy cơ dữ liệu bị thay đổi tự do trong chương trình. Vì dữ liệu đã được đóng gói vào các đối tượng. Nếu muốn truy nhập vào dữ liệu phải thông qua các phương thức được cho phép của đối tượng. • Khi thay đổi cấu trúc dữ liệu của một đối tượng, không cần thay đổi mã nguồn của các đối tượng khác, mà chỉ cần thay đổi một số thành phần của đối tượng dẫn xuất. Điều này hạn chế sự ảnh hưởng xấu của việc thay đổi dữ liệu đến các đối tượng khác trong chương trình. • Có thể sử dụng lại mã nguồn, tiết kiệm tài nguyên, chi phí thời gian. Vì nguyên tắc kế thừa cho phép các lớp dẫn xuất sử dụng các phương thức từ lớp cơ sở như những phương thức của chính nó, mà không cần thiết phải định nghĩa lại. • Phù hợp với các dự án phần mềm lớn, phức tạp. 1.3.2 Một số khái niệm cơ bản Trong mục này, chúng ta sẽ làm quen với một số khái niệm cơ bản trong lập trình hướng đối tượng. Bao gồm: • Khái niệm đối tượng (object) • Khái niệm đóng gói dữ liệu (encapsulation) • Khái niệm kế thừa (inheritance) • Khái niệm đa hình (polymorphism) Đối tượng (Object) Trong lập trình hướng đối tượng, đối tượng được coi là đơn vị cơ bản nhỏ nhất. Các dữ diệu và cách xử lí chỉ là thành phần của đối tượng mà không được coi là thực thể. Một đối tượng chứa các dữ liệu của riêng nó, đồng thời có các phương thức (hành động) thao tác trên các dữ liệu đó: Đối tượng = dữ liệu + phương thức 8
  • 10. Chương 1: Giới thiệu về các phương pháp lập trình Lớp (Class) Khi có nhiều đối tượng giống nhau về mặt dữ liệu và phương thức, chúng được nhóm lại với nhau và gọi chung là lớp: • Lớp là sự trừu tượng hoá của đối tượng • Đối tượng là một thể hiện của lớp. Đóng gói dữ liệu (Encapsulation) • Các dữ liệu được đóng gói vào trong đối tượng. Mỗi dữ liệu có một phạm vi truy nhập riêng. • Không thể truy nhập đến dữ liệu một cách tự do như lập trình cấu trúc • Muốn truy nhập đến các dữ liệu đã được bảo vệ, phải thông qua các đối tượng, nghĩa là phải sử dụng các phương thức mà đối tượng cung cấp mới có thể truy nhập đến dữ liệu của đối tượng đó. Tuy nhiên, vì C++ chỉ là ngôn ngữ lập trình nửa đối tượng, cho nên C++ vẫn cho phép định nghĩa các biến dữ liệu và các hàm tự do, đây là kết quả kế thừa từ ngôn ngữ C, một ngôn ngữ lập trình thuần cấu trúc. Kế thừa (Inheritance) Tính kế thừa của lập trình hướng đối tượng cho phép một lớp có thể kế thừa từ một số lớp đã tồn tại. Khi đó, lớp mới có thể sử dụng dữ liệu và phương thức của các lớp cơ sở như là của mình. Ngoài ra, lớp dẫn xuất còn có thể bổ sung thêm một số dữ liệu và phương thức. Ưu điểm của kế thừa là khi thay đổi dữ liệu của một lớp, chỉ cần thay đổi các phương thức trong phạm vi lớp cơ sở mà không cần thay đổi trong các lớp dẫn xuất. Đa hình (Polymorphsim) Đa hình là khái niệm luôn đi kèm với kế thừa. Do tính kế thừa, một lớp có thể sử dụng lại các phương thức của lớp khác. Tuy nhiên, nếu cần thiết, lớp dẫn xuất cũng có thể định nghĩa lại một số phương thức của lớp cơ sở. Đó là sự nạp chồng phương thức trong kế thừa. Nhờ sự nạp chồng phương thức này, ta chỉ cần gọi tên phương thức bị nạp chồng từ đối tượng mà không cần quan tâm đó là đối tượng của lớp nào. Chương trình sẽ tự động kiểm tra xem đối tượng là thuộc kiểu lớp cơ sở hay thuộc lớp dẫn xuất, sau đó sẽ gọi phương thức tương ứng với lớp đó. Đó là tính đa hình. 1.3.3 Lập trình hướng đối tượng trong C++ Vì C++ là một ngôn ngữ lập trình được mở rộng từ một ngôn ngữ lập trình cấu trúc C nên C++ được xem là ngôn ngữ lập trình nửa hướng đối tượng, nửa hướng cấu trúc. Những đặc trưng hướng đối tượng của C++ • Cho phép định nghĩa lớp đối tượng. • Cho phép đóng gói dữ liệu vào các lớp đối tượng. Cho phép định nghĩa phạm vi truy nhập dữ liệu của lớp bằng các từ khoá phạm vi: public, protected, private. 9
  • 11. Chương 1: Giới thiệu về các phương pháp lập trình • Cho phép kế thừa lớp với các kiểu kế thừa khác nhau tuỳ vào từ khoá dẫn xuất. • Cho phép lớp dẫn xuất sử dụng các phương thức của lớp cơ sở (trong phạm vi quy định). • Cho phép định nghĩa chồng phương thức trong lớp dẫn xuất. Những hạn chế hướng đối tượng của C++ Những hạn chế này là do C++ được phát triển từ một ngôn ngữ lập trình thuần cấu trúc C. • Cho phép định nghĩa và sử dụng các biến dữ liệu tự do. • Cho phép định nghĩa và sử dụng các hàm tự do. • Ngay cả khi dữ liệu được đóng gói vào lớp, dữ liệu vẫn có thể truy nhập trực tiếp như dữ liệu tự do bởi các hàm bạn, lớp bạn (friend) trong C++. TỔNG KẾT CHƯƠNG 1 Chương 1 đã trình bày tổng quan về các phương pháp lập trình hiện nay. Nội dung tập trung vào ba phương pháp lập trình có liên quan trực tiếp đến ngôn ngữ lập trình C++: • Lập trình tuyến tính • Lập trình hướng cấu trúc • Lập trình hướng đối tượng. C++ là ngôn ngữ lập trình được mở rộng từ ngôn ngữ lập trình cấu trúc C. Do đó, C++ vừa có những đặc trưng của lập trình cấu trúc, vừa có những đặc trưng của lập trình hướng đối tượng. 10
  • 12. Chương 2: Con trỏ và mảng CHƯƠNG 2 CON TRỎ VÀ MẢNG Nội dung của chương này tập trung trình bày các vấn đề cơ bản liên quan đến các thao tác trên kiểu dữ liệu con trỏ và mảng trong C++: • Khái niệm con trỏ, cách khai báo và sử dụng con trỏ. • Mối quan hệ giữa con trỏ và mảng • Con trỏ hàm • Cấp phát bộ nhớ cho con trỏ 2.1 KHÁI NIỆM CON TRỎ 2.1.1 Khai báo con trỏ Con trỏ là một biến đặc biệt chứa địa chỉ của một biến khác. Con trỏ có cùng kiểu dữ liệu với kiểu dữ liệu của biến mà nó trỏ tới. Cú pháp khai báo một con trỏ như sau: <Kiểu dữ liệu> *<Tên con trỏ>; Trong đó: • Kiểu dữ liệu: Có thể là các kiểu dữ liệu cơ bản của C++, hoặc là kiểu dữ liệu có cấu trúc, hoặc là kiểu đối tượng do người dùng tự định nghĩa. • Tên con trỏ: Tuân theo qui tắc đặt tên biến của C++: - Chỉ được bắt đầu bằng một kí tự (chữ), hoặc dấu gạch dưới “_”. - Bắt đầu từ kí tự thứ hai, có thể có kiểu kí tự số. - Không có dấu trống (space bar) trong tên biến. - Có phân biệt chữ hoa và chữ thường. - Không giới hạn độ dài tên biến. Ví dụ, để khai báo một biến con trỏ có kiểu là int và tên là pointerInt, ta viết như sau: int *pointerInt; Lưu ý • Có thể viết dấu con trỏ “*” ngay sau kiểu dữ liệu, nghĩa là hai cách khai báo sau là tương đương: int *pointerInt; int* pointerInt; • Các cách khai báo con trỏ như sau là sai cú pháp: *int pointerInt; // Khai báo sai con trỏ int pointerInt*; // Khai báo sai con trỏ 2.1.2 Sử dụng con trỏ Con trỏ được sử dụng theo hai cách: 11
  • 13. Chương 2: Con trỏ và mảng • Dùng con trỏ để lưu địa chỉ của biến để thao tác • Lấy giá trị của biến do con trỏ trỏ đến để thao tác Dùng con trỏ để lưu địa chỉ của biến Bản thân con trỏ sẽ được trỏ vào địa chỉ của một biến có cùng kiểu dữ liệu với nó. Cú pháp của phép gán như sau: <Tên con trỏ> = &<tên biến>; Lưu ý • Trong phép toán này, tên con trỏ không có dấu “*”. Ví dụ: int x, *px; px = &x; sẽ cho con trỏ px có kiểu int trỏ vào địa chỉ của biến x có kiểu nguyên. Phép toán &<Tên biến> sẽ cho địa chỉ của biến tương ứng. Lấy giá trị của biến do con trỏ trỏ đến Phép lấy giá trị của biến do con trỏ trỏ đến được thực hiện bằng cách gọi tên: *<Tên con trỏ>; Lưu ý • Trong phép toán này, phải có dấu con trỏ “*”. Nếu không có dấu con trỏ, sẽ trở thành phép lấy địa chỉ của biến do con trỏ trỏ tới. Ví dụ: int x = 12, y, *px; px = &y; *px = x; Quá trình diễn ra như sau: int x = 12, y, *px; x = 12 y=0 px null px = &y; x = 12 y=0 px px = x; x = 12 y=x =12 px con trỏ px vẫn trỏ tới địa chỉ biến y và giá trị của biến y sẽ là 12. Phép gán giữa các con trỏ Các con trỏ cùng kiểu có thể gán cho nhau thông qua phép gán và lấy địa chỉ con trỏ: <Tên con trỏ 1> = <Tên con trỏ 2>; Lưu ý • Trong phép gán giữa các con trỏ, bắt buộc phải dùng phép lấy địa chỉ của biến do con trỏ trỏ tới (không có dấu “*” trong tên con trỏ) mà không được dùng phép lấy giá trị của biến do con trỏ trỏ tới. 12
  • 14. Chương 2: Con trỏ và mảng • Hai con trỏ phải cùng kiểu. Trong trường hợp hai con trỏ khác kiểu, phải sử dụng các phương thức ép kiểu tương tự như trong phép gán các biến thông thường có kiểu khác nhau. Ví dụ: int x = 12, *px, *py; px = &x; py = px; int x = 12, *px, *py; x = 12 px null py null px = &x; x = 12 px py null py = px; x = 12 px py con trỏ py cũng trỏ vào địa chỉ của biến x như con trỏ px. Khi đó *py cũng có giá trị 12 giống như *px và là giá trị của biến x. Chương trình 2.1 minh hoạ việc dùng con trỏ giữa các biến của một chương trình C++. Chương trình 2.1 #include <stdio.h> #include <conio.h> void main(void){ int x = 12, *px, *py; cout << ”x = ” << x << endl; px = &x; // Con trỏ px trỏ tới địa chỉ của x cout << ”px = &x, *px = ” << *px << endl; *px = *px + 20; // Nội dung của px là 32 cout << ”*px = *px+20, x = ” << x << endl; py = px; // Cho py trỏ tới chỗ mà px trỏ: địa chỉ của x *py += 15; // Nội dung của py là 47 cout << ”py = px, *py +=15, x = ” << x << endl; } Trong chương trình 2.1, ban đầu biến x có giá trị 12. Sau đó, con trỏ px trỏ vào địa chỉ của biến x nên con trỏ px cũng có giá trị 12. Tiếp theo, ta tăng giá trị của con trỏ px thêm 20, giá trị của con trỏ px là 32. Vì px đang trỏ đến địa chỉ của x nên x cũng có giá trị là 32. Sau đó, ta cho con trỏ py trỏ đến vị trí mà px đang trỏ tới (địa chỉ của biến x) nên py cũng có giá trị 32. Cuối cùng, ta tăng giá trị của con trỏ py thêm 15, py sẽ có giá trị 37. Vì py cũng đang trỏ đến địa chỉ của x nên x cũng có giá trị 37. Do đó, ví dụ 2.1 sẽ in ra kết quả như sau: 13
  • 15. Chương 2: Con trỏ và mảng x = 12 px = &x, *px = 12 *px = *px + 20, x = 32 py = px, *py += 15, x = 37 2.2 CON TRỎ VÀ MẢNG 2.2.1 Con trỏ và mảng một chiều Mảng một chiều Trong C++, tên một mảng được coi là một kiểu con trỏ hằng, được định vị tại một vùng nhớ xác định và địa chỉ của tên mảng trùng với địa chỉ của phần tử đầu tiên của mảng. Ví dụ khai báo: int A[5]; thì địa chỉ của mảng A (cũng viết là A) sẽ trùng với địa chỉ phần tử đầu tiên của mảng A (là &A[0]) nghĩa là: A = &A[0]; Quan hệ giữa con trỏ và mảng Vì tên của mảng được coi như một con trỏ hằng, nên nó có thể được gán cho một con trỏ có cùng kiểu. Ví dụ khai báo: int A[5] = {5, 10, 15, 20, 25}; int *pa = A; int A[5] = {5, 10, 15, 20, 25}; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 int *pa = A; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 pa thì con trỏ pa sẽ trỏ đến mảng A, tức là trỏ đến địa chỉ của phần tử A[0], cho nên hai khai báo sau là tương đương: pa = A; pa = &A[0]; Với khai báo này, thì địa chỉ trỏ tới của con trỏ pa là địa chỉ của phần tử A[0] và giá trị của con trỏ pa là giá trị của phần tử A[0], tức là *pa = 5; Phép toán trên con trỏ và mảng Khi một con trỏ trỏ đến mảng, thì các phép toán tăng hay giảm trên con trỏ sẽ tương ứng với phép dịch chuyển trên mảng. Ví dụ khai báo: int A[5] = {5, 10, 15, 20, 25}; 14
  • 16. Chương 2: Con trỏ và mảng int *pa = &A[2]; thì con trỏ pa sẽ trỏ đến địa chỉ của phần tử A[2] và giá trị của pa là: *pa = A[2] = 15. Khi đó, phép toán: pa = pa + 1; sẽ đưa con trỏ pa trỏ đến địa chỉ của phần tử tiếp theo của mảng A, đó là địa chỉ của A[3]. Sau đó, phép toán: pa = pa – 2; sẽ đưa con trỏ pa trỏ đến địa chỉ của phần tử A[1]. int A[5] = {5, 10, 15, 20, 25}; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 int *pa = &A[2]; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 pa pa = pa + 1; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 pa pa = pa - 2; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 pa Lưu ý: • Hai phép toán pa++ và *pa++ có tác dụng hoàn toàn khác nhau trên mảng, pa++ là thao tác trên con trỏ, tức là trên bộ nhớ, nó sẽ đưa con trỏ pa trỏ đến địa chỉ của phần tử tiếp theo của mảng. *pa++ là phép toán trên giá trị, nó tăng giá trị hiện tại của phần tử mảng lên một đơn vị. Ví dụ: int A[5] = {5, 10, 15, 20, 25}; int *pa = &A[2]; thì pa++ là tương đương với pa = &A[3] và *pa = 20. nhưng *pa++ lại tương đương với pa = &A[2] và *pa = 15+1 = 16, A[2] = 16. 15
  • 17. Chương 2: Con trỏ và mảng int A[5] = {5, 10, 15, 20, 25}; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 int *pa = &A[2]; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 pa pa ++; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 pa Nhưng *pa ++; A A[0]=5 A[1]=10 A[2]=16 A[3]=20 A[4]= 25 pa • Trong trường hợp: int A[5] = {5, 10, 15, 20, 25}; int *pa = &A[4]; thì phép toán pa++ sẽ đưa con trỏ pa trỏ đến một địa chỉ không xác định. Lí do là A[4] là phần tử cuối của mảng A, nên pa++ sẽ trỏ đến địa chỉ ngay sau địa chỉ của A[4], địa chỉ này nằm ngoài vùng chỉ số của mảng A nên không xác định. Tương tự với trường hợp pa=&A[0], phép toán pa-- cũng đưa pa trỏ đến một địa chỉ không xác định. int A[5] = {5, 10, 15, 20, 25}; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 int *pa = &A[2]; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 pa pa ++; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 null pa pa = &A[0]; A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 pa pa --; null A A[0]=5 A[1]=10 A[2]=15 A[3]=20 A[4]= 25 pa • Vì mảng A là con trỏ hằng, cho nên không thể thực hiện các phép toán trên A mà chỉ có thể thực hiện trên các con trỏ trỏ đến A: các phép toán pa++ hoặc pa--là hợp lệ, nhưng các phép toán A++ hoặc A--là không hợp lệ. Chương trình 2.2a minh hoạ việc cài đặt một thủ tục sắp xếp các phần tử của một mảng theo cách thông thường. 16
  • 18. Chương 2: Con trỏ và mảng Chương trình 2.2a void SortArray(int A[], int n){ int temp; for(int i=0; i<n-1; i++) for(int j=i+1; j<n; j++) if(A[i] > A[j]){ temp = A[i]; A[i] = A[j]; A[j] = temp; } } Chương trình 2.2b cài đặt một thủ tục tương tự bằng con trỏ. Hai thủ tục này có chức năng hoàn toàn giống nhau. Chương trình 2.2b void SortArray(int *A, int n){ int temp; for(int i=0; i<n-1; i++) for(int j=i+1; j<n; j++) if(*(A+i) > *(A+j)){ temp = *(A+i); *(A+i) = *(A+j); *(A+j) = temp; } } Trong chương trình 2.2b, thay vì dùng một mảng, ta dùng một con trỏ để trỏ đến mảng cần sắp xếp. Khi đó, ta có thể dùng các thao tác trên con trỏ thay vì các thao tác trên các phần tử mảng. 2.2.2 Con trỏ và mảng nhiều chiều Con trỏ và mảng nhiều chiều Một câu hỏi đặt ra là nếu một ma trận một chiều thì tương đương với một con trỏ, vậy một mảng nhiều chiều thì tương đương với con trỏ như thế nào? Xét ví dụ: int A[3][3] = { {5, 10, 15}, {20, 25, 30}, {35, 40, 45} }; 17
  • 19. Chương 2: Con trỏ và mảng Khi đó, địa chỉ của ma trận A chính là địa chỉ của hàng đầu tiên của ma trận A, và cũng là địa chỉ của phần tử đầu tiên của hàng đầu tiên của ma trận A: • Địa chỉ của ma trận A: A = A[0] = *(A+0) = &A[0][0]; • Địa chỉ của hàng thứ nhất: A[1] = *(A+1) = &A[1][0]; • Địa chỉ của hàng thứ i: A[i] = *(A+i) = &A[i][0]; • Địa chỉ phần tử &A[i][j] = (*(A+i)) + j; • Giá trị phần tử A[i][j] = *((*(A+i)) + j); Như vậy, một mảng hai chiều có thể thay thế bằng một mảng một chiều các con trỏ cùng kiểu: int A[3][3]; có thể thay thế bằng: int (*A)[3]; Con trỏ trỏ tới con trỏ Vì một mảng hai chiều int A[3][3] có thể thay thế bằng một mảng các con trỏ int (*A)[3]. Hơn nữa, một mảng int A[3] lại có thể thay thế bằng một con trỏ int *A. Do vậy, một mảng hai chiều có thể thay thế bằng một mảng các con trỏ, hoặc một con trỏ trỏ đến con trỏ. Nghĩa là các cách viết sau là tương đương: int A[3][3]; int (*A)[3]; int **A; 2.3 CON TRỎ HÀM Mặc dù hàm không phải là một biến cụ thể nên không có một địa chỉ xác định. Nhưng trong khi chạy, mỗi một hàm trong C++ cũng có một vùng nhớ xác định, do vậy, C++ cho phép dùng con trỏ để trỏ đến hàm. Con trỏ hàm được dùng để truyền tham số có dạng hàm. Khai báo con trỏ hàm Con trỏ hàm được khai báo tương tự như khai báo nguyên mẫu hàm thông thường trong C++, ngoại trừ việc có thêm kí hiệu con trỏ “*” trước tên hàm. Cú pháp khai báo con trỏ hàm như sau: <Kiểu dữ liệu trả về> (*<Tên hàm>)([<Các tham số>]); Trong đó: • Kiểu dữ liệu trả về: là các kiểu dữ liệu thông thường của C++ hoặc kiểu do người dùng tự định nghĩa. • Tên hàm: tên do người dùng tự định nghĩa, tuân thủ theo quy tắc đặt tên biến trong C++. • Các tham số: có thể có hoặc không (phần trong dấu “[]” là tuỳ chọn). Nếu có nhiều tham số, mỗi tham số được phân cách nhau bởi dấu phẩy. Ví dụ khai báo: int (*Calcul)(int a, int b); là khai báo một con trỏ hàm, tên là Calcul, có kiểu int và có hai tham số cũng là kiểu int. Lưu ý: 18
  • 20. Chương 2: Con trỏ và mảng • Dấu “()” bao bọc tên hàm là cần thiết để chỉ ra rằng ta đang khai báo một con trỏ hàm. Nếu không có dấu ngoặc đơn này, trình biên dịch sẽ hiểu rằng ta đang khai báo một hàm thông thường và có giá trị trả về là một con trỏ. Ví dụ, hai khai báo sau là khác nhau hoàn toàn: // Khai báo một con trỏ hàm int (*Calcul)(int a, int b); // Khai báo một hàm trả về kiểu con trỏ int *Calcul(int a, int b); Sử dụng con trỏ hàm Con trỏ hàm được dùng khi cần gọi một hàm như là tham số của một hàm khác. Khi đó, một hàm được gọi phải có khuôn mẫu giống với con trỏ hàm đã được khai báo. Ví dụ, với khai báo: int (*Calcul)(int a, int b); thì có thể gọi các hàm có hai tham số kiểu int và trả về cũng kiểu int như sau: int add(int a, int b); int sub(int a, int b); nhưng không được gọi các hàm khác kiểu tham số hoặc kiểu trả về như sau: int add(float a, int b); int add(int a); char* sub(char* a, char* b); Chương trình 2.3 minh hoạ việc khai báo và sử dụng con trỏ hàm. Chương trình 2.3 #include <ctype.h> #include <string> // Hàm có sử dụng con trỏ hàm như tham số void Display(char[] str, int (*Xtype)(int c)){ int index = 0; while(str[index] != ‘0’){ cout << (*Xtype)(str[index]); // Sử dụng con trỏ hàm index ++; } return; } // Hàm main, dùng lời gọi hàm đến con trỏ hàm void main(){ char input[500]; cout << “Enter the string: ”; cin >> input; 19
  • 21. Chương 2: Con trỏ và mảng char reply; cout << “Display the string in uppercase or lowercase (u,l): ”; cin >> reply; if(reply == ‘l’) // Hiển thị theo dạng lowercase Display(str, tolower); else // Hiển thị theo dạng uppercase Display(str, toupper); return; } Chương trình 2.3 khai báo hàm Display() có sử dụng con trỏ hàm có khuôn mẫu int (*Xtype)(int c); Trong hàm main, con trỏ hàm này được gọi bởi hai thể hiện là các hàm tolower() và hàm toupper(). Hai hàm này được khai báo trong thư viện ctype.h với mẫu như sau: int tolower(int c); int toupper(int c); Hai khuôn mẫu này phù hợp với con trỏ hàm Xtype trong hàm Display() nên lời gọi hàm Display() trong hàm main là hợp lệ. 2.4 CẤP PHÁT BỘ NHỚ ĐỘNG Xét hai trường hợp sau đây: • Trường hợp 1, khai báo một con trỏ và gán giá trị cho nó: int *pa = 12; • Trường hợp 2, khai báo con trỏ đến phần tử cuối cùng của mảng rồi tăng thêm một đơn vị cho nó: int A[5] = {5, 10, 15, 20, 25}; int *pa = &A[4]; pa++; Trong cả hai trường hợp, ta đều không biết thực sự con trỏ pa đang trỏ đến địa chỉ nào trong bộ nhớ: trường hợp 1 chỉ ra rằng con trỏ pa đang trỏ tới một địa chỉ không xác định, nhưng lại chứa giá trị là 12 do được gán vào. Trường hợp 2, con trỏ pa đã trỏ đến địa chỉ ngay sau địa chỉ phần tử cuối cùng của mảng A, đó cũng là một địa chỉ không xác định. Các địa chỉ không xác định này là các địa chỉ nằm ở vùng nhớ tự do còn thừa của bộ nhớ. Vùng nhớ này có thể bị chiếm dụng bởi bất kì một chương trình nào đang chạy. Do đó, rất có thể các chương trình khác sẽ chiếm mất các địa chỉ mà con trỏ pa đang trỏ tới. Khi đó, nếu các chương trình thay đổi giá trị của địa chỉ đó, giá trị pa cũng bị thay đổi theo mà ta không thể kiểm soát được. Để tránh các rủi ro có thể gặp phải, C++ yêu cầu phải cấp phát bộ nhớ một cách tường minh cho con trỏ trước khi sử dụng chúng. 20
  • 22. Chương 2: Con trỏ và mảng 2.4.1 Cấp phát bộ nhớ động cho biến Cấp phát bộ nhớ động Thao tác cấp phát bộ nhớ cho con trỏ thực chất là gán cho con trỏ một địa chỉ xác định và đưa địa chỉ đó vào vùng đã bị chiếm dụng, các chương trình khác không thể sử dụng địa chỉ đó. Cú pháp cấp phát bộ nhớ cho con trỏ như sau: <tên con trỏ> = new <kiểu con trỏ>; Ví dụ, khai báo: int *pa; pa = new int; sẽ cấp phát bộ nhớ hợp lệ cho con trỏ pa. Lưu ý: • Ta có thể vừa cấp phát bộ nhớ, vừa khởi tạo giá trị cho con trỏ theo cú pháp sau: int *pa; pa = new int(12); sẽ cấp phát cho con trỏ pa một địa chỉ xác định, đồng thời gán giá trị của con trỏ *pa = 12. Giải phóng bộ nhớ động Địa chỉ của con trỏ sau khi được cấp phát bởi thao tác new sẽ trở thành vùng nhớ đã bị chiếm dụng, các chương trình khác không thể sử dụng vùng nhớ đó ngay cả khi ta không dùng con trỏ nữa. Để tiết kiệm bộ nhớ, ta phải huỷ bỏ vùng nhớ của con trỏ ngay sau khi không dùng đến con trỏ nữa. Cú pháp huỷ bỏ vùng nhớ của con trỏ như sau: delete <tên con trỏ>; Ví dụ: int *pa = new int(12); // Khai báo con trỏ pa, cấp phát bộ nhớ // và gán giá trị ban đầu cho pa là 12. delete pa; // Giải phóng vùng nhớ vừa cấp cho pa. Lưu ý: • Một con trỏ, sau khi bị giải phóng địa chỉ, vẫn có thể được cấp phát một vùng nhớ mới hoặc trỏ đến một địa chỉ mới: int *pa = new int(12); // Khai báo con trỏ pa, cấp phát bộ nhớ // và gán giá trị ban đầu cho pa là 12. delete pa; // Giải phóng vùng nhớ vừa cấp cho pa. int A[5] = {5, 10, 15, 20, 25}; pa = A; // Cho pa trỏ đến địa chỉ của mảng A • Nếu có nhiều con trỏ cùng trỏ vào một địa chỉ, thì chỉ cần giải phóng bộ nhớ của một con trỏ, tất cả các con trỏ còn lại cũng bị giải phóng bộ nhớ: int *pa = new int(12); // *pa = 12 int *pb = pa; // pb trỏ đến cùng địa chỉ pa. *pb += 5; // *pa = *pb = 17 delete pa; // Giải phóng cả pa lẫn pb 21
  • 23. Chương 2: Con trỏ và mảng • Một con trỏ sau khi cấp phát bộ nhớ động bằng thao tác new, cần phải phóng bộ nhớ trước khi trỏ đến một địa chỉ mới hoặc cấp phát bộ nhớ mới: int *pa = new int(12); // pa được cấp bộ nhớ và *pa = 12 *pa = new int(15); // pa trỏ đến địa chỉ khác và *pa = 15. // địa chỉ cũ của pa vẫn bị coi là bận 2.4.2 Cấp phát bộ nhớ cho mảng động một chiều Cấp phát bộ nhớ cho mảng động một chiều Mảng một chiều được coi là tương ứng với một con trỏ cùng kiểu. Tuy nhiên, cú pháp cấp phát bộ nhớ cho mảng động một chiều là khác với cú pháp cấp phát bộ nhớ cho con trỏ thông thường: <Tên con trỏ> = new <Kiểu con trỏ>[<Độ dài mảng>]; Trong đó: • Tên con trỏ: tên do người dùng đặt, tuân thủ theo quy tắc đặt tên biến của C++. • Kiểu con trỏ: Kiểu dữ liệu cơ bản của C++ hoặc là kiểu do người dùng tự định nghĩa. • Độ dài mảng: số lượng các phần tử cần cấp phát bộ nhớ của mảng. Ví dụ: int *A = new int[5]; sẽ khai báo một mảng A có 5 phần tử kiểu int được cấp phát bộ nhớ động. Lưu ý: • Khi cấp phát bộ nhớ cho con trỏ có khởi tạo thông thường, ta dùng dấu “()”, khi cấp phát bộ nhớ cho mảng, ta dùng dấu “[]”. Hai lệnh cấp phát sau là hoàn toàn khác nhau: // Cấp phát bộ nhớ và khởi tạo cho một con trỏ int int *A = new int(5); // Cấp phát bộ nhớ cho một mảng 5 phần tử kiểu int int *A = new int[5]; Giải phóng bộ nhớ của mảng động một chiều Để giải phóng vùng nhớ đã được cấp phát cho một mảng động, ta dùng cú pháp sau: delete [] <tên con trỏ>; Ví dụ: // Cấp phát bộ nhớ cho một mảng có 5 phần tử kiểu int int *A = new int[5]; // Giải phóng vùng nhớ do mảng A đang chiếm giữ. delete [] A; Chương trình 2.4 minh hoạ hai thủ tục khởi tạo và giải phóng một mảng động một chiều. Chương trình 2.4 void InitArray(int *A, int length){ A = new int[length]; for(int i=0; i<length; i++) 22
  • 24. Chương 2: Con trỏ và mảng A[i] = 0; return; } void DeleteArray(int *A){ delete [] A; return; } 2.4.3 Cấp phát bộ nhớ cho mảng động nhiều chiều Cấp phát bộ nhớ cho mảng động nhiều chiều Một mảng hai chiều là một con trỏ đến một con trỏ. Do vậy, ta phải cấp phát bộ nhớ theo từng chiều theo cú pháp cấp phát bộ nhớ cho mảng động một chiều. Ví dụ: int **A; const int length = 10; A = new int*[length]; // Cấp phát bộ nhớ cho số dòng của ma trận A for(int i=0; i<length; i++) // Cấp phát bộ nhớ cho các phần tử của mỗi dòng A[i] = new int[length]; sẽ cấp phát bộ nhớ cho một mảng động hai chiều, tương đương với một ma trận có kích thước 10*10. Lưu ý: • Trong lệnh cấp phát A = new int*[length], cần phải có dấu “*” để chỉ ra rằng cần cấp phát bộ nhớ cho một mảng các phần tử có kiểu là con trỏ int (int*), khác với kiểu int bình thường. Giải phóng bộ nhớ của mảng động nhiều chiều Ngược lại với khi cấp phát, ta phải giải phóng lần lượt bộ nhớ cho con trỏ tương ứng với cột và hàng của mảng động. Ví dụ: int **A; …; // cấp phát bộ nhớ … for(int i=0; i<length; i++) delete [] A[i]; // Giải phóng bộ nhớ cho mỗi dòng delete [] A; // Giải phóng bộ nhớ cho mảng các dòng sẽ giải phóng bộ nhớ cho một mảng động hai chiều. Chương trình 2.5 minh hoạ việc dùng mảng động hai chiều để tính tổng của hai ma trận. 23
  • 25. Chương 2: Con trỏ và mảng Chương trình 2.5 #include<stdio.h> #include<conio.h> /* Khai báo nguyên mẫu hàm */ void InitArray(int **A, int row, int colum); void AddArray(int **A, int **B, int row, int colum); void DisplayArray(int **A, int row, int colum); void DeleteArray(int **A, int row); void InitArray(int **A, int row, int colum){ A = new int*[row]; for(int i=0; i<row; i++){ A[i] = new int[colum]; for(int j=0; j<colum; j++){ cout << “Phan tu [” << i << “,” << j << “] = ”; cin >> A[i][j]; } return; } void AddArray(int **A, int **B, int row, int colum){ for(int i=0; i<row; i++) for(int j=0; j<colum; j++) A[i][j] += B[i][j]; return; } void DisplayArray(int **A, int row, int colum){ for(int i=0; i<row; i++){ for(int j=0; j<colum; j++) cout << A[i][j] << “ ”; cout << endl; // Xuống dòng return; } void DeleteArray(int **A, int row){ for(int i=0; i<row; i++) delete [] A[i]; delete [] A; return; } 24
  • 26. Chương 2: Con trỏ và mảng void main(){ clrscr(); int **A, **B, row, colum; cout << “So dong: ”; cin >> row; cout << “So cot: ”; cin >> colum; /* Khởi tạo các ma trận */ cout << “Khoi tao mang A:” << endl; InitArray(A, row, colum); cout << “Khoi tao mang B:” << endl; InitArray(B, row, colum); // Cộng hai ma trận AddArray(A, B, row, colum); // Hiển thị ma trận kết quả cout << “Tong hai mang A va mang B:” << endl; DisplayArray(A, row, colum); // Giải phóng bộ nhớ DeleteArray(A, row); DeleteArray(B, row); return; } TỔNG KẾT CHƯƠNG 2 Nội dung chương 2 đã trình bày các vấn đề liên quan đến việc khai báo và sử dụng con trỏ và mảng trong ngôn ngữ C++: • Con trỏ là một kiểu biến đặc biệt, nó trỏ đến địa chỉ của một biến khác. Có hai cách truy nhập đến con trỏ là truy nhập đến địa chỉ hoặc truy nhập đến giá trị của địa chỉ mà con trỏ trỏ đến. • Con trỏ có thể tham gia vào các phép toán như các biến thông thường bằng phép lấy giá trị. • Một con trỏ có sự tương ứng với một mảng một chiều có cùng kiểu. • Một ma trận hai chiều có thể thay thế bằng một mảng các con trỏ hoặc một con trỏ trỏ đến con trỏ. • Một con trỏ có thể trỏ đến một hàm, khi đó, nó được dùng để gọi một hàm như là một tham số cho hàm khác. 25
  • 27. Chương 2: Con trỏ và mảng • Một con trỏ cần phải trỏ vào một địa chỉ xác định hoặc phải được cấp phát bộ nhớ qua phép toán new và giải phóng bộ nhớ sau khi dùng bằng thao tác delete. CÂU HỎI VÀ BÀI TẬP CHƯƠNG 2 1. Trong các khai báo con trỏ sau, những khai báo nào là đúng: a. int A*; b. *int A; c. int* A, B; d. int* A, *B; e. int *A, *B; 2. Với khai báo: int a = 12; int *pa; Các phép gán nào sau đây là hợp lệ: a. pa = &a; b. pa = a; c. *pa = &a; d. *pa = a; 3. Với khai báo: int A[5] = {10, 20, 30, 40, 50}; int *pa = A+2; Khi đó, *pa = ? a. 10 b. 20 c. 30 d. 40 e. 50 4. Với đoạn chương trình: int A[5] = {10, 20, 30, 40, 50}; int *pa = A; *pa += 2; Khi đó, *pa = ? a. 10 b. 12 c. 30 d. 32 5. Với đoạn chương trình: int A[5] = {10, 20, 30, 40, 50}; int *pa = A; 26
  • 28. Chương 2: Con trỏ và mảng pa += 2; Khi đó, *pa = ? a. 10 b. 12 c. 30 d. 32 6. Với đoạn chương trình: int A[5] = {10, 20, 30, 40, 50}; int *pa = A; pa += 2; Khi đó, pa = ? a. &A[0] b. A[2] c. &A[2] d. Không xác định 7. Với đoạn chương trình: int A[5] = {10, 20, 30, 40, 50}; int *pa = A; pa -= 2; Khi đó, pa = ? a. &A[0] b. &A[2] c. &A[4] d. Không xác định 8. Với đoạn chương trình: int A[3][3] = { {10, 20, 30}, {40, 50, 60}, {70, 80, 90} }; int *pa; Khi đó, để có được kết quả *pa = 50, các lệnh nào sau đây là đúng? a. pa = A + 4; b. pa = (*(A+1)) + 1; c. pa = &A[1][1]; d. pa = *((*(A+1)) + 1); 9. Giả sử ta khai báo một hàm có sử dụng con trỏ hàm với khuôn mẫu như sau: int Calcul(int a, int b, int (*Xcalcul)(int x, int y)){} Và ta có cài đặt một số hàm như sau: 27
  • 29. Chương 2: Con trỏ và mảng int add(int a, int b); void cal(int a, int b); int squere(int a); Khi đó, lời gọi hàm nào sau đây là đúng: a. Calcul(5, 10, add); b. Calcul(5, 10, add(2, 3)); c. Calcul(5, 10, cal); d. Calcul(5, 10, squere); 10. Ta muốn cấp phát bộ nhớ cho một con trỏ kiểu int và khởi đầu giá trị cho nó là 20. Lệnh nào sau đây là đúng: a. int *pa = 20; b. int *pa = new int{20}; c. int *pa = new int(20); d. int *pa = new int[20]; 11. Ta muốn cấp phát bộ nhớ cho một mảng động kiểu int có chiều dài là 20. Lệnh nào sau đây là đúng: a. int *pa = 20; b. int *pa = new int{20}; c. int *pa = new int(20); d. int *pa = new int[20]; 12. Xét đoạn chương trình sau: int A[5] = {10, 20, 30, 40, 50}; int *pa = A; pa = new int(2); Khi đó, *pa = ? a. 10 b. 30 c. 2 d. Không xác định 13. Xét đoạn chương trình sau: 1> int A[5] = {10, 20, 30, 40, 50}; 2> int *pa = A; 3> pa += 15; 4> delete pa; Đoạn chương trình trên có lỗi ở dòng nào? a. 1 b. 2 c. 3 d. 4 28
  • 30. Chương 2: Con trỏ và mảng 14. Viết chương trình thực hiện các phép toán cộng, trừ, nhân, chia trên đa thức. Các đa thức được biểu diễn bằng mảng động một chiều. Bậc của đa thức và các hệ số tương ứng được nhập từ bàn phím. 15. Viết chương trình thực hiện các phép toán cộng, trừ, nhân hai ma trận kích thước m*n. Các ma trận được biểu diễn bằng mảng động hai chiều. Giá trị kích cỡ ma trận (m, n) và giá trị các phần tử của ma trận được nhập từ bàn phím. 29
  • 31. Chương 3: Kiểu dữ liệu cấu trúc CHƯƠNG 3 KIỂU DỮ LIỆU CẤU TRÚC Nội dung chương này tập trung trình bày các vấn đề liên quan đến kiểu dữ liệu có cấu trúc trong C++: • Định nghĩa một cấu trúc • Sử dụng một cấu trúc bằng các phép toán cơ bản trên cấu trúc • Con trỏ cấu trúc, khai báo và sử dụng con trỏ cấu trúc • Mảng các cấu trúc, khai báo và sử dụng mảng các cấu trúc • Một số kiểu dữ liệu trừu tượng khác như ngăn xếp, hàng đợi, danh sách liên kết. 3.1 ĐỊNH NGHĨA CẤU TRÚC Kiểu dữ liệu có cấu trúc được dùng khi ta cần nhóm một số biến dữ liệu luôn đi kèm với nhau. Khi đó, việc xử lí trên một nhóm các biến được thực hiện như trên các biến cơ bản thông thường. 3.1.1 Khai báo cấu trúc Trong C++, một cấu trúc do người dùng tự định nghĩa được khai báo thông qua từ khoá struct: struct <Tên cấu trúc>{ <Kiểu dữ liệu 1> <Tên thuộc tính 1>; <Kiểu dữ liệu 2> <Tên thuộc tính 2>; … <Kiểu dữ liệu n> <Tên thuộc tính n>; }; Trong đó: • struct: là tên từ khoá để khai báo một cấu trúc, bắt buộc phải có khi định nghĩa cấu trúc. • Tên cấu trúc: là tên do người dùng tự định nghĩa, tuân thủ theo quy tắc đặt tên biến trong C++. Tên này sẽ trở thành tên của kiểu dữ liệu có cấu trúc tương ứng. • Thuộc tính: mỗi thuộc tính của cấu trúc được khai báo như khai báo một biến thuộc kiểu dữ liệu thông thường, gồm có kiểu dữ liệu và tên biến tương ứng. Mỗi khai báo thuộc tính phải kết thúc bằng dấu chấm phẩy “;” như một câu lệnh C++ thông thường. Ví dụ, để quản lí nhân viên của một công ty, khi xử lí thông tin về mỗi nhân viên, ta luôn phải xử lí các thông tin liên quan như: • Tên • Tuổi • Chức vụ • Lương Do đó, ta sẽ dùng cấu trúc để lưu giữ thông tin về mỗi nhân viên bằng cách định nghĩa một cấu trúc có tên là Employeee với các thuộc tính như sau: 30
  • 32. Chương 3: Kiểu dữ liệu cấu trúc struct Employeee{ char name[20]; // Tên nhân viên int age; // Tuổi nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên }; Lưu ý: • Cấu trúc chỉ cần định nghĩa một lần trong chương trình và có thể được khai báo biến cấu trúc nhiều lần. Khi cấu trúc đã được định nghĩa, việc khai báo biến ở lần khác trong chương trình được thực hiện như khai báo biến thông thường: <Tên cấu trúc> <tên biến 1>, <tên biến 2>; Ví dụ, sau khi đã định nghĩa cấu trúc Employeee, muốn có biến myEmployeee, ta khai báo như sau: Employee myEmployeee; 3.1.2 Cấu trúc lồng nhau Các cấu trúc có thể được định nghĩa lồng nhau khi một thuộc tính của một cấu trúc cũng cần có kiểu là một cấu trúc khác. Khi đó, việc định nghĩa cấu trúc cha được thực hiện như một cấu trúc bình thường, với khai báo về thuộc tính đó là một cấu trúc con: struct <Tên cấu trúc cha>{ <Kiểu dữ liệu 1> <Tên thuộc tính 1>; // Có kiểu cấu trúc <Kiểu cấu trúc con> <Tên thuộc tính 2>; … <Kiểu dữ liệu n> <Tên thuộc tính n>; }; Ví dụ, với kiểu cấu trúc Employee, ta không quan tâm đến tuổi nhân viên nữa, mà quan tâm đến ngày sinh của nhân viên. Vì ngày sinh cần có các thông tin luôn đi với nhau là ngày sinh, tháng sinh, năm sinh. Do đó, ta định nghĩa một kiểu cấu trúc con cho kiểu ngày sinh: struct Date{ int day; int month; int year; }; khi đó, cấu trúc Employee trở thành: struct Employee{ char name[20]; // Tên nhân viên Date birthDay; // Ngày sinh của nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên }; 31
  • 33. Chương 3: Kiểu dữ liệu cấu trúc Lưu ý: • Trong định nghĩa các cấu trúc lồng nhau, cấu trúc con phải được định nghĩa trước cấu trúc cha để đảm bảo các kiểu dữ liệu của các thuộc tính của cấu trúc cha là tường minh tại thời điểm nó được định nghĩa. 3.1.3 Định nghĩa cấu trúc với từ khoá typedef Để tránh phải dùng từ khoá struct mỗi khi khai báo biến cấu trúc, ta có thể dùng từ khóa typedef khi định nghĩa cấu trúc: typedef struct { <Kiểu dữ liệu 1> <Tên thuộc tính 1>; <Kiểu dữ liệu 2> <Tên thuộc tính 2>; … <Kiểu dữ liệu n> <Tên thuộc tính n>; } <Tên kiểu dữ liệu cấu trúc>; Trong đó: • Tên kiểu dữ liệu cấu trúc: là tên kiểu dữ liệu của cấu trúc vừa định nghĩa. Tên này sẽ được dùng như một kiểu dữ liệu thông thường khi khai báo biến cấu trúc. Ví dụ, muốn có kiểu dữ liệu có cấu trúc nhân viên, có tên là Employee, ta dùng từ khoá typedef để định nghĩa cấu trúc như sau: typedef struct { char name[20]; // Tên nhân viên int age; // Tuổi nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên } Employee; Khi đó, muốn có hai biến là myEmployee1 và myEmployee2 có kiểu cấu trúc Employee, ta chỉ cần khai báo như sau mà không cần từ khoá struct: Employee myEmployee1, myEmployee2; Trong ví dụ khai báo lồng cấu trúc Employee, dùng từ khoá typedef cho kiểu Date: typedef struct { int day; int month; int year; } Date; cấu trúc Employee trở thành: typedef struct { char name[20]; // Tên nhân viên Date birthDay; // Ngày sinh của nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên } Employee; 32
  • 34. Chương 3: Kiểu dữ liệu cấu trúc Lưu ý: • Khi không dùng từ khoá typedef, tên cấu trúc (nằm sau từ khoá struct) được dùng để khai báo biến. Trong khi đó, khi có từ khoá typedef, tên kiểu dữ liệu cấu trúc (dòng cuối cùng trong định nghĩa) mới được dùng để khai báo biến. • Khi dùng từ khoá typedef thì không thể khai báo biến đồng thời với định nghĩa cấu trúc. 3.2 THAO TÁC TRÊN CẤU TRÚC Các thao tác trên cấu trúc bao gồm: • Khai báo và khởi tạo giá trị ban đầu cho biến cấu trúc • Truy nhập đến các thuộc tính của cấu trúc 3.2.1 Khởi tạo giá trị ban đầu cho cấu trúc Khởi tạo biến có cấu trúc đơn Biến cấu trúc được khai báo theo các cách sau: <Tên kiểu dữ liệu cấu trúc> <tên biến>; Ngoài ra, ta có thể khởi tạo các giá trị cho các thuộc tính của cấu trúc ngay khi khai báo bằng các cú pháp sau: <Tên kiểu dữ liệu cấu trúc> <tên biến> = { <giá trị thuộc tính 1>, <giá trị thuộc tính 2>, … <giá trị thuộc tính n> }; Trong đó: • Giá trị thuộc tính: là giá trị khởi đầu cho mỗi thuộc tính, có kiểu phù hợp với kiểu dữ liệu của thuộc tính. Mỗi giá trị của thuộc tính được phân cách bằng dấu phẩy “,”. Ví dụ, với định nghĩa cấu trúc: typedef struct { char name[20]; // Tên nhân viên int age; // Tuổi nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên } Employee; thì có thể khai báo và khởi tạo cho một biến như sau: Employee myEmployee1 = { “Nguyen Van A”, 27, “Nhan vien”, 300f 33
  • 35. Chương 3: Kiểu dữ liệu cấu trúc }; Khởi tạo các biến có cấu trúc lồng nhau Trong trường hợp các cấu trúc lồng nhau, phép khởi tạo cũng thực hiện như thông thường với phép khởi tạo cho tất cả các cấu trúc con. Ví dụ với khai báo cấu trúc như sau: typedef struct { int day; int month; int year; } Date; và: typedef struct { char name[20]; // Tên nhân viên Date birthDay; // Ngày sinh của nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên } Employee; Thì khai báo và khởi tạo một biến có kiểu Employee có thể thực hiện như sau: Employee myEmployee1 = { “Nguyen Van A”, {15, 05, 1980}, // Khởi tạo cấu trúc con “Nhan vien”, 300f }; 3.2.2 Truy nhập đến thuộc tính của cấu trúc Việc truy nhập đến thuộc tính của cấu trúc được thực hiện bằng cú pháp: <Tên biến cấu trúc>.<tên thuộc tính> Ví dụ, với một biến cấu trúc kiểu Employee đơn: Employee myEmployee1 = { “Nguyen Van A”, 27, “Nhan vien”, 300f }; ta có thể truy xuất như sau: cout << myEmployee1.name; // hiển thị ra “Nguyen Van A” myEmployee1.age += 1; // Tăng số tuổi lên 1 Đối với kiểu cấu trúc lồng nhau, phép truy nhập đến thuộc tính được thực hiện lần lượt từ cấu trúc cha đến cấu trúc con. Ví dụ, với một biến cấu trúc kiểu Employee lồng nhau: 34
  • 36. Chương 3: Kiểu dữ liệu cấu trúc Employee myEmployee1 = { “Nguyen Van A”, {15, 05, 1980}, “Nhan vien”, 300f }; ta có thể truy xuất như sau: cout << myEmployee1.name; // hiển thị ra “Nguyen Van A” myEmployee1.birthDay.day = 16; // Sửa lại ngày sinh thành 16 myEmployee1.birthDay.month = 07; // Sửa lại tháng sinh thành 07 Chương trình 3.1a minh hoạ việc tạo lập và sử dụng cấu trúc Employee đơn, không dùng từ khoá typedef. Chương trình 3.1a #include<stdio.h> #include<conio.h> #include<string.h> struct Employee{ char name[20]; // Tên nhân viên int age; // Tuổi nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên }; /* Khai báo khuôn mẫu hàm */ void Display(Employee myEmployee); void Display(Employee myEmployee){ cout << “Name: ” << myEmployee.name << endl; cout << “Age: ” << myEmployee.age << endl; cout << “Role: ” << myEmployee.role << endl; cout << “Salary: ” << myEmployee.salary << endl; return; } void main(){ clrscr(); // Hiển thị giá trị mặc định Employee myEmployee = 35
  • 37. Chương 3: Kiểu dữ liệu cấu trúc {“Nguyen Van A”, 27, “Nhan vien”, 300f}; cout << “Thông tin mặc định:” << endl; Display(myEmployee); // Thay đổi giá trị cho các thuộc tính cout << “Name: ”; cin >> myEmployee.name; cout << “Age: ”; cin >> myEmployee.age; cout << “Role: ”; cin >> myEmployee.role; cout << “Salary: ”; cin >> myEmployee.salary; cout << “Thông tin sau khi thay đổi:” << endl; Display(myEmployee); return; } Chương trình 3.1b minh hoạ việc tạo lập và sử dụng cấu trúc Employee lồng nhau, có dùng từ khoá typedef. Chương trình 3.1b #include<stdio.h> #include<conio.h> #include<string.h> typedef struct { int day; int month; int year; } Date; typedef struct { char name[20]; // Tên nhân viên Date birthDay; // Ngày sinh của nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên } Employee; /* Khai báo khuôn mẫu hàm */ void Display(Employee myEmployee); 36
  • 38. Chương 3: Kiểu dữ liệu cấu trúc void Display(Employee myEmployee){ cout << “Name: ” << myEmployee.name << endl; cout << “Birth day: ” << myEmployee.birthDay.day << “/” << myEmployee.birthDay.month << “/” << myEmployee.birthDay.year << endl; cout << “Role: ” << myEmployee.role << endl; cout << “Salary: ” << myEmployee.salary << endl; return; } void main(){ clrscr(); // Hiển thị giá trị mặc định Employee myEmployee = {“Nguyen Van A”, {15, 5, 1980}, “Nhan vien”, 300f}; cout << “Thông tin mặc định:” << endl; Display(myEmployee); // Thay đổi giá trị cho các thuộc tính cout << “Name: ”; cin >> myEmployee.name; cout << “Day of birth: ”; cin >> myEmployee.birthDay.day; cout << “Month of birth: ”; cin >> myEmployee.birthDay.month; cout << “Year of birth: ”; cin >> myEmployee.birthDay.year; cout << “Role: ”; cin >> myEmployee.role; cout << “Salary: ”; cin >> myEmployee.salary; cout << “Thông tin sau khi thay đổi:” << endl; Display(myEmployee); return; } 37
  • 39. Chương 3: Kiểu dữ liệu cấu trúc 3.3 CON TRỎ CẤU TRÚC VÀ MẢNG CẤU TRÚC 3.3.1 Con trỏ cấu trúc Con trỏ cấu trúc là một con trỏ trỏ đến địa chỉ của một biến có kiểu cấu trúc. Cách khai báo và sử dụng con trỏ cấu trúc được thực hiện như con trỏ thông thường. Khai báo con trỏ cấu trúc Con trỏ cấu trúc được khai báo theo cú pháp: <Tên kiểu cấu trúc> *<Tên biến>; Ví dụ, với kiểu khai báo cấu trúc: typedef struct { int day; int month; int year; } Date; và: typedef struct { char name[20]; // Tên nhân viên Date birthDay; // Ngày sinh của nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên } Employee; thì ta có thể khai báo một con trỏ cấu trúc như sau: Employee *ptrEmployee; Lưu ý: • Cũng như khai báo con trỏ thông thường, dấu con trỏ “*” có thể nằm ngay trước tên biến hoặc nằm ngay sau tên kiểu cấu trúc. Cũng giống con trỏ thông thường, con trỏ cấu trúc được sử dụng khi: • Cho nó trỏ đến địa chỉ của một biến cấu trúc • Cấp phát cho nó một vùng nhớ xác định. Gán địa chỉ cho con trỏ cấu trúc Một con trỏ cấu trúc có thể trỏ đến địa chỉ của một biến cấu trúc có cùng kiểu thông qua phép gán: <Tên biến con trỏ> = &<Tên biến thường>; Ví dụ, khai báo và phép gán: Employee *ptrEmployee, myEmployee; ptrEmployee = &myEmployee; sẽ đưa con trỏ ptrEmployee trỏ đến địa chỉ của biến cấu trúc myEmployee. 38
  • 40. Chương 3: Kiểu dữ liệu cấu trúc Cấp phát bộ nhớ động cho con trỏ cấu trúc Trong trường hợp ta muốn tạo ra một con trỏ cấu trúc mới, không trỏ vào một biến cấu trúc có sẵn nào, để sử dụng con trỏ mới này, ta phải cấp phát vùng nhớ cho nó. Cú pháp cấp phát vùng nhớ cho con trỏ cấu trúc: <Tên biến con trỏ> = new <Kiểu cấu trúc>; Ví dụ, cấu trúc Employee được khai báo bằng từ khoá typedef, ta có thể cấp phát vùng nhớ cho con trỏ cấu trúc như sau: Employee *ptrEmployee; ptrEmployee = new Employee; hoặc cấp phát ngay khi khai báo: Employee *ptrEmployee = new Employee; Sau khi cấp phát vùng nhớ cho con trỏ bằng thao tác new, khi con trỏ không được dùng nữa, hoặc cần trỏ sang một địa chỉ khác, ta phải giải phóng vùng nhớ vừa được cấp phát cho con trỏ bằng thao tác: delete <Tên biến con trỏ>; Ví dụ: Employee *ptrEmployee = new Employee; … // Thực hiện các thao tác trên con trỏ … delete ptrEmployee; Lưu ý: • Thao tác delete chỉ được thực hiện đối với con trỏ mà trước đó, nó được cấp phát bộ nhớ động thông qua thao tác new: Employee *ptrEmployee = new Employee; delete ptrEmployee; //đúng mà không thể thực hiện với con trỏ chỉ trỏ đến địa chỉ của một biến cấu trúc khác: Employee *ptrEmployee, myEmployee; ptrEmployee = &myEmployee; delete ptrEmployee; //lỗi Truy nhập thuộc tính của con trỏ cấu trúc Thuộc tính của con trỏ cấu trúc có thể được truy nhập thông qua hai cách: Cách 1: <Tên biến con trỏ> -> <Tên thuộc tính>; Cách 2: (*<Tên biến con trỏ>).<Tên thuộc tính>; Ví dụ, thuộc tính tên nhân viên của cấu trúc Employee có thể được truy nhập thông qua hai cách: Employee *ptrEmployee = new Employee; cin >> ptrEmployee -> name; hoặc: 39
  • 41. Chương 3: Kiểu dữ liệu cấu trúc cin >> (*ptrEmployee).name; Lưu ý: • Trong cách truy nhập thứ hai, phải có dấu ngoặc đơn “()” quanh tên con trỏ vì phép toán truy nhập thuộc tính “.” có độ ưu tiên cao hơn phép toán lấy giá trị con trỏ “*”. • Thông thường, ta dùng cách thứ nhất cho đơn giản và thuận tiện. Chương trình 3.2 cài đặt việc khởi tạo và hiển thị nội dung của một con trỏ cấu trúc. Chương trình 3.2 #include<stdio.h> #include<conio.h> #include<string.h> typedef struct { int day; int month; int year; } Date; typedef struct { char name[20]; // Tên nhân viên Date birthDay; // Ngày sinh của nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên } Employee; /* Khai báo khuôn mẫu hàm */ void InitStruct(Employee *myEmployee); void Display(Employee *myEmployee); void InitStruct(Employee *myEmployee){ myEmployee = new Employee; cout << “Name: ”; cin >> myEmployee->name; cout << “Day of birth: ”; cin >> myEmployee->birthDay.day; cout << “Month of birth: ”; cin >> myEmployee->birthDay.month; cout << “Year of birth: ”; cin >> myEmployee->birthDay.year; cout << “Role: ”; cin >> myEmployee->role; cout << “Salary: ”; 40
  • 42. Chương 3: Kiểu dữ liệu cấu trúc cin >> myEmployee->salary; } void Display(Employee myEmployee){ cout << “Name: ” << myEmployee->name << endl; cout << “Birth day: ” << myEmployee->birthDay.day << “/” << myEmployee->birthDay.month << “/” << myEmployee->birthDay.year << endl; cout << “Role: ” << myEmployee->role << endl; cout << “Salary: ” << myEmployee->salary << endl; return; } void main(){ clrscr(); Employee *myEmployee; InitStruct(myEmployee); Display(myEmployee); return; } 3.3.2 Mảng cấu trúc Khi cần xử lí nhiều đối tượng có dùng kiểu dữ liệu cấu trúc, ta có thể sử dụng mảng các cấu trúc. Vì một mảng một chiều là tương đương với một con trỏ có cùng kiểu. Do đó, có thể khai báo mảng theo hai cách: Khai báo mảng tĩnh như thông thường hoặc khai báo mảng động thông qua con trỏ. Khai báo mảng tĩnh các cấu trúc Khai báo mảng tĩnh các cấu trúc theo cú pháp: <Tên kiểu cấu trúc> <Tên biến mảng>[<Số phần tử mảng>]; Ví dụ: Employee employees[10]; là khai báo một mảng tên là employees gồm 10 phần tử có kiểu là cấu trúc Employee. Khai báo mảng động các cấu trúc Khai báo một mảng động các cấu trúc hoàn toàn tương tự khai báo một con trỏ cấu trúc cùng kiểu: <Tên kiểu cấu trúc> *<Tên biến>; Ví dụ, khai báo: Employee *employees; vừa có thể coi là khai báo một con trỏ thông thường có cấu trúc Employee, vừa có thể coi là khai báo một mảng động các cấu trúc có kiểu cấu trúc Employee. 41
  • 43. Chương 3: Kiểu dữ liệu cấu trúc Tuy nhiên, cách cấp phát bộ nhớ động cho mảng các cấu trúc khác với một con trỏ. Đây là cách để chương trình nhận biết ta đang dùng một con trỏ cấu trúc hay một mảng động có cấu trúc. Cú pháp cấp phát bộ nhớ cho mảng động như sau: <Tên biến mảng> = new <Kiểu cấu trúc>[<Số lượng phần tử>]; Ví dụ, khai báo: Employee *employees = new Employee[10]; sẽ cấp phát bộ nhớ cho một mảng động employees có 10 phần tử kiểu cấu trúc Employee. Truy nhập đến phần tử của mảng cấu trúc Việc truy nhập đến các phần tử của mảng cấu trúc được thực hiện như truy cập đến phần tử của mảng thông thường. Ví dụ muốn truy nhập đến thuộc tính tên nhân viên phần tử nhân viên thứ i trong mảng cấu trúc, ta viết như sau: Employee *employees = new Employee[10]; employees[i].name; Chương trình 3.3 cài đặt việc khởi tạo một mảng các nhân viên của một phòng trong một công ty. Sau đó, chương trình sẽ tìm và in ra thông tin về nhân viên có lương cao nhất và nhân viên có lương thấp nhất trong phòng. Chương trình 3.3 #include<stdio.h> #include<conio.h> #include<string.h> typedef struct { int day; int month; int year; } Date; typedef struct { char name[20]; // Tên nhân viên Date birthDay; // Ngày sinh của nhân viên char role[20]; // Chức vụ của nhân viên float salary; // Lương của nhân viên } Employee; /* Khai báo khuôn mẫu hàm */ void InitArray(Employee *myEmployee, int length); Employee searchSalaryMax(Employee *myEmployee, int length); Employee searchSalaryMin(Employee *myEmployee, int length); void Display(Employee myEmployee); 42
  • 44. Chương 3: Kiểu dữ liệu cấu trúc void InitArray(Employee *myEmployee, int length){ myEmployee = new Employee[length]; for(int i=0; i<length; i++){ cout << “Nhan vien thu ” << i << endl; cout << “Name: ”; cin >> myEmployee[i].name; cout << “Day of birth: ”; cin >> myEmployee[i].birthDay.day; cout << “Month of birth: ”; cin >> myEmployee[i].birthDay.month; cout << “Year of birth: ”; cin >> myEmployee[i].birthDay.year; cout << “Role: ”; cin >> myEmployee[i].role; cout << “Salary: ”; cin >> myEmployee[i].salary; } return; } Employee searchSalaryMax(Employee *myEmployee, int length){ int index = 0; int maxSalary = myEmployee[0].salary; for(int i=1; i<length; i++) if(myEmployee[i].salary > maxSalary){ maxSalary = myEmployee[i].salary; index = i; } return myEmployee[index]; } Employee searchSalaryMin(Employee *myEmployee, int length){ int index = 0; int minSalary = myEmployee[0].salary; for(int i=1; i<length; i++) if(myEmployee[i].salary < minSalary){ minSalary = myEmployee[i].salary; index = i; } return myEmployee[index]; } void Display(Employee myEmployee){ 43
  • 45. Chương 3: Kiểu dữ liệu cấu trúc cout << “Name: ” << myEmployee.name << endl; cout << “Birth day: ” << myEmployee.birthDay.day << “/” << myEmployee.birthDay.month << “/” << myEmployee.birthDay.year << endl; cout << “Role: ” << myEmployee.role << endl; cout << “Salary: ” << myEmployee.salary << endl; return; } void main(){ clrscr(); Employee *myEmployee, tmpEmployee; int length = 0; cout << “So luong nhan vien: ”; cin >> length; // Khởi tạo danh sách nhân viên InitArray(myEmployee); // Nhân viên có lương cao nhất tmpEmployee = searchSalaryMax(myEmployee, length); Display(tmpEmployee); // Nhân viên có lương thấp nhất tmpEmployee = searchSalaryMin(myEmployee, length); Display(tmpEmployee); // Giải phóng vùng nhớ delete [] myEmployee; return; } 3.4 MỘT SỐ KIỂU DỮ LIỆU TRỪU TƯỢNG Nội dung phần này tập trung trình bày việc cài đặt một số cấu trúc dữ liệu trừu tượng, bao gồm: • Ngăn xếp (stack) • Hàng đợi (queue) • Danh sách liên kết (list) 44
  • 46. Chương 3: Kiểu dữ liệu cấu trúc 3.4.1 Ngăn xếp Ngăn xếp (stack) là một kiểu danh sách cho phép thêm và bớt các phần tử ở một đầu danh sách, gọi là đỉnh của ngăn xếp. Ngăn xếp hoạt động theo nguyên lí: phần tử nào được đưa vào sau, sẽ được lấy ra trước. Định nghĩa cấu trúc ngăn xếp Vì ta chỉ cần quan tâm đến hai thuộc tính của ngăn xếp là: • Danh sách các phần tử của ngăn xếp • Vị trí đỉnh của ngăn xếp nên ta có thể định nghĩa cấu trúc ngăn xếp như sau (các phần tử của ngăn xếp có kiểu int): typedef SIZE 100; typedef struct { int top; // Vị trí của đỉnh int nodes[SIZE]; // Danh sách các phần tử } Stack; Tuy nhiên, định nghĩa này tồn tại một vấn đề, đó là kích thước (SIZE) của danh sách chứa các phần tử là tĩnh. Do đó: • Nếu ta chọn SIZE lớn, nhưng khi gặp ứng dụng chỉ cần một số ít phần tử cho ngăn xếp thì rất tốn bộ nhớ. • Nếu ta khai báo SIZE nhỏ, thì khi gặp bài toán cần ngăn xếp có nhiều phần tử, ta sẽ không thêm được các phần tử mới vào, chương trình sẽ có lỗi. Để khắc phục hạn chế này, ta có thể sử dụng bộ nhớ động (mảng động thông qua con trỏ) để lưu danh sách các phần tử của ngăn xếp. Khi đó, định nghĩa cấu trúc ngăn xếp sẽ có dạng như sau: typedef struct { int top; // Vị trí của đỉnh int *nodes; // Danh sách các phần tử } Stack; Ta sẽ sử dụng định nghĩa này trong các chương trình ứng dụng ngăn xếp. Các thao tác trên ngăn xếp Đối với các thao tác trên ngăn xếp, ta quan tâm đến hai thao tác cơ bản: • Thêm một phần tử mới vào đỉnh ngăn xếp, gọi là push. • Lấy ra một phần tử từ đỉnh ngăn xếp, gọi là pop. Khi thêm một phần tử mới vào ngăn xếp, ta làm các bước như sau: 1. Số phần tử trong ngăn xếp cũ là (top+1). Do đó, ta cấp phát một vùng nhớ mới để lưu được (top+1+1) = (top+2) phần tử. 2. Sao chép (top+1) phần tử cũ sang vùng mới. Nếu danh sách ban đầu rỗng (top = -1) thì không cần thực hiện bước này. 3. Thêm phần tử mới vào cuối vùng nhớ mới 4. Giải phóng vùng nhớ của danh sách cũ 45
  • 47. Chương 3: Kiểu dữ liệu cấu trúc 5. Cho danh sách nodes trỏ vào vùng nhớ mới. Chương trình 3.4a cài đặt thủ tục thêm một phần tử mới vào ngăn xếp. Chương trình 3.4a void push(Stack *stack, int node){ int *tmpNodes = new int[stack->top + 2];// Cấp phát vùng nhớ mới stack->top ++; // Tăng chỉ số của node đỉnh for(int i=0; i<stack->top; i++) // Sao chép sang vùng nhớ mới tmpNodes[i] = stack->nodes[i]; tmpNodes[stack->top] = node; // Thêm node mới vào đỉnh delete [] stack->nodes; // Giải phóng vùng nhớ cũ stack->nodes = tmpNodes; // Trỏ vào vùng nhớ mới return; } Khi lấy ra một phần tử của ngăn xếp, ta làm các bước như sau: • Kiểm tra xem ngăn xếp có rỗng (top = -1) hay không. Nếu không rỗng thì thực hiện các bước tiếp theo. • Lấy phần tử ở đỉnh ngăn xếp ra • Cấp phát một vùng nhớ mới có (top+1) -1 = top phần tử • Sao chép top phần tử từ danh sách cũ sang vùng nhớ mới (trừ phần tử ở đỉnh). • Giải phóng vùng nhớ cũ • Cho con trỏ danh sách trỏ vào vùng nhớ mới. • Trả về giá trị phần tử ở đỉnh đã lấy ra. Chương trình 3.4b cài đặt thủ tục lấy một phần tử từ ngăn xếp. Chương trình 3.4b int pop(Stack *stack){ if(stack->top < 0){ // Kiểm tra ngăn xếp rỗng cout << “Stack is empty!” << endl; return 0; } int result = stack->nodes[stack->top];// Lưu giữ giá trị đỉnh int *tmpNodes = new int[stack->top];// Cấp phát vùng nhớ mới for(int i=0; i<stack->top; i++) // Sao chép sang vùng nhớ mới tmpNodes[i] = stack->nodes[i]; stack->top --; // Giảm chỉ số của node đỉnh delete [] stack->nodes; // Giải phóng vùng nhớ cũ stack->nodes = tmpNodes; // Trỏ vào vùng nhớ mới return result; // Trả về giá trị node đỉnh 46
  • 48. Chương 3: Kiểu dữ liệu cấu trúc } Áp dụng Ngăn xếp được sử dụng trong các ứng dụng thoả mãn nguyên tắc: cái nào đặt vào trước sẽ được lấy ra sau. Chương trính 3.4c minh hoạ việc dùng ngăn xếp để đảo ngược một xâu kí tự được nhập vào từ bàn phím. Chương trình 3.4c #include<stdio.h> #include<stdlib.h> #include<string.h> typedef struct { int top; // Vị trí node đỉnh int *nodes; // Danh sách phần tử } Stack; /* Khai báo nguyên mẫu hàm */ void init(Stack *stack); void push(Stack *stack, int node); int pop(Stack *stack); void release(Stack *stack); void init(Stack *stack){ stack = new Stack; // Cấp phát vùng nhớ cho con trỏ stack->top = -1; // Khởi tạo ngăn xếp rỗng } void push(Stack *stack, int node){ int *tmpNodes = new int[stack->top + 2];// Cấp phát vùng nhớ mới stack->top ++; // Tăng chỉ số của node đỉnh for(int i=0; i<stack->top; i++) // Sao chép sang vùng nhớ mới tmpNodes[i] = stack->nodes[i]; tmpNodes[stack->top] = node; // Thêm node mới vào đỉnh delete [] stack->nodes; // Giải phóng vùng nhớ cũ stack->nodes = tmpNodes; // Trỏ vào vùng nhớ mới return; } int pop(Stack *stack){ if(stack->top < 0){ // Kiểm tra ngăn xếp rỗng 47
  • 49. Chương 3: Kiểu dữ liệu cấu trúc cout << “Stack is empty!” << endl; return 0; } int result = stack->nodes[stack->top];// Lưu giữ giá trị đỉnh int *tmpNodes = new int[stack->top];// Cấp phát vùng nhớ mới for(int i=0; i<stack->top; i++) // Sao chép sang vùng nhớ mới tmpNodes[i] = stack->nodes[i]; stack->top --; // Giảm chỉ số của node đỉnh delete [] stack->nodes; // Giải phóng vùng nhớ cũ stack->nodes = tmpNodes; // Trỏ vào vùng nhớ mới return result; // Trả về giá trị node đỉnh } void release(Stack *stack){ delete [] stack->nodes; // Giải phóng vùng danh sách delete stack; // Giải phóng con trỏ return; } void main(){ clrscr(); Stack *stack; init(stack); // Khởi tạo ngăn xếp char strIn[250]; // Nhập chuỗi kí tự từ bàn phím cout << “Nhap chuoi: ”; cin >> strIn; for(int i=0; i<strlen(strIn); i++) // Đặt vào ngăn xếp push(stack, strIn[i]); while(stack->top > -1) // Lấy ra từ ngăn xếp cout << pop(stack); release(stack); // Giải phóng bộ nhớ return; } 3.4.2 Hàng đợi Hàng đợi (queue) cũng là một cấu trúc tuyến tính các phần tử. Trong đó, các phần tử luôn được thêm vào ở một đầu, gọi là đầu cuối hàng đợi, và việc lấy ra các phần tử luôn được thực hiện ở đầu còn lại, gọi là đầu mặt của hàng đợi. Hàng đợi hoạt động theo nguyên lí: phần tử nào được đưa vào trước, sẽ được lấy ra trước. 48