《Essential C++》读书笔记(五)

来源:互联网 发布:java中的字符串反转 编辑:程序博客网 时间:2024/05/01 12:00

第四章 基于对象的编程风格

4.1如何实现一个class

lippman提倡根据类的行为来确定类的数据成员。不过我的习惯还是先定义数据。
类的前置声明只不过是声明了一个类型,而不是定义了一个类型,所以前置声明使我们得以进行类指针的定义,或以此class作为数据型别,如:
class Stack;
Stack *pt;
void process(const Stack&);
若是想定义类的对象,就必须先定义类。
关于inline成员函数。定义于类体内的函数自动视为inline函数,而定义于类体外的函数,若想称为inline函数,就应该在函数声明处或定义时显示指定其为inline。在类体外定义的inline函数,函数的定义式也应该放入头文件中。

4.2什么是Constructors和Destructors

constructor的函数名必须与class名称相同,constructor不应指定返回型别,亦不需返回任何值,应为其有形参,所以可以被重载。
以前读到这里时有个疑问
class Triangular{
public:
Triangular();
Triangular(int len);
Triangular(int len,int beg_pos);
//.....
};
书中问 :Triangular t3=8;调用的是constructor还是assignment operator?作者说是constructor,当时我就很疑惑,现在就比较明了了。的确是调用了constructor,不过发生了隐式转换。先调用了Triangular(int len);构造函数,将8转化为一个Triangular对象,然后的过程真是不清楚,我看见有人说经过了编译器的优化,直接将这个Triangular对象给t3了。若是这样:Triangular t3(8); 虽然也是调用了Triangular(int len);,不过没有发生隐式转化,而是的的确确构造了t3.究其原因,我想还是C++中初始化与赋值的区别。
还有默认构造函数的问题,一直以为默认构造函数是无形参的呢,原来还有一种情况就是每个形参都有默认实参的构造函数。若是没有提供任何构造函数,编译器便会帮我们定义一个无形参的默认构造函数,采用值初始化。
关于成员初始化列表:
另一种构造函数的定义是使用成员初始化表,虽不常用但很重要。
Triangular::Triangular (const Triangular &rhs):_length(rhs._length),_beg_pos(rhs._beg_pos),_next(rhs._beg_pos-1)
{}
书中说了member initialization list主要用来将参数传给member class object 的constructors,还举了个例子:
class Triangular{
public:
//......
private:
string _name;
int _next,_length,_beg_pos;
};
为了将_name的初值传给string constructor,必须以member initialization list 完成:
Triangular::Triangular (int len,int bp):_name("Triangular"){
_length=len>0?len:1;
_beg_pos=bp>0?bp:1;
_next=_beg_pos-1;
}
注意书中的字眼,为了传给string constructor,必须用member initialization list ,也就是说不这么用的就不是传给string constructor。
看了《c++ primer》就知道了,在构造函数里,也就是大括号里面部分叫做赋值,而不是初始化。初始化伴随这变量的定义。也就是说,在函数体的内部,变量的赋值之前,对象的该成员变量已经定义过了。在哪定义的呢?就是在member initialization list里了。知道这点很重要,假如我们的类的成员变量中有一个引用变量,或是有一个无默认构造函数的类。那么这两个必须在初始化列表中初始化,因为引用变量定义的同时必须赋值,无默认构造函数类的定义必须提供初值。
接下来就是析构函数。
很简单,做的是一些善后工作,假如我在定义对象时,new了一块内存,如果不显示将其delete掉,那么它便会一直被占用,直到程序运行结束后,操作系统将其收回,这是很不好的。所以我们必须自己把它delete掉。析构函数便是这个作用。在对象生命周期结束时自动执行其内部的语句。主要就是在它自动执行上,其它成员函数,除了构造函数都不会自动执行了。一个负责开始,一个负责结束。太搭了。析构函数可以执行正常的语句,没必要非得delete。而且许多时候析构函数都是不必要的。内置类型的成员变量或是含析构函数的成员变量都会自己释放其所占内存的。还有,析构函数不可以被重载,它没有形参。
关于成员逐一初始化,嗯,这个《primer》中讲的很细,不过我忘了。好像是有默认的编译器提供的赋值构造函数,像书中这个:
Triangular tri1(8);
Triangular tri2=tri1;
就调用了复制构造函数,你若是没定义,编译其就会给你定义一个,这是好事,但有时也会弄巧成拙。
如成员变量中有指向new出来的变量的指针,你以复制了,就共享地址了,这个改动一下,就变了,或是哪个delete了,另一个还在操作呢,这就闹心了。
所以,有时我们应该提供显示的默认复制构造函数,改变赋值的方式。
书中说,如果有必要为某个class撰写copy constructor,那么同样有必要为它撰写copy assigmment operator。道理如出一辙。

4.3何谓mutable和const

为保证成员函数不会更动其调用者(无论是显示改动还是隐式改动),须将该成员函数声明为const。
class Triangular{
public:
//一下是const member functions
int length()const{ return _length;}
int beg_pos()const{return _beg_pos;}
int elem(int pos) const;
//以下是non-const member functions
bool next(int &val);
void next_reset(){_next=_beg_pos-1;}
//......
};
凡是在class主体以外定义者,如果它是一个const member function,那就必须同时在声明式与定义式中都指定为const。
虽然编译器不会为每个函数进行分析,决定它究竟是const还是non-const,但它会检查每个声明为const的member function,看看它们是否真的没有更改class object的内容。
书中的一个例子:
class val_class{
public:
//这样没有问题吗?
Bigclass& val()const{return _val;}
private;
Bigclass _val;
};
嗯,这样的确是有问题的,书中说,这样返回一个non-const reference指向_val,实际上等同于将_val开放出去,外界便可以对_val进行修改。
我试了下,我的IDE会highlight这个错误,所以我们应该在返回型别前加上const。
member function可根据const与否而重载,所以以防万一,还是多定义一份的好。如:
class val_class{
public:
const Bigclass& val() cost{return _val;}
Bigclass& val{return _val;}
//......
};
void example(const Bigclass *pbc,Bigclass &rbc){
pbc->val();        //调用const版本
vbc.val();  //调用non-const版本
}
设计class时,鉴定const member function 是一件很重要的事情,为什么呢?作者说,没有一个const reference class参数可以调用公开接口中的non-const成分(但目前许多编译器对此类情况都只给予警告,反正我的是不让)。
我用4.1节的习题Stack类试了下:
void show_size(const Stack &stack){
cout<<stack.size()<<endl;
}
由于size()未定义为const,所以会显示错误:

这个问题挺细节的,而且总记不住,挺闹心啊。
mutable data member
在不改变class object的状态或说不算是破坏对象的常数性的前提下,可以将const member function中需要修改的member data声明为mutable。

4.4 什么是this指针

刚学类的时候就想,若是每个对象都定义一份自己的成员函数,这得多浪费空间啊,果不其然,同一类的所以那个对象公用一份函数,而数据个人是个人的。而当调用成员函数时,是通过隐含的this指针辨别调用者的。挺简单的,也挺巧妙地。

仅需记住一点,this指针本身是const指针,而若成员函数是const,则该成员函数中的this指针便是指向const对象的const指针,所以若函数return *this;则函数生命中的返回型别应为指向const对象的引用。

突然对类有了更深一步的理解。类体内包含的是声明而不是定义。声明该类的对象的内容,所以类体内的成员函数的声明应与其定义分开。也就是为什么类的数据成员可以含有初始化的引用变量,因为只有当定义了一个对象的时候,该引用变量才开始定义,在那时才初始化。

4.5 static class member

类比下局部静态变量。
static data member 用来表示唯一一份可共享的member,它可以在同型的所有对象中存取。
class Triangular{
public:
//......
private:
static vector<int> _elems;
};
vector<int> Triangular::_elems;    //不必再声明static,声明了编译器也会报错的。
也可指定其初值:
vector<int> Triangular::_elems=9; 
注意,类体内的是声明,类外的是定义。
对class而言,static data member 只有唯一一份实体(如member function),因此,我们必须在程序代码文件中提供其清楚的定义。
对static data member的使用如一般的数据成员是一样一样的,而对于const static data member可在类体内声明时为其指定初值,但一样要在类外定义,不过别忘了写const。
class intBuffer{
public:
//......
private:
static const int _buf_size=1024;
int _buffer[_buf_size];
};
const int intBuffer::_buf_size;//一样要定义
static member function
声明方式是在声明式之前加上关键词static。
只有在“不存取任何non-static member”的条件下才能够被声明为static。调用时使用class scope(::)运算符,当然这样调用public static code member也是可以的。
当我们在class体外部进行member function的定义时,不需重复关键字static。
当我定义类时,将类的定义放入头文件中,而将一部分成员函数的定义也放入头文件中,一部分放在源文件中,这样编译的时候会出错,不知道是为什么。

4.6 打造一个Iterator Class

这一节是以该主题为线索,教我们如何实现运算符的重载。
首先明确一个Iterator必须支持的操作:!=、*、++、--、==等,运算符的重载是通过定义函数而实现的。不需指定函数名,只需在运算符号之前加上关键字operator即可。
重载规则:
1.不可引入新的运算符,而且 .  、  .*   、  ::   、  ?: 这四个运算符不可被重载
2.运算符的操作数数目不可改变。
3.运算符的优先级不可改变。
4.运算符函数的参数列表中,必须至少有一个参数为class型别,我们不能为内置型别的运算符重定义其行为。
重载运算符可定义为member function 也可定义为 non-member function,记住this指针隐喻代表做操作数。
嵌套型别 
就是在类里typedef了一个类型名,遵守正常的权限。
public是可直接 classname::typedef_name使用
如:vector<int>::iterator
注意类的内部typedef_name 的定义必须在使用之前,如:
class Stack{
private:
svec _stack;
public:
typedef std::vector<std::string> svec;
};//error
只能:
class Stack{
public:
typedef std::vector<std::string> svec;
private:
svec _stack;
};
我记得对于名字查找《primer》中说的很详细。对于类型名是向前查找,对于函数名是先在整个类的内部查找。
(为什么++、--运算符可以重载呢?我想是因为其有两种使用方式。而且+、-、* 运算符也都可以重载)。

4.7 合作关系必须建立在友谊的基础上

非该类的成员函数的函数(可以是单独的一个函数或是其它类的member function)若想直接调用该类的private成员,必须将该函数声明为该类的友元friend。友元的声明可以出现在class定义式的任意位置上,不受private、public的影响。如:
class Triangular{
friend int operator*(const Triangular_iterator &rhs);  //这个需要的是Triangular_iterator的前置声明
friend int Triangular_iterator::check_integrity();//这个连Triangular_iterator的定义都需要
//......
};
为了让上述定义成功通过编译,我们必须在上述两行之前,先提供Triangular_iterator的定义给Triangular知道,否则编译器就没有足够的信息可以确定上述两个函数原型是否正确,也无法确定它们是否的确是Triangular_iterator的member function,而对于普通的function,则不用了。
若是让class A成为 class B的友元类的话,则A中的member function都可以使用B中的private member,这时不用像上面的那样非得给出class A的声明或是定义。
建立友元关系通常是为了效率考虑。还可以在类中定义一个static member function 用以返回private member data,取代friend的建立。
有时我总会潜意识的认为迭代器是个指针,这对于迭代器的使用很方便,但当设计一个iterator时,这是很闹心的。

4.8 实现一个copy assignment operator

默认的=重载函数实现成员逐一复制操作,像默认的复制构造函数一样,有时需要我们自己重定义一个。就像4.2节的那个例子。

4.9 实现一个function object

所谓function object乃是一种“提供有function call运算符”的class,function call运算符可接受任意数目的参数。很简单上一章提到过,不多说了。

4.10 将iostream运算符重载

作为一个member function,其左侧操作数必须是隶属同一个class之下的对象。
所以iostream运算符的重载函数必须是非non-member function。返回值类型应该是指向iostream对象的引用,这样便可以连接多个output或inpu运算符。注意,该函数内部可能会改变流的状态,所以参数表中不应该声明为const。
output运算符的重载不用多说。而input运算符的重载要考虑异常的出现。

4.11指针:指向Class Member Function

这个很有意思,当初可是想了又想呢。
对于本节中作者举得例子,我可是无语了,可能是我的境界跟不上吧,反正我感觉这个例子举得不好。
我们不需要深究于例子,而应该把重点放在知识点的学习上。
pointer to member functions和pointer to non-member functions的差别在于其声明方式,而声明方式也反映其作用域范围的不同,而其使用方式也是不同的。pointer to member functions必须由对象调用,假如its便是一个pointer to member functions,*itr只是调用了这个成员函数,但是,还没有给隐含的this指针传值呢,所以必须要有类的对象调用,以便让该成员函数知道是哪个对象将其调用的。这里就用了.*和->*操作符。
pointer to member functions的生命方式,如:
void (num_sequence::*pm)(int);
从里向外理解,pm是一个指针,指向num_sequence中的东西,该东西是一个函数,有一个int型形参,返回void。注意,class scope必须写出,若不写出便是一个普通的函数指针。
对于pointer to member function必须用指定类中的函数名初始化或赋值,且函数名前必须加上&classname::
如:pm=&num_sequence::triangular;
而对于pointer to member function,只能用类外的函数名初始化或赋值,可直接使用函数名,也可在函数名前加&。
对于其使用方式,pointer to non-member function
直接把指针名当函数名使用就行,或在前加*
如:void(*pm)(int);
pm();或(*pm)();
而pointer to member function 的使用必须经类的对象调用。
num_sequence ns,*pns=&ns;
void (num_sequence::*pm)(int);
(ns.*pm)(); (pns->*pm)();
以上是pointer to member function在类外的使用,而在类内部时,pointer to member function的初始化与赋值都是与在类外一样的,而使用方式则不同,便是用this指针调用。
有个maximal much编译规则,此规则要求每个符号序列总是以“合法符号序列”中最长的那个解释之。
如 vector<vector<int>> ps;//编译错误
a+++p; //是 (a++)+p;


呼!好累
原创粉丝点击