C++(学习笔记)类和动态内存分配

文章目录

  • 前言
  • 一、动态内存和类
    • 1.静态类成员
    • 2.复制构造函数
    • 3.赋值运算符
  • 二、自己编写String类
    • 1.比较成员函数
    • 2.使用中括号表示法访问字符串
    • 3.静态成员函数
    • 4.重载赋值运算符
  • 三、再谈定位new运算符
  • 总结


前言

  本人在阅读C++ primer plus(第六版)的过程中做的笔记,写这篇文章既是为了分享,也是为了以后查阅。以下是本篇文章正式内容。


一、动态内存和类

1.静态类成员

class StringBad
{
private:
	……
	static int num_strings;
	……
};

  将num_strings类成员声明为静态存储类,它的特点是不管创建了多少类对象,都只创建一个静态类变量,也就是说,所有的类对象共享一个静态成员。这对于全部对象具有相等私有值是非常方便的。下面这条语句是静态成员的初始化语句:
  int StringBad::num_strings = 0;
  静态成员的初始化应该在该类的实现文件中执行,因为类声明文件只是声明了如何分配内存,并不分配内存,而且类声明文件通常包含在其他几个文件中,在类声明文件中进行初始化将出现多个初始化语句而引发错误;还有就是静态类成员是单独存储的,而不是对象的组成部分。初始化时使用作用域解析运算符指出静态成员所属的类。如果静态成员是整型或枚举型const,则可以在声明中初始化(见对象和类一章中作用域为类的常量)。

class StringBad
{
private:
	char *str;
	int len;
	static int num_strings;
	……
};

  构造函数:

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

  字符串并不保存在对象中,而是单独保存在堆内存中,对象只保存了字符串的地址。
  析构函数:

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

  当对象过期时,对象占用的内存会自动释放,但是对象成员str指向的内存是由new分配的,这部分内存并不会自动释放,需要使用delete释放。

2.复制构造函数

  复制构造函数用于将一个对象复制到一个新创建的对象中,也就是说复制构造函数用于对象的初始化过程中,而不是常规的赋值过程,复制构造函数原型如下:
  class_name(const class_name &);
  例如,StringBad类的复制构造函数原型如下:
  StringBad(const StringBad &);
  复制构造函数在新建一个对象并将其初始化为同类现有对象时被调用。假设motto是StringBad类的一个现有对象,下面4种声明都将调用复制构造函数:
  StringBad ditto(motto);
  StringBad metoo = motto;
  StringBad also = StringBad(motto);
  StringBad *pStringBad = new StringBad(motto);
  编译器生成临时对象时也将调用复制构造函数。
  复制构造函数逐个复制非静态成员的值,静态成员不受影响,因为静态成员属于整个类而不属于某一个具体的对象。
  如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,复制指向的数据,而不是指针。

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

3.赋值运算符

  为类重载赋值运算符,原型如下:
  class_name & class_name::operator=(const class_name &);
  StringBad类赋值运算符原型如下:
  StringBad & StringBad::operator=(const StringBad &);
  将已有的对象赋给另一个对象时将使用赋值运算符。返回类型是指向类对象的引用,这样可以连续赋值,比如S0、S1、S2都是StringBad类的对象,则可以S0 = S1 = S2。下面是重载赋值运算符的函数定义:

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类

1.比较成员函数

  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);
  要实现比较字符串函数,最简单的方法是使用trcmp()函数,依照字母顺序,第一个参数位于第二个参数之前,返回一个负值;两个字符串相同,返回0;第一个参数位于第二个参数之后,返回一个正值。

bool operator<(const String &st1, const String &st2)
{
	if (std::trcmp(st1.str, st2.str) < 0)
		return true;
	else
		return false;
}

  代码可以简化为:

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

  另外两个比较函数:

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

  将比较函数作为友元函数,有助于将String对象与常规C字符串比较。假设object是String对象,则
  if (“love” == object)
  将被转换为
  if (operator==(“love”, object))
  然后编译器将使用某个构造函数将代码转换为
  if (operator==(String(“love”), object))

2.使用中括号表示法访问字符串

  char & operator[](int i);
  假设object是String的一个对象,则object[4]将被转换为object.operator[](4),下面是该方法的实现:

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

  将返回类型声明为char &便于给特定元素赋值。
  假设有下面的常量对象:
  const String answer(“futile”);
  则cout << answer[1]将出错,原因是不能保证该语句不修改数据,解决办法是提供一个const String版本的operator[]方法:

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

3.静态成员函数

  static int HowMany();
  可以将成员函数声明为静态的,函数声明必须包含关键字static。
  首先,不能通过对象调用静态成员函数,静态成员函数也不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它;静态成员函数不与特定的对象关联,只能访问静态数据成员,例如num_strings。
  static int HowMany() {return num_strings;}
  调用它的方式:
  int count = String::HowMany();
  如果静态成员函数在实现文件中定义,则定义时要去掉关键字static。

4.重载赋值运算符

  重载赋值运算符可以将常规字符串复制到String对象中。

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

三、再谈定位new运算符

const int BUF = 512;
class JustTesting
{
private:
	string words;
	int number;
public:
	JustTesting(const JustTesting &s = “Just Testing”, int n = 0)
	{words = s; number = n;}
	~JustTesting() {}
};
int main()
{
	char *buffer = new char[BUF];
		
	JustTesting *pc1, *pc2;
	pc1 = new (buffer) JustTesting;
	pc2 = new JustTesting(“Heap1”, 20);
		
	JustTesting *pc3, *pc4;
	//与pc1的内存单元分隔开并且保证互不重叠
	pc3 = new (buffer + sizeof(JustTesting)) JustTesting(“Good Idea”, 6);
	pc4 = new JustTesting(“Heap2”, 10);
		
	delete [] pc2;
	delete [] pc4;
	pc3->~JustTesting();								//显示调用析构函数
	pc1->~JustTesting();								//显示调用析构函数
	delete [] buffer;
		
	return 0;
}

  该程序首先使用new运算符创建了一个512字节的内存缓冲区,然后在这块缓冲区中为对象分配内存。如果要在这块缓冲区中为多个对象分配内存,尽量不要用后一个对象的内存单元覆盖前一个对象的内存单元,也就是说不要像下面这样:
  pc1 = new (buffer) JustTesting;
  pc3 = new (buffer) JustTesting(“Bad Idea”, 6);
  这样pc3内存单元覆盖了pc1的内存单元,如果对象的某些数据成员需要动态分配内存,这样做将发生错误,所以尽量将它们的内存单元分开并且不重叠。
  另一个引人注意的地方是pc1和pc3没有使用delete来释放内存,而是显示调用析构函数。pc2和pc4的内存由常规new运算符分配,而定位new运算符本质上不分配内存,所以也就不应该用delete去释放内存。但是buffer可以用delete来释放,因为buffer的内存是由常规new运算符分配的。用定位new运算符创建的对象不会自动调用析构函数,而需要显示调用析构函数,而且要与创建对象相反的顺序来调用,因为后一个对象的地址可能依赖于前一个对象。将内存缓冲区中的对象的内存单元全都释放后方可释放内存缓冲区的内存。


总结

  以上就是本文的内容——静态类成员、复制构造函数、重载赋值运算符、自己编写String类,最后再次讨论定位new运算符。

你可能感兴趣的:(C++基础知识,c++,开发语言,后端)