19. 继承方式
使用 using 关键字可以改变基类成员在派生类中的访问权限(只能改变基类中 public 和 protected 成员的访问权限):
class People{
public:
void show();
};
class Student: public People{
private:
using People::show; //将public改为private
};
20. C++继承时名字屏蔽问题
派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员,但是基类中的成员仍可以访问,但是要加上类名和域解析符。
class People{
public:
void show();
};
class Student: public People{
public:
void show();//屏蔽了基类的show()
};
int main(){
Student stu;
stu.show();//执行的是自身的show()
stu.People::show(); //使用的是从基类继承来的show()
return 0;
}
基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。
21. 基类和派生类的构造函数
类的构造函数不能被继承,在派生类的构造函数中调用基类的构造函数,且总是先调用基类构造函数,且是直接基类的构造函数
,再执行其他代码。
class People{
public:
People(); //基类的默认构造函数
People(char*, int);
};
People::People(): m_name("xxx"), m_age(0){ }
People::People(char *name, int age): m_name(name), m_age(age){}
class Student: public People{
public:
Student(); //派生类的默认构造函数
Student(char *name, int age, float score);
};
Student::Student(): m_score(0.0){ } //这里会调用基类的默认构造函数,如果基类没有默认构造函数,这里会报错
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
析构函数也不能被继承,但是与构造函数不同的是析构函数不用显示的调用,编译器知道如何选择,析构顺序与构造顺序相反
。
22. 多继承->虚继承
为了解决多继承时的命名冲突和冗余数据问题,在继承方式前面加上 virtual 关键字变成虚继承。
- 虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身
- 在虚继承的最终派生类中只保留了一份虚基类A的成员,不会产生二义性
不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承
注意:在虚继承中,虚基类A是由最终的派生类D初始化的,换句话说,最终派生类D的构造函数必须要调用虚基类A的构造函数。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的
23. 将派生类赋值给基类(向上转型)
类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型。相应地,将基类赋值给派生类称为向下转型。
1)对象赋值:赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。换句话说,对象之间的赋值不会影响成员函数,也不会影响 this 指针。
2)指针赋值:对象指针之间的赋值仅仅是改变了指针的指向。
- 通过基类指针访问派生类的成员。通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数(非虚函数),
概括起来说就是:编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数
。
3)引用赋值:表现和指针类似
24. 虚函数->多态
在23
中提到派生类指针赋值给基类指针后,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数,这就有点尴尬,不利于使用,为此使用虚函数:
class People(){
public:
virtual void displry(); //声明为虚函数
};
class Student: public People{
public:
virtual void display();
}
int main(){
People *p = new People();
p->display(); //调用People的display()函数
p = new Student();
p->displey(); //调用Student类的display()函数
return 0;
}
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。引用也可以实现多态,但是不如指针灵活方便
。
24.虚函数和多态注意事项
虚函数注意事项:
- 只需要在虚函数的声明处加上 virtual 关键字
- 可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数
- 只有派生类的虚函数覆盖基类的虚函数(函数原型相同
函数原型式的函数类型声明:声明了函数名、返回值的类型、参数的类型(和个数)
)才能构成多态(通过基类指针访问派生类函数) - 构造函数不能是虚函数,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义
- 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数
多态构成:
- 必须存在继承关系
- 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)
- 存在基类的指针,通过该指针调用虚函数
class Base{
public:
virtual void func();
virtual void func(int);
};
class Derived: public Base{
public:
void func(); //跟Base类中的func()函数原型相同,自动成为虚函数
void func(char *);
};
int main(){
Base *p = new Derived();
p->func(); // 调用Derived类中的func()方法,实现了多态
p->func(10); // 调用了Base类中的func()方法,实现了继承,也没有实现覆盖
//p->func("hello"); // 编译出错,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员
return 0;
}
25. 纯虚函数和抽象类
virtual 返回值类型 函数名 (函数参数) = 0;
包含纯虚函数的类称为抽象类,抽象类无法实例化,通常是作为基类,让派生类实现纯虚函数的功能,且派生类必须实现基类中的纯虚函数才能被实例化。
- 抽象基类中除了包含纯虚函数外,还可以包含其它的成员
- 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数
26. typeid
typeid 运算符用来获取一个表达式的类型信息,操作对象既可以是表达式,也可以是数据类型。typeid 会把获取到的类型信息保存到一个 type_info 类型的对象里面,并返回该对象的常引用。
typeid(dataType)
typeid(expression)
#include <iostream>
#include <typeinfo>
using namespace std;
int main(){
int n = 100;
const type_info &nInfo = typeid(n);
cout<<nInfo.name()<<" | "<<nInfo.raw_name()<<" | "<<nInfo.hash_code()<<endl; //int | .H | 529034928
Base obj; //对象
const type_info &objInfo = typeid(obj);
cout<<objInfo.name()<<" | "<<objInfo.raw_name()<<" | "<<objInfo.hash_code()<<endl; //class Base | .?AVBase@@ | 1035034353
}
type_info 类的几个成员函数:
- name() 用来返回类型的名称
- raw_name() 用来返回名字编码(Name Mangling)算法产生的新名称
- hash_code() 用来返回当前类型对应的 hash 值
1)内置类型比较
27. 运算符重载
运算符重载其实就是定义一个函数,在函数体内实现想要的功能,当用到该运算符时,编译器会自动调用这个函数。也就是说,运算符重载是通过函数实现的,它本质上是函数重载。
返回值类型 operator 运算符名称 (形参表列){
//TODO:
}
//两个复数相加 c1.operator+(c2)
complex complex::operator+(const complex &A)const{
return complex(this->m_real + A.m_real, this->m_imag + A.m_imag);
}
//全局函数 两个复数相加 operator+(c1, c2)
complex operator+(const complex &A, const complex &B){
complex C;
C.m_real = A.m_real + B.m_real;
C.m_imag = A.m_imag + B.m_imag;
return C;
}
运算符重载函数不仅可以作为类的成员函数,还可以作为全局函数,比如在complex类中将operator+()申明为友元函数,且operator+()是全局函数,这样就可以在全局范围里重载“+”
=,[],(),->只能通过成员函数来重载:如果把赋值运算符=重载为类的友员函数(以全局函数来实现),在程序中执行类对象的赋值语句时,程序就会出现两种矛盾的选择。一是因为它认为类中并没有重载赋值运算符的成员函数,所以它根据C++的规则,会去调用相应的构造函数。但是在全局里已经重载了参数类型为此类类型的赋值运算符函数,而这赋值语句刚好和这函数匹配上了,根据C++的规则,也会去调用这函数。
以下运算符不能被重载:.
、.*
、::
、? :
、sizeof
28. 重载>>和<<
//在Complex类中声明为友元函数
istream & operator>>(istream &in, complex &A){
in>>A.m_real>>A.m_imag;
return in;
}
//由于采用了引用的方式进行参数传递,并且也返回了对象的引用,所以重载后的运算符可以实现连续输出
ostream & operator<<(ostream &out, complex &A){
out<<A.m_real<<" + "<<A.m_imag<<"i" ;
return out;
}
29. 重载[]
返回值类型 & operator[ ] (参数); //[]不仅可以访问元素,还可以修改元素
const 返回值类型 & operator[ ] (参数) const; //[]只能访问而不能修改元素
//变长数组
class Array{
public:
Array(int lenght = 0);
~Array();
public:
int & operator[](int i);
const int & operator[](int i) const; //这样做是为了适应 const对象,因为通过const对象只能调用 const 成员函数
int length() const;
void display() const;
private:
int m_length; //数组长度
int *m_p; //指向数组内存的指针
};
Array::Array(int length): m_length(length){
if(length == 0){
m_p = nullptr;
}else{
m_p = new int[length];
}
}
Array::~Array(){
delete[] m_p;
}
int & Array::operator[](int i){
return m_p[i];
}
const int & Array::operator[](int i) const{
return m_p[i];
}
void Array::display(){
for(int i = 0; i < m_length; i++){
if(i == m_length - 1){
cout<<m_p[i]<<endl;
}else{
cout<<m_p[i]<<", ";
}
}
}
30. 重载new和delete运算符
以成员函数的形式重载
void * className::operator new( size_t size ){
//TODO:
}void className::operator delete( void *ptr){
//TODO:
}
以全局函数的形式重载
void * operator new( size_t size ){
//TODO:
}void operator delete( void *ptr){
//TODO:
}
在重载 new 或 new[] 时,无论是作为成员函数还是作为全局函数,它的第一个参数必须是 size_t 类型。size_t 表示的是要分配空间的大小,对于 new[] 的重载函数而言,size_t 则表示所需要分配的所有空间的总和。size_t 在头文件 <cstdio> 中被定义为typedef unsigned int size_t;,也就是无符号整型
delete两种重载形式的返回值都是 void 类型,并且都必须有一个 void 类型的指针作为参数,该指针指向需要释放的内存空间。
31. 重载()强制类型转换运算符
class Complex
{
double real, imag;
public:
Complex(double r = 0, double i = 0) :real(r), imag(i) {};
operator double() { return real; } //重载强制类型转换运算符 double
};
int main(){
Complex c(1.2, 3.4);
cout << (double)c << endl; //(double)c等价于c.operator double(),输出 1.2
double n = 2 + c; //等价于 double n = 2 + c. operator double()
cout << n; //输出 3.2
}
参考
1.http://c.biancheng.net/cplus/