C++ 学习——const解析

Const 作用

1.   const类型定义:指明变量或对象的值是不能被更新,引入目的是为了取代预编译指令

2.   可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。

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

4.    可以节省空间,避免不必要的内存分配。

例如:

     #define PI 3.14159         file://常量宏

     const doulbe Pi=3.14159; file://此时并未将Pi放入ROM

        ......

     double i=Pi;             file://此时为Pi分配内存,以后不再分配!

     double I=PI;               file://编译期间进行宏替换,分配内存

     double j=Pi;               file://没有内存分配

     double J=PI;               file://再进行宏替换,又一次分配内存!

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。

对于基本声明

1. const int r=100; //标准const变量声明加初始化,因为默认内部连接所以必须被初始化,其作用域为此文件,编译器经过类型检查后直接用100在编译时替换

2. extend const int r=100; //const改为外部连接,作用于扩大至全局,编译时会分配内存,并且可以不进行初始化,仅仅作为声明,编译器认为在程序其他地方进行了定义

但是如果外部想链接r,不能这样用

extern const int r=10; //错误!常量不可以被再次赋值

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

struct S {int a,b;};

const S s[ ]={(1,2),(3.4)}; //以上两种都是常量集合,编译器会为其分配内存,所以不能在编译期间使用其中的值,例如:int temp[r[2]];这样的编译器会报告不能找到常量表达式

但是

const int Max=100;

int Array[Max];

正确。

还有

定义数组必须用常量,可以用const或者#define定义。 Static 虽然是编译时确定,也不能用来声明数组。

对于指针和引用

1.   const int *r=&x; //声明r为一个指向常量的x的指针,r指向的对象不能被修改,但他可以指向任何地址的常量

pointer const 可以指定普通变量,用改指针不能修改它指向的对象,并不表示指向的对象是const不能被改变,例如:

int i = 10;

const int * p = &i;

*p = 11; //wrong

i = 11 ; //correct

自己的一个经验:一个具体的概念可以用范型的概念来赋值,但是一个范型的概念不能用具体的概念来赋值。

我们可以把const指针当成普通指针的父类,因为普通指针改写了const属性,而具有比const指针更多的功能。 这样的话只有父类指针可以指向子类,而子类指针不能指向父类。

2.   int const *r=&x; //与用法1完全等价,没有任何区别

3.   int * const r=&x; //声明r为一个常量指针,他指向xr这个指针的指向不能被修改,但他指向的地址的内容可以修改

4. const int * const r=&x; //综合13用法,r是一个指向常量的常量型指针

5.   const double & v;      该引用所引用的对象不能被更新

引用必须定义是初始话,而且初始化后这个引用不能指向其他的对象。但是这里加的const声明不是这个意思,它是指不能改变v引用对象本身,也就是只能调用该对象里面的const成员函数。

对于类型检查

可以把一个非const对象赋给一个指向const的指针,因为有时候我们不想从这个指针来修改其对象的值;但是不可以把一个const对象赋值给一个非const指针,因为这样可能会通过这个指针改变指向对象的值,但也存在使这种操作通过的合法化写法,使用类型强制转换可以通过指针改变const对象:

const int r=100;

int * ptr = const_cast<int*>(&r); //C++标准,C语言使用:int * ptr =(int*)&r;

对于字符数组

char * name = “china”; 这样的语句,在编译时是能够通过的,但是”china”是常量字符数组,任何想修改他的操作也能通过编译但会引起运行时错误,如果我们想修改字符数组的话就要使用char name[ ] = “china”; 这种形式。

对于函数

1. void Fuction1 ( const int r ); //此处为参数传递const值,意义是变量初值不能被函数改变

2. const int Fuction1 (int); //此处返回const值,意思指返回的原函数里的变量的初值不能被修改,但是函数按值返回的这个变量被制成副本,能不能被修改就没有了意义,它可以被赋给任何的const或非const类型变量,完全不需要加上这个const关键字。但这只对于内部类型而言(因为内部类型返回的肯定是一个值,而不会返回一个变量,不会作为左值使用),对于用户自定义类型,返回值是常量是非常重要的,见下面条款3

3. Class CX; //内部有构造函数,声明如CX(int r =0)

CX Fuction1 () { return CX(); }

const CX Fuction2 () { return CX(); }

如有上面的自定义类CX,和函数Fuction1()Fuction2(),我们进行如下操作时:

Fuction1() = CX(1); //没有问题,可以作为左值调用

Fuction2() = CX(1); //编译错误,const返回值禁止作为左值调用。因为左值把返回值作为变量会修改其返回值,const声明禁止这种修改。

4.  函数中指针的const传递和返回:

int F1 (const char * pstr); //作为传递的时候使用const修饰可以保证不会通过这个指针来修改传递参数的初值,这里在函数内部任何修改*pstr的企图都会引起编译错误。

const char * F2 (); //意义是函数返回的指针指向的对象是一个const对象,它必须赋给一个同样是指向const对象的指针。

const char * const F3(); //比上面多了一个const,这个const的意义只是在他被用作左值时有效,它表明了这个指针除了指向const对象外,它本身也不能被修改,所以就不能当作左值来处理。

5.   函数中引用的const传递:

void F1 ( const X& px); //这样的一个const引用传递和最普通的函数按值传递的效果是一模一样的,他禁止对引用的对象的一切修改,唯一不同的是按值传递会先建立一个类对象的副本,然后传递过去,而它直接传递地址,所以这种传递比按值传递更有效。

**另外只有引用的const传递可以传递一个临时对象,因为临时对象都是const属性,且是不可见的,他短时间存在一个局部域中,所以不能使用指针,只有引用的const传递能够捕捉到这个家伙。

6. 有一点可以注意一下

    const为函数重载提供了一个参考。

         class A

         {

           ......

           void f(int i)       {......} file://一个函数

           void f(int i) const {......} file://上一个函数的重载

            ......

          };

        关于函数overloading 不能根据返回值类型来确定

        double max( int a, int b);

        int        max( int a, int b);

        也不能根据参数的默认值来判断

        int max( int a, int b);

        int max( int a, int b, int c=12);

        一句话不能让编译器有多个选择就ok

对于类

1.  首先,对于const的成员变量,只能在构造函数里使用初始化成员列表来初始化,试图在构造函数体内进行初始化const成员变量会引起编译错误。初始化成员列表形如:

X:: X ( int ir ): r(ir) {} //假设r是类Xconst成员变量

2. const成员函数。提到这个概念首先要谈到const对象,正象内置类型能够定义const对象一样(const int r=10;),用户自定义类型也可以定义const对象(const X px(10);),编译器要保证这个对象在其生命周期内不能够被改变。如果你定义了这样的一个const对象,那么对于这个对象的一切非const成员函数的调用,编译器为了保证对象的const特性,都会禁止并在编译期间报错。所以如果你想让你的成员函数能够在const对象上进行操作的话,就要把这个函数声明为const成员函数。假如f( )是类中的成员函数的话,它的声明形如:

int f( ) const; //const放在函数的最后,编译器会对这个函数进行检查,在这个函数中的任何试图改变成员变量和调用非const成员函数的操作都被视为非法

**类的构造和析构函数都不能是const函数。

3. 建立了一个const成员函数,但仍然想用这个函数改变对象内部的数据。这样的一个要求也会经常遇到,尤其是在一个苛刻的面试考官那里。首先我们要弄清楚考官的要求,因为有两种方法可以实现,如果这位考官要求不改变原来类的任何东西,只让你从当前这个const成员函数入手,那么你只有使用前面提到的类型强制转换方法。实例如下:

//假如有一个叫做X的类,它有一个int成员变量r,我们需要通过一个const成员函数f( )来对这个r进行++r操作,代码如下

void X::f( ) const

{ (const_cast<X*>(this)) -> ++r; } //通过this指针进行类型强制转换实现

另外一种方法就是使用关键字:mutable。如果你的成员变量在定义时是这个样子的:

mutable int r ;

那么它就告诉编译器这个成员变量可以通过const成员函数改变。编译器就不会再理会对他的检查了。

关于const一些问题

[思考1] 以下的这种赋值方法正确吗?

const A_class* c=new A_class();

A_class* e = c;

这种方法不正确,因为声明指针的目的是为了对其指向的内容进行改变,而声明的指针e指向的是一个常量,所以不正确;

[思考2] 以下的这种赋值方法正确吗?

A_class* const c = new A_class();

A_class* b = c;

这种方法正确,因为声明指针所指向的内容可变;

[思考3] 这样定义赋值操作符重载函数可以吗?

const A_class& operator=(const A_class& a);

不正确;在const A_class::operator=(const A_class& a)中,参数列表中的const的用法正确,而当这样连续赋值的时侯,问题就出现了:A_class a,b,c:(a=b)=c;因为a.operator=(b)的返回值是对aconst引用,不能再将c赋值给const常量。

几点值得讨论的地方:
1 const 究竟意味着什么?
说了这么多,你认为 const 意味着什么?一种修饰符?接口抽象?一种新类型?
也许都是,在 Stroustup 最初引入这个关键字时,只是为对象放入 ROM 做出了一种可能,对于 const 对象, C++ 既允许对其进行静态初始化,也允许对他进行动态初始化。理想的 const 对象应该在其构造函数完成之前都是可写的,在析够函数执行开始后也都是可写的,换句话说, const 对象具有从构造函数完成到析够函数执行之前的不变性,如果违反了这条规则,结果都是未定义的!虽然我们把 const 放入 ROM 中,但这并不能够保证 const 的任何形式的堕落,我们后面会给出具体的办法。无论 const 对象被放入 ROM 中,还是通过存储保护机制加以保护,都只能保证,对于用户而言这个对象没有改变。换句话说,废料收集器(我们以后会详细讨论,这就一笔带过)或数据库系统对一个 const 的修改怎没有任何问题。
2 )位元 const V.S. 抽象 const?
对于关键字 const 的解释有好几种方式,最常见的就是位元 const 抽象 const 。下面我们看一个例子:
        class A
        {
         public:
               ......
               A f(const A& a);
               ......
         };
如果采用抽象 const 进行解释,那就是 f 函数不会去改变所引用对象的抽象值,如果采用位元 const 进行解释,那就成了 f 函数不会去改变所引用对象的任何位元。
我们可以看到位元解释正是 c++ const 问题的定义, const 成员函数不被允许修改它所在对象的任何一个数据成员。
为什么这样呢?因为使用位元 const 2 个好处:
最大的好处是可以很容易地检测到违反位元 const 规定的事件:编译器只用去寻找有没有对数据成员的赋值就可以了。另外,如果我们采用了位元 const ,那么,对于一些比较简单的 const 对象,我们就可以把它安全的放入 ROM 中,对于一些程序而言,这无疑是一个很重要的优化方式。(关于优化处理,我们到时候专门进行讨论)
当然,位元 const 也有缺点,要不然,抽象 const 也就没有产生的必要了。
首先,位元 const 的抽象性比抽象 const 的级别更低!实际上,大家都知道,一个库接口的抽象性级别越低,使用这个库就越困难。
其次,使用位元 const 的库接口会暴露库的一些实现细节,而这往往会带来一些负面效应。所以,在库接口和程序实现细节上,我们都应该采用抽象 const
有时,我们可能希望对 const 做出一些其它的解释,那么,就要注意了,目前,大多数对 const 的解释都是类型不安全的,这里我们就不举例子了,你可以自己考虑一下,总之,我们尽量避免对 const 的重新解释。
3 )放在类内部的常量有什么限制?
看看下面这个例子:
    class A
    {
      private:
           const int c3 = 7;           // ???
          static int c4 = 7;          // ???
            static const float c5 = 7;  // ???
          ......
     };
你认为上面的 3 句对吗?呵呵,都不对!使用这种类内部的初始化语法的时候,常量必须是被一个常量表达式初始化的整型或枚举类型,而且必须是 static const 形式。这显然是一个很严重的限制!
那么,我们的标准委员会为什么做这样的规定呢?一般来说,类在一个头文件中被声明,而头文件被包含到许多互相调用的单元去。但是,为了避免复杂的编译器规则, C++ 要求每一个对象只有一个单独的定义。如果 C++ 允许在类内部定义一个和对象一样占据内存的实体的话,这种规则就被破坏了。
4 )如何初始化类内部的常量?
一种方法就是 static const 并用,在内部初始化,如上面的例子;
另一个很常见的方法就是初始化列表:
      class A
      {
          public:
                A(int i=0):test(i) {}
          private:
                const int i;
       }

      
还有一种方式就是在外部初始化,例如:
      class A
      {
          public:
                A() {}
          private:
                static const int i;  file://注
意必须是静态的!
       }

          const int A::i=3;
5 )常量与数组的组合有什么特殊吗?
 
我们给出下面的代码:
           const int size[3]={10,20,50};
           int array[size[2]];
有什么问题吗?对了,编译通不过!为什么呢?
const
可以用于集合,但编译器不能把一个集合存放在它的符号表里,所以必须分配内存。在这种情况下, const 意味着 不能改变的一块存储 。然而,其值在编译时不能被使用,因为编译器在编译时不需要知道存储的内容。自然,作为数组的大小就不行了:)
你再看看下面的例子:
       class A
       {
          public:
                A(int i=0):test[2]({1,2}) {} file://你
认为行吗?
          private:
                const int test[2];
        }

vc6
下编译通不过,为什么呢?
关于这个问题,前些时间, njboy 问我是怎么回事?我反问他: 你认为呢? 他 想了想,给出了一下解释,大家可以看看:我们知道编译器堆初始化列表的操作是在构造函数之内,显式调用可用代码之前,初始化的次序依据数据声明的次序。初 始化时机应该没有什么问题,那么就只有是编译器对数组做了什么手脚!其实做什么手脚,我也不知道,我只好对他进行猜测:编译器搜索到 test 发现是一个非静态的数组,于是,为他分配内存空间,这里需要注意了,它应该是一下分配完,并非先分配 test[0], 然后利用初始化列表初始化,再分配 test[1], 这就导致数组的初始化实际上是赋值!然而,常量不允许赋值,所以无法通过。
呵呵,看了这一段冠冕堂皇的话,真让我笑死了! njboy 别怪我揭你短呀:)我对此的解释是这样的: C++ 标准有一个规定,不允许无序对象在类内部初始化,数组显然是一个无序的,所以这样的初始化是错误的!对于他,只能在类的外部进行初始化,如果想让它通过,只需要声明为静态的,然后初始化。
这里我们看到,常量与数组的组合没有什么特殊!一切都是数组惹的祸!
6 this 指针是不是 const 类型的?
this
指针是一个很重要的概念,那该如何理解她呢?也许这个话题太大了,那我们缩小一些: this 指针是个什么类型的?这要看具体情况:如果在非 const 成员函数中, this 指针只是一个类类型的;如果在 const 成员函数中, this 指针是一个 const 类类型的;如果在 volatile 成员函数中 ,this 指针就是一个 volatile 类类型的。
7 const 到底是不是一个重载的参考对象?
       
先看一下下面的例子:
        class A
         {
                  ......
                void f(int i)       {......} file://一
个函数
                void f(int i) const {......} file://上
一个函数的重载
                ......
          };
       
上面是重载是没有问题的了,那么下面的呢?
         class A
         {
                ......
                void f(int i)       {......} file://一
个函数
                void f(const int i) {......} file://?????
                  ......
         };
这个是错误的,编译通不过。那么是不是说明内部参数的 const 不予重载呢?再看下面的例子:
        class A
         {
                ......
                void f(int& )       {......} file://一
个函数
                void f(const int& ) {......} file://?????
                ......
         };
这 个程序是正确的,看来上面的结论是错误的。为什么会这样呢?这要涉及到接口的透明度问题。按值传递时,对用户而言,这是透明的,用户不知道函数对形参做了 什么手脚,在这种情况下进行重载是没有意义的,所以规定不能重载!当指针或引用被引入时,用户就会对函数的操作有了一定的了解,不再是透明的了,这时重载 是有意义的,所以规定可以重载。
8 )什么情况下为 const 分配内存?
      
以下是我想到的可能情况,当然,有的编译器进行了优化,可能不分配内存。
        A
、作为非静态的类成员时;
        B
、用于集合时;
        C
、被取地址时;
        D
、在 main 函数体内部通过函数来获得值时;
        E
const class struct 有用户定义的构造函数、析构函数或基类时;。
        F
、当 const 的长度比计算机字长还长时;
        G
、参数中的 const
        H
、使用了 extern 时。
       
不知道还有没有其他情况,欢迎高手指点:)        
9 )临时变量到底是不是常量?
很多情况下,编译器必须建立临时对象。像其他任何对象一样,它们需要存储空间而且必须被构造和删除。区别是我们从来看不到编译器负责决定它们的去留以及它们存在的细节。对于 C++ 标准草案而言:临时对象自动地成为常量。因为我们通常接触不到临时对象,不能使用与之相关的信息,所以告诉临时对象做一些改变有可能会出错。当然,这与编译器有关,例如: vc6 vc7 都对此作了扩展,所以,用临时对象做左值,编译器并没有报错。
10 )与 static 搭配会不会有问题?
       
假设有一个类:
        class A
        {
         public:
                ......
                static void f() const { ......}
                ......
         };
 
我们发现编译器会报错,因为在这种情况下 static 不能够与 const 共存!
 
为什么呢?因为 static 没有 this 指针,但是 const 修饰 this 指针,所以 ...
11 )如何修改常量?
 
有时候我们却不得不对类内的数据进行修改,但是我们的接口却被声明了 const ,那该怎么处理呢?我对这个问题的看法如下:
  1
)标准用法: mutable
              class A
              {
               public:
                      A(int i=0):test(i)        { }
                      void SetValue(int i)const { test=i; }
               private:
                      mutable int test;   file://这
里处理!
               }
2 )强制转换: const_cast
               class A
               {
               public:
                      A(int i=0):test(i)        { }
                      void SetValue(int i)const
                      { const_cast <int>(test)=i; }//
这里处理!
               private:
                      int test;  
               }
3 )灵活的指针: int*
               class A
              {
               public:
                      A(int i=0):test(i)        { }
                      void SetValue(int i)const
                      { *test=i; }
               private:
                      int* test;   file://这
里处理!
               }

4
)未定义的处理
              class A
              {
               public:
                      A(int i=0):test(i)        { }
                      void SetValue(int i)const
                      { int *p=(int*)&test; *p=i; }//
这里处理!
               private:
                      int test;  
               }

               
注意,这里虽然说可以这样修改,但结果是未定义的,避免使用!
5
)内部处理: this 指针
              class A
              {
               public:
                      A(int i=0):test(i)        { }
                      void SetValue(int i)const
                      { ((A*)this)->test=i; }//
这里处理!
               private:
                      int test;  
               }

             6
)最另类的处理:空间布局
               class A
               {
                public:
                      A(int i=0):test(i),c('a') {  }
                private:
                      char c;
                      const int test;
                };
                int main()
                {
                    A a(3);
                    A* pa=&a;
                    char* p=(char*)pa;    
                    int*  pi=(int*)(p+4
); // 利用边缘调整
                    *pi=5;                 file://此
处改变了 test 的值!
                    return 0;
                 }
虽然我给出了 6 中方法,但是我只是想说明如何更改,但出了第一种用法之外,另外 5 种用法,我们并不提倡,不要因为我这么写了,你就这么用,否则,我真是要误人子弟了:)   
12 )最后我们来讨论一下常量对象的动态创建。
 
既然编译器可以动态初始化常量,就自然可以动态创建,例如:
  const int* pi=new const int(10);
这里要注意 2 点:
1
const 对象必须被初始化!所以 (10) 是不能够少的。
2
new 返回的指针必须是 const 类型的。
那么我们可不可以动态创建一个数组呢?答案是否定的,因为 new 内置类型的数组,不能被初始化。
这里我们忽视了数组是类类型的,同样对于类内部数组初始化我们也做出了这样的忽视,因为这涉及到数组的问题,我们以后再讨论。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值