【C++】超详细入门 —— 一文带你搞懂const限定符
const限定符的用途很广,普通变量、指针与引用、函数与函数参数、类成员变量成员函数都能用const修饰。虽然const能用的地方很多,但是大都万变不离其宗,它的作用也比较单一,今天博仔就带你来详细解析 const。
1、变量中的const
编写程序过程中,我们有时不希望改变某个变量的值。此时就可以使用关键字 const 对变量的类型加以限定。
1.1 普通变量
直接在普通变量类型声明符前加上 const,可以将声明为 const 类型:
const int a = 0;
这样就把 a 声明成了一个 const 类型的常量,所以我们不能再改变它的值了,所以下面试图改变 a 的语句将会编译报错:
a = 10;
修改局部变量的值:
但是如果 a 是局部变量,就可以通过指针来修改 a 的值:
const int a = 0; int *p = (int *)&a; *p = 10; cout << "a = " << a << endl; cout << "*p = " << *p << endl; cout << "p = " << p << endl; cout << "&p = " << &p << endl; ------------------------------------------------------ out: a = 0 *p = 10 p = 0x61ff0c &p = 0x61ff08
程序通过强制类型转换将 a 的地址转换为 int * 类型,并赋值给整型指针 p,然后通过 p 将 a 的值修改为 10。
程序正常运行,但是 a 的值和 p 的值并不相同,明明已经通过指针改变了地址中的内容,这是为什么呢?难道一个地址能存储两个值?当然不能。
这就是 C++ 中的*常量折叠** ,因为常量是在运行时初始化的,编译器对常量进行优化,直接将常量值放在编译器的符号表中,使用常量时直接从符号表中取出常量的值,省去了访存这一步骤。
a 是常量,编译器对 a 在预处理时就进行了替换。a 的地址中的值则被 p 所改变。从 a 的地址与 p 的地址可以看出,a 存储在栈中,所以能对其进行修改。
修改全局变量的值
通过指针修改位于静态存储区的的const变量,语法上没有报错,编译不会出错,一旦运行就会报告异常。因为全局变量存储于静态存储区,静态存储区中的常量只有读权限,不能修改它的值。
const volatile
在局部 const 变量的类型声明符前加上 volatile 关键字可以使用到该常量的地方不会使用对应符号表中的值,而会间接使用栈中的值。
const volatile int a = 0; int *p = (int *)&a; *p = 10; cout << "a = " << a << endl; cout << "*p = " << *p << endl; cout << "p = " << p << endl; cout << "&p = " << &p << endl; ------------------------------------------------- out: a = 10 *p = 10 p = 0x61ff0c &p = 0x61ff08
从上面代码中输出的结果就能看出,所有用到该常量的地方不会替换成了定义时所赋予的值,在运行的时候将会使用通过指针修改后的值。这样就避免了常量折叠的问题。
1.2 const 修饰引用
我们还可以对引用使用 const 限定符,在引用声明的类型声明符前加上 const 就可以声明对const的引用,常量引用不能用来修改它所绑定的对象。
引用绑定到同一种类型,并修改值
直接上例子:
int i = 0; const int j = 0; const int &r1 = i; r1 = 20; const int &r2 = j; r2 = 20; int &r3 = j;
第三行将非常量对象 i 绑定到 const 引用 r1 上,此过程中发生了隐式类型转换,i 的类型为 int,r1 的类型为 const int &, 所以这个过程 i 就从 int 转换为了 const int,所以不能通过 r1 改变 i 的值,但可以直接改变 i 的值。但是 const int 类型不能转换为 int。
:bell:可以这样想,一个普通变量,能被修改也可以不被修改,所以可以转换为const类型;一个const类型变量,不能被修改,所以不能转换为普通变量。
第五行将常量对象 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,不能指向其他变量。
1.4 顶层与底层const
任意常量对象为顶层const,包括常量指针;指向常量的指针和声明const的引用都为底层const。
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 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 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; } };
在上面成员函数的定义中,ok()的定义是合法的,error()的定义则非法。
值得注意的是,把一个成员函数声明为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 修改对象的成员了。
🎉到此 const 限定符的讲解就到此结束了,本文主要讲解了 const 的三个大的应用方面,整体来说还是比较详细的,如果有讲解不到位或有误的地方恳请大家批评与交流。
希望大家多多关注,三连支持。你们的支持是我源源不断创作的动力。