Lập trình

Lập trình

Thứ Ba, 22 tháng 12, 2015

Chuyển đổi kiểu trong lập trình (type conversion) : Chuyển đổi ngầm (implicit conversion)

Khi lập trình có rất nhiều lúc xảy ra chuyển kiểu, hôm nay ta sẽ tìm hiểu về các loại của chuyển kiểu và các lưu ý khi chuyển kiểu.

Chuyển kiểu (conversion) Có hai kiểu chuyển đổi cơ bản : implicit conversion, compiler tự động chuyển từ kiểu này đến kiểu khác và explicit conversion, lập trình viên sử dụng toán tử casting để điều khiển việc chuyển đổi. Hôm nay ta sẽ tìm hiểu chuyển kiểu ngầm

+ Chuyển đổi ngầm (implicit conversion)

Ta đã biết giá trị của một biến được lưu trữ với một dãy bit, và kiểu dữ liệu của biến nói cho trình biên dịch diễn tả các bit đó vào các giá trị có ý nghĩa. Các kiểu dữ liệu khác nhau có thể tượng trưng với cùng một số - ví dụ, kiểu integer có giá trị là 3 và float có giá trị là 3.0 được lưu trữ dưới các dạng binary hoàn toàn khác nhau.

Vậy điều gì xảy ra nếu ta làm thứ như sau :

float f = 3; // gán một integer 3 cho biến float

Với ví dụ trên, compiler không thể chỉ copy dãy bit đại diện cho giá trị của 3 sang f được. Thay vào đó nó cần chuyển int 3 sang kiểu số thực, cái mà có thể gán cho f.

Tiến trình của sự chuyển đổi giá trị từ một kiểu dữ liệu này sang kiểu dữ liệu khác được gọi là chuyển đổi kiểu "type conversion". Chuyển đổi kiểu có thể xảy ra ở rất nhiều trường hợp khác nhau:

Gán hoặc khởi tạo một biến với giá trị không cùng kiểu :

double d = 3; // gán một số nguyên có giá trị bằng 3 cho một biến kiểu double
double s(3); // khởi tạo kiểu double từ một kiểu nguyên có giá trị bằng 3

Truyền giá trị vào hàm mà tham số của hàm có kiểu dữ liệu khác :

void doSomething(long i){
// do anything
}

doSomething(3); // truyền vào giá trị 3 kiểu int cho hàm cần tham số kiểu long

Trả về một giá trị từ một hàm mà kiểu trả về của hàm cần là kiểu khác :

float fo(){
return 3.0; // Trả về kiểu double có giá trị là 3.0 cho hàm cần trả về kiểu float
}

Sử dụng toán tử với hai toán hạng khác kiểu :

double divi = 4.0 / 3; // Phép chia với double và intteger

Với các ví dụ trên (và một vài ví dụ khác), C++ sẽ sử dụng chuyển đổi kiểu để chuyển đỗi dữ liệu từ kiểu này sang kiểu khác

Có hai loại chuyển đổi kiểu cơ bản là implicit type conversion, trình biên dịch sẽ tự động chuyển dữ liệu từ một kiểu cơ bản sang một kiểu dữ liệu khác, và explicit type conversions, lập trình viên sử dụng các toán tử chuyển đổi để điều khiển trực tiếp việc chuyển đổi.

Chúng ta sẽ xem xét implicit type conversion ở bài này và tìm hiểu explicit type conversion ở bài tiếp.

Implicit type conversion


Implicit type conversion cũng được gọi là kiểu chuyển đổi tự động hay coercion được chuyển đổi bất cứ khi nào một kiểu dữ liệu cơ bản được mong đợi, nhưng lại cung cấp một kiểu dữ liệu cơ bản khác, và người dùng không nói rõ ràng cho máy tính là chuyển đổi như thế nào.

Tất cả các ví dụ trên đều là trường hợp của chuyển đổi ngầm được sử dụng.

Có hai loại cơ bản của chuyển đổi kiểu ngầm là : promotions và conversions.

.Numeric promotion


Bất cứ khi nào một giá trị từ một kiểu được chuyển đến giá trị của kiểu tương tự nhưng rộng hơn, nó được gọi là numeric promotion (hay là widening , nó thường xuyên dành cho kiểu int). Cho ví dụ, kiểu int có thể chuyển đến kiểu rộng hơn là long, float chuyển đến double.

long l(64); // mở rộng số nguyên 64 trong long
double d(0.12f); // "thăng cấp" float 0.12 trong kiểu double

Trong thuật ngữ "numeric promotion" chuyển một vài loại hình promotion,cũng có hai kiểu chuyển đổi khác cần lưu ý :

+ Intergral promotion liên quan đến chuyển đổi kiểu đổi của integer đến kiểu có phạm vi hẹp hơn int (ví dụ như bool, char, usigned char, signed char, usigned short , signed short) đến kiểu int (nếu có thể) hoặc kiểu unsigned int.

+Float point promotion liên quan đến chuyển đổi từ kiểu float sang double.


Hai loại trên đều được sử dụng trong các trườn hợp cụ thể đặc biệt để chuyển một kiểu dữ liệu nhờ nhơn đến int/ usigned int hay double, bởi vì thực hiện trên kiểu dữ liệu đó có hiệu suất nhất.

Một thứ quan trọng cần nhớ về promotions là chúng luôn an toán và không có dữ liệu nào bị mất.

.Numeric conversions


Khi chúng ta chuyển đổi một giá trị từ liệu kiểu dữ liệu rộng hơn rang kiểu dữ liệu tương tự nhưng bé hơn hoặc hai kiểu khác biệt, nó được gọi là numeric conversions. Ví dụ :

double d = 3; // chuyển kiểu integer 3 đến kiểu double
short s = 2; // chuyển integer 2 đến short

Không giống như loại promotions luôn an toàn thì chuyển đổi kiểu numeric conversions có thể có hoặc cũng có thể không gây ra mất dữ liệu.  Do điều này , một vài code thực hiện chuyển đổi ngầm sẽ thường xuyên dẫn tới compiler đưa ra cảnh báo.

Luật của kiểu chuyển đổi này rất phức tạp và nhiều, vì vậy chúng ta chỉ đề cập đến các trường hợp phổ biến ở đây.

Trong tất cả các trường hợp, chuyển đổi giá trị đến một kiểu không có phạm vi đủ lớn để hỗ trọ biểu diễn giá trị đó sẽ dẫn tới kết quả ngoài mong đợi. Nên tránh điều này. Ví dụ:

int main(){
int i = 30000;
char c = i;

std::cout << static_cast<int>(c);

return 0;
}

Trong ví dụ này chúng ta đã gán một giá trị rất lớn kiểu int cho char (giới hạn từ -128 đến 127). Điều này dẫn tới char sẽ bị tràn. Nó sẽ đưa ra kết quả là : 48

Tuy nhiên , việc chuyển đổi từ kiểu rộng hay kiểu dấu phảy động đến kiểu tương tự nhở hơn nhìn chung là vẫn làm việc miễn là giá trị này phù hợp với phạm vi của kiểu nhỏ hơn. Ví dụ:

int i = 2;
short s = i; // int đến short
std:: cout << i;

double d = 0.1234;
float f = d;
std:: cout << f;

có kết quả là 2 và 0.1234 .

Trong trường hợp giá trị là dấu phảy động, một vài phép làm tròn có thể xảy ra dẫn tới việc mất dữ liệu cho kiểu nhỏ hơn.

Ví dụ :
float f = 0.123456789; // kiểu double 0.123456789 có 9 chữ số thập phân, nhưng float chỉ có thể
                       // biểu diễn được 7 số

std:: cout << std::setprecision(9) << f;

Trong trường hợp này ta nhìn mất đi một số sau dấu phẩy bởi vì float không thể tổ chức nhiều số chữ số sau dấu phảy bằng double. (nó chỉ lưu trữ được 7 số) nên có kết quả :
0.123456791

Chuyển đổi từ một integer đến dấu phảy động nhìn chung là làm việc được miễn là trong phạm vi của nó. Ví dụ :

int i = 10;
float f = i;
std::cout << f;

kết quả là 10 .

Chuyển từ dấu phảy động sang integer làm việc được miễn là nằm trong khoảng giá trị của nó, nhưng phần thập phân bị mất ví dụ :

int i = 3.5;
cout << i;

kết quả là 3


.Phỏng đoán một biểu thức


Khi tính toán một biểu thức với các kiểu khác nhau, compiler tách từng biểu thức ra làm biểu thức con. Các toán tử toán học yêu cầu toán hạng của nó cùng kiểu. Nếu các toán hạng không cùng kiểu, compiler sẽ thực hiện chuyển đổi ngầm để chuyển một toán hạng. Để làm được điều này, nó phải tuân thủ các luật lệ sau:

+ NẾu toán hạng là kiểu nguyên, nó trải qua sự chuyển đổi "integral promotion" như đã nói ở trên.

+ Nếu toán hạng vẫn không phù hợp, compiler tìm toán hạng ưu tiên nhất (trong các toán hạng có trong biểu thức đó) và chuyển nó sang để phù hợp.

Toán hạng được ưu tiên như sau :

+ long double (highest)
+ double
+float
+ unsigned long long
+ long long
+unsigned long
+ long
+ usigned int
+ int (lowest)

Chúng ta có thể xem sự chuyển đổi số học qua việc sử dụng typeid() (trong file typeinfo header), nó có thể thường được dùng để chỉ ra kết quả của việc chuyển đổi.


#include <iostream>
  #include <typeinfo> // for typeid()

int main()
{
    short a(4);
    short b(5);
    std::cout << typeid(a + b).name() << " " << a + b << std::endl; // show us the type of a + b

   return 0;
}


Vì short là integer , chúng được trải qua "integral promotion" để chuyển thành int trước khi cộng. Kết quả của phép cộng 2 int là int, như chúng ta mong đợi :
int 9

Chúng ta xem một trường hợp khác :

#include <iostream>
#include <typeinfo> // for typeid()

int main()
{
    double d(4.0);
    short s(2);
    std::cout << typeid(d + s).name() << " " << d + s << std::endl; // show us the type of d + s

    return 0;
}

Trong trường hợp này , short trải qua "integral promotion" thành int. Tuy nhiên, int cộng double vẫn không hoạt động (do không cùng kiểu). Do double là kiểu có độ ưu tiên cao hoen nên integer được chuyển thành double 2.0, và double + double là double.

=> kết quả : double 6.0

Hệ thống cấp bậc này có thể dẫn tới một vài vấn đề thú vị. Ví dụ, xem code :
std::cout << 5u - 10; // 5u có nghĩa là 5 như kiểu unsigned intteger

Bạn có lẽ mong rằng biểu thức 5u - 10 được tính là -5 vì 5 - 10 = -5 . Nhưng thực tế kết quả là
4294967291

Trong trường hợp này, signed integer (10) được "nâng cấp" lên thành unsigned int( vì nó cod thự tự ưu tiên cao hơn), và biểu thức được tính với unsigned integer. Kết quả overflow  và chúng ta nhận kết quả ko như mong đợi

Đây là một trong rất nhiều lý do để tránh việc sử dụng unsigned int.

Cảm ơn các bạn đã dành thời gian để xem .

Không có nhận xét nào:

Đăng nhận xét