常量和变量相关以及类型转换和对临时变量(对象)具有常性的误解

常量指值不可改变的量,在 C++ 中常量分为两种,文字常量(Literal Constant)和常变量(Constant Variable)。常变量也是常量。

文字常量和常变量的本质区别: 文字常量编译之后存储在代码段,不可寻址,常变量存储在数据区(堆、栈、数据段、BSS 段),可寻址。

1.文字常量
文字常量又称为“字面常量”,包括数值常量、字符常量和符号常量。其特点是编译后写在代码区,不可寻址,不可更改,属于指令的一部分。

int& r=5;    //编译错误
这条语句出现编译错误,原因是文字常量不可寻址,因而无法为文字常量建立引用。下面这条语句又是合法的:

const int& r=5;
原因是编译器将一个文字常量转化成常变量的过程。在数据区开辟一个值为5的无名整型常变量,然后将引用r与这个整型常变量进行绑定。

2.常变量
用const修饰的变量叫常变量。常变量指定义时必须显示初始化且值不可修改的变量。与其他变量一样被分配空间,可以寻址。注意,字符串常量(类型是const char*而不是string)是常变量的一种,名称为其本身,存储在代码段,可寻址但不可修改。

cout << &”hello world” << endl;    // 打印输出字符串常量”hello world”存储地址
 

常变量在 C++ 中由 const 关键字来定义,分为全局常变量和局部常变量。

二者的区别在于:全局常变量存储在代码段的只读内存区域,不可修改有操作系统来保障,局部常变量存储在栈区而不是常量区,在编程语言语义层面上由编辑器做语法检查来保障其值不可修改,因不是放在只读内存中,可以获得局部常变量的地址,运行时再间接修改。参考如下代码:

#include <iostream>
using namespace std;

 

const int con1=3;
void showValue(const int& i)

{
    cout<<i<<endl;
}

int main(int argc,char* argv[])

{
    const int con2=4;
    int* ptr=NULL;
    ptr=const_cast<int*>(&con2);
    *ptr=5;
    showValue(con2);                //1,输出5
    cout<<"con2:"<<con2<<endl;        //2,输出4
    ptr= const_cast<int*>(&con1);
    *ptr=6;                            //3,运行时错误,写入冲突
}

程序 1 处输出 5,表明局部常量 con2 的值已经被修改,2 处输出为结果仍然为 4,并不是说明常变量 con2 的值没有被修改,而是因为编译器在代码优化的过程中已经将 con2 替换成了文字常量 4,在使用con2的时候不会去内存中读取数据,直接用4代替con2,但是con2本质已经变成了5,所以可以利用解引用指针如*ptr,或者利用引用&来获得改变后的变量,不可直接用之前的变量名,因为前面也说了,con2被替换成整形文字常量4了。还可以加上关键字volatile,如volatile const int con2=4;告诉编译器不要将con2优化,要去内存中读取数据。程序 3 处,运行时出错,表明全局常变量存储在只读内存,无法间接改写。

 576c92b94d8946c18ec245dfbf84699c.png

之前我一直有一个误解,认为常量是不可以赋值给变量的,比如认为上图中const类型赋值给非const类型,比如认为int x=1这个1不是直接作为常量赋值给变量x,而是中间生成了一个值为1的临时对象赋值给x,现在想想也是可笑,临时对象也具有常性,也是常量,按照之前的想法,也不可以直接赋值给变量x。

之所以造成常量不可赋值给变量这一误解,是弄混了引用和赋值,只有const引用可以引用常量,权限不变,也可以引用变量,此时权限缩小,非const引用不可以引用常量,防止权限放大。非const引用可以引用变量,此时权限不变。

而赋值是和权限无关的,只是值的拷贝。所以常量可以直接赋值给变量。

接下来说说临时对象或者说临时变量

只有内置类型,如int,double,char等等的临时变量或者临时对象是真正的具有常性,也就是真正的const类型,不可被修改。

所有非内置类型,如vector,list,自定义类等等的临时对象都不是真正的具有常性,因为它可以被修改,如下图。

fdd07ac6a80c414f9c8197026527969e.png

所以说我们一般说的临时对象具有常性,但这个常性不能绝对地说这个临时对象是const类型对象。

那么这个常性体现在哪里呢?在常量的引用(简称常引用)中可以体现。如下图:

61df9e8220114baf87113709fc11ec58.png

hello world会生成一个string类的临时变量,这个临时变量就具有“常性”,所以赋值给非const引用类型的对象会报错但注意:这个临时对象并不是常量,只是编译器从语义层面限制了临时变量传递给非const引用类型对象

ba07dfb923ac4dd38d718c022c652b95.png

为什么要限制呢?如果一个实参以非const引用传入函数,编译器有理由认为该实参会在函数中被修改,并且这个被修改的引用在函数返回后要发挥作用。但如果把一个临时变量当作非const引用参数传进来,由于临时变量所在的表达式执行结束后,临时变量就会被释放,所以,一般说来, 修改一个临时变量是毫无意义的,据此,C++编译器加入了临时变量只能作为const引用类型对象的实参这个语义限制,让这个参数在函数中不能被修改,意在限制这个非常规用法的潜在错误。

但在几乎所有情况下,上面所说的情况都不会用到,所以只需要记住,临时对象具有常性,虽然有些类型的临时对象可以被修改,但临时对象不是左值,而是右值即将亡值(自定义类型的右值是将亡值)。

类型转换

上面的临时变量花了这么大的篇幅介绍,就可以用在类型转换中。

分为隐式类型转换和强制类型转换。如int i=2.09;2.09是一个double或者float类型的字面常量值,一般编译器默认是double,由于它不是int,并且可以隐式转换成int,所以会用2.09生成一个const int类型的临时整形对象,然后将这个对象赋值给i。 也就是说:即使是字面常量,只要和变量类型相同,赋值是不需要产生临时变量的,如int i=4;4本身就是整形的字面常量。如果类型不同,不管是赋值还是引用,都会产生临时变量。

 

在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换

1. 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败。C++规定两种函数可以作为隐式的类型转换函数:单参数构造函数(注意多个带有缺省值的参数也视为单参构造)和类型转换函数即operator type()。

 

2. 显式类型转化:需要用户自己处理。

 

不管是隐式类型转换,还是强制类型转换,都是自动调用类型转换函数,这个函数有可能是构造函数,有可能是operator type()。

 

一般如果隐式类型转换失败,可以试试强制类型转换。强制类型转换可以转换完全不相关的两种类型。

 

注意:所有的类型转换都会产生临时对象

 

不仅在=赋值的时候会发生类型转换,由于<和>也是运算符,所以在比较大小的时候如果类型不相同也会发生类型转换,所以在迭代区间里用size_t类型的变量要注意0-1此时不会变成-1,而是变成INT_MAX。

 

缺陷: 转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换

因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格

C++强制类型转换

标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:

static_castreinterpret_castconst_castdynamic_cast。

static_cast 用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用
static_cast ,但它不能用于两个不相关的类型进行转换。
 
reinterpret_cast 操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换
为另一种不同的类型。
 
const_cast 最常用的用途就是删除变量的 const属性,方便赋值,但操作对象只能是指向对象的指针或者对象的引用,
 

使用dynamic_test时必须确保基类里含有虚函数,不然报错。

在dynamic_test中,如果父类指针指向子类对象,那么可以将这个父类指针转换成子类。如果父类指针指向父类对象,那么不可以将父类指针转换成子类,因为会越界访问。所以说用dynamic向下转换(即父类指针转换成子类指针)是安全的。

5ee098e91cac43beb23a8a989c8b36d1.png

 

下图中A类是父类,有成员_a,B类是子类,有成员_b。

6b9775b336fd486cb5fa727bd9ab3866.pngdynamic_cast不可以将指向父类对象的父类指针强制转换成子类指针,但C语言或者C++的强制类型转换可以,如下图:0455fd488f6241519d9c83c8b5b9cd32.png

decltype的作用:当不知道一个对象的类型时,但想定义一个同类型对象,就用decltype。如

04a74d0016c642b4b01bd2589e307201.png

类型转换函数

1.类型转换函数格式为operator type(){  } ,这个函数只能以一个类的成员函数出现,所以作用就是将当前类对象转换成其他类的对象。转换成什么样的类型由返回值决定。type可以是内置类型,也可以是自定义类型。

 

2.类型转换函数和运算符的重载非常相似,都使用 operator 关键字,因此也把类型转换函数称为类型转换运算符。

 

3.类型转换函数可以被继承,可以是虚函数。

 

4.一个类虽然可以有多个类型转换函数(类似于函数重载),但是如果多个类型转换函数要转换的目标类型本身又可以相互转换(类型相近),那么有时候就会产生二义性。

如下例子编译器就会报错:

38062fb812e4408b8dd1c8ea2ce9a385.png

上图中干掉任意一个类型转换函数即可正常运行。 

explicit的作用:如果在只需要显示传一个参数的构造函数前面声明了explicit,那么将不允许一个和声明的形参类型相同的对象隐式转换成这个(构造函数所属的类)的对象,注意前面说了只需要显示传一个参数,也就是说可以有多个形参,但形参有缺省值,这时explicit依然生效。

下图中的A aa1=1就是一个例子,由于A类的构造函数只需要传一个实参就可以构造,此时又没有explicit的话,1就可以隐式转换成A类的临时对象,然后aa1利用临时对象拷贝构造。编译器可能会优化,直接用1构造aa1,不产生临时对象。此时如果加入explicit,将不允许1直接隐式转换成A类对象。

5e69bfa5d68c4008a89c12edff6b7a97.png

上图红框中aa1是自定义类型对象,想要赋值给i必须转换成内置类型,由于自定义类型 隐式类型转换 或者 强制类型转换 成内置类型要通过调用类型转换函数完成,所以红框写了一个operator int()辅助。operator int()是类型转换操作符函数。

1.基本数据类型转换为类对象

将内置类型隐式转换或显示转换成自定义类型,类中只需要一个参数的构造函数就可以完成这个需求。(需要多个参数,但参数除了一个没有缺省值,其他参数都有缺省值的情况也算只需要一个参数)。

2.类对象转换为基本数据类

利用类型转换函数:如operator 基本数据类(){}即可。

3.不同类对象的相互转换不同自定义类型间的隐式类型转换或者强制类型转换也可以由各自类的构造函数完成。

9f99607f6bcf4e90a4ec3b83dc81a218.jpeg

 

43c85b77a5e24a75a887a5e2039da565.png

 

内置类型在隐式类型转换或者强制类型转换的时候不会调用函数,但自定义类型在隐式类型转换或者强制类型转换的时候是会调用对应函数的,如上面三张图:cin>>str的返回值是一个istream&的对象,即cin这个对象本身,然后while需要条件判断,参数必须是bool值或者整形,由于istrean类里实现了operator bool()这个类型转换函数,所以去调用operator bool()这个类型函数,函数返回一个bool类型的值,完成从cin到bool值的转换。这个operator bool()函数是自定义类型隐式类型转换的时候自动调用的,如果不明白看看这个例子:如string str=“1111” ,“1111”显然首先隐式类型转换成string类的临时对象,然后str利用临时对象拷贝构造。所以OJ题如果需要多次输入可以用while(cin>>x>>y...)。

 

  • 9
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值