Variadic là từ kêu kêu để chỉ hàm nhận số đối số tuỳ ý.
Hàm thường nhận một số đối số cụ thể, ví dụ:
int add(int x, int y)
{
return x + y;
}Bạn chỉ có thể gọi nó với đúng hai đối số tương ứng tham số x và y.
add(2, 3);
add(5, 12);Nhưng nếu thử nhiều hơn, compiler không cho:
add(2, 3, 4); // ERROR
add(5); // ERRORHàm variadic vượt qua giới hạn này ở một mức nào đó.
Ta đã thấy một ví dụ nổi tiếng trong printf()! Bạn có thể truyền đủ kiểu thứ vào nó.
printf("Hello, world!\n");
printf("The number is %d\n", 2);
printf("The number is %d and pi is %f\n", 2, 3.14159);Nó có vẻ chẳng quan tâm bạn đưa bao nhiêu đối số!
Ừ, không hẳn. Không đối số nào sẽ cho lỗi:
printf(); // ERRORĐiều này dẫn ta tới một giới hạn của hàm variadic trong C: chúng phải có ít nhất một đối số.
Nhưng ngoài chuyện đó, chúng khá linh hoạt, thậm chí cho phép đối số có kiểu khác nhau như printf() làm.
Xem chúng hoạt động sao nhé!
Vậy nó chạy thế nào, về cú pháp?
Việc bạn làm là đặt mọi đối số bắt buộc phải truyền vào trước (và nhớ phải có ít nhất một) và sau đó, bạn đặt .... Như vầy:
void func(int a, ...) // Literally 3 dots hereĐây là ít code để demo:
#include <stdio.h>
void func(int a, ...)
{
printf("a is %d\n", a); // Prints "a is 2"
}
int main(void)
{
func(2, 3, 4, 5, 6);
}Rồi, hay, ta lấy được đối số đầu ở biến a, nhưng còn phần đối số còn lại thì sao? Làm sao tới được chúng?
Đây là chỗ vui bắt đầu!
Bạn sẽ muốn include <stdarg.h> để làm mấy chuyện này.
Trước tiên, ta sẽ dùng một biến đặc biệt kiểu va_list (variable argument list) để theo dõi ta đang truy cập biến nào tại thời điểm đó.
Ý tưởng là ta bắt đầu xử lý đối số bằng một lời gọi va_start(), xử lý từng đối số một bằng va_arg(), rồi, khi xong, kết bằng va_end().
Khi bạn gọi va_start(), bạn cần truyền tham số có tên cuối cùng (cái ngay trước ...) để nó biết chỗ cần bắt đầu tìm các đối số thêm.
Và khi bạn gọi va_arg() để lấy đối số kế, bạn phải cho nó biết kiểu của đối số kế tiếp cần lấy.
Đây là demo cộng lại một số tuỳ ý các số nguyên. Đối số đầu là số lượng số nguyên cần cộng. Ta sẽ dùng nó để biết phải gọi va_arg() bao nhiêu lần.
#include <stdio.h>
#include <stdarg.h>
int add(int count, ...)
{
int total = 0;
va_list va;
va_start(va, count); // Start with arguments after "count"
for (int i = 0; i < count; i++) {
int n = va_arg(va, int); // Get the next int
total += n;
}
va_end(va); // All done
return total;
}
int main(void)
{
printf("%d\n", add(4, 6, 2, -4, 17)); // 6 + 2 - 4 + 17 = 21
printf("%d\n", add(2, 22, 44)); // 22 + 44 = 66
}
(Lưu ý khi gọi printf(), nó dùng số %d (hay bất cứ thứ gì) trong chuỗi format để biết còn bao nhiêu đối số nữa!)
Nếu cú pháp của va_arg() trông lạ với bạn (vì có tên kiểu lơ lửng trong đó), bạn không đơn độc. Chúng được cài đặt bằng macro preprocessor để có được mọi phép màu cần thiết.
va_listCái biến va_list ta đang dùng ở trên là gì? Đó là biến mờ154 giữ thông tin về việc ta sẽ lấy đối số nào kế tiếp bằng va_arg(). Thấy cách ta gọi va_arg() lặp đi lặp lại đấy? Biến va_list là chỗ giữ chỗ đang theo dõi tiến độ cho tới giờ.
Nhưng ta phải khởi tạo biến đó bằng một giá trị hợp lý. Đó là lúc va_start() ra sân.
Khi ta gọi va_start(va, count) ở trên, ta đang nói, “Khởi tạo biến va để trỏ tới đối số biến ngay sau count.”
Và đó là lý do ta cần có ít nhất một biến có tên trong danh sách đối số155.
Một khi có pointer tới tham số ban đầu, bạn có thể dễ dàng lấy các giá trị đối số sau bằng cách gọi va_arg() lặp đi lặp lại. Khi làm vậy, bạn phải truyền vào biến va_list của mình (để nó tiếp tục theo dõi bạn đang ở đâu), cùng với kiểu của đối số bạn sắp copy ra.
Tùy bạn, người lập trình, nghĩ ra kiểu bạn sẽ truyền cho va_arg(). Trong ví dụ ở trên, ta chỉ làm int. Nhưng trong trường hợp printf(), nó dùng format specifier để xác định kiểu nào cần lấy kế tiếp.
Và khi xong, gọi va_end() để kết lại. Bạn phải (spec nói) gọi cái này trên một biến va_list cụ thể trước khi bạn quyết định gọi lại va_start() hay va_copy() trên nó lần nữa. Tôi biết ta chưa nói về va_copy().
Vậy tiến trình chuẩn là:
va_start() để khởi tạo biến va_list của bạnva_arg() để lấy giá trịva_end() để kết biến va_list của bạn
Tôi cũng có nhắc va_copy() ở trên; nó làm bản sao biến va_list của bạn ở đúng cùng trạng thái. Tức là, nếu bạn chưa bắt đầu dùng va_arg() với biến nguồn, biến mới cũng chưa bắt đầu. Nếu bạn đã ngốn 5 biến bằng va_arg() cho tới giờ, bản sao cũng phản ánh y vậy.
va_copy() có thể hữu ích nếu bạn cần quét trước qua đối số nhưng vẫn cần nhớ vị trí hiện tại.
va_listMột trong những cách dùng khác của mấy cái này khá hay: viết biến thể printf() tuỳ ý của riêng bạn. Sẽ đau đầu nếu phải xử mọi format specifier đó phải không? Cả tỷ cái?
May thay, có các biến thể printf() nhận một va_list đang hoạt động làm đối số. Bạn có thể dùng chúng để bọc lại và tự làm printf() riêng!
Các hàm này bắt đầu bằng chữ v, như vprintf(), vfprintf(), vsprintf() và vsnprintf(). Về cơ bản là mọi bản hit kinh điển của printf() chỉ thêm v đằng trước.
Hãy làm hàm my_printf() chạy y printf() chỉ khác là nhận thêm một đối số đầu.
#include <stdio.h>
#include <stdarg.h>
int my_printf(int serial, const char *format, ...)
{
va_list va;
int rv;
// Do my custom work
printf("The serial number is: %d\n", serial);
// Then pass the rest off to vprintf()
va_start(va, format);
rv = vprintf(format, va);
va_end(va);
return rv;
}
int main(void)
{
int x = 10;
float y = 3.2;
my_printf(3490, "x is %d, y is %f\n", x, y);
}Thấy ta làm gì đó chưa? Ở dòng 12-14 ta mở một biến va_list mới, rồi cứ thế truyền thẳng vào vprintf(). Và nó biết ngay phải làm gì với nó, vì nó có sẵn mọi đầu óc của printf() gài vào.
Tuy vậy, ta vẫn phải gọi va_end() khi xong, nên đừng quên!
Như tôi đã nhắc, va_start() và va_end() có thể là macro. Một hệ quả của chuyện này có thể là chúng có tiềm năng mở ra một scope cục bộ mới. (Tức là, nếu va_start() có { và va_end() chứa }.)
Nên ta cần cảnh giác với chuyện scope có thể gặp vấn đề. Lấy ví dụ sau:
va_start(va, format); // may contain {
int rv = vprintf(format, va);
va_end(va); // may contain }
return rv;Nếu va_start() mở scope mới, rv sẽ cục bộ trong scope đó rồi câu return sẽ fail. Nhưng chuyện này sẽ âm thầm chỉ xảy ra trên các compiler tình cờ làm vậy với macro va.