查看: 3225|回复: 4

C C++ 关键字讨论 欢迎大手的到来

[复制链接]
  • TA的每日心情
    奋斗
    2014-6-4 13:58
  • 签到天数: 188 天

    连续签到: 1 天

    [LV.7]常住居民III

    发表于 2012-11-10 10:14:59 | 显示全部楼层 |阅读模式
    分享到:
    本帖最后由 zwj108 于 2012-11-10 10:17 编辑

    也许大家在看STM32 库函数的时候会发现很多关键字{:soso_e135:},下面讨论了C和C++中的关键字,欢迎各位大手{:soso_e179:}前来指点,下面有说的不对的,或者你有更好的见解,欢迎跟帖。或者我们有啥不懂的也可以跟帖,期待大侠的到来。下面当然是我从网上找的啦{:soso_e151:}。{:soso_e124:}帖子很长哦..........
    C/C++中关键字讨论
    1 基本解释
         extern可以置于
    变量或者函数前,以标示变量或者函数的定义在别的文件中提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外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生成的目标代码中找到此函数。

    2 问题:extern 变量  在一个源文件里定义了一个数组:char a[6];
      在另外一个文件里用下列语句进行了声明:extern char *a;

      请问,这样可以吗?
      答案与分析:

      1)、不可以,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类型T的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a[ ]
      2)、例子分析如下,如果a[] = "abcd",则外部变量a=0x61626364 (abcd的ASCII码值),*a显然没有意义
      显然a指向的空间(0x61626364)没有意义,易出现非法内存访问。
      3)、这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错误屡见不鲜。
      4)、extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明
    3 问题单方面修改extern 函数原型
      当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会照成系统错误,这种情况应该如何解决?
      答案与分析:
      目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用方include该头文件,从而省去extern这一步。以避免这种错误。
      宝剑有双锋,对extern的应用,不同的场合应该选择不同的做法。
    4 问题:extern “C”  在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?
      答案与分析:
      C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名
      下面是一个标准的写法:
    //在.h文件的头上
    #ifdef __cplusplus
    #if __cplusplus
    extern "C"{
     #endif
     
    #endif /* __cplusplus */
     

     

     //.h文件结束的地方

     #ifdef __cplusplus
     
    #if __cplusplus
    }
    #endif
    #endif /* __cplusplus */

    5 问题:extern 函数声明  常常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在函数的声明中起什么作用?
      答案与分析:
      如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有明显的区别:
    extern int f(); int f();
      当然,这样的用处还是有的,就是在程序中取代include “*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰。关于这样做的原因和利弊可见下面的这个例子:“用extern修饰的全局变量”

        (1) 在test1.h中有下列声明:
        #ifndef TEST1H
        #define TEST1H
        extern char g_str[]; // 声明全局变量
    g_str
        void fun1();
        #endif
        (2) 在test1.cpp中

        #include "test1.h"
            char g_str[] = "123456"; //
    定义全局变量g_str
            void fun1() { cout << g_str << endl; }
        (3) 以上是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这个编译单元 不需要再有一份了,不然会在连接时报告重复定义这个错误
    !
        (4) 有些人喜欢
    把全局变量的声明和定义放在一起,这样可以防止忘记了定义,如把上面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修饰,这样的话你光声明的函数就要一大串,而且头文件的作用就是要给外部提供接口使用的,所以 请记住, 只在头文件中做声明,真理总是这么简单

    6. extern static (1) extern 表明该变量在别的地方已经定义过了,在这里要使用那个变量.
    (2) static 表示静态的变量,分配内存的时候, 存储在静态区,不存储在栈上面.
        static 作用范围是内部连接的关系, 和extern有点相反.它和对象本身是分开存储的,extern也是分开存储的,但是extern可以被其他的对象用extern 引用,而static 不可以,只允许对象本身用它. 具体差别首先,static与extern是一对“水火不容”的家伙,也就是说extern和static不能同时修饰一个变量;其次,static修饰的全局变量声明与定义同时进行也就是说当你在头文件中使用static声明了全局变量后,它也同时被定义了;最后,static修饰全局变量的作用域只能是本身的编译单元也就是说它的“全局”只对本编译单元有效,其他编译单元则看不到它,如:
        (1) test1.h:
        #ifndef TEST1H
        #define TEST1H
        static char g_str[] = "123456";
        void fun1();
        #endif

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

        (2) test2.cpp
        #include "test1.h"
        void fun2()  {  cout << g_str << endl;  }
        (3) void main()     {
            fun1(); // a23456
            fun2(); // 123456
        }
        这个时候你在跟踪代码时,就会发现两个编译单元中的g_str地址并不相同,因为你在一处修改了它,所以编译器被强行的恢复内存的原貌,在内存中存在了两份拷贝给两个模块中的变量使用。正是因为static有以上的特性,
    所以一般定义static全局变量时,都把它放在原文件中而不是头文件,这样就不会给其他模块造成不必要的信息污染,同样记住这个原则吧!

    7. extern const   C++中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"
    为什么使用const?采用符号常量写出的代码更容易维护;指针常常是边读边移动,而不是边写边移动;许多函数参数是只读不写的。const最常见用途是作为数组的界和switch分情况标号(也可以用枚举符代替),分类如下:
      常变量:  const 类型说明符 变量名
      常引用:  const 类型说明符 &引用名
      常对象:  类名 const 对象名
      常成员函数:  类名::fun(形参) const
      常数组:  类型说明符 const 数组名[大小]   
      常指针:  const 类型说明符* 指针名,类型说明符* const 指针名
    首先提示的是:在常变量(const 类型说明符 变量名)、常引用(const 类型说明符 &引用名)、常对象(类名 const 对象名)、 常数组(类型说明符 const 数组名[大小]), const” 与 “类型说明符”或“类名”(其实类名是一种自定义的类型说明符) 的位置可以互换。如:
         const int a=5; int const a=5; 等同
         类名 const 对象名 const 类名 对象名 等同
    用法1:常量
        取代了C中的宏定义,声明时必须进行初始化(!c++类中则不然)。const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。―《C++ Program Language》
        用const声明的变量虽然增加了分配空间,但是可以保证类型安全。
        C标准中,const定义的常量是全局的,C++中视声明位置而定
    用法2:指针和常量
        使用指针时涉及到两个对象:该指针本身和被它所指的对象。将一个指针的声明用const“预先固定”将使那个对象而不是使这个指针成为常量。要将指针本身而不是被指对象声明为常量,必须使用声明运算符*const。
        所以出现在 * 之前的const是作为基础类型的一部分:
    char *const cp; //到char的const指针
    char const *pc1; //到const char的指针
    const char *pc2; //到const char的指针(后两个声明是等同的)
    从右向左读的记忆方式: cp is a const pointer to char. 故pc不能指向别的字符串,但可以修改其指向的字符串的内容
    pc2 is a pointer to const char.
    故*pc2的内容不可以改变,但pc2可以指向别的字符串
    且注意:允许把非 const 对象的地址赋给指向 const 对象的指针,不允许把一个 const 对象的地址赋给一个普通的、非 const 对象的指针
    用法3:const修饰函数传入参数
        将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
        通常修饰指针参数和引用参数:
    void Fun( const A *in); //修饰指针型传入参数
    void Fun(const A &in); //修饰引用型传入参数
    用法4:修饰函数返回值
        可以阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。
    用法5:const修饰成员函数(c++特性)
    const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;

    const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;
    const成员函数不可以修改对象的数据不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。
    具体展开来讲:
    (一). 常量与指针
    常量与指针放在一起很容易让人迷糊。对于常量指针和指针常量也不是所有的学习C/C++的人都能说清除。例如:
        const int *m1 = new int(10);
        int* const m2 = new int(20);
    在上面的两个表达式中,最容易让人迷惑的是const到底是修饰指针还是指针指向的内存区域?其实,只要知道:const只对它左边的东西起作用, 唯一的例外就是const本身就是最左边的修饰符,那么它才会对右边的东西起作用。根据这个规则来判断,m1应该是常量指针(即,不能通过m1来修改它所 指向的内容。);而m2应该是指针常量(即,不能让m2指向其他的内存模块)。由此可见:
       1. 对于常量指针,不能通过该指针来改变所指的内容。即,下面的操作是错误的:
          int i = 10;
          const int *pi = &i;
          *pi = 100;
          因为你在试图通过pi改变它所指向的内容。但是,并不是说该内存块中的内容不能被修改。我们仍然可以通过其他方式去修改其中的值。例如:
          // 1: 通过i直接修改。
          i = 100;
          // 2: 使用另外一个指针来修改。
          int *p = (int*)pi;
          *p = 100;
          实际上,在将程序载入内存的时候,会有专门的一块内存区域来存放常量。但是,上面的i本身不是常量,是存放在栈或者堆中的。我们仍然可以修改它的值。而pi不能修改指向的值应该说是编译器的一个限制。
       2. 根据上面const的规则,const int *m1 = new int(10);我们也可写作:
          int const *m1 = new int(10);
          这是,理由就不须作过多说明了。
       3. 函数参数中指针常量时表示不允许将该指针指向其他内容
          void func_02(int* const p)
          {
          int *pi = new int(100);
          //错误!P是指针常量。不能对它赋值。
          p = pi;
          }
          int main()
          {
          int* p = new int(10);
          func_02(p);
          delete p;
          return 0;
          }
       4. 在函数参数中使用常量指针时表示在函数中不能改变指针所指向的内容。
        void func(const int *pi)
        {
        //错误!不能通过pi去改变pi所指向的内容!
        *pi = 100;
        }
        int main()
        {
        int* p = new int(10);
        func(p); 
        delete p;
        return 0;
        }
      我们可以使用这样的方法来防止函数调用者改变参数的值。但是,这样的限制是有限的,作为参数调用者,我们也不要试图去改变参数中的值。因此,下面的操作是在语法上是正确的,但是可能破还参数的值:
        #include <iostream>
        #include <string>
        void func(const int *pi)
        {
        //这里相当于重新构建了一个指针,指向相同的内存区域。当然就可以通过该指针修改内存中的值了。
       int* pp = (int*)pi;
        *pp = 100;
        }
        int main()
        {
        using namespace std;
        int* p = new int(10);
        cout << "*p = " << *p << endl;
        func(p);
        cout << "*p = " << *p << endl;
        delete p;
        return 0;
        }
    (二):常量与引用
        常量与引用的关系稍微简单一点。因为引用就是另一个变量的别名,它本身就是一个常量。也就是说不能再让一个引用成为另外一个变量的别名, 那么他们只剩下代表的内存区域是否可变。即:
        int i = 10;
        // 正确:表示不能通过该引用去修改对应的内存的内容。
        const int& ri = i;
        // 错误!不能这样写。
        int& const rci = i;
        由此可见,如果我们不希望函数的调用者改变参数的值。最可靠的方法应该是使用引用。下面的操作会存在编译错误:
        void func(const int& i)
        {
        // 错误!不能通过i去改变它所代表的内存区域。
        i = 100;
        }
        int main()
        {
        int i = 10;
        func(i);
        return 0;
        }
        这里已经明白了常量与指针以及常量与引用的关系。但是,有必要深入的说明以下。在系统加载程序的时候,系统会将内存分为4个区域:堆区 栈区全局区(静态)和代码区。从这里可以看出,对于常量来说,系统没有划定专门的区域来保护其中的数据不能被更改。也就是说,使用常量的方式对数据进行保护是通过编译器作语法限制来实现的。我们仍然可以绕过编译器的限制去修改被定义为“常量”的内存区域。看下面的代码:
        const int i = 10;
        // 这里i已经被定义为常量,但是我们仍然可以通过另外的方式去修改它的值。
        // 这说明把i定义为常量,实际上是防止通过i去修改所代表的内存。
        int *pi = (int*) &i;
    (三):常量函数
        常量函数是C++对常量的一个扩展,它很好的确保了C++中类的封装性。在C++中,为了防止类的数据成员被非法访问,将类的成员函数分成了两类,一类是常量成员函数(也被称为观察着);另一类是非常量成员函数(也被成为变异者)。在一个函数的签名后面加上关键字const后该函数就成了常量函数。对于常量函数,最关键的不同是编译器不允许其修改类的数据成员。例如:
        class Test
        {
        public:
        void func() const;
        private:
        int intValue;
        };
        void Test::func() const
        {
        intValue = 100;
        }
        上面的代码中,常量函数func函数内试图去改变数据成员intValue的值,因此将在编译的时候引发异常
        当然,对于非常量的成员函数,我们可以根据需要读取或修改数据成员的值。但是,这要依赖调用函数的对象是否是常量。通常,如果我们把一个类定义为常量,我们的本意是希望他的状态(数据成员)不会被改变。那么,如果一个常量的对象调用它的非常量函数会产生什么后果呢?看下面的代码:
        class Fred{
        public:
        void inspect() const;
        void mutate();
        };
        void UserCode(Fred& changeable, const Fred& unChangeable)
        {
        changeable.inspect(); // 正确,非常量对象可以调用常量函数
        changeable.mutate(); // 正确,非常量对象也允许修改调用非常量成员函数修改数据成员。
        unChangeable.inspect(); // 正确,常量对象只能调用常理函数。因为不希望修改对象状态。
        unChangeable.mutate(); // 错误!常量对象的状态不能被修改,而非常量函数存在修改对象状态的可能
        }
        从上面的代码可以看出,由于常量对象的状态不允许被修改,因此,通过常量对象调用非常量函数时将会产生语法错误。实际上,我们知道每个成员函数都有一个隐含的指向对象本身的this指针。而常量函数则包含一个this的常量指针。如下:
        void inspect(const Fred* this) const;
        void mutate(Fred* this);
         也就是说对于常量函数,我们不能通过this指针去修改对象对应的内存块。但是,在上面我们已经知道,这仅仅是编译器的限制,我们仍然可以绕过编译器的限制,去改变对象的状态。看下面的代码:
        class Fred{
        public:
        void inspect() const;
        private:

        int intValue;
        };
        void Fred::inspect() const
        {
        cout << "At the beginning. intValue = "<< intValue << endl;
        // 这里,我们根据this指针重新定义了一个指向同一块内存地址的指针
        // 通过这个新定义的指针,我们仍然可以修改对象的状态。
        Fred* pFred = (Fred*)this;
        pFred->intValue = 50;
        cout << "Fred::inspect() called. intValue = "<< intValue << endl;
        }
        int main()
        {
        Fred fred;
        fred.inspect();
        return 0;
        }
        上面的代码说明,只要我们愿意,我们还是可以通过常量函数修改对象的状态。同理,对于常量对象,我们也可以构造另外一个指向同一块内存的指针去修改它的状态。这里就不作过多描述了。
        另外,也有这样的情况,虽然我们可以绕过编译器的错误去修改类的数据成员。但是C++也允许我们在数据成员的定义前面加上mutable以允许该成员可以在常量函数中被修改。例如:
        class Fred{
        public:
        void inspect() const;
        private:
        mutable int intValue;
        };
        void Fred::inspect() const
        {
        intValue = 100;
        }
        但是,并不是所有的编译器都支持mutable关键字。这个时候我们上面的歪门邪道就有用了。
        关于常量函数,还有一个问题是重载。
        #include <iostream>
        #include <string>
        using namespace std;
        class Fred{
        public:
        void func() const;
        void func();
        };
        void Fred::func() const
        {
        cout << "const function is called."<< endl;
        }
        void Fred::func()
        {
        cout << "non-const function is called."<< endl;
        }
        void UserCode(Fred& fred, const Fred& cFred)
        {
        cout << "fred is non-const object, and the result of fred.func() is:" << endl;
        fred.func();
        cout << "cFred is const object, and the result of cFred.func() is:" << endl;
        cFred.func();
        }
        int main()
        {
        Fred fred;
        UserCode(fred, fred);
        return 0;
        }
        输出结果为:
        fred is non-const object, and the result of fred.func() is:
        non-const function is called.
        cFred is const object, and the result of cFred.func() is:
        const function is called.
        从上面的输出结果,我们可以看出。当存在同名同参数和返回值的常量函数和非常量函数时,具体调用哪个函数是根据调用对象是常量对像还是非常量对象来决定的。常量对象调用常量成员;非常量对象调用非常量的成员。
        总之,我们需要明白常量函数是为了最大程度的保证对象的安全。通过使用常量函数,我们可以只允许必要的操作去改变对象的状态,从而防止误操作对对象状态的破坏。但是,就像上面看见的一样,这样的保护其实是有限的。关键还是在于我们开发人员要严格的遵守使用规则。另外需要注意的是常量对象不允许调用非常量的 函数。这样的规定虽然很武断,但如果我们都根据原则去编写或使用类的话这样的规定也就完全可以理解了。
    (四):常量返回值
         很多时候,我们的函数中会返回一个地址或者引用。调用这得到这个返回的地址或者引用后就可以修改所指向或者代表的对象。这个时候如果我们不希望这个函数的调用这修改这个返回的内容,就应该返回一个常量。这应该很好理解,大家可以去试试。
    +++++++++++++++++++++++++++++++++++++++
    c++ const const常量有数据类型,而宏常量没有数据类型+++++++++++++++++++++++++++++++++++++++
    1. const常量,如const int max = 100;  
    优点:const常量有数据类型,而
    宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误(边际效应)

    2.  const 修饰类的数据成员。如:
    class A
    {

        const int size;
        …
    }
    const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么。如
    class A
    {
    const int size = 100;    //错误
    int array[size];         //错误,未知的size
    }
    const数据成员的初始化只能在类的构造函数的初始化表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现。如

    class A
    {…
    enum {size1=100, size2 = 200 };
    int array1[size1];
    int array2[size2];
    }
    枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。
    3. const修饰指针的情况,见下式:
    int b = 500;
    const int* a = &                  [1]
    int const *a = &                  [2]
    int* const a = &                  [3]
    const int* const a = &       [4]

    如果你能区分出上述四种情况,那么,恭喜你,你已经迈出了可喜的一步。不知道,也没关系,我们可以参考《Effectivec++》Item21上 的做法,如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修 饰指针本身,即指针本身是常量。因此,[1]和[2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下不允许对内容进行更改操作,如不能*a = 3;[3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;[4]为指针本身和指向的内容均为常量
    4. const的初始化
    先看一下const变量初始化的情况
    1) 非指针const常量初始化的情况:
    A b;
    const A a = b;

    2) 指针const常量初始化的情况:
    A* d = new A();
    const A* c = d;
    或者:
    const A* c = new A();

    3)引用const常量初始化的情况:
    A f;
    const A& e = f;      // 这样作e只能访问声明为const的函数,而不能访问一般的成员函数;

        [思考1]: 以下的这种赋值方法正确吗?
        const A* c=new A();
        A* e = c;
        [思考2]: 以下的这种赋值方法正确吗?

        A* const c = new A();
        A* b = c;

    5. 另外const 的一些强大的功能在于它在函数声明中的应用。在一个函数声明中,const可以修饰函数的返回值,或某个参数;对于成员函数,还可以修饰是整个函数。有如 下几种情况,以下会逐渐的说明用法:A&operator=(const A& a);
    void fun0(const A* a );
    void fun1( ) const; // fun1( ) 为类成员函数

    const A fun2( );

    1) 修饰参数的const,如 void fun0(const A* a ); void fun1(const A& a);
    调 用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,如形参为const A*a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容;如形参为const A&a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。

    [注意]:
    参数const通常用于参数为指针或引用的情况,且只能修饰输入参数;若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。

    [总结对于非内部数据类型的输入参数,因该将“值传递”的方式改为“const引用传递”,目的是为了提高效率。例如,将void Func(A a)改为void Func(const A &a)
          对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x)不应该改为void Func(const int &x)
    2)  修饰返回值的const,如const A fun2( ); const A* fun3( );
    这样声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。
    const Rational operator*(const Rational& lhs, const Rational& rhs)
    {
    return Rational(lhs.numerator() * rhs.numerator(),
    lhs.denominator() * rhs.denominator());
    }

    返回值用const修饰可以防止允许这样的操作发生:Rational a,b;
    Radional c;
    (a*b) = c;

    一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。
    [总结]

    1.  一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对 某个对象引用的情况。原因如下:如果返回值为某个对象为const(const A test = A实例)或某个对象的引用为const(const A& test = A实例),则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情 况下很少用到。
    2.  如果给采用“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。如:
    const char * GetString(void);
    如下语句将出现编译错误:
    char *str=GetString();
    正确的用法是:
    const char *str=GetString();
    3.     函数返回值采用“引用传递”的场合不多,这种方式一般只出现在类的赙值函数中,目的是为了实现链式表达。如:
    class A
    {…
    A &operate = (const A &other);  //负值函数
    }
    A a,b,c;              //a,b,c为A的对象


    a=b=c;            //正常
    (a=b)=c;          //不正常,但是合法
    若负值函数的返回值加const修饰,那么该返回值的内容不允许修改,上例中a=b=c依然正确。(a=b)=c就不正确了。
    [思考3]: 这样定义赋值操作符重载函数可以吗?
    const A& operator=(const A& a);

    6.     类成员函数中const的使用一般放在函数体后,形如:void fun() const;
    任何不会修改数据成员的函数都因该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将报错,这大大提高了程序的健壮性。如:

    class Stack
    {
    public:
          void Push(int elem);
          int Pop(void);
          int GetCount(void) const;   //const 成员函数
    private:
          int m_num;
          int m_data[100];
    };
    int Stack::GetCount(void) const
    {
      ++m_num;              //编译错误,企图修改数据成员m_num
      Pop();                    //编译错误,企图调用非const函数
      Return m_num;
    }
    7. 使用const的一些建议 1) 要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
    2) 要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;

    3) 在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;

    4) const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;

    5) 不要轻易的将函数的返回值类型定为
    const;
    6) 除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;

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

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

    3) 这种做法不正确;

    在const A:perator=(const A& a)中,参数列表中的const的用法正确,而当这样连续赋值的时侯,问题就出现了:

    A a,b,c:
    (a=b)=c;
    因为a.operator=(b)的返回值是对a的const引用,不能再将c赋值给const常量。

    ++++++++++++++++++++++++++++++++++++++++
    const cc++中的区别 http://tech.e800.com.cn/articles/2009/722/1248229886744_1.html
    ++++++++++++++++++++++++++++++++++++++++
    1. C++中的const正常情况下是看成编译期的常量,编译器并不为const分配空间,只是在编译的时候将期值保存在名字表中,并在适当的时候折合在代码中.所以,以下代码:
    using namespace std;
    int main()
    {
    const int a = 1;
    const int b = 2;
    int array[ a + b ] = {0};
    for (int i = 0; i < sizeof array / sizeof *array; i++)
    {
    cout << array << endl;
    }
    }
    在可以通过编译,并且正常运行.但稍加修改后,放在C编译器中,便会出现错误
    :
    int main()
    {
    int i;
    const int a = 1;
    const int b = 2;
    int array[ a + b ] = {0};
    for (i = 0; i < sizeof array / sizeof *array; i++)
    {
    printf("%d",array);
    }
    }
    错误消息
    :
    c:\test1\te.c(8): error C2057: 应输入常数表达式

    c:\test1\te.c(8): error C2466: 不能分配常数大小为 0 的数组
    出现这种情况的原因是:在C中,const是一个不能被改变的普通变量,既然是变量,就要占用存储空间,所以编译器不知道编译时的值.而且,数组定义时的下标必须为常量.
    2. 在C语言中: const int size; 这个语句是正确的,因为它被C编译器看作一个声明,指明在别的地方分配存储空间.但在C++中这样写是不正确的
    .C++中const默认是内部连接,如果想在C++中达到以上的效果,必须要用extern关键字.即C++中,const默认使用内部连接.而C中使用外部连接.

    (1) 内连接:编译器只对正被编译的文件创建存储空间,别的文件可以使用相同的表示符或全局变量.C/C++中内连接使用static关键字指定.
    (2) 外连接:所有被编译过的文件创建一片单独存储空间.一旦空间被创建,连接器必须解决对这片存储空间的引用.全局变量和函数使用外部连接.通过extern关键字声明,可以从其他文件访问相应的变量和函数
    .
    /* C++代码
      header.h */
    const int test = 1;
    /* C++代码
      test1.cpp */
    #include "header.h"
    using namespace std;
    int main() { cout << "in test1 :" << test << endl; }
    /* C++代码
    test2.cpp */
    #include "header.h"
    using namespace std;
    void print() { cout << "in test2:" << test << endl;}
    以上代码编译连接完全不会出问题,但如果把header.h改为
    :
    extern const int test = 1;
    在连接的时候,便会出现以下错误信息
    :
    test2 error LNK2005: "int const test" (
    ?test@@3HB) 已经在 test1.obj 中定义
        因为extern关键字告诉C++编译器test会在其他地方引用,所以,C++编译器就会为test创建存储空间,不再是简单的存储在名字表里面.所以,当两个文件同时包含header.h的时候,会发生名字上的冲突.
    此种情况和C中const含义相似
    :
    /* C代码
    header.h */
    const int test = 1;
    /* C代码
    test1.c */
    #include "header.h"
    int main() { printf("in test1:%d\n",test); }
    /* C代码
    test2.c */
    #include "header.h"
    void print() { printf("in test2:%d\n",test); }
    错误消息
    :
    test3 fatal error LNK1169: 找到一个或多个多重定义的符号

    test3 error LNK2005: _test 已经在 test1.obj 中定义
    也就是说:在c++ 中const 对象默认为文件的局部变量。与其他变量不同,除非特别说明,在全局作用域声明的 const 变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。通过指定 const 变更为 extern,就可以在整个程序中访问 const 对象:
          // file_1.cc
          // defines and initializes a const that is accessible to other files
          extern const int bufSize = fcn();
          // file_2.cc
          extern const int bufSize; // uses bufSize from file_1
          // uses bufSize defined in file_1
          for (int index = 0; index != bufSize; ++index)
                // ...

    3. C++中,是否为const分配空间要看具体情况.如果加上关键字extern或者取const变量地址,则编译器就要为const分配存储空间.
    4. C++中定义常量的时候不再采用define,因为define只做简单的宏替换,并不提供类型检查.
    Cvolatile关键字volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数 据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。下面举例说明。在DSP开发中,经常需要等待某个事件的触发,所以经常会写出这样的程序:
    short flag;
    void test()
    {
    do1();
    while(flag==0);
    do2();
    }

    这段程序等待内存变量flag的值变为1(怀疑此处是0,有点疑问,)之后才运行do2()。变量flag的值由别的程序更改,这个程序可能是某个硬件中 断服务程序。例如:如果某个按钮按下的话,就会对DSP产生中断,在按键中断程序中修改flag为1,这样上面的程序就能够得以继续运行。但是,编译器并 不知道flag的值会被别的程序修改,因此在它进行优化的时候,可能会把flag的值先读入某个寄存器,然后等待那个寄存器变为1。如果不幸进行了这样的优化,那么while循环就变成了死循环,因为寄存器的内容不可能被中断服务程序修改。为了让程序每次都读取真正flag变量的值,就需要定义为如下形 式:
    volatile short flag;
       
    需要注意的是,没有volatile也可能能正常运行,但是可能修改了编译器的优化级别之后就又不能正常运行了。因此经常会出现debug版本正常,但是 release版本却不能正常的问题。所以为了安全起见,只要是等待别的程序修改某个变量的话,就加上volatile关键字。

    volatile的本意是“易变的”
          由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:
    static int i=0;
    int main(void)
    {
    ...
    while (1)
    {
    if (i) do_something();
    }
    }
    /* Interrupt service routine. */
    void ISR_2(void)
    {
    i=1;
    }
       
    程序的本意是希望ISR_2中断产生时,在main当中调用do_something函数,但是,由于编译器判断在main函数里面没有修改过i,因此可 能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致do_something永远也不会被调用。如果变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。
    volatile用在如下的几个地方:
    1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
         2、多任务环境下各任务间共享的标志应该加volatile;
         3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
    另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。
    二、volatile 的含义
         volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以死代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化,volatile的字面含义是易变的,它有下面的作用:
    1.
    不会在两个操作之间把volatile变量缓存在寄存器中。在多任务、中断、甚至setjmp环境下,变量可能被其他的程序改变,编译器自己无法知道,volatile就是告诉编译器这种情况。

    2 .不做常量合并、常量传播等优化,所以像下面的代码:
    volatile int i = 1;
    if (i > 0) ...
    if的条件不会当作无条件真。

    3 .
    对volatile变量的读写不会被优化掉。如果你对一个变量赋值但后面没用到,编译器常常可以省略那个赋值操作,然而对Memory Mapped IO的处理是不能这样优化的。

        前面有人说volatile可以保证对内存操作的原子性,这种说法不大准确,其一,x86需要LOCK前缀才能在SMP下保证原子性,其二,RISC根本不能对内存直接运算,要保证原子性得用别的方法,如atomic_inc。

        对于jiffies,它已经声明为volatile变量,我认为直接用jiffies++就可以了,没必要用那种复杂的形式,因为那样也不能保证原子性。

        你可能不知道在Pentium及后续CPU中,下面两组指令

    inc jiffies
    ;;
    mov jiffies, %eax
    inc %eax
    mov %eax, jiffies
    作用相同,但一条指令反而不如三条指令快。

    三、编译器优化 → C关键字volatile → memory破坏描述符zz
        “memory”比较特殊,可能是内嵌汇编中最难懂部分。为解释清楚它,先介绍一下编译器的优化知识,再看C关键字volatile。最后去看该描述符。
    1、编译器优化介绍

         内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。另外在现代CPU中指令的执行并不一 定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。以上是硬件级别的优化。再看软件一级的优化:一种是在 编写代码时由程序员优化,另一种是由编译器进行优化。
    编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令对 常规内存进行优化的时候,这些优化是透明的,而且效率很好。由编译器优化或者硬件重新排序引起的问题的解决办法是在从硬件(或者其他处理器)的角度看必须以特定顺序执行的操作之间设置内存屏障(memory barrier),linux 提供了一个宏解决编译器的执行顺序问题。
    void Barrier(void)
         这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前CPU寄存器中的所有修改过的数值存入内存,需要这些数据的时候再重新从内存中读出。

    2、C语言关键字
    volatile
         C语言关键字volatile(注意它是用来修饰变量而不是上面介绍的__volatile__)
    表明某个变量的值可能在外部被改变因此对这些变量的 存取不能缓存到寄存器,每次使用时需要重新存取。该关键字在多线程环境下经常使用,因为在编写多线程的程序时,同一个变量可能被多个线程修改,而程序通过该变量同步各个线程,例如:

    DWORD __stdcall threadFunc(LPVOID signal)
    {
    int* intSignal=reinterpret_cast<int*>(signal);
    *intSignal=2;
    while(*intSignal!=1)
    sleep(1000);
    return 0;
    }
         该线程启动时将intSignal 置为2,然后循环等待直到intSignal 为1 时退出。显然intSignal的值必须在外部被改变,否则该线程不会退出。但是实际运行的时候该线程却不会退出,即使在外部将它的值改为1,看一下对应 的伪汇编代码就明白了:

    mov ax,signal
    label:
    if(ax!=1)
    goto label
         对于C编译器来说,它并不知道这个值会被其他线程修改。自然就把它cache在寄存器里面。记住,C 编译器是没有线程概念的!这时候就需要用到volatile。volatile 的本意是指:这个值可能会在当前线程外部被改变。也就是说,我们要在threadFunc中的intSignal前面加上volatile关键字,这时 候,编译器知道该变量的值会在外部改变,因此每次访问该变量时会重新读取,所作的循环变为如下面伪码所示:

    label:
    mov ax,signal
    if(ax!=1)
    goto label
    3、
    Memory
          有了上面的知识就不难理解Memory修改描述符了,Memory描述符告知GCC:

    1)不要将该段内嵌汇编指令与前面的指令重新排序;也就是在执行内嵌汇编代码之前,它前面的指令都执行完毕

    2)不要将变量缓存到寄存器,因为这段代码可能会用到内存变量,而这些内存变量会以不可预知的方式发生改变,因此GCC插入必要的代码先将缓存到寄存器的变量值写回内存,如果后面又访问这些变量,需要重新访问内存。

         如果汇编指令修改了内存,但是GCC 本身却察觉不到,因为在输出部分没有描述,此时就需要在修改描述部分增加“memory”,告诉GCC 内存已经被修改,GCC 得知这个信息后,就会在这段指令之前,插入必要的指令将前面因为优化Cache 到寄存器中的变量值先写回内存,如果以后又要使用这些变量再重新读取。

         使用“volatile”也可以达到这个目的,但是我们在每个变量前增加该关键字,不如使用“memory”方便

    volatile 和 const
      正像上面所描述的那样,volatile的意思是“易变的”。C/C++中还有另外一个 关键词叫const,用来限定变量不可被改变。似乎volatile和const意思相反,水火不容。但volatile const int N = 0;这样的语句是允许存在的。为什么呢?其实,这个语句可以这样来理解:变量N在本程序(或者设备)中是不可(禁止)改变的,但它可能会被其它程序(或者 设备)改变。你或许知道,const只是编译器的限定,而没有执行期的检查,因此const也只是限于单个进程,因此volatile和const这两个 关键词并不冲突。
      另外,普通的整型const变量,比如const int N = 10; int a[N]; 中,变量N可能并不会被分配内存(除非程序里面有显式使用N地址的表达式)。但是,根据volatile变量的特性,volatile const int N = 10;中变量N是一定会占用内存的。这一点,亦可以通过观察相应的汇编代码来验证。
    typedefdefine具体的详细区别
    1) #define是预处理指令,在编译预处理时进行简单的替换,不作正确性检查,不关含义是否正确照样带入,只有在编译已被展开的源程序时才会发现可能的错误并报错。例如:
    #define PI 3.1415926
    程序中的:area=PI*r*r 会替换为
    3.1415926*r*r
    如果你把#define语句中的数字9 写成字母g 预处理也照样带入。


    2)
    typedef是在编译时处理的。它在自己的作用域内给一个已经存在的类型一个别名,但是You cannot use the typedef specifier inside a function definition。

    3)
    typedef int * int_ptr;

    #define int_ptr int *
    作用都是用int_ptr代表 int * ,但是二者不同,正如前面所说,
    #define在预处理 时进行简单的替换而typedef不是简单替换 ,而是采用如同定义变量的方法那样来声明一种类型。也就是说;

    //refer to (xzgyb(老达摩
    ))
    #define int_ptr int *
    int_ptr a, b; //相当于int * a, b; 只是简单的宏替换


    typedef int* int_ptr;
    int_ptr a, b; //a, b 都为指向int的指针,typedef为int* 引入了一个新的助记符



    这也说明了为什么下面观点成立

    //QunKangLi(维护成本与程序员的创造力的平方成正比
    )
    typedef int * pint ;
    #define PINT int *

    那么:

    const pint p ;//p不可更改,但p指向的内容可更改

    const PINT p ;//p可更改,但是p指向的内容不可更改。


    pint是一种指针类型 const pint p 就是把指针给锁住了 p不可更改

    而const PINT p 是const int * p 锁的是指针p所指的对象。
    3也许您已经注意到#define 不是语句 不要在行末加分号,否则 会连分号一块置换

    一、typedef的用法
        在C/C++语言中,typedef常用来定义一个标识符及关键字的别名,它是语言编译过程的一部分,但它并不实际分配内存空间,实例像:
    typedef int INT;
    typedef int ARRAY[10];
    typedef (int*) pINT;
        typedef可以增强程序的可读性,以及标识符的灵活性,但它也有“非直观性”等缺点。
    二、#define的用法
        #define为一宏定义语句,通常用它来定义常量(包括无参量与带参量),以及用来实现那些“表面似和善、背后一长串”的宏,它本身并不在编译过程中进行,而是在这之前(预处理过程)就已经完成了,但也因此难以发现潜在的错误及其它代码维护问题,它的实例像:
    #define  INT int   
    #define TRUE         
        在Scott Meyer的Effective C++一书的条款1中有关于#define语句弊端的分析,以及好的替代方法,大家可参看。
    三、typedef与#define的区别
        从以上的概念便也能基本清楚,typedef只是为了增加可读性而为标识符另起的新名称(仅仅只是个别名),而#define原本在C中是为了定义常量, 到了C++,const、enum、inline的出现使它也渐渐成为了起别名的工具。有时很容易搞不清楚与typedef两者到底该用哪个好,如 #define INT int这样的语句,用typedef一样可以完成,用哪个好呢?我主张用typedef,因为在早期的许多C编译器中这条语句是非法的,只是现今的编译器 又做了扩充。为了尽可能地兼容,一般都遵循#define定义“可读”的常量以及一些宏语句的任务,而typedef则常用来定义关键字、冗长的类型的别名。
        宏定义只是简单的字符串代换(原地扩展),而typedef则不是原地扩展,它的新名字具有一定的封装性,以致于新命名的标识符具有更易定义变量的功能。请看上面第一大点代码的第三行:
    typedef    (int*)      pINT;
    以及下面这行:#define    pINT2   int*
        效果相同?实则不同!实践中见差别:pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量。而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b。
        注意:两者还有一个行尾;号的区别哦!
    回复

    使用道具 举报

  • TA的每日心情
    无聊
    2016-12-20 15:08
  • 签到天数: 510 天

    连续签到: 1 天

    [LV.9]以坛为家II

    发表于 2012-11-10 12:46:53 | 显示全部楼层
    好长啊,还没看完,先MARK下吧
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    2015-9-1 22:24
  • 签到天数: 497 天

    连续签到: 1 天

    [LV.9]以坛为家II

    发表于 2013-6-12 13:45:24 | 显示全部楼层
    竟然在第二次看完了,虽然没有完全理解
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    2014-6-4 13:58
  • 签到天数: 188 天

    连续签到: 1 天

    [LV.7]常住居民III

     楼主| 发表于 2013-6-12 15:08:02 | 显示全部楼层
    Xy201207 发表于 2013-6-12 13:45
    竟然在第二次看完了,虽然没有完全理解

    哈哈  慢慢来啊......................................
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    2024-9-30 02:33
  • 签到天数: 444 天

    连续签到: 1 天

    [LV.9]以坛为家II

    发表于 2017-10-20 10:02:27 | 显示全部楼层
    自己上个月做过C++的一个小项目  用到了全局函数 全局变量   还有可怕的指针     真的是怕了C++     后来赶紧转到了 c#   上手非常快!灰常好!  
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /4 下一条

    手机版|小黑屋|与非网

    GMT+8, 2024-11-26 22:43 , Processed in 0.180993 second(s), 23 queries , MemCache On.

    ICP经营许可证 苏B2-20140176  苏ICP备14012660号-2   苏州灵动帧格网络科技有限公司 版权所有.

    苏公网安备 32059002001037号

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.