c++中const关键字被用来表示一种"常量属性",即变量的值不可被修改。这个关键字也是从C语言中继承来的,但是C语言的const由于无法保证运行时变量的常量属性,所以C++对此引入了常量表来进行升级;const应用于指针的用法和特性几乎和C语言一致;同时,C++也针对引用类型提供了const关键字支持。为了支持OOP,const也可以用来修饰方法。

1.普通用法

为了维持运行时的const语义,c++引入了常量表来记录const对象的名称和值,并将之后所有对const对象的访问修改为访问常量表中的值,以此保证const对象始终如一的常量属性。同时为了保持向下兼容C,C++采取另外一种常量对象的内存分配方式,即:代码中的const常量并不会立刻分配内存,只有当需要该对象的地址时,才为其分配内存,但从不使用该内存。

int main()
{
    const int a = 0; // a成为常量,<a, 0>被保存进入了常量表中,但是并未在栈上为a分配内存。
    //a = 10; // 编译错误,因为试图修改一个常量。
  
    int *pa = (int *)&a; // 此时,需要常量a的地址,因此编译器被迫在栈上为a分配一块内存。
    *pa = 20; // 此时,编译器被迫为a分配的内存被写为20,但是编译器不会使用这块内存。
    printf("a = %d\n", a); // 但是,每次对a的访问都会去访问常量表,因此a仍然为0,保持了常量属性
    printf("*pa = %d\n", *pa);
    return 0;
}

以上代码输出为:

a = 0 *pa = 20

  • C++下const不会分配内存,当取地址(指针)时会分配临时的内存,修改该内存不会改变const修饰变量的值
  • extern 提高作用域编译器也会给const修饰变量分配内存

然而,并非所有的常量值都能在编译期被确定。当只有在运行时才能确定常量的值时,该常量将不会进入常量表,仍然保持C语言的运行时不安全的常量属性:

int main()
{
    int v = 0;
    const int a = v; // 编译期间无法确定a的值,因此a不会进入常量表。
   // a = 10; // 编译错误,因为试图修改一个常量。
  
    int *pa = (int *)&a; // 取得了真正a变量的地址。
    *pa = 20; // 此时a的值被修改为20,失去常量属性。
    printf("a = %d", a); //输出 a = 20
    return 0;
}
int main()
{
    const volatile int a = 0; // 编译期间无法确定a的值,因此a不会进入常量表。
    //a = 10; // 编译错误,因为试图修改一个常量。
    int *pa = (int *)&a; // 取得了真正a变量的地址。
    *pa = 20; // 此时a的值被修改为20,失去常量属性。
    return 0;
}
由此可见,使通变量初始化const的值,编译器会给const修饰变量分配内存,使用指针修改后原变量的值也会改变

在另外一种情况下,如果赋值号两边的数据类型不同,那么将会产生类型截断,此时const也会失去常量语义:

int main()
{
    const long a = 0; // a成为常量,<a, 0>被保存进入了常量表中,但是并未在栈上为a分配内存。
    const int b = a; // 发生了类型截断,因此即便编译期可以确定值,也不会进入常量表。
    int *pb = (int *)&b; // 取得了真正a变量的地址。
    *pb = 20; // 此时b的值被修改为20,失去常量属性。
    return 0;
}

2.指针用法

const修饰指针,涉及到两个很重要的概念,顶层const底层const

指针自身是一个对象,它的值为一个整数,表明指向对象的内存地址。因此指针长度所指向对象类型无关,在32位系统下为4字节,64位系统下为8字节。进而,指针本身是否是常量以及所指向的对象是否是常量就是两个独立的问题。

顶层const(top-level const): 指针本身是个常量

底层const(low-level const): 指针指向对象是一个常量

通俗来说,就是顶层const表示指针不能再指向其它地址,底层const保证目前指向的地址中的数据不可被修改

int main(int argc, char *argv[])
{
    const int *a = nullptr; // 底层const,表示地址中的数据不可被修改。
    int *const b = nullptr; // 顶层const,表示指针不能指向其它位置。
    const int *const c = nullptr; // 顶层和底层const,表示指针不能指向其它位置,
                                  // 并且地址中的数据不可被修改。 
    return 0;
}

根据从内到外,由近到远读符号的规则

p1依次解读为:p1是个指针(*),指向一个int型对象(int),该对象是个常量(const)。 因此这是一个底层cosnt

p2依次解读为:p2是个常量(const),p2是个指针(*),指向一个int对象(int)。 因此这是一个顶层const

3.引用用法

引用没有顶层和底层之分,所有的const引用都是底层的。当针对左值引用使用const时,其行为如同C语言中的const一样,会产生一个新的编译期常量:

int main(int argc, char *argv[])
{
    const int &a = 0; 
    a = 10; // 编译错误,因为试图修改一个常量。
    int *pa = (int *)&a;
    *pa = 20; // 正确,a的值被修改为20.
    return 0;
}

右值不能绑定到左值引用上,但是可以绑定到常量左值引用上,这是因为它可以保证部分右值的不可修改属性。同理,从语义上来看,没有必要使用const修饰右值引用。

4.参数用法

const修饰参数是为了防止函数体内可能会修改参数原始对象。因此,有三种情况可讨论:

  1. 函数参数为值传递:值传递(pass-by-value)是传递一份参数的拷贝给函数,因此不论函数体代码如何运行,也只会修改拷贝而无法修改原始对象,这种情况不需要将参数声明为const。
  2. 函数参数为指针:指针传递(pass-by-pointer)只会进行浅拷贝,拷贝一份指针给函数,而不会拷贝一份原始对象。因此,给指针参数加上顶层const可以防止指针指向被篡改,加上底层const可以防止指向对象被篡改。
  3. 函数参数为引用:引用传递(pass-by-reference)有一个很重要的作用,由于引用就是对象的一个别名,因此不需要拷贝对象,减小了开销。这同时也导致可以通过修改引用直接修改原始对象(毕竟引用和原始对象其实是同一个东西),因此,大多数时候,推荐函数参数设置为pass-by-reference-to-const。给引用加上底层const,既可以减小拷贝开销,又可以防止修改底层所引用的对象。

5.类方法

在类的方法声明中使用const,则表示这个方法不会导致当前对象的改变;同时,为了保持对象的常量属性,const对象只能调用const方法。

class Object
{
    int _value;
public:
    Object(int value) : _value(value) {}
    int get_value() const { return _value; }
    void set_value(int value) { _value = value; }
};

int main()
{
    Object o1(10);
    o1.get_value(); // 正确, 10
    o1.set_value(20); // 正确
  
    const Object o2(10); 
    o2.get_value(); // 正确, 10
    o2.set_value(20); // 错误,const对象只能调用const方法。
    return 0;
}
最后修改:2024 年 07 月 17 日
如果觉得我的文章对你有用,请随意赞赏