【深度C++】之“const”

0. const限定符

有时我们希望定义这样一种变量,它的值不能被改变,比如缓冲区的大小、π(3.1415926)等,既可以防止其他协作者编写代码时一不小心改变了这个重要的值,而且该数值是有类型的,又可以限定其参与的运算。

在C++中,可以使用const修饰变量来实现。

const int buff_size = 32;

当然,const不仅仅可以修饰变量,它还有其他很多用法:

  1. 修饰基本内置类型
  2. 修饰自定义对象
  3. const与数组
  4. const与引用
  5. const与指针(顶层const与底层const)
  6. 修饰函数参数
  7. 修饰函数返回值
  8. 修饰成员函数

1. const修饰基本内置类型

const int buff_size = 32;

这是const最基本的用法,我们在以后的代码不能如下一样修改buff_size

const int buff_size = 32;
buff_size = 64; // 错误,编译不通过

在定义const变量的时候,必须进行初始化。编译器在编译过程中,会把用到该变量的地方全部替换成相应的值

默认情况下,const仅在当前文件中有效。若希望多个文件使用同一个const变量,可以使用extern来修改,在任何声明和定义处使用(当然只需要定义一次)。

2. const修饰自定义对象

const MyClass obj;

这样的声明意味着obj中的成员变量不可以被修改,且只能调用常成员函数(见本文第8节)。

但是C++还提供了一个关键字mutable(参考【深度C++】之“mutable”),它可以允许我们修改常量对象中的成员变量。

3. const与数组

const与数组只有一种形式,即:

const int arr[] = {2, 3};

同样,必须初始化;不可以执行arr[0] = 1类似的代码。

参考【深度C++】之“数组”,我们了解编译器通常把数组处理为指向首元素的响应类型的指针,这里也是,类似于const int *类型。

const与指针相关内容,请看本文第5节。

4. const与引用

4.1 定义方法

可以把引用绑定到const对象上,我们称之为对常量的引用

const int ci = 422;
const int &r_ci = ci;

上述代码第二行将一个引用绑定到了一个常量。为了正确的绑定,必须将引用声明为const。

我们通常称对常量的引用常量引用

这样的叫法,非常非常非常容易和常量指针搞混。这样我们就不可以通过引用r_ci修改ci

r_ci = 500;  // 错误

4.2 常量引用的初始化

C++的引用必须与其所引用的对象的类型严格一致。

但是在初始化常量引用的时候,可以用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。

例如非常量对象、字面值、表达式。

int i = 42;
const int &r1 = i;  // 正确,常量引用绑定在一个非const变量上
const int &r2 = 42;  // 正确,常量引用绑定在字面值上
const int &r3 = r1 * 2;  // 正确,常量引用绑定在表达式结果上

当一个常量引用被绑定到另外一种数据类型上的时候,编译器执行了如下的过程:

  1. 生成另一个临时常量,用目标对象的值初始化临时常量;
  2. 将常量引用绑定到上述的临时常量上。

看如下代码解释:

double d = 3.14;
const int &ri = d;

// 会被编译器解释成如下代码
double d = 3.14;
const int temp = d;  // 执行double -> int的隐式转换
const int &ri = temp;  // 将常量引用ri绑定到临时量上

若变量类型和常量引用不需要执行转换,则没有生成临时量这一步。

double d = 3.14;
const double &rd = d;  // 不需要生成临时量

C++规定,将左值引用绑定到临时量是非法的;但是却可以将常量引用绑定在临时量上。因此,我们可以将函数的返回值声明成常量引用:

const int &get_a() {
    int a = 3;
    return a;
}

调用get_a将得到数值3。这一点也在【深度C++】之“引用”中初始化引用的例外情况1提及:可以用临时量初始化初始化常量引用。

这里其实可以用C++11的右值引用来直观感受一下。我们可以写成:

double d = 3.14;
const int &&ri = d;
// 编译会通过,会产生一个临时量,可以绑定右值引用

但是却不能写成:

double d = 3.14;
const double &&rd = d;
// 此时编译不通过了,因为没有临时量产生
// 初始化右值引用的必须是一个右值

5. const与指针

5.1 指向常量的指针

对常量的引用(常量引用)一样,我们可以定义一个指向常量的指针

const double pi = 3.14;
const double *ptr_pi = π

// 错误调用,不可以通过ptr_pi修改pi
*ptr_pi = 3.1415926;

指针的类型必须与其所指向的类型严格一致。

但是允许一个指向常量的指针,指向一个非常量对象。

例如:

double pi = 3.14;
const double *ptr_pi = π

可以理解为是指针的“自以为是”的常量。

这种用法,和常量引用类似,但是没有临时量的概念加入,基本数据类型必须是严格匹配的。

其实不论是常量引用还是指向常量的指针,都是引用或指针的“自以为是”。它们觉得自己引用或指向了一个常量,所以自觉不去改变目标的值。

5.2 常量指针

因为指针是C++中的对象,引用仅仅是别名,因此不可以将引用设置为const,但是可以将一个指针声明为const

(╯‵□′)╯︵┻━┻

实现的效果就是,该指针一旦初始化完成之后,就不能更改指针内容,即不允许将该指针再指向别的对象。

int buff_size = 512;
// pti将始终指向buff_size
int *const pti = &buff_size;

// 允许通过常量指针修改原对象的值
// 因为原对象也是非常量
*pti = 1024;

const double pi = 3.14;
// pt_pi是一个常量指针,指向常量的常量指针
const double *const pt_pi = π

5.3 顶层const与底层const

指针本身是不是常量及所指的是不是一个常量,是两个毫无关联的问题。

  • 使用术语顶层const,表示指针本身是个常量;
  • 使用术语底层const,表示指向的对象是个常量。

在这里插入图片描述

更一般的,任意对象的const,都是顶层const

int i = 0;

// 顶层const,修饰普通对象
// 不允许改变ci的值
// 其实可以写成 int const ci = 5;
const int ci = 5;

// 顶层const,修饰指针对象
// 不允许改变cp_i的值
int *const cp_i = &i;

// 底层const,修饰指针指向的对象
// 不允许通过p_ci改变ci的值,即*p_ci = 10
const int *p_ci = &ci;

// 左侧的是底层const,右侧的是顶层const
// 既不允许改变cp_ci的值,也不能使用*cp_ci的改变ci的值
const int *const cp_ci = &ci;

// 底层const,引用都是底层const
// 不允许通过r_ci改变ci的值
const int &r_ci = ci;

注意:只有两处有底层const,一个是指针,一个是引用。

当执行对象的拷贝操作时,常量是顶层const还是底层const区别很大。

顶层const不受什么影响,对拷贝源不做过多要求,因为拷贝只关心拷贝源的值。

// ci是顶层const的常量
// 可以赋值给变量i
i = ci;

// cp_ci有顶层const
// 可以赋值给没有顶层const的p_ci
p_ci = cp_ci;

但是,如果拷贝目标拥有底层const,那么要求拷贝源也必须是一个常量,拷贝目标和拷贝源必须具有相同的底层const

  • 可以把非底层const的变量赋值给一个底层const
int a = 20;
int *p_a = &a;
// 非底层const赋值给底层const
const int *p_ca = p_a;
  • 但是不可以使用底层const赋值给非底层const
int a = 20;
const int *p_ca = &a;
// 编译出错
// 底层const赋值给非底层const
int *p_a = p_ca;

这么严格的匹配关系,在函数调用时传递形参时会用到,尤其是当含有函数重载的时候,会决定调用哪个重载函数,见下一节。

6. const修饰函数参数

我们可以将函数参数声明为const,且在函数调用处,利用传递给函数的实参对函数形参初始化。

形参的初始化方式和变量的初始化方式是一样的,因此调用函数传递const实参的规则,和上述的顶层const和底层const的规则一模一样。

因为C++支持函数的重载,又因为顶层const的要求很松,若如下声明了两个函数:

void func(const int i);
void func(int i);  // 错误

当我们调用的时候并区分不开,因此上述声明重复定义了func,是不允许的,这一点在【深度C++】之“函数重载”也提及,在【深度C++】之“函数匹配”中详细介绍了上述示例错误的原因。

C++标准建议,当我们不需要修改某个函数参数的值的时候,尽量将其声明为常量引用,即:

void func(const int &i);

这样就可以避免出现二义性问题,还可以提高程序的运行效率(因为使用引用)。

7. const修饰函数返回值

函数返回一个值的方式和初始化一个变量或者形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

通常情况下,返回基本内置类型,不需要添加const,因为函数返回的是一个右值。

返回指针和引用的时候,如果不添加const,我们可能会写出如下代码:

string &shorter_string(string &s1, string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

shorter_string(s1, s2) = "new string";

虽然正确,但是很迷惑,通常添加const。

const string &shorter_string(const string &s1, const string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

在第4节我们也提及了返回常量引用的函数,可以返回函数体内的临时量:

// 调用get_a会得到数值3
const int &get_a() {
    int a = 3;
    return a;
}

要想做到这种效果,必须添加const。

8. const修饰成员函数

可以如以下方式定义一个const成员函数(常量成员函数):

class MyClass {
private:
    int i = 0;
public:
    int func() const;
};

紧随参数列表之后的const关键字,其作用是修改隐式常量指针this的类型

默认情况下,this的类型是指向类类型非常量版本的常量指针

// 示例,C++很可能就是这么声明this的
// 这是一个顶层const,this指针的值是不允许被修改的
// 即只能指向本类的一个实例自己
MyClass *const this

加入我们将MyClass对象声明为了常量,指针变成了:

// 既包含顶层const,又包含底层const
const MyClass *const this

导致不能在一个常量对象上调用普通的成员函数,包括常量对象的引用和常量对象的指针:

const MyClass mc;
// 无法调用,底层const约束不能改变任何成员变量
mc.non_const_func();

当我们把const添加在成员函数之后,就可以使用常量对象、及其引用和指针调用了。

此时,我们不能再在const成员函数中修改任何的成员变量

const MyClass my_class;
my_class.func();

关于函数的调用,有以下规则:

  • 若my_class不是常量,且类只有func的常量版本,则会调用func的常量版本;
  • 若my_class不是常量,同时声明了func的非常量版本和常量版本,则会调用func的非常量版本
  • 若my_class是常量,那么只会调用常量版本

见如下两个示例:

class MyClass {
public:
    int func() const { return 1; }
};

int main() {
    // 不是常量
    MyClass mc;
    // 输出1
    std::cout << mc.func() << std::endl;
}
class MyClass {
public:
    int func() { return 0; }
    int func() const { return 1; }
};

int main() {
    // 不是常量
    MyClass mc;
    // 调用普通版本,输出0
    std::cout << mc.func() << std::endl;
    // 是常量
    const MyClass c_mc;
    // 输出1
    std::cout << c_mc.func() << std::endl;
}

9. 总结

C++中的const的是将对象和函数声明为只读状态的一个关键字。

const可以与基本内置类型使用,此时定义的变量初始化之后就不会改变,编译器会将用到该变量的地方都替换成变量的值。

const可以修饰自定义对象,此时只能调用const成员函数。

const可以与引用使用,表明这是一个对常量的引用,通常称作常量引用。

const可以与指针使用,要区分指向常量的指针和常量指针,以及顶层const和底层const的定义。

const可以修饰函数的参数,通常传递常量引用时使用。

const可以修饰函数的返回值,通常在返回指针和引用时使用。

const可以修饰成员函数,修改隐式常量指针this的类型,使常量对象可以调用成员函数。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值