一、类指针和类引用
在海纳百川的Cpp世界里,指向类的引用和指向类的指针当然是存在的!比如我设计了如下一个类:
#include <iostream>
class Student
{
private:
int age;
int grade;
public:
void set(int p_age,int p_grade)
{
age=p_age;
grade=p_grade;
}
Student(int p_age=19,int p_grade=1)
{
age=p_age;
grade=p_grade;
}
};
那么我可以定义指向Student类的指针和引用:
int main(void)
{
Student a1;
Student* pa1=&a1;
Student& ra1=a1;
}
也可以直接利用指向Student的指针创建一个对象:
int main(void)
{
Student* pa2=new Student;
}
可以利用直接成员作用符(.)或间接成员作用符(->)来访问成员和调用方法!
pa1->set(20,2);
ra1.set(20,2);
派生类和基类都有其对应类型的指针和引用。如果程序员严格使用指针和引用(派生类对象就用派生类指针和引用,基类对象就用基类指针和引用),那么一切安好。但我们知道,基类指针和引用可以指向派生类对象。这就是C++的尿性。这个现象意味着,基类和派生类指针和引用隐藏着许多性质。为了不引发错误,需要对这些性质有基本的理解!
二、利用指针或引用为基类和派生类对象调用方法
1--
如果直接为基类和派生类对象调用方法(没有使用指针或引用),那么基类对象只能调用基类的方法,派生类对象可以调用派生类和基类的方法。
如下,让基类对象a2调用派生类方法显然是不可行的,但派生类对象a1可以调用基类方法:
#include <iostream>
class Student
{
private:
int age;
public:
Student():age(20)
{
}
Student(int p_age)
{
age=p_age;
}
void set_age(int p_age);
};
class CS_Student:public Student
{
private:
int grade;
public:
CS_Student():grade(1),Student()
{
}
CS_Student(int p_grade,int p_age):Student(p_age)
{
grade=p_grade;
}
void set_grade(int p_grade);
};
int main(void)
{
CS_Student a1(20,2);
a1.set_age(15);
Student a2(20);
a2.set_grade(2);
//这一句是会报错的!
}
void Student::set_age(int p_age)
{
age=p_age;
}
void CS_Student::set_grade(int p_grade)
{
grade=p_grade;
}
2--
利用指针或引用为基类和派生类对象调用方法,情况不太一样。在正常情况下(也就是类方法不添加任何限定词),派生类类型的指针和引用可以调用基类和派生类的方法(所以它只能指向派生类对象),但基类类型的指针和引用只能调用基类的方法,不管它具体指向的是派生类对象还是基类对象。
对于上面的程序,把main函数改为下列形式毫不意外地是可行的!派生类类型的指针和引用调用了基类的方法。
int main(void)
{
CS_Student* pa1=new CS_Student(1,20);
pa1->set_age(15);
CS_Student a2(2,21);
CS_Student& ra2=a2;
ra2.set_age(15);
return 0;
}
但改成下列形式就不行了,基类指针和引用只能调用基类方法:
int main(void)
{
Student* pa3=new CS_Student(1,20);
pa3->set_grade(3);
CS_Student a4(3,22);
Student& ra4=a4;
ra4.set_grade(4);
return 0;
}
从中,我们可以管窥蠡测,如果是用指针和引用为对象调用方法的,编译器是通过指针和引用类型来判断能调用什么方法,而不是考察对象本身是基类的还是派生类的。
3--
如果基类和派生类的方法出现重名的情况(这是允许的),编译器也是通过指针和引用的类型来判断使用哪一种方法的,而不是考察对象本身是基类还是派生类。
#include <iostream>
class Student
{
private:
int age;
public:
Student():age(20)
{
}
Student(int p_age)
{
age=p_age;
}
void set(int p_age);
void read()
{
std::cout<<"age:"<<age<<std::endl;
}
};
class CS_Student:public Student
{
private:
int grade;
public:
CS_Student():grade(1),Student()
{
}
CS_Student(int p_grade,int p_age):Student(p_age)
{
grade=p_grade;
}
void set(int p_grade);
void read()
{
Student::read();
std::cout<<"grade:"<<grade<<std::endl;
}
};
int main(void)
{
Student* pa1=new CS_Student(1,20);
pa1->set(21); //调用了Student::set(int p_age)函数
pa1->read();
CS_Student* pa2=(CS_Student*)pa1;
pa2->set(2); //调用了Student::set(int p_grade)函数
pa2->read();
return 0;
}
void Student::set(int p_age)
{
age=p_age;
}
void CS_Student::set(int p_grade)
{
grade=p_grade;
}
这个例子就很好地说明了这一点。第一个set调用的是基类的方法,第二个set调用的是派生类的方法。只因为pa1是Student类型的指针,pa2是CS_Student类型的指针!
调用read方法,也是按照这个原则的。所以,这段代码的结果应该是:
age:21
age:21
grade:2
读者可以自行编译验证。
事实上,这两个指针指的是同一个对象。但由于指针类型不一样,最终调用的具体方法也不一样。
4--
上述代码派生类设计中有这样的一段:
void read()
{
Student::read();
std::cout<<"grade:"<<grade<<std::endl;
}
这意味着,若存在重名的基类方法和派生类方法,若要在派生类设计中调用基类方法,必须添加作用域解析作用符进行区分。但如果没有重名情况,则不是一定需要这么做。下面这段也是可以运行的。
#include <iostream>
class Student
{
private:
int age;
public:
Student():age(20)
{
}
Student(int p_age)
{
age=p_age;
}
void set(int p_age);
void read1()
{
std::cout<<"age:"<<age<<std::endl;
}
};
class CS_Student:public Student
{
private:
int grade;
public:
CS_Student():grade(1),Student()
{
}
CS_Student(int p_grade,int p_age):Student(p_age)
{
grade=p_grade;
}
void set(int p_grade);
void read()
{
read1();
std::cout<<"grade:"<<grade<<std::endl;
}
};
int main(void)
{
Student* pa1=new CS_Student(1,20);
pa1->set(21); //调用了Student::set(int p_age)函数
pa1->read1();
CS_Student* pa2=(CS_Student*)pa1;
pa2->set(2); //调用了Student::set(int p_grade)函数
pa2->read();
return 0;
}
void Student::set(int p_age)
{
age=p_age;
}
void CS_Student::set(int p_grade)
{
grade=p_grade;
}
还有,如果把read函数定义在类外面,也是一样的!
三、虚方法
搞清楚上面的特性,我们就可以开始介绍虚方法。
1--如何定义虚方法?
在函数原型的前面加上关键字virtual,就能把该方法变为虚方法:
virtual void read();
如果函数是直接在class声明中定义的,则这样做即可:
virtual void read()
{
std::cout<<"age:"<<age<<std::endl;
}
注意,如果是前一种情况,函数定义中就不用再添加virtual了!(和友元函数friend的添加方法一样!)
2--虚方法的作用
就是让方法的调用凭据从指针和引用的类型转为指针和引用指向的类型!如果用基类指针指向一个派生类对象,那么本来是只能调用基类方法的,但如果加上virtual,由于现在调用方法的依据是指针或引用真正指向的对象了,所以这个基类指针会调用派生类的方法了!
#include <iostream>
class Student
{
private:
int age;
public:
Student():age(20)
{
}
Student(int p_age)
{
age=p_age;
}
void set(int p_age);
virtual void read()
{
std::cout<<"age:"<<age<<std::endl;
}
};
class CS_Student:public Student
{
private:
int grade;
public:
CS_Student():grade(1),Student()
{
}
CS_Student(int p_grade,int p_age):Student(p_age)
{
grade=p_grade;
}
void set(int p_grade);
void read();
};
int main(void)
{
Student* pa1=new CS_Student(1,20);
pa1->set(21); //调用了Student::set(int p_age)函数
pa1->read();
CS_Student* pa2=(CS_Student*)pa1;
pa2->set(2); //调用了Student::set(int p_grade)函数
pa2->read();
return 0;
}
void Student::set(int p_age)
{
age=p_age;
}
void CS_Student::set(int p_grade)
{
grade=p_grade;
}
void CS_Student::read()
{
Student::read();
std::cout<<"grade:"<<grade<<std::endl;
}
我只在Student类的read方法前加了一个virtual,输出结果就变成:
age:21
grade:1
age:21
grade:2
而不是先前的:
age:21
age:21
grade:2
3--
如果要在派生类中重新定义基类方法,最好在基类中把相应方法声明为虚。
基类中析构函数应该声明为虚,这是一个惯例。(防止用基类指针指向的派生类对象空间释放不干净!)
4--虚方法的优势:
可以创建出一个基类的指针数组,中间储存指向不同类型的指针。从而在一个数组中管理不同类型的对象。这便是C++的多态!
四、protected访问控制
protected像public和private一样,可以作为类的一个部分。
对于普通类而言,protected部分和private部分是一样的,没有区别;但这个类作为基类时,protected部分可以被继承类当作自身私有部分一样直接访问,而private部分不行。
所以,如果有下面这个类:
class Student
{
private:
int a;
protected:
int b;
public:
int c;
};
它在被如下继承后:
class CS_Student:public Student
{
private:
int d;
public:
int e;
};
实际上会变成这样:
class CS_Student
{
no direct access:
int a;
private:
int b;
int d;
public:
int c;
int e;
};