C++基础(十四):固有的不可移植的特性 (位域、volatile、链接指示)

本文为《C++ Primer》的读书笔记

  • 为了支持低层编程,C++定义了一些固有的不可移植的特性。所谓不可移植的特性是指因机器而异的特性, 当我们将含有不可移植特性的程序从一台机器转移到另一台机器上时, 通常需要重新编写该程序。算术类型的大小在不同机器上不一样, 这是我们使用过的不可移植特性的一个典型示例

位域

  • 类可以将其(非静态)数据成员定义成位域 (bit-field), 在一个位域中含有一定数量的二进制位,位域在内存中的布局是与机器相关的

当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域


  • 位域的类型必须是整型或枚举类型。因为带符号位域的行为是由具体实现确定的,所以通常用无符号类型保存一个位域
  • 位域的声明形式是在成员名字之后紧跟一个冒号以及一个常量表达式,该表达式用于指定成员所占的二进制位数
typedef unsigned int Bit;
class File {
    
    
	Bit mode: 2;			// mode 占 2 位
	Bit modified: 1;
	Bit prot_owner: 3;
	Bit prot_group: 3;
	Bit prot_world: 3;
	// File的操作和数据成员
public:
	// 文件类型以八进制的形式表示
	enum modes {
    
     READ = 01, WRITE = 02, EXECUTE = 03 };
	File &open(modes);
	void close();
	void write();
	bool isRead() const;
	void setWrite();
};
  • 如果可能的话,在类的内部连续定义的位域压缩在同一整数的相邻位,从而提供存储压缩。例如上面的声明中,五个位域可能会存储在同一个 unsigned int 中。这些二进制位是否能压缩到一个整数中以及如何压缩是与机器相关的

使用位域

  • 如果一个类定义了位域成员,则它通常也会定义一组内联的成员函数以检验或设置位域的值
    • 取地址运算符(&)不能作用于位域,因此任何指针都无法指向类的位域
    • 通常使用内置的位运算符操作超过 1 位的位域
void File::write()
{
    
    
	modified = 1;
	// ...
}

void File::close()
{
    
    
	if(modified)
		// ······ 保存内容
}

File &File::open(File::modes m)
{
    
    
	mode |= READ;	// 按默认方式设置READ
	// 其他处理
	if(m & WRITE)	// 如果打开了READ和WRITE
	// 按照读/写方式打开文件
	return *this;
}

inline bool File::isRead() const {
    
     return mode & READ; }
inline void File::setWrite() {
    
     mode |= WRITE; }

volatile 限定符

不可移植特性

  • volatile 的确切含义与机器有关, 只能通过阅读编译器文档来理解。要想让使用了 volatile 的程序在移植到新机器或新编译器后仍然有效, 通常需要对该程序进行某些改变
  • 直接处理硬件的程序常常包含这样的数据元素,它们的值由程序直接控制之外的过程控制
    • 例如, 程序可能包含一个由系统时钟定时更新的变量。当对象的值可能在程序的控制或检测之外被改变时, 应该将该对象声明为 volatile告诉编译器不应对这样的对象进行优化

volatile 的使用和 const 很相似

  • 就像一个类可以定义 const 成员函数一样, 它也可以将成员函数定义成 volatile。只有 volatile 的成员函数才能被 volatile 的对象调用
volatile int iax [max_size]; 	// iax的每个元素都是volatile
volatile Screen bitmapBuf; 		// bitmapBuf的每个成员都是volatile

volatile int v; 				// 该 int 值可能发生改变
int *volatile vip; 				// vip是一个volatile指针,它指向int
volatile int *ivp; 				// ivp是一个指针, 它指向一个volatile int

int *ip = &v;					// 错误:必须使用指向volatile的指针
ivp = &v;						// 正确
volatile int &rv = v;			// 正确,只有当某个引用是volatile的时,我们才能使用一个volatile对象初始化该引用

合成的拷贝对 volatile 对象无效

  • 我们不能使用合成的拷贝/移动构造函数及赋值运算符初始化 volatile 对象或从 volatile 对象赋值。合成的成员接受的形参类型是(非volatile) 常量引用,显然我们不能把一个非 volatile 引用绑定到一个 volatile 对象上
  • 如果一个类希望拷贝、移动或赋值它的 volatile 对象,则该类必须自定义拷贝或移动操作
    • 例如,我们可以将形参类型指定为 const volatile 引用,这样我们就能利用打意类型的 Foo 进行拷贝或赋值操作了
class Foo {
    
    
public:
	Foo(const volatile Foo&); 	// 从一个volatile对象进行拷贝
	// 将一个volatile对象赋值给一个非volatile对象
	Foo& operator=(volatile const Foo&);
	// 将一个volatile对象赋值给一个volatile对象
	Foo& operator=(volatile const Foo&) volatile;
	// ...
}

链接指示: extern "C"

  • C++程序有时需要调用其他语言编写的函数,最常见的是调用C语言编写的函数。其他语言中的函数名字也必须在C++中进行声明,并且该声明必须指定返同类型和形参列表。对于其他语言编写的函数来说,编译器检查其调用的方式与处理普通C++函数的方式相同,但是生成的代码有所区别

要想把C++代码和其他语言(包括C语言)编写的代码放在一起使用,要求我们必须有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的

声明一个非C++的函数

  • C++使用链接指示 (linkage directive) 指出任意非C++函数所用的语言;链接指示可以有两种形式:单个的或复合的。链接指示不能出现在类定义或函数定义的内部。同样的链接指示必须在函数的每个声明中都出现
    • 链接指示包含一个关键字 extern, 后面是一个字符串字面值常量以及一个 “普通的” 函数声明。其中的字符串字面值常品指出了编写函数所用的语言。编译器应该支持对C语言的链接指示
// 可能出现在 C++ 头文件 <cstring> 中的链接指示

// 单语句链接指示
extern "C" size_t strlen(const char *);

// 复合语句链接指示
// 花括号中声明的函数名字就是可见的,就好像在花括号之外声明的一样
extern "C" {
    
    
	int strcmp(const char*, const char*);
	char *strcat(char*, const char*);
}

// 多重声明的形式可以应用于整个头文件
// 链接指示可以嵌套,因此如果头文件包含带自带链接指示的函数,则该函数的链接不受影响
extern "C" {
    
    
#include <string.h> 	// 操作c风格字符串的c函数
}

C++从C语言继承的标准库函数可以定义成C函数,但并非必须:决定使用C还是C++实现C标准库,是每个C++实现的事情

指向 extern "C" 函数的指针

  • 编写函数所用的语言是函数类型的一部分。因此,对于使用链接指示定义的函数来说,它的每个声明都必须使用相同的链接指示。而且,指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示
// pf指向一个c函数,该函数接受一个int返回void
// 当我们使用pf调用函数时,编译器认定当前调用的是一个C函数
extern "C" void (*pf) (int);

  • 指向C函数的指针与指向C++函数的指针是不一样的类型。一个指向C函数的指针不能用在执行初始化或赋值操作后指向C++函数, 反之亦然。就像其他类型不匹配的问题一样, 如果我们试图在两个链接指示不同的指针之间进行赋值操作, 则程序将发生错误
void (*pf1) (int);				// 指向一个C++函数
extern "C" void (*pf2) (int);	// 指向一个c函数
pf1 = pf2;		// 错误: pf1和pf2的类型不同

链接指示对整个声明都有效

  • 当我们使用链接指示时, 它不仅对函数有效, 而且对作为返回类型或形参类型的函数指针也有效
// f1是一个 c 函数, 它的形参是一个指向 c 函数的指针
extern "C" void f1(void(*) (int));
  • 如果我们希望给C++函数传入一个指向C函数的指针, 则必须使用类型别名
// FC是一个指向c函数的指针
extern "C" typedef void FC(int);

// f2是一个C++函数, 该函数的形参是指向c函数的指针
void f2(FC *);

导出C++函数到其他语言

  • 通过使用链接指示对函数进行定义,编译器将为该函数生成适合于指定语言的代码,因此我们可以令一个C++函数在其他语言编写的程序中可用:
// calc 函数可以被 C 程序调用
extern "C" double calc(double dparm) {
    
     /* ... */ }

  • 值得注意的是,可被多种语言共享的函数的返回类型或形参类型受到很多限制
    • 例如,我们不太可能把一个C++类的对象传给C程序, 因为C程序根本无法理解构造函数、析构函数以及其他类特有的操作

对链接到C的预处理器的支持

  • 有时需要在C和C++中编译同一个源文件,为了实现这一目的,在编译C++版本的程序时预处理器定义 __cplusplus (两个下画线)。利用这个变量, 我们可以在编译C++程序的时候有条件地包含进来一些代码:
#ifdef __cplusplus
// 正确:我们正在编译C++程序
extern "C"
#endif
int strcmp(const char*, const char*);

重载函数与链接指示

  • 链接指示与重载函数的相互作用依赖于目标语言。如果目标语言支持重载函数, 则为该语言实现链接指示的编译器很可能也支持重载这些C++的函数
  • C语言不支持函数冲载,因此也就不难理解为什么一个C链接指示只能用于说明一组重载函数中的某一个了;如果在一组重载函数中有一个是C函数, 则其余的必定都是C++函数
// 错误: 两个extern "C"函数的名字相同
extern "C" void print(const char*);
extern "C" void print(int);
class SmallInt {
    
     /* ... */ };
class BigNum {
    
     /* ... */ };
// c函数可以在C或C++程序中调用
// C++函数重载了该函数, 可以在c++程序中调用
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);	// 使用了类类型形参的C++函数只能在C++程序中调用
extern BigNum calc(const BigNum&);

猜你喜欢

转载自blog.csdn.net/weixin_42437114/article/details/113406068