第十六章 模板与泛型编程
- 模板是C++泛型编程的基础
- 一个模板就是一个创建类或函数的蓝图或者说公式
- 转换发生在编译时
- 模板定义
- 在模板定义中,模板参数列表不能为空
- 运行时,调用者提供实参来初始化形参
- 当调用一个函数模板时,编译器用函数实参来为我们推断模板参数
- 模板类型参数
- 类型参数前必须使用关键字class或typename
- 非类型模板参数
- 表示一个值而非一个类型
- 可以是整型,或者是一个指向对象或函数类型的指针(左值)
- 非类型模板参数的模板实参必须是常量表达式
- 函数模板可以声明为inline或constexpr, 这两个说明符放在模板参数列表之后,返回类型之前
- 编写泛型代码的两个重要原则
- 模板中的函数参数是const引用
- 函数体中的条件判断仅使用<比较运算
- 通过将函数参数设定为const的引用,我们保证了函数可以用于不能拷贝的类型
- 模板程序尽量减少对参数类型的要求
- 模板编译
- 当编译器遇到一个模板定义时,它并不生成代码。
- 只有当我们实例化出模板的一个特定版本时,编译器才会生成代码
- 当我们使用(而不是定义)模板时,编译器才生成代码
- 函数模板和类模板成员函数的定义通常放在头文件中
- 模板直到实例化时才会生成代码,这一特性影响了我们何时才会获知模板内代码的编译错误,通常,编译器会在三个阶段报告错误
- 保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任
- 类模板
- 是用来生成类的蓝图的,与函数模板不同之处是,编译器不能为类模板推断模板参数类型
- 一个类模板的每个实例都形成一个独立的类,与任何其他使用该类模板的类型没有关联
- 成员函数
- 其本身是一个普通函数,但是类模板的每个实例都有自己版本的成员函数。
- 因此,类模板的成员函数具有和模板相同的模板参数
- 在类模板之外的成员函数必须以关键字template开始,后接类模板参数列表
template<typename T> return-type StrBlob<T>::member-name(parm-list) - 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化
- 在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参
BlobPtr<T> BlobPtr ret = *this; - 类模板和友元
- 当一个类包含一个友元声明时,类与友元各自是否是模板互相无关
- 如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例
- 如果一个友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例
- 一对一友好关系
- 通过和特定的模板友好关系
friend class Pal<C>; 特定实例化友元template <typename T> friend class Pal2; 模板友元 - 令模板自己的类型参数为友元
- 模板类型别名
typedef Blob<string> StrBlob; - 类模板可以声明static成员
- 模板参数
- 模板参数遵循普通的作用域规则
- 一个模板参数名的可用范围在其声明之后
- 模板声明必须包含模板参数
- 当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class
- 默认模板实参
- 在新标准中,我们可以为函数和模板类提供默认实参。而更早的C++标准只允许为类模板提供默认实参
template <typename T, typename F = less<T>> - 无论何时使用一个类模板,我们都必须在模板名之后接上尖括号
- 成员模板
- 成员模板不能是虚函数
- 对于类模板,我们也可以为其定义成员模板
- 为了实例化一个类模板的成员模板,我们必须同时提供类和函数模板的实参
- 控制实例化
-
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化来避免这种开销。 extern template declaration;
template declaration
extern template class Blob<string>;
template int compare(const int&, const int&);
- 当编译器遇到extern模板声明时,它不会在本文件中生产实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有实例化的一个非extern声明(定义)。 对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义
- 由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前。
-
对于每个实例化声明,在程序中某个位置必须有其显式的实例化定义 -
在一个类模板的实例化定义时,所用类型必须能用于模板的所有成员函数 - 模板实参推断
- 从函数实参来确定模板实参的过程称为模板实参推断
- 顶层const无论是在形参中还是在实参中,都被忽略。其他类型转换中,能在调用中应用于函数模板的包括如下两项
- const 转换
- 可以将一个非const对象的引用或指针传递给一个const的引用或指针形参
- 数组或函数指针转换
- 如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针
- 将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换
- 使用相同模板参数类型的函数形参
- 一个模板类型参数可以作用多个函数形参的类型。由于只允许有限几种类型转换,因此传递给这些形参的实参必须具有相同的类型。
- 如果函数参数类型不是模板参数,则对实参进行正常的类型转换
- 进行类型转换的标准库模板类
- 为了获取元素类型,我们可以使用标准库的类型转换模板,这些模板定义在头文件type_traits中
- 标准类型转换模板《参照606页表》
- 当参数是一个函数模板实例的地址时,程序上下文必须满足
- 模板实参推断和引用
- 当一个函数是模板类型参数的一个普通(左值)引用时,只能传递给它一个左值
- 实参可以是const类型,也可以不是
- 如果实参是const的,则T将被推断为const类型
- 引用折叠和右值引用参数
- 如果将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。
- 当我们间接创建一个引用的引用,则形成引用折叠
X& &、X& &&和X&& &都折叠成类型X& 类型X&& &&折叠成X&& - 引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
- 如果一个函数参数是指向模板参数类型的右值引用,则可以传递给它任意类型的实参。如果将一个右值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。
- 标准库move函数是使用右值引用的模板的一个很好的例子
- 工作方式:
std::move(string("bye!"));
- 推断出的T类型为string
- 因此,remove_reference用string进行实例化
remove_reference<string> 的type成员是string- move的返回类型是string&&
- move的函数参数t的类型为string&&
- 虽然不能隐式的将一个左值转换为右值引用,但是可以用static_cast显式的将一个左值转换为一个右值引用。
- 如果一个函数参数是指向模板类型参数的右值引用(如T&&), 它对应的实参的const属性和左值/右值属性将得到保持
- std::forward(建议不使用using声明)
- 能保持原始实参的类型
- 定义在头文件utility中
- 于move不同,forward必须通过显式模板实参来调用,返回该显式实参类型的右值引用。即。forward的返回类型是T&&。
finalFcn(std::forward<Type>(arg)); - 当用于一个指向参数模板类型的右值引用函数参数时,forward会保持实参类型的所有细节。
- 重载与模板
- 函数模板可以被一个模板或一个普通非模板函数重载
- 与函数重载相同,名字相同的函数必须具有不同数量或类型的参数
- 涉及函数模板,重载函数匹配规则
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板
- 与往常一样,可行函数(模板与非模板)按类型转换来排序
- 与往常一样,如果恰好一个函数提供比任何其他函数都更好的匹配,则选择此函数。
- 如果多个函数提供同样好的匹配
- 如果这些函数中有一个是非模板函数,则选择此函数
- 如果这些函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板
- 否则,此调用有歧义
- 正确定义一组重载的函数模板需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解。
- 在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本
- 可变参数模板
- 是一个接受可变数目的模板函数或模板类
- 参数包
- 我们用一个省略号来指出一个模板参数或函数参数表示一个包
- 在一个模板参数列表中,class … 或typename… 指出接下来的参数表示零个或多个类型的列表
- sizeof… 运算符
- 当我们需要知道包中有多少元素时,可以使用sizeof…运算符
- 返回一个常量表达式,而且不会对其实参求值
template<typename .. Args> void g(Args ... args){} sizeof...(Args) - 当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归。
- 包扩展
- 转发参数包
- 在新标准下,可以组合使用可变参数模板于forward机制来编写函数,实现将其实参不变的传递给其他函数
work(std::forward<Args>(args)...); - 模板特例化
第十七章 标准库特殊设施
tuple 类型
- tuple是类似pair的模板
- 当我们希望将一些数据组合成单一对象,又不想定义一个新数据结构来表示这些数据时使用
- 支持的操作
tuple<T1, T2, ..., Tn> t;
- 成员数为n。第i个成员的类型为
T
i
T_i
Ti?,所有成员都进行初始化
tuple<T1, T2, ..., Tn>t(v1, v2, ..., vn);
- 每个元素使用
v
i
v_i
vi?进行初始化,此构造函数是explicit的
make_tuple(v1, v2, ..., vn);
- 与pair相同,tuple的类型从初始值的类型推断
t1 == t2
- 当两个tuple具有相同数量的成员且成员对应相等时,两个tuple相等。一旦发现某个成员不相等,接下来的成员就不用比较了
t1 != t2
t1 relop t2
- tuple的关系运算符使用字典序。两个tuple必须具有相同数量的成员。
get<i>(t)
- 返回t的第i个数据成员引用,
- 如果t是一个左值,结果是一个左值引用
- 否则,结果是一个右值引用
- tuple的所有成员都是public的
tuple_size<tupleType>::value
- 一个类模板,可以通过一个tuple类型来初始化
- 它有一个名为value的public constexpr static 类型成员
tuple_element<i, tupleType>::type
- 一个模板类,可以通过一个整型常量和一个tuple类型来初始化
- 它有一个名为type的public成员
- 我们可以将tuple看作一个 “快速而随意”的数据结构
- 由于tuple定义了<和==运算符。我们可以将tuple序列传递给算法,并且可以在无序容器中将tuple作为关键字类型
- 可以使用tuple返回多个值
bitset类型
- 定义
bitset<32> bitvec(1U); 32位;低位为1, 其他位为0
- 编号从0开始的二进制位被称为低位
- 编号到31结束的二进制位被称为高位
- 初始化bitset方法
bitset<n> b;
- b有n位,每一位均为0
- 此构造函数是一个constexpr
bitset<n> b(u);
- b是unsigned long long 值u的低n位的拷贝
- 如果n大于unsigned long long的大小,则b中超出unsigned long long的高位被置为0
- 此构造函数是一个constexpr
bitset<n> b(s, pos, m, zero, one);
- b是string s从位置pos开始m个字符的拷贝。s只能包含字符zero或one
- 如果s包含任何其他字符,构造函数会抛出invalid_argument异常
- 字符在b中分别保存为zero或one。
- pos默认为0
- m默认为string::npos
- zero默认为’0’
- one默认为’1’
bitset<n> b(cp, pos, m, zero, one);
- 与上一个构造函数相同,但从cp指向的字符数组中拷贝字符
- 如果未提供m,则cp必须指向一个C风格字符串
- 如果提供了m,则从cp开始必须至少有m个zero或one字符
- 接受一个string或一个字符指针的构造函数是explicit的,在新标准中增加了为0和1指定其他字符的功能
- 如果string包含的字符数比bitset少,则bitset的高位被置为0
- bitset操作
b.any()
b.all()
b.none()
b.count()
b.size()
b.test(pos)
- 若pos位置的位是置位的,则返回true,否则返回false
b.set(pos, v)
- 将位置pos处的位设置为bool值v。v默认为true
b.set()
b.reset(pos)
b.reset()
b.flip(pos)
b.flip()
b[pos] **需要验证
- 访问b中位置pos处的位
- 如果b是const的,则当该位置位时b[pos]返回一个bool值true,否则返回false
b.to_ulong() b.to_ullong()
- 返回一个unsigned long或一个unsigned long long值,其位模式与b相同。如果b中位模式不能放入指定的结果类型,则抛出一个overflow_error异常。
b.to_string(zero, one)
- 返回一个string,表示b中的位模式。zero和one的默认值分别为0,1,用来表示b中的0和1
os << b
is>>b
- 从is读取字符存入b,当下一个字符不是1或0时,或是已经读入b.size()个位时,读取过程停止
正则表达式
- 正则表达式库组件
- regex
- regex_match
- regex_search
- regex_replace
- sregex_iterator
- 迭代器适配器,调用regex_search来遍历一个string中所有匹配的子串
- smatch
- ssub_match
- regex_search 和 regex_match
- 这些操作返回bool值,指出是否找到匹配
- 参数
(seq, m , r, mft) (seq, r, mft)
- 在字符序列seq中查找regex对象r中的正则表达式
- seq可以是一个string、表示范围的一对迭代器以及一个指向空字符结尾的字符数组的指针
- m是一个match对象,用来保存匹配结果的相关细节。
- mft是一个可选的regex_constants::match_flag_type值,影响匹配过程
- regex(和 wregex)选项
regex r(re)
- re表示一个正则表达式,它可以是一个string、一个表示字符范围的迭代器对、一个指向空字符结尾的字符数组的指针、一个字符指针和一个计数器或是一个花括号包围的字符列表,
regex r(re, f)
- f是指出对象如何处理的标志。f通过下面列出的值来设置。如果未指定f,其默认值为ECMAScript
r1 = re
- 将r1中的正则表达式替换为re。re表示一个正则表达式,它可以是另一个regex对象、一个string、一个指向空字符结尾的字符数组的指针或一个花括号包围的字符列表
r1.assign(re, f)
- 与使用赋值运算符(=)效果相同:可选的标志f也与regex的构造函数中对应的参数含义相同
r.mark_count()
r.flags
- 构造函数和赋值操作可能抛出类型为regex_error的异常
- 定义regex时指定的标志
- 定义在regex和regex_constants::syntax_option_type中
- icase
- nosubs
- optimize
- ECMAScript
- 使用ECMA-262指定的语法(很多Web浏览器使用的语言)
- basic
- extended
- awk
- grep
- egrep
- 一个正则表达式的语法是否正确是在运行时解析的
- 正则表达式错误类型
- 定义在regex和regex_constants::error_type中
error_collate
error_ctype
error_escape
error_backref
error_brack
error_paren
error_brace
error_badbrace
error_range
error_space
error_badrepeat
- 重复字符(*、?、+或{)之前没有有效的正则表达式
error_complexity
error_stack
- 正则表达式的编译是一个非常慢的操作
- 为了最小化这种开销,应该避免创建很多不必要的regex
- 如果在一个循环中使用正则表达式,应该在循环外创建他
- 正则表达式库类
- 输入序列类型
- string
- regex、smatch、ssub_math和sregex_iterator
- const char*
- regex、cmatch、csub_math和cregex_iterator
- wstring
- wregex、wsmatch、wssub_math和wsregex_iterator
- const wchar_t*
- wregex、wcmatch、wcsub_math和wcregex_iterator
- smatch操作
- 这些操作也适用于cmatch、wsmatch、wcmatch和对应的csub_match、wssub_match和wcsub_match
- m.ready()
- 如果已经通过调用regex_serach或regex_match设置了, 则返回true;否则返回false。
- 如果ready返回false, 则对m进行操作是未定义的
- m.size()
- 如果匹配失败,则返回0;否则返回最近一次匹配的正则表达式中子表达式的数目
- m.empty()
- m.prefix()
- 一个ssub_match对象,表示当前匹配之前的序列
- m.suffix()
- 一个ssub_match对象,表示当前匹配之后的部分
- m.format(…) 后面有解释
- 在接受一个索引操作中,n的默认值为0且必须小于m.size()
- 第一个子匹配(索引为0)表示整个匹配
- m.length(n)
- m.position(n)
- m.str(n)
- m[n]
- m.begin(), m.end()
- m.cbegin(), m.cend()
- 表示m中sub_match元素范围的迭代器。与往常一样,cbegin和cend返回const_iterator
- 子匹配操作
- 这些操作适用于ssub_match、csub_match、wssub_match、wcsub_match
- matched
- 一个public bool数据成员,指出此ssub_match是否匹配了
- first
- second
- public数据成员,指向匹配序列首元素和尾后位置的迭代器
- 如果未匹配,则first和second是相等的
- length()
- 匹配的大小,如果matched为false,则返回0
- str()
- 返回一个包含输入中匹配部分的string
- 如果matched为false,则返回空string
- s = ssub
- 将ssub_match对象ssub转化为string对象s
- 等价于s = ssub.str()
- 转换运算符不是explicit
- 正则表达式的替换操作
- m.format(dest, fmt, mft)
- m.format(fmt, mft)
- 使用格式字符串fmt生成格式化输出,匹配在m中,可选的match_flag_type标志在mft中。
- 第一个版本写入迭代器dest指向的目的位置并接受fmt参数,可以是一个string,也可以是表示字符数组中范围的一对指针。
- 第二个版本返回一个string,保存输出,并接受fmt参数,可以是一个string,也可以是一个指向空字符结尾的字符数组的指针。mft的默认值为format_default
- regex_replace(dest, seq, r, fmt, mft)
- regex_replace(seq, r, fmt, mft)
- 遍历seq,用regex_search查找与regex对象r匹配的子串。使用格式字符串fmt和可选的match_flag_type标志来生成输出。
- 第一个版本将输出写入到迭代器dest指定的位置,并接受一对迭代器seq表示范围。
- 第二个版本返回一个string,保存输出,且seq既可以是一个string也可以是一个指向空字符结尾的字符数组的指针。
- 在所有情况下,fmt既可以是一个string也可以是一个指向空字符结尾的字符数组的指针。且mft的默认值为match_default
- 匹配标志
- 定义在regex_constants::match_flag_type中
match_default
match_not_bol
match_not_eol
match_not_bow
match_not_eow
match_any
match_not_null
match_continuous
match_prev_avail
format_default
format_sed
format_no_copy
format_first_only
随机数
- rand函数有一些问题
- 即使不是大多数,也有很多程序需要不同范围的随机数。
- 一些应用需要随机浮点数。而程序员为了解决这些问题而试图转换rand生成的随机数范围、类型或分布时,常常会引入非随机性
- 随机数库定义在头文件random中
- C++程序不应该使用库函数rand,而应使用default_random_engine类和恰当的分布类对象
- 随机数引擎和分布
- 生成随机无序号数
default_random_engine e; e(); - 随机数引擎操作
Engine e;
Engine e(s);
s.seed(s);
e.min(); e.max();
Engine::result_type
e.discard(u)
- 将u定义为uniform_int_distribution
- 当我们说随机数发生器时,是指分布对象和引擎对象的组合
- 一个给定的随机数发生器一直会生成相同的随机数序列。一个函数如果定义了局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static的。否则,每次调用函数都会生成相同的序列
- 设置随机数发生器种子
- 随机数发生器会生成相同的随机数序列这一特性在调试中很有用。但是一旦程序调试完毕,可以提供一个种子让程序每次都生成不同的随机结果
- 引擎设置种子的两种方式
- 在创建引擎对象时提供种子,或者调用引擎的seed成员
- 时间作为种子
- 由于time返回以秒计的时间,因此适用于生成种子的间隔为秒级或更长的应用。
- 如果程序作为一个自动过程的一部分反复运行,将time返回值作为种子的方式就无效了
- 生成随机实数
- 程序常需要一个随机浮点数的源
default_random_engine e; uniform_real_distribution<double> u(0, 1); u(e) - 分布类型操作
Dist d; d(e)
- 用相同的e连续调用d的话,会根据d的分布式类型生成一个随机数序列
- e是一个随机数引擎对象
d.min() d.max()
d.reset()
- 重建d的状态,使得随后对d的使用不依赖于d已经生成的值
uniform_real_distribution<> u(0, 1);
- 生成非均匀分布的随机数
default_random_engine e; normal_distribution<> n(4, 1.5);
- bernoulli_distribution类
- 不接受模板参数
- 此分布总是返回一个bool值,概率默认为0.5
default_random_engine e; bernoulli_distribution b; b(e);
IO库再探
- 三个更特殊的IO库特性
- 很多操作符改变格式状态
- 操纵符用于两大类输出控制
- 大多数改变格式状态的操纵符都是设置/复原成对的
- 当操纵符改变流的格式状态时,通常改变后的状态对所有后续IO都生效。
- 控制布尔值的格式
- 指定整数值的进制, 浮点数不受影响
- 在输出中指出进制
- 使用showbase操纵符
- 规范
- 前导0x表示十六进制
- 前导0表示八进制
- 无前导字符串表示十进制
cout << showbase; ... cout << noshowbase; - 控制浮点数格式
- 可以控制浮点数输出三种格式
- 以多高精度(多少个字符)打印浮点值
- 数值是打印为十六进制、定点十进制还是科学记数法形式
- 对于没有小数部分的浮点值是否打印小数点
- 默认情况下,浮点值按六位数值精度打印
- 如果浮点值没有小数部分,则不打印小数点
- 指定打印精度
- 浮点值按当前精度舍入而非截断
- 通过调用IO对象的precision成员或使用setprecision操纵符来改变精度
- 操纵符 setprecision和其他接受参数的操纵符都定义在头文件iomanip中
cout.precision
cout.precision(12)
cout << setprecision(3);
- 定义在iostream中的操纵符
- boolalpha
- noboolalpha
- showbase
- noshowbase
- showpoint
- noshowpoint
- showpos
- noshowpos
- uppercase
- nouppercase
- dec
- hex
- oct
- left
- right
- internal
- fixed
- scientific
- hexfloat
- defaultfloat
- unitbuf
- nounitbuf
- skipws
- noskipws
- flush
- ends
- endl
- 除非你需要控制浮点数的表示形式,否则由标准库选择记数法是最好的方式
- 定义在iomanip中的操纵符
setfill(ch)
setprecision(n)
set(w)
setbase(b)
- 未格式化的输入输出操作
- 我们可以使用未格式化IO操作get和put来读取和写入一个字符
- 单字节底层IO操作
is.get(ch)
- 从istream is读取下一个字节存入字符ch中,返回is
op.put(ch)
is.get()
is.putback(ch)
is.unget()
is.peek()
- 头文件cstdio定义了一个名为EOF的const
- 多字节底层IO操作
is.get(sink, size, delim)
- 从is中读取最多size个字节,并保存在字符数组中,字符数组的起始地址由sink给出。
- 读取过程直至遇到字符delim或读取了size个字节或遇到文件尾时停止。
- 如果遇到delim,则将其留在输入流中,不读取出来存入sink
is.getline(sink, size, delim)
- 与接受三个参数的get版本类似,但会读取并丢弃delim
is.read(sink, size)
- 读取最多size个字节,存入字符数数组sink中,返回is
is.gcount()
os.write(source, size)
- 将字符数组source中的size个字节写入os,返回os
is.ignore(size, delim)
- 读取并忽略最多size个字符,包括delim
- 与其他未格式化函数不同,ignore有默认参数;
- size的默认值为1,delime默认值为文件结尾
- get和getline函数接受相同的参数,它们的行为类似但不相同。
- 在两个函数中,sink都是一个char数组,用来保存数据。两个函数都一直读取数据,直至下面条件之一发生
- 常见的错误
- 本想从流中删除分隔符,但却忘了做
- 将get或peek的返回值赋予了一个char而不是一个int
- 在一台char被实现为signed char的机器上,不能确定循环行为
- 流随机访问
- 首先读取最后一行,然后读取第一行,依此类推
- 随机IO本质上是依赖于系统的。
- 由于istream和ostream类型通常不支持随机访问,适用于fstream和sstream
- 为了支持随机访问,IO类型维护一个标记来确定下一个读写操作在哪里进行
- 一个函数通过seek到一个给定位置来重定位它
- 另一个函数tell我们标记的当前位置
- seek和tell函数
tellg() tellp()
- 返回一个输入流中(tellg)或输出流中(tellp)标记的当前位置
seekg(pos) seekp(pos)
- 在一个输入流或输出流中将标记重定位到给定的绝对地址
seekp(off, from) seekg(off, from)
- 在一个输入流或输出流中将标记定位当from之前或之后off个字符,from可以是下列值之一
- 由于只有单一的标记,因此只要我们在读写操作间切换,就必须进行seek操作来重定位标记
第十八章 用于大型程序的工具
异常处理
- 当执行一个throw时, 跟在throw后面的语句将不再被执行。相反,程序的控制权从throw转移到与之匹配的cath模块。
- 该catch可能是同一个函数中的局部catch,也可能位于直接或间接调用了发生异常的函数的另一个函数中。
- 控制权从一处转移到另一处有两个重要含义
- 沿着调用链的函数可能会提早退出
- 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁
- 栈展开
- 程序暂停当前函数的执行过程并立即开始寻找与异常匹配的catch子句。
- 当throw出现在一个try语句块内时,检查与该try块关联的catch子句
- 如果还是找不到匹配的catch,则退出当前的函数
- 栈展开过程沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的catch子句
- 或者也可能一直没有找到匹配的catch,则退出主函数后查找过程终止
- 一个异常如果没有被捕获,则它将终止当前的程序
- 块退出后它的局部对象也随之销毁
- 析构函数与异常
- 如果我们使用类来控制资源的分配,就能确保无论函数正常结束,还是遭遇异常,资源都能被正确的释放
- 出于栈展开可能使用析构函数的考虑,析构函数不应该抛出不能被它自身处理的异常
- 换句话说,如果析构函数需要执行某个可能抛出异常的操作,则该操作应该被放置在一个try语句中,并且在析构函数内部得到处理
- 在栈展开的过程中,运行类类型的局部对象的析构函数。因为这些析构函数是自动执行的,所以他们不应该抛出异常。一旦在栈展开的过程中析构函数抛出了异常。并且析构函数自身没能力捕获该异常,则程序将被终止。
- 抛出指针要求在任何对应的处理代码存在的地方,指针所指向的对象都必须存在
- 异常捕获
- catch子句中的异常声明看起来像是只包含一个形参的函数形参列表
- 声明的类型决定了处理代码所能捕获的异常类型
- 这个类型必须是完全类型,它可以是左值引用,但不能是右值引用
- 通常情况下,如果catch接受的异常与某个继承体有关系,则最好将该catch的参数定义成引用类型。
- 查找匹配的处理代码
- 越是专门的catch越应该置于整个catch列表的前端
- 除了一些极细小的差别之外,要求异常的类型和catch声明的类型是精确匹配的
- 允许从非常量的类型转换,也就是说,一条非常量对象的throw语句可以匹配一个接受常量引用的catch语句
- 允许从派生类向基类的类型转换
- 数组被转换成指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针
- 如果在多个catch语句的类型之间存在着继承关系,则我们应该把继承链最底端的类放在前面,而将继承链最顶端的类放在后面
- 重新抛出
- 通过重新抛出的操作将异常传递给另一个catch语句
- 捕获所有异常的处理代码
- catch(…)
- 既能单独出现,也能与其他几个catch语句一起出现
- 如果catch(…)与其他几个catch语句一起出现,则catch(…)必须在最后的位置。
- 出现在捕获所有异常语句后面的catch语句将永远不会被匹配
- 函数try语句块与构造函数
- 想要处理构造函数初始值抛出的异常,我们必须将构造函数写成函数try语句块的形式(也称函数测试块形式)
- noexcept异常说明
- C++新标准中,我们可以通过提供noexcept说明指定某个函数不会抛出异常
- 对于一个函数来说,noexcept 说明要么出现在函数的所有声明语句中和定义语句中要么一次也不会出现
- noexcept说明符需要跟在const及引用限定符之后,而在final、override或虚函数的=0 之前
- 违反异常说明
- 通常情况下,编译器不能也不必在编译时验证异常说明
- 早期C++版本设计了一套更加详细的异常说明方案,该方案使得我们可以指定某个函数可能抛出的异常类型。
- 函数可以指定一个关键字throw,在后面跟上括号括起来的异常类型列表。
- throw说明符所在的位置与新版C++中noexcept所在的位置相同
- 上述使用的throw的异常说明方案在C++11版本中已经被取消了
- 异常说明参数
- noexcept 说明符接受一个可选的参数,该实参必须能转换为bool类型
- 如果是true,则函数不会抛出异常
- 如果实参是false,则函数可能抛出异常
void recoup(int) noexcept(true); void alloc(int) noexcept(false); - noexcept运算符
- noexcept有两层含义
- 当跟在函数参数列表后面时它是异常说明符
- 当作为noexcept异常说明的bool实参出现时,它是一个运算符
- 异常说明与指针、虚函数和拷贝控制
- 如果一个虚函数承诺了它不会抛出异常,则后续派生类出来的虚函数也必须做出同样的承诺
- 与之相反,如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常
- 如果对所有成员和基类的所有操作都承诺不会抛出异常,则合成的成员是noexcept的
- 异常类层次
- exception
- bad_cast
- runtime_error
- overflow_error
- underflow_error
- range_error
- logic_error
- domain_error
- invalid_argument
- out_of_range
- length_error
- bad_alloc
- 类exception、bad_cast和bad_alloc定义了默认构造函数
- 类runtime_error和logic_error没有默认构造函数
- 继承体系的第二层将exception划分为两大类别
命名空间
- 命名空间的定义
- 包含两个部分
- 和其他名字一样,命名空间的名字也必须在定义它的作用域内保持唯一。
- 命名空间既可以定义在全局作用域内,也可以定义在其他命名空间中
- 但是不能定义在函数或类内部
- 命名空间作用域后面无须分号
- 命名空间的组织方式
- 命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中
- 命名空间成员的定义部分则置于另外的源文件中
- 定义多个类型不相关的命名空间应该使用单独的文件分别表示每个类型(或关联类型构成的集合)
- 全局命名空间
- 全局作用域中定义的名字,也就是定义在全局命名空间中
- 因为全局作用域是隐式的,所以它并没有名字。
::member_name - 表示全局命名空间的一个成员
- 內联命名空间
- C++11新标准引入了一种新的嵌套命名空间,称为內联命名空间。
- 和普通的嵌套命名空间不同,內联命名空间中的名字可以被外层命名空间直接使用
- 也就是说,我们无须在內联命名空间的名字前添加表示命名空间的前缀,通过外层命名空间的名字就可以直接访问它。
inline namespace madata{ } - 当应用程序的代码在一次发布和另一次发布之间发生了改变时,常常会用到內联命名空间
- 未命名的命名空间
- 是指关键字namespace后紧跟花括号括起来的一系列声明语句
- 未命名的命名空间中定义的变量拥有静态生命周期
- 一个未命名的命名空间可以在某个给定的文件內不连续,但是不能跨越多个文件
- 和其他命名空间不同,未命名的命名空间仅在特定的文件内部有效,其作用范围不会横跨多个不同的文件
- 未命名的命名空间取代文件中的静态声明
- 在标准C++引入命名空间的概念之前,程序需要将名字声明成static以使得其对整个文件有效。
- 在文件中进行静态声明的做法是从C语言继承而来。在C语言中,声明为static的全局实体在其所在的文件外不可见
- 在文件中进行静态声明的做法已经被C++标准取消了,现在的做法是使用未命名的命名空间
- 使用命名空间成员
- 像namespace_name::member_name这样使用命名空间的成员显然非常繁琐
- 简便方法
- 命名空间别名
namespace primer = cplusplus_primer; - using指示
- 一条using声明语句一次只能引入命名空间的一个成员
- using指示和using声明类似的地方是
- 和using声明不同的地方是
using namespace namespace_name; - 如果我们提供了一个对std等命名空间的using指示而未做任何特殊控制的话,将重新引入由于使用了多个库而造成的名字冲突问题
- 头文件与using声明或指示
- 头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中,
- 通常情况下,头文件应该只负责定义接口部分的名字,而不定义实现部分的名字
- 头文件最多只能在它的函数或命名空间内使用using指示或using声明
- 避免using指示
- using指示一次性注入某个命名空间的所有名字,这种用法看似简单实则充满了风险
- 只使用一条语句就突然将命名空间中所有成员的名字变得可见
- 另一种风险是由using指示引发的二义性错误只有在使用了冲突名字的地方才能被发现
- 相比于使用using指示,在程序中对命名空间的每个成员分别使用using声明效果更好,这么做可以减少注入到命名空间的名字数量
- 类,命名空间与作用域
- 对命名空间内部名字的查找遵循常规的查找规则
- 由内向外依次查找每个外层作用域
- 外层作用域也可能是一个或多个嵌套的命名空间,直到最外层的全局命名空间查找过程终止
- 可以从函数的限定名推断出查找名字时检查作用域的次序,限定名以相反次序指出被查找的作用域
多重继承与虚继承
- 多重继承
- 每个基类包含一个可选的访问说明符,如果访问说明符被忽略掉,则关键字class对应的默认访问说明符是private,关键字struct对应的是pubic
- 多重继承下的类作用域
- 当一个类拥有多个基类对象时,有可能出现派生类从两个或更多基类中继承了同名成员的情况,此时,不加前缀限定符直接使用该名字将引发二义性
- 虚继承
- 尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类,派生类可以通过它的两个直接基类分别继承用一个间接基类,也可以直接继承某个基类,然后通过另一个基类再一次间接继承该类
- 虚继承的目的是令某个类作出声明,承诺愿意共享它的基类。
- 其中,共享的基类子对象称为虚基类
- 在这种机制下,无论虚基类在继承体系中出现多少次,在派生类中都只包含唯一一个共享的虚基类子对象
- 虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身
class Bear : virtual public ZooAnimal {}; - 支持向基类的常规转换
- 无论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作
- 虚基类的可见性
- 虚继承的对象的构造方式
- 含有虚基类的对象的构造顺序与一般的顺序稍有区别
- 首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类部分
- 接下来按照直接基类在派生列表中出现的次序依次对其进行初始化
- 虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关
- 构造函数与析构函数的次序
- 一个类有多个虚基类,这些虚的子对象按照他们在派生列表中出现的顺序从左向右依次构造
- 编译器按照直接接类的声明顺序对其依次进行检查,以确定其是否含有虚基类。如果有,则先构造虚基类,然后按照声明顺序逐一构造其他非虚基类
第十九章 特殊工具与技术
控制内存分配
- 重载new和delete
- new表达式的工作原理
- 第一步,new表达式调用一个名为operator new(或者 operator new[])的标准库函数。
- 该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象的数组)
- 第二步,编译器运行相应的构造函数以构造这些对象。并为其传入初始值
- 第三步,对象被分配了空间并构造完成,返回一个指向该对象的指针
- delete表达式的工作原理
- 第一步,对对象指针所指的对象或者数组中的元素执行对象对应的析构函数
- 第二步,编译器调用名为operator delete(或者 operator delete[])的标准库函数释放内存空间
- 当自定义了全局的operator new函数和operator delete函数后,我们就负担起了控制动态内存分配的指责。
- 这两个函数必须正确,因为它们是程序整个处理过程至关重要的一部分
- 应用程序可以在全局作用域定义operator new函数和operator delete函数,也可以将它们定义为成员函数
- 当编译器发现一条new表达式或delete表达式后,将在程序中查找可供调用的operator函数。
- 如果被分配(释放)的对象是类类型,则编译器首先在类及其基类作用域中查找,此时如果该类有operator new成员或operator delete成员,则相应的表达式将调用这些成员,
- 否则,编译器会在全局作用域查找匹配函数
- 此时如果编译器找到了用户自定义的版本,则使用该版本执行new表达式或delete表达式
- 如果没有找到,则使用标准库定义的版本
- 可以使用作用域运算符令new表达式或delete表达式忽略定义在类中的函数,直接执行全局作用域中的版本
- 标准库定义了operator new函数和operator delete函数的8个重载版本
- 其中前四个版本可能抛出bad_alloc异常,后四个版本则不会抛出异常
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,不包含任何成员
- malloc函数与free函数
- 从C语言继承了这些函数,定义在cstdlib头文件中
- 定位new表达式
- 对于operator new分配的内存空间来说,我们无法使用construct函数构造对象。
- 我们可以使用new的定位new形式构造对象
- 定位new的形式
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中提供一个(可能为空)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象
- 当只传入一个指针类型的实参时,定位new表达式构造对象但不分配内存
- 显式的析构函数调用
- 就像定位new与使用allocate类似一样,对析构函数的显式调用也与使用destroy类似
- 可以直接调用一个析构函数
- 和destroy类似,调用析构函数可以清除给定对象,但不会释放该对象所在的空间。如果需要的话,我们可以重新使用该空间
- 调用析构函数会销毁对象,但不会释放内存
运行时类型识别
- RTTI的功能由两个运算符实现
- typeid运算符
- dynamic_cast运算符
- 用于将基类的指针或引用安全的转换成派生类的指针或引用
- 使用RTTI运算符蕴含更多潜在风险
- 程序员必须清楚的知道转换的目标类型,并且必须检查类型转换是否被成功执行
- 使用RTTI必须加倍小心,在可能的情况下,最好定义虚函数而非直接接管类型管理的重任
- dynamic_cast运算符
- 形式
dynamic_cast<type*>(e) dynamic_cast<type&>(e) dynamic_cast<type&&>(e) - e的类型必须符合以下三个条件中任意一个
- e的类型是目标type的公有派生类
- e的类型是目标type的公有基类
- e的类型是目标type的类型
- 指针类型的dynamic_cast
- 我们可以对一个空指针执行dynaic_cast,结果是所需类型的空指针
- 在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在用一条表达式中完成
- typeid运算符
- 它允许程序向表达式提问:你的对象是什么类型
- typeid运算符可以作用于任意类型表达式
- 如果对数组a执行typeid(a),则所得到的结果是数组类型而非指针类型
- 比较两个指针指向对象类型是否相同
if(typeid(*bp) == typeid(*dp)) {} - 当typeid作用于指针时(而非指针所指的对象),返回的结果是该指针的静态编译时类型
- type_info类
- 其精确定义随着编译器的不同而略有不同
- C++标准规定,type_info类必须定义在typeinfo头文件中
- 至少要提供的操作
- t1 == t2
- 如果type_info对象t1和t2表示同一个类型,返回true
- t1 != t2
- t.name()
- 返回一个C风格字符串,表示类型名字的可打印形式。类名的生成因系统而异
- t1.before(t2)
- 返回一个bool值,表示t1是否位于t2之前,before所采用的顺序关系是依赖于编译器的
typeid(42).name()
枚举类型
- C++包含两种枚举
- 限定作用域的枚举类型
- 关键字enum class 或enum struct, 随后是枚举类型名字以及用花括号括起来以逗号分隔的枚举成员列表,最后是一个分号。
- 不限定作用域的枚举类型
- 省略掉关键字class(或struct),枚举类型的名字是可选的
- 在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的
- 在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同
- 一个不限定作用域的枚举类型的对象或枚举成员自动的转换成整型,而限定作用域的枚举类型不会进行隐式转换
- 指定enum大小
- C++11中,可以在enum的名字后面加上冒号以及我们想要在该enum中使用的类型
enum intValues : unsigned long long {…}; - 如果我们没有指定enum的潜在类型,默认情况下限定作用域的enum成员类型是int
- 对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型
- 枚举类型的前置声明
enum intValues : unsigned long long; enum class open_modes;
类成员指针
- 数据成员指针
- 成员指针必须保护成员所属的类
- 必须在*之前添加classname::以表示当前定义的指针可以指向classname的成员
const string Screen::*pdata; - 在C++11新标准中声明成员指针最简单的方法是使用auto或decltype
auto pdata = &Screen::contents; - 使用数据成员指针
- 与成员访问运算符.和->类似,也有两种成员指针访问运算符
- .*
- ->*
- 这两个运算符使得我们可以解引用指针并获取该对象成员
Screen myScreen; Screen *pScreen = &myScreen; auto s = myScreen.*pdata; s = pScreen->*pdata; - 返回数据成员指针的函数
- 数据成员一般是私有的,如果希望能访问数据成员,最好定义一个函数,令其返回值是指向该成员的指针。
static const std::string Screen::* data() {return &Screen::contents;} const std::string Screen::*pdata = Screen::data(); - 成员函数指针
- 与成员指针类似
auto pms = &Screen::get_cursor; - 使用classname::*的形式声明一个指向成员函数的指针
- 和普通函数指针类似,如果成员存在重载函数,则我们必须显式的声明函数类型个指针
char (Screen::*pmf2)(Screen::pos, Screen::pos) const; pmf2 = &Screen::get; - 上述声明中
Screen::* 两端的括号必不可少。如果没有,编译器将认为该声明是一个无效函数声明 - 成员函数和指向该成员的指针之间不存在自动转换规则
- 使用成员函数指针
char c1 = (pScreen->*pmf)(); char c2 = (myScreen.*pmf2)(0, 0); - 因为函数调用运算符的优先级较高,所以在声明指向成员函数的指针并使用这样的指针进行函数调用时,括号必不可少
(C::*p)(parms) (obj.*p)(args) - 使用成员指针的类型别名
- 使用类型别名或typedef可以让成员指针更容易理解
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); - action包含两个形参的函数,其中一个形参是Screen对象的引用,另一个形参是指向Screen成员函数的指针,成员函数必须接受两个pos形参并返回一个char。
- 当我们调用action时,只需将Screen的一个符合要求的函数的指针或地址传入即可
- 成员指针函数表
- 对于普通函数指针和指向成员函数的指针来说,一种常见的用法是将其存入一个函数表当中。
-
如果一个类含有几个相同类型的成员,则这样一张表可以帮助我们从这些成员中选择一个 -
假定Screen类含有几个成员函数,每个函数负责将光标向指定的方向移动 class Screen
{
public:
Screen& home();
Screen& forward();
Screen& back();
Screen& up();
Screen& down();
};
-
这几个新函数有一个共同点
- 它们都不接受任何一个参数,并且返回值是发生光标移动的Screen引用
- 我们希望定义一个move函数,使其可以调用上面的任意一个函数并执行对应的操作。
- 为了支持这个新函数,我们将在Screen中添加一个静态成员。该成员是指向光标移动函数的指针的数组
class Screen
{
public:
using Acton = Screen& (Screen::*)();
enum Direction { HOME, FORWARD, BACK, UP, DOWN };
Screen& move(Direction);
private:
static Action Menu[];
};
-
move函数接受一个枚举成员并调用相应的函数 Screen& Screen::move(Directions cm)
{
return (this->*Menu[cm])();
}
- 当我们调用move函数时,给它传入一个表示光标移动方向的枚举成员
Screen myScreen; myScreen.move(Screen::HOME); myScreen.move(Screen::DOWN); -
定义并初始化函数表本身 Screen::Action Screen::Menu[] =
{
&Screen::home,
&Screen::forward,
&Screen::back,
&Screen::up,
&Screen::down,
};
- 将成员函数用作可调用对象
- 使用function生成一个可调用对象
function<bool (const string&)> fcn = &string::empty; find_if(svec.begin(), svec.end(), fcn); - 使用mem_fn生成一个可调用对象
- 使用fucntion,必须提供成员的调用形式
- 可以通过标准库功能mem_fn来让编译器负责推断成员的类型
- mem_fn也定义在functional头文件中
find_if(svec.begin(), svec.end(), mem_fn(&string::empty)); - 使用bind生成一个可调用对象
auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1));
- 当使用bind时,必须将函数中用于表示执行对象的隐式形参转换成显式的
auto f = bind(&string::empty, _1); f(*sevc.begin()); f(&svec[0]);
嵌套类
- 一个类可以定义在另一个类的内部,前者称为嵌套类,或嵌套类型
- 嵌套类是一个独立的类,与外层类基本没有关系
- 嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见
- 位于外层类public部分的嵌套类实际上定义了一种可随处访问的类型
- 位于外层类protected部分的嵌套类定义的类型只能被外层类及友元和派生类访问
- 位于外层类private部分的嵌套类定义的类型只能被外层类的成员和友元访问
- 在嵌套类在其外层类之外完成真正的定义之前,它都是一个不完全类型
union:一种节省空间的类
- 联合(union) 是一种特殊的类,一个union可以有多个数据成员,但在任意时刻只有一个数据成员可以有值
- 在C++11新标准中,含有构造函数或析构函数的类类型也可以作为union的成员类型
- union可以定义为其成员指定public、protected和private等保护标记
- 默认情况下,union的成员是公有的,与struct相同
- union既不能继承自其他类,也不能作为基类使用,所以在union中不能含有虚函数
- 匿名 union
- 一个未命名的union
- 匿名union不能包含受保护的成员或私有成员,也不能定义成员函数
- 如果union的成员类型定义了自己的构造函数/或拷贝控制成员,则该union的用法要比只含有内置类型成员的union复杂得多
- 带有string的union可以参照书中示例,
局部类
- 类可以定义在某个函数的内部,称为局部类
- 局部类的类型只在定义它的作用域内可见
- 和嵌套类不同,局部类的成员受到严格限制
- 局部类的所有成员(包括函数在内)都必须完整定义在类的内部。因此,局部类的作用与嵌套类相比相差很远
- 局部类不允许声明静态数据成员
- 局部类不能使用函数作用域中的变量
- 常规的访问保护规则对局部类同样适用
固有的不可移植的特性
- 为了支持低层编程,C++定义了一些固有的不可移植的特性
- 所谓不可移植的特性就是指因机器而异的特性
- 当我们含有不可移植特性的程序从一台机器转移到另一台机器上时,通常需要重新编写该程序。
- C++从C语言继承而来的另外两种不可移植的特性
- 链接指示
- 位域
- volatile限定符
- 应该用不到
- volatile 的确切含义与机器有关,只能通过阅读编译器文档来理解。
- 想要让使用了volatile的程序在移植到新机器或新编译器后仍有效,通常需要对程序进行某些改变
- 当对象的值可能在程序的控制或检测之外被改变时,应该将对象声明为volatile
- 关键字volatile告诉编译器不应该对这样的对象进行优化
- volatile限定符的用法和const很相似,它起到对类型额外修饰的作用
volatile int display_register; //该int值可能发生改变 - 合成的拷贝对volatile对象无效
- 如果一个类希望拷贝、移动或赋值它的volatile对象,则该类必须定义拷贝或移动操作。
- 可以将形参类型指定为const volatile引用
- 链接指示:
extern “C”
- c++程序有时需要调用其他语言编写的函数,最常见是调用C语言编写的函数。
- 其他语言中的函数名字也必须在C++中进行声明,并且该声明必须指定返回类型和形参列表。
- 对于其他语言编写的函数来说,编译器检查其调用的方式与处理普通C++函数的方式相同,但是生成的代码有所区别。
- C++使用链接指示指出任意非C++函数所用的语言
- 要想把C++代码和其他语言(包括C语言)编写的代码放在一起使用,要求我们必须有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的
- 声明一个非C++的函数
- 链接指示可以有两种形式
- 链接指示不能出现在类定义或函数定义内部
- 同样的链接指示必须在函数的每个声明中都出现
- 例子:声明显示cstring头文件的某些C函数
- 单语句链接指示
extern “C” size_t strlen(const char *); - 复合语句链接指示
extern “C” { int strcmp(const char*, const char*) char *strcat(char*, const char*); } - 编译器也可能支持其他语言的链接指示
extern “Ada” extern “FORTRAN” - 链接指示与头文件
- 我们可以令链接指示后面跟上花括号括起来的若干函数的声明,从而一次性建立多个链接
- 多重声明的形式可以应用于整个头文件
extern “C” {
} - 当一个#include指示被放置在复合链接指示花括号中时,头文件中的所有普通函数声明都被认为是由链接指示的语言编写的。
- 链接指示可以嵌套,因此如果头文件包含有自己的链接指示函数,则该函数的链接不受影响
- C++从C语言继承的标准库函数可以定义成C函数,但并非必须
- 决定使用C还是C++实现C标准库,是每个C++实现的事情
- 指向extern “C” 函数的指针
- 指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示
extern “C” void (*pf)(int);
- pf指向一个C函数,该函数接受一个int返回void
- 指向C函数的指针与指向C++函数的指针是不一样的类型。
- 一个指向C函数的指针不能用在执行初始化或赋值操作后指向C++函数,反之亦然
- 如果试图在两个链接指示不同的指针之间进行赋值操作,则程序发生错误
void (*pf1)(int); extern “C” void (*pf2)(int); pf1 = pf2; - 有的C++编译器会接受上述这种赋值操作并将其作为对语言的扩展,尽管从严格意义上来看它是非法的
- 链接指示对整个声明都有效
- 使用链接指示时,它不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效
- 导出C++函数到其他语言
- 通过使用链接指示对函数进行定义,我们可以令一个C++函数在其他语言编写的程序中可用
extern “C” double calc(double dparm) { /* …*/ } - 需注意
- 可被多种语言共享的函数的返回类型或形参类型受到很多限制
- 例如:我们不太可能把一个C++类的对象传给C程序
- 因为C程序根本无法理解构造函数、析构函数以及其他类特有的操作
- 对链接到C的预处理的支持
- 有时候需要在C和C++中编译同一个源文件,为了实现这一目的,在编译C++版本的程序时预处理定义
_ _cplusplus (两个下划线)。利用这个变量,我们可以在编译C++程序的时候有条件的包含进来一些代码
#ifdef __cplusplus extern “C” #endif int strcmp(const char*, const char*); - 重载函数与链接指示
- 链接指示与重载函数的相互作用依赖于目标语言。如果目标语言支持重载函数,则为该语言实现链接指示的编译器很可能也支持重载这些C++的函数
- C语言不支持函数重载,因此也就不难理解为什么一个C链接指示只能说明一组重载函数中的某一个了
|