现在的位置: 首页 > 综合 > 正文

Const 用法总结

2019年11月02日 ⁄ 综合 ⁄ 共 12457字 ⁄ 字号 评论关闭

    转载:http://blog.csdn.net/ychw365/article/details/6734894

  (1)首先const 定义的常量必须被初始化

         因为默认内部连接所以必须被初始化,其作用域为此文件,编译器经过类型检查后直接用100在编译时替换

        修饰符const可以用在类型说明符前,也可以用在类型说明符后

         例如:

         int const x=2;  或  const int x=2;
     class A;      const A a;  或     A const a;

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

         以上是常量集合,编译器会为其分配内存,所以不能在编译期间使用其中的值,例如:int temp[r[2]];这样的编译器会报告不能找到常量表达式

        类内部初始化

        初始化类内部的常量
        1 初始化列表:
         class A
         {
          public:
                A(int i=0):test(i) {}
          private:
                const int i;
          };
        2 外部初始化,例如:
         class A
         {
          public:
                A() {}
          private:
                static const int i;  
          };
         const int A::i=3; 

   (2)便于进行类型检查            

       例如:
             void f(const int i) { .........}
        编译器就会知道i是一个常量,不允许修改;
   (3)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。

    任何不修改成员数据的函数都应该声明为const函数,这样有助于提高程序的可读性和可靠性。
          还是上面的例子,如果在函数体内修改了i,编译器就会报错;
          例如: 
               void f(const int i) { i=10;//error! }

         类的成员函数:
    为了使成员函数的意义更加清楚,我们可在不改变对象的成员函数的函数原型中加上const说明:

  class Point

  {

  public:

  int GetX() const;

  int GetY() const;

  void SetPt (int, int);

  void OffsetPt (int, int);

  private:

  int xVal, yVal;

  };

  const成员函数应该在函数原型说明和函数定义中都增加const限定:

  int Point::GetY() const

  {

  return yVal;

  }

  class Set {

  public:

  Set (void){ card = 0; }

  bool Member(const int) const;

  void AddElem(const int);

  //...

  };

  bool Set::Member (const int elem) const

  {

  //...

  }

  非常量成员函数不能被常量成员对象调用,因为它可能企图修改常量的数据成员:

  const Set s;

  s.AddElem(10); // 非法: AddElem不是常量成员函数

  s.Member(10); // 正确

  *******但构造函数和析构函数对这个规则例外,它们从不定义为常量成员,但可被常量对象调用(被自动调用)。它们也能给常量的数据成员赋值,除非数据成员本身是常量。

        为了确保const对象的数据成员不会被改变,在C++中,const对象只能调用const成员函数。如果一个成员函数实际上没有对数据成员作任何形式的修改,但是它没有被const关键字限定的,也不能被常量对象调用。下面通过一个例子来说明这个问题:

           const Stock land =Stock("hyd");

   land.show();

   land 是常量,但是类成员函数show无法保证不修改land,所以编译器将拒绝执行该段代码。除非你能保证show像const函数一样,但这需要另外一种语法,即:

   void show() const;(声明)

   void Stock::show() const{}(定义)。

         指针 

            const指针可以接受const和非const地址,但是非const指针只能接受非const地址。所以const指针的能力更强一些,所以尽量多用const指针,这是一种习惯

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

     

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

       const int r=100;

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

 

        引用

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

        返回值

            const int Fun1(); 
            const MyClass Fun2();

         将const改为外部连接,作用于扩大至全局,编译时会分配内存,并且可以不进行初始化,仅仅作为声明,编译器认为在程序其他地方进行了定义
         extern const int i;     //正确的引用
         extern const int j=10;  //错误!常量不可以被再次赋值

          对于字符数组

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

   (4) 为函数重载提供了一个参考。
         class A
         {
           ......
           void f(int i)       {......} file://一个函数
           void f(int i) const {......} file://上一个函数的重载
            ......
          };
     (5) 可以节省空间,避免不必要的内存分配。
         例如:

           const与define。两者都可以用来定义常量,但是const定义时,定义了常量的类型,所以更精确一些。#define只是简单的文本替换,除了可以定义常量外,还可以用来定义一些简单的函数,有点类似内置函数。const和define定义的常量可以放在头文件里面。(小注:可以多次声明,但只能定义一次)
              #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定义的常量在内存中有若干个拷贝。
     (6) 提高了效率。
           编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

 

 

 推荐一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

  1). 并行设备的硬件寄存器(如:状态寄存器)

  2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

  3). 多线程应用中被几个任务共享的变量

      应用更偏向于底层 不赘述了

  1. // static_volatile.cpp : 定义控制台应用程序的入口点。  
  2. //  
  3.   
  4. #include "stdafx.h"  
  5. #include <iostream>  
  6. using namespace std;  
  7.   
  8. int square(volatile int *ptr)//volatile类型的数据是指该数据可能会变化。由于*ptr的值会意想不到的变化,导致求出来的不是平方值。  
  9. {  
  10.     return *ptr * *ptr;  
  11. }  
  12.   
  13. int square2(volatile int *ptr)//这样的话才是一定为平方值。  
  14. {  
  15.     int a = *ptr;  
  16.     return a*a;  
  17. }  
  18.   
  19. int _tmain(int argc, _TCHAR* argv[])  
  20. {  
  21.     int i = 10;  
  22.     const int k = 50;  
  23.     int j = 20;  
  24.     const int* a = &k;//指向常整型数的指针,指针的指向可以修改,但是整型数不能修改。  
  25.     a = &j;//yes:a可以指向别的地址。  
  26.     //*a = k;//error:“a”: 不能给常量赋值  
  27.     cout<<*a<<endl;//20  
  28.   
  29.     int *c = &j;//c是指向变量的指针,指针的值可以修改,整型数也可以修改。  
  30.     cout<<*c<<endl;//20  
  31.     *c = 5;//yes:可以给指向变量的指针c赋值。  
  32.     cout<<*c<<endl;//5  
  33.   
  34.     intconst b = &i;//指向整型数的常指针,指针的指向不可修改,但是整型能修改。  
  35.     //b = &j;//error:“b”: 不能给常量赋值  
  36.     cout<<*b<<endl;//10  
  37.     i = 30;  
  38.     cout<<*b<<endl;//30.当指针b所指向的整型i的值变化了,*b的值也随之变化。  
  39.       
  40.     int f = 3;  
  41.     volatile int *d = (volatile int *)(&f);  
  42.     cout<<square(d)<<endl;//该结果有可能会变。  
  43.   
  44.       
  45.     return 0;  
  46. }  

 

 [思考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)的返回值是对a的const引用,不能再将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内置类型的数组,不能被初始化

抱歉!评论已关闭.