由于最近使用java比较多,于是简单系统的复习一下C++的面向对象相关知识
一.构造函数与析构函数
这两放一起是因为它们有一些相同点:
①在类里,和类名相同(ps:析构函数名字前有个~,用以与构造函数区分),没有返回值。
②不定义的话,系统会默认生成一个空的默认形式。
不同点:
①构造函数进行专门的初始化对象用;析构函数主要是做对象释放后的清理善后工作。
②构造函数可以重载;析构函数不能重载,一个类只有一个析构函数。
③析构函数的调用顺序和构造函数的调用顺序不同。(这个用代码来说明)。
#include<cstdio>
#include<cstring>
using namespace std;
class People{
private:
char name[50];
int age;
public:
People(char *n,int a);
~People();
};
People::People(char *n,int a){
printf("调用构造函数\n");
strcpy(name,n);
age=a;
printf("%s %d\n",name,age);
}
People::~People(){
printf("调用析构函数\n");
printf("%s %d\n",name,age);
}
int main(){
People p1("张三",18);
People p2("李四",24);
return 0;
}
打印结果
发现对象p1和p2的构造函数的调用顺序以及构造函数的调用顺序是完全相反的。原因在于p1和p2对象同属局部对象,在栈区存储,则遵循“先进后出”的顺序。
二.拷贝构造函数
与构造函数相比
相同点:
①不主动定义的时候,系统也会自动生成一个
②与类名同名
不同点:
①拷贝构造函数的形参是本类对象的引用类型。
代码如下:
#include<cstdio>
#include<cstring>
using namespace std;
class People{
private:
char name[50];
int age;
public:
People(char *n,int a);
~People();
People(People &p); //拷贝构造函数
int print();
};
People::print(){
printf("%s %d\n",name,age);
}
People::People(char *n,int a){
printf("调用构造函数\n");
strcpy(name,n);
age=a;
printf("%s %d\n",name,age);
}
People::~People(){
printf("调用析构函数\n");
printf("%s %d\n",name,age);
}
People::People(People &p){
strcpy(name,p.name);
age=p.age;
}
int main(){
People p1("张三",18);
People p2("李四",24);
People p3(p2);
p3.print();
return 0;
}
结果
显然p2内容拷给了p3,两一样。
三.友元函数和友元类
1)由于类中的私有成员,只有被类里的成员函数访问,在类外是不能访问的。
2)把外部的函数声明为友元类型,赋予它可以访问类内私有成员的权利。
3)友元的对象,它可以是全局的一般函数,也可以是其他类里的成员函数,这种叫做友元函数。不仅如此,友元还可以是一个类,这种叫做友元类(把一个类A声明为另一个类B的友元类,则类A中的所有成员函数都可以访问B类中的成员)。
4)对于友元函数,只需要在类内对这个函数进行声明,并在之前加上friend关键字。这个函数就具有了独特的权限,成为友元函数。
5)友元并不属于这个类本身,无论是友元函数还是友元类。都不能使用类内的this指针,同时也不可以被继承。
代码
#include<cstdio>
#include<cstring>
using namespace std;
class People{
private:
char name[50];
int age;
public:
People(char *n,int a);
~People();
People(People &p);
int print();
friend int ageSum(People &a,People &b); //友元函数
friend class Tool;//友元类
};
People::print(){
printf("%s %d\n",name,age);
}
People::People(char *n,int a){
printf("调用构造函数\n");
strcpy(name,n);
age=a;
printf("%s %d\n",name,age);
}
People::~People(){
printf("调用析构函数\n");
printf("%s %d\n",name,age);
}
People::People(People &p){
strcpy(name,p.name);
age=p.age;
}
int ageSum(People &a,People &b){
//拥有了访问私有成员的权利
printf("年龄和:%d\n",a.age+b.age);
return a.age+b.age;
}
class Tool{
public:
int getAge(People &p){
//该类的成员函数可以访问People类的成员
printf("查询到该人的年龄为:%d\n",p.age);
return p.age;
}
};
int main(){
People p1("张三",18);
People p2("李四",24);
// People p3(p2);
// p3.print();
ageSum(p1,p2);
Tool t;
t.getAge(p1);
return 0;
}
结果
很明显,求两人的年龄和函数以及Tool类的获取年龄函数都可以访问People类中私有成员。
四.继承与派生
1)有两个类,新类拥有原有类的全部属性叫做继承。原有类产生新类的过程叫做派生。
2)原有的这个类称之为父类或基类。由基类派生出的类叫做派生类或者叫做子类
使用其的好处:
①体现面向对象的思想,更形象的表达类型之间的关系。
②派生类除了可以继承基类的全部信息外,还可以添加自己的那些不同的、有差异的信息,就像生物进化的道理一样,派生类在拥有基类的全部基础之上还将更强大。
③派生类继承到基类的成员是自动、隐藏的拥有,即不需要我们重新定义,这就节省了大量的代码,体现了代码重用的软件工程思想。
继承有三种方式:公有继承、保护继承、私有继承,区别如下表
代码如下:
#include<cstdio>
#include<string>
using namespace std;
class People{
private:
int age;
string phone;
public:
People(int a,string p){
printf("People类的构造函数调用\n");
age=a;
phone=p;
}
~People(){
printf("People类的析构函数调用\n");
}
int getInfo(){
printf("年龄:%d 电话:%s\n",age,phone.c_str());
};
};
class Student:public People{
private:
int score;
public:
//构造函数传参写法:
//派生类构造函数名(总形参表列):基类构造函数(实参表列)
Student(int a,string p,int s):People(a,p){
score=s;
printf("Student类的构造函数调用\n");
}
~Student(){
printf("Student类的析构函数调用\n");
}
int getScore(){
printf("分数:%d\n",score);
}
};
int main(){
Student s(13,"1301234578",95);
s.getInfo();
s.getScore();
}
结果如下:
发现构造函数与析构函数调用顺序如下:
构造函数调用顺序:基类->派生类
析构函数调用顺序:派生类->基类
补充:继承中的二义性问题
代码
#include <iostream>
using namespace std;
class Grandfather
{
public:
int key;
public:
};
class Father1:public Grandfather
{
};
class Father2:public Grandfather
{
};
class Grandson:public Father1,public Father2
{
};
int main()
{
Grandson A;
A.key=9;
return 0;
}
解释:Grandson类继承两个father类,会有两个key成员,这个时候如果试图使用这个key(声明为public可以直接使用),在主函数中试图赋值时候,会有“不唯一、模棱两可”的错误提示,即所谓的二义性问题发生
结果
如何避免?
使用虚基类。虚基类就是在继承的时候在继承类型public之前用virtual修饰一下 。
这样使得派生类和基类就只维护一份一个基类对象。避免多次拷贝,出现歧义。
修改后代码
#include <iostream>
using namespace std;
class Grandfather
{
public:
int key;
public:
};
class Father1:virtual public Grandfather
{
};
class Father2:virtual public Grandfather
{
};
class Grandson:public Father1,public Father2
{
};
int main()
{
Grandson A;
A.key=9;
return 0;
}
这样就可以正常运行,不会报错了。
五.多态
顾名思义:多种形态,多个样子
在面向对象程序设计中,指同样的方法被不同对象执行时会有不同的执行效果。
多态可以分为两种:
①编译时多态
编译的时候就确定了具体的操作过程
②运行时多态
在程序运行过程中才确定的操作过程
(ps:确定操作过程的就是联编,也称为绑定)
联编在编译和连接时确认的,叫做静态联编。(函数重载属于这一类)
静态联编的例子:
#include<cstdio>
#include<string>
using namespace std;
class People{
private:
int age;
string phone;
public:
People(int a,string p){
age=a;
phone=p;
}
int getInfo(){
printf("年龄:%d 电话:%s\n",age,phone.c_str());
};
};
class Student:public People{
private:
int score;
public:
//构造函数传参写法:
//派生类构造函数名(总形参表列):基类构造函数(实参表列)
Student(int a,string p,int s):People(a,p){
score=s;
}
int getInfo(){
printf("分数:%d\n",score);
}
};
int main(){
People p(13,"1301234578");
p.getInfo();
Student s(13,"1301234578",95);
s.getInfo();
People *p1;
p1=&s;
//People类型指针p1指向的Student类对象的getInfo方法
p1->getInfo();
//Student类型的对象赋给People类型的引用
People &p2=s;
p2.getInfo();
}
结果
很明显,这不是我们更期望的结果,实际上,对于指针、引用,我们更希望执行实际对象的方法,而不是因为这个指针、引用的类型而盲目的确定。
无论指针和引用为什么类型,都以实际所指向的对象为依据灵活决定。那么就要更改这种默认的静态联编的方法,采用动态联编,即在运行的时候灵活决定。
于是这里就需要使用到虚函数了。
顾名思义,就是函数前面用virtual声明的函数,如下
virtual 函数返回值 函数名(形参)
{
函数体
}
虚函数的出现,允许函数在调用时与函数体的联系在运行的时候才建立(动态联编)。
那么在虚函数的派生类的运行时候,就可以在运行的时候根据动态联编实现都是执行一个方法,却出现不同结果的效果,就样就实现了多态。
修改后的结果
把基类中的getInfo方法声明为虚函数,那么主函数中无论People类型的指针还是引用就都可以大胆调用,无用关心类型问题了。因为他们会依据实际指向的对象类型来决定调用谁的方法,来实现动态联编。
当然这个virtual还可以用在析构函数上,即虚析构函数。
声明为虚析构函数,这样就可以在用基类的指针指向派生类的对象在释放时,可以根据实际所指向的对象类型动态联编调用子类的析构函数,实现正确的对象内存释放。
代码为
#include<cstdio>
#include<string>
using namespace std;
class People{
private:
int age;
int *phone;
public:
People(int a){
age=a;
phone=new int[11];
}
~People(){
delete []phone;
printf("调用People类的析构函数\n");
}
};
class Student:public People{
private:
int score;
int *num;
public:
//构造函数传参写法:
//派生类构造函数名(总形参表列):基类构造函数(实参表列)
Student(int a,int s):People(a){
score=s;
num=new int[12];
}
~Student(){
delete []num;
printf("调用Student类的析构函数\n");
}
};
int main(){
People *p;
p=new Student(13,90);
delete p;
}
结果为
基类中没有用virtual声明的析构函数,且基类和派生类当中都有动态内存开辟,那么我们在主函数中也动态开辟内存的方式创建一个Student类,然后删除。
后果,结果调用了基类的析构函数,这样一来派生类中new出来的4*12字节的内存就会残留,造成内存泄漏
修改后(加上virtual)结果如下:
会先调用释放派生类的空间,然后再释放基类的内存空间。优雅的释放空间并结束。