C++之类型转换

104 阅读4分钟

导读

提到类型转换,相信有过编程经验的小伙伴们都不陌生了。之前笔者在《NDK编程Java如何保存C或C++对象》 一文就中使用了类型强转的方式。

既然C++是继承于C的语言,那么它在类型转换上又做了哪些扩展呢?

C语言式转换

C语言式的类型转换很简单,通过一个括号即可完成强转:(Type)var;。虽然C语言式转换简单,但是它是有不少缺点的,比如它可以在任意类型之间进行转换,比如将const类型的对象转换成非const类型的对象, 可以将一个基类的对象指针转化成一个派生类的对象指针等。这些强制转换对于C++来说显然是不太合理的,c++为了克服这些缺点,引进了4新的类型转换操作符,他们分别是:静态类型转换、动态类型转换、常类型转换和重解析类型转换。

C++的类型转换

1、静态类型转换 静态类型转换使用的是static_cast,它可以被用于强制隐型转换,例如将non-const对象转型为const对象,将int转型为double等, 它还可以用于很多这样的转换的反向转换,例如,void* 指针转型为有类型指针,基类指针转型为派生类指针等。

所谓静态类型,就是在编译器就能确定的类型,静态类型转换的使用格式是:

static_cast<目标类型>(标识符)

例如:

int main() {
    double pi = 3.1415926;
    int a = (int)pi;    //c语言的旧式类型转换
    int b = pi;         //隐式类型转换
    int c = static_cast<int> (pi); //c++的新式的类型转换运算符
    return 0;
}

2、动态类型转换 与静态类型转换相对的是动态类型转换,动态类型是表示在编译期间不能确定的类型,需要到运行时才能确定的类型,它使用dynamic_cast标识,它格式是

dynamic_cast<目标类型>(标识符)

动态类型转换的一个重要作用就是将父类对象转换成派生类的对象,如果转换失败则会返回空指针。

例子:

main.cpp
class Animal{
public:

    virtual void print(){
        std::cout << "print print" << std::endl;
    }

    virtual ~Animal(){

    }
};

class Cat: public Animal{
public:
    void print() override{
        std::cout << "print Cat" << std::endl;
    }
};

class Dog:public Animal{
    void print() override{
        std::cout << "print Dog" << std::endl;
    }
};

int main() {
    Animal *animal = new Cat();
    Dog *dog = dynamic_cast<Dog*>(animal); // 如果转换失败会返回空指针
    if(nullptr == dog){
        std::cout << "转换失败" << std::endl;
    }
    Cat *cat = dynamic_cast<Cat*>(animal);
    if(cat){
        std::cout << "转换成功" << std::endl;
    }
    return 0;
}

dynamic_cast的转换一定要建立在多态的基础上,也就是说父类一定要有一个虚函数或者纯虚函数才行,否则转换是编译不通过的。

public:
    int a;
    Fruit():a(10){

    }
    // 一定需要一个虚函数或者纯虚函数,否则dynamic_cast转换无法编译
    virtual void show();
};

class Apple: public Fruit{
    void show() override{
        
    }
};

void printApple(Apple* apple){

}

int main() {
    Fruit* apple = new Apple();
    // printApple(apple); // 错误,*apple的类型是Fruit,但是函数printApple需要的是Apple指针
    printApple(dynamic_cast<Apple*>(apple));
    return 0;
}

3、常类型转换 常类型转换又叫脱常转换,意思就是可以将一个const变量转换成一个非const变量,使用的标识符是const_cast

** const_cast 最常见的用途就是将某个对象的常量性去除掉,并且只能应用于指针或引用。**

我们知道const对象只能调用const的函数,那么如果const对象也想调用非const函数怎么办?这时候就需要使用const_cast进行脱常了。

我们来看下以下的两个例子:

void func(int & a){
    std::cout << "func---a:" << a << std::endl;
    a = 100; // 这里改变了引用的值,会起作用吗?需要看传递进来的变量是否是const的
}

int main() {
    const int d = 30;
    func(const_cast<int &>(d)); // 如果在func中修改了d的值,会起作用吗?不会
    std::cout << "d:" << d << std::endl;
    return 0;
}
class Fruit{
public:
    int a;
    Fruit():a(10){

    }
};

void print(Fruit& fruit){
    fruit.a = 20;
    std::cout << "fruit地址:" << &fruit << std::endl;
}

int main() {
    Fruit fruit;
    const Fruit& constFruit = fruit;
    std::cout << "a:" << fruit.a << std::endl;
    std::cout << "constFruit地址:" << &constFruit << std::endl;
    constFruit.a = 20; // 错误,变量a不能修改
    print(&constFruit); // 错误,不能将一个const修饰的变量传递给一个非const修饰的函数
    print(const_cast<Fruit&>(constFruit)); // 可以,通过const_cast类型转换,并且函数内部可以修改constFruit的变量a了
    std::cout << "constFruit.a:" << constFruit.a << std::endl;
    return 0;
}

以上两个例子当中,为什么一个类通过const_cast转换之后在函数内部修改该类的成员变量后会生效,而int类型经过了const_cast转换后在函数内部修改了值却不生效呢?

这里需要记住一个结论:

使用const_cast去除const限定的属性的目的不是为了修改它的内容,而是是为了函数能够接受这个实际参数。 因此使用const_cast去除const限定的属性的类对象是可以改变成员变量的,但是对于内置数据类型,却表现未定义行为.

4、reinterpret_cast reinterpret_cast。是特意用于底层的强制转型,这个操作符的转换结果几乎总是与编译平台息息相关。也就是说reinterpret_casts不具跨平台移植性, 所以这里就不多做介绍了。

5、隐式转换

隐式转换给我们带来了便利的同时也会给我们带来各种各样的隐患。

让我们通过以下程序简单看看隐式转换带来的隐患:

namespace Flyer {
    class A {
    public:
        A(int age) : age(age) {
            std::cout << "自定义构造函数" << endl;
        }

        A(const A &a) {
            std::cout << "拷贝构造" << endl;
        }

        ~A() {
            std::cout << "析构函数" << endl;
        }
    public:
        int age;
    };
}

int main() {
    Flyer::A a = 10; // 隐式转换初始化,实际上是调用了A的构造函数,歧义了
    a = 30;         //  调用了A的构造函数
    return 0;
}

在上面的程序中因为隐式转换的存在,可能是简单的赋值操作,却变成了类的构造,给人一种欺骗了我的眼睛的感觉....

如果想要去除隐式转换,彻底消除这样的隐患那该怎么办呢?答案也很简单,就是在类的构造函数上增加explicit关键字即可:

using namespace std;
namespace Flyer {
    class A {
    public:
        explicit A(int age) : age(age) {
            std::cout << "自定义构造函数" << endl;
        }

        A(const A &a) {
            std::cout << "拷贝构造" << endl;
        }

        ~A() {
            std::cout << "析构函数" << endl;
        }
    public:
        int age;
    };
}

int main() {
    Flyer::A a = 10; // 错误,explicit不运行隐式转换
    Flyer::A b{30}; // 正确,调用构造函数
    b = 40;      //错误,explicit不运行隐式转换
    return 0;
}

如果你需要进行转换但是又不想接受隐式转换带来的隐患,那怎么办呢?在《More Effective C++》一书中作者给了我们建议:

条款 21:利用重载技术(overload)避免隐式类型转换(implicit type conversions)

推荐阅读

《C++之指针扫盲》
《C++之智能指针》
《C++之指针与引用》 《C++之右值引用》

关注我,一起进步,人生不止coding!!! 公粽号:思想觉悟