第十九章:特殊工具与技术

控制内存分配

  • 某些程序对内存分配有特殊要求,不能直接使用标准内存管理机制
  • 重载newdelete算符可控制内存分配的过程

重载new和delete

  • 说法“重载new和delete”,但重载new和delete与重载其他算符的过程非常不同
  • 使用new/new []表达式的过程:
    1. 调用名为operator new/operator new[]的标准库函数,分配足够大的原始的未命名的内存空间
    2. 编译器运行对应的构造函数以构造对象,并传入初值
    3. 返回指向该对象的指针
  • 使用delete/delete []表达式的过程:
    1. 对指针所指对象或数组中的元素执行对应的析构函数
    2. 调用名为operator delete/operator delete[]的标准库函数,释放内存空间
  • 若程序希望控制内存分配的过程,则需要定义自己的operator new/operator delete函数。即使标准库中已有它们的定义也可自定义,编译器将使用自定义的版本代替标准库的版本
  • 自定义了全局的operator new/operator delete函数后,程序负责控制动态内存
  • operator new/operator delete函数只能在全局作用域类作用域中定义
  • 编译器处理new/delete表达式时查找operator的过程(对象是类类型):
    1. 查找类类型及其基类类型的作用域中的operator
    2. 查找全局作用域中的operator
    3. 使用标准库的operator
  • 可使用作用域算符令new和delete表达式忽略定义在类中的operator函数,直接执行全局作用域的版本,如::new只查找全局作用域中的operator new
  • 标准库定义了operator new/operator delete的8个重载:
1
2
3
4
5
6
7
8
9
void *operator new(size_t);
void *operator new[](size_t);
void operator delete(void *) noexcept;
void operator delete[](void *) noexcept;

void *operator new(size_t,nothrow_t &) noexcept;
void *operator new[](size_t,nothrow_t &) noexcept;
void operator delete(void *,nothrow_t &) noexcept;
void operator delete[](void *,nothrow_t &) noexcept;
  • 类型nothrow_t是定义于头文件new的一个struct,该类型不包含任何成员。同时定义了一个名为nothrow的const对象,通过该对象可手动请求new的非抛出版本
  • operator delete不允许抛出异常(类似析构函数),重载它时必须指定noexcept
  • 应用程序可自定义8个operator重载中的任一个,只要自定义的版本位于全局作用域或类作用域
  • operator new在对象构造之前而operator delete在对象析构之后,故将这些operator定义为类的成员时是隐式的static,且不能操作类的任何数据成员
  • 对于operator new/operator new[]:
    • 返回类型必须是void *,第一个形参必须是size_t且该形参不能有默认实参
    • 编译器调用operator new时,将存储指定类型对象所需的byte数传给size_t形参
    • 编译器调用operator new[]时,将存储数组中所有元素所需的空间传给size_t形参
    • 自定义operator new时可提供额外形参,此时使用它的new表达式必须用定位new形式传递实参
    • 函数void *operator new(size_t,void *);不可被用户重载,它只供标准库使用
  • 对于operator delete/operator delete[]:
    • 返回类型必须是void,第一个形参必须是void *
    • 执行delete表达式将调用相应的operator函数,用指向待释放内存的指针初始化void *形参
    • 定义为类的成员时,该函数可包含另外一个size_t形参,该形参的初始值是void *形参所指对象的字节数
    • size_t形参可用于删除继承体系的对象,若基类有虚析构函数则传给size_t形参的字节数由void *所指对象的动态类型决定,且operator delete的版本也由对象的动态类型决定
  • 重定义operator new/delete的目的在于改变内存分配的方式,并未重载new/delete表达式(与其他重载算符不同),这两个表达式的行为不可改变
  • C++从C中继承了malloc/free函数,它们定义于cstdlib头文件,用于控制内存的分配
    • malloc函数接受一个表示待分配字节数的size_t,返回指向分配空间的指针,或返回0表示分配失败
    • free函数接受一个void *,它是malloc返回的指针的副本,free将相关内存返回给系统。free(0)无意义
  • 例子:使用malloc/free重新定义operator new/delete
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//使用malloc重新定义operator new
void *operator new(size_t size){
    if(void *mem=malloc(size))
        return mem;                         //若分配成功则返回指针
    else
        throw bad_alloc();                  //若分配失败则抛出异常
}
//使用free重新定义operator delete
void operator delete(void *mem) noexcept {  //不应该抛出异常,用noexcept
    free(mem);                              //使用free时不需要考虑指针是否为空,直接释放
}

定位new表达式

  • operator new/delete可用于new/delete表达式,但也可被用户代码手动调用,实现分配空间和构造对象的分离:
    • operator new用于分配空间
    • 传递地址的定位new用于在已分配的空间中构造对象
  • operator new/delete的行为和allocator类的allocate/deallocate成员函数很相似,都只负责空间的分配/释放,不会构造/销毁对象。在C++11引入allocator之前,常用operator new/delete
  • operator new的行为类似allocator::allocate,它们的区别:
    • allocator::allocate应使用allocator::construct构造对象
    • operator new应使用传递地址的定位new构造对象
  • 使用operator new分配的内存空间应使用定位new来构造对象,可向定位new传递一个地址,在该地址中构造对象
  • 传递地址的定位new形式:
1
2
3
4
new(place_address) type
new(place_address) type(initializers)
new(place_address) type[size]
new(place_address) type[size]{braced initializer list}
  • 其中place_address是指针,initializers提供实参调用type类型的构造函数,size是构造元素的数量
  • 向定位new传递地址时,使用void *operator new(size_t,void *);来分配内存。该函数并不分配任何内存,且不能被用户重定义。因此,传递地址的定位new可用于在一个已分配好的内存地址上构造对象
  • 传递地址的定位new的行为类似allocator::construct,它们的区别:
    • 传给allocator::construct的指针必须指向allocator对象分配的空间
    • 传给定位new的指针不需指向operator new分配的内存,甚至不需要指向动态内存
  • 析构函数可被对象/引用/指针显式调用,类似allocator分配的对象用allocator::destroy销毁
  • 析构函数调用时清除给定对象,但不释放内存,类似allocator::destroy

运行时类型识别

  • 运行时类型识别(RTTI)的功能由两个算符实现:
    • typeid算符用于返回表达式的类型
    • dynamic_cast算符用于将基类引用/指针安全地转换为派生类引用/指针
  • 将typeid和dynamic_cast用于某种类型的指针/引用,且该类型有虚函数时,算符将使用指针/引用所指对象的动态类型
  • 若想使用基类引用/指针来执行派生类操作,且该操作不是虚函数(应尽量使用虚函数,但有时无法定义虚函数,如static/inline/构造函数),则可使用RTTI
  • 与虚函数相比,RTTI风险更多:程序员必须知道转换的目标类型,且必须检查类型转换是否成功执行
  • 应尽量使用虚函数而不是RTTI

dynamic_cast算符

  • dynamic_cast的使用形式:
    • dynamic_cast<type *>(e),e必须是有效的指针
    • dynamic_cast<type &>(e),e必须是左值
    • dynamic_cast<type &&>(e),e必须是右值
    • type是目标类型,必须是类类型,通常还应该有虚函数
    • e的类型必须是type的公有派生类/公有基类/type本身,否则转换失败
  • dynamic_cast转换失败时:
    • 若目标是指针类型且失败,则结果为0
    • 若目标是引用类型且失败,则抛出std::bad_cast异常,该异常定义于typeinfo头文件
  • 对空指针执行dynamic_cast的结果是所需类型的空指针
  • 对于指针类型,在条件部分使用dynamic_cast可确保一条表达式中同时完成类型转换和结果检查
  • 例子:对指针类型的dynamic_cast使用if来处理失败情形
1
2
3
4
5
6
7
//转换成功执行if,转换失败执行else,且dp在if外不可访问,确保转换失败时安全
if(Derived *dp=dynamic_cast<Derived *>(bp)){
    /* 使用dp指向的Derived对象 */
}
else{
    /* 使用bp指向的Base对象 */
}
  • 对于引用类型,不存在空引用,故无法使用与指针相同的报错策略,只能使用bad_cast抛出异常
  • 例子:对引用类型的dynamic_cast使用try-catch来处理失败情形
1
2
3
4
5
6
7
8
9
void f(const Base &b){
    try{
        const Derived &d=dynamic_cast<const Derived &>(b);
        /* 使用d引用的Derived对象 */
    }
    catch(bad_cast){
        /* 处理转换失败的情况 */
    }
}

typeid算符

  • typeid算符允许程序询问表达式的对象类型
  • typeid表达式的形式是typeid(e)
    • 形参e是任意表达式或类型名
    • 返回值是一个常量对象的引用,该对象的类型是type_info或其公有派生类。type_info类定义于头文件typeinfo
  • typeid算符可作用于任意类型的表达式,在求类型时:
    • 表达式中的顶层const被忽略
    • 若表达式是引用,则返回所引对象的类型
    • 若表达式是数组/函数,不会转换为对应的指针
    • 若表达式不是类类型,或是不包含虚函数的类类型,则返回静态类型
    • 若表达式是定义了虚函数的类的左值,则返回动态类型,即运行时才可知
    • 若表达式是指针,即使所指对象是有虚函数的类类型,仍返回该指针的静态类型
  • 通常使用typeid比较两条表达式的类型是否相同,或是比较一条表达式的类型与指定类型是否相同
  • 例子:使用typeid判断类型是否相同
1
2
3
4
5
Derived *dp=new Derived;
Base *bp=dp;
if(typeid(*bp)==typeid(*dp)){}      //解引用得到对象的引用,由于指向同一对象,故true
if(typeid(*bp)==typeid(Derived)){}  //bp实际指向Derived类型对象,故true
if(typeid(bp)==typeid(Derived)){}   //指针未解引用,推出类型是静态类型的指针,故永远是false
  • typeid中的表达式是否会被求值,取决于是否需要运行时检查:
    • 只有类型含有虚函数时,才会在运行时检查
    • 类型不含虚函数时,返回静态类型,不需对表达式求值
  • 若表达式的动态类型可能与静态类型不同,则必须在运行时求值。
  • 对于类似typeid(*p)的表达式,
    • 若p所指类型有虚函数,则必须求值才能得知类型,故p不可为空指针,否则抛出bad_typeid异常
    • 若p所指类型没有虚函数,则不需求值,故p可为空指针

使用RTTI

  • 若想为继承体系中的类实现相等算符,RTTI非常有用。
  • 若两个对象的类型相同且对应数据成员的值相等则这两个对象相等。
  • 设计:
    • 派生类的相等算符必须考虑派生类新加的成员
    • 容易想到的方案是:定义一套虚函数equal,在继承体系的各个层次上分别判断成员相等
    • 存在的问题:
      • 虚函数的基类版本和派生类版本必须有相同的形参
      • 若定义虚函数equal则形参必须是基类的引用,此时只能使用基类成员,无法比较派生类
    • 解决方案:
      • 先比较类型,类型不相同直接false
      • 使用typeid比较对象的运行时类型,类型相同才调用虚函数equal逐级比较
      • 这些equal的形参都是基类引用,但在两边类型相同时可以安全地把两边都转为self的类型
  • 例子:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//定义基类
class Base{
    friend bool operator==(const Base &,const Base &);
protected:
    virtual bool equal(const Base &) const;
    /* 其他成员 */
};
//定义派生类
class Derived: public Base{
protected:
    bool equal(const Base &) const;
    /* 其他成员 */
};
//委托虚函数equal实现operator==
//传入基类类型的常量引用,确保可对基类和派生类同样适用
bool operator==(const Base &lhs,const Base &rhs){
    //使用typeid运行时求类型,类型不相等时根据&&的短路求值,直接false
    //类型相等后执行虚函数equal,执行的是动态类型对应的版本
    return typeid(lhs)==typeid(rhs) && lhs.equal(rhs);
}
//定义派生类的equal函数
//由于派生类的虚函数形参必须和基类相同,故传入基类引用
bool Derived::equal(const Base &rhs) const{
    //只有将基类引用转为派生类引用才能比较派生类的数据成员
    //由于equal是虚函数,执行的是动态版本,故比较派生类版本时将引用转换为派生类引用即可
    auto r=dynamic_cast<const Derived &>(rhs);
    //执行比较两个Derived对象的操作并返回结果
}
//定义基类的equal函数
bool Base::equal(const Base &rhs) const{
    //执行比较两个Base对象的操作并返回结果
}

type_info类

  • type_info类的精确定义随编译器的不同略有差异
  • C++标准规定type_info类必须定义于头文件typeinfo,且至少提供表19.1所示操作: tab_19_1
  • 由于tpye_info一般是作为基类出现(需要提供额外的类型信息时通常在其派生类中完成),故有public的虚析构函数
  • 创建type_info对象的唯一途径是使用typeid算符:type_info没有默认构造函数,且拷贝/移动构造函数和赋值算符都被定义为删除的。因此无法定义/拷贝/移动type_info类型的对象,也不能为type_info类型对象赋值。
  • type_info类的name成员函数返回C风格字符串,表示对象的类型名字
    • name的返回值因编译器而异,不一定与程序中使用的类型名一致
    • 对name返回值的唯一要求是:不同类型的返回字符串必须有所区别
  • 有的编译器会为type_info提供额外的成员函数以提供类型的额外信息,使用时应阅读编译器手册

枚举类型

  • 枚举类型可将一组整型常量组织在一起,每个枚举类型定义了一种新类型。枚举是字面值常量类型
  • C++包含两种枚举:
    • 限定作用域的枚举(C++11引入):关键字enum class/struct后接枚举类型名和花括号括起来的枚举成员列表,以分号结束
    • 不限定作用域的枚举:省略关键字class/struct,枚举类型名可选
  • 在不限定作用域的枚举中,若无枚举类型名则只能在定义枚举时在花括号和分号之间定义它的对象,类似定义类的同时定义对象
  • 例子:定义枚举类型
1
2
3
enum class open_modes{input,output,append};             //限定作用域的枚举
enum color{red,yellow,green};                           //不限定作用域的枚举
enum {floatPrec=6,doublePrec=10,double_doublePrec=10};  //未命名的不限定作用域的枚举
  • 限定作用域的枚举中,枚举成员名字的作用域在枚举类型内部
  • 不限定作用域的枚举中,枚举成员名字的作用域与枚举类型本身的作用域相同
  • 例子:不限定作用域的枚举和限定作用域的枚举
1
2
3
4
5
6
7
enum color{red,yellow,green};           //对,是不限定作用域的枚举,成员作用域和枚举类型相同
enum stoplight{red,yellow,green};       //错,是不限定作用域的枚举,成员作用域和枚举类型相同,名字和上一行冲突
enum class peppers{red,yellow,green};   //对,是限定作用域的枚举,成员的作用域在枚举类型内部
color eyes=green;                       //对,color的枚举成员在全局作用域
peppers p=green;                        //错,peppers的枚举成员不在全局作用域
color hair=color::red;                  //对,使用color枚举类中的red
peppers p2=peppers::red;                //对,使用peppers枚举类中的red
  • 默认枚举成员的值从0开始依次+1,也可为一个或几个枚举成员指定专门的值。
  • 不同枚举成员可以有相同的值
  • 若未显式提供枚举成员的值,则当前枚举成员的值等于它左侧枚举成员的值+1
  • 枚举成员是const,故初始化枚举成员时提供的初始值必须是constexpr
  • 每个枚举成员本身是constexpr,可在任何需要constexpr的地方使用枚举成员
  • 枚举成员是constexpr:
    • 可将枚举类型的对象作为switch语句的表达式,将枚举值作为case标签
    • 可将枚举类型作为非类型模板的形参
    • 可在类的定义中初始化枚举类型的static数据成员
  • 例子:枚举成员的值
1
2
3
4
5
enum class intTypes{
    charTyp=8,shortTyp=16,intTyp=16,            //不同枚举成员的值可相同
    longTyp=32,long_longTyp=64
};
constexpr intTypes charbits=intTypes::charTyp;  //枚举成员是constexpr
  • 只要枚举类型有名字,就可定义并初始化该类型的对象。要想初始化枚举对象或为其赋值,必须使用该类型的枚举成员或该类型的枚举类对象,而不能用整型
  • 不限定作用域的枚举类型对象或枚举成员会自动转换成整型,可在任何需要整型的地方使用它们。而限定作用域的枚举对象/枚举成员不会转换为整型
  • 例子:整型和枚举类型的转换
1
2
3
4
5
6
7
enum class open_modes{input,output,append};
open_modes om=2;        //错,不可用整型初始化枚举类型对象
om=open_modes::input;   //对,可用枚举类型的成员初始化枚举类型对象
enum color{red,yellow,green};
enum class peppers{red,yellow,green};
int i=color::red;       //对,不限定作用域的枚举成员可隐式转换为整型
int j=peppers::red;     //错,限定作用域的枚举成员不可隐式转换为整型
  • 尽管每个枚举类型都定义了唯一的类型,但枚举成员和枚举对象都是由某种整型表示的
  • C++11中,可在枚举类型名后加冒号:和类型名来表明使用该类型(潜在类型)来表示该枚举
  • 若未指定枚举类型的潜在类型,则默认:
    • 限定作用域的枚举成员类型默认是int
    • 不限定作用域的枚举成员不存在默认类型,只知道潜在类型足够大,可容纳枚举值
  • 若指定了枚举成员的潜在类型(包括限定作用域默认指定为int)而某个枚举成员的值发生溢出,则引发错误
  • 例子:枚举类型指定潜在类型
1
2
3
4
5
enum intValues: unsigned long long{
    charTyp=255,shortTyp=65535,intTyp=65535,
    longTyp=4294967295UL,
    long_longTyp=18446744073709551615ULL
};
  • C++11中可对枚举类型使用前置声明,但前置声明时必须指定枚举成员的大小(显式或隐式)
  • 不限定作用域的枚举类型前置声明必须手动指明大小,限定作用域的枚举类型前置声明可被隐式指定为int
  • 枚举类型的前置声明必须和定义匹配:
    • 前置声明和定义中的成员大小必须一致
    • 同一个上下文中对同一枚举类型的前置声明和定义,必须都是限定作用域或都是不限定作用域
  • 例子:前置声明枚举类型
1
2
enum intValues: unsigned long long; //不限定作用域的枚举类型前置声明必须指定成员大小
enum class open_modes;              //限定作用域的枚举类型前置声明可将成员大小隐式指定为int
  • 要想初始化一个枚举类型对象,必须使用该枚举类型的另一对象,或是该枚举类型的成员。不可使用整型初始化/赋值
  • 枚举对象的大小与该枚举类型的成员大小一样
  • 可使用不限定作用域的枚举类型对象或枚举成员对整型进行初始化/赋值,此时枚举值可能被提升为int或更大的整型
  • 例子:整型和枚举类型的转换
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum Tokens{INLINE=128,VIRTUAL=129};
void ff(Tokens);
void ff(int);
Tokens curTok=INLINE;
ff(128);                    //匹配ff(int)
ff(INLINE);                 //匹配ff(Tokens)
ff(curTok);                 //匹配ff(Tokens)
void newf(unsigned char);
void newf(int);
unsigned char uc=VIRTUAL;   //不限定作用域的枚举成员可转换为整型
newf(VIRTUAL);              //匹配newf(int),不限定作用域的枚举成员可转换为整型
newf(uc);                   //匹配newf(unsigned char)

类成员指针

  • 成员指针是可以指向类的非static成员的指针。static成员不属于任何对象故可用普通指针、
  • 普通指针指向类的对象,成员指针指向类的成员
  • 成员指针的类型中包括类类型和成员类型
  • 初始化成员指针时,令其指向类的某个成员,但不指定该成员所属的对象。只有使用成员指针时才指定成员所属的对象
  • 例子:本节使用7.3.1的Screen类作为示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Screen{
public:
    typedef string::size_type pos;
    char get_cursor() const {return contents[cursor];}
    char get() const;
    char get(pos ht,pos wd) const;
private:
    string contents;
    pos cursor;
    pos height,width;
};

数据成员指针

  • 声明成员指针时必须包含成员所属的类,应在*之前添加classname::以表示该指针指向classname类的成员
  • 将成员指针声明为const可使指针既能指向const对象又能指向非const对象,但它不能修改成员
  • 初始化成员指针或对其赋值时,需使用取地址符&,且需指定它所指的成员名
  • C++11中声明并定义成员指针最简单的方法是使用autodecltype
  • 例子:声明/定义成员指针
1
2
3
4
/* 上下文:本节开头定义的Screen类 */
const string Screen::*pdata;    //声明成员指针pdata指向Screen类的string类型成员,且该指针为const
pdata=&Screen::contents;        //为成员指针pdata赋值,使其指向Screen类的contents成员
auto pdata=&Screen::contents;   //使用auto声明并定义成员指针
  • 初始化成员指针或为其赋值时,该指针并未指向任何数据。成员指针仅指定成员而非其所属的对象,只有解引用时才提供对象信息
  • 两种成员指针访问算符,用于解引用成员指针并获得对象的成员:
    • .*类似.,作用于对象,取其成员
    • ->*类似->,作用于指向对象的指针,取其成员
    • 这两个算符先用*解引用成员指针来得知需访问哪个成员,再用./->通过传入的对象/指向对象的指针来访问该成员
  • 例子:解引用成员指针
1
2
3
4
5
/* 上下文:本节开头定义的Screen类 */
Screen myScreen,*pScreen=&myScreen; //定义对象及其指针
auto pdata=&Screen::contents;       //定义成员指针指向Screen类型的contents成员
auto s=myScreen.*pdata;             //取对象myScreen的contents成员
s=pScreen->*pdata;                  //取pScreen所指对象的contents成员
  • 常规的访问控制规则对成员指针同样有效,即private成员必须在类内访问或用友元访问。
  • 类的数据成员一般是private,指向数据成员的指针需要权限:
    • 最好在类内定义成员函数,令其返回值是指向该数据成员的指针
    • 该成员函数应该是static的,因为初始化成员指针时要用类名而不是对象来访问该成员函数
  • 例子:使用static成员函数来返回指向private数据成员的成员指针
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Screen{
public:
    //该函数返回类型是const string Screen::*,即是返回指向数据成员的指针
    //由于使用该函数获得成员指针时并无对象,故定义为static
    static const string Screen::*data(){
        return &Screen::contents;
    }
private:
    string contents;    //被访问的private数据成员
};
//使用static成员函数来获得指向private数据成员的成员指针
const string Screen::*pdata=Screen::data();
Screen myScreen;
auto s=myScreen.*pdata; //传入对象,解引用成员指针(顺利访问private数据成员)

成员函数指针

  • 可以定义指向类的成员函数的指针,最简单的方法是用auto推断类型
  • 使用classname::*的形式声明指向成员函数的指针,成员函数指针也要指定函数的返回类型和形参列表,若函数是const成员或引用成员,指针也应是const或引用
  • 若成员函数存在重载问题,则必须显式声明函数类型,以指明想要哪个函数
  • 书写成员函数类型时,classname::*pointername必须用括号,否则pointername与右侧结合,错误
  • 在成员函数和指向成员函数的指针之间不存在自动转换(和普通函数指针不同)
  • 例子:定义成员函数指针
1
2
3
4
5
6
auto pmf=&Screen::get_cursor;                           //对,定义时用auto推导类型
char (Screen::*pmf2)(Screen::pos,Screen::pos) const;    //对,声明时手动指定类型(指定重载版本)
pmf2=&Screen::get;                                      //对,给已声明的成员函数指针赋值
char Screen::*p(Screen::pos,Screen::pos) const;         //错,书写成员函数指针的类型时括号必不可少,否则是声明普通函数p并返回Screen类的char成员,但普通函数不可有const限定符
pmf=&Screen::get;                                       //对,显式取地址得到指针
pmf=Screen::get;                                        //错,成员函数不可转换为指针,必须取地址
  • 使用./->算符来解引用成员函数指针,并传入对象得到其对应成员函数
  • 函数调用算符的优先级较高,故声明指向成员函数的指针或使用成员函数指针进行函数调用时,括号必不可少,即(Class::*p)(obg.*p)(args)
  • 例子:用成员函数指针调用成员函数
1
2
3
4
5
6
7
auto pmf=&Screen::get_cursor;                           //使用auto定义成员函数指针
char (Screen::*pmf2)(Screen::pos,Screen::pos) const=;   //手动声明类型,指定重载的版本
pmf2=&Screen::get;
Screen myScreen,*pScreen=&myScreen;
char c1=(pScreen->*pmf)();                              //使用成员函数指针和指向对象的指针来调用成员函数
char c2=(myScreen.*pmf2)(0,0);                          //使用成员函数指针和对象来调用成员函数
pScreen->*pmf();                                        //错,去掉括号后等价于pScreen->*(pmf()),然而不存在函数pmf
  • 使用类型别名或typedef可让成员指针更容易理解
  • 可将成员函数指针作为某个函数的返回类型或形参类型,其中成员指针作为形参时也可拥有默认实参
  • 例子:成员指针作为形参
1
2
3
4
5
6
7
using Action=char (Screen::*)(Screen::pos,Screen::pos) const;   //为成员函数指针定义类型别名
Action get=&Screen::get;                                        //用类型别名定义成员函数指针
Screen &action(Screen &,Action=&Screen::get);                   //成员函数指针用作形参,且默认实参
Screen myScreen;
action(myScreen);                                               //使用默认实参
action(myScreen,get);                                           //使用自己定义的成员函数指针
action(myScreen,&Screen::get);                                  //使用地址初始化形参中的成员函数指针
  • 对于普通函数指针和成员函数指针,常见的用法是将其存入函数表(若一个类有几个相同类型的成员函数,可用函数表)
  • 例子:用函数表管理相同类型的成员函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Screen{
public:
    //类型相同的五个函数
    Screen &home();
    Screen &forward();
    Screen &back();
    Screen &up();
    Screen &down();
    //上面五个函数类型相同,可使用同一类型的成员函数指针
    using Action=Screen &(Screen::*)();
    //不限定作用域的枚举,代表上面五个函数的索引数
    enum Directions{HOME,FORWARD,BACK,UP,DOWN};
    //move函数根据枚举值来调用对应成员函数
    Screen &move(Directions);
private:
    //函数表,用于存储类型相同的函数指针
    static Action Menu[];
};
//定义函数表,表中函数的索引数和对应枚举成员的整型值一一对应
Screen::Action Screen::Menu[]={&Screen::home,&Screen::forward,&Screen::back,&Screen::up,&Screen::down};
//实现move函数,根据传入的枚举类型调用函数表中的对应函数
Screen &Screen::move(Directions cm){
    return (this->*Menu[cm])();
}
//使用函数表
Screen myScreen;
myScreen.move(Screen::HOME);
myScreen.move(Screen::DOWN);

将成员函数用作可调用对象

  • 要想通过成员函数指针调用函数,必须首先用.*/->*算符将指针绑定到对象
  • 成员函数指针本身不是可调用对象,也不可自动转换为函数类型,故不可直接使用调用算符()
  • 由于成员函数指针不可调用,故不可将其传递给需要可调用对象的形参,例如标准库算法
  • 例子:形参为可调用对象时不可传入成员函数指针
1
2
3
4
5
auto fp=&string::empty;
find_if(svec.begin(),svec.end(),fp);    //错,成员函数指针必须通过对象和`.*`/`->*`算符调用
//假设容器svec中的迭代器是it,则find_if中有如下调用:
//在内部使用圆括号()调用传入的函数指针,因此不可传入成员函数指针
if(fp(*it))
  • 可使用标准库模板function来从成员函数指针中获取可调用对象
  • 使用function为成员函数生成可调用对象时,需将隐式的this形参变为显式
  • 当function对象包含有成员函数指针时,function可自动选择正确的算符.*/->*来执行调用
  • 定义function对象时,必须指定该对象的调用形式:
    • 若使用成员函数定义function,则第一个形参必须表示调用该成员的对象的类型(即显式写出this形参)
    • 提供给function的调用形式必须指明调用该成员函数的对象的传入形式是否是指针/引用
  • 例子:使用function从成员函数指针中获取可调用对象
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* 使用对象的常量引用来调用成员函数指针 */
//使用成员函数定义function时,必须在调用形式中显式写出调用该成员函数的对象的类型
function<bool (const string &)> fcn=&string::empty;
find_if(svec.begin(),svec.end(),fcn);
//find_if内部以如下形式调用fcn:
if(fcn(*it));
//fcn实际将调用转换为如下形式:
if(((*it).*p)());
/* 使用指向常量对象的指针来调用成员函数指针 */
//假设使用指针来调用成员函数
vector<string *> pvec;
//则function的模板实参中必须指出:用于调用该成员函数的是对象的指针
function<bool (const string *)> fp=&string::empty;
find_if(pvec.begin(),pvec.end(),fp);
//显式指明调用该成员函数的是对象指针后,fp实际调用如下形式:
if(((*it)->*fp)());
  • 使用function时必须手动提供成员的调用形式
  • 可使用标准库函数mem_fn来从成员函数指针生成可调用对象,且可推断成员函数的类型,mem_fn也定义于头文件functional
  • 成员函数通过mem_fn生成的可调用对象,既可被对象调用也可被指向对象的指针调用,可以认为mem_fn生成的可调用对象有一对重载的调用算符,一个接受object *另一个接受object &
  • 例子:使用mem_fn从成员函数指针中获取可调用对象
1
2
3
4
5
6
//使用mem_fn自动从成员函数指针中获取可调用对象,在find_if内部绑定到容器的元素上
find_if(svec.begin(),svec.end(),mem_fn(&string::empty));
//定义mem_fn产生的可调用对象,f可接受对象的引用形式或指针形式
auto f=mem_fn(&string::empty);
f(*svec.begin());   //mem_fn的可调用对象可通过对象调用其成员函数,即使用.*
f(&svec[0]);        //mem_fn的可调用对象课通过对象的指针调用其成员函数,即使用->*
  • 可使用标准库函数bind来从成员含指针生成可调用对象
    • 类似function:若对成员函数使用bind,则必须将隐式的this形参写为显式
    • 类似mem_fn:可自动推导类型,且生成的可调用对象可用对象调用也可用对象的指针调用
  • 例子:使用bind从成员函数指针中获取可调用对象
1
2
3
4
//使用bind获取可调用对象,成员函数的this形参需显式写出
find_if(svec.begin(),svec.end(),bind(&string::empty,_1));
f(*svec.begin());   //通过对象调用成员函数
f(&svec[0]);        //通过对象的指针调用成员函数

嵌套类

  • 定义在另一个类内部的类称为嵌套类嵌套类型,嵌套类常用作实现部分
  • 嵌套类是独立的类,与外层类没有关系。它们互相独立,外层类的对象中不包含嵌套类的成员,反之亦然
  • 嵌套类的名字在外层类的作用域中可见,在外层类之外不可见。因此常用作外部类某功能的实现
  • 嵌套类使用访问限定符来控制外界成员对其成员的访问权限。外层类对嵌套类成员没有特殊的访问权限,反之亦然
  • 嵌套类在外层类中定义了一个类型成员,该类型成员的访问权限取决于外层类(外层类指定嵌套类型成员为public/protected/private)
  • 嵌套类必须声明在外层类的内部,但可以定义在外层类的内部或外部。在外层类之外定义嵌套类时,必须用::指定外层类作用域
  • 嵌套类在外层类的作用域中,故嵌套类中可直接使用外层类的名字
  • 嵌套类直到自身定义结束前,一直是不完全类型
  • 例子:定义嵌套类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* 上下文:12.3中的文本查询程序 */
//在外层类中声明嵌套类
class TextQuery{
public:
    class QueryResult;
};
//在外层类外定义嵌套类,要指定在外层类的作用域中
class TextQuery::QueryResult{
    friend ostream &print(ostream &,const QueryResult &);
public:
    //嵌套类中可直接使用外层类定义的名字
    QueryResult(string,shared_ptr<set<line_no>>,shared_ptr<vector<string>>);
};
  • 在嵌套类和外层类外定义嵌套类的成员函数时,必须用::指定外层类和嵌套类的作用域
  • 若嵌套类声明了static成员,则该static成员的定义将位于外层类之外
  • 例子:嵌套类和外层类外部定义嵌套类的成员函数和static成员
1
2
3
4
5
6
//外部定义嵌套类的成员函数
TextQuery::QueryResult::QueryResult(string s,shared_ptr<set<line_no>> p,shared_ptr<vector<string>> f):
                                   sought(s),lines(p),file(f)
                                   {}
//外部定义嵌套类的static成员
int TextQuery::QueryResult::static_mem=1024;
  • 名字查找的一般规则适用于嵌套类,嵌套类本身是一个嵌套作用域
  • 嵌套类是外层类的类型成员,外层类的其他成员可像使用任何其他类型成员一样使用嵌套类的名字
  • 在嵌套类和外层类外部定义函数时,返回类型不在类的作用域中,而函数名后面的部分在嵌套类的作用域中
  • 尽管嵌套类定义在外层类的作用域中,但它们的对象没有任何关系。
  • 例子:外层类外部定义外层类的成员函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//外层类的成员函数,返回类型是嵌套类,需指明外层类的作用域
TextQuery::QueryResult TextQuery::query(const string &sought) const{
    static shared_ptr<set<line_no>> nodata(new set<line_no>);
    auto loc=wm.find(sought);
    if(loc==wm.end())
        //函数体在外层类的作用域中,可直接使用嵌套类
        return QueryResult(sought,nodata,file);
    else
        return QueryResult(sought,loc->second,file);
}

union:一种节省空间的类

  • 联合(union)是一种特殊的类,一个union定义了一种新类型
  • 一个union可有多个数据成员,但任意时刻只有一个数据成员可以有值,给union的某个成员赋值后,其他成员变为未定义
  • 分配给union对象的存储空间至少要能容纳它的最大成员
  • union不能含有引用类型的成员,除此之外可以是绝大多数类型。C++11中,含构造/析构函数的类也可作为union的成员类型
  • union可为其成员指定public/protected/private权限,默认为public
  • union可定义成员函数(包括构造/析构函数)
  • unioin不可作为基类/派生类,故union不可有虚函数
  • 定义union时,首先是关键字union,随后是可选的名字和花括号后的一组成员声明,分号结束
  • union的名字是一个类型名,默认union是未初始化的,可用一对花括号内的初始值显式初始化union(类似显式初始化聚合类),该初始值被用于初始化第一个成员
  • 使用通用的成员访问算符来访问union对象的成员
  • 为union的一个数据成员赋值会让其他数据成员变成未定义,故使用union时必须清楚地知道当前存储的是哪个成员
  • 例子:使用union
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//用union定义一个类型
union Token{
    char cval;
    int ival;
    double dval;
};
Token first_token={'a'};    //用花括号初始化union实际上是在初始化第一个成员
Token last_token;           //声明union不初始化
Token *pt=new Token;        //声明动态的union和指针
last_token.cval='z';        //为union的成员赋值
pt->ival=42;                //通过指向union的指针为union成员赋值
  • 匿名union是未命名的union,且在花括号和分号间无任何对象的声明
  • 一旦定义了一个匿名union,编译器自动为该匿名union创建一个未命名的对象
  • 在匿名union的定义所在作用域内,该匿名union的成员都可直接访问,不需要说明union作用域
  • 匿名union不可包含protected/private成员,也不可定义成员函数
  • 例子:使用匿名union
1
2
3
4
5
6
7
8
9
//定义匿名union,同时隐式定义一个未命名的对象
union{
    char cval;
    int ival;
    double dval;
};
//在匿名union定义所在的作用域中,可直接访问其成员
cval='c';   //为隐式定义的对象的成员赋值
ival=42;    //为隐式定义的对象的成员赋值
  • 旧标准规定union中不可含有定义了构造函数/拷贝控制成员的类类型成员,这一限制在C++11中被取消
  • 若union的成员类型有自己的构造函数/拷贝控制成员,则该union的用法比只含内置类型成员的union复杂得多:
    • 若union仅包含内置类型成员,则可用普通的赋值语句改变union保存的值
    • 若union包含拥有构造函数/拷贝控制成员的对象,则:
      • 将union值改为类类型成员的值时,必须运行该类型的构造函数
      • 将union值从类类型成员改为其他值时,必须运行该类型的析构函数
    • 若union仅包含内置类型成员时,编译器按照成员次序为该union合成默认构造函数和拷贝控制成员
    • 若union包含拥有构造函数/拷贝控制成员的对象,编译器将为该union合成对应的构造函数/拷贝控制成员并将其声明为删除的
    • 例如,string类型定义了默认构造函数和五个拷贝控制成员,若union含有string类型的成员,且未自定义默认构造函数和拷贝控制成员,则编译器将为该union合成缺少的成员并将其声明为删除的。
  • 对于含有类类型成员的union,构造/析构类类型成员的操作很复杂,通常的管理方式是:
    • 把含有类类型成员的union定义为匿名union并内嵌在另一个类中,使用外部类管理union的类类型成员有关的状态转换
    • 在外部类中定义一个独立的枚举类型成员,称为union的判别式,用于追踪union当前存储的是哪个成员
    • 在union中处理类类型成员:
      • 构造时不需要重新申请内存,使用定位new构造
      • 析构时显式调用析构函数
  • 作为union成员的类对象不能被自动销毁,因为union的析构函数不知道union当前是什么类型
  • 例子:管理含类类型成员的union
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//用类管理以类作为成员的union
class Token{
public:
    //默认构造时将union初始化为int成员,值为0
    Token():tok(INT),ival(0) {}
    //拷贝构造时更新union判别式,并调用copyUnion拷贝底层结构体
    Token(const Token &t):tok(t.tok) {copyUnion(t);}
    //拷贝赋值
    Token &operator=(const Token &);
    //析构时判断union当前是否是string类型,若是则显式调用string的析构函数
    ~Token() {if(tok==STR) sval.~string();}
    //以下4个成员分别是用union的4个成员类型来初始化
    Token &operator=(const string &);
    Token &operator=(char);
    Token &operator=(int);
    Token &operator=(double);
private:
    //union判别式,使用不限定作用域的枚举来表示union当前的成员
    enum {INT,CHAR,DBL,STR} tok;
    //底层存储数据的匿名union,每个时刻只有一个有效的对象,节省空间
    union{
        char cval;
        int ival;
        double dval;
        string sval;
    };
    //将实参中的Token内容拷贝进this,是拷贝赋值的组成部分
    void copyUnion(const Token &);
};
//将int拷贝给union的成员,char和double同理
Token &Token::operator=(int i){
    if(tok==STR) sval.~string();            //若当前union是string,则手动销毁string
    ival=i;                                 //赋值
    tok=INT;                                //更新union判别式
    return *this;
}
//将string拷贝给union的成员
Token &Token::operator=(const string &s){
    if(tok==STR)    sval=s;                 //若当前union是string,则直接赋值
    else            new(&sval) string(s);   //若当前union不是string,则在已有内存中用定位new构造
    tok=STR;                                //更新new判别式
    return *this;
}
//将实参中的union的内容拷贝进this的union,是拷贝赋值的组成部分
//只负责处理this不是string的情形(this是string时交给其他部分处理)
void Token::copyUnion(const Token &t){
    switch(t.tok){                              //对实参中的union成员讨论
        case Token::INT:    ival=t.ival; break; //若实参中union不是string,则直接拷贝
        case Token::CHAR:   cval=t.cval; break;
        case Token::DBL:    dval=t.dval; break;
        //若实参中的union是string,则在已有内存中使用定位new构造string
        case Token::STR:    new(&sval) string(t.sval); break;
    }
}
//拷贝赋值函数
Token &Token::operator=(const Token &t){
    //当左侧的union是string,右侧的union不是string,则将左侧的string析构
    if(tok==STR && t.tok!=STR)  sval.~string();
    //当左侧的union是string,右侧的union也是string,则直接拷贝
    if(tok==STR && t.tok==STR)  sval=t.sval();
    //当左侧的union不是string,则调用copyUnion进行处理(故copyUnion不需要考虑this是string的情形)
    else                        copyUnion(t);
    //更新union判别式
    tok=t.tok;
    return *this;
}

局部类

  • 类可定义在函数的内部,称这样的类为局部类(和类内部定义的嵌套类区分)
  • 局部类定义的类型只在定义它的作用域内可见
  • 局部类的所有成员(包括成员函数)都必须完整定义在局部类内,因此成员函数的复杂性不可太高,一般只有几行
  • 局部类不允许有static数据成员,因为static成员需要在类外定义
  • 局部类对其外层作用域中名字的访问受到诸多限制,它只能访问外层作用域中的类型名静态变量枚举成员
  • 若局部类定义在某函数内部,则该函数的普通局部变量不可被该局部类使用
  • 例子:定义局部类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int a,val;
void foo(int val){
    static int si;
    enum Loc{a=1024,b};
    //在函数内部定义局部类
    struct Bar{
        Loc locVal;             //对,可使用外层函数中的局部类型名
        int barVal;
        void fooBar(Loc l=a){   //局部类的成员函数,默认实参是外层函数中的枚举成员Loc::a
            barVal=val;         //错,val是外层函数的局部变量,在局部类中不可见
            barVal=::val;       //对,显式使用全局作用域的变量
            barVal=si;          //对,使用外层函数的局部静态变量
            locVal=b;           //对,使用外层函数的枚举成员
        }
    };
}
  • 外层函数对局部类的protected/private成员没有访问特权,但局部类可将外层函数声明为友元
  • 由于局部类在函数作用域中,故程序中可访问局部类的代码十分有限
  • 局部类内的名字查找和其他类相似,即二段编译:声明时用到的其他名字必须确保已存在,但定义中用到的名字可出现在类内的任意位置
  • 若局部类内的某个名字不是局部类的成员,则继续在外层函数作用域中查找,若还未找到则在外层作用域之外查找
  • 可在局部类的内部再嵌套一个类,此时嵌套类的定义可在局部类之外,但只能在与局部类相同的作用域中,且定义时应指明作用域在局部类中
  • 局部类内的嵌套类也是局部类,必须遵循局部类的规定,该嵌套类的所有成员都必须定义在嵌套类内部
  • 例子:局部类中的嵌套类
1
2
3
4
5
6
7
void foo(){
    class Bar{          //定义局部类
    public:
        class Nested;   //声明局部类中的嵌套类
    };
    class Bar::Nested{/* 局部类中嵌套类的定义 */};
}

固有的不可移植的特性

  • 为了支持底层编程,C++定义了一些固有的不可移植的特性,即因机器而异的特性,例如算术类型的大小因机器而异
  • 位域volatile限定符是从C中继承而来的特性,链接指示是C++新增的特性

位域

  • 类可将其非static数据成员定义为位域,一个位域中含有一定数量的二进制位
  • 程序若要向硬件设备或其他程序传递二进制数据,通常会用到位域。但位域在内存中的布局是机器相关
  • 位域的类型必须是整型或枚举类型
  • 由于signed的位域行为与signed的具体实现相关,故通常使用unsigned类型来存储位域
  • 位域的声明形式是在成员名字后紧跟冒号:constexpr,该constexpr用于指定成员所占的二进制位数
  • 如果可能的话,类内部连续定义的位域会被压缩到同一整型的相邻位,从而提供存储压缩。但这些比特是否能压缩到一个整型以及如何压缩都是机器相关
  • 取地址算符&不可用于位域,因为寻址的单位是字节。因此任何指针都不可指向类的位域
  • 对于超过1位的位域,通常使用内置位运算符处理
  • 若一个类定义了位域成员,通常会定义一组inline成员函数用于检验或设置对应位域
  • 例子:使用位域
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
typedef unsigned int Bit;   //用unsigned int表示位域
//用于操作文件的类
class File{
    //比特形式存储的文件描述符,用位域作为flag
    Bit mode:2;             //占2位
    Bit modified:1;         //占1位
    Bit prot_owner:3;       //占3位
    Bit prot_group:3;       //占3位
    Bit prot_world:3;       //占3位
    /* 其他数据成员 */
public:
    //以八进制的枚举类型来表示比特常量
    enum modes{READ=01,WRITE=02,EXECUTE=03};
    File &open(modes);      //输入一个枚举成员,按该模式打开文件并返回该类的引用
    void close();           //关闭该文件
    void write();           //写入该文件
    bool isRead() const;
    void setWrite();
};
void File::write(){
    modified=1;             //将位域modified置为1,标记已被修改过
    //操作
}
void File::close(){
    if(modified)            //如果位域modified是1,则保存内容
        //保存内容
}
File &File::open(File::modes m){
    mode|=READ;             //将mode设置为READ
    //其他处理
    if(m&WRITE)             //如果同时设置了READ和WRITE  
        //按照可读可写模式打开文件
    return *this;
}
//返回是否为READ模式
inline bool File::isRead() const {return mode&READ;}
//设置为WRITE模式
inline void File::setWrite() {mode|=WRITE;}

volatile限定符

  • volatile的确切含义与机器相关,只能阅读编译器文档。要想让使用volatile的程序移植到新机器/新编译器仍有效,经常需要修改程序
  • 直接处理硬件的程序经常包含这样的数据:它们的值由程序之外的过程控制,例如系统时钟等
  • 当对象的值可能在程序的控制/检测之外被改变,则应将该对象声明为volatile,该关键字告诉编译器,不应该对该对象进行优化
  • volatile的用法和const很像,都是对类型额外修饰:
    • const和volatile相互不影响,可同时使用
    • 可将类的成员函数定义为volatile的,且只有volatile的成员函数才能被volatile的对象调用
    • 可声明volatile指针、指向volatile对象的指针、指向volatile对象的volatile指针
    • volatile对象的地址(或指向volatile类型的指针)只能被赋值给指向volatile对象的指针
    • volatile对象只能用于初始化volatile引用
  • 例子:使用volatile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
volatile int display_register;  //该变量可能在程序控制之外发生改变
volatile Task *curr_task;       //指向volatile对象的指针
volatile int iax[max_size];     //数组的每个元素都是volatile
volatile Screen bitmapBuf;      //类的每个成员都是volatile
volatile int v;                 //定义volatile对象
int *volatile vip;              //指针是volatile,指向对象非volatile
volatile int *ivp;              //指针是非volatile,指向对象是volatile
volatile int *volatile vivp;    //指针是volatile,指向对象是volatile
int *ip=&v;                     //错,对于volatile对象,必须使用指向volatile对象的指针
ivp=&v;                         //对,可用指向volatile对象的非volatile指针来指向volatile对象
vivp=&v                         //对,可用指向volatile对象的volatile指针来指向volatile对象
  • const和volatile的重要区别之一是:不能使用合成的拷贝/移动构造函数/赋值算符来初始化volatile对象,或为volatile对象赋值。因为合成的成员接受的形参类型是非volatile的const引用
  • 若一个类希望拷贝/移动/赋值它的volatile对象,则必须自定义拷贝/移动操作,例如将形参设置为const volatile &
  • 可为volatile对象定义拷贝/赋值操作,但拷贝/赋值volatile对象不一定有意义,该对象可能与设备相关(例如stream)
  • 例子:自定义拷贝控制,以支持volatile对象
1
2
3
4
5
6
class Foo{
public:
    Foo(const volatile Foo &);                      //从volatile对象拷贝
    Foo &operator=(volatile const Foo &);           //将volatile对象赋值给非volatile对象
    Foo &operator=(volatile const Foo &) volatile;  //将volatile对象赋值给volatile对象
};

链接指示:extern “C”

  • C++有时需要调用其他语言编写的函数,如C语言
  • 调用其他语言中的函数时,函数名也必须在C++中声明,且必须指定返回类型和形参列表
  • 对于其他语言编写的函数,编译器检查其调用的方式与普通C++函数相同,但对该函数生成的代码有所不同
  • C++使用链接指示指出任意非C++函数使用的语言
  • 若要将C++的代码与其他语言的代码放在一起使用,必须有权访问该语言的编译器,且该语言编译器与当前C++编译器兼容
  • 链接指示可以有两种形式:
    • 单个的链接指示:以关键字extern开头,后接表示另一种语言的字符串字面值常量,最后是普通的函数声明,该函数是另一种语言的函数
    • 复合的链接指示:以关键字extern开头,后接表示另一种语言的字符串字面值常量,最后是一个花括号的语句块,块中声明若干个函数,它们都被标记为另一种语言的函数。花括号内声明的函数名字对外可见,就像是在花括号外声明的一样
  • 链接指示不能出现在类定义或函数定义内部
  • 同一个函数的链接指示必须在它的每个声明中都出现
  • 复合的链接指示可应用于整个头文件,当#include指示被放在复合链接指示的花括号中时,头文件中的所有普通函数声明都被认为是链接指示的语言编写的。
  • 链接指示可嵌套,即,若链接指示中包含的头文件内也有链接指示的函数,则函数的链接不受影响,仍是头文件中的版本
  • C++从C中继承的标准库函数可定义为C函数,但并非必须。这取决于具体实现。
  • 例子:链接指示
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//单个的链接指示,将该函数标记为C语言的函数
extern "C" size_t strlen(const char *);
//复合的链接指示,将这些函数都标记为C语言的函数
extern "C" {
    int strcmp(const char *,const char *);
    char *strcat(char *,const char *);
}
//复合的链接指示应用于整个头文件
extern "C" {
    #include<string.h>
}
  • 编写函数使用的语言是函数类型的一部分。故对于使用链接指示的函数,其每个声明都必须有相同的链接指示,且函数指针必须与所指函数使用相同的链接指示
  • 指向C函数的指针与指向C++函数的指针是不同类型。指向C函数的指针不能被C++的函数初始化或赋值,反之亦然。也不能在两个链接指示不同的函数指针之间互相赋值。
  • 有的C++编译器可使链接指示不同的函数指针之间互相赋值,这被视作对语言的扩展。严格来说这是非法的
  • 例子:链接指示是函数类型的一部分
1
2
3
4
extern "C" void (*pf)(int);     //链接指示是函数类型的一部分
void (*pf1)(int);               //pf1与pf类型不同
extern "C" void (*pf2)(int);    //pf2与pf类型相同
pf1=pf2;                        //错,两指针指向函数的链接指示不一样,类型不同
  • 使用链接指示时,它不仅对函数声明有效,且对函数声明中的返回类型和形参列表中的函数指针也有效。即,链接指示同时对声明语句中的所有函数有效
  • 若希望对C++函数传入一个指向C函数的指针,需使用类型别名
  • 例子:链接指示同时对声明语句中的所有函数有效
1
2
3
extern "C" void f1(void (*)(int));  //f1是C函数,其形参也是指向C函数的指针
extern "C" typedef void FC(int);    //使用类型别名定义C函数类型
void f2(FC *);                      //将C函数的指针传给C++函数
  • 通过使用链接指示对函数进行定义,可令C++函数在其他语言编写的程序中可用。例如,标记为extern "C"的函数可被C程序调用
  • 可被多种语言共享的函数,其返回类型和形参类型受到诸多限制。例如,不可能把C++的类对象传递给C程序,它无法理解类特有的操作
  • 有时需要在C和C++中编译同一个源文件,利用预处理器定义的__cplusplus宏变量,可实现仅在编译C++时才包含一些代码
  • 例子:使用__cplusplus条件编译C++代码
1
2
3
4
#ifdef __cplusplus
/* 这里是仅对C++可见的代码 */
#endif
/* 其他地方都对C和C++同样可见 */
  • 链接指示与重载函数的相互作用依赖于目标语言:若目标语言支持函数重载,则为该语言实现链接指示的编译器很可能也支持重载这些C++函数
  • C语言不支持函数重载,故C的链接指示只能用于说明一组重载函数中的一个,否则报错
  • 若在一组重载函数中有一个是C函数,则其他的必定都是C++函数
  • 使用了类类型形参/返回类型的C++函数都只能在C++程序中调用
  • 例子:C语言不支持函数重载
1
2
3
4
5
6
7
extern "C" void print(const char *);    //声明为C函数
extern "C" void print(int);             //错,C函数不支持重载,和上面一句冲突
class SmallInt{/* 定义类 */};
class BigNum{/* 定义类 */};
extern "C" double calc(double);         //该重载版本可被C使用
extern SmallInt calc(const SmallInt &); //该重载版本不可被C使用
extern BigNum calc(const BigNum &);     //该重载版本不可被C使用
原文地址:https://www.cnblogs.com/updown/p/14281333.html