S19特殊工具与技术

来源:互联网 发布:windows开机启动后黑屏 编辑:程序博客网 时间:2024/06/08 04:11

S19特殊工具与技术


一、控制内存分配

1、重载newdelete
(1)new/delete的工作机制

调用new时执行了:

  • new表达式调用了名为operator new/operator new[]的标准库函数,分配足够大、原始、未命名的内存空间
  • 编译器运行相应的构造函数,来初始化对象并传入初始值
  • 分配空间并构造完成,返回一个指向该对象的指针

调用delete时执行了:

  • 对指针所指的对象执行相应的析构函数
  • 编译器调用名为operator delete/operator delete[]的标准库函数,释放内存空间

当遇到new/delete时,编译器:

  • (若对象是类类型)首先在类及其基类的作用域中查找是否有重载new/delete
  • 若未找到,进一步在全局作用域查找匹配的new/delete
  • 若未找到,调用标准库的new/delete

(2)operator new接口和operator delete接口
重载标准库中任意一个new/delete函数时,必须位于全局作用域或类作用域,若出现在类作用域则重载的new/delete是隐式static的,自定义new函数时可以为其提供额外的参数(使用定位new的方式)

注意:void *operator new(size_t, void *); 不允许被重新定义

注意:区分new表达式与operator new函数,重载的是operator new函数的运行方式,而new表达式永远都是调用operator new函数这一行为,是不能改变的

(3)malloc函数与free函数

2、定位new表达式
(1)对于operator new分配的内存空间采用定位new的形式构造对象

new (place_address) typenew (place_address) type (initializers)new (place_address) type [size]new (place_address) type [size] { braced initializer list }

place_address必须是一个指针,同时在initializers中提供以,分隔的初始值列表用于构造新分配的对象;当没有initializers仅有一个指针时定位new调用operator new(size_t, void *),此时只简单返回指针实参,然后由new表达式负责初始化

注意:可以与allocatorallocate/deallocate成员类比,但传给定位new的指针无须是operator new分配返回的

(2)显式的析构函数调用
调用析构函数会销毁对象,但不会释放内存,这块内存可以重新使用

string *sp = new string("a value");sp->~spring();        //sp所指的内存还可以使用,但是"a value"已被销毁

二、运行时类型识别

运行时类型识别(run-time type identification, RTTI)的功能由两个运算符实现

  • typeid,返回表达式的类型
  • dynamic_cast,用于将基类的指针或引用安全地转换成派生类的指针或引用

注意:当想要使用基类对象的指针或引用执行某个派生类操作且该操作不是虚函数时可以使用RTTI运算符,但是会带来直接接管类型管理的风险

1、dynamic_cast运算符
(1)dynamic_cast的使用,通常type类型应含有虚函数

dynamic_cast<type*>(e)        //e必须是一个有效的指针dynamic_cast<type&>(e)        //e必须是一个左值dynamic_cast<type&&>(e)       //e不能是左值

在以上三种使用中,e的类型也必须符合以下三个条件中的任意一个

  • e的类型是目标type的公有派生类
  • e的类型是目标type的公有基类
  • e的类型就是目标type的类型

不符合则转换失败,若转换目标是指针类型且失败则结果为nullptr,若转换目标是引用类型且失败则抛bad_cast异常

(2)指针类型的dynamic_cast

if(Derived *dp = dynamic_cast<Derived*>(bp)) //在条件部分定义dp并转换,若失败则dp = 0完成条件判断{...} else {...}

注意:在条件部分执行dynamic_cast可以确保类型转换和结果检查在同一条表达式中完成,确保程序安全

(3)引用类型的dynamic_cast

try{    const Derived &d = dynamic_cast<const Derived&>(b); //由于引用的转换与指针不同,因此用try方式}catch(bad_cast){...}

2、typeid运算符
(1)typeid运算符可以作用于任意类型的表达式,且会忽略顶层const,返回一个常量对象的引用,这个常量对象是type_info类或其公有派生类类型

注意:若运算对象不属于类类型或是一个不包含任何虚函数的类时,typeid获得对象的静态类型,若是定义了至少一个虚函数的类的左值时,typeid直到运行时才会返回结果

(2)使用typeid运算符
通常使用typeid比较两条表达式的类型是否相同,或者比较一条表达式类型是否与指定类型相同

Derived *dp = new Derived;Base *bp = dp;if(typeid(*bp) == typeid(*dp)) {...}    //注意,由于比较的是对象类型,因此要*bp而不能是bp,后者是指针if(typeid(*bp) == typeid(Derived)) {...}if(typeid(bp) == typeid(Derived)) {...} //if判断永远不成立

注意:当typeid作用于指针时(而非指针所指的对象),返回的结果是该指针的静态类型

3、使用RTTI

//使用RTTI为继承关系的类实现相等运算符:class Base {    friend bool operator==(const Base&, const Base&);public:    //Base接口成员protected:    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){    //如果lhs/rhs的typeid不同,则返回false,否则虚调用equal比较    return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);}bool Derived::equal(const Base &rhs) const{    //虚调用equal后首先把传入的基类引用rhs转换为对应的派生类的引用    //因为在operator==中lhs.equal(rhs)到这里说明进入的是Derived::equal,则rhs一定是Derived的引用    auto r = dynamic_cast<const Derived&>(rhs);     //执行转换后才能利用派生类引用r进一步比较两个派生类对象并返回结果,否则基类rhs不能访问派生类成员}bool Base::equal(const Base &rhs) const{    //执行比较Base对象}//typeid的练习19.10:class A {public:virtual ~A() {}};class B : public A {};class C : public B {};int main(){    A *pa = new C;    cout << typeid(pa).name() << endl;       //class A * __ptr64//虽然pa指向C对象,但是pa是指针,返回静态编译类型    C cobj;    A &ra1 = cobj;    cout << typeid(&ra1).name() << endl;     //class A * __ptr64//虽然ra1是C对象的引用,但是&ra1取地址是指针,返回静态编译类型    B *px = new B;    A &ra2 = *px;    cout << typeid(ra2).name() << endl;      //class B//ra2是B对象的引用,且B有虚函数(虚析构),故运行时计算typeid,返回ra2实际应用的对象的类型}

4、type_info
type_info类定义在typeinfo头文件中,并有如下操作

t1 == t2       //如果type_info对象t1/t2表示同一种类型,则返回true,否则返回falset1 != t2       t.name()       //返回C风格字符串,表示类型名字的可打印形式t1.before(t2)  //返回一个bool值,表示t1是否位于t2之前

注意:type_info类没有默认构造函数且拷贝/移动/赋值都定义为删除的,因此通过typeid是获得type_info的唯一方式

三、枚举类型

1、枚举类型属于字面值常量类型
C++包含两种枚举类型:限定作用域(enum class/struct ..)和不限定作用域(enum ..)的

2、枚举成员
(1)限定作用域的枚举类型中枚举成员的名字也遵循常规作用域规则,并且在枚举类型外不可访问;不限定作用域的则与定义本身的有效域一样
(2)默认第一个成员值是0,每个没有初始值的成员默认是前一个成员值加1,枚举成员是const,初始化时要用常量表达式来初始化,因此枚举成员本身也可以用在需要常量表达式的位置

enum color {red, yellow, green};                 //不限定作用域的枚举类型enum stoplight {red, yellow, green};             //错误,重复定义枚举成员enum class peppers {red, yellow, green};         //正确,隐藏了全局color中的名字color eyes = green;                              //正确,color是全局的,这一语句在有效的作用域中peppers p = green;                               //错误,离开了peppers作用域,不可访问其成员color hair = color::red;                         //正确,显式访问peppers p2 = peppers::red;                       //正确,显式访问

3、和类一样,枚举也定义新的类型
要初始化enum对象或为enum对象赋值,必须使用该类型的一个枚举成员或该类型的另一个对象

color clothes = 2;                               //错误,必须使用color的成员来初始化int j = peppers::red;                            //错误,限定作用域的不会自动向int转换

注意:不限定作用域的枚举类型可以隐式自动转换成int,限定作用域的不会进行隐式转换

4、指定enum大小
在enum的名字后加上冒号来显式要求我们在这个类里使用的类型,限定作用域的枚举默认是int

enum intValues : unsigned long long { longTyp = 4294967295UL };

5、枚举类型的前置声明
声明需要显式/隐式指出成员的数据类型

6、形参匹配和枚举类型
初始化一个enum对象,必须用其类型的另一个对象或是一个成员

enum Tokens { INLINE = 128, VIRTUAL = 129 };void ff(Tokens);void ff(int);int main(){    Token curTok = INLINE;    ff(128);            //精确匹配ff(int)    ff(INLINE);         //精确匹配ff(Tokens)    ff(curTok);         //精确匹配ff(Tokens)}

四、类成员指针

注意:成员指针是指可以指向类的非静态成员的指针,静态成员不属于任何对象因此其指针和普通指针没有区别

1、数据成员指针
(1)声明定义数据成员指针

const string Screen::*pdata;        //pdata可以指向Screen类对象的const string成员pdata = &Screen::contents;          //pdata指向某个非特定Screen对象的contents成员

(2)使用数据成员指针
成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时我们才提供对象的信息(.*/->*

Screen myScreen, *pScreen = &myScreen;auto s = myScreen.*pdata;   //pdata指向Screen的string,此时提供具体对象myScreen并通过.来获取*pdatas = pScreen->*pdata;        //pScreen指向具体对象myScreen,->来获取*pdata,进而得到pdata所指的string内容

(3)返回数据成员指针的函数
由于常规的访问控制对成员指针依然有效,因此一般定义一个static的函数返回成员指针(由于成员指针在解引用之前并不绑定任何具体对象,因此也是一个独立于对象之外的静态成员函数来返回成员指针)

class Screen {public:    static const string Screen::*data() { return &Screen::contents; }    //其他成员}const string Screen::*pdata = Screen::data();  //调用Screen::data()返回指向Screen的contents的常量指针auto s = myScreen.*pData;                      //pData是指向成员而非实际数据,使用时绑定具体Screen类对象

2、成员函数指针
(1)声明定义成员函数指针
若成员存在重载,则必须显式声明函数类型避免二义性

auto pmf = &Screen::get_cursor;char (Screen::*pmf2)(Screen::pos, Screen::pos) const;  //显式声明具体函数版本,且(Scr..pmf2)要有括号pmf2 = &Screen::get;                                   //成员函数指针与所指函数不能自动转换,必须取地址&

注意:成员函数指针也要在参数列表后指名是否是const的(这也是隐含的参数)

(2)使用成员函数指针

char c1 = (pScreen->*pmf)();char c1 = (myScreen.*pmf2)(0, 0);

注意:由于函数调用运算符优先级高,因此必须有括号

(3)使用成员指针的类型别名

注意:通过类型别名,使得含有成员指针的代码更容易读写

(4)成员指针函数表

class Screen{public:    using Action = Screen& (Screen::*)();  //Action是可以指向任意一个移动函数的成员函数指针类型    Screen &home();    Screen &forward();    Screen &back();    Screen &up();    Screen &down();    enum Directions { HOME, FORWARD, BACK, UP, DOWN };    Screen &move(Directions);              //外界实际调用的接口private:    static Action Menu[];                  //保存成员函数指针的函数表};Screen::Action Screen::Menu[] = { &Screen::home,                                  &Screen::forward,                                  &Screen::back,                                  &Screen::up,                                  &Screen::down,                                };Screen &Screen::move(Directions cm){    return (this->*Menu[cm])();            //Menu[cm]是某个成员函数指针,用this->*来调用它}Screen myScreen;myScreen.move(Screen::HOME);               //调用myScreen.homemyScreen.move(Screen::DOWN);               //调用myScreen.down

3、将成员函数用作可调用对象
(1)使用function生成一个可调用对象
由于成员函数指针需要用.*/->*才能将指针绑定到特定对象上,因此是不可调用对象,可以通过function来生成一个可调用对象,此时必须显式告诉function执行成员函数的方法

auto fp = &string::empty;find_if(svec.begin(), svec.end(), fp);                //错误,fp不可调用,未绑定具体对象//(const string &)显式说明调用这个成员函数的是string对象,fcn接受一个const string &,然后使用.*调用emptyfunction<bool (const string &)> fcn = &string::empty; find_if(svec.begin(), svec.end(), fcn);               //正确,fcn可调用//fp接受指向const string的指针,然后使用->*调用empty//即如果可调用对象是一个成员函数,则第一个形参必须表示该成员在哪个具体对象上执行function<bool (const string *)> fp = &string::empty;  

(2)使用mem_fn生成一个可调用对象
mem_fn定义在头文件functional中,可以根据成员指针的类型推断可调用对象的类型而无须显式指定,由mem_fn生成的可调用对象可以通过对象调用,也可以通过指针调用

find_if(svec.begin(), svec.end(), mem_fn(&string::empty));auto f = mem_fn(&string::empty);   //f接受一个string或一个string*f(*svec.begin());                  //正确,传入一个string,f使用.*调用emptyf(&svec[0]);                       //正确,传入一个指向string的指针,f通过->*调用empty

(3)使用bind生成一个可调用对象
不同于function需要区分指针/引用,bind是不用显式区分传入的是指针/引用的

auto f = bind(&string::empty, _1); //通过bind将传入的对象绑定到empty的第一个参数f(*svec.begin());                  //正确,传入一个string,f使用.*调用emptyf(&svec[0]);                       //正确,传入一个指向string的指针,f通过->*调用empty

五、嵌套类

1、嵌套类的基本特点
定义在一个类内部的类称为嵌套类,嵌套类是个独立的类,与外层类是相互独立的,嵌套类对象中不包含外层类的成员并对外层类成员没有特殊访问权限,同时外层类的对象不包含嵌套类的成员并对嵌套类的成员也没有特殊访问权限

注意:定义在public/protected/private不同位置的嵌套类,嵌套类本身访问权限是由外层类和访问限定符决定的

2、声明一个嵌套类

class TextQuery {public:    class QueryResult;   //嵌套类稍后定义    //other members};

3、在外层类之外定义一个嵌套类
嵌套类必须在外层类内部声明,但可以在内部或外部定义,在外部定义时需要加上外层类名和作用域运算符

class TextQuery::QueryResult {...};

注意:在嵌套类在其外层类之外完成真正的定义之前,它(嵌套类)都是一个不完全类型

4、定义嵌套类的成员
由于嵌套类定义在外层类内,嵌套类的成员定义时要有class_name::nested_class_name::member_name这样的前缀

TextQuery::QueryResult::QueryResult(string s..) : .. { .. }  //嵌套类的构造函数在最外层定义

5、嵌套类的静态成员定义

int TextQuery::QueryResult::static_mem = 1024;

6、嵌套类作用域中的名字查找
名字查找的一般规则在嵌套类中同样适用,由于嵌套类本身也是嵌套作用域,因此也会查找嵌套类的外层作用域

7、嵌套类和外层类是相互独立的

六、union:一种节省空间的类

1、基本特性
union不能含有引用类型的成员,默认情况下union的成员都是公有的,可以定义包括构造/析构函数在内的成员函数,但是union不能继承,因此不能有虚函数

2、定义union

    union Token {char cval; int ival; double dval; };

3、使用union类型
默认情况下union是未初始化的,可以用花括号内的初始值显式初始化一个union,给union对象的数据成员赋值会使其他数据成员变成未定义的状态

Token first = {'a'};Token *pt = new Token;first.cval = 'z';pt->ival = 42;

4、匿名union
匿名union是一个未命名的union,其内部成员在该union定义所在的作用域内可以直接访问,并且匿名union不能有protected/private成员且不能定义成员函数

5、含有类类型成员的union
union有类类型时,当将union的值改为类类型成员对应的值时必须运行该类型的构造函数,当改为其他值时必须运行该类型的析构函数

6、使用类管理union成员
由于含有类类型的union管理非常复杂,因此一般将其内嵌在另一个类中,并定义成员函数来管理union

7、管理判别式并销毁string

注意:需要根据原先类型来决定如何处理

8、管理需要拷贝控制的联合成员

//union练习:class Token{    friend ostream &operator<<(std::ostream &os, const Token &t);public:    Token() :tok(INT), ival(0) {}                       //默认初始化使用union的int,初始化为0并设置tok为INT    Token(const Token &t) :tok(t.tok) { copyUnion(t); } //拷贝构造,调用copyUnion成员函数完成union的拷贝    Token(Token &&t) :tok(std::move(t.tok)) { moveUnion(std::move(t)); }  //移动构造    Token &operator=(const Token &);    Token &operator=(Token &&);    ~Token() { if (tok == STR) sval.~string(); }        //若union此时是string,则销毁必须调用~string()    Token &operator=(const string &);                   //根据union内每个成员都定义一个设置成员的运算符    Token &operator=(char);    Token &operator=(int);    Token &operator=(double);    void copyUnion(const Token &);    void moveUnion(Token &&);    void free();private:    enum {INT, CHR, DBL, STR} tok;                      //不限定作用域的enum类内直接使用,代表union状态    union                                               //匿名union,在类内可以直接使用union成员    {        char cval;        int ival;        double dval;        string sval;    };};Token &Token::operator=(int i) {        //设定union表示int的赋值运算    if (tok == STR) sval.~string();     //若原来表示string,则必须先调用~string销毁    ival = i;    tok = INT;                          //enum标记此时union表示int    return *this;}Token &Token::operator=(char i) {...}Token &Token::operator=(double i) {...}Token &Token::operator=(const string &i) {    if (tok == STR)                     //检验当前类型,若已经是string,则直接=,会调用string的赋值运算符        sval = i;    else {        new(&sval) string(i);           //若原先非string,此时需要在union中构建一个string对象                                        //因此利用placement new,向sval地址new一个string并由i初始化        tok = STR;    }    return *this;}void Token::copyUnion(const Token &t) {    switch (t.tok) {    case INT:ival = t.ival; break;    case CHR:cval = t.cval; break;    case DBL:dval = t.dval; break;    case STR:new(&sval) string(t.sval); break;//拷贝union中有类类型时,需要用placement new    }}void Token::moveUnion(Token &&t) {    switch (t.tok) {    case INT:ival = std::move(t.ival); break;    case CHR:cval = std::move(t.cval); break;    case DBL:dval = std::move(t.dval); break;    case STR:new(&sval) string(std::move(t.sval)); break;//移动union中有类类型时,需要用placement new    }}Token &Token::operator=(const Token &t) {    if (tok == STR && t.tok != STR)      //需要分别按原先/现在是否是类类型来做特别处理        sval.~string();    if (tok == STR && t.tok == STR)          sval = t.sval;    else          copyUnion(t);    tok = t.tok;    return *this;}Token &Token::operator=(Token &&t) {    if (this != &t) {                    //移动赋值需要判断是否是自身赋值         free();                          //若自身union是string则需要调用free先析构string        moveUnion(std::move(t));        tok = std::move(t.tok);    }    return *this;}void Token::free() {    if (tok == STR)        sval.~string();}

七、局部类

1、定义在某个函数内部的类称为局部类,局部类的使用受到严格限制,其所有成员都必须完整定义在类的内部,且不允许声明静态数据成员

2、局部类不能使用函数作用域中的变量,只能访问外层作用域定义的类型名、静态变量和枚举成员

3、常规的访问保护规则对局部类同样适用,但一般没有必要在局部类内再有private成员

4、局部类中的名字查找与其他类相同,一层一层向外查找

5、嵌套的局部类也是局部类,必须遵循局部类的各项规定,唯一差别在于嵌套类可以定义在外层局部类之外(依然要在函数体内)

八、固有的不可移植的特性

1、位域
(1)位域的定义
类可以将其非静态的数据成员定义为位域,在一个位域中有一定数量的二进制位,位域的类型必须是整型或枚举类型,用:const expression来说明这个成员占几位

unsigned int mode : 2;        //mode占据2位,这个位域是unsigned int类型的unsigned int modified : 1;

注意:不能对位域取地址,且最好设位域为无符号类型,因为有符号类型的行为具体实现可能不同

(2)使用位域
一般一位位域可以直接赋值0/1来操作,而超过1位的位域用内置的位运算符来操作

modified = 1;mode |= READ;mode = 4;      //mode只有2位,而4是0100,故mode = 4之后mode实际变00,避免直接对位域对象赋值

2、volatile限定符
(1)volatile的确切含义与机器有关,volatile告诉编译器不要对这样的对象进行优化,volatile的值可能在程序控制/检测之外被改变,而有时候优化会将某些对象放进寄存器,此时若程序用了寄存器的对象,而程序之外改变了内存中真实的对象,则会出现程序行为的非原子性
(2)和const相同,只能将一个volatile对象的地址赋给指向volatile对象的指针,当某个引用是volatile时只能绑定一个volatile对象
(3)合成的拷贝对volatile对象无效
合成的拷贝/移动/赋值操作接受非volatile的常量引用,显然不能绑定到一个volatile对象上,因此需要自定义拷贝/移动/赋值操作来处理volatile对象

class Foo{public:    Foo(const volatile Foo&);    Foo &operator=(const volatile Foo&);    Foo &operator=(const volatile Foo&) volatile;}

注意:使用volatile需要参考具体的环境

3、链接指示:extern "C"
参考《C++ primer》p.759

原创粉丝点击