C++中的 const 关键字

转载自知乎:https://zhuanlan.zhihu.com/p/37514756

1. 基本描述

定义变量时的限定符,表示变量值不能改变。

const int bufSize = 512;
bufSize = 512; // 错误:试图向const对象写值

由于const一旦创建就不可更改,所以const对象必须初始化(否则定义一个默认值且不可修改的变量没有任何意义)。

const int i = get_size(); // 正确
const int j = 42; // 正确
const int k;      // 错误:未初始化

使用值传递初始化时,被初始化的对象是否为const与初始化对象是否为const无关。也即,const对象与非const对象可以互为初始化。

2. const初始化引用时的例外

C++规定引用类型必须与被引用对象一致:

int i = 2;
double &j = i; // 错误:引用类型与对象类型不一致

C++还规定引用必须绑定到左值:

注:左值和右值的辨别方法是,能取地址的是左值。

int &i = 2;    // 错误:不允许用右值初始化
int &j = a * 2 // 错误:不允许用表达式初始化,实际上表达式(a*2)是右值

但是用const初始化引用时会有例外:

const引用类型与对象类型不一致(但可以转化):

int i = 2;
const double &j = i; // 正确:j是常量引用

const引用绑定到一个非左值上(类型一致或可以转化):

const int &i = 2;    // 正确:i是常量引用
const int &j = a * 2 // 正确:j是常量引用

原因在于,const引用将会额外创建一个临时变量,并绑定上去。

C++支持这种做法的目的在于,既然不能通过const引用修改对象值,那么额外创建一个常量和直接绑定对象并没有什么区别,所以干脆让const引用支持这种非常规做法。

3. 顶层const和底层const

通常在指针/引用与const符同时使用时会用到这个概念。修饰指针本身的const称为顶层const,修饰指针所指向对象的const称为底层const。底层const与顶层const是两个互相独立的修饰符,互不影响。

1. const与指针

指针本身是一个独立的对象,它又可以指向另一个对象。所以指针和const同时使用时,有两种情况:

int i = 0;
int *const j = &i; // 指针j指向i,const修饰指针j本身,所以j的地址值不允许修改,但可以通过j修改i的值
const int *k = &i; // 指针k指向i,const修饰k指向的i,所以k的地址值可以修改,但不可以通过k修改i的值

第一行j不可改,i可改
第二行k可改,i不可改

2. const与引用

引用一旦初始化,就不能再修改(绑定),所以引用本身就具有"const"的性质。

与指针相比,引用相当于内置了顶层const(也就是后置的const)。

所以使用引用时,就只需考虑是否为底层const:

int i = 0;
const int &j = i; // j为绑定到i的const引用,不允许使用j来修改i

j默认不能改,此时i也不能改

3. 其他

(1). 可以将底层const的指针(或引用)指向(或绑定)到非const对象,但不允许非底层const的指针(或引用)指向(或绑定)到const对象。 (即:const对象不允许通过任何方式(指针/引用)被修改。)
//非底层const的指针和引用可以改变指向对象的值,所以不能绑定到const对象上
(2). 修饰值本身的const均为顶层const:

const int i = 0; // 顶层const;

4. const与函数

1. 值传递的const形参

void fcn(const int i) { /* ... */ }

这个函数中,变量i为值传递形参,根据值传递的初始化规则**,形参i是否为const与传入的实参是否为const是完全无关的**。这里的const仅表示i在函数体中不允许修改。

如下的调用均为合法调用:

int x = 0;
fcn(x);
const int y = 0;
fcn(y);

因为值传递的const形参在调用上与非const形参没有区别(大概是指,无论形参是否为const,实参都不会被修改。因为是值传递本身就不能修改实参的值),所以仅仅使用const无法区分参数类别,所以无法实现函数重载,如下的重载是错误的:

void fcn1(const int i) { /* ... */ }
void fcn1(int i) { /* ... */ } // 错误:重复定义函数,不能实现重载

2. const指针/引用的形参
对于顶层const的指针,与上一小节一样,其const性质与实参无关,顶层const仅表示指针/引用本身在函数体中不允许修改。

所以我们只需要讨论底层const的指针/引用。

void fcn2(const int &x) { /* ... */ } // 接受const或非const的int引用,但是不允许通过x修改传入的对象
void fcn2(const int *y) { /* ... */ } // 接受const或非const的int指针,但是不允许通过y修改传入的对象

如上两个函数都定义了底层const的形式参数,它们可以接受const或非const对象,但是不能在函数体内修改这些对象。所以如下的调用都是合法的:

int i = 0;
fcn2(i);  // 正确:调用第一个函数
fcn2(&i); // 正确:调用第二个函数

const int j = 0;
fcn2(j);  // 正确:调用第一个函数
fcn2(&j); // 正确:调用第二个函数

由于底层const描述实参性质(不允许在调用函数内部被修改),可以在调用时区分const,所以使用底层const的指针/引用可以实现函数重载:

void fcn3(int &x) { /* ... */ } 
void fcn3(const int &x) { /* ... */ } // 新函数,作用于const的引用

所以可以分别调用两个函数:

int i = 0;
fcn3(i); // 正确:调用第一个函数

const int j = 0;
fcn3(j); // 正确:调用第二个函数

注意,当传递非常量对象时,编译器会优先调用非常量版本的函数。
总结

  • 顶层const的形式参数不能实现函数重载,但底层const形参可以
  • 当函数不修改参数值时,尽可能将形式参数定义为(底层)const参数。一方面,(底层)const参数可以保护参数对象;另一方面,因为(底层)const参数可以接受常量与非常量对象,但非(底层)const参数只能接受非常量对象。

5. const与类

1. const与类的成员变量

一个类通常包含成员函数和成员变量。

  1. 类的对象的const修饰表示该对象的成员变量不允许被修改。
  2. 无论类的成员变量本身是否为const,只要对象声明为const,成员变量就不允许被修改。
class Number
{
public:
    int number = 0;
};

int main()
{
    const Number n;
    n.number = 1; // 错误,n为const对象,不允许被修改
    return 0;
}

2. const与类的成员函数
当对象被声明为const时,该对象不能调用非const函数,因为非const函数可能修改成员变量。

class Number
{
public:
    void set(int num) { number = num; }
    int get() { return number; }

    int number = 0;
};

int main()
{
    const Number n;
    n.set(1); // 错误,n为const对象,不能调用非const函数
    cout << n.get() << endl; // 错误,原因同上
    return 0;
}
  1. 将成员函数声明为const函数,则可以被const对象调用,声明const函数的方法为在其参数列表后添加const关键字。
  2. const成员函数中不允许修改成员变量。也即,并非所有成员函数都可以被声明为const函数,C++会在编译时检查被声明为const的函数是否修改了成员变量,若是,则报错,编译不通过。
class Number
{
public:
    void set(int num) const { number = num; } // 错误:const函数不允许修改成员变量
    int get() const { return number; } // 正确:没有修改成员变量,可被声明为const函数

    int number = 0;
};

int main()
{
    const Number n;
    n.set(1);                // 错误,const函数不允许修改成员变量
    cout << n.get() << endl; // 正确,const对象可以调用const函数
    return 0;
}

与底层const形参一样,const成员函数也可以实现重载。同样,当非常量对象调用函数时,编译器会优先调用非常量版本的函数。

class T
{
public:
    int fcn() { return 1; }
    int fcn() const { return 2; } // 正确:定义了可以重载的新函数
};

int main()
{
    T t1;
    cout << t1.fcn() << endl; // 调用第一个函数,输出"1"

    const T t2;
    cout << t2.fcn() << endl; // 调用第二个函数,输出"2"
    return 0;
}

3. 总结

  1. 当函数不修改成员变量时,尽可能将函数声明为const函数,因为const函数可以被非const对象和const对象调用,而非const函数只能被非const对象调用。
  2. const函数并不意味着数据安全,虽然不能通过const函数修改成员变量,但是这样的const仅为顶层const(即成员变量本身不能被修改),若成员变量包含非底层const的指针/引用,虽然成员变量本身不能被修改,但依然可以通过这些指针/引用修改其指向/绑定的对象。

4. const成员函数实现机制
一个类包含成员变量和成员函数,更简单一点,一个类包含数据和代码。对象是类的实例,一个类可以构造许多对象,对象们的数据(成员变量)各自独立,而代码(成员函数)共用一份。

Number n;
n.number; // 调用成员变量
n.set(2); // 调用成员函数

实际上,由于成员函数共享,所以调用成员函数的机制与调用成员变量的机制略有区别,简而言之,编译器先找到类,然后调用类的函数,再隐式地在参数列表中传入一个对象指针(this指针),表示需要操作该对象。所以,成员函数set()的声明和定义可以理解为:

void Number::set(Number *const this, int num) { number = num; } 
// 仅作为参考,实际上,C++规定显式定义this指针为非法操作

即,任何一个成员函数都隐式地接受了一个指向对象的this指针。

而在成员函数中对成员变量的默认调用实际上都是使用this指针的隐式调用,比如 number = num 等价于 this->number = num。

那么,C++编译器检查const函数是否修改了成员变量的机制就很好理解了。

只需要将this指针定义为底层const,以表示不能通过该指针修改成员变量:

void Number::set(const Number *const this, int num) { number = num; } 
// 仅作为参考,实际上,C++规定显式定义this指针为非法操作

第一个const声明了this指针为底层const,而函数中的 number = num 实际为 this->number = num,由于this为底层const,所不能通过this修改number,该操作非法,所以该函数不能声明为const。
本质上,const函数还是通过传统的const机制逐条语句检查来实现的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值