<c++ primer plus>第六版
目录//以下两行代码等价 // 都是使用一个对象来初始化新对象, // 调用的构造函数为: StringBad(const StringBad &); StringBad sailor = sports; StringBad sailor = StringBad(sports);
有些成员函数是自动定义的, c++自动提供的成员函数有:
如果类的构造函数中用到静态成员或使用动态内存分配, 则隐式的复制构造函数 和 隐式的赋值运算符 会引起一系列问题.
1.1 定义一个类Klunk, 且没提供任何构造函数, 则编译器将提供如下默认构造函数
Klunk::Klunk() {} //默认构造函数, 不接收任何参数, 也不执行任何操作. Klunk lunk; //该语句会调用默认构造函数. ```cpp 1.2 如果定义了构造函数, 则编译器将不会定义默认构造函数, 如果需要不带参数的构造函数, 需要自己定义. ```cpp Klunk::Klunk() //定义不带参数的构造函数 { klunk_ct = 0; }
1.3 带参数的构造函数也可以是默认构造函数, 只要所有参数都有默认值.
Klunk (int n=0) { klunk_ct = n; }
但是只能有一个默认构造函数, 如下两个默认构造函数有二义性, 当用户使用Klunk bus语句时, 将匹配两个构造函数, 会报错.
Klunk () { klunk_ct = 0; } Klunk (int n=0) { klunk_ct = n; }
2.1 复制构造函数原型:
ClassName(const ClassName &) //接受一个指向对象的常量引用作为参数.
2.2 何时调用复制构造函数
新建一个对象, 并将其初始化为同类现有对象时, 将调用复制构造函数. 假设motto是StringBad的对象, 则下面4语句要调用复制构造函数
StringBad ditto(motto); StringBad metoo = motto; StringBad also = StringBad(motto); StringBad * pStringBad = new StringBad(motto);
每当程序生成了对象副本时, 编译器都将使用复制构造函数: 函数按值传递对象, 函数返回对象.
2.4 默认复制构造函数的功能:
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制), 复制的成员的值. 静态函数不受影响, 因为它们不属于各个对象.
比如:
cpp StringBad sailor = sports,
等价于:
cpp StringBad sailor; sailor.str = sports.str; sailor.len = sports.len;
c++允许类对象赋值, 这是通过自动为类重载赋值运算符实现的.
ClassName & ClassName::operator=(const ClassName &);
它接受一个指向类对象的引用, 并返回一个指向类对象的引用.
将已有的对象赋值给另一个对象时, 将使用重载的赋值运算符.
StringBad headline1("Celery Stalks at Midnight"); StringBad knot; knot = headline1; //将调用赋值运算符
注意: 初始化对象时, 并不一定会使用赋值运算符:
StringBad metoo = knot; //将调用复制构造函数(实现时可能分两步: 1. 使用复制构造函数创建一个临时对象, 然后调用赋值运算符将临时对象复制到新对象).
所以: 初始化总是会调用复制构造函数, 而使用=运算符时也可能调用赋值运算符.
与复制构造函数相似, 赋值运算符的隐式实现也对成员进行逐个复制. 如果成员本身就是类对象, 则程序将使用为这个类定义的赋值运算符来复制该成员. 静态数据不受影响.
使用new初始化对象的指针时要特别小心:
当成员函数或独立函数返回对象时, 有几种返回方式:
返回const引用的主要目的是提高效率.
Vector force1(50, 60); Vector force2(10, 70); Vector max; max = Max(force1, force2);
其中Max函数的以下两种实现方法都可行:
//version 1, 返回对象 Vector Max(const Vector &v1, const Vector &v2) { if (v1.magval()>v2.magval()) return v1; else return v2; } //version 2, 返回引用 const Vector & Max(const Vector &v1, const Vector &v2) //第一个const表示返回值是const { if (v1.magval()>v2.magval()) return v1; else return v2; }
注意:
有两种常见的情形要返回非const对象(前者旨在提高效率, 后者必须这样做):
operator=()的返回值用于连续赋值:
String s1("Good Stuff"); String s2, s3; s3 = s2 = s1;
这里s2.operator=()的返回值被赋值给s3, 返回对象或返回引用都可行, 但返回引用可避免调用String的复制构造函数来创建一个新的String对象.
operator<<()的返回值用于串接输出:
String s1("Good Stuff"); cout << s1 << " is coming!";
这里operator<<(cout, s1)的返回值成为一个用于显示字符串" is coming!"的对象.
返回类型必须是ostream &, 而不能是ostream. 如果返回ostream, 将会调用ostream类的复制构造函数, 但ostream没有公有的复制构造函数.
注意: 如果被返回的对象是被调用函数中的局部变量, 则不能按引用的方式返回它. 因为函数执行完后局部变量将调用其析构函数, 引用指向的对象将不再存在.
即: 返回局部变量时, 应该返回对象, 而不是返回引用.
通常, 被重载的算术运算符属于这一类.
例如:
Vector force1(50, 60); Vector force2(10, 70); Vector net; net = force1 + force2;
返回的不是force1也不是force2, 因此返回值不能是调用函数时已经存在的对象的引用, 而是新的临时对象.
Vector Vector::operator+(const Vector &b) const //最后一个const表示是const函数, 不能修改类成员 { return Vector(x+b.x, y+b.y); }
这时, 存在调用复制构造函数(用来创建被返回的对象)的开销, 然而这是无法避免的.
以如下3个语句为例:
net = force1 + force2; //1, 将两个对象相加, 赋值给第三个对象. force1 + force2 = net; //2, 将第三个对象赋值给两个对象相加. cout << (force1 + force2 = net).magval() << endl; //3, 在2的基础上再调用对象方法.
其中第2/3条语句比较奇怪, 提三个问题:
为何编写这样的语句?
没有要编写这种语句的理由, 但并非所有代码都是合理的.
这些语句为何可行?
因为表达式force1+force2的结果为一个临时对象(复制构造函数将创建一个临时对象来表示返回值).
在语句1中, 将该临时对象赋值给net.
在语句2和3中, 将net赋值给该临时对象.
这些语句有何功能?
使用完临时对象后, 将把它丢弃.
比如语句2, 程序计算force1与force2之和, 将结果复制到临时变量中, 再后net的内容覆盖临时对象的内容, 然后将该临时对象丢弃, 原来的矢量全都保持不变.
比如语句3, 程序显示临时对象的长度, 然后将其删除.
如果担心force1 + force2 = net这种语句可能引发的误用和滥用(比如在条件判断语句中将force1+force2==net误写为force1+force2=net),
有一种简单的解决方案: 将返回类型声明为const Vector, 则由于语句2和语句3都有对临时对象的赋值操作, 所以这两语句是非法的.
总结:
如果: ClassName是类, value的类型为TypeName, 则如下语句:
ClassName * pclass = new ClassName(value); //声明一个指向对象的指针, 将调用构造函数ClassName(TypeName);
如下初始化方式:
ClassName *ptr = new ClassName; //将调用默认构造函数
在构造函数中使用new为对象分配存储空间, 在析构函数中使用delete来释放这些内存.
String * favorite = new String(sayings[choice]);
注意: 这是为对象分配内存, 而不是为要存储的字符串分配内存. 也就是说分配的内存情况为:
创建对象时将调用构造函数, 在构造函数中才会分配用于保存字符串的内存, 并将字符串的地址赋值给str.
当程序不再需要该对象时, 使用delete删除它.
程序删除对象时, 将只释放用于保存str指针和len成员的空间, 并不释放str指向的内存.
释放str指向的内存的任务由析构函数来完成.
在下述情况下, 将调用析构函数:
使用对象指针时, 要注意几点:
定位new运算符的作用: 在分配内存时能够指定内存位置.
#include <iostream> #include <string> #include <sstream> #include <new> using namespace std; const int BUF = 512; class JustTesting { private: string words; int number; public: JustTesting(const string &s="Just Testing", int n=0) { words = s; number = n; cout << "construct : " << words << endl; } ~JustTesting() { cout << "destroy : " << words << endl; } void Show() const { cout << words << ", " << number << endl; } string to_str() { string str; stringstream ss; ss << number; ss >> str; return words + ", " + str; } }; int main() { char * buffer = new char[BUF]; // get a block of memory JustTesting *pc1, *pc2; pc1 = new (buffer) JustTesting; // place object in buffer, 创建一个512字节的内存缓冲区 pc2 = new JustTesting("Heap2", 20); // place object on heap cout << endl; cout << "Memory block addresses:" << endl; cout << " buffer: " << (void *)buffer << endl; cout << " heap : " << pc2 << endl; cout << endl; cout << "Memory contents:" << endl; cout << " " << pc1 << ": " << pc1->to_str() << endl; cout << " " << pc2 << ": " << pc2->to_str() << endl; cout << endl; JustTesting *pc3, *pc4; pc3 = new (buffer) JustTesting("Bad Idea", 6); //会覆盖pc1对应的内存单元 pc4 = new JustTesting("Heap4", 10); cout << "Memory contents:" << endl; cout << " " << pc3 << ": " << pc3->to_str() << endl; cout << " " << pc4 << ": " << pc4->to_str() << endl; cout << endl; delete pc2; //会调用析构函数 delete pc4; //会调用析构函数 delete [] buffer; //不会调用析构函数 cout << "Done" << endl; return 0; }
教训1:
pc1和pc3对应的缓冲区内存单元相同, 会引发问题, 所以需要提供位于缓冲区的两个地址, 比如:
pc1 = new (buffer) JustTesting;
pc3 = new (buffer + sizeof(JustTesting)) JustTesting("Better Idea", 6);
教训2:
如果使用定位new运算符来为对象分配内存, 必须确保其析构函数被调用.
在堆中创建的对象可以使用delete pc2, 但缓冲区中的不能使用delete pc1. 因为delete可以与常规new运算符配合使用, 但不能与定位new运算符配合使用. delete [] buffer释放了使用常规new运算符分配的整个内存块, 但它没有为定位new运算符在该内存块中创建的对象调用析构函数, 需要显式地调用: pc3->~JustTesting(); pc1->~JustTesting();
队列是一种抽象的数据类型(Abstract Data Type, ADT), 可以存储有序的项目序列.
队列: 在队尾添加项目, 在队首删除项目(FIFO).
栈 : 在同一端进行添加和删除(LIFO).
本节定义一个Queue类(第16单将介绍标准模板库类queue).
class Queue { enum {Q_SIZE=10}; private: //to be developed later public: Queue(int qs=Q_SIZE); //构造函数, 指定队列长度, 创建一个空队列 ~Queue(); //析构函数 bool isempty() const; //常函数, 队列是否为空 bool isfull() const; //常函数, 队列是否为满 int queuecount() const; //? bool enqueue(const Item &item); //给队列添加项目, 可以使用typedef来定义Item(见第14章类模板) bool dequeue(Item &item); //给队列删除项目 }
Queue line1; //一个队列, 最多10个项目(默认值) Queue line1(20);//一个队列, 最多20个项目
struct Node { Item item; //存储在node中的数据 struct Node * next; //指向下一个Node的指针 };
class Queue { private: struct Node //在class中嵌套struct声明, 使Node的作用域为整个class, 不与其它class或全局声明冲突. { Item item; struct Node * next; }; enum {Q_SIZE = 10}; Node * front; //指向队列Queue的头 Node * rear ; //指向队列Queue的尾 int items; //队列Queue中当前项目数 const int qsize; //队列Queue的最大项目数 ... public: ... };
Queue::Queue(int qs) //构造函数, 队列开始是空的 { front = rear = NULL; //队首队尾设置为NULL items = 0; //项目数为0 qsize = qs; //最大长度从函数参数qs获取(这行代码有问题, 见后描述). }
上述代码有个问题, qsize是常量, 只能对它初始化(在执行函数体之前, 即创建对象时进行初始化), 不能给它赋值.
c++提供了特殊语法来应对const赋值的操作, 它叫做成员初始化列表(member initializer list).
示例修改如下:
Queue::Queue(int qs): qsize(qs) //构造函数, 带成员初始化列表 { front = rear = NULL; //队首队尾设置为NULL items = 0; //项目数为0 }
通常:
1) 初始化对象: 可以是const成员, 也可以是非const成员,
2) 初始化值 : 可以是参数列表中的参数, 也可以是常量(NULL/0等)
3) 只有构造函数可以使用这种初始化列表语法.
4) 对于const成员, 必须使用这种初始化列表语法;
5) 对于被声明为引用的类成员, 也必须使用这种语法(因为引用也只能在创建时进行初始化);
6) 数据成员被初始化的顺序必须与它们出现在类声明的中的顺序相同, 与初始化器中的排列顺序无关.
//构造函数, 带成员初始化列表, //初始化对象: 可以是const成员, 也可以是非const成员, //初始化值 : 可以是参数列表中的参数, 也可以是常量(NULL/0等) Queue::Queue(int qs): qsize(qs), front(NULL), rear(NULL), items(0) { }
//引用类型的成员必须在初始化列表中初始化 class Agency{...}; class Agent { private: Agency & belong; //一个引用, 必须在初始化列表中初始化. }; Agent::Agent(Agency &a): belong(a){...} //在初始化列表中初始化.
成员初始化列表的语法:
Classy是一个类, mem1/mem2/mem3是这个类的成员,
Classy::Classy(int m, int n): mem1(m), mem2(0), mem3(m*n+2)
{
...
}
c++11可以在类内初始化, 但优先级比初始化列表低.
class Classy
{
int mem1 = 10;
const int mem2 = 20;
};