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

4.constexpr 限定符

4.1常量表达式

常量表达式:指值不会改变并且在编译过程就能得到结果的表达式;字面值、用常量表达式初始化的const对象也是常量表达式。
字面值类型:算术类型、引用和指针都属于字面值类型,自定义类、IO库,string类型则不属于字面值类型,不能被定义成constexpr;

一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定, 例如:

const int max_files = 20;//max_files是常量表达式
const int limit = max_files + 1; //limit是常量表达式
int staff_size = 27;//staff_size不是常量表达式 
const int sz = get_size();//sz不是常责表达式

  尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。

4.2constexpr变量

  在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。当然可以定义一个const变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。可以这么说,在此种情况下,对象的定义和使用根本就是两回事儿。

  C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:

constexpr int mf = 20;//20是常量表达式
constexpr int limit= mf + l;//mf + 1是常量表达式
constexpr int sz = size();//只有当size是一个constexpr函数时才是一条正确的声明语句

4.3constexpr函数

  尽管不能使用普通函数作为constexpr变量的初始值,但是正如C++Primer6.5.2节(第214页) 将要介绍的,新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。

constexpr函数:指能用于常量表达式的函数,其定义方式和普通函数类型;
定义规则

  • 函数的返回类型及所有形参类型都是字面值类型
  • 函数体中只有一条return 语句
constexpr int func(int n)
{
	return n;
}
int main()
{
	int n=10;
	const int m=10;
	constexpr int i = func(10);//正确,i是一个常量表达式
	constexpr int j = func(n);//错误,n不是字面值
	constexpr int k = func(m+1);//正确,k是一个常量表达式
	return 0;
}
由于func是constexpr函数,编译器能在程序编译时验证func函数返回的是常量表达式,如上面的j,因为n不是常量,所以会在编译期间报错,方便修正
不仅如此,对于constexpr函数,编译器会把constexpr函数的调用替换成其结果值,为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数,关于内联函数在这篇C++ 内联函数博客中有详细介绍
简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值,如果传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了

————————————————
版权声明:本文为CSDN博主「倒地不起的土豆」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37766667/article/details/123915233

字面值类型

  常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为”字 面值类型”(literaltype)。

  到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不屈千字面值类型,也就不能被定义成 constexpr。其他一些字面值类型将在C++Primer 7.5.6节(第267页)和19.3节(第736页)介绍。
  尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。

  6.1.1节(第184页)将要提到,函数体内定义的变量一般来说并非存放在固定地址中, 因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同样是在6.1.1节(第185页)中还将提 到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量。

const和constexpr区别

  • 对于修饰对象来说,const并未区分出编译期常量和运行期常量,constexpr限定在了编译期常量
  • 在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。
const int*p=nullptr;  			//p是一个指向整型常量的指针
constexpr int*q=nullptr;		//q是一个指向整数的常量指针
constexpr const int*p3=nullptr; //p3是一个指向常量的常量指针

  p和q的类型相差甚远, p是一个指向常量的指针,而q是一个常量指针, 其中的关键在于constexpr把它所定义的对象置为了顶层const(参见2.4.3节, 第57页)。

热门相关:帝少的专属:小甜心,太缠人   人间欢喜   照见星星的她   刺客之王   情生意动