持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情
从C++类型之间的关系讲起
C++基础类型
C++基础类型有这么几个大类:
- 整型数据类
- 浮点数据类
- Null Pointer
- Void
每个大类里又包含几个具体的类型, 用整型数据类和浮点数据类来说明一下:
整型数据类
比如说:
- char
- short
- int
- long
- bool
- 等等
浮点数据类
比如说:
- float
- double
- 等
基础类型所占字节数
C++标准里对每一种基础类型都规定了相应的size,我们在代码里可以使用sizeof来获得相应的size,单位是字节:
#include <iostream>
int main()
{
std::cout << "sizeof(int) == " << sizeof(int) << std::endl;
std::cout << "sizeof(char) == " << sizeof(char) << std::endl;
std::cout << "sizeof(bool) == " << sizeof(bool) << std::endl;
std::cout << "sizeof(double) == " << sizeof(double) << std::endl;
std::cout << "sizeof(float) == " << sizeof(float) << std::endl;
return 0;
}
在我的机器上, 打印如下:
sizeof(int) == 4
sizeof(char) == 1
sizeof(bool) == 1
sizeof(double) == 8
sizeof(float) == 4
如果你运行的结果跟我不一样,不重要,这跟具体的编译器有关系。
基础类型之间的关系
如果两个基础类型A和B,他们能不能互相转换呢?
能转换,但是还分下面两种:
- 类型提升
- 类型转换
基础类型的类型提升
这又分两种:
- 整型数据的类型提升
- 浮点数据的类型提升
整型数据的类型提升
- char 转 int
- short 转 int
- bool 转 int
- 等等
浮点数据的类型提升
- float 转 double
这里给一个反例,比如说 char 转 short,叫不叫类型提升?
答案是:不是类型提升。
究其原因是因为类型提升,并非是仅仅用一个占内存大的去接收一个占内存小的,而是为了让操作系统更快的处理这个数据。
类型转换
再来说说类型转换,除去类型提升之外的所有转换都称之为:'类型转换'。
提升和转换的区别
- 类型转换可能包含了,变窄(接收者所能表达的数据范围小),进而引起数据的丢失。
- 类型提升是绝对不可能引起数据丢失的。
再来看看在函数重载的时候,优先级问题:
void foo(int input){}
void foo(short input){}
void test()
{
char a{'1'};
foo(a); // 问,这个foo 是哪个foo?
}
上面的问题,就说明了,类型提升的优先级要高于类型转换
- char -> int 是类型提升
- char -> short 是类型转换
所以上面的foo调用,是选取了 foo(int)。
类型转换的过程
猜测:字节拷贝?
比如说:
float f{1.0f};
int a{f};
我们知道,
- float 占四个字节
- int 也占四个字节
那么上述代码能不能通过编译呢?
如果按照字节拷贝的说法,那是可以的,就直接拷贝,不会造成内存上的问题。
但是,这个是编译不过的,因为float所能表达的数据范围要比int大, 所以这里属于变窄转换。
所以要加上强制转换:
float f{1.0f};
int a{static_cast<int>(f)};
std::cout << a << std::endl;
这回能通过编译,并且能打印: 1。
我们知道,int类型和float类型在编码的时候,是不一样的,所以f所占的四个字节和a所占的四个字节,肯定是不一样的。
具体的编码算法要研究float编码和int编码。
我们这里写一个函数,打印各自的四个字节到底是什么, 来确定他们确实存储的字节不一样:
#include <iostream>
template <typename T>
void print_byte(T input)
{
unsigned char *p{reinterpret_cast<unsigned char *>(&input)};
for (int index = 0; index < 4; ++index)
{
std::cout << static_cast<unsigned int>(p[index]) << " ";
}
std::cout << std::endl;
}
int main()
{
float f{1.0f};
int a{static_cast<int>(f)};
print_byte(f);
print_byte(a);
return 0;
}
在我的机器上:
0 0 128 63
1 0 0 0
一目了然了,所以结论就是,基础类型转换的过程,并不是字节拷贝,而是根据各自的编码重新计算。
C++中的class
两个class类型之间的关系,有哪几种呢?
-
是
- A 是 B,且 B 也是 A
- A 是 B, 但 B 不是 A
-
不是但可转换
-
不是但不能转换
类型别名用于class
class Animal{};
using Dongwu = Animal;
上面有两个类型,Animal,Dongwu,他们在编译器看来,是完全相同的两个类型,这种就是:
Animal 就是 Dongwu,Dongwu就是Animal, 他们可以任意互换。
这种就是 A是B,且B也是A的关系。
这种完全等价的两个类型,不存在转换的概念。
子类与父类
class Animal{};
class Fish: public Animal{};
Fish是Animal,但Animal不是Fish,这就是所谓
A是B,但B不是A。
如果B不是A,在通常情况下,无法进行转换,也就是说:
Animal a;
static_cast<Fish>(a); // 无法转换,因为 Animal 不是 Fish
反过来:
Fish f;
static_cast<Animal>(f); // 这句可以编译, 因为 Fish 是 Animal
既然可以编译,我们来分析一下,这种是如何转换的。
首先从子类对象与父类对象的内存构成来看,一个子类对象里包含了完整的父类对象。
所以父类对象的size肯定是要比子类对象的size要小的。
上面的static_cast<Animal>(f);, 无疑于将一个大size的类型转换到一个小size的类型。
我们可以非常自然的想到,这种转换,肯定是只保留了子类对象里的父类部分,而丢失了子类部分。
一般的,这种转换称之为object slicing, 对象裁剪。
看下面的例子:
Fish f1{};
Fish f2{};
Animal& a{f1};
a = f2; // 这里出现了 Fish -> Animal ,也就是裁剪
我们画两个图,来解释上面的问题:
- 未裁剪之前, 注意紫色的部分,是f2中的Animal部分
- 裁剪之后, a和f1本身就是一个,这一块内存成了缝合体。
那这里能不能不裁剪,让行为正常一点呢:
重载Animal的 operator= , 并将这个操作符变成virtual, 这里就不提了。
不同的class类型
上面说了子类与父类这种特殊的关系。
再来说说一般情况下,类与类的关系。
class A{};
class B{};
void foo(A a){
}
void test()
{
B b;
foo(b); // 这里能行吗
}
显然是不行的。A和B完全八竿子打不着。
但是:
C++有时候烦就烦在这个地方,这两个八竿子打不着的,也能产生关系:
这里说两种,能让上述代码编译通过的办法:
- A 有一个 constructor,接受 B 类型做为参数
- B 重载了 operator A, 让自己可以转换成 A
先说第一种:
class B;
class A
{
public:
A(const B &b) {}
};
class B
{
};
这种能行,是因为编译发现foo函数需要的类型是A,但是传了B,它就琢磨:
- 我能不能找找,能不能就地构造一个A出来,然后就去看A的constructor,果然发现能行!!
再说第二种:
class A
{
};
class B
{
public:
operator A()
{
return A{};
}
};
这个operator A是一个重载的操作符,它的作用就是,调用这个操作符的时候,能返回一个A。
编译器的想法:
- B有没有重载
operator A呢,我去瞅瞅,果然重载了,就这样调用这个函数吧,生成一个A,出去然后传给foo。
这两种,如果你的代码里都实现了,那么 operator A 这种优先级更高。
explicit 关键字可以防止隐式构建对象
class B;
class A
{
public:
explicit A(const B& b) {} // 注意,前面加了explicit关键字
}
class B{};
void foo(A a)
{
}
void test()
{
B b;
foo(b); // 编译器无权调用上面的explicit的constructor,来就地构建一个A
}
此时此刻,上面的代码是编译不了的,但是这仅仅是说,编译器没有这种隐式构建的权限,如果我们非要编译器就地构建,它还是得老实照办:
void test()
{
B b;
foo(static_cast<A>(b));
}
上面,我们加了这种static_cast,就是相当于给了编译器这种就地构建的权利!
所以就又能编译通过了!
总结
本文总结了C++类型之间的关系,这包含基本类型与class类型。
基本类型之间可以互相转换。
class之间也可以互相转换。
class之间的转换又可以通过我们的自定义代码进行控制。
如果能熟练掌握,那么我们的代码就更上一层楼。