一、C++继承
c++的一个很大的特点是面向对象编程,而继承机制是面向对象程序设计使代码可以复用的最重要手段;它可以让程序员在保留原有类特性的基础上进行功能拓展,在类的基础上衍生功能模块,使得设计的代码更加有层次感,更加的简洁,冗余性大大降低;
类的继承分为基类(也称父类)和派生类(也称子类),举个例子,一般形式为:
基类:
class Operator
{
public:
Operator(){std::cout << "======Operator build !" << std::endl;}
virtual ~Operator(){std::cout << "======Operator release !" << std::endl;}
virtual void print_op_name();
virtual int op_func_(int a, int b) = 0;
};
子类:
class Add : public Operator
{
public:
Add(){std::cout << "======Add build !" << std::endl;}
~Add(){std::cout << "======Add release !" << std::endl;}
virtual void print_op_name();
virtual int op_func_(int a, int b);
};
类Add继承了类Operator,所以可以去复用Operator中的成员变量和成员函数;
(1)继承方式:继承方式有public、private、protected,如果在继承时没有指定继承方式,例如:
class Add : Operator
{
.........
};
则默认是private方式,这种私有继承方式 基类指针无法指向派生类 ,正常我们选择继承方式都是public显式继承。
(2)类中的访问权限:
类中的访问修饰符有private、public 和 protected,其决定了基类的哪些成员将被派生类获取和访问,以及上面说的派生类继承基类的方式;类中的定义通常形式为:
class base {
public:
//
protected:
//
private:
//
};
在类的继承中:
在基类中,private修饰的变量,无法在派生类中访问,只能被基类成员函数访问;
在基类中,被public和protected修饰的变量,在派生类中也会对应的会变成public和protected;
不管在基类还是派生类中,变量被private修饰的,都只能被成员函数或者友元函数访问,不能在类外部访问,举个例子:
class Base
{
public:
void print() {
std::cout << num << std::endl;
}
private:
int num;
};
int main()
{
Base P;
P.num = 2;
P.print();
}
上述代码编译错误,因为被private修饰的变量num无法在类的外部被访问,而在成员函数print()中就可以访问;
这些修饰符修饰类的成员时,总结如下:
public修饰的成员可以被任意实体访问,这和c语言中的结构体成员相似;
private修饰的成员不能直接被类的实体访问,也不能被子类的实体访问,但是可以被类的成员函数访问;
protected修饰的成员不能直接被类的实体访问,但是可以被子类访问,也可以被类的成员函数访问;
二、C++类中的虚函数
c++类中的虚函数一般形式为:
virtual 返回值类型 函数名(参数表)
{ 函数体 }
和正常的函数相比,就是在开头加了个virtual修饰;类中的虚函数一个主要的作用是实现C++多态的机制(第三节详细介绍);
(1)纯虚函数
一般形式为:
virtual 返回值类型 函数名(参数表)= 0
{ 函数体 }
在类的继承中,如果基类中的某个成员函数定义为纯虚函数,则在其派生类中就应该重写这个成员函数的实现方法;
和虚函数不同的是,纯虚函数不需要在基类中定义实现的方法,而必须在派生类中实现;
虚函数则需要在基类中定义实现方法,也可以在派生类中定义实现方法;
有纯虚函数的类叫做抽象类,或者接口类。抽象类无法实例化出对象。抽象类的子类也无法实例化出对象,除非重写父类的虚函数。
(2)虚析构
虚析构一般在类中指的是virtual去修饰类的析构函数,一般形式为:
virtual ~Base();
析构函数的重要作用是为了防止内存泄漏!!
具体一点说就是,当一个基类指针指向一个派生类的对象时,当删除这个基类指针时,此时基类中的析构函数为虚析构情况下,会自动调用派生类的析构函数,释放后派生类的内存;而如果基类中的析构函数是普通析构函数时,就不会释放派生类的内存,造成内存泄漏。
举个例子:
class Operator // 基类
{
public:
Operator(){std::cout << "======Operator build !" << std::endl;}
virtual ~Operator(){std::cout << "======Operator release !" << std::endl;}
virtual void print_op_name();
virtual int op_func_(int a, int b);
public:
std::string op_name;
};
class Add : public Operator // 派生类
{
public:
Add(){std::cout << "======Add build !" << std::endl;}
~Add(){std::cout << "======Add release !" << std::endl;}
virtual void print_op_name();
virtual int op_func_(int a, int b);
};
// 主函数
int main()
{
int number = 0;
Operator *op = new Add;
op->op_name = "Add";
op->print_op_name();
number = op->op_func_(6, 3);
std::cout << "number = " << number << std::endl;
delete op;
}
若Operator类中的析构函数加了virtual 修饰(即为虚析构函数),输出结果:
若Operator类中的析构函数没有加virtual 修饰(即为普通析构函数),输出结果:
可以看到不是虚析构的情况下,派生类Add只做了构造的操作,而没有调取析构函数进行内存释放,因此造成内存泄漏。
所以,当一个类作为基类的时候,尽量加上virtual关键字!! 使之成为虚析构函数;如果此类没有派生类,可以不加。另外,子类中的虚函数也不用加。
但是有一点是,让基类的析构函数编程虚析构函数,会增加这个类的内存的占用(系统会分配一个虚函数表),如果不需要使用基类对派生类的对象操作时,可以不用使之成为虚析构函数;
三、C++多态
C++多态,即多种形态;类比一下,可以理解为我们不同的人去做同一件事情,完成同一个目的,每个人会有每个人的方法;
上面已经介绍了类中的虚函数的使用方法,多态的构成条件之一就是用到虚函数;
举个例子说明多态的实现方式:
class Operator // 基类
{
public:
Operator(){std::cout << "======Operator build !" << std::endl;}
virtual ~Operator(){std::cout << "======Operator release !" << std::endl;}
virtual void print_op_name(){
std::cout << "[Operator] print op name !" << std::endl;
}
virtual int op_func_(int a, int b){
std::cout << "[Operator] op_func_ !" << std::endl;
return a + b;
}
public:
std::string op_name;
};
class Add : public Operator // 派生类
{
public:
Add(){std::cout << "======Add build !" << std::endl;}
~Add(){std::cout << "======Add release !" << std::endl;}
virtual void print_op_name(){
std::cout << "[Add] op name : " << op_name << std::endl;
}
virtual int op_func_(int a, int b) {
std::cout << "[Add] op_func_ !" << std::endl;
return a + b;
}
};
// 主函数
int main()
{
int number = 0;
Operator *op = new Add;
op->op_name = "Add";
op->print_op_name();
number = op->op_func_(6, 3);
std::cout << "number = " << number << std::endl;
delete op;
}
执行:
可以看到实际调用的op_func_函数为Add子类中op_func_的实现;
如果把其中的op_func_函数前的virtual去掉:
class Operator // 基类
{
public:
Operator(){std::cout << "======Operator build !" << std::endl;}
virtual ~Operator(){std::cout << "======Operator release !" << std::endl;}
virtual void print_op_name(){
std::cout << "[Operator] print op name !" << std::endl;
}
int op_func_(int a, int b){
std::cout << "[Operator] op_func_ !" << std::endl;
return a + b;
}
public:
std::string op_name;
};
class Add : public Operator // 派生类
{
public:
Add(){std::cout << "======Add build !" << std::endl;}
~Add(){std::cout << "======Add release !" << std::endl;}
virtual void print_op_name(){
std::cout << "[Add] op name : " << op_name << std::endl;
}
int op_func_(int a, int b) {
std::cout << "[Add] op_func_ !" << std::endl;
return a + b;
}
};
// 主函数
int main()
{
int number = 0;
Operator *op = new Add;
op->op_name = "Add";
op->print_op_name();
number = op->op_func_(6, 3);
std::cout << "number = " << number << std::endl;
delete op;
}
执行:
可以看到op_func_调用的实现是基类Operator中的实现方式了,而print_op_name依旧是派生类中的实现;
这里派生类中的函数和基类中的函数拥有相同的函数名,参数,返回值,可以称之为派生类对基类的函数进行了重写。
综合以上,可以总结一下多态的构成条件:
- 派生类必须对基类的函数进行了重写,并且这个函数必须为虚函数;
- 必须通过基类的指针或者引用去调虚函数;
四、友元类
C++中的友元类是指一个类可以访问另一个类的私有成员,举例说明更直观:
class A
{
friend class B;
public:
A(){}
~A(){}
void print()
{
std::cout << "A name : " << a_name << std::endl;
}
private:
string a_name;
};
class B
{
public:
B() {}
~B(){}
void creat()
{
_p.a_name = "jack";
}
void print()
{
std::cout << "A name : " << _p.a_name << std::endl;
std::cout << "B name : " << b_name << std::endl;
}
public:
string b_name;
A _p;
};
int main()
{
B p;
p.b_name = "marry";
p.creat();
p.print();
return 0;
}
例中,友元类的定义形式为在A类中定义friend class B; 也就是B是A的友元类。
那么B可以访问A的私有成员。例如上面例子中,B中定义了A类的对象,在creat()函数中就可以给A类私有成员a_name赋值,print()函数中就可以打印a_name。
应用场景:在不公开自身私有成员的情况下,将这些私有成员暴露给其他类,既想保留Private属性,又想使用其他类来修改或者调用其中的参数;
使用友元类可以增加代码的灵活性,使得我们可以在不破坏封装性的情况下,让其他类或函数访问私有成员。比如,如果一个类需要访问另一个类的私有成员,但是这两个类之间没有继承关系,那么就可以将其中一个类声明为另一个类的友元类。
五、类占用的内存
c++类占用的内存计算方法:
(1)类的内存空间占用只包含变量,不包含函数;
(2)静态变量或者静态函数不占类内存空间;
(3)有虚函数的类,有一个指向虚函数表的指针,无论类中有多少虚函数,内存计算只按照一个虚函数计算,因为只有一个虚表指针(32位系统中占4字节,64位系统中占8字节);
(4)类和结构体都有内存对齐原则;
(5)空的类占一个字节;