3、面向对象

C++面向对象的三大特性为:封装、继承、多态

C++认为万事万物都皆为对象,对象上有其属性和行为,和java特别相似

封装

语法class 类名{ 访问权限: 属性 / 行为 };

例子:

#include<iostream>

using namespace std;
#define PI 3.14
//圆类
class  Circle {
	//声明访问权限
	public:
		//圆的半径
		int r;
		//计算圆的周长
		double computePerimeter() {
			return 2 * PI * r;
		}
};

int main() {
	Circle c;
	c.r = 20;
	double per = c.computePerimeter();
	cout << per << endl;
	return 0;
}

权限修饰符

cpp中修饰类成员的修饰符一共有三个:public、private、protected,没有java中的缺省,如果不写,默认是private

public:公有成员在程序中类的外部是可访问的

private:私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。

protected:受保护成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。

例子:

class  MyClass {
	int a; //默认是private
	private: // 私有
		int b;
	protected: //受保护
		int c;
	public: //公共
		void func() {

		}
};

类和结构体的区别

在C++中 struct和class唯一的区别就在于 默认的访问权限不同

区别:

对象的初始化

C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置

构造函数和析构函数

对象的初始化和清理也是两个非常重要的安全问题

1、一个对象或者变量没有初始状态,对其使用后果是未知

2、同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

c++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。

构造函数

语法:类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
  5. 可以声明权限修饰符
析构函数

语法:~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
  5. 可以声明权限修饰符

例子:

#include<iostream>

using namespace std;

class Person {
	public:
		Person() {
			cout << "无参构造函数" << endl;
		}
		Person(int a) {
			cout << "有参构造函数" << endl;
		}
		~Person() {
			cout << "析构函数,在对象销毁前调用" << endl;
		}
};

int main() {
	//调用无参构造
	Person p1;
	//调用有参构造
	Person p2(10);
	system("pause");
	return 0;
}

构造函数的分类和调用

例子:

#include<iostream>

using namespace std;

class Person {
	private:
		int age;
	public:
		//无参(默认)构造
		Person() {
			cout << "无参构造函数" << endl;
		}
		//有参构造
		Person(int a) {
			age = a;
			cout << "有参构造函数" << endl;
		}
		//拷贝构造
		Person(const Person & p) {
			age = p.age;
			cout << "拷贝构造函数" << endl;
		}

		int getAge() {
			return age;
		}
};

int main() {
	//调用无参构造
	Person p1;
	//调用有参构造,括号法调用
	Person p2(18);
	//调用拷贝构造,显式法
	Person p3 = Person(p2);
    //调用拷贝构造,隐式转换法
    Person p4 = p2;
	cout << p3.getAge() << endl;
	system("pause");
	return 0;
}

构造函数调用规则

默认情况下,c++编译器至少给一个类添加3个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

深拷贝与浅拷贝

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

静态成员分为:

例子:

#include<iostream>

using namespace std;

class Person {
	public:
		//静态变量也是有权限修饰符的,而且在类内只可以进行声明
		static int TYPE;
		//静态函数也是由权限修饰符,但是可以在类内进行初始化
		static void func() {
			cout << "静态函数" << endl;
		}
};
//类外进行静态变量初始化,使用作用域运算符::,需要声明变量类型
int Person::TYPE = 20;

int main() {
	//进行调用,使用作用域运算符::
	cout << Person::TYPE << endl;
	Person::func();
	system("pause");
	return 0;
}

C++对象模型和this指针

在C++中,类内的成员变量和成员函数分开存储

只有非静态成员变量才属于类的对象上

class Person {
public:
	Person() {
		mA = 0;
	}
	//非静态成员变量占对象空间
	int mA;
	//静态成员变量不占对象空间
	static int mB; 
	//函数也不占对象空间,所有函数共享一个函数实例
	void func() {
		cout << "mA:" << this->mA << endl;
	}
	//静态成员函数也不占对象空间
	static void sfunc() {
	}
};

int main() {

	cout << sizeof(Person) << endl;

	system("pause");

	return 0;
}
this指针

在C++中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码,那么问题是:这一块代码是如何区分那个对象调用自己的呢?

c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针,本质是指针常量,也就是指针的指向是不可以修改的

this指针不需要定义,直接使用即可,和java中的this意义一样,只不过使用方式不同

this指针的用途:

语法:this -> 非静态成员

例子:

#include<iostream>
#include<string>

using namespace std;

class Person {
private:
	int age;
	string name;
public:
	//和java中的getter、setter一样
	void setAge(int age) {
		//this->代表,当前对象的
		this->age = age;
	}
	void setName(string name) {
		this->name = name;
	}
	int getAge() {
		return age;
	}
	string getName() {
		return name;
	}
	string getInfo() {
		return "姓名:" + name + ",年龄:" + to_string(age);
	}
};


int main() {
	Person person;
	person.setAge(18);
	person.setName("Lucy");
	cout << person.getInfo() << endl; //姓名:Lucy,年龄:18
	system("pause");
	return 0;
}
空指针调用成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针

如果用到this指针,需要加以判断保证代码的健壮性

例子:

#include<iostream>
#include<string>

using namespace std;

class Person {
private:
	int age = 12;
	string name = "Lucy";
public:
	void showMsg() {
		cout << "你好" << endl;
	}
	void showName() {
		cout << this->name << endl;
	}
	void showAge() {
		cout << age << endl;
	}
};

int main() {
	Person * person = NULL;
	person->showMsg(); // 正常运行,因为函数为调用this的成员
	person->showAge(); // 报错,因为this是NULL
	person->showName(); // 报错,因为this是NULL
	system("pause");
	return 0;
}

常函数和常对象

常函数
常对象

例子:

#include<iostream>
#include<string>

using namespace std;

class Person {
private:
	int a;
	mutable int b;
public:
	void test1() const{
		//a = 10; 常函数中,普通的成员变量不可以进行修改
		//只有添加mutable的成员变量,才可以在常函数中进行修改
		b = 10;
		cout << b << endl;
	}
	void test2() {
		cout << "不是常函数" << endl;
	}
};

int main() {
	//常对象
	const Person person;
	person.test1();
	//person.test2(); 常对象只可以调用常函数
	system("pause");
	return 0;
}

友元

在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术

友元的关键字为 :friend

友元的三种实现

全局函数做友元

#include<iostream>

using namespace std;

class Person {
	//声明了全局函数test,可以访问Person的私有属性
	friend void test(Person * person);
private:
	int a = 10;
	int b = 20;
};
//全局函数test
void test(Person * person) {
	//可以访问Person对象的私有成员
	cout << person->a << endl;
	cout << person->b << endl;
}

int main() {
	Person person;
	test(&person);
    return 0;
}

类做友元

#include<iostream>

using namespace std;

class Person {
	//PersonFriend类作为Person类的友元
	friend class PersonFriend;
private:
	int a = 10;
	int b = 20;
};

class PersonFriend {
private:
	Person person;
public:
	PersonFriend(Person & person) {
		this->person = person;
	}
	void showPerson() {
		//PersonFriend类中可以访问Person对象的私有成员
		cout << person.a << endl;
		cout << person.b << endl;
	}
};


int main() {
	Person person;
	PersonFriend f(person);
	f.showPerson();
	return 0;
}

其他类成员函数做友元

这个相对麻烦,顺序需要限制

#include<iostream>

using namespace std;

//必须按照这个顺序进行,先声明Person
class Person;
//然后声明友元函数所在类,其中友元函数只声明,不定义
class PersonFriend{
public:
	void showPerson();
};
//定义Person类,以及友元
class Person {
	friend void PersonFriend::showPerson();
private:
	int a = 10;
	int b = 20;
};
//定义友元函数
void PersonFriend::showPerson() {
	Person* person = new Person;
	cout << person->a << endl;
}
int main() {
	PersonFriend f;
	f.showPerson();
	return 0;
}

继承

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。

这个时候我们就可以考虑利用继承的技术,减少重复代码

继承的基本语法

class A : public B; 

A 类称为子类 或 派生类

B 类称为父类 或 基类

例子:

#include<iostream>

using namespace std;

//人类
class Person {
public:
	void personMsg() {
		cout << "人可以吃饭、睡觉" << endl;
	}
};
//老师类,继承于人类
class Teacher : public Person {
public:
	void teacherMsg() {
		cout << "老师可以教书育人" << endl;
	}
};

int main() {
	Teacher t;
	//老师类对象可以调用人类对象的方法
	t.personMsg();
	t.teacherMsg();
	return 0;
}

继承方式

继承方式一共有三种

无论哪种继承方式,父类中私有的成员,子类都无法访问

image-20211225133536120

继承中的对象模型

#include<iostream>

using namespace std;

class A {
public:
	int a = 10;
protected:
	int b = 20;
private:
	int c = 30;
};
class B : public A {
public:
	int d = 40;
};

int main() {
	//也就是说,父类里面的所有非静态的成员都会被子类继承
	//虽然父类中private的属性子类无法访问,只是被编译器隐藏了,但是确实被继承下去了
	cout << sizeof(B) << endl; //16

	return 0;
}

也可以使用Visual Studio的工具来查看

image-20211225135302537

打开工具窗口后,定位到当前CPP文件的位置

然后输入: cl /d1 reportSingleClassLayout查看的类名 所属文件名

例如,查看demo1.cpp中的B类

image-20211225135734083

image-20211225135821899

继承中构造和析构的顺序

构造:先父类后子类

析构:先子类后父类

#include<iostream>

using namespace std;

class A {
public:
	A() {
		cout << "父类A的构造函数" << endl;
	}
	~A() {
		cout << "父类A的析构函数" << endl;
	}
};
class B : public A {
public:
	B() {
		cout << "子类B的构造函数" << endl;
	}
	~B() {
		cout << "子类B的析构函数" << endl;
	}
};

int main() {

	//创建子类B的对象
	B b;
	/*
		父类A的构造函数
		子类B的构造函数
		子类B的析构函数
		父类A的析构函数
	*/

	return 0;
}

继承中同名的成员处理

#include<iostream>

using namespace std;

class A {
public:
	int num = 10;
	void test() {
		cout << "父类的test" << endl;
	}
};

class B : public A {
public:
	int num = 20;
	void test() {
		cout << "子类的test" << endl;
	}
};

int main() {
	B b;
	//访问子类中的num
	cout << b.num << endl;
	//访问父类中的num
	cout << b.A::num << endl;
	//访问子类的test()
	b.test();
	//访问父类的test()
	b.A::test();

	return 0;
}

多继承语法

C++允许一个类继承多个类

语法: class 子类 :继承方式 父类1 , 继承方式 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议用多继承

例子:

#include<iostream>

using namespace std;

class A {
public:
	int num = 10;
};

class B {
public:
	int num = 20;
};

class C : public A , public B{
public:
	int num = 30;
};

int main() {
	C c;
	
	cout << c.num << endl;

	cout << c.A::num << endl;

	cout << c.B::num << endl;

	return 0;
}

菱形继承问题

菱形继承概念

两个派生类继承同一个基类

又有某个类同时继承者两个派生类

这种继承被称为菱形继承,或者钻石继承

image-20211225142434048

菱形继承问题

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
    
  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
    

image-20211225143656273

利用工具,可以看到,YangTuo类继承了两份age

#include<iostream>

using namespace std;

//动物
class Animal {
public:
	int age;
};
//羊
class Yang : public Animal {

};
//驼
class Tuo : public Animal {

};
//羊驼
class YangTuo : public Yang, public Tuo {

};

int main() {

	YangTuo y;
	//问题1: 报错,age不明确
	//y.age = 10;

	//问题2:虽然可以解决问题1,但是一个羊驼不可能有两个变量
	y.Yang::age = 18;
	y.Tuo::age = 28;
	cout << y.Yang::age << endl; //18
	cout << y.Tuo::age << endl; //28

	return 0;
}

利用虚继承virtual解决

#include<iostream>

using namespace std;

//动物
class Animal {
public:
	int age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
//羊
class Yang :virtual public Animal {

};
//驼
class Tuo :virtual public Animal {

};
//羊驼
class YangTuo : public Yang, public Tuo {

};

int main() {

	YangTuo y;
	y.age = 18;
	cout << y.Yang::age << endl;
	cout << y.Tuo::age << endl;
	cout << y.age << endl;

	return 0;
}
此时采用工具查看

age只有一份了

image-20211225144356139

此时vbptr(v-virtual;b-base;ptr-pointer):虚基类指针,指向vbtable(虚基类表),即下方显示的,该指针的偏移量4,刚好就是这个表里面的唯一的数据

多态

多态分为两类

静态多态和动态多态区别:

早绑定:

#include<iostream>

using namespace std;
//动物
class Animal {
public:
	void eat() {
		cout << "动物在吃饭" << endl;
	}
};
//猫,继承于动物
class Cat : public Animal{
public:
	void eat() {
		cout << "猫吃猫粮" << endl;
	}
};
//狗,继承于动物
class Dog : public Animal {
public:
	void eat() {
		cout << "狗吃狗粮" << endl;
	}
};
//使用父类引用指向子类对象
void eatTest(Animal & animal) {
	animal.eat();
}
int main() {

	Cat cat;
	eatTest(cat); //动物在吃饭,原因是eatTest()中函数的地址是早绑定,在编译时已经确定

	return 0;
}

晚绑定,在函数前添加virtual关键字:

#include<iostream>

using namespace std;
//动物
class Animal {
public:
	//添加关键字virtual,可以使函数虚绑定,也就是在运行时,绑定函数
	virtual void eat() {
		cout << "动物在吃饭" << endl;
	}
};
//猫,继承于动物
class Cat : public Animal{
public:
	void eat() {
		cout << "猫吃猫粮" << endl;
	}
};
//狗,继承于动物
class Dog : public Animal {
public:
	void eat() {
		cout << "狗吃狗粮" << endl;
	}
};
//使用父类引用指向子类对象
void eatTest(Animal & animal) {
	animal.eat();
}
int main() {

	Cat cat;
	eatTest(cat); //猫吃猫粮
	Dog dog;
	eatTest(dog); //狗吃狗粮

	return 0;
}

多态满足条件

重写

函数返回值类型 、函数名、参数列表、完全一致称为重写

多态使用条件

多态的底层原理

#include<iostream>

using namespace std;

class A {
public:
	void test() {

	}
};
class B {
public:
	virtual void test() {

	}
};

int main() {
	//普通的成员函数是不和类放在一起的,所以A只有类的占位空间1
	cout << sizeof(A) << endl; //1
    //对于虚函数,类中记录的虚函数的指针地址,所以占4
	cout << sizeof(B) << endl; //4


	return 0;
}

image-20211225153006668

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;

当类中有了纯虚函数,这个类也称为抽象类

抽象类特点

例子:

#include<iostream>

using namespace std;

class Person {
public:
	int a = 10;
	virtual void eat() = 0;
};

class Student : public Person {
public:
	void eat() {
		cout << "学生吃饭" << endl;
	}
};

int main() {
	Student s;
	cout << s.a << endl;
	s.eat();
}

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

​ 1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

​ 2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构

​ 3. 拥有纯虚析构函数的类也属于抽象类

虚析构和纯虚析构共性:

虚析构和纯虚析构区别:

虚析构语法:virtual ~类名(){}

纯虚析构语法:virtual ~类名() = 0;然后再定义:类名::~类名(){}

虚析构

#include<iostream>
#include<string>

using namespace std;

class A {
public:
	// 虚析构
	virtual ~A() {

	};
	virtual void msg() {

	};
};

class B : public A {
public:
	string * name = new string("haha");
	//子类析构,清空堆区的name
	~B() {
		cout << "子类B的析构函数被调用" << endl;
		if (name != NULL) {
			delete(name);
		}
	}
	void msg() {
		cout << "重写的msg函数" << endl;
	}
};

int main() {
	// 父类指针,指向子类
	A * a = new B;
	a->msg();
	delete a;
	system("pause");
	return 0;
}

纯虚析构

必须要声明并定义纯虚析构,因为父类也有可能有属性在堆区

#include<iostream>
#include<string>

using namespace std;

class A {
public:
	// 纯虚析构
	virtual ~A() = 0;
	virtual void msg() = 0;
};
//定义纯虚析构
A::~A(){}
class B : public A {
public:
	string * name = new string("haha");
	//子类析构,清空堆区的name
	~B() {
		cout << "子类B的析构函数被调用" << endl;
		if (name != NULL) {
			delete(name);
		}
	}
	void msg() {
		cout << "重写的msg函数" << endl;
	}
};

int main() {
	// 父类指针,指向子类
	A * a = new B;
	a->msg();
	delete a;
	system("pause");
	return 0;
}