《C++ Primer Plus》(第6版)中文版—学习笔记—类和动态内存分配

第12章 类和动态内存分配

动态内存和类

为什么需要动态分配?思考这样一个问题:如果要创建一个类,类中一个成员表示一个人的姓名,如果我们指定使用40个字符的数组,但是实际使用的时候,我们的姓名往往只占用了5个字符,而40个字符沾满的很少,所以这35个字符都是空闲的,但是我们又不能随便变更,那我们创建类的时候,如果需要100个类,这样极大的浪费了内存。其实这个问题是可以通过string类进行解决的。

原文为了展示动态内存分配的用法,编写了一个StringBad类来进行演示,详情请看原文。贴出类的设计:

#include <iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_
class StringBad
{
private:
    char * str;                // pointer to string
    int len;                   // length of string
    static int num_strings;    // number of objects
public:
    StringBad(const char * s); // constructor
    StringBad();               // default constructor
    ~StringBad();              // destructor
// friend function
    friend std::ostream & operator<<(std::ostream & os, 
                       const StringBad & st);
};
// 部分执行代码或函数
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";
}

书中的程序运行之后会出现很多的错误,这些错误正是因为我们没有设计一个完美的string类,所以称为bad。程序将会出现乱码、num_strings计数出现负值的状况。上面的代码的callme1、callme2函数只有传入参数方式不同,callme1是采用了引用的方式,而callme2是采用了参数传入的方式,这就会出现一些问题,假设我创建了一个StringBad类headline2,而我们将headline2传入callme2的时候,程序将会调用析构函数,最终导致headline2显示乱码。

还有一个就是num_strings出现负值的情况,正是因为这些原因,每一次调用了析构函数,num_strings就会调用一次,但是程序运行过程中出现了很多隐式参数,也就是这些参数只是存在一段时间就会被删除的,也就是说并没有调用构造函数去构造一个StringBad类,而析构函数却频繁的被调用,这就是会出现num_strings负值的情况。

这些情况其实都是编译器自动生成的成员函数引起的:

  • 默认构造函数,如果没有定义构造函数,默认构造函数不执行任何操作
  • 默认析构函数,如果没有定义
  • 复制构造函数,如果没有定义
  • 赋值运算符,如果没有定义
  • 地址运算符,如果没有定义

那么我们就可以解释,上述两个问题都是因为隐式复制构造函数和隐式赋值运算符引起的。

隐式地址运算符返回调用对象的地址(即this指针的值)

默认构造函数

默认构造函数将构造一个常规的自动变量,也就是说,它的值在初始化时是未知的。

默认构造函数只能有一个

默认析构函数

复制构造函数

复制构造函数用于将一个类复制到新创建的对象中,也就是说,它用于初始化中(包括按值传参)

下面的操作都会调用复制构造函数

StringBad ditto(motto);								// calls StringBad(const StringBad &)
StringBad metoo = motto;							// calls StringBad(const StringBad &)
StringBad also = StringBad(motto);					// calls StringBad(const StringBad &)
StringBad ditto(motto);								// calls StringBad(const StringBad &)
StringBad *pStringBad = new StringBad(motto)		// calls StringBad(const StringBad &)

默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。

还出现的情况就是乱码情况,甚至出现了程序崩溃的情况,这是由于上述的原因调用了两次的析构函数,第一次的时候我们释放了数组指针的值,这让那些指针内存被释放,当我们再次调用析构函数的时候,就会出错,因为我们尝试释放已经被释放过的内存,这是不允许的。所以我们需要显式地写出复制函数,将浅复制写成深复制。

StringBad::StringBad(const StringBad & st)
{
	num_strings++;
	len = st.len;
	str = new char [len + 1];
	std::strcpy(str, st.str);
	cout << num_strings << ": \"" << str << "\" object created\n";
}

浅复制只是复制一些指针值,比如说指针数组,它只是复制了指针,而深复制复制了指针指向的数据。

赋值运算符

问题还有一部分出自默认的赋值运算符。

Class_name & Class_name::operator=(const Class_name &);
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;
}

改进后的新string类

nullptr空指针

中括号表示法访问字符

char & String::operator[](int i)
{
	return str[i];
}

静态成员函数只能使用静态数据成员。

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

在使用new初始化对象的指针成员时必须特别小心,应该这么做:

  • 在构造函数中使用了new来初始化指针成员,那么应该在析构函数中使用delete
  • new和delete必须相互兼容,new对应delete,new[]对应delete[]
  • 如果有多个析构函数,那么构造函数中的new需要一致,要么new,要么new[]。
  • 定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。
  • 应当定义一个赋值运算符,通过深度复制将一个对象初复制给另一个对象。

有关返回对象的说明

返回指向const对象的引用

使用const引用的常见原因是旨在提高效率,但对于何时可以使用这种方法存在一些限制。如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高其效率。

const Vector & Max(const Vector & v1, const Vector & v2)
{
	if (v1.magval() > v2.magval())
	{
		return v1;
	}
	else
	{
		return v2;
	}
}

首先,返回对象将调用复制构造函数,而返回引用不会。这样就可以提高效率。

返回指向非const对象的引用

两种常见的额返回非const对象情形是,重载赋值运算符以及重载与cout一起使用的<<运算符。重载赋值运算符是为了提高效率,但是后者必须这样做。

返回对象

返回const对象

使用指向对象的指针

  • 使用常规表示法来声明指向对象的指针

    String * a;

  • 可以将指针初始化为指向已有的对象

    String * a = new String(sayings[choice]);

  • 可以使用new来初始化指针,这将创建一个新的对象

    String * a = new String;

    String * a = new String(“afsdfasfs”);

    String * a = new String(sayings[choice]);

  • 对类使用new将调用相应的类构造函数来初始化新创建的对象

    if (sayings[i].length() < shortest0>length())

  • 可以使用->运算符通过指针访问类方法

  • 可以对对象指针引用解除引用运算符(*)来获得对象

    if (sayings[i] < *first)

定位new运算符

定位new运算符能够在分配内存时能够指定内存未知。

队列模拟

class Queue
{
private:
	struct Node { Item item; struct Node * next; };
	enum{Q_SIZE = 10};
	Node * front;
	Node * rear;
	int items;
	const int qsize;
public:
...
}

C++提供一种特殊的初始化方式,叫做成员初始化列表

Queue::Queue(int qs) : qsize(qs), front(NULL), rear(NULL), items(0) { }
// 只有构造函数可以使用这种初始化列表语法。对于const成员,必须使用这种语法。
// 另外对于被声明为引用的类成员,也必须使用这种语法
class Agency { };
class Agent
{
private:
    Agency & belong;
    ...
}
Agent::Agent(Agent & a) : belong(a) { };

对于本身就是类对象的成员来说,使用成员初始化列表的效率更高。

猜你喜欢

转载自blog.csdn.net/weixin_49643423/article/details/113830715