目录
一、const的定义
1、const概念
- const是C++中的一个限定符,它限定一个变量不允许被改变,使用const在一定程度上可以提高程序的安全性和可靠性;
- const是C++中的关键字,在编译期间告诉编译器被修饰对象是不能被修改的,只能被访问,也就意味着const “只读”。
☆注意:
- const离谁近,谁就不能被修改;
- const修饰一个变量时,一定要初始化该变量,若不进行初始化,由于其无法被改变,后面也就不能对其进行初始化。
2、const的作用
- 可以用来定义常量,修饰函数参数,修饰函数返回值,且被const修饰的东西都受到强制保护,可以预防其它代码无意识地进行修改,提高程序的健壮性;
- 使编译器保护那些不希望被修改的参数;
- 给读代码的人传递有用的信息,声明一个参数,是为了告诉用户这个参数的应用目的。
3、const的优点
- 编译器可以对const进行类型安全检查;
- 有些集成化的调试工具可以对const常量进行调试,使编译器对处理内容有更多的了解,消除一些隐患;
- 介绍那个空间,避免不必要的内存分配,因为编译器通常不为const常量分配内存空间,而是将其保护在符号表中;
- 可以很方便地进行参数的修改和调整,同时避免模糊的数字出现。
☆符号通常是变量或函数的名称;
☆符号表则是程序或单元中符号的列表,可用于确定变量或函数在内存中的位置,简而言之,符号表就是分配给变量和函数的名称与其在内存中的地址的映射,包括类型、范围和大小等数据,最终由编译器使用。
二、const使用原理
const 类型说明符 变量名;//1.常变量
const 类型说明符 &引用名;//2.常引用
类名 const 对象名;//3.常对象
类名::fun(形参)const;//4.常成员函数
类型说明符 const 数组名[大小];//5.常数组
const 类型说明符 *指针名, 类型说明符* const 指针名;//6.常指针
值得注意的是:在1、2、3、5(常变量、常引用、常对象、常数组)中,const和“类型说明符”的位置可以互换,例如:
const 类型说明符 变量名;//1.常变量
等同于
类型说明符 const 变量名;//常对象
const int a = 5;
等同于
int const a = 5;
1、const+全局/局部变量
1. const修饰全局变量
在文件a.cpp中定义了一个全局变量a:
int a = 1;
在文件test.cpp中使用全局变量a:
#include<iostram>
using namespace std;
extern int a;
int main(){
int *p = (int*)(&a);//将地址&a转换为整型指针int*并赋值给指针变量P
*p = 8;//将指针变量p指向的内存位置的内容修改为8,p中放的是a的内存地址,所以此步本质上是将a的值变为8
cout<<"a="<<a<<endl;
cout<<"*p="<<*p<<endl;
system("pause");
return 0;
}
程序输出结果为:
a=8
*p=8
如果将全局变量a定义为const:
const int a = 1;
#include<iostram>
using namespace std;
extern const int a;
int main(){
int *p = (int*)(&a);
*p = 8;
cout<<"a="<<a<<endl;
cout<<"*p="<<*p<<endl;
system("pause");
return 0;
}
此时程序就会报错,因为a被const修饰,其值无法被改变。
2. const修饰局部变量
#include<iostram>
using namespace std;
int main(){
const int a = 7;//a作为局部变量
int *p = (int*)(&a);
*p = 8;
cout<<"a="<<a<<endl;
cout<<"*p="<<*p<<endl;
system("pause");
return 0;
}
程序运行结果为:
a=7
*p=8
使用volatile关键字可以获取修改后的const局部变量的真实值:
#include<iostram>
using namespace std;
int main(){
const volatile int a = 7;//a作为局部变量
int *p = (int*)(&a);
*p = 8;
cout<<"a="<<a<<endl;
cout<<"*p="<<*p<<endl;
system("pause");
return 0;
}
程序运行结果为:
a=8
*p=8
volatile关键字使得程序每次直接去内存中读取变量而不是读寄存器值,在解决一些不是程序而是由于别的原因修改了变量值时非常有用。
2、const修饰指针/引用
1. const修饰指针
指针自身就是一个对象,它的值为一个整数,表明对象的内存地址,因此指针长度和所指向对象类型无关,在32位系统下,指针长度为4个字节,在64位系统下,指针长度为8个字节。指针本身是否为常量和其所指向的对象是否为常量是两个独立的问题。
在const修饰指针时,涉及到两个重要的概念:顶层const和底层const。
const int* pInt;//*pInt不能改变
int* const pInt = &someInt;//pInt不能改变
☆巧记:const离谁近,谁不能变
用顶层表示指针本身是个常量,用底层表示指针指向的对象是个常量。更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用;底层const则与指针和引用等复合类型相关。比较特殊的是,指针类型既可以是顶层const,也可以是底层const,或者二者兼备。
int a = 1;
int b = 2;
const int* p1 = &a;
int* const p2 = &b;
//指针常量(指针不可改变,指针指向的对象可以改变)
int a = 10;
int b = 5;
int* const p1 = &a;
p1 = &b;//不合法,因为p1不可以改变
*p1 = b;//合法,指针指向的对象可以改变
//常量指针(指针可以改变,指针指向的对象不可以改变)
int a = 10;
int b = 5;
const int* p2 = &b;
p2 = &a;//合法,指针可以改变
*p2 = a;//不合法,指针指向的对象不可以改变
拷贝中的顶层const和底层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
进行拷贝时,常量是顶层const,不受影响:
i = ci; // 正确:拷贝 ci 的值给 i,ci 是一个顶层 const,对此操作无影响。
p2 = p3; // 正确:p2 和 p3 指向的对象相同,p3 顶层 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 上。
2. 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<<endl<<b<<endl;
}
void Example::print() const {
cout<<"print() const:"<<a<<endl<<b<<endl;
}
3、const修饰函数参数
作用: const修饰函数参数是为了防止函数体内可能会修改参数原始对象
分三种情况进行讨论:
1、 函数参数为值传递
值传递(pass-by-value)是传递一份参数的拷贝给函数,因此不论函数体代码如何运行,也只会修改拷贝而无法修改原始对象,这种情况不需要将参数声明为const;
void pass_by_value(int num){
num++;
}
int main(){
int original = 8;
pass_by_value(original);
cout<<"original="<<original<<endl;
return 0;
}
程序输出结果为:
original=8
2、 函数参数为指针
指针传递(pass-by-pointer)只会进行浅拷贝,而不会拷贝一份原始对象。因此,给指针参数加上顶层const可以防止指针本身被篡改,加上底层const可以防止指针指向对象被篡改。
3、 函数参数为引用
引用传递(pass-by-reference)有一个很重要的作用,由于引用是对象的一个别名,因此不需要拷贝对象,减小了开销。这同时可以通过修改引用直接修改原始对象,因此,大多数时候,推荐函数参数设置为pass-by-reference-to-const,给引用加上底层const,既可以减小开销,又可以防止修改底层所引用的对象。
4、const修饰函数返回值
令函数返回一个常量,可以有效防止因用户错误造成的意外。const修饰函数返回值的含义与const修饰普通常量以及指针的含义基本相同,可以防止外部对object的内部成员进行修改。
5、const成员函数和数据成员
1、const的成员函数
由于C++会保护const对象不被改变,为了防止类的对象出现意外更新,禁止const对象调用类的非常成员函数,因此,常成员函数成为类对象的唯一对外接口。
- const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
- const对象的成员是不可以修改的,而通过指针维护的对象是可以修改的;
- const成员韩式不可以修改对象的数据,不管对象是否具有const性质,编译时以是否修改成员数据为依据进行检查。
class A{
public:
int& getvalue() const{
a = 10;//会报错,因为getvalue()是个const成员函数,不能修改成员变量的数据
return a;
private:
int a;//非const成员变量
}
};
要点如下:
- 常成员函数的定义和声明都要含有const关键字;
- 一个函数含有const关键字可以作为重载函数,const对象默认调用const函数,非const对象默认调用非const函数,如果没有非const函数,也可以调用const函数;
- const函数中不能更新目的对象的任何成员(mutable修饰的变量除外),以此方法来保证const对象不被修改;
- 如果const成员函数想修改成员变量值,可以用mutable修饰目标变量。
2、 const的数据成员
类的数据成员不能在任何函数中被赋值和修改,但必须在构造函数中使用初始化列表的方式赋初值。
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<<endl<<b<<endl;
}
void Example::print() const{
cout<<"print() const:"<<a<<endl<<b<<endl;
}
如果为静态常数据成员,由于不属于具体对象,所以不能在构造函数里赋值,仍然应该在类外进行赋值。
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;//在类外给静态常量c赋值
特别地,如果静态常量是整型或者枚举类型,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;//c为整型,所以可以在定义时就给静态常量赋值
};
6、const修饰类对象
用const修饰的类对象,该对象内的任何成员变量都不能被修改。因此,不能调用该对象的任何非const成员函数,因为对非const成员函数的调用会有修噶成员变量的企图。
class A{
public:
void funcA(){}//非const成员函数
void funcA() const{}//重载成员函数
void funcB() const{}//const成员函数
};
int main(){
const A, a;//类A为const
a.funcA();//错误,不能调用非const成员函数
a.funcB();//正确
const A* b = new A();//定义一个指向const A*的指针b,并动态创建了一个新的对象A
b->funcA();错误
b->funcB();正确
}
二、const与宏定义的区别
- 编译器处理方式不同:
define宏是在预处理阶段展开;
const常量是在编译运行阶段使用。 - 类型和安全检查不同:
define宏没有类型,不做任何类型检查,仅仅是展开;
const常量有具体的类型,在编译阶段会执行类型检查。 - 存储方式不同:
define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存;
const常量会在内存中分配内存(可以是栈中也可以是堆中)。 - const可以节省空间,避免不必要的内存分配,例如:
#define PI 3.14159//常亮宏
const double Pi = 3.14159;// 此时并未将Pi放入存储器ROM中
double i = Pi;//此时为Pi分配内存,以后便不再分配
double I = PI;//编译期间进行宏替换,分配内存
double j = Pi;//不进行内存分配
double J = PI;//再次进行宏替换,再一次分配内存
const定义常量从汇编的角度看,只是给出了相应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
常量在C++中的定义就是一个顶层const加上数据类型,且常量的定义必须初始化。对于局部对象,常量存放在栈区;对于全局对象,常量存放在全局/静态存储区;对于字面值常量,其存放在常量存储区。
- 效率不同:
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储和读内存的操作,提升了效率。
三、const与static的区别
- static局部变量将一个变量声明为函数的局部变量,其在这个函数执行之后不会被释放,而是继续保留在内存中;
- static全局变量表示一个变量在当前文件的全局内可访问;
- static函数表示一个函数只能在当前文件中可以被访问;
- static类成员变量标志这个成员为全类所共有;
- static类成员函数表示这个函数为全类所共有,而且只能访问静态成员变量。