Skip to content

🔧 Functions — Hàm trong C++

Functions là building blocks của mọi chương trình. Hiểu cách truyền tham số quyết định performance và tính đúng đắn của code.

Declaration vs Definition

Declaration (Prototype) — Chỉ khai báo

cpp
// Declaration: Nói "hàm này tồn tại"
// Thường đặt trong header file (.h, .hpp)
int add(int a, int b);                    // OK
double calculateArea(double radius);       // OK
void greet(std::string name);              // OK - tên param là optional
void process(int, int);                    // OK - không cần tên param

Definition — Cài đặt đầy đủ

cpp
// Definition: Cài đặt logic của hàm
// Thường đặt trong source file (.cpp)
int add(int a, int b) {
    return a + b;
}

double calculateArea(double radius) {
    return 3.14159 * radius * radius;
}

Tại sao tách Declaration và Definition?

┌─────────────────────────────────────────────────────────────────┐
│  math.h (Header)                 math.cpp (Source)              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  // Declaration only             #include "math.h"              │
│  int add(int a, int b);                                         │
│  int multiply(int a, int b);     // Definitions                 │
│                                  int add(int a, int b) {        │
│                                      return a + b;              │
│                                  }                              │
│                                                                 │
│                                  int multiply(int a, int b) {   │
│                                      return a * b;              │
│                                  }                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘


          main.cpp chỉ cần #include "math.h"
          → Compile nhanh hơn (không cần recompile math.cpp nếu main thay đổi)

Pass by Value — Truyền bản sao

cpp
#include <iostream>

void doubleValue(int x) {
    x = x * 2;  // Chỉ modify bản sao local
    std::cout << "Inside function: x = " << x << std::endl;
}

int main() {
    int num = 10;
    std::cout << "Before: num = " << num << std::endl;
    
    doubleValue(num);  // Truyền bản sao của num
    
    std::cout << "After: num = " << num << std::endl;
    
    return 0;
}

Output:

Before: num = 10
Inside function: x = 20
After: num = 10        ← num không thay đổi!

Ưu/Nhược điểm Pass by Value

✅ Ưu điểm❌ Nhược điểm
An toàn — không thay đổi originalTốn memory cho copy
Dễ hiểu, predictableChậm với objects lớn
Thread-safe (mỗi thread có bản sao riêng)

Pass by Reference — Truyền tham chiếu

cpp
#include <iostream>

void doubleValue(int& x) {  // Note: int& (reference)
    x = x * 2;  // Modify trực tiếp biến gốc!
}

int main() {
    int num = 10;
    std::cout << "Before: num = " << num << std::endl;
    
    doubleValue(num);  // Truyền reference
    
    std::cout << "After: num = " << num << std::endl;
    
    return 0;
}

Output:

Before: num = 10
After: num = 20        ← num đã thay đổi!

Pass by Const Reference — Read-only, không copy

cpp
#include <iostream>
#include <string>
#include <vector>

// ❌ BAD: Copy toàn bộ vector (có thể hàng triệu elements!)
void printBad(std::vector<int> v) {
    for (const auto& x : v) {
        std::cout << x << " ";
    }
}

// ✅ GOOD: Không copy, chỉ reference, không modify
void printGood(const std::vector<int>& v) {
    for (const auto& x : v) {
        std::cout << x << " ";
    }
    // v.push_back(100);  // ❌ Error: v is const
}

// ✅ GOOD: Với string
void greet(const std::string& name) {
    std::cout << "Hello, " << name << "!" << std::endl;
}

📌 HPN Standard: Khi nào dùng gì?

SituationSyntaxReason
Modify originalT&Thay đổi biến gốc
Read large objectconst T&Tránh copy, không modify
Small primitivesT (by value)Copy rẻ, đơn giản
Optional/nullableT* (pointer)Có thể nullptr

Golden Rule: Mặc định dùng const T& cho objects. Chỉ dùng by-value cho primitives (int, double, char, bool).


Pass by Pointer vs Reference

cpp
#include <iostream>

// Pass by pointer — Có thể null
void processPtr(int* ptr) {
    if (ptr != nullptr) {
        *ptr = 100;
    }
}

// Pass by reference — Guaranteed non-null
void processRef(int& ref) {
    ref = 100;
}

int main() {
    int x = 42;
    
    processPtr(&x);  // Phải dùng &
    std::cout << "After ptr: x = " << x << std::endl;
    
    processRef(x);   // Không cần &, tự động bind
    std::cout << "After ref: x = " << x << std::endl;
    
    // Pointer có thể null
    processPtr(nullptr);  // OK, hàm handle được
    
    // Reference KHÔNG thể null
    // processRef(nullptr);  // ❌ Compiler error
    
    return 0;
}
AspectPointer (T*)Reference (T&)
Nullable?✅ Có thể nullptr❌ Không thể
Rebindable?ptr = &other;❌ Một lần mãi mãi
Syntax*ptr để accessTrực tiếp ref
Use caseOptional paramsRequired params

Default Arguments

cpp
#include <iostream>
#include <string>

// Default arguments: Từ phải sang trái!
void greet(const std::string& name, const std::string& greeting = "Hello") {
    std::cout << greeting << ", " << name << "!" << std::endl;
}

// ❌ Invalid: default argument không ở cuối
// void bad(int a = 10, int b) { ... }

int main() {
    greet("Alice");              // Hello, Alice!
    greet("Bob", "Xin chào");    // Xin chào, Bob!
    
    return 0;
}

Function Overloading

cpp
#include <iostream>
#include <string>

// Cùng tên, khác parameters
int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

std::string add(const std::string& a, const std::string& b) {
    return a + b;
}

int main() {
    std::cout << add(1, 2) << std::endl;           // 3 (int version)
    std::cout << add(1.5, 2.5) << std::endl;       // 4.0 (double version)
    std::cout << add("Hello, ", "World!") << std::endl;  // Hello, World!
    
    return 0;
}

⚠️ Overloading không dựa trên return type

cpp
int getValue();
double getValue();  // ❌ Error: chỉ khác return type

// Compiler không biết gọi cái nào khi:
// auto x = getValue();

Return by Value, Reference, and Pointer

cpp
#include <iostream>
#include <vector>
#include <string>

// Return by value — OK cho small objects
int getAnswer() {
    return 42;
}

// Return by reference — DANGEROUS nếu return local!
// ❌ BAD: Dangling reference
std::string& badGetName() {
    std::string name = "Alice";
    return name;  // name bị destroy sau khi function ends!
}

// ✅ GOOD: Return reference đến parameter hoặc member
std::string& getLonger(std::string& a, std::string& b) {
    return (a.size() > b.size()) ? a : b;
}

// ✅ GOOD: Return by value (Move semantics xử lý efficiency)
std::vector<int> createVector() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    return v;  // Move, không copy (C++11)
}

🐛 Bug Hunt Challenge

🐛 Bug Hunt

Đoạn code sau có bug gì?

cpp
#include <iostream>

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

int main() {
    int x = 10, y = 20;
    swap(x, y);
    std::cout << "x = " << x << ", y = " << y << std::endl;
    return 0;
}
💡 Gợi ý

Hàm swap nhận parameters như thế nào?

🔍 Giải thích & Fix

Output: x = 10, y = 20 — Không swap!

Bug: Parameters ab được pass by value → chỉ swap bản copy.

Fix:

cpp
void swap(int& a, int& b) {  // Pass by reference
    int temp = a;
    a = b;
    b = temp;
}

Hoặc dùng std::swap:

cpp
#include <utility>
std::swap(x, y);

📚 Tổng kết

ConceptSyntaxUse Case
Pass by valuefunc(T x)Small types, cần copy
Pass by referencefunc(T& x)Modify original
Pass by const reffunc(const T& x)Read-only, avoid copy
Pass by pointerfunc(T* x)Nullable, optional
Default argumentsfunc(int x = 10)Optional params
OverloadingSame name, diff paramsPolymorphism

➡️ Tiếp theo

Tiếp theo: Scope & Lifetime — Local, Global, và Static variables.

🧠 Quiz

Câu 1: Đoạn code sau in ra kết quả gì?

cpp
void change(int x) { x = 100; }
int main() {
    int a = 5;
    change(a);
    std::cout << a;
}
  • [x] A) 5
  • [ ] B) 100
  • [ ] C) 0
  • [ ] D) Lỗi biên dịch

💡 Giải thích: Hàm change nhận tham số pass-by-value, nghĩa là x là bản copy của a. Thay đổi x bên trong hàm không ảnh hưởng đến a gốc. Muốn thay đổi a, cần dùng void change(int& x).

Câu 2: Hàm nào sau đây là overloading hợp lệ với int calc(int a, int b)?

  • [ ] A) double calc(int a, int b)
  • [x] B) int calc(int a, int b, int c)
  • [ ] C) int calc(int x, int y)
  • [ ] D) long calc(int a, int b)

💡 Giải thích: Function overloading yêu cầu danh sách tham số khác nhau (số lượng hoặc kiểu). Chỉ khác return type (A, D) hoặc chỉ khác tên tham số (C) là KHÔNG đủ. Lựa chọn B có 3 tham số thay vì 2, nên hợp lệ.

Câu 3: Đoạn code sau có vấn đề gì?

cpp
void print(int x = 10, int y) {
    std::cout << x + y;
}
  • [ ] A) Không có vấn đề gì
  • [ ] B) Thiếu return type
  • [x] C) Default argument phải từ phải sang trái
  • [ ] D) Không thể có 2 tham số

💡 Giải thích: Trong C++, default arguments phải bắt đầu từ tham số bên phải nhất. Nếu x có default value thì y cũng phải có. Cách đúng: void print(int x, int y = 0) hoặc void print(int x = 10, int y = 0).