44.指针和const的用法
44.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修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突。
当以编译时初始化的方式定义一个const对象时,就如对bufSize的定义一样:
const int bufSize = 512;//输入缓冲区大小
编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。为了执行上述替换,编译器必须知道变量的初始值 。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义(参见C++Primer 2.2.2节,第41页)。为了支持这一用法, 同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中 定义const,而在其他多个文件中声明并使用它。
解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:
//file_l.c定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize =fen();
//file_l.h头文件
extern const int bufSize;//与file_l.c中定义的bufSize是同一个
假设我们有两个文件:main.cpp和helper.cpp。我们在main.cpp中定义了一个全局变量,并使用const关键字进行修饰:
```cpp
// main.cpp
const int MAX_VALUE = 100;
int main()
{
// 使用MAX_VALUE
return 0;
}
```
然后我们在helper.cpp中尝试引用这个变量:
```cpp
// helper.cpp
extern const int MAX_VALUE;
void helperFunction()
{
// 使用MAX_VALUE
}
```
这时候编译器会报错,提示MAX_VALUE未定义。这是因为const修饰的变量默认具有内部链接(internal linkage),只能在当前文件中使用,其他文件无法访问。因此,我们需要在helper.cpp中重新定义MAX_VALUE:
```cpp
// helper.cpp
const int MAX_VALUE = 100;
void helperFunction()
{
// 使用MAX_VALUE
}
```
这样就可以在helper.cpp中使用MAX_VALUE了。注意,这里重新定义的MAX_VALUE和main.cpp中定义的MAX_VALUE虽然名字相同,但是它们是两个不同的变量,它们的地址也不同。
const修饰变量的隐藏作用是只能在该文件中使用,其他文件无法引用声明使用。如果想在其他文件中使用这个const修饰的变量,可以通过将该变量的定义放在头文件中,然后在其他文件中包含该头文件来实现。
下面是一个例子:
// header.h
#ifndef HEADER_H
#define HEADER_H
const int MAX_VALUE = 100;
#endif // HEADER_H
// file1.cpp
#include "header.h"
int main()
{
const int value = MAX_VALUE;
// ...
}
// file2.cpp
#include "header.h"
int main()
{
int value = MAX_VALUE;
// ...
}
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 上,报错,不能将常量对象绑定到常量引用上。
绑定到另一种类型,并修改值
例子:
void func()
{
double i = 1.0;
const int& r1 = i;
i = 2.0;
cout << "i = " << i << endl;
cout << "r1 = " << r1 << endl;
double j = 1.0;
const double& r2 = j;
j = 2.0;
//r2 = 3.0;//err不能通过r2修改j的值
cout << "j = " << j << endl;
cout << "r2 = " << r2 << endl;
}
---------------------------------------
out:
i = 2
r1 = 1
j = 2
r2 = 2
上面的代码将 int 型的引用 r1 绑定到 double 型变量 i 上,然后改变 i 的值,我们发现 r1 并没有改变,它的值反而是绑定 i 时 i 的值。这是因为引用变量的类型与被引用对象的类型不同时,中间会有如下操作:
double i = 1.0;
int temp = i;
const int &r1 = temp;
r1 引用的是临时量 temp,而不是 i,所以才会出现上面的情况。
原文链接:https://blog.csdn.net/weixin_45773137/article/details/126297568
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;
二三行是一个意思,表示 p1和p2是指向常量的指针;第四行表示 p3是常量指针;第五行表示p4是指向常量的常量指针。
上面二三行的赋值同样发生了类型转换,从 int * 转换为 const int *。
常量指针和常量指针
指向常量的指针变量——const int* p;//p的内容(即值,也就是它指向的东西的地址)是可以改变的;*的左边是const int,表明p指向的东西是一个const的int,我们不能通过p来修改这个int,因为它是const的。
指向变量的指针常量——int* const p;
指向常量的指针常量——const int* const p;
void func()
{
const int i1 = 1;
int i2 = 2;
const int j1 = 3;
int j2 = 4;
const int k1 = 5;
int k2 = 6;
//const int *p和int const *p同样含义,原因:const修饰的是后面跟的整体
const int* p1 = &i1;//指向常量的指针变量 const后跟了*和p1,(*p1)不可修改(同时p1指向的变量也要求不能修改)
//i1 = 2;//err常量i1是常量不能通过自身修改
//*p1 = 2;//err p1指向的常量i1是常量不能通过自身修改
p1 = &i2;//p1的指向可以修改,可以将非const变量赋值给const修饰的变量(压缩非const的操作)
cout << *p1 << endl;
int* const p2 = &j2;//指向变量的指针常量
//p2 = j2;//错误
//p2 = k1;//错误 p2指向不可修改 const后跟的p2,p2值不可修改(指向不可改),(*p2)前没有const限定,可以修改
*p2 = 10;//可以通过p2修改p2所指向的变量的值(j2本身可以修改)
const int* const p31 = &k1; //指向常量的指针常量 指针值(指针指向)和指针指向的值都不可改变
const int* const p32 = &k2; //指向常量的指针常量
}
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;
}
此外,形参的底层 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");
调用setName时隐式传入 this 形参,通过改变 this->m_sName 的值来改变bozai对象的m_sName。
当调用getName时,同样是隐式传入 this 形参,不过此时的 this 被 const 修饰了,所以不能通过 this 修改对象的成员了。
原文链接: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页)。