C++ Cơ Bản
Giới thiệu về C++
C++ là ngôn ngữ lập trình mạnh mẽ được phát triển từ ngôn ngữ C. Nó hỗ trợ lập trình hướng đối tượng, quản lý bộ nhớ thủ công và hiệu suất cao. C++ được sử dụng rộng rãi trong các hệ thống lớn như game engine, trình biên dịch, hệ điều hành và phần mềm nhúng.
Hello World
#include <iostream>
using namespace std;
int main() {
cout << "Hello, World!" << endl;
return 0;
}
#include <iostream> cho phép sử dụng cin và cout. using namespace std giúp không phải viết std::cout mỗi lần.
Biến và kiểu dữ liệu
Các biến trong C++ được khai báo với kiểu dữ liệu cụ thể để xác định loại giá trị mà biến đó có thể chứa. Dưới đây là một số kiểu dữ liệu cơ bản:
- int: Số nguyên có dấu (ví dụ: -10, 0, 42). Thường chiếm 4 byte và dùng cho các giá trị đếm được. (size 4 byte)
- float: Số thực đơn chính xác, dùng khi không yêu cầu độ chính xác cao. Cần hậu tố
f(ví dụ: 3.14f). (size 4 byte) - double: Số thực có độ chính xác cao hơn float, thích hợp cho các phép tính khoa học. (size 8 byte)
- char: Ký tự đơn, đặt trong dấu nháy đơn (ví dụ: 'A'). (size 1 byte)
- bool: Biến logic có hai giá trị:
truehoặcfalse. (size 1 byte) - string: Chuỗi ký tự, là một lớp trong thư viện chuẩn STL. Cần
#include <string>.
int age = 25; // kiểu int, lưu tuổi
float pi = 3.14f; // float, cần hậu tố f để phân biệt với double
double g = 9.80665; // double, chính xác cao hơn float
char grade = 'A'; // char, ký tự duy nhất
bool passed = true; // bool, true hoặc false
string name = "Alice"; // string, chuỗi ký tự
Mỗi kiểu dữ liệu có kích thước bộ nhớ và mục đích sử dụng riêng. Nên chọn kiểu phù hợp để tiết kiệm bộ nhớ và tăng hiệu suất.
Muốn sử dụng kiểu string, cần thêm #include <string> vào đầu file.
Toán tử
C++ cung cấp nhiều loại toán tử để thực hiện các phép tính và thao tác logic:
- Toán tử số học:
+(cộng),-(trừ),*(nhân),/(chia),%(chia lấy dư)
Ví dụ:5 % 2sẽ trả về1vì 5 chia 2 dư 1. - Toán tử so sánh:
==(bằng),!=(khác),>,<,>=,<= - Toán tử logic:
&&(và),||(hoặc),!(phủ định) - Toán tử gán:
=,+=,-=,*=,/= - Toán tử bit:
&,|,^(XOR),~(NOT),<<(dịch trái),>>(dịch phải)
int a = 5, b = 2;
cout << a + b << endl; // 7
cout << a - b << endl; // 3
cout << a * b << endl; // 10
cout << a / b << endl; // 2 (vì chia nguyên)
cout << a % b << endl; // 1 (dư)
cout << (a == b) << endl; // 0 (false)
cout << (a > b) << endl; // 1 (true)
cout << ((a > 0) && (b > 0)) << endl; // 1 (true)
cout << (a & b) << endl; // bitwise AND: 5 & 2 = 00000101 & 00000010 = 00000000
Chú ý: % chỉ dùng cho số nguyên. Để lấy phần dư số thực, hãy dùng hàm
fmod()
từ thư viện <cmath>.
Phân biệt rõ giữa && (AND logic) và & (AND bit).
Câu lệnh điều kiện
Trong C++, câu lệnh điều kiện được sử dụng để kiểm tra điều kiện và thực hiện các khối lệnh khác nhau dựa vào kết quả đúng/sai.
1. if - else if - else
int score = 75;
if (score >= 90) {
cout << "Giỏi";
} else if (score >= 70) {
cout << "Khá";
} else {
cout << "Yếu";
}
Câu lệnh được kiểm tra lần lượt từ trên xuống. Điều kiện đầu tiên đúng thì các phần sau sẽ không được kiểm tra nữa.
2. switch-case
Dùng khi có nhiều giá trị rẽ nhánh rõ ràng, ví dụ theo số hoặc ký tự cụ thể:
char grade = 'B';
switch (grade) {
case 'A':
cout << "Tuyệt vời";
break;
case 'B':
cout << "Tốt";
break;
case 'C':
cout << "Trung bình";
break;
default:
cout << "Chưa đạt";
}
Mỗi case nên kết thúc bằng break để tránh rơi qua các nhánh sau.
default là mặc định nếu không khớp case nào.
3. Toán tử ba ngôi (Ternary Operator)
Dùng để viết ngắn gọn điều kiện if-else:
int score = 85;
string result = (score >= 60) ? "Đậu" : "Rớt";
cout << result;
Toán tử ba ngôi có dạng: điều_kiện ? giá_trị_nếu_đúng : giá_trị_nếu_sai.
4. if constexpr (từ C++17)
Dùng trong template để kiểm tra điều kiện ngay tại thời điểm biên dịch (compile-time):
#include
template
void print_type() {
if constexpr (std::is_integral::value)
cout << "Số nguyên";
else
cout << "Không phải số nguyên";
}
Giúp loại bỏ nhánh sai hoàn toàn khi biên dịch, tối ưu hiệu năng.
5. goto (không khuyến khích)
Cho phép nhảy đến một nhãn cụ thể trong mã. Dễ gây khó đọc và khó debug.
int x = 5;
if (x < 0) goto negative;
cout << "Số dương";
return;
negative:
cout << "Số âm";
Chỉ nên dùng trong những trường hợp rất đặc biệt như thoát lồng nhiều vòng lặp phức tạp.
Vòng lặp
Vòng lặp giúp lặp đi lặp lại một khối lệnh nhiều lần trong khi điều kiện còn đúng. Có ba loại vòng lặp chính:
1. for
for (int i = 1; i <= 5; i++) {
cout << i << " ";
}
Thích hợp khi biết trước số lần lặp.
2. while
int n = 5;
while (n > 0) {
cout << n-- << " ";
}
Lặp khi điều kiện còn đúng. Điều kiện được kiểm tra trước mỗi vòng.
3. do-while
int x = 0;
do {
cout << x++ << " ";
} while (x < 3);
Khối lệnh được thực hiện ít nhất một lần trước khi kiểm tra điều kiện.
4. break và continue
for (int i = 1; i <= 10; i++) {
if (i == 5) continue; // bỏ qua giá trị 5
if (i == 8) break; // dừng vòng lặp tại 8
cout << i << " ";
}
break: thoát khỏi vòng lặp ngay lập tức.continue: bỏ qua phần còn lại của vòng lặp hiện tại và sang vòng tiếp theo.
5. Biến phạm vi vòng lặp (C++11+)
vector nums = {1, 2, 3};
for (int x : nums) {
cout << x << " ";
}
C++11 hỗ trợ vòng lặp range-based for (còn gọi là foreach) để duyệt qua container như
mảng, vector, set... Đây là cách viết ngắn gọn, an toàn và dễ đọc hơn.
Bạn cũng có thể dùng tham chiếu để tránh copy giá trị:
for (int& x : nums) x *= 2;
Hoặc nếu không cần chỉnh sửa, dùng const để đảm bảo an toàn:
for (const int& x : nums) cout << x;
Hàm và tham số
Hàm trong C++ giúp chia nhỏ chương trình thành các phần logic dễ quản lý, tái sử dụng và kiểm thử.
1. Khai báo và định nghĩa hàm
// Khai báo (có thể đặt ở đầu file)
int cong(int a, int b);
// Định nghĩa (chi tiết hàm)
int cong(int a, int b) {
return a + b;
}
Cần khai báo trước nếu hàm được định nghĩa sau hàm main().
2. Gọi hàm
int kq = cong(3, 4); // kết quả: 7
cout << kq;
Gọi hàm bằng cách truyền giá trị hoặc biến tương ứng với tham số.
3. Hàm không có giá trị trả về (void)
void xinChao() {
cout << "Xin chào C++";
}
4. Hàm không có tham số
int layGiaTriCoDinh() {
return 42;
}
5. Tham số mặc định
void say(string name = "bạn") {
cout << "Xin chào, " << name;
}
Nếu không truyền đối số, giá trị mặc định sẽ được dùng.
6. Truyền tham trị vs tham chiếu
void tang1(int x) { x++; } // truyền giá trị, không ảnh hưởng bên ngoài
void tang2(int& x) { x++; } // truyền tham chiếu, ảnh hưởng biến gốc
7. Đệ quy
int giaiThua(int n) {
if (n <= 1) return 1;
return n * giaiThua(n - 1);
}
Hàm gọi lại chính nó. Cần có điều kiện dừng để tránh lặp vô hạn.
8. Biến trong hàm
- Biến cục bộ: chỉ tồn tại trong hàm.
- static: giữ giá trị giữa các lần gọi hàm.
void demSoLanGoi() {
static int dem = 0;
dem++;
cout << "Gọi lần thứ: " << dem;
}
Các biến khai báo trong hàm sẽ được cấp phát bộ nhớ mỗi lần gọi (trừ biến static).
Con trỏ và Tham chiếu
- Biến con trỏ là gì?
- Con trỏ (Pointer)
- Cấp phát bộ nhớ động
- Mảng động
- Tham chiếu (Reference)
- So sánh con trỏ và tham chiếu
- Con trỏ kết hợp const
- Con trỏ cấp 2, cấp 3
- Truyền con trỏ cho hàm
- Con trỏ void (void*)
- Con trỏ hàm (function pointer)
- Null pointer và nullptr
0. Biến con trỏ là gì?
Con trỏ là một biến lưu địa chỉ của một biến khác. Con trỏ có thể trỏ tới bất kỳ kiểu dữ liệu nào: int, float, struct...
1. Con trỏ (Pointer)
Con trỏ là biến lưu địa chỉ của biến khác.
int a = 10;
int* p = &a; // p lưu địa chỉ của a
cout << *p; // *p là giá trị tại địa chỉ đó => 10
Toán tử & lấy địa chỉ, * truy cập giá trị qua địa chỉ (dereference).
2. Cấp phát bộ nhớ động
int* ptr = new int(5); // cấp phát vùng nhớ mới lưu giá trị 5
cout << *ptr;
delete ptr; // giải phóng vùng nhớ
Luôn dùng delete sau khi new để tránh rò rỉ bộ nhớ.
3. Mảng động
int* arr = new int[3]{1, 2, 3};
for (int i = 0; i < 3; i++) cout << arr[i];
delete[] arr;
Dùng delete[] cho mảng cấp phát động.
4. Tham chiếu (Reference)
int x = 5;
int& ref = x; // ref là tên khác của x
ref = 10;
cout << x; // in ra 10
Tham chiếu là alias (bí danh), không cấp phát bộ nhớ mới.
5. So sánh con trỏ và tham chiếu
- Con trỏ có thể
null, tham chiếu thì không. - Con trỏ có thể thay đổi địa chỉ trỏ tới, tham chiếu thì cố định sau khi gán.
- Con trỏ có thể trỏ tới
constđể bảo vệ dữ liệu, ví dụ:const int* p(không cho thay đổi giá trị).
6. Con trỏ kết hợp const
int x = 10;
const int* p1 = &x; // Không cho thay đổi *p1, nhưng p1 có thể trỏ nơi khác
int* const p2 = &x; // Cho thay đổi *p2, nhưng không đổi địa chỉ p2
const int* const p3 = &x; // Không cho thay đổi *p3 và không đổi địa chỉ
Ghi nhớ nguyên tắc đọc từ phải sang trái: const áp dụng cho bên trái của dấu *
7. Con trỏ cấp 2, cấp 3
$18. Truyền con trỏ cho hàm
void tang(int* p) {
(*p)++;
}
int x = 5;
tang(&x);
cout << x; // 6
Con trỏ cho phép thay đổi giá trị gốc từ trong hàm.
9. Con trỏ void (void*)
void* p;
int a = 10;
p = &a;
cout << *(int*)p;
Con trỏ void có thể trỏ đến bất kỳ kiểu nào, nhưng cần ép kiểu trước khi dùng.
10. Con trỏ hàm (function pointer)
int cong(int a, int b) { return a + b; }
int (*f)(int, int) = cong;
cout << f(3, 4); // 7
Hữu ích khi truyền hàm như tham số.
11. Null pointer và nullptr
int* p = nullptr;
if (p == nullptr) cout << "Chưa cấp phát";
Từ C++11 dùng nullptr thay cho NULL hoặc 0.
Kiểu dữ liệu tự định nghĩa (struct)
struct cho phép nhóm nhiều biến (có thể khác kiểu) vào cùng một kiểu dữ liệu mới. Hữu ích để mô tả
các
thực thể như Sinh viên, Học sinh, Sản phẩm...
1. Khai báo và sử dụng struct
struct SinhVien {
string ten;
int tuoi;
float diem;
};
int main() {
SinhVien sv1;
sv1.ten = "Nam";
sv1.tuoi = 20;
sv1.diem = 8.5;
cout << sv1.ten;
}
Sau khi định nghĩa, bạn có thể khai báo biến kiểu đó như một kiểu dữ liệu thông thường.
2. Khởi tạo nhanh (C++11+)
SinhVien sv2 = {"Lan", 21, 9.0};
Cách này giúp tạo biến và gán giá trị ngay lập tức theo thứ tự khai báo.
3. Truyền struct cho hàm
void inThongTin(SinhVien sv) {
cout << sv.ten << " - " << sv.tuoi << endl;
}
Nên truyền tham chiếu nếu struct lớn để tránh sao chép:
void inThongTin(const SinhVien& sv);
4. struct lồng nhau
struct Ngay {
int ngay, thang, nam;
};
struct HocSinh {
string ten;
Ngay ngaySinh;
};
HocSinh hs = {"Mai", {1, 1, 2005}};
Có thể lồng struct trong struct để tổ chức dữ liệu rõ ràng hơn.
5. struct có hàm thành viên
struct HinhChuNhat {
int dai, rong;
int dienTich() {
return dai * rong;
}
};
HinhChuNhat hcn = {5, 3};
cout << hcn.dienTich();
struct hoàn toàn có thể có hàm thành viên giống như class.
6. Các mức truy cập trong struct
struct A {
public:
int x;
private:
int y;
};
struct mặc định là public, nhưng có thể khai báo các phần private, protected như class.
7. struct và cấp phát động
struct HocSinh {
string ten;
int tuoi;
};
HocSinh* hs = new HocSinh;
hs->ten = "Nam";
hs->tuoi = 20;
delete hs;
Có thể cấp phát struct động bằng new như con trỏ bình thường.
8. struct và con trỏ thành viên
struct Node {
int data;
Node* next;
};
struct có thể chứa con trỏ trỏ tới chính kiểu của nó. Dùng trong danh sách liên kết, cây nhị phân, v.v.
9. struct và constructor (C++11+)
struct Diem {
int x, y;
Diem(int a, int b) : x(a), y(b) {}
};
Diem d(3, 4);
struct có thể có constructor như class.
10. So sánh struct với class
structmặc định làpublic, cònclassmặc định làprivate.- Cả hai đều hỗ trợ tính kế thừa, đóng gói và đa hình.
- Có thể dùng
friend,virtual, v.v. trong struct như trong class.
11. Kích thước bộ nhớ
struct A {
char c;
int i;
};
cout << sizeof(A);
Kích thước struct phụ thuộc thứ tự các thành viên, do có thể có padding (đệm) để tối ưu truy cập bộ nhớ.
12. Tổng kết
structnên dùng khi dữ liệu chủ yếu là public và đơn giản.- Dễ dàng mở rộng lên class khi cần tính kế thừa hoặc private mạnh mẽ.
enum
enum TrangThai { DUNG, CHAY, TAM_DUNG };
TrangThai st = CHAY;
enum giúp đặt tên cho các hằng số nguyên để dễ đọc.
typedef / using
typedef unsigned int uint;
using uint = unsigned int;
Giúp đặt tên ngắn gọn cho kiểu dữ liệu.
Hằng số
const int SIZE = 100;
constexpr double PI = 3.1415;
#define MAX 50
Nhập xuất dữ liệu
int a;
cin >> a;
cout << "a = " << a;
char name[20];
scanf("%s", name);
printf("Ten: %s", name);
Xử lý chuỗi
string s1 = "Hello", s2 = "World";
string s3 = s1 + " " + s2;
cout << s3 << endl;
cout << s3.substr(0, 5); // Hello
Comment và include
// Đây là comment dòng đơn
/* Đây là comment nhiều dòng */
#include <iostream>
#pragma once
Các vùng bộ nhớ trong C++
Trong C++, khi một chương trình chạy, bộ nhớ của nó được chia thành nhiều vùng chính:
- Text (Code Segment): chứa mã máy của chương trình.
- Global/Static: chứa biến toàn cục, biến static có thời gian sống dài.
- Heap (Free Store): nơi cấp phát bộ nhớ động bằng
new/malloc, phảidelete/free. - Stack: chứa các biến cục bộ, tham số hàm, và được quản lý tự động (LIFO).
- Literal/Constant Pool: lưu trữ hằng số như chuỗi ký tự trong chương trình.
Hiểu rõ cách tổ chức bộ nhớ giúp bạn tránh các lỗi như tràn stack, rò rỉ bộ nhớ, hoặc dùng con trỏ không hợp lệ.
Các bước biên dịch một chương trình C++
Trước khi chạy chương trình, mã C++ phải trải qua các bước sau:
- Tiền xử lý (Preprocessing): xử lý các dòng bắt đầu bằng
#như#include,#define. - Biên dịch (Compilation): chuyển mã nguồn (.cpp) thành mã máy (object code, .o/.obj).
- Liên kết (Linking): kết hợp nhiều file object và thư viện thành file thực thi (.exe).
- Chạy (Execution): hệ điều hành nạp file thực thi vào bộ nhớ và chạy.
Công cụ như g++ tự động thực hiện các bước trên. Bạn có thể chỉ dừng ở từng bước bằng
các
flag như -E, -c, -o.
Các lỗi thường gặp
- Dùng biến chưa khởi tạo (undefined behavior)
- Truy cập vùng nhớ đã bị giải phóng (dangling pointer)
- Gọi hàm chưa định nghĩa hoặc thiếu prototype
- Truy cập ngoài chỉ số mảng (array out of bounds)
- Dùng sai kiểu dữ liệu (ví dụ: gán float cho int)
- Không giải phóng bộ nhớ sau khi dùng
new(memory leak) - Tham chiếu null hoặc sử dụng
nullptrsai cách - Dùng
==thay vì=trong biểu thức gán - Sai phạm vi biến: biến cục bộ bị che khuất hoặc biến toàn cục bị ghi đè
- Nhập dữ liệu bằng
cinkhông kiểm tra lỗi nhập (cin.fail())
Debug cơ bản
cout << "x = " << x << endl;
// Biên dịch với flag -g để dùng GDB
// g++ -g main.cpp -o main
// gdb ./main