Một đoạn nhập môn tí hon về số phức185 ăn cắp thẳng từ Wikipedia:
Số phức là số có thể được biểu diễn dưới dạng \(a+bi\), trong đó \(a\) và \(b\) là số thực [tức là kiểu dấu chấm động trong C], và \(i\) đại diện cho đơn vị ảo, thoả mãn phương trình \(i^2=−1\). Vì không có số thực nào thoả phương trình này, \(i\) được gọi là số ảo. Với số phức \(a+bi\), \(a\) được gọi là phần thực, và \(b\) được gọi là phần ảo.
Nhưng tới đây là hết rồi. Ta giả định nếu bạn đọc chương này, bạn biết số phức là gì và bạn muốn làm gì với chúng.
Và tất cả ta cần lo là tiện ích của C để làm điều đó.
Hoá ra, hỗ trợ số phức trong compiler là tính năng tuỳ chọn. Không phải compiler tuân chuẩn nào cũng làm được. Và những compiler làm được, có thể làm ở các mức độ hoàn chỉnh khác nhau.
Bạn có thể check xem hệ của bạn có hỗ trợ số phức không với:
#ifdef __STDC_NO_COMPLEX__
#error Complex numbers not supported!
#endifThêm nữa, có một macro báo việc tuân theo chuẩn ISO 60559 (IEEE 754) cho toán dấu chấm động với số phức, cũng như sự hiện diện của kiểu _Imaginary.
#if __STDC_IEC_559_COMPLEX__ != 1
#error Need IEC 60559 complex support!
#endifChi tiết thêm về chuyện đó ghi ở Annex G trong spec C11.
Để dùng số phức, #include <complex.h>.
Với cái đó, bạn có ít nhất hai kiểu:
_Complex
complexCả hai đều có cùng nghĩa, nên cứ dùng complex cho đẹp.
Bạn cũng có vài kiểu cho số ảo nếu cài đặt của bạn tuân IEC 60559:
_Imaginary
imaginaryCả hai này cũng có cùng nghĩa, nên cứ dùng imaginary cho đẹp.
Bạn cũng có giá trị cho số ảo \(i\), chính nó:
I
_Complex_I
_Imaginary_IMacro I được set thành _Imaginary_I (nếu có), hoặc _Complex_I. Nên cứ dùng I cho số ảo.
Lề một chút: tôi đã nói nếu compiler set __STDC_IEC_559_COMPLEX__ thành 1, nó phải hỗ trợ kiểu _Imaginary để tuân chuẩn. Đó là cách tôi đọc spec. Tuy nhiên, tôi không biết một compiler nào thực sự hỗ trợ _Imaginary dù chúng có set __STDC_IEC_559_COMPLEX__. Nên tôi sẽ viết một số code với kiểu đó ở đây mà tôi không có cách nào test. Xin lỗi!
OK, giờ ta biết có kiểu complex, ta dùng nó sao?
Vì số phức có phần thực và phần ảo, nhưng cả hai đều dựa vào số dấu chấm động để lưu giá trị, ta cũng cần báo C dùng độ chính xác nào cho các phần đó của số phức.
Ta làm chuyện đó bằng cách đính kèm float, double, hay long double vào complex, trước hay sau đều được.
Định nghĩa một số phức dùng float cho các thành phần của nó:
float complex c; // Spec prefers this way
complex float c; // Same thing--order doesn't matterVậy khai báo thì ổn rồi, còn khởi tạo hay gán thì sao?
Hoá ra ta được dùng ký pháp khá tự nhiên. Ví dụ!
double complex x = 5 + 2*I;
double complex y = 10 + 3*I;Cho \(5+2i\) và \(10+3i\), tương ứng.
Ta đang tới đó…
Ta đã thấy một cách viết số phức:
double complex x = 5 + 2*I;Cũng không vấn đề gì khi dùng số dấu chấm động khác để dựng nó:
double a = 5;
double b = 2;
double complex x = a + b*I;Cũng có một bộ macro để giúp dựng mấy cái này. Code trên có thể được viết dùng macro CMPLX(), như vầy:
double complex x = CMPLX(5, 2);Theo như tôi tìm hiểu, mấy cái này gần như tương đương:
double complex x = 5 + 2*I;
double complex x = CMPLX(5, 2);Nhưng macro CMPLX() sẽ xử lý zero âm ở phần ảo đúng mỗi lần, còn cách kia có thể chuyển chúng thành zero dương. Tôi nghĩ186 Cái này có vẻ hàm ý rằng nếu có khả năng phần ảo là zero, bạn nên dùng macro… nhưng ai đó nên sửa tôi về điều này nếu tôi nhầm!
Macro CMPLX() chạy trên kiểu double. Có hai macro khác cho float và long double: CMPLXF() và CMPLXL(). (Hậu tố “f” và “l” xuất hiện hầu như trong tất cả các hàm liên quan tới số phức.)
Giờ thử ngược lại: nếu ta có số phức, làm sao tách nó ra phần thực và phần ảo?
Ta có một cặp hàm sẽ trích phần thực và phần ảo từ số: creal() và cimag():
double complex x = 5 + 2*I;
double complex y = 10 + 3*I;
printf("x = %f + %fi\n", creal(x), cimag(x));
printf("y = %f + %fi\n", creal(y), cimag(y));cho output:
x = 5.000000 + 2.000000i
y = 10.000000 + 3.000000iLưu ý rằng chữ i tôi có trong chuỗi format của printf() là chữ i theo nghĩa đen được in ra, nó không phải phần của format specifier. Cả hai giá trị trả về từ creal() và cimag() đều là double.
Và như thường, có các biến thể float và long double của các hàm này: crealf(), cimagf(), creall(), và cimagl().
Số học có thể được thực hiện trên số phức, tuy cách nó chạy về mặt toán học nằm ngoài phạm vi guide này.
#include <stdio.h>
#include <complex.h>
int main(void)
{
double complex x = 1 + 2*I;
double complex y = 3 + 4*I;
double complex z;
z = x + y;
printf("x + y = %f + %fi\n", creal(z), cimag(z));
z = x - y;
printf("x - y = %f + %fi\n", creal(z), cimag(z));
z = x * y;
printf("x * y = %f + %fi\n", creal(z), cimag(z));
z = x / y;
printf("x / y = %f + %fi\n", creal(z), cimag(z));
}cho kết quả:
x + y = 4.000000 + 6.000000i
x - y = -2.000000 + -2.000000i
x * y = -5.000000 + 10.000000i
x / y = 0.440000 + 0.080000iBạn cũng có thể so sánh hai số phức về bằng nhau (hoặc không bằng):
#include <stdio.h>
#include <complex.h>
int main(void)
{
double complex x = 1 + 2*I;
double complex y = 3 + 4*I;
printf("x == y = %d\n", x == y); // 0
printf("x != y = %d\n", x != y); // 1
}với output:
x == y = 0
x != y = 1Chúng bằng nhau nếu cả hai thành phần test bằng. Lưu ý rằng như với mọi dấu chấm động, chúng có thể bằng nếu đủ gần do lỗi làm tròn187.
Nhưng khoan! Còn nhiều hơn chỉ là số học số phức đơn giản!
Đây là bảng tổng hợp mọi hàm toán có sẵn cho bạn với số phức.
Tôi sẽ chỉ liệt kê phiên bản double của mỗi hàm, nhưng với tất cả chúng đều có phiên bản float bạn lấy được bằng cách thêm f vào tên hàm, và phiên bản long double bạn lấy được bằng cách thêm l.
Ví dụ, hàm cabs() dùng tính giá trị tuyệt đối của số phức cũng có biến thể cabsf() và cabsl(). Tôi bỏ chúng cho ngắn.
| Hàm | Mô tả |
|---|---|
ccos() |
Cosine |
csin() |
Sine |
ctan() |
Tangent |
cacos() |
Arc cosine |
casin() |
Arc sine |
catan() |
Chơi Settlers of Catan |
ccosh() |
Hyperbolic cosine |
csinh() |
Hyperbolic sine |
ctanh() |
Hyperbolic tangent |
cacosh() |
Arc hyperbolic cosine |
casinh() |
Arc hyperbolic sine |
catanh() |
Arc hyperbolic tangent |
| Hàm | Mô tả |
|---|---|
cexp() |
Mũ cơ số \(e\) |
clog() |
Logarit tự nhiên (cơ số \(e\)) |
| Hàm | Mô tả |
|---|---|
cabs() |
Giá trị tuyệt đối |
cpow() |
Luỹ thừa |
csqrt() |
Căn bậc hai |
| Hàm | Mô tả |
|---|---|
creal() |
Trả phần thực |
cimag() |
Trả phần ảo |
CMPLX() |
Dựng số phức |
carg() |
Argument / góc pha |
conj() |
Liên hợp188 |
cproj() |
Phép chiếu lên mặt cầu Riemann |