C++ 类&对象 学习总结

类&对象

C++是面向对象的编程语言,类是C++的核心特性,类包含了属性和行为,而对象就是类的实例化。
例如:房屋就好比一个类,而这个类里面有特定的属性和行为,例如大小、楼层、颜色都是它的属性,开门、关门等都是它的行为。而每个具体的房屋就是房屋类的一个实例。

  • 属性(成员变量):类中定义的变量,用于存储对象的状态。
  • 行为(成员函数或成员方法):类中定义的函数,用于描述对象可以执行的操作。

封装

简单理解封装就是将属性和行为作为一个整体。
下面是一个圆类的定义和封装,class是声明类的关键字,circle是这个类的名字,c_r是类内定义的变量即属性:半径,calculrateZC是类内定义的函数即行为:计算周长。public是访问修饰符,关于访问修饰符在稍后介绍。

注意:类必须要在main函数之外定义,不能在main函数内定义。

class circle // 封装一个圆类,求周长
{
public: // 授予访问权限:公开
	int c_r; // 属性:半径
	double calculrateZC() // 行为:计算周长函数
	{
		return (2 * PI * c_r);
	}
};

对象的声明和成员访问

  • 在上述代码中,若要实例化圆类的对象只需要写:”类名 对象名“即可。

例如:circle c; // 定义了一个对象名为c的circle类对象。

  • 若要对类内的属性(变量)、方法(函数)进行访问,在实例化对象后可以通过:“对象名.属性" 、"对象名.行为”进行访问。

例如:c.c_r // 访问对象c的属性(变量)
c.calculrateZC() // 访问对象c的方法(函数)
需要注意的是:若要访问类内的方法(函数)必须后面要带括号。

下面是访问代码:

int main()
{
	circle c;
	c.c_r = 3;
	double res = c.calculrateZC();
	cout << res << endl;
}

访问修饰符

在类中 访问修饰符 可以对属性和方法进行访问权限的控制。
修饰符分别是:public(公开的)、private(私有的)、protected(受保护的)
public:若成员(属性、行为)被定义为public,则代表任何人(类内、类外)都可以访问它们。
private:若成员(属性、行为)被定义为private,则代表只有类本身(属性、行为)才可以访问它们。
protected:若成员(属性、行为)被定义为protected,则代表只有类本身(属性、行为)才可以访问它们。

注意:private和protected都是只允许类内访问,但是protected主要用于继承。关于继承在后面会介绍到。

class person
{
public:
	string name;
	void func()
	{
		name = "wjc";
		car = "tesla";
		password = 123456;
	}
protected:
	string car;
private:
	int password;
};
int main()
{
	person p;
	p.name = "hmm";
	/*p.car = "byd"; // 保护权限,类内可访问,类外不可访问
	p.password = 123;*/ // 私有权限,类内可访问,类外不可访问
	system("pause");
	return 0;
}

在如上代码中,我定义了一个person类,其中name、car、password分别是这个类的属性(变量),func是这个类的行为(函数)。在main函数里我声明了person类的实例化对象p,通过“.”进行成员name的访问,由于name的权限修饰符是public,可以在类外直接访问。但是car和password属性的权限修饰符分别是protected和private,在main函数里则无法直接访问。

注意:若不声明权限修饰符,则默认的权限都是private。

访问修饰符的作用

  • 可以自己控制读写权限
  • 可以在自己定义的读、写方法里设置有效性规则
class person
{
private:
	string name; // 可读可写
	int age = 190; // 只读
	string idol; // 只写
public:
	void setName(string name)
	{
		this->name = name;
	}
	string getName()
	{
		return name;
	}
	int getAge()
	{
		if (age <= 0 || age > 150)
			return -1;
		else
			return age;
	}
	void setIdol(string idol)
	{
		this->idol = idol;
	}
};
int main()
{
	person p;
	//p.name = "123"; // 错误:类外不可访问
	p.setName("abc");
	cout << p.getName() << endl;
	
	if (p.getAge() != -1)
		cout << p.getAge() << endl;
	else
		cout << "年龄不合法" << endl;

	p.setIdol("小明");
	system("pause");
	return 0;
}

在如上代码中,person类的属性(变量)都是私有的,类外通过 “对象.属性/行为" 不能进行访问。因此在类内定义了一个get(获取)和set(设置)方法,分别获取和设置属性(变量),并将get和set方法的权限设置为public,通过这2个方法分别对成员属性(变量)name和age进行读取和设置。在set和get方法里分别对输入和获取进行了数值的输入检测。
关于this关键字,在稍后进行介绍,现在只需要理解this->属性代表的是类内的属性,而=右边的代表的是形参。

struct和class的区别

权限不同,如果不声明权限,class默认权限是私有。struct默认权限是公共。

struct s_student
{
	int s_age = 18;
	void func() {
		cout << "anc";
	}
};
class c_student
{
	int c_age = 18;
};
int main()
{
	s_student s;
	s.s_age = 23; // struct默认是公共权限
	c_student c;
	//c.c_age = 23; // class没注明权限,默认是私有,类外不可访问
	system("pause");
	return 0;
}

作用域解析运算符::

作用域:指的是C++的变量、函数或其他标识符所能访问的范围。
作用域解析运算符:用于指定一个特定的作用域。例如:类内做函数声明定义,类外做函数实现。
例如:

class MyClass {
public:
    void myMethod();  // 方法声明
};

在MyClass类内做函数声明。在类外做函数实现。

void MyClass::myMethod() {
    // 方法的实现
}

在 方法名前面加 类名::,则代表告诉编译器做的是MyClass类的myMethod函数实现,即使不在类内,但是定义的是这个类内的函数实现。

将类作为另一个类的成员变量

C++允许将一个类嵌套在另一个类内作为该类的成员变量。参考如下代码:

class point // 点类
{
private:
	int x;
	int y;
public:
	void setX(int x);
	void setY(int y);
	int getX();
	int getY();
};
class circle // 圆类
{
private:
	// 在类中可以让另一个类作为本类的成员
	point pCenter; // 圆心 
	int r; // 半径
public:
	void setR(int r);
	int getR();
	void setCircleCenter(point pCenter); // 设置圆心
	point getCircleCenter(); // 获取圆心
};

上述代码中,分别定义了point类和circle类的属性、和函数声明。在circle类里,属性pCenter是point类的实例化对象,应有point类的所有属性和方法。这里可以抽象理解成point类是一个数据类型,定义的是point型的变量。但是这个变量有点特殊,他拥有自己的属性和行为。

访问circle类pCenter对象内的成员:
circle c;
c.pCenter.getX();

构造函数&析构函数

  • 构造函数:当创建类的对象时自动调用的特殊函数。
  • 析构函数:当类的对象被销毁时自动调用的特殊函数。

构造函数:
1、构造函数无返回值,不用写void
2、构造函数与类名相同
3、构造函数可以有函数,可以重载
4、创建对象时,默认调用且只调用一次构造函数

析构函数:
1、析构函数无返回值,不用写void
2、构造函数与类名相同,在名称前面加~
3、析构没有函数,不可以重载
4、释放对象时,默认调用析构函数

class person
{
public:
	person()
	{
		cout << "构造函数" << endl;
	}
	~person()
	{
		cout << "析构函数" << endl;
	}
};
void test()
{
	person p; // 函数的变量 形参都放在栈区,函数运行完成时,自动释放
}
int main()
{
	test();
	person p; // 存放在栈区,但是main函数没结束,所以只会调用构造函数
	system("pause");
	return 0;

上述代码中,分别在person类内定义了构造函数和析构函数,输出结果为:
在这里插入图片描述
因为test()里的变量p是函数的局部变量,存放在栈区,因此当test函数调用完成后,对象p会被立刻释放,所以输出结果为构造函数、析构函数。而main函数里的对象p也存放在栈区,所以先输出构造函数,但代码system(“pause”),这行代码使程序暂停了,所以main函数没有执行完成,因此p不会被释放,因此也不会调用析构函数。关于栈区和堆区的知识可以看我之前整理的笔记。

构造函数的分类

  • 无参构造(默认构造):没有参数。
  • 有参构造:参数可自定。
  • 拷贝构造:参数一般为类对象,传入一个对象用于创建一个新的对象副本。形参为const 类 &对象名,const是为限定原对象不被篡改,至于为什么用引用在后面会介绍,先记住即可。

注意:有参构造和拷贝构造可以看成是无参构造的重载,根据传入参数的不同来调用不同的构造函数。

class person
{
private:
	int age = 18;
public:
	person() // 默认无参构造函数
	{
		cout << "默认无参构造函数" << endl;
	}
	person(int age) // 有参构造函数
	{
		this->age = age;
		cout << "有参构造函数,age = " << age << endl;
	}
	person(const person &p) // 拷贝函数:若不理解为什么引用传入,先记住这写法,防止原数据被篡改加const限定
	{
		age = p.age; // 拷贝数据
		cout << "拷贝构造函数,age = " << age << endl;
	}
	~person()
	{
		cout << "析构函数" << endl;
	}
};
void test()
{
	person p1; // 调默认无参构造函数
	person p2(10); // 调用有参构造函数
	person p3(p1); // 调用拷贝构造函数
	person p4(); // 这行代码并没有执行,这行代码不等于person p1
}
int main()
{
	test();
	system("pause");
	return 0;
}

上述代码的输出结果为:
在这里插入图片描述
main函数里根据传入参数的不同来调用不同的构造函数。需要注意的是,拷贝构造函数是传入一个person类的对象,用于拷贝之前对象的属性值来创建一份新的对象副本。

注意:main函数里的person p4()不等价于person p4。
在C++中,允许函数内嵌套函数声明,比如void func(); 返回值类型 函数名() ,这段代码相当于函数声明,因此person p4();相当于创建一个返回值类型为person,函数名为p4的无参函数声明。

对象创建的三种方式

类 参考如上代码

  • 括号法
person p1; // 调默认无参构造函数
person p2(10); // 调用有参构造函数
person p3(p1); // 调用拷贝构造函数
  • 显示法
person pa;
person pb = person(10);
person pc = person(pb);
  • 隐式转换法
person pd = 10; // 相当于person pd(10) 或 person pd = person(10),有参构造
person pe = pd; // 相当于person pe(pd) 或 person pe = person(pd),拷贝构造

匿名对象
匿名对象声明方式:

person(14);  
person(pc); // 错误

person(14) 相当于直接生成一个对象,没有对象名,传入参数int 14,调用有参构造。前行执行结束后,系统会立即释放回收匿名对象。
person(pc) 相当于传入拷贝对象,但是此行代码会报错。注意:不要用拷贝构造函数初始化匿名对象。编译器会认为此行代码等效于person pc创建person类的无参构造,会报错 变量重定义。

拷贝构造函数调用时机

参考案例-类代码:

class person
{
private:
	int age = 0;
public:
	person()
	{
		cout << "无参构造函数" << endl;
	}
	person(int age)
	{
		this->age = age;
		cout << "有参构造函数,age = " << age << endl;
	}
	person(const person &p)
	{
		age = p.age;
		cout << "拷贝构造函数,age = " << age << endl;
	}
	~person()
	{
		cout << "析构函数" << endl;
	}
};
  • 使用一个已经创建完毕的对象初始化一个新对象
 void test01()
{
	person p1(10); // person p1 = person(10) 或 person p1 = 10
	person p2(p1); // person p2 = person(p1) 或 person p2 = p1
}
int main()
{
	test01();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

  • 以值传递的方式给函数参数传值
 void func(person p) 
{
}
void test02()
{
	person p1; // 调用默认构造
	func(p1); // 调用拷贝构造
}
int main()
{
	test02();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

上述代码中,func函数的形参接收到的是person p1,相当于形参person p = person p1(显示法),会调用拷贝构造函数。

  • 值方式返回局部对象
person test03()
{
	person p1;
	cout << (int)&p1 << endl; 
	return p1; 
}
int main()
{
	person p = test03(); 
	cout << (int)&p << endl; 
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

上述代码中,test03函数里的p1变量在函数调用完成后会释放掉栈区的内存,因此返回的是p1的副本,main函数和test03函数里的两次打印输出返回值的地址是为了验证是否返回的同一个地址。我这里输出的地址一样是因为编译器完成了返回值优化(RV0)用于减少从函数返回对象时的拷贝操作。但是实际上test03函数体内的返回值和main函数接收到的返回值地址是不一样的。
注意:在这里会输出无参构造,这谁因为RV0技术在优化时,将test03函数返回的p1副本对象直接变为p。若不优化则调用拷贝构造。

构造函数调用规则

C++编译器默认给类添加三个函数

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性进行拷贝

(1)如果自定义有参构造,则默认不提供无参构造函数,但会默认提供拷贝构造

class person
{
private:
	int age = 0;
public:
	person(int age)
	{
		this->age = age;
		cout << "有参构造函数,age = " << age << endl;
	}
	~person()
	{
		cout << "析构函数" << endl;
	}
	int getAge()
	{
		return age;
	}
};
void test01()
{
    /*person p;*/ // 当把person类的默认构造函数注释后,此行代码会报错,提示没有默认构造,但会提供默认拷贝构造
	person p(15);
	person p2(p);
	cout << "p2.age = " << p2.getAge() << endl; 
}

输出结果:
在这里插入图片描述

上述代码中,自定义了有参构造,因此默认不提供无参构造,但提供默认拷贝构造,p2接收了p创建出了一份对象副本,默认拷贝构造里将所有的属性(变量)进行赋值拷贝。

(2)如果自定义拷贝构造,则默认不提供其他构造函数

class person
{
private:
	int age = 0;
public:
	person(const person &p)
	{
		this->age = p.age;
	}
	~person()
	{
		cout << "析构函数" << endl;
	}
	int getAge()
	{
		return age;
	}
};
void test()
{
	//person p; 
	//person p(15);
}

test函数里的两个定义会报错,提示未定义无参构造和有参构造。因为自定义拷贝构造后,默认不提供其他构造。

深拷贝&浅拷贝

  • 浅拷贝:仅复制对象的成员变量的值,对于指针类型的成员变量,这意味着复制指针的值(即内存地址),而不是它指向的实际数据。
  • 深拷贝:不仅复制成员变量的值,还复制指针指向的数据。

参考案例:

class person
{
private:
	int age = 0; // 存放在栈区
	int* height; // 在堆区开辟数据
public:
	person()
	{
		cout << "无参构造函数" << endl;
	}
	person(int age,int height)
	{
		this->age = age;
		this->height = new int(height);
		cout << "有参构造函数,age = " << age << ",height = " << *this->height << endl;
	}
	~person()
	{
		// 浅拷贝:如果使用编译器的默认拷贝构造,会拷贝之前的堆区内存,在调用有参构造的析构时,会释放掉开辟的堆区内存,第二次析构时会重复释放报错
		if (height != NULL) // 若不为空则在调用析构函数时释放堆区手动开辟的内存空间
		{
			delete height;
			height = NULL; // 置空,为避免野指针
		}
		cout << "析构函数" << endl;
	}
	
	person(const person& p) // 深拷贝,重载默认拷贝构造
	{
		age = p.age; // 可以直接访问p的成员属性,因为都是在person类内
		height = new int(*p.height);
	}
	int getAge()
	{
		return age;
	}
	int getHeight()
	{
		return *height;
	}
};
void test()
{
	person p(10,180); // person p = 10; person p = person(10)
	person p2(p); // person p2 = p; person p2 = person(p) 
	cout << "p2.age = " << p2.getAge() << ",p2.height = " << p2.getHeight() << endl;
	// 自定义有参构造,默认不提供无参构造,但提供默认拷贝函数,进行 值拷贝, 这就是浅拷贝
	// 浅拷贝带来的问题是 堆区内存重复释放,要用深拷贝解决,深拷贝就是对默认拷贝构造进行重载
	// 如果属性有在堆区开辟的,必须要自己重载默认构造来解决浅拷贝的问题
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

上述代码中, 成员属性:age(局部变量,存放在栈区)、height(int指针,未做指向)
构造函数:默认构造、有参构造(函数体内把height指向开辟的堆区内存,用形参对age、*height赋值)
Get函数:分别获取age、*height的值 析构函数:调用时输出。
拷贝构造:获得person对象,对age属性赋值,height重新开辟堆内存并拷贝传入对象的height解引用。
析构函数:对象释放时,检查height指针是否为空,为空则释放堆区内存并置空。

浅拷贝带来的问题:堆区同一块内存区域重复释放,必须要用深拷贝解决。如果有属性在堆区开辟的,必须要重载默认拷贝构造函数。

初始化列表

在创建类对象时,利用构造函数对成员属性进行批量初始化值。

写法:
类名(形参1,形参2,形参……):属性1(形参1),属性2(形参2),属性……(形参……)
{ }

示例代码如下:

class person
{
public:
	int a, b, c;
	//person(int a, int b, int c)
	//{
	//	this->a = a;
	//	this->b = b;
	//	this->c = c;
	//}

	// 初始化列表
	//person() :a(10), b(20), c(30) 
	//{

	//}
	person(int a, int b, int c) :a(a), b(b), c(c) 
	{

	}
};
void test01()
{
	// 传统赋值操作
	//person p(10,20,30);

	//person p;

	person p(10, 20, 30);
	cout << "a = " << p.a << ",b = " << p.b << ",c = " << p.c << endl;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

初始化列表其实就相当于有参构造的简化写法,上述初始化列表代码等价于person(int a,int b,int c){this->a = a;this->b = b;this->c = c}

类对象作为类成员

案例代码:

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
class phone
{
public:
	string pName; // 手机品牌名
	phone(string pName)
	{
		cout << "调用phone类有参构造" << endl;
		this->pName = pName;
	}
	~phone()
	{
		cout << "调用phone类析构函数" << endl;
	}
};
class person
{
public:
	string name;
	phone p;
	person(string name, string pName) :name(name), p(pName) 
	{
		// 初始化列表,相当于this.name = name, (隐式转换)person p = pName;
		cout << "调用person类有参构造" << endl;
	}
	~person()
	{
		cout << "调用person类析构函数" << endl;
	}
};
void test()
{
	person p("wjc","iphone XS Max");
	cout << "person.name = " << p.name << ",phone.pName = " << p.p.pName << endl;
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

上述代码中, phone类含有string类型的成员变量pName、有参构造(用形参对pName赋值)、析构
person类含有string类型的成员变量name、person类对象p、初始化列表(形参name、string类型的pName对成员变量赋值)、析构
test函数内创建person类对象p,实参传入字符串,构造函数内属性name被赋值,phone类对象p被赋值字符串,调用phone类默认拷贝构造赋值

注意:
1、phone类拷贝构造形参传入字符串相当于隐式转换法phone p = "iphone XS Max"调用有参构造生成对象。
2、当其他类对象作为本类成员,构造时先构造类内的成员对象,在构造自身。
3、析构时,先析构自身,在析构类内的成员对象。

静态成员变量&静态成员函数

静态成员变量:

特点:

  • 所有对象共享同一份内存数据。
  • 编译阶段就分配内存。
  • 类内声明,类外初始化操作。
  • 静态成员变量默认访问权限是:private。

定义及初始化:

  • 定义:static 数据类型 变量名;
  • 初始化(类外):数据类型 作用域::变量名 = ?

访问方式:

  • 对象名.静态成员变量名
  • 类名::静态成员变量名

静态成员函数:

特点:

  • 所有对象共享同一份函数。
  • 静态成员函数只能访问静态成员变量。
  • 静态成员函数默认访问权限是:private。

定义:
static 返回值类型 函数名()

访问方式:

  • 对象名.静态成员函数名
  • 类名::静态成员函数名

案例代码:

 class person
{
public:
	static int a; // 静态变量声明
	int b;
	static void func()
	{
		cout << "静态成员函数调用:a = " << a << endl;
		// cout << b << endl;
	}
};
int person::a = 100; // 变量初始化
void test()
{
	person p;
	cout << p.a << endl;
	person p2; // p2对象更改a的值
	p2.a = 200;
	cout << p.a << endl; // p对象的a也一样被修改,数据是共享的

	cout << "========================" << endl;

	// 静态成员变量的两种访问方式
	// 1、对象名.静态成员变量名
	cout << p2.a << endl;
	// 2、类名::静态成员变量名
	cout << person::a << endl;
}
void test2()
{
	cout << "========================" << endl;
	person p;
	// 静态成员函数的两种访问方式
	// 1、对象名.静态成员函数名
	p.func();
	// 2、类名::静态成员函数名
	person::func();
}
int main()
{
	test();
	test2();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

注意:静态成员函数内不能调用非静态成员变量,因为非静态成员变量是针对于实例化的特定的对象,而静态成员变量是属于整个类。如果使用非静态成员变量,编译器分不清属于哪个对象。

成员函数和成员变变量分开存储

  • 空对象占用1个字节,C++编译器会对空对象也分配一个字节空间,目的是为了区分当前对象在内存中的位置。每个空对象都有独一无二的内存地址,即使是空对象,也应该能够有一个有效的地址,以便于执行例如对象定位或对象之间比较等操作。

案例代码:

class person
{
};
void test()
{
	person p;
	cout << "person = " << sizeof(p) << endl;
	cout << "person address = " << (int)&p << endl;
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

  • 静态成员变量是在编译阶段分配内存,不属于类,因此不占对象的内存空间。成员函数(包括静态成员函数、非静态成员函数)是类的所有对象共享的。无论创建多少个对象,成员函数的代码只有一份副本,因此不属于类,不占对象的内存空间。
class mainkind
{
	int m_A; // 占用4字节
	// 静态成员 在编译阶段分配内存,不属于对象
	static int s_m_A;
	static void func()
	{}
	void func2() // 属于类,不属于某个对象,0占用
	{}
};
void test()
{
	mainkind mk;
	cout << "mainkind = " << sizeof(mainkind) << endl;
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

this指针

this指针指向被调用成员函数所属的对象,用于解决非静态成员函数多个类型的对象共用同一块代码时 区分是何对象调用自己的问题。

  • 当形参和变量同名时,可以用this指针区分。
  • 在类的非静态成员函数返回自身对象时 可以用return *this。

案例代码:

class person
{
public:
	int age;
	person(int age)
	{
		this->age = age;
	}
	person& add(const person &p)
	{
		this->age += p.age;
		return *this; // 返回的是当前对象的引用
	}
	// 如果返回的是对象本体
	person add2(const person &p)
	{
		this->age += p.age;
		return *this; // 返回的是当前对象
	}
};
void test()
{
	person p(10); // 等价于person p = person(10); 或 person p = 10;
	cout << p.age << endl;
}
void test2()
{
	person p(10);
	person p2(20);
	p2.add(p).add(p); // 20 + 10 + 10 = 40
	cout << p2.age << endl;
	cout << "==========" << endl;
	p2.add2(p).add2(p); // 返回的是当前对象,在test2函数里会创建返回对象的副本 也就是相当于 40+10.40+10
	cout << p2.age << endl;
}
int main()
{
	test();
	test2();
	system("pause");
	return 0;
}

上述代码中。

  1. person类含有int类型的成员变量age、有参构造(用形参对age赋值,利用this指针)、add函数(形参是const修饰的person类引用对象,const代表不可修改原引用的值,返回值为person类对象的引用)、add2(同add函数,返回值为person类对象)
  2. add函数内对被调用成员函数的对象属性age值和传入person类对象引用的属性age值做相加,然后赋值到被调用成员函数的对象属性age值,返回被调用成员函数的对象引用。
  3. add2与add函数操作相同,返回值不一样,add2返回被调用成员函数对象本体。
  4. test函数创建person类对象p调用有参构造,对p的age属性赋值10,并做输出。
  5. test2函数分别创建person类对象p、p2,p2调用add函数,传入对象p的引用,并且返回的对象引用再次调用add函数,结果做输出。然后打印换行,p2调用add2函数,传入对象p的引用,并且返回对象副本再次调用add2函数做输出。

输出结果:
在这里插入图片描述

空指针访问成员函数

案例代码:

class person
{
public :
	int age;
	void showClassName()
	{
		cout << "this is person class" << endl;
	}
	void showClassAttribute()
	{
		cout << "this is person class attribute : age = " << age << endl; // 这里被调用时触发异常 提示this指向空指针
	}
};
void test()
{
	person *p = NULL; // 指针置空
	p->showClassName(); // 这里不报错的原因:成员函数不属于类,不依赖类实例化,没有任何类的成员变量,即使p是空指针,因为没有涉及到解引用
	system("pause");
	p->showClassAttribute(); // 变量是属于类,age存放在对象内存中,p是空指针,访问age相当于对空指针解引用,所以程序崩溃
}
int main()
{
	test();
	system("pause");
	return 0;
}

上述代码中,person类含有int类型的成员变量age,成员函数:showClassName、showClassAttribute分别做输出字符串、输出字符串和属性age的值。
test函数内创建了person类对象指针p并置空,分别调用2个成员函数,showClassName正常输出,showClassAttribute报错。

注意:报错原因是this指向空指针,调用成员变量前面默认有this关键字,返回被调用成员函数的对象,因为p是空指针,所以返回的对象指针引用为空,触发空指针异常。调用showClassName不报错的原因是因为成员函数不属于类,不依赖于类的实例化,也没有任何类的成员变量。即使p是空指针,因为没有涉及到解引用,所以不报错。而p调用showClassAttribute报错是因为age属性存放在对象内存中,p是空指针,访问age相当于对空指针解引用,所以崩溃。

常函数&常对象(const修饰成员函数)

  • 常函数:指成员函数后有const修饰的函数。如:void func() const{}。
    特点:不会修改对象的任何成员变量(包括值和址,除了被声明为 mutable 的变量,如mutable int a)
  • 常对象:指被const修饰的对象。如:const person p;。
    特点:
    1、一旦一个对象被声明为常对象,它的任何非 mutable 成员变量都不能被修改。
    2、常对象只能调用常函数,不能调用非常函数。因为常对象属性值不可以修改,而成员函数内可以修改值。避免逻辑矛盾。

案例代码:

class person
{
public:
	int age;
	mutable int b; // 在常函数内可以被修改的类型
	void func()
	{
		// this指针本质:指针常量 person * const p; 指针常量可以改变指针指向的值,不可以改变指针的指向
		this->age = 100; // 让被调用成员函数的对象属性age赋值100
		//this = NULL; // 这里就报错了,因为指针常量不可以改变指向
	};
	void func2() const
	{
		//this->age = 200; // 报错 提示必须是可修改的左值
		// 若不想改变指向也不想改变值:const person * const p; 对于成员函数来说,限定this指针的值可以在函数名()后面加const ,被成为“常函数”
		this->b = 200;
	}
	person(int age,int b)
	{
		this->age = age;
		this->b = b;
	}
};
void test()
{
	person p(10,20);
	p.func();
	cout << "age = " << p.age << ",b =" << p.b << endl;
	p.func2();
	cout << "age = " << p.age << ",b =" << p.b << endl;

}
void test2()
{
	const person p(10,20); 
	//p.age = 100; // 报错
	//p.func(); // 报错
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

this指针的本质就是指针常量,例如:person * const p;const限定的是指针p,指针常量可以改变指针指向的值,但是不可以改变指针的指向。因此func()里this=NULL这行代码会报错。
在fun2()里,函数名后面加了const限定,就变成了常函数。在常函数体内,不可以更改成员变量(值和址)。

友元

一种允许某些特定的非成员函数或其他类访问一个类的私有(private)或受保护(protected)成员。
声明方式:在需要访问私有或受保护成员的类中做类或者函数的声明,前面加关键词:friend。

注意:友元关系是不互通的,例如B是A的友元,但不代表A是B的友元。

方式:

  • 全局函数做友元
  • 友元类
  • 成员函数做友元

案例代码(全局函数做友元):

class Building
{
	friend void GoodGay(Building *building); // 友元声明:相当于告诉编译器GoodGay是Buliding类的好朋友可以访问私有属性
private:
	string bedRoom; // 卧室
public:
	string sittingRoom; // 客厅
	Building()
	{
		sittingRoom = "客厅";
		bedRoom = "卧室";
	}
};
void GoodGay(Building *building)
{
	cout << "好基友正在访问:" << building->sittingRoom << endl;
	cout << "好基友正在访问:" << building->bedRoom << endl;
}
void test()
{
	Building building;
	GoodGay(&building);
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述
案例代码(友元类):

class Building
{
	friend class GoodGay; // 友元类
private:
	string bedRoom; // 卧室
public:
	string sittingRoom; // 客厅
	Building();
};
class GoodGay
{
public:
	Building* building;
	GoodGay();
	void visit();
};
Building::Building()
{
	sittingRoom = "客厅";
	bedRoom = "卧室";
}
GoodGay::GoodGay()
{
	building = new Building(); // 堆区开辟Buliding类型的内存赋值给buliding对象
}
void GoodGay::visit()
{
	cout << "好基友正在访问:" << building->sittingRoom << endl;
	cout << "好基友正在访问:" << building->bedRoom << endl;
}
void test()
{
	GoodGay goodGay; // 创建goodGay对象调用goodgay的无参构造
	goodGay.visit();
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述
上述代码中,Building类含有默认构造(对2个属性进行赋值)和2个成员属性分别是public和private权限。
GoodGay类中构造方法对属性(Building类指针building)进行堆区内存开辟创建对象。当building对象创建完成时,调用其构造方法完成2个属性的赋值。visit函数对Building类中的2个属性进行访问输出。
在test函数内创建GoodGay类对象goodGay并调用其构造方法。然后调用visit方法对Building类属性访问。

案例代码(成员函数做友元):

class Building; // 声明Building是一个类
class GoodGay
{
public:
	Building* building;
	GoodGay();
	void visitFriend();
	void visit();
};
class Building
{
	friend void GoodGay::visitFriend(); // 友元
private:
	string bedRoom; // 卧室
public:
	string sittingRoom; // 客厅
	Building();
};
Building::Building()
{
	sittingRoom = "客厅";
	bedRoom = "卧室";
}
GoodGay::GoodGay()
{
	building = new Building(); // 堆区开辟Buliding类型的内存赋值给buliding对象
}
void GoodGay::visitFriend()
{
	cout << "好基友正在访问:" << building->sittingRoom << endl;
	cout << "好基友正在访问:" << building->bedRoom << endl;
}
void GoodGay::visit()
{
	cout << "好基友正在访问:" << building->sittingRoom << endl;
	//cout << "好基友正在访问:" << building->bedRoom << endl; // 报错,因为visit方法不是Building类的友元
}
void test()
{
	GoodGay goodGay; // 创建goodGay对象调用goodgay的无参构造
	goodGay.visitFriend();
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

注意:在代码顶部声明了Building类,因为编译器是自上而下的执行顺序,Building类需要声明GoodGay::visitFriend()是友元,所以要把GoodGay放到Building类的前面,而GoodGay类里的属性Building类指针编译器不知道是什么,所以在开头需要做一个声明,声明Building是一个类。

运算符重载&加号运算符

运算符重载:以自定义的方式为已有的运算符赋予新的功能。
重载方式:

  • 通过成员函数重载

成员函数重载声明方式:
返回值类型 operator+(被调用成员函数对象, 运算对象){实现的功能}
成员函数运算符重载的本质:
返回对象 = 被调用成员函数对象.operator+(运算对象);

  • 通过全局函数重载

全局函数重载声明方式:
返回值类型 operator+(运算对象, 运算对象){实现的功能}
全局函数运算符重载的本质:
返回对象 = operator+(运算对象,运算对象)

注意:对于内置数据类型表达式运算符是不可能改变的,例如int c = 3+5; 这种不可能改变。

案例代码:

class calculate
{
public:
	int m_a;
	int m_b;
	calculate(){} // 默认构造
	calculate(int m_a,int m_b)
	{
		this->m_a = m_a;
		this->m_b = m_b;
	}
	 // 1、通过成员函数重载+号
	calculate operator+(const calculate &c) 
	{
		calculate temp;
		temp.m_a = this->m_a + c.m_a; // this指针指向的是c1的成员属性
		temp.m_b = this->m_b + c.m_b;
		return temp; // 不能返回对象引用或者对象指针,因为在函数结束后temp会被销毁
	}
};
// 2、通过全局函数重载+号
calculate operator+(const calculate& c1, const calculate& c2)
{
	calculate temp;
	temp.m_a = c1.m_a + c2.m_a;
	temp.m_b = c1.m_b + c2.m_b;
	return temp;
}
// 3、函数重载:calculate res = calculate c + 10; 让c的2个属性m_A、m_B分别+10
calculate operator+(const calculate& c, int num)
{
	calculate temp;
	temp.m_a = c.m_a + num;
	temp.m_b = c.m_b + num;
	return temp;
}
void test()
{
	calculate res;
	calculate c1(10,20);
	calculate c2(20, 30);
	res = c1 + c2; // res是接受了temp,创建了一份对象副本。
	cout << res.m_a << "," << res.m_b << endl;
	res = c1 + 10;
	cout << res.m_a << "," << res.m_b << endl;
}
int main()
{
	// 成员函数重载+号本质:res = c1.operator+(c2);
	// 全局函数重载+号本质:res = operator+(c1,c2);
	// 注意:对于内置数据类型表达式运算符是不可能改变的,例如int c = 3+5; 这种不可能改变
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

运算符重载&左移运算符

重载方式:

  • 通过成员函数重载

一般不会这么采用,因为无法实现cout在左侧 本质上是:c.operator<<(cout)

  • 通过全局函数重载

全局函数重载声明方式:
返回值类型 operator<<(运算对象, 运算对象){实现的功能}
全局函数运算符重载的本质:
返回输出对象 = operator<<(输出对象引用,运算对象)

案例代码:

class calculate
{
	friend ostream& operator<<(ostream& c1, calculate c2);
private:
	int m_a;
	int m_b;
public:
	calculate(int m_a, int m_b)
	{
		this->m_a = m_a;
		this->m_b = m_b;
	}
};
// 2、通过全局函数重载左移运算符
ostream& operator<<(ostream &c1,calculate c2) // 想要的效果是 cout << c; 输出c.m_a和c.m_b的值
{
	// 如果需要做到cout << c,那么第一个参数必须是cout对象引用,第二个参数是calculate类对象 cout函数在ostream类里
	// 因为calculate的属性是私有的,可以让此函数作为calculate类的友元或者在类里提供get和set方法
	c1 << "m_a:" << c2.m_a << ",m_b:" << c2.m_b;
	return c1; // 为了方便后续的继续输出 返回osteam类对象cout
}
void test()
{
	calculate c(10, 20);
	cout << c << endl; // 链式编程思想
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

如上代码,全局重载左移运算符函数 需要的效果是cout << 类对象,输出对象的属性值。那么<<左侧必须是cout也就是ostream类,右侧是calculate类,因此函数形参就是ostream类对象引用,和calculate对象。返回ostream类对象的引用,满足链式编程思想,方便继续做追加输出。需要注意的是,因为calculate类的属性是私有的,可以让此函数作为calculate类的友元或者在类里提供get和set方法。代码里用的是在calculate类里声明重载函数是此类的友元来访问私有属性的。

运算符重载&递增运算符

前置递增运算符:
重载方式:

  • 通过成员函数重载

成员函数重载声明方式:
对象类型 operator++(){实现的功能}
成员函数运算符重载的本质:
返回对象 = 被调用成员函数对象.operator++();

  • 通过全局函数重载

全局函数重载声明方式:
对象类型 operator++(运算对象){实现的功能}
全局函数运算符重载的本质:
返回对象 = operator++(运算对象)

后置递增运算符:
重载方式:

  • 通过成员函数重载

成员函数重载声明方式:
对象类型 operator++(int占位参数){实现的功能}
成员函数运算符重载的本质:
返回对象 = 被调用成员函数对象.operator++(int占位参数);

  • 通过全局函数重载

全局函数重载声明方式:
对象类型 operator++(运算对象,int占位参数){实现的功能}
全局函数运算符重载的本质:
返回对象 = operator++(运算对象,int占位参数)

案例代码:

class MyInteger 
{
	friend ostream& operator<<(ostream& p1, const MyInteger& p2); // 友元,能够访问私有属性
	//friend MyInteger& operator++(MyInteger& p);
	//friend MyInteger operator++(MyInteger& p, int);
private:
	int num;
public:
	MyInteger(int num)
	{
		this->num = num;
	}
	// 1、通过成员函数重载前置运算符
	MyInteger& operator++() // 前置递增运算符重载,本质上是:对象.operator()
	{
		++num;
		return *this; // 返回的是对象引用,this指向被调用成员函数的对象。返回引用的目的是避免重复的对象副本创建和数据的一致性
	}
	// 1、通过成员函数重载后置运算符
	MyInteger operator++(int) // 后置运算符重载,固定语法 int是占位参数,形参里加了int默认告诉编译器这是后置运算符重载
	{
		// 步骤:记录对象 -> num递增 -> 返回记录的对象
		MyInteger temp = *this;
		this->num++;
		return temp; // 返回值类型是对象副本而不是引用,因为temp在函数完成后在栈区会被销毁
	}
};
// 2、通过全局函数重载前置运算符
//MyInteger& operator++(MyInteger &p)
//{
//	p.num++;
//	return p;
//}
// 2、通过全局函数重载后置运算符
//MyInteger operator++(MyInteger &p,int)
//{
//	MyInteger temp = p;
//	p.num++;
//	return temp;
//}
ostream& operator<<(ostream &p1,const MyInteger &p2) // 左移运算符重载
{
	p1 << p2.num;
	return p1;
}
void test()
{
	// 1、先要保证cout 能输出MyInterger类对象,所以要先重载左移运算符
	MyInteger myInteger(10);
	//cout << myInteger << endl;
	cout << ++myInteger << endl; // 执行顺序:++的优先级最高,优先执行++myInteger,然后在执行cout << myInteger。
	/* 如果operator++()返回值类型是MyInteger,那么每次返回都只会返回一个临时的副本,它并不会影响myInteger,因此下面的代码输出结果为
	   12、11 */
	//cout << ++(++myInteger) << endl;
	//cout << myInteger << endl;

	cout << myInteger++ << endl; 
	/* 如果要做2次后置递增运算符可以写2遍myInteger++的代码,不要写(myInteger++)++,因为返回的是副本对象,在调用重载后置递增运算符会
	   相当于是未做递增时候的副本重新做后置递增 */ 
	cout << myInteger << endl;
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

注意:
1、通过全局函数重载前置/后置自增运算符,如果要访问MyInteger类里的私有属性必须在类内声明友元。
2、重载前置运算符返回值必须是引用,如果返回的是MyInteger类对象本身,在接收时会创建这个对象的副本。
3、重载后置运算符返回值必须是对象,因为函数内记录对象的变量在函数完成后在栈区会被销毁 4、cout << ++myInteger;的执行顺序是先执行++myInteger,因为++的运算符优先级最高,然后执行cout <<。

运算符重载&赋值运算符

赋值运算符只可以通过成员函数重载,不可以通过全局函数重载。赋值运算符需要直接访问和修改对象的私有成员,且其左操作数是赋值的目标对象,使得它必须是类的一个成员。
通过成员函数重载:

成员函数重载声明方式:
对象类型 operator=(运算对象){实现的功能}
成员函数运算符重载的本质: 返回对象 = 被调用成员函数对象.operator=(运算对象);

案例代码:

class person
{
	friend void test01();
private:
	int *age;
public:
	friend ostream& operator<<(ostream& p1, person& p2);
	person(int age)
	{
		this->age = new int(age);
	}
	~person()
	{
		if (age != NULL) // 所有成员变量前面都默认加了this
		{
			delete age;
			age = NULL;
		}
	}
	// 1、通过成员函数重载赋值运算符
	person& operator=(person &p) // 本质:p1.operator(p2)
	{
		// 编译器提供默认的赋值操作:this->age = *p.age;
		// 先判断this->age是否有内存空间在堆区没被释放,如果有就清空
		if (this->age != NULL)
		{
			delete this->age;
			this->age = NULL;
		}
		this->age = new int(*p.age);
		return *this; // 满足链式编程思想返回对象引用
	}
};
// 2、不可以通过全局函数重载赋值运算符,赋值运算符需要直接访问和修改对象的私有成员,且其左操作数是赋值的目标对象,使得它必须是类的一个成员
ostream& operator<<(ostream& p1,person &p2) // 左移运算符重载
{
	p1 << *p2.age;
	return p1;
}
void test01()
{
	person p1(18);
	person p2(20);
	p1 = p2; 
	cout << p1 << endl; // 会崩溃,堆区内存重复释放了。用深拷贝的方式重载赋值运算符解决。
	
	person p3(100);
	p1 = p2 = p3; // 本质等于:p1.operator(p2).operator(p3);
	cout << p1 << endl;

	int a = 1,b = 2,c = 3;
	c = b = a;
	cout << "a = " << a << ",b = " << b << ",c = " << c << endl; // C++ 允许变量连续赋值,所以赋值运算符重载代码需要修改
}
int main()
{
	test01();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

运算符重载&关系运算符

重载方式:

  • 通过成员函数重载

成员函数重载声明方式:
逻辑型 operator==(比较对象){实现的功能}
成员函数运算符重载的本质:
逻辑型 = 被调用成员函数对象.operator==(比较对象);

  • 通过全局函数重载

全局函数重载声明方式:
对象类型 operator==(比较对象1,比较对象2){实现的功能}
全局函数运算符重载的本质:
返回对象 = operator==(比较对象1,比较对象2)

案例代码:

class person
{
	friend bool operator==(person& p1, person& p2);
private:
	string name;
	int age;
public:
	person(string name,int age)
	{
		this->name = name;
		this->age = age;
	}
	// 1、通过成员函数重载关系运算符
	//bool operator==(person &p) // 本质:当前对象.operator(比较对象);
	//{
	//	if (this->name == p.name && this->age == p.age)
	//		return true;
	//	else
	//		return false;
	//}
	bool operator!=(person& p)
	{
		if (this->name != p.name && this->age != p.age)
			return true;
		else
			return false;
	}
};
// 2、通过全局函数重载关系运算符
bool operator==(person& p1, person& p2) // 本质:operator(比较对象1,比较对象2)
{
	if (p1.name == p2.name && p1.age == p2.age)
		return true;
	else
		return false;
}
void test01()
{
	person p1("wjc",23);
	person p2(p1);
	person p3("abc",18);
	if (p1 == p2)
		cout << "true" << endl;
	else
		cout << "false" << endl;

	if(p1 != p2)
		cout << "true" << endl;
	else
		cout << "false" << endl;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

运算符重载&函数调用运算符

函数调用运算符本质上是把()赋予新的功能。

1、函数调用运算符只允许通过成员函数重载,不能通过全局函数重载函数调用运算符。
2、因为和函数调用非常像,所以又被成为 仿函数。
3、仿函数非常灵活,没有固定的写法,传入的参数根据实际需要决定。

重载方式:

  • 通过成员函数重载

成员函数重载声明方式:
返回值类型 operator()(参数根据实际需要决定是否传入){实现的功能}
成员函数运算符重载的本质:
返回值类型 = 被调用成员函数对象.operator()(参数根据实际需要决定是否传入);

案例代码:

class MyPrint
{
public:
	// 通过成员函数重载函数调用运算符,不能通过全局函数重载函数调用运算符
	void operator()(string str) // 本质上相当于:当前对象.operator(string);
	{
		cout << str << endl;
	}
};
void myPrint02(string str)
{
	cout << str << endl;
}
// 仿函数非常灵活,没有固定的写法
class MyAdd
{
public:
	int operator()(int num1, int num2)
	{
		return num1 + num2;
	}
};
void test01()
{
	MyPrint myPrint;
	myPrint("hello world"); // 因为和函数调用非常像,所以又被成为 仿函数
	myPrint("hello world");
}
void test02()
{
	MyAdd myAdd;
	cout << myAdd(10, 20) << endl;
	cout << MyAdd()(10, 20) << endl; // 相当于MyAdd()创建了一个匿名对象,当前行执行完成后自动释放,因此此行等效于 匿名对象.(10,20);
}
int main()
{
	test01();
	test02();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

注意:上述代码中,cout << MyAdd()(10, 20) << endl;
相当于MyAdd()创建了一个匿名对象,当前行执行完成后自动释放,因此此行等效于 匿名对象.(10,20);

继承

继承&基本语法:
继承通俗解释就是将被继承类的相关特征复制到到继承类里。
被继承类一般被成为父类或基类。继承类一般被成为子类或派生类。
继承好处:减少重复代码复用。

继承语法:class 子类 :继承方式 父类

案例代码:

class basePage
{
public:
	void header()
	{
		cout << "首页、公开课、登记、注册……(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图……(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java、Python、C++……(公共分类列表)" << endl;
	}
};
// Java页面继承公共内容
class Java :public basePage
{
public:
	void content()
	{
		cout << "Java学科视频" << endl;
	}
};
// Python页面继承公共内容
class Python :public basePage
{
public:
	void content()
	{
		cout << "Python学科视频" << endl;
	}
};
// C++页面继承公共内容
class Cpp :public basePage
{
public:
	void content()
	{
		cout << "C++学科视频" << endl;
	}
};
void test01()
{
	Java java;
	java.header();
	java.footer();
	java.left();
	java.content();
	cout << "========================" << endl;
	Python python;
	python.header();
	python.footer();
	python.left();
	python.content();
	cout << "========================" << endl;
	Cpp cpp;
	cpp.header();
	cpp.footer();
	cpp.left();
	cpp.content();
}
int main()
{
	test01();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

继承&继承方式

继承方式有:

  • public:可继承public、protected成员为public权限,父类private属性,子类不可访问。
  • protected:可继承public、protected成员为protected权限,父类private属性,子类不可访问。
  • private:可继承public、protected成员为private权限,父类private属性,子类不可访问。
    在这里插入图片描述

案例代码:

class father
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};
class son1 :public father // 继承方式为public,可继承public、protected成员为public权限
{
	void func()
	{
		cout << "m_A = " << m_A << endl; // 通过public继承方式继承后,m_A为public权限
		cout << "m_B = " << m_B << endl; // 通过public继承方式继承后,m_B为protected权限
		//cout << "m_C = " << m_C << endl; // 父类私有属性,子类不可访问
	}
};
class son2 :protected father // 继承方式为protected,可继承public、protected成员为protected权限
{
	void func()
	{
		cout << "m_A = " << m_A << endl; // 通过protected继承方式继承后,m_A为protected权限
		cout << "m_B = " << m_B << endl; // 通过protected继承方式继承后,m_B为protected权限
		//cout << "m_C = " << m_C << endl; // 父类私有属性,子类不可访问
	}
};
class son3 :private father // 继承方式为private,可继承public、protected成员为private权限
{
	void func()
	{
		cout << "m_A = " << m_A << endl; // 通过private继承方式继承后,m_A为private权限
		cout << "m_B = " << m_B << endl; // 通过private继承方式继承后,m_B为private权限
		//cout << "m_C = " << m_C << endl; // 父类私有属性,子类不可访问
	}
};
class grandSon3 :public son3
{
	void func()
	{
		//cout << "m_A = " << m_A << endl; // son3父类私有属性,子类不可访问
		//cout << "m_B = " << m_B << endl; // son3父类私有属性,子类不可访问
	}
};
void test()
{
	son1 s1;
	s1.m_A = 10; // public权限 可访问
	//s1.m_B = 10; // protected权限 不可访问,但是类内可访问

	son2 s2;
	//s2.m_A = 10; // protected权限 不可访问,但是类内可访问
	//s2.m_C = 10; // protected权限 不可访问,但是类内可访问

	son3 s3;
	//s3.m_A = 10; // private权限 不可访问,但是类内可访问
	//s3.m_B = 10; // private权限 不可访问,但是类内可访问
}
int main()
{
	test();
	system("pause");
	return 0;
}

继承&继承中的对象模型(private属性继承)

父类中所有的非静态成员都会被继承下去,父类的私有属性只是被编译器隐藏,但是子类中确实继承了私有属性。
查看类对象内存布局:
https://blog.csdn.net/weixin_45231931/article/details/135733306
案例代码:

class father
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};
class son1 :public father
{
	int m_D;
};
void test()
{
	// 父类中所有的非静态成员都会被继承下去,父类的私有属性只是被编译器隐藏了,但是确实被继承了
	cout << "son1 = " << sizeof(son1) << endl; // sizeof = 16
}
int main()
{
	test();
	system("pause");
	return 0;
}

在这里插入图片描述
输出结果:
在这里插入图片描述

注意:可以看到son1类的对象内存布局里,父类的属性m_A,m_B,m_C确实被继承了下去。通过sizeof关键字也可以查看到占用内存为16(4个变量)。

继承&子类和父类的构造析构顺序

顺序为子类构造->父类构造->子类析构->父类析构。
案例代码:

class base
{
public:
	base()
	{
		cout << "base构造函数" << endl;
	}
	~base()
	{
		cout << "base析构函数" << endl;
	}
};
class son :public base
{
public:
	son()
	{
		cout << "son构造函数" << endl;
	}
	~son()
	{
		cout << "son析构函数" << endl;
	}
};
void test()
{
	son s; // 父类构造->子类构造->子类析构->父类析构
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

继承&同名成员处理

若子类和父类中含有相同名称的成员

1、成员变量:

  • 在子类中直接访问成员变量默认调用子类内的变量。
  • 如需调用从父类继承下来的成员变量,需加作用域。如:子类对象.父类名::成员变量名。

2、成员函数:

  • 在子类中直接访问成员函数默认调用子类内的函数。
  • 如需调用从父类继承下来的成员函数,需加作用域。如:子类对象.父类名::成员函数名。

注意:如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有的同名成员函数。如需调用需加作用域。
例如:如果父类有函数func()和func(int),子类对象直接访问func(int),会报错。

案例代码:

class base
{
public:
	int m_Num;
	base()
	{
		m_Num = 100;
	}
	void func()
	{
		cout << "base类函数调用" << endl;
	}
	void func(int m_Num)
	{
		this->m_Num = m_Num;
		cout << "base类重载函数调用" << endl;
	}
};
class son :public base
{
public:
	int m_Num;
	son(int m_Num)
	{
		this->m_Num = m_Num;
	}
	//void func()
	//{
	//	cout << "son类函数调用" << endl;
	//}
};
void test()
{
	son s(200);
	cout << s.m_Num << endl; // 输出200,默认调用子类内的属性
	cout << s.base::m_Num << endl; // 输出100,如需子类调用父类重复属性,需要在变量名之前加作用域
	s.func(); // 默认调用子类内的函数
	s.base::func(); // 如需子类调用父类重复函数,需要在变量名之前加作用域

	// 如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有的同名成员函数。如需调用需加作用域
	//s.func(100);
	s.base::func(666);
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

继承&同名静态成员处理

若子类和父类中含有相同名称的静态成员
1、静态成员变量:

  • 通过对象访问
    在子类中直接访问静态成员变量默认调用子类内的静态变量。
    如需调用从父类继承下来的成员变量,需加作用域。如:子类对象.父类名::静态成员变量名。
  • 通过类名访问
    类名::静态成员变量名。
    如需调用从父类继承下来的成员变量,需加作用域。如:子类::父类::静态成员变量名 或 父类::静态成员变量名。

2、静态成员函数:

  • 通过对象访问
    在子类中直接访问静态成员函数默认调用子类内的静态函数。
    如需调用从父类继承下来的成员函数,需加作用域。如:子类对象.父类名::静态成员函数名。
  • 通过类名访问
    类名::静态成员函数名。
    如需调用从父类继承下来的成员函数,需加作用域。如:子类::父类::静态成员函数名 或 父类::静态成员函数名。

注意:如果子类中出现和父类同名的静态成员函数,子类的同名成员会隐藏掉父类中所有的同名静态成员函数。如需调用需加作用域。
例如:如果父类有静态函数func()和func(int),子类对象直接访问func(int),会报错。

案例代码:

class base
{
public:
	static int num;
	static void func()
	{
		cout << "父类下的func()函数调用" << endl;
	}
	static void func(int num)
	{
		cout << "父类下的func()重载函数调用" << endl;
	}
};
int base::num = 100;
class son :public base
{
public:
	static int num;
	static void func()
	{
		cout << "子类下的func()函数调用" << endl;
	}
};
int son::num = 200;
void test() // 同名静态成员变量
{
	// 1、通过对象访问
	cout << "通过对象访问同名静态成员变量:" << endl;
	son s;
	cout << s.num << endl; // 输出200,默认调用子类内的属性
	cout << s.base::num << endl; // 输出100,如需子类调用父类重复属性,需要在变量名之前加作用域
	// 2、通过类名访问
	cout << "通过类名访问同名静态成员变量:" << endl;
	cout << base::num << endl; // 或 son::base::num 通过类名访问::父类作用于下::静态成员变量 也可以 
	cout << son::num << endl;
}
void test2() // 同名静态成员函数
{
	// 1、通过对象访问
	cout << "通过对象访问同名静态成员函数" << endl;
	son s;
	s.func();
	s.base::func();
	// 2、通过类名访问
	cout << "通过类名访问同名静态成员函数" << endl;
	base::func(); // 或 son::base::func() 通过类名访问::父类作用于下::静态成员函数 也可以
	son::func();

	cout << "===========" << endl;
	// 如果子类中出现和父类同名的静态成员函数,子类的同名静态成员会隐藏掉父类中所有的同名静态成员函数。如需调用需加作用域
	//s.func(100);
	s.func();
	s.base::func(); // 或 son::base::func() 
	s.base::func(100); // 或 son::base::func(100) 
}
int main()
{
	test();
	test2();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

继承&多继承

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

注意:如果多个父类有相同的成员变量名则需要加作用域区分,否则会报错。

案例代码:

class base1
{
public:
	int m_A;
	base1()
	{
		m_A = 100;
	}
	void func()
	{
		cout << "base1类的函数调用" << endl;
	}
};
class base2
{
public:
	int m_A;
	base2()
	{
		m_A = 100;
		cout << "base2类的函数调用" << endl;
	}
	void func()
	{
		cout << "base2类的函数调用" << endl;
	}
};
class son :public base1, public base2  // 同时继承base1和base2
{
public:
	int m_C, m_D;
	son()
	{
		m_C = 300;
		m_D = 400;
	}
};
void test()
{
	cout << "son的大小为:" << sizeof(son) << endl; // 输出16
	son s;
	// 如果多个父类有相同的成员变量名则需要加作用域区分,否则会报错
	cout << s.base1::m_A << endl;
	// 如果多个父类有相同的成员函数名则需要加作用域区分,否则会报错
	s.base1::func();
	// 打开vs开发人员工具包 找到源码目录下,输入cl /d1 reportSingleClassLayout要查看的类名 源码文件名(可以按TAB选取) 查看内存布局
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

继承&菱形继承的问题及解决办法

概念:两个派生类继承同一个基类,又有某一个类同时继承两个派生类。这种继承称为菱形继承或钻石继承。
在这里插入图片描述
菱形继承问题:
1、羊和驼同时继承了基类的属性,那么羊驼继承羊和驼的相同属性,调用时会出现二义性。这个可以通过作用域解决。
2、羊驼同时继承了两份相同的数据,如何避免资源浪费问题。
菱形继承解决办法:虚继承
虚继承关键字:virtual。
虚继承语法:class 子类 :virtual 继承方式 父类
虚继承概念:在虚继承的类内,使用一个虚基指针vbptr(visual base point)指向该虚基类对应的虚基表vbtable(visual base table),vbtable里存放的是该虚基类地址到基类被继承属性的地址偏移量。
案例代码:

class animal
{
public:
	int m_Age;
};
class sheep :virtual public animal 
{
};
class camel :virtual public animal
{
};
class sheepCamel :public sheep, public camel
{
};
void test()
{
	sheepCamel sc;
	sc.sheep::m_Age = 18;
	sc.camel::m_Age = 23;
	// 当菱形继承两个父类拥有相同数据,需要加作用域以区分
	cout << "sheep m_Age = " << sc.sheep::m_Age << endl;
	cout << "camel m_Age = " << sc.camel::m_Age << endl;
	// 菱形继承会继承相同的数据,可以通过虚继承来解决资源浪费问题
	// Animal类成为 虚基类,:后面加vitual关键字 变为虚继承
	cout << "sheepCamel m_Age = " << sc.m_Age << endl;

	// 打开vs开发人员工具包 找到源码目录下,输入cl /d1 reportSingleClassLayout要查看的类名 源码文件名(可以按TAB选取) 查看内存布局
	/* 在内存布局模型里可以看到sheep和camel类下是一个指针vbptr(virtual base point 虚基类指针)他们各自的指针指向各自对应的vbtable
	   (virtual base table 虚基表)而虚基表内存放着当前地址到父类animal类里age属性的地址偏移量。
	   例如:sheep和camel类里的vbptr地址分别是0和4,父类animal的age属性存放的地址是8,那么他们各自的vbptr存放的偏移量就是8和4 */
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

注意:
1、打开vs开发人员工具包 找到源码目录下,输入cl /d1 reportSingleClassLayout要查看的类名
源码文件名(可以按TAB选取) 查看内存布局。
2、在内存布局模型里可以看到sheep和camel类下是一个指针vbptr(virtual base point 虚基类指针)他们各自的指针指向各自对应的vbtable (virtual base table 虚基表) 而虚基表内存放着当前地址到父类animal类里age属性的地址偏移量。例如:sheep和camel类里的vbptr地址分别是0和4,父类animal的age属性存放的地址是8,那么他们各自vbptr存放的偏移量就是8和4。
在这里插入图片描述

多态&使用及原理剖析

多态:通俗点解释就是允许对象以共享接口的方式展现出不同的行为。

多态分类:

  • 静态多态:函数重载、运算符重载。
  • 动态多态:继承、虚函数。

静态多态和动态多态的区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址

动态多态(虚函数)满足条件:
1、有继承关系
2、子类重写父类的虚函数

动态多态使用:
父类的指针或引用 执行子类对象

案例代码:

class animal
{
public:
	virtual void speak() // 虚函数
	{
		cout << "动物在说话" << endl;
	}
};
class cat :public animal 
{
public:
	void speak()
	{
		cout << "猫在说话" << endl;
	}
};
class dog :public animal
{
public:
	// 没有显式用virtual标记,但仍然是虚函数。当派生类重写基类中的虚函数时,即使不显式使用 virtual 关键字,依然是虚函数
	 void speak() // 相当于virtual void speak()
	{
		cout << "狗在说话" << endl;
	}
};
void doSpeak(animal &a) // 形参接收animal类的引用
{
	a.speak();
}
void test()
{
	cat c;
	doSpeak(c); // C++允许
	dog d;
	doSpeak(d);

	animal& a = c;
	doSpeak(a);

}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

注意:派生类没有显式用virtual标记,但仍然是虚函数。当派生类重写基类中的虚函数时,即使不显式使用 virtual 关键字,依然是虚函数。

原理剖析:
在这里插入图片描述

如上图里,当animal类的speak函数是虚函数时,类的内存里存放的是vfptr指针(visual function point),这个指针指向了虚函数表vftable(visual function table)。虚函数表内记录虚函数的地址,也就是&animal::speak()。当派生类重写了基类的虚函数speak时,虚函数表里的地址就变为了该派生类的speak函数地址。当基类的指针或引用指向该派生类对象时,发生多态。

以下是派生类重写speak函数前后内存布局图:
重写前:
在这里插入图片描述

如上图,cat类的虚函数表里存放的是基类的虚函数speak函数地址。

重写后:
在这里插入图片描述

如上图,cat类的虚函数表里存放的是cat类重写的虚函数speak函数地址。

多态案例1-计算器类

文章地址:https://blog.csdn.net/weixin_45231931/article/details/135761333

多态&纯虚函数和抽象类

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

注意:只要有一个纯虚函数,这个类就成为抽象类。

抽象类特点:
1、无法实例化对象。
2、抽象类的子类 必须要重写父类中的纯虚函数 否则也属于抽象类。

案例代码:

class base // 抽象类
{
	virtual void func() = 0; // 纯虚函数
};
class son1 :public base
{
};
class son2 :public base
{
	void func(){}
};
void test()
{
	// 只要有一个纯虚函数,这个类就成为抽象类
	/* 抽象类特点:
	   1、无法实例化对象
	   2、抽象类的子类 必须要重写父类中的纯虚函数 否则也属于抽象类 */

	//base b; // 报错
	//base* bp = new base; // 报错
	//son1 s1; // 报错
	//son1* s1p = new son1; // 报错
	son2 s2;
	son2* s2p = new son2;
}
int main()
{
	system("pause");
	return 0;
}

多态案例2-制作饮品类

文章地址:https://blog.csdn.net/weixin_45231931/article/details/135761505

多态&虚析构和纯虚析构

虚析构和纯虚析构:用来解决父类指针无法释放子类对象(无法调用子类对象的析构函数)问题。

注意:
1、拥有纯虚析构的类为抽象类,无法被实例化。
2、若子类中没有堆区数据,不涉及释放内存,则可以不写虚析构或纯虚析构。

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

纯虚析构语法:
【类内定义】virtual ~类名() = 0;
【类外实现,也可在类内实现但不常见】类名::~类名(){}

案例代码:

class animal
{
public:
	virtual void speak() = 0; // 纯虚函数
	animal() {
		cout << "animal类构造函数" << endl;
	}
	//~animal() {
	//	cout << "animal类析构函数" << endl;
	//}

	// 虚析构:
	//virtual ~animal(){
	//	cout << "animal类虚析构函数" << endl;
	//}
	// 纯虚析构:1、类内声明 2、类外实现
	virtual ~animal() = 0;
};
animal::~animal() 
{
	cout << "animal类纯虚析构函数" << endl;
}
/* 虚析构和纯虚析构区别:若类内有春纯虚析构,则该类是抽象类,无法被实例化 */
class cat :public animal 
{
public:
	string *m_name;
	void speak()
	{
		cout << *m_name << "猫在说话" << endl;
	}
	cat(string m_name) {
		this->m_name = new string(m_name); // 在堆区开辟内存
		cout << "cat类构造函数" << endl;
	}
	~cat() {
		cout << "cat类析构函数" << endl;
		delete m_name;
		m_name = NULL;
	}
};
void test()
{
	animal* a = new cat("Tom");
	a->speak();
	delete a; // 当父类指针指向子类对象时,释放父类指针只会调用父类的析构函数,子类对象并没有被调用析构函数释放
	// 解决办法:在父类构造虚析构或纯虚析构
}
int main()
{
	test();
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述

多态案例3-电脑组装

文章地址:https://blog.csdn.net/weixin_45231931/article/details/135784785

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值