Tham số trong C: Cách sử dụng tham số, truyền tham số và thực tiễn lập trình hiệu quả

1. Cơ bản về tham số trong ngôn ngữ C

Tham số là gì?

Tham số là dữ liệu được truyền từ bên ngoài vào hàm khi hàm được gọi. Việc sử dụng tham số giúp hàm nhận các giá trị đầu vào khác nhau và xử lý dựa trên những giá trị đó. Việc thành thạo cách sử dụng tham số trong C là điều cần thiết để tăng khả năng tái sử dụng và linh hoạt cho chương trình.

Tham số thực và tham số hình thức

Giá trị được cung cấp ở phía gọi hàm gọi là tham số thực, còn giá trị được nhận trong định nghĩa hàm gọi là tham số hình thức. Ví dụ, trong PrintScore(score); thì score là tham số thực, còn score trong void PrintScore(int score) là tham số hình thức. Để sử dụng hàm đúng, việc hiểu sự khác biệt này là rất quan trọng.

2. Sự khác biệt giữa tham số thực và tham số hình thức

Tham số thực

Tham số thực là giá trị thực tế được truyền vào khi gọi hàm. Ví dụ, trong PrintScore(100);, 100 là tham số thực. Tham số thực được chuyển vào hàm và sử dụng trong hàm đó.

Tham số hình thức

Tham số hình thức là tên tạm thời cho dữ liệu nhận trong phần định nghĩa hàm. Tham số hình thức tham chiếu đến giá trị của tham số thực bên trong hàm, nhưng không thể thay đổi giá trị đó bên ngoài hàm. Ví dụ, score trong void PrintScore(int score) là tham số hình thức.

侍エンジニア塾

3. Cách truyền tham số

Truyền theo giá trị

Truyền theo giá trị là phương pháp sao chép giá trị của tham số thực sang tham số hình thức. Khi đó, việc thay đổi giá trị tham số hình thức trong hàm sẽ không ảnh hưởng tới tham số thực bên ngoài. Xem ví dụ sau:

void LevelUp(int lv) {
    lv++;
}

int main() {
    int level = 1;
    LevelUp(level);
    printf("Level: %dn", level); // Kết quả: Level: 1
}

Trong ví dụ này, lv trong hàm LevelUp được tăng lên, nhưng giá trị của level trong hàm main không thay đổi. Ưu điểm của truyền theo giá trị là bảo vệ dữ liệu gốc, tuy nhiên khi truyền dữ liệu lớn, việc này có thể tốn bộ nhớ hơn.

Truyền theo con trỏ

Truyền theo con trỏ là cách truyền địa chỉ của tham số thực vào tham số hình thức. Phương pháp này cho phép thay đổi trực tiếp giá trị của tham số thực trong hàm.

void LevelUp(int *plv) {
    (*plv)++;
}

int main() {
    int level = 1;
    LevelUp(&level);
    printf("Level: %dn", level); // Kết quả: Level: 2
}

Ở ví dụ này, giá trị level được thay đổi trực tiếp trong hàm LevelUp. Ưu điểm của truyền con trỏ là có thể thay đổi nhiều giá trị hoặc trả về kết quả từ hàm. Tuy nhiên, thao tác sai với con trỏ dễ gây lỗi hoặc rò rỉ bộ nhớ, cần sử dụng cẩn thận.

4. Kết hợp số lượng tham số và giá trị trả về

Có tham số, không trả về giá trị

Đây là ví dụ hàm nhận tham số nhưng không trả về giá trị. Ví dụ: void PrintScore(int score) nhận tham số score để hiển thị nhưng không trả về gì cả.

Không có tham số, có giá trị trả về

Ví dụ về hàm không nhận tham số nhưng trả về giá trị xử lý. int GetCurrentScore() là hàm tính toán và trả về điểm hiện tại.

Có tham số, có giá trị trả về

Đây là kiểu hàm nhận tham số và trả về kết quả. Ví dụ: int Add(int a, int b) nhận hai số và trả về tổng của chúng. Loại hàm này rất linh hoạt và thường được sử dụng trong nhiều tình huống khác nhau.

5. Đệ quy và tham số

Đệ quy là gì?

Đệ quy là kỹ thuật một hàm tự gọi lại chính nó. Đệ quy rất hiệu quả khi giải quyết các vấn đề bằng cách chia nhỏ ra, nhưng nếu không kiểm soát đúng sẽ dễ gây tràn bộ nhớ (stack overflow).

Ví dụ về đệ quy

Dưới đây là ví dụ đệ quy sử dụng tham số để chia số cho 2 liên tục:

int funcA(int num) {
    if(num % 2 != 0) {
        return num;
    }
    return funcA(num / 2);
}

int main() {
    int result = funcA(20);
    printf("Result: %dn", result); // Kết quả: Result: 5
}

Ở ví dụ này, hàm funcA tự gọi lại chính nó và lặp lại xử lý với tham số truyền vào. Khi sử dụng đệ quy, phải chú ý đặt điều kiện dừng để tránh lặp vô hạn.

6. Macro dạng hàm và tham số

Macro dạng hàm là gì?

Macro dạng hàm là macro có tham số, mã sẽ được thay thế ngay tại thời điểm biên dịch. Nhờ đó, hiệu năng chương trình được cải thiện.

Ví dụ macro dạng hàm

Dưới đây là ví dụ macro dạng hàm dùng để lấy số phần tử của mảng:

#define ARRAY_SIZE(array) (sizeof(array) / sizeof(array[0]))

int main() {
    int arr[10];
    printf("Array size: %dn", ARRAY_SIZE(arr)); // Kết quả: Array size: 10
}

Macro dạng hàm thay thế mã nguồn trước khi chương trình chạy, nên không có overhead thực thi. Tuy nhiên, do không kiểm tra kiểu dữ liệu, nên dùng sai dễ gây lỗi bất ngờ.

7. Hàm thư viện chuẩn C và tham số

Sử dụng hàm thư viện chuẩn

Ngôn ngữ C có rất nhiều hàm thư viện chuẩn, hầu hết đều sử dụng tham số để xử lý. Ví dụ, hàm printf nhận tham số với số lượng thay đổi để in dữ liệu theo định dạng chỉ định.

Ví dụ về hàm thư viện chuẩn

Dưới đây là ví dụ sử dụng hàm printf:

printf("Name: %s, Age: %dn", "Alice", 30); // Kết quả: Name: Alice, Age: 30

Ở ví dụ này, hàm printf dùng tham số để in chuỗi và số. Sử dụng hàm thư viện giúp code dễ đọc, dễ bảo trì và hiệu quả hơn.

8. Tổng kết

Sử dụng tham số biến đổi (variadic arguments)

Trong C, có thể định nghĩa hàm nhận số lượng tham số thay đổi nhờ tham số biến đổi (variadic). Dùng dấu ba chấm ... trong khai báo hàm. Điều này cho phép hàm nhận số lượng tham số không cố định. Ví dụ tiêu biểu là printf có thể nhận số tham số khác nhau tùy định dạng.

Ví dụ tham số biến đổi

Dưới đây là ví dụ hàm nhận nhiều số nguyên và tính tổng:

#include <stdarg.h>
#include <stdio.h>

int sum(int count, ...) {
    va_list args;
    va_start(args, count);
    int total = 0;

    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);
    }

    va_end(args);
    return total;
}

int main() {
    printf("Sum: %dn", sum(4, 1, 2, 3, 4)); // Kết quả: Sum: 10
}

Ở ví dụ này, hàm sum nhận nhiều số và trả về tổng. Có thể dùng macro như va_list, va_start, va_arg, va_end để xử lý tham số biến đổi.

Lưu ý khi dùng tham số biến đổi

Khi dùng tham số biến đổi, cần cẩn trọng với kiểu và số lượng tham số được truyền. Nếu không khớp giữa phía gọi và phía định nghĩa hàm, có thể gây lỗi hoặc chương trình dừng bất ngờ.

Ứng dụng thực tế của tham số

Cách dùng tham số hiệu quả

Khai thác tham số hợp lý giúp code dễ đọc và dễ tái sử dụng. Khi nhiều hàm xử lý chung một dữ liệu, nên truyền qua tham số thay vì dùng biến toàn cục để đảm bảo tính độc lập và hạn chế ảnh hưởng lên phần còn lại của chương trình.

Tối ưu bộ nhớ và hiệu năng

Khi truyền dữ liệu lớn vào hàm, sử dụng con trỏ sẽ tiết kiệm bộ nhớ hơn. Nếu truyền mảng hoặc struct lớn theo giá trị, toàn bộ dữ liệu sẽ bị sao chép; còn truyền theo con trỏ chỉ truyền địa chỉ, tiết kiệm tài nguyên.

Best practice khi viết hàm

Khi viết hàm, nên xác định số lượng và kiểu tham số thật hợp lý. Truyền dư tham số làm phức tạp hàm và dễ gây lỗi; ngược lại, truyền đủ thông tin cần thiết giúp code rõ ràng, dễ bảo trì.

9. Kỹ thuật nâng cao liên quan đến tham số

Hàm callback

Hàm callback là kỹ thuật truyền một hàm như tham số cho hàm khác và gọi hàm đó trong hàm nhận. Cách này giúp xử lý linh hoạt, đặc biệt phù hợp với lập trình hướng sự kiện hoặc xử lý bất đồng bộ.

#include <stdio.h>

void executeCallback(void (*callback)(int)) {
    callback(10);
}

void printValue(int val) {
    printf("Value: %dn", val);
}

int main() {
    executeCallback(printValue); // Kết quả: Value: 10
}

Ví dụ này truyền hàm printValue làm callback và thực thi trong executeCallback.

Con trỏ hàm

Con trỏ hàm cho phép xử lý hàm như một biến. Bạn có thể truyền hàm qua tham số hoặc gọi các hàm khác nhau tại runtime. Cách này rất linh hoạt cho các tình huống động.

#include <stdio.h>

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

int main() {
    int (*operation)(int, int) = add;
    printf("Result: %dn", operation(2, 3)); // Kết quả: Result: 5
}

Ở đây, hàm add được gán cho biến con trỏ hàm operation và gọi như một hàm thông thường.

10. Tham số hàm và quản lý bộ nhớ

Bộ nhớ động và tham số

Trong C, bạn có thể cấp phát động bộ nhớ bằng malloc và giải phóng bằng free. Khi truyền con trỏ bộ nhớ động vào hàm, cần chú ý quản lý bộ nhớ để tránh rò rỉ.

#include <stdlib.h>
#include <stdio.h>

void allocateMemory(int **ptr, int size) {
    *ptr = (int *)malloc(size * sizeof(int));
}

int main() {
    int *arr;
    allocateMemory(&arr, 5);
    for (int i = 0; i < 5; i++) {
        arr[i] = i + 1;
    }
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]); // Kết quả: 1 2 3 4 5
    }
    free(arr); // Giải phóng bộ nhớ
}

Ví dụ này cấp phát động mảng và truyền qua tham số cho hàm. Nếu không quản lý tốt sẽ gây rò rỉ bộ nhớ.