C++ 知识点记录(5)类和动态内存分配(Cap12)

1. 动态内存和类

(1)不能在类声明中初始化静态成员变量。

        这是因为声明描述了如何分配内存,但并不分配内存。对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。

        注意:静态数据成员在类声明中声明,在包含类方法文件中初始化,初始化时使用作用域运算符来指出静态成员所属的类。但如果静态数据成员是整型枚举型const,则可以在类声明中初始化。

#pragma once
// stringbad.h
#ifndef STRINGBAD_H_
#define STRINGBAD_H_
#include <iostream>
class StringBad {
private:
	char* str;
	int len;       // 字符串长度
	static int num_strings;   // 构造对象的数量
public:
	StringBad(const char* s);
	StringBad();
	~StringBad();
	friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
};
#endif // !STRINGBAD_H_
// stringbad.cpp
#include <iostream>
#include <cstring>
#include "stringbad.h"
using std::cout;

// 初始化静态类成员
int StringBad::num_strings = 0;

// 类方法
StringBad::StringBad(const char* s) {
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
	num_strings++;
	cout << num_strings << ": \"" << str << "\" object created.\n";
}

StringBad::StringBad() {
	len = 4;
	str = new char[len];
	std::strcpy(str, "C++");
	num_strings++;
	cout << num_strings << ": \"" << str << "\" default object created.\n";
}

StringBad::~StringBad() {
	cout << "\"" << str << "\" object deleted, ";
	--num_strings;
	cout << num_strings << " left.\n";
	delete[]str;
} 

std::ostream& operator<<(std::ostream& os, const StringBad& st) {
	os << st.str;
	return os;
}

        字符串并不保存在对象中,字符串单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串的信息。

        删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此必须使用析构函数,在其中使用delete语句释放new分配的内存。

        下述代码的问题由隐式复制构造函数和隐式赋值运算符引起。

// 由于编译器自动生成的复制构造函数,而造成很多奇怪现象,甚至无法编译
// vegnews.cpp
#include <iostream>
#include "stringbad.h"
using std::cout;

void callme1(StringBad&);   // 按引用传递
void callme2(StringBad);    // 按值传递

int main() {
	using std::endl; 
	{
		cout << "Starting an inner block.\n";
		StringBad headline1("Celery Stalks at Midnight");
		StringBad headline2("Lettuce Prey");
		StringBad sports("Spinach Leaves Bowl for Dollars");
		cout << "headline1: " << headline1 << endl;
		cout << "headline2: " << headline2 << endl;
		cout << "sports: " << sports << endl;
		callme1(headline1);
		cout << "headline1: " << headline1 << endl;
		callme2(headline2);
		cout << "headline2: " << headline2 << endl;

		cout << "Initialize one object to another:\n";
		// 不适用默认构造函数,这种形式的初始化等价于:
		// StringBad sailor = StringBad(sports);
		// 其原型为:StringBad(const StringBad&)
		// 当使用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数(复制构造函数,因为它创建对象的一个副本)
		StringBad sailor = sports;     

		cout << "sailor: " << sailor << endl;
		cout << "Assign one object to another:\n";
		StringBad knot;
		knot = headline1;
		cout << "knot: " << knot << endl;
		cout << "Exiting the block.\n";
	}
	cout << "End of main()\n";

	return 0;
}

void callme1(StringBad& rsb) {
	cout << "String passed by reference:\n";
	cout << "    \"" << rsb << "\"\n";
}

void callme2(StringBad sb) {
	cout << "String passed by value:\n";
	cout << "    \"" << sb << "\"\n";
}

(2)特殊成员函数

        C++自动提供了下面这些成员函数:

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

        更准确地说,编译器将生成上述最后三个函数地定义——如果程序使用对象地方式要求这样做。例如,如果程序员将一个对象赋给另一个对象,编译器将提供赋值运算符的定义。

        C++11提供另外两个特殊成员函数

  • 移动构造函数
  • 移动赋值运算符

        1)默认构造函数

        默认构造函数使类类似于一个常规的自动变量,也就是说它的值在初始化时是未知的。带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。但只能有一个默认构造函数。

        2)复制构造函数

        复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:

        Class_name(const Class_name&);

        何时调用?

        新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。

例如:

StringBad ditto(motto);

StringBad metoo = motto;

StringBad also = StringBad(motto);

StringBad* pStringBad = new StringBad(motto); // 使用motto初始化一个匿名对象,并将新对象的地址赋给pstring指针。

        注意:每当程序生成了对象副本时,编译器都将使用复制构造函数。当函数按值传递对象函数返回对象时,都将使用复制构造函数。按值传递意味着创建原始变量的一个副本。编译器生成临时对象时,也将使用复制构造函数。由于按值传递对象将调用复制构造函数,因此应该按引用传递对象,这样可以节省调用构造函数的时间以及存储新对象的空间。

        默认的复制构造函数的功能?

        默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。静态成员不受影响,因为它们属于整个类

        3)深度复制

        复制构造函数应该复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串地址。这样每个对象都有自己的字符串。

        必须定义复制构造函数的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身

        注意:如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。

        4)赋值运算符

何时使用重载的赋值运算符?

  • 将已有的对象赋给另一个对象时
  • 初始化对象时,并不一定会使用赋值运算符

        与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响

        提供赋值运算符(进行深度复制)定义:

  • 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据;
  • 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容;
  • 函数返回一个指向调用对象的引用。

        赋值运算符是只能由类成员函数重载的运算符之一。

#pragma once
// 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;
	int len;       // 字符串长度
	static int num_strings;   // 构造对象的数量
	static const int CINLIM = 80;  // cin input limit
public:
	// 构造函数和其他函数
	String(const char* s);   // 带一个参数的构造函数
	String();                // 默认构造函数
	String(const String& st);   // 复制构造函数
	~String();               // 析构函数
	int length() const { return len; }  // 内联函数:获取字符串长度

	// 重载运算符成员方法
	String& operator=(const String&);  // 重载赋值运算符
	String& operator=(const char*);    // 同上
	char& operator[](int i);       
	const char& operator[](int i) const;

	// 重载运算符友元函数
	// 将比较函数作为友元,有助于将String对象与常规的C字符串进行比较
	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 std::ostream& operator<<(std::ostream& os, const String& st);
	friend std::istream& operator>>(std::istream& is, String& st);

	// 静态函数
	static int HowMany();
};
#endif // !STRING1_H_
// string1.cpp 
#include <cstring>
#include "string1.h"
using std::cin;
using std::cout;

// 初始化静态成员
int String::num_strings = 0;

// 静态方法
int String::HowMany() {
	return num_strings;
}

// 类方法
String::String(const char* s) {
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
	num_strings++;
}

String::String() {
	len = 0;
	str = new char[1];  //delete[]与使用new[]初始化的指针和空指针都兼容
	str[0] = '\0';
	num_strings++;
}

String::String(const String& st) {
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	num_strings++;
}

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

// !!重点学习
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;
}

String& String::operator=(const char* s) {
	delete[] str;
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
	return *this;
}

// 只读形式的数组访问
char& String::operator[](int i) {
	return str[i];
}
// 制度形式的数组访问(const String)
const char& String::operator[](int i) const {
	return str[i];
}

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

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

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

std::ostream& operator<<(std::ostream& os, const String& st) {
	os << st.str;
	return os;
}

std::istream& operator>>(std::istream& is, String& st) {
	char temp[String::CINLIM];
	is.get(temp, String::CINLIM);
	if (is)
		st = temp;
	while (is && is.get() != '\n')
		continue;
	return is;
}
// saying1.cpp
#include <iostream>
#include "string1.h"
const int ArSize = 10;
const int MaxLen = 81;
int main() {
	using std::cout;
	using std::cin;
	using std::endl;
	String name;
	cout << "Hi, what's your name?\n>> ";
	cin >> name;

	cout << name << ", please enter up to " << ArSize << " short sayings <empty line to quit>:\n";
	String sayings[ArSize];
	char temp[MaxLen];
	int i;
	for (i = 0; i < ArSize; i++) {
		cout << i + 1 << ": ";
		cin.get(temp, MaxLen);
		while (cin && cin.get() != '\n')  continue;   // 去掉非法输入
		// 较早的get(char*, int)版本在读取空行后,返回的值不为false。
		// 然而对于这些版本来说,如果读取了一个空行,则字符串中第一个字符将是一个空字符
		if (!cin || temp[0] == '\0')  //空行?
			break;
		else
			sayings[i] = temp;
	}
	int total = i;
	if (total > 0) {
		cout << "Here are your sayings:\n";
		for (i = 0; i < total; i++)
			cout << sayings[i][0] << ": " << sayings[i] << endl;
		int shortest = 0;
		int first = 0;
		for (i = 1; i < total; i++) {
			if (sayings[i].length() < sayings[shortest].length())  shortest = i;
			if (sayings[i] < sayings[first])
				first = i;
		}
		cout << "Shortest sayings:\n" << sayings[shortest] << endl;
		cout << "First alphabetically:\n" << sayings[first] << endl;
		cout << "This program used " << String::HowMany() << " String objects. Bye.\n";
	}
	else {
		cout << "No input! Bye.\n";
	}

	return 0;
}

2. 在构造函数中使用new时应注意的事项

        使用new初始化对象的指针成员时必须特别小心:

  • 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete;
  • new和delete必须互相兼容。new对应于delete,new[]对应于delete[];
  • 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中的nullptr),这是因为delete(无论带不带中括号)可以用于空指针。
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。具体来说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受影响的静态类成员。
  • 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。具体地说,该方法应该完成这些操作:检查自我赋值的情况释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。

3. 有关返回对象的说明

        如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,则应首选引用,因为其效率更高。

4. 类和定位new运算符

        程序员使用定位new运算符,要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保这两个内存单元不重叠。

        其次,如果使用定位new运算符来为对象分配内存,必须确保其析构函数被调用。delete可与常规new运算符配合使用,但不能与定位new运算符配合使用。这种问题的解决方案是,显式地为使用定位new运算符创建的对象调用析构函数。

        以下程序清单对定位new运算符使用的内存单元进行管理,加入到合适的delete和显式析构函数调用。需注意删除顺序,对于使用定位new运算符创建的对象,应以与创建顺序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于早创建的对象。

// placenew2.cpp
#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF = 512;
class JustTesting {
private:
	string words;
	int number;
public:
	JustTesting(const string& s = "Just Testing", int n = 0) {
		words = s;
		number = n;
		cout << words << " constructed\n";
	}
	~JustTesting() {
		cout << words << " destroyed\n" << endl;
	}
	void Show() const {
		cout << words << ", " << number << endl;
	}
};

int main() {
	char* buffer = new char[BUF];
	JustTesting* pc1, * pc2;

	pc1 = new(buffer) JustTesting;
	pc2 = new JustTesting("Heap1", 20);

	cout << "Memory block addresses:\n" << "buffer: "
		<< (void*)buffer << " heap: " << pc2 << endl;
	cout << "Memory contents:\n";
	cout << pc1 << ": ";
	pc1->Show();
	cout << pc2 << ": ";
	pc2->Show();

	JustTesting* pc3, * pc4;
	pc3 = new(buffer + sizeof(JustTesting))JustTesting("Better Idea", 6);
	pc4 = new JustTesting("Heap2", 10);

	cout << "Memory contents:\n";
	cout << pc3 << ": ";
	pc3->Show();
	cout << pc4 << ": ";
	pc4->Show();

	delete pc2;
	delete pc4;

	pc3->~JustTesting();
	pc1->~JustTesting();

	delete[] buffer;
	cout << "Done\n";
	return 0;
}

5. 队列类

        只有构造函数可以使用初始化列表语法。对于const类成员,必须使用这种语法。对于被声明为引用的类成员,也必须使用这种语法。这是因为引用与const数据类似,只能在被创建时初始化。对于本身就是类对象的成员来说,使用成员初始化列表的效率更高。

        C++11的类内初始化。

模拟顾客队列

#pragma once
// queue.h -- 12.10 -- interface for a queue
#ifndef QUEUE_H
#define QUEUE_H
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;

class Queue {
private:
	struct Node {
		Item item;
		struct Node* next;
	};
	enum { Q_SIZE = 10 };

	Node* front;      // 指向链表头
	Node* rear;       // 指向链表尾
	int items;        // 目前链表里的成员数量
	const int qsize;  // 链表里Item的最大数量

	Queue(const Queue& q): qsize(0){}
	Queue& operator=(const Queue& q) { return *this; }

public:
	Queue(int qs = Q_SIZE);
	~Queue();
	bool isempty() const;
	bool isfull() const;
	int queuecount() const;
	bool enqueue(const Item& item);   // 结尾加节点
	bool dequeue(Item& item);         // 链表头删节点
};

#endif // !QUEUE_H
// queue.cpp
#include <cstdlib>
#include "queue.h"

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

Queue::~Queue() {
	Node* temp;
	while (front != nullptr) {
		temp = front;
		front = front->next;
		delete temp;
	}
}

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

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

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

bool Queue::enqueue(const Item& item) {
	if (isfull())  return false;
	Node* add = new Node;
	// on failure, new throws std::bad_alloc exception
	add->item = item;
	add->next = nullptr;
	items++;
	if (front == nullptr)
		front = add;
	else  
		rear->next = add;
	rear = add;
	return true;
}

bool Queue::dequeue(Item& item) {
	if (front == nullptr)
		return false;
	item = front->item;
	items--;
	Node* temp = front;
	front = front->next;
	delete temp;
	if (items == 0)
		rear = nullptr;
	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;
}
// bank.cpp
#include <iostream> 
#include <cstdlib>    // rand(), srand(), RAND_MAX
#include <ctime>      // time()
#include "queue.h"
const int MIN_PER_HR = 60;

bool newcustomer(double x);   // 判断是否有新用户到来

int main() {
	using std::cin;
	using std::cout;
	using std::endl;
	using std::ios_base;
	std::srand(std::time(0));  // 随机初始化rand()

	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;
	// 循环每1分钟运行一次
	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++) {
		if (newcustomer(min_per_cust))   // 有新顾客到来
		{
			if (line.isfull())
				turnaways++;
			else {
				customers++;
				temp.set(cycle);          // cycle = time of arrival
				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 << "custormers 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;
}

bool newcustomer(double x) {
	return (std::rand() * x / RAND_MAX < 1);
}

6. 小结

(1)如果定义了一个类,其指针成员是使用new初始化的,请指出可能出现的3个问题以及如何纠正这些问题。

        首先,当这种类型的对象过期时,对象的成员指针指向的数据仍将保留在内存中,这将占用空间,同时不可访问,因为指针已经丢失。可以让类析构函数删除构造函数中new分配的内存,来解决这种问题。

        其次,析构函数释放这种内存后,如果程序将这样的对象初始化为另一个对象,则析构函数将试图释放这些内存两次。这是因为将一个对象初始化为另一个对象的默认初始化,将复制指针值,但不复制指向的数据,这将使两个指针指向相同的数据。解决方法是,定义一个复制构造函数,使初始化复制指向的数据。

        第三,将一个对象赋值给另一个对象也导致两个指针指向相同的数据。解决方法是重载赋值运算符,使之复制数据而不是指针。

(2)如果没有显式提供类方法,编译器将自动生成哪些类方法?请描述这些隐式生成的函数的行为。

  • 默认构造函数(不完成任何工作,但使得能够声明数组和未初始化的对象)
  • 复制构造函数(使用成员赋值)
  • 赋值运算符(使用成员赋值)
  • 默认析构函数(不完成任何工作)
  • 地址运算符(返回调用对象的地址,即this指针的值)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值