一、拷贝构造函数(复制构造函数)
1、概念
拷贝构造函数,它只有一个参数,参数类型是本类的引用。如果类的设计者不写拷贝构造函数,编译器就会自动生成拷贝构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,让目标对象的每个成员变量都变得和源对象相等。编译器自动生成的拷贝构造函数称为“默认拷贝构造函数”。
2、格式
class 类名{
public:
//普通构造函数
类名()
{
}
//拷贝构造函数 ,参数是本类的引用
//如果我们没有自定义,那么编译器就自动生成默认的拷贝构造函数
类名(const 类名 &a)
{
}
};
3、例子
#include<iostream>
#include<cstring>
using namespace std;
class Student{
public:
Student(const char *name,int age);//普通构造函数
~Student();
//如果没有定义拷贝构造函数,编译器就自动生成默认拷贝构造函数--浅拷贝---值的拷贝
/* Student(Student &a)
{
this->name = a->name;
this->age = age;
} */
//自定义的拷贝构造函数,深拷贝
Student(Student &a)
{
cout<<"Student(Student &a)"<<endl;
//分配内存空间
this->name = new char[256];
strcpy(this->name,name);
this->age = age;
}
private:
char *name;
int age;
};
Student::Student(const char *name,int age=20) //name 记录的是 数据段.rodata 中的"张3" 内存空间起始地址
{
cout<<"Student(char *name,int age=20)"<<endl;
//分配内存空间
this->name = new char[256];
strcpy(this->name,name);
this->age = age;
}
Student::~Student()
{
cout<<"~Student()"<<endl;
delete []this->name;
}
int main()
{
//根据参数调用普通构造函数
Student mya("张3");
//如果是在定义一个对象的时候通过另一个对象来初始化,那么会调用拷贝构造函数
//Student myb(mya);
Student myb = mya;
//打印内存空间的地址
cout<<"mya.name:"<<static_cast<const void *>(mya.name)<<endl;
cout<<"myb.name:"<<static_cast<const void *>(myb.name)<<endl;
return 0;
}
4、什么时候需要自己定义拷贝构造函数
当类的数据成员中 有指针成员的时候,需要申请内存空间
5、什么时候会调用到拷贝构造函数
在定义一个对象通过另一个对象来初始化,那么会调用拷贝构造函数
6、深拷贝 和 浅拷贝
浅拷贝:只拷贝对象本身空间里面的内容
深拷贝:拷贝对象本身空间内容的同时,还要分配成员指向的堆空间并且进行拷贝
二、类的组合
1、概念
一个类的对象作为另外一个类的数据成员。
也就是说,两个及以上相互独立的类能够放在一起,然后通过一个类就可以调用另一个类的对象从而调用另一个类的功能。
2、案例
一台电脑computer和一台打印机DaYinJi两个独立的类,computer有显示、算数,DaYinJi有打印功能,当组合在一起后,对于computer类就可以调用打印机的打印功能。
#include <iostream>
using namespace std;
//打印机类
class DaYinJi{
public:
DaYinJi(){
cout<<"DaYinJi()"<<endl;
}
~DaYinJi(){
cout<<"~DaYinJi()"<<endl;
}
DaYinJi(int data):a(data)
{
cout<<"DaYinJi(int data)"<<endl;
}
int a;
void DaYin()
{
cout<<"正在打印....."<<endl;
}
};
class Demo{
public:
Demo(int c){
cout<<"Demo()"<<endl;
}
~Demo(){
cout<<"~Demo()"<<endl;
}
};
//类的组合
/*
需要解决的问题??
1、在类的组合中,如何指定 内嵌对象的构造函数??? 在本类的构造函数的初始化列表上指定内嵌对象的构造函数
2、构造函数的执行顺序 是 打印机类的构造函数 再到 电脑类的构造函数
*/
//电脑类
class Computer{
public:
//在本类的构造函数的初始化列表上指定内嵌对象的构造函数
Computer():e(2000),d(1000)
{
cout<<"Computer()"<<endl;
}
~Computer(){
cout<<"~Computer()"<<endl;
}
void exec(){
d.DaYin();
}
private:
//私有成员 是 另一个类的对象 --如何去指定 对象的构造函数???
//内嵌对象
//如果有多个内嵌对象,那么多个内嵌对象的 构造函数的执行顺序跟 定义的先后顺序有关 跟 构造函数指定的先后顺序 无关
DaYinJi d;
Demo e;
};
int main()
{
//实例化一个电脑类的对象
Computer c1;
//我要用电脑打印文件
c1.exec();
return 0;
}
3、构造函数调用顺序
在构造函数的列表中指定内嵌对象的构造函数的形式
如果不指定内嵌对象构造函数的形式,则调用的默认构造函数
顺序:内嵌对象 ==》本类
多个内嵌对象
顺序:按照定义内嵌对象的先后顺序调用内嵌对象的构造函数
【注意】不能在构造函数的初始化列表中初始化内嵌对象成员
4、析构函数调用顺序
顺序:本类==》内嵌对象
练习1:设计一个圆形类(Circle),和一个点类(Point),计算点和圆的关系(判断该点是在圆内、圆外还是圆上)。
#include <iostream>
using namespace std;
//点类
class Point
{
public:
//设置X坐标
void setX(int x) {
m_x = x;
}
//获取X坐标
int getX() {
return m_x;
}
//设置Y坐标
void setY(int y){
m_y = y;
}
//获取Y坐标
int getY(){
return m_y;
}
private:
int m_x;
int m_y;
};
//设置一个圆类Circle
class Circle
{
public:
//设置半径
void setR(int r){
m_R = r;
}
//获取半径
int getR(){
return m_R;
}
//设置圆心
void setCenter(Point center) {
m_center = center;
}
//获取圆心
Point getCenter() //m_center是Piont类的数据
{
return m_center;
}
private:
int m_R;
//在类中可以让另一个类 作为本类中的成员--与结构体相似
Point m_center;
};
//判断点和圆的关系
void isInCircle(Circle &c, Point &p)
{
if ((p.getX() - c.getCenter().getX()) * (p.getX() - c.getCenter().getX()) + (p.getY() - c.getCenter().getY()) * (p.getY() - c.getCenter().getY()) == c.getR() * c.getR())
cout << "点在圆上" << endl;
else if ((p.getX() - c.getCenter().getX()) * (p.getX() - c.getCenter().getX()) + (p.getY() - c.getCenter().getY()) * (p.getY() - c.getCenter().getY()) > c.getR() * c.getR())
cout << "点在圆外" << endl;
else
cout << "点在圆内" << endl;
}
int main()
{
//创建并设置点P1
Point P1;
P1.setX(10);
P1.setY(9);
//创建并设置点P2--圆心
Point P2;
P2.setX(10);
P2.setY(0);
//设置圆C1
Circle C1;
C1.setR(10);
C1.setCenter(P2);
isInCircle(C1, P1);
system("pause");
return 0;
}
三、类的继承
1、概念
新的类(子类)从已知的类(父类)中得到已有的特征的过程
新类叫派生类/子类
已知的类叫基类/父类
2、作用
继承可以减少重复的代码。比如父类已经提供的方法,子类可以直接使用,不必再去实现。
class 子类名:继承方式 父类(继承方式省略默认是私有继承)
{
子类成员
};
4、例子
#include<iostream>
using namespace std;
//父类/基类
class Base{
public:
Base(){cout<<"Base()"<<endl;}
~Base(){cout<<"~Base()"<<endl;}
void setValue(int value){
base_a = value;
}
int getValue(){
return base_a;
}
void showValue(){
cout<<"base_a:"<<base_a<<endl;
}
int base_a;
};
class Child:public Base{
public:
Child(){
cout<<"Child()"<<endl;
}
~Child(){
cout<<"~Child()"<<endl;
}
};
int main()
{
Child mya;
//直接调用基类的函数成员
mya.setValue(100);
mya.showValue();
return 0;
}
5、什么情况下用继承
基类和派生类之间存在is-a关系
6、总结
- 在派生类的构造函数的初始化列表中指定基类的构造函数
- 构造函数调用顺序:基类–》派生类
- 析构函数调用顺序:派生类–》基类
练习2:设计一个人的类(基类),再分别设计一个学生类(子类) 和 教师类 (子类)单继承 人类
人类:
属性:姓名、年龄、性别
方法:吃饭、睡觉
学生类:
属性:姓名、年龄、性别、分数、学号
方法:吃饭、睡觉、打游戏、学习
教师类:
属性:姓名、年龄、性别、教龄、工作类别(教的是语文还是数学还是英语)
方法:吃饭、睡觉、学习、备课
四、继承方式
继承方式有三种: 公有继承(public)、保护继承(protected)、私有继承(private)
上面表格权限是基类中的成员继承到子类后的成员权限
-
如果派生类在继承基类的时候选择 公有继承(public)
那么基类的公有成员就是在派生类中也是充当公有成员,可以直接在派生类的内部和外部使用
那么基类的保护成员就是在派生类中也是充当保护成员,可以在派生类的内部使用,但是不能外部使用
那么 基类的私有成员可以继承,但是不能直接访问,可以通过基类的公有方法和保护方法间接访问 -
如果派生类在继承基类的时候选择 保护继承(protected)
那么基类的公有成员就是在派生类中是充当保护成员,可以在派生类的内部使用,但是不能外部使用
那么基类的保护成员就是在派生类中是充当保护成员,可以在派生类的内部使用,但是不能外部使用
那么 基类的私有成员可以继承,但是不能直接访问,可以通过基类的公有方法和保护方法间接访问 -
如果派生类在继承基类的时候选择 私有继承(private)
那么基类的公有成员就是在派生类中是充当私有成员,可以在派生类的内部使用,但是不能外部使用
那么基类的保护成员就是在派生类中是充当私有成员,可以在派生类的内部使用,但是不能外部使用
那么 基类的私有成员可以继承,但是不能直接访问,可以通过基类的公有方法和保护方法间接访问
#include<iostream>
using namespace std;
//基类
class Base{
public:
Base(int data=100):baseData(data){
cout<<"Base()"<<endl;
}
~Base(){
cout<<"~Base()"<<endl;
}
void setData(int data){
baseData = data;
}
int getData(){
return baseData;
}
protected:
void showData(){
cout<<"baseData:"<<baseData<<endl;
}
private:
int baseData;
};
class Child:private Base{
public:
Child(int data=20):Base(data){
showData();
cout<<"Child()"<<endl;
}
~Child(){
cout<<"~Child()"<<endl;
}
};
int main()
{
Child mya(200);
//mya.setData(1000);
return 0;
}
四、总结:
类的组合(has-a)和类的继承(is-a)是面向对象编程中两种重要的关系,它们在概念、使用场景、实现方式等方面存在显著的区别。以下是这两者的详细对比:
一、概念区别
类的组合(has-a):
指的是一个类(或对象)包含另一个类的对象作为其属性。这种关系强调的是“拥有”或“包含”的关系。
例如,一个汽车类可以包含一个发动机类的对象作为属性,表示汽车拥有发动机。
类的继承(is-a):
指的是一个类(子类)继承另一个类(父类)的属性和方法。这种关系强调的是“是一种”的关系。
例如,一个猫类可以继承一个动物类,表示猫是动物的一种。
二、使用场景区别
类的组合:
当一个类需要复用另一个类的功能,但又不希望与其产生过于紧密的联系时,使用组合。
组合关系有助于降低类之间的耦合度,提高系统的灵活性和可维护性。
例如,在设计一个图形界面时,一个窗口类可以包含多个按钮类的对象,而不需要通过继承来实现。
类的继承:
当一个类需要继承另一个类的属性和方法,并且这些属性和方法对于子类来说具有普遍意义时,使用继承。
继承关系有助于实现代码的重用和扩展,但也可能导致类之间的耦合度过高,增加系统的复杂性。
例如,在设计一个动物类库时,可以使用继承来定义不同种类的动物,如猫、狗等,它们都继承自动物类。
三、实现方式区别
类的组合:
在组合类中,通过属性来持有另一个类的对象。
可以通过这个属性来调用被包含类的方法,但不需要继承其所有方法和属性。
组合关系通常是在运行期确定的,因为可以在运行时动态地创建和销毁被包含类的对象。
类的继承:
在继承关系中,子类继承父类的所有非私有属性和方法(在Java等语言中,私有属性和方法不能被继承)。
子类可以覆盖(Override)父类的方法,以提供自己的实现。
继承关系在编译期就已经确定,因为子类需要知道父类的所有接口才能正确地实现它们。
四、其他区别
耦合度:组合关系是一种松耦合关系,而继承关系则是一种紧耦合关系。松耦合关系有助于降低系统各部分之间的依赖程度,提高系统的灵活性和可维护性。
多态性:在继承关系中,可以实现类型的回溯(即子类对象可以被当作父类对象来使用),从而实现多态性。而在组合关系中,通常不具备这种特性。
设计原则:在面向对象设计中,通常建议优先考虑组合而不是继承。因为组合关系更加灵活,可以更好地应对变化。而继承关系则可能导致系统结构过于复杂和僵化。
综上所述,类的组合和类的继承在面向对象编程中各有其独特的优势和适用场景。在实际应用中,应根据具体需求和设计原则来选择合适的关系来实现代码的组织和复用。