开始之前,我们先说一句,我们打算用三篇笔记搞定C++面向对象基础,后面开始就要写泛型编程和STL了。节奏有点快是不是?
一、基本概念
从一个类派生出另一个类的格式如下:
class A//基类
{
public:
int n1;
};
class B:public A//派生类,继承方式一般用public,当然也有其他方式
{
public:
int n2;
};
此时类A公有派生出了类B,类A称为基类,类B称为派生类,派生类对象也是基类对象。
在公有派生的情况下,存在着三条赋值兼容规则(这个和C++三种传递方式(值传递、指针传递和引用传递)相对应):
- 派生类对象可以赋值给基类对象;
- 派生类对象可以用来初始化基类引用;
- 派生类对象的地址(或派生类的指针)可以赋值给基类的指针。
注意,这三条规则仅限于公有派生,如果是私有派生和保护派生则不成立。
这个规则很好理解。基类没有派生类的全部成员,但派生类有基类的全部成员,因此派生类>基类,基类对象当然可以接收派生类的值、址和引用了,但是反过来却不行。
下面这个例子(续上面的程序)展示了这三条规则:
A a;
B b;
a = b;//派生类对象赋值给基类对象
A &r = b;//派生类对象初始化基类引用
A *pa = &b;//派生类对象的地址赋值给基类的指针
B *pb = &b;
pa = pb;//派生类的指针赋值给基类的指针
如果这三条反过来是不成立的。一般来讲,基类的指针不能赋值给派生类的指针。但是通过强制类型转换,也可以将基类指针强制转换成派生类指针,然后再赋值给派生类指针。只是这样做是有风险的。比如下面这个例子(续上面的程序):
A *pa = &b; //基类指针能指向派生类对象
B *pb = &a; //出错,派生类指针不能指向基类对象
B *pb = (B*) &a; //强制转换类型,慎用
pa -> n1 = 1;
pa -> n2 = 2; //出错,pa是基类指针,编译器只会去找基类的成员,不会去找派生类的成员
pb -> n1 = 3; //能编译成功,但运行时可能会出现意外的结果
另外,还有多层次的派生。下面这个例子展示了多重派生:
class A//基类
{
int n1;
};
class B:public A//派生类
{
int n2;
};
class C:public B//派生类
{
int n3;
};
在本例中,类A派生类B,类B派生类C;类A是类B的直接基类,类B是类C的直接基类;类A是类C的间接基类。
二、覆盖
派生类和基类可能有同名成员变量或同名成员函数(或二者兼有),这种现象叫做覆盖。看看下面我的程序是如何解决这个问题的:
#include<iostream>
using namespace std;
class A //基类
{
public:
int n;
void test()
{
cout<<"class A!"<<endl;
}
};
class B:public A //派生类
{
public:
int n;//与类A的成员变量发生重名,一般不要这样做
void test()//与类A的成员函数发生重名,这种情况反而很常见
{
cout<<"class B!"<<endl;
}
void func();
};
void B::func()
{
n = 1;//默认是派生类(B)的
A::n = 2;//这个肯定是基类(A)的
cout<<n<<" "<<A::n<<endl;
test();//默认是派生类(B)的
A::test();//这个肯定是基类(A)的
}
int main()
{
B b;//注意!对象b既是类A又是类B!
//因此,对象b也包含类A应有的成员变量
b.func();
return 0;
}
程序输出结果:
1 2
class B!
class A!
对于多重派生来说也是一样的,对于重名的成员变量或成员函数,你不写类名::
,那么系统默认是属于本类的。为防止困惑,遇到重名的还是最好都写上类名::
吧。
三、保护(protected)
访问范围说明符protected,是用来修饰成员变量或成员函数的。protected
一般用来修饰基类的成员变量或成员函数,这样派生类也可以去访问基类的成员变量或成员函数了,但是在类定义外部是访问不了的(三种范围说明符的关系如下图,可能不太严谨)。
因此,保护成员的可被访问范围(自己的和我派生的,都能访问,其他类不能访问)大于私有成员(只能自己类内部访问,派生的也不行),而小于公有成员(谁都能访问)。
举一个下面程序的例子:
#include<iostream>
using namespace std;
class A{
private:
int pri;//私有成员
public:
int pub;//公有成员
protected:
int pro;//保护成员
};
class B:public A
{
void test()
{
pri = 1;//出错!不能访问基类私有成员
pub = 2;
pro = 3;//可以访问基类保护成员
}
};
int main()
{
B b;
b.pri = 1;//出错!类定义外部不能访问私有成员
b.pub = 2;
b.pro = 3;//出错!类定义外部不能访问保护成员
return 0;
}
注意,在基类中,一般都将需要隐藏的成员说明为保护成员而非私有成员。
四、派生类的构造函数&析构函数
派生类对象创建时,也是要调用构造函数进行初始化的,这个过程跟封闭类对象比较类似,但又不尽相同。在派生类内部定义构造函数的格式如下:
构造函数名(形参表):基类名(基类构造函数实参表)
{
函数体;
}
构造函数和析构函数在基类对象和派生类对象里有不同的生存期,下面我们用一个程序来说明它们是怎么运作的:
#include<iostream>
using namespace std;
class A{ //基类
private:
int n1, n2;
public:
A(int _n1, int _n2):n1(_n1), n2(_n2)
{
cout<<"A被建造!"<<endl;
}
~A()
{
cout<<"A被拆毁!"<<endl;
}
void Print()
{
cout<<n1<<","<<n2<<endl;
}
};
class B:public A{ //派生类
private:
int m1, m2;
public:
//该构造函数说明前两个参数要用基类的构造函数初始化处理
//这样派生类对象b的n1、n2也有初始值了
B(int _n1, int _n2, int _m1, int _m2):A(_n1, _n2), m1(_m1), m2(_m2)
{
cout<<"B被建造!"<<endl;
}
~B()
{
cout<<"B被拆毁!"<<endl;
}
void Print()
{
cout<<m1<<","<<m2<<endl;
}
};
int main()
{
B b(1, 2, 3, 4);
b.B::Print();
//调用自己类的同名成员函数时,其实不用注明哪个类也行,但注明也无妨
b.A::Print();
//调用基类或间接派生类的同名成员函数时,要注明是哪个类的
return 0;
}
输出结果如下:
A被建造!
B被建造!
3,4
1,2
B被拆毁!
A被拆毁!
由输出结果可以看到,在派生类对象构建时,先执行基类的构造函数,再执行派生类的构造函数;在派生类对象消亡时恰好相反,先执行派生类的析构函数,再执行基类的析构函数。这个步骤与封闭类对象的创建和消亡恰好相反。
对于多层次的派生结构来说,派生类对象生成时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后再执行自身的构造函数;当派生类对象消亡时,会先执行自身的析构函数,然后从底向上依次执行各个基类的析构函数。
假如在上面程序main函数里加上A a(5, 6);
,那么出现的情况是:整个程序A被建造了两次(这个正常),A被销毁了两次,而不是一次。如果是多层次派生结构,又定义了许多对象,那么中间的构造和析构函数就会被调用很多次了。
五、私有派生和保护派生
上面四节说的都是公有派生的大前提下实现的,因为公有派生是最常用的。实际上还有私有派生和保护派生,这两个反而不常用。下表说明了不同派生方式导致派生类的可访问范围的不同:
基类成员 | 公有派生 | 私有派生 | 保护派生 |
---|---|---|---|
私有成员 | 不可访问 | 不可访问 | 不可访问 |
保护成员 | 保护 | 私有 | 保护 |
公有成员 | 公有 | 私有 | 保护 |
下面我们用程序来说明私有派生的访问范围(一看就懂):
#include<iostream>
using namespace std;
class A{
private:
int pri;
public:
int pub;
protected:
int pro;
};
class B:private A{
void test()
{
pri = 1;//出错,不能访问类A的私有成员
pub = 2;//pub在类B里变成私有成员了,不会出错
pro = 3;//pro在类B里变成私有成员了,不会出错
}
};
class C:public B{
void test()
{
pri = 1;//出错,不能访问类B的私有成员
pub = 2;//出错,不能访问类B的私有成员
pro = 3;//出错,不能访问类B的私有成员
}
};
int main()
{
B b;
b.pri = 1;//出错,不能访问私有成员
b.pub = 2;//出错,不能访问私有成员
b.pro = 3;//出错,不能访问私有成员
return 0;
}
下面我们用程序来说明保护派生的访问范围(一看就懂):
#include<iostream>
using namespace std;
class A{
private:
int pri;
public:
int pub;
protected:
int pro;
};
class B:protected A{
void test()
{
pri = 1;//出错,不能访问类A的私有成员
pub = 2;//pub在类B里变成保护成员了,不会出错
pro = 3;//pro在类B里变成保护成员了,不会出错
}
};
class C:public B{
void test()
{
pri = 1;//出错,不能访问类A的私有成员
pub = 2;//pub在类C里变成保护成员了,不会出错
pro = 3;//pro在类C里变成保护成员了,不会出错
}
};
int main()
{
B b;
b.pri = 1;//出错,不能访问私有成员
b.pub = 2;//出错,不能访问保护成员
b.pro = 3;//出错,不能访问保护成员
return 0;
}
无论是什么派生方式,私有成员都只允许自己访问,其他类是不可访问的。公有派生的特点是,基类该是什么成员,派生类也该是什么成员,原有访问范围不变。保护派生的特点是,无论基类是什么成员(除了私有成员),到了派生类就都是保护成员。私有派生的特点是,无论基类是什么成员(除了私有成员),到了派生类就都是私有成员。
一般情况下,都应该使用公有派生。
暂时先写到这吧,往后修改可能会有补充。