首页 > 编程 > C# > 正文

C++中const的实现细节介绍(C,C#同理)

2020-01-24 02:53:36
字体:
来源:转载
供稿:网友

1、什么是const? 
常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的。(当然,我们可以偷梁换柱进行更新:) 

2、为什么引入const? 
const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点。 

3、cons有什么主要的作用? 

(1)可以定义const常量,具有不可变性。 例如: 
const int Max=100; int Array[Max]; 

(2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。例如: void f(const int i) { .........} 编译器就会知道i是一个常量,不允许修改; (3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。 同宏定义一样,可以做到不变则已,一变都变!如(1)中,如果想修改Max的内容,只需要:const int Max=you want;即可! 

(4)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。 还是上面的例子,如果在函数体内修改了i,编译器就会报错; 例如: 
void f(const int i) { i=10;//error! } 

(5) 为函数重载提供了一个参考。 
class A { ...... 
void f(int i) {......} //一个函数 
void f(int i) const {......} //上一个函数的重载 ...... 
}; 

(6) 可以节省空间,避免不必要的内存分配。 例如: 
#define PI 3.14159 //常量宏 
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ...... 
double i=Pi; //此时为Pi分配内存,以后不再分配! 
double I=PI; //编译期间进行宏替换,分配内存 
double j=Pi; //没有内存分配 
double J=PI; //再进行宏替换,又一次分配内存! 
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。 

(7) 提高了效率。 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。 

4、如何使用const? 

(1)修饰一般常量 一般常量是指简单类型的常量。这种常量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后。 例如: 
int const x=2; 或 const int x=2; 

(2)修饰常数组 定义或说明一个常数组可采用如下格式:  
int const a[5]={1, 2, 3, 4, 5};  
const int a[5]={1, 2, 3, 4, 5}; 

(3)修饰常对象 常对象是指对象常量,定义格式如下: 
class A; const A a; 
A const a; 定义常对象时,同样要进行初始化,并且该对象不能再被更新,修饰符const可以放在类名后面,也可以放在类名前面。  

(4)修饰常指针 
const int *A; //const修饰指向的对象,A可变,A指向的对象不可变 
int const *A; //const修饰指向的对象,A可变,A指向的对象不可变 
int *const A; //const修饰指针A, A不可变,A指向的对象可变 
const int *const A;//指针A和A指向的对象都不可变 

(5)修饰常引用 使用const修饰符也可以说明引用,被说明的引用为常引用,该引用所引用的对象不能被更新。其定义格式如下:  
const double & v;  

(6)修饰函数的常参数 const修饰符也可以修饰函数的传递参数,格式如下: 
void Fun(const int Var); 告诉编译器Var在函数体中的无法改变,从而防止了使用者的一些无意的或错误的修改。 

(7)修饰函数的返回值: const修饰符也可以修饰函数的返回值,是返回值不可被改变,格式如下: 
const int Fun1(); const MyClass Fun2(); 

(8)修饰类的成员函数: const修饰符也可以修饰类的成员函数,格式如下: 
class ClassName { 
public:  
int Fun() const; ..... 
};
这样,在调用函数Fun时就不能修改类里面的数据 

(9)在另一连接文件中引用const常量 
extern const int i;//正确的引用 
extern const int j=10;//错误!常量不可以被再次赋值 另外,还要注意,常量必须初始化! 例如: const int i=5; 


所谓C++编译器,C++编译器是C++中的一个与标准化高度兼容的编译环境,编译器对不同的CPU会进行不同的优化,下面说明C++编译器进行Const常量分配存储空间的说明介绍。

Const 是C++中常用的类型修饰符,有某些微妙的应用场合,如果没有搞清本源,则错误在所难免。本篇中将对const进行辨析。溯其本源,究其实质,希望能对大家理解const有所帮助,根据思维的承接关系,分为如下几个部分进行阐述。C++的提出者当初是基于什么样的目的引入(或者说保留)const关键字呢?,这是一个有趣又有益的话题,对理解const很有帮助。

1. 大家知道,C++有一个类型严格的编译系统,这使得C++程序的错误在编译阶段即可发现许多,从而使得出错率大为减少,因此,也成为了C++与C相比,有着突出优点的一个方面。

2. C++中很常见的预处理指令 #define VariableName VariableValue 可以很方便地进行值替代,这种值替代至少在三个方面优点突出:一是避免了意义模糊的数字出现,使得程序语义流畅清晰。

二是可以很方便地进行参数的调整与修改,如上例,当人数由107变为201时,进改动此处即可,三是提高了程序的执行效率,由于使用了预编译器进行值替代,并不需要为这些常量分配存储空间,所以执行的效率较高。鉴于以上的优点,这种预定义指令的使用在程序中随处可见。

3. 说到这里,大家可能会迷惑上述的1点、2点与const有什么关系呢?,好,请接着向下看来:

预处理语句虽然有以上的许多优点,但它有个比较致命的缺点,即,预处理语句仅仅只是简单值替代,缺乏类型的检测机制。这样预处理语句就不能享受C++严格类型检查的好处,从而可能成为引发一系列错误的隐患。

4.好了,第一阶段结论出来了:

结论: Const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点。

现在它的形式变成了:

Const DataType VariableName = VariableValue ;为什么const能很好地取代预定义语句?const 到底有什么大神通,使它可以振臂一挥取代预定义语句呢?

1. 首先,以const 修饰的常量值,具有不可变性,这是它能取代预定义语句的基础。

2. 第二,很明显,它也同样可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。

3. 第三,C++的编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高,同时,这也是它取代预定义语句的重要基础。

这里,我要提一下,为什么说这一点是也是它能取代预定义语句的基础,这是因为,编译器不会去读存储的内容,如果编译器为const分配了存储空间,它就不能够成为一个编译期间的常量了。

4. 最后,const定义也像一个普通的变量定义一样,它会由编译器对它进行类型的检测,消除了预定义语句的隐患。

我们也许学习过const的使用,但是对于const的细致的技术细节却不一定掌握。const的用法在许多的教材上只是简单的介绍,在这里我们对 const进行细致的概念以及用法剖析。const 是由c++采用,并加进标准c中,但是他们的意义完全不同,在旧版本(标准前)的c中,如果想建立一个常量,必须使用预处理器:
#define PI 3.14159

此后无论在何处使用PI,都会被预处理器以3.14159替代。编译器不对PI进行类型检查,也就是说可以不受限制的建立宏并用它来替代值,如果使用不慎,很可能由预处理引入错误,这些错误往往很难发现。

我们也不能得到PI的地址(即不能向PI传递指针和引用)。
c++引入了命名常量的概念,命名常量就像变量一样,只是它的值不能改变,如果试图改变一个const 对象,编译器将会产生错误。 const 和正常变量一样有作用域,所以函数内部的const也不会影响程序的其余部分。在c++中const可以取代预处理器#define来进行值替代, const有安全的类型检查,所以不用担心会像预处理器一样引入错误。

在通常的情况下const同预处理器#define一样只是将所赋值保存入编译器的符号表中(符号表仅仅在编译时存在,在编译过程中编译器将程序中的名字与之在符号表中定义的数值作简单的替换),在使用的时候进行值替换,并不为const创建存储空间。我们将const的定义放进头文件里,这样通过包含头文件,可以把const定义单独放在一个地方并把它分配给一个编译单元,const默认为内部连接(内部连接意味着只对正在编译的文件创建存储空间,别的文件可以使用相同的标示符和全局变量,编译器不会发现冲突,外部连接意味着为所有被编译过的文件创建一片单独的存储空间,一般全局变量和函数名的外部连接通过extern声明,可以通过其他的文件访问)也就是说const仅能被它所定义过的文件访问,在定义一个const时,必须赋一个值给它,除非用extern做出说明:

extern const int a;

这表示const的定义在其他的什么地方,这里仅仅是一个声明,但是这样的做法使const使用了外部连接,也就是说上面的extern强制进行了对const的存储空间分配,这样我们就无法再用const作为常量折叠(在可能的情况下,符号常量的值会代替改名字的出现,这个替代过程叫做常量折叠)使用了,即使我们在其他地方定义了const的值,如:

extern const int a=3;

因为const的值被放入了存储单元,在编译的过程中,编译器不会去读存储单元的内容。如果我们这样做:

int b[a];

编译器就会给我们一个错误信息。

想不为const分配存储空间是不可能的,因为对于复杂的结构,例如集合,编译器不会复杂到将集合保存到它的符号表中,所以必须分配内存空间,这就意味着“这是一块不能改变的存储空间”,当然也就不能在编译期间使用它的值,因为编译器不知道存储的内容:

const int i[]={1,2,3,4};

//float f[i[2]];
//将得到错误信息,编译器提示不能在数组定义里找到一个常数表达式。

因为编译器靠移动栈指针来存储和读取数据。
也因此,由于无法避免为const分配内存,所以const的定义必须默认为内部连接,否则由于众多的const在多个文件中分配内存,就会引起错误。下面我们看一段简单有效的代码来说明const的常量折叠:

复制代码 代码如下:

#include <iostream.h>
const int a=3;
const int b=a+1;
float *f=(float*)&b;
char c[b+3];
void main()
{
const char gc=cin.get();
const char c2=gc+3;
}

我们可以看到,a是一个编译器期间的const,b是从a中计算出来的,由于a是一个const,b的计算值来自一个常数表达式,而它自身也是一个编译器间的const,接着下面指针f取得了b的地址,所以迫使编译器给b分配了存储空间,不过即使分配了存储空间,由于编译器已经知道了b的值,所以仍然不妨碍在决定数组c的大小时使用b。

在主函数main()里,标识符gc的值在编译期间是不知道的,这也意味着需要存储空间,但是初始化要在定义点进行,而且一旦初始化,其值就不能改变,我们发现c2是由gc计算出来的,它的作用域与其他类型const的作用域是一样的,这是对#define用法的一种改进。

在c++引进常量的时候,标准c也引入了const,但是在c中const的意思和在c++中有很大不同,在c中const的意思是“一个不能改变的普通变量”,const常量总是被分配存储空间而且它的名字是全局符即const使用外部连接。于是在c中:

const int size=100;
char c[size];

得出一个错误。但是在c中可以这样写:

const int size;

因为c中的const被默认为外部连接,所以这样做是合理的。
在c语言中使用限定符const不是很有用,如果希望在常数表达式里(必须在编译期间被求值)使用一个已命名的值,必须使用预处理器#define。

在c++中可以使指针成为const,这很有用,如果以后想在程序代码中改变const这种指针的使用,编译器将给出通知,这样大大提高了安全性。在用带有const的指针时,我们有两种选择:const修饰指针指向的对象,或者const修饰指针自己指向的存储空间。

如果要使指向的对象不发生改变,则需要这样写:

const int *p;

这里p是一个指向const int 的指针,它不需要初始化,因为p可以指向任何标识符,它自己并不是一个const,但是它所指的值是不能改变的,同样的,我们可以这样写:

int const *p;

这两种方法是等同的,依据个人习惯以及编码风格不同,程序员自己决定使用哪一种形式。
如果希望使指针成为一个const必须将const标明的部分放在*右边。

int a=3;
int *const j=&a

编译器要求给它一个初始值,这个值在指针的生命期间内不变,也就是说指针始终指向a的地址,不过要改变它地址中的值是可以的:

*j+=4;

也可以是一个const指针指向一个const对象:
const int *j1=&a;
int const *j2=&a;

这样指针和对象都不能改变,这两种形式同样是等同的。在赋值的的时候需要注意,我们可以将一个非const的对象地址赋给一个const指针,但是不能将一个const对象地址赋给一个非const指针,因为这样可能通过被赋值的指针改变对象的值,当然也可以用类型的强制转换来进行const对象的赋值,但是这样做打破了const提供的安全性。

const也被用于限定函数参数和函数的返回值,如果函数参数是按值传递时,即表示变量的初值不会被函数改变,如果函数的返回值为const那么对于内部类型来说按值返回的是否是一个cosnt是无关紧要的,编译器不让它成为一个左值,因为它是一个值而不是一个变量,所以使用const是多余的,例如:
const int f(){return 1;}
void main(){int a=f();}

但是当处理用户定义类型的时候,按值返回常量就很有意义了,这时候函数的返回值不能被直接赋值也不能被修改。仅仅是非const返回值能作为一个左值使用,但是这往往失去意义,因为函数返回值在使用时通常保存为一个临时量,临时量被作为左值使用并修改后,编译器将临时量清除。结果丢失了所有的修改。
可以用const限定传递或返回一个地址(即一个指针或一个引用):

复制代码 代码如下:

const int * const func(const int *p)
{ static int a=*p;
return &a;
}

参数内的const限定指针p指向的数据不能被改变,此后p的值被赋给静态变量a,然后将a的地址返回,这里a是一个静态变量,在函数运行结束后,它的生命期并没有结束,所以可以将它的地址返回。因为函数返回一个const int* 型,所以函数func的返回值不可以赋给一个非指向const的指针,但它同时接受一个const int * const和一个const int *指针,这是因为在函数返回时产生一个const临时指针用以存放a的地址,所以自动产生了这种原始变量不能被改变的约定,于是*右边的const只有当作左值使用时才有意义。

const同样运用于类中,但是它的意义又有所不同,我们可以创建const的数据成员,const的成员函数,甚至是const的对象,但是保持类的对象为const比较复杂,所以const对象只能调用const成员函数。

const的数据成员在类的每一个对象中分配存储,并且一旦初始化这个值在对象的生命期内是一个常量,因此在类中建立一个const数据成员时,初始化工作必须在构造函数初始化列表中。如果我们希望创建一个有编译期间的常量成员,这就需要在该常量成员的前面使用static限定符,这样所有的对象都仅有一个实例:

复制代码 代码如下:

class X
{
static const int size=50;
int a[size];
public:
X();
};

const对象只能调用const成员函数,一个普通对象同样可以调用const成员函数,因此,const成员函数更具有一般性,但是成员函数不会默认为const。声明一个const成员函数,需要将const限定符放在函数名的后面:

void f (void ) const;

当我们运用const成员函数时,遇到需要改变数据成员,可以用mutable进行特别的指定:

复制代码 代码如下:

class X
{
mutable int i;
public:
X();
void nochange() const;
};

void X::nochange const(){i++;}


const消除了预处理器的值替代的不良影响,并且提供了良好的类型检查形式和安全性,在可能的地方尽可能的使用const对我们的编程有很大的帮助。

小结:

const int i=10;//这个类似宏替换,也就是说,它优化之后可能是放一个符号表里面。所有使用i的地方都用10代替,但是当你对i取址后,没办法,编译器必须为i在常量区找个地方安身。这就是所谓的常量折叠.

内容取自互联网与《C++编程思想》

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表