第12章 类和动态内存分配
建议下载下来用Typora软件阅读markdown文件
12类和动态内存分配
- 动态内存和类
- –>让程序运行时决定内存分配,而不是在编译时决定。
- —->使用new和delete运算符来动态控制内存
- ——>在类中使用这些运算符将导致许多新的编程问题。这种情况下,析构函数将是必不可少的,而不再是可有可无的。
- ——–>有时候还必须重载赋值运算符。
C++为类自动提供了下面这些成员函数
1.默认构造函数,如果没有定义构造函数;记得自己定义了构造函数时,编译器不会再提供默认构造函数,记得自己再定义一个默认构造函数。
带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。2.默认析构函数,如果没有定义;用于销毁对象
3.复制(拷贝)构造函数,如果没有定义;用于将一个对象赋值到新创建的对象中(将一个对象初始化为另外一个对象)。用于初始化的过程中,而不是常规的赋值过程。
- 每当程序生成对象副本时(函数按值传递对象,函数返回对象时),编译器都将使用复制构造函数
- 编译器生成临时对象是,也将使用复制构造函数
默认的复制构造函数的功能--->逐个复制非静态成员(成员复制也叫浅复制,给两个对象的成员划上等号),复制的是成员的值;如果成员本身就是类对象,则将使用这个类的复制构造函数复制成员对象,静态成员变量不受影响,因为它们属于整个类,而不是各个对象
浅复制面对指针时会出现错误,在复制构造函数中浅复制的等价于sailor.len=sport.len;sailor.str=sport.str;前一个语句正确,后一个语句错误,因为成员char* str是指针,得到的是指向同一字符串的指针!!!
当出现动态内存分配时,要定义一个现实复制构造函数—>进行深度复制(deep copy)
1
2
3
4
5
6StringBad::StringBad(const StringBad & st)
{
len=st.len;
str=new char[len+1];
std::strcpy(str,st.str);
}4.赋值运算符,如果没有定义;赋值运算符只能由类成员函数重载的运算符之一。将已有的对象赋值给另外一个对象时(将一个对象复制给另外一个对象),使用赋值运算符。
原型:class_name & class_name::operator==(const class_name &);
接受并返回一个指向类对象的引用。- 与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制
解决:提供赋值运算符(进行深度复制)定义,其实现业余复制构造函数相似,但有一些差别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//代码1:先申请内存,再delete
CMyString& CMyString::operator=(const CMyString& str)
{
if(this==str)
{
char *temp_pData=new char[strlen(str.m_pData)+1)];
delete[]m_pData;
m_pData=temp_pData;
strcpy(m_pData,str.m_pData);
}
return *this;
}
//代码2:调用复制构造函数
CMyString& CMyString::operator=(const CMyString& str)
{
if(this==str)
{
CMyString strTemp(str);//复制构造函数创建临时对象,临时对象失效时会自动调用析构函数
char* pTemp=strTemp.m_pData;//创建一个指针指向临时对象的数据成员m_pData
strTemp.m_pData=m_pData;//交换
m_pData=pTemp;//交换
}
return *this;
}5.地址运算符,如果没有定义;
6.移动构造函数(move construtor),如果没有定义;
7.移动赋值运算符(move assignment operator)。
静态类成员函数
- 1.静态函数:静态函数与普通函数不同,它只能在声明它的文件中可以见,不能被其他文件使用。定义静态函数的好处:静态函数不会被其他文件使用。其他文件中可以定义相同名字的函数,不会发生冲突。
- 2.静态类成员函数:与静态成员数据一样,我们可以创建一个静态成员函数,它为类的全部服务,而不是为某一个类的具体对象服务。静态成员函数与静态成员数据一样,都是在类的内部实现,属于类定义的一部分。普通成员函数一般都隐藏了一个this指针,this指针指向类的对象本身。
- 3.静态成员函数由于不是与任何对象相联系,因此不具有this指针,从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数
静态成员函数总结:
- 1.出现在类体外的函数不能指定关键字static;
- 2.静态成员之间可以互相访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
- 3.非静态成员函数可以任意地访问静态成员函数和静态数据成员;
- 4.静态成员函数不能访问非静态成员函数和非静态数据成员
- 5.由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比,速度上会有少许的增长
- 6.调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指调用静态成员函数。
构造函数中使用new时应注意的事项
- 1.如果构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
- 2.new和delete必须相互兼容,new对应于delete,new[]对应于delete[]
- 3.如果有多个构造函数,则必须以相同的方式使用new,要么中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new来初始化指针,而在另外一个构造函数中初始化为空(0或nullptr),这是因为delete(无论是带括号,还是不带括号)可以用于空指针。
- 4.应定义一个复制构造函数,通过深度复制将一个对象初始化为另外一个对象。
- 5.应当定义一个赋值运算符,通过深度复制将一个对象复制给另外一个对象。
有关返回对象的引用
1.首先,返回对象将调用复制构造函数(给新创建的临时对象复制(初始化)),而返回引用不会
2.其次,返回引用指向的对象因该在调用函数执行时存在。
3.返回作为参数输入的常量引用,返回类型必须为const,这样才匹配。
使用指向对象的指针
Class_name* ptr = new Class_name;
调用默认构造函数定位new运算符/常规new运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//使用new运算符创建一个512字节的内存缓冲区
char* buffer =new char[512];//地址:(void*)buffer=00320AB0
//创建两个指针;
JustTesting *pc1,*pc2;
//给两个指针赋值
pc1=new (buffer)JustTesting;//使用了new定位符,pc1指向的内存在缓冲区 地址:pc1=00320AB0
pc2=new JustTesting("help",20);//使用了常规new运算符,pc2指向的内存在堆中
//创建两个指针;
JustTesting *pc3,*pc4;
//给两个指针赋值
pc3=new (buffer)JustTesting("Bad Idea",6);//使用了new定位符,pc3指向的内存在缓冲区 地址:pc3=00320AB0
//创建时,定位new运算符使用一个新对象覆盖pc1指向的内存单元。
//问题1:显然,如果类动态地为其成员分配内存,该内存还没有释放,成员就没了,这将引发问题。
pc4=new JustTesting("help",10);//使用了常规new运算符,pc4指向的内存在堆中
//释放内存
delete pc2;//free heap1
delete pc4;//free heap2
delete[] buffer//free buffer解决问题1,代码如下:
1
2pc1=new (buffer)JustTesting;
pc3=new (buffer+sizeof(JustTesting))("Bad Idea",6);问题2:
将delete用于pc2,pc4时,将自动调用为pc2和pc4指向的对象调用析构函数;问题2:然而,将的delete[]用于buffer时,不会为使用定位new运算符创建的对象调用析构函数
解决问题2:显式调用析构函数
1
2pc3->~JustTesting;
pc1->~JustTesting;
嵌套结构和类
在类声明的结构、类或枚举被认为是被嵌套在类中,其作用域为整个类
这种声明不会创建数据对象,而是指定了可以在类中使用的类型。
- 1.如果声明是在类的私有部分进行的,则只能在这个类中使用被声明的类型。
- 2.如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型
例如,如果Node是在Queue类的公有部分声明的,则可以在外部声明Queue::Node类型的变量。
默认初始化
a.内置类型的变量初始化
如果定义变量时没有指定初始值,则变量被默认初始化。默认值由变量类型和定义变量的位置决定。
如果是内置类型的变量未被显示初始化,它的值由定义位置决定。定义于任何函数体外的变量被初始化为0。
1
2//不在块中
int i;//正确,i会被值初始化为0,也称为零初始化定义于函数体内部的内置类型变量将不被初始化(uninitialized)。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或其他形式的访问此类型将引发错误
1
2
3
4
51 {//在一个块中
2 int i;//默认初始化,不可直接使用
3 int j=0;//值初始化
4 j=1;//赋值
5 }
b.类类型的对象初始化
类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数(constructor)。构造函数的任务是初始化类对象的数据成员。
由编译器提供的构造函数叫(合成的默认构造函数)。
合成的默认构造函数将按照如下规则初始化类的数据成员。
如果存在类内初始值(C++11新特性),用它来初始化成员。
1
2
3
4
5
6
7
8class CC
{
public:
CC() {}
~CC() {}
private:
int a = 7; // 类内初始化,C++11 可用
}否则,没有初始值的成员将被默认初始化。
成员列表初始化
使用成员初始化列表的构造函数将覆盖相应的类内初始化
对于简单数据成员,使用成员初始化列表和在函数体中使用复制没什么区别
对于本身就是类对象的成员来说,使用成员初始化列表的效率更高
如果Classy是一个类,而mem1,mem2,mem3都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员。
1
2
3
4Classy::Classy(int n,intm):mem1(n),mem2(0),men3(n*m+2)
{
//...
}1.这种格式只能用于构造函数
2.必须用这种格式来初始化非静态const数据成员(至少在C++之前是这样的);
3.必须用这种格式来初始化引用数据成员
数据成员被初始化顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关