C++类与动态内存分配

11.10 类与动态内存分配

通常,最好是在程序运行时(而不是编译时)确定诸如使用多少内存等问题。对于在对象中存储姓名来说,通常的C++方法是,在类构造函数中使用new运算符在程序运行时分配所需的内存。为此,通常的方法是使用string类,它将为您处理内存管理细节。下面将深入讨论动态内存分配。

11.10.1 动态内存分配和类

C++在内存分配时采取的策略为,让程序在运行时决定内存分配,而不是在编译时决定。

在构造函数中使用new来为字符串分配空间,这避免了在类声明中预先定义字符串的长度。

11.10.1.1 静态数据成员

静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但如果静态成员是整型或枚举型常量,则可以在类声明中初始化(初始化是在方法文件中,在包含类方法的文件中初始化,这是因为类声明位于头文件中,可能会多次包含该头文件,导致出现多个初始化语句副本出错。)

在这里插入图片描述

11.10.1.2 new 和delete

在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存。如果使用new []来分配内存,则应使用delete []来释放内存。

11.10.2 特殊成员函数

下面这些成员函数是自动定义的

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

更准确地说,编译器将生成上述最后三个函数的定义—如果程序使用对象的方式要求这样做。

隐式地址运算符返回调用对象的地址(即this指针)。这与我们的初衷是一致的,在此不详细讨论该成员函数。

11.10.2.1 默认构造函数

如果没有提供任何构造函数,C++将创建默认构造函数。

11.10.2.1.1 自动构造函数

默认构造函数如下:

Klunk::Klunk() { } // implicit default constructor

就算自定义了构造函数,也需要定义一个默认构造函数:因为创建对象时总是会调用默认构造函数:

Klunk lunk; // invokes default constructor

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

11.10.2.1.2 在函数体初始化成员变量的构造函数

默认构造函数的特征就是没有形参,没有返回值,所以在默认构造函数内部初始化成员变量的值也是允许的:

Klunk::Klunk() // explicit default constructor
{
    klunk_ct = 0;
    ...
}
11.10.2.1.3 有默认形参的构造函数

带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。

Klunk(int n = 0) { klunk_ct = n; }
11.10.2.1.4 注意事项

前述三个只能任选其一。

11.10.2.2 复制构造函数

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

Class_name(const Class_name &);
11.10.2.2.1 什么时候使用复制构造函数

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

最常见的情况是将新对象显式地初始化为现有的对象。

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 &)

每当程序生成了对象副本时,编译器都将使用复制构造函数。

具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。

编译器生成临时对象时,也将使用复制构造函数。

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

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

默认的复制构造两数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
提示:如果类中包含这样的静态数据成员,即其值将在新对象被创建时发生变化,则应该提供一个显式复制构造西数来处理计数问題。
警告:如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函教,以复制指向的数据,而不是指针,这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘” 以复制指针引用的结构。

11.10.2.2.3 复制构造函数格式
//这是复制构造函数
StringBad::StringBad(const StringBad& st)
{
	cout << "Copy Contructor is Called!\n";
	num_strings++; // handle static member update
	len = st.len; // same length
	str = new char[len + 1]; // allot space
	strcpy_s(str,len+1, st.str); // copy string to new location
	cout << num_strings << ": \"" << str
		<< "\" object created\n"; // For Your Information
}
11.10.2.3 赋值运算符

赋值运算符的功能即何时使用它

将已有的对象赋给另一个对象时,将使用重载的赋值运算符。

StringBad knot;
knot = headline1; // assignment operator invoked

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

11.10.3 静态类成员函数

可以将成员函数声明为静态,这有两个重要的效果:

  • 首先,静态成员函数不必由对象调用;实际上,它甚至没有这个指针来操作。如果static成员函数是在public部分声明的,则可以使用类名和作用域解析操作符调用它。
  • 第二个结果是,由于静态成员函数与特定对象没有关联,因此它只能使用静态数据成员。

静态成员函数可用于设置类范围的标志,该标志控制类接口的某些方面的行为。例如,它可以控制显示类内容的方法所使用的格式。

11.10.4 String类举例—针对前述知识点

code:

  • strngbad.h
#pragma once
// strngbad.h -- flawed string class definition
#include <iostream>
using std::istream;
using std::ostream;
#ifndef STRNGBAD_H_
#define STRNGBAD_H_
class StringBad
{
private:
	char* str; // pointer to string
	int len; // length of string
	//In general, a string class doesn’t need such a member of num_strings.
	//在这里使用num_strings是为了指出本程序潜在的问题
	static int num_strings; // number of objects 
	static const int CINLIM = 80; // cin input limit
public:
	StringBad(const char* s); // constructor
	StringBad(); // default constructor
	StringBad(const StringBad& s);//复制构造函数
	~StringBad(); // destructor
	int length() const { return len; }
	StringBad& operator=(const StringBad& st);
	StringBad& operator=(const char* s);
	char& operator[](int i);
	const char& operator[](int i) const;
	friend bool operator<(const StringBad& st1, const StringBad& st2);
	friend bool operator>(const StringBad& st1, const StringBad& st2);
	friend bool operator==(const StringBad& st1, const StringBad& st2);
	friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
	friend istream& operator>>(istream& is, StringBad& st);
	static int HowMany();
};
#endif

  • strngbad.cpp
// strngbad.cpp -- StringBad class methods
#include <cstring> // string.h for some
#include "strngbad.h"
using std::cout;
using std::cin;
// initializing static class member
//Note that the initialization statement gives the type and uses the scope operator, but it doesn’t use the static keyword.
int StringBad::num_strings = 0;
// class methods
// static method
int StringBad::HowMany()
{
	return num_strings;
}
// construct StringBad from C string
StringBad::StringBad(const char* s)
{
	len = std::strlen(s); // set size
	str = new char[len + 1]; // allot storage
	strcpy_s(str, len+1, s); // initialize pointer
	num_strings++; // set object count
	cout << num_strings << ": \"" << str
		<< "\" object created\n"; // For Your Information
}
StringBad::StringBad() // default constructor
{
	num_strings++; // set object count
	len = 0;
	str = new char[1];
	str[0] = '\0'; // default string
}

//这是复制构造函数
StringBad::StringBad(const StringBad& st)
{
	cout << "Copy Contructor is Called!\n";
	num_strings++; // handle static member update
	len = st.len; // same length
	str = new char[len + 1]; // allot space
	strcpy_s(str,len+1, st.str); // copy string to new location
	cout << num_strings << ": \"" << str
		<< "\" object created\n"; // For Your Information
}

StringBad::~StringBad() // necessary destructor
{
	cout << "\"" << str << "\" object deleted, "; // FYI
	--num_strings; // required
	cout << num_strings << " left\n"; // FYI
	delete[] str; // required
}

StringBad& StringBad::operator=(const StringBad& st)
{
	cout << "Operator= is Called!\n";
	if (this == &st) // object assigned to itself
		return *this; // all done
	delete[] str; // free old string
	len = st.len;
	str = new char[len + 1]; // get space for new string
	strcpy_s(str, len + 1, st.str); // copy the string
	return *this; // return reference to invoking object
}

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

// read-write char access for non-const String
char& StringBad::operator[](int i)
{
	return str[i];
}
// read-only char access for const String
const char& StringBad::operator[](int i) const
{
	return str[i];
}

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

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

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

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

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

  • main.cpp
// vegnews.cpp -- using new and delete with classes
// compile with strngbad.cpp
#include <iostream>
#include "strngbad.h"
using std::cout;
const int ArSize = 10;
const int MaxLen = 81;
void callme1(StringBad&); // pass by reference
void callme2(StringBad); // pass by value


int main()
{
	using std::cout;
	using std::cin;
	using std::endl;

	{
		cout << "\nStarting an inner block 1.\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 = sports;
		cout << "sailor: " << sailor << endl;
		cout << "Assign one object to another:\n";
		StringBad knot;
		cout << "knot: " << knot << endl;
		knot = headline1;
		cout << "knot: " << knot << endl;
		cout << "Exiting the block 1.\n";
	}

	{
		cout << "\nStarting an inner block 2.\n";
		StringBad name;
		cout << "Hi, what's your name?\n>> ";
		cin >> name;
		cout << name << ", please enter up to " << ArSize<< " short sayings <empty line to quit>:\n";
		StringBad sayings[ArSize]; // array of objects
		char temp[MaxLen]; // temporary string storage
		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') // empty line?
				break; // i not incremented
			else
				sayings[i] = temp; // overloaded assignment
		}
		int total = i; // total # of lines read
		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 " << StringBad::HowMany()
				<< " String objects. Bye.\n";
		}
		else
			cout << "No input! Bye.\n";
		cout << "Exiting the block 2.\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";
}

运行结果:

String passed by reference:
 "Celery Stalks at Midnight"
headline1: Celery Stalks at Midnight
Copy Contructor is Called!
4: "Lettuce Prey" object created
String passed by value:
 "Lettuce Prey"
"Lettuce Prey" object deleted, 3 left
headline2: Lettuce Prey
Initialize one object to another:
Copy Contructor is Called!
4: "Spinach Leaves Bowl for Dollars" object created
sailor: Spinach Leaves Bowl for Dollars
Assign one object to another:
knot:
Operator= is Called!
knot: Celery Stalks at Midnight
Exiting the block 1.
"Celery Stalks at Midnight" object deleted, 4 left
"Spinach Leaves Bowl for Dollars" object deleted, 3 left
"Spinach Leaves Bowl for Dollars" object deleted, 2 left
"Lettuce Prey" object deleted, 1 left
"Celery Stalks at Midnight" object deleted, 0 left

Starting an inner block 2.
Hi, what's your name?
>> Jasmine
Jasmine, please enter up to 10 short sayings <empty line to quit>:
1: Boooooo
2: Jasmine
3: Boly
4: Lily
5: Petrichor
6: Bose
7: Sony
8: Ipad
9: Iphone
10: Mua~
Here are your sayings:
B: Boooooo
J: Jasmine
B: Boly
L: Lily
P: Petrichor
B: Bose
S: Sony
I: Ipad
I: Iphone
M: Mua~
Shortest saying:
Boly
First alphabetically:
Boly
This program used 11 String objects. Bye.
Exiting the block 2.
"Mua~" object deleted, 10 left
"Iphone" object deleted, 9 left
"Ipad" object deleted, 8 left
"Sony" object deleted, 7 left
"Bose" object deleted, 6 left
"Petrichor" object deleted, 5 left
"Lily" object deleted, 4 left
"Boly" object deleted, 3 left
"Jasmine" object deleted, 2 left
"Boooooo" object deleted, 1 left
"Jasmine" object deleted, 0 left
End of main()

D:\Prj\C++\Dynamic_Memory_String\Debug\Dynamic_Memory_String.exe (进程 3792)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

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

11.10.5.1 类构造函数使用new
  • Any class member that points to memory allocated by new should have the delete operator applied to it in the class destructor.This frees the allocated memory.
  • If a destructor frees memory by applying delete to a pointer that is a class member, every constructor for that class should initialize that pointer, either by using new or by setting the pointer to the null pointer.
  • Constructors should settle on using either new [] or new, but not a mixture of both.The destructor should use delete [] if the constructors use new [], and it should use delete if the constructors use new.
  • You should define a copy constructor that allocates new memory rather than copying a pointer to existing memory.This enables a program to initialize one class object to another.The constructor should normally have the following prototype:
    className(const className &)
  • You should define a class member function that overloads the assignment operator and that has a function definition.
11.10.5.2 使用new的注意事项
  • 如果使用new初始化构造函数中的指针成员,则应在析构函数中使用delete。
  • new和delete的用法应该兼容。您应该将new与delete配对,将new[]与delete[]配对。
  • 如果有多个构造两数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构两数,所有的构造两数都必须与它兼容。然而,可以在一个构造函数中使用 new 初始化指针,而在另一个构造两数中将指针初始化为空(0或 C++11 中的nullptr),这是因为 delete(无论是带中括号还是不带中括号)可以用于空指针。
11.10.5.3 NULL 或0或nullptr?

NULL、0还是nullptr:以前,空指针可以用0 或NULL(在很多头文件中,NULL是一个被定义为0的符号常量)来表示。C程序员通常使用NULL 而不是0,以指出这是一个指针,就像使用’\0’而不是0来表示空宇符,以指出这是一个宇符一样。然而,C++传统上更喜欢用简单的0,而不是等价的NULL。但正如前面指出的,C++11提供了关键宇nullptr,这是一种更好的选择

11.10.5.4 应该和不应该

构造函数:

String::String()//NOT OK
{
    str = "default string"; // oops, no new []
    len = std::strlen(str);
}
String::String(const char * s)//NOT OK
{
    len = std::strlen(s);
    str = new char; // oops, no []
    std::strcpy(str, s); // oops, no room
}
String::String(const String & st)//OK
{
    len = st.len;
    str = new char[len + 1]; // good, allocate space
    std::strcpy(str, st.str); // good, copy value
}
String::String()//OK
{
    len = 0;
    str = new char[1]; // uses new with []
    str[0] = '\0';
}
String::String()//OK
{
    len = 0;
    str = 0; // or, with C++11, str = nullptr;OK
}
String::String()//OK
{
    static const char * s = "C++"; // initialized just once
    len = std::strlen(s);
    str = new char[len + 1]; // uses new with []
    std::strcpy(str, s);
}

析构函数:

String::~String()//NOT OK
{
    delete str; // oops, should be delete [] str;
}
String::~String()//OK
{
    delete[] str; 
}

11.10.5.5 类成员的逐成员复制
class Magazine
{
private:
    String title;
    string publisher;
    ...
};

String和String都使用动态内存分配。这是否意味着您需要为Magazine类编写复制构造函数和赋值操作符?不

默认的逐成员复制和赋值行为有一定的智能。如果您将一个Magazine 对象复制或赋值给另一个 Magazine 对象,逐成员复制将使用成员类型定义的复制构造两数和赋值运算符。也就是说,复制成员 title 时,将使用 String 的复制构造两数,而将成员 title 赋给另一个 Magazine对象时,将使用 String 的赋值运算符,依此类推。然市,如果 Magazinc 类因其他成员需要定义复制构造函数和赋值运算符,情况将更复杂:在这种情况下,这些两数必须显式地调用 String 和 string 的复制构造函数和赋值运算符。

11.10.6 有关返回对象的说明

11.10.6.1 返回const对象的引用

如果函数返回(调用对象或参数对象)传递给他的对象,可以通过返回引用来提高效率。

以下两种实现都有效:

// 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,这样才匹配。
11.10.6.2 返回非const对象的引用

两种常见的返回非const对象的情形是,重载赋值运算符以及重载与cout一起使用的<<运算符。

11.10.6.2.1 重载=操作符

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

String s1("Good stuff");
String s2, s3;
s3 = s2 = s1;

在上述代码中,s2.operator=()的返回值被赋给s3。为此,返回 String 对象或 String 对象的引用都是可行的,但与 Vector 示例中一样,通过使用引用,可避免该两数调用 String 的复制构造两数来创建一个新的String 对象。在这个例子中,返回类型不是const,因为方法 operator=0返回一个指向s2 的引用,可以对其进行修收。

11.10.6.2.2 重载<<操作符

operator<<()的返回值是为了串联输出:

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

在上述代码中,operator<< (cout,s1)的返回值成为一个用于显示字符串“is coming!” 的对象。返回类型必须是 ostream &,而不能仅仅是ostream。如果使用返回类型ostream,将要求调用 ostream 类的复制构造函数,而ostream 没有公有的复制构造两数。幸运的是,返回一个指向 cout 的引用不会带来任何问题,因为 cout 已经在调用两数的作用域内。

11.10.6.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);
}


总之,如果方法或两数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或两数要返回一个没有公有复制构造两数的类(如 ostream类)的对象,它必须返回一个指向这种对象的引用。

11.10.6.4 返回const对象

正常情况下是这样使用的:

net = force1 + force2;

但是呢,程序员在编写程序的过程中可能会出现下面这样的语句,这是不应该允许的,因为可能会运行出错:

force1 + force2 = net;
cout << (force1 + force2 = net).magval() << endl;

因此:

r如果您担心这种行为可能引发的误用和滥用,有一种简单的解决方案:将返回类型声明为const Vector。

11.10.7 将指针用于对象

11.10.7.1 何时调用析构函数?
  • 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
  • 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数。
  • 如果对象是用 new 创建的,则仪当您最式使用 delete 刷除对象时,其析构两数才会被调用。
11.10.7.2 指针和对象总结

在这里插入图片描述
在这里插入图片描述

11.10.7.3 置换new

Code:

// placenew1.cpp -- new, placement new, no delete
#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";}
    void Show() const { cout << words << ", " << number << endl;}
};
int main()
{
    char * buffer = new char[BUF]; // get a block of memory
    JustTesting *pc1, *pc2;
    pc1 = new (buffer) JustTesting; // place object in buffer
    pc2 = new JustTesting("Heap1", 20); // place object on heap
    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) JustTesting("Bad Idea", 6);
    pc4 = new JustTesting("Heap2", 10);
    cout << "Memory contents:\n";
    cout << pc3 << ": ";
    pc3->Show();
    cout << pc4 << ": ";
    pc4->Show();
    delete pc2; // free Heap1
    delete pc4; // free Heap2
    delete [] buffer; // free buffer
    cout << "Done\n";
    return 0;
}

上述代码使用置换new运算符存在两个问题:

首先,在创建第二个对象时,定位 new 运算符使用一个新对象来覆盖用于第一个对象的内存单元。显然,如果类动态地为其成员分配内存,这将引发问题。
其次,将delete 用于 pc2和pc4 时,将自动调用为 pc2 和pc4 指向的对象调用析构函数:然而,将delete用于buffer 时,不会为使用定位new 运算符创建的对象调用析构两数。
程序员必须负责缓冲区内存单元分配。要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保这两个内存单元不重叠。

pc1 = new (buffer) JustTesting;
pc3 = new (buffer + sizeof (JustTesting)) JustTesting("Better Idea", 6);

其中指针pc3相对于pc1的偏移量为JustTesting对象的大小。

如果使用定位new运算符来对对象分配内存,必须确保其析构函数被调用。

delete pc2; // delete object pointed to by pc2

但是不能这样:

delete pc1; // delete object pointed to by pc1? NO!
delete pc3; // delete object pointed to by pc3? NO!

原因在于delete可与常规new运算符配合使用,但不能与定位new 运算符配合使用。例如,指针 pc3 没有收到new运算符返回的地址,因此delete pc3 将导致运行阶段错误。在另一方面,指针pc1指向的地址与buffer 相同,但 buffer 是使用new[]初始化的,因此必须使用 delete []而不是delete来释放。即使buffer 是使用new 而不是new[]初始化的,delete pc1也将释放buffer,而不是pc1。这是因为 new/delete 系统知道已分配的 512字节块buffer,但对定位 new运算符对该内存块做了何种处理一无所知。

该程序确实释放了buffer:

delete [] buffer; // free buffer

显式地为使用定位new运算符创建的对象调用析构函数。正常情况下将自动调用析构函数,但是需要显式调用析构函数的少数情形之一。显式地调用析构函数时,必须指定要销毁的对象。

pc3->~JustTesting(); // destroy object pointed to by pc3
pc1->~JustTesting(); // destroy object pointed to by pc1

对于使用定位new运算符创建的对象,应该与创建循序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。

修改后的版本:

// placenew2.cpp -- new, placement new, no delete
#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";}
    void Show() const { cout << words << ", " << number << endl;}
};
int main()
{
    char * buffer = new char[BUF]; // get a block of memory
    JustTesting *pc1, *pc2;
    pc1 = new (buffer) JustTesting; // place object in buffer
    pc2 = new JustTesting("Heap1", 20); // place object on heap
    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;
    // fix placement new location
    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; // free Heap1
    delete pc4; // free Heap2
    // explicitly destroy placement new objects
    pc3->~JustTesting(); // destroy object pointed to by pc3
    pc1->~JustTesting(); // destroy object pointed to by pc1
    delete [] buffer; // free buffer
    cout << "Done\n";
    return 0;
}

11.10.8 嵌套类型

11.10.8.1 简介

C++允许将结构体、类和枚举定义在类声明中。

class Queue
{
private:
    // class scope definitions
    // Node is a nested structure definition local to this class
    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
    ...
public:
//...
};

在类声明中声明的结构、类或枚举被称为是被嵌套在类中,共作用城为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明是在类的私有部分进行的,则只能在这个类使用被声明的类型;如果声明是在公有部分进行的,则可以从类的外部通过作用城解析运算符使用被声明的类型。例如,如果Node是在Queue 类的公有部分声明的,则可以在类的外面声明Qucue:Node 类型的变量。

11.10.8.2 嵌套类

模板嵌套类:见模板template笔记。

11.10.8.2.1 嵌套类是什么?

嵌套类就是声明外部类的类(此处称声明嵌套类的类为外部类),它解决了不同名称空间的名称冲突的问题;
成员函数可以创建或使用嵌套类对象;在外部只能使用声明在public部分的嵌套类并且使用::操作符。

11.10.8.2.2 嵌套类与容器

嵌套类只是在外部类中声明,并不实例化;
而容器是在外部类中声明并且实例化一个对象作为外部类的成员属性。

11.10.8.2.3 嵌套类的作用域

声明在外部类public部分:外部类可以使用,外部类继承类可以使用,其他类可以使用(使用::操作符)
声明在外部类protected部分:外部类可以使用,外部类继承类可以使用,其他类不可以使用
声明在外部类private部分:外部类可以使用,外部类继承类不可以使用,其他类不可以使用

11.10.8.2.4 嵌套类的访问控制

嵌套类的public成员:外部类能访问
嵌套类的protected成员:外部类不能访问(感觉不会经常使用)
嵌套类的private成员:外部类不能访问

11.10.8.2.5 举例

queue.h

#pragma once
#include <string>
#ifndef QUEUE_H_
#define QUEUE_H_

using std::string;
typedef string Item;
class Queue
{
private:
	// class scope definitions
	// Node is a nested structure definition local to this class
	class Node
	{
	public:
		Item item;
		Node* next;
		Node(const Item& i) : item(i), next(0) { }
	};
	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

#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(item); // create, initialize node
	// on failure, new throws std::bad_alloc exception
	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;
}

main.cpp

/*
Project name :			_16Nested_class
Last modified Date:		2022年3月28日09点39分
Last Version:			V1.0
Descriptions:			嵌套类
*/
#include<iostream>
#include"queuetp.h"
#include"queue.h"
#include<string>

int main()
{
	using std::string;
	using std::cin;
	using std::cout;
	/*
	关于嵌套类和队列的仿真
	相关的文件:queue.h  and  queue.cpp
	*/
	Queue cs1(2);
	string temp;
	while (!cs1.isfull())
	{
		cout << "Please enter your name. You will be "
			"served in the order of arrival.\n"
			"name: ";
		getline(cin, temp);
		cs1.enqueue(temp);
	}
	cout << "The queue is full. Processing begins!\n";
	while (!cs1.isempty())
	{
		cs1.dequeue(temp);
		cout << "Now processing " << temp << "...\n";
	}
	return 0;
}

运行结果:

Please enter your name. You will be served in the order of arrival.
name: Jasmine
Please enter your name. You will be served in the order of arrival.
name: Lily
The queue is full. Processing begins!
Now processing Jasmine...
Now processing Lily...

D:\Prj\_C++Self\_16Nested_class\Debug\_16Nested_class.exe (进程 12300)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

11.10.9 成员初始化列表的语法

如果Classy是一个类,而mem1、mem2和mem3都是这个类的数据成员,则类构造函数可以使用如下语法来初始化数据成员:

Classy::Classy(int n, int m) :mem1(n), mem2(0), mem3(n*m + 2)
{
//...
}


上述代码将mem1初始化为n,将mem2 初始化为0,将mem3 初始化为n*m +2。从概念上说,这些初始化工作是在对象创建时完成的,此时还未执行括号中的任何代码。请注意以下几点:

  • 这种格式只能用于构造函数;

  • 必须用这种格式来初始化非静态const 数据成员(至少在C++11之前是这样的);

  • 必须用这种格式来初始化引用数据成员。

    数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。
    成员初始化列表使用的括号方式也可用于常规初始化。

int games = 162;
double talk = 2.71828; 
with
int games(162);
double talk(2.71828);

11.10.10 C++11类内初始化

初始化在类声明内部:

class Queue
{
private:
    ...
    Node * front = NULL;
    enum {Q_SIZE = 10};
    Node * rear = NULL;
    int items = 0;
    const int qsize = Q_SIZE;
    ...
};

11.10.11 虚拟私有方法

对于一些现在不需要使用的成员函数,但是未来可能需要,可以定义为虚拟私有方法。

class Queue
{
    private:
        Queue(const Queue & q) : qsize(0) { } // preemptive definition
        Queue & operator=(const Queue & q) { return *this;}
        //...
};

这样做有两个作用:第一,它避免了本来将自动生成的默认方法定义。第二,因为这些方法是私有的,所以不能被广泛使用。也就是说,如果nip和tuck是Queue对象,则编译器就不允许这样做:

Queue snick(nip); // not allowed
tuck = nip; // not allowed

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jasmine-Lily

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值