【C++ Primer】特殊的工具和技术

本文介绍了C++中高级主题,包括内存管理、运行时类型识别、枚举类型、类成员指针及联合体等,帮助读者掌握更复杂的编程概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、控制内存分配

1、重载new和delete

  new的操作过程:第一步、new表达式调用operator new(或者 operator new[])的标准库函数,该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或数组)。第二步、编译器运行相应的构造函数以构造这些对象,并为其传入初始值。第三步、对象分配了空间并构造完成,返回一个指向该对象的指针
  delete的操作过程:第一步:对指针所指对象的数组指针所指数组执行相应的析构函数。第二步:编译器调用operator delete(或者operator delete[])的标准库函数释放内存空间
  重载new和delete:若分配(释放)的对象是类类型,则编译器首先在类及其基类的作用域中查找,若该类中含有operator new成员或者operator delete成员,则相应表达式将调用这些成员,否则,编译器将在全局作用域查找匹配函数,若找到用户自定义版本,则执行,找不到,会使用标准库版本。

  C++从C语言中继承了malloc和free函数,头文件为cstdlib,malloc接受一个表示待分配字节数的size_t,返回指向该内存空间的指针或者返回0表示分配失败。free()函数接受一个void *,它是malloc返回指针的副本,free将相关内存返回给系统,free(0)无意义。所以operator new可以用malloc来实现。

#include <iostream>  
#include <cstdlib>  
using namespace std;  

void *operator new(size_t n)
{  
    cout << "new(size_t)" << endl;  
    if (void *mem = malloc(n))  
        return mem;  
    else  
        throw std::bad_alloc();  
}  

void operator delete(void *mem) noexcept
{  
    cout << "delete(void*)" << endl;  
    free(mem);  
} 

int main()  
{  
    int *a = new int(486);
    cout << a << " " << *a << endl;  
    delete a;
    system("pause");
    return 0;
}  

2、定位new表达式

  operator new和operator delete和alloctor类的allocate和deallocate很像,都是负责分配和释放内存的函数,但是对于operator new分配的内存空间我们无法使用construct函数构造对象,我们应该使用new的定位new形式构造对象。

#include <iostream>
#include <new>
const intchunk = 16;
class Foo
{
public :
	int val( ) { return _val; }
	Foo( ) { _val = 0; }
private :
	int_val;
};

//预分配内存,但没有Foo对象
char*buf = new char[ sizeof(Foo) * chunk ];
int main( void )
{
	//在buf中创建一个Foo对象
	Foo*pb = new (buf) Foo;
	//检查一个对象是否被放在buf中
	if ( pb->val() == 0 )
	{
		cout <<"new expressio worked!" <<endl;
	}
	//到这里不能再使用pb
	delete[] buf;
	return 0;
}

  placement new的作用就是:创建对象但是不分配内存,而是在已有的内存块上面创建对象。用于需要反复 创建并删除的对象上,可以降低分配释放内存的性能消耗。定位new表达式(placement new expression),允许程序员将对象创建在已经被分配好的内存中,new表的式的形式如下:

new (place_address) type
new (palce_address) type (initializer-list)

  place_address必须是个指针,指向已经分配好的内存。为了使用这种形式的new表达式,必须包含头文件new。

【Note】:
1)当只传入一个指针类型的实参时,定位new表达式构造对象但是不分配内存,这个指针没有要求,甚至可能是一个不是一个指向动态内存的指针
2)调用析构函数会销毁对象,但是不会释放内存。
这里写图片描述

二、运行时类型识别(RTTI)

  RTTI的功能由两个运算符实现:
  typeid:用于返回表达式的类型。
  dynamic_cast:用于将基类的指针或引用转换成派生类的指针或引用,当我们将这两个运算符用于某种类型的引用或指针时,并且该类型含有虚函数,运算符将会使用指针或引用所绑定对象的动态类型。
【Note】:
1)使用基类的指针或引用调用派生类的操作并且该操作不是虚函数,一般来说,我们应该尽量将其定义为虚函数(最好是定义为虚函数),编译器会根据对象的动态类型自动选择正确版本的函数。

1、dynamic_cast运算符

  三种形式:

dynamic_cast<type *> (e)dynamic_cast<type &> (e)dynamic_cast<type &&> (e)

  type的类型必须含有虚函数,e分别为指针、左值、除了左值以外的类型,e需要满足的条件:目标type的共有派生类、目标type的共有基类或者就是目标type类型,转换失败时,指针类型返回0,引用类型转换失败抛出bad_cast异常,头文件为typeinfo。
  可以对空指针进行dynamic_cast,结果是所需的空指针,在条件部分执行dynamic_cast可以确保类型转换和检查结果在同一条表达式中完成!
【Note】:
1)只能用于引用和指针的转换。
2)要转换的类型中必须包含虚函数。
3)转换成功返回子类地址,失败返回null。

2、typeid运算符

  typeid(e),其中e可以是任何类型或表达式的名字,若为表达式,返回的是引用所引对象的类型,若作用域数组名,返回的是数组类型,若作用于指针,返回的是该指针的静态编译的类型。
  type_info的name()成员返回一个C风格的字符串,表示对象的类型名字,由编译器决定。

3、使用RTTI

#include <iostream>  
#include <cstdlib>  
#include <typeinfo>//type_info对象定义在此头文件中。
using namespace std;  

class Flyable
{
public:
	Flyable() = default;
	~Flyable() = default;
	//定义纯虚函数。
	virtual void takeoff() = 0;
	virtual void land() = 0;
};

class Bird : public Flyable
{
public:
	Bird() = default;
	~Bird() = default;
	void foraging() { cout << "Bird-foraging" << endl; }//觅食。
	virtual void takeoff() { cout << "Bird-takeoff" << endl; } 
	virtual void land() { cout << "Bird-land" << endl; } 
};

class Plane : public Flyable
{
public:
	Plane() = default;
	~Plane() = default;
	void carry() { cout << "plane-carry" << endl; }  //运输。
	virtual void takeoff() { cout << "plane-takeoff" << endl; } 
	virtual void land() { cout << "plane-land" << endl; } 
};

void doSomething(Flyable *obj)
{
	//name的返回值因系统而异,类型不同返回的字符串有所区别即可。
	cout << typeid(*obj).name() << endl;
	obj->takeoff();
	if(typeid(*obj) == typeid(Bird))
	{
		Bird *bird = dynamic_cast<Bird *>(obj);
		bird->foraging();
	}
	if(typeid(*obj) == typeid(Plane))
	{
		Plane *plane = dynamic_cast<Plane *>(obj);
		plane->carry();
	}
	obj->land();
}

int main(int argc, char const *argv[])
{
	Bird b;
	doSomething(&b);
	Plane p;
	doSomething(&p);
	system("pause");
	return 0;
}

这里写图片描述

三、枚举类型

  枚举类型使得我们可以将一组整型常量组织在一起,但是每一个枚举类型都定义了一种新的类型。
  在限定作用域的枚举类型中,遵循常规的作用域准则,在枚举类型作用域外不可访问,不限定作用域的枚举类型,枚举成员的作用域和枚举类型本身的作用域相同(一般来说两个不限定作用域的枚举类型的成员不可以相同,因为其作用域是相同的,会重复定义)。

  默认情况下,枚举成员的枚举值从0开始,依次加1,但我们也可以为枚举成员指定初值。

  枚举成员是const的,所以赋值时必须使用常量表达式。

  要想初始化一个enum对象或者为enum赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象,不限定作用域的枚举类型的对象或其枚举成员会自动转换为整形。

  C++11新标准,可以在enum之后加上冒号再加上我们想在enum中使用的类型:enum:unsigned int {},默认情况下为int。可以先声明不定义。

四、类成员指针

  成员指针是指可以指向类的非静态成员的指针,由于类的静态成员不属于任何对象,所以无需特殊的指向该成员的指针,成员指针的类型需要包括类的类型和成员的类型,例如:const string Screen:: *p,一个指向Screen类的const string成员的指针p

1、数据成员指针

  在给成员指针赋值时,我们将取址运算符作用域screen的成员而不是内存中的一个类的对象的成员。
  我们需要注意,当我们初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据,成员指针只指定了成员而非该成员所属的对象,只有解引用成员指针时我们才提供对象的信息。

   成员指针的访问运算符:.或者->,可以获得对象的指定成员。
  由于在类中数据成员一般都是私有类型的,所以通常我们不能直接获得数据成员的指针,最好的方法就是定义一个函数,返回一个指向该成员的指针(return &XX)。

class Screen
{
	static const string Screen::*data()//定义一个函数。
	{
		return &Screen::contents;
	}
	pos cursor;//公有成员。
}

//数据成员指针:
Screen myScreen("abc", 1), *pScreen = &myScreen;
auto pdata1 = &Screen::cursor;//初始化一个数据成员指针。
auto s1 = pScreen->*pdata1;//使用数据成员指针(->*)。
cout << s1 << endl;

auto pdata2 = Screen::data();
auto s2 = myScreen.*pdata2;
cout << s2 << endl;

2、成员函数指针

【Note】:
1)使用指向成员函数的指针时,在解引用完成之后需要加一对小括号,在其之前的括号也是必须的,另外还可以传入实参:char c = (MyScreen.*P) (0,0),指向成员函数的一个指针
2)由于有时指向成员函数的指针较为复杂,我们可以使用类型别名来简化处理:using Action = char (Screen::*p) (Screen::pos,Screen::pos) const; Action的类型就是指向成员函数的指针。

  成员函数表,保存自身函数的指针,为private,组成一个数组,有时会比较有用:

//初始化函数表。
Screen::Action Screen::Menu[] = 
{
	&Screen::home,
	&Screen::left,
	&Screen::right,
	&Screen::up,
	&Screen::down
};

  因为成员指针不是可调用对象,所以我们不能直接将一个指向成员函数的指针传递给算法,若想从指向成员函数的指针获取可调用对象的一种方法是使用标准库模版function,头文件为functional,标准库功能mem_fn让编译器负责推断成员的类型。

3、本节demo:

#include <iostream>
#include <functional>
#include <vector>
using namespace std;

class Screen
{
 public:
	using pos = string::size_type;
	Screen(string s, pos p) : contents(s), cursor(p) { }
	~Screen() = default;
	char get_cursor() const { return contents[cursor]; }

	static const string Screen::*data()//定义一个函数。
	{
		return &Screen::contents;
	}
	pos cursor;//公有成员。

	//这些函数均不接受任何参数,并且都返回类的引用。
	Screen& home() { cout << "home" << endl; }
	Screen& left() { cout << "left" << endl; }
	Screen& right() { cout << "right" << endl; }
	Screen& up() { cout << "up" << endl; }
	Screen& down() { cout << "down" << endl; }

	using Action = Screen& (Screen::*)();//声明成员函数指针。
	//定义枚举类型,默认从零开始。
	enum Directions
	{
		HOME, LEFT, RIGHT, UP, DOWN
	};

	Screen& move(Directions cm)
	{
		//运行this对象中索引值为cm的元素,Menu[cm]指向一个成员函数。
		return (this->*Menu[cm])();
	}

 private:
	string contents;//私有成员无法访问,因此声明为公有或者定义一个函数。

	static Action Menu[];//定义函数表。
};

//初始化函数表。
Screen::Action Screen::Menu[] = 
{
	&Screen::home,
	&Screen::left,
	&Screen::right,
	&Screen::up,
	&Screen::down
};

int main(int argc, char const *argv[])
{
	//数据成员指针:
	Screen myScreen("abc", 1), *pScreen = &myScreen;
	auto pdata1 = &Screen::cursor;//初始化一个数据成员指针。
	auto s1 = pScreen->*pdata1;//使用数据成员指针(->*)。
	cout << s1 << endl;
	
	auto pdata2 = Screen::data();
	auto s2 = myScreen.*pdata2;
	cout << s2 << endl;


	//成员函数指针:
	auto pdata3 = &Screen::get_cursor;//初始化成员函数指针。
	auto s3 = (pScreen->*pdata3)();//使用成员函数指针。
	cout << s3 << endl;

	myScreen.move(Screen::HOME);
	myScreen.move(Screen::LEFT);


	//将成员函数用作可调用对象:
	vector<string> svec = {"ABC","EDF"};
	auto f = mem_fn(&string::empty);//使用mem_fn让编译器负责推断成员的类型。
	cout << f(*svec.begin()) << endl;

	return 0;
}

这里写图片描述

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

  union是一种特殊的类,可以包含多个数据成员,但是在任意时刻只能有一个数据成员可以有值,其他成员属于未定义的状态,分配给union的内存只要能存储它的最大数据成员即可。

  union中不能含有引用类型的成员,但不可继承、不含有虚函数。

  union的名字就相当于一个类型名,可以使用一对花括号显式初始化。
  匿名的union,没有名字,其中的成员可以直接访问,匿名union不能包含受保护成员和私有成员,也不能包含成员函数。

  我们通常将含有类类型成员的union内嵌在另一个类之中,将其定义为匿名union,将自身类类型成员的控制权转移给该类。

#include <iostream>    
#include <string>    
#include <utility>   

class Token 
{    
    enum { INT, CHAR, DBL, STR} tok;    

    union 
    {    
        char cval;    
        int ival;    
        double dval;    
        std::string sval;        
    };    

    void copyUnion(const Token &t)
    {    
        switch (t.tok) 
        {    
          case INT: ival = t.ival;    
            break;    
          case CHAR:cval = t.cval;    
            break;    
          case DBL:dval = t.dval;    
            break;    
          case STR:new(&sval) std::string(t.sval);    
            break;    
        }    
    }    

    void moveUnion(Token &&t) 
    {
        switch (t.tok) 
        {    
          case INT:    
            ival = std::move(t.ival);    
            break;    
          case CHAR:    
            cval = std::move(t.cval);    
            break;    
          case DBL:    
            dval = std::move(t.dval);    
            break;    
          case STR:    
            new(&sval) std::string(std::move(t.sval));    
            break;    
        }    
    }    

    void free() 
    {    
        if (tok == STR)    
            sval.std::string::~string();    
    }    
public:
    Token() :tok(INT), ival{ 0 } {};    
    Token(const Token &t) :tok(t.tok) { copyUnion(t); }    
    Token(Token &&t) :tok(std::move(t.tok)) 
    {
        moveUnion(std::move(t));    
    }    
    Token &operator=(Token &&t)
    {   
        if(this != &t)
        {    
            free();    
            moveUnion(std::move(t));    
            tok = std::move(t.tok);    
        }    
        return *this;    
    }    
    
    Token &operator=(const Token &t) //赋值运算符处理三种情况:左右都是string、左右都不是、只有一个是。
    {    
        if (tok == STR && t.tok != STR) sval.std::string::~string();    
        if (tok == STR && t.tok == STR)    
            sval = t.sval;     
        else    
            copyUnion(t);    
        tok = t.tok;    
        return *this;    
    }    

    ~Token() 
    {    
        if (tok == STR) //当前是string类型,要调用string的析构函数。
            sval.std::string::~string();
    }    

    Token &operator=(const std::string &s) 
    {    
        free();    
        new(&sval) std::string(s);//使用定位new表达式构造一个string。
        tok = STR;    
        return *this;    
    }    

    Token &operator=(char c) 
    {    
        free();    
        cval = c;    
        tok = CHAR;    
        return *this;    
    }    

    Token &operator=(int i) 
    {    
        free();    
        ival = i;    
        tok = INT;    
        return *this;    
    }    
    
    Token &operator=(double d) 
    {    
        free();    
        dval = d;    
        tok = DBL;    
        return *this;    
    }    

    friend std::ostream &operator<<(std::ostream &os, const Token &t) 
    {    
        switch (t.tok) 
        {    
            case Token::INT: os << t.ival; break;    
            case Token::CHAR: os << t.cval; break;    
            case Token::DBL: os << t.dval; break;    
            case Token::STR: os << t.sval; break;    
        }    
        return os;    
    }    
    
};    

int main(int argc, char const *argv[])
{
    std::string s = "string";    
    int i = 12;    
    char c = 'c';    
    double d = 1.28;    
    Token t;    
    t = i;    
    std::cout << t << "\n";    
    t = c;    
    std::cout << t << "\n";    
    t = d;    
    std::cout << t << "\n";    
    t = s;    
    std::cout << t << "\n";    
    return 0;
}

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~青萍之末~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值