《C++ Primer》笔记 第19章 特殊工具与技术
控制内存分配
- 应用程序可以在全局作用域中定义operator new函数和operator delete函数,也可以将它们定义为成员函数。当编译器发现一条new表达式或delete表达式后,将在程序中查找可供调用的operator函数。
- 如果被分配(释放)的对象是类类型,则编译器首先在类及其基类的作用域中查找。此时如果该类含有operator new成员或operator delete成员,则相应的表达式将调用这些成员。否则,编译器在全局作用域查找匹配的函数。此时如果编译器找到了用户自定义的版本,则使用该版本执行new表达式或delete表达式;如果没找到,则使用标准库定义的版本。
- 我们可以使用作用域运算符令new表达式或delete表达式忽略定义在类中的函数,直接执行全局作用域中的版本。例如,::new只在全局作用域中查找匹配的operator new函数,::delete与之类似。
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;
/*
对于operator new函数或者operator new[]函数来说,它的返回类型必须是void*,第一个形参的类型必须是size_t且该形参不能含有默认实参。当我们为一个对象分配空间时使用operator new;为一个数组分配空间时使用operator new[]。当编译器调用operator new时,把存储指定类型对象所需的字节数传给size_t形参;当调用operator new[]时,传入函数的则是存储数组中所有元素所需的空间。
如果我们想要定义operator new函数,则可以为它提供额外的形参。此时,用到这些自定义函数的new表达式必须使用new的定位形式将实参传给新增的形参。尽管在一般情况下我们可以自定义具有任何形参的operator new,但是下面这个函数却无论如何不能被用户重载:void* operator new(size_t, void*);这种形式只供标准库使用,不能被用户重新定义。
对于operator delete函数或者operator delete[]函数来说,它们的返回类型必须是void,第一个形参的类型必须是void*。执行一条delete表达式将调用相应的operator函数,并用指向待释放内存的指针来初始化void*形参。
*/
-
应用程序可以自定义上面函数版本中的任意一个,前提是自定义的版本必须位于全局作用域或者类作用域中。当我们将上述运算符函数定义成类的成员时,它们是隐式静态的(最好还是声明static吧)。因为operator new用在对象构造之前而operator delete用在对象销毁之后,所以这两个成员(new和delete)必须是静态的,而且它们不能操作类的任何数据成员。
-
当我们将
operator delete
或operator delete[]
定义成类的成员时,该函数可以包含另外一个类型为size_t
的形参。此时,该形参的初始值是第一个形参所指对象的字节数。size_t
形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给operator delete
的字节数将因待删除指针所指对象的动态类型不同而有所区别。而且,实际运行的operator delete
函数版本也由对象的动态类型决定。 -
一条new表达式的执行过程总是先调用operator new函数以获取内存空间,然后在得到的内存空间中构造对象。与之相反,一条delete表达式的执行过程总是先销毁对象,然后调用operator delete函数释放对象所占的空间。
void *operator new(size_t size) { if (void *mem = malloc(size)) return mem; else throw bad_alloc(); } void operator delete(void *mem) noexcept { free(mem); }
-
定位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使用
operator new(size_t, void*)
”分配”它的内存。这是一个我们无法自定义的operator new
版本。 -
事实上,定位new允许我们在一个特定的、预先分配的内存地址上构造对象。当只传入一个指针类型的实参时,定位new表达式构造对象但是不分配内存。
-
我们传给
construct
的指针必须指向同一个allocator
对象分配的空间,但是传给定位new
的指针无需指向operator new
分配的内存。 -
调用析构函数会销毁对象,但是不会释放内存。
运行时类型识别
-
运行时类型识别的功能由两个运算符实现:
- typeid运算符,用于返回表达式的类型。
- dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。
-
使用RTTI必须要加倍小心。在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。
-
dynamic_cast 运算符的使用形式如下形式:
dynamic_cast<type*>(e) dynamic_cast<type&>(e) dynamic_cast<type&&>(e)
其中,type 必须是一个类类型,并且通常情况下该类型应该含有虚函数。
- 第一种形式中,e 必须是一个有效的指针;
- 第二种形式中,e 必须是一个左值;
- 第三种形式中,e 不能是左值。
在上面的所有形式中,e 的类型必须符合以下三个条件中的任意一个:
- e 的类型是目标 type 的公有派生类;
- e 的类型是目标 type 的公有基类;
- e 的类型就是目标 type 的类型。
如果符合,则类型转换可以成功,否则,转换失败。
-
如果一条 dynamic_cast 语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败了,则 dynamic_cast 运算符将抛出一个 bad_cast 异常。
if (Derived *dp = dynamic_cast<Derived*>(bp)) { // 使用dp指向的Derived对象 } else { // bp指向一个Base对象 // 使用bp指向的Base对象 }
-
我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。
-
在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式中完成。
-
因为不存在所谓的空引用,所以对于引用类型来说无法使用与指针类型完全相同的错误报告策略,当对引用的类型转换失败时,程序将抛出一个名为
std::bad_cast
的异常,该异常定义在typeinfo
标准库头文件中。void f(const Base &b) { try { const Derived &d = dynamic_cast<const Derived&>(b); // 使用 b 引用的 Derived 对象 } catch (bad_cast) { // 处理类型转换失败的情况 } }
-
typeid
表达式的形式是:typeid(e)
,其中 e 可以是任意表达式或类型的名字。typeid
操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info
或者type_info
的公有派生类型。type_info
类定义在typeinfo
头文件中。 -
typeid
运算符可以用于任意类型的表达式。和往常一样,顶层const
将被忽略。如果表达式是一个引用,则typeid
返回该引用所引对象的类型。不过当typeid
作用于数组或函数时,所得的结果是数组类型或函数类型而非指针类型。 -
当运算对象不属于类类型或者是一个不包含任何虚函数的类时,
typeid
运算符指示的是运算对象的静态类型。而当运算对象是定义了至少一个虚函数的类的左值时,typeid
的结果直到运行时才会求得。 -
通常情况下使用
typeid
比较两条表达式的类型是否相同或者比较一条表达式的类型是否与指定的类型相同:Derived *dp = new Derived; Base *bp = dp; // 两个指针都指向 Derived 对象 // 在运行时比较两个对象的类型 if (typeid(*bp) == typeid(*dp)) { // bp 和 dp 指向同一类型的对象 } // 检查运行时类型是否是指定的类型 if (typeid(*bp) == typeid(Derived)) { // bp 实际指向 Derived 对象 }
-
typeid
作用于指针时(而非指针所指的对象),返回的结果是该指针的静态编译时类型:// bp 是指向 Base 的指针,下面的判断将永远失败 if (typeid(bp) == typeid(Derived)) { // 此处代码永远不会执行 }
-
typeid
是否需要运行时检查决定了表达式是否会被求值。只有当类型含有虚函数时,编译器才会对表达式求值。反之,如果类型不含有虚函数,则typeid
返回表示的静态类型,编译器无须对表达式求值也能知道表达式的静态类型。 -
如果表达式的动态类型可能与静态类型不同,则必须在运行时对表达式求值以确定返回的类型。这条规则适用于
typeid(*p)
的情况。如果 p 所指的类型不含有虚函数,则 p 不必非得是一个有效的指针。否则,*p
将在运行时求值,此时p必须是一个有效的指针。如果 p 是一个空指针,则typeid(*p)
将抛出bad_typeid
异常。 -
使用RTTI的一个示例:
class Base { friend bool operator == (const Base&, const Base&); public: // Base 的接口成员 protected: virtual bool equal(const Base&) const; }; class Derived : public Base { public: // Derived 的接口成员 protected: bool equal(const Base&) const; }; bool operator == (const Base& lhs, const Base& rhs) { // 如果 typeid 不相同,返回false;否则虚调用 equal // 因为 equal 是虚函数,所以会动态调用 equal 的版本 return typeid(lhs) == typeid(rhs) && lhs.equal(rhs); } bool Derived::equal(const Base& rhs) const { // 因为已经清楚两个类型是相等的,所以转换过程不会抛出异常 auto r = dynamic_cast<const Derived&>(rhs); // 执行比较两个 Derived 对象的操作并返回结果 } bool Base::equal(const Base& rhs) const { // 执行比较 Base 对象的操作 }
-
type_info
类定义在typeinfo
头文件中,并且至少提供:type_info的操作 解释 t1 == t2 如果 type_info 对象t1 和 t2 表示同一种类型,返回 true,否则返回 false t1 != t2 如果 type_info 对象t1 和 t2 表示不同的类型,返回 true,否则返回 false t.name() 返回 C 风格字符串,表示类型名字的可打印形式。类型的名字生成方式因系统而异。 t1.before(t2) 返回一个 bool 值,表示t1 是否位于 t2 之前。before 所采用的顺序关系是依赖于编译器的。 -
因为
type_info
类通常作为基类出现,所以一般会定义虚析构函数。当编译器希望提供额外的类型信息时,通常在type_info
的派生类中完成。 -
type_info
类没有默认构造函数,而且它的拷贝和移动构造函数以及赋值运算符都被定义成删除的。因此,无法定义或者拷贝type_info
类型的对象,也不能为type_info
类型的对象赋值。创建type_info
对象的唯一途径是使用typeid
运算符。
枚举类型
-
限定作用域的枚举类型定义:
enum class Color {red, yellow, green}; // 使用 class 关键字 enum struct Color {red, yellow, green}; // 使用 struct 关键字
不限定作用域的枚举类型定义,省去 class 或者 struct 关键字,枚举类型的名字是可选的:
enum Color {red, yellow, green}; // 命名的不限定作用域的枚举类型 enum {red, yellow, green}; // 未命名的不限定作用域的枚举类型
-
在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的。与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同。、
enum color{ red, yellow, green }; // 不限定作用域的枚举类型 enum stoplight{ red, yellow, green }; // 错误,重复定义了枚举成员 enum class peppers{ red, yellow, green }; // 正确,枚举成员被隐藏了 color eyes = green; // 正确,不限定作用域的枚举类型的枚举成员位于有效的作用域中 peppers p = green; // 错误,peppers 的枚举成员不在有效的作用域中 // color::green 在有效作用域中,但是类型错误 color hair = color::green; // 正确,允许显示地访问枚举成员 peppers p2 = peppers::green; // 正确,使用peppers::green
-
默认情况下,枚举值从0开始,依次加1,不过我们也能为一个或几个枚举成员指定专门的值。
-
枚举值不一定唯一。
enum class intTypes { charTyp = 8, shortTyp = 16, intTyp = 16, longTyp =32, long_longTyp = 64 };
-
如果我们没有显式地提供初始值,则当前枚举成员的值等于之前枚举成员的值加1。
-
枚举成员是 const,因此初始化枚举成员时提供的初始值必须是常量表达式。也就是说,每个枚举成员本身就是一条常量表达式,我们可以在任何需要常量表达式的地方使用枚举成员:
- 可以将 enum 作为语句的表达式,而将枚举值作为 case 标签。
- 可以将 enum 作为一个非类型模板形参使用。
- 可以在类的定义中初始化枚举类型的静态数据成员。
-
一个不限定作用域的枚举类型的对象或枚举成员自动转换成整型。因此,我们可以在任何需要整型值的地方使用它们:
int i = color::red; // 正确:不限定作用于的枚举类型的枚举成员隐式地转换成int
int j = peppers::red; // 错误:限定作用域的枚举类型不会进行隐式转换
-
尽管每个 enum 都定义了唯一的类型,但是实际上 enum 是由某种整数类型表示的。在 C++11标准中,我们可以在 enum 的名字后面加上冒号以及我们想在该 enum 中使用的类型:
enum intValues : unsigned long long { charType = 255, shortType = 65535, intType = 65535, longType = 4294967295UL, long_longType = 18446744073709551615ULL };
-
如果没有指定 enum 的潜在类型,则默认情况下限定作用域的 enum 成员类型是 int。对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型,我们只知道成员的潜在类型足够大,肯定能够容纳枚举值。
-
如果我们指定了枚举成员的潜在类型(包括对限定作用域的 enum 的隐式指定),则一旦某个枚举成员的值超出了该类型所能容纳的范围,将引发程序错误。
-
在 C++ 11 新标准中,可以提前声明 enum。enum的前置声明(无论隐式地还是显式地)必须指定其成员的大小。
// 不限定作用域的枚举类型 intValues 的前置声明必须指明成员类型 enum intValues : unsigned long long; // 限定作用域的枚举类型可以使用默认成员类型 int enum class open_modes;
因为不限定作用域的 enum 未指定成员的默认大小,因此每个声明必须指定成员的大小。对于限定作用域的 enum 来说,可以不指定其成员的大小,这个值被隐式地定义成 int。
-
enum 的声明和定义必须匹配,这意味着该 enum 的所有声明和定义中成员的大小必须一致。而且,我们不能在同一个上下文中先声明一个不限定作用域的 enum 名字,然后再声明一个同名的限定作用域的 enum。
-
要想初始化一个 enum 对象,必须使用该 enum 类型的另一个对象或者它的一个枚举成员。因此,即使某个整型值恰好与枚举成员的值相等,它也不能作为函数的 enum 实参使用。
-
尽管我们不能直接将整型值传递给 enum 形参,但是可以将一个不限定作用域的枚举类型的对象或枚举成员传给整型形参。此时,enum 的值提升为 int 或者更大的整型,实际提升的结果由枚举类型的潜在类型决定。
类成员指针
-
成员指针是指可以指向非静态成员的指针。
-
成员指针的类型囊括了类的类型以及成员的类型。当初始化一个这样的指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象;直到使用成员指针时,才提供成员所属的对象。
-
成员指针必须包含成员所属的类。
// pdata可以指向一个常量(非常量)Screen对象的string成员 const string Screen::*pdata; /* 上述语句将 pdata 声明成“一个指向 Screen 类的 const string 成员的指针”。常量对象的数据成员本身也是常量,因此将我们的指针声明成指向 const string 成员的指针意味着 pdada 可以指向任何 Screen 对象的一个成员,而不管该 Screen 对象是否是常量。作为交换条件,我们只能使用 pdata 读取它所指的成员,而不能向它写入内容。 */ const string Screen::*pdata; pdata = &Screen::contents; // 另一种方式 auto pdata = &Screen::contents;
-
当我们初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据。成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时我们才能提供对象的信息。
Screen myScreen, *pScreen = &myScreen; // .* 解引用 pdata 以获得 myScreen 对象的 contents 成员 auto s = myScreen.*pdata; // ->* 解引用 pdata 以获得 pScreen 所指对象的 contents 成员 s = pScreen->*pdata;
从概念上来说,这些运算符执行两步操作:它们首先解引用成员指针以得到所需的成员;然后像成员访问运算符一样,通过对象(.*)或指针(->*)获取成员。
-
常规的访问控制规则对成员指针同样有效。例如 Screen 的 contents 成员是私有的,因此之前对于 pdata 的使用必须位于 Screen 类的成员或友元内部,否则程序将引发错误。最好是定义一个函数,令其返回值是指向该成员的指针:
class Screen { public: // data 是一个静态成员,返回一个成员指针 static const std::string Screen::*data() { return &Screen::contents; } };
-
成员函数指针
// pmf 是一个指针,它可以指向 Screen 的某个常量成员函数 // 前提是该函数不接受任何实参,并且返回一个 char auto pmf = &Screen::get_cursor;
-
如果成员函数是 const 成员或者引用成员,则必须将 const 限定符或引用包含进来。
-
和普通的函数指针类似,如果成员存在重载的问题,则必须显式地声明函数类型以明确指出想要使用哪个函数。
-
和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则。
-
因为函数调用运算符的优先级较高,所以在声明指向成员函数的指针并使用这样的指针进行函数调用时,括号必不可少:(C::*p)(parms)和(obj.*p)(args)。
-
通过使用类型别名,可以令含有成员指针的代码更易读写。
-
成员指针不是一个可调用对象,这样的指针不支持函数调用运算符。因为成员指针不是可调用对象,所以不能直接将一个指向成员函数的指针传递给算法。
-
使用function生成一个可调用对象:
vector<string*> pvec; function<bool(const string*)> fp = &Screen::empty; // fp 接受一个指向 string 的指针,然后使用 ->* 调用 empty find_if(pvec.begin(), pvec.end(), fp);
-
使用mem_fn生成一个可调用对象:
find_if(svec.begin(), svec.end(), mem_fn(&string::empty)); // mem_fn生成的可调用对象可以通过对象调用,也可以通过指针调用。 auto f = mem_fn(&string::empty); // f 接受一个 string 或者 string* f(*svec.begin()); // ok,传入一个 string 对象,f 使用 .* 调用 empty f(&svec[0]); // ok,传入一个 string 的指针,f 使用 ->* 调用 empty
- 通过使用标准库功能
mem_fn
来让编译器负责推断成员类型。 mem_fn
也定义在function
头文件中,并且可以从成员指针生成一个可调用对象。- 和
function
不同的是,mem_fn
可以根据成员指针的类型推断可调用对象的类型,而用户无须显式地指定。 mem_fn
生成的可调用对象可以通过对象调用,也可以通过指针调用。
- 通过使用标准库功能
-
使用bind生成一个可调用对象
// 选择范围中的每个 string,并将其 bind 到 empty 的第一个隐式实参上 auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1)); // 可调用对象的第一个实参既可以是 string 的指针,也可以是 string 的引用 auto f = bind(&string::empty, _1); f(*svec.begin()); // ok,传入一个 string 对象,f 使用 .* 调用 empty f(&svec[0]); // ok,传入一个 string 的指针,f 使用 ->* 调用 empty
- 和 function 类似的地方是,使用 bind 时,必须将函数中用于表示执行对象的隐式形参转换成显示的。
- 和 mem_fn 类似的地方是,bind 生成的可调用对象的第一个实参既可以是 string 的指针,也可以是 string 的引用。
-
嵌套类
- 一个类可以定义在另一个类的内部,前者称为嵌套类或者嵌套类型。嵌套类常用于定义作为实现部分的类。
- 嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见。
- 嵌套类也使用访问限定符来控制外界对其成员的访问权限。外层类对嵌套类的成员没有特殊的访问权限,同样,嵌套类对外层类的成员也没有特殊的访问权限。
- 嵌套类在其外层类中定义了一个类型成员,该类型的访问权限由外层类决定。
- 在嵌套类在其外层类之外完成真正的定义之前,它都是一个不完全类型。
- 名字查找的一般规则在嵌套类中同样适用。当然,因为嵌套类本身是一个嵌套作用域,所以还必须查找嵌套类的外层作用域。
- 嵌套类是其外层类的一个类型成员,因此外层类的成员可以像使用任何其他类型成员一样使用嵌套类的名字。
- 尽管嵌套类定义在其外层类的作用域中,但是读者必须谨记外层类的对象和嵌套类的对象没有任何关系。
class TextQuery {
public:
class QueryResult; // 声明一个嵌套类
};
class TextQuery::QueryResult {
// 参数 QueryResult 位于类的作用域内,因此不需要加限定符
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
// 嵌套类可以直接使用外层类的成员,无须对该成员的名字进行限定
QueryResult(std::string,
std::shared_ptr<std::set<line_no>>,
std::shared_ptr<std::vector<std::string>>);
};
union
-
一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当我们给 union 的某个成员赋值之后,该 union 的其他成员就变成未定义的状态了。
-
union不能含有引用类型的成员,除此之外,它的成员可以是绝大多数类型。
-
union 可以为其成员指定 public、protected、private 等保护标记。默认情况下,union的成员都是公有的,这一点与struct相同。
-
union可以定义包括构造函数和析构函数在内的成员函数。但是由于union既不能继承自其他类,也不能作为基类使用,所以在union中不能含有虚函数。
-
我们可以像显式地初始化聚合类一样使用一对花括号内的初始值显式地初始化一个union。
-
如果提供了初始值,则该初始值被用于初始化第一个成员。
-
匿名union是一个未命名的union,并且在右花括号和分号之间没有任何声明。一旦我们定义了一个匿名 union,编译器就自动地为该 union 创建一个未命名的对象:
union { // 匿名 union char cval; int ival; double dval; }; // 可以直接访问匿名 union 对象的成员 cval = 'c'; ival = 42;
-
在匿名union的定义所在的作用域内该union的成员都是可以直接访问的。
-
匿名 union 不能包含受保护的成员或私有成员,也不能定义成员函数。
-
当 union 中包含的是内置类型的成员时,可以使用普通的赋值语句改变 union 保存的值。但是,如果 union 中含有特殊类类型的成员:
- 如果想将 union 的值改为类类型成员对应的值,必须运行该类型的构造函数。
- 如果将 union 的成员的值改成其他的值,必须运行该类型的析构函数。
-
当 union 中包含的是内置类型的成员时,编译器将按照成员的次序依次合成默认构造函数或拷贝控制成员。但是如果 union 中含有类类型的成员,并且该类型自定义了默认构造函数或拷贝控制成员,则编译器将为union合成对应的版本并将其声明为删除的。
-
union的判别式:使用一个对象判断union的当前值类型。
-
和普通的类类型成员不一样,作为 union 组成部分的类成员无法自动销毁。因为析构函数不清楚当前 union 存储的是什么类型,所以无法确定销毁哪个成员。
-
管理判别式并销毁string
Token& Token::operator=(int i) { if (tok == STR) // 如果当前存储的是 string,则首先释放它 sval.~string(); ival = i; // 为成员赋值 tok = INT; // 更新判别式 return *this; } Token& Token::operator=(const std::string& s) { if (tok == STR) // 如果当前存储的是 string 类型,则可以直接赋值 sval = s; else new(&sval) string(s); // 否则需要先构造一个 string // 在sval中构造一个string tok = STR; // 更新判别式 return *this; }
-
管理需要拷贝控制的联合成员
void Token::copyUnion(const Token& t) { switch (t.tok) { case Token::INT: ival = t.ival; break; case Token::CHAR: cval = t.cval; break; case Token::DBL: dval = t.dval; break; // 拷贝 string 可以使用定位 new 表达式来构造 case Token::STR:new(&sval) string(t.sval); break; } } Token& Token::operator=(const Token& t) { // 如果此对象的值是 string,t 的值不是,则必须释放原来的 string // 先排除tok是string的情况 if (tok == STR && t.tok != STR) sval.~string(); if (tok == STR && t.tok == STR) sval = t.sval; else copyUnion(t); // 如果 t.tok 是STR,这需要构造一个string tok = t.tok; return *this; }
局部类
- 类可以定义在某个函数的内部,称这样的类为局部类。
- 局部类的所有成员(包括函数在内)都必须完整定义在类的内部。因此,局部类的作用与嵌套类相比相差很远。
- 在局部类中不允许声明静态数据成员,因为我们没法定义这样的成员。
- 局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员。如果局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类。
- 外层函数对于局部类的私有成员没有任何访问特权。当然,局部类可以将外层函数声明为友元;或者更常见的情况是局部类将其成员声明成公有的。在程序中有权访问局部类的代码非常有限。局部类已经封装在函数作用域中,通过信息隐藏进一步封装显得没什么必要了。
- 局部类内部的名字查找次序与其他类相似。在声明类的成员时,必须先确保用到的名字位于作用域中,然后再使用该名字。定义成员时用到的名字可以出现在类的任意位置。如果名字不是局部类的成员,则继续在外层函数作用域中查找;如果还没有找到,在外层函数所在作用域中查找。
- 可以在局部类的内部再嵌套一个类。此时,嵌套类的定义可以出现在局部类之外。不过,嵌套类必须定义在与局部类相同的作用域中。
- 局部类内的嵌套类也是一个局部类,必须遵守局部类的各种规定。嵌套类的所有成员都必须定义在嵌套类内部。
int a, val;
void foo(int val)
{
static int s_i;
enum Loc{a=1024,b};
struct Bar {
Loc locVal;
int barVal;
void fooBar(Loc l = a)
{
barVal = val; // error,val 是 foo 的局部变量
barVal = ::val; // ok,使用一个全局对象
barVal = s_i; // ok,使用一个静态局部变量
locVal = b; // ok,使用一个枚举成员
}
};
// ....
}
位域
-
类可以将其(非静态)数据成员定义成位域,在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
-
位域在内存中的布局是与机器相关的。
-
位域的类型必须是整型或枚举类型。
typedef unsigned int Bit; class File { Bit mode : 2; // mode 占2位 Bit modified : 1; // modified 占1位 Bit prot_owner : 3; // prot_owner 占3位 Bit prot_group : 3; // prot_group 占3位 Bit prot_world : 3; // prot_world 占3位 public: // 文件类型以八进制的形式表示 enum modes {READ=01,WRITE=02,EXECUTE=03}; FILE &open(modes); void close(); void write(); bool isRead() const; void setWrite(); };
-
取地址符(&)不能作用于位域,因此任何指针都无法指向类的位域。
-
通常情况下最好将位域设为无符号类型,存储在带符号类型中的位域的行为将因具体实现而定。
-
通常使用内置的位运算符操作超过1位的位域。
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 bool File::setWrite() { mode |= WRITE; }
volatile限定符
-
当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 volatile。关键字 volatile 告诉编译器不应对这样的对象进行优化。
volatile int display_register; // 该 int 值可能发生改变 volatile Task *curr_task; // curr_task 指向一个 volatile 对象 volatile int iax[max_size]; // iax 中的每个元素都是 volatile volatile Screen bitmapBuf; // bitmapBuf 的每个成员都是 volatile
-
const 和 volatile 限定符互相没有影响,某种类型可能既是 const 的也是 volatile 的,此时它同时具有二者的属性。
-
就像一个类可以定义 const 成员函数一样,它可以将成员函数定义成 volatile 的。只有 volatile 的成员函数才能被 volatile 的对象调用。
-
我们可以声明volatile指针、指向volatile对象的指针以及指向volatile对象的volatile指针。
volatile int v; // v 是一个 volatile int int* volatile vip; // vip 是一个 volatile 指针,它指向 int volatile int* ivp; // ivp 是一个指针,它指向一个 volatile int volatile int *volatile vivp; // vivp 是一个 volatile 指针,它指向一个 volatile int int* ip = &v; // error,必须使用指向 volatile 的指针 ivp = &v; // ok,ivp 是一个指向 volatile 的指针 vivp = &v; // ok,vivp 是一个指向 volatile 的 volatile 指针
-
和const一样,我们只能将一个 volatile 对象的地址(或拷贝一个指向volatile类型的指针)赋给一个指向 volatile 的指针。同时,只有当某个引用是 volatile 的时,我们才能使用一个 volatile 对象初始化该引用。
-
const 和 volatile 的一个重要区别是我们不能使用合成的拷贝/移动构造函数及赋值运算符初始化 volatile 对象或从 volatile 对象赋值。合成的成员接受的形参类型是(非 volatile )常量引用,显然我们不能把一个非 volatile 引用绑定到一个 volatile 对象上。
-
如果一个类希望拷贝、移动或赋值它的 volatile 对象,则该类必须自定义拷贝或移动操作。
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"
-
链接指示可以有两种形式:单个的或复合的。链接指示不能出现在类或函数定义的内部。同样的链接指示必须在函数的每个声明中都出现。
// 单语句链接指示 extern "C" size_t strlen(const char*); // 复合语句链接指示 extern "C" { int strcmp(const char*, const char*); char *strcat(char*, const char*); }
-
我们可以令链接指示后面跟上花括号括起来的若干函数的声明,从而一次性建立多个链接。花括号的作用是将适用于该链接指示的多个声明聚合在一起,否则花括号就会被忽略,花括号中声明的函数名字就是可见的,就好像在花括号之外声明的一样。
-
多重声明的形式可以应用于整个头文件,例如,C++ 的 cstring 头文件可能形如:
extern "C" { #include <string.h> // 操作 C 风格字符串的 C 函数 }
-
当一个 #include 指示被放置在复合链接指示的花括号中时,头文件中的所有普通函数声明都被认为是由链接指示的语言编写的。链接指示可以嵌套,因此如果头文件包含带自带链接指示的函数,则该函数的链接不受影响。
-
对于使用链接指示定义的函数来说,它的每个声明都必须使用相同的链接指示。而且,指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示:
// pf 指向一个 C 函数,该函数接受一个 int 返回 void extern "C" void(*pf)(int);
-
指向 C 函数的指针与指向 C++ 函数的指针是不一样的类型。一个指向 C 函数的指针不能用在执行初始化或赋值操作后指向 C++ 函数,反之亦然。
-
当我们使用链接指示时,它不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效:
// f1 是一个 C 函数,它的形参是一个指向 C 函数的指针 extent "C" void f1(void(*) (int));
f1 是一个不返回任何值的 C 函数,它有一个类型是函数指针的形参。这里的链接指示不仅对 f1 有效,对函数指针同样有效。当我们调用 f1 时,必须传给它一个 C 函数的名字或者指向 C 函数的指针。
-
如果我们希望给 C++ 函数传入一个指向 C 函数的指针,则必须使用类型别名:
// FC 是一个指向 C 函数的指针 extern "C" typedef void FC(int); // f2 是一个 C++ 函数,该函数的形参指向一个 C 函数的指针 void f2(FC*);
-
通过连接指示对函数的定义可以令一个 C++ 函数在其他语言编写的程序中使用:
// calc 函数可以被C程序调用 extern "C" double calc(double dparm) {/*...*/}
编译器将为该函数生成适合于指定语言的代码。需要注意的是可被多种语言共享的函数的返回类型或形参类型受到诸多限制,例如不能把一个C++ 对象传递给C程序。
-
有时需要在 C 和 C++ 中编译同一个源文件:
#ifdef __cplusplus extern "C" #endif int strcmp(const char*, const char*);
-
链接指示与重载函数的相互作用依赖于目标语言。C 语言不支持重载函数,因此 C 链接指示只能用于说明一组重载函数中的某一个:
// error, 两个 extern "C" 函数名字相同 extern "C" void print(const char*); extern "C" void print(int);
如果在一组重载函数中有一个 C 函数,则其余的必定是 C++ 函数:
// C 函数可以在 C 或者 C++ 程序中调用 extern "C" double calc(double); // C++ 重载了这个函数,可以在 C++ 中程序中调用 extern SmallInt calc(const SmallInt&); extern BigNum calc(const BigNum&);