类和动态内存分配(1)

假设我们要创建一个类,其中有一个成员表示某人的姓,最简单的就是用字符串数组来保存,开始使用14个字符的数组,发现太小,保险的方法是使用40个字符的数组,但是当创建2000多个这个样的对象时,必定会造成内存浪费。通常使用string类,该类有良好的内存管理细节。但是这样就没有机会深入的学习内存管理了。

c++在内存分配方面,采用这样的策略,在程序运行时决定内存分配,而不是编译时决定。使用new和delete运算符进行动态控制内存,但在类中使用这些运算符将导致新的编程问题,这时候析构函数是必不可少的,而不再是可有可无的。有时候还必须重载赋值运算符。

复习new、delete和静态成员变量的工作原理:
首先我们设计一个string类stringbad,之所以bad是因为这个类存在比较多的问题。

//stringbad.h
#include <iostream>
#ifndef STRINGBAD_H_
#define STRINGBAD_H_
class StringBad 
{
private :
      char *str;
       int len;
       static int num_strings;
 public:
       StringBad();
       StringBad(const char * s);
       ~StringBad();
       //friend
        friend std::ostream &operator<<(std::ostream &os, const StringBad &st);
};
#endif 
//stingbad.cpp
#include "stringbad.h"

int StringBad::num_strings = 0;

StringBad::StringBad()
{
	len = 4;
	str = new char[4];
	std::strcpy(str, "c++");
	num_strings++;
	std::cout << "string create:" << str << " , " << num_strings << " object created" << std::endl;
}

StringBad::StringBad(const char * s)
{
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
	num_strings++;
	std::cout << "string create:"<<str<<" , "<<num_strings << " object created" << std::endl;
}

StringBad::~StringBad()
{
	num_strings--;
	std::cout << "string delete:" << str << " , " << num_strings << " object left" << std::endl;
	delete[]str;
}

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

有几点需要注意:

  • 这个类使用了char指针成员,而不是char数组,意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间,这样避免的在类声明中预先定义了字符串的长度。
  • num_strings声明为静态成员,静态成员的特点是:无论创建多少对象,程序都只创建一个静态成员类变量副本,即该类的所有对象共享一个静态成员。
    int StringBad::num_strings = 0;
    这条语句将静态成员num_strings初始化为0。注意,不能在类声明中初始化静态成员变量,因为声明只是描述了如何分配内存,但并不分配内存。对于静态成员,可以在类声明之外使用单独的语句进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。初始化语句指明了类型,并使用了作用于运算符,但是没有使用关键字static。
    静态成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来表示静态成员所属的类。但如果静态成员是const整数类型或者枚举时,则可以在类声明中初始化。
//mian.cpp
#include"stringbad.h"
void callme1(StringBad &s)
{
	std::cout << "按引用传递" << std::endl;
	std::cout << s << std::endl;
}
void callme2(StringBad s)
{
	std::cout << "按值传递" << std::endl;
	std::cout << s << std::endl;
}
void main()
{
	std::cout << "---begin---" << std::endl;
	{
		StringBad A("the first string");
		StringBad B("the second string");
		StringBad C("the third string");
		StringBad D;
		StringBad E=A;

		callme1(A);
		std::cout << A << std::endl;
		callme2(B);
		std::cout << B << std::endl;

	}
	std::cout << "---end---" << std::endl;
}

编写上面的文件对生成的类进行测试,运行结果如下:
在这里插入图片描述
代码运行到callme1(A);没什么问题,关键就是下面的callme2(B);这个callme2函数是按值传递的。
将B作函数为参数来传递,从而导致析构函数被调用。虽然按值传递 可以防止原始参数被修改,但实际函数已使原始字符无法识别导致一些非法字符。

首先分析下程序:

  • 创建了4个对象A B C D E ,其中
    StringBad E=A;
    没有使用我们定义的那两个构造函数,这时候上面的语句等效于下面的句子:
    StringBad E=StringBad(A);
    因此相应的构造函数的原型应是:
    StringBad(const StringBad &);
    当使用一个对象去初始化另一个对象时,自动成上述的构造函数,称为复制构造函数,该函数创建了一个对象的副本。自动生成的复制构造函数不知道需要更新num_strings

  • 调用按引用传递的函数callme2(B),为了不修改B,通过默认复制构造函数创建了一个B的副本,这时,副本和B的指针成员指向的是同一个地址,这个副本的作用于只限于这个函数内,函数结束时,将析构这个副本对象,但同时也将B的字符串指针也释放了,导致后面B析构出现问题,试图对释放过的内存进行释放操作是很危险的。

  • 对象的析构顺序和创建顺序是相反的,正常的删除顺序应该是 E D C B A ,删除E D C是正常的,但是删除B出现了错误。程序终止,A没有析构成功,并没有执行最后一句。

特殊成员函数:

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

上述程序的问题就是由隐式复制构造函数和隐式赋值运算符引起的。
默认构造函数,不接受任何参数,也不进行任何操作。
隐式地址运算符返回调用对象的地址,即this指针的值,这正是我们需要的。

复制构造函数
用于复制一个对象到新创建的对象中,即用于初始化(包括按值传递参数),而不是常规的赋值过程中。
原型:Class_name (const Class_name &);//指向对象的常量引用作为参数
何时调用:
StringBad jack(luck);
StringBad jack=luck;
StringBad jack=StringBad(luck);
StringBad * jack=new StringBad(luck);
函数按值传递和返回对象.

默认复制构造函数
默认的赋值构造函数逐个赋值非静态成员(成员复制也称为浅复制)。
如果成员本身是其他类对象,则将使用这个类的复制构造函数来复制成员变量。
静态成员和静态变量不受影响,因为他们属于整个类而不是各个对象。

定义一个显式的复制构造函数和改进的析构函数解决上述问题:

StringBad::StringBad(const StringBad & s)
{
	num_strings++;
	len = s.len + 1;
	str = new char[len + 1];
	std::strcpy(str, s.str);
	std::cout << "string create:" << str << " , " 
			  << num_strings << " object created" << std::endl;
}

必须定义复制构造函数的原因在于:一些类成员是使用new初始化、指向指针的数据,而不是数据本身。
深复制和浅复制:深复制是复制指向的数据而不是指针,浅复制仅仅复制指针信息,而不会深入“挖掘”以复制指针引用的结构。

赋值运算符:
重载赋值运算符:
原型:Class_name& Class_name ::operator =(const Class_name &);//接受并返回一个指向类对象的引用
何时调用:

  • 将已有的对象赋给另一个对象时:
    StringBad head(“this is a string”);
    StringBad k;
    k=head;
  • 初始化对象时,并不一定使用赋值运算符:
    StringBad met=k;
    新创建一个对象met,被初始化为k的值,因此使用复制构造函数。但是实现时可能分两步来处理:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。
    也就是说,初始化总是调用复制构造函数,而使用=运算符也允许调用赋值运算符。

与复制构造函数相似,赋值运算符的隐式实现也是对每个成员进行逐个复制。如果成员本身是类对象,这使用该类的赋值运算符来复制该成员,但静态成员不受影响。

StringBad & StringBad::operator=(const StringBad & s)
{
	if (this == &s)
		return *this;

	delete[]str;//重置原来的字符串
	len = s.len;
	str = new char[len + 1];
	std::strcpy(str, s.str);
	return *this;
}

与复制构造函数的一些差别:

  • 由于目标对象可能引用了以前分配的数据,因此必须使用delete[]进行重置,释放这些数据
  • 函数应该避免将对象赋给自己;否则,给对象重新赋值前,释放内存操作可能删除对象的内容
  • 函数返回一个指向调用对象的引用,可以编写s0=s1=s2;这样的代码

猜你喜欢

转载自blog.csdn.net/qq_29689907/article/details/84074648