C++学习之类与动态内存分配

动态内存

动态分配内存——防止大量浪费或者增加计算机内存负载。也就是程序运行时(而不是编译时)确定使用多少内存的问题。在类构造函数中使用new,在这种情况下,析构函数是必不可少的。有时候,还必须重载赋值运算符。

为了学习动态内存分配在类设计中的相关知识,我们先设计一个StringBad类,然后设计一个功能稍强的String类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//stringbad.h -- 有缺陷的string类声明
#include<iostream>
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);
};

#include<cstring>
using std::cout;

int StringBad::num_strings = 0;

StringBad::StringBad(const char *s)
{
len = std::strlen(s);
str = new char[len + 1];//用到了new,动态内存分配
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;//析构函数保证对象过期时,由new分配的内存被释放
}

std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
os << st.str;
return os;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void callme1(StringBad &);//按引用传递的函数
void callme2(StringBad);//按值传递的函数

int main()
{
using std::endl;
{
StringBad a1("abc");
StringBad a2("def");
StringBad a3("ghi");
cout << "用a1传递给某按引用传递的函数" << endl;
callme1(a1);
cout << "将a2传递给某按值传递的函数" << endl;
callme2(a1);

cout << "初始化对象a4,用a3为其赋值" << endl;
StringBad a4 = a3;

cout << "初始化对象a5,分配给另一个对象a1" << endl;
StringBad a5;
a5 = a1;
cout << "a5:" << a5 << endl;

cout << "main函数结束" << endl;
}
}

void callme1(StringBad &s)
{
cout << "按引用传递了字符串:" << " " << s << "\n";
}

void callme2(StringBad s)
{
cout << "按值传递了字符串:" << " " << s << "\n";
}

执行结果如下:

从执行结果来看,StringBad类是一个名副其实的,有缺陷的类。存在以下问题:

  1. 按值传递的函数,使得对象作为函数参数来传递时会导致析构函数被调用,会使得原始字符串无法识别。a2过早的被delete。
  2. main里删除对象和建立对象的顺序相反,所以最先删除的三个对象会是a5、a4和a3。a5中出现了很奇怪的字符串↓h

下面的语句:StringBad a4 = a3;这用的是哪个构造函数呢?显然不是默认构造函数,也不是参数为const char *的构造函数,而是复制构造函数,原型如下:

1
StringBad(const StringBad &);

上述main代码测试出的问题都是由编译器自动形成的特殊成员函数引起的。下面介绍这个主题。

特殊成员函数

对于C++的类,在没有提供这种类型的函数时,会自动提供如下的成员函数:

• 默认构造函数
• 默认析构函数
• 复制构造函数
• 赋值运算符
• 地址运算符

默认复制构造函数(copy constructor)

定义

原型如下:

1
Class_name(const Class_name &);

调用时机:新建一个对象并将其初始化为同类现有对象时。例如a1是一个StringBad对象,则下面四种声明都会调用复制构造函数

1
2
3
4
StringBad a2(a1);
StringBad a3 = a1;
StringBad a4 = StringBad(a1);
StringBad *a5 = new StringBad(a1);

还有就是每当程序生成对象副本(按值传递时意味着创建原始变量的一个副本),编译器都将使用复制构造函数。具体地说,当函数按值传递对象,(如上一章的callme2()函数)或按值传递的函数返回对象时,都将调用复制构造函数。

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

功能和存在的问题

默认的复制构造函数逐个复制非静态成员,复制的是成员的

callme2()被调用时,复制构造函数被用来初始化callme2()的形参,以及语句:StringBad a4 = a3;也会用到复制构造函数。

第一个异常是计数器的异常。由于是默认的复制构造函数,所以不会增加计数器num_strings的值(num_strings的值是在构造函数中自增的)。但是析构函数会在任何对象过期时调用,而不管对象是如何被创建的。

第二个异常更加危险。由于隐式复制构造函数是按值复制的。所以复制的并不是字符串是一个指向字符串的指针。也就是说,将a4初始化为a3后,得到的是两个指向同一个字符串的指针。当析构函数被调用的时候,会出现问题。当a4先被删除时,会被正常删除,但a3对象过期时,调用析构函数时会把已经释放的内存再次释放,对同一块内存delete两次,这有可能导致程序崩溃。

解决方法:进行深度复制,而不是浅度复制。也就是说,复制的时候应当创建副本,并将副本赋给str成员,而不仅仅复制字符串地址。这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。

浅复制

浅复制的原理图:

深度复制

深度复制的原理图:

定义新的复制构造函数,实现深度复制

如果类中包含使用了new初始化的指针成员,应当定义一个复制构造函数,用来复制指向的数据,而不是复制指向数据的指针(默认复制构造函数就是复制成员的值,导致错误)。

1
2
3
4
5
6
7
8
9
10
11
StringBad(const StringBad & st);

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

赋值运算符(assignment)

定义

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

1
2
StringBad a5;
a5 = a1;

如果在一条语句内初始化对象,不一定会使用赋值运算符,而是复制构造函数。
1
String a4 = a3;

这个是使用复制构造函数的例子。但实际也可能分两步来实现,就是使用复制构造函数创建临时对象,再把临时对象赋值给新对象。

也就是说,初始化一定会调用复制构造函数,而使用=运算符时允许调用赋值运算符。

默认重载的赋值运算符对StringBad类有什么问题?

和隐式复制构造函数一样,存在数据受损的问题,原理也是成员复制的问题。也会进行浅度复制。也会造成同一段内存被释放两次的错误。

提供赋值运算符的深度复制的定义。与复制构造函数有一些差别是:
• 由于目标对象可能引用了以前分配的数据,所以应先使用delete[]
应当避免对象赋值给自身;否则释放内存的操作可能删除对象的内容。
返回一个引用。使得可以连续赋值

为StringBad类编写赋值运算符

赋值运算符是只能用成员函数重载的几个运算符之一。详见重载限制。

1
2
3
4
5
6
7
8
9
10
11
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;
}

总结

如果在构造函数中使用new来初始化指针对象,则

• 析构函数不再是可有可无的了,必须在析构函数中使用delete。
• new和delete必须互相兼容。new对应delete,new []对应delete[];
• 如果有多个构造函数,则必须用相同的方法使用new。要么都带中括号,要么都不带。因为只能有一个析构函数,要与所有构造函数兼容。
• 应定义一个复制构造函数,通过深度复制将一个对象初始化给另一个对象。
• 应定义一个赋值运算符,通过深度复制将一个对象赋值给另一个对象。

改进后的String类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//string1.h -- 改进后的string类
#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; //通过cin输入的上限
public:
String(const char *s); //构造函数
String(); //默认构造函数
String(const String &st); //复制构造函数
~String(); //析构函数
int length()const { return len; }

//运算符重载
String & operator=(const 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 ostream & operator<<(ostream & os, const String &st);
friend istream & operator>>(istream & is, String &st);

//静态类成员函数
static int HowMany();
};
#endif // ! STRING1_H_


//string1.cpp -- 方法定义
#include<cstring>
//#include "string1.h"
using std::cin;
using std::cout;

int String::num_strings = 0;

int String::HowMany()
{
return num_strings;
}

String::String(const char *s)//由c风格字符串来创建类对象
{
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
num_strings++;
}

String::String()
{
len = 4;
str = new char[1];
str[0] = '\0';
num_strings++;
}

String::String(const String &st)//深度复制的复制构造函数
{
num_strings++;
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
}

String::~String()//必要的析构函数
{
--num_strings;
delete[]str;
}

String & String::operator=(const String &st)//赋值运算符,用一个String对象赋值给另一个对象
{
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)//赋值运算符,用C风格字符串赋值给一个String对象
{
delete[]str;
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
return *this;
}

char & String::operator[](int i)//重载中括号,使得可以使用中括号访问法访问String类对象的字符
{
return str[i];
}

const char & String::operator[](int i)const//为const String对象编写的相同功能的重载版本
{
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 st2 < st1;
}

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