C++: const关键字详解

用const修饰的变量,表明该变量只能被读取,不能被修改。

常变量

形式

const 类型说明符 变量名 (其中类型说明符const的位置可以互换。

作用域

默认作用域是本地的

当前源文件中。在不同的源文件中可以定义同一个名字的常变量而不会引起重定义冲突,这是因为常变量的作用域是当前源文件。

例如下面的代码

// ======= f1.h ======== //
#pragma once
#ifndef _F1_H_
#define _F1_H_

void print1();

#endif

// ======= f1.cpp ======== //
#include <iostream>
#include "f1.h"
using namespace std;

const int i = 1; // 定义一个常量i

void print1() {
	cout << "In f1.h, i = " << i << ", address = " << &i << endl;
}

// ======= f2.h ======== //
#pragma once
#ifndef _F2_H_
#define _F2_H_

void print2();

#endif

// ======= f2.cpp ======== //
#include <iostream>
#include "f2.h"
using namespace std;

const int i = 2;  // 定义一个和f1.cpp中同名的变量i

void print2() {
	cout << "In f2.h, i = " << i << ", address = " << &i << endl;
}

// ======= main.cpp ======== //
#include <iostream>
#include "f1.h"
#include "f2.h"
using namespace std;

int main() {
	print1();
	print2();
	int in;
	cin >> in;
	return 0;
}

在我的机器上,输出的结果是

In f1.h, i = 1, address = 010C8B30
In f2.h, i = 2, address = 010C8B54

可以发现,两个不同源文件中的常量i的地址不同,说明它们是两个不同的变量。因此,常量的作用域是local的。

使用extern声明全局常变量

将上面的代码稍作修改,如下

// ======= f1.h ======== //
#pragma once
#ifndef _F1_H_
#define _F1_H_

extern const int i; // 在f1的头文件里加入extern的声明
void print1();

#endif

// ======= f1.cpp ======== //
#include <iostream>
#include "f1.h"
using namespace std;

const int i = 1; // 定义一个常量i

void print1() {
	cout << "In f1.h, i = " << i << ", address = " << &i << endl;
}

// ======= f2.h ======== //
#pragma once
#ifndef _F2_H_
#define _F2_H_

void print2();

#endif

// ======= f2.cpp ======== //
#include <iostream>
#include "f1.h"
#include "f2.h"
using namespace std;

// const int i = 2;  // 删掉原有的声明,如果不删除会产生重定义错误。

void print2() {
	cout << "In f2.h, i = " << i << ", address = " << &i << endl;
}

// ======= main.cpp ======== //
#include <iostream>
#include "f1.h"
#include "f2.h"
using namespace std;

int main() {
	print1();
	print2();
	int in;
	cin >> in;
	return 0;
}

此时,输出如下:

In f1.h, i = 1, address = 00DC8B30
In f2.h, i = 1, address = 00DC8B30

补充说明

如果在两个不同的源文件中声明两个名字相同的变量,编译的时候不会出错,因为编译时是每个源文件单独编译,但是在链接的时候会报重定义错误。但是如果在两个不同的源文件中声明两个const变量不会报错,因为const变量的作用域是局部的。

上面两个程序的另外一种实现方法。

定义一个公共的头文件,比如g.h,并在g.h中使用const int m = 5; 来定义一个常变量,然后在f1.cpp和f2.cpp中#include "g.h"#include的作用就是将对应的头文件复制一份到对应的文件中。这样的话,f1.cpp和f2.cpp里面都有一个const int m =5;的定义语句,经过编译之后,会生成两个独立的文件内可引用的常变量。

虽然两种方法都可以实现全局常变量,但是推荐使用的方法是第一种,即在使用该常变量的源文件中定义,在对应的头文件中加上extern关键字声明,在另外一个需要使用该全局常变量的源文件中#include对应的头文件。

注意:不能在头文件中使用

extern const int m = 5;

来定义常变量。这样会产生重定义错误。

常指针

形式

const 类型说明符 *变量名指向常量的指针。指针所指向的内存不能改变,但是指针本身的内容可以改变,即可以重新赋值,使之指向另外一个变量。 (其中类型说明符const的位置可以互换。)

类型说明符 * const 变量名:指向某个变量的常指针。指针所指向的内存可以改变,但是不能重新给指针赋值使之指向另外一个变量。

const 类型说明符 * const 变量名指向常量的常指针。指针本身和指针所指向的内存空间都不能改变。

区分多种形式

对于以上三种不同形式但很容易混淆的形式,有如下两种简单区分的方法。

第一种:按照从右向左的方式读。比如

const int *p;:p左边是*,说明p是一个指针,再左面是int,说明p指向的是一个int类型的变量,再左面是const,说明p指向的是一个常整型的变量。

再比如

int * const p; :p左边是const,说明p是一个常量,再左面是*,说明p是一个常指针,再左面是int,说明p是一个指向int的常指针。

同理,const int *const p;中p的类型就是指向常整型的常指针。

第二种:从*所在的位置画一条线,左边的const是修饰指针所指的类型,右边的是指针本身的类型。比如

const int *p;:从*所在的位置画一条线,左面有const,说明p指向的是一个常整型,右边没有const,说明p是一个非常量。综上,p是一个指向常整型的指针。

同理,int * const p;const int *const p;的类型也可以判断出来。

常引用

因为引用在声明的时候就必须初始化,而且初始化之后不能重新绑定到另外一个对象上。因此,引用本身就是常量。

int i = 5;

const int & ri = i; // ri是常引用,不能通过ri来修改所引用对象的值。

ri = 6; // error: ri是常引用,不能通过ri来修改所引用对象的值。

常成员函数

形式

type func(parameter list) const

对形式的解释

常成员函数的声明与普通函数的声明唯一的不同之处在于,常成员函数在参数列表之后加入了const修饰符。总所周知,成员函数中都有一个隐藏的变量——this指针,且this指针的类型为classType *const ,即this是一个指向本对象的常指针,其本身的值不可变,但是所指对象的内存中的值可以变。在常成员函数参数列表之后加入的const修饰符将this的类型改为了指向常对象的常指针,即const classType *const,即加入了一个底层const(low-level const)。这时,只能通过this指针访问类中的所有的成员变量(包括const和nonconst)以及const成员函数,但是不能修改nonconst成员变量。

另外需要说明的是,参数列表后面有无const可以构成函数重载(返回值有无const无法构成函数重载,因为函数重载需要满足两个条件中的一个:①参数的类型不同;②参数的个数不同),比如

void print();

void print() const;

将构成函数重载。

在使用的时候,如果类对象是nonconst,那么将使用void print(),如果类对象是const,那么将使用void print() const

const修饰函数传入参数

将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
通常修饰指针参数和引用参数:
void Fun( const A *in); //修饰指针型传入参数
void Fun(const A &in); //修饰引用型传入参数

一般而言,使用const修饰指针型、引用型、类对象传入参数可以提高效率,但是修饰基本类型,比如int,double等并不会提高效率,因为传入基本类型时是值传递。

常成员变量

具体看下面的代码,以及其中的注释。

f1.h

#pragma once
#ifndef _F1_H_
#define _F1_H_

void print1();

class A {
public:
	A(int& a) :ci2(5),r(a) { // 只有非静态(non-static)成员变量才能在初始化列表中初始化。
		                 // const常量没有类内初始化的话,必须在初始化列表中初始化,而不能
		                 // 在构造函数的函数体内赋值,因为构造函数的函数体从本质上讲已经不是
		                 // 初始化了,而是赋值,只有初始化列表才是初始化。
	}
	static int psi;
private:
	const int ci = 1;        // 很多博客说const变量只能在初始化列表中初始化,但在VS2015中可以类内初始化。
                                 // 虽然说在VS2015中可以类内初始化,但是不推荐这样做,因为const变量对于某个
                                 // 对象来说是一个常量,但是对不同的对象来说是一个可变的量,即不同的对象可以拥有
                                 // 不同的ci值。因此,推荐的做法是const变量在初始化列表中初始化。
	const double cd = 3.5;
	const int ci2;           // 没有类内初始化,必须在构造函数的初始化列表中初始化。
	int i = 3;               // 必须使用类内初始化,默认初始化,构造函数初始化列表初始化,
                                 // 构造函数函数体初始化,不能使用类外初始化。
	int oi;                  // 该变量是不会被初始化的(VS2015观察到的现象)
	static int si;           // 类外初始化不能加staic。类外初始化不能在头文件中初始化,因为多个cpp文件包含
                                 // 头文件时会产生重定义错误。
	static const int sci = 3; //  ok,static const int可以类内初始化
	// static const double scd = 3.9; // 只有static const int才能类内初始化
	int& r;                   // 引用必须在构造函数的初始化列表中初始化。
};

// int A::oi = 5; // error: 非静态类内成员不能在类外初始化
#endif

f1.cpp

#include <iostream>
#include "f1.h"
using namespace std;

int A::si = 5; // 类外初始化不能加staic
			   // static成员的初始化要在实现中进行,不能在头文件进行。

int A::psi = 6; // 类外初始化不能加staic
			   // static成员的初始化要在实现中进行,不能在头文件进行。

void print1() {
	cout << A::psi << endl;
}

main.cpp

#include <iostream>
#include "f1.h"
using namespace std;

int main() {
	print1();
	int abc = 5;
	int &rabc = abc;
	A a(rabc);
	int in;
	cin >> in;
	return 0;
}

常对象

const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;
const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。

生命周期

全局静态变量

生命周期:程序运行期一直存在;

作用域:文件作用域;

内存分布:全局(静态存储区)。

定义方法:static关键字,const关键字(注意C/C++意义不同)

注意:只要文件不相互包含,两个不同的文件中是可以定义完全相同的两个全局静态变量的。

静态局部变量

生命周期:程序运行期一直存在;(超过其作用域便无法被引用

作用域:局部作用域(只在局部作用于可见)

内存分布:全局(静态存储区)。

定义方法:局部作用域中用static定义。

注意:只被初始化一次,多线程中需要加锁保护。

其实常量是可以修改的

const是编译期使用的,是为了让编译器发现程序猿修改本不想修改的内存空间。但是我们可以使用小技巧骗过编译器,达到修改常量的目的。

下面的操作是错误的:

int i = 10;
const int *pi = &i;
*pi = 100;

因为你在试图通过pi改变它所指向的内容。但是,并不是说该内存块中的内容不能被修改。我们仍然可以通过其他方式去修改其中的值。例如:

// 1: 通过i直接修改。

i = 100;

// 2: 使用另外一个指针来修改。

int *p = (int*)pi;
*p = 100;

实际上,在将程序载入内存的时候,会有专门的一块内存区域来存放常量。但是,上面的i本身不是常量,是存放在栈或者堆中的。我们仍然可以修改它的值。而pi不能修改指向的值应该说是编译器的一个限制。

完整程序如下:

const int i = 5;
const int *pi = &i;
int *p = (int *)pi;
*p = 6;

另外,也有这样的情况,虽然我们可以绕过编译器的错误去修改类的数据成员。但是C++也允许我们在数据成员的定义前面加上mutable,以允许该成员可以在常量函数中被修改。

杂项

const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么。

要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现。如

class A{
    enum{size1=100,size2=200};
    int array1[size1];
    int array2[size2];
};

 

枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。

一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。原因如下:如果返回值为某个对象为const(const A test = A实例)或某个对象的引用为const(const A& test = A实例),则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。

函数返回值采用“引用传递”的场合不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。

const与constexpr的区别

const是运行时初始化,constexpr是编译时初始化。

int plus1(int i, int j) {
	return i + j;
}

constexpr int plus2() { // constexpr关键字必须加上,否则即使返回值是字面值(constant expression)
                        // 编译器也不认为这个函数是常量表达式函数
        return 2 + 3;
}

int in1;
int in2;
cin >> in1 >> in2;
const int ccc = plus1(in1, in2); // ok,运行时初始化
constexpr int cstr = plus1(in1, in2); // error,编译时不知道plus1返回的结果
constexpr int cstr2 = plus2(); // ok, plus2返回的是constant expression

constexpr函数

constexpr函数(constexpr function)是指能用于常量表达式的函数。定义constexpr函数的方式与其他函数类似,不过需要遵循几项约定:函数返回值类型和所有形参类型都必须是字面值类型,而且函数中必须只有一条return语句。

为了在编译中随时展开,constexpr函数被隐式地指定为内联函数(inline function)。

constexpr函数不一定返回常量表达式。这与形参和函数行为有关。

Note: 内联函数和constexpr函数通常定义在头文件内,为了编译器能够随时展开。因为编译器要展开一个内联函数或者constexpr函数,仅仅知道函数的声明是不够的。

使用const的一些建议

  1. 要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
  2. 要避免最一般的赋值操作错误,如将const变量赋值;
  3. 在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
  4. const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
  5. 不要轻易的将函数的返回值类型定为const;
  6. 除了重载操作符外一般不要将返回值类型定为对某个对象的const引用。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

2 + 2 =

41 − 34 =