本文主要C++的基本数据类型如 int, float 等静态数据类型,然后进一步探讨C++中的动态数据类型以及运行时类型辨识(Run-Time Type Identification, RTTI机制)。
1. C++基本的静态类型
在 c++ 中的静态数据类型指的是:变量的数据类型在编译的时候已经被决定,而不是在运行的过程,也就是说变量只能被特定类型的数据进行幅值。(可以类比于 python,python在定义一个变量、编译一个变量时,是不需要指定类型的,在执行的过程通过数据传入实时定义类型)。故而C++是一门 静态类型语言,在编译时会进行类型的匹配矫正。
常见的数据类型及其尺寸:
数据类型 | 全称 | 尺寸 |
---|---|---|
int | integer (包括正负) | 4 bytes (64位系统) |
short | short integer | 2 bytes (64位系统) |
long | long integer | 4 bytes (64位系统, linux 8 bytes) |
long long | long long integer | 8 bytes |
float | floating-point type | 4 bytes |
double | double-precision floating-point | 8 bytes |
char | single character (ASCII) | 1 byte |
bool | boolean (True/False) | 1 byte |
auto
数据类型并不是动态的数据类型,在编译过程中,编译器会根据其初始化值自动推断其数据类型,以下展示auto
关键字的常见用法:
#include <iostream>
#include <vector>
using namespace std;
// 0. 用 auto 作为函数返回值类型
auto add(int a, int b){
return a+b;
}
int main(){
// 1. 用 auto 作为变量类型进行变量定义,注意必须在声明的同时进行变量的初始化
auto autoVar = 9.6;
// auto autoVar; //报错,因为没有进行赋值初始化
// 2. 用 auto 作为 vector 的迭代器
vector<int> myVector = {1, 2, 3, 4, 5};
for(auto it = myVector.begin(); it != myVector.end(); it++){
cout << "number: " << *it << endl;
}
// 等价表达
for(vector<int>::iterator it = myVector.begin(); it != myVector.end(); it++){
cout << "number: " << *it << endl;
}
}
常见的衍生数据类型:
- Array 数组:使用连续的空间存储多个数值,例如:
int nums[5] = {1, 2, 3, 4, 5}; // 数据类型 数组名称[数组长度] = {数据定义}
- *指针:一种用于存储地址的变量,如:
int num = 5;
int* pNum = # // 数据类型* 指针名称 = &被指变量名称
- &引用:用于变量间共享地址操作(别名),如:
int nums = 5;
int& numRef = num; // 数据类型& 变量名称 = 变量名称
用户自定义类型:
- struct:结构体,用一个变量存储不同的数据类型,且所有的数据变量、函数或类都是公共的(public,即任意成员属性都可以直接访问或修改),如:
// 结构体类型的定义
struct Person{ // struct 结构体类型名称{ 属性1; 属性2;...;属性n;};
string name;
int age;
float height;
};
// 变量定义
Person p1 = {'Peter', 30, 168.0} ; // 结构体类型 变量名称 = {属性1, 属性2,...,属性n};
- class:类,类似于结构体,不过其成员函数可以设置为公有(public)或者私有(private,即指定部分成员属性不能访问或修改,可能有疑问,不能访问的属性要来何用,可以解释的一个方向是:如要写一个加权求和函数 y=k1x1+k2x2,这个权重k1,k2是不需要访问/修改的,且我们希望在类的所有的函数(作用域)都访问这个权重,这样的话就可以把权重k1,k2设为私有);
// 创建类
class Score{
public:
string name;
float score1;
float score2;
void printScore(){
cout << k1 * score1 + k2 * score2 << endl;
}
private:
float k1 = 0.618;
float k2 = 0.348;
}
// 实例化对象
int main(){
// 实例化对象
Score s;
s.name = "peter";
s.score1 = 95;
s.score2 = 50;
s.printScore();
};
- union:联合,它是一种特殊的类。通过关键字union进行定义,一个union可以有多个数据成员。联合中只能有一个数据成员可以有值。当给联合中某个成员赋值之后,该联合中的其它成员就变成未定义状态了。如:
union Data{
int num;
float decimal;
};
int main(){
// 实例化对象
Data s;
s.num = 4;
std::cout << "num: " << s.num << "\tdecimal: " << s.decimal << std::endl; // Output: num: 4 decimal: 5.60519e-45
s.decimal = 4.01;
std::cout << "num: " << s.num << "\tdecimal: " << s.decimal << std::endl; // Output: num: 1082151404 decimal: 4.01
};
2. 动态类型
C++是一门静态数据类型的语言,但是也提供了一定程度的动态数据类型的接口,即变量的数据类型在运行的时候才能决定。但是在使用时,也需要在代码中指定数据类型,无法达到 python 那种肆意妄为。所以动态类型在C++用的很少,数据冷门且没用的知识!以下主要讨论空指针和any数据类型两种操作。
- void* :空指针,空指针是一种通用的指针,可以指向任意的数据类型。通常被用于存储任意类型变量的引用,如:
std::string name = "Peter";
int age = 18;
void* ptr; // 空类型的指针
ptr = &name; //存储字符串类型的引用
// 调用指针时,表达式必须是指向完整对象类型的指针,即 static_cast<类型*>
std::cout << "name: " << *(static_cast<std::string*>(ptr)) << std::endl; // Output: name: Peter
ptr = &age;
std::cout << "age: " << *(static_cast<int*>(ptr)) << std::endl; // Output: age: 18
- std::any :相比于空指针,在C++17引入的 any 任意数据类型更加直接粗暴,直接看例子,一看就懂!
#include<iostream>
#include<any> //注意引用 any 头文件
int main(){
std::any value;
// 整型
value = 4;
// 类似于 空指针,需要指定运行时的数据类型,通过关键字:std::anycast<类型>(变量)
std::cout << "int: " << std::any_cast<int>(value) << std::endl; // 输出:int: 4
//浮点数,使用float会报错,原因不详;
value = 4.0001;
std::cout << "double: " << std::any_cast<double>(value) << std::endl; // 输出:double: 4.0001
};
3. 运行时类型辨识(RTTI机制)
RTTI机制是动态数据类型的另一种方法,与空指针和any相比,RTTI机制常用于类(class)数据类型,处理基类和派生类的关系,值得注意的是:RTTI只适用于包含虚函数的类。因为只有对于这种类层次结构,才应该将派生类的地址赋给基类指针。通俗来说,这里的动态属性体现在:基类的指针既可以指向基类,也可指向由基类产生的派生类;
以下主要讨论 typeid 和dynamic_cast 两种操作。
- typeid:它允许程序向表达式提问对象是什么类型,返回值是对一个常量对象的引用,该对象的类型是标准库类型 type_info 或者 type_info 的公有派生类型;举个例子,我有两个基类对象指针 base_ptr1和 base_ptr2,但是我并不清楚它们指向基类还是派生类,因此可以用 typeid 查看一下:
#include<iostream>
#include<typeinfo> // 引入关键头文件
// 定义一个基类
class Base{
virtual void dummy(){}; // virtual 关键字定义虚函数,RTTI机制适用于包含虚函数的类
};
// 由基类产生的派生类
class Derived1: public Base{}; // 派生类 Derived1 公开继承基类
int main(){
Base* base_ptr1 = new Derived1; // 定义一个基类指针,指向派生类1
Base* base_ptr2 = new Base; // 定义一个基类指针,指向基类
std::cout << "Type of ptr1: " << typeid(*base_ptr1).name() << std::endl; // 输出:Type of ptr1: 8Derived1
std::cout << "Type of ptr2: " << typeid(*base_ptr2).name() << std::endl; // 输出:Type of ptr2: 4Base
delete base_ptr1, base_ptr2; //删除指针,防止内存泄漏
};
- dynamic_cast:动态转换,看到 cast 就应该想到跟类型转换相关,dynamic_cast 的作用是把指针的类型转化成其基类或者派生类的类型。举个例子,我有1个基类对象指针 base_ptr1和两个派生类指针 derived_ptr1、derived_ptr2。我想把派生类1指针 derived_ptr1转化为基类是可以的,但是转化成派生类2的对象指针是不行的。当转化失败的时候就会返回0。
#include<iostream>
#include<typeinfo> // 引入关键头文件
// 定义一个基类
class Base{
virtual void dummy(){}; // virtual 关键字定义虚函数,RTTI机制适用于包含虚函数的类
};
class Derived1: public Base{}; // 派生类 Derived1 公开继承基类
class Derived2: public Base{}; // 派生类 Derived2 公开继承基类
int main(){
Base* base_ptr1 = new Derived1; // 定义一个基类指针,指向派生类1
Derived1* derived1 = dynamic_cast<Derived1*>(base_ptr1);
if (derived1){
std::cout << "Successful!"; // 输出: Successful! 因为 Derived1类是Base类的派生类
}
else{
std::cout << "Failed!";
}
Derived2* derived2 = dynamic_cast<Derived2*>(base_ptr1);
if (derived2){
std::cout << "Successful!";
}
else{
std::cout << "Failed!"; // 输出: Failed! 因为 Derived1类和 Derived2类没有派生关系
}
delete base_ptr1, derived1, derived2; //删除指针,防止内存泄漏
};
4. 类型转换
C++中提供了5种类型转换方法,包括C语言中的直接转换、static_cast
、const_cast
、dynamic_cast
和 reinterpret_cast
,以下代码块展示五种类型转换方法的使用场景以及一些错误的用法。
#include<iostream>
using namespace std;
void modifyVariance(int* ptr){
*ptr = 66;
};
int main(){
// 1. c 语言直接类型转换
// 使用场景:变量的类型直接转换
int initial_c = 1;
float transform_c = float(initial_c);
cout << "Variance after transform by c: " << typeid(transform_c).name() << endl; // Variance after transform by c: f
// 2. static_cast
// 使用场景:常见变量类型或指针的类型转换,相比于C转换,优势在于:
//(1) 在编译过程中会检查转换是否有意义,更安全;
// char* char_ptr = static_cast<char*>(&initial_c); 报错:编译器检测到并认定为无效的类型转换
//(2) 应用的类型更加广泛(类、指针)
class Base { /* ... */ };
class Derived : public Base { /* ... */ };
Base *initial_bPtr = new Derived;
Base initial_obj;
// Derived transform_obj = (Derived)initial_obj; 报错:无法直接按照 C 语言方法转换类对象
// Derived *transform_bPtr = Derived* (&initial_bPtr); 报错:无法直接按照 C 语言方法转换类指针类型
// Derived transform_obj = static_cast<Derived>(initial_obj); 报错:只能转换类指针
Derived *transform_bPtr = static_cast<Derived *>(initial_bPtr);
cout << "Variance after transform by static_cast: " << typeid(initial_bPtr).name() << endl; // Variance after transform by static_cast: PZ4mainE7Derived
delete initial_bPtr;
// delete transform_bPtr; 报错:类型转换前后共享内存,故而导致重复释放
// 3. const_cast
// 使用场景:将常量 const 或 易变变量 volatile 转化为正常变量或者把一个指针/引用转化为 const/volatile 类型
const int initial_const = 6;
// int transform_const = const_cast<int>(initial_const); 报错:const_cast 中的类型必须是指针、引用或指向对象类型成员的指针
// modifyVariance(&initial_const); 报错:const int* 类型的实参和函数中的 int* 类型的形参不匹配
int* transform_const = const_cast<int*>(&initial_const);
modifyVariance(transform_const);
cout << "Variance after transform by const_cast: " << initial_const << endl; // Variance after transform by const_cast: 6 并没有改变原来常量值,证明 const_cast 转换得到的结果与原变量并非共享内存;
cout << "Variance after transform by const_cast: " << *transform_const << endl; // Variance after transform by const_cast: 66
// 4. dynamic_cast
// 使用场景:在程序运行时(动态),将包含多态(虚函数)的基类指针/引用转化为其派生类指针,若源对象并非目标转化类型,则返还空指针
Base* initial_Ptr = new Derived;
// Derived* transform_Ptr = dynamic_cast<Derived*>(initial_Ptr); 报错:运行时 dynamic_cast 的操作数必须包含多态类类型
class BaseVirtual {
public:
virtual void display(){cout<<"this is base class!"<<endl;};
};
class DerivedVirtual : public BaseVirtual {
public:
void display() override {cout<<"this is drived class!"<<endl;};
};
BaseVirtual* initial_PtrVirtual = new DerivedVirtual;
Derived* transform_PtrVirtual = dynamic_cast<Derived*>(initial_PtrVirtual);
cout << "Variance after transform by static_cast: " << typeid(initial_PtrVirtual).name() << endl; // Variance after transform by static_cast: PZ4mainE11BaseVirtual
cout << "Variance after transform by static_cast: " << typeid(transform_PtrVirtual).name() << endl; // Variance after transform by static_cast: PZ4mainE7Derived
delete initial_PtrVirtual, transform_PtrVirtual; // 由于转换前后为深拷贝,所以需要分别进行释放(与static_cast不同)
// 5. reinterpret_cast
// 使用场景:强力转换,用于底层代码的编写或其他转换方法无法转化的情形。谨慎使用。
int num = 42;
char *char_ptr = reinterpret_cast<char *>(&num);
for (size_t i = 0; i < sizeof(int); ++i) {
// Print the individual bytes of the integer as characters
std::cout << "Byte " << i << ": " << char_ptr[i] << std::endl; // Byte 0: *, Byte 1:, Byte 2:, Byte 3:
}
}
恭喜你!终于读完本篇文章,对于新手来说并不友好,读到这里你小小的脑袋可能会有些大大的疑惑,如:
- 虚函数是什么意思?
- 类的继承有什么作用?
- 指针的用法是如何?
- std::xx命名空间有什么用?
- 基类指针转化为派生类在实际中有什么应用场景?
- …
且听后续娓娓道来!