C++ 继承派生多态
面向对象程序设计有4个主要特点:抽象,封装,继承,多态。如果没有掌握这些主要特点,就是没有掌握面向对象程序设计的真谛。下面归纳一下在面向对象编程中一些基本的要点和注意事项。
1.当基类派生出一个子类的时候,子类会继承基类的所有成员(包括私有成员),但是私有成员在子类中是不可访问的,而且基类中的宏定义,typedef定义语句在子类中都是不可见的,也就是说子类不会继承这种类型的变量,通过下面的代码我们可以看出来:
class A
{
typedef int* PINT;
#define n 10
private:
int a;
int b;
public:
double c;
};
class B: public A
{
};
class C: private A
{
};
class D
{
};
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d %d %d\n", sizeof(B), sizeof(C), sizeof(D));
return 0;
}
结果如下:
2.在C++中,通过基类的引用或者指针调用虚函数时,发生动态绑定。用引用或者指针调用虚函数在运行时确定,被调用的是引用或者指针索指对象的实际类型所定义的。除了构造函数之外,任何非静态成员函数都可以是虚函数。
3.通常如果有用在给定调用中的默认实参值,该值将在编译时确定。通过基类的引用或者指针调用虚函数的时候,默认实参为在基类虚函数中指定的值,如果通过派生类的指针或者引用调用虚函数,则默认实参实在派生类的版本中申明的值。在同一虚函数的基类和派生类版本中使用不同的默认实参几乎一定会引起麻烦。代码如下:
class A
{
public:
virtual void fun(int a = 1, int b = 2)
{
printf("%d %d\n",a, b);
}
};
class B : public A
{
public:
virtual void fun(int a = 3, int b = 4)
{
printf("%d %d\n",a, b);
}
};
int _tmain(int argc, _TCHAR* argv[])
{
B *b = new B;
A *a = b;
a->fun();
return 0;
}
结果如下:
4.如果子类继承基类的方式为私有继承,那么当子类再派生子类的时候,子类的子类将不能访问子类从基类继承过来的成员,如果想要子类的子类能够访问到基类的成员,则可以在子类的public或protected部分增加一个using声明,如下:
class A
{
public:
int a;
};
class B : private A
{
public:
using A::a;
};
这样,B的子类就可以访问A类中的变量a了。
5.友元关系不能继承,基类的友元对派生类没有特殊的访问权限。如果基类被授予友元关系则只有基类具有特殊的访问权限,该基类的派生类不鞥访问授予友元关系的类。如果想要派生类将自己的成员访问授予其基类的友元,派生类也必须显式的这样做。如果派生类和基类都要访问另一个类,那个类必须特地的将访问权限授予每一个派生类。
6.如果基类定义了static成员,则整个继承层次只有一个这样的成员,无论基类派生多少个派生类,每个static成员只有一个实例。
7.创建子类的时候,如果子类构造的时候没有调用基类的构造函数,系统将调用基类的默认构造函数,代码如下:
class Base
{
public:
Base()
{
a = 1;
cout<<"Base:默认构造函数, a = "<<a<<endl;
}
int a;
};
class Derived : public Base
{
public:
Derived()
{
cout<<"Derived:默认构造函数, a = "<<a<<endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Derived* pd = new Derived;
Base* pb = pd;
return 0;
}
运行结果:
但是,如果基类中定义了其它构造函数而没有定义默认构造函数,当子类想通过基类的默认构造函数来构造积累的时候将会出现编译错误,代码如下:
class Base
{
public:
Base(int x)
{
a = x;
cout<<"Base:直接构造函数, a = "<<a<<endl;
}
Base(const Base& b)
{
a = b.a;
cout<<"Base:复制构造函数, a = "<<a<<endl;
}
int a;
};
class Derived : public Base
{
public:
Derived():Base() //error C2512: “Base”: 没有合适的默认构造函数可用
{
cout<<"Derived:默认构造函数, a = "<<a<<endl;
}
Derived(int x):Base(x)
{
cout<<"Derived:直接构造函数, a = "<<a<<endl;
}
Derived(const Derived& d):Base(d)
{
cout<<"Derived:复制构造函数, a = "<<a<<endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Derived* pd = new Derived;
Base* pb = pd;
return 0;
}
在Base中定义了其它构造函数,所以系统将不会为其合成一个默认构造函数,当Derived想要调用Base的默认构造函数的时候,系统无法找到Base的默认构造函数,所以会报错,即使在Derived中不显式调用Base的默认构造函数,比如:
Derived() //error C2512: “Base”: 没有合适的默认构造函数可用
{
cout<<"Derived:默认构造函数, a = "<<a<<endl;
}
也会报错,因为Derived在构造的时候系统会调用Base的默认构造函数。
8.一个类只能直接初始化自己的直接基类,直接基类就是在派生列表中指定的类。
9.像其它虚函数一样,析构函数的虚函数性质都将继承。如果层次中的基类的析构函数为虚函数,则派生类的析构函数也是虚函数,无论派生类是否显式定义析构函数还是使用合成析构函数,派生类的析构函数都将是虚函数。
10.在基类的构造函数或者析构函数中,将派生类对象当作基类类型对待。如果在构造函数或者析构函数中调用虚函数,则运行的是为构造函数或者析构函数自身类型定义的版本。代码如下:
class Base
{
public:
Base()
{
printf("基类构造!\n");
fun();
}
virtual~Base()
{
printf("基类析构!\n");
fun();
}
virtual void fun()
{
printf("Base!\n");
}
};
class Derived : public Base
{
public:
Derived()
{
printf("派生类构造!\n");
fun();
}
virtual~Derived()
{
printf("派生类析构!\n");
fun();
}
virtual void fun()
{
printf("Derived!\n");
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Derived* pd = new Derived;
Base *pb = pd;
delete pb;
return 0;
}
运行结果:
在基类的构造和析构时,调用的都是基类的函数,而不是派生类的函数。
11.在基类和派生类中使用同一名字的成员函数,其行为与数据一样,在派生类作用域中派生类成员将屏蔽基类成员,即使函数原型不同,也会被屏蔽。代码如下:
class Base
{
public:
void fun()
{
}
};
class Derived : public Base
{
public:
void fun(int a)
{
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Base b;
Derived d;
b.fun(); //调用基类的函数
d.fun(0); //调用子类的函数
d.Base::fun();//调用基类的函数
d.fun(); //错误,基类中的函数被子类的同名函数覆盖
return 0;
}
d.fun();想通过子类的对象调用基类中的fun()函数,编译器首先在子类中查找名字fun,找到之后便不再向基类中查找,即使是参数不匹配,这个调用希望接受一个int实参,而这个函数在子类中被屏蔽了,因此出错。
12.如果类使用的是protected或者priva继承派生的,则用户不能将派生类对象转换为基类对象。
13.任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,如下面的代码:
Base1 *b1 = new Derive();
b1->f1(); //编译出错
这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。