12.1 动态内存和类
不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存;
初始化在方法文件中,而不是在类声明文件中进行,因为类声明文件位于头文件中,程序可能将头文件包括在其他几个文件中,如果 在头文件初始化,将出现多个初始化副本,引发错误;
- 当然,也有例外:静态成员为整型或枚举型const
特殊成员函数:如果没有定义,C++自动提供了下面这些成员函数
默认构造函数
默认析构函数
复制构造函数
赋值运算符
地址运算符
C++11还提供了另外两个特殊成员函数:移动构造函数和移动赋值运算符
默认构造函数:
- 带参数的构造函数也可以是默认构造函数,只要所有的参数都有默认值,但只能有一个默认参数,不然会产生二义性
复制构造函数:将一个对象复制到新创建的对象中
用于初始化过程中(包括按值传递参数),而不是常规的赋值
类的复制构造函数原型:
Class_name(const Class_name &);
假设motto是一个StringBad对象,下面4种情况都调用复制构造函数:
1 2 3 4 5 6
//calls StringBad(const StringBad &); StringBad ditto(motto); StringBad metoo = motto; StringBad also = StringBad(motto); //使用motto初始化了一个匿名对象,并将新对象的地址赋给pstring指针 StringBad * pStringBad = new StringBad(motto);
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象
字符出现乱码:隐式复制构造函数是按值复制的,复制字符串(char*)复制的是指针,也就是说两个对象指向同一个字符串,一旦其中一个销毁了,另一个就用不了这个字符串了,所以在显式复制构造函数种需要新建一个字符串
必须定义复制构造函数(深度复制)的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身
赋值运算符:
运算符原型:
Class_name & Class_name::oprator=(const StringBad &);
将已有的对象赋给另一个对象时,将使用重载运算符:
1 2 3
StringBad headline1("ABC"); StringBad knot; knot = headline1;
初始化对象使用
=
,不一定会使用赋值运算符(比如还使用了复制构造函数),那么为什么说不一定呢,这是因为实现时也有可能分两步来完成,先使用复制构造函数生成一个临时对象,在把临时对象赋值给那个要初始化的对象。也就是说,初始化总会调用复制构造函数,而使用=
运算符时也可能调用赋值运算符函数应当避免将对象赋给自身,否则给对象重新赋值前,释放内存操作可能删除对象的内容(所以要先检查自我复制
if(this == &st) return *this;
)由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据
函数返回一个指向调用对象的引用
我认为一定要返回一个类的引用,是因为会有连等的情况比如
A = B = C;
这样的的话,B = C
,然后返回一个类的引用也就是B,接着就会执行A = B
;
12.2 改造后的新String类
在C++98中,字面值0有两个含义:可以表示数字值0,也可以表示空指针,有些程序员使用
(void *) 0
来表示空指针或者NULL;C++11引入新关键字nullptr,表示空指针静态成员函数:函数声明必须包含static,但如果函数定义是独立的,则其中不能包含static
不能通过对象调用静态成员函数,静态成员函数甚至不能使用this指针
只能使用静态数据成员
可以使用静态成员函数设置类级(classwide)标记,以空值某些类接口的行为
如果是在公有部分声明的,则可以使用类名和作用域解析运算符调用:
1 2
static int HowMany() {return num_strings;} int count = String::HowMany();
12.3 在构造函数中使用new时应注意的
在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
new和delete必须相互兼容;new对应delete,new[]对应delete[]
如果有多个构造函数,必须以相同的方式使用new,要么都到中括号要么都不带,因为只有一个析构函数
应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象
- 具体来说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址,另外,还应该更新所有受影响的静态类成员
应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象
- 具体来说,该方法应完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,返回一个指向调用调用对象的引用我
12.4 有关返回对象的说明
当成员函数或独立的函数返回对象时,可以返回指向对象的引用、指向对象的const引用或const对象
返回指向const对象的引用,旨在提高效率
返回对象将调用复制构造函数,而返回引用不会
引用指向的对象在调用函数执行时存在(也就是说不能是字面值,字面值的话就说明这对象之前没创建,开始调函数了,给了个字面值)
参数被声明为const引用,那么返回类型必须为const,这样才匹配
1 2 3 4 5 6 7
const Vector & Max(const Vector &v1, const Vector &v2) { if(v1.magval() > v2.magval()) return v1; else return v2; }
返回指向非const对象的引用
- 两种常见情形:重载赋值运算符以及重载与cout一起使用的
<<
运算符。前者这样做旨在提高效率(因为传非引用的对象也行,就是慢点),后者必须这样做,因为要用于串接输出cout<<a<<b<<c;
,如果不返回引用而返回ostream,将要求调用复制构造函数,但ostream类没有公有的复制构造函数
- 两种常见情形:重载赋值运算符以及重载与cout一起使用的
返回对象
- 如果被返回的对象是被调用函数的局部变量,则不应该按引用方式返回它,因为执行完了函数会销毁的
返回const对象
- 其实就是增强代码鲁棒性,防止一些情况发生,比如
a+b=c
,如果没有const修饰,那么这句话是对的,只是产生了一个临时对象,然后销毁,但加上const,这句话就是不对的了
- 其实就是增强代码鲁棒性,防止一些情况发生,比如
12.5 使用指向对象的指针
使用new初始化对象:
Class_name * pclass = new Class_name(value);
如果不存在二义性,则将发生由原型匹配导致的转换
在下述情况下析构函数被调用
对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数
如果对象是静态变量,则在程序结束时调用
如果是new创建的,则仅当您显式使用delete删除对象时调用
定位new运算符:能够在分配内存时能够指定内存位置
比如:
1 2 3 4 5
const int BUF = 512; char * buffer = new char[BUF]; JustTesting *pc1, *pc2; pc1 = new(buffer)JustTesting; pc2 = new(buffer + sizeof(JustTesting))JustTesting;
delete可以跟常规new配合使用,但不能和定位new搭配使用,比如指针pc2没有收到new返回的地址,因此delete会导致运行阶段错误;pc1指向的buffer是new[]初始化的,就算delete也要加[]
12.6 复习各种技术
重载
<<
运算符转换函数
构造函数使用new的类
这些前面都有,包括第11章,直接翻前面就行了
12.7 队列模拟
栈是后进先出LIFO(last-in, first-out)
队列是先进先出FIFO
就是模拟了下ATM,写了个queue类和customer类,详情p460