C++基础学习之类和动态内存分配(9)

主要学习内容:

  • 对类成员使用动态内存分配。
  • 隐式显式复制构造函数。
  • 隐式显式重载赋值运算符。
  • 在构造函数中使用new所必须完成的工作。
  • 使用静态类成员。
  • 将定位new运算符用于对象。
  • 使用指向对象的指针。
  • 实现队列抽象数据类型。(像第(7)篇中的stack实现一样)

动态内存和类

首先说一下类中的静态类成员。类似如下声明:

class StringBad
{
private:
	char * str;
	int len;
	static int num_strings;		// 静态类成员
public:
...
}

静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象共享同一个静态成员。所以上面定义的num_strings成员可以记录所创建的对象数目。另外,静态类成员在类声明中声明,但需要在类声明之外使用单独的语句进行初始化。初始化的形式如下:

// initializing static class member
int StringBad::num_strings = 0;

然后就说一下隐式显式的构造函数,C++会自动提供下面这些成员函数:

  • 默认构造函数,如果没有定义构造函数;
  • 默认析构函数,如果没有定义;
  • 复制构造函数,如果没有定义;
  • 赋值运算符,如果没有定义;
  • 地址运算符,如果没有定义。

最后一个先不讨论,这就说明了如果自己没有定义上面这些函数,那么C++会自己生成这些函数,但是当使用对应的类方法时,这些自动生成的函数却不一定会是我们想要的方法。由此就会带来一些错误。接下来说一下这些成员函数以及它们被调用的一些情况。

  1. 默认构造函数
    如果没有提供任何构造函数,C++就会创建一个空构造函数。例如,假如定义了一个Klunk类,但没有提供任何构造函数,则编译器将提供下面的构造函数:
    Klunk::Klunk() {} 	//implicit default constructor
    
    这种构造函数会在创建对象是调用,最好自己定义一个构造函数来初始化类。
  2. 复制构造函数
    复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:
    Class_name(const Class_name &);
    
    例如:
    StringBad(const StingBad &);
    
    新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。最常见的情况时将新对象显式地初始化为现有的对象。下面4种声明都将调用复制构造函数:
    StringBad ditto(motto);	// calls StringBad(const StringBad &)
    StringBad metoo = motto;	// calls StringBad(const StringBad &)
    StringBad also = StringBad(motto);	// calls StringBad(const StringBad &)
    StringBad * pStringBad = new StringBad(motto);	// calls StringBad(const StringBad &)
    
    其中中间的2种声明可能会使用复制构造函数直接创建metoo和also,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给metoo和also,这取决于具体的实现。最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给pstring指针。每当程序生成了对象副本时,编译器都将使用复制构造函数。具体的说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。因为按值传递意味着创建原始变量的一个副本。而编译器生成临时对象时,也将使用复制构造函数。
    默认的复制构造函数只会逐个复制非静态成员(成员复制叶称为浅复制),复制的是成员的值。所以也应该定义一个显式的复制构造函数防止出现问题。
  3. 赋值运算符
    当使用赋值运算符时,默认的赋值运算符函数也是只进行浅复制,所以如果类成员中有指针的话会直接给指针赋值,这样两个类对象内的类成员指针指向同一块内存,一旦释放其中一个类,那么这块内存就会被释放,会造成内存泄漏。
    解决问题的方法就是自己重新定义赋值运算符函数:
    StringBad & StringBad::operator=(const StringBad & st)
    {
    	if (this == &st)
    		return *this;
    	delete [] str;		// free old string
    	len = st.len;
    	str = new char [len+1];		// get space for new string
    	std::strcpy(str, st.str);		// copy the string
    	return *this;
    }
    
    len+1是因为len方法或者strlen函数都是只计算字符串长度,不计算最后一个空字符\0,所以要加这个空字符的空间。

构造一个String类

这个类是仿照C++的string写的一个类,一方面可以巩固和练习之前的知识,另一方面可以了解一个string类的实现方法。

// string1.h -- fixed and augmented string class definition
#ifndef STRING1_H_
#define STRING1_H_
#include <iostream>
using std::ostream;
using std::istream;

class String
{
private:
	char * str;		// pointer to string
	int len;		// length of string
	static int num_strings;	// number of objects
	static const int CINLIM = 80;	// cin input limit
public:
// constructors and other methods
	String(const char * s);	// constructor
	String();				//default constructor
	String(const String &);	// copy constructor
	~String();				//destructor
	int length() const { return len;}
// overloaded operator methods
	String & operator=(const String &);
	String & operator=(const char *);
	char & operator[](int i);
	const char & operator[](int i) const;
// overloaded operator friends
	friend bool operator<(const String &st, const String &st2);
	friend bool operator>(const String &st1, const String &st2);
	friend bool operator==(const String &st, const String &st2);
	friend ostream & operator<<(ostream & os, const String & st);
	friend istream & operator>>(istream & is, String & st);
// static function
	static int HowMany();
}
#endif

方法定义:

// string1.cpp -- String class method
#include <cstring>		// string.h for some
#include "string1.h"	// includes <iostream>
using std::cin;
using std::cout;

//intializing static class member
int String::num_strings = 0;

// static method
int String::HowMany()
{
	return num_strings;
}
// class methods
String::String(const char * s)		// construct String from C string
{
	len = std::strlen(s);			// set size
	str = new char[len + 1];		// allot storage
	std::strcpy(str, s);			// initialize pointer
	num_strings++;
}

String::String()					// default constructor
{
	len = 4;
	str = new char[1];
	str[0] = '\0';
	num_strings++;
}

String::String(const Stirng & st)
{
	num_strings++;				// handle static member update
	len = st.len;				// same length
	str = new char [len + 1];	// allot space
	std::strcpy(str, st.str);	// copy string to new location
}

String::~String()
{
	--num_strings;
	delete [] str;
}

// overloaded operator methods

// assign a String to a String
String & String::operator=(const String & st)
{
	if (this == &st)
		return *this;
	delete [] str;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	return *this;
}
// read-write char access for non-const String
char & String::operator[](int i)
{
	return str[i];
}
// read-only char access for const String
const char & String::operator[](int i) const
{
	return str[i];
}
// overloaded operator friends
bool operator<(const String &st1, const String &st2)
{
	return (std::strcmp(st1.str, st2.str) < 0);
}

bool operator>(const String &st1, const String &st2)
{
	return st2 < st1;
}

bool operator==(const String &st1, const String &st2)
{
	return (std::strcmp(st1.str, st2.str) == 0);
}

// simple String output
ostream & operator<<(ostream & os, const String & st)
{
	os << st.str;
	return os;
}
// quick and dirty String input
istream & operator>>(ostream & is, String & st)
{
	char temp[String::CINLIM];
	is.get(temp, String::CINLIM);
	if (is)
		st = temp;
	while (is && is.get() != '\n')
		continue;
	return is;
}

构造函数中使用new的注意事项

使用new初始化对象的指针成员时应该这样做:

  • 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
  • new和delete必须相互兼容。new对应于delete,new[]对应于delete[]。
  • 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。
  • 应定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。
    具体的说,该方法应完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。

有关返回对象的说明

当成员函数或独立的函数返回对象时,有三种返回方式:返回指向对象的引用、返回指向对象的const引用或返回const对象。

  1. 返回指向const对象的引用
    最好如下编写函数:
const Vector & Max(const Vector & v1, const Vector & v2)
{
	if (v1.magval() > v2.magval())
		return v1;
	else
		return v2;
}
  1. 返回指向非const对象的引用
    两种常见的返回非const对象情形是重载运算符以及重载与cout一起使用的<<运算符。前者为了效率,后者必须要这么做。所以这两种情况也返回引用。
  2. 返回对象
    如果被返回对象是被调函数中的局部变量,则不应该按引用方式返回它,因为在被调函数执行完毕时,局部对象将调用其析构函数。因此,当控制权回到调用函数时,引用指向的对象将不再存在。在这种情况下,应返回对象而不是引用。通常,被重载的算术运算符属于这一类。
  3. 返回const 对象
    在上面的那种情况中如果担心一些误操作会将返回的对象值改变,则应该返回const对象。

使用指向对象的指针

使用new初始化对象:
通常,如果Class_name是类,value的类型为Type_name,则下面的语句:
Class_name * pclass = new Class_name(value);
将调用如下构造函数:
Class_name (const Type_name &);
另外,如果不存在二义性,则将发生由原型匹配导致的转换(如从int到double)。下面的初始化方式将调用默认构造函数:
Class_name * ptr = new Class_name;
如果使用如下语句来创建对象:
String * favorite = new String(sayings[choice]);
这里使用new来为整个对象分配内存,之后如果不再需要该对象就用delete删除:
delete favorite;
这里的释放只是释放保存这个对象的指针,使用这句话后会自动调用析构函数来释放对象的内容。
另外指向对象的指针可以使用->运算符来访问类方法,也可以对对象指针应用运算符(*)来获得对象。(也就是说和基本标准变量指针一样的用法)。

队列模拟(queue)

queue.h

// queue.h -- interface for a queue
#ifndef QUEUE_H_
#define QUEUE_H_
// this queue will contain Customer items
class Customer
{
private:
	long arrive;	// arrival time for customer
	int processtime;	// processing time for customer
public:
	Customer() { arrive = precesstime = 0; }
	
	void set(long when);
	long when() const { return arrive; }
	int ptime() const { return processtime; }
};

typedef Customer Item;

class Queue
{
private:
// class scope definitions
	// Node is a nested structure definition local to this c
	struct Node { Item item; struct Node * next;};
	enum {Q_SIZE = 10};
// private class members
	Node * front;	// pointer to front of Queue
	Node * rear;	// pointer to rear of Queue
	int items;	// current number of items in Queue
	const int qsize;	// maximum number of items in Queue
	// preemptive definitions to prevent public copying
	Queue(const Queue & q) : qsize(0) { }
	Queue & operator=(const Queue & q) { return *this; }
public:
	Queue(int qs = Q_SIZE);	// create queue with a qs limit
	~Queue();
	bool isempty() const;
	bool isfull() const;
	int queuecount() const;
	bool enqueue(const Item &item);	// add item to end
	bool dequeue(Item &item);		// remove item from front
}
#endif

queue.cpp

// queue.cpp -- Queue and Customer methods
#include "queue.h"
#include <cstdlib>		// (or stdlib.h) for rand()

// Queue methods
Queue::Queue(int qs) : qsize(qs)
{
	front = rear = NULL;	// or nullptr
	items = 0;
}

Queue::~Queue()
{
	Node * temp;
	while (front != NULL)	// while queue is not yet empty
	{
		temp = front;	// save address of front item
		front = front->next;	// reset pointer to next item
		delete temp;	// delete former front
	}
}

bool Queue::isempty() const
{
	return items == 0;
}
bool Queue::isfull() const
{
	return items == qsize;
}

int Queue::queuecount() const
{
	return items;
}

// Add item to queue
bool Queue::enqueue(const Item & item)
{
	if (isfull())
		return false;
	Node * add = new Node;	// create node
// on failure, new throws std::bad_alloc exception
	add->item = item;	// set node pointers
	add->next = NULL;	// or nullptr
	items++;
	if (front == NULL)	// if queue is empty,
		front = add;	// place item at front
	else
		rear->next = add;	// else place at rear
	rear = add;		// have rear point to new node
	return true;
}

// Place front item into item variable and remove from queue
bool Queue::dequeue(Item & item)
{
	if (front == NULL)
		return false;
	item = front->item;		// set item to first item in queue
	items--;
	Node * temp = front;	//save location of first item
	front = front->next;	// reset front to next item
	delete temp;		// delete former first item
	if (items == 0)
		rear = NULL;
	return true;
}
// time set to a random value in the range 1 - 3
void Customer::set(long when)
{
	processtime = std::rand() % 3 + 1;
	arrive = when;
}

这个程序中涉及到的一些点:

  1. 嵌套结构和类
    在类声明中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明是在类的私有部分进行的,则只能在这个类中使用被声明的类型;如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型。例如,如果Node是在Queue类的公有部分声明的,则可以在类的外面声明Queue::Node类型的变量。
  2. 成员初始化列表
    成员初始化列表是C++提供的一种特殊语法,用来对于const数据成员在执行到构造函数体之前,即创建对象时进行初始化。成员初始化列表由逗号分隔的初始化列表组成(前面带冒号)。它位于参数列表的右括号之后、函数体左括号之前。例如:
    Queue:: Queue(int qs) : qsize(qs)	// initialize qsize to qs
    {
    	front = raer = NULL;
    	items = 0;
    }
    
    通常,初值可以是常量或构造函数的参数列表中的参数。但这种方法并不限于初始化常量。例如:
    Queue:: Queue(int qs) : qsize(qs), front(NULL), rear(NULL), items(0)
    {
    }
    
    只有构造函数可使用这种初始化列表语法。对于const类成员,必须使用这种语法。另外,对于被声明为引用的类成员,也必须使用这种语法:
    class Agency {...};
    calss Agent
    {
    private:
    	Agency & belong;	// must use initializer list to initialize
    	...
    };
    Agent::Agent(Agency & a) : belong(a) {...}
    
    因为引用和const数据类似,只能在被创建时进行初始化。对于简单数据成员,使用成员初始化列表和在函数体种使用赋值没有什么区别。然而,对于本身就是类对象的类成员来说,使用成员初始化列表的效率更高。
  3. C++11的类内初始化
    C++11种可以使用更加直观的方式进行初始化:
    class Classy
    {
    	int mem1 = 10;		// in-class initialization
    	const int mem2 = 20;	// in-class initialization
    //...
    };
    
    这与在构造函数中使用成员初始化列表等价:
    Classy::Classy() : mem1(10), mem2(20) {…}
    成员mem1和mem2将分别被初始化为10和20,除非调用了使用成员初始化列表的构造函数,在这种情况下,实际列表将覆盖这些默认初始值:
    Classy::Classy(int n) : mem1(n) {…}
    在这里,构造函数将使用n来初始化mem1,但mem2仍被设置为20。

猜你喜欢

转载自blog.csdn.net/x603560617/article/details/83750276