Học C++ từ Cơ Bản đến Nâng Cao

Website tự học lập trình C++ miễn phí, thực chiến, dễ hiểu

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 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:

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 << " ";
}

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

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

  1. Biến con trỏ là gì?
  2. Con trỏ (Pointer)
  3. Cấp phát bộ nhớ động
  4. Mảng động
  5. Tham chiếu (Reference)
  6. So sánh con trỏ và tham chiếu
  7. Con trỏ kết hợp const
  8. Con trỏ cấp 2, cấp 3
  9. Truyền con trỏ cho hàm
  10. Con trỏ void (void*)
  11. Con trỏ hàm (function pointer)
  12. 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

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

$1

8. 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

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

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:

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:

  1. Tiền xử lý (Preprocessing): xử lý các dòng bắt đầu bằng # như #include, #define.
  2. Biên dịch (Compilation): chuyển mã nguồn (.cpp) thành mã máy (object code, .o/.obj).
  3. Liên kết (Linking): kết hợp nhiều file object và thư viện thành file thực thi (.exe).
  4. 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

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

← Quay lại Trang chính