C++ const关键字的总结(全局/局部变量、修饰指针和引用、成员函数和数据成员、修饰类对象、const与宏定义的区别、Static与Const的区别)
const关键字
const关键字
1、什么是const
2、使用原理
2.1、const全局/局部变量
2.2、cosnt修饰指针和引用
2.3、const修饰函数参数
2.4、const修饰函数返回值
2.5、const成员函数和数据成员
2.6、const修饰类对象
3、const_cast的知识
4、const与宏定义的区别
5、Static与Const的区别
参考
const关键字
1、什么是const
const是一个C++语言的限定符,它限定一个变量不允许被改变。使用const在一定程度上可以提高程序的安全性和可靠性。
只要一个变量前用const来修饰,就意味着该变量里的数据只能被访问,而不能被修改,也就是意味着const“只读”(readonly)
const 是 C++ 中的关键字,它会在编译期间(时机很重要),告诉编译器这个对象是不能被修改的。
规则:
const离谁近,谁就不能被修改;
const修饰一个变量时,一定要给这个变量初始化,若不初始化,在后面也不能初始化。
const作用:
1:可以用来定义常量,修饰函数参数,修饰函数返回值,且被const修饰的东西,都受到强制保护,可以预防其它代码无意识的进行修改,从而提高了程序的健壮性(是指系统对于规范要求以外的输入能够判断这个输入不符合规范要求,并能有合理的处理方式。ps:即所谓高手写的程序不容易死);
2:使编译器保护那些不希望被修改的参数,防止无意代码的修改,减少bug;
3:给读代码的人传递有用的信息,声明一个参数,是为了告诉用户这个参数的应用目的;
const优点:
1:编译器可以对const进行类型安全检查(所谓的类型安全检查,能将程序集间彼此隔离开来,这种隔离能确保程序集彼此间不会产生负面影响,提高程序的可读性);
2:有些集成化的调试工具可以对const常量进行调试,使编译器对处理内容有了更多的了解,消除了一些隐患。
eg:void hanshu(const int i){.......} 编译器就会知道i是一个不允许被修改的常量
1
3:可以节省空间,避免不必要的内存分配,因为编译器通常不为const常量分配内存空间,而是将它保存在符号表中,这样就没有了存储于读内存的操作,使效率也得以提高;
4:可以很方便的进行参数的修改和调整,同时避免意义模糊的数字出现;
2、使用原理
分类如下:
常变量: const 类型说明符 变量名
常引用: const 类型说明符 &引用名
常对象: 类名 const 对象名
常成员函数: 类名::fun(形参) const
常数组: 类型说明符 const 数组名[大小]
常指针: const 类型说明符* 指针名 ,类型说明符* const 指针名
1
2
3
4
5
6
7
8
9
10
11
首先提示的是:在常变量(const 类型说明符 变量名)、常引用(const 类型说明符 &引用名)、常对象(类名 const 对象名)、 常数组(类型说明符 const 数组名[大小]), const” 与 “类型说明符”或“类名”(其实类名是一种自定义的类型说明符) 的位置可以互换。如:
const int a=5; 与 int const a=5; 等同
类名 const 对象名 与 const 类名 对象名 等同
1
2
3
2.1、const全局/局部变量
const全局变量
在文件a.cpp中定义了一个全局变量a
int a = 1;
1
在文件test.cpp中使用全局变量a
#include <iostream>
using namespace std;
extern int a;
int main()
{
//const volatile int a = 7;
int *p = (int *)(&a);
*p = 8;
cout << "a=" << a << endl;
cout << "*p=" << *p;
system("pause");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
结果为:
a = 8
*p = 8
1
2
如果将全局变量a定义为const
const int a = 1;
1
#include <iostream>
using namespace std;
extern const int a;
int main()
{
//const volatile int a = 7;
int *p = (int *)(&a);
*p = 8;
cout << "a=" << a << endl;
cout << "*p=" << *p;
system("pause");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
这里可以看出const在修饰全局变量时第一个作用,会限定全局变量的作用范围到其定义时所在的编译单元。
const全局变量使得我们指定了一个语义约束,即被修饰的全局变量不允许被修改,而编译器会强制实施这个约束。
#include <iostream>
using namespace std;
const int a = 7;
int main()
{
//const volatile int a = 7;
int *p = (int *)(&a);
*p = 8;
cout << "a=" << a << endl;
cout << "*p=" << *p;
system("pause");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
运行这段代码,会发现编译器报异常。编译器不允许对const全局变量的改动。
const局部变量
#include <iostream>
using namespace std;
int main()
{
//const volatile int a = 7;
const int a = 7;
int *p = (int *)(&a);
*p = 8;
cout << "a=" << a << endl;
cout << "*p=" << *p;
system("pause");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = 7
*p = 8
1
2
运行结果显示const局部变量被修改了,但是在使用变量名输出时,编译器会出现一种类似宏定义的功能一样的行为,将变量名替换为初始值。可见,const局部变量并不能做到真正的不变,而是编译器对其进行了一些优化行为,这导致了const局部变量与真实值产生了不一致。(常量折叠现象)
那么,如果想获取修改后的const局部变量真实值,该怎么办呢?答案是使用volatile关键字。
#include <iostream>
using namespace std;
int main()
{
const volatile int a = 7;
//const int a = 7;
int *p = (int *)(&a);
*p = 8;
cout << "a=" << a << endl;
cout << "*p=" << *p;
system("pause");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = 8
*p = 8
1
2
volatile关键字使得程序每次直接去内存中读取变量值而不是读寄存器值,这个作用在解决一些不是程序而是由于别的原因修改了变量值时非常有用。
2.2、cosnt修饰指针和引用
cosnt修饰指针
const修饰指针,涉及到两个很重要的概念,顶层const和底层cosnt
指针自身是一个对象,它的值为一个整数,表明指向对象的内存地址。因此指针长度所指向对象类型无关,在32位系统下为4字节,64位系统下为8字节。进而,指针本身是否是常量以及所指向的对象是否是常量就是两个独立的问题。
从 const 指针开始说起。const int* pInt; 和 int *const pInt = &someInt;,前者是 *pInt 不能改变,而后者是 pInt 不能改变。因此指针本身是不是常量和指针所指向的对象是不是常量就是两个互相独立的问题。用顶层表示指针本身是个常量,底层表示指针所指向的对象是个常量。
更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用;底层 const 则与指针和引用等复合类型有关,比较特殊的是,指针类型既可以是顶层 const 也可以是底层 const 或者二者兼备。
int a = 1;
int b = 2;
const int* p1 = &a;
int* const p2 = &a;
1.指针常量(指针不可改,指针指向的对象可改)
int a = 10;
int b = 5;
int * const p1 = &a;
p1 = &b; //指针不可改,不合法
*p1 = b; //指针指向的对象可改,合法
2.常量指针(指针可改,指针指向的对象不可改)
int a = 10;
int b = 5;
const int* p2 = &a;
p2 = &b; //指针可改, 合法
*p2 = b; //不合法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
拷贝与顶层和底层 const
int i = 0;
int *const p1 = &i; // 不能改变 p1 的值,这是一个顶层
const int ci = 42; // 不能改变 ci 的值,这是一个顶层
const int *p2 = &ci; // 允许改变 p2 的值,这是一个底层
const int *const p3 = p2; // 靠右的 const 是顶层 const,靠左的是底层 const
const int &r = ci; // 所有的引用本身都是顶层 const,因为引用一旦初始化就不能再改为其他对象的引用,这里用于声明引用的 const 都是底层 const
1
2
3
4
5
6
当执行对象的拷贝操作时,常量是顶层const还是底层const的区别明显。其中,顶层 const 不受什么影响。
i = ci; // 正确:拷贝 ci 的值给 i,ci 是一个顶层 const,对此操作无影响。
p2 = p3; // 正确:p2 和 p3 指向的对象相同,p3 顶层 const 的部分不影响。
1
2
与此相对的,底层 const 的限制却不能被忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换,一般来说,非常量可以转化为常量,反之不行。
int *p = p3; // 错误:p3 包含底层 const 的定义,而p没有。假设成功,p 就可以改变 p3 指向的对象的值。
p2 = p3; // 正确:p2 和 p3 都是底层 const
p2 = &i; // 正确:int* 能够转化为 const int*,这也是形参是底层const的函数形参传递外部非 const 指针的基础。
int &r = ci; // 错误:普通 int& 不能绑定到 int 常量中。
const int &r2 = i; // 正确:const int& 可以绑定到一个普通 int 上。
1
2
3
4
5
cosnt修饰引用
常引用所引用的对象不能更新,使用方法为:const 类型说明符 &引用名。
非const引用只能绑定非const对象,const引用可以绑定任意对象,并且都当做常对象。
常引用经常用作形参,防止函数内对象被意外修改。对于在函数中不会修改其值的参数,最好都声明为常引用。复制构造函数的参数一般均为常引用。
仍然是上面那个例子:
class Example{
public:
Example(int x, int y):a(x),b(y){}
Example(const Example &e):a(e.a),b(e.b){} //复制构造函数
void print();
void print() const;
private:
const int a,b;
static const int c = 10;
};
void Example::print() {cout<<"print():"<<a<<ends<<b<<endl;}
void Example::print() const {cout<<"print() const:"<<a<<ends<<b<<endl;}
1
2
3
4
5
6
7
8
9
10
11
12
2.3、const修饰函数参数
const修饰参数是为了防止函数体内可能会修改参数原始对象。因此,有三种情况可讨论:
1、函数参数为值传递:值传递(pass-by-value)是传递一份参数的拷贝给函数,因此不论函数体代码如何运行,也只会修改拷贝而无法修改原始对象,这种情况不需要将参数声明为const。
2、函数参数为指针:指针传递(pass-by-pointer)只会进行浅拷贝,拷贝一份指针给函数,而不会拷贝一份原始对象。因此,给指针参数加上顶层const可以防止指针指向被篡改,加上底层const可以防止指向对象被篡改。
3、函数参数为引用:引用传递(pass-by-reference)有一个很重要的作用,由于引用就是对象的一个别名,因此不需要拷贝对象,减小了开销。这同时也导致可以通过修改引用直接修改原始对象(毕竟引用和原始对象其实是同一个东西),因此,大多数时候,推荐函数参数设置为pass-by-reference-to-const。给引用加上底层const,既可以减小拷贝开销,又可以防止修改底层所引用的对象。
void Fun( const A *in); //修饰指针型传入参数
void Fun(const A &in); //修饰引用型传入参数
void func (const int& n)
{
n = 10; // 编译错误
}
1
2
3
4
5
6
7
8
2.4、const修饰函数返回值
令函数返回一个常量,可以有效防止因用户错误造成的意外。
if (a*b = c)
1
如果a,b,c都是如同int的内置类型,编译器会直接报错
因为对于内置类型的*操作返回的不是一个左值,因此不能放在=的左边。为什么会出现这种情况呢?可能用户只是想比较是否相等,却打字打漏了一个等号(ORZ)。因此,对于很多自定义类型的函数,应该尽量与内置类型兼容,在应该返回右值的函数返回那里应该加上const。
if (a*b == c)
1
const修饰函数返回值的含义和用const修饰普通变量以及指针的含义基本相同。这样可以防止外部对 object 的内部成员进行修改。
const int* func() // 返回的指针所指向的内容不能修改
{
// return p;
}
1
2
3
4
2.5、const成员函数和数据成员
类的常成员函数
由于C++会保护const对象不被更新,为了防止类的对象出现意外更新,禁止const对象调用类的非常成员函数。因此,常成员函数为常对象的唯一对外接口。
常成员函数的声明方式:类型说明符 函数名(参数表) const
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;
const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。
class A
{
public:
int& getValue() const
{
// a = 10; // 错误
return a;
}
private:
int a; // 非const成员变量
};
1
2
3
4
5
6
7
8
9
10
11
12
有如下几个要点:
常成员函数的定义和声明都要含有const关键字;
一个函数是否含有const关键字可以作为重载函数,const对象默认调用const函数,非const对象默认调用非const函数,如果没有非const函数,也可以调用const函数;
const函数中不能更新目的对象的任何成员(mutable修饰的变量除外,这里不展开阐述),以此方法来保证const对象不被修改。
如果const成员函数想修改成员变量值,可以用mutable修饰目标成员变量。
类的常数据成员
类的数据成员不能在任何函数中被赋值或修改,但必须在构造函数中使用初始化列表的方式赋初值。
举个例子,刚才的类如果a, b为常数据成员,则应该改写为如下形式:
class Example{
public:
Example(int x, int y):a(x),b(y){} //初始化列表方式赋初值
void print();
void print() const;
private:
const int a,b;
};
void Example::print() {cout<<"print():"<<a<<ends<<b<<endl;}
void Example::print() const {cout<<"print() const:"<<a<<ends<<b<<endl;}
1
2
3
4
5
6
7
8
9
10
如果为静态常数据成员,由于不属于具体对象,所以不能在构造函数里赋值,仍然应该在类外赋值。特别地,如果静态常量为整数或枚举类型,C++允许在类内定义时指定常量值。
比如以下两种方式均合法:
class Example{
public:
Example(int x, int y):a(x),b(y){}
void print();
void print() const;
private:
const int a,b;
static const int c = 10; //静态常量
};
1
2
3
4
5
6
7
8
9
class Example{
public:
Example(int x, int y):a(x),b(y){}
void print();
void print() const;
private:
const int a,b;
static const int c; //静态常量
};
const int Example::c = 10;
1
2
3
4
5
6
7
8
9
10
2.6、const修饰类对象
用const修饰的类对象,该对象内的任何成员变量都不能被修改。
因此不能调用该对象的任何非const成员函数,因为对非const成员函数的调用会有修改成员变量的企图。
class A
{
public:
void funcA() {}
void funcB() const {}
};
int main
{
const A a;
a.funcB(); // 可以
a.funcA(); // 错误
const A* b = new A();
b->funcB(); // 可以
b->funcA(); // 错误
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在类内重载成员函数
class A
{
public:
void func() {}
void func() const {} // 重载
};
1
2
3
4
5
6
3、const_cast的知识
const_cast运算符用来修改类型的const或volatile属性。
一、常量指针被转化成非常量的指针,并且仍然指向原来的对象;
二、常量引用被转换成非常量的引用,并且仍然指向原来的对象。
简单来说,const_cast可以将常指针或常引用的const属性去除,注意:const_cast不用来将常对象转换为普通对象。只是为调用他的对象提供接口:
举个例子:
const int a = 4;
const int * p = &a;
int * m = const_cast<int*>(p);
* m = 5;
cout<<" a:"<<a<<ends<<&a<<endl;
cout<<"*p:"<<*p<<ends<<p<<endl;
cout<<"*m:"<<*m<<ends<<m<<endl;
1
2
3
4
5
6
7
输出结果:
a:4 0x61ff04
*p:5 0x61ff04
*m:5 0x61ff04
1
2
3
4
可见,const_cast并没用真正修改a中的值,只是修改了m和p指针中的值,并且它们指向的是同一块空间。
const_cast的一个安全用法是,在类中的一个非常成员函数中,调用重载的常成员函数,以此来实现代码重用。
再举个例子:
const int & get_element(int n) const{
if(n>=0 && n <len){
throw "invalid memory visit";
}
return element[n];
}
int & get_element(int n){
return const_cast<int &>(
static_cast<const Array *>(this)->get_element(n)
);
}
1
2
3
4
5
6
7
8
9
10
11
这是一个类中的两个同名函数,第一个函数仅供const对象调用,非const的函数为了重用const函数的代码,需要先将当前对象的指针转化为const类型,以此调用const函数,然后再将返回的结果用const_cast去除const属性,返回的引用就是可修改的了。
4、const与宏定义的区别
(1) 编译器处理方式不同
define宏是在预处理阶段展开。
const常量是编译运行阶段使用。
(2) 类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。
const常量有具体的类型,在编译阶段会执行类型检查。
(3) 存储方式不同
define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
const常量会在内存中分配(可以是堆中也可以是栈中)。
(4)const 可以节省空间,避免不必要的内存分配。 例如:
#define PI 3.14159 //常量宏
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ......
double i=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存!
1
2
3
4
5
6
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而 #define定义的常量在内存中有若干个拷贝。
(5) 提高了效率。
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
5、Static与Const的区别
static
1、static局部变量 将一个变量声明为函数的局部变量,那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中
2、static 全局变量 表示一个变量在当前文件的全局内可访问
3、static 函数 表示一个函数只能在当前文件中被访问
4、static 类成员变量 表示这个成员为全类所共有
5、static 类成员函数 表示这个函数为全类所共有,而且只能访问静态成员变量
static关键字的作用:
(1)函数体内static变量的作用范围为该函数体,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
(2)在模块内的static全局变量和函数可以被模块内的函数访问,但不能被模块外其它函数访问;
(3)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(4)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
const关键字的作用
(1)阻止一个变量被改变
(2)声明常量指针和指针常量
(3)const修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为”左值”。
参考
1、https://blog.csdn.net/u011333734/article/details/81294043
2、https://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777416.html
3、https://www.cnblogs.com/jiabei521/p/3335676.html
4、https://www.jianshu.com/p/a346cb1ca104
5、https://www.jianshu.com/p/5c35cc218bb4
————————————————
版权声明:本文为CSDN博主「JMW1407」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/JMW1407/article/details/108443185
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律