• 《C++ primer plus》精炼(OOP部分)——对象和类(7)


    “The beautiful thing about learning is that no one can take it away from you.” - B.B. King

    对于返回值类型的说明

    返回值有两种类型,以值的方式返回和以引用的方式返回。当返回的对象在该函数结束时被销毁,则必须以值的方式返回,如果是函数外的对象,则两种方式皆可。
    如果要返回const类型的对象,那么引用返回时必须声明为const引用,值返回则无需如此,这是因为值返回时通过复制构造函数构造了一个临时对象,本质上已经不是原来那个对象了,而引用指向的仍是原来的对象。
    当你确定要对返回的对象进行修改时,不要用const,典型的例子是重载赋值运算符和重载与cout一起使用的<<运算符。赋值运算符需要返回非const的类对象,以便于连续赋值时返回值能作为左值;<<运算符则要返回ostream&类对象,因为还需要改变cout来输出数据。
    对于值引用的返回值,虽然在某些情况下可以作为左值被改动,但是由于本质上是构造了一个临时对象作为返回值,对临时对象改动值没有任何意义。
    当你想要让编译器判定你是否不小心错误的改动了返回的临时对象时,可以将返回值设为const对象。

    再谈new运算符和delete运算符

    当new或delete一个类对象时,分为两个步骤:

    1. 申请/解除申请一块内存,用来存储
    2. 使用适当的构造函数/析构函数来初始化/结束使用类对象。

    事实上,new和delete运算符只完成第一部分的操作,我们现在使用的可以理解为将这两个操作封装在一起重载的new和delete。

    定位new运算符

    定位new运算符可以在分配内存给对象时指定内存位置:

    char* buffer = new char[BUF];//得到一块内存
    
    JustTesting* pc1, * pc2;
    pc1 = new (buffer)JustTesting;//将对象放入buffer指针指向的内存块
    pc2 = new JustTesting("Heap1", 20);//将对象放在堆中
    
    JustTesting* pc3, * pc4;
    pc3 = new (buffer+sizeof(JustTesting))JustTesting("Better idea", 6);//避开pc1指向的对象占据的内存块,不然会发生错误
    
    pc3->~JustTesting();//显式调用析构函数
    pc1->~JustTesting();
    
    delete pc2;//释放Heap1
    
    delete[]buffer;//释放buffer的内存块,此时不会调用析构函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. 注意使用方法,先用buffer申请一块内存,此时buffer成为一个指向内存块的指针,然后pc1使用定位new运算符以buffer为起点初始化一个类对象。
    2. 使用定位new运算符new的类对象放在指定内存块中,普通new出来的类对象放在堆中
    3. 如果要在用定位new运算符new一个类对象,注意要避开之前new的类对象所占据的内存,否则pc1指向的对象无法被辨别。
    4. 使用定位new运算符new的类对象不能用delete来析构,需要显式调用析构函数。
    5. 在将其中所有对象析构后,才能将申请的内存块(buffer)释放掉。

    练习:用类模拟队列(queue)

    队列是基本的数据结构之一,具有先进先出(FIFO)的属性,我们可以自己编写一个类来表示阉割版的队列,然后我们将使用这个队列来编写一个超市的收银系统,这个系统接待最先来的顾客并让他先走。
    以下是队列的头文件声明,如果想自己试着写一下,可以直接跳过这段代码去看下面的说明:

    class Queue
    {
    private:
    	//类范围定义
    	//Node是嵌套类结构定义
    	struct Node {
    		Item item;
    		struct Node* next;
    	};
    	enum { Q_SIZE = 10 };
    	Node* front;//指向队列最前端的指针
    	Node* rear;//指向队列最后端的指针
    	int items;//队列当中现有的对象数量
    	const int qsize;//队列之中最大的对象数量
    	//提前定义以阻止编译器构造默认复制构造函数和重载赋值运算符函数
    	Queue(const Queue& q) :qsize(0) {};
    	Queue& operator=(const Queue& q) {
    		return *this;
    	}
    public:
    	Queue(int qs = Q_SIZE);//创建一个对象数量限制为qs的队列
    	~Queue();
    	bool isempty()const;
    	bool isfull()const;
    	int queuecount()const;
    	bool enqueue(const Item& item);//将item添加到队尾
    	bool dequeue(Item& item);//从队首删除Item
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    1. 首先考虑队列中的数据成员。队列要有一个结构体表示结点(node),结点的数据域为item,指针域为next;一个枚举量(Q_SIZE=10)作为这个类所有对象共享的最大节点数量,两个指向结构体的指针,一个指向队列头(front),一个指向队列尾(rear),一个代表队列中结点数的变量(items ),一个表示队列中容纳的最大结点数的常量(qsize),这两个变量用于对某些语法进行说明。
    2. 队列中有两个伪私有方法。伪私有方法的意思是,我们虽然将它们声明为私有方法,但我们并不在公有接口上使用它们,换句话讲,他们根本不会被用到,那为什么还要声明他们呢?为了防止编译器自动生成这些函数。在这个例子中,我们编写了拷贝构造函数和赋值运算符重载函数两个内联函数作为伪私有方法,尽管我们用不到也不想用,但是如果有人在使用这个类时用复制构造或赋值的方式试图构造一个新对象,编译器将报错。如果不编写这些函数,编译器将自动生成一个浅拷贝或浅赋值函数,而对于用两个指针来指向队列的queue类来说,这样的方式是错误的。
    3. 公有接口包括传入赋给qsize的参数的构造函数(函数参数为int qs=Q_SIZE),析构函数,检验队列是否为空(isempty),是否为满(isfull),返回队列节点数量(queuecount),入队函数(enqueue),出队函数(dequeue)。

    下面是一些重要函数的实现:

    Queue::Queue(int qs) :qsize(qs)
    {
    	front = rear = NULL;
    	items = 0;
    }
    
    Queue::~Queue()
    {
    	Node* temp;
    	while (front != NULL)
    	{
    		temp = front;//保存队首的指针
    		front = front->next;//队首指针指向下一个对象
    		delete temp;//删除之前的队首
    	}
    }
    
    //将新的对象加入队列
    bool Queue::enqueue(const Item& item)
    {
    	if (isfull())
    		return false;
    	Node* add = new Node;//创建结点
    	add->item = item;//设置结点指针
    	add->next = NULL;//设置空指针
    	items++;
    	if (front == NULL)
    		front = add;
    	else
    		rear->next = add;
    	rear = add;
    	return true;
    }
    
    //将队首对象出列
    bool Queue::dequeue(Item& item)
    {
    	if (front == NULL)
    		return false;
    	item = front->item;
    	items--;
    	Node* temp = front;
    	front = front->next;
    	delete temp;
    	if (items == 0)rear = NULL;
    	return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    1. 使用qs赋值给qsize时不能使用常规赋值的方式,因为qsize是常量,除了初始化外不能进行重新赋值,所以要在构造函数进行到函数体代码前将值给予qsize,这就需要使用列表初始化方法,下面是这个语法的大概格式:

    构造函数名(函数参数):要被初始化的数据成员名(初始化该数据成员的参数名称){…};

    这种方法也可以一次初始化多个数据成员,且可用范围不限于const成员,所有成员均可。
    有些数据成员必须用这种方式进行初始化,不仅是const成员,还有引用数据成员,因为引用数据成员从一开始初始化就和对象绑定在一起且无法分开,所以它必须进行初始化。

    1. 析构函数每次从队列头推出队列中的一个结点,直到没有结点为止。
    2. 入列,就是传入一个item,使用item构造一个结点并将这个新的结点接到队列尾。
    3. 出列,从队列头推出一个结点,返回这个结点的item。

    编写出queue类后,我们编写customer(顾客)类,这个类作为queue类的测试item。下面是customer类的头文件:

    class Customer
    {
    private:
    	long arrive;//顾客的到达时间
    	int processtime;//顾客的交易时间
    public:
    	Customer()
    	{
    		arrive = processtime = 0;
    	}
    	void set(long when);
    	long when()const
    	{
    		return arrive;
    	}
    	int ptime()const
    	{
    		return processtime;
    	}
    };
    
    typedef Customer Item;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    1. customer类包括两个数据成员,每个顾客的到达时间arrive,每个顾客在到达队列头后交易的时间(即停留的时间)processive。
    2. 公有接口包括默认构造函数(arrive和processive都设为0),设置arrive和processive的函数set(传入参数when设为arrive的值,processive使用rand函数设为1-3之间的随机值),when函数返回arrive,ptime函数返回processive。
    3. 使用typedef将Customer定义为Item。

    下面是set类的定义:

    void Customer::set(long when)
    {
    	processtime = std::rand() % 3 + 1;
    	arrive = when; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    rand()函数生成一个随机数,对3取模生成一个[0,3)的数,再加一生成[1,4)的数,转换成int类型时舍去小数,于是processive是一个[1,3]的数。
    下面是测试代码,这段代码有点难度,而且有些小漏洞,建议明白代码逻辑即可:

    const int MIN_PER_HR = 60;
    
    bool newcustomer(double x);
    
    int main(void)
    {
    	using namespace std;
    	srand(time(0));
    
    	cout << "Case Study:Bank of Heather Automatic Teller\n";
    	cout << "Enter maximum size of queue:";
    	int qs;
    	cin >> qs;
    	Queue line(qs);
    
    	cout << "Enter the number of simulation hours:";
    	int hours;
    	cin >> hours;
    	//模拟每分钟循环一次
    	long cyclelimit = MIN_PER_HR * hours;
    
    	cout << "Enter the average number of customers per hour:";
    	double perhour;//每小时平均到来的顾客的个数
    	cin >> perhour;
    	double min_per_cust;//两次顾客到达之间的平均时间间隙
    	min_per_cust = MIN_PER_HR / perhour;
    
    	Item temp;//新的顾客数据
    	long turnaways = 0;//因为队列排满被拒绝入队的顾客的数量
    	long customers = 0;//队列中的数量和服务过的顾客数量之和
    	long served = 0;//模拟期间服务的数量
    	long sum_line = 0;//累积的线路长度
    	int wait_time = 0;//正在处理的顾客业务的剩余处理时间
    	long line_wait = 0;//队列中所有顾客等待时间之和
    	//运行模拟
    	for (int cycle = 0; cycle < cyclelimit; cycle++)
    	{
    	//如果在这一次cycle中来了新顾客,那么尝试将新顾客入队
    		if (newcustomer(min_per_cust))
    		{
    			if (line.isfull())
    				turnaways++;
    			else
    			{
    				customers++;
    				temp.set(cycle);
    				line.enqueue(temp);
    			}
    		}
    		//如果上一个顾客已经交易完,那么将新顾客出队,处理新顾客的交易事宜
    		if (wait_time <= 0 && !line.isempty())
    		{
    			line.dequeue(temp);
    			wait_time = temp.ptime();
    			line_wait += cycle - temp.when();
    			served++;
    		}
    		//该顾客还未处理完
    		if (wait_time > 0)
    			wait_time--;
    		sum_line += line.queuecount();
    	}
    	//打印结果
    	if (customers > 0)
    	{
    		cout << "customers accepted:" << customers << endl;
    		cout << "  customers served:" << served << endl;
    		cout << "		  turnaways:" << turnaways << endl;
    		cout << "average queue size:";
    		//设置以精确为两位小数的形式输出
    		cout.precision(2);
    		cout.setf(ios_base::fixed, ios_base::floatfield);
    		cout << (double)sum_line / cyclelimit << endl;
    		cout << " average wait time:" << (double)line_wait / served << " minutes\n";
    	}
    	else
    		cout << "No customers!\n";
    	cout << "Done!\n";
    
    	return 0;
    }
    //x是顾客平均到来的时间
    //如果1分钟内出现了新顾客那么返回true
    bool newcustomer(double x)
    {
    	return (rand() * x / RAND_MAX < 1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    请添加图片描述
    我是霜_哀,在算法之路上努力前行的一位萌新,感谢你的阅读!如果觉得好的话,可以关注一下,我会在将来带来更多更全面的知识讲解!

  • 相关阅读:
    k8s-实战——基于nfs实现动态存储
    Android - toolbar 优化 title修改边距和navigation icon修改padding值
    【代码源每日一题Div1】#665. 数组划分「位运算贪心+动态规划」
    Dubbo-聊聊通信模块设计
    QGraphicsView图形视图框架使用(六)图元动画
    c++文件服务器相关知识点记录-1
    BLE Mesh蓝牙mesh传输大数据包传输文件照片等大数据量通讯
    NodeRed Modbus学习一(配置Modsim32)
    基于javaweb的医院管理系统(java+springboot+mybatis+vue+mysql)
    OA管理系统源码
  • 原文地址:https://blog.csdn.net/m0_72987309/article/details/133232817