第十九章 特殊工具与技术
19.1 控制内存分配
19.1.1 重载new和delete
- new表达式的工作机理
//new表达式
string *sp = new string("a value"); //分配并初始化一个string对象
string *arr = new string[10]; //分配10个默认初始化的string对象
new表达式调用一个名为operator new(或者operator new[])的标准库函数。分配一块足够大的、原始的、未命名的内存空间。
编译器运行相应的构造函数以构造这些对象,并为其传入初始值。
对象被分配了空间并构造完成,返回一个指向该对象的指针。
2. delete表达式的工作机理
delete sp; //销毁*sp,然后释放sp指向的内存空间
delete[] arr; //销毁数组中的元素,然后释放对应的内存空间
对指针所指的对象执行对应的析构函数。
编译器调用名为operator delete(或者operator delete[])的标准库函数释放内存空间。
3. 可以使用作用域运算符令new或delete忽略定义在类中的函数,直接执行全局作用域中的版本,如::new。
4. 提供新的operator new函数和operator delete函数的目的在于改变内存分配的方式,不管怎样,都不能改变new运算符和delete运算符的基本含义。
#include <iostream> // std::coutstruct MyClass {MyClass() { std::cout << "MyClass constructed\n"; }~MyClass() { std::cout << "MyClass destroyed\n"; }
//operator new函数和operator delete函数,可以定义为全局作用域,也可以定义为成员函数。void* operator new(size_t t) {puts("override version:normal...");void* m = malloc(t);if (m) return m;else throw std::bad_alloc(); }void* operator new(size_t t, std::nothrow_t obj) noexcept {puts("override version:nothrow");//return operator new(t);void* m = malloc(t);return m;}
//如果刚刚失败的new是nothrow版本,那么要调用的delete操作符将是nothrow版本,否则将是正常版本。这个过程是自动的。void operator delete(void* p) noexcept {puts("deleting normal...");free(p);}void operator delete(void* p, std::nothrow_t obj) noexcept {puts("deleting nothrow...");free(p);}
};int main() {MyClass* pt = new (std::nothrow) MyClass;delete pt; //MyClass::operator delete(pt,std::nothrow);std::cout << std::endl;MyClass* pt2 = new MyClass;delete pt2;
}
19.1.2 定位new表达式
- 当只传入一个指针类型的实参时,定位new表达式构造对象但是不分配内存。
- 调用析构函数会销毁对象,但是不会释放内存。
#include <iostream>
class Foo {
public:Foo(int val = 0){_val = val;}void writeFoo() {std::cout << "_val:" << _val << " address:" << this;std::cout << std::endl;
}
~Foo() = default;private:int _val;
};int main(int argc, char* argv[]) {//创建char数组,大小为3个Foochar* buf = new char[sizeof(Foo) * 3];//实例化Foo对象,并将其放置到buf中第1个Foo“位置”处Foo* pb = new (buf) Foo(0);//实例化Foo对象,并将其放置到buf中第3个Foo“位置”处Foo* pb1= new (buf + sizeof(Foo) * 2) Foo(1);//实例化Foo对象,并将其放置到buf中第2个Foo“位置”处Foo* pb2= new (buf + sizeof(Foo)) Foo(2);pb->~Foo();//delete pb;pb->writeFoo();pb1->writeFoo();pb2->writeFoo();
}
19.2 运行时类型识别
- 运行时类型识别(RTTI)的功能由两个运算符实现:
typeid运算符,用于返回表达式的类型。
dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。 - 使用RTTI必须加倍小心。在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。
struct Base {virtual ~Base() {};
};struct Derived : public Base { };int main() {Base* bp;bp = new Derived; // bp actually points to a Derived object//bp = new Base; // bp points to a Base objectif (Derived * dp = dynamic_cast<Derived*>(bp)) {puts(" use the Derived object to which dp points");}else { // bp points at a Base objectputs(" use the Base object to which bp points");}
}
19.2.1 dynamic_cast运算符
- 如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个bad_cast异常。
- 可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。
- 在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式中完成。
19.2.2 typeid运算符
- 通常情况下,使用typeid比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同。
- 当typeid作用于指针时(而非指针所指的对象),返回的结果是该指针的静态编译时类型。
- typeid是否需要运行时检查决定了表达式是否会被求值。只有当类型含有虚函数时,编译器才会对表达式求值。
- 如果表达式的动态类型可能与静态类型不同,则必须在运行时对表达式求值以确定返回的类型。
Derived* dp = new Derived;
Base* bp = dp; //两个指针都指向Derived对象
//在运行时比较两个对象的类型
if (typeid(*bp) == typeid(*dp))cout << "指向的是同一种类型" << endl;
//检查运行时类型是否是某种给定的类型
if (typeid(*bp) == typeid(Derived))cout << "bp实际指向Derived对象" << endl;//下面的检查永远是失败的:bp的类型是指向Base的指针
if (typeid(bp) == typeid(Derived)) {}
19.2.3 使用RTTI
- 当为具有继承关系的类实现相等运算符时,RTTI非常有用。
class Base {friend bool operator==(const Base&, const Base&);
public://Base的接口成员
protected://如果定义一个虚函数equal,则该函数的形参必须是基类的引用。此时,equal函数将只能使用基类的成员,而不能比较派生类独有的成员virtual bool equal(const Base&) const;//Base的数据成员和其他用于实现的成员
};
class Derived :public Base {
public://Derived的其他成员
protected://虚函数的基类版本和派生类版本必须具有相同的形参类型。bool equal(const Base&) const;//Derived的数据成员和其他用于实现的成员
};bool operator==(const Base& lhs, const Base& rhs) {//如果typeid不同,返回false;否则虚调用equalreturn typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}bool Derived::equal(const Base& rhs) const {//我们清楚这两个类型是相等的,所以转换过程不会抛出异常auto r = dynamic_cast<const Derived&>(rhs);//指向比较两个Derived对象的操作并返回结果
}
19.2.4 type_info类
- type_info类的精确定义随着编译器的不同而略有差异。
- type_info类一般作为基类出现,它没有默认构造函数,而且它的拷贝和移动构造函数以及赋值运算符都被定义为删除的。因此,无法定义或拷贝type_info类型的对象,也不能为type_info类型的对象赋值。创建type_info对象的唯一途径是使用typeid运算符。
int main() {int arr[10];Derived d;Base *p = &d;cout << typeid(42).name() << ", " << std::endl<< typeid(arr).name() << ", " << std::endl<< typeid(std::string).name() << ", " << std::endl<< typeid(p).name() << ", " << std::endl<< typeid(*p).name() << endl;return 0;
}
19.3 枚举类型
- 枚举类型:将一组整形常量组织在一起。
- C++包含两种枚举:限定作用域的(C++11)和不限定作用域的。
enum class open_modes { input, output, append };
enum color {red, yellow, green}; //不限定作用域的枚举类型
//未命名的、不限定作用域的枚举类型
enum { floatPrec = 6, doublePrec = 10, double_doublePrec = 10 };
- 限定作用域的枚举类型,在枚举类型的作用域外是不可以访问的。
enum color { red, yellow, green };
enum stoplight { red, yellow, green }; //错误:重复定义了枚举成员
enum class peppers { red, yellow, green }; //正确:枚举成员被隐藏了
color eyes = green; //正确:枚举类型的枚举成员位于有效的作用域中
peppers p = green; //错误:pepers的枚举成员不在有效的作用域中//color::green在有效的作用域中,但是类型错误
color hair = color::red; //正确
peppers p2 = peppers::red; //正确:显式的使用pappers的red
- 枚举值不一定唯一。
enum class intTypes {charTyp = 8, shortTyp = 16, intTyp = 16,longTyp = 32, long_longTyp = 64
};
- 默认情况下,枚举值从0开始,依次加1。
- 和类一样,枚举也定义新的类型。
color red1 = 1; //错误:
int i = color::red; //正确:不限定作用域的枚举类型的枚举成员隐式地转换成int
int j = peppers::red; //错误:限定作用域的枚举类型不会隐式转换
- 枚举成员是const,因此在初始化枚举成员时提供的初始值必须是常量表达式。
- 一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型。
- 尽管每个enum都定义了唯一的类型,但实际上enum是由某种整数类型表示的。
- 可以在enum的名字后加上冒号以及想在该enum中使用的类型。
- enum的前置声明必须指定其成员的大小。
- 默认情况下限定作用域的enum成员类型是int,对于不限定作用域的enum成员,没有默认类型,我们只知道潜在类型足够大。
enum intValues : unsigned long long {charType = 255, shortTyp=65535, intTyp = 65535,longTyp = 4294967295UL,long_longTyp = 1844674407370951615ULL
};
- 即使某个整型值恰好与枚举成员的值相等,它也不能作为函数的enum实参使用。
- 不限定作用域的枚举类型,潜在类型因机器而异。
enum Tokens { INLINE = 128, VIRTUAL = 129 };
void ff(Tokens);
void ff(int);
int main() {Tokens curTok = INLINE;ff(128); //精确匹配ff(int)ff(INLINE); //精确匹配ff(Tokens)ff(curTok); //精确匹配ff(Tokens)return 0;
}
- 可以将一个不限定作用域的枚举类型的对象或枚举成员转给整形形参。
void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;
//不管Tokens的潜在类型到底是什么,它的对象和枚举成员都提升成int
newf(VIRTUAL); //调用newf(int)
newf(uc); //调用newf(unsigned char)
19.4 类成员指针
- 成员指针是指可以指向类的非静态成员的指针,它指示的是类的成员,而非类的对象。
class Screen {
public:typedef std::string::size_type pos;char get_cursor() const { return contents[cursor]; }char get() const;char get(pos ht, pos wd) const;
private:std::string contents;pos cursor;pos height, width;
};
19.4.1 数据成员指针
- 当初始化一个成员指针(或向它赋值)时,需指定它所指的成员。
//pdata可以指向一个常量(非常量)Screen对象的string成员
const string Screen::*pdata;
//一个指向Screen类的const string成员的指针
pdata = &Screen::contents; //初始化
//初始化后,该指针并没有指向任何对象
//auto pdata = &Screen::contenets;
- 成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时才提供对象的信息。
Screen myScreen, *pScreen = &myScreen;
//首先解引用成员指针以得到所需的成员
//然后向成员访问运算符一样,通过对象(.*)或指针(->*)获得成员。
//.*解引用pdata以获得myScreen对象的contents成员
auto s = myScreen.*pdata;
//->*解引用pdata以获得pScreen指向对象的contents成员
s = pScreen->*pdata;
- 数据成员一般是私有的,通常需要定义一个函数,返回指向该成员的指针。
class Screen {
public://data是一个静态成员,返回一个成员指针static const std::string Screen::*data() { return &Screen::contents; }//其他成员与之前的版本一致
};
//data()返回一个指向Screen类的contents成员的指针
const string Screen::*pdata = Screen::data();
//获得myScreen对象的contents成员
auto s = myScreen.*pdata;
19.4.2 成员函数指针
- 和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则。
//pmf是一个指针,它可以指向Screen的某个常量成员函数
auto pmf = &Screen::get_cursor;char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;//&不能省
- 因为函数调用运算符的优先级较高,所以在声明指向成员函数的指针并使用这样的指针进行函数调用时,括号必不可少:(C::*p)(parms)和(obj.*p)(args)。
Screen myScreen, *pScreen = &myScreen;
//通过pScreen所指的对象调用pmf所指的函数
char c1 = (pScreen->*pmf)();
//通过myScreen对象将实参0,0传给含有两个形参的get函数
char c2 = (myScreen.*pmf2)(0,0);
//指向Screen成员函数的指针,接受两个pos实参,返回一个char
using Action = char(Screen::*)(Screen::pos, Screen::pos) const;
Action get = &Screen::get; //get指向Screen的get成员//接受一个Screen的引用,和一个指向Screen成员函数的指针
Screen& action(Screen&, Action = &Screen::get);Screen myScreen;
//等价调用
action(myScreen); //使用默认实参
action(myScreen, get); //使用我们之前定义的变量get
action(myScreen, &Screen::get); //显式传入地址
- 对于普通函数指针和指向成员函数的指针来说,一种常见的用法是将其存入一个函数表当中。如果一个类含有几个相同类型的成员,则这样一张表可以帮助我们从这些成员中选择一个。
class Screen {
public://其他接口和实现成员与之前一致using Action = Screen&(Screen::*)();//光标移动函数Screen& home() { cursor = 0; return *this; }Screen& forward() { ++cursor; return *this; }Screen& back() { --cursor; return *this; }Screen& up() { cursor += height; return *this; }Screen& down() {cursor -= height; return *this; }enum Directions { HOME, FORWARD, BACK, UP, DOWN };Screen& move(Directions);
private:static Action Menu[]; // 函数表
};Screen& Screen::move(Directions cm)
{// run the element indexed by cm on this objectreturn (this->*Menu[cm])(); // Menu[cm] points to a member function
}Screen::Action Screen::Menu[] = { &Screen::home,&Screen::forward,&Screen::back,&Screen::up,&Screen::down,};
Screen myScreen;
myScreen.move(Screen::HOME); // invokes myScreen.home
myScreen.move(Screen::DOWN); // invokes myScreen.down
19.4.3 将成员函数用作可调用对象
- 要想通过一个指向成员函数的指针进行函数调用,必须首先利用.*运算符或->*运算符将该指针绑定到特定的对象上。
vector<string> svec;
auto fp = &string::empty; //fp指向string的empty函数
//错误,必须使用.*或->*调用成员指针
find_if(svec.begin(), svec.end(), fp);//检查对当前元素的断言是否真
if(fp(*it)) //错误:要想通过成员指针调用函数,必须使用->*运算符
- 与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符。
- 从指向成员函数的指针获取可调用对象的一种方法是使用标准库模板function,必须提供成员的调用形式。另一种方法通过使用标准库功能mem_fn来让编译器负责推断成员的类型。
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);//指向成员函数的对象将被传给隐式的this形参
//假设it是find_if内部的迭代器,则*it是给定范围的一个对象
if(fcn(*it))
//本质上function将函数调用转换成了如下形式:
if((*it).*p)()) //假设p是fcn内部的一个指向成员函数的指针
- 和function不同的是,mem_fn可以根据成员指针的类型推断可调用对象的类型,而无须用户显式地指定。
//mem_fn(&string::empty)生成一个可调用对象
find_if(svec.begin(), svc.end(), mem_fn(&string::empty));
//mem_fn生成的可调用对象含有一对重载的函数调用运算符
auto f = mem_fn(&string::empty); //f接受一个string或者一个string*
f(*svec.begin()); //正确:传入一个string对象,f使用.*调用empty
f(&svec[0]); //正确:传入一个string的指针,f使用->*调用emtpy
- 还可以使用bind从成员函数生成一个可调用对象。
//选择范围中的每个string,并将其bind到empty的第一个隐式实参上
auto it = find_if(svec.begin(), svec.end(), bind(&string::empty,_1));
auto f = bind(&string::empty, _1);
f(*svec.begin()); //正确:实参是一个string,f使用.*调用empty
f(&svec[0]); //正确:实参是一个string的指针,f使用->*调用empty
19.5 嵌套类
- 嵌套类常用于定义作为实现部分的类。
class TextQuery {
public://嵌套的类就像是一个外层类的成员一样,受到访问限制符的影响class QueryResult;//...
};
- 嵌套类是一个独立的类,与外层类基本没什么关系。特别是,外层类的对象和嵌套类的对象是相互独立的。
class TextQuery::QueryResult {friend std::ostream& print(std::ostream&, const QueryResult&);
public://无须定义QueryResult::line_no//嵌套类使用外层类的成员时无需对该成员的名字进行限定QueryResult(std::string, std::shared_ptr<std::set<line_no>>,std::shared_ptr<st::vector<std::string>>);//...
};
- 嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见。
- 和其他类类似,嵌套类也使用访问限定符来控制外界对其成员的访问权限。
- 嵌套类在其外层类中定义了一个类型成员。和其他成员类似,该类型的访问权限由外层类决定。
- 在外层类之外定义一个嵌套类时,必须以外层类的名字限定嵌套类的名字。
- 在嵌套类在其外层类之外完成真正的定义之前,它都是一个不完全类型。
TextQuery::QueryResult::QueryResult(string s,shared_ptr<set<line_no>> p,shared_ptr<vector<string>> f):sought(s),clines(p), file(f) { }
//假设QueryResult有一个静态成员,则该成员的定义形式如下:
int TextQuery::QueryResult::static_mem = 1024;
//返回类型必须指明QueryResult是一个嵌套类
TextQuery::QueryResult
TextQuery::query(const string &sought) const {//如果没有找到sought,则返回set的指针static shared_ptr<set<line_no>> nodata(new set<line_no>);//使用find而非下标以避免向wm中添加单词auto loc = wm.find(sought);if(loc == wm.end())return QueryResult(sought, nodata, file);elsereturn QueryResult(sought, loc->second, file);
}
19.6 union:一种节省空间的类
- union不能含有引用类型的成员。
- 默认情况下,union的成员都是公有的。
- union可以有多个数据成员,但在任意时刻只有一个数据成员可以有值。
union Token {//默认情况下成员是公有的char cval;int ival;double dval;
};Token first_token = {'a'}; //初始化union的第一个成员
Token last_token; //未初始化的Token对象
Token *pt = new Token; //指向一个未初始化的Token对象的指针last_token.cavl = 'z';
pt->ival = 42;
- 由于union既不能继承自其他类,也不能作为基类使用,所以在union中不能含有虚函数。
- union可以方便地表示一组类型不同的互斥值。
- 为union的一个数据成员赋值会令其他数据成员变成未定义的状态。
- 未命名的union:编译器会自动地为其创建一个未命名的对象。
union {char cval;int ival;double dval;
}; //定义一个未命名的对象,我们可以直接访问它的成员//在匿名union的定义所在的作用域,union的成员都可以直接访问
cval = 'c'; //为未命名的对象赋值
ival = 42; //该对象状态改变
- 匿名union不能包含受保护的成员或私有成员,也不能定义成员函数。
- 将union的值改为类类型成员对应的值时,必须运行该类型的构造函数;反之,将类类型成员的值改为一个其他值时,必须运行该类型的析构函数。
- 当union包含的是内置类型的成员时,编译器将按照成员的次序依次合成默认构造函数或拷贝控制成员。但如果union含有类类型的成员,并且该类型自定义了默认构造函数或拷贝控制成员,则编译器将为union合成对应的版本并将其声明为删除的。
- 通常把含有类类型成员的union内嵌在另一个类中,这个类可以管理并控制与union的类类型成员有关的状态转换。
class Token {
public:// copy control needed because our class has a union with a string memberToken(): tok(INT), ival(0) { }Token(const Token &t): tok(t.tok) { copyUnion(t); }Token &operator=(const Token&);// if the union holds a string, we must destroy it; ~Token() { if (tok == STR) sval.~string(); }// assignment operators to set the differing members of the unionToken &operator=(const std::string&);Token &operator=(char);Token &operator=(int);Token &operator=(double);
private:enum {INT, CHAR, DBL, STR} tok; // 判别式:追踪union的状态union { // anonymous union char cval;int ival;double dval;std::string sval;
}; // each Token object has an unnamed member of this unnamed union type// 检查判别式,然后酌情拷贝union成员
void copyUnion(const Token&);
};
Token &Token::operator=(int i) {if (tok == STR) sval.~string(); // if we have a string, free itival = i; // assign to the appropriate membertok = INT; // update the discriminantreturn *this;
}
// char,double的版本基本和上面一致…
Token &Token::operator=(const std::string &s) {if (tok == STR) // if we already hold a string, just do an assignmentsval = s;elsenew(&sval) std::string(s); // otherwise construct a stringtok = STR; // update the discriminantreturn *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;// to copy a string, construct it using placement new; case Token::STR: new(&sval) std::string(t.sval); break;}
}Token &Token::operator=(const Token &t) {// if this object holds a string and t doesn't, we have to free the old stringif (tok == STR && t.tok != STR) sval.~string();if (tok == STR && t.tok == STR)sval = t.sval; // no need to construct a new stringelsecopyUnion(t); // will construct a string if t.tok is STRtok = t.tok;return *this;
}
19.7 局部类
- 局部类:定义在某个函数的内部。
- 局部类的所有成员(包括函数在内)都必须完整定义在类的内部。因此,局部类的作用与嵌套类相比相差很远。
- 局部类中不允许声明静态数据成员,因为没法定义这样的成员。
- 局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员。不能使用函数作用域中的变量。
- 常规的访问保护规则对局部类同样适用。
int a, val;
void foo(int val)
{static int si;enum Loc { a = 1024, b };//Bar是foo的局部类struct Bar {Loc locVal; //正确:使用一个局部类型名int barVar;void fooBar(Loc i = a) //正确:默认实参是Loc::a{barVal = val; //错误:val是foo的局部变量barVal = ::val; //正确:使用一个全局对象barVal = si; //正确:使用一个静态局部对象locVal = b; //正确:使用一个枚举成员}};//...
}
- 可以在局部类的内部再嵌套一个类,该类也是一个局部类。
void foo()
{class Bar {public://...class Nested; //声明Nested类};//定义Nested类class Bar::Nested {//...};
}
19.8 固有的不可移植的特性
- 因机器而异的特性,移植需要重新编译,例如:算术类型。
19.8.1 位域
- 当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
- 位域在内存中的布局是与机器相关的。
- 位域的类型必须是整型或枚举类型。
- 取地址运算符&不能作用于位域,因此任何指针都无法指向类的位域。
- 通常情况下最好将位域设为无符号类型,存储在带符号类型中的位域的行为将因具体实现而定。
- 类可以将其(非静态)数据成员定义为位域。
- 如果一个类定义了位域成员,则它通常也会定义一组内联的成员函数以检验或设置位域的值。
typedef unsigned int Bit;
class File {Bit mode: 2; // mode has 2 bitsBit modified: 1; // modified has 1 bitBit prot_owner: 3; // prot_owner has 3 bitsBit prot_group: 3; // prot_group has 3 bitsBit prot_world: 3; // prot_world has 3 bits
public:enum modes { READ = 01, WRITE = 02, EXECUTE = 03 };File &open(modes);void close();void write();bool isRead() const;void setWrite();void execute();bool isExecute() const;
};void File::write(){ modified = 1; // . . .}
void File::close(){if (modified) // . . . save contents; }inline bool File::isRead() const { return mode & READ; }
inline void File::setWrite() { mode |= WRITE; }File &File::open(File::modes m) {mode |= READ; // set the READ bit by default// other processingif (m & WRITE) // if opening READ and WRITE // processing to open the file in read/write modecout << "myFile.mode WRITE is set" << endl;return *this;
}int main() {File myFile;myFile.open(File::READ);if (myFile.isRead()) cout << "reading" << endl;return 0;
}
19.8.2 volatile限定符
- volatile限定符:告诉编译器不应对这样的对象进行优化。
- 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; //bitmapBufd的每个成员都是volatilevolatile int v; //v是一个volatile int
int *volatile vip; //vip是一个volatile指针,它指向int
volatile int * ivp; //vip是一个指针,指向volatile int
//vivp是一个volatile指针,它指向一个volatile int
volatile int *volatile vivp;int *ip = &v; //错误:必须使用指向volatile的指针
ivp = &v; //正确:ivp是一个指向volatile的指针
vivp = &v; //正确:vivp是一个指向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;//Foo类的剩余部分...
}
19.8.3 链接指示:extern “C”
- C++使用链接指示指出任意非C++函数所用的语言。
- 要想把C++代码和其他语言(包括C语言)编写的代码放在一起使用,要求我们必须有权访问该语言的编译器,并且这个编译器与当前的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函数//头文件中的所有普通函数声明都被认为是由链接指示的语言编写的//链接指示可以嵌套
}
//f1是一个C函数,它的形参是一个指向C函数的指针
extern "C" void f1(void(*)(int));//FC是一个指向C函数的指针
extern "C" typedef void FC(int);
//f2是一个C++函数,该函数的形参是执行C函数的指针
void f2(FC*);
- 如果希望给C++函数传入一个指向C函数的指针,则必须使用类型别名。
//pf指向一个C函数,该函数接受一个int返回void
extern "C" void (*pf)(int);void (*pf1)(int); //指向一个C++函数
extern "C" void (*pf2)(int); //指向一个C函数
pf1 = pf2; //错误:pf1和pf2的类型不同
- 通过使用链接指示对函数进行定义,可以令一个C++函数在其他语言编写的程序中可用。
//calc函数可以被C程序调用
extern "C" double calc(double dparm) {/*...*/}
//编译器将为该函数生成适合于指定语言的代码
- 重载函数与链接指示
//错误:两个extern"C"函数的名字相同
extern "C" void print(const char*);
extern "C" void print(int);class SamllInt { /*...*/ };
class BigNum { /*...*/ };
//C函数可以在C或C++程序中调用
//C++函数重载了该函数,可以在C++程序中调用
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNum&);