《C++ Primer Plus》 第十二章 类和动态内存分配 Shepard-Wang

这一章,我们先来通过对 string 类的模拟实现,来学习其他几个类的默认成员函数和其他的知识。

然后,通过我们的学习来实现 队列 这种数据类型。

零 带资源的构造函数和析构函数

实现:

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

	std::cout << "String() invoked: " << num_strings << " "
		<< str << std::endl;
}

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

	std::cout << "String(const char* s) invoked: " << num_strings << " "
		<< str << std::endl;
}

String::~String()
{
	if (str)
	{
		len = 0;
		num_strings--;
		delete[] str;

		std::cout << "~String() invoked: " << num_strings << " "
			<< std::endl;
	}
}

一 复制构造函数

拷贝构造如何调用:

StringBad(const StringBad& s);
StringBad b;

StringBad a(b);
StringBad c = b;
StringBad d = StringBad(b);
StringBad* p = new StringBad(b);

其中中间的两种声明可能会直接使用复制构造函数,也可能使用复制构造生成一个临时对象,然后将新的内容赋给 c 和 d(赋值运算符重载函数)

浅拷贝

深拷贝

实现:

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

	std::cout << "String(const String& s) invoked: " << num_strings << " "
		<< str << std::endl;
}

二 赋值操作符重载

注意事项 :

  • 目标对象可能引用了以前分配的数据,所以函数应该使用 delete[] 来释放这些数据
  • 函数应当避免赋值给自身。否则,重新赋值之前,释放内存操作可能删除对象的内容。
  • 函数返回指向调用对象的引用。(连续赋值)
s1 = s2 = s3;
// 等价于
s1.operator=(s2.operator=(s3));

实现:

String& String::operator=(const String& s)
{
	if (&s != this)
	{
		delete[] str;
		len = s.len;
		str = new char[len + 1];
		std::strcpy(str, s.str);
		std::cout << "operator=(const String& s) invoked: " << num_strings << " "
			<< std::endl;
	}
	return *this;
}

进一步重载赋值运算符

思考下面的代码:

char name[20] = "Shepard";
String s;
s = name; // use constructor to convert type

如果需要经常这样做,这将不是一种理想的解决方案。

最后一条语句是如何工作的:

  1. 程序使用 String(const char* s) 来构造一个临时对象,其中包括对 name 字符串的拷贝
  2. 然后调用赋值运算符重载函数
  3. 最后调用析构删除临时对象

为了提高效率,最简单的办法就是重载赋值运算符,使之可以直接用于常规字符串。

三 比较成员函数

bool operator<(const String& s1, const String& s2)
{
	// 如果第一个字符串的字符在第二个字符串的字符之前,返回负值
	// 相等,返回 0
	return (std::strcmp(s1.str, s2.str) < 0);
}

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

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

四 [] 运算符重载函数

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

/*
 * const String s("Shepard");
 * cout << s[1]; // 要确保 s[1] 不被改变 
 */
const char& String::operator[](int n) const
{
	return str[n];
}

五 静态类型成员函数

静态类型成员的性质:

  1. 不能通过对象调用静态成员

  2. 静态成员不能使用 this 指针

  3. 使用类名和作用域解析操作符来调用

    // 调用方式:
    int cnt = String::HowMany();
    
  4. 静态成员函数只能访问静态成员

.h 文件中定义:

	static int HowMany()
	{
		return num_strings;
	}

string实现代码

六 返回对象的说明

1. 返回指向 const 对象的引用

Vector force1(50, 60);
Vector force2(10, 70);
Vector max;
max = Max(force1, force2);

下面两种实现都是可以的:

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

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

首先,返回对象将调用复制构造函数,而返回引用不会。因此,第二个版本工作更少,效率更高。

其次,引用指向的对象应该在调用函数执行时存在。

第三,v1 和 v2 都被声明为 const 引用 ,因此返回类型必须是 const,这样才匹配。

2. 返回指向非 const 对象的引用

常见的返回非 const 对象的情形是:

  • 重载赋值运算符
  • 重载 << 运算符

前者这样做旨在提高效率,而后者必须这样做。

operator=() 的返回值用于连续赋值:

String s1("Shepard");
String s2, s3;
s3 = s2 = s1;

s2.operator(s1) 的返回值被赋给了 s3 .返回引用或者对象都是可以的。

Operator<<() 的返回值用于串接输出。

String s1("Good stuff");
cout << s1 << "is coming";

operator<<(cout, s1) 的返回值成为一个用于显示后面字符串的对象。返回类型必须是 ostream& 而不能仅仅是 ostream 。如果使用返回类型 ostream ,将要求调用 ostream 类的复制构造函数,而 ostream 类没有共有的复制构造函数。

3. 返回对象

如果被返回的对象是被调用函数中的局部变量,则不应该用引用返回。

通常,被重载的操作符属于这一类。

Vector force1(50, 60);
Vector force2(10, 70);
Vector net;
net = force1 + force2;

Vector Vector::operator+(const Vector& b) const
{
    return Vector(x + b.x, y + b.y);
}

返回语句引发的对复制构造函数的隐式调用创建一个调用程序能够访问的对象。

4. 返回 const 对象

前面的 operator+() 有一个奇异的属性,你也可以这样调用它:

force1 + force2 = net;

force1 + force2 的结果为一临时对象,将 net 赋值给该临时对象。使用完临时对象后,将把他丢弃。

有一种简单的解决办法,将返回值声明为 const Vector ,则不会出现上面这种情况。

七 再谈 布局 new 操作符

char buffer[256];
String* p1 = new (buffer) String;

需要注意的是这里的 new 并不会与 delete 匹配。

需要手动调用 p1 指向的类的析构函数

p1->~String();

而且应该注意调用的顺序(与创建顺序相反)。原因在于:晚创建的对象可能会依赖于早创建的对象 ,仅当所有对象被销毁后才能释放用于存储这些对象的缓冲区。

八 队列模拟

成员初始化列表:

  • 这种格式 只能 用于构造函数
  • 必须用这种格式初始化非静态的 const 成员
  • 必须用这种格式初始化引用数据成员
  • 成员被初始化的顺序与他们出现在类声明中的顺序相同,与初始化器的排列顺序无关

图解队列实现原理

队列实现代码

九 总结