Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 8.718 phút đọc

Truyền theo giá trị

Sau bài này bạn sẽ làm được

  • Giải thích vì sao C luôn truyền theo giá trị, không có ngoại lệ
  • Viết hàm swap đúng bằng con trỏ
  • Giải thích vì sao mảng trông như được truyền theo tham chiếu
  • Biết khi nào nên truyền struct bằng con trỏ thay vì bằng giá trị

C có đúng một cách truyền tham số, và bài này nói hết về nó trong một câu: hàm luôn nhận bản sao. Mọi thứ trông như ngoại lệ đều chỉ là hệ quả của việc bản sao đó đang chứa một địa chỉ.

#Quy tắc duy nhất

Truyền theo giá trị
Khi gọi hàm, giá trị của mỗi đối số được sao chép vào tham số hình thức tương ứng. Hàm làm việc trên bản sao. Sửa bản sao không đụng gì tới bản gốc.
ban-sao.c
#include <stdio.h>

void tang(int x)
{
    x = x + 1;
    printf("trong ham: %d\n", x);
}

int main(void)
{
    int a = 5;

    tang(a);
    printf("trong main: %d\n", a);

    return 0;
}
terminal
./ban-sao
trong ham: 6
trong main: 5

Muốn hàm sửa được biến của người gọi, bạn phải đưa cho nó địa chỉ của biến. Toán tử & lấy địa chỉ, toán tử * đi tới nội dung ở địa chỉ đó. Chương 12 dạy con trỏ đầy đủ, bài này chỉ dùng đúng hai toán tử ấy.

qua-con-tro.c
void tang2(int *x)
{
    *x = *x + 1;        /* ghi vào ô nhớ mà x đang trỏ tới */
}

int b = 5;

tang2(&b);              /* truyền địa chỉ của b */
printf("%d\n", b);      /* 6 */
Bên trái hàm nhận bản sao giá trị nên không đụng được vào bản gốc. Bên phải hàm nhận bản sao địa chỉ, và từ địa chỉ đó ghi thẳng vào ô nhớ của người gọi.

#Hàm hoán đổi kinh điển

Nhận bản sao, không đổi được gì
void swap_sai(int a, int b)
{
    int t = a;

    a = b;
    b = t;
}

int x = 1, y = 2;

swap_sai(x, y);
printf("%d %d\n", x, y);   /* 1 2, không đổi gì */
Nhận địa chỉ, đổi được thật
void swap_dung(int *a, int *b)
{
    int t = *a;

    *a = *b;
    *b = t;
}

int x = 1, y = 2;

swap_dung(&x, &y);
printf("%d %d\n", x, y);   /* 2 1 */

Hàm bên trái vẫn chạy, vẫn hoán đổi thật, chỉ là nó hoán đổi hai bản sao rồi vứt cả hai đi khi hàm kết thúc. Đây là bài kiểm tra kinh điển để xem người học đã hiểu truyền theo giá trị hay chưa.

#Mảng: ngoại lệ chỉ có vẻ

mang.c
#include <stdio.h>

void sua_mang(int a[], size_t n)
{
    if (n > 0)
        a[0] = 999;
}

int main(void)
{
    int arr[3] = { 1, 2, 3 };

    sua_mang(arr, 3);
    printf("%d\n", arr[0]);   /* 999, đã đổi thật */

    return 0;
}

Trông như C truyền mảng theo tham chiếu. Không phải. Chuyện xảy ra là phân rã mảng: khi tên mảng xuất hiện trong hầu hết các biểu thức, nó tự động biến thành con trỏ tới phần tử đầu tiên. Nên lời gọi sua_mang(arr, 3) thực chất là sua_mang(&arr[0], 3). Bạn đang truyền một con trỏ theo giá trị, đúng quy tắc duy nhất.

Biểu thứcKiểu thậtGhi chú
arrint *Phân rã thành con trỏ tới arr[0]
&arr[0]int *Hoàn toàn tương đương với dòng trên
&arrint (*)[3]Con trỏ tới cả mảng, kiểu khác hẳn
sizeof arrsize_tKhông phân rã, cho ra 12 chứ không phải 8

Muốn hàm không sửa được mảng thì làm sao

void chi_doc(const int *a, size_t n)
{
    for (size_t i = 0; i < n; ++i)
        printf("%d ", a[i]);

    /* a[0] = 1;  <- lỗi biên dịch: assignment of read-only location */
}

Đây là lý do thực dụng nhất để dùng const: nó khôi phục lại phần bảo vệ mà cơ chế phân rã mảng đã lấy đi.

#Truyền struct

Struct là ngoại lệ thú vị: nó không phân rã. Truyền struct theo giá trị sẽ sao chép toàn bộ nội dung, kể cả mảng nằm bên trong nó.

struct-to.c
typedef struct {
    int a[1000];        /* 4000 byte */
} To;

void f(To t)            /* sao chép 4000 byte mỗi lời gọi */
{
    t.a[0] = 1;         /* chỉ sửa bản sao */
}

void g(const To *t)     /* sao chép 8 byte, và hứa không sửa */
{
    printf("%d\n", t->a[0]);
}

void h(To *t)           /* sao chép 8 byte, có sửa thật */
{
    t->a[0] = 1;
}
Kích thước structNên truyền thế nàoVì sao
Tới khoảng 16 byteTheo giá trịQuy ước gọi hàm đặt gọn vào thanh ghi, không tốn gì thêm
Lớn hơn 16 byte, hàm không sửaconst con trỏChỉ sao chép 8 byte, và trình biên dịch kiểm tra hộ
Lớn hơn 16 byte, hàm có sửaCon trỏ không constSửa trực tiếp lên bản gốc
Cần bản sao độc lập để nghịchTheo giá trị, dù lớnĐôi khi bản sao chính là thứ bạn muốn

#Bảng tra: truyền cái gì thì thế nào

Truyền vàoHàm nhận đượcHàm sửa được bản gốc
int abản sao của giá trịKhông
int *p, gọi bằng &abản sao của địa chỉCó, qua *p
mảng int arr[10]bản sao con trỏ tới arr[0]Có, qua arr[i]
struct S sbản sao toàn bộ structKhông
struct S *psbản sao của địa chỉCó, qua ps->truong
const int *pbản sao của địa chỉKhông, trình biên dịch chặn
char *s cho chuỗibản sao con trỏ tới ký tự đầuCó, nếu chuỗi ghi được

Tự làm thử

  1. Viết cả swap_saiswap_dung, in giá trị trước và sau mỗi lần gọi, giải thích từng dòng kết quả.
  2. Viết hàm nhận con trỏ rồi gán NULL cho chính con trỏ đó bên trong hàm. In địa chỉ ở cả hai nơi để chứng minh con trỏ cũng là bản sao.
  3. Viết void tang_het(int *a, size_t n) cộng 1 vào mọi phần tử, và long long tong(const int *a, size_t n). Giải thích vì sao một hàm có const còn hàm kia thì không.
  4. Tạo struct chứa mảng bốn nghìn byte, viết hai hàm nhận nó theo giá trị và theo con trỏ, gọi mỗi hàm mười triệu lần và đo bằng clock() như Bài 7.5.
  5. Viết hàm nhận struct có con trỏ bên trong, sửa vùng nhớ qua bản sao, rồi kiểm tra bản gốc có bị đổi không. Giải thích kết quả.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • C chỉ có truyền theo giá trị. Hàm luôn nhận bản sao của đối số.
  • Muốn hàm sửa được biến của người gọi thì truyền địa chỉ. Bản thân con trỏ vẫn là bản sao, chỉ ô nhớ nó trỏ tới mới được chia sẻ.
  • Mảng không phải ngoại lệ. Tên mảng phân rã thành con trỏ tới phần tử đầu trước khi được truyền.
  • Struct không phân rã. Struct lớn hơn khoảng mười sáu byte nên truyền bằng con trỏ, kèm const nếu hàm không sửa.
  • Sao chép struct là sao chép nông: con trỏ bên trong được chép nguyên giá trị, hai bản cùng trỏ một chỗ.