• const 和 static 的作用


    static的作用:

    1.先来介绍它的第一条也是最重要的一条:隐藏

         当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。为理解这句话,我举例来说明。我们要同时编译两个源文件,一个是a.c,另一个是main.c.

    下面是a.c的内容:

     char a = 'A'; // global variable
                void msg()
                {
                printf("Hello\n");
                }
                

    下面是main.c的内容:

     int main(void)
                {
                extern char a; // extern variable must be declared before use
                printf("%c ", a);
                (void)msg();
                return 0;
                }
                

    程序的运行结果是:

    A Hello

         你可能会问:为什么在a.c中定义的全局变量a和函数msg能在main.c中使用?前面说过,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。此例中,a是全局变量,msg是函数,并且都没有加static前缀,因此对于另外的源文件main.c是可见的。

    如果加了static,就会对其它源文件隐藏。例如在a和msg的定义前加上static,main.c就看不到它们了。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。Static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏,而对于变量,static还有下面两个作用。

    2. static的第二个作用是保持变量内容的持久

         存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。虽然这种用法不常见,但我还是举一个例子。

     #include <stdio.h>
                int fun(void){
                static int count = 10; // 事实上此赋值语句从来没有执行过
                return count--;
                }
                int count = 1;
                int main(void)
                {
                printf("global\t\tlocal static\n");
                for(; count <= 10; ++count)
                printf("%d\t\t%d\n", count, fun());
                return 0;
                }

    程序的运行结果是:

    global local static
    1  10
    2   9
    3   8
    4   7
    5   6
    6   5
    7   4
    8   3
    9   2
    10  1

    3. static的第三个作用是默认初始化为0.其实全局变量也具备这一属性,因为全局变量也存储在静态数据区

         在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置0,然后把不是0的几个元素赋值。如果定义成静态的,就省去了一开始置0的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加‘\0’太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是‘\0’。不妨做个小实验验证一下。

    #include <stdio.h>
                int a;
                int main(void)
                {
                int i;
                static char str[10];
                printf("integer: %d; string: (begin)%s(end)", a, str);
                return 0;
                }
                

    程序的运行结果如下integer: 0; string: (begin)(end)

    最后对static的三条作用做一句话总结。首先static的最主要功能是隐藏,其次因为static变量存放在静态存储区,所以它具备持久性和默认值0.

    4. 用static声明的函数和变量小结

    static 声明的变量在C语言中有两方面的特征:


      1)、变量会被放在程序的全局存储区中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
      2)、变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。


    Tips:
      A.若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度;
      B.若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;
      C.设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题;
        D.
    如果我们需要一个可重入的函数,那么,我们一定要避免函数中使用static变量(这样的函数被称为:带内部存储器功能的的函数)
          E.
    函数中必须要使用static变量情况:比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针。

        函数前加static使得函数成为静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名。

    扩展分析:

          术语static有着不寻常的历史.起初,在C中引入关键字static是为了表示退出一个块后仍然存在的局部变量。随后,staticC中有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。为了避免引入新的关键字,所以仍使用static关键字来表示这第二种含义。最后,C++重用了这个关键字,并赋予它与前面不同的第三种含义:表示属于一个类而不是属于此类的任何特定对象的变量和函数(Java中此关键字的含义相同)

    全局变量、静态全局变量、静态局部变量和局部变量的区别

    变量可以分为:全局变量、静态全局变量、静态局部变量和局部变量。


              存储区域分,全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区
              
    作用域分,  全局变量在整个工程文件内都有效;静态全局变量只在定义它的文件内有效;静态局部变量只在定义它的函数内有效,只是程序仅分配一次内存,函数返回后,该变量不会消失;局部变量在定义它的函数内有效,但是函数返回后失效

        全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此可以避免在其它源文件中引起错误。

      从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域 限制了它的使用范围。

      static 函数与普通函数作用域不同。仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件

      static全局变量与普通的全局变量有什么区别:static全局变量只初始化一次,防止在其他文件单元中被引用;
      static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值;

         static
    函数与普通函数有什么区别:static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝
         
    全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。

    Cosnt的作用:

    const给人的第一印象就是定义常量。

    (1)const用于定义常量。

         例如:const int N = 100;const int M = 200;
         这样程序中只要用到 N、M 就分别代表为整型100、200,N、M 为一常量,在程序中不可改变。
         但有人说他编程时从来不用const定义常量。我相信。但他是不懂得真正的编程艺术,用const定义常量不仅能方便我们编程而且能提高程序的清晰性。你是愿意看到程序中100、200 满天飞,还是愿意只看到简单清晰的N、M。相信有没有好处你慢慢体会。
         还有人说他不用const定义常量,他用#define宏定义常量。可以。但不知道你有没有发现有时#define宏并没有如你所愿在定义常量。下面我们比较比较const和#define。
         1。
         (a) const定义常量是有数据类型的:
         这样const定义的常量编译器可以对其进行数据静态类型安全检查,而#define宏定义的常量却只是进行简单的字符替换,没有类型安全检查,且有时还会产生边际效应(不如你愿处)。所谓边际效应举例如下:
               #define N 100
               #define M 200 + N
               当程序中使用 M*N 时,原本想要 100 * (200+ N )的却变成了 100 * 200 + N。
         (b)#define宏定义常量却没有。#define <宏名><字符串>,字符串可以是常数、表达式、格式串等。在程序被编译的时候,如果遇到宏名就哟内指定的字符串进行替换,然后再进行编译。
         2。
         有些调试程序可对const进行调试,但不对#define进行调试。
         3。
         当定义局部变量时,const作用域仅限于定义局部变量的函数体内。但用#define时其作用域不仅限于定义局部变量的函数体内,而是从定义点到整个程序的结束点。但也可以用#undef取消其定义从而限定其作用域范围。只用const定义常量,并不能起到其强大的作用。const还可修饰函数形式参数、返回值和类的成员函数等。从而提高函数的健壮性。因为const修饰的东西能受到c/c++的静态类型安全检查机制的强制保护,防止意外的修改。

    (2)const修饰函数形式参数

         形式参数有输入形式参数和输出形式参数。参数用于输出时不能加const修饰,那样会使函数失去输出功能。因为const修饰的东西是不能改变的。
         const只能用于修饰输入参数。
         谈const只能用于修饰输入参数之前先谈谈C++函数的三种传递方式。
         C++函数的三种传递方式为:值传递、指针传递和引用传递。简单举例说明之,详细说明请参考别的资料。
         值传递:
           void fun(int x){
                 x += 5;       //修改的只是y在栈中copy x,x只是y的一个副本,在内存中重新开辟的一块临时空间把y的值 送给了x;这样也增加了程序运行的时间,降低了程序的效率。
           }
           void main(void){
                 int y = 0;
                 fun(y);
                 cout<</"y = /"<<y<<endl;  //y = 0;
           }
         指针传递:
            void fun(int *x){
                 *x += 5;      //修改的是指针x指向的内存单元值
            }
            void main(void){
                 int y = 0;
                 fun(&y);
                 cout<<<</"y = /"<<y<<endl;  //y = 5;
            }
          引用传递:
             void fun(int &x){
                 x += 5;      //修改的是x引用的对象值 &x = y;
            }
            void main(void){
                 int y = 0;
                 fun(y);
                 cout<<<</"y = /"<<y<<endl;  //y = 5;
            }
          看了传递方式后我们继续来谈“const只能用于修饰输入参数”的情况。

             当输入参数用“值传递”方式时,我们不需要加const修饰,因为用值传递时,函数将自动用实际参数的拷贝初始化形式参数,当在函数体内改变形式参数时,改变的也只是栈上的拷贝而不是实际参数。
             但要注意的是,当输入参数为ADT/UDT(用户自定义类型和抽象数据类型)时,应该将“值传递”改为“const &传递”,目的可以提高效率。
             例如:
                void fun(A a);//效率底。函数体内产生A类型的临时对象用于复制参数 a,但是临时对象的
                              //构造、复制、析构过程都将消耗时间。
                void fun(A const &a);//提高效率。用“引用传递”不需要产生临时对象,省了临时对象的
                                     //构造、复制、析构过程消耗的时间。但光用引用有可能改变a,所以加const


             当输入参数用“指针传递”方式时,加const修饰可防止意外修改指针指向的内存单元,起到保护作用。
             例如:
                void funstrcopy(char *strdest,const char *strsrc)//任何改变strsrc指向的内存单元,
                                                                 //编译器都将报错
                些时保护了指针的内存单元,也可以保护指针本身,防止其地址改变。
             例如:
               void funstrcopy(char *strdest,const char *const strsrc)

    (3)const修饰函数的返回值

         如给“指针传递”的函数返回值加const,则返回值不能被直接修改,且该返回值只能被赋值给加const修饰的同类型指针。
         例如:
            const char *GetChar(void){};
          赋值 char *ch = GetChar();//错误    const char *ch = GetChar();//正确

    (4)const修饰类的成员函数(函数定义体)

         任何不会修改数据成员的函数都应用const修饰,这样当不小心修改了数据成员或调用了非const成员函数时,编译器都会报错。
         const修饰类的成员函数形式为:int GetCount(void)  const;
    (5)用传引用给const取代传值
    缺省情况下,C++ 以传值方式将对象传入或传出函数(这是一个从 C 继承来的特性)。除非你特别指定其它方式,否则函数的参数就会以实际参数(actual argument)的拷贝进行初始化,而函数的调用者会收到函数返回值的一个拷贝。这个拷贝由对象的拷贝构造函数生成。这就使得传值(pass-by-value)成为一个代价不菲的操作。例如,考虑下面这个类层级结构:

    class Person {
     public:
      Person(); // parameters omitted for simplicity
      virtual ~Person(); // see Item 7 for why this is virtual
      ...

     private:
      std::string name;
      std::string address;
    };

    class Student: public Person {
     public:
      Student(); // parameters again omitted
      ~Student();
      ...

     private:
      std::string schoolName;
      std::string schoolAddress;
    };

      现在,考虑以下代码,在此我们调用一个函数—— validateStudent,它得到一个 Student 参数(以传值的方式),并返回它是否验证有效的结果:

    bool validateStudent(Student s); // function taking a Student
    // by value

    Student plato; // Plato studied under Socrates

    bool platoIsOK = validateStudent(plato); // call the function

      当这个函数被调用时会发生什么呢?

      很明显,Student 的拷贝构造函数被调用,用 plato 来初始化参数 s。同样明显的是,当 validateStudent 返回时,s 就会被销毁。所以这个函数的参数传递代价是一次 Student 的拷贝构造函数的调用和一次 Student 的析构函数的调用。

      但这还不是全部。一个 Student 对象内部包含两个 string 对象,所以每次你构造一个 Student 对象的时候,你也必须构造两个 string 对象。一个 Student 对象还要从一个 Person 对象继承,所以每次你构造一个 Student 对象的时候,你也必须构造一个 Person 对象。一个 Person 对象内部又包含两个额外的 string 对象,所以每个 Person 的构造也承担着另外两个 string 的构造。最终,以传值方式传递一个 Student 对象的后果就是引起一次 Student 的拷贝构造函数的调用,一次 Person 的拷贝构造函数的调用,以及四次 string 的拷贝构造函数调用。当 Student 对象的拷贝被销毁时,每一个构造函数的调用都对应一个析构函数的调用,所以以传值方式传递一个 Student 的全部代价是六个构造函数和六个析构函数!

      好了,这是正确的和值得的行为。毕竟,你希望你的全部对象都得到可靠的初始化和销毁。尽管如此,如果有一种办法可以绕过所有这些构造和析构过程,应该变得更好,这就是:传引用给 const(pass by reference-to-const):

    bool validateStudent(const Student& s);

      这样做非常有效:没有任何构造函数和析构函数被调用,因为没有新的对象被构造。被修改的参数声明中的 const 是非常重要的。 validateStudent 的最初版本接受一个 Student 值参数,所以调用者知道它们屏蔽了函数对它们传入的 Student 的任何可能的改变;validateStudent 也只能改变它的一个拷贝。现在 Student 以引用方式传递,同时将它声明为 const 是必要的,否则调用者必然担心 validateStudent 改变了它们传入的 Student。

      以传引用方式传递参数还可以避免切断问题(slicing problem)。当一个派生类对象作为一个基类对象被传递(传值方式),基类的拷贝构造函数被调用,而那些使得对象的行为像一个派生类对象的特殊特性被“切断”了。你只剩下一个纯粹的基类对象——这没什么可吃惊的,因为是一个基类的构造函数创建了它。这几乎绝不是你希望的。例如,假设你在一组实现一个图形窗口系统的类上工作:

    class Window {
     public:
      ...
      std::string name() const; // return name of window
      virtual void display() const; // draw window and contents
    };

    class WindowWithScrollBars: public Window {
     public:
      ...
      virtual void display() const;
    };

      所有 Window 对象都有一个名字,你能通过 name 函数得到它,而且所有的窗口都可以显示,你可一个通过调用 display 函数来做到这一点。display 为 virtual 的事实清楚地告诉你:一个纯粹的基类的 Window 对象的显示方法有可能不同于专门的 WindowWithScrollBars 对象的显示方法。

      现在,假设你想写一个函数打印出一个窗口的名字,并随后显示这个窗口。以下这个函数的写法是错误的:

    void printNameAndDisplay(Window w) // incorrect! parameter

     // may be sliced!
     std::cout << w.name();
     w.display();
    }

      考虑当你用一个 WindowWithScrollBars 对象调用这个函数时会发生什么:

    WindowWithScrollBars wwsb;

    printNameAndDisplay(wwsb);

      参数 w 将被作为一个 Window 对象构造——它是被传值的,记得吗?而且使 wwsb 表现得像一个 WindowWithScrollBars 对象的特殊信息都被切断了。在 printNameAndDisplay 中,全然不顾传递给函数的那个对象的类型,w 将始终表现得像一个 Window 类的对象(因为它就是一个 Window 类的对象)。特别是,在 printNameAndDisplay 中调用 display 将总是调用 Window::display,绝不会是 WindowWithScrollBars::display。

      绕过切断问题的方法就是以传引用给 const 的方式传递 w:

    void printNameAndDisplay(const Window& w) // fine, parameter won’t
    {
     // be sliced
     std::cout << w.name();
     w.display();
    }

      现在 w 将表现得像实际传入的那种窗口。

      如果你掀开编译器的盖头偷看一下,你会发现用指针实现引用是非常典型的做法,所以以引用传递某物实际上通常意味着传递一个指针。由此可以得出结论,如果你有一个内建类型的对象(例如,一个 int),以传值方式传递它常常比传引用方式更高效。那么,对于内建类型,当你需要在传值和传引用给 const 之间做一个选择时,没有道理不选择传值。同样的建议也适用于 STL 中的迭代器(iterators)和函数对象(function objects),因为,作为惯例,它们就是为传值设计的。迭代器(iterators)和函数对象(function objects)的实现有责任保证拷贝的高效并且不受切断问题的影响。(这是一个“规则如何变化,依赖于你使用 C++ 的哪一个部分”的实例。)

      内建类型很小,所以有人就断定所有的小类型都是传值的上等候选者,即使它们是用户定义的。这样的推论是不可靠的。仅仅因为一个对象小,并不意味着调用它的拷贝构造函数就是廉价的。很多对象——大多数 STL 容器也在其中——容纳的和指针一样,但是拷贝这样的对象必须同时拷贝它们指向的每一样东西。那可能是非常昂贵的。

      即使当一个小对象有一个廉价的拷贝构造函数,也会存在性能问题。一些编译器对内建类型和用户定义类型并不一视同仁,即使他们有同样的底层表示。例如,一些编译器拒绝将仅由一个 double 组成的对象放入一个寄存器中,即使在常规上它们非常愿意将一个纯粹的 double 放入那里。如果发生了这种事情,你以传引用方式传递这样的对象更好一些,因为编译器理所当然会将一个指针(引用的实现)放入寄存器。

      小的用户定义类型不一定是传值的上等候选者的另一个原因是:作为用户定义类型,它的大小常常变化。一个现在较小的类型在将来版本中可能变得更大,因为它的内部实现可能会变化。甚至当你换了一个不同的 C++ 实现时,事情都可能会变化。例如,就在我这样写的时候,一些标准库的 string 类型的实现的大小就是另外一些实现的七倍。

      通常情况下,你能合理地假设传值廉价的类型仅有内建类型及 STL 中的迭代器和函数对象类型。对其他任何类型,请遵循本 Item 的建议,并用传引用给 const 取代传值。

      Things to Remember

      ·用传引用给 const 取代传值。典型情况下它更高效而且可以避免切断问题。

      ·这条规则并不适用于内建类型及 STL 中的迭代器和函数对象类型。对于它们,传值通常更合适。
       

  • 相关阅读:
    计算与软件工程 作业四
    计算与软件工程作业三
    计算机与软件工程作业二
    计算机与软件工程 作业一
    《WWXS团队》:团队项目选题报告
    计算与软件工程作业五
    计算与软件工程作业四
    计算与软件工程 作业三
    计算与软件工程 作业二
    计算与软件工程 作业一
  • 原文地址:https://www.cnblogs.com/DswCnblog/p/2875785.html
Copyright © 2020-2023  润新知