68.C++中的const

  编写程序过程中,我们有时不希望改变某个变量的值。此时就可以使用关键字 const 对变量的类型加以限定。

初始化和const

  因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。一如既往,初始值可以是任意复杂的表达式:

const int i = get_size();//正确:运行时初始化
const int j = 42;//正确:编译时初始化
const int k;//错误:k是一个未经初始化的常量 

  正如之前反复提到的,对象的类型决定了其上的操作。与非const类型所能参与的操作相比,const类型的对象能完成其中大部分,但也不是所有的操作都适合。主要的限制就是只能在const类型的对象上执行不改变其内容的操作。例如,const int和普通的int一样都能参与算术运算,也都能转换成一个布尔值,等等 。

在不改变const对象的操作中还有一种是初始化,如果利用一个对象去初始化另外 一个对象,则它们是不是const都无关紧要:

int i = 42;
const int ci = i;//正确: 1的值被拷贝给了ci
int j = ci;//正确:ci的值被拷贝给了J

默认状态下,const对象仅在文件内有效

  当以编译时 初始化的方式定义 一个const对象时,就如对bufSize的定义 一样:

const int bufSize = 512;//输入缓冲区大小

  编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到 bufSize的地方,然后用512替换。为了执行上述替换,编译器必须知道变量的初始值 。如果程序包含多个文件,则每个 用了const对象的 文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每 一个用到变量的文件中都有对它的定义(参见C++Primer2.2.2节,第41页)。为了支持这一用法, 同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。

  某些时候有这样一种const变量,它的初始值不是一个常旦表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中 定义const,而在其他多个文件中声明并使用它。

  解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:

//file_l.cc定义并初始化了一个常岳,该常量能被其他文件访问
extern const int bufSize =fen();
//file_l.h头文件
extern const int bufSize;//与file_l.cc中定义的bufSize是同一个

1.变量中的const

1.1 普通变量

直接在普通变量类型声明符前加上 const,可以将其声明为 const 类型:

const int a = 0;

这样就把 a 声明成了一个 const 类型的常量,所以我们不能再改变它的值了,所以下面试图改变 a 的语句将会编译报错:

a = 10;

修改局部变量的值:

1.如果const修饰的局部变量是基础的类型(int char double等等),并且初始化使用字面常量的话,不会给该变量分配空间。

例如:

void test()
{
        const int a = 10;//用字面常量10来初始化
        a = 20;//error
}

2.但是,当我们对这个变量进行取地址的操作的时候,系统会为该变量分配空间。

void test() 
{
        const int a = 10;
        //a = 20;//error
        int* p = (int*)&a;
        *p = 20;
        cout << a << endl;
        cout << *p << endl;
}

上面的结果是:10和20

  这是因为,当我们定义一个被const修饰并且使用字面常量来初始化的局部变量的时候,系统会把这个变量看作是一个符号,放入到符号表中,这么变量名就是一个符号,值就是这个符号的值,类似于#define的作用。(这就是 C++ 中的常量折叠 ,因为常量是在运行时初始化的,编译器对常量进行优化,直接将常量值放在编译器的符号表中,使用常量时直接从符号表中取出常量的值,省去了访存这一步骤。)

  当我们对这个变量取地址的时候,由于原来没有空间,就没有地址,现在需要取地址,所以才被迫分配一块空间,我们通过地址的解引用可以修改这个空间的值,这也就是为什么第二个结果为20的原因,但是如果我们还是通过变量名来访问数据的话,系统会认为这还是一个符号,直接用符号表里面的值替换。

但是!

3.如果初始化不是用字面常量而是用变量,那么系统会直接分配空间。

void test() 
{
        int b = 20;
        const int a = b;
}

这时候的a是有空间的,不会被放入到符号表中。

修改全局变量的值

  通过指针修改位于静态存储区的的const变量,语法上没有报错,编译不会出错,一旦运行就会报告异常。因为全局变量存储于静态存储区,静态存储区中的常量只有读权限,不能修改它的值。

  与C一样,当const修饰普通的全局变量的时候,不能通过变量名和地址来修改变量的值。

另外

  与C不一样的是,C语言中的const修饰的普通全局变量默认是外部链接属性的,但是在C++中被const修饰的普通全局变量是内部链接属性的。

  也就是说当我们在一个文件中定义了一个如下的全局变量

const int a = 10;//定义全局变量

int main() 
{
        return 0;
}

  我们在另外一个文件中,使用extern来声明,也是不可以的。

//另外一个文件

extern const int a;//在另外的文件中声明

  上面这种做法是不可以的,C++中被const修饰的全局变量默认是内部链接属性,不能直接在另外的文件中使用,如果想要在另外的文件中使用,就需要在定义该全局的变量的文件中用extern来修饰(另一个文件也需要extern修饰)。

//定义的文件
extern const int a = 10;
//另外一个文件声明
extern const int a;

原文链接:https://blog.csdn.net/weixin_61021362/article/details/121544469

1.2 const 修饰引用

  我们还可以对引用使用 const 限定符,在引用声明的类型声明符前加上 const 就可以声明对const的引用,常量引用不能用来修改它所绑定的对象。

引用绑定到同一种类型,并修改值

直接上例子:

int i = 0;
const int j = 0;
const int &r1 = i;
//r1 = 20;//err不能给常量赋值  
const int &r2 = j;
//r2 = 20;//err不能给常量赋值  
int &r3 = j;

  第三行将非常量对象 i 绑定到 const 引用 r1 上,此过程中发生了隐式类型转换,i 的类型为 int,r1 的类型为 const int &, 所以这个过程 i 就从 int 转换为了 const int,所以不能通过 r1 改变 i 的值,但可以直接改变 i 的值。但是 const int 类型不能转换为 int。

可以这样理解:const int是int的一种,但是范围更小,将int限定在一个范围之类,(本身int = const int类型 + 非const类型),没有问题。但是const int到int范围扩大,超出权限。

  第五行将常量对象 j 绑定到 const 引用 r2 上,不能直接改变 j 的值也不能通过常量引用改变 j 的值。

  第七行将常量对象绑定到 const 引用 r3 上,报错,不能将常量对象绑定到常量引用上。

绑定到另一种类型,并修改值

直接上例子:

double i= 1.0;
const int &r1 = i; 
i = 2.0;
cout << "i = " << i << endl;
cout << "r1 = " << r1 <<endl;
---------------------------------------
out:
i = 2;
r1 = 1;

  上面的代码将 int 型的引用 r1 绑定到 double 型变量 i 上,然后改变 i 的值,我们发现 r1 并没有改变,它的值反而是绑定 i 时 i 的值。这是因为引用变量的类型与被引用对象的类型不同时,中间会有如下操作:

double i = 1.0;
int temp = i;
const int &r1 = temp;

  r1 引用的是临时量 temp,而不是 i,所以才会出现上面的情况。

1.3 const 修饰指针

  当使用const修饰指针变量时,情况就复杂起来了。const可以放置在不同的地方,因此具有不同的含义。来看下面一个例子:

int age = 39;
const int * p1 = &age;
int const * p2 = &age;
int * const p3 = &age;
const int * const p4 = &age;

  二三行是一个意思,表示 p 是指向常量的指针;第四行表示 p 是常量指针;第五行表示 p 是指向常量的常量指针。

  上面二三行的赋值同样发生了类型转换,从 int * 转换为 const int *。

指向常量的指针和常量指针

顾名思义:常量指针就是指针本身是常量,指针的值不能改变,也就是指针不能改变指向的对象,所以常量指针必须初始化;

指向常量的指针就是指向的变量时常量,被指变量不能被修改。

也可以将两者结合,就有了指向常量的常量指针,其具有指向常量的指针和常量指针的共同性质。

修改指向常量的指针和常量指针

int age2 = 20;
*p1 = 20;
*p3 = 20;
p1 = &age2;
p3 = &age2;

  第二行会报错,因为 p1 是指向常量的指针,不能通过指针修改 age 的值;第五行会报错,因为 p3 是常量指针,只能指向 age,不能指向其他变量。

  如果对age2进行修改是不会报错的。

原文链接:https://blog.csdn.net/weixin_45773137/article/details/126297568

1.4顶层与底层const

  任意常量对象为顶层const,包括常量指针;指向常量的指针和声明const的引用都为底层const

  顶层const(top-level const)表示指针本身是个常量 int* const ptr=&m;

  此时指针不可以发生改变,但是指针所指向的对象值是可以改变的

  底层const(low-level const)表示指针所指的对象是常量 const int* ptr=&m;

  此时指针可以发生改变,但是指针所指向的对象值是不可以改变的

  顶层const可以表示任意的对象是常量(指针、引用、int、double都可以)

  于是只有指针和引用等复合类型可以是底层const

  执行对象的拷贝构造时,常量是顶层const还是底层const差别明显

  顶层const并不会有任何影响

进行拷贝操作的时候,仅仅只是从右值(顶层const)拷贝一个值并给自己赋值,虽然右值是一个不可变的量,但是貌似对我自己的拷贝完全没有影响吧

const int m = 10;
int n = m;
int* const ptr2 = &n;
int* ptr3 = ptr2;
int i= 0; 
int *const p1 = &i;//不能改变p1的值,这是一个顶层const
const int ci = 42;//不能改变ci的值,这是一个顶层const
const int *p2 = &ci;//允许改变p2的值 这是一个底层const
const int *const p3 = p2;//靠右的const是顶层const, 靠左的是底层
const const int &r = ci;//用于声明引用的const都是底层const

  当执行对象的拷贝操作时, 常量是顶层const还是底层const区别明显。 其中,顶层const不受什么影响:

i = ci;//正确:拷贝ci的值,CI是 一个顶层const, 对此操作无影响
p2 = p3;//正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响

  执行拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么 影响。

  另一方面,底层const的限制却不能忽视。 当执行对象的拷贝操作时拷入和拷出的对象必须具有相同的底层const资格, 或者两个对象的数据类型必须能够转换。非常量可以转换成常扯, 反之则不行:

int *p = p3;//错误:p3包含底层const的定义,而p没有
p2 = p3;//正确:p2和p3都是底层const
p2 = &i;//正确:int*能转换成const int* 
int &r = ci;//错误:普通的int&不能绑定到int常量上
const int &r2 = 1;//正确:const int&可以绑定到一个普通int上

  p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p, 因为p指向的是一 个普通的(非常量)整数。 另一方面,p3的值可以赋给p2,是因为这两个指针都是底层 const,尽管p3同时也是一个常量指针(顶层const), 仅就这次赋值而言不会有什么影响。

原文链接:https://blog.csdn.net/m0_64860543/article/details/128269607

2.const 函数形参

  我们已经了解了变量中const修饰符的作用,调用函数就会涉及变量参数的问题,那么在形参列表中const形参与非const形参有什么区别呢?

2.1 const 修饰普通形参

同样,先来看看普通变量:

void fun(const int i)
{
        i = 0;
    cout << i << endl;
}

void fun(int i)
{
        i = 0;
    cout << i << endl;
}

int main()
{
    const int i = 1;
    fun(i);
    return 0;
}

  形参的顶层 const 在初始化时会被忽略,所以上面定义的两个函数实际上是一个函数。编译时会出现void fun(int) previously defined here错误。

  • 由于普通变量是拷贝传值,所以const int实参可以传给 int 形参。

  • 与普通 const 变量一样,第一个 fun 中的形参 i 只可读;第二个function中的 i 则可读可写。

2.2 const 修饰指针形参

  与 const 指针变量一样,指向常量的指针形参指向的值不能修改;常量指针形参不能指向其他变量;指向常量的常量指针形参指向的值不能被修改,也不能指向其他变量。

#include<iostream>
using namespace std;
void fun(const int* i)
{
    cout << *i << endl;
}

void fun(int* i)
{
    *i = 0;
    cout << *i << endl;
}

int main()
{
    const int i = 1;
    //调用 fun(const int* i),没有 fun(const int* i),则会编译报错,因为没有匹配形参的函数。
    fun(&i);  
    int j = 1;
    //调用 fun(int* i),没有 fun(int* i),则会调用 fun(const int* i),此时 j 的值不会被改变
    fun(&j);  
    return 0;
}

  p1 指向的值不能修改;p2 不能指向其他变量;p3 指向的值不能被修改,也不能指向其他变量。

此外,形参的底层 const 在初始化时不会被忽略,所以上面的两个函数是不同的函数,即重载函数,上面例子编译并不会报错,若果再加上一个void fun(int *const i)就会报错,因为这个函数定义里面 i 是顶层 const。

2.3 const 修饰引用形参

  与 const 引用一样,const 引用不会改变被引用变量的值。

#include<iostream>
using namespace std;
void fun(const int& i)
{
    cout << i << endl;
}

void fun(int& i)
{
    i = 0;
    cout << i << endl;
}

int main()
{
    const int i = 1;
    //调用 fun(const int& i),没有 fun(const int& i),则会编译报错,因为没有匹配形参的函数。
    fun(i);
    int j = 1;
    //调用 fun(int& i),没有 fun(int& i),则会调用 fun(const int& i),此时 j 的值不会被改变
    fun(j);
    return 0;
}

由于 const 引用也是底层 const ,所以上面两个函数是不同的函数,即重载函数,编译并不会报错。

3.类常量成员函数

  面向对象程序设计中,为了体现封装性,通常不允许直接修改类对象的数据成员。若要修改类对象,应调用公有成员函数来完成。为了保证const对象的常量性,编译器须区分试图修改类对象与不修改类对象的函数。例如:

const Screen blankScreen;
blankScreen.display();   // 对象的读操作
blankScreen.set(‘*’);    // 错误:const类对象不允许修改

  C++中的常量对象,以及常量对象的指针或引用都只能调用常量成员函数。

  要声明一个const类型的类成员函数,只需要在成员函数参数列表后加上关键字const,例如:

class Screen 
{
public:
   char get() const;
};

在类外定义const成员函数时,还必须加上const关键字:

char Screen::get() const 
{
   return screen[cursor];
}

若将成员成员函数声明为const,则该函数不允许修改类的数据成员。例如:

class Screen 
{
public:
    int get_cursor() const {return cursor; }
    int set_cursor(int intival) const { cursor = intival; }
};

在上面成员函数的定义中,get_cursor()的定义是合法的,set_cursor()的定义则非法。

值得注意的是,把一个成员函数声明为const可以保证这个成员函数不修改数据成员,但是,如果据成员是指针,则const成员函数并不能保证不修改指针指向的对象,编译器不会把这种修改检测为错误。例如:

class Name
{
public:
    void setName(const string &s) const;
    char *getName() const;
private:
    char *m_sName;
};
 
void setName(const string &s) const 
{
    m_sName = s.c_str();      // 错误!不能修改m_sName;
 
    for (int i = 0; i < s.size(); ++i) 
        m_sName[i] = s[i];    // 不是错误的
}

const成员函数可以被具有相同参数列表的非const成员函数重载,例如:

class Screen 
{
public:
    char get(int x,int y);
    char get(int x,int y) const;
};

在这种情况下,类对象的常量性决定调用哪个函数。

const Screen cs;
Screen cc2;
char ch = cs.get(0, 0);  // 调用const成员函数
ch = cs2.get(0, 0);     // 调用非const成员函数

const成员函数不能修改类对象数据成员的深层解析:

调用成员函数时,通过一个名为this的隐式参数来访问调用该函数的对象成员。例如:

Name bozai;
bozai.setName("bozai");
bozai.getName("BOZAI");

原文链接:https://blog.csdn.net/weixin_45773137/article/details/126297568