C++学习 第十二章

本文深入探讨了C++中的静态类成员,强调它们在内存管理和对象共享中的特性。此外,文章还详细介绍了C++中的特殊成员函数,如默认构造函数、复制构造函数和赋值运算符,以及它们在对象初始化、复制和赋值过程中的作用。作者通过实例分析了默认复制构造函数可能导致的问题,并提出了深复制的解决方案。同时,文章还提到了析构函数的调用时机以及new和delete的使用注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.复习new和delete以及学习静态类成员

// stringbad.h 表示一个正确完成了显而易见的工作,但是有一些有益的功能被省略了的类
#include<iostream>
#ifndef STRINGBAD_H_
#define STRINGBAD_H_
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

num_strings是静态存储类,这种静态存储类的特点是:无论创建了多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象都共享同一个静态类成员。本程序中的num_strings表示的是所创建的对象数目。

// stringbad.cpp 类方法实现
#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[4];
	std::strcpy(str,"C++");
	num_strings++;
	cout << num_strings << ":\"" << str << "\" object created\n"; 
}
StringBad::~StringBad()
{
	cout << "\"" << str << "\" object deleted, ";
	--num_strings;
	cout << num_strings << " left\n";
	delete [] str;  
}
friend std::ostream & operator<<(std::ostream & os,const StringBad & st)
{
	os << st.str;
	return os;
}

int StringBad::num_strings = 0;
这条语句将静态成员num_strings的值初始化为零,注意,不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。初始化之所以在方法文件中进行,而不再头文件中进行,具体原因是因为如果在头文件中进行,可能会产生很多初始化副本,导致程序运行错误。
注意:const整数类型的和枚举型的静态数据成员可以在类中声明。
注意:字符串并不保存在对象中,而是单独保存在堆内存里,对象仅保存了指出到哪里去查找字符串的信息。

// vegnews.cpp
#include<iostream>
using std::cout;
#include"stringbad.h"

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 = 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";
}

注意:这个程序的输出结果是糟糕的,首先是调用析构函数之后,最后的剩余字符串数目结果不正确,其次是字符串的内容也遭到了破坏。字符串数目不正确的原因是系统默认提供了一个构造函数----复制构造函数,这也是*StringBad sailor = sports;*这条语句能够使用的原因,系统提供一个构造函数,格式为:*StringBad(const StringBad &);*当我们使用一个对象初始化另一个对象的时候就会调用这个默认构造函数,而在整个默认构造函数中,会导致new_strings不自增。字符串内容不正确的原因是,在按值传递内容的时候,系统会调用析构函数。

2.特殊成员函数
C++中会自动提供下列成员函数:

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

注意:更加准确的说法是,编译器将生产上述最后三个函数的定义,如果程序使用对象的方式要求这样做。也就是如果您需要用一个对象去初始化另一个对象,则系统会提供复制构造函数。如果您要将一个对象的值赋值给另一个对象,系统会默认提供赋值运算符的定义。

  • 默认构造函数
    如果没有提供任何构造函数,C++将创建默认构造函数。例如,假如定义了一个Klunk类,但没有提供任何构造函数,则编译器将提供下述默认构造函数:
    Klunk::Klunk();{ }
    也就是说,编译器会提供一个不接受任何参数,也不进行任何操作的构造函数,该函数会在创建对象的时候调用。
    如果显式的定义了构造函数,默认构造函数将不会被定义和使用,如果想要使用这种没有参数的构造函数,需要自行定义。
    注意:默认构造函数使用的时候应该避免二义性,比如使用带有一个默认参数的构造函数和没有参数的构造函数同时使用,会造成问题。
  • 复制构造函数
    复制构造函数用于将一个对象赋值到一个新对象中,也就是说,它用于初始化的过程中,而非常规的赋值过程中(因为它是一个构造函数)。默认原型如下:
    Klunk::Klunk(const Klunk &);
    它接受一个指向类对象的常量引用作为参数。在新建一个对象并将其初始化为同类现有对象的时候,复制构造函数将被调用,最常见的情况是将一个新定义的对象显式的初始化为现有对象。例如下面的例子都将调用复制构造函数:(假设motto是一个StringBad类的对象)
    StringBad ditto(motto);
    StringBad metoo = motto;
    StringBad also = StringBad(motto);
    *StringBad p = new StringBad(motto);
    其中中间的两种声明方式可能使用复制构造函数直接创建metoo和also,也可能使用复制构造函数生成一个临时对象,并且将临时对象的值赋值给metoo和also。最后一种声明方式是使用motto初始化一个匿名对象,并且将新对象的地址赋值给p指针。
    注意:每当程序生成了对象副本的时候,编译器都会调用复制构造函数,也就是说,当函数按值传递对象或者函数返回对象时,都会使用复制构造函数。因为按值传递意味着要创建一个原对象的副本。编译器生成临时对象的时候也会使用复制构造函数,例如:(假设t是vector类型变量)
    t = t1 + t2 + t3;
    在这种情况下,可能会使用复制构造函数创建一个vector类型的中间变量存储中间结果。
    默认的复制构造函数逐个复制非静态成员,复制的是成员的值,也就是浅复制。当然如果成员本身是指针,存储的内容是指向的地址,则复制构造函数复制的也是地址。

3.StringBad类的问题出在哪里
两个异常之处

  • 程序输出表明,析构函数调用的次数要比构造函数调用的次数多了两次,原因可能是在使用callme2进行值传递的时候,以及对sailor进行赋值的时候调用了两次默认的复制构造函数,创建了两个对象,因为复制构造函数我们并没有显式定义,所以执行的是默认的操作,解决的方法是提供一个能够更新num_strings的复制构造函数。
StringBad::StringBad(const String & s)
{
	num_strings++;
	...//其他内容
}
  • 第二个异常之处则更加的危险,症状是字符串的内容乱码,原因在于隐式复制构造函数是按值进行复制的,复制的内容并不是字符串,而是指针,内容是指向字符串的地址。也就是说,将sailor初始化为sport后,得到的是两个指向同一个字符串的指针。当operator<<()函数使用指针来显示字符串时,这并不会产生问题。但是当析构函数调用时,这将引发问题,析构函数~StringBad释放str指针指向的内存,因此释放sailor的效果如下:
    delete [] sailor.str; //delete the string that ditto.str points to
    sailor.str指针指向"Spinach Leaves Bowl for Dollars",因为他被赋值为sports.str。而释放sports的时候,则会出现如下效果:
    delete [] sports.str; // effect is undefined
    因为sports.str的内容已经被sailor的析构函数释放,再次释放将导致不确定,可能有害的后果。试图释放内存两次也可能会导致程序异常终止。
    问题是出在复制构造函数上,所以解决问题也要从复制构造函数下手,我们需要显式定义一个复制构造函数:
//能够解决问题的复制构造函数
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"; 	
}

注意:这种复制方法称为深复制,也就是说它是新创建了一个字符串,而非与原对象使用同一个地址指向的字符串,新对象和原对象所指向的字符串内容相同,地址不相同,也就是修改原字符串并不会对新字符串产生影响,反之亦然。

4.StringBad的其他问题:赋值运算符
C++所允许的类对象赋值,这是通过自动为类重载赋值运算符来实现的,这种运算符的原型如下:
StringBad & StringBad::operator=(const StringBad &);
将已有的对象赋值给另一个对象,将使用重载的赋值运算符:

//使用重载的赋值运算符
StringBad headline1("Celery Stalks at Midnight");
StringBad knot;
knot = headline1;

与复制构造函数一样,赋值运算符的隐式实现也是对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
赋值出现的问题:与隐式复制构造函数相同,出现的问题都是会导致内存被释放两次,原因也是一样的,赋值运算符也属于浅复制,原对象和新对象共用同一个地址所指向的内容,所以当knot被释放之后,headline1所指向的内容就已经被销毁,再次释放headline1的时候就会出错。
解决方法如下:

  • 由于目标对象可能引用了以前分配的数据,所以函数应该使用delete[]来释放这些数据
  • 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
  • 函数返回一个指向调用对象的引用
StringBad & StringBad::operator=(const StringBad & st)
{
	if(this == &st)
		return *this;
	delete [] str;
	len = std::strlen(st);
	str = new char[len+1];
	std::strcpy(str,st.str);
	return *this;
}

注意:程序首先检查自我复制,这是通过查看两个内容的地址是否相同,如果相同则返回*this直接结束。如果不是则直接进行深复制,将原字符串的内容复制给新对象的字符串。因为新对象是一已经创建好的对象,所以num_strings并不需要自增。
注意:赋值运算符,函数调用运算符,下标运算符,通过指针访问类成员的运算符只能通过类的成员函数进行重载。

5.改进后的新String类(demo)
新String类包含以下的功能:

  • int length() const { return len;} 计算字符串中的字符个数
  • friend bool operator<(const String &st1, const String &st2); 比较字符串函数
  • friend bool operator>(const String &st1, const String &st2); 比较字符串函数
  • friend bool operator==(const String &st1, const String &st2); 比较字符串函数
  • friend bool operator>>(istream & os, String &st); 重载运算符输入
  • char & operator[](int i); 重载运算符下标使用
  • const char & operator[](int i) const; 重载运算符下标使用
  • static int HowMany(); 补充静态类数据成员

①修正后的默认构造函数

String::String()
{
	len = 0;
	//之所以写成char[1]而不写成char的原因是
	//为了使之能够与delete []兼容
	str = new char[1];
	str[0] = '\0';
	//在C++11中推荐如下表达方式
	//str = nullptr;
}

②比较成员函数

//比较函数逻辑:
//按照字母排序,第一个字符串在第二个字符串之前,则operator<()返回true  其他返回false
//按照字母排序,第一个字符串在第二个字符串之后,则operator>()返回true  其他返回false
//按照字母排序,第一个字符串在第二个字符串相同,则operator==()返回true 其他返回false
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);
}

③使用中括号表示法访问字符

//char & String::operator[](int i);
//const char & String::operator[](int i) const;
//构成重载 原因是成员函数所省略的参数,第二条语句中声明了const,所以两个函数特征标不相同
//为啥要使用重载:
//如果定义了一个const String answer("Yes"); 当只有第一个函数原型的时候
//cout << answer[1]; 这条语句将会报错,因为系统并不确定函数使是否会对常量内容进行修改
//所以需要重载一个const类型的函数,以保证内容并不会被修改
//具体实现方法如下:
char & String::operator[](int i)
{
	return str[i];
}
const char & String::operator[](int i) const
{
	return str[i];
}
//第二种方法因为返回的引用类型是常量引用,而且声明了参数类型为const
//所以不会对原内容进行修改
//系统也会区别常量函数和非常量函数的特征标,保证常规String对象可以被读写 而常量对象为只读状态

④静态类成员函数
可以将成员函数声明为静态的(函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static)。使用这种方式有两个重要的后果:

  • 不能通过对象调用静态成员函数,实际上,静态成员函数甚至不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用。
  • 由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。也就是说静态方法可以访问num_strings但是并不能访问str和len。

⑤进一步重载赋值运算符
如果使用原来的重载赋值运算符,那么要实现下面的赋值语句需要执行下述步骤:

String name;
char temp[40];
cin.getline(temp,40);
name = temp;
//①首先是需要将temp变量通过构造函数转化为String类型,也就是创建一个临时的String类型变量
//前面介绍过,这有一个参数的构造函数可以作为转换函数
//②将临时的String类型变量的信息复制到name对象之中
//③使用析构函数删除中间变量
//可以看出效率十分的低,所以我们可以对赋值运算符进行重载,让其兼容char *类型
String & String::operator=(const char *s)
{
	delete [] str;
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str,s);
	return *this;
}

6.String类的完整内容

//String1.h
#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;
public:
	String(const char *s);
	String();
	String(const String &);
	~String();
	int length() const {return len;}
	String & operator=(cosnt String &);
	String & operator=(const char *);
	char & operator[](int i);
	const char & operator[](int i) const;
	friend bool operator<(const String &st1, const String &st2);
	friend bool operator>(const String &st1, const String &st2);
	friend bool operator==(const String &st1, const String &st2);
	friend istream & operator>>(istream & is, String &st);
	friend ostream & operator<<(ostream & os, const String &st);
	static int HowMany();
};
#endif
//string1.cpp
#include <cstring>
#include "string1.h"
uisng std::cin;
using std::cout;
//初始化静态类成员
int StringBad::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 = nullptr;
	num_strings++;
}
String::String(const String &s)
{
	len = s.len;
	str = new char[len+1];
	std::strcpy(str,s.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 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;
}
istream & operator>>(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;
}

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

  • 如果在构造函数中使用new来初始化指针成员,则应该在析构函数中使用delete。
  • new和delete必须兼容 new要对应delete new[] 要对应delete[]
  • 如果有多个构造函数,必须以相同的方式使用new,要么都带中括号没要么都不带。因为只有一个析构函数,所有的构造函数都必须与他进行兼容。然而可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空,因为delete无论有无中括号都有可以用于空指针。
  • 应该定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。同时要注意更新静态类成员,同时注意应该复制内容而非只是复制地址。
  • 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。需要注意的是:要检查自我赋值的情况,释放成员指针原来指向的内存。
//需要注意的几种错误
//第一种:没有使用new来初始化str
//如果出现这种情况会导致析构函数调用时使用delete的时候可能出现不确定甚至有害的结果
String::String()
{
	str = "wuhu";
	len = std::strlen(str);
}
//第二种:没有规定好申请内存的大小,使用的是new char 只是获得一个字符的内存
//这种情况会导致内存问题
String::String(const char *s)
{
	len = std::strlen(s);
	str = new char;
	std::strcpy(str,s);
	
}
//正确的方式
String::String(const String &s)
{
	len = s.len;
	str = new char[len+1];
	std::strcpy(str,s.str);
}

8.包含类成员的类的逐成员复制
默认的逐成员复制和赋值行为有一定的智能,他会使用成员所属的类的复制构造函数和赋值运算符,所以如果类中只是包含标准类,则不必再编写复制构造函数和赋值运算符。

9.有关返回对象的说明
当成员函数或独立函数返回对象的时候,有三种返回方式

  • 返回指向对象的引用
  • 返回指向对象的const引用
  • 返回const对象

①返回指向const对象的引用
使用const引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制。如果函数返回传递给他的对象,可以通过返回引用提高效率,例如:

Vector force1(70,90);
Vector force2(80,60);
Vector max;
max = Max(force1,force2);
Vector Max(const Vector & f1, const Vector & f2)
{
	if(f1.magval() > f2.magval())
		return f1;
	else
		return f2;
}
const Vector & Max(const Vector & f1, const Vector & f2)
{
	if(f1.magval() > f2.magval())
		return f1;
	else
		return f2;
}

注意:首先,返回对象的时候需要使用复制构造函数,而返回引用的时候不需要使用复制构造函数,所以第二个版本的工作更少,效率更高。其次,引用指向的对象应该在调用函数执行时存在,在这个例子中,引用指向force1或force2,他们都是在调用函数中定义的,因此满足这种条件。最后,f1,f2都被声明称const类型,所以返回的引用类型也应该是const,这样才匹配。

②返回指向非const对象的引用
两种常见的返回非const对象的情形是,重载赋值运算符以及重载与cont一起使用的<<运算符。前者是为了提高效率,后者是必须这么做。
cont一起使用的<<运算符必须使用非const对象的引用的原因是:如果不使用引用而是直接返回一个ostream类的对象的话,需要调用复制构造函数,但是ostream类并没有公有的复制构造函数供我们调用,幸运的是,返回一个指向cout的引用不会带来任何问题,因为cout已经在调用函数的作用域内。

③返回对象
如果返回的对象是被调用函数中的局部变量,则不应该按引用方式返回他,因为在被调用函数执行完毕时,局部对象将调用其析构函数。因此,当控制权回到调用函数时,引用指向的对象将不再存在,所以在这种情况下应该返回对象而非引用。

④返回const对象
因为复制构造函数会创建一个临时对象来标识返回值,而如果不对这个临时对象加以限定,则这个对象完全可以进行一些无意义的操作,例如:
f1 + f2 = net
(f1和f2的加法是重载加法运算符完成的,函数原型为Vector operator+(Vector ,Vector ) )
而要杜绝这种不规范的代码产生,可以通过将返回值类型加以限定,这样就不允许对返回值进行改动,也就避免了上述那种无意义的代码。

⑤总结
如果方法或函数要返回局部对象,则应该返回对象,而非指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类的对象,他必须返回一个指向对象的引用。最后,有些方法或函数可以返回对象,也可以返回对象的引用,在这种情况下首选引用返回方式,因为效率更高。

10.new和delete
析构函数被调用的时机

  • 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
  • 如果对象是静态变量,则当程序结束时调用对象的析构函数。
  • 如果对象是new创建的,则仅当您显式使用delete删除对象时,析构函数才被调用。

11.指针和对象小结

  • 使用常规表示法来声明指向对象的指针
String * glamour;
  • 可以将指针初始化为指向已有的对象
String * first = &saying[0];
  • 可以使用new来初始化指针,这将创建一个新的对象
String * favorite = new String(sayings[choice]);
  • 对类使用new将调用相应类的构造函数来初始化新创建的对象
String * gleep = new String;
  • 可以使用->运算符通过指针访问类方法
if (sayings[i].length() < shortest -> length()) {...}
  • 可以对对象指针应用解除引用运算符来获得对象
if(sayings[i] < *first) {...}

12.再谈new定位运算符

#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()
{
	//使用new 创建一个512字节的内存缓冲区
	char * buffer = new char[BUF];
	JustTesting *pc1,*pc2;
	//使用new在堆中创建一个JustTesting对象 
	//试图使用定位new运算符在缓冲区内创建一个JustTesting对象
	pc1 = new (buffer) JustTesting;
	pc2 = new JustTesting("Heap1",20);
	cout << "Memory block address:\n" << "buffer:" << (void *) buffer << "  heap:" << pc2 << endl;
	cout << "Memory contents:\n";
	cout << pc1 << ": ";
	pc1-> show();
	cout << pc2 << ": ";
	pc2-> show();
	JustTesting *pc3,*pc4;
	//使用new在堆中创建一个JustTesting对象 
	//试图使用定位new运算符在缓冲区内创建一个JustTesting对象
	pc3 = new (buffer) JustTesting("Bad Idea",6);
	pc4 = new JustTesting("Heap2",20);
	cout << "Memory contents:\n";
	cout << pc3 << ": ";
	pc3-> show();
	cout << pc4 << ": ";
	pc4-> show();
	delete pc2;
	delete pc4;
	delete [] buffer;
	cout << "Done\n";
	return 0;
}

本程序使用定位new运算符的两个问题

  • 第一:使用第二个定位new运算符创建对象的时候,定位new运算符使用一个新的对象来覆盖用于存储第一个对象的内存单元。
  • 第二:将delete用于pc2和pc4的时候,将自动调用为pc2和pc4指向的对象调用析构函数,然而,delete[]用于buffer时,不会为使用定位new运算符创建的对象调用析构函数。

从问题得到的两个教训

  • 第一:程序员必须负责定位new运算符使用的缓冲区内存单元不重叠,也就是需要提供两个位于缓冲区的不同地址。
  • 第二:delete只能和常规new运算符一起使用,delete只能释放使用常规new运算符申请的内存单元,而无法释放定位运算符new对于这块内存单元的处理,解决的方法是显式调用析构函数来释放内存。

修改正确的程序

#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()
{
	//使用new 创建一个512字节的内存缓冲区
	char * buffer = new char[BUF];
	JustTesting *pc1,*pc2;
	//使用new在堆中创建一个JustTesting对象 
	//试图使用定位new运算符在缓冲区内创建一个JustTesting对象
	pc1 = new (buffer) JustTesting;
	pc2 = new JustTesting("Heap1",20);
	cout << "Memory block address:\n" << "buffer:" << (void *) buffer << "  heap:" << pc2 << endl;
	cout << "Memory contents:\n";
	cout << pc1 << ": ";
	pc1-> show();
	cout << pc2 << ": ";
	pc2-> show();
	JustTesting *pc3,*pc4;
	//使用new在堆中创建一个JustTesting对象 
	//试图使用定位new运算符在缓冲区内创建一个JustTesting对象
	pc3 = new (buffer+sizeof(JustTesting)) JustTesting("Bad Idea",6);
	pc4 = new JustTesting("Heap2",20);
	cout << "Memory contents:\n";
	cout << pc3 << ": ";
	pc3-> show();
	cout << pc4 << ": ";
	pc4-> show();
	delete pc2;
	delete pc4;
	pc3->~JustTesting();
	pc4->~JustTesting();
	delete [] buffer;
	cout << "Done\n";
	return 0;
}

13.复习各种技术
①重载<<运算符
c_name为类名,如果该类提供了能够返回所需内容的公有方法,则可在运算符函数中使用这些方法,就不需要将他们设置成友元函数了。

ostream & operator<<(ostream & os, const c_name & obj)
{
	os << ...;
	return os;
}

②转换函数
要将单个值转化为类类型,需要创建原型如下的构造函数:
c_name(tyoeName value);
要将类类型转化为其他类型,需要创建原型如下的转换函数:
operator typeName();
注意:该函数没有声明返回类型,但是应该返回所需类型的值
转换的时候需要小心:为了防止隐式转化,可以使用explicit关键字

③其构造函数使用new的类

  • 如果在构造函数中使用new来初始化指针成员,则应该在析构函数中使用delete。
  • new和delete必须兼容 new要对应delete new[] 要对应delete[]
  • 如果有多个构造函数,必须以相同的方式使用new,要么都带中括号没要么都不带。因为只有一个析构函数,所有的构造函数都必须与他进行兼容。然而可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空,因为delete无论有无中括号都有可以用于空指针。
  • 应该定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。同时要注意更新静态类成员,同时注意应该复制内容而非只是复制地址。
  • 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。需要注意的是:要检查自我赋值的情况,释放成员指针原来指向的内存。

14.ADT模拟(队列)
①队列类的编写

class ADT
{
	enum {Q_SIZE = 10};
private:
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);
};

②队列类的实现

//队列的基本元素---节点
//节点由结构体表示 所包含的内容有两个部分
//一个是它本身存储的值 另一部分是下一个节点的指针
struct Node
{
	Item item;
	struct Node * next;
};
//队列的补充
class ADT
{
private:
	struct Node
	{
		Item item;
		struct Node * next;
	};
	enum {Q_SIZE = 10};
	Node * front;
	Node * rear;
	int items;
	const int qsize;
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);
};
//由于qsize是常量,只能对其进行初始化,并不能对其赋值,如果进行下述的这种构造方法的话
//首先会给四个对象分配内存,然后按照赋值的方式将值存储在内存之中
//所以如果使用下述的这种构造函数,会导致qsize没有办法进行初始化,造成错误
Queue::Queue(int qs)
{
	front = rear = nullptr;
	items = 0;
	//qsize没办法初始化 造成错误
	qsize = qs;
}
//为了解决这个问题 C++提供了一种特殊的语法
//成员初始化列表
Classy::Classy(int n, int m):men1(n),men2(0),men3(n*m+2)
{
...
}
//由逗号分割的初始化列表组成(前面带冒号)
//注意:只有构造函数可以使用这种初始化列表语法
//对于const类成员一定要使用这种语法
//另外,对于被声明为引用的成员也必须使用这种语法
Queue::Queue(int qs) : qsize(qs)
{
	front = rear = nullptr;
	items = 0;
}
//被声明为引用的类成员使用方式
class Agency {...};
class Agent
{
private:
	Agency & belong;
	...
};
Agent::Agent(Agency & a) : belong(a)
{
...
}

注意:只有构造函数可以使用初始化列表语法,对于const类对象和被声明为引用的类成员必须使用这种方法来进行初始化。原因是被声明为引用的类成员和const类对象一样都只能被创建的时候初始化。

//C++11的类内初始化
class Classy
{
	int men1 = 10;
	const int men2 = 20;
};
//这等价于
Classy::Classy(int n, int m):men1(10),men2(20)
{
...
}
//除非调用了使用成员初始化列表的构造函数
Classy::Classy(int n, int m):men1(20)
{
...
}
//如果使用了上述构造函数,则men1的值将会被设置为20
//men2的值不变还是20

③入队函数和出队函数

//入队逻辑
//1.检查队列是否已满 满了就结束
//2.创建一个新节点
//3.在节点中放入正确的值,并将节点的next指向nullptr
//4.将项目计数加一
//5.将节点附加到队尾,首先将先队尾的next指向新添加元素,然后将队尾指针指向新元素
bool Queue::enqueue(const Item & item)
{
	if(isfull())
		return false;
	Node * add = new Node;
	add->item = item;
	add->next = nullptr;
	items++;
	if(front == nullptr)
		front = add;
	else
		rear -> next = add;
	rear = add;
	return true;
}
//出队逻辑
//1.检查队列是否为空,为空结束
//2.将队列的第一个项目提供给调用函数
//3.将项目数减一
//4.保存front节点的位置,供以后删除
//5.让节点出队,也就是将Queue成员指针front指向下一个节点front.next
//6.删除以前第一个节点
//7.如果链表为空了,则设置rear为nullptr
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;
}

④析构函数

//析构函数
//为什么需要析构函数?
//因为在入队的时候使用new新建了节点,虽然说在出队函数中会进行delete
//不过不能保证在程序结束的时候,是否会对队列所有节点进行出队
//所以要创建一个显式的析构函数,确保所有节点都出队
Queue::~Queue()
{
	Node * temp;
	while (front != nullptr)
	{
		temp = front;
		front = front->next;
		delete temp;
	}
}

⑤复制构造函数和赋值运算符
这里并不需要对队列进行复制,赋值,或者是作为按值引用,亦或者是连续相加出现中间对象,所以现在并不需要进行一下操作,但是如果不对复制构造函数和赋值运算符进行规定的话,很可能因为误操作对队列进行毁灭性的打击,因为默认的赋值运算符和复制构造函数都是浅复制,也就是新的队列和原队列都是同一个队列,新队列只是原队列的别名,而且新队列添加元素的时候,原队列的最后结尾指针并不会改变,这回造成很不好的效果,所以我们可以使用一种小技巧,将复制构造函数和赋值运算符设置为私有方法,禁止类外使用。C++11中有新的方法解决,delete关键字,后面我们会介绍。

//伪私有方法
class Queue
{
private:
	Queue(const Queue & q) :qsize(0) {}
	Queue & operator=(cosnt Queue & q) {return * this}
}
  • 它避免了本来将自动生成的默认方法定义
  • 因为这些方法是私有的,所以不能被广泛使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值