C++面向对象复习

由于最近使用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)结果如下:

会先调用释放派生类的空间,然后再释放基类的内存空间。优雅的释放空间并结束。

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值