Professional Documents
Culture Documents
Ngôn ngữ lập trình C++ (chuẩn 2011)
Ngôn ngữ lập trình C++ (chuẩn 2011)
C++
Ngôn ngữ lập trình
C++
TPHCM − 02/2018
“C makes it easy to shoot yourself in the foot;
C++ makes it harder, but when you do it blows your whole leg off.”
— Bjarne Stroustrup
Mục lục
1
Mục lục 2
4 Mảng và chuỗi 37
4.1 Mảng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.1.1 Khai báo mảng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.1.2 Thao tác với các phần tử trong mảng . . . . . . . . . . . . . . . . . . . . . . 38
4.1.3 Mảng nhiều chiều . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.2 Chuỗi kí tự . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.2.1 Chuỗi kí tự theo kiểu C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.2.2 Lớp chuỗi kí tự . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.2.3 Mảng chứa các chuỗi kí tự . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
4.2.4 Chuỗi thô . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
5 Hàm 47
5.1 Khai báo hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.1.1 Cú pháp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.1.2 Khai báo hai hay nhiều hàm có cùng tên . . . . . . . . . . . . . . . . . . . . . 48
5.1.3 Nguyên mẫu hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
5.2 Đối số của hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.2.1 Giá trị mặc định . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.2.2 Cách truyền đối số . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.3 Hàm đệ quy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
5.4 Hàm nội tuyến . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
5.5 Kĩ thuật nạp chồng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.5.1 Nạp chồng hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.5.2 Nạp chồng toán tử . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.6 Các hàm lambda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6 Con trỏ 57
6.1 Thao tác với con trỏ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.1.1 Khai báo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.1.2 Con trỏ NULL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
6.1.3 Con trỏ void . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
6.1.4 Phép tính số học với con trỏ . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
6.1.5 Phép so sánh với con trỏ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
6.2 Con trỏ và tham chiếu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
6.3 Con trỏ và hằng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
6.4 Con trỏ và mảng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
6.4.1 Con trỏ và địa chỉ mảng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
6.4.2 Mảng con trỏ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
6.5 Con trỏ và hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
6.5.1 Truyền con trỏ cho hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
6.5.2 Trả về con trỏ từ hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
6.5.3 Con trỏ hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
6.6 Bộ nhớ động . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
6.6.1 Toán tử cấp phát bộ nhớ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
6.6.2 Hàm cấp phát bộ nhớ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
6.6.3 Các vùng bộ nhớ trên máy tính . . . . . . . . . . . . . . . . . . . . . . . . . . 67
6.6.4 Các lỗi thường gặp khi sử dụng bộ nhớ động . . . . . . . . . . . . . . . . . . 67
6.7 Smart pointer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
7 Tập tin 71
7.1 Thao tác với tập tin theo kiểu C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
7.1.1 Mở/đóng tập tin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
7.1.2 Đọc dữ liệu từ tập tin văn bản . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3 MỤC LỤC
Ngôn ngữ lập trình C/C++ là một trong những ngôn ngữ lập trình phổ biến nhất trên thế giới,
được ứng dụng trong nhiều ngành khác nhau, và là nền tảng để xây dựng nên một số hệ điều
hành, ngôn ngữ lập trình khác (xem danh sách một số ứng dụng tiêu biểu của C++ tại đây
http://www.stroustrup.com/applications.html). Trong lĩnh vực vật lý hạt nhân và hạt cơ
bản, ngôn ngữ lập trình C++ được sử dụng để xây dựng các chương trình tính toán lý thuyết, mô
phỏng và xử lý số liệu, chẳng hạn như
• LISE: chương trình tính toán phản ứng hạt nhân
http://lise.nscl.msu.edu/lise.html
• GALPROP: chương trình tính toán cho vật lý thiên văn
http://galprop.stanford.edu/
• PYTHIA: chương trình tạo các event cho vật lý hạt cơ bản
http://home.thep.lu.se/~torbjorn/Pythia.html
• GEANT4: chương trình mô phỏng vận chuyển hạt bằng phương pháp Monte Carlo
http://geant4.cern.ch/
• ROOT: chương trình xử lý số liệu
http://root.cern.ch/drupal/
• ...
Tài liệu này được biên soạn nhằm giúp các bạn sinh viên chuyên ngành vật lý hạt nhân và hạt cơ
bản có thể tìm hiểu về ngôn ngữ lập trình C++ và ứng dụng của nó vào giải quyết các bài toán
chuyên ngành. Phần đầu của tài liệu sẽ trình bày một số kiến thức cơ bản về ngôn ngữ lập trình
C++ cũng như lịch sử phát triển của nó nhằm giúp cho các bạn có một cái nhìn tổng quát về ngôn
ngữ lập trình này. Một số khái niệm cơ bản của kĩ thuật lập trình hướng đối tượng cũng được đề
cập đến trong phần này.
Các Chương 2 - 4 là phần dành cho các bạn mới bắt đầu làm quen với việc lập trình C++. Một số
kiến thức cơ bản về ngôn ngữ lập trình C++ sẽ được trình bày trong các chương này chẳng hạn
như các khái niệm về biến (variable), hằng (constant), toán tử (operator ); các cấu trúc điều khiển;
và đặc biệt là các kiểu dữ liệu mảng (array) và chuỗi (string). Đây chính là các khái niệm cơ bản
mà các bạn phải nắm vững nếu muốn viết một chương trình C++.
Đối với các bạn đã biết căn bản về C++, các bạn có thể chuyển thẳng vào Chương 5 để làm quen
với các kiến thức về hàm cũng như về con trỏ (Chương 6). Sau đó, các bạn sẽ được làm quen với
các thao tác sử lý tập tin (file) trong Chương 7. Các kiến thức về lớp (class) và đối tượng (object)
sẽ được trình bày trong Chương 8.
Trong Chương 9, tác giả sẽ trình bày các kiến thức về template và đặc biệt là một số kiến thức
cơ bản về thư viện chuẩn của C++. Chương cuối sẽ được dành cho việc giới thiệu những kiến thức
5
LỜI NÓI ĐẦU 6
nâng cao cũng như những cập nhật mới nhất của chuẩn C++11 là tiêu chuẩn đang được sử dụng
trong tài liệu này.
Mục đich của tài liệu là giới thiệu nhưng kiến thức, khái niệm cơ bản nhất của C++ do đó nội dung
của nó khá cô đọng. Mặc dù vẫn còn rất nhiều khái niệm chưa được trình bày trong tài liệu này,
nhưng hi vọng nó sẽ giúp ích cho các bạn sinh viên có mong muốn sử dụng thành thạo một ngôn
ngữ lập trình mạnh trong công việc tính toán, xử lý số liệu hay mô phỏng.
Ngôn ngữ lập trình được chia làm 5 thế hệ (Hình 1.1):
• Thế hệ 1: ngôn ngữ máy (machine language), phụ thuộc vào từng loại máy, đây là ngôn ngữ
lập trình có hiệu quả cao tuy nhiên lại khó hiểu và khó sử dụng đối với người dùng.
• Thế hệ 2: hợp ngữ (assembly language) được xem là ngôn ngữ cấp thấp, sử dụng các thuật
nhớ (mnemonic) thay thế cho mã máy (binary), do đó dễ viết hơn so với ngôn ngữ máy. Các
chương trình hợp ngữ thường phụ thuộc chặt chẽ vào một kiến trúc máy tính xác định. Việc
chuyển đổi giữa hợp ngữ và mã máy được thực hiện thông qua các trình hợp dịch (assembler )
và trình phân dịch (disassembler ).
• Thế hệ 3: các ngôn ngữ lập trình có cấu trúc (structured language) hay ngôn ngữ lập trình
thủ tục (procedural language), các chương trình được viết dưới dạng mã nguồn (source code)
và được biên dịch thành ngôn ngữ máy. Các chương trình thực hiện việc biên dịch này được
gọi là các trình biên dịch (compiler ), hoặc trong một số trường hợp ta còn sử dụng các trình
thông dịch (interpreter ). Đặc điểm của các ngôn ngữ thế hệ thứ 3 này so với các ngôn ngữ
thế hệ trước đó là chúng độc lập với kiến trúc máy tính. Các ngôn ngữ tiêu biểu có thể kể
đến chẳng hạn như Fortran, Basic, Pascal, C/C++,...
• Thế hệ 4: các ngôn ngữ lập trình theo lĩnh vực (domain-specific language), thường tập trung
vào một lĩnh vực cụ thể hơn so với các ngôn ngữ thế hệ thứ 3 (thường là các ngôn ngữ đa
mục đích − general purpose). Đồng thời, các chương trình thuộc ngôn ngữ thế hệ này cũng
đòi hỏi lập trình ít hơn để hoàn thành công việc so với ngôn ngữ thế hệ thứ 3. Các ngôn ngữ
tiêu biểu có thể kể đến chẳng hạn như Mathematica, Matlab, Python, Ruby, Perl,...
• Thế hệ 5: các ngôn ngữ ứng dụng (applicative language) hay ngôn ngữ khai báo (declarative
language), thường tập trung giải quyết những vấn đề cụ thể mà không cần đến người lập
trình chuyên nghiệp, không quá chú trọng đến thuật toán xứ lý vấn đề. Các ngôn ngữ tiêu
biểu có thể kể đến chẳng hạn như Lisp, Scheme, SML, Prolog,...
Các ngôn ngữ lập trình cũng được chia là hai nhóm chính:
7
1.2. Lập trình hướng đối tượng 8
• Nhóm ngôn ngữ truyền thống: xây dựng một chuỗi các bước thực hiện, nhóm này bao gồm
các ngôn ngữ trong 3 thế hệ đầu tiên1 .
• Nhóm ngôn ngữ hướng đối tượng: tạo ra các đối tượng thay vì chuỗi các bước thực hiện, bao
gồm một số ngôn ngữ thế hệ 3, các ngôn ngữ thế hệ 4 và 5.
Hình 1.2: Minh họa đối tượng; một đối tượng, chẳng hạn như một chiếc xe hơi, sẽ có các thông tin
(thuộc tính) như mẫu xe, năm sản xuất, màu sắc và các hành động (phương thức) như khởi động,
chạy và dừng.
Mỗi đối tượng có một tên riêng biệt và tất cả các tham chiếu đến đối tượng đó được tiến hành qua
tên của nó. Như vậy, mỗi đối tượng có khả năng nhận vào các thông báo, xử lý dữ liệu (bên trong
của nó), và gửi ra hay trả lời đến các đối tượng khác hay đến môi trường.
Một số tính chất chính của lập trình hướng đối tượng:
• Tính trừu tượng (abstraction): khả năng của chương trình bỏ qua hay không chú ý đến một
số khía cạnh của thông tin mà nó đang trực tiếp làm việc lên. Mỗi đối tượng có thể hoàn tất
các công việc một cách nội bộ và liên lạc với các đối tượng khác mà không cần cho biết làm
cách nào đối tượng tiến hành được các thao tác. Tính trừu tượng còn thể hiện qua việc một
đối tượng ban đầu có thể có một số đặc điểm chung cho nhiều đối tượng khác như là sự mở
rộng của nó nhưng bản thân đối tượng ban đầu này có thể không có các biện pháp thi hành.
Tính trừu tượng này thường được xác định trong khái niệm gọi là lớp trừu tượng (abstract
class) hay lớp cơ sở trừu tượng (abstract base class).
• Tính đóng gói (encapsulation): không cho phép người sử dụng các đối tượng thay đổi trạng
thái nội tại của một đối tượng, chỉ có các phương thức nội tại của đối tượng cho phép thay
đổi trạng thái của nó. Việc cho phép môi trường bên ngoài tác động lên các dữ liệu nội tại
của một đối tượng theo cách nào là hoàn toàn tùy thuộc vào người lập trình. Đây là tính
chất đảm bảo sự toàn vẹn của đối tượng.
• Tính kế thừa (inheritance): cho phép một đối tượng có thể có sẵn các đặc tính mà đối tượng
khác đã có thông qua kế thừa. Điều này cho phép các đối tượng chia sẻ hay mở rộng các đặc
tính sẵn có mà không cần phải tiến hành định nghĩa lại. Nếu ta có một lớp B kế thừa từ lớp
A thì A được gọi là lớp cơ sở (base class) và B được gọi là lớp dẫn xuất (derived class).
• Tính đa hình (polymorphism): thể hiện thông qua việc gửi các thông điệp (message), các
phương thức dùng trả lời cho một thông điệp sẽ tùy theo đối tượng mà thông điệp đó được
gửi tới sẽ có các phản ứng khác nhau. Người lập trình có thể định nghĩa một đặc tính (chẳng
hạn như một phương thức) cho một loạt các đối tượng gần nhau, khi thi hành chương trình
sẽ tự động kiểm tra xem đối tượng là thuộc kiểu lớp nào sau đó sẽ gọi phương thức tương
ứng với lớp đó.
• Dữ liệu được đóng gói vào trong 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, do đó sẽ giảm thiểu nguy cơ dữ liệu bị
thay đổi tự do trong chương trình.
1.3. Ngôn ngữ C++ 10
• 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, điều này cũng 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 và giúp cho việc bảo trì, nâng cấp chương trình dễ dàng hơn.
• Nhờ nguyên tắc kế thừa, ta có thể sử dụng lại mã nguồn, tiết kiệm tài nguyên, chi phí thời
gian.
Tuy nhiên phương pháp lập trình hướng đối tượng cũng có những nhược điểm:
• Khái niệm hướng đối tượng còn tương đối mới lạ đối với một số người, do đó tốn không ít
thời gian để tìm hiểu và xây dựng một chương trình hướng đối tượng.
• Các chương trình hướng đối tượng thông thường đòi hỏi việc viết mã nguồn dài hơn so với
các chương trình thủ tục thông thường.
• Thời gian thực thi các chương trình hướng đối tượng nói chung cũng chậm hơn so với các
chương trình thủ tục.
C++ là một ngôn ngữ lập trình đa mục đích (general purpose) được Bjarne Stroustrup phát triển
từ những năm 1980 tại Bell Labs. Đây là một trong những ngôn ngữ lập trình phổ biến nhất trên
thế giới (cùng với C, Objective-C, và Java). Các đặc trưng của ngôn ngữ này gồm có:
• Kiểu tĩnh (static type): tất cả các biến phải được gán kiểu trước khi biên dịch, và kiểu của
biến sẽ được kiểm tra trong thời gian biên dịch (compile-time)2
• Hỗ trợ lập trình thủ tục (procedural programing) hay lập trình cấu trúc (structured program-
ming): phân chia công việc chính thành các công việc nhỏ hơn và giao cho một hàm đảm
nhiệm, chương trình chính sẽ gọi các hàm này vào những thời điểm cần thiết.
• Dữ liệu trừu trượng (abstract data type): có sử dụng các kiểu dữ liệu do người dùng tự định
nghĩa.
• Lập trình hướng đối tượng (object-oriented programming): coi chương trình là tập hợp của
các đối tượng (object) có quan hệ nào đó với nhau, mỗi đối tượng có dữ liệu và phương thức
(method ) của riêng mình.
• Lập trình đa hình (polymorphism): một biến có thể có nhiều kiểu (thông qua con trỏ) hoặc
có thể dùng nhiều hàm có cùng tên.
• Phần ngôn ngữ cốt lõi (core language) bao gồm ngôn ngữ lập trình, một số thư viện gốc và
các danh định (identifier ) được biết đến với tên gọi “từ khóa” (keyword ).
• Thư viện chuẩn C++ (Standard Template Library − STL) là một tập hợp các lớp và các hàm
được viết bằng ngôn ngữ cốt lõi. Thư viện này cung cấp các container, hàm để làm tiện ích,
các đối tượng hàm, các dãy kí tự tổng quát và các dòng dữ liệu (bao gồm I/O tương tác và
tập tin), hỗ trợ một số tính năng ngôn ngữ (bao gồm cả thư viện chuẩn C).
2
ngược lại là kiểu động (dynamic type), các biến có thể mang bất kì kiểu nào, việc gán kiểu cho biến sẽ được thực
hiện trong thời gian chạy chương trình (run-time)
11 CHƯƠNG 1. GIỚI THIỆU NGÔN NGỮ LẬP TRÌNH C++
• Vẫn tương thích với các phiên bản C++ cũ (C++98, C++03).
• Không đưa thêm vào quá nhiều các khái niệm mới hoặc các tính năng chưa ổn định.
• Làm cho ngôn ngữ C++ trở nên dễ học và dễ dạy hơn bằng các giảm thiểu các tính năng mà
cần các lập trình viên chuyên nghiệp.
• ...
Chuẩn C++11 cũng chính là chuẩn được sử dụng trong tài liệu này. Các nâng cấp của chuẩn này
so với các chuẩn trước đó sẽ được trình bày trong một chương riêng.
Hình 1.3: Bảng liệt kê một số trình biên dịch thông dụng
1.5 Hướng dẫn cài đặt và sử dụng một số trình biên dịch C++
Phần này sẽ hướng dẫn các bạn cách thức cài đặt và sử dụng của 2 trong số những trình biên dịch
C++ thông dụng nhất hiện nay là VC++ và GCC.
Trong phần hướng dẫn này, phiên bản Visual C++ 2010 Express được sử dụng. Các bước thực hiện
việc cài đặt như sau:
• Sau khi download thành công file setup (vc_web.exe), double click để chạy chương trình cài
đặt và làm theo các bước hướng dẫn của chương trình.
• Sau khi cài đặt hoàn tất, Microsoft còn có yêu cầu đăng kí để được sử dụng sản phẩm. Vào
Start → All Programs, mở Microsoft Visual C++ 2010 Express, trên thanh menu nhấp vào
Help → Register Product và làm theo các hướng dẫn đăng kí.
• Sau khi cài đặt xong, chương trình sẽ tạo một file vcvars32.bat (nằm trong thư mục bin),
chung ta cần chạy file này để thiết lập các giá trị cho các biến môi trường PATH, LIB và
INCLUDE.
• Ở mục Visual C++ chọn Win32 Console Application, nhập tên của project, chọn OK rồi
Finish.
• File mã nguồn có tên gọi <project.cpp> sẽ được tạo ra, chúng ta sẽ viết mã nguồn vào trong
file này.
• Trên thanh menu Debug, chọn Debug (F5) hoặc Build (F7) để biên dịch. File thực thi tạo ra
sẽ được chứa trong thư mục <project>\Debug.
Ngoài ra, việc biên dịch các file mã nguồn C++ còn có thể được thực hiện trên giao diện dòng lệnh
Command Prompt với file cl.exe, cú pháp cho lệnh biên dịch như sau
cl [ options ] < source codes >
Ví dụ 1.1: Biên dịch và thực thi một chương trình ‘helloworld’ với CL
$ cl helloworld . cpp
Để thực thi chương trình ứng dụng helloworld.exe vừa được tạo ra, ta gõ lệnh
$ helloworld
1.5.2 GCC
Trong hầu hết các phiên bản Linux, trình biên dịch được mặc định cài đặt sẵn là GCC (GNU
Compiler Collection), đây là một bộ các trình biên dịch có khả năng biên dịch nhiều ngôn ngữ
khác nhau như C (gcc), C++ (g++), Fortran (gfortran),...
Tên gốc của GCC là GNU C Compiler do ban đầu nó chỉ hỗ trợ dịch ngôn ngữ lập trình C. Phiên
bản đầu tiên GCC 1.0 được phát hành vào năm 1987, sau đó được mở rộng hỗ trợ dịch C++ vào
tháng 12 cùng năm đó. Sau đó, GCC được phát triển cho các ngôn ngữ lập trình Fortran, Pascal,
Objective C, Java, and Ada,... Bảng dưới trình bày một số trình biên dịch thông dụng nhất trong
GCC.
Ngôn ngữ Trình biên dịch
C gcc
C++ g++
Fortran gfortran
Pascal gpc
Java gcj
Ada gnat
D gdc
VHDL ghdl
Các bạn có thể tham khảo thêm thông tin về các trình biên dịch này tại https://gcc.gnu.org/.
Tuỳ thuộc vào bản phân phối Linux mà chúng ta có thể cài đặt với những lệnh khác nhau
• CentOS/RedHat
$ yum install gcc gcc - c ++
• Debian/Ubuntu
$ sudo apt - get install gcc
Các trình biên dịch GCC thường được đặt trong thư mục /usr/bin hay /usr/local/bin. Để kiểm
tra phiên bản GCC vừa được cài đặt, ta gõ lệnh
$ gcc -- version
1.5. Hướng dẫn cài đặt và sử dụng một số trình biên dịch C++ 16
7
Cygwin là một bộ các công cụ cung cấp các chức năng tương tự như một môi trường Linux cho hệ điều hành
Windows.
8
MinGW (Minimalist GNU for Windows) là một bản phân phối các trình biên dịch của GNU trên môi trường
Windows.
Chương 2
Một số khái niệm C++ cơ bản
Trong chương này, một số khái niệm cơ bản về C++ sẽ được trình bày nhằm giúp cho người đọc
mới bắt đầu làm quen với ngôn ngữ sẽ dễ dàng nắm bắt hơn những kiến thức được trình bày ở
những phần sau.
2.1 Cấu trúc đơn giản của một chương trình C++
Cấu trúc phổ biến của một file C++ là như sau
# include < header >
int main ()
{
// Noi dung
}
• Các dòng bắt đầu bằng kí tự ‘#’ được gọi là các chỉ thị tiền xử lý (preprocessor directive),
dùng để báo hiệu cho trình biên dịch. Ví dụ như lệnh #include <header> sẽ báo cho trình
biên dịch sử dụng các file header trong khi biên dịch chương trình.
• Kí tự ‘//’ có tác dụng comment toàn bộ kí tự sau nó trên cùng 1 dòng, trong trường hợp ta
muốn comment nhiều hơn 1 dòng thì ta sử dụng cặp kí tự ‘/*’ và ‘*/’ để mở đầu và kết thúc.
• Hàm main() là hàm chính của chương trình, đây là nơi mà chương trình bắt đầu thi hành
các lệnh. Hàm này bắt buộc phải có khi muốn biên dịch một chương trình viết bằng C++.
Hàm main() có thể được đặt ở bất kì vị trí nào trong chương trình (đầu, cuối hoặc giữa) và
luôn là hàm được thực hiện đầu tiên khi chạy chương trình.
17
2.3. Biến 18
int main ()
{
std :: cout << " Hello World ! " << std :: endl ;
return 0;
}
• Toán tử << được gọi là toán tử chèn vì nó chèn dữ liệu đi sau nó vào dòng dữ liệu đứng
trước.
• Để xuống dòng ta có thể sử dụng kí tự \n hoặc tham số endl
cout << " Hello World !\ n " ;
• Để sử dụng được các lệnh cin và cout, ta cần phải khai báo thư viện iostream
# include < iostream >
Chúng ta sử dụng không gian tên std:: trước các lệnh này (sẽ bàn cụ thể hơn ở các phần sau).
2.3 Biến
Biến (variable) có thể được xem như là vùng nhớ chứa dữ liệu tạm thời trong khi thực thi chương
trình. Các dữ liệu được lưu trữ trong biến có thể là các giá trị số, chuỗi kí tự,... và các dữ liệu này
có thể thay đổi được trong quá trình thực thi chương trình.
Để có thể sử dụng một biến trong C++, đầu tiên chúng ta cần phải khai báo biến đó. Cách thức
khai báo một biến là ghi ra tên kiểu (vd: int, float,...) và sau đó là tên của biến.
Một tên biến được xem là hợp lệ khi nó là một chuỗi gồm các chữ cái, chữ số hoặc kí tự gạch dưới
(không chứa kí tự trống hoặc kí tự đặc biệt). Chiều dài của tên biến là không giới hạn. Một số lưu
ý khi đặt tên cho biến
• Tên biến thường bắt đầu bằng một chữ cái.
• Các tên bắt đầu bằng kí tự gạch dưới ‘_’ thường được dành cho các liên kết bên ngoài
(external link ).
• Không bao giờ bắt đầu tên biến bằng một chữ số.
• Không được đặt trùng tên biến với các từ khóa của C++ (xem danh sách tại http://en.
cppreference.com/w/cpp/keyword).
• C++ phân biệt chữ hoa và chữ thường.
Ví dụ 2.2: Khai báo hai biến kiểu nguyên x, y và một biến kiểu kí tự a
int x ;
int y ;
char a ;
Trong trường hợp nhiều biến có cùng kiểu, ta có thể khai báo
int x , y ;
Khi khai báo một biến, giá trị của nó mặc nhiên là không xác định. Trong trường hợp muốn khởi
tạo giá trị ban đầu cho biến ngay khi khai báo biến (ví dụ khởi tạo giá trị ban đầu cho biến y là
5), ta có thể sử dụng một trong những cách sau đây
int x = 5;
int x (5) ;
int x {5};
int x {}; // x = 0
int x = int () ; // x = 0
int g ; // bien toan cuc , duoc su dung trong toan bo file nay
int main ()
{
int x ; // bien cuc bo , chi duoc su dung trong ham main ()
return 0;
}
Trong trường hợp file global.h được sử dụng trong file source.cpp, chúng ta có thể sử dụng biến
ngoài này
// source . cpp
# include < iostream >
# include " global . h "
int main ()
{
std :: cout << " Gia tri cua bien g : " << g << std :: endl ;
return 0;
}
Mặc dù biến g không được khai báo trực tiếp trong file source.cpp nhưng kết quả xuất ra vẫn là
Gia tri cua bien g : 1
int main ()
{
func () ; // x = 1
func () ; // x = 2
func () ; // x = 3
return 0;
}
Trong ví dụ này, mặc dù chỉ được khai báo một cách cục bộ bên trong hàm func() nhưng do x là
một biến tĩnh nên nó có tác dụng trong toàn bộ chương trình, giá trị của nó không bị khởi tạo lại
mỗi khi gọi hàm func().
Trong ví dụ này, biến n được khởi tạo lại trong mỗi vòng lặp, do đó giá trị in ra luôn là 1.
21 CHƯƠNG 2. MỘT SỐ KHÁI NIỆM C++ CƠ BẢN
int a = 1; // a la lvalue
int b = 1; // b la lvalue
int c = a + b ; // a va b duoc chuyen thanh rvalue , tra ve rvalue
int a ; // a la lvalue
int b ; // b la lvalue
int c = a + b ; // a va b khong chuyen duoc thanh rvalue --> ERROR
Để lấy thông tin về kiểu của đối tượng ta sử dụng từ khóa decltype
auto m = 100; // m co kieu la int
decltype (100) n = 100; // gan kieu int ( la kieu cua 100) cho n
2.4 Hằng
Hằng (constant) là bất kì một biểu thức nào mang một giá trị cố định.
5
Từ khóa auto không phải là từ khóa mới, nó được dùng trong những chuẩn C/C++ trước đó nhằm để chỉ rằng
các đối tượng được tạo ra trong vùng nhớ động và sẽ tự động được giải phóng khỏi bộ nhớ chương trình khi không
dùng đến nữa, C++11 đã sử dụng lại từ khóa này với ý nghĩa hoàn toàn mới.
2.5. Toán tử 22
• Kí tự, chuỗi kí tự
'a '
" Good morning ! "
• Mã điều khiển: \n (xuống dòng), \b (backspace), \r (lùi về đầu dòng), \t (tab), \v (căn thẳng
theo chiều dọc), \f (sang trang), \a (kêu bíp), \’ (dấu nháy đơn), \” (dấu nháy kép),...
• Sử dụng chỉ thị tiền xử lý #define. Khi khai báo hằng bằng #define, chương trình sẽ thay
thế các tên hằng bằng giá trị của nó tại bất kì chỗ nào chúng xuất hiện. Các hằng số được
định nghĩa theo cách này ược coi là các hằng số macro.
• Sử dụng tiền tố const và khai báo các hằng với một kiểu xác định như là làm với một biến.
Trong trường hợp kiểu dữ liệu không được chỉ rõ trình biên dịch sẽ coi nó là kiểu int.
2.5 Toán tử
Dưới dây là một số các toán tử thông dụng
Các toán tử số học dùng để tính toán các giá trị, gồm có: +, −, *, /, % (chia lấy dư)
Các toán tử tăng giảm tăng hoặc giảm giá trị của biến đi 1 đơn vị, gồm có
++ tăng lên 1 đơn vị
- - giảm đi 1 đơn vị
Ví dụ 2.13: Minh hoạ các toán tử tăng giảm
int a = 5;
a ++; // tuong duong voi a = a + 1
Lưu ý rằng có sự khác biệt giữa việc đặt toán tử tăng giảm phía trước hoặc phía sau của biến.
Trong trường hợp đặt phía trước, giá trị của biến sẽ được tăng/giảm trước khi thực hiên câu lệnh,
còn trường hợp đặt phía sau thì giá trị của biến sẽ được tăng/giảm sau khi hoàn tất câu lệnh, ví
dụ như
int a = 5;
int b = a ++; // a = 5 , b = 4
int b = ++ a ; // a = 5 , b = 5
Các toán tử gán phức hợp gán giá trị cho biến cùng lúc với thực hiện một toán tử số học lên
nó, tương dương với việc gán giá trị của phép toán lên toán hạng đầu tiên, các toán tử này gồm có
+= tăng lên một lượng
-= giảm đi một lượng
*= nhân thêm một lượng
/= chia cho một lượng
Ví dụ 2.14: Minh hoạ các toán tử gán phức hợp
int a = 10 , b = 3;
a -= 2; // tuong duong voi a = a - 2
b *= a +1; // tuong duong voi b = b * ( a +1)
Các toán tử so sánh so sánh giá trị giữa hai biểu thức với nhau, gồm có
== so sánh bằng
> so sánh lớn hơn
< so sánh nhỏ hơn
>= so sánh lớn hơn hoặc bằng
<= so sánh nhỏ hơn hoặc bằng
!= so sánh khác (không bằng)
Ví dụ 2.15: Minh hoạ các toán tử so sánh
int a = 7;
( a == 7) // true
( a < 8) // true
( a == 1) // false
( a != 3) // true
( a *1 >= 7) // true
Các toán tử logic thực hiện các phép toán logic, gồm có: && (and ), || (or ), ! (not)
Ví dụ 2.16: Minh hoạ các toán tử logic
int a = 7;
( a == 7) // true
!( a == 7) // false
( a == 7 && 0) // false
2.6. Các hàm toán học 24
( a == 7 || 0) // true
Toán tử dấu phẩy (,) dùng để phân chia hai hay nhiều biểu thức trong trường hợp cần sử dụng
duy nhất một biểu thức
Ví dụ 2.18: Minh hoạ toán tử dấu phẩy
int a , b = 3;
int c = ( a = 1 , a + b ) ; // dau tien gan a = 1 , roi sau do c = a + b
Các toán tử thao tác bit thực hiện thao tác trên các bit, gồm có: & (logical and ), | (logical
or ), ∧ (logical xor ), ∼ (logical not), >> (dịch bit sang phải), << (dịch bit sang trái)
Ví dụ 2.19: Minh hoạ toán tử thao tác bit
int a = 6; // 6 = 0000 0110
int b = 10; // 10 = 0000 1010
int c = a & b ; // 2 = 0000 0010
Các chỉ thị định nghĩa (#define và #undef) dùng để định nghĩa hay hủy bỏ định nghĩa các
macro.
Ví dụ 2.23: Minh hoạ các chỉ thị định nghĩa
# define SIZE 100
int table1 [ SIZE ]; // tuong duong voi khai bao : int table1 [100]
# undef SIZE // bo dinh nghia macro SIZE
# define SIZE 200 // dinh nghia lai macro SIZE
int table2 [ SIZE ]; // table2 [200]
Ta cũng có thể định nghĩa một macro hàm có tham số
# define getmax (a , b ) a > b ? a : b
Trong định nghĩa macro hàm, toán tử # dùng để báo rằng tham số sẽ được thay thế bởi một chuỗi
# define str ( x ) # x
std :: cout << str ( hello ) ; // tuong duong std :: cout << " hello ";
và toán tử ## dùng để nối hai tham số (không có khoảng trắng ở giữa)
# define glue (a , b ) a ## b
glue (c , out ) << " hello " ; // tuong duong cout << " hello ";
Các chỉ thị điều kiện (#ifdef, #ifndef, #if, #endif, #else, #elif) cho phép bao gồm hoặc
loại bỏ một phần các dòng lệnh nếu những điều kiện được thỏa.
Ví dụ 2.24: Minh hoạ các chỉ thị điều kiện
# ifdef SIZE
int table [ SIZE ]; // neu macro SIZE da duoc dinh nghia thi khai bao table [
SIZE ]
# endif
2.7. Các chỉ thị tiền xử lý 26
# ifndef SIZE
# define SIZE 100 // neu macro SIZE chua duoc dinh nghia thi dinh nghia no
# endif
Các chỉ thị #ifdef, #ifndef có thể được thay thế bởi các toán tử đặc biệt defined và !defined
# if defined ROW_SIZE
# define COL_SIZE ROW_SIZE
# elif ! defined BUFFER_SIZE
# define COL_SIZE 128
# else
# define COL_SIZE BUFFER_SIZE
# endif
Chỉ thị điều khiển dòng (#line) khi trình biên dịch phát hiện ra lỗi trong quá trình biên dịch,
nó sẽ hiển thị thông báo lỗi với tham chiếu đến dòng lệnh gây lỗi trong file. Chỉ thị #line sẽ giúp
ta điều khiển thông tin này.
Ví dụ 2.25: Minh hoạ chỉ thị điều khiển dòng
int main () { // Dong 1
# line 1 " main . cpp : error detected " // Dong 2
int a = 10; // Dong 3
int b 20; // Dong 4
return 0; // Dong 5
} // Dong 6
Trong ví dụ trên ta thấy lỗi xảy ra tại dòng 4, chương trình sẽ hiện câu thông báo lỗi như định
nghĩa trong chỉ thị #line và vị trí dòng có lỗi là 2 (do ta cung cấp giá trị 1 cho chỉ thị #line, giá
trị 1 nay sẽ được gán cho dòng tiếp theo sau nó như là vị trí tham chiếu, dòng xảy ra lỗi sẽ ở vị trí
tham chiếu số 2).
Chỉ thị lỗi (#error) dùng để thoát khỏi quá trình biên dịch khi nó được tìm thấy.
Ví dụ 2.26: Minh hoạ chỉ thị lỗi
# ifndef __cplusplus
# error A C ++ compiler is required !
# endif
Trình biên dịch sẽ in ra báo lỗi và thoát khỏi quá trình biên dịch nếu như macro __cplusplus
không đươc định nghĩa (macro này được định nghĩa một cách mặc định bởi tất cả các trình biên
dịch C++).
Chỉ thị tùy biến (#pragma) cho trình biên dịch biết cách dịch chương trình theo một số “tùy
chọn” đặc biệt (tùy thuộc vào từng nền tảng hệ điều hành và trình biên dịch). Nếu trình biên dịch
không hỗ trợ các tùy chọn đó nó sẽ bỏ qua (không gây lỗi biên dịch). Dưới đây là một số ví dụ cho
chỉ thị #pragma
Ví dụ 2.27: Minh hoạ chỉ thị lỗi
Báo cho trình biên dịch include các file header 1 lần duy nhất, cho chúng được khai báo bao nhiêu
lần đi nữa
# pragma once
Loại bỏ cảnh báo (số 4018) khi biên dịch
# pragma warning ( disable : 4018 )
Thêm các thư viện vào danh sách các thư viện phụ thuộc (dependency) khi biên dịch
27 CHƯƠNG 2. MỘT SỐ KHÁI NIỆM C++ CƠ BẢN
Để gọi hàm với không gian tên tương ứng ta sử dụng kí hiệu ‘::’
int main ()
{
// Goi ham voi khong gian ten thu nhat
first :: func () ;
// Goi ham voi khong gian ten thu hai
second :: func () ;
return 0;
}
Để có thể truy xuất trực tiếp mà không cần đặt thông qua không gian tên, ta sử dụng từ khóa
using
int main ()
{
using namespace first ;
// Goi ham voi khong gian ten thu nhat
func () ;
// Goi ham voi khong gian ten thu hai
second :: func () ;
return 0;
}
Chúng ta cũng có thể định nghĩa lại các không gian tên đã khai báo, chẳng hạn như
namespace dau_tien = first ;
2.9. Thư viện chuẩn 28
Tất cả định nghĩa của các lớp, đối tượng và hàm của thư viện chuẩn (STL) đều được định nghĩa
trong không gian tên std, chẳng hạn như các lệnh cin và cout mà ta nói ở trên, nếu viết đầy đủ
thì sẽ là
std :: cout << " Nhap ten cua ban : " ;
std :: cin >> ten ;
std :: cout << " Xin chao " << ten << " ! " << std :: endl ;
hoặc ta cũng có thể lược đi bằng cách sử dụng từ khóa using
using namespace std ;
cout << " Nhap ten cua ban : " ;
cin >> ten ;
cout << " Xin chao " << ten << " ! " << endl ;
Các thư viện chuẩn C được đưa vào trong bộ thư viện chuẩn C++ STL với kí tự ’c’ ở đầu
# include < cstring > // thu vien chuan C ++ cua cac ham C
Chương 3
Cấu trúc điều khiển
Khi xây dựng chương trình, đôi lúc ta cần thực hiện một nhóm các lệnh tương ứng với một điều
kiện nào đó hay là lặp lại nhóm lệnh một số lần nhất định. Trong trường hợp đó, ta sẽ sử dụng cấu
trúc điều khiển. Nhóm các lệnh (nhiều hơn 1) sẽ được gộp lại với nhau bằng cặp ngoặc nhọn { }
Ngoài ra, chúng ta cũng có thể sử dụng nhiều khối lệnh if...else... liên tiếp nhau.
Ví dụ 3.2: Thông báo học lực của học sinh
float diem = 6.5;
if ( diem >= 8.0) {
std :: cout << " Hoc sinh gioi " << std :: endl ;
} else if ( diem >= 6.5) {
std :: cout << " Hoc sinh kha " << std :: endl ;
} else if ( diem >= 5.0) {
29
3.2. Cấu trúc lựa chọn switch 30
std :: cout << " Hoc sinh trung binh " << std :: endl ;
} else {
std :: cout << " Hoc sinh kem " << std :: endl ;
}
int x = 5;
switch ( x %2) {
case 0:
std :: cout << " So chan " << std :: endl ;
break ;
case 1:
std :: cout << " So le " << std :: endl ;
break ;
default :
std :: cout << " Khong xac dinh " << std :: endl ;
}
Trong trường hợp chúng ta muốn tạo ra một vòng lặp vô hạn với for, chúng ta có thể viết như sau
(lưu ý là trong cặp ngoặc tròn luôn tồn tại hai dấu ‘;’ cho dù có biểu thức hay không)
for (;;) {
// cac lenh trong vong lap
}
Với trường hợp sử dụng vòng lặp vô hạn, chúng ta cần thêm vào bên trong vòng lặp ít nhất một
điều kiện để thoát (ngưng) vòng lặp (xem Phần 3.5), nếu không vòng lặp sẽ không thể ngừng và
gây ra lỗi cho chương trình.
Theo chuẩn C++11, chúng ta còn có thể tạo vòng lặp for theo danh sách
for ( int i : {1 , 3 , 6 , 9}) {
// cac lenh trong vong lap
}
33 CHƯƠNG 3. CẤU TRÚC ĐIỀU KHIỂN
Trong trường hợp chúng ta muốn tạo ra một vòng lặp vô hạn với while, chúng ta có thể gán cho
giá trị điều kiện là 1
3.5. Các lệnh cho vòng lặp 34
while (1) {
// cac lenh trong vong lap
}
hoặc true
while ( true ) {
// cac lenh trong vong lap
}
Cũng tương tự như với trường hợp vòng lặp vô hạn for, chúng ta cần thêm vào bên trong vòng
lặp một điều kiện để thoát (xem Phần 3.5).
Trong chương trước, chúng ta đã làm quen với các kiểu dữ liệu cơ bản của một biến (phần tử).
Tiếp theo sau, chương này được dùng để trình bày các vấn đề liên quan đến các thao tác xử lý một
tập hợp các phần tử có cùng kiểu dữ liệu, tập hợp này được gọi là mảng. Trong trường hợp mảng
chỉ chứa toàn dữ liệu dạng kí tự, nó được gọi là một chuỗi (kí tự).
4.1 Mảng
Mảng (array) là một dãy các phần tử có cùng kiểu được đặt liên tiếp trong bộ nhớ và có thể truy
xuất đến từng phần tử bằng cách thêm một chỉ số vào sau tên của mảng.
Hình 4.1 minh hoạ cấu trúc cơ bản của một mảng. Lưu ý rằng chỉ số của phần tử đầu tiên trong
mảng luôn là 0, nếu mảng có kích thước là N thì các phần tử sẽ có chỉ số từ 0 đến (N−1).
Hình 4.1: Minh hoạ cấu trúc cơ bản của mang[6] chứa các phần tử là số nguyên. Mảng này gồm
có 6 phần tử, mỗi phần tử trong mảng chứa một giá trị nguyên khác nhau, phần tử đầu tiên có chỉ
số là 0, các phần tử kế tiếp có chỉ số từ 1 đến 5.
37
4.1. Mảng 38
Ta cũng có thể khởi tạo giá trị của các phần tử trong mảng ngay khi khai báo bằng những cách
sau
int A [10] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10};
int A [10] = {} // tat ca cac phan tu deu bang 0
int A [10] = {1 , 2} // hai phan tu dau mang gia tri 1 va 2 , cac phan tu sau
deu bang 0
int A [] = {1 , 2} // mang co kich thuoc la 2
int main ()
{
float A [3];
A [0] = 1.5;
A [1] = -2.9;
A [2] = 3.0;
return 0;
}
Với vòng lặp for, chúng ta có thể lặp theo khoảng giá trị (range-based loop) đối với mảng
float A [3];
A [0] = 1.5;
A [1] = -2.9;
A [2] = 3.0;
for ( auto n : A )
std :: cout << n << std :: endl ;
Dạng đơn giản nhất của mảng nhiều chiều là mảng hai chiều, thực chất đây là một dãy các mảng
một chiều. Chúng ta cũng có thể xem mảng hai chiều như là một bảng có các hàng và cột (xem
Hình 4.2).
39 CHƯƠNG 4. MẢNG VÀ CHUỖI
Hình 4.2: Minh hoạ cấu trúc cơ bản của mộ mảng 2 chiều A[3][4] chứa các phần tử là số nguyên,
chỉ số đầu tương ứng với chỉ số dòng, chỉ số sau tương ứng với chỉ số cột.
Ta có thể khởi tạo giá trị cho mảng 2 chiều với một trong những cách sau
int A [3][4] = {
{2 , -1 , 3 , 14} ,
{4 , 51 , 26 , 73} ,
{ -48 , 39 , 100 , 11}
};
int A [][4] = { // co the bo qua kich thuoc dau tien cua mang khi khai bao
{2 , -1 , 3 , 14} ,
{4 , 51 , 26 , 73} ,
{ -48 , 39 , 100 , 11}
};
int A [3][4] = {0}; // mang gom cac phan tu co gia tri bang 0
Để truy cập tới từng phần tử trong mảng 2 chiều, chúng ta có thể sử dụng vòng lặp for để duyệt
qua các chỉ số dòng và cột của mảng
for ( int row = 0; row < Nrows ; ++ row ) // duyet qua cac chi so dong
for ( int col = 0; col < Ncols ; ++ col ) // duyet qua cac chi so cot
A [ row ][ col ] = ....
hoặc áp dụng cách lặp theo khoảng
for ( auto row : A ) // duyet qua cac dong trong mang
for ( auto element : row ) // duyet qua cac phan tu trong 1 dong
element = ....
int main ()
{
// khoi tao mang 2 chieu
int A [3][4] = {
{2 , -1 , 3 , 14} ,
{4 , 51 , 26 , 73} ,
{ -48 , 39 , 100 , 11}
};
}
}
return 0;
}
4.2 Chuỗi kí tự
Chuỗi (string) là một mảng (dãy) bao gồm các kí tự (character ). Trong C++ có hai dạng cuỗi kí tự
• Chuỗi kí tự theo kiểu C (C-style character string)
• Chuỗi kí tự với lớp string trong C++
Nội dung của chuỗi kí tự luôn được kết thúc với kí tự null (\0).
hoặc
char ten [] = " Phuong " ;
Lưu ý rằng việc gán nhiều hằng cho các phần tử trong mảng chỉ hợp lệ khi khởi tạo mảng. Thao
tác gán chỉ có thể được thực hiện với tứng phần tử một trong khi thực thi chương trình.
Một cách khác để gán giá trị cho một chuỗi kí tự là sử dụng trực tiếp dòng nhập dữ liệu (cin)
cin.getline (char buffer[], int length, char delimiter = ‘\n’)
Trong đó buffer là bộ đệm (mảng), length là độ dài cực đại của mảng và delimiter là kí tự được
dùng để kết thúc việc nhập (mặc định là \n).
char ten [100];
std :: cout << " Nhap ten : " ;
std :: cin . getline ( ten ,100) ;
Trong trường hợp chỉ muốn nhận các từ đơn (không có khoảng trắng ) thay vì cả câu, ta có thể
sử dụng
std :: cin >> ten ;
41 CHƯƠNG 4. MẢNG VÀ CHUỖI
Khi ta có chuỗi kí tự là một số (vd: “1234”), ta có thể chuyển đổi chuỗi này sang số bằng cách sử
dụng các hàm có trong thư viện cstdlib (stdlib.h)
• atoi: chuyển chuỗi thành kiểu int
• atol: chuyển chuỗi thành kiểu long
• atof: chuyển chuỗi thành kiểu float
Ví dụ 4.6: Nhập chuỗi kí tự tương ứng với giá tiền và chuyển thành số
char mybuffer [100];
float price ;
std :: cout << " Enter price : " ;
std :: cin . getline ( mybuffer ,100) ;
price = std :: atof ( mybuffer ) ;
Một số hàm khác trong thư viện cstring (string.h) thường hay được sử dụng để thao tác trên
chuỗi bên cạnh hàm strcpy() gồm có
• strcat(): gắn thêm chuỗi string2 vào phía cuối của chuỗi string1 và trả về string
char* strcat (char* string1, const char* string2)
• strcmp(): so sánh hai chuỗi string1 và string2, trả về 0 nếu hai chuỗi là bằng nhau
int strcmp (const char* string1, const char* string2)
• strlen(): Trả về độ dài của chuỗi
size_t strlen (const char* string)
Ví dụ 4.7: Minh hoạ các thao tác trên chuỗi
# include < iostream >
# include < cstring >
int main ()
{
char string1 [20] = " Hello " ;
char string2 [20] = " World " ;
char string3 [20];
int length ;
return 0;
}
4.2. Chuỗi kí tự 42
Ta cũng có thể sử dụng phương thức assign() (dùng được với cả string lẫn char*)
std :: string str1 = " Hello World " ;
str . assign ( str1 ) ;
int main ()
{
std :: string str ;
std :: string base = " Ngon ngu lap trinh C ++ " ;
return 0;
}
Cách chuyển đổi giữa string (chuỗi kí tự theo chuẩn C++) và char* (chuỗi kí tự theo chuẩn C)
như sau
std :: string str = " Hello " ;
const char * c = str . c_str () ; // chuyen tu string thanh char *
std :: string str1 ( c ) ; // chuyen tu char * thanh string
• Độ dài chuỗi
int len = str3 . size () ; // kich thuoc chuoi
std :: cout << " Do dai duoi str3 : " << len << std :: endl ;
• So sánh chuỗi
std :: string str1 ( " mau do " ) ;
std :: string str2 ( " mau xanh " ) ;
if ( str1 . compare ( str2 ) !=0)
std :: cout << " Hai chuoi khong giong nhau " << std :: endl ;
Lưu ý: size_t là kiểu số nguyên không dấu của giá trị trả về của toán tử sizeof. Trong
trường hợp này ta có thể thay thế bằng khai báo kiểu int, tuy nhiên điểm khác biệt giữa
hai kiểu này nằm ở điều kiện của kiểu size_t là phải có kích thước (miền giá trị) đủ lớn để
chứa đối tượng có kích thước lớn nhất mà hệ thống có thể xử lý được (ví dụ một mảng tĩnh
có kích thước 8GB), trong khi kiểu int thi không nhất thiết.
int main ()
{
std :: string string1 = " Hello " ;
std :: string string2 = " World " ;
std :: string string3 ;
int length ;
return 0;
}
Mảng chứa các chuỗi kí tự được khai báo tương tự như các mảng thông thường, ví dụ
std :: string str [3] = { " Hello " , " ABC " , " 123 " };
Để truy cập tới tất cả các phần tử trong mảng, ta có thể sử dụng các vòng lặp, chẳng hạn như
for ( i = 0; i < 3; ++ i )
std :: cout << str [ i ] << std :: endl ;
hoặc
for ( auto i : str )
std :: cout << i << std :: endl ;
45 CHƯƠNG 4. MẢNG VÀ CHUỖI
Trong C++, hàm là một khối lệnh dùng để thực thi một tác vụ nào đó và trả kết quả về khi được
gọi. Việc sử dụng hàm sẽ giúp cho chương trình của chúng ta được gọn gàng, rõ ràng và dễ sửa lỗi
hơn, đặc biệt đối với các chương trình sử dụng một tác vụ nào đó nhiều hơn một lần.
int main ()
{
int x = 5 , y = 6;
std :: cout << " Tong cua hai so x va y la : " << tong (x , y ) << std :: endl ;
return 0;
}
Trong ví dụ này, chúng ta khai báo hai hàm tong() và main(). Đầu tiên chương trình sẽ thực thi
hàm main() (mặc định), khi thực hiện đến lời gọi hàm tong(x,y) chương trình sẽ thực thị hàm
47
5.1. Khai báo hàm 48
tong() với các đối số x và y tương ứng được truyền cho hàm. Giá trị tổng được trả về thông qua
lệnh return (x+y);.
Ví dụ 5.2: Khai báo hàm tính tổng hai số nguyên nhưng không trả về giá trị
# include < iostream >
int main ()
{
int x = 5 , y = 6;
tong (x , y ) ;
return 0;
}
int main ()
{
int x = 5 , y = 6;
std :: cout << " Tong cua hai so x va y la : " << tong (x , y ) << std :: endl ;
return 0;
}
liệu trả về của hàm. Khai báo nguyên mẫu không bao gồm phần thân hàm và được kết thúc bằng
dấu ‘;’.
Ví dụ 5.4: Khai báo nguyên mẫu cho hàm tính tổng hai số nguyên
# include < iostream >
void main ()
{
int x = 5 , y = 6;
cout << " Tong cua hai so x va y la : " << tong (x , y ) << endl ;
}
Khi gọi hàm, ta không nhất thiết phải khai báo đầy đủ tất cả các đối số. Ví dụ như hàm tong(x,y)
có thể được gọi chỉ với 1 đối số tong(2), khi đó giá trị trả về sẽ là 3 (x = 2 được truyền cho hàm
và y = 1 được thiết lập mặc định).
Ví dụ 5.5: Khai báo giá trị mặc định cho đối số của hàm tính tổng hai số nguyên
# include < iostream >
void main ()
{
int x = 5 , y = 6;
tong (x , y ) ; // 11
tong ( x ) ; // 4
tong ( y ) ; // 5
}
đó bất kì sự thay đổi nào của đối số đó bên trong hàm sẽ ảnh hưởng trực tiếp lên biến. Để khai
báo đối số theo dạng tham chiếu, ta sử dụng kí tự ‘&’ đặt trước tên biến được truyền.
Ví dụ 5.6: Hàm hoán đổi giá trị hai biến
# include < iostream >
int main ()
{
int x = 1 , y = 4;
hoandoi (x , y ) ;
std :: cout << " x = " << x << " , y = " << y << std :: endl ;
return 0;
}
Ngoài ra còn một cách truyền đối số khác đó là truyền dưới dạng tham biến (by variable) hay còn
gọi là truyền bằng con trỏ (by pointer ). Cách truyền này được thực hiện bằng cách sử dụng kí tự
‘*’ đặt trước tên biến (sẽ bàn kĩ hơn trong Phần 6.5.1).
int hoandoi ( int *x , int * y )
{
int tmp = * x ;
*x = *y;
* y = tmp ;
}
Trong trường hợp muốn truyền tham số là mảng cho hàm thì chúng ta cần phải chỉ định trong
phần đối số kiểu dữ liệu cơ bản của mảng, tên mảng và cặp ngoặc vuông (trống hoặc có giá trị kích
thước mảng) khi khai báo đối số của hàm.
void function ( int arg []) // mang 1 chieu
void function ( int arg [10]) // mang 1 chieu
void function ( int arg [][3][4]) // mang nhieu chieu
void function ( int arg [10][3][4]) // mang nhieu chieu
int main ()
{
int a [5] = {1 , -2 , 3 , -4 , 5};
int s = tong (a ,5) ;
std :: cout << " Gia tri tong la : " << s << std :: endl ;
51 CHƯƠNG 5. HÀM
return 0;
}
int main ()
{
int n ;
std :: cout << " Nhap mot so nguyen : " ;
std :: cin >> n ;
std :: cout << n << " ! = " << giaithua ( n ) << std :: endl ;
return 0;
}
Cách này thường được dùng cho các hàm thực thi thường xuyên nhằm loại bỏ thời gian quá dụng
(overhead ) khi gọi hàm, tuy nhiên đôi khi cũng gây ra vấn đề khi biên dịch nếu số lượng hàm nội
tuyến quá lớn.
Ví dụ 5.9: Tìm giá trị lớn nhất giữa hai số nguyên
# include < iostream >
int main ()
{
std :: cout << " Gia tri lon nhat cua (2 , -1) : " << max (2 , -1) << std :: endl ;
std :: cout << " Gia tri lon nhat cua (5 ,10) : " << max (5 ,10) << std :: endl ;
return 0;
5.5. Kĩ thuật nạp chồng 52
Trong C++ ta có thể xây dựng các hàm hoặc toán tử có cùng tên nhưng khác nhau về nội dung
hoặc chức năng thực hiện nhiệm vụ, cách thức này được gọi là nạp chồng hay tái định nghĩa
(overloading).
Khi chúng ta gọi các hàm hay toán tử có cùng tên với nhau, trình biên dịch sẽ xác định hàm (toán
tử) nào là phù hợp nhất bằng cách so sánh các đối số được truyền cho hàm (toán tử) với các kiểu
đối số được khai báo trong phần định nghĩa hàm (toán tử).
Ta có thể định nghĩa nhiều hàm có cùng tên với nhau, nhưng phải có các đối số khác nhau (về kiểu
dữ liệu hay số lượng đối số)1 . Phương pháp này được gọi là phương pháp nạp chồng hàm (function
overloading).
int main ()
{
int a = 2 , b = 3 , c = -1;
float d = 0.5 , e = -1.2;
std :: cout << " Tong cua hai so nguyen : " << tong (a , b ) << std :: endl ;
std :: cout << " Tong cua ba so nguyen : " << tong (a ,b , c ) << std :: endl ;
std :: cout << " Tong cua hai so thuc : " << tong (d , e ) << std :: endl ;
return 0;
}
1
Còn một khái niệm tái định nghĩa (hay nạp chồng) hàm khác nữa gọi là overriding, điểm khác biệt giữa hai khái
niệm này là overloading sử dụng cùng một tên hàm nhưng khác nhau các đối số, còn overriding có cùng tên hàm
lẫn đối số tuy nhiên nội dung hàm lại khác. Do đó, overriding thường được sử dụng trong việc tái định nghĩa các
phương thức của lớp dẫn xuất so với lớp cơ sở (xem phần ??)
53 CHƯƠNG 5. HÀM
Nạp chồng toán tử (operator overloading) cho phép định nghĩa lại một toán tử (ví dụ như các toán
tử +, −, *, /,...) cho một kiểu dữ liệu được định nghĩa bởi người dùng.
Ví dụ 5.11: Định nghĩa phép cộng phân số
# include < iostream >
int main ()
{
Phanso a , b , c ;
a . tuso = -1; a . mauso = 2;
b . tuso = 5; b . mauso = 3;
c = a + b ; // thuc hien phep cong phan so
std :: cout << " Tong cua hai phan so la : " << c . tuso << " / " << c . mauso <<
std :: endl ;
return 0;
}
int main ()
{
auto func = [] () { std :: cout << " Hello world " << std :: endl ; };
func () ; // goi ham
return 0;
}
2
Còn gọi là biểu thức lambda (lambda expression)
3
Xem Phần ??
5.6. Các hàm lambda 54
Trong ví dụ trên, ta thấy có xuất hiện cặp ngoặc vuông [], đó là cặp ngoặc capture specification,
nó cho phép một hàm lambda có thể sử dụng các biến ngoài theo cách mà một hàm bình thường
tác động lên các biến nằm trong phạm vi hoạt động của nó. Đồng thời đây cũng là dấu hiệu của
hàm lambda, báo cho trình biên dịch biết là ta đang khai báo một hàm lambda. Cặp ngoặc tròn ()
theo sau cung cấp danh sách các đối số, do ta không trả về bất cứ kiểu dữ liệu nào nên sẽ không
có dấu ->, và cuối dùng là phần thân hàm (xuất ra dòng "Hello world").
Ví dụ 5.13: Hàm lambda tính tổng hai số nguyện
# include < iostream >
int main ()
{
auto func = []( int x , int y ) { return x + y ; }; // tinh tong hai so
int result = func (2 ,3) ;
std :: cout << result << std :: endl ;
return 0;
}
int main ()
{
int result = []( int x , int y ) -> int { return x + y ; }(2 ,4) ;
std :: cout << result << std :: endl ;
return 0;
}
Ta cũng có thể sử dụng hàm lambda bên trong một hàm lambda khác.
Ví dụ 5.15: Minh hoạ sử dụng lambda bên trong hàm lambda khác
# include < iostream >
int main ()
{
int result = []( int m , int n ) -> int { return m + n ; } ([]( int a ) -> int
{ return a ; }(2) ,[]( int a ) -> int { return a ; }(3) ) ;
std :: cout << result << std :: endl ;
return 0;
}
Một trong những ưu điểm của hàm lambda là nó giảm bớt việc định nghĩa các lớp hoặc hàm, đặc
biệt khi dùng với các algorithm trong thư viện chuẩn STL (xem Phần 9.6).
Ví dụ 5.16: Định nghĩa hàm max() trả về giá trị lớn nhất trong hai số nguyên
# include < iostream >
int main ()
{
const int N = 5;
int a [ N ] = {1 , 3 , -3 , 0 , 9};
return 0;
}
Ta có thể bỏ qua định nghĩa hàm max() khi sử dụng hàm lambda
# include < iostream >
int main ()
{
const int N = 5;
int a [ N ] = {1 , 3 , -3 , 0 , 9};
return 0;
}
5.6. Các hàm lambda 56
Chương 6
Con trỏ
Như chúng ta đã biết, bộ nhớ máy tính có thể xem như là một dãy gồm các ô nhớ (có kích thước
1 byte), mỗi ô có một địa chỉ xác định. Các biến chính là các ô nhớ mà chúng ta có thể truy xuất
dưới các tên. Khi chúng ta khai báo một biến thì nó phải được lưu trữ trong một vị trí cụ thể trong
bộ nhớ, vị trí của biến được lưu trữ sẽ do trình biên dịch và hệ điều hành quyết định. Để có thể
biết được chính xác vị trí (hay địa chỉ) của biến đó ở đầu trong bộ nhớ, chúng ta cần sử dụng một
công cụ hỗ trợ, đó chính là con trỏ.
Ta cũng có thể khởi tạo con trỏ ngay khi định nghĩa
57
6.1. Thao tác với con trỏ 58
Hình 6.1: Minh hoạ con trỏ: biến a có địa chỉ trong bộ nhớ là 1011 và có giá trị là 3, biến con trỏ
p có giá trị là địa chỉ của biến a.
double a = 5.43;
double * p = & a ;
Ngoài ra, chuẩn C++11 có định nghĩa một từ khóa mới là nullptr có kiểu là nullptr_t tương
thích với tất cả các con trỏ và kiểu bool nhưng không tương thích với các kiểu cơ bản như int,
long, double,...
Ví dụ 6.4: Minh hoạ nullptr
void f ( int ) ; // ham thu 1
void f ( char *) ; // ham thu 2
// Chuan C ++ cu
f (0) ; // 0 la kieu int hay con tro null ? Ham nao duoc goi ?
// Chuan C ++11
f ( nullptr ) // con tro null , goi ham thu 2
int n = 10;
1
Cần phân biệt với con trỏ void (xem Phần 6.1.3).
2
NULL có kiểu int và giá trị là 0. Con trỏ NULL được định nghĩa trong nhiều thư viện, chẳng hạn như thư viện
iostream.
59 CHƯƠNG 6. CON TRỎ
float x = -2.;
Tuy nhiên, do con trỏ void không biết kiểu dữ liệu mà nó trỏ tới nên ta không thể lấy nội dung của
địa chỉ mà nó trỏ tới (dereference) một cách trực tiếp được. Vì vậy, ta cần phải chuyển đổi kiểu
trước khi sử dụng
# include < iostream >
int main ()
{
int n = 10;
void * vp = & n ;
int * ip = ( int *) vp ; // chuyen doi kieu ( void *) thanh ( int *)
int main ()
{
int a [] = {1 , 10 , 100};
int * p ;
// gan dia chi cua phan tu dau tien cho con tro
p = & a [0];
int i = 0;
while ( p <= & a [2]) { // thuc hien phep so sanh de kiem tra xem toi vi
tri cuoi cung trong mang hay chua
6.2. Con trỏ và tham chiếu 60
• Không (nên) có tham chiếu NULL, một tham chiếu phải luôn chỉ đến một đối tượng nào đó.
Tuỳ trường hợp trình biên dịch có thể báo lỗi hoặc không.
int * p ; // con tro null ( OK )
int & r = * p ; // tham chieu den con tro null ( WARN )
• Đối với con trỏ, ta cần phải kiểm tra nó có phải là con trỏ NULL hay không trước khi sử
dụng, còn tham chiếu thì không có tham chiếu NULL nào được tạo ra do đó nếu có tham
chiếu thì ta có thể sử dụng nó mà không cần kiểm tra
void f ( float * p ) {
if ( p ) { // kiem tra con tro null
std :: cout << * p ;
}
}
• Con trỏ có thể được chỉ định để truy cập đến nhiều đối tượng khác nhau, còn một tham chiếu
thì chỉ truy cập đến đối tượng đã được khởi tạo cho nó.
int a = 10 , b = 5;
int & r = a ; // r tham chieu den a
int * p = a ; // p tro den a
r = b; // r van tham chieu den a
p = &b; // p tro den b
Ví dụ 6.10: Khai báo một mảng chứa biến kiểu số nguyên, địa chỉ của con trỏ trỏ tới cũng chính
là địa chỉ của phần tử đầu tiên trong mảng
# include < iostream >
int main ()
{
int n [] = {1 , 10 , 100};
std :: cout << " Dia chi phan tu dau tien : " << & n [0] << std :: endl ;
std :: cout << " Dia chi cua mang : " << n << std :: endl ;
}
4
Để chuyển từ con trỏ hằng thành con trỏ thường ta sử dụng const_cast (xem Phần )
6.4. Con trỏ và mảng 62
Tuy nhiên con trỏ và mảng không phải hoàn toàn có thể hoán đổi cho nhau được, chẳng hạn như
int a [10];
int * p = a ; // lenh gan nguoc lai a = p la sai vi a [0] la hang con tro
Chúng ta cũng có thể gán giá trị cho mảng thông qua con trỏ
a [5] = 10; // phan tu thu 5 cua mang a co gia tri la 10
*( a +5) = 10; // tuong duong a [5] = 10 ( a cung la con tro )
int main ()
{
int a [] = {10 , 100 , 200};
return 0;
}
Ngoài ra, chúng ta cũng có thể xem mảng N chiều như là một mảng một chiều chứa các con trỏ
trỏ tới các mảng (N−1) chiều
int ** p ; // mang 2 chieu
float * a [5]; // mang 2 chieu co 5 dong
int main ()
{
int a [] = {0 , 1 , 10 , 100};
int * p []= {a , a +1 , a +2 , a +3}; // mang con tro p [] chua cac dia chi cua
mang a []
for ( int i = 0; i < 4; ++ i ) {
std :: cout << * p [ i ] << std :: endl ; // xuat ra noi dung cua mang a []
}
return 0;
}
63 CHƯƠNG 6. CON TRỎ
int main ()
{
int x = 1 , y = 4;
hoandoi (& x ,& y ) ; // truyen dia chi cua bien
std :: cout << " x = " << x << " , y = " << y << std :: endl ;
return 0;
}
Chúng ta cũng có thể truyền đối số là mảng cho hàm dưới dạng con trỏ.
Ví dụ 6.14: Tính giá trị trung bình của mảng
# include < iostream >
int main ()
{
float a [] = { -1.0 , 2.4 , 10.8 , -4.1};
float tb = trungbinh (a ,4) ;
std :: cout << " Gia tri trung binh cua mang la : " << tb << std :: endl ;
return 0;
}
int main ()
{
float mang [] = { -1.0 , 2.4 , 10.8 , -4.1};
float * mang1 = saochep ( mang ) ;
for ( int i = 0; i < 4; ++ i ) {
std :: cout << " Phan tu " << i +1 << " la : " << mang1 [ i ] << std :: endl ;
}
return 0;
}
int (* tru ) ( int , int ) = hieu ; // con tro ham cua ham hieu ()
int main ()
{
int x = 10 , y = 3;
int z = tinh (x , y , & tong ) ;
std :: cout << " Tong cua hai so la : " << z << std :: endl ;
z = tinh (x , y , tru ) ;
std :: cout << " Hieu cua hai so la : " << z << std :: endl ;
return 0;
}
65 CHƯƠNG 6. CON TRỎ
Trong ví dụ trên, chúng ta xây dựng một hàm tinh() để tính tổng hoặc hiệu của hai số nguyên x
và y. Hàm tinh() này sẽ gọi giá trị của hai biến x,y và đồng thời cũng gọi con trỏ của các hàm
tong() và hieu() tương ứng. Việc truyền con trỏ cho hàm tinh() có thể được thực hiện bằng
cách truyền địa chỉ của hàm (vd: &tong) hoặc tạo một con trỏ cho hàm ((*tru)(int,int)= hieu)
và gọi con trỏ này.
Tất cả các biến cấp phát động mới đều được đặt trong vùng nhớ free store. Trong trường hợp
hệ điều hành hết bộ nhớ để cấp phát, một con trỏ null (0) sẽ được trả về
int * p = new int ;
if ( p == NULL ) {
std :: cout << " Khong du bo nho " << std :: endl ;
}
• Toán tử delete được dùng để giải phóng bộ nhớ được cấp phát khi nó không cần dùng đến
nữa, có cú pháp như sau
delete <con trỏ>
hoặc delete [] <con trỏ>
delete p ;
Ví dụ 6.17: Cấp phát và giải phóng bộ nhớ cho mảng với new/delete
# include < iostream >
int main ()
{
int n ;
int * a ;
return 0;
}
• Hàm free giải phóng một khối nhớ động đã được cấp phát
free ( p ) ;
Lưu ý: khi sử dụng, các toán tử new/delete sẽ gọi các hàm khởi tạo và hủy của kiểu dữ liệu, còn
các hàm malloc/free thì không. Ngoài ra không nên trộn lẫn các cách cấp phát và thu hồi bộ nhớ
với nhau (ví dụ như new dùng với free() hay malloc() dùng với delete), điều này có thể gây
nên các lỗi trong quá trình thực thi.
Ví dụ 6.18: Cấp phát và giải phóng bộ nhớ cho mảng với malloc()/free()
# include < iostream >
int main ()
{
int n ;
int * a ;
return 0;
}
(raw pointer ) trong C/C++ 5 nhưng có bổ sung thêm các khả năng như tự động quản lý bộ nhớ,
kiểm tra truy xuất ngoài vùng được cấp phát,. . .
Chức năng quản lý các đối tượng được tạo ra bằng toán tử new, các đối tượng sẽ được tự động xóa
bỏ theo một cách thích hợp nhất (dựa trên cơ chế về quyền sở hữu) và các lập trình viên sẽ không
cần phải quan tâm tới việc quyết định xem cần loại bỏ chúng khi nào và ở đâu trong chương trình
của mình. Nếu không có các smart pointer, chúng ta chỉ có một cách duy nhất để loại bỏ các đối
tượng cấp phát động bằng toán tử delete, ví dụ như
type * p = new type () ;
// p = ...
delete p ;
Nhưng không phải lúc nào chúng ta cũng thực hiện được điều này một cách tường minh: một lệnh
gán con trỏ sai sẽ làm mất địa chỉ của biến đã cấp phát, hoặc chương trình gặp lỗi không mong
muốn trước khi có thể thực hiện lệnh delete, cả hai khả năng đều dẫn tới lỗi memory leak. Với
các smart pointer, công việc dọn dẹp này sẽ được thực hiện một cách tự động.
SmartPointer < type > p ( new type () ) ;;
// p = ...
// ham huy se duoc goi tu dong tuy theo tung loai smart pointer
std :: cout << " * foo : " << * foo << '\ n ';
std :: cout << " * bar : " << * bar << '\ n ';
5
Để đạt được điều này, người ta đã tạo ra các đối tượng để “đóng gói” các con trỏ thông thường, các đối tượng
này còn được gọi là proxy object. Để các đối tượng này hành xử như một con trỏ thật sự thì các toán tử đặc trưng
của con trỏ thông thường như các toán tử tham chiếu * và -> đều được tái định nghĩa lại trong các đối tượng này.
6
Từ chuẩn C++03 trở về trước chỉ có duy nhất một loại smart pointer là auto_ptr.
69 CHƯƠNG 6. CON TRỎ
sp . reset ( new int ) ; // xoa bo doi tuong dang duoc quan ly , tao con tro
moi
* sp =20;
std :: cout << * sp << '\ n ';
Các bạn có thể tìm hiểu thêm các phương thức khác của smart pointer tại:
http://en.cppreference.com/w/cpp/memory/unique_ptr
http://en.cppreference.com/w/cpp/memory/shared_ptr
http://en.cppreference.com/w/cpp/memory/weak_ptr
6.7. Smart pointer 70
Chương 7
Tập tin
Tập tin (file) là một trong những hình thức lưu trữ dữ liệu phổ biến, có hai loại tập tin chính
• Tập tin văn bản (text file): là loại tập tin chỉ lưu trữ thuần túy văn bản, các kí tự được biểu
diễn bằng mã ASCII của nó, và người dùng có thể dễ dàng đọc được nội dung của tập tin
này.
• Tập tin nhị phân (binary file): là loại tập tin chứa các đoạn mã nhị phân, người dùng không
thể đọc được nội dung các tập tin này.
Các thao tác trên tập tin có thể được thực hiện theo chuẩn C (sử dụng các hàm trong thư viện
cstdio) hoặc theo chuẩn C++ (các hàm trong các thư viện dòng xuất nhập iostream).
71
7.1. Thao tác với tập tin theo kiểu C 72
Các bạn có thể tham khảo thêm thông tin tại http://en.cppreference.com/w/cpp/io/c.
int main ()
{
FILE * fp ; // Khai bao con tro FILE *
fp = std :: fopen ( " file . txt " ," w " ) ; // Mo mot file co ten la file . txt de
ghi du lieu
else
std :: cout << " Tap tin da duoc xoa " << std :: endl ;
int main ()
{
FILE * fp ;
float a [100];
fp = std :: fopen ( " so . txt " ," r " ) ;
return 0;
}
int main ()
{
FILE * fp ;
char s [100];
fp = std :: fopen ( " chuoi . txt " ," r " ) ;
std :: fgets (s , 100 , fp ) ; // doc chuoi
std :: cout << s << std :: endl ;
std :: fclose ( fp ) ;
return 0;
}
7.1. Thao tác với tập tin theo kiểu C 74
Bên cạnh việc kiểm tra cuối tập tin bằng hàm feof(), ta cũng có thể sử dụng macro EOF3 .
Ví dụ 7.5: Đọc từng kí tự từ tập tin chuoi.txt
# include < iostream >
# include < cstdio >
int main ()
{
FILE * fp ;
char s [100];
fp = std :: fopen ( " chuoi . txt " ," r " ) ;
int c ; // luu y c co kieu int , khong phai char , de so sanh voi EOF
while ( ( c = std :: fgetc ( fp ) ) != EOF ) {
std :: putchar ( c ) ; // ham ghi ki tu ra man hinh
}
std :: fclose ( fp ) ;
return 0;
}
int main ()
{
FILE * fp ;
float a [] = {1.0 , 2.3 , -2.1 , 4.9};
fp = std :: fopen ( " so . txt " ," w " ) ;
std :: fputs ( " Cac so tu mang :\ n " , fp ) ;
// Ghi ra tap tin
for ( int i = 0; i < 4; ++ i )
std :: fprintf ( fp , " %.2 f \ n " , a [ i ]) ;
std :: fclose ( fp ) ;
return 0;
}
int main ()
{
FILE * fp ;
char c [] = { 'a ' , 'b ' , 'c ' , 'd ' , 'e ' };
fp = std :: fopen ( " kitu " , " wb " ) ;
std :: fwrite ( c , sizeof ( char ) , sizeof ( c ) , fp ) ; // ghi cac ki tu ra tap
tin
std :: fclose ( fp ) ;
return 0;
}
int main ()
{
FILE * fp ;
fp = std :: fopen ( " kitu " , " rb " ) ;
// In ra ket qua
for ( int i = 0; i < size ; ++ i )
std :: cout << c [ i ] << std :: endl ;
delete c ;
return 0;
}
Các hàm fseek() và ftell() cũng được sử dụng để di chuyển và xác định vị trí hiện tại của chúng
ta trong quá trình thao tác với tập tin
int fseek(FILE* fp, long offset, int origin)
long ftell(FILE* fp)
Trong đó, origin là vị trí tham chiếu cho offset, có 3 giá trị chính: SEEK_SET (vị trí đầu tập tin),
SEEK_CUR (vị trí hiện tại) và SEEK_END (vị trí cuối tập tin).
Ví dụ 7.9: Ghi các kí tự và số ra tập tin kitu
# include < iostream >
# include < cstdio >
int main ()
7.2. Thao tác với tập tin theo kiểu C++ 76
{
FILE * fp ;
fp = std :: fopen ( " kitu " , " wb " ) ;
std :: fputs ( " abcdef " , fp ) ; // Ghi chuoi " abcdef " ra tap tin
std :: fseek ( fp , 2 , SEEK_SET ) ; // Di chuyen den vi tri ki tu thu 3 tinh
tu dau tap tin
std :: fputs ( " 123 " , fp ) ; // Ghi de chuoi "123" --> " ab123f "
std :: fclose ( fp ) ;
return 0;
}
4
Được kế thừa từ lớp istream.
5
Được kế thừa từ lớp ostream.
6
Được kế thừa từ lớp iostream.
77 CHƯƠNG 7. TẬP TIN
Việc mở/đóng tập tin được thực hiện thông qua các phương thức open() và close()
void open(const char* filename, ios_base::openmode mode)
void close()
Trong đó, đối số mode mặc định cho đối tượng ifstream là ios_base::in và cho đối tượng
ofstream là ios_base::out. Ngoài ra chúng ta còn có các giá trị khác như
• binary: thao tác với tập tin nhị phân.
• ate (at end ): vị trí đầu ra bắt đầu ở cuối tập tin.
• app (append ): tất cả thao tác đầu ra đều được thực hiện ở cuối tập tin (thêm nội dung vào
cuối tập tin).
• trunc (truncate): tất cả nội dung có sẵn trước đó trong tập tin đều bị xoá bỏ.
Ví dụ 7.10: Minh họa thao tác mở/đóng tập tin
# include < iostream >
# include < fstream >
int main ()
{
std :: ofstream ofs ; // doi tuong dong dau ra ( xuat noi dung ra tap tin )
ofs . open ( " out . txt " , std :: ofstream :: out | std :: ofstream :: app ) ; // mo tap
tin out . txt de xuat du lieu , du lieu duoc them vao cuoi tap tin
ofs . close () ; // dong tap tin
return 0;
}
Chúng ta cũng có thể khai báo tên tập tin ngay khi tạo đối tượng dòng thay vì sử dụng phương
thức open()
std :: ofstream ofs ( " out . txt " ) ;
Ngoài ra, chúng ta còn một số phương thức khác để kiểm tra trạng thái của tập tin:
• good(): trả về true nếu không có bit nào bị lỗi và dòng đã sẵn sàng (is_open()).
• bad(): trả về true nếu có bit bị lỗi.
• fail(): trả về true nếu có bit bị lỗi hoặc tập tin không sử dụng được (invalid ).
• eof(): trả về true nếu con trỏ đã tới cuối tập tin.
Thay vì phương thức good, chúng ta cũng có thể sử dụng cách sau để kiểm tra tập tin
if ( ofs ) // tra ve true neu tap tin good
if (! ofs ) // tra ve true neu tap tin khong good
int main ()
{
std :: ifstream ifs ( " input . txt " ) ;
if ( ifs . good () ) {
std :: cout << " Tap tin duoc mo thanh cong " << std :: endl ;
ifs . close () ;
} else
std :: cout << " Co loi xay ra khi mo tap tin " << std :: endl ;
return 0;
}
int main ()
{
std :: string texts [] = { " Hello " , " ABC " , " 123 " };
std :: ofstream ofs ;
ofs . open ( " out . txt " ) ;
if ( ofs . is_open () ) {
for ( auto text : texts )
ofs << text ;
ofs . close () ;
}
return 0;
}
int main ()
{
string line ;
std :: ifstream ifs ( " input . txt " ) ;
if ( ifs . is_open () ) {
while (! ifs . eof ) { // lap cho den cuoi tap tin
ifs >> line ; // doc chuoi tu tap tin
std :: cout << line ; // xuat chuoi ra man hinh
}
ifs . close () ;
}
return 0;
}
Ngoài ra, chúng ta cũng có thể sử dụng các hàm getline(), get() và put() để đọc một dòng hay
đọc/ghi một kí tự ra tập tin
79 CHƯƠNG 7. TẬP TIN
Ví dụ 7.14: Minh họa đọc một dòng kí tự từ tập tin văn bản
# include < iostream >
# include < fstream >
int main ()
{
string line ;
std :: ifstream ifs ( " input . txt " ) ;
if ( ifs . is_open () ) {
while ( getline ( ifs , line ) ) // lap cho den dong cuoi cua tap tin
std :: cout << line ; // xuat chuoi ra man hinh
ifs . close () ;
}
return 0;
}
int main ()
{
std :: ofstream ofs ( " out . txt " ) ;
if ( ofs . is_open () ) {
ofs . put ( 'a ') ;
ofs . close () ;
}
return 0;
}
int main () {
std :: streampos size ;
char * char_block ;
ifstream ifs ( " input " , ios :: in | ios :: binary | ios :: ate ) ; // mo tap tin
nhi phan , vi tri con tro nam o cuoi tap tin
if ( ifs . is_open () ) {
size = input . tellg () ; // xac dinh kich thuoc tap tin ( vi tri con tro
o cuoi tap tin )
7.2. Thao tác với tập tin theo kiểu C++ 80
Các hàm seekg()/seekp() và tellg()/tellp() được sử dụng để di chuyển và xác định vị trí hiện
tại của chúng ta trong quá trình thao tác với tập tin đầu vào/đầu ra
streampos tellg()
streampos tellp()
istream& seekg (streamoff off, ios_base::seekdir way)
ostream& seekp (streamoff off, ios_base::seekdir way)
Trong đó, way là vị trí tham chiếu cho off (offset), có 3 giá trị chính: ios_base::beg (vị trí đầu
tập tin), ios_base::cur (vị trí hiện tại) và ios_base::end (vị trí cuối tập tin).
Ví dụ 7.17: Thay đổi chuỗi kí tự ghi ra tập tin
# include < iostream >
# include < fstream >
int main ()
{
std :: ofstream outfile ;
outfile . open ( " vidu . txt " ) ;
outfile . close () ;
return 0;
}
Chương 8
Lớp và đối tượng
Bên cạnh các kiểu dữ liệu có sẵn trong C++, người dùng cũng có thể tự mình định nghĩa các kiểu
dữ liệu riêng, đặc biệt là các kiểu dữ liệu có cấu trúc (structured data). Các kiểu dữ liệu có cấu
trúc này giúp người dùng có thể nhóm (group) các thành phần dữ liệu khác nhau vào trong cùng
một kiểu dữ liệu duy nhất.
81
8.2. Dữ liệu có cấu trúc 82
Để truy xuất các phần tử của biến, ta sử dụng dấu ‘.’, ví dụ:
A . tuoi = 20;
Ta cũng có thể định nghĩa các cấu trúc lồng nhau, chẳng hạn như
struct diem {
float toan ;
float anhvan ;
}
struct sinhvien {
char ten [100];
int tuoi ;
diem ketqua ; // bien ketqua thuoc kieu " diem "
}
sinhvien A ;
Ngoài ra, chúng ta cũng có thể sử dụng con trỏ để khai báo biến có cấu trúc
sinhvien * pA = & A ;
Để truy cập phần tử của biến con trỏ ta sử dụng dấu ->
pA - > ketqua . toan
Tuy nhiên điểm khác biệt của union so với struct là tất cả các phần tử của union đều chiếm cùng
một chỗ trong bộ nhớ, kích thước của nó là kích thước của phần tử lớn nhất. Do đó bất kì một sự
thay đổi nào đối với một phần tử sẽ ảnh hưởng tới tất cả các phần tử còn lại.
Enum: kiểu dữ liệu liệt kê dùng để tạo ra các kiểu dữ liệu chứa một cái gì đó hơi đặc biệt một
chút (không phải kiểu số hay kiểu kí tự), cấu trúc của nó như sau
Trên thực tế kiểu dữ liệu liệt kê được dịch là một số nguyên, giá trị tương ứng với phần tử đầu
tiên là 0 và các giá trị tiếp theo cứ thế tăng lên 1. Ví dụ trong kiểu dữ liệu mà chúng ta định nghĩa
ở trên, black tương đương với 0 , blue tương đương với 1 , green tương đương với 2 và cứ tiếp tục
như thế. Ngoài ra, chúng ta có thể chỉ định giá trị tương ứng bằng cách gán
enum colors { black =2 , blue , green , cyan , red , purple , yellow , white };
8.3 Lớp
Lớp (class)1 là một kiểu dữ liệu do người dùng tự định nghĩa, có thể bao gồm trong nó các dữ liệu
và hàm. Cách thức khai báo một lớp như sau
class <tên của lớp> {
<kiểu thuộc tính>:
dữ liệu 1;
dữ liệu 2;
hàm 1;
....
} <biến đối tượng>;
Các thành viên (member ) của lớp được chia làm 2 loại: các thành phần chứa dữ liệu của lớp được
gọi là các thuộc tính (attribute) của lớp, các thành phần này thường là các biến; các thành phần
chỉ hành động của lớp được gọi là các phương thức (method ) của lớp, các thành phần này thường
là các hàm.
Thuộc tính của dữ liệu và hàm có các dạng: private (chỉ được truy xuất bởi các thành viên trong
cùng lớp), protected (có thể được truy xuất bởi các thành viên trong cùng lớp hoặc từ các lớp kế
thừa), hoặc public (có thể được truy xuất từ bên ngoài lớp).
Ví dụ 8.4: Minh hoạ khai báo một lớp
class Sinhvien { // Tao mot lop co ten la Sinhvien
private :
char ten [100];
int tuoi ;
float diemthi ;
public :
Sinhvien () {}; // Constructor
~ Sinhvien () {}; // Destructor
void ketqua () {
if ( diemthi >= 5)
cout << " Dau " << endl ;
else
cout << " Rot " << endl ;
}
};
Hàm khởi tạo (constructor ) của một lớp sẽ được gọi tự động khi khai báo một đối tượng mới thuộc
lớp đó, ngược lại hàm hủy (destructor ) sẽ được gọi tự động khi đối tượng đó kết thúc hoạt động
hoặc được giải phóng khỏi bộ nhớ. Một số đặc tính của hàm khởi tạo và hàm hủy:
• Hàm khởi tạo phải có tên trùng với tên của lớp; hàm hủy có tên bắt đầu bằng ∼ và theo sau
là tên của lớp tương ứng.
1
Trong C++, struct và class là gần như nhau, điểm khác biệt duy nhất giữa chúng là các thành viên (member )
của struct được mặc định là dạng public còn các thành viên của class thì mặc định là private. Tuy nhiên, struct trong
C không có hàm thành viên (member function) và chúng ta cũng không thể khởi tạo giá trị trực tiếp cho các biến
thành viên trong C như trong C++ được.
8.4. Hàm bạn và lớp bạn 84
• Cả hai hàm không có giá trị trả về và đều có thuộc tính public.
• Một lớp có thể có nhiều hàm khởi tạo nhưng chỉ có duy nhất một hàm hủy.
Nếu định nghĩa thuộc tính hoặc phương thức bên ngoài phạm vi định nghĩa lớp, ta phải dùng chỉ
thị phạm vi được thể hiện qua dấu ::
class Sinhvien { // Tao mot lop co ten la Sinhvien
private :
char ten [100];
int tuoi ;
float diemthi ;
public :
Sinhvien () {}; // Constructor
~ Sinhvien () {}; // Destructor
void ketqua () ;
};
Muốn truy xuất các thành viên của đối tượng thuộc một lớp nào đó, ta có thể sử dụng toán tử dấu
. đặt ngay sau đối tượng đó
void main ()
{
Sinhvien A ; // Doi tuong A thuoc lop Sinhvien
A . ten = " Nguyen Van A " ;
A . diemthi = 6.5;
A . ketqua () ;
}
Chú ý: cần phân biệt giữa toán tử dấu chấm (.) dùng để truy cập các thành phần của đối tượng
và toán tử mũi tên (->) dùng để truy cập các thành phần của con trỏ tới đối tượng. Ta có mối
quan hệ như sau
A - > ten = (* A ) . ten
Khi một đối tượng của lớp dẫn xuất được tạo ra thì hàm khởi tạo của lớp cơ sở được áp dụng
trước rồi mới tới hàm khỏi tạo của lớp dẫn xuất, còn khi đối tượng kết thúc thì hàm hủy của lớp
dẫn xuất được áp dụng trước rồi mới tới hàm hủy của lớp cơ sở.
Về mặt dữ liệu, một lớp dẫn xuất bao giờ cũng chứa toàn bộ dữ liệu của lớp cơ sở, do đó ta có thể
thực hiện phép gán một đối tượng thuộc lớp dẫn xuất cho đối tượng thuộc lớp cơ sở (nhưng không
thể theo chiều ngược lại)
Sinhvien A ;
Nghenghiep B ;
B = A ; // dung
A = B ; // sai
Đa kế thừa một lớp có thể được dẫn xuất từ những lớp cơ sở khác nhau với những kiểu dẫn
dẫn xuất khác nhau, cú pháp như sau
class <tên lớp dẫn xuất>: <từ khóa dẫn xuất 1> <tên lớp cơ sở 1>, .... ,
<từ khóa dẫn xuất N> <tên lớp cơ sở N> {
............... nội dung .................
};
Ví dụ 8.6: Minh hoạ đa kế thừa
// Lop Sinhvien la dan xuat cua hai lop Nghenghiep va Truong
class Sinhvien : public Nghenghiep , public Truong {
. . .
}
Hàm khởi tạo và hủy trong đa kế thừa được khai báo tương tự như trong đơn kế thừa, ngoại trừ
việc phải sắp xếp thứ tự gọi hàm của các lớp cơ sở. Thông thường, thứ tự gọi hàm của các lớp cơ
sở nên tuân theo thứ tự dẫn xuất từ các lớp cơ sở trong đa kế thừa.
Hàm thành viên showData() có thể truy cập đến biến thành viên data theo một trong hai cách
sau
87 CHƯƠNG 8. LỚP VÀ ĐỐI TƯỢNG
Lưu ý: con trỏ this không được dùng cho các phương thức tĩnh (static member function)2 trong
lớp.
8.6.2 Hàm ảo
Hàm ảo (virtual function) hay phương thức ảo (virtual method ) là những hàm hoặc phương thức
mà cách thức hoạt động của nó có thể được tái định nghĩa trong các lớp thừa kế. Hàm ảo được
khai báo với từ khóa virtual.
Ví dụ 8.7: Minh hoạ hàm ảo
Giả sử ta có hai lớp Base và Derived như sau
class Base {
public :
const char * getName () { return " lop co so " ; }
};
int main ()
{
Derived c ;
Base & b = c ;
std :: cout << " b la " << b . getName () << std :: endl ;
}
Đó là bởi vì b là một con trỏ của lớp Base do đó nó sẽ gọi hàm Base::getName() mặc dù cho nó
được trỏ tới đối tượng thuộc lớp Derived. Bây giờ nếu ta thêm từ khóa virtual trước phương
thức Base::getName()
class Base {
public :
virtual const char * getName () { return " lop co so " ; }
};
Lúc này phương thức Base::getName() sẽ là phương thức ảo, chương trình sẽ tìm kiếm xem có
lớp dẫn xuất nào của lớp Base tái định nghĩa lại phương thức này hay không. Kết quả thu được sẽ
là
b la lop dan xuat
Nếu phần khai báo hàm ảo kết thúc với = 0, nó được gọi là hàm ảo thuần túy (pure virtual function)
class Base {
...
virtual void f () = 0;
2
Xem trong Phần 10.1.3
8.6. Một số vấn đề thêm về lớp 88
...
}
Các hàm ảo phải được định nghĩa (không chỉ là khai báo) ở trong lớp mà nó xuất hiện lần đầu tiên.
Những lớp dẫn xuất từ lớp đó sẽ tiến hành tái định nghĩa những hàm này nếu cần thiết. Nhưng
nếu ta khai báo chúng như các hàm ảo thuần túy, thì ta không phải định nghĩa chúng trong lớp
mà ta khai báo.
* ptr = len ;
}
int main ( )
{
Line line1 (10) ;
Line line2 = line1 ; // dong thoi cung goi ham khoi tao sao chep
return 0;
}
Bởi vì C++ không biết về lớp mà chúng ta xây dựng nên các hàm khởi tạo sao chép và toán tử gán
mặc định sẽ là shallow copy (hay còn gọi là memberwise copy). Ta cũng có thể thực hiện deep copy
bằng cách xây dựng tường minh hàm khởi tạo sao chép, chẳng hạn như trong ví dụ về hàm khởi
tạo sao chép phía trên
Line :: Line ( const Line & obj ) {
ptr = new int ;
* ptr = * obj . ptr ; // deep copy
}
Thay vì thực hiện lệnh gán bên trong hàm khởi tạo, ta có thể sử dụng danh sách khởi tạo (con-
structor initialization list) như sau
class MyClass {
public :
MyClass ( int x ) : data ( x ) { } // danh sach khoi tao
private :
int data ;
};
Nếu ta có thành viên của lớp có cùng tên với đối số ví dụ như
class MyClass {
public :
MyClass ( std :: string data ) { this - > data = data ; }
private :
std :: string data ;
};
Trong trường hợp thành viên hằng, việc sử dụng danh sách khởi tạo là cần thiết do thành viên này
chỉ được định nghĩa duy nhất 1 lần đầu tiên
class MyClass {
public :
MyClass () : data (10) { }
// chuong trinh se bao loi neu khai bao nhu ben duoi
// MyClass () : { data = 10 }
private :
const int data ;
};
Chương 9
Thư viện chuẩn
Thư viện chuẩn (Standard Template Library − STL) được xây dựng đầu tiên bởi Alexander
Stepanov (1979) với mục đích phát triển phương pháp lập trình tổng quát (generic programming).
Thư viện đầu tiên được xây dựng với ngôn ngữ lập trình Ada bởi Stepanov và Musser năm 1987,
tuy nhiên ngôn ngữ Ada lại không được phát triển mạnh nên họ đã chuyển sang ngôn ngữ C++.
Với sự giúp đỡ của Meng Lee và Andrew Koenig, bộ thư viện chuẩn đã được hoàn thiện và đưa vào
chuẩn ANSI/ISO C++ từ năm 1994. Ngoại trừ lớp string, tất cả các thành phần còn lại của STL
đều dưới dạng các template.
9.1 Template
Template thường được sử dụng trong lập trình tổng quát (generic programming), đây là một từ
khóa của C++ nhằm thông báo cho trình biên dịch biết rằng các hàm, các lớp, hay các xử lý liên
quan sẽ được quyết định sử dụng kiểu dữ liệu nào cho phù hợp trong khi biên dịch. Khi đó trình
biên dịch sẽ để lại các xử lý đó tới khi có dữ liệu thực sự được tạo ra và sẽ tạo ra một khuôn mẫu
chung cho các xử lý. Khi xử lý đó được gọi với những kiểu dữ liệu đã xác định thì trình biên dịch
lúc đó sẽ tự biết dùng xử lý nào với kiểu dữ liệu được truyền vào một cách phù hợp nhất.
Giả sử ta có hai hàm tìm số lớn hơn trong hai số
// Voi kieu int
int maxInt ( int a , int b ) {
return ( a > b ? a : b ) ;
}
Cả hai hàm đều có chung một cách giải quyết vấn đề, chỉ khác ở kiểu dữ liệu được truyền vào và
trả về. Mặc dù ta có thể sử dụng hàm maxDouble() cho cả hai xử lý tuy nhiên kiểu dữ liệu trả về
không được như mong muốn. Để tránh điều đó, ta có thể gom hai kiểu dữ liệu thành 1 kiểu duy
nhất và xây dựng 1 hàm duy nhất
template < class T >
T maxNumber ( T a , T b ) {
return ( a > b ? a : b ) ;
}
91
9.2. Các thành phần chính của STL 92
return 0;
}
Để tránh nhầm lẫn với từ khóa class trong khai báo lớp, ta có thể sử dụng từ khóa typename
template < typename T >
T maxNumber ( T a , T b ) {
return ( a > b ? a : b ) ;
}
• Container : là các đối tượng chứa những đối tượng khác (cấu trúc dữ liệu của template), vd:
vector, set, map,...
• Iterator : giống con trỏ, dùng để truy nhập các phần tử dữ liệu của các container.
• Algorithm: các thuật toán để thao tác dữ liệu như tìm kiếm (find), sắp xếp (sort),...
• Adapter : các bộ thương thích container phục vụ cho những nhu cầu đặc biệt.
• Ultility: các ứng dụng của STL, gồm có hai phần: các ứng dụng cho ngôn ngữ (lập trình) và
ứng dụng đa mục đích.
• Numeric: bộ các hàm và kiểu dữ liệu phục vụ cho việc tính toán, xử lý các mảng số.
• Stream: các lớp hỗ trợ các phép toán cho dòng xuất nhập.
• ...
Lưu ý: các file thư viện chuẩn của C++ đều không có phần mở rộng .h, ví dụ
# include < string > // thu vien chuan C ++
# include < string .h > // thu vien chuan C
Các thư viện chuẩn C được đưa vào trong bộ thư viện chuẩn C++ STL với kí tự ’c’ ở đầu
# include < cstring > // thu vien chuan C ++ cua cac ham C
Phần dưới sẽ trình bày một số lớp thông dụng của STL.
93 CHƯƠNG 9. THƯ VIỆN CHUẨN
9.3 Container
Container hay còn gọi là các lớp chứa, là các lớp template có cấu trúc dùng để lưu trữ các kiểu dữ
liệu khác nhau. Có hai loại container chính
• Container tuyến tính (sequential container ) có cấu trúc dữ liệu tuyến tính, ví dụ vector,
list,...
• Container liên kết (asociative container ) có cấu trúc dữ liệu phi tuyến, ví dụ map, set,...
Ngoài ra chúng ta còn có các bộ tương thích container (container adapter ) như stack, queue,...
9.3.1 Vector
Vector định nghĩa một mảng động trong C++ (tương tự như array), đặc điểm của vector là có thể
tự động cấp phát bộ nhớ. Cách thức khai báo một vector như sau
Lưu ý rằng ta cần khai báo #include<vector> để có thể sử dụng được kiểu vector.
Để đưa giá trị vào phần tử trong vector ta sử dụng phương thức push_back() (để bỏ phần tử cuối
cùng của vector ta sử dụng pop_back()) và để truy xuất giá trị phần tử ta sử dụng phương thức
at() (hoặc dùng cặp ngoặc vuông [] như trong array).
std :: vector < int > myvector ;
int number ;
std :: cout << " Nhap vao mot so nguyen : " ;
std :: cin >> number ;
myvector . push_back ( number ) ;
std :: cout << myvector . at (0) << std :: endl ;
9.4. Iterator 94
Để cấp phát vùng nhớ cho vector ta có thể sử dụng phương thức reserve() (trong trường hợp ta
biết chắc kích thước vùng nhớ cần thiết cho vector), việc làm này sẽ giúp hạn chế vector tự cấp
phát vùng nhớ không cần thiết1 .
std :: vector < int > myvector ;
std :: vector < int >:: size_type sz ; // size_type tuong tu nhu size_t
sz = myvector . capacity () ; // kich thuoc vung nho cap phat cho vector
myvector . reserve (100) ; // cap phat vung nho co kich thuoc 100
9.3.2 Map
Map cho phép ta lấy tương ứng giữa một giá trị với một giá trị khác, hai giá trị này tạo thành một
cặp giá trị. Trong đó giá trị đầu của cặp là key, giá trị của key là duy nhất (không có 2 key cùng
xuất hiện trong 1 map).
std :: map < char , int > m ;
m [ 'a ' ]=10;
m [ 'b ' ]=30;
m [ 'c ' ]=50;
m [ 'd ' ]=70;
std :: map < char , int > m1 ( m . begin () ,m . end () ) ;
std :: map < char , int > m2 ( m ) ;
9.4 Iterator
Iterator là khái niệm sử dụng để chỉ một con trỏ trỏ đến các phần tử trong 1 container, mỗi
container có một loại iterator khác nhau.
Trong thư viện STL thì người ta tích hợp lớp đối tượng iterator cùng với các container, ta có thể
khai báo iterator như là một phần nằm trong container, các phương thức begin() và end() sẽ trả
lại con trỏ kiểu đối tượng iterartor đến phần tử đầu tiên và cuối cùng của container.
Có tất cả 5 loại iterator
1
Khi một vector được khai báo, một vùng nhớ sẽ được cấp phát cho vector. Khi vector này đã chiếm đầy vùng
nhớ được cấp phát mà vẫn tiếp tục nhận thêm phần tử, một vùng nhớ mới sẽ được tạo ra (thường có kích thước gấp
đôi vùng nhớ cũ), hệ điều hành sẽ copy dữ liệu ở vùng nhớ cũ sang vùng nhớ mới và tiếp tục công việc nhận thêm
phần tử. Quá trình cấp phát vùng nhớ mới này tiếp diễn liên tục và đôi khi sẽ làm chậm chương trình, đặc biệt trong
trường hợp các vector có kích thước lớn, thậm chí có thể gây ra lỗi memory leak nghiêm trọng.
95 CHƯƠNG 9. THƯ VIỆN CHUẨN
• Random access iterator (RandIter): chứa và nhận giá trị, các thành phần có thể truy xuất
ngẫu nhiên.
• Bidirectional iterator (BiIter): chứa và nhận giá trị, di chuyển tới trước và sau.
• Forward iterator (ForIter): chứa và nhận giá trị, chỉ cho phép di chuyển tới.
• Input iterator (InIter): nhận nhưng không chứa giá trị, chỉ cho phép di chuyển tới.
• Output iterator (OutIter) chứa nhưng không nhận giá trị, chỉ cho phép di chuyển tới.
Ta có thể truy xuất đến các thành phần của container bằng cách sử dụng iterator
< container > coll ;
for ( < container >:: iterator it = coll . begin () ; it != coll . end () ; ++ it )
{
... noi dung ...
}
Nếu một container được khai báo với từ khóa const thì chúng ta phải dùng const_iterator thay
vì iterator
const vector < string > v ;
vector < string >:: const_iterator i = v . begin () ;
9.5 Functor
Functor hay function object (đối tượng hàm) là một đối tượng được sử dụng như một hàm (hay
nói cách khác đó là ta có một lớp mà có định nghĩa operator() trong lớp đó). Vì là một đối tượng
nên các functor có trạng thái, trong khi các hàm bình thường thì không, do đó các functor có thể
ứng xử khác nhau tùy vào trạng thái, tạo nên sự linh hoạt hơn so với các hàm bình thường.
Ví dụ:
class add_x {
private :
int x ;
public :
add_x ( int x ) : x ( x ) {}
int operator () ( int y ) { return x + y ; }
};
hoặc truyền functor như là một đối số (hàm std::transform sẽ biết tự động gọi add_x::operator
())
std :: vector < int > in ; // gia su cac vector nay chua cac phan tu
std :: vector < int > out ;
// out [ i ] == in [ i ] + 1
std :: transform ( in . begin () , in . end () , out . begin () , add_x (1) ) ;
Một số functor cơ bản (để sử dụng được cần khai báo #include <functional>) như cộng (plus),
trừ (minus), nhân (muliplies),...
int first []={1 ,2 ,3 ,4 ,5};
int second []={10 ,20 ,30 ,40 ,50};
int results [5];
std :: transform ( first , first +5 , second , results , std :: plus < int >() ) ;
9.6 Algorithm
Nhằm giúp cho người dùng không phải viết lại những giải thuật quá cơ bản như sắp xếp, thay thế,
tìm kiếm,... thư viện STL có cung cấp một số thuật toán cơ bản, để sử dụng các thuật toán này
cần khai báo #include <algorithm>
• Các thuật toán tìm kiếm
// find
int myints [] = { 10 , 20 , 30 , 40 };
int * p ;
p = std :: find ( myints , myints +4 , 30) ;
std :: cout << " Phan tu tim thay : " << * p << std :: endl ;
// find_if
bool IsOdd ( int i ) { return (( i %2) ==1) ; }
std :: vector < int >:: iterator it = std :: find_if ( myvector . begin () ,
myvector . end () , IsOdd ) ;
std :: cout << " Phan tu le dau tien duoc tim thay la " << * it << std ::
endl ;
// search
std :: vector < int >:: iterator it = std :: search ( myvector . begin () ,
myvector . end () , myints , myints +4) ;
std :: cout << " Mang myints duoc tim thay tai vi tri " << ( it - myvector .
begin () ) << std :: endl ;
vector < int >:: iterator union_it = set_union ( first , first +5 , second ,
second +5 , v . begin () ) ; // hop
vector < int >:: iterator inter_it = set_intersection ( first , first +5 ,
second , second +5 , v . begin () ) ; // giao
vector < int >:: iterator diff_it = set_difference ( first , first +5 , second ,
second +5 , v . begin () ) ; // lay ra cac phan tu khong giong nhau
int A [] = {3 ,4 ,2 ,6 ,3 ,1 ,2 ,3 ,2 ,3 ,4 ,5 ,6 ,4 ,3 ,2 ,1};
int N = sizeof ( A ) / sizeof (* A ) ;
std :: cout << " So nho nhat trong mang : " << * min_element (A , A + N ) << std
:: endl ;
std :: cout << " So lon nhat trong mang : " << * max_element (A , A + N ) << std
:: endl ;
Việc gọi hàm Function() sẽ không làm thay đổi đối tượng thuộc lớp Class
void Class :: Function () const
Việc truyền đối số cho hàm Function() sẽ không làm thay đổi argument
void Class :: Function ( Argument const & argument )
Không thể thay đổi đối tượng được trả về bởi hàm Function()
Result const & Class :: Function ()
Để loại bỏ ảnh hưởng của const, ta có thể dùng từ khóa mutable đặt trước đối tượng.
/* File2 . cpp */
extern int x ; // bao cho trinh bien dich biet bien x da duoc khai bao o
noi khac
int main ()
{
std :: cout << x << std :: endl ;
return 0;
}
99
10.1. Cách sử dụng một số từ khóa 100
Trong trường hợp biến cục bộ có tên trùng với tên của biến toàn cục trong chương trình, thì đó
biến cục bộ sẽ được ưu tiên sử dụng. Tuy nhiên, nếu ta muốn sử dụng biến toàn cục thì ta có thể
sử dụng toàn tử :: để báo cho trình biên dịch biết biến mà nó đang sử dụng là biến toàn cục.
int x = 1;
int main ()
{
int x = 5;
std :: cout << :: x << std :: endl ; // xuat ra gia tri la 1
return 0;
}
Trong trường hợp khi dùng cụm từ extern ``C'' sẽ báo cho trình biên dịch biết rằng các hàm
trong danh sách phải được biên dịch theo dạng C, tức là không được dùng các phương pháp thay
đổi tên hàm (name mangling) của C++. Lý do chính phải dùng lệnh này là khi biên dịch mã nguồn
C++ thành các thư viện động, các hàm có thể được gọi bởi một ngôn ngữ khác (chẳng hạn như
Python) nên nếu biên dịch theo kiểu C++ thì ngôn ngữ kia không nối (link ) được1 . Nếu ta chỉ cần
nối các module C++ với nhau thôi thì không cần sử dụng lệnh này.
Ví dụ 10.1: Một số cách sử dụng extern ``C''
extern " C " void f ( int ) ;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
extern " C "
{
void g ( char ) ;
int i ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
extern " C " {
# include " C_header . h "
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ifdef __cplusplus
extern " C " {
# endif
/* ... */
# ifdef __cplusplus
}
# endif
Công dụng thứ hai của từ khóa static là ở bên trong phần định nghĩa lớp. Các thành viên của lớp
được khai báo với từ khóa static ở phía trước được gọi là các thành viên tĩnh (static member ),
1
Xem thêm trong phần 6.1 của tài liệu “Giới thiệu ngôn ngữ lập trình Python” (http://goo.gl/OyjKiB)
101 CHƯƠNG 10. MỘT SỐ VẤN ĐỀ NÂNG CAO
dữ liệu của các thành viên tĩnh của lớp được xem như là toàn cục của lớp đó. Do vậy mọi sự thay
đổi dữ liệu thành viên tĩnh của đối tượng này đều có tác dụng lên toàn bộ các dữ liệu thành viên
tĩnh của các đối tượng khác.
class Item {
private :
static int next_item ;
public :
int getItem () { return next_item ++; }
};
int main ()
{
Item first ;
Item second ;
std :: cout << first . getItem () << std :: endl ; // next_item = 2
std :: cout << second . getItem () << std :: endl ; // next_item = 3
return 0;
}
Lưu ý: các phương thức tĩnh trong lớp chỉ được truy cập đến các thành viên dữ liệu tĩnh của lớp
đó.
int main () {
std :: cout << std :: setw (8) << " 1000000\ n " ;
std :: cout << std :: setw (8) << " 1\ n " ;
std :: cout << std :: setw (8) << " 10000\ n " ;
std :: cout << std :: setw (8) << " 1000000000\ n " ;
return 0;
}
/* Ket qua :
1000000
1
10000
1000000000
*/
int main () {
double a = 3.14159;
double b = 1.0 e -5;
10.2. Định dạng dòng dữ liệu xuất 102
std :: cout . precision (3) ; // dinh dang do chinh xac ( so chu so sau dau
thap phan )
/* Ket qua :
fixed :
3.142
0.000
scientific :
3.142 e +00
1.000 e -05
*/
• Tự động điền kí tự vào khoảng trống được tạo ra bởi std::setw(n) (std::setfill(ch))
int main () {
std :: cout << std :: setfill ( ' - ') << std :: setw (8) ;
std :: cout << 100 << std :: endl ;
return 0;
}
/* Ket qua :
- - - - -100
*/
• Định dạng thập phân (std::dec), bát phân (std::oct), thập lục phân (std::hex)
int main () {
int n = 100;
std :: cout << std :: dec << n << '\ n ';
std :: cout << std :: hex << n << '\ n ';
std :: cout << std :: oct << n << '\ n ';
return 0;
}
/* Ket qua :
100
64
144
*/
103 CHƯƠNG 10. MỘT SỐ VẤN ĐỀ NÂNG CAO
class MyClass {
public :
void func () ;
int data ;
};
Mỗi file mã nguồn của lớp MyClass cần phải #include ``MyClass.h''. Lưu ý rằng ta nên dùng
ngoặc kép “ ” hơn là ngoặc nhọn <> khi include những file header, dấu ngoặc kép sẽ cho trình biên
dịch biết phải tìm kiếm header trong thư mục chương trình trước, rồi mới đến các header chuẩn
của trình biên dịch.
File mã nguồn chính main.cpp có nội dung
# include " MyClass . h " // dinh nghia MyClass
int main ()
{
MyClass a ;
a . func () ;
return 0;
}
Những file header sẽ trở thành phần chung giữa các module hay lớp con. Bằng cách #include
một header, ta có thể truy xuất đến toàn bộ những định nghĩa cấu trúc, tên hàm, hàng số. . . của
module hoặc lớp tương ứng.
Một số lỗi thường gặp khi biên dịch với header2
• Không tìm thấy định nghĩa cần thiết: điều này sẽ làm cho file mã nguồn không biên dịch
được bởi vì có một số những định nghĩa chưa được khai báo do không được include từ các
file header. Giả sử ta có 3 file (Header1.h, Header2.h và File.cpp) như sau
/* Header1 . h */
class ClassOne { ... };
/* Header2 . h */
# include " Header1 . h "
class ClassTwo { ... };
/* File . cpp */
# include " Header2 . h "
ClassOne myClassOne ;
ClassTwo myClassTwo ;
Trong trường hợp này, File.cpp sẽ biên dịch tốt, vì đã include Header2.h và gián tiếp include
Header1.h, có nghĩa là File.cpp có thể truy xuất đến lớp ClassOne. Nhưng nếu một thời gian
sau, ai đó cho rằng Header2.h không cần thiết phải include Header1.h và xoá dòng include
đó đi, hậu quả là File.cpp sẽ không thể biên dịch được. Do đó, ta cần phải dứt khoát khi
2
Cách thức biên dịch chương trình với file header được hướng dẫn ở Phần 6 trong tài liệu “Nhập môn hệ điều
hành Linux” (http://goo.gl/11gQjF)
105 CHƯƠNG 10. MỘT SỐ VẤN ĐỀ NÂNG CAO
include bất kì header nào cần cho file mã nguồn để biên dịch, không nên chỉ dựa vào những
file header include gián tiếp mà có thể sẽ thay đổi.
Tuy nhiên việc này đôi khi lại dẫn tới một số lỗi sẽ được trình bày tiếp theo sau.
• Phụ thuộc vòng tròn: khi những header xuất hiện khi cần include lẫn nhau để làm việc một
cách có dụng ý. Chẳng hạn như ta có hai lớp ClassOne và ClassTwo phụ thuộc lẫn nhau
/* Header1 . h */
# include " Header2 . h "
class ClassOne { ClassTwo two ; };
/* Header2 . h */
# include " Header1 . h "
class ClassTwo { ClassOne one ; };
Thật ra lớp ClassOne không cần phải biết chi tiết của lớp ClassTwo, do nó chỉ sử dụng con
trỏ của lớp ClassTwo chứ không sử dụng toàn bộ đối tượng của lớp này. Con trỏ không cần
biết nó chỉ đến đâu, do đó ta không cần phải định nghĩa cấn trúc hoặc lớp để chứa con trỏ.
Điều này có nghĩa là dòng #include ở đây là không cần thiết.
Tuy nhiên, khi trình biên dịch hoạt động, khi biên dịch lớp ClassOne nó sẽ cần tới ClassTwo
mà lúc này chưa được biên dịch do nó cũng cần tới ClassOne được biên dịch (nên nhớ là trình
biên dịch mỗi lúc chỉ hoạt động với 1 file), do đó sẽ gây ra lỗi “Undeclared identifier”. Để khắc
phục lỗi này, thay vì sử dụng include, ta sẽ sử dụng khai báo trước (forward declaration)
/* Header1 . h */
class ClassTwo ; // khai bao truoc ClassTwo
class ClassOne { ClassTwo two ; };
/* Header2 . h */
class ClassOne ; // khai bao truoc ClassOne
class ClassTwo { ClassOne one ; };
• Định nghĩa chồng: khi 1 lớp hoặc cấu trúc được gọi 2 lần trong 1 file nguồn. Điều này sẽ gây
ra lỗi trong thời gian biên dịch (compile-time error ) và thường xuất hiện khi gọi nhiều file
header trong 1 file header khác, làm cho header được gọi 2 lần khi bạn biên dịch file nguồn.
Để khắc phục được lỗi này ta sử dụng các include guard để đảm bảo rằng một header file
chỉ được include một lần trong chương trình. Chẳng hạn như file MyClass.h lúc này sẽ có nội
dung
# ifndef MYCLASS_H // neu MyClass chua duoc dinh nghia
# define MYCLASS_H // thi dinh nghia MyClass
class MyClass {
public :
void func () ;
int data ;
};
# endif
Trước đây các tính toán song song với ngôn ngữ C++ được thực hiện chủ yếu thông qua các tiện
ích của hệ điều hành (OS facilities) chẳng hạn như thư viện pthread (POSIX thread ) của các hệ
điều hành họ Unix, hay các thư viện MPI3 , OpenMP,...
Tuy nhiên kể từ chuẩn C++11 trở đi, tính năng hỗ trợ lập trình song song đa luồng (multithread )
đã được tích hợp vào trong ngôn ngữ C++. Để có thể sử dụng được tính năng này ta cần sử dụng
thư viện thread
Ví dụ 10.2: Tính toán song song với C++11
# include < iostream >
# include < thread >
int main () {
std :: thread t [ num_threads ];
return 0;
}
với các phiên bản trình biên dịch cũ hơn gcc4.7, ta thay thế tùy chỉnh -std=c++11 bằng -std=c
++0x.
Hầu như tất cả các lớp chứa của STL đều có các hàm thành viên là begin() và end() để trả về
iterator cho phép truy cập tới các phần tử của lớp. C++11 đưa ra hai hàm begin() và end() không
phải là thành viên của bất cứ lớp chứa nào và có thể dễ dàng được nạp chồng cho bất cứ lớp hay
một mảng nào.
for ( auto it = v . begin () ; it != v . end () ; ++ it ) // C ++03
for ( auto it = begin ( v ) ; it != end ( v ) ; ++ it ) // C ++11
3
Xem thêm tài liệu “Cơ bản lập trình song song MPI cho C/C++” (http://goo.gl/J8kIJA)
107 CHƯƠNG 10. MỘT SỐ VẤN ĐỀ NÂNG CAO
Kiểu Ret là tổng của kiểu Lhs và Rhs Với type-inference. Trong C++11 mọi việc trở nên đơn giản
hơn với từ khóa decltype và cú pháp trailing-return-type
template < class Lhs , class Rhs >
auto adding_func ( const Lhs & lhs , const Rhs & rhs ) -> decltype ( lhs + rhs ) {
return lhs + rhs ;}
Tuy nhiên ta không thể làm điều này với các container mà phải thêm lần lượt từng phần tử vào
std :: vector < int > v ;
for ( int i =0; i <5; i ++) {
v . push_back ( arr [ i ]) ;
}
Với chuẩn C++11, việc khởi tạo giá trị ban đầu bằng cặp ngoặc nhọn được chấp nhận
std :: vector < int > v {1 , 2 , 3 , 4 , 5};
Ví dụ 10.3: Một số ví dụ cho việc khởi tạo giá trị ban đầu
class C {
public :
C ( int i , int j ) ;
};
class X {
int a [4];
public :
X () : a {1 ,2 ,3 ,4} {} // khoi tao gia tri ban dau cho mang a
};
struct A {
int x ;
A ( int x = 1) : x ( x ) {} // ham khoi tao do nguoi dung dinh nghia
};
struct B : A {
B ( int y ) : A ( y ) {}
// ham khoi tao B :: B () khong duoc dinh nghia boi vi da co dinh nghia
roi
};
struct C : A {
C ( int y ) : A ( y ) {}
C () = default ; // ham khoi tao C :: C () duoc dinh nghia va ham nay se
goi A :: A ()
};
niệm tham chiếu rvalue ra đời giúp kéo dài tuổi thọ của các đối tượng rvalue và đồng thời cũng
giúp cho việc lập trình hiệu quả hơn.
Mục đích chính của việc sử dụng tham chiếu rvalue là kĩ thuật move semantics, trong đó thay vì
tạo ra một bản sao của đối tượng và gán vào địa chỉ của lvalue, trình biên dịch sẽ chuyển toàn bộ
nội dung của rvalue cho lvalue, điều này giúp tránh được việc loại bỏ rvalue ra khỏi bộ nhớ, cũng
như việc thực hiện copy dữ liệu.
Ví dụ:
std :: vector < BigType > v1 ;
for ( int i = 0; i < n ; ++ i ) {
BigType obj ;
v1 . push_back ( obj ) ;
}
Trong ví dụ trên ta có đối tượng obj có kích thước lớn (BigType), mỗi khi phương thức push_back
() được thực hiện, một bản sao của obj sẽ được tạo ra, và sau đó được dùng để tạo ra đối tượng
v1[i] qua hàm khởi tạo copy (copy constructor ). Tiếp đến đối tượng bản sao cũng sẽ bị hủy. Có
thể thấy rằng việc truyền đối tượng obj như vậy sẽ là một thao tác tốn thời gian (cấp phát bộ
nhớ, copy dữ liệu) vì kích thước của nó có thể rất lớn. Thay vào đó ta thực hiện
std :: vector < BigType > v1 ;
for ( int i = 0; i < n ; ++ i ) {
BigType obj ;
v1 . push_back ( std :: move ( obj ) ) ;
}
Chương trình sẽ tiến hành chuyển toàn bộ nội dung của obj cho v1[i], với std::move(obj) là
một tham chiếu rvalue.
Trong trường hợp chúng ta muốn đưa kĩ thuật move vào trong lớp, ta cần khai báo một hàm khởi
tạo move (move constructor ) và toán tử move (move assignment operator ) như sau
class MyClass {
MyClass ( MyClass &&) ; // ham khoi tao cho ki thuat move
MyClass && operator =( MyClass &&) ; // toan tu move
};
class Person {
public :
std :: string name ;
int age ;
public :
Person ( std :: string name , int age ) : name ( name ) , age ( age ) {}
10.4. Một số nâng cấp, bổ sung trong C++11 110
~ Person () {}
};
int main ()
{
std :: vector < Person > list = {};
return 0;
}
Tài liệu tham khảo
[1] Trần Đình Quế, Nguyễn Mạnh Hùng, Ngôn ngữ lập trình C++ , Học viện Công nghệ Bưu
chính Viễn thông (2006).
[2] H.W. Bell, C++ Programming for Physicists (2009).
[3] H. Watson, Intro to Standard Template Library,
http://web.eng.fiu.edu/watsonh/eel3160/Lectures/STL-Lectures.pdf
[4] Ebook team (updatesofts.com), C++ cơ bản và nâng cao
[5] http://en.wikipedia.org/wiki/Standard_Template_Library
[6] http://en.wikipedia.org/wiki/Anonymous_function
[7] http://vi.wikipedia.org/wiki/C++
[8] http://vi.wikipedia.org/wiki/C++11
[9] http://vi.wikipedia.org/wiki/Tr%C3%ACnh_bi%C3%AAn_d%E1%BB%8Bch
[10] http://vi.wikipedia.org/wiki/L%E1%BA%ADp_tr%C3%ACnh_h%C6%B0%E1%BB%9Bng_%C4%
91%E1%BB%91i_t%C6%B0%E1%BB%A3ng
[11] http://vi.wikipedia.org/wiki/Th%C6%B0_vi%E1%BB%87n_chu%E1%BA%A9n_C%2B%2B
[12] http://vi.wikipedia.org/wiki/Ng%C3%B4n_ng%E1%BB%AF_l%E1%BA%ADp_tr%C3%ACnh
[13] http://moriator.wordpress.com/2007/11/06/cach-t%E1%BB%95-ch%E1%BB%A9c-file-c-
va-c/
[14] http://moriator.wordpress.com/2007/11/07/cach-t%E1%BB%95-ch%E1%BB%A9c-file-c-
va-c-ph%E1%BA%A7n-2/
[15] http://sinhvienit.net/@tut/lap-trinh-phan-mem/c-ascii-c/43-bai-5.2-
namespaces.html
[16] http://diendan.congdongcviet.com/threads/t46021::cach-su-dung-cac-stl-
container.cpp
[17] http://blog.smartbear.com/c-plus-plus/the-biggest-changes-in-c11-and-why-
you-should-care/
[18] http://www.pcworld.com.vn/articles/cong-nghe/cong-nghe/2014/03/1234591/chuan-
c-11-nhu-mot-ngon-ngu-lap-trinh-moi/
[19] http://root.cern.ch/drupal/content/cling
[20] http://isocpp.org/std/status
111
Tài liệu tham khảo 112
[21] http://www.functionx.com/cpp/index.htm
[22] http://www.cplusplus.com/info/history/
[23] http://www.cplusplus.com/doc/tutorial/preprocessor/
[24] http://www.gotw.ca/gotw/009.htm
[25] http://www.fotech.org/forum/index.php?showtopic=36547
[26] http://www.cplusplus.com/forum/articles/10627/
[27] http://cs.brown.edu/~jak/proglang/cpp/stltut/tut.html
[28] http://www.cim.mcgill.ca/~mpersson/cxx_diagrams.html
[29] http://www.cs.uic.edu/~jbell/CourseNotes/CPlus/FileIO.html
[30] http://www.cplusplus.com/doc/tutorial/polymorphism/
[31] https://www.scribd.com/doc/63794401/Stl
[32] https://solarianprogrammer.com/archives/