L9. Con Trỏ Cơ Bản (Pointer)

1. Biến và Vùng Nhớ

1.1. Bộ Nhớ Máy Tính

Cấu trúc bộ nhớ RAM:

  • Bộ nhớ RAM chứa rất nhiều ô nhớ
  • Mỗi ô nhớ có kích thước 1 byte
  • Mỗi ô nhớ có địa chỉ duy nhất
  • Địa chỉ được đánh số từ 0 trở đi
Memory Layout (bytes)
┌────────┬──────────┐
│ Địa chỉ│ Giá trị  │
├────────┼──────────┤
│   0    │          │
│   1    │          │
│   2    │          │
│   3    │          │
│   4    │          │
│   5    │          │
│   6    │          │
│   7    │          │
│  ...   │   ...    │
└────────┴──────────┘

1.2. Biến và Vùng Nhớ

Khi khai báo biến:

  • Máy tính dành riêng một vùng nhớ để lưu biến đó
  • Kích thước vùng nhớ phụ thuộc vào kiểu dữ liệu

Khi gọi tên biến:

  1. Tìm kiếm địa chỉ ô nhớ của biến
  2. Truy xuất hoặc thiết lập giá trị tại địa chỉ đó

Ví dụ:

int main() {
    char ch = 'x';
    int a = 7;
}
Memory Layout
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến     │ Giá trị  │
├────────┼──────────┼──────────┤
│  ...   │          │          │
│ 0x10   │  ch      │   'x'    │
│ 0x11   │          │          │
│ 0x12   │          │          │
│ 0x13   │          │          │
│ 0x14   │  a       │    7     │
│ 0x15   │  a       │          │
│ 0x16   │  a       │          │
│ 0x17   │  a       │          │
│  ...   │          │          │
└────────┴──────────┴──────────┘

1.3. Toán Tử & và *

Toán tử & (Address-of Operator):

Đặt trước tên biến và cho biết địa chỉ của vùng nhớ của biến.

&<tên_biến>

Toán tử * (Dereferencing/Indirection Operator):

Đặt trước một địa chỉ và cho biết giá trị lưu trữ tại địa chỉ đó.

*(<địa_chỉ>)

Ví dụ:

int value = 3200;
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến     │ Giá trị  │
├────────┼──────────┼──────────┤
│ 0x50   │  value   │   3200   │
└────────┴──────────┴──────────┘
cout << "value = " << value;
// Output: value = 3200

cout << "&value = " << &value;
// Output: &value = 0x50

cout << "*(&value) = " << *(&value);
// Output: *(&value) = 3200

2. Khái Niệm Con Trỏ

2.1. Định Nghĩa

Con trỏ (Pointer):

Là một biến lưu trữ địa chỉ của một địa chỉ bộ nhớ. Địa chỉ này thường là địa chỉ của một biến khác.

graph LR A[Biến con trỏ x] -->|chứa địa chỉ| B[Biến y] A -.->|"ta nói x 'trỏ tới' y"| B

Ví dụ minh họa:

┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến     │ Giá trị  │
├────────┼──────────┼──────────┤
│ 0x34   │    y     │   100    │
│  ...   │          │          │
│ 0x90   │    x     │   0x34   │ ← x trỏ tới y
└────────┴──────────┴──────────┘

2.2. Phân Loại Con Trỏ

Con trỏ được phân loại theo kiểu dữ liệu mà nó trỏ tới:

  • Con trỏ kiểu int: Trỏ tới biến kiểu int
  • Con trỏ kiểu float: Trỏ tới biến kiểu float
  • Con trỏ kiểu char: Trỏ tới biến kiểu char
  • Con trỏ kiểu double: Trỏ tới biến kiểu double

3. Khai Báo Con Trỏ

3.1. Cú Pháp Khai Báo

<kiểu_dữ_liệu> *<tên_biến_con_trỏ>;

Ví dụ:

char char1;
int *ptrI;
float *ptrF;
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến     │ Giá trị  │
├────────┼──────────┼──────────┤
│ 0x10   │  ptrF    │    ?     │
│  ...   │          │          │
│ 0x50   │  char1   │    ?     │
│  ...   │          │          │
│ 0x80   │  ptrI    │    ?     │
└────────┴──────────┴──────────┘

3.2. Khởi Tạo Con Trỏ

Cú pháp:

<kiểu_dữ_liệu> *<tên_con_trỏ> = &<tên_biến>;

Ví dụ đúng:

int a;
int *ptr = &a;  // ĐÚNG: Con trỏ int trỏ tới biến int
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến     │ Giá trị  │
├────────┼──────────┼──────────┤
│ 0x34   │    a     │    ?     │
│  ...   │          │          │
│ 0x90   │   ptr    │   0x34   │
└────────┴──────────┴──────────┘

Ví dụ sai:

double a;
int *ptr = &a;  // SAI: Không khớp kiểu!

4. Con Trỏ và Toán Tử & , *

4.1. Sử Dụng Toán Tử *

Truy xuất giá trị:

int a = 1000;
int *ptr = &a;

cout << ptr;     // 0x34 (địa chỉ của a)
cout << *ptr;    // 1000 (giá trị của a)

Thay đổi giá trị:

*ptr = 3200;     // Thay đổi giá trị của a
cout << a;       // 3200
cout << *ptr;    // 3200

(*ptr)++;        // Tăng giá trị của a lên 1
cout << a;       // 3201
Trước:                    Sau:
┌────────┬──────┐         ┌────────┬──────┐
│ 0x34   │ 1000 │ a       │ 0x34   │ 3201 │ a
├────────┼──────┤         ├────────┼──────┤
│ 0x90   │ 0x34 │ ptr     │ 0x90   │ 0x34 │ ptr
└────────┴──────┘         └────────┴──────┘

4.2. Ví Dụ Truy Vết

#include <iostream>
using namespace std;

int main() {
    int a;
    int *ptr;
    int value;
    
    a = 3200;
    ptr = &a;
    value = --(*ptr);
}

Truy vết:

Bước 1: a = 3200
┌────────┬──────┐
│ 0x34   │ 3200 │ a
├────────┼──────┤
│ 0x90   │  ?   │ ptr
├────────┼──────┤
│ 0x50   │  ?   │ value
└────────┴──────┘

Bước 2: ptr = &a
┌────────┬──────┐
│ 0x34   │ 3200 │ a
├────────┼──────┤
│ 0x90   │ 0x34 │ ptr
├────────┼──────┤
│ 0x50   │  ?   │ value
└────────┴──────┘

Bước 3: value = --(*ptr)
┌────────┬──────┐
│ 0x34   │ 3199 │ a  ← Giảm 1
├────────┼──────┤
│ 0x90   │ 0x34 │ ptr
├────────┼──────┤
│ 0x50   │ 3199 │ value ← Gán giá trị mới
└────────┴──────┘

Các giá trị:

value     = 3199
ptr       = 0x34
a         = 3199
&value    = 0x50
&ptr      = 0x90
&a        = 0x34
*ptr      = 3199
&(*ptr)   = 0x34
*(*ptr)   = ERROR! (3199 không phải địa chỉ)
*(&(*ptr))= 3199

5. Phép Gán Con Trỏ

5.1. Gán Con Trỏ Cho Con Trỏ

int *p1, *p2;
p2 = p1;  // p2 trỏ tới nơi mà p1 đang trỏ

Ví dụ:

int x = 27, y = 5;
int *p1 = &x;
int *p2 = &y;

p1 = p2;  // p1 bây giờ trỏ tới y
Trước:                    Sau:
┌────────┬──────┐         ┌────────┬──────┐
│ addr_x │  27  │ x       │ addr_x │  27  │ x
├────────┼──────┤         ├────────┼──────┤
│ addr_y │   5  │ y       │ addr_y │   5  │ y
├────────┼──────┤         ├────────┼──────┤
│        │addr_x│ p1      │        │addr_y│ p1 ← Thay đổi
├────────┼──────┤         ├────────┼──────┤
│        │addr_y│ p2      │        │addr_y│ p2
└────────┴──────┘         └────────┴──────┘

5.2. Gán Giá Trị Qua Con Trỏ

*p2 = *p1;  // Gán giá trị trỏ bởi p1 cho giá trị trỏ bởi p2

Ví dụ:

int x = 27, y = 5;
int *p1 = &x;
int *p2 = &y;

*p1 = *p2;  // x = y = 5
Trước:                    Sau:
┌────────┬──────┐         ┌────────┬──────┐
│ addr_x │  27  │ x       │ addr_x │   5  │ x ← Thay đổi
├────────┼──────┤         ├────────┼──────┤
│ addr_y │   5  │ y       │ addr_y │   5  │ y
├────────┼──────┤         ├────────┼──────┤
│        │addr_x│ p1      │        │addr_x│ p1
├────────┼──────┤         ├────────┼──────┤
│        │addr_y│ p2      │        │addr_y│ p2
└────────┴──────┘         └────────┴──────┘

6. Con Trỏ NULL

6.1. Khái Niệm

Con trỏ NULL:

Con trỏ không trỏ vào đâu cả.

int n;
int *p1 = &n;      // p1 trỏ tới n
int *p2;           // p2 chưa khởi tạo (nguy hiểm!)
int *p3 = NULL;    // p3 không trỏ vào đâu (an toàn)

Sử dụng:

int *ptr = NULL;

if (ptr == NULL) {
    cout << "Con tro chua tro toi bien nao";
}

6.2. Con Trỏ Chưa Khởi Tạo vs NULL

int *p1;        // unreferenced local variable (nguy hiểm)
int *p2 = NULL; // an toàn, biết rằng chưa trỏ vào đâu

7. Kích Thước Con Trỏ

Toán tử sizeof:

sizeof(<kiểu_dữ_liệu>)
sizeof(<tên_biến>)

Ví dụ:

int a;
double b;
char c;
int *pa;
double *pb;
char *pc;

// Kích thước biến
sizeof(a)       = 4   sizeof(int)     = 4
sizeof(b)       = 8   sizeof(double)  = 8
sizeof(c)       = 1   sizeof(char)    = 1

// Kích thước con trỏ
sizeof(pa)      = 4   sizeof(int*)    = 4
sizeof(pb)      = 4   sizeof(double*) = 4
sizeof(pc)      = 4   sizeof(char*)   = 4

8. Từ Khóa const và Con Trỏ

8.1. Con Trỏ Thường

int x;
int *p1 = &x;  // Non-const pointer to non-const int
  • Có thể thay đổi địa chỉ: p1 = &y;
  • Có thể thay đổi giá trị: *p1 = 10;

8.2. Con Trỏ Tới Hằng

const int *p2 = &x;  // Non-const pointer to const int
  • Có thể thay đổi địa chỉ: p2 = &y;
  • KHÔNG thể thay đổi giá trị: *p2 = 10;

8.3. Con Trỏ Hằng

int * const p3 = &x;  // Const pointer to non-const int
  • KHÔNG thể thay đổi địa chỉ: p3 = &y;
  • Có thể thay đổi giá trị: *p3 = 10;

8.4. Con Trỏ Hằng Tới Hằng

const int * const p4 = &x;  // Const pointer to const int
  • KHÔNG thể thay đổi địa chỉ: p4 = &y;
  • KHÔNG thể thay đổi giá trị: *p4 = 10;

Bảng tổng hợp:

LoạiThay đổi địa chỉThay đổi giá trị
int *p
const int *p
int * const p
const int * const p

9. Con Trỏ và Hàm

9.1. Truyền Tham Số Bằng Con Trỏ

Cách 1: Truyền giá trị (không thay đổi)

int NhapGiaTri() {
    int b;
    cout << "Nhap gia tri vao: ";
    cin >> b;
    return b;
}

int main() {
    int a;
    a = NhapGiaTri();
    cout << a;
}

Cách 2: Truyền con trỏ (có thể thay đổi)

void NhapGiaTri(int *b) {
    cout << "Nhap gia tri vao: ";
    cin >> *b;
}

int main() {
    int a;
    NhapGiaTri(&a);
    cout << a;
}

Cách 3: Truyền tham chiếu

void NhapGiaTri(int &b) {
    cout << "Nhap gia tri vao: ";
    cin >> b;
}

int main() {
    int a;
    NhapGiaTri(a);
    cout << a;
}

9.2. Hàm Hoán Vị

Sử dụng con trỏ:

void Swap(int *a, int *b) {
    int temp = *b;
    *b = *a;
    *a = temp;
}

int main() {
    int x = 7, y = 8;
    Swap(&x, &y);
    cout << "x = " << x << ", y = " << y;
    // Output: x = 8, y = 7
}

Sử dụng tham chiếu:

void Swap(int &a, int &b) {
    int temp = b;
    b = a;
    a = temp;
}

int main() {
    int x = 7, y = 8;
    Swap(x, y);
    cout << "x = " << x << ", y = " << y;
    // Output: x = 8, y = 7
}

10. Một Số Lưu Ý

10.1. Quy Tắc Quan Trọng

Với int a, *pa = &a;

*pa    a       // Cùng chỉ nội dung của biến a
pa     &a      // Cùng chỉ địa chỉ của biến a

10.2. Lỗi Thường Gặp

Lỗi 1: Sử dụng con trỏ chưa khởi tạo

int *pa;
*pa = 1904;  // SAI! pa chưa trỏ vào đâu

Đúng:

int a;
int *pa = &a;
*pa = 1904;  // ĐÚNG

Lỗi 2: Không khớp kiểu

double a;
int *ptr = &a;  // SAI! Không khớp kiểu

10.3. Khuyến Nghị

  1. Không dùng con trỏ khi chưa khởi tạo
  2. Khởi tạo con trỏ = NULL nếu chưa dùng
  3. Kiểm tra con trỏ != NULL trước khi dùng
  4. Đảm bảo kiểu con trỏ khớp với kiểu biến

11. Bài Tập

Bài Tập 1: Tìm Lỗi

int main() {
    int x, *p;
    x = 10;
    *p = x;  // LỖI: p chưa khởi tạo!
    return 0;
}

Sửa:

int main() {
    int x, *p;
    x = 10;
    p = &x;  // Khởi tạo p trước
    *p = 20; // Thay đổi giá trị x
    return 0;
}

Bài Tập 2: Truy Vết

#include <iostream>
using namespace std;

int main() {
    int i = 12;
    int *p1;
    p1 = &i;
    *p1 = 24;
    cout << *p1 << " " << i << endl;
}

Output: 24 24

Bài Tập 3: Hoán Vị Hai Số

Đề bài: Viết hàm hoán đổi giá trị của 2 tham số

Giải:

// Cách 1: Con trỏ
void Swap(int *a, int *b) {
    int temp = *b;
    *b = *a;
    *a = temp;
}

int main() {
    int x = 7, y = 8;
    Swap(&x, &y);
    cout << "x = " << x << ", y = " << y;
}

// Cách 2: Tham chiếu
void Swap(int &a, int &b) {
    int temp = b;
    b = a;
    a = temp;
}

int main() {
    int x = 7, y = 8;
    Swap(x, y);
    cout << "x = " << x << ", y = " << y;
}