Các kiểu chuyển đổi trong C++: static cast, reinterpret cast, const cast, và dynamic cast

From CodeForLife
Jump to: navigation, search

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

Các chuyển đổi ngầm sẽ tự động được thực hiện khi một giá trị được sao chép tới một kiểu tương thích. Ví dụ:

short a=2000;
int b;
b=a;

Ở đây giá trị a từ kiểu short tư động chuyển về kiểu int mà không cần bất kỳ toán tử ép kiểu nào. Điều này được biết đến như là chuyển đổi chuẩn (standard conversion). Chuyển đổi chuẩn tác dụng với kiểu dữ liệu cơ bản và cho phép chuyển đổi giữa các kiểu số (short sang int, int sang float, double sang int, ...), từ bool sang kiểu khác và ngược lại, một vài chuyển đổi con trỏ. Các chuyển đổi từ kiểu nhỏ hơn sang kiểu int, hoặc từ float sang double sẽ tạo ra giá trị chính xác ở kiểu đích. Còn các chuyển đổi khác giữa các kiểu số học khác thì không phải lúc nào cho kết quả chính xác:

  • Nếu một số nguyên có dấu được chuyển về không dấu, kết quả trả về sẽ là số bù 2 của nó. Ví dụ: -1 sẽ chuyển thành số lớn nhất trong kiểu không dấu, -2 là số lớn thứ 2 trong kiểu không dấu.
  • Khi chuyển từ kiểu khác về bool hoặc ngược lại, thì false sẽ tương đương với 0 (Cho kiểu số) và NULL (Cho kiểu con trỏ), true tương đương với các giá trị khác và được chuyển đổi về 1.
  • Nếu chuyển đổi từ floating-point sang kiểu int, giá trị được làm tròn (Phần thập phân bị loại bỏ). Nếu kết quả nằm ngoài phạm vi giá trị tương ứng kiểu đích, chuyển đổi sẽ gây ra "undefined behavior".
  • Ngược lại, nếu chuyển đổi những kiểu số cùng loại (integer-to-integer or floating-to-floating), chuyển đổi là hợp lệ, nhưng giá trị là "implementation-specific".

Những chuyển đổi gây mất mát dữ liệu, trình biên dịch sẽ đưa ra cảnh báo. Cảnh báo này sẽ mất nếu sử dụng thực hiện các chuyển đổi rõ ràng (explicit conversion).

Với những kiểu dữ liệu không phải kiểu cơ bản, như mảng hoặc hàm sẽ ngầm chuyển sang dạng con trỏ. Với con trỏ cho phép các chuyển đổi sau:

  • Con trỏ NULL có thể chuyển đổi sang con trỏ kiểu bất kỳ.
  • Con trỏ kiểu bất kỳ có thể chuyển đổi thành con trỏ void.
  • Pointer upcast: Con trỏ trở tới lớp kế thừa có thể chuyển đổi thành con trỏ lớp cơ sở mà không thay đổi tính chất const hay volatile của nó.

Chuyển đổi ngầm với các lớp

Với các lớp, các chuyển đổi ngầm có thể được điều khiển bởi ba hàm thành viên:

  • Các hàm khởi tạo 1 tham số (Single-argument constructors): Cho phép chuyển đổi từ một kiểu cụ thể để khởi tạo đối tượng.
  • Toán tử gán (Assignment operator): Cho phép chuyển đổi từ kiểu cụ thể bằng toán tử gán.
  • Toán tử ép kiểu (Type-cast operator): Cho phép chuyển đổi ngầm sang một kiểu cụ thể.

Cụ thể hãy xem ví dụ sau:

#include <iostream>
using namespace std;

class A {};

class B {
public:
  // conversion from A (constructor):
  B (const A& x) {}
  // conversion from A (assignment):
  B& operator= (const A& x) {return *this;}
  // conversion to A (type-cast operator)
  operator A() {return A();}
};

int main ()
{
  A foo;
  B bar = foo;    // calls constructor
  bar = foo;      // calls assignment
  foo = bar;      // calls type-cast operator
  return 0;
}

Toán tử ép kiểu sử dụng theo cú pháp: Sử dụng từ khóa operator theo sau là kiểu đích cùng với dấu mở và đóng ngoặc.

Từ khóa explicit

Khi gọi một hàm, C++ thực hiện chuyển đổi ngầm cho mỗi tham số. Điều này đôi khi có vấn đề với các lớp, vì nhiều khi không đúng mong muốn của người lập trình. Ví dụ hàm sau:
void fn (B arg) {}
Hàm này yêu cầu tham số kiểu B, nhưng nó có thể được gọi với tham số kiểu A:
fn (foo);
Việc này có thể đúng, hoặc không đúng mong muốn của bạn. Trong trường hợp bạn muốn ngăn cản chuyển đổi ngầm định thông qua hàm khởi tạo, bạn hãy sử dụng từ khóa explicit:

// explicit:
#include <iostream>
using namespace std;

class A {};

class B {
public:
  explicit B (const A& x) {}
  B& operator= (const A& x) {return *this;}
  operator A() {return A();}
};

void fn (B x) {}

int main ()
{
  A foo;
  B bar (foo);
  bar = foo;
  foo = bar;
  
//  fn (foo);  // not allowed for explicit ctor.
  fn (bar);  

  return 0;
}

Hơn nữa, hàm khởi tạo được đánh dấu với từ khóa explicit không thể được gọi thông qua cú pháp  gán. Như ví dụ ở trên, bạn không thể thực hiện khởi tạo sau:
B bar = foo;

Ngoài ra, các hàm thành viên của lớp cũng có thể sử dụng từ khóa expicit để ngăn cản các chuyển đổi ngầm định không mong muốn.

Ép kiểu (Type casting)

C++ là ngôn ngữ mạnh. Nhiều chuyển đổi, đặc biệt chuyển đổi mang ý nghĩa giải thích sự khác nhau giữa các kiểu giữa liệu, trong C++ được biết đến "Ép kiểu (Type-casting). Có hai cú pháp chính sử dụng ép kiểu: functional và c-like:

double x = 10.3;
int y;
y = int (x);    // functional notation
y = (int) x;    // c-like cast notation

Cú pháp này đủ để áp dụng cho các kiểu dữ liệu cơ bản. Tuy nhiên cú pháp nây cho phép áp dụng với các lớp và con trỏ các lớp, và việc áp dụng bừa bãi  trong trường hợp này có thể gây ra lỗi khi thực thi. Ví dụ đoạn mã sau không có lỗi khi biên dịch:

// class type-casting
#include <iostream>
using namespace std;

class Dummy {
    double i,j;
};

class Addition {
    int x,y;
  public:
    Addition (int a, int b) { x=a; y=b; }
    int result() { return x+y;}
};

int main () {
  Dummy d;
  Addition * padd;
  padd = (Addition*) &d;
  cout << padd->result();
  return 0;
}

Chương trình khai báo một con trỏ tới lớp Addition, nhưng sau đó lại gán nó trỏ tới một đối tượng khác không mong muốn thông qua ép kiểu:
padd = (Addition*) &d;
Việc không hạn chế ép kiểu cho phép chuyển con trỏ kiểu bất kỳ sang con trỏ kiểu bất kỳ khác không liên quan gì tới kiểu ban đầu. Điều này sẽ sinh ra các lỗi khi thực thi hoặc sinh ra kết quả không mong muốn.

Để kiểm soát việc ép kiểu này giữa các lớp, chúng ta có 04 toán tử ếp kiểu với cú pháp như sau:
dynamic_cast <new_type> (expression)
reinterpret_cast <new_type> (expression)
static_cast <new_type> (expression)
const_cast <new_type> (expression)

Mỗi các ép kiểu này đều có đặc điểm riêng mà chúng ta sẽ tìm hiểu chi tiết ở phần sau.

dynamic_cast

dynamic_cast chỉ sử dụng với các con trỏ và các tham chiếu tới các lớp (Hoặc với void *). Mục đích của nó là đảm bảo rẳng kết quả của việc chuyển đổi sẽ trỏ đến một đối tượng hoàn chỉnh hợp lệ của kiểu con trỏ đích. Việc này tự nhiên bao gồm việc chuyển đổi upcast (Chuyển đổi từ con trỏ ở lớp kế thừa tới con trỏ lớp cơ sở) như cách chuyển đổi ngầm định. Nhưng dynamic_cast cũng hỗ trợ chuyển đổi downcast (Chuyển đổi từ con trỏ ở lớp cơ sở sang con trỏ ở lớp kế thừa), nhưng việc chuyển đổi chỉ thành công khi đối tượng trỏ tới là đối tượng hoàn chỉnh hợp lệ của kiểu đích. Ví dụ:

// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;

class Base { virtual void dummy() {} };
class Derived: public Base { int a; };

int main () {
  try {
    Base * pba = new Derived;
    Base * pbb = new Base;
    Derived * pd;

    pd = dynamic_cast<Derived*>(pba);
    if (pd==0) cout << "Null pointer on first type-cast.\n";

    pd = dynamic_cast<Derived*>(pbb);
    if (pd==0) cout << "Null pointer on second type-cast.\n";

  } catch (exception& e) {cout << "Exception: " << e.what();}
  return 0;
}

Kết quả chạy chương trình là:

Null pointer on second type-cast.

Đoạn code trên cố gắng thực hiện hai chuyển đổi động từ con trỏ trỏ tới đối tượng kiểu Base* (pba và pbb) tới một con trỏ kiểu Derived*, nhưng chỉ chuyển đổi đầu tiên thành công. Tại sao như vậy, bạn chú ý tới khởi tạo tương ứng từng biến:

Base * pba = new Derived;
Base * pbb = new Base;

Mặc dù cả hai đều trỏ tới kiểuBase*, nhưng pba thực sự trỏ tới đối tượng kiểu Derived, trong khi pbb trỏ tới đối tượng kiểu Base. Do đó khi thực hiện ép kiểu bằng dynamic_cast, pba trỏ đối một đối tượng đầu đủ của lớp Derived, trong khi pbb trỏ tới đối tượng Base, một đối tượng không đầu đủ của lớp Derived.

Khi việc ép kiểu thất bại, dynamic_cast sẽ trả về con trỏ kiểu null. Với trường hợp tham chiếu, dynamic_cast sẽ bắn ra ngoại lệ bad_cast khi chuyển đổi không thành công.

dynamic_cast cũng có thể thực hiện các chuyển đổi ngầm định khác như:

  • Chuyển đổit từ con trỏ null giữa các kiểu con trỏ (Ngay cả với các lớp không liên quan)
  • Chuyển đổi kiểu con trỏ với kiểu bất kỳ về con trỏ kiểu void*

static_cast

 

 

 

http://www.cplusplus.com/doc/tutorial/typecasting/