前言:
类型一直是C++中最重要的部分,相比于其他高级语言,C++的类型会复杂许多,往往一个类型匹配错误就会导致程序报错,本篇主要讲解一些常用类型的概念以及细节,如果对于C++有一定基础的,可以跳转到思考部分,从中了解自己的掌握程度;
一、初始化与赋值
定义:初始化与赋值语句是程序中最基本的语句,功能是将某个值与一个对象关联起来;
- 值:字面量、对象(变量或常量)所表示的值等
- 标识符(对象):变量、常量、引用
初始化的基本操作:
- 1、在内存中开辟空间、保存相应的数值;
- 2、在编译器中构造符号表、将标识符与相关内存空间关联起来;
二、 类型概述
下面通过几点概要说明:
1、类型是编译期概念,可执行程序中不存在类型的概念;
2、C++是强类型语言;
强类型语言定义: 一旦一个变量被定义类型,如果不经过强制转换,那么它永远就是该数据类型;
弱类型语言定义: 某一变量被定义类型,该变量可根据环境变化自动进行转换,不需要强转;
3、引入类型是为了更好描述程序,防止误用;
4、类型描述的信息:
存储所需要的大小: (sizeof,标准没有严格限制,根据硬件不同字节数也不同)
取值空间: (可用std::numeric_limits来判断,超过范围可能产生溢出)
#include<iostream>
#include<limits>
int main() {
int x = 10;
std::cout << std::numeric_limits<int>::min() << std::endl; //-2147483648
std::cout << std::numeric_limits<int>::max() << std::endl; //2147483647
std::cout << std::numeric_limits<unsigned int>::min() << std::endl; //0
std::cout << std::numeric_limits<unsigned int>::max() << std::endl; //4294967295
}
由上面程序运行结果可知,无符号int类型占4个字节,也就是32个比特位,所以最大范围为232,在不同的硬件下可能不同;
- 对齐信息(一般存放在内存中按类型的对齐信息的整数倍存储,比如int的对齐信息为4个字节,那存储的空间首地址为4的倍数,在结构体中,因为存在对齐信息,char也会按4个字节保存)
- 类型可执行的操作
三、类型分类
类型可以划分为基本类型和复杂类型;
基本(内建)类型:C++语言中支持的类型,包含以下几种:
1、数值类型
字符类型:char
、wchar_t
、char16_t
、char32_t
,通常为1个字节,表示256个值,也就是ASCII编码的字符;
整数类型:带符号整数类型(short
、int
、long
、long long
),无符号整数类型(unsigned+带符号整数类型)
浮点类型:float
、double
、long double
注意:在C++11中引入了固定尺寸的整数类型,如int32_t等,之前在针对开发板的程序中有见过该类型,主要是便于硬件的可移植性:
2、void类型
复杂类型:由基本类型组合、变种所产生的类型,可能是标准库引入,或自定义类型;
四、字面值及其类型
字面值:在程序中直接表示为一个具体数值或字符串的值;
每个字面值都有其类型,例子如下:
- 整数字面值(int):20(十进制)、024(八进制)、0x14(十六进制)
- 浮点数(double):1.3、1e8
- 字符字面值(char): ‘c’、’\n’
- 字符串字面值(char[4]): “cpp”,注意这里字符串后会默认加/0,所以是四个字符长度
- 布尔字面值(bool): True、False
像如果想要定义float类型的数,可以加入后缀如1.3f
;
C++提供了用户创建自定义后缀的函数:
#include<iostream>
// 后缀可自行定义,我这里用_bang
int operator "" _bang(long double x)
{
return (int)x * 2;
}
int main() {
int x = 7.14_bang;
std::cout << x << std::endl;
}
上面代码将7.14的浮点类型转换成整型并增大一倍,可自行定义后缀试一下;
五、变量及其类型
变量:对应一段存储空间,可以改变其中内容;
声明与定义的区别:不能重定义已经初始化的变量,需要加入extern用来声明;
初始化:全局变量会默认初始化为0,局部变量会缺省初始化(随机数值);
六、复合类型
1、指针:一种间接类型;
如上图为一个指针p指向一段内存,p保存的为val的地址,我们通过打印尺寸可知,指针p为8个字节;
特点:
- 可以"指向"不同的对象;
- 具有相同的尺寸;
- 指针与
bool
的隐式转换:非空指针可以转换为true
、空指针可以转换为false
;
注意:两个符号:*(解引用符)、&(取地址符);
解引用符在不同环境下含义不同,看如下代码:
int x = 10;
int* p = &x; // 表示p为一个int指针类型
*p; // 表示解引用,获取指针指向地址的值
关于nullptr:
- 一个特殊的对象(类型为
nullptr_t
),表示空指针; - 类似于C中的
NULL
,但更加安全;
void 指针*:没有记录对象的尺寸,可以表示任意类型指针,一般作为形参或返回值;
指针对比对象:指针复制成本低,引用成本高;
总结:指针在程序中的作用,最重要的就是作为参数传入,由于数据类型可能很大,传入指针大小固定为8个字节,并且指针值为地址可复制,复制成本低,并且可在函数中改变变量的值;
2、引用
取地址符&也有两个含义:
int x = 10;
&x; // 取地址符
int& ret = x; // 定义ret为一个引用类型
特点:
- 是对象的别名,不能绑定字面值(指针也不能指向字面值);
- 构造时绑定对象,在其生命周期内不能绑定其他对象(赋值操作会改变对象内容);
- 不存在空引用,但可能存在非法引用,总体比指针安全;
- 属于编译期概念,在底层还是通过指针实现;
七、常量类型
- 使用关键字
const
声明常量对象; - 是编译期概念,由编译器保证,作用为防止非法操作、优化程序逻辑;
常量指针(顶层常量):
int* const p = &x;
常量指针表示指针为常量,指针不能更改指向;
底层常量:
const int* p = &x;
底层常量表示指针指向的地址的内容不能发生改变,指针指向可改变;
常量引用:
用const int&
定义一个常量引用;
主要用于函数形参(对于较复杂的数据类型);
可以绑定字面值;
常量表达式:
constexpr int x = 1; // x的类型仍为const int
声明的是编译期的常量,编译器可以对其进行优化;
八、类型别名
类型别名:引入特殊的含义或便于使用,例如size_t
;
引入类型别名的两种方式:
1、typedef int Mytype;
2、using Mytype = int;(C++11后)
第二种方式更好;
- 应将指针类型别名视为一个整体,引入常量
const
表示指针为常量的类型; - 不能通过类型别名构造引用的引用;
九、类型自动推导
定义:通过初始化表达式定义对象类型,编译器会自动推导得到;(C++11开始)
推导得到的类型还是强类型,并不是弱类型;
自动推导的几种形式:
1、auto
:最常用的形式,会产生类型退化(由于左值右值的类型区别);
2、const auto
、constexpr auto
:推导出的是常量、常量表达式类型;
3、auto
&:推导出引用类型,避免类型退化;
4、decltype(exp)
:返回exp表达式的类型(左值加引用);
5、decltype(val)
:返回val的类型;
6、decltype(auto)
:简化decltype
的使用,C++14开始支持;
补充:类型退化表示一个变量作为左值和右值时类型不同,例如数组作为右值为指针;
十、域与对象声明周期
域(scope):
表示程序中的一部分,其中的名称有唯一含义,有全局域、块域等;
域可以嵌套,嵌套域中定义的名称可以隐藏外部域中定义的名称;
对象的生命周期起始于被初始的时刻,终止于被销毁的时刻;
全局对象的生命周期是整个程序运行期间,局部对象终止在所在域执行完成;
思考
1、思考下下面关于指针的两行代码的含义
int x = 1;
int* p = &x;
int y = 0;
*p = y; // 第一行
p = &y; // 第二行
这两行表明了指针的一个特定,可改变性,每一行的含义如下:
第一行:将指针p指向的内存地址的值改变为y;
第二行:不改变x的值,而是将指针p的指向改成y;
2、经过指针的思考后,我们看看关于引用的思考
int x = 1;
int& f = x;
int y = 0;
f = y; // 思考一下这一行的作用,是改变了引用f的绑定吗?
上面这行代码并不改变f的绑定,而是改变了f的值,同时引用对象x的值也发生改变;
3、经过了指针和引用的思考
下面思考下两者在底层有什么关联:
int x;
int* p = &x; *p = 1;
int& f = x; f = 1;
分析下上面两行代码,他们底层实现会相同吗?
这是两者的汇编代码实现,可以发现是完全相同的,引用底层也是通过指针实现的;
4、思考以下代码中&x是什么数据类型?
int x = 1;
const int* p = &x;
如果我们只考虑&x的话,这是一个int*
的类型,但由于第二行代码执行拷贝构造,隐式地将&x转换为左值所需要的 const int *
类型;
5、思考下面函数传参的区别?
void fun(int x){}
void fun(const int& x){}
从本质上来说,上面两种传参实现的作用是一致的,第一个进行拷贝构造传递,所以在函数内部无法改变外部x变量的值,而下面的传参传入引用可以在函数内部改变外部x的值,加入const
强制成变量;第二种其实是画蛇添足地做法,但常量引用对于复杂的数据类型来说,是能够节省很多空间的,比如自定义的结构体;
6、下面常量表示底层常量还是顶层常量?
using mytype = int*;
int x = 1;
const mytype p = &x;
这里我们容易误导,还会认为这是一个底层常量,但由于别名的定义,这里其实是一个顶层常量,我们可以将mytype看作一个整体,那么指针的指向不可发生改变;
7、下面auto&自动推导出的y是什么类型?
const int x = 1;
auto& y = x;
相信大部分人会认为x会类型退化,从而y为int&
类型,实际上这里类型不会退化,所以y为const int&
类型;
8、下面来看看decltype自动推导的类型是什么?
int x = 1;
decltype(x); // 1
decltype((x)); // 2
decltype
在传入参数为左值时加入引用,那么第一行为一个变量,所以为int
类型,第二行为表达式,所以加入引用为int&类型;
总结:
本篇讲解的类型知识点很杂,并且涵盖很多小的知识点,很多细节部分在实际工程中不一定会接触到,当然在工程中也会遇到很多自己不理解的类型转换,需要多通过debug模式来查看类型;
本篇知识点较多,可以选择自己想了解的部分进行查看,后续会继续推出更深层次的内容;
到此这篇关于C++ 中的类型详细的文章就介绍到这了,更多相关C++ 类型内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!