Chương 0: Giới thiệu môn học

Mục tiêu

Trang bị kiến thức và kỹ năng về lập trình hướng đối tượng, các nguyên lý cơ bản của thiết kế hướng đối tượng, bao gồm: cây thừa kế, đa hình, tính chất đối tượng, phân lớp và cách thức trao đổi giữa các đối tượng.

Nội dung môn học

  • Chương 1: Các đặc điểm của C++
  • Chương 2: Tổng quan về lập trình HĐT
  • Chương 3: Lớp và đối tượng
  • Chương 4: Khởi tạo đối tượng, hàm bạn, lớp bạn
  • Chương 5: Tái định nghĩa toán tử
  • Chương 6: Tính kế thừa
  • Chương 7: Tính đa hình
  • Chương 8: Một số vấn đề khác

Chương 1: Tổng quan về C++


1. Phong cách lập trình

Phong cách lập trình tốt giúp code dễ đọc, dễ bảo trì. Một số quy tắc cần tuân thủ:

  • Đặt tên biến/hàm: dùng camelCase hoặc snake_case nhất quán, tên phải có nghĩa.
  • Xuống hàng: mỗi câu lệnh một dòng, không viết dồn.
  • Dùng Tab và {}: thụt lề đúng chuẩn, mở/đóng ngoặc rõ ràng.
  • Khai báo prototype (nguyên mẫu hàm): nếu hàm được định nghĩa sau lời gọi hàm thì bắt buộc khai báo prototype trước.
// Khai báo prototype trước
int tinhTong(int a, int b);

int main() {
    cout << tinhTong(3, 4); // Gọi trước khi định nghĩa — OK vì có prototype
    return 0;
}

// Định nghĩa hàm sau
int tinhTong(int a, int b) {
    return a + b;
}

2. Lịch sử ngôn ngữ lập trình

timeline title Lịch sử ngôn ngữ lập trình 1957 : FORTRAN 1960 : COBOL · LISP · ALGOL 60 1965 : SIMULA · BASIC 1967 : SIMULA 67 (nền tảng OOP) 1970 : PASCAL · PROLOG 1972 : C 1980 : Smalltalk 80 · ADA 1985 : C++ · Eiffel 1995 : Java

Ghi chú: SIMULA 67 là ngôn ngữ đầu tiên giới thiệu khái niệm classobject, đặt nền móng cho OOP hiện đại. C++ được Bjarne Stroustrup phát triển từ C, bổ sung các tính năng hướng đối tượng.


3. C và C++

So sánh lập trình cấu trúc vs hướng đối tượng

flowchart LR subgraph C["Lập trình cấu trúc (C)"] direction TB F1[Hàm/Thủ tục] --> D1[Dữ liệu] F2[Hàm/Thủ tục] --> D1 note1["Dữ liệu và xử lý tách rời\nTrao đổi qua tham số & biến toàn cục"] end subgraph CPP["Lập trình HĐT (C++)"] direction TB CL1["Lớp A\n(data + methods)"] CL2["Lớp B\n(data + methods)"] CL1 <--> CL2 note2["Dữ liệu và phương thức đóng gói cùng nhau\nTrao đổi qua giao tiếp giữa đối tượng"] end
Tiêu chíLập trình cấu trúc (C)Lập trình HĐT (C++)
Tổ chứcHàm/thủ tụcLớp (class)
Dữ liệuTách rời khỏi hàmĐóng gói trong lớp
Trao đổiTham số, biến toàn cụcThông điệp giữa đối tượng
Tái sử dụngHạn chếCao (kế thừa, đa hình)

C++ là ngôn ngữ lai (hybrid): vừa hỗ trợ lập trình cấu trúc (như C), vừa hỗ trợ lập trình hướng đối tượng. Khác với Smalltalk là ngôn ngữ thuần OOP.

Các mở rộng của C++ so với C

Tính năngCC++
Comment một dòngKhông có// comment
Ép kiểu(float)iThêm float(i+1)
Khai báo biếnPhải đặt đầu blockĐặt bất kỳ đâu trước khi dùng
Hằng số#define MAX 100const int MAX = 100;
// Hằng số với const
const int MAXSIZE = 1000;
int a[MAXSIZE]; // hợp lệ

// Có thể dùng hàm để khởi gán hằng
const int CENTERX = getmaxx() / 2;

4. Ngôn ngữ C++

Môi trường biên dịch

flowchart TD A["Soạn thảo (Editor)\n.cpp file"] --> B["Tiền xử lý (Preprocessor)\nXử lý #include, #define,..."] B --> C["Biên dịch (Compiler)\nSinh ra .obj / .o"] C --> D["Liên kết (Linker)\nKết hợp object + thư viện → a.out / .exe"] D --> E["Nạp vào bộ nhớ (Loader)"] E --> F["Thực thi (CPU Execute)"]

4.1 Nhập xuất trong C++

C++ sử dụng stream (luồng dữ liệu) thay vì printf/scanf của C.

#include <iostream>
using namespace std;
Câu lệnhChức năng
cin >> bienNhập từ bàn phím
cout << bieuthucXuất ra màn hình
cout << endlXuống dòng + flush buffer
// Ví dụ 1: Hello World
#include <iostream>
using namespace std;

int main() {
    cout << "Welcome to C++!" << endl;
    return 0;
}
// Ví dụ 2: Nhập và tính tổng hai số nguyên
#include <iostream>
using namespace std;

int main() {
    int a, b;
    cout << "Nhap so thu nhat: ";
    cin >> a;
    cout << "Nhap so thu hai: ";
    cin >> b;
    cout << "Tong = " << a + b << endl;
    return 0;
}

Nhập chuỗi có khoảng trắng

// Cách 1: dùng mảng char
char hoTen[50];
cin.getline(hoTen, 50);     // đọc đến '\n', bỏ '\n'
// hoặc: cin.get(hoTen, 50, '\n');

// Cách 2: dùng string (khuyến dùng)
#include <string>
string s;
getline(cin, s);

Định dạng xuất với <iomanip>

#include <iomanip>

// setprecision: số chữ số thập phân, có hiệu lực cho tất cả cout sau đó
cout << setiosflags(ios::showpoint) << setprecision(4);
cout << 3.14159; // xuất: 3.1416

// setw: độ rộng tối thiểu, CHỈ có hiệu lực cho 1 giá trị gần nhất
cout << setw(10) << 42;    // "        42" (căn phải, 10 ký tự)
cout << setw(10) << 100;   // vẫn áp dụng setw vì đây là lần xuất tiếp

4.2 Toán tử phạm vi ::

Toán tử :: (scope resolution operator) có 3 công dụng chính:

1. Truy cập biến toàn cục khi bị biến cục bộ che khuất:

int x = 100; // biến toàn cục

int main() {
    int x = 5;       // biến cục bộ
    cout << x;       // in 5 (cục bộ)
    cout << ::x;     // in 100 (toàn cục)
    return 0;
}

2. Chỉ định phương thức thuộc lớp nào (khi viết bên ngoài lớp):

class HinhTron {
public:
    void tinhDienTich(); // khai báo trong lớp
};

// Định nghĩa bên ngoài — dùng :: để chỉ rõ thuộc lớp HinhTron
void HinhTron::tinhDienTich() {
    // ...
}

3. Chỉ định namespace:

using std::cout;
using std::cin;

Ví dụ minh họa sự khác nhau giữa floatdouble:

const double PI = 3.14159265358979; // toàn cục

int main() {
    const float PI = static_cast<float>(::PI); // cục bộ, ép kiểu từ toàn cục

    cout << setprecision(20);
    cout << "Local float PI  = " << PI   << endl;
    // → 3.141592741012573242   (float kém chính xác hơn)
    cout << "Global double PI = " << ::PI << endl;
    // → 3.141592653589790007
}

4.3 Các kiểu dữ liệu trong C++

flowchart TD KDL["Kiểu dữ liệu C++"] KDL --> Simple["Đơn giản (Simple)"] KDL --> Structured["Có cấu trúc (Structured)"] KDL --> Address["Địa chỉ (Address)"] Simple --> Integral["Nguyên (Integral)"] Simple --> Float["Thực (Floating)"] Integral --> t1["char · short · int · long · bool · enum"] Float --> t2["float · double · long double"] Structured --> t3["array · struct · union · class"] Address --> t4["pointer · reference"]

Struct và cách sử dụng

// Định nghĩa kiểu struct
struct TS {
    char hoTen[25];
    long soBaoDanh;
};

TS a;           // biến cấu trúc
TS ts[1000];    // mảng cấu trúc

// Truy xuất trường
cin >> a.hoTen >> a.soBaoDanh;

// Qua con trỏ
TS *p = &a;
cout << p->hoTen;

Enum (kiểu liệt kê)

enum MAU { xanh, do, tim, vang };
// xanh=0, do=1, tim=2, vang=3 (mặc định)

MAU mauHoa = do;

typedef — đặt tên mới cho kiểu

typedef struct {
    int tu, mau;
} PS; // PS là tên kiểu phân số

PS p; // khai báo biến phân số

typedef int MT[20][20];
MT m; // mảng 2 chiều 20x20

Struct mô tả Phân số, Ma trận, Vector

// Phân số
struct PS {
    int tu, mau;
};

// Ma trận
struct MT {
    double a[20][20]; // các phần tử
    int m;            // số dòng
    int n;            // số cột
};

// Vector
struct VT {
    double b[20]; // các phần tử
    int n;        // số phần tử
};

4.4 Cấp phát bộ nhớ động

C++ dùng newdelete thay cho malloc/free của C.

// Cấp phát 1 phần tử
float *p = new float;
*p = 3.14;

// Cấp phát mảng n phần tử
int *pn = new int[100];
pn[0] = 10;

// Giải phóng bộ nhớ
delete p;       // cho 1 phần tử
delete[] pn;    // cho mảng

4.5 Hàm trong C++


4.5.1 Đối số có giá trị mặc định

Cho phép gán sẵn giá trị cho tham số hàm. Khi gọi hàm, nếu không truyền giá trị cho tham số đó thì giá trị mặc định sẽ được dùng.

Quy tắc: Các tham số có giá trị mặc định phải đặt ở cuối danh sách tham số (từ phải sang trái).

// Khai báo prototype — gán mặc định ở đây
void delay(int n = 1000);

// Định nghĩa — KHÔNG gán lại
void delay(int n) {
    // ...
}
void f(int d1, float d2,
       char *d3 = "Ha Noi",
       int d4 = 100,
       double d5 = 3.14);

// Lời gọi hợp lệ:
f(3, 3.4);                    // d3="Ha Noi", d4=100, d5=3.14
f(3, 3.4, "ABC");             // d4=100, d5=3.14
f(3, 3.4, "ABC", 10);        // d5=3.14
f(3, 3.4, "ABC", 10, 1.0);   // đầy đủ

// Lời gọi SAI:
f(3);  // thiếu d2 (không có mặc định)

4.5.2 Biến tham chiếu (Reference)

C++ có 3 loại biến:

LoạiMô tảVí dụ
Biến giá trịChứa dữ liệu trực tiếpint x = 10;
Biến con trỏChứa địa chỉ của biến khácint *px = &x;
Biến tham chiếuBí danh (alias) cho biến giá trịint &y = x;
int x = 10;
int *px = &x;   // con trỏ
int &y = x;     // tham chiếu: y là bí danh của x

y = 30;
cout << x; // in 30 — x thay đổi theo y vì cùng vùng nhớ

Đặc điểm quan trọng của biến tham chiếu:

  • Không được cấp phát vùng nhớ riêng — dùng chung vùng nhớ với biến gốc.
  • Phải được khởi tạo ngay khi khai báo (không thể khai báo rồi gán sau).
  • Có thể tham chiếu đến phần tử mảng: int &r = a[0];
  • Không được khai báo mảng tham chiếu.
int x = 3;
int &y = x;  // OK

int &z;      // LỖI BIÊN DỊCH: phải khởi tạo ngay

4.5.3 Truyền tham số: Giá trị vs Tham chiếu

// Swap SAI — truyền giá trị, không ảnh hưởng bên ngoài
void swap1(int x, int y) {
    int t = x; x = y; y = t;
    // x, y chỉ là bản sao — biến gốc không đổi
}

// Swap ĐÚNG một phần — đổi chỗ con trỏ, không đổi giá trị
void swap2(int *x, int *y) {
    int *t = x; x = y; y = t;
    // con trỏ cục bộ thay đổi — biến gốc vẫn không đổi
}

// Swap ĐÚNG — truyền tham chiếu
void swap3(int &x, int &y) {
    int t = x; x = y; y = t;
    // x, y là alias của biến gốc — giá trị gốc thay đổi
}
flowchart LR subgraph ThamTri["Truyền tham trị"] main1["main: a=5, b=3"] -->|"bản sao"| f1["swap1: x=5, y=3"] f1 -->|"x,y đổi"| f1b["swap1: x=3, y=5"] f1b -->|"hàm kết thúc"| main1b["main: a=5, b=3 (không đổi)"] end subgraph ThamChieu["Truyền tham chiếu"] main2["main: a=5, b=3"] -->|"alias"| f2["swap3: x↔a, y↔b"] f2 -->|"x,y đổi = a,b đổi"| main2b["main: a=3, b=5"] end
Truyền tham trịTruyền tham chiếu
Thay đổi biến gốcKhông
Bộ nhớTạo bản saoDùng trực tiếp
Dùng khiKhông muốn thay đổi gốcMuốn thay đổi hoặc tránh copy lớn

4.5.4 Con trỏ hàm

Con trỏ hàm lưu trữ địa chỉ của một hàm, cho phép gọi hàm một cách linh hoạt (hữu ích cho callback, bảng dispatch).

// Khai báo con trỏ hàm: kiểu trả về (*tên)(danh sách tham số)
double (*f)(double*, int);

// Ví dụ
double tinh_max(double *a, int n) { /* ... */ }
f = tinh_max;   // gán địa chỉ hàm
f(arr, 5);      // gọi qua con trỏ
// Ứng dụng: truyền hàm như tham số
double nhandoi(double x) { return x * 2; }
double nhanba(double x)  { return x * 3; }

void tinh(double (*f)(double), double x) {
    cout << f(x) << endl;
}

int main() {
    tinh(nhandoi, 2); // in 4
    tinh(nhanba, 3);  // in 9
    return 0;
}

4.5.5 Inline Function

Khi gọi một hàm thông thường, CPU phải: cấp phát bộ nhớ cho tham số, truyền dữ liệu, nhảy đến địa chỉ hàm, thực thi, rồi quay về. Đây là overhead (chi phí gọi hàm).

inline yêu cầu compiler chèn thẳng code của hàm vào nơi gọi, loại bỏ overhead này.

inline float sqr(float x) {
    return x * x;
}

inline int Max(int a, int b) {
    return (a > b) ? a : b;
}

int main() {
    float r = sqr(5.0); // compiler chèn: float r = 5.0 * 5.0;
}

4.5.6 Định nghĩa chồng hàm (Function Overloading)

Cho phép nhiều hàm cùng tên nhưng khác nhau về danh sách tham số (số lượng, kiểu, thứ tự).

int abs(int i);
long abs(long l);
double abs(double d);

// Compiler chọn hàm phù hợp dựa trên kiểu tham số
abs(123);    // → abs(int i)
abs(123L);   // → abs(long l)
abs(3.14);   // → abs(double d)
abs('A');    // → abs(int i)  — char ép kiểu sang int

Ví dụ thực tế — hàm Max cho nhiều kiểu:

int Max(int a, int b) {
    return (a > b) ? a : b;
}

float Max(float a, float b) {
    return (a > b) ? a : b;
}

int main() {
    cout << Max(1, 2)       << endl; // gọi Max(int, int)
    cout << Max(3.0f, 4.0f) << endl; // gọi Max(float, float)
}

4.5.7 Định nghĩa chồng toán tử (Operator Overloading)

C++ cho phép định nghĩa lại ý nghĩa của các toán tử (+, -, *, …) cho kiểu dữ liệu tự định nghĩa.

Cú pháp:

KieuTraVe operator<ten_toan_tu>(danh_sach_tham_so) { ... }

Ví dụ — phép toán trên phân số:

struct PS {
    int tu, mau;
};

PS operator+(PS p1, PS p2) {
    PS kq;
    kq.tu  = p1.tu * p2.mau + p2.tu * p1.mau;
    kq.mau = p1.mau * p2.mau;
    return kq;
}

PS operator*(PS p1, PS p2) {
    PS kq;
    kq.tu  = p1.tu * p2.tu;
    kq.mau = p1.mau * p2.mau;
    return kq;
}

int main() {
    PS a = {1, 2};  // 1/2
    PS b = {1, 3};  // 1/3
    PS c = a + b;   // gọi operator+(a, b) → 5/6
    PS d = a * b;   // gọi operator*(a, b) → 1/6
}

Bài tập & Lời giải


Bài tập 1: Tìm lỗi sai trong các prototype

int func1 (int);
float func1 (int);        // (1)
int func1 (float);        // (2)
void func1 (int = 0, int); // (3)
void func2 (int, int = 0); // (4)
void func2 (int);          // (5)
void func2 (float);        // (6)

Bài tập 2: Cho biết kết xuất

void func(int i, int j = 0) {
    cout << "So nguyen: " << i << " " << j << endl;
}

void func(float i = 0, float j = 0) {
    cout << "So thuc: " << i << " " << j << endl;
}

int main() {
    int i = 1, j = 2;
    float f = 1.5, g = 2.5;
    func();
    func(i);
    func(f);
    func(i, j);
    func(f, g);
}

Bài tập 3: Bài toán phân số và ngày tháng


Bài tập 4: Quản lý nhân viên