C++中的const和constexpr详解

C++中的const可用于修饰变量、函数,且在不同的地方有着不同的含义,现总结如下。

const的语义

C++中的const的目的是通过编译器来保证对象的常量性,强制编译器将所有可能违背const对象的常量性的操作都视为error。

对象的常量性可以分为两种:物理常量性(即每个bit都不可改变)和逻辑常量性(即对象的表现保持不变)。C++中采用的是物理常量性,例如下面的例子:

struct A {
  int *ptr;
};
int k = 5, r = 6;
const A a = {&k};
a.ptr = &r; // !error
*a.ptr = 7; // no error

a是const对象,则对a的任何成员进行赋值都会被视为error,但如果不改动ptr,而是改动ptr指向的对象,编译器就不会报错。这实际上违背了逻辑常量性,因为A的表现已经改变了!

逻辑常量性的另一个特点是,const对象中可以有某些用户不可见的域,改变它们不会违背逻辑常量性。Effective C++中的例子是:

class CTextBlock {
public:
  ...
  std::size_t length() const;
private:
  char *pText;
  std::size_t textLength;      // last calculated length of textblock
  bool lengthIsValid;        // whether length is currently valid
};

CTextBlock对象每次调用length方法后,都会将当前的长度缓存到textLength成员中,而lengthIsValid对象则表示缓存的有效性。这个场景中textLength和lengthIsValid如果改变了,其实是不违背CTextBlock对象的逻辑常量性的,但因为改变了对象中的某些bit,就会被编译器阻止。C++中为了解决此问题,增加了mutable关键字。

本部分总结:C++中const的语义是保证物理常量性,但通过mutable关键字可以支持一部分的逻辑常量性。

const修饰变量

如上节所述,用const修饰变量的语义是要求编译器去阻止所有对该变量的赋值行为。因此,必须在const变量初始化时就提供给它初值:

const int i;
i = 5; // !error
const int j = 10; // ok

这个初值可以是编译时即确定的值,也可以是运行期才确定的值。如果给整数类型的const变量一个编译时初值,那么可以用这个变量作为声明数组时的长度:

const int COMPILE_CONST = 10;
const int RunTimeConst = cin.get();
int a1[COMPLIE_CONST]; // ok in C++ and error in C
int a2[RunTimeConst]; // !error in C++

因为C++编译器可以将数组长度中出现的编译时常量直接替换为其字面值,相当于自动的宏替换。(gcc验证发现,只有数组长度那里直接做了替换,而其它用COMPILE_CONST赋值的地方并没有进行替换。)

文件域的const变量默认是文件内可见的,如果需要在b.cpp中使用a.cpp中的const变量M,需要在M的初始化处增加extern:

//a.cpp
extern const int M = 20;

//b.cpp
extern const int M;

一般认为将变量的定义放在.h文件中会导致所有include该.h文件的.cpp文件都有此变量的定义,在链接时会造成冲突。但将const变量的定义放在.h文件中是可以的,编译器会将这个变量放入每个.cpp文件的匿名namespace中,因而属于是不同变量,不会造成链接冲突。(注意:但如果头文件中的const量的初始值依赖于某个函数,而每次调用此函数的返回值不固定的话,会导致不同的编译单元中看到的该const量的值不相等。猜测:此时将该const量作为某个类的static成员可能会解决此问题。)

const修饰指针与引用

const修饰引用时,其意义与修饰变量相同。但const在修饰指针时,规则就有些复杂了。

简单的说,可以将指针变量的类型按变量名左边最近的‘*'分成两部分,右边的部分表示指针变量自己的性质,而左边的部分则表示它指向元素的性质:

const int *p1; // p1 is a non-const pointer and points to a const int
int * const p2; // p2 is a const pointer and points to a non-const int
const int * const p3; // p3 is a const pointer and points to a const it
const int *pa1[10]; // pa1 is an array and contains 10 non-const pointer point to a const int
int * const pa2[10]; // pa2 is an array and contains 10 const pointer point to a non-const int
const int (* p4)[10]; // p4 is a non-const pointer and points to an array contains 10 const int
const int (*pf)(); // pf is a non-const pointer and points to a function which has no arguments and returns a const int
...

const指针的解读规则差不多就是这些了……

指针自身为const表示不可对该指针进行赋值,而指向物为const则表示不可对其指向进行赋值。因此可以将引用看成是一个自身为const的指针,而const引用则是const Type * const指针。

指向为const的指针是不可以赋值给指向为非const的指针,const引用也不可以赋值给非const引用,但反过来就没有问题了,这也是为了保证const语义不被破坏。

可以用const_cast来去掉某个指针或引用的const性质,或者用static_cast来为某个非const指针或引用加上const性质:

int i;
const int *cp = &i;
int *p = const_cast<int *>(cp);
const int *cp2 = static_cast<const int *>(p); // here the static_cast is optional

C++类中的this指针就是一个自身为const的指针,而类的const方法中的this指针则是自身和指向都为const的指针。

类中的const成员变量

类中的const成员变量可分为两种:非static常量和static常量。

非static常量:
类中的非static常量必须在构造函数的初始化列表中进行初始化,因为类中的非static成员是在进入构造函数的函数体之前就要构造完成的,而const常量在构造时就必须初始化,构造后的赋值会被编译器阻止。

class B {
public:
  B(): name("aaa") {
    name = "bbb"; // !error
  }
private:
  const std::string name;
};

static常量:
static常量是在类中直接声明的,但要在类外进行唯一的定义和初始值,常用的方法是在对应的.cpp中包含类的static常量的定义:

// a.h
class A {
  ...
  static const std::string name;
};

// a.cpp
const std::string A::name("aaa");

一个特例是,如果static常量的类型是内置的整数类型,如char、int、size_t等,那么可以在类中直接给出初始值,且不需要在类外再进行定义了。编译器会将这种static常量直接替换为相应的初始值,相当于宏替换。但如果在代码中我们像正常变量那样使用这个static常量,如取它的地址,而不是像宏一样只使用它的值,那么我们还是需要在类外给它提供一个定义,但不需要初始值了(因为在声明处已经有了)。

// a.h
class A {
  ...
  static const int SIZE = 50;
};

// a.cpp
const int A::SIZE = 50; // if use SIZE as a variable, not a macro

const修饰函数

C++中可以用const去修饰一个类的非static成员函数,其语义是保证该函数所对应的对象本身的const性。在const成员函数中,所有可能违背this指针const性(const成员函数中的this指针是一个双const指针)的操作都会被阻止,如对其它成员变量的赋值以及调用它们的非const方法、调用对象本身的非const方法。但对一个声明为mutable的成员变量所做的任何操作都不会被阻止。这里保证了一定的逻辑常量性。

另外,const修饰函数时还会参与到函数的重载中,即通过const对象、const指针或引用调用方法时,优先调用const方法。

class A {
public:
  int &operator[](int i) {
    ++cachedReadCount;
    return data[i];
  }
  const int &operator[](int i) const {
    ++size; // !error
    --size; // !error
    ++cachedReadCount; // ok
    return data[i];
  }
private:
  int size;
  mutable cachedReadCount;
  std::vector<int> data;
};

A &a = ...;
const A &ca = ...;
int i = a[0]; // call operator[]
int j = ca[0]; // call const operator[]
a[0] = 2; // ok
ca[0] = 2; // !error

这个例子中,如果两个版本的operator[]有着基本相同的代码,可以考虑在其中一个函数中去调用另一个函数来实现代码的重用(参考Effective C++)。这里我们只能用非const版本去调用const版本。

int &A::operator[](int i) {
  return const_cast<int &>(static_cast<const A &>(*this).operator[](i));
}

其中为了避免调用自身导致死循环,首先要将*this转型为const A &,可以使用static_cast来完成。而在获取到const operator[]的返回值后,还要手动去掉它的const,可以使用const_cast来完成。一般来说const_cast是不推荐使用的,但这里我们明确知道我们处理的对象其实是非const的,那么这里使用const_cast就是安全的。

constexpr

constexpr是C++11中新增的关键字,其语义是“常量表达式”,也就是在编译期可求值的表达式。最基础的常量表达式就是字面值或全局变量/函数的地址或sizeof等关键字返回的结果,而其它常量表达式都是由基础表达式通过各种确定的运算得到的。constexpr值可用于enum、switch、数组长度等场合。

constexpr所修饰的变量一定是编译期可求值的,所修饰的函数在其所有参数都是constexpr时,一定会返回constexpr。

constexpr int Inc(int i) {
  return i + 1;
}

constexpr int a = Inc(1); // ok
constexpr int b = Inc(cin.get()); // !error
constexpr int c = a * 2 + 1; // ok

constexpr还能用于修饰类的构造函数,即保证如果提供给该构造函数的参数都是constexpr,那么产生的对象中的所有成员都会是constexpr,该对象也就是constexpr对象了,可用于各种只能使用constexpr的场合。注意,constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。

struct A {
  constexpr A(int xx, int yy): x(xx), y(yy) {}
  int x, y;
};

constexpr A a(1, 2);
enum {SIZE_X = a.x, SIZE_Y = a.y};

constexpr的好处:

是一种很强的约束,更好地保证程序的正确语义不被破坏。
编译器可以在编译期对constexpr的代码进行非常大的优化,比如将用到的constexpr表达式都直接替换成最终结果等。
相比宏来说,没有额外的开销,但更安全可靠。

(0)

相关推荐

  • 从C语言过渡到C++之const

    1. 定义常量 1.1 C语言中定义常量的方法 在C语言从零开始这个系列中,我们讲了C语言定义常量的方法.没有看过的同学请参考:C语言从零开始(五)-常量&变量 为什么要定义常量我就不再赘述了,这里重点说说这么定义有什么不好.经常有这样的面试题:请写出下面这段代码的执行结果: #include <stdio.h> #define SUM 5 + 1; void main() { int a = 2 * SUM; printf("%d", a); } 经常有人答12,

  • C++ const修饰变量和修饰函数介绍

    const修饰变量 关于const最常见的一个面试题是这样的:char *const和const char*有什么区别,大家都知道const修饰符代表的是常量,即const修饰的变量一旦被初始化是不能被更改的,这两个类型一个代表的是指针不可变,一个代表指针指向内容不可变,但具体哪个对应哪个,很多人一直搞不清楚. 有这样一个规律,const修饰的是它前面所有的数据类型,如果const在最前面,那么把它和它后面第一个数据类行交换.比如上面的const char*交换之后就是char const *,

  • c++中const的使用详解

    Const 是C++中常用的类型修饰符,常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的. 1.定义常量(1)const修饰变量,以下两种定义形式在本质上是一样的.它的含义是:const修饰的类型为TYPE的变量value是不可变的. TYPE const ValueName = value;      const TYPE ValueName = value; (2)将const改为外部连接,作用于扩大至全局,编译时会分配内存,并且可以不进行初始化,仅仅作为声

  • C++中const的用法详细总结

    1. const修饰普通变量和指针 const修饰变量,一般有两种写法: const TYPE value;TYPE const value; 这两种写法在本质上是一样的.它的含义是:const修饰的类型为TYPE的变量value是不可变的. 对于一个非指针的类型TYPE,无论怎么写,都是一个含义,即value只不可变. 例如: const int nValue: //nValue是constint const nValue: // nValue是const 但是对于指针类型的TYPE,不同的写

  • c++ 尽量不要使用#define 而是用const、enum、inline替换。

    例如:这里程序文件开头有如下#define语句 复制代码 代码如下: #define N 10 #define PI 3.14 #define MAX 10000 #define Heigth 6.65 ... ... 假设这里程序运行出错误,而且就是在我们使用这些常量有错误,此时编辑器应该会抛出错误信息.如果该信息提示6.65这里有错误,Ok如果你运气好你正好记得或者程序简单一眼能找到6.65表示什么,如果程序很复杂,而且报出6.65的文件是引用该文件,不记得,那么你会困惑这是什么?或许会花大

  • C++中的类型转换static_cast、dynamic_cast、const_cast和reinterpret_cast总结

    前言 这篇文章总结的是C++中的类型转换,这些小的知识点,有的时候,自己不是很注意,但是在实际开发中确实经常使用的.俗话说的好,不懂自己写的代码的程序员,不是好的程序员:如果一个程序员对于自己写的代码都不懂,只是知道一昧的的去使用,终有一天,你会迷失你自己的. C++中的类型转换分为两种: 1.隐式类型转换: 2.显式类型转换. 而对于隐式变换,就是标准的转换,在很多时候,不经意间就发生了,比如int类型和float类型相加时,int类型就会被隐式的转换位float类型,然后再进行相加运算.而关

  • c++ const引用与非const引用介绍

    const引用是指向const对象的引用. 复制代码 代码如下: const int i = 10; const int &ref = i; 可以读取ref,但不能修改.这样做是有意义的,因为i本身就不可修改,当然也不能通过ref来修改了.所以也就有将const变量赋值给非const引用是非法的. 复制代码 代码如下: int &ref1 = i; // error: nonconst reference to a const object 非const引用是指向非const类型变量的引用

  • C++中const的实现机制深入分析

    问题 C语言以及C++语言中的const究竟表示什么?其具体的实现机制又是如何实现的呢? 本文将对这两个问题进行一些分析,简单解释const的含义以及实现机制. 问题分析 简单的说const在C语言中表示只读的变量,而在C++语言中表示常量.关于const在C与C++语言中的使用以及更多的区别,以后有时间另开一贴说明. 那么const究竟是如何实现的呢? 对于声明为const的内置类型,例如int,short,long等等,编译器会如何实现const的本意?那么对于非内置类型是否也是与内置数据类

  • C/C++中static,const,inline三种关键字详细总结

    一.关于staticstatic 是C++中很常用的修饰符,它被用来控制变量的存储方式和可见性,下面我将从 static 修饰符的产生原因.作用谈起,全面分析static 修饰符的实质. static 的两大作用: 一.控制存储方式 static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间. 引出原因:函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,大家知道,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保

  • C++中的const和constexpr详解

    C++中的const可用于修饰变量.函数,且在不同的地方有着不同的含义,现总结如下. const的语义 C++中的const的目的是通过编译器来保证对象的常量性,强制编译器将所有可能违背const对象的常量性的操作都视为error. 对象的常量性可以分为两种:物理常量性(即每个bit都不可改变)和逻辑常量性(即对象的表现保持不变).C++中采用的是物理常量性,例如下面的例子: struct A { int *ptr; }; int k = 5, r = 6; const A a = {&k};

  • C++中的const的使用详解

     C++中的const的使用详解 const在c/c++中还是会经常出现的,并且如果不理解const会在编程出现的错误而不知所措,无法理解.下面从几个角度简要理解const的内容,应该还是蛮有用的. const与指针类型 const int*p = NULL; 和int const*p = NULL;是等价的.因为const都在" * "的前面,其实是以*为标志的. 1. int x = 3; const int *p = &x; // p = &y;正确 , //*p

  • C语言中二级指针的实例详解

    C语言中二级指针的实例详解 用图说明 示例代码: #include <stdio.h> int main(int argc, const char * argv[]) { // int a = 5; int *p1 = &a; //-打印地址-----地址相同--------------- printf("&a = %p\n", &a);// printf("p1 = %p\n", p1);// int **p2 = &p

  • C++ 中RTTI的使用方法详解

    C++ 中RTTI的使用方法详解 RTTI是运行阶段类型识别(Runtime Type Identification)的简称.这是新添加到c++中的特性之一,很多老式实现不支持.另一些实现可能包含开关RTTI的编译器设置.RTTI旨在为程序在运行阶段确定对象类型提供一种标准方式.很多类库已经成为其父类对象提供了实现这种方式的功能.但由于c++内部并不支持,因此各个厂商的机制通常互不兼容.创建一种RTTI语言标准将使得未来的库能够彼此兼容. c++有3个支持RTTI的元素 如果可能的话,dynam

  • C++ 中消息队列函数实例详解

    C++ 中消息队列函数实例详解 1.消息队列结构体的定义 typedef struct{ uid_t uid; /* owner`s user id */ gid_t gid; /* owner`s group id */ udi_t cuid; /* creator`s user id */ gid_t cgid; /* creator`s group id */ mode_t mode; /* read-write permissions 0400 MSG_R 0200 MSG_W*/ ul

  • Node.Js中实现端口重用原理详解

    本文介绍了Node.Js中实现端口重用原理详解,分享给大家,具体如下: 起源,从官方实例中看多进程共用端口 const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); for (let i =

  • socket在egg中的使用实例代码详解

    config/config.default.js exports.io = { init: {}, namespace: { '/': { //对应router.js里的 of('/') connectionMiddleware: [ 'auth' ], //对应io/middleware/auth packetMiddleware: [ 'filter' ], }, }, }; config/plugin.js exports.io = { enable: true, package: 'eg

  • vue中引入mxGraph的步骤详解

    第一步:下载npm包 npm install mxgraph --save 第二步:新建一个index.js文件 文件内容如下 import mx from 'mxgraph'; const mxgraph = mx({ mxImageBasePath: './src/images', mxBasePath: './src' }); // decode bug https://github.com/jgraph/mxgraph/issues/49 window.mxGraph = mxgraph

  • C语言中的正则表达式使用示例详解

    正则表达式,又称正规表示法.常规表示法(英语:Regular Expression,在代码中常简写为regex.regexp或RE).正则表达式是使用单个字符串来描述.匹配一系列符合某个句法规则的字符串. 在c语言中,用regcomp.regexec.regfree 和regerror处理正则表达式.处理正则表达式分三步: 编译正则表达式,regcomp: 匹配正则表达式,regexec: 释放正则表达式,regfree. 函数原型 /* 函数说明:Regcomp将正则表达式字符串regex编译

  • 对python中Librosa的mfcc步骤详解

    1.对语音数据归一化 如16000hz的数据,会将每个点/32768 2.计算窗函数:(*注意librosa中不进行预处理) 3.进行数据扩展填充,他进行的是镜像填充("reflect") 如原数据为 12345 -> 填充为4的,左右各填充4 即:5432123454321 即:5432-12345-4321 4.分帧 5.加窗:对每一帧进行加窗, 6.进行fft傅里叶变换 librosa中fft计算,可以使用.net中的System.Numerics MathNet.Nume

随机推荐