【C++】【类型转换】都在这了(肝货一万字!!!)

目录

1. 前言

2. C++类型基础知识

C语言风格的类型转换

3. 隐式类型转换

3.1 隐式类型转换介绍

3.2 explicit关键字

3.2.1 介绍

3.2.2 限制

3.2.3 使用场景

4. 显式类型转换

4.1 static_cast

4.2 reinterpret_cast

4.3 dynamic_cast

4.4 const_cast

5. C++标准库中的类型操作类模板

5.1 typeid运算符

5.2 std::is_same模板

5.3 std::remove_cv模板

5.4 std::enable_if模板

5.5 std::add_pointer模板

5.6std::conditional模板

6. 类型转换的潜在问题

6.1 数据精度损失

6.2 类型转换不明确

6.3 未定义的行为

7.C++11一些新的特性

8.总结


1. 前言

        C++是一种静态类型语言,因此在编写程序时需要明确定义变量的数据类型。在程序中,数据类型通常需要进行转换,例如将浮点数转换为整数、将指针类型转换为不同类型的指针等等。C++提供了多种类型转换的方式,包括隐式类型转换和显式类型转换

        隐式类型转换由编译器自动执行,通常发生在算术计算或需要分配空间的情况下。显式类型转换由开发者手动执行,在数据类型转换的过程中,开发者需要考虑数据精度的损失、数据类型的大小等问题。C++提供了四种强制类型转换方式,覆盖了许多开发者的需求。

        本篇文章将从C++类型转换的基础知识入手,深入探讨C++中各种类型转换的使用场景、注意事项,以及剖析转换的潜在问题,并从C++标准库中的类型操作类模板、以及C++11中的一些新特性等方面对类型转换进行全面解析。

2. C++类型基础知识

        在深入探讨C++类型转换的各种技术之前,有必要先了解C++类型的一些基础知识。

        C++中的数据类型主要分为基础类型和复合类型。基础类型包括整型、浮点型、字符型和布尔型;复合类型包括数组、结构体、联合和类等。

在使用这些类型时,开发者需要考虑以下几个问题:

  1. 需要存储多少数据?
  2. 数据的类型是什么?
  3. 数据在内存中的布局是什么?

        根据这些问题,开发者可以选择不同的数据类型来存储数据。例如,需要存储整数的时候,可以使用int类型,当需要存储浮点数时,则可以使用float或double类型。

        对于复合类型,在C++中,可以通过结构体或类来自定义类型。结构体是一个数据成员的集合,类是一种封闭了数据成员和成员函数的数据类型。

C语言风格的类型转换

        此外这里了解一下C语言风格的类型转换,C语言风格的类型转换是最古老的类型转换方式,它使用了一种特殊的语法来进行类型转换。其语法如下:

(type_name)expression

        其中,type_name是要转换的数据类型,expression是要转换的表达式。

        例如,在对两个整数进行除法运算时,如果我们想得到一个浮点数结果,可以使用C语言风格的类型转换,如下所示:

int a = 10, b = 3;
float c = (float)a / (float)b;

        在这个例子中,我们将整型变量a和b分别强制转换为浮点型,再进行除法操作。这样就可以得到浮点型结果。C语言风格的类型转换虽然简单易用,但它有很大的安全隐患,在实际编程中应尽量避免使用。

        综上,对于类型的转换,C++提供了多种转换方式,包括隐式和显式转换方式。下面将分别介绍这两种转换方式。

3. 隐式类型转换

3.1 隐式类型转换介绍

        隐式类型转换是一种由编译器自动执行的转换方式,通常发生在算术计算或需要分配空间的情况下。C++编译器会尝试将较小的类型转换为较大的类型,以避免信息丢失。

例如,以下是一个隐式类型转换的例子:

short a = 8;
int b = a;  // 隐式类型转换

        在这个例子中,变量a是short类型,变量b是int类型,因为short类型比int类型小,所以变量a会隐式地转换成int类型,然后赋值给变量b。

此外,在计算表达式时,C++编译器会隐式地将所有操作数的类型转换为相同的类型。例如:

int a = 8;
double b = 4.5;
double c = a + b;  // 隐式类型转换

        在这个例子中,变量a是整型,变量b是浮点型,在变量c中将a和b相加会隐式地将变量a转换为浮点型,以便与变量b进行加法运算。

        需要注意的是,隐式类型转换虽然方便了开发者,但也可能会导致一些预期之外的结果。因此,在实际编程中建议尽量使用显式类型转换,以免产生错误。

3.2 explicit关键字

3.2.1 介绍

  explicit 关键字通常用于构造函数,其作用是抑制隐式转换。在 C++ 中,我们可以通过将一个参数的构造函数标记为 explicit ,来避免 charintdouble 等类型的隐式转换。

        一个显式的构造函数不能用于隐式类型转换,但它可以用于显示类型转换。下面是使用 explicit 和不使用 explicit 的基本示例代码:

#include 
using namespace std;

class A {
public:
    A(int) { std::cout << "A(int)" << std::endl; }
};

class B {
public:
    explicit B(int) { std::cout << "B(int)" << std::endl; }
};

int main(){
    A a1=1;  // OK: 隐式转换
    B b1=1;  // OK:显式转换
    A a2='x';  // OK: 隐式转换 
    // B b2='x';  // 编译错误:显式构造函数,禁止隐式转换  
    return 0;
}

        在 A 中,我们定义了一个具有参数 int 的构造函数,并没有使用 explicit 进行标记,所以它可以隐式转换为 A 类型。在 B 中,我们使用了 explicit 进行标记,所以不能隐式转换为类型 B

3.2.2 限制

  • explicit 关键字只能用于构造函数。
  • 必须应用于只包含单个参数的构造函数。
  • explicit 不能与 virtual 同时使用。
  • 不能应用于默认构造函数。
  • 不能应用于复制构造函数。
  • 仅适用于直接初始化,当执行隐式转换时,编译器将忽略 explicit 关键字。

3.2.3 使用场景

        1)避免不必要的隐式转换

        在实际编程中,由于各种隐式转换的存在,可能会出现一些难以察觉的错误,特别是在参数传递和函数模板的定义中。因此,为了避免意外的类型转换,可以使用 explicit 关键字限制隐式转换。

        2) 明确类的使用方式

        一些类可能会有多种用途,使用 explicit 可以明确告诉使用者如何使用该类,从而减少程序员的错误使用。

        3)避免精度损失

        当将 int 类型隐式转换为 float 类型时,可能会导致精度损失,使用 explicit 可以避免该问题。

class Float {
public:
  explicit Float(int val) : val_(val) {}
  float GetVal() const { return val_; }

private:
  float val_;
};

int main() {
  Float f = Float(5);
  return 0;
}

        在上述示例中,使用了 explicit 来防止直接将 int 类型隐式转换为浮点型,避免由此引发的精度损失。

4. 显式类型转换

        显式类型转换是一种由开发者手动执行的操作,它允许开发者实现不同类型之间的转换,以及精度的提升或降低。C++提供了四种强制类型转换方式,包括static_cast、dynamic_cast、const_cast和reinterpret_cast。

4.1 static_cast

        static_cast是一种较为常用的类型转换操作符。使用static_cast时,我们需要明确知道要进行类型转换的类型,并且要求源类型和目标类型之间存在隐含或显式的类型转换规则。

其语法如下:

static_cast(expression)

例如,在进行基本数据类型转换时,可以使用static_cast,如下所示:

double a = 1.23;
int b = static_cast(a);

(static_cast指针类型转换的注意事项)

        需要注意的是,在进行指针类型转换时,static_cast并不会进行运行时的类型检查。因此,在使用static_cast将一个指针类型转换为另一个指针类型时,我们必须确保两个指针类型之间是类型兼容的,并且要保证指针指向的实际对象的类型与转换后的指针类型匹配。

例如,下面的代码中,将一个指向int类型的指针p转换为指向double类型的指针q:

int x = 123;
int *p = &x;
double *q = static_cast(p);

        在这个例子中,将指向int类型的指针p强制转换为double类型指针q。但是,由于p指向的是int类型的变量,而将其转换为double类型可能会导致精度损失和undefind行为。

因此,在进行指针类型转换时,我们需要慎重考虑,尽量避免隐患。

4.2 reinterpret_cast

        reinterpret_cast是一种非常危险的类型转换模板,它可以将一个指针类型转换为另一种指针类型,也可以将一个整型类型转换为一个指针类型。

其语法如下:

reinterpret_cast(expression)

例如,在进行类型强制转换时,可以使用reinterpret_cast,如下所示:

int x = 123;
char *p = reinterpret_cast(&x);

        在这个例子中,将指向int类型变量x的指针强制转换为指向char类型的指针p。此时,p指向的是元素1的地址,但是由于指针类型之间的兼容性是不确定的,因此这种类型转换是非常危险的,可能存在严重的安全隐患。

        需要注意的是,在进行reinterpret_cast类型转换时,编译器不会进行任何类型检查。因此,在使用reinterpret_cast时,我们必须确保类型转换的安全性,并尽量避免类型转换的潜在问题。

4.3 dynamic_cast

        dynamic_cast是一种比较特殊的类型转换操作符,它通常用于进行运行时类型识别和多态类型转换。使用dynamic_cast时,需要使用指向对象的指针或引用来进行类型转换。

其语法如下:

dynamic_cast(expression)

        在进行dynamic_cast类型转换时,编译器会进行一次运行时类型检查。如果源类型是目标类型的子类类型,那么就会将指针或引用转换为目标类型;否则,就会返回nullptr。因此,在使用dynamic_cast进行类型转换时,我们需要确保源类型是目标类型的子类类型,并进行判空处理。

例如,下面的代码中,使用dynamic_cast从一个指向基类的指针p转换为一个指向子类的指针t:

class Base {
public:
    virtual void fun() {}
};

class Derived: public Base {
public:
    void fun() override {}
};

int main() {
    Base *p = new Derived;
    Derived *t = dynamic_cast(p);
    if (t) {
        t->fun();
    }
    return 0;
}

        在这个例子中,将指向Base类的指针p转换为指向Derived类的指针t。由于Derived类是Base类的子类,因此可以使用dynamic_cast进行类型转换,t指向的是 Derived类的实例。

        需要注意的是,dynamic_cast只能用于多态类型的转换,即需要源类型和目标类型都是多态类型。如果源类型和目标类型不是多态类型,就无法进行dynamic_cast类型转换。

除此之外,我们还需要注意以下几点:

  • 使用dynamic_cast进行类型转换时,类的虚函数表必须完整可访问。如果在动态类型转换时,虚函数表无法访问,就会导致类型转换失败。

  • dynamic_cast只能用于将指向子类的指针或引用转换为指向父类的指针或引用,或者将指向父类的指针或引用转换为指向子类的指针或引用。在同级类之间相互转换时,dynamic_cast是不能使用的。

  • dynamic_cast进行类型转换时比较耗时,因此在实际使用中要慎重选择。

4.4 const_cast

        const_cast用于移除变量的常量限定符(const),从而允许变量被修改。以下是一个使用const_cast的示例:

const int a = 10;
int& r = const_cast(a);  // 将 const int 类型转换为 int 类型的引用
r = 20;

        在这个例子中,将const int类型的变量a转换为int类型的变量r,并将其修改为20。需要注意的是,在使用const_cast时,必须确保变量原本是非常量,否则会导致未定义的行为。

5. C++标准库中的类型操作类模板

        除了四种强制类型转换方式外,C++标准库中还提供了多种类型操作类模板,这些模板可以用于实现类型转换和其他一些类型操作的功能。

  • typeid运算符:用于获取对象的类型信息,返回的是一个type_info对象。

  • std::is_same模板:用于比较两个类型是否相同,返回值是一个bool类型的常量表达式。

  • std::remove_cv模板:用于去除类型的const和volatile限定符,返回的是一个去除了const和volatile限定符的类型。

  • std::remove_reference模板:用于去除类型的引用限定符,返回的是一个去除了引用限定符的类型。

  • std::remove_pointer模板:用于去除类型的指针限定符,返回的是一个去除了指针限定符的类型。

  • std::enable_if模板:用于根据类型的条件判断进行类型选择,返回的是一个指定条件下的类型。        

5.1 typeid运算符

#include 
#include 

int main() {
    int a = 10;
    double b = 3.14;
    std::cout << typeid(a).name() << std::endl;
    std::cout << typeid(b).name() << std::endl;
    return 0;
}

        在上面例子中,使用typeid运算符来获取变量a和b的类型信息,并输出类型信息。

5.2 std::is_same模板

#include 
#include 

template
void check_type() {
    if(std::is_same::value) {
        std::cout << "Two types are the same." << std::endl;
    } else {
        std::cout << "Two types are different." << std::endl;
    }
}

int main() {
    check_type();
    check_type();
    return 0;
}

        在上面例子中,使用std::is_same模板来比较两个类型是否相同,如果相同就输出"Two types are the same.“,如果不同就输出"Two types are different.”。

5.3 std::remove_cv模板

#include 
#include 

int main() {
    typedef const volatile int CVInt;
    typedef std::remove_cv::type NewInt;
    std::cout << std::is_same::value << std::endl; //输出1,表示两个类型相同
    return 0;
}

        在上面例子中,使用std::remove_cv模板来去除一个类型的const和volatile限定符,得到去除限定符后的类型。

5.4 std::enable_if模板

std::enable_if模板常用于根据类型的条件判断进行类型选择。例如:

#include 
#include 

using namespace std;

template 
typename enable_if::value, void>::type
print_value(T val) {
    cout << *val << endl;
}

int main() {
    int x = 10;
    int* y = &x;
    print_value(y);  // 输出 "10"
    print_value(x);  // 编译错误,因为 x 不是指针类型
    return 0;
}

        在上面例子中,模板函数print_value只有当传入类型是指针类型时才被声明和定义。在main函数中,我们调用print_value(y)时会输出指针所指向的值,而调用print_value(x)会导致编译错误,因为x不是指针类型。

5.5 std::add_pointer模板

std::add_pointer模板可以把一个类型转换为指针类型,例如:

#include 
#include 

using namespace std;

int main() {
    typedef add_pointer::type ptr; // ptr 是 int* 类型
    cout << is_same::value << endl;  // 输出 true
    return 0;
}

        在上面例子中,我们使用add_pointer模板将类型int转换为指针类型int*,并声明了别名ptr。可以看到,使用add_pointer模板可以快速实现类型转换。

5.6std::conditional模板

std::conditional模板可以根据条件进行类型选择。例如:

#include 
#include 

using namespace std;

template 
void print_value(const T& val) {
    typedef typename conditional::value, const char*, const T&>::type output_type;
    output_type output = is_pointer::value ? "pointer" : val;
    cout << output << endl;
}

int main() {
    int x = 10;
    int* y = &x;
    print_value(x);   // 输出 "10"
    print_value(y);   // 输出 "pointer"
    return 0;
}

        在上面例子中,我们使用conditional模板将val的类型转换为指针类型const char*或者const T&类型,然后将其存储在变量output中,并输出output。可以看到,conditional模板可以根据条件决定类型,从而实现灵活的类型转换。

6. 类型转换的潜在问题

        在进行类型转换时,可能会导致一些潜在的问题,例如数据精度损失、类型转换不明确、未定义的行为等。下面针对一些可能的问题进行详细说明。

6.1 数据精度损失

        在进行类型转换时,可能会导致数据精度损失,例如将浮点数转换为整型时,会截断小数部分。

以下是一个数据精度损失的示例:

double d =101.999;
int i = static_cast(d);
std::cout << "d = " << d << ", i = " << i << std::endl;

        在这个示例中,将一个double类型的变量d转换为int类型的变量i,由于int类型无法表示小数部分,小数部分会被截断,因此i的值为101,而不是102。

        为了避免数据精度损失,可以使用round函数对浮点数进行四舍五入,并将结果转换为整型。例如:

double d = 101.999;
int i = static_cast(std::round(d));
std::cout << "d = " << d << ", i = " << i << std::endl;

        在这个示例中,使用std::round对d进行四舍五入,然后将结果转换为整型i,i的值为102。

6.2 类型转换不明确

        在进行类型转换时,可能会出现类型转换不明确的情况。例如,将void*指针转换为指向具体类型的指针时,必须确保转换的目标类型是准确无误的,否则会导致未定义的行为。

以下是一个类型转换不明确的示例:

void* ptr = new double(3.14);
int* pInt = static_cast(ptr);

        在这个示例中,试图将void类型的指针ptr转换为int类型的指针pInt,但由于ptr指向的是double类型的对象,因此此类指针转换通常是不明确的,将导致未定义的行为。

        为了避免类型转换不明确的问题,应该在进行指针类型转换时,确保转换的目标类型是可识别的,例如是派生或基类类型。

6.3 未定义的行为

        在进行类型转换时,可能会导致未定义的行为,例如使用reinterpret_cast转换指针类型时,需要确保转换结果能够合法访问,否则会导致未定义的行为。

以下是一个未定义的行为示例:

int a = 10;
char* pc = reinterpret_cast(&a);  // 使用 reinterpret_cast 将 int 型指针转换为 char 型指针
std::cout << *pc << std::endl;

        在这个示例中,试图将一个int类型的指针转换为char类型的指针pc,并打印其内容。由于char类型只有1个字节,而int类型有4个字节,在使用char类型指针pc访问int类型的值时,会发生未定义的行为。

        为了避免未定义的行为,应该确保在进行类型转换时,转换的目标指针能够合法地访问内存中的数据,同时避免对指针类型所指向的数据进行未经转换的访问。

7.C++11一些新的特性

C++11引入了一些新的特性,其中有一些与数据类型转换相关,包括:

  • static_assert关键字:用于在编译时断言某个条件是否成立,可以用于检查类型转换是否安全等。

  • nullptr关键字:用于表示空指针,可以是auto_ptr智能指针、void指针、char指针等类型转换时的向NULL的替代。

  • std::move函数:用于将对象转移,可以用于实现移动语义,并避免不必要的复制操作。

  • rvalue reference(右值引用):用于将实现移动语义,并实现对对象的“盗用”,减少需要执行对象副本的拷贝构造函数的调用,提高程序的执行效率。

  • initializer_list(初始化列表):用于对数组、容器等进行初始化,可以用于实现类型转换和类型检查。

        这些新特性都为类型转换和类型操作提供了更加简单、有效的方式,可以提高程序的执行效率和可读性,使类型转换更加安全和简便。

8.总结

        本篇文章详细讲解了C++中各种类型转换的使用方法和注意事项。隐式类型转换由编译器自动执行,通常发生在算术计算或需要分配空间的情况下;显式类型转化由开发者手动执行,包括四种强制类型转换方式:static_cast、dynamic_cast、const_cast和reinterpret_cast。

        除了四种强制类型转换方式外,C++标准库中还提供了多种类型操作类模板,这些模板可以用于实现类型转换和其他一些类型操作的功能。

        在进行类型转换时,可能会出现一些潜在的问题,包括数据精度损失、类型转换不明确和未定义的行为等。在实际编程中,需要注意这些问题以及如何避免这些问题的出现,以保证程序的正确性和稳定性。

你可能感兴趣的:(C/C++,c++,c语言,开发语言,数据类型转换,explicit)