上面main中调用ABC而产生的局部变量a所对应的地址和由于调用BCD,而在BCD中调用ABC而产生的a所对应的地址就不一样,原理在《C++从零开始(十五)》中说明。因此静态局部变量就表示那个变量的地址不管是通过什么途径调用它所在的函数,都不变化。如下:
void ABC() { static long a = 0; a++; } void BCD() { long d = 0; d++; ABC(); } void main() { ABC(); BCD(); }
上面的变量a的地址是固定值,而不再是原来那种相对值了。这样从main中调用ABC和从BCD中调用ABC得到的变量a的地址是相同的。上面等同于下面:
long g_ABC_a = 0; void ABC() { g_ABC_a++; } void BCD() { long d = 0; d++; ABC(); } void main() { ABC(); BCD(); }
因此上面ABC中的静态局部变量a的初始化实际在执行main之前就已经做了,而不是想象的在第一次调用ABC时才初始化,进而上面代码执行完后,ABC中的a的值为2,因为ABC的两次调用。 它的意义?表示这个变量只在这个函数中才被使用,而它的生命期又需要超过函数的执行期。它并不能提供什么语义(因为能提供的“在这个函数才被使用”使用局部变量就可以做到),只是当某些算法需要使用全局变量,而此时这个算法又被映射成了一个函数,则使用静态变量具有很好的命名效果——既需要全局变量的生存期又应该有局部变量的语义。 四、inline(嵌入)
函数调用的效率较低,调用前需要将参数按照调用规则存放起来,然后传递存放参数的内存,还要记录调用时的地址以保证函数执行完后能回到调用处(关于细节在《C++从零开始(十五)》中讨论),但它能降低代码的长度,尤其是函数体比较大而代码中调用它的地方又比较多,可以大幅度减小代码的长度(就好像循环10次,如果不写循环语句,则需要将循环体内的代码复制10遍)。但也可能倒过来,调用次数少而函数体较小,这时之所以还映射成函数是为了语义更明确。此时可能更注重的是执行效率而不是代码长度,为此C++提供了inline关键字。
在函数定义时,在定义语句的前面书写inline即可,表示当调用这个函数时,在调用处不像原来那样书写存放、传递参数的代码,而将此函数的函数体在调用处展开,就好像前面说的将循环体里的代码复制10遍一样。这样将不用做传递参数等工作,代码的执行效率将提高,但最终生成的代码的长度可能由于过多的展开而变长。如下:
void ABCD(); void main() { ABCD(); } inline void ABCD() { long a = 0; a++; }
上面的ABCD就是inline函数。注意ABCD的声明并没有书写inline,因为inline并不是类型修饰符,它只是告诉编译器在生成这个函数时,要多记录一些信息,然后由连接器根据这些信息在连接前视情况展开它。注意是“视情况”,即编译器可能足够智能以至于在连接时发现对相应函数的调用太多而不适合展开进而不展开。对此,不同的编译器给出了不同的处理方式,对于VC,其就提供了一个关键字__forceinline以表示相应函数必须展开,不用去管它被调用的情况。
前面说过,对于在类型定义符中书写的函数定义,编译器将把它们看成inline函数。变成了inline函数后,就不用再由于多个中间文件都给出了函数的定义而不知应该选用哪个定义所产生的地址,因为所有调用这些函数的地方都不再需要函数的地址,函数将直接在那里展开。 五、const(常量)
前面提到某公司造的房子的门的高度和宽度应该为静态成员变量,但很明显,在房子的实例存在的整个期间,门的高度和宽度都不会变化。C++对此专门提出了一种类型修饰符——const。它所修饰的类型表示那个类型所修饰的地址类型的数字不能被用于写操作,即地址类型的数字如果是const类型将只能被读,不能被修改。如:const long a = 10, b = 20; a++; a = 4;(注意不能cosnt long a;,因为后续代码都不能修改a,而a的值又不能被改变,则a就没有意义了)。这里a++;和a = 4;都将报错,因为a的类型为cosnt long,表示a的地址所对应的内存的值不能被改变,而a++;和a = 4;都欲改变这个值。
由于const long是一个类型,因此也就很正常地有const long*,表示类型为const long的指针,因此按照类型匹配,有:const long *p = &b; p = &a; *p = 10;。这里p = &a;按照类型匹配很正常,而p是常量的long类型的指针,没有任何问题。但是*p = 10;将报错,因为*p将p的数字直接转换成地址类型,也就成了常量的long类型的地址类型,因此对它进行写入操作错误。 注意有:const long* const p = &a; p = &a; *p = 10;,按照从左到右修饰的顺序,上面的p的类型为const long* const,是常量的long类型的指针的常量,表示p的地址所对应的内存的值不能被修改,因此后边的p = &a;将错误,违反const的意义。同样*p = 10;也错误。不过可以: long a = 3, *const p = &a; p = &a; *p = 10;
上面的p的类型为long* const,为long类型的常量,因此其必须被初始化。后续的p = &a;将报错,因为p是long* const,但*p = 10;却没有任何问题,因为将long*转成long后没有任何问题。所以也有:
const long a = 0; const long* const p = &a; const long* const *pp = &p;
只要按照从左到右的修饰顺序,而所有的const修饰均由于取内容操作符“*”的转换而变成相应类型中指针类型修饰符“*”左边的类型,因此*pp的类型是const long* const,*p的类型是const long。
应注意C++还允许如下使用:
struct A { long a, b; void ABC() const; }; void A::ABC() const { a = 10; b = 10; }
上面的A::ABC的类型为void( A:: )() const,其等同于: void A_ABC( const A *this ) { this->a = 10; this->b = 10; } 因此上面的a = 10;和b = 10;将报错,因为this的类型是const A*。上面的意思就是函数A::ABC中不能修改成员变量的值,因为各this的参数变成了const A*,但可以修改类的静态成员变量的值,如:
struct A { static long c; long a, b; void ABC() const; } long A::c; void A::ABC() const { a = b = 10; c = 20; }
等同于:void A_ABC( const A *this ) { this->a = this->b = 10; A::c = 20; }。故依旧可以修改A::c的值。
有什么意义?出于篇幅,有关const的语义还请参考我写的另一篇文章《语义的需要》。 六、friend(友员)
发信机具有发送电波的功能,收信机具有接收电波的功能,而发信机、收信机和电波这三个类,首先发信机由于将信息传递给电波而必定可以修改电波的一些成员变量,但电波的这些成员应该是protected,否则随便一个石头都能接收或修改电波所携带的信息。同样,收信机要接收电波就需要能访问电波的一些用protected修饰的成员,这样就麻烦了。如果在电波中定义两个公共成员函数,让发信机和收信机可以通过它们来访问被protected的成员,不就行了?这也正是许多人犯的毛病,既然发信机可以通过那个公共成员函数修改电波的成员,那石头就不能用那个成员函数修改电波吗?这等于是原来没有门,后来有个门却不上锁。为了消除这个问题,C++提出了友员的概念。
在定义某个自定义类型时,在类型定义符“{}”中声明一个自定义类型或一个函数,在声明或定义语句的前面加上关键字friend即可,如:
class Receiver; class Sender; class Wave { private: long b, c; friend class Receiver; friend class Sender; };
上面就声明了Wave的两个友员类,以表示Receiver和Sender具备了Wave的资格,即如下:
class A { private: long a; }; class Wave : public A { … }; void Receiver::ABC() { Wave wav; wav.a = 10; wav.b = 10; wav.A::a = 10; }
上面由于Receiver是Wave的友员类,所以在Receiver::ABC中可以直接访问Wave::a、Wave::b,但wav.A::a = 10;就将报错,因为A::a是A的私有成员,Wave不具备反问它的权限,而Receiver的权限等同于Wave,故权限不够。
同样,也可有友员函数,即给出函数的声明或定义,在语句前加上friend,如下:
class Receiver { public: void ABC(); }; class A { private: long a; friend void Receiver::ABC(); };
这样,就将Receiver::ABC作为了A的友员函数,则在Receiver::ABC中,具有类A具有的所有权限。
应注意按照给出信息的思想,上面还可以如下:
class A { private: long a; friend void Receiver::ABC() { long a = 0; } };
这里就定义了函数Receiver::ABC,由于是在类型定义符中定义的,前面已经说过,Receiver::ABC将被修饰为inline函数。
那么友员函数的意义呢?一个操作需要同时操作两个资源中被保护了的成员,则这个操作应该被映射为友员函数。如盖章需要用到文件和章两个资源,则盖章映射成的函数应该为文件和章的友员函数。 七、名字空间
前面说明了静态成员变量,它的语义是专用于某个类而又独立于类的实例,它与全局变量的关键不同就是名字多了个限定符(即“::”,表示从属关系),如A::a是A的静态成员变量,则A::a这个名字就可以表现出a从属于A。因此为了表现这种从属关系,就需要将变量定义为静态成员变量。
考虑一种情况,映射采矿。但是在陆地上采矿和在海底采矿很明显地不同,那么应该怎么办?映射两个函数,名字分别为MiningOnLand和MiningOnSeabed。好,然后又需要映射在陆地勘探和在海底勘探,怎么办?映射为ProspectOnLand和ProspectOnSeabed。如果又需要映射在陆地钻井和在海底钻井,在陆地爆破和在海底爆破,怎么办?很明显,这里通过名字来表现语义已经显得牵强了,而使用静态成员函数则显得更加不合理,为此C++提供了名字空间,格式为namespace <名字> { <各声明或定义语句> }。其中的<名字>为定义的名字空间的名字,而<各声明或定义语句>就是多条声明或定义语句。如下:
namespace OnLand { void Mining(); void Prospect(); void ArtesianWell(){} } namespace OnSeabed { void Mining(); void Prospect(); void ArtesianWell(){} } void OnLand::Mining() { long a = 0; a++; } void OnLand::Prospect() { long a = 0; a++; } void OnSeabed::Mining() { long a = 0; a++; } void OnSeabed::Prospect() { long a = 0; a++; }
上面就定义了6个元素,每个的类型都为void()。注意上面OnLand::ArtesianWell和OnSeabed::ArtesianWell的定义直接写在“{}”中,将是inline函数。这样定义的六个变量它们的名字就带有限定符,能够从名字上体现从属关系,语义表现得比原来更好,OnSeabed::Prospect就表示在海底勘探。注意也可以如下:
namespace A { long b = 0; long a = 0; namespace B { long B = 0; float a = 0.0f } } namespace C { struct ABC { long a, b, c, d; void ABCD() { a = b = c = d = 12; } } ab; } namespace D { void ABC(); void ABC() { long a = 0; a++; } extern float bd; }
即名字空间里面可以放任何声明或定义语句,也可以用于修饰自定义结构,因此就可以C::ABC a; a.ABCD();。应注意C++还允许给名字空间别名,比如:namespace AB = C; AB::ABC a; a.ABCD();。这里就给名字空间C另起了个名字AB,就好像之前提过的typedef一样。
还应注意自定义类型的定义的效果和名字空间很像,如struct A { long a; };将生成A::a,和名字空间一样为映射元素的名字加上了限定符,但应该了解到结构A并不是名字空间,即namespace ABC = A;将失败。名字空间就好像所有成员都是静态成员的自定义结构。
为了方便名字空间的使用,C++提供了using关键字,其后面接namespace和名字空间的名字,将把相应名字空间中的所有映射元素复制一份,但是去掉了名字前的所有限定符,并且这些元素的有效区域就在using所在的位置,如:
void main() { { using namespace C; ABC a; a.ABCD(); } ABC b; b.ABCD(); }
上面的ABC b;将失败,因为using namespace C;的有效区域只在前面的“{}”内,出了就无效了,因此应该C::ABC b; b.ABCD();。有什么用?方便书写。因为每次调用OnLand::Prospect时都要写OnLand::,显得有点烦琐,如果知道在某个区域内并不会用到OnSeabed的成员,则可以using namespace OnLand;以减小代码的繁杂度。
注意C++还提供了using更好的使用方式,即只希望去掉名字空间中的某一个映射元素的限定符而不用全部去掉,比如只去掉OnLand::Prospect而其它的保持,则可以:using OnLand::Prospect; Prospect(); Mining();。这里的Mining();将失败,而Prospect();将成功,因为using OnLand::Prospect;只去掉了OnLand::Prospect的限定符。
至此基本上已经说明了C++的大部分内容,只是还剩下模板和异常没有说明(还有自定义类型的操作符重载,出于篇幅,在《C++从零开始(十七)》中说明),它们带的语义都很少,很大程度上就和switch语句一样,只是一种算法的包装而已。下篇介绍面向对象编程思想,并给出“世界”的概念以从语义出发来说明如何设计类及类的继承体系。  
2/2 首页 上一页 1 2 |