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

C++ Primer学习笔记——$24 C++中不可移植的语言特性

2018年03月30日 ⁄ 综合 ⁄ 共 3431字 ⁄ 字号 评论关闭

题记:本系列学习笔记(C++ Primer学习笔记)主要目的是讨论一些容易被大家忽略或者容易形成错误认识的内容。只适合于有了一定的C++基础的读者(至少学完一本C++教程)。

 
作者: tyc611, 2007-04-08


   本文主要讨论C++中不可移植的语言特性。
   如果文中有错误或遗漏之处,敬请指出,谢谢!


   一般意义上说,C程序是可移植的。但为了支持低级编程,C语言也定义了一些不可移植的特性。比如,算术类型的大小是实现相关的,随机器而异;位域和volatile限定符。C++也继承了这些语言特性,另外还增加了另一个不可移植的特性,那就是链接指示。
 
位域
 
   位域(bit-field)是一种特殊的类数据成员,用来保存特定位数的数据。这主要用于操作特定数据的二进制形式的特定位,而这了常用于与硬件打交道的场合。但必须清楚的是,位域在内存中的布局是与机器相关的,不具有可移植性。
 
   位域必须是整型数据类型,可以是signed或者unsigned。通过在成员名后面加一个冒号以及指定位数的常量表达式,指出成员是一个位域,同时了指明了其占有多少位。例如:
   class File {
      unsigned int mode: 2;
      unsigned int modified: 1;
      unsigned int prot_owner: 3;
      // ...
   };
由上面的示例可以看出,位域主要用来节约内存空间,但是是以降低可移植性为代价的。当位域与联合一块合作使用时,就可以方面地取到一数据对象相应位的数据。通常,最好把位域设置为unsigned类型,因为存储signed类型中的位域的行为由实现定义。
 
   对于类成员的位域的访问遵循普通的访问控制规则。位域不能是类的静态成员,也不能用地址操作符(&)来作用于位域。
 
   为了隐藏这种低级操作,通常把位域设置为私有的,然后定义相应的设置/测试成员函数来提供设置和测试操作。在操作位域时,通常也是通过位操作来实现的,例如:
   enum{READ = 1, WRITE = 2};
   File file;  // 假定其构造函数将位域全初始化为0
   file.mode file.mode |= READ;  // 设置READ位
   if (file.mode & READ) // 测试READ位
      cout << "file.mode READ is set" ;
 
volatile限定符
 
   volatile的确切含义与具体机器相关,使用了volatile的程序在移植到新的机器时往往需要修改这些代码。volatile限定符是给编译器的指示,指出编译器不能优化其修饰的对象。其使用方式与const限定符相同,是一个对类型的附加修饰符,特别是对聚合的元素的作用效果也与const相同。例如:
   volatile int a;             // a是int类型的volatile对象
   volatile int arr[10];       // arr是int类型的volatile数组对象,每个元素都是volatile的
   volatile Screen sc;         // sc是Screen类型的volatile对象,其每个成员也是volatile的
 
   还可以像const一样,把成员函数也定义为volatile的,volatile对象只能调用volatile成员函数;也可以用volatile修饰指针,volatile与指针的相互作用和const与指针的相互作用相同。
 
   另外,const和volatile有一个重要的区别,那就是:不能使用合成的拷贝和赋值操作符从volatile对象进行初始化或者赋值。如果类希望允许拷贝volatile对象,或者类希望从volatile操作数或对volatile操作数进行赋值,它必须定义自己的拷贝构造函数和/或赋值操作符版本。例如:
   class Foo {
   public:
      Foo(const volatile Foo&);
      Foo& operator = (volatile const Foo&)
      Foo& operator = (volatile const Foo&) volatile;
      // ...
   };
 
链接指针
 
   C++程序有时需要调用其他程序语言编写的函数,当然最常见的是C语言编写的函数。由于不同语言在处理函数的内部链接名的规则不同,比如,C语言因为函数不能重名,所以其内部表示就可能很简单(比如就用该函数名),而C++因为允许重载函数,所以为了区分函数的各重载版本,需要在内部唯一表示相应版本函数时使用更多的信息来支持。此时,当这些链接方式混杂时,就需要采取一定的手段通知编译器采取相应的链接策略。C++使用链接指示(linkage directive)指出任意非C++函数所用的语言,从而使用相应语言的链接规则。
 
   语法形式:
   extern "L" function-declaration;
或者
   extern "L" {
      function-declaration1;
      function-declaration2;
      // ...
   }
其中,"L"是语言说明字符串,如:"C", "Ada", "FORTRAN"等等,我们主要是用"C"来声明C函数。
 
   由上面知,声明非C++函数有两种形式:单个的和复合的。例如:
   extern "C" void func();
   extern "C" {
      int getchar();
      size_t strlen(const char*);
   }
   可以将复合声明形式应用于整个文件。例如,C++的cstring头文件可以这样写:
   extern "C" {
   #include <string.h>
   }
当将#include指示放在复合链接指示的花括号中时,假定头文件中的所有普通函数声明都是用链接指示的语言编写的函数。链接指针可以嵌套,所以,如果头文件中若包含了带链接指示的函数,该函数的链接不受影响。(注意:允许将C++从C函数库继承而来的函数定义为C函数,但不是必须定义为C函数;实现为C函数还是C++函数是由实现决定的。)
 
   反过来,也可以把C++函数通过链接指示产生适合于其它语言的代码,例如:
   extern "C" double calc(double d) { /* ... */ }
可以用C++的语法定义calc函数,而C程序却可以调用该函数。
 
   这种链接指示在库的头文件中很常见,例如,有下面一段代码:
   #ifdef __cplusplus
   extern "C"
   #endif
   int strcmp(const char*, const char*);
上面的代码中,编译器根据是否定义了宏__cplusplus来决定是以什么风格的链接(C或者C++)来链接该库函数。
 
   如果把链接指示作用于有多个重载版本的C++函数,则只能为其中一个函数指定链接指示。
 
   为了声明用其他语言编写的函数的指针,必须使用链接指示。例如:
   extern "C" void (*pf) (int);
使用pf调用函数的时候,假定该调用是一个C函数调用而编译该函数。注意,C函数的指针和C++函数的指针具有不同的类型,不能将C函数的指针初始化或者赋值为C++函数的指针(反之亦然)(注意:有些编译器可能做了扩展,以支持C到C++形式的赋值)。
 
   使用链接指示时,它作用于作为返回类型或形参类型使用的函数和任何函数指针。例如:
   extern "C" void f1(void(*)(int)); // 声明一个C函数,其参数是一个C函数的指针
因此,如果要将C函数的指针传递给C++函数,应该分开写:
   extern "C" typedef void FC(int);  // C function
   void f2(FC *);  // C++ function with a parameter that is a pointer to a C function
 
   另外,链接指示还可以作用于变量。此时,两者形式的链接指示(单个的和复合的)是有所区别的。具体内容可参考我的这篇文章:C++中的extern C
 

参考文献:

[1] C++ Primer(Edition 4)
[2] Thinking in C++(Edition 2)
[3] International Standard:ISO/IEC 14882:1998

抱歉!评论已关闭.