原创 C++之常量(一)

1概述

一个C++程序就是一系列数据与操做的集合。当一个C++程序开始运行的时候,与该程序相关的数据就会被加载到内存中。当数据与内存发生关联的时候,这些数据就会具备以下的特性:ios

  • 数据在内存中的地址。这个地址决定了数据在内存中的存储位置。在32位的系统中,每个C++程序都具备4GB大小的内存地址空间,这个4GB大小的内存空间又被划分为若干个区域,如:栈区,堆区,全局(静态)区,文字常量区,以及程序代码区。不一样内存地址的数据将会被存储在不一样的内存区域中;
  • 数据在内存中的值。若是该值可变,那么该数据就是变量;若是该值不可变,那么该数据就是常量;
  • 数据的类型。数据的类型决定了数据占用内存的多少。如:Int型占4个字节,Short型占两个字节;
  • 内存分配的时间点。有的数据在编译时由编译器分配内存,这些数据是:全局变量/常量,文字常量,静态变量。在编译阶段,编译器就为这些数据分配了内存,当程序运行的时候,这些数据就会被直接加载到已分配的内存位置上;而有的数据只能在运行时由系统分配内存,如:函数内部的局部变量,以及使用new操做符分配的变量。这些数据的内存地址在程序运行时刻才能肯定。

在编码阶段,程序员能够定义各类常量和变量;在编译阶段,编译器会将程序员定义的变量和常量放到不一样的数据段中;在程序运行阶段,这些变量和常量又会被加载到不一样的内存区域。如下,将从编码阶段,编译阶段,程序运行阶段,三个阶段来讲明变量/常量在内存,数据段中的分布状况。程序员

在C++的编码阶段,程序员能够定义各类做用域或生命周期的变量或常量,如:全局变量/常量,静态变量/常量,局部变量/常量,以及使用new操做符分配的变量或常量。数组

在C++的编译阶段,编译器将在编译时刻可以肯定内存地址的变量或常量放到不一样的数据段中。在编译后的可执行文件中,通常会包含以下的数据段:函数

  • .text段,该数据段存放程序代码;
  • .bss段,该数据段存放未初始化的全局变量或静态变量;
  • .data段,该数据段存放已初始化的全局变量或静态变量;
  • .rdata段,该数据段存放文字常量,以及在全局做用域中定义的字符常量。

在C++程序的执行阶段,程序代码中定义的变量和常量会被加载到不一样的内存区域。如:全局变量,静态变量加载到全局区,文字常量加载到文字常量区。编码

具体状况以下表所示:spa

建立时刻线程

程序代码指针

可执行文件对象

内存blog

备注

编译时刻

已初始化的全局变量,静态变量

.data段

全局区(初始化)

可读,可写

未初始化的全局变量,静态变量

.bss段

全局区(未初始化)

可读,可写

文字常量

.rdata段

文字常量区

只读

全局符号常量

.rdata段

文字常量区

只读

运行时刻

局部自动变量,符号常量

 

栈区

属于线程

New操做符分配的变量,常量

 

堆区

属于进程

 

2常量的分类

2.1文字常量

当一个数值,字符或者字符串,例如 1,’A’,”Test Const”等,出如今程序代码中的时候,它们被称为文字常量。称之为“文字”是由于咱们只能以值的形式表述它们;称之为“常量”是由于它们的值不能被改变。文字常量出如今C++代码中,通常为其余变量赋初始值。具体的示例代码以下:

Int a = 10;//10为整型文字常量。在编译时,数值类型文字常量被看成当即数处理。从汇编指令中能够看到这个状况

     int a = 10;

001F153D  mov         dword ptr [ebp-14h],A  //10被当当即数处理。直接出如今指令中

Double pi = 3.1415;//3.1415为浮点型文字常量。

Char b = ‘a’;//a为字符型文字常量。在初始化的时候,从文字常量区将a拷贝到变量b所在的内存地址处。

Char* strName = “wolf”;//wolf为字符串型文字常量。指针strName指向文字常量区中字符串wolf的首地址。

Char[] arrName = “wolf”;//wolf为字符串型文字常量。数组初始化的时候,从文字常量区将wolf拷贝到数组所在的地址中。若是arrName在局部定义,那么就拷贝到栈中,若是在全局做用域定义,那么就拷贝到内存的全局区。

//wolf在文字常量区只有一份拷贝。

在C++代码中,直接以值的形式出现的各类类型的数据都属于文字常量。在编译阶段,编译器为字符型文字常量分配内存地址,将数值型文字常量处理成当即数;在编译后的可执行文件(PE文件)中,文字常量被放到.rdata数据段中;在程序运行阶段,文字常量被加载到内存的文字常量区。在具体使用的时候,将会从文字常量区将文字常量的值拷贝到使用文字常量的变量的内存地址处,如:Char[] arrName = “wolf”;或者变量的指针直接指向文字常量区中文字常量的位置,如:char * strName = “wolf”。若是在代码中存在多个相同的文字常量,那么在存储的时候,在文字常量区只保持该文字常量的一份拷贝,使用该文字常量的多个变量将今后文字常量处取值。

按照数据类型的不一样,文字常量能够划分为数值常量,字符常量,以及字符串常量。其中,数值常量又包括整型常量和浮点型常量,字符常量又包括普通字符常量和转义字符常量。具体分类结构以下图所示:

                       

字符常量用单引号表示,字符串常量用双引号表示。如:’a’表示字符常量,而”a”则表示字符串常量。

2.2符号常量

在C++代码中,经常用一个符号名称来表明一个常量,这个符号名称被称为符号常量。一般有两种方式定义符号常量,一种方式是使用关键字const,另一种方式是使用宏定义。

使用关键字const定义符号常量的格式以下:

Const常量的数据类型 符号常量名 = 文字常量;

举例:

Const double PI = 3.14;//定义double类型的符号常量。PI是符号常量,3.14是文字常量

Const char* strName = “wolf”;

使用宏定义方式定义符号常量的格式以下:

#define 符号常量名文字常量。

#define PI 3.14   //定义符号常量PI,从这个语句中区分不出数据类型。

#define strName“wolf”

在实际的编码过程当中,建议尽量多地使用关键字const定义符号常量,而不是使用宏定义的方式。

使用关键字const定义符号常量之后,必需要进行初始化工做,而且其值在随后的代码中不能被改变。C++有两种方式来保证符号常量的常量性。一种方法是使用内存区域的只读属性来保证,这种方法适用于全局常量,若是该符号常量的值发生变化,系统会报错;另一种方法是在编译阶段,由编译器利用常量规则来保证,这种方法适用于局部常量。在编译器编译的时候,若是在代码中发现更改符号常量值的状况,编译器就会报错。可是当代码编译经过之后,在运行阶段,能够采用一些特殊的方法改变该符号常量的值,而且系统不会报错。

若是对符号常量执行取地址操做,或者对符号常量使用关键字extern,那么在编译的时候,编译器将为符号常量分配内存;不然,在编译的时候,符号常量的名称只被放到符号列表中,不执行内存分配,在使用符号常量的地方执行常量折叠。

能够在各类做用域中定义符号常量。

3符号常量与做用域的关系

3.1常量折叠

若是符号常量的定义在当前文件可见,那么在编译的时候,编译器将执行常量折叠。在使用符号常量的代码处,编译器会用符号常量的实际值去替换符号常量。若是符号常量的定义在当前文件不可见,将没法进行常量折叠。具体代码举例以下:

----------------------------------------------A.h-------------------------------------------------------

Extern const int fa;//声明符号常量。该符号常量具备全局做用域

--------------------------------A.cpp---------------------------------------

#include"A.h"

constint fa = 3;//定义符号常量。

--------------------------------------------main.cpp-------------------------------------------------

#include<iostream>

#include"A.h"//在main.cpp文件中,只知道符号常量fa的声明,不知道它的定义

 

usingnamespace std;

 

int main()

{

constint a = 10;//定义局部常量。在栈中为该符号常量分配了内存。

 

int b = a + 2;//符号常量定义可见,在编译时刻执行了常量折叠。

 

int c = fa + 2;//符号常量不可见,没有执行常量折叠。

 

constint * pa = &a;//执行取常量地址操做。该符号常量的常量性由编译器在编译阶段保证。

  *(const_cast<int*>(pa)) = 100;//取消指针的常量性,改变常量的值。能够这么干,彻底没有问题

 

//也能够采用下面的方式更改常量的值。

//int* c = (int*)pa;

//*c = 100;

 

   cout << *pa <<endl;

   cout << a <<endl;//符号常量定义可见,在编译时刻执行了常量折叠。

   cout << b <<endl;

 

 

int k;

   cin >> k;

}

 

----------------------------------------汇编代码------------------------------------

  const int a = 10;

002C13EE  mov         dword ptr [a],0Ah     //在栈中为a分配了内存,并将10存储在该内存处

 

  int b = a + 2;//符号常量定义可见,在编译时刻执行了常量折叠,已经计算出10+2的值。所以下面汇编代码中,只给出了当即数0Ch,也就是十进制的数值12。

002C13F5  mov         dword ptr [b],0Ch

 

  int c = fa + 2;//没有进行常量折叠,只能在运行时计算结果。

002C13FC  mov         eax,dword ptr [a (2C5800h)]

002C1401  add         eax,2

002C1404  mov         dword ptr [c],eax

 

 

//在现实中,通常不多出现定义了符号常量,而后又改变其值的蛋疼操做。这里只是说明问题而已。将常量值的改变与常量折叠进行对比,更能深刻理解常量折叠这个操做。

 

上面的代码执行完毕之后,输出的结果是:100,10,12。在上面的代码中,*pa输出了100,那是由于在程序运行阶段,咱们改变了符号常量的值;a输出10,是由于在编译阶段,编译器执行了常量折叠,用10替换了a;代码cout<< a <<endl; 变成了cout<< 10 <<endl;b输出12也是这个道理。

在局部做用域中定义的符号常量,在运行时刻能够改变它的值。由于它的内存被分配到栈上,它的常量性是在编译阶段由编译器经过规则保证的。在运行阶段,编译器就没有办法了。

对于在全局做用域中定义的符号常量,如上面代码中的fa,在运行时刻没法改变其值。由于在全局做用域中定义的符号常量的内存地址被分配到了文字常量区,该区具备只读属性。全局做用域中的符号常量的常量性是经过内存的只读属性来控制的。

 

3.2符号常量与文件做用域的关系

头文件是用来声明而不是定义函数和变量的,而源文件则是用来实现变量和函数定义的。头文件中通常包含类的定义,枚举的定义,符号常量的定义,内联函数的定义,extern变量的声明,函数的声明,typedef声明。若是将变量定义到头文件中,那么当其余文件引入该头文件的时候,就会认为该对头文件中的变量又执行了一次定义。根据一次定义法则,这是不容许的。可是也有三个例外,类,内联函数,在编译时值能够肯定的符号常量是能够在头文件中定义的。

符号常量默认具备文件做用域,因此符号常量能够定义到头文件中,而后被其余源文件引用。在多个源文件中,能够存在同名的符号常量,它们是独立的个体,互不影响。具体代码举例以下:

------------------------------A.h------------------------------------

Const double PI = 3.14;//定义符号常量,虽然该符号常量被定义在全局做用域中,但它默认只具备文件做用域。在其余文件中是没法访问该符号常量的。

----------------------------B.cpp------------------------------------

#include “A.h”//引入头文件,在编译阶段,头文件A.h和源文件B.cpp被合并为一个文件,在头文件A.h中定义的符号常量PI,至关于在新合并后的源文件中被定义。

Double GetMJ(double r)

{

Return r * r * PI;//使用常量,由于符号常量的定义在当前文件可见,因此编译器用3.14替换符号常量PI。具体的代码就变成了“return r*r*3.14;”。这个替换的过程叫常量折叠

}

----------------------------C.cpp------------------------------------------

#include “A.h”//引入头文件,至关于在C.cpp源文件中又定义了一次符号常量PI。可是符号常量默认具备文件做用域,B.cpp源文件中的符号常量PI与C.cpp源文件中的PI互不影响。

Double GetZC(double r)

{

Return 2 * PI * r;//编译时执行常量折叠。

}

在文件做用域中定义的符号常量,通常其值明确,因此编译器在编译阶段执行了常量折叠。而且,没有为该符号常量分配内存,只是将该符号常量的名称保存到了符号列表中而已。符号列表是编译技术中的一个概念。

3.3符号常量与全局做用域的关系

符号常量默认具备文件做用域。当在编译时刻符号常量的值已知的状况下,编译器只是将符号常量的名称放到符号列表中,并不对该符号常量分配内存。在使用该符号常量的代码处,编译器执行了常量折叠。

若是对符号常量使用了关键字extern,那么该符号常量就具有了全局做用域。编译器在编译阶段将为该符号常量分配内存。

若是符号常量的值在编译阶段未知,那么也须要对该符号常量使用关键字extern。在这两种状况下,编译器没法进行常量折叠。

具体代码举例以下:

-----------------------------------A.h----------------------------------

externconstint a;//在全局做用域中声明符号常量,使用了关键字extern,因此该符号常量具备全局做用域

-------------------------A.cpp------------------------

#include"A.h"

constint a = 10;//在此处定义符号常量,并初始化其值为10.编译器为该符号常量分配内存。

------------------------main.cpp------------------------

 

#include<iostream>

usingnamespace std;

 

#include"A.h"//引用了头文件,至关于声明了一次符号常量,符号常量的真正定义在A.cpp中。Main.cpp不知道该符号常量的定义。

int main()

{

int b = 5 * a;//这里没有进行常量折叠。在运行时,从符号常量a的内存地址处取得其值。具体状况见汇编代码。

Int c = 5*10;

 

   cout << a <<endl;

   cout << b <<endl;

 

int k;

   cin >> k;

}

--------------------------汇编代码--------------------------

   int b = 5 * a;

0104360E  mov         eax,dword ptr [a (1045800h)]      //将a值从内存地址1045800h出取出,放到eax中。

01043613  imul        eax,eax,5                        //执行乘法操做。若是执行常量折叠的话,这里根本不会执行乘法操做。

01043616  mov         dword ptr [b],eax               //将乘法结果放到b中。

 

 

   int c = 5*10;

00AF3619  mov         dword ptr [c],32h               // 编译阶段已经计算完毕,这里直接给出当即数

 

从上面的示例能够看出,关键字extern使符号常量具有了全局做用域,而且为该符号常量分配了内存地址,同时,在编译的时候,编译器不为该符号常量进行常量折叠。由于该符号常量的定义在当前文件不可见。

因此,在使用符号常量的时候,若是该符号常量的值在编译时刻已知,那么就将该符号常量处理成具备文件做用域便可。这样,在编译阶段,编译器可以为该符号常量进行了常量折叠,从而提升了执行阶段的效率。

3.4符号常量与类域的关系

能够在类中定义符号常量,该符号常量做为该类的数据成员出现。对于每一个类对象来讲,符号常量的值是固定的;可是,对于整个类的类型来讲,符号常量的值是可变的。

在具体操做的时候,能够将符号常量定义在实现该类定义的头文件中,可是,符号常量的初始化操做必须在该类构造函数的初始化列表中实现。

具体代码以下:

---------------------------------------A.h---------------------------------------

#ifndef _MyClass_H

#define _MyClass_H

 

class MyClass

{

public:

     MyClass(int Para);

 

private:

     constint m_conVal;//定义常量

 

};

 

#endif

 

--------------------------------A.cpp------------------------------

#include"B.h"

 

MyClass::MyClass(int Para):m_conVal(Para)//初始化常量

{

 

}

由于常量是在类的构造函数中被初始化,因此对于每个类对象来讲,常量的值是固定的;对于整个类的类型来讲,在每次初始化类对象的时候,向构造函数中传递的参数可能不一样,所以,常量的值也会不一样。

若是想要创建在整个类中都恒定的常量,应该用类中的枚举常量来实现。具体示例代码以下:

Class myClass

{

Enum { size1 = 100,size2 = 200,size3 = 300};

Int arr1[size1];

Int arr2[size2];

 

};

枚举常量不会占用对象的存储空间,他们在编译时被所有求值。可是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。

3.5符号常量与局部做用域的关系

能够在局部做用域中定义符号常量,在运行时刻,该符号常量在栈中被分配内存。该符号常量的常量性由编译器在编译时刻保证,若是在代码中发现有改变该符号常量的值的语句,那么编译器就会报错。

若是符号常量的定义在当前文件是可见的,那么在编译阶段,编译器将会执行常量折叠;若是符号常量的定义在当前文件不可见,那么将会在运行时刻,从符号常量的存储位置取得该值。

程序员能够改变运行时刻局部符号常量的值。具体代码举例以下:

---------------------------------main.cpp------------------------------------------

#include<iostream>

usingnamespace std;

 

int main()

{

constint a = 10;

 

int b = 10;

 

int c = a * 5;

int d = b * 5;

 

int k;

   cin >> k;

}

-----------------------------------汇编代码-------------------------------------

00D713EC  rep stos    dword ptr es:[edi]

  const int a = 10;  //为a分配了内存

00D713EE  mov         dword ptr [a],0Ah

 

  int b = 10;       //为b分配了内存

00D713F5  mov         dword ptr [b],0Ah

 

  int c = a * 5;    //执行了常量折叠,汇编代码中只给出了当即数32h,也就是十进制的50

00D713FC  mov         dword ptr [c],32h

  int d = b * 5;  //对于变量,执行了计算操做。

00D71403  mov         eax,dword ptr [b]

00D71406  imul        eax,eax,5

00D71409  mov         dword ptr [d],eax

相关文章
相关标签/搜索