类和动态内存分配(C++)

动态内存和类:

C++使用new和delete运算符来动态控制内存。C++在分配内存时采取的是:让程序在运行时决定内存分配,而不是在编译时决定。

首先,我们先构造一个只有字符串、字符串长度和字符串个数的类:

#pragma once
#include<iostream>
class StringBad
{
private:
	char *str;
	int len;
	static int num_strings;

public:
	StringBad();
	~StringBad();
	StringBad(const char *s);
	friend std::ostream &operator<<(std::ostream &os, const StringBad &st);
};


其中,我们用char指针来表示字符串,这意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间。这避免了在类声明中预先定义字符串长度。
其次,将num_strings成员定义为静态存储类。静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本。

其中,构造函数如下:


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++;
}


StringBad::StringBad()
{
	len = 4;
	str = new char[4];
	std::strcpy(str, "C++");
	num_strings++;
}

其中,num_strings的值被初始化为0。不能再类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但不分配内存。
初始化是在方法文件中,而不是在类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。
字符串并不保存在对象中,字符串单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串的信息。

析构函数中包含了示例中对处理类来说最重要的东西:

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

str成员指向new分配的内存。当StringBad对象过期时,str指针也将过期。但str指向的内存仍被分配,除非使用delete将其释放。删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此,必须使用析构函数,在析构函数中使用delete语句可确保对象过期时,由构造函数使用new分配的内存被释放。

但这样子定义时,当StringBad类的对象被函数当作参数使用时,StringBad类的对象将会使用析构函数。例如:

StringBad head("Lettuce Prey");

void callme(StringBad st) {
	std::cout << "String passed by value:\n" << "    \""<<head<<"\"\n";
}

callme(head);

当使用callme(head)时,head在函数结束后将会运行其析构函数。
又或者,当使用下列代码时:

StringBad head("Lettuce Prey");
StringBad haha = head;

StringBad haha = head;使用的是哪个构造函数呢,不是默认构造函数,也不是参数为const char *的构造函数。这种形式的初始化等效于StringBad haha = String(head);因为haha的类型为StringBad,因此相应的构造函数原型应为:StringBad(const StringBad &);自动生成的构造函数不知道需要更新静态变量num_strings,因此,上述代码会出现问题:在初始化haha时,num_strings的数量没有增加,且在释放haha内存之后,由于haha和head的字符串指针指向同一位置,head的字符串指针指向的位置已被释放,因此若输出会乱码。

特殊成员函数:

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

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

更准确地说,编译器将生成上述最后三个函数的定义——如果程序使用对象的方式要求这样做。
C++11提供了另外两个特殊成员函数:移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。

  1. 默认构造函数:
    如果希望在创建对象时不显式地对它进行初始化,则必须显式地定义默认构造函数。带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。

  2. 复制构造函数:
    复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中,而不是常规的赋值过程中。

Class_name(const Class_name &);
  1. 何时调用复制构造函数:
    新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。例如(motto是一个StringBad对象):

    StringBad ditto(motto);
    StringBad metoo = motto;
    StringBad also = StringBad(motto);
    StringBad * pStringBad = new StringBad(motto);
    

    其中中间的2个声明可能会使用复制构造函数直接创建metoo和also,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给metoo和also。最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给pstring指针。

    每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或返回对象时,都将使用复制构造对象函数。编译器生成临时对象时,也将使用复制构造函数。

    由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。这样可以节省调用构造函数的时间以及存储新对象的时间。

  2. 默认的复制构造函数的功能:
    默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。

因此,上述的错误,可以靠定义一个显式复制构造函数以解决问题。

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

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

赋值运算符:

ANSI C允许结构赋值,而C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。

  1. 赋值运算符的功能以及何时使用它:
    将已有的对象赋给另一个对象时,将使用重载的赋值运算符:

    StringBad headline1("Celery Stalks at Midnight");
    StringBad knot;
    knot = headline1;
    

    初始化对象时,并不一定会使用赋值运算符:

    StringBad metoo = knot;
    

    这里,metoo是一个新创建的对象,被初始化为knot的值,因此使用复制构造函数。然而,实现时可能分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过复制将临时对象的值复制到新对象中。也就是说,初始化总是会调用复制构造函数,而使用=运算符也允许调用赋值运算符。
    与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个赋值。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。

  2. 赋值的问题:
    同样的,对于上面的赋值代码:

    StringBad headline1("Celery Stalks at Midnight");
    StringBad knot;
    knot = headline1;
    

    我们会发现,当程序运行结束时,会与复制构造函数出现同样的问题,因此,我们也需要重载赋值运算符。

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

    通过返回一个对象,函数可以像常规赋值操作那样,连续进行赋值。

    StringBad &StringBad::operator=(const StringBad &st) {
    	if (this == &st)	return *this;
    	delete [] str;
    	len = st.len;
    	str = new char[len + 1];
    	std::strcpy(str, st.str);
    	return *this;
    }
    

    代码首先检查自我复制,如果相同直接返回*this,如果地址不同,则删除之前str指向的内存,再将一个新的字符串赋给str。

改进后新String类:

String.h

#pragma once
#include<iostream>
using std::ostream;
using std::istream;
class String
{
private:
	char *str;
	int len;
	static int num_strings;
	static const int CINLIM = 80;
public:
	String();
	~String();
	String(const char *s);
	String(const String &);
	int length() const { return len; }
	String &operator=(const String &);
	String &operator=(const char *);
	char & operator[](int i);
	const char & operator[](int i) const;
	friend bool operator<(const String &st, const String &st2);
	friend bool operator>(const String &st, 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) {
		char temp[CINLIM];
		is.get(temp, CINLIM);
		if (is)	st = temp;
		while (is&&is.get() != '\n')	continue;
		return is;
	}
	static int HowMany();
};



String.cpp

#include "String.h"
#include<cstring>
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 = 4;
	str = new char[1];
	str[0] = '\0';
	num_strings++;
}

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

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

String &String::operator=(const String &st) {
	if (this == &st)	return *this;
	delete[] str;
	len = std::strlen(st.str);
	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 char &String::operator[](int i)const {
	return str[i];
}

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 (std::strcmp(st1.str, st2.str) > 0);
}

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

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



sayings.cpp:

#include<iostream>
#include"String.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;
		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 saying:\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;
}

在vs2017编译器中不能在方法文件中使用String::CINLIM,因此就把重载>>的函数写成了内联函数,至于为什么不能,还没搞清楚。等以后搞清楚了再回来更新。

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

使用new初始化对象的指针成员时小心的事项:

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

包含类成员的类的逐成员复制:

假设类成员的类型为String类或标准string类:

class Magazine {
private:
	String title;
	string publisher;
	...
};

String和string都是用动态内存分配,这是否意味着需要为Magazine类编写复制构造函数和赋值运算符:不需要,默认的逐成员复制和赋值行为有一个一定的智能。

声明:以上整理自个人理解和Stephen Prata 著的《C++ Primer Plus》

猜你喜欢

转载自blog.csdn.net/MoooLi/article/details/82791954