C++中的const

C++ 中的 const 是一项语义约束,它允许你告诉编译器和其他程序员某个值应该保持不变。需要注意的是,常量在定义之后就不能修改,因此定义时必须初始化。

const 关键字介绍

const 在 C++ 中主要用来修饰内置类型变量, 指针,函数参数, 自定义对象,成员函数,返回值。 C++ 中的 const 默认是内部链接,只在当前文件中可见,不能用于外部文件(这和 C 相反),如果要在外部文件中使用,应该加上 extern 关键字。

const 修饰变量

const 修饰变量,以下两种定义形式在本质上是一样的。它的含义是: const 修饰的类型为 TYPE 的变量 value 是不可变的。

 TYPE const ValueName = value;
 const TYPE ValueName = value;

将 const 改为外部链接,作用于扩大至全局,编译时会分配内存,并且可以不进行初始化,仅仅作为声明,编译器认为在程序其他地方进行了定义。关于 C++ 中 const 变量的内存分配,可以参考这里

 extend const int ValueName = value;

const修饰指针

对于指针,你可以将指针本身、指针所指向的对象或者这两者都(或都不)定义为 const。这里比较容易弄混的是,看到一个加了 const 的指针,无法确定指针是 const 还是所指向的对象是 const,但只要记住以下规律,就可以分辨清楚:

char greeting[] = "Hello";
char* p = greeting;                   // non-const pointer, non-const data

// const出现在*左边,表示被指向的对象是const
const char* p = greeting;             // non-const pointer, const data
char const * p = greeting;            // non-const pointer, const data

// const出现在*右边,表示指针是const
char* const p = greeting;             // const pointer, non-const data

const char* const p = greeting;       // const pointer, const data

此外, STL 迭代器是以指针为根据塑模而成,所以 STL 迭代器的作用就像个 T* 指针。声明迭代器为 const 的方式和声明指针为 const 一样(即声明一个 T* const 指针),表示这个迭代器不得指向不同的东西,但它所指向的值是可以改动的。如果希望迭代器所指向的值不可以被改动(即希望 STL 模拟一个 const T* 指针),可以使用 const_iterator:

vector<int> vec;
...
const vector<int>::iterator iter = vec.begin(); // iter的作用像个 T* const
*iter = 10; //没问题,改变iter所指向对象的值
++iter; //错误,iter 是const
vector<int>const_iterator cIter = vec.begin(); // cIter的作用像个const T*
*cIter = 10; //错误,*cIter是const
++cIter; //没问题,改变cIter

const 修饰函数参数

const 修饰函数参数分为以下四种情况:

(1)const 修饰值传递参数

void func(const int a);

一般这种情况不需要 const 修饰,因为函数会自动产生临时变量复制实参值,而且对形参的修改也不会对实参造成影响。

(2)const 修饰指针所指向变量

void func(const int* a){
  ...
  *a = 0; // 错误
  int b = 0;
  a = &b; // 正确,但是对实参无影响,即不会修改实参所指向的对象
  ...
}

这种方式比较常用,当函数内部对指针所指向的值进行修改时,编译器会报告错误。 而且 const 指针可以接收非 const 和 const 指针,而非 const 指针只能接收非 const 指针。

(3)const 修饰指针

void func(int* const a){
  int *b = 1;
  a = b; // 错误,无法对read-only参数赋值
  *a = 1; // 正确,可以修改a所指向对象的值
}

这种方式表明指针不可以指向其它对象,但实际上作用不大,因为 *a 是一个形参,即使在函数体内修改了 a 所指向的对象,也不会对实参造成任何影响。

(4)const 修饰引用传递参数

void func(const Type &a);
void func(const class &a);

这样的一个 const 引用传递和最普通的函数按值传递的效果是一模一样的,它禁止对引用的对象的一切修改,唯一不同的是按值传递会先建立一个实参的副本, 然后传递过去,而这里直接传递地址。对于何时使用值传递合适使用引用传递,可以参考这里

const 修饰自定义对象

在 C++ 中,const 也可以用来修饰对象,称为常对象。一旦将对象定义为常对象之后,就只能调用类的 const 成员(包括 const 成员变量和 const 成员函数)。主要有两种修饰方式:

// 定义常对象
const  class  object(params);
class const object(params);
// 定义指向常对象的指针
const class *p = new class(params);
class const *p = new class(params);

注意,我们如果将对象 a 作为参数传入函数中,传递进来的参数a是实参对象的副本,要调用构造函数来构造这个副本,而且函数结束后要调用析构函数来释放这个副本,在空间和时间上都造成了浪费。所以函数参数为类对象时,推荐用引用。但按引用传递,造成了安全隐患,通过函数参数的引用可以修改实参的内部数据成员,所以用 const 来保护实参。

const 修饰成员函数

const 修饰成员函数时,放到函数体的行尾处,表明在该函数体内,不能修改对象的数据成员,且不能调用非 const 成员函数。比如:

void SetAge(int age)
void SetAgeConst(int age) const

两者的区别在于:前者可以修改类的数据成员,而后者不可以。为什么不能调用非 const 函数?因为非 const 函数可能修改数据成员, const 成员函数是不能修改数据成员的,所以在 const 成员函数内只能调用 const 函数。

const 修饰函数返回值

用 const 修饰返回的指针或引用,保护指针或引用的内容不被修改。比如:

#include <iostream>
using namespace std;

class Student {
public:
    int& GetAge() {
        return m_age;
    }
    // 值得注意的时,两个GetAge的常量性(constness)不同,也可以被重载
    const int& GetAgeConst() {
        return m_age;
    }

    void ShowAge() {
        cout << "Age: " << m_age << endl;
    }

private:
    int m_age = 0;
};

int main()
{
    Student stu;
    stu.ShowAge();

    stu.GetAge() = 5; // 会修改成员变量的值
    stu.ShowAge();

    stu.GetAgeConst() = 8; // 编译器会报错
    stu.ShowAge();

    return 0;
}

两者的区别在于:前者返回的是一个左值,其引用的内容可以被修改;后者返回的是一个右值,其引用的内容不可被修改。因此,当函数返回值是一个引用时,如果不允许其作为左值,就加上 const 。

一般来说,一个 const 成员函数是不可用修改成员变量的值的,但是如果一定要在其中修改,那么可以使用 mutable 关键字。

class C {
public:
  void funcA() const;
  void funcB();
private:
  mutable int a;
  int b;
};

void C::funcA() const{
  a = 1; // 可以
  b = 1; // 不可以
}

void C::funcB(){
  a = 1; // 可以
  b = 1; // 可以
}

注意到,如果一个对象是 const 对象,那么它只能调用类的 const 函数,或者 const 成员变量。因此,大部分情况下我们面临着重写两次代码的困境,此时我们可以将常量性移除。

class C{
public:
  const int& operator[](int a) const{
    ...
    return data[a];
  }

  int & operator[](int a){
    ...
    return const_cast<int&>(static_cast<const C&>(*this)[a]);
  }
private:
  int* data;
}

在这里,我们让非 const 成员函数调用 const 成员函数,使用了两次转型操作。第一次将非 const 对象转换为 const 对象;第二次使用 const_cast 移除 const 。

小结

  • 将某些东西声明为 const 可以帮助编译器检测出错误用法。 const 可以被施加在任何作用域内的对象、函数参数、返回值、成员函数体。

  • 编译器强制实施 bitwise constness (即成员函数不能修改任何成员变量),但编写程序时应该使用“概念上的常量性”。

  • 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可以避免代码重复。

参考

[1] Effective C++ 中文版,第三版 / (美)梅耶(Meyers, S.)著;侯捷译。北京:电子工业出版社,2006.7.

[2] C++ const对象(常对象)

[3] 关于C++ const 的全面总结

[4] C++ const 关键字小结

[5] C++中const的强大用法:修饰函数参数/返回值/函数体

[6] C++ const修饰函数、函数参数、函数返回值

posted @ 2022-12-04 13:46  greatestchen  阅读(37)  评论(0编辑  收藏  举报