类之间的关系(三大类)
概念
在面向对象中类之间的关系分为三类:继承,复合,委托。
和我们熟知的面向对象的三大特性:封装,继承,多态有一定的联系。
关于复合
复合(composition),表示拥有(has a)。也就是在一个类中包含另一个类。
比如在STL中所说的stack和queue就是在内部拥有一个deque,叫做容器适配器。
复合关系下的构造与析构
当两个类属于复合关系时:
构造是要由内而外的,也就是需要先构造包含的类componment,再构造拥有者container。
析构刚好相反,是由外而内的。也就是先析构container再析构component。
关于委托
委托其实也是一种拥有。只不过是指针层面上的拥有。
就像你想让另一个类做些什么事情,但是又不想影响那个类本身的实现,想视情况而定,这就是委托。
class StringRep;
class String{
public:
String();
String(const char* s);
String(const String& s);
String &operator=(const String& s);
~String();
...
private:
StringRep* rep;
}
这里StringRep的具体实现是不受影响的。
class StringRep{
friend class String;
StringRep(const char* s);
~StringRep();
int count;
char* rep;
};
关于继承
继承(inheritance)是子类与父类的关系,是一种“是”(is a)。
struct _list_node_base
{
...
}
struct _list_node:public _list_node_base
{
...
}
list_node继承于list_node_base,我们也说list_ node_base是list_node
继承关系下的构造与析构
与复合关系类似,继承关系的构造也是由内而外的,
构造时先调用base的构造再调用derived的构造。
析构时先调用自己的析构,再调用base的析构。
继承与虚函数
在继承这个部分要特别注意虚函数,以上面的base与derived的关系举例。
如果是base定义了非虚函数,那么不希望derived重新定义。
如果是base定义了虚函数,那么希望derived重新定义,并且已经有默认的定义了。
如果是base定义了纯虚函数,那么希望derived一定要重新定义(因为没有默认定义)
class A{
public:
virtual void draw() const=0;//纯虚函数
virtual void error(const std::string& msg);//虚函数
int objectID() const;//非虚函数
...
}
关于虚函数
关于explicit关键字
explicit关键字用来修饰类的构造函数,主要作用是防止其隐式调用,告诉编译器必须进行显式调用,也就是不要自动进行类型转换。
举个例子
class A{
public:
A(int a):aaa(a){}
private:
int aaa;
};
比如这样一个类,构造函数的参数是int类型。
A num=10;
当你这样构造时,没有我们前面说的赋值构造,编译器就会进行一个隐式的转换,因为10也是int类型的。所以可以成功。
当我们在构造前加上explicit时,编译器就会报错,提示无法从int转换为A。
关于const
我们在第一篇中提到了const
有一个较为简单的使用方法
在你不想改变值的情况下就加上const,可以在函数后或者变量之前。
class complex{
public:
complex (double r=0,double i=0) :re(r),im(i){}
double real() const {return re;}
double imag() const {return im;}
private:
double re,im;
}
为了不死记硬背,我们来理解一下这个机制。
首先,类的对象分为加const的常量对象和不加const的非常量对象。
成员函数也分为加const的成员函数和不加const的成员函数。
这里用一个表格来理解。
- 常量对象
- 不想让数据成员有变动,而加const的成员函数可以保证不改变数据,所以可以调用。
- 不想让数据成员有变动,而不加const的成员函数不可以保证不改变数据,所以不可以调用。
- 非常量对象
- 可变动数据成员,加不加const都可以,因为不在乎是否改变。
这样解释就可以理解了。
举个例子:
const String str("hello");
str.print();
这时候对象为const类型,只可以调用const的成员函数,如果非const就不能保证不修改数据,所以以上代码会报错。
当然还有一种特殊情况,就是当成员函数的const和不加const两个版本同时存在时。
比如
charT operator[](size_type pos)const{...}
reference operator[](size_type pos){...}
这时候设计者会想让const和非const区分开,因为非const的对象既可以调用const成员函数也可以调用非const成员函数,会出现错误。
所以这时候有一条规定:当这两者同时存在时,const的对象只能调用const的成员函数,非const的对象只能调用非const的成员函数。
关于引用
引用本质上其实也是指针构成的,其实是在指针的基础上做了一些包装。
这里举一个非常生动的例子来说明值,指针,引用之间的关系。
当我们为变量x赋值后,它会在内存中占用空间。
当我们用指针p指向x的地址时,这个指向int类型的指针会占用空间。
当我们用引用r指向x时,这个指向int类型的引用会占用空间。同理,引用r2也是一样的,指向r的空间。
可以看出引用和指针本质上是一样的,只是引用做了一些包装。
让引用和原值的大小以及地址都相同,并且不能代表其他变量,这就是包装后的结果。