zoukankan      html  css  js  c++  java
  • 【C++学习】零散笔记

    作者:gnuhpc
    出处:http://www.cnblogs.com/gnuhpc/

    问题1:expected unqualified-id before ‘using’

    一般都是类声明后面没有加分号导致的。

    类声明的时候没有加分号,还可能导致一个错误--错误:一个声明指定了多个类型

    问题2:类中的数据成员是不是根据前导函数的初始化顺序进行初始化的?

    数据成员根据它们在类定义中出现的顺序进行初始化的。

    问题3:继承类进行构造和析构时的顺序是怎么样的?

    在构造时先从基类开始构造,在析构时从最高层的子类开始析构,过程正好相反。

    问题:对象之间有几种关系,分别是什么?

    简单来讲,对象间的关系可以描述为:

    has-a 关系:包含关系,A对象中包含B对象。这是在一个类中嵌入另一个类的实例实现的。

    is -a 关系:继承关系,B对象是对A对象的继承。这是由继承技术实现的。

    has-access-to 关系:可访问关系,即B可以访问A中的数据成员。这是由友元类和引用实现的。

    问题:什么是友元类?

    友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。       
    当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
          friend class 类名;
          其中:friend和class是关键字,类名必须是程序中的一个已定义过的类。
          例如,以下语句说明类B是类A的友元类:
          class A
          {
                 …
          public:
                 friend class B;
                 …
          };
          经过以上说明后,类B的所有成员函数都是类A的友元函数,能存取类A的私有成员和保护成员。
          使用友元类时注意:
          (1) 友元关系不能被继承。
          (2) 友元关系是单向的,不具有交换性,不具有传递性。

    问题:引用的使用需要注意一些什么?

    引用在创建之时必须初始化,或在其声明中完成。对于成员引用,在构造方法的前导中完成初始化。或者在调用一个带有引用的方法时,在调用之时完成。

    问题:右移位运算符的工作方式有什么不同

    对于无符号数,1011>>1 == 0101,对于有符号类型,1011>>1 == 1101,因为无论怎么移位,是不会改变其符号的,对于任何一个数字来说,移位运算符,右移都是除以2,左移都是乘以2(假设不溢出)。

    问题:动态内存分配和指针之间怎么使用?

    使用new,返回的内存从所谓堆(heap),即由C++运行时管理的内存区域开始分配,如何访问呢?就是使用指针,例如,

    int *pNumber =new int(2001);

    Star *pStar = new Star(1234.5,10.2);

    当分配一个带有构造函数的对象时,初始化是强制性的。即使一个对象没有显式构造函数,编译器也可能不得不生成一些代码来构建它:

    • 若一个类没有有效的构造函数,编译器将产生一个缺省的无参数构造函数;
    • 若一个类有一个显式定义的无参数构造函数,则就在分配的时候调用;
    • 若一个类只有一个带参数的构造函数,比如叫做CBody类,则CBody *bodes = new CBody[100];编译器将不予编译。

    所以在定义CBody的时候可以使用如下的构造函数:

    CBody (double mass =0.0):_mass(mass){}

    这等价于具备参数个数为0和1的两个重载构造函数。

    为了释放则使用delete,例如,delete pNumber;若有析构函数则自动调用,例如,delete pStar;

    若想删除数组则在其前边放上[],例如: delete [] pNumbers;

    删除一个空指针是安全的,所以在调用delete前不需要检查是否为空指针。

    问题:如何实现链表?

    List.h

    [cpp] view plaincopy

    1. #if !defined (LIST_H)
    2. #define LIST_H
    3. // (c) Bartosz Milewski 2000
    4. class Link 
    5. public: 
    6.     Link (Link* pNext, int id) 
    7.         : _pNext (pNext), _id (id) {} 
    8.     Link *  Next () const { return _pNext; } 
    9. int     Id () const { return _id; } 
    10. private: 
    11. int     _id; 
    12.     Link *  _pNext; 
    13. }; 
    14. class List 
    15. public: 
    16.     List (): _pHead (0) {} 
    17.     ~List (); 
    18. void Add ( int id ); 
    19.     Link const * GetHead () const { return _pHead; } 
    20. private: 
    21.     Link* _pHead; 
    22. }; 
    23. #endif

    List.cpp

    [cpp] view plaincopy

    1. // (c) Bartosz Milewski 2000
    2. #include "List.h"
    3. #include <iostream>
    4. List::~List () 
    5. // free the list
    6. while ( _pHead != 0 ) 
    7.     { 
    8.         Link* pLink = _pHead; 
    9.         _pHead = _pHead->Next(); // unlink pLink
    10. delete pLink; 
    11.     } 
    12. void List::Add ( int id ) 
    13. // add in front of the list
    14.     Link * pLink = new Link (_pHead, id); 
    15.     _pHead = pLink; 
    16. int main () 
    17.     List list; 
    18.     list.Add (1); 
    19.     list.Add (2); 
    20.     std::cout << "List contents:/n"; 
    21. for (Link const * pLink = list.GetHead(); 
    22.          pLink != 0; 
    23.          pLink = pLink->Next ()) 
    24.     { 
    25.         std::cout << pLink->Id() << ", "; 
    26.     } 
    27.     std::cout << std::endl; 

    我们要注意区分的是指向常量的指针和常量指针,前者所指向的想对象不能通过此指针来修改,后者则表示一旦初始化,就不能指向其他任何对象。

    Link const *pLink;//指向常量的指针

    Link * const pLink = pInitPtr;//常量指针

    可以组合为指向常量的常量指:

    Link Const * const pLink=pInitPtr;

    我们从右往左念,星号表示“指向一个”,这样就好理解记忆了。

    另外,const作为返回值的一些东西也需要我们注意:我们经常看到一些函数返回const   char*后者返回char   *,这有什么区别呢?

    const   char*说明函数返回一个你不能改变指针所指向单元值的指针. 也就是说指针所指向的值是一个常数,你不能通过*操作来改变它的值.
    而char*则可以改变指针所指向的值.要是不需要处理返回值,或者不允许处理返回值的话,那么其实使用前者比较好。

    再一个问题:用const修饰引用,通过引用本身不可修改其值,但这并不耽误被引用变量的被外界修改。Const加在数据类型前后均可。
    例如:void   main(void)
    {
            int   i   =   10;
            int   j   =   100;
            const   int   &r   =   i;
            int   const   &s   =   j;
            r   =   20;                     //错,不能改变内容
            s   =   50;                     //错,不能改变内容
            i   =   15;                     //   i和r   都等于15
            j   =   25;                     //   j和s   都等于25
    }

    声明成员函数时,末尾加const修饰,表示在成员函数内不得改变该对象的任何数据。这种模式常被用来表示对象数据只读的访问模式。例如:   class   MyClass
    {
            char   *str   = "Hello,   World ";
            char   ValueAt(int   pos)   const         //const   method   is   an   accessor   method
            {
                    if(pos   > =   12)
                                  return   0;
                    *str   =   ' 'M ' ';             //错误,不得修改该对象
                    return   str[pos];         //return   the   value   at   position   pos
            }
    }

    重载函数的时候也可以使用const。

    Why inline?
    通常一个函数调用会将控制权从主线程序转移到函数中,在这个代码执行之后再返回主线程序。这样的机制节省了空间和内存空间,因为函数仅仅在一个地方保存这个函数,仅仅是被调用的时候执行。但是这就带来了时间上的损失。大函数上还比较划算,但是小函数的就得不偿失了。这时候我们就可以使用inline(内联)函数,编译器将内联函数的代码直接插入到调用的地方,这有点像宏的概念,在某些时候会提高性能。当然有人会问你为什么不直接写一段程序呢?为了代码的清晰。
    例如:
    void f()
    {
       int x =
    /*...*/ ;
       int y =
    /*...*/ ;
       int z =
    /*...*/ ;
    ...code that uses x , y  and z ...
       g(x, y, z);
    ...more code that uses x , y  and z ...
    }
    void g(int x, int y, int z)
    {
    ...code that uses x , y  and z ...
    }
    我们可以看到没有必要去将x、y、z不断压栈、返回。而此时使用内联就是件不错的事情。

    How to inline?
    我们有许多种方法将一个函数设置为内联,有些需要inline关键词,有些不用,但是需要注意的是,不管怎么样,编译器都可能忽略你的内联要求。不要因此沮丧,因为这其实是一个巨大的有点:这让编译器对大函数和小函数进行不同的处理,并且若编译选项选择得当就能使得编译器产生更加方便调试的代码。
    我们举以下例子说明内联函数如何定义?
    1)自动内联
    class MyClass {
      int i; // private by default
    public:   
      int getInt() { return i; } //inline
      void setInt(int j) { i = j; }
    } ;
    2)显式内联
    class MyClass {
      int i; // private by default
    public:
      int getInt();
      void setInt(int j);
    } ;
    inline int MyClass::getInt()
    {
      return i;
    }
    inline void MyClass::setInt(int j)
    {
      i = j;
    }
    How's inline?
    有好有坏吧,可能提高性能也可能影响性能。可能让可执行文件变大,也可能变小。
    变快: 像上边带有过程融合的部分可以去除一顿不必要的指令,这会让程序加快。
    变慢: 太多的内联函数会让程序变得臃肿,这可能会导致thrashing的出现。换句话说,可执行代码太大,系统会频繁去取各部分的数据造成开销。
    变大: 这个是显而易见的,若一个系统有一百个内联函数,每个内联函数都会展开100字节的可执行代码并在100个地方被调用,那么程序就增大1MB。这1MB会有多大问题?鬼知道,说不定这1MB就导致了系统thrashing。
    变小: 编译器常常产生的用于push/pop 进出register/parameters的代码会比内联展开的代码大,这往往发生在小函数中或优化器能够使大函数变小时出现。
    导致thrashing: 内联可能增大可执行文件大小导致thrashing。
    不导致thrashing: 内存中一次需要加载的页数即使可执行文件变大时也可能减小。当f()调用g()时,代码往往在两个不同的页上。而使用内联时就会在一页。
    增加cache misses: From Wiki, A cache miss refers to a failed attempt to read or write a piece of data in the cache, which results in a main memory access with much longer latency. 内连函数可能导致一个内部循环跨越内存缓冲的多行,最终导致cache misses。
    减少cache misses :内联函数通常可以改进二进制代码中的引用定位,这可能减少一个内部循环内部需要存储的cache的行数。
    Why not Macro?
    这个东西如此相似于宏,为什么不用宏呢?下边这个例子就足以说明一些问题,当然远远不止这一个事情,我们只是举一个例子。
    // A macro that returns the absolute value of i
    #define unsafe(i)  /
             ( (i) >= 0 ? (i) : -(i) )
    // An inline  function that returns the absolute value of i
    inline
    int safe(int i)
    {
       return i >= 0 ? i : -i;
    }
    int f();
    void userCode(int x)
    {
       int ans;
       ans = unsafe(x++);  
    // Error! x  is incremented twice
       ans = unsafe(f());  
    // Danger! f()  is called twice
       ans = safe(x++);    
    // Correct! x  is incremented once
       ans = safe(f());    
    // Correct! f()  is called once
    }

    参考文献:http://www.parashift.com/c++-faq-lite/inline-functions.html

    1.C语言标准的本质:标准C语言没有提供实现,只是定义了标准的函数接口,所有工作都是通过库函数完成的。
    2.什么是语言实现:
    具体实现一种语言的各种特征并支持特定编程模式的技术和工具,具体说就是编译器和连接器或者是解释器。
    3. 基于应用程序框架(比如MFC),生成的源代码往往没有main(),并不是说这些程序不需要main函数,而只是Application Framework将main的实现隐藏起来了,并且它的实现具有固定的模式,所以不需要程序员来编写,在应用程序的连接阶段,框架会将包含main() 实现的Library加进来一起连接。
    4.内部名称:C/C++都会按照特定的规则把程序员定义的标识符转换为相应的内部名称——在前边 添加下划线 " _ ",在C语言中,所有函数不是局部于编译单元(文件作用域)的static函数,就是具有extern连接类型和global作用域的全局函数,从唯一识 别函数上看并没有大的不同,但在C++中,允许用户在不同的作用域定义同名的函数,作用域不单单是文件,可能是class namespace等,甚至在同一作用域中也可以定义同名的函数——重载。在源码级别,通过它们各自的对象和成员标识符区分,但是在连接器层面,所有函数 都是全局函数,能够用来区分不同函数调用的除了作用域外就是函数名称了,C++中使用Name-Mangling避免连接二义性。

    5.变量初始化需要注意的事项:
    a. 在C++/C中,全局变量(extern或static的)存放在程序的静态数据区中,在程序进入main()之前创建,在main()结束后销毁,因此 在代码中根本没有机会初始化它们,语言及其实现提供了一个默认的全局初始化器0——没有明确初始化全局变量则将0转换为所需类型来完成初始化。函数内 的static局部变量和类的static数据成员都具有static存储类型,因此最终也被移动到程序的静态数据区中,因此也会默认初始化为0.除非你明确的提供了初值。
    b.在一个编译单元中定义的全局变量的初始值不要依赖定义于另一个编译单元中的全局变量的初始值,因为编译器和连接器无法确定两个编译单元连接在一起时哪一个全局变量的初始化优先于另一个编译单元的全局变量的初始化。

    c.存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。这两种变量都是保持变量内容持久性的方法。它们默认初始化都为0。

    6.区别编译时和运行时的不同
    编译时指的是编译预处理器、编译器和连接器工作的阶段,只对基本的规范和语法进行处理,而像容器越界访问、虚函数动态决议、函数动态连接、动态内存分配等需要在运行时才能确定的问题是运行时。
    下边这个例子就说明了C++的访问控制策略是为了防止意外时间而不是防止对编译器的恶意欺骗。

    /*
    * =====================================================================================
    *
    *       Filename:  test.cpp
    *
    *    Description:  区分编译时和运行时的不同
    *
    *        Version:  1.0
    *        Created:  02/26/2010 09:24:06 AM
    *       Revision:  none
    *       Compiler:  gcc
    *
    *         Author:  gnuhpc (http://www.gnuhpc.info), warmbupt@gmail.com
    *        Company:  IBM CDL
    *
    * =====================================================================================
    */
    #include <iostream>
    using namespace std;
    class Base
    {
        public:
            virtual void Say(){
                cout<<"Base::Say()was invoked!/n";
            }
    }; /* -----  end of class Base  ----- */
    class Derived:public Base{
        private:
            virtual void Say(){
                cout << "Derived::Say() was invoked!/n";
            }
    };
    int main(int argc, char *argv[])
    {
        Base *p = new Derived;
        p->Say();
    }

    输入:Derived::Say() was invoked!  这违背了private 本来的意愿。
    7.字节是内存编制的最小单位,所以最小的对象(包括空对象)也至少会占据一个字节的内存空间。一个bool变量也占据了1字节内存,只是浪费了
    8.void 是空类型,意思是这种类型的大小无法确定,不存在void类型的对象,void指针可以作为通用指针,因为它可以指向任何类型的对象。void类型指针和 NULL指针的区别,NULL是可以赋值给任何类型的指针的值0,在C中它为(void *)0,而在C++中,由于允许从0到任何指针类型的隐式转化,NULL就是整数0.一个Void *类型的指针是一个合法的指针,常用于函数参数来传递一个函数与其调用者约定好类型的对象地址,如线程函数。在C中允许任何非void类型指针和void 类型的指针之间进行直接的相互转化,但是在C++中只允许任何类型的指针向void类型的指针转化,而不允许反过来将void类型的指针直接指派给任何非 void类型指针,除非进行强制转换。这样就避免了内存扩张和截断的安全问题。
    9.默认类型:C中为int,C++没有默认类型,但是在模板中有“默认类型参数”的概念。
    10.高低地址存放:
    所 谓自然对齐,基本数据类型(主要是short、int和double)的变量不能简单的存储在内存中的任意地址处,它们的起始地址必须能够被它们的大小整 除。RiSC的都是Big Endian存储,即高字节高字在低地址存放,要求自然对齐。而Intel的都是Little Endian,即高字节高字在高地址存放,不要求自然对齐。
    11.类型转换:
    一般占用内存比较少的类型会隐式的转换为表达式中占 用内存最多的操作数类型,类型转换并不是改变原来的类型和值,而是生成了新的临时变元:char is-a int, int is-a long, long is-a float, lfoat is-a double.
    从内存的角度,一个类型转换过的指针所能够访问的范围受到其类型的限制,
    例如,这实际上就是内存的截断,因为int指针能访问的范围小于Double型:
    double d1= 1000.25100212;
    *pInt =(int*)(&d1);
    cout << *pInt <<endl;
    注意,这里要区分值的截断和内存的截断,下边是值的截断:
    double d2 = 10.20;
    int i2 = (int)d2;
    cout << i2 << endl;
    而下边的这个例子就是内存的扩张,因为double指针能访问的范围大于int型:
    int i1 = 1023;
    double *pDouble = (double*)(&i1);
    cout << *pDouble <<endl;
    同理,在OO中,不能把基类的对象,直接转换为派生类对象,无论是直接赋值还是强制转换,因为这不是“自然的”。
    12.++ --的效率问题:当单独使用时前置后置都一样,而当复杂的表达式中使用时,比如当应用于用户定义类型,尤其是大队想的时候,前置版本会比后置版本效率高许 多,原因是后置版本,比如b=a++, 其实质并非某些教科上所写的“先使用其操作数的值,然后再进行加1运算”,而是首先创建一个临时变量temp存储a的值,然后做a+=1的运算,随后把 temp的值赋给b,最后销毁这个临时变量(若是对象则还会调用其拷贝构造函数),所有这些是有代价的。所以在可以选择的情况下,尽量使用前置版本。下边 就写一个重载++运算符的例子:
    /*
    * =====================================================================================
    *
    *       Filename:  test.cpp
    *
    *    Description: 重载++运算符
    *
    *        Version:  1.0
    *        Created:  02/26/2010 09:24:06 AM
    *       Revision:  none
    *       Compiler:  gcc
    *
    *         Author:  gnuhpc (http://www.gnuhpc.info), warmbupt@gmail.com
    *        Company:  IBM CDL
    *
    * =====================================================================================
    */
    #include <iostream>
    using namespace std;
    class Integer{
        public:
            Integer(double data):m_data(data){}
            Integer& operator++(){
                cout << "前置版本,返回引用" <<endl;
                m_data++;
                return *this;
            }
            Integer operator++(int){
                cout << "后置版本,返回对象的值" <<endl;
                Integer temp = *this;
                m_data++;
                return temp;
            }
            int getData(){
                return m_data;
            }
        private:
            double m_data;
    };
    int main(int argc, char *argv[])
    {
        Integer x=1;
        ++x;
        cout <<x.getData() <<endl;
        x++;
        cout <<x.getData() <<endl;
    }
    13.bool类型:c++中0->false,而任何非0值为true,所以应该总是和false比较。
    14.不建议使用==和!=来比较浮点数是否相等(用abs比较),但是可以直接比较浮点数谁大谁小。
    15.遍历数组的效率:
    对于多维数组而言,高效率的遍历方法是看语言以什么顺序来安排数组元素的存储空间,我们看看c/c++是用什么方式存储的:

    /*
    * =====================================================================================
    *
    *       Filename:  test.cpp
    *
    *    Description:  C/C++多维数组存储是以先行后列的方式存储的,所以遍历时外循环是行,内循环是列效率较高
    *
    *        Version:  1.0
    *        Created:  02/26/2010 09:24:06 AM
    *       Revision:  none
    *       Compiler:  gcc
    *
    *         Author:  gnuhpc (http://www.gnuhpc.info), warmbupt@gmail.com
    *        Company:  IBM CDL
    *
    * =====================================================================================
    */
    #include <iostream>
    using namespace std;
    int main(int argc, char *argv[])
    {
        int a[5][5];
        for( int i=0 ; i<5 ; i++ )
        {
            for( int j=0 ; j<5 ; j++ )
            {
                cout << "a[" <<i <<"][" <<j <<"]=" << &a[i][j] <<" ";
            }
            cout <<endl;   
        }
    }

    影响效率的实际上是大数组遍历时来回跳转导致的内存页面交换次数以及cache命中率的高低,而不是循环次数本身。
    16.循环体内存在逻辑判断,并且循环次数很大时,最好将逻辑判断移到循环体外,虽然看起来很罗嗦,但是编译器可以对循环进行优化处理:

    17.字面常量,比如char  c ='a', 只能引用不能修改,其保存在程序的符号表中而不是一般的数据区,为只读。在编译时通常把合并常量的开关打开可优化程序效率。
    18. 在标准的C语言中,const符合常量默认是extern的,也就是说你不能在两个或以上的编译单元中同时定义一个同名的const符号常量,或者把一个 const符号常量定义放在一个头文件中而多个编译单元同时包含该文件。但是在标准的C++中,const为内连接,可以定义在头文件中。当在不同的编译 单元中同时包含该文件时,编译器认为它们是不同的符号常量,因为每个编译单元独立编译时分别为它们分配空间,连接时进行合并。
    19.标准C++/C中的枚举常量的值可以很大,比如300000000000000000
    20.在C++中应该尽可能使用const定义符号常量。
    21.C++需要对外公开的常量放在头文件中,不需要对外公开的常量定义在文件的头部。为便于管理可以把不同模块的常量集中存放在一个公用的头文件中。
    22.const 定义的常量在函数执行之后其空间会被释放,而 static 定义的静态常量在函数执行后不会被释放其空间。

    static 表示的是静态的。类的静态成员函数,成员变量是和类相关的,不是和类的具体对象相关,即使没有具体的对象,也能调用类的静态成员函数,成员变量。一般的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。

    在 c++ 中, static 静态成员变量不能在类内部初始化。

    在 c++ 中, const 常量成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。

    const 数据成员只在 某个对象生存期内是常量 , 而对于整个类而言却是可变的 。因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。所以不能在类声明中初始化 const 数据成员,因为类的对象未被创建时,编译器不知道 const
    数据成员的值是什么。

    const 数据成员的初始化 只能在类的构造函数的初始化表中进行 。 要想建立在整个类中都恒定的常量 ,应该用类中的枚举常量来实现,或者 static
    const。

    如:

    class
    Test

    {

    public:

         Test():
    a(0){}

       enum {size1=100, size2 = 200 };

    private:

         const
    int a;  //
    只能在构造函数初始化列表中初始化

         static
    int b ;

         const
    static int c; // 与 static const int
    c; 相同,可以 在这里定义 (如果以后在类中需要使用该变量的话 ).

    }

    int Test :: b = 0;
    // 不能以成员列表初始化,不能在定义处初始化,因为不属于某个对象。

    const
    int Test:: c = 0 ; // 注意:给静态成员变量赋值时,不在需要加 static 修饰。但 const 要加。

    在这转载一篇写的比较清晰的文字:

    全局变量/常量几种方法的区别(C/C++)

    在讨论全局变量之前我们先要明白几个基本的概念:

    1. 编译单元(模块):
    在IDE开发工具大行其道的今天,对于编译的一些概念很多人已经不再清楚了,很多程序员最怕的就是处理连接错误(LINK ERROR),
    因为它不像编译错误那样可以给出你程序错误的具体位置,你常常对这种错误感到懊恼,但是如果你经常使用gcc,makefile等工具在linux或者嵌
    入式下做开发工作的话,那么你可能非常的理解编译与连接的区别!当在VC这样的开发工具上编写完代码,点击编译按钮准备生成exe文件时,VC其实做了两
    步工作,第一步,将每个.cpp(.c)和相应.h文件编译成obj文件;第二步,将工程中所有的obj文件进行LINK生成最终的.exe文件,那么错
    误就有可能在两个地方产生,一个是编译时的错误,这个主要是语法错误,另一个是连接错误,主要是重复定义变量等。我们所说的编译单元就是指在编译阶段生成
    的每个obj文件,一个obj文件就是一个编译单元,也就是说一个cpp(.c)和它相应的.h文件共同组成了一个编译单元,一个工程由很多个编译单元组
    成,每个obj文件里包含了变量存储的相对地址等 。

    2. 声明与定义的区别
        函数或变量在声明时,并没有给它实际的物理内存空间,它有时候可以保证你的程序编译通过,
    但是当函数或变量定义的时候,它就在内存中有了实际的物理空间,如果你在编译模块中引用的外部变量没有在整个工程中任何一个地方定义的话,
    那么即使它在编译时可以通过,在连接时也会报错,因为程序在内存中找不到这个变量!你也可以这样理解,
    对同一个变量或函数的声明可以有多次,而定义只能有一次!

    3. extern的作用
        extern有两个作用,第一个,当它与"C"一起连用时,如: extern "C" void
    fun(int a, int b); 则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,
    C++的规则在翻译这个函数名时会把fun这个名字变得面目全非,可能是fun@aBc_int_int#%$ 也可能是别的,这要看编译器的"脾气"了(不同的编译器采用的方法不一样),为什么这么做呢,因为C++支持函数的重载啊,在这里不去过多的论述这个问题,如果你有兴趣可以去网上搜索,相信你可以得到满意的解释!
    当extern不与"C"在一起修饰变量或函数时,如在头文件中: extern int g_Int;
    它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块活其他模块中使用,记住它是一个声明不是定义!也就是说B模块(编译
    单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,
    在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

        如果你对以上几个概念已经非常明白的话,那么让我们一起来看以下几种全局变量/常量的使用区别:

    1. 用extern修饰的全局变量
        以上已经说了extern的作用,下面我们来举个例子,如:
        在test1.h中有下列声明:
        #ifndef TEST1H
        #define TEST1H
        extern char g_str[]; // 声明全局变量g_str
        void fun1();
        #endif
        在test1.cpp中
        #include "test1.h"
    char g_str[] = "123456"; // 定义全局变量g_str
        void fun1()
        {
            cout << g_str << endl;
        }
        以上是test1模块, 它的编译和连接都可以通过,如果我们还有test2模块也想使用g_str,只需要在原文件中引用就可以了
        #include "test1.h"

        void fun2()
        {
            cout << g_str << endl;
        }
    以上test1和test2可以同时编译连接通过,如果你感兴趣的话可以用ultraEdit打开test1.obj,你可以在里面着"123456"这
    个字符串,但是你却不能在test2.obj里面找到,这是因为g_str是整个工程的全局变量,在内存中只存在一份,
    test2.obj这个编译单元不需要再有一份了,不然会在连接时报告重复定义这个错误!
        有些人喜欢把全局变量的声明和定义放在一起,这样可以防止忘记了定义,如把上面test1.h改为
        extern char g_str[] = "123456"; // 这个时候相当于没有extern
    然后把test1.cpp中的g_str的定义去掉,这个时候再编译连接test1和test2两个模块时,会报连接错误,这是因为你把全局变量g_str的定义放在了头文件之后,test1.cpp这个模块包含了test1.h所以定义了一次g_str,而test2.cpp也包含了test1.h所以再一次定义了g_str,这个时候连接器在连接test1和test2时发现两个g_str。

    如果你非要把g_str的定义放在test1.h中的话,那么就把test2的代码中#include "test1.h"去掉 换成:
        extern char g_str[];
        void fun2()
        {
            cout << g_str << endl;
        }
    这个时候编译器就知道g_str是引自于外部的一个编译模块了,不会在本模块中再重复定义一个出来,但是我想说这样做非常糟糕,因为你由于无法在test2.cpp中使用#include "test1.h",那么test1.h中声明的其他函数你也无法使用了,除非也用都用extern修饰,这样的话你光声明的函数就要一大串,而且头文件的作用就是要给外部提供接口使用的,所以 请记住, 只在头文件中做声明,真理总是这么简单。

    2. 用static修饰的全局变量
    首 先,我要告诉你static与extern是一对“水火不容”的家伙,也就是说extern和static不能同时修饰一个变量;其次,static修饰 的全局变量声明与定义同时进行,也就是说当你在头文件中使用static声明了全局变量后,它也同时被定义了;最后,static修饰全局变量的作用域只 能是本身的编译单元,也就是说它的“全局”只对本编译单元有效,其他编译单元则看不到它 。利用这一特性可以在不同的文件中定义 同名函数和同名变量,而不必担心命名冲突。如:
        test1.h:
        #ifndef TEST1H
        #define TEST1H
    static char g_str[] = "123456";
        void fun1();
        #endif

        test1.cpp:
        #include "test1.h"
        void fun1()
        {
            cout << g_str << endl;
        }
        test2.cpp
        #include "test1.h"
        void fun2()
        {
            cout << g_str << endl;
        }
    以上两个编译单元可以连接成功, 当你打开test1.obj时,你可以在它里面找到字符串"123456",
    同时你也可以在test2.obj中找到它们,它们之所以可以连接成功而没有报重复定义的错误是因为虽然它们有相同的内容,但是存储的物理地址并不一样,就像是两个不同变量赋了相同的值一样,而这两个变量分别作用于它们各自的编译单元。
    也许你比较较真,自己偷偷的跟踪调试上面的代码,结果你发现两个编译单元(test1,test2)的g_str的内存地址相同,于是你下结论static修饰的变量也可以作用于其他模块,但是我要告诉你,那是你的编译器在欺骗你,大多数编
    译器都对代码都有优化功能,以达到生成的目标程序更节省内存,执行效率更高,当编译器在连接各个编译单元的时候,它会把相同内容的内存只拷贝一份,比如上面的"123456", 位于两个编译单元中的变量都是同样的内容,那么在连接的时候它在内存中就只会存在一份了,如果你把上面的代码改成下面的样子,你马上就可以拆穿编译器的谎言:
        test1.cpp:
        #include "test1.h"
        void fun1()
        {
            g_str[0] = ''a'';
            cout << g_str << endl;
        }

        test2.cpp
        #include "test1.h"
        void fun2()
        {
            cout << g_str << endl;
        }
        void main()
        {
            fun1(); // a23456
            fun2(); // 123456
        }
        这个时候你在跟踪代码时,就会发现两个编译单元中的g_str地址并不相同,因为你在一处修改了它,所以编译器被强行的恢复内存的原貌,在内存中存在了两份拷贝给两个模块中的变量使用。

        正是因为static有以上的特性,所以一般定义static全局变量时,都把它放在原文件中而不是头文件,这样就不会给其他模块造成不必要的信息污染,同样记住这个原则吧!


    3 const修饰的全局常量

        const修饰的全局常量用途很广,比如软件中的错误信息字符串都是用全局常量来定义的。const修饰的全局常量据有跟static相同的特性,即它们只能作用于本编译模块中,但是const可以与extern连用来声明该常量可以作用于其他编译模块中 ,
        extern const char g_str[];
        然后在原文件中别忘了定义:
        const char g_str[] = "123456";

    所以当const单独使用时它就与static相同,而当与extern一起合作的时候,它的特性就跟extern的一样了!所以对const我没有什么可以过多的描述,我只是想提醒你,const char* g_str = "123456" 与 const char g_str[] =
    "123465"是不同的, 前面那个const 修饰的是char *而不是g_str,它的g_str并不是常量,它被看做是一个定义了的全局变量(可以被其他编译单元使用)
    , 所以如果你像让char*g_str遵守const的全局常量的规则,最好这么定义const char* const g_str="123456".

    比较常用的在多个文件的工程中定义全局常量的方法:
    方法1:在某个公用头文件中将符号常量定义为static(c++有无static无所谓),并初始化,例如:
    //CommDef.h
    static const int MAX=1024;
    然后每一个使用它的编译单元包含该头文件即可。
    方法2:在某个公用的头文件中将符号常量声明 为extern,例如
    //CommDef.h
    extern const int MAX;
    并且在某一个源文件中定义一次:
    const int MAX=1024;
    然 后每一个使用它的编译单元包含上述头文件即可。

    方法1的优点是维护方便,但是由于每一个符号常量在每一个包含了它们的编译单元内都存在一份独立的拷贝,若修改常量的初值则将影响到多个编译单元而导致必须重新编译,而且浪费空间。
    方法2的优点是节约存储、编译后修改再编译节省时间,但维护比较不便。

    断言

    23.C++/C语言,要取得一个变量或对象的内存地址的通用方法是:强制转换为void*,然后输出。

    24.若输入参数以值传递的方式传递对象,则宜改用"const &"方式来传递,因为引用的创建和销毁不会调用对象的构造和析构函数,从而可提高效率。若函数的返回值是一个对象,有些场合可以使用“返回引用”替换“返回对象值”。而有时只能返回对象值。

    25.不要将正常值和错误标志混在一起返回,建议正常值用输出参数获得,而错误标志用return语句返回,另外一种方法是将正常情况下的返回值和错误标志绑定成一个键值对<value,bool>,例如std::map的insert()方法。

    26.有时候函数原本不需要返回值,但是为了增加灵活性,如支持链式表达可以附加返回值,比如strcpy。

    27.标准C语言有4种存储类型,即:extern ,auto , static , register ,分为永久生存——extern和static,以及临时生存期限——auto和register。一个变量或函数只能有一种存储类型。

    28. 连接类型有三种:外、内、无,表明了一个标识符的可见性,所以常常和作用域的概念混淆。所谓外连接,就是这个标识符可以在其他编译单元中或者在定义它的编 译单元中的其他范围内被调用。它需要在运行时分配空间。所外内连接指一个标识符能在定义它的编译单元中的其他范围内被调用,但是不能在其他的编译单元中被 调用。无连接指的是只能在声明它的范围内被调用。

    29.assert不是函数而是宏,是一个完全无害的测试手段。

    出错是程序员的错误,比如说程序员误传进了一个NULL指针,传进去了一个NULL的窗口句柄,或者编写不当,而不是程序使用者(用
    户)的操作错误。在发行版本(Release)中,可以定义NDEBUG宏来取消所有断言。所以,断言不能够完全代替参数检查。

    30. const 只能修饰函数的输入参数,输入参数若是使用“指针传递”则使用const进行保护,例如void Stringcpy(char *strDest, const char *strSrc),若是也想保护指针本身则可以在指针前加const:void Stringcpy(char *strDest, const char* const strSrc)。若是“值传递”,但是多次使用到传递进来的初值,则也可以加上const,保证代码不会无意修改它。而定义诸如void Func1(A a)这样的函数一定是效率比较低的,因为函数体内将产生A类型的临时变量来拷贝a,而临时变量的构造、拷贝和析构都有const,所以可以使用传引用—— 只借助参数的别名,本质上是传递地址,此时需要加上const来进行保护:void Vunc1(const A &a),对于基本数据类型,这样的操作完全是没有必要的。另外,若是给返回指针的函数返回值前加上const,则返回值是一个契约性常量,不能被 直接修改,返回值只能被赋值给有const修饰的同类型指针。
    参考文献:
    http://blog.sina.com.cn/s/blog_60be7ec80100gzhm.html
    http://blog.csdn.net/wengyb/archive/2008/02/21/2112120.aspx

    作者:gnuhpc
    出处:http://www.cnblogs.com/gnuhpc/


                   作者:gnuhpc
                   出处:http://www.cnblogs.com/gnuhpc/
                   除非另有声明,本网站采用知识共享“署名 2.5 中国大陆”许可协议授权。


    分享到:

  • 相关阅读:
    笔试题总结
    ubuntu 14.04 vim install youcompleteme
    c语言位域
    strcmp函数的使用
    Spring多数据源的配置和使用
    根据出生日期计算年龄的sql各种数据库写法
    BZOJ3165 : [Heoi2013]Segment
    BZOJ2725 : [Violet 6]故乡的梦
    BZOJ2851 : 极限满月
    BZOJ2837 : 小强的形状
  • 原文地址:https://www.cnblogs.com/gnuhpc/p/2811968.html
Copyright © 2011-2022 走看看