背景

我们都知道,const作为修饰符的时候,用来表明这个变量所代表的内存不可修改。因此,const修饰的变量必须在定义的时候就完成初始化,不然以后也没有机会了:

const 

但是请注意,这个不可修改是编译期的概念,如果你试图修改gemfield,那么编译器就会报错。而在运行时是没有const的概念的。事实上,在编译的时候,编译器大概率会将用到gemfield的地方直接替换为7030。

有了这个朴素的语法和概念后,我们下面就开始来详细介绍C++中const的语义,特别是在C++11的新标准中,我们新增加了constexpr关键字来强化和丰富const语义。

references to const

当我们将const概念应用到reference类型上时,会产生两种语义:const reference和reference to const,一个是说自身是const,一个是说绑定/指向的对象是const类型。但是,因为reference自身本来就是初始化后不能修改的,因此天然具备const语义。由此,上述的两种语义我们只会说第二种,也就是reference to const。

设想我们要将上面的gemfield绑定到一个reference上,我们可能会这么做:

int& r = gemfield;

但不好意思哈,编译器会报错,以Mac上的clang为例,编译器会给出错误binding value of type 'const int' to reference to type 'int' drops 'const' qualifier:

error: binding value of type 'const int' to reference to type 'int' drops 'const' qualifierint& r = gemfield;^   ~~~~~~~~

因为要绑定/指向const对象的reference必须也得是const类型,等等,这么说有点奇怪,向上文说过的那样,因为reference本来初始化后就不能修改,天然具有const属性,因此上面那句话的表述应该修正为:绑定/指向const对象的reference必须得是"reference to const"类型,也就是:

const 

那么reference to const 类型如果指向的是non const类型的变量呢?

int not_const_gemfield = 7030;
const int& r = non_const_gemfield;

这样是可以的,但要注意一点,虽然此时可以通过non_const_gemfield变量来修改其上的值,但通过r是不可能的,不然clang会报如下错误:

gemfield.cpp:6:7: error: cannot assign to variable 'r' with const-qualified type 'const int &'r = 56;~ ^
gemfield.cpp:4:16: note: variable 'r' declared const hereconst int& r = gemfield;~~~~~~~~~~~^~~~~~~~~~~~
1 error generated.

const和临时对象

在C++中,要进一步理解const就不得不提到临时对象(temporary object)这个概念。如果试图将一个临时对象绑定在非reference to const类型的reference上,那么编译器会给出错误

先看下面的例子:

double 

咦?临时对象在哪里呢?这是因为你试图将double类型的gemfield绑定到int型的reference上,编译器就会进行隐含的类型转化,相当于:

double gemfield = 7030;
int temp = gemfield
int& r = temp;

临时对象temp就产生了。在C++中,如果你将一个引用绑定在临时对象上(temp),编译器会认为这是完全没有道理的事情,肯定不是程序员的意图,因此直截了当的给出错误:

error: non-const lvalue reference to type 'int' cannot bind to a value of unrelated type 'double'int& r = gemfield;^   ~~~~~~~~
1 error generated.

但是临时对象可以绑定在reference to const类型的reference上,因为这种类型的reference显式的向编译器表明了态度:程序员的我将不会通过该reference去改变绑定在其上的对象的值(也就是临时对象上的值),那这种情况下就显得很有道理了,因此编译器会通过:

double 

有意思的事情来了,这种情况下你修改了gemfield对象的值:从7030到17030,那么r的输出是什么呢?答案是7030,也就是说根本没发生变化。正如上文所说,这是因为:r自始至终绑定的是那个临时对象temp,并不是gemfield

指针和const

在上文中,我们知道对于reference来说,有两种const语义:一个是const reference,一个是reference to const,但是因为reference天然的具备const语义,因此我们只会提到reference to const。那么对于指针呢?指针自身作为可以实实在在修改的对象,是具备两种const语义的,也就是const pointer和pointer to const。

先来一段简单的例子:

const int gemfield = 7030;
int* p = &gemfield;

这段代码会导致编译器报错:error: cannot initialize a variable of type 'int *' with an rvalue of type 'const int *'。这是因为gemfield是const类型的,因此左侧的pointer的类型必须是pointer to const的。为什么呢?还记得文中一开始提到的吗:“事实上,在编译的时候,编译器大概率会将用到gemfield的地方直接替换为7030“。至少从这个小细节上,我们就可以看出,gemfield对象已经或多或少的变成了临时语义,编译器认为用一个普通的指针指向它已经毫无道理了,程序员的你的意图一定不是这样,因此会直截了当的给出错误。

如果要修复上述错误,必须将p的类型变为pointer to const,像下面这样:

const 

既然现在我们已经知道了pointer to const类型,我们再来说说const pointer类型。前者pointer to const类型的指针是说一个指针指向的对象是不可修改的,但是指针本身的值是可以修改的;而后者const pointer类型的指针则是说,指针本身是不可修改的。const pointer的语法是这样的:将const关键字放到*之后:

int gemfield = 7030;
int* const p = &gemfield;

上述p就是const pointer,如果p既是const pointer又是pointer to const的呢?那就是:

const int gemfield = 7030;
const int* const p = &gemfield;

一个比较好的阅读理解方式是从变量名p开始,从右到左看:

p //pointer名字
const p // const pointer
int* const p //const pointer point to int
const int* const p //const pointer pointer to const int

top-level const和low-level const

我们在这里可以提出top-level和low-level这两个概念是因为,这两个概念在好几处都会被使用到:1,在拷贝对象时,可以无视top-level的const,但必须尊重low-level的const;2,在类型推导时,top-level的const会被无视/重视;3,在类型转换时,top-level和low-level的const有不同的方法。

简单来说,const pointer 是top-level const,pointer to const是low-level const。从人的眼睛出发,我们是先看到pointer(top level),再看到pointer指向的对象(low level),层层剥开,高屋建瓴。对于 reference中的const语义来说,都是low-level的

关于拷贝对象,我们来举个例子:

int gemfield = 7030;
int* const p1 = &gemfield;const int c_gemfield = 17030;
const int* p2 = &c_gemfield;//没问题,top-level被无视
gemfield = c_gemfield;//错误!p2是low-level const,但是tmp不是
int* tmp = p2;

如果不尊重low-level const语义,编译器就会给出下面的错误:

error: cannot initialize a variable of type 'int *' with an lvalue of type 'const int *'int* tmp = p;^     ~
1 error generated

关于类型推导,因为过于复杂,放到下面单独的章节理了。请往下看。

constexpr和constant expressions(常量表达式)

常量表达式(constant expressions)是说一个表达式的值不会被改变,并且在编译期就能获得这个表达式的值。相对应的,一个表达式是否是constant expressions就取决于这两个方面:类型(是否const)和初始化方式(编译期是否能拿到值):

//下面是常量表达式

上面的例子中,gemfield3不是const类型,gemfield4的值,也就是func()不能在编译期得到,因此这两者都不是constant expression。在C++11的新标准中,我们定义了constexpr关键字,用法如下所示:

constexpr int gemfield = 7030;
constexpr int gemfield2 = gemfield + 1;
constexpr int gemfield3 = func();

这个关键字告诉编译器,你来帮我确认下这些个变量是否(可以)是constant expression,不行就报错!上面的例子中,gemfield和gemfield2被编译器裁定为可以是,但是gemfield3是不是呢?当func是一个constexpr的函数时,那就是!如果func是一个普通的函数时,那就不是!

那什么是constexpr函数呢?这是C++11的新标准,一个constexpr函数就是一个普通的函数,再加上这些限制条件:1,参数的形参的类型必须是literal type(编译期可以参与运算的类型);2,参数的实参必须是constant expression;3,函数体只能是一个return语句;4,并且语句中的表达式必须在编译期可以resolve,而不是等到运行时。

因为constexpr函数的目的就是在编译器用它的值来替换到使用它的地方,因此constexpr函数默认具有inline语义,因此需要定义在多个编译单元中,为了保证多个编译单元中的同一个constexpr函数定义一致,我们通常需要把constexpr函数定义在头文件中。

需要说明的是,当指针遇到constexpr时,constexpr定义的const语义是top-level的

constexpr int* gemfield = nullptr;
const int* gemfield2 = nullptr;

gemfield是const pointer,而gemfield2是pointer to const

const和类型推导

在C++11中,和const语义相关的,标准包含了两种类型推导:auto、decltype,以及RTTI中的类型识别:typeid。

1,auto

先说说auto,当const语义遇到auto后,top-level的const会被auto忽略,这个和reference遇到auto的行为很像:

int gemfield = 7030;
int& r = gemfield;
auto a = r; //a的类型是int,而不是referenceconst int gemfield = 7030;
auto a = gemfield; //a的类型是int,而不是const int

如果在使用auto的时候想要带reference或者const语义,那就显式的加上:

auto& a = r;
const auto& a = gemfield;

2,decltype

auto的类型推导是根据初始化表达式来的,但有时候我们只想要表达式的类型,而不想用这个表达式来进行初始化,这就是decltype:

decltype(func()) gemfield = x;

值得说明的时候,func()并不会被调用,decltype只是通过其推导出它的返回值类型而已。decltype的行为和auto有很大的区别,并且decltype进行类型推导的时候,可以输入一个变量,也可以输入一个表达式。

当decltype的输入是变量的时候,decltype返回这个变量的类型,并且会保留top-level的const语义,也会保留reference语义:

const int gemfield = 7030;
const int& r = gemfield;decltype(gemfield) x = 0; //x是const int 类型
decltype(r) y = x; //y是const int&类型,因此必须初始化

当decltype的输入是表达式的时候,decltype得到的类型是这个表达式返回的类型,下面是两个有趣的例子:

int gemfield = 7030;
int* p = &gemfield;
int& r = gemfield;decltype(r) x; //x是int&
decltype(r + 0) x; //x是int,不是int&
decltype(*p) y; //y是int&,而不是int

y之所以是int&,是因为*p是通过一个地址的索引来得到的值,更像是一个引用而不是普通的int。

说完了表达式,我们再回到变量。当把变量用括号扩起来时,编译器就任务这是一个表达式,当作为decltype的输入时,decltype会返回该类型的引用:

int gemfield = 7030;decltype(gemfield) x; //x是int
decltype((gemfield)) x; //x是int&,不是int

3,typeid

typeid是为RTTI提供的第二个operator,意思是问入参:Hi,你的类型是什么呀?typeid的返回值是一个type_info类,在标准库中定义。当typeid的入参是const类型时,top-level的const语义会被忽略(顺便说一句,当typeid的入参是reference类型时,reference语义也会被忽略)。哇,这个像极了auto类型推导啊!

函数参数中的const语义

const语义在函数参数的初始化中和变量的初始化中的行为是类似的。形参上的top-level的const会被无视:也即,如果形参是top-level的const语义,我们可以把const和non const的对象赋给形参。像下面这样:

void gemfield(const int i) {/* can read i but not write to i */}

对于low-level的const来说,记住一点:往更严格的方向转换是没有问题的,反之则不行。

void gemfield(int* i){}
void gemfield(int& i){}

由此得出一个好的实践:函数的形参尽可能的使用reference to const。这样带来的一个好处就是,什么都可以传。比如下面这样:

void gemfield(const string& s1){}
void gemfield(string& s2){} //不太好

如果是第一种定义,我们的实参类型甚至可以是字符串常量。我们可以这样调用函数:gemfield("gemfield, a civilnet maintainer");如果是第二种定义,则会报错。

最后我们还得提到函数重载,还记得重载的条件吗:函数名相同、形参列表不同。其中,形参列表不同体现在两个地方:参数个数不同,参数的类型不同。那么有趣的地方来了:

  1. 因为top-level的const被无视了,因此,不同的top-level的const语义,却是相同的形参类型;
  2. low-level的const语义可以产生不同的形参类型,因此可以产生重载;

类型转换和const_cast

众所周知,C++中的类型转换分为隐式和显式转换。

在类型的隐式转换中,我们可以加上low-level的const,如下所示:

int 

但是,如果是想在类型的隐式转换过程中去掉low-level的const,那则是万万不行的。

我们再来说说显式转换:const_cast。这个是专门用来操作low-level的const的,并且只能是这三种类型上的const语义:reference, pointer-to-object, or pointer-to-data-member。我们来看看下面的例子:

int gemfield = 7030;
int& r = gemfield;
const int& r2 = const_cast<const int&>(r);

const_cast可以加上low-level的const语义,如上面所述;也可以去掉一个low-level的const语义,如下面所示:

const int gemfield = 7030;
const int& r = gemfield;
int& r2 = const_cast<int&>(r);

这两个的区别是,前者中r2指向的还是gemfield所在的内存;而后者中r2则指向的是临时对象,对r2的改动在标准是未定义的。

另外还有一个有趣的事实,就是显式转换中的static_cast,可以强制转换任何类型,就是不能转换low-level的const语义。对应的,const_cast可以转换low-level的const语义,但是不能进行其它类型的转换。

类的const成员和const对象

类的const成员分为数据成员和函数成员,其中数据成员的语义和上述介绍没有什么区别,只不过要注意的是,const数据成员的初始化方式——只能在构造函数之前初始化;如果不对const数据成员显式的进行初始化,编译器将予以拦截并且报错如下:

error: constructor for 'Gemfield' must explicitly initialize the const member 'data'

而const函数成员指明了这个函数不会修改该类的任何成员数据的值,称为常量成员函数——如果在const成员函数的定义中出现了任何修改对象成员数据的情况,都会在编译时被编译器拦截住。有了const成员函数,我们就可以实例化const类型的对象(否则也没有意义了),并且我们只能在const对象上调用const成员函数,任何在const对象上调用非const成员函数的行为,都会被编译器拦截住,并且报错:

error: 'this' argument to member function 'getV' has type 'const Gemfield', but function is not marked const

模版中const的语义以及完美转发

这篇文章的内容已经太多了,这一小节的内容将在《C++的perfect forwarding》中进行讲述。

const int是什么类型_C++的const语义相关推荐

  1. C++ Primer 5th笔记(2)chapter 2变量和基本类型:引用、const

    1.引用refrence 定义:给对象起另外一个名字. 1.1 是两个类型一样的对象之间的: eg. int &i = 10;//错误double &f = i;//错误 引申:上面的 ...

  2. 【转】const int *p和int * const p的区别(常量指针与指向常量的指针)

    [转]作者:xwdreamer   出处:http://www.cnblogs.com/xwdreamer 对于指针和常量,有以下三种形式都是正确的: const char * myPtr = &am ...

  3. const int, const int const, 和 int const 的区别

    const int*, const int * const 和 int const *的区别 声明:本文翻译自:Difference between const int*, const int * c ...

  4. c语言中 int和const int的区别

    一.const int 和int 的区别 具体的是 int定义的是一个变量,不需要初始化const int定义的是常量,需要初始化 1.返回值 const int & 是返回这个数值的一个常量 ...

  5. const int INF=0x3f3f3f3f;——ACM中的无穷大常量

    0x3f3f3f3f 0x开头的 是十六进制常数, 等于 十进制 1061109567 等于 二进制: 00111111 00111111 00111111 00111111 const int in ...

  6. C语言中的类型限定符.const限定符

    目录 1.1const限定符 1.1.1const限定符修饰普通对象 1.1.2const限定符修饰数组元素 1.1.3const限定符修饰指针类型对象 1.1.4const限定符修饰函数形参类型为数 ...

  7. c语言const 修饰二级指针,C++中const修饰二级指针(从类型‘int**’到类型‘const int**’的转换无效)...

    先上代码: void func(const int ** arg) { } int main(int argc, char **argv) { int **p; func(p); return 0; ...

  8. const int *p说明不能修改_C语言关键字const和指针结合的使用

    C语言中,const 的作用是把变量变为一个只读的变量.与指针结合起来,有以下几种用法,下面分别进行说明. const int p; const int *p; int * const p; cons ...

  9. 2020-09-21C++学习笔记之与C语言区别和加强——四种const意义(const int a; int const b; const int *c; int * const d)

    2020-09-21C++学习笔记(const int a; int const b; const int *c; int * const d) 这两天在上课更新晚了. 1.C/C++中的const ...

最新文章

  1. dataframe,python,numpy 问题索引1
  2. Codeup-问题 C: 畅通工程
  3. 【活动】HoloLens 黑科技等你来探秘
  4. ASP.NET AntiXSS的作用
  5. linux内核那些事之buddy(慢速申请内存__alloc_pages_slowpath)(5)
  6. python归一化改变图像大小_基于Python+PIL-Speed问题的图像强度归一化
  7. 关于Chrome内核88版本无法正常使用Adobe Flash Player公告
  8. ORACLE PL/SQL 实例精解之第二章 通用编程语言基础
  9. LINQ聚合算法解释
  10. Boost Asio 使用技巧
  11. 【答题卡识别】基于matlab GUI hough变换答题卡判定与成绩统计(带面板)【含Matlab源码 1017期】
  12. “杜绝电子垃圾,我们需要软件永远更新下去!”
  13. 城市公交、地铁站点和线路数据纠偏与矢量化
  14. python画椭圆形_Python易学就会(五)turtle绘制椭圆与递归
  15. 定制婚礼小程序开发功能
  16. 同济大学软件学院特聘教授朱少民谈《测试,从哪里来,到哪里去》
  17. Lecture 13: Bernoulli Process
  18. 语音算法笔记(3)——从序列建模的角度理解ASR
  19. Gear 在 Polkadot 网络中的作用是什么?
  20. 网站提示https证书危险怎么处理

热门文章

  1. 为什么我那么努力,模电还是学不懂?
  2. windows 11 预览版来了
  3. 「权威发布」2019年全国大学生电子设计竞赛获奖名单【涵盖国一、二等奖】
  4. MATLAB加入螺旋相位板调制,平板式螺旋相位板的设计与应用
  5. deepin编译Linux内核,为Deepin编译Linux 5.10内核的悲伤故事经历:从入门到卸载
  6. oracle 长事务 逻辑日志,goldengate中长事务引起的问题
  7. vscode里面如何配置库_VSCode中C/C++库文件的配置
  8. python列表元组字符串都属于有序数列_列表、元组、字符串是Python的__________(有序、无序?)序列。...
  9. python opencv 识别角度_OpenCV入门之获取图像的旋转角度
  10. Dockerfile详解(二)