类和动态内存分配

1、动态内存和类

(1)静态类成员

class stringBad
{
private:
	char * str;
	int len;
	static int num_strings;//静态成员变量
public:
	StringBad(const char * s);	
};

//类外初始化静态变量
int StringBad::num_strings = 0;

特点:
1、无论创建了多少个对象,程序都只创建一个静态类变量副本。即所有对象共享同一个静态成员。
2、不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。
3、可以在类声明之外使用单独的语句进行初始化,因为静态类成员是单独存储的,而不是对象的组成部分。
4、初始化时不适用关键字static
5、静态数据成员在类声明中声明,在包含类方法的文件中进行初始化。初始化时使用作用域运算符来指出静态成员所属类。
6、如果静态成员时整形或枚举型const,则可以在类声明中初始化

(2)动态内存分配

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

1、构造函数必须分配足够的内存来存储字符串,其中字符串并不保存在对象中,字符串单独保存在堆内存中,对象仅保存了指向该字符串的指针。
2、在构造函数使用new来分配内存时,必须在相应的析构函数里面调用delete,释放申请的内存。若使用new[],则应使用delete[]来释放内存。

(3)复制构造函数
当使用一个对象来初始化另一个对象时,编译器将自动调用复制构造函数
1、复制构造函数用于将一个对象复制到新创建的对象中。用于初始化过程中(包括按值传递),而不是常规的赋值过程。
2、当程序生成了对象副本时,编译器都将使用复制构造函数:当函数按值传递对象(创建原始变量的一个副本)、函数按值返回对象、生成一个临时对象
3、新创建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用,最常见的情况是将新对象显式地初始化为现有的对象

StringBad ditto(motto);
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad * pStringBad = new StringBad(motto);//使用motto初始化一个匿名对象,并将新对象的地址赋给pStringBad指针

4、默认的复制构造函数逐个复制非静态成员(成员复制也成为浅复制),复制的是成员的值。若试图释放内存两次可能导致程序异常终止。
5、解决4中问题,可以定义一个显示的复制构造函数,即深度复制。复制构造函数应当复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串地址。这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。调用析构函数时都将释放不同的字符串,而不会试图区释放已经被释放的字符串。

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

6、必须定义复制构造函数的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。

(4)赋值运算符
C允许结构赋值,C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。
赋值运算符的原型为:

Class_name & Class_name::operator=(const Class_name &);

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

StringBad & StringBad::operator=(const StringBad & );
StringBad headline1("123");
StringBad knot;
knot = headline1;

2、初始化对象时,并不一定会使用赋值运算符

StringBad metoo = knot;

metoo是一个新创建的对象,被初始化为knot的值,因此使用复制构造函数。实现时也可能分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过赋值运算符将临时对象的值复制到新对象中。
3、赋值的问题
上述1中将headline1赋给knot,为knot调用析构函数后,再为headline1调用析构函数,会释放同一块内存,造成程序崩溃。
4、解决赋值的问题
提供赋值运算符定义,其实现与复制构造函数相似。

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

(5)静态类成员函数
将成员函数声明为静态类成员函数,函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static。
1、静态成员函数不能通过对象调用静态成员函数。静态成员函数甚至不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。

static int HowMany(){return num_strings;}

int count = StringBad::HowMany();//调用方式

2、静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。例如静态方法HowMany()可以访问静态成员num_strings,但不能访问str和len
3、可以使用静态成员函数设计类级的标记(数据),以控制某些类接口的行为

2、构造函数使用new时应注意的事项

  • 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
  • new和delete必须兼容。new对应于delete,new[]对应delete[]
  • 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或c++11中的nullptr),这是因为delete(无论带不带中括号)可以用于空指针。
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。
  • 应定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。该方法应检查自我赋值的情况、释放成员指针之前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用
  • 包含类成员的类的逐成员复制,将使用成员类型定义的复制构造函数和赋值运算符

3、返回对象的说明

当成员函数或独立的函数返回对象时,有几种返回方式可供选择:指向对象的引用、指向对象的const引用、const对象
1、返回指向const对象的引用

  • 返回对象将调用复制构造函数,而引用不会
  • 引用指向的对象应该在调用函数执行时存在
  • 若函数参数为const,且返回该参数,则返回引用时应为const
const Vector & Max(const Vector & v1, const Vector & v2)
{
	if (v1.magval() > v2.magval())
		return v1;
	else
		return v2;
}

2、返回指向非const对象的引用
两种常见的返回非const对象的情形是,重载赋值运算符以及重载与cout一起使用的<<运算符,前者旨在提高效率,后者必须这样做。
3、返回对象
如果返回的对象是被调用函数中的局部变量,则不应按引用方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数,通常被重载的算术运算符属于这一类
总之,如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类的对象,它必须返回一个一个指向这种对象的引用。最后,有些方法和函数可以返回对象或指向对象的引用,则首选引用,因为其效率更高

4、类中成员数据特性

1、在类声明中声明为结构、类或枚举被称为时被嵌套在类中,其作用域为整个类。如果声明是在类的私有部分,则只能在这个类使用这些成员;如果声明是在公有部分进行,则可以在类外通过作用域解析运算符使用声明的成员变量
2、如果类成员为const类型,则只能对此种类型的值进行初始化,不能进行赋值。从概念上说,调用构造函数时,对象将在括号中的代码指向之前被创建,因此,调用构造函数将为成员变量分配内存。然后,程序流程执行到构造函数体,使用常规的赋值方式将值存储到内存中。因此,必须在执行到构造函数之前,即创建对象时初始化const变量。C++提供了初始化列表来完成此种赋值操作。
3、只有构造函数可以使用这种初始化列表语法。对于const类成员和被声明为引用的类成员,必须使用初始化列表。

你可能感兴趣的:(C++,类内存分配)