| Contents |

36 Số phức

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\)\(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!
#endif

Thê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!
#endif

Chi tiết thêm về chuyện đó ghi ở Annex G trong spec C11.

36.1 Kiểu phức

Để dùng số phức, #include <complex.h>.

Với cái đó, bạn có ít nhất hai kiểu:

_Complex
complex

Cả 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
imaginary

Cả 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_I

Macro 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?

36.2 Gán số phức

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 matter

Vậ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\)\(10+3i\), tương ứng.

36.3 Dựng, xé, và in

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 floatlong double: CMPLXF()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()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.000000i

Lư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()cimag() đều là double.

Và như thường, có các biến thể floatlong double của các hàm này: crealf(), cimagf(), creall(), và cimagl().

36.4 Số học và so sánh số phức

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.080000i

Bạ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 = 1

Chú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.

36.5 Toán số phức

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()cabsl(). Tôi bỏ chúng cho ngắn.

36.5.1 Hàm lượng giác

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

36.5.2 Hàm mũ và logarit

Hàm Mô tả
cexp() Mũ cơ số \(e\)
clog() Logarit tự nhiên (cơ số \(e\))

36.5.3 Hàm luỹ thừa và giá trị tuyệt đối

Hàm Mô tả
cabs() Giá trị tuyệt đối
cpow() Luỹ thừa
csqrt() Căn bậc hai

36.5.4 Hàm thao tác

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


| Contents |