第十二章 类和动态内存分配(1)动态内存和类

本章将介绍如何对类使用new和delete以及如何处理由于使用动态内存而引起的一些微妙的问题。也就是构造函数使用new与析构函数使用delete要配对的问题,还有包括复制构造函数和赋值运算符等的需要重载的问题,也就是深度复制和浅复制的区别。

(一)动态内存和类

1.New和静态类成员。

(1)类数据中使用指针char *,说明类声明并没有为字符串本身分配存储空间,而是在构造函数使用new来为字符串分配空间。也就是在对象创建的时候再决定其数据要使用多少内存并分配相应的内存,同时构造函数中的new和析构函数中的delete要配对使用。

(2)静态类成员有一个特点:就是不论创建了多少对象,程序都只创建一个静态类变量的副本,这对于所有的类对象都共享的相同的私有数据非常方便的。静态类成员在编译之后就存在了(准确的说是创建第一个对象的时候创建的,但其实在编译完成后就存在了),以后每一次运行程序它都是相同的值,但我们在程序运行的时候可以改变它。

(3)静态类成员的初始化

一般不可以原型声明中直接初始化(除非是const常量限定),这是因为声明描述了如何分配内存,但并不分配内存,也就是说是描述了创建对象的方法,但是对象没有创建,因此数据是不存在的。对于静态类成员,可以在类声明之外,用单独的语句来进行初始化。这是因为静态类成员是单独存储的,而不属于某个对象。还要注意的是,静态类成员的初始化使用了数据类型,使用了作用域运算符,但并没有使用static关键字。比如我在类StringBad中声明了一个静态类成员static int number;那么我们对它的初始化一般是在源文件中使用int StringBad::number=0;这样来进行初始化,此时(初始化时)不要使用static关键字。

静态类成员可以在原型声明中初始化的情况是我们定义一个静态类成员常量,比如static const int a=2;

(4)!!!!!类中静态类成员,静态类常量,普通类常量的解析(非常重要)

静态类成员程序编译成功就存在了,是编译过程中初始化的,它是和程序的代码一样的东西,而不是在创建对象的时候初始化的。但是这个静态类成员不能在声明中定义(这一点是因为会造成多重声明,毕竟它不是常量,多重声明会造成混乱)。静态类常量是可以在类声明的时候初始化的,因为它编译成功就存在,所有对象共享,而且是永远不变的,多次声明也完全没有问题。普通的类常量各个对象都不相同的,尽管它也是常量,但却是在创建对象的时候创建的,普通类常量要在创建对象的初始化列表中来初始化,初始化后就不能再改变。另外,我们可以使用enum枚举来代替静态类常量,实际上,enum与静态类常量的作用完全相同,并且都可以在类声明中完全定义(包括初始值),而且enum用符号的方式来显示,概念和书写更简洁,因此可以使用enum来代替静态类常量。使用的时候直接enum{green=3,black,white=6...}就可以了,可以不用写上枚举类型名,之后我们就可以使用里面的名称来代替相应的值,此时black值为4,如果写上了枚举类型,则可以将4强制类型转换成black,否则4是4,black是black,尽管他们内在相同,但是不能通用。(我们使用的时候,枚举值可以当成相应的数字使用,但是数字不能当成枚举值来用,需要进行强制类型转换)。

(5)删除对象可以释放对象本身所占的内存,但并不会释放对象中的指针成员所指向的内存,因此如果构造函数中用new来动态分配内存,那么析构函数一定要用delete来释放所分配的内存。如果使用new name[]来动态分配内存,则要在析构函数中使用delete[] name来释放分配的内存

(6)当用一个对象来初始化另一个同类型的对象的时候,编译器将自动生成一个跟我们定义的构造函数完全不同的构造函数,也叫复制构造函数(也称为拷贝构造函数),因为他创建了一个对象的副本。比如类是StringBad,StringBad x;StringBad(x);后者就是一个复制构造函数。再比如String类中StringBad sailor=sports;就相当于StringBad sailor=StringBad(sports);相应的构造函数的原型是StringBad(const StringBad &);当你用一个对象初始化另一个同类对象的时候,编译器会自动生成这样一个复制构造函数。

2.特殊成员函数

所谓特殊成员函数,指的是编译器自动为我们添加的成员函数,比如默认析构函数等。

c++自动定义了下面这些成员函数:默认构造函数(如果没有定义构造函数),默认析构函数(如果没有定义析构函数),默认复制构造函数(如果没有定义),默认赋值运算符(如果没有定义),地址运算符(如果没有定义)。隐式地址运算符返回调用对象的地址,即this指针,是我们所需要的,这里不做过多解释(比如a是一个对象,&a就是a对象的地址,对a内部数据来说,就是this指针,&在这里被隐式重载了)。

(1)默认构造函数(如果没有定义构造函数):如果没有定义构造函数,那么编译器会为我们自动生成一个默认的构造函数,这个函数不接受任何参数,也不执行任何操作,默认构造函数使类类似于常规的自动变量,相当于生成了一个不确定的对象。当我们定义了构造函数的时候编译器便不会再生成默认构造函数,如果此时我们仍然希望使用不带参数的默认构造函数,那么必须显式地定义默认构造函数。这种构造函数没有任何参数,但可以用它来设定特定的值。带参数的构造函数也可以设定成默认构造函数,只需要将所有参数都设置成默认值(在声明中完成),也还要注意不要设置多种默认构造函数,会产生二义性错误。也就是说,我在创建对象时候不给他初始化,那么此时调用的构造函数就可以称为默认构造函数。

(2)复制构造函数(如果没有定义):用于将一个对象复制给另一个新创建的对象中的时候,复制构造函数接受一个指向同类对象的常量引用作为参数

什么时候会用到复制构造函数呢?新建一个对象,并将其初始化为同类现有对象的时候,会调用复制构造函数。另外,每当程序生成了对象副本时(按值传递对象或者返回对象副本的时候),编译器都会使用复制构造函数。比如,string b;string a(b);string c=a;都会调用复制构造函数,最后一种情况会复杂一点,可能直接调用复制构造函数,也可能利用复制构造函数生成临时对象,这取决于具体的实现,然而都是要用到复制构造函数的。复制构造函数是用于对象初始化的时候的,不是用于普通赋值情况的。

默认的复制构造函数会带来一系列的问题,首先默认复制构造函数不调用我们定义的显式的构造函数,如果我们定义的构造函数中有一些静态类成员计数等的操作,则默认复制构造函数也不会执行。其次,默认构造函数是生成了一个与原有类对象完全相同的对象,这里的完全相同是包括指针变量的,因此如果原对象有一个指针,那么复制构造函数会使后来的对象也有一个指向同一位置的指针,那么析构的时候,这个内存位置将会被释放两次,这是完全错误的,可能会引发问题。再者如果前一个对象被释放了,那么后一个对象如果使用这个指针的话,将会使用一个不知道会存放什么数据的指针,会造成显式错误而使程序崩溃。

解决的方式是定义一个显式的复制构造函数以解决问题,我们在这个复制构造函数中并不是复制指针,而是连同指针指向的内存一并复制。也叫作深度复制(deep copy)。如果类中有使用new来初始化的指针成员,应该进行深度复制,以避免可能产生的由于复制构造函数带来的问题。

(3)默认析构函数(如果没有定义,不会进行任何操作,除了释放对象的内存),因此一般不会出问题。

(4)默认赋值运算符(如果没有定义)

接受并返回一个指向类对象的引用。将一个对象赋值给另一个同类型对象的时候,将使用重载的赋值运算符,赋值运算符的问题,与默认复制构造函数差不多,也可以通过深度复制来解决。用一个已有的类对象初始化一个同类的对象的时候,一定会使用复制构造函数(用一个对象来构造一个新对象),可能会使用赋值运算符(可能直接构造一个新的对象,也可能构建一个临时对象然后赋值给新对象),这取决于实现。而将一个已有的对象赋值给另一个已有的对象的时候,会使用赋值运算符,接收一个对象引用作为参数,返回一个对象引用,这个返回值其实就是左值对象。注意:复制构造函数与赋值运算符的最大区别是,赋值构造函数是创建了一个新的对象,申请了一个新的内存块;而赋值运算符并没有这么做,它是修改了原有的内存块中对象的相应的数据项。

默认赋值运算符与默认复制构造函数相似,都是按值来复制(浅复制),但也有不同点,主要是赋值运算符前面的对象可能是已有的正常的对象,因此在显式声明的时候需要delete掉原有指针指向的内存。而默认复制构造函数都是新创建的对象或临时对象,因此指针无需释放,都是刚创建的新指针。还有一点比较重要的是,函数应该尽量避免将对象赋值给自身,因为给对象重新赋值前,对原有对象释放内存操作可能会删除对象的内容,造成错误,此时应该进行判断,比如:if(this=&st) return *this;通过这样的语句来排除赋值给自身的操作。

赋值运算符只能由类成员函数重载的运算符之一,其实它可以没有返回值(因为左值对象的内部数据都已经修改过了,目标已经完成),但是为了能够使用连续赋值(比如a=b=c),因此要使用本身的引用来进行返回(比如return *this),返回的引用将成为下一次赋值的参数。

你可能感兴趣的:(第十二章 类和动态内存分配(1)动态内存和类)