黑马C++学习笔记——核心编程篇

目录

1 内存分区模型

1.1 程序运行前 

1.2 程序运行后

1.3 new操作符

2  引用

2.1 引用的基本使用

2.2 引用的注意事项

2.3 引用做函数参数

2.4 引用做函数的返回值

2.5 引用的本质

2.6 常量引用

3 函数提高

3.1 函数默认参数

3.2 函数占位参数

3.3 函数重载

3.3.1 函数重载概述

3.3.2 函数重载注意事项

4 类和对象

4.1 封装

4.1.1 封装的意义

4.1.2 struct 和 class区别

4.1.3 将成员属性设置为私有

4.1.4 类的作用域

4.1.5 类的实例化

4.1.6 类对象的存储方式

4.2 对象的初始化和清理

4.2.1 构造函数和析构函数

4.2.2 构造函数的分类及调用

4.2.3 拷贝构造函数调用时机

4.2.4 构造函数调用规则

4.2.5  深拷贝与浅拷贝

4.2.6 初始化列表参数

4.2.7 类对象作为类成员

4.2.8 静态成员

4.3 C++对象模型和this指针

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

4.3.2 this指针概念

4.3.3 空指针访问成员函数

4.3.4 const修饰成员函数

4.4 友元

4.5 运算符重载

4.5.1 加号运算符重载(+)

4.5.2 左移运算符的重载(<<)

4.5.3 递增运算符重载(++)

4.5.4 赋值运算符重载(=)

4.5.5 关系运算符重载(<,>,=,!=,大于等于,小于等于)

4.5.6 函数调用运算符重载

4.6 继承

4.6.1 继承的基本语法

4.6.2 继承方式

4.6.3 继承中的对象模型

4.6.4 继承中构造和析构顺序

4.6.5 继承同名成员处理方式

4.6.6 继承同名静态成员处理方式

4.6.7 多继承语法

4.6.8 菱形继承

4.7 多态

4.7.1 多态的基本概念

4.7.2 多态案例—计算器类

4.7.3 纯虚构函数和抽象类

4.7.4 多态案例二——制作饮品

4.7.5 虚析构和纯虚析构

4.7.6 多态案例三—电脑组装

5 文件操作

5.1 文本文件

 5.1.1 写文件

 5.1.2 读文件 

5.2 二进制文件

5.2.1 写文件

5.2.2 读文件


1 内存分区模型

1.1 程序运行前 

1.2 程序运行后

1.3 new操作符

2  引用

       引用通俗的理解就是给一个变量再起一个名字(别名),用别名操作或者原名操作均操作的是同一块内存。

2.1 引用的基本使用

在这里插入图片描述

int main()
{
  int a=10;
  int &b=a;//创建引用

  cout<<"a="<<a<<endl;
  cout<<"b="<<b<<endl;

  b=100;//用别名修改内存数据
   
  cout<<"a="<<a<<endl;//100
  cout<<"b="<<b<<endl;//100

  system("pause");
  return 0;
}

2.2 引用的注意事项

  •  一个变量可以有多个多个引用:比如我有个变量a,你可以给其取个别名b,也可以取个别名c,甚至给别名c再取别名d都可以,并且这些别名和a的地址均是一样的,改变其中一个,其它的也会随之改变

int main()
{

 int a=10;
 int &b=a;

 //1、引用必须初始化
 //int &b;//错误的,引用必须初始化


 //2、引用在初始化后,不可以改变
 int c=20;
 b=c;//赋值操作,而不是更改引用

 cout<<"a= "<<a<<endl;
 cout<<"b= "<<b<<endl;
 cout<<"c= "<<c<<endl;//20

 system("pause");
 return 0;
}

   b=c,相当于把 b指向的内存也赋值为20,b和a指向的是同一块内存,因此a也为20。

2.3 引用做函数参数

      函数传参有两种,一种是值传递,一种是地址传递。值传递时,形参不可以修饰实参,地址传递时形参可以修饰实参。当学习了引用后,就可以用引用的技术用形参修饰实参,从而简化指针。

//2.3 引用做函数参数
// 
// 
//1、值传递
void mySwap01(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;

	/*cout << "swap01 a= " << a << endl;
	cout << "swap01 b= " << b << endl;*/
}


///2、地址传递
void mySwap02(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}


//3、引用传递
void mySwap03(int &a, int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

int main()
{
	int a = 10;
	int b = 20;

	//mySwap01(a, b);//a=10,b=20,未发生改变—值传递,形参不会修饰实参

	//mySwap02(&a, &b);//a=20,b=10,——地址传递,形参会修饰实参

	mySwap03(a, b);//a=20,b=10,,——引用传递,形参会修饰实参

	cout << "a= " << a << endl;
	cout << "b= " << b << endl;

	system("pause");
	return 0;
}

引用相当于起别名,用别名修改和原名修改均操作的是同一块内存。

用指针传参时,使用时还得解引用,不容易理解。

总结:通过引用传参产生的效果同地址产地是一样的,引用的语法更清楚简单。

2.4 引用做函数的返回值

作用:引用是可以作为函数的返回值存在的。

注意:不要返回局部变量引用。

用法:函数调用作为左值。

(1)引用做函数返回值不要返回局部变量的引用

原因:局部变量在函数调用结束后就会销毁,继续调用则会出错。


//2.4引用做函数的返回值

//1、不要返回局部变量的引用
int& test01()
{
	int a = 10;//局部变量存放在四区中的 栈区(函数运行完则被释放)
	return a;
}


int main()
{
    //不能返回局部变量的引用
	int &ref = test01();

	cout << "ref=  " << ref <<endl;//10,第一次结果正确,是因为编译器做了保留
	cout << "ref=  " << ref << endl;//乱码,第二次结果错误,因为a的内存已经被释放

	system("pause");
	return 0;
}

 (2)函数的调用可以作为左值

//2、函数的调用可以作为左值
int& test02()  //函数相当于返回的变量值
{
	static int a = 10;
	return a;
}
int main()
{

	//如果函数做左值,那么必须返回引用
	int& ref2 = test02();
	cout << "ref2=  " << ref2 << endl;
	cout << "ref2=  " << ref2 << endl;
	cout << "ref2=  " << ref2 << endl;

	test02() = 1000;  //a=1000;如果函数的返回值是引用,这个函数调用可以作为左值存在

	cout << "ref2=  " << ref2 << endl;//ref2为a的别名
	cout << "ref2=  " << ref2 << endl;

	system("pause");
	return 0;

传值返回:

传值返回是有讲究的。正如这段代码:

int Count()
{
	int n = 0;
	n++;
	return n;
}
int main()
{
	int ret = Count();
	return 0;
}

在传值返回的过程中会产生一个临时变量(类型是int),如果这个临时变量小它会用寄存器替代,如果大就不会用寄存器替代。

具体返回的过程是先把函数的n拷贝给临时变量,再把临时变量拷贝给ret

为什么要设计这个临时变量呢?

上述代码不可以直接把n返回给ret,这里我们简要画个栈帧图即可看出:

 main函数里有个变量ret,汇编时会call一个指令跳到函数Count,Count里有一个变量n。这里不能把n直接传给ret,因为在函数Count调用完成后要拿一个值赋给ret,且函数调用完后函数栈帧就销毁了,所以赋给ret的这个值就是设计出的临时变量。

如何证明我这中间会产生临时变量呢?只需要加个引用即可。

这里很明显编译发生错误。ret之所以出错不就是因为其引用的是临时变量呢,因为临时变量具有常性,只读不可修改,直接引用则会出现上文所述的权限放大问题。 所以这不很巧合的验证了此函数调用中途会产生临时变量。

解决方法也很简单,保持权限不变即可,即加上const修饰:
 

 传引用返回

对上面的代码进行微调:

int& Count()
{
	int n = 0;
	n++;
	return n;
}
int main()
{
	int ret = Count();
	return 0;
}

这里加上了引用&后,中间也会产生一个临时变量,只是这个临时变量的类型是int&。我们把这个临时变量假定为tmp,那么此时tmp就是n的别名,再把tmp赋值给ret。这个过程不就是直接把n赋给ret吗。这里区分于传值返回的核心就在于传引用的返回就是n的别名

如何证明传引用返回的是n的别名?

只需要在函数调用时加个引用即可。

 通过打印来验证:

这里ret和n的地址一样,也就意味着ret其实就是n的别名。综上,传值返回和传引用的返回的区别如下:

  • 传值返回:会有一个拷贝。
  • 传引用返回:没有这个拷贝了,返回的直接就是返回变量的别名 。

传引用的代码对不对?

我传引用返回后,ret就是n的别名,但是有没有想过,出了函数出了这个作用域我n不是都销毁了吗,怎么还会有别名呢?

空间的销毁不是说空间就不在了。空间的归还就好比你退房,虽然你退房了,但是这个房间还是在的,只是说使用权不是你的了。但是假说你在不小心的情况下留了一把钥匙,你依旧是可以进入这个房间,不过你这个行为是非法的。这个例子也就足矣说明了上述的代码是有问题的。是一个间接的非法访问。

这里第一次打印ret的值为1,第二次打印的ret为随机值,这就是因为发生了覆盖。这里你第一次打印是正常的,随后打印完后,函数栈帧销毁,此时又打印了其它东西,又会函数调用覆盖了原来函数的位置,当你第二次打印ret的值时自然就是随机值了。

综上这种情况是不能进行引用返回的

如何正确使用引用返回?加上static即可。

int& Count()
{
	static int n = 0;
	n++;
	cout << "&n: " << &n << endl;
	return n;
}
int main()
{
	int& ret = Count();
	cout << ret << endl;
	cout << "&ret: " << &ret << endl;
	cout << ret << endl;
	return 0;
}

加上了static后就把n放在了静态区了,出了作用域不会销毁,自然而然可以正确使用引用返回了,并且输出结果也是我们预期的:

 注意:

如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。否则就可能会出越界问题。

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int& ret = Add(1, 2);
	Add(3, 4);
	cout << "Add(1, 2) is :" << ret << endl;  //7
	return 0;
}

这段代码执行的结果ret的值为7,首先我Add(1,2),调用完后,返回c的别名给ret,随即调用完Add栈帧销毁,当我第二次调用时c的值就被修改为7了,这里想表达的是这里是不安全的。也就是不能返回局部变量的引用。

正常情况下我们应该加上static:

 加上static后这里ret的值就是3了,因为加上了static初始化只有一次。此时c在静态区了,销毁栈帧它还在

例子演示:

加static :

不加static:

传值、传应用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
	TestRefAndValue();
}

 值和引用的作为返回值类型的性能比较

#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
	TestReturnByRefOrValue();
}

引用和指针的区别

引用和指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

接下来就上述指针与引用不同点做详细解析:

  • 引用在定义时必须初始化,指针没有要求
int& r; //err 引用没有初始化
int* p; //right 指针可以不初始化
  • 在sizeof中含义不同:引用结果为引用类型的大小,但直至始终时地址空间所占字节个数(32位平台下占4个字节)。
	double d = 2.2;
	double& r = d;
	cout << sizeof(r) << endl; //8
  • 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

 在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

int main()
{
	int a = 10;
	//语法角度而言:ra是a的别名,没有额外开空间
	//底层的角度:它们是一样的方式实现的
	int& ra = a;
	ra = 20;
	//语法角度而言:pa存储a的空间地址,pa开了4/8字节的空间
	//底层的角度:它们是一样的方式实现的
	int* pa = &a;
	*pa = 20;
	return 0;
}

我们来看下引用和指针的汇编代码对比:

通过反汇编我们可以看出:引用是按照指针方式来实现的。

2.5 引用的本质

本质:引用的本质在C+内部实现是一个指针常量。

指针常量:int * const,指针的指向不可以改,指向的值可以改。(引用一旦初始化就不可以改变)

//2.5 引用的本质
//发现是引用,转换为 int *const ref=&a;
void func(int& ref)
{
	ref = 100;//ref是引用,转换为*ref=100;

}
int main()
{
	int a = 10;

	//自动转换为int *const ref=&a; 指针的指向不可以改,也说明为什么引用不可以更改
	int& ref = a;

	ref = 20;//内部发现ref是引用,自动帮我们转换为 *ref=20;

	cout << "a: " << a << endl;
	cout << "ref: " << ref << endl;

	func(a);

    cout << "a: " << a << endl;
	cout << "ref: " << ref << endl;
	return 0;

}

结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了。

2.6 常量引用

作用:常量引用主要用来修饰形参,防止形参误操作。

在函数形参列表中,可以加const修饰形参,防止形参改变实参

//2.6 常量引用
 
//使用场景:用来修饰行参,防止误操作
//打印数据函数
//引用使用的场景,通常用来修饰形参
void showValue(const int &val)
{
	//val=1000;不能修改
	cout << "val=  " << val << endl;
}

int main()
{
	// int & ref =10;  //错误
	
	加上const之后,编译器将代码修改  int temp=10;  int &ref=temp;
	
	const int& ref = 10;//引用必须引用一块合法的内存空间,而10为常量,存在常量区——前面必须加const
	
	ref = 20;//错误,加入const之后变为只读,不可以修改


	//函数中利用常量引用防止误操作修改实参
	int a = 100;
	showValue(a);
	cout << "a=  " << a << endl;

	system("pause");
	return 0;
}

取别名规则:

在取别名的时候,不是在所有情况下都可以随便取的,要在一定范围内。

对原引用变量,权限只能缩小,不能放大

权限放大error:

我们都清楚C语言有个const,在C++的引用这一块也是有const引用的

假如我们现在有const修饰过的变量x,现在想对x取别名,还能像如下的方式进行吗?

//权限放大 err
const int x = 20;
int& y = x;

这就是典型的权限放大,学过C语言我们都清楚,const是只读的,对于变量x,我们只可以进行读,不能进行修改。而此时我们对x引用成y,且是int型的,此时y是可读可写的,不满足x的只读条件。

那怎么样才能对x进行引用呢?只需要确保权限不变即可,见下文:

权限不变:

想要控制权限不变非常简单,只需要对x引用的同时加上const修饰即可,让变量y也是只读的

//权限不变
const int x = 20;
const int& y = x;

 那如果变量没有加const修饰,但是在引用时加了const可以吗?这就是权限缩小,看下文:

权限缩小:

//权限缩小
int c = 30;
const int& d = c;

我们针对上述代码进行编译,发现编译器没有任何报错,答案是可以的。

这里的c是可读可写的,对c进行const引用,顶多就是把c改变为只读的,只是权限缩小,不违反要求,当然是可以的。

拓展1:如何给常量取别名

可以给常量取别名吗?     int& c = 20; // err

其实是不可以直接进行取别名的,但是我们加上const就可以了:const int& c = 20; // right

拓展2:临时变量具有常性

double d = 2.2;
int& e = d;

上面代码编译出错。但是加上const,发现它竟然就不会出错了:

double d = 2.2;
const int& e = d;

怎么解释上述代码呢?这就需要我们先回顾下C语言的类型转换

C++本身是在C语言的基础上走的,C语言在相似类型是允许隐式类型转换的。大给小会截断,小给大会提升。看如下代码:

double d = 2.2;
int f = d;

 这里在把d的值赋给f时并不是直接赋值的,会把d的整数部分取出来,赋值给一个临时变量,该临时变量大小4个字节,随后再把这个临时变量给给f。

 临时变量具有常性,就像被const修饰了一样,不能被修改。

因此,上文的这段代码要加上const才能编译通过:

double d = 2.2;
const int& e = d;

这里e引用的是临时变量,临时变量具有常性,不能直接引用,否则就是放大了权限,加上const才能保证其权限不变。

3 函数提高

3.1 函数默认参数

在C++中,函数的形参列表中的形参是可以有默认值的。

语法:返回类型   函数名(参数=默认值){  }

//3. 函数的默认参数
//函数默认参数

//如果我们自己传入数据,就用自己传入的数据,如果没有,那么用默认值
int func(int a, int b=20, int c=30)
{
	return a + b + c;
}

//注意事项
//1、如果某个位置已经有了默认值,那么从这个位置往后,从左向右,必须都要有默认值
//int func2(int a = 10, int b, int c, int d)//错误
//{
//	return a + b + c;
//}

//2、如果函数声明有默认值,函数实现的时候就不能有默认值
// 声明和事项只能有一个有默认参数
int func2(int a=10, int b=10);

int func2(int a, int b)
{
	return a + b;
}
 
int main()
{
	//cout << func(10, 30) << endl;//70,按传入的算
	
	//func(10,20,30);

	cout << func2(10, 10) << endl;
	system("pause");
	return 0;
}

 总结:1、如果某个位置已经有了默认值,那么从这个位置往后,从左向右,必须都要有默认值;

2、如果函数声明有默认值,函数实现的时候就不能有默认值。(声明和实现只能有一个有默认参数)。

3.2 函数占位参数

C++函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置。

语法:返回类型  函数名(数据类型){  }      只写数据类型,不写数据

在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术。

//3.2 函数占位参数

//占位参数,没有用到
//占位参数,还可以有默认参数
void func(int a, int)
{
	cout << "this is func" << endl;
}


int main()
{
	func(10,10);//占位参数必须填补
	system("pause");
	return 0;
}

3.3 函数重载

3.3.1 函数重载概述

作用:函数名可以相同,提高复用性。

函数重载满足条件:

(1)同一个作用域。

(2)函数名称相同。

(3)函数参数  类型不同  或者  个数不同  或者  顺序不同 。

 注意:函数的返回值不可以作为函数重载的条件。 

//全局作用域

void func()
{
	cout << "func 的调用" << endl;
}

void func(int a)
{
	cout << "func(int a) 的调用" << endl;
}

void func(double a)
{
	cout << "func(double a)的调用" << endl;
}

void func(int a,double b)
{
	cout << "func(int a,double b)的调用" << endl;
}

void func(double b,int a)
{
	cout << "func(double b,int a)的调用" << endl;
}

int func(double b,int a)
{
	cout << "func(double b,int a)的调用" << endl;
}
int main()
{
	func();//调用最上面的

	func(10);

	func(3.14);

	func(10, 3.14);

	func(3.14, 10);
	system("pause");
	return 0;
}

注意事项:函数的返回值不可以作为函数重载的条件

其他参数相同,仅函数返回值类型不同,会发生错误,产生二义性。

3.3.2 函数重载注意事项

(1)引用作为重载条件;

(2)函数重载碰到函数默认参数。

//1、引用作为重载条件
void func(int& a)  //int &a=10;不合法
{
	cout << "func(int &a)调用" << endl;
}

void func(const int& a)// const int &a=10;合法
{
	cout << "func(const int &a)调用" << endl;
}


int main()
{
	//int a;
	func(a);//调用无const

	func(10);//调用有const

	system("pause");
	return 0;
}

//2、函数重载碰到默认参数
void func2(int a,int b=10)
{
	cout << "func2(int a)的调用" << endl;
}

void func2(int a)
{
	cout << "func2(int a)的调用" << endl;
}

int main()
{
	

	func2(10);//错误,两个均可以调用——当函数重载碰到默认参数,出现二义性,尽量避免


	system("pause");
	return 0;
}

4 类和对象

4.1 封装

4.1.1 封装的意义

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

类的定义

class className
{
    // 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号

  • class为定义类的关键字ClassName为类的名字{}中为类的主体注意类定义结束时后面分号
  • 类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数

封装的意义

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

清楚C语言的数据和方法是分离的,我们以栈为例:

//C语言的数据和方法是分离的
struct Stack
{
	int* _a;
	int _top;
	int _capacity;
};
void StackInit(struct Stack* ps)
{}
void StackPush(struct Stack* ps, int x)
{}
int StackTop(struct Stack* ps)
{}
int main()
{
	struct Stack st;
	StackInit(&st);
	StackPush(&st, 1);
	StackPush(&st, 3);
	StackPush(&st, 5);
	return 0;
}

像C语言这样数据和方法分离会存在一个巨大的问题:太过自由!我没办法去对它很好的管理,就比如我现在想访问栈顶的数据,如若是C语言,我们就可以这样写:

	printf("%d\n", StackTop(&st));  //规范
	printf("%d\n", st._a[st._top]); //不规范

按理来说,第一行的代码才是正确的,合理的,中肯的访问形式,第二行代码直接用结构去操纵成员,虽然也可以,但是会涉及到一个问题:对top到底是栈顶元素还是栈顶元素的下一个位置?
如果初始化里的top给的是0,那st.top就是栈顶元素的下一个位置,会越界,如果是-1,那么刚好就是栈顶元素)此时就会出现误用。相反直接调用StackTop函数就完全不会出现这个问题,因为我在之前就已经处理好了,返回的值必是栈顶元素。

  • 由此可见,C语言在这一方面太过自由了,接下来来看看C++是如何来解决的。

这就需要用到封装了。C++设计出了类,类里除了可以定义成员变量,还可以定义函数,所以我们就可以把上述栈的初始化,插入,取栈顶全放到类里,也就实现了数据和方法封装到一起。

//1、数据和方法封装到一起,类里面
//2、想给你自由访问的设计成公有,不想给你访问的设计成私有
class Stack
{
public:
	void Init()
	{}
	void Push(int x)
	{}
	int Top(struct Stack* ps)
	{}
private:
	int* _a;
	int _top;
	int _capacity;
};

并且此段代码还实现了我想给你访问的就设计成公有,不想给你访问的就设计成私有。此时就完美避免了C语言过渡自由懒散的弊端,此时就不能再像C语言那样直接访问成员变量了,而只能用成员函数。

int main()
{
	Stack st;
	st.Init();
	st.Push(1);
	st.Push(2);
	st.Push(3);
	st.Push(4);
	cout << st.Top() << endl;
	//cout << st._a[st._top] << endl; //err
	return 0;
}

C++通过强制的就能让用的人更规范。并且一般情况设计类,成员数据都是私有或保护,想给你访问的函数是公有,不想给你访问的是私有或保护。

综上,C++的封装本质上就是一种更严格的管理。

1、圆类 

//4.1.1 封装

//圆周律
const double PI = 3.14;
//设计一个圆类,求圆的周长
//class代表设计一个类,类后面紧跟着的就是类名
class Circle
{
	// 访问权限
	//公共权限
public:

    //属性(通常为一些变量)
	int m_r;

	//行为(通常为一些函数)
	//获取圆的周长
	double calculateZC()
	{
		return 2 * PI * m_r;
	}
};
int main()
{
	//同构圆类创建具体的圆()对象
    //实例化(通过一个类创建一个对象的过程)
	Circle cl;
	//给圆对象的属性进行赋值
	cl.m_r = 10;
	//2*PI*10=62.8
	cout << "圆的周长为:" << cl.calculateZC() << endl;
	system("pause");
	return 0;
}

 2、学生类

设计一个学生类,属性有姓名和学号,可以给学生和学号赋值,可以显示学生的姓名和学号。 

#include<iostream>
using namespace std;
#include<string>

//设计学生类
class Student
{
	//权限(公共权限)
public:
	//属性
	string m_Name;//姓名
	int m_Id;//学号

	//行为
	//显示姓名和学号
	void showStudent()
	{
		cout << "姓名: " << m_Name << " 学号: " << m_Id << endl;
	}
	//给姓名赋值
	void setName(string name)
	{
		m_Name = name;
	}
	//给学号赋值
	void setId(int id)
	{
		m_Id = id;
	}
};

int main()
{
	//创建一个具体学生,实例化对象
	Student s1;
	//给s1对象  进行属性赋值操作
	//s1.m_Name = "张三";
	s1.setName("张三");//用行为给属性赋值

	//s1.m_Id = 1;//用对象给属性直接赋值
	s1.setId(1);//用行为给属性赋值
	//显示学生信息
	s1.showStudent();
	   
	Student s2;
	s2.m_Name = "李四";
	s2.m_Id = 2;
	s2.showStudent();


	system("pause");
	return 0;
}

类中的属性和行为 统称为成员。

属性:成员属性 成员变量

行为:成员函数 成员方法 

公共权限 public       成员  类内可以访问   类外可以访问
保护权限 protected  成员 类内可以访问,类外不可以访问,儿子也可以访问父亲中的保护内容
私有权限 private      成员 类内可以访问,类外不可以访问,儿子不可以访问父亲的私有内容

注意:

  1. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
  2. 如果后面没有访问限定符,作用域就到}类结束。
  3. class的默认访问权限为private,struct为public(因为struct要兼容C)。
  4. 注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
//访问权限
//三种
//公共权限 public 成员类内可以访问 类外可以访问
//保护权限 protected 成员类内可以访问,类外不可以访问,儿子也可以访问父亲中的保护内容
//私有权限 private  成员 类内可以访问,类外不可以访问,儿子不可以访问父亲的私有内容
class Person
{
public:
	//公共权限
	string m_Name;//姓名
protected:
	//保护权限
	string m_Car;//汽车
private:
	//私有权限
	int  m_Password;//银行卡密码

public:
	void func()
	{
		m_Name = "张三";
		m_Car = "拖拉机";
		m_Password = 123456;
	}
};
int main()
{
	//实例化具体对象
	Person p1;
	p1.m_Name = "李四";
	//p1.m_Car = "奔驰";//保护权限内容,在类外访问不到
	//p1.m_Password = 123;//私有权限内容,类外访问不到
	system("pause");
	return 0;
}

4.1.2 struct 和 class区别

问题:C++中struct和class的区别是什么?

解答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private。

class C1
{
	int m_A;//默认权限 是 私有

};
struct C2
{
	int m_A;//默认权限 是 公共
};
//struct和class的区别
   //struct 默认权限是 公有 public
   //class  默认权限是 私有 private
int main()
{
	C1 c1;
	//c1.m_A = 100;//class中的属性默认为私有,类外不可以访问

	C2 c2;
	c2.m_A = 100;//在class默认的权限是公共,因此可以访问

	system("pause");
	return 0;
}

4.1.3 将成员属性设置为私有

优点1:将所有成员属性设置为私有,可以自己控制读写权限。

优点2:对于写权限,我们可以检测数据的有效性。

//将成员变量设置为私有的两优点案例
#include<string>
class Person
{
public:    //侧面的提供一个公有函数对私有的成员属性进行操作

	//设置姓名(写姓名)
	void setName(string name)
	{
		m_Name = name;
	}
	//获取姓名
	string getName()
	{
		return m_Name;
	}
	//获取年龄 只读
	int getAge()
	{
		//m_Age = 0;//初始化为0岁
		return m_Age;
	}

	//设置年龄  (年龄的范围必须为0——150)
	void setAge(int age)  //检测数据有效性
	{
		if (age < 0 || age>150)
		{
			m_Age = 0;//设施失败时,为0岁
			cout << "你个老妖精!" << endl;
			return;
		}
		m_Age = age;
	}

	//设置情人为只写
	void  setLover(string lover)
	{
		m_Lover = lover;
	}
	
private:
	string m_Name;//姓名 可读可写

	int m_Age;//年龄 只读

	string m_Lover;//情人 只写

};
int main()
{
	Person p;
	p.setName("张三");//姓名设置
	cout << "姓名: " << p.getName() << endl;

	p.setAge(50);//年龄设置
	cout << "年龄: "<< p.getAge() << endl;

	//情人设置
	p.setLover("苍井");
	//cout << "情人: " << p.m_Lover << endl;//只读属性,不可以读取

	system("pause");
	return 0;
}

 案例1:设计立方体类

 

//立方体类设计
//1、创建立方体的类 
//2、设计属性
//3、设计行为获取立方体面积和体积
//4、分别利用全局函数和成员函数 判断两个立方体是否相等
class Cube
{
public:
	//设置长
	void setL(int l)
	{
		m_L = l;
	}
	//获取长
	int getL()
	{
		return m_L;
	}
	//设置宽
	void setW(int w)
	{
		m_W = w;
	}
	//设置宽
	int getW()
	{
		return m_W;
	}
	//设置高
	void setH(int h)
	{
		m_H = h;
	}
	//设置高
	int getH()
	{
		return m_H;
	}
    //获取立方体面积
	int calculateS()
	{
		return 2 * m_L * m_W + 2 * m_W * m_H + 2 * m_L * m_H;
	}
	//获取立方体体积
	int calculateV()
	{
		return m_L * m_W * m_H;
	}


	//利用成员函数判断两个立方体是否相等
	//(成员函数判断只需要传一个对象即可)
	bool isSameByClass(Cube &c)
	{
		if (m_L == c.getL() && m_W == c.getW() && m_H == c.getH())
		{
			return true;
		}
		return false;
	}

private:
	int m_L;//长
	int m_W;//宽
	int m_H;//高
};

//利用全局函数判断 两个立方体是否相等
bool isSame(Cube &c1,Cube &c2)//用引用的方式传递函数
{
	if (c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH())
	{
		return true; 
	}
	else
	{
		return false;
	}		
}

int main()
{
	//创建一个立方体对象
	Cube c1;
	c1.setL(10);
	c1.setW(10);
	c1.setH(10);
	cout << "c1的面积为:" << c1.calculateS() << endl;
	cout << "c1的体积为:" << c1.calculateV() << endl;
	//创建第二个立方体
	Cube c2;
	c2.setL(10);
	c2.setW(10);
	c2.setH(10);
	
	//利用全局函数判断
	bool ret=isSame(c1, c2);
	if (ret)
	{
		cout << "c1和c2是相等的" << endl;
	}
	else
	{
		cout << "c1和c2是不相等的" << endl;
	}
 
	//利用成员函数判断
	ret = c1.isSameByClass(c2);//用已知c1的去调用,再传入一个未知的
	if (ret)
	{
		cout << "成员函数判断的结果:c1和c2是相等的" << endl;
	}
	else
	{
		cout << "成员函数判断的结果:c1和c2是不相等的" << endl;
	}

	system("pause");
	return 0;
}

 

 案例2:点和圆的关系

 

 

//封装案例2:点和圆的关系
//设计一个圆形类(Circle),和一个点类(Point)
//点类
//(属性:点的坐标)
//(方法:设置坐标点,获取坐标点)
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;
};


//圆类
class Circle
{
public:
	//设置半径
	void setR(int r)
	{
		m_R = r;
	}
	//获取半径
	int getR()
	{
		return m_R;
	}
	//设置圆心
	void setCenter(Point center)
	{
		m_Center = center;
	}
	//获取圆心
	Point getCenter()
	{
		return m_Center;
	}

private:
	int m_R;//半径
	Point m_Center;//圆心(让Point类作为圆类的成员)
};


//判断点和圆的关系
void isInCircle(Circle &c, Point  &p)
{
	//计算两点之间距离的平方
	int distance =
		(c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX()) +
		(c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY());
		//计算半径的平方
		int rDistance = c.getR() * c.getR();
		//判断关系
		if (distance == rDistance)
		{
			cout << "点在圆上" << endl;
		}
		else if (distance > rDistance)
		{
			cout << "点在圆外" << endl;
		}
		else
			cout << "点在圆内" << endl;
}

int main()
{
	//创建圆
	Circle c;//Circle类实例化
	c.setR(10);
	Point center;//Point 类实例化,创建了一个center对象
	center.setX(10);
	center.setY(0);
	c.setCenter(center);

	//创建点
	Point p;
	p.setX(10);
	p.setY(10);

	//判断关系
	isInCircle(c, p);

	system("pause");
	return 0;
}

 

 案例核心结论:在类中可以让另一个类作为本类中的成员。

                          可以将不同的类放在不同的文件中。

4.1.4 类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。

如下我们定义两个类(栈和队列):

//栈
class Stack
{
public:
	void Push(int x)
	{}
};
//队列
class Queue
{
	void Push(int x)
	{}
};

这两个类中,我都定义了一个Push函数,此时编译器不会报错,这两个Push函数也不会构成函数重载,因为Stack类和Queue类是完全两个不同的作用域

再比如我在Stack.h声明如下内容:

 

 现在想在Stack.cpp文件里进行定义,就要指定作用域,否则会报错:

 类的定义方式

  • 方式1:声明和定义全部放在类体中

 需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。因此,一般情况下,短小函数可以直接在类里面定义,长一点的声明和定义分离。

  • 方式2:声明放在.h文件中,类的定义放在.cpp文件中

此方式就如同上文类的作用域一开始放出来的两张截图。

补充:类成员变量仅仅只是声明,不是定义!!因为没有开辟空间。

4.1.5 类的实例化

用类类型创建对象的过程,称为类的实例化

  1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
  2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
  3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。

4

4.1.6 类对象的存储方式

只保存成员变量,成员函数存放在公共的代码段。

class Stack
{
public:
	//在类里面定义
	//在类里定义的函数默认是内联
	void Init()
	{
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
	//在类里面声明
	void Push(int x);
	void Pop();
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st;
	st.Init();
	cout << sizeof(st) << endl; //12
}

 通过反汇编得知,调用两次相同的Init()函数call的地址都是一样的,所以就没有必要把函数在对象里面存一份,因为调用的都是同一个函数,如果我每一个函数都存一个函数指针就浪费了。

相反成员变量必须得各自存一份,因为各个成员变量的值是不一样的,我函数是一样的可以不存,但是成员变量就不能不存了,所以我们就可以把函数放到公共代码区里了。也就是说我调用函数是在这个公共代码段里调用的。综上类大小的计算不考虑成员函数。

计算下面几个类的存储空间大小:

// 类中既有成员变量,又有成员函数
class A1 {
public:
	void f1() {}
private:
	int _a;
	char _ch;
};
// 类中仅有成员函数
class A2 {
public:
	void f2() {}
};
// 类中什么都没有---空类
class A3
{};
int main()
{
	A1 a1;
	A2 a2;
	A3 a3;
	cout << sizeof(a1) << endl; // 8
	cout << sizeof(a2) << endl; // 1
	cout << sizeof(a3) << endl; // 1
	return 0;
}

注意:当类里没有成员变量时,至少会给它开一个字节大小的空间,这1个字节不是为例存储有效数据,而是为了占位,表示对象存在过。

结论:

  1. 没有成员变量的类对象,编译器会给它们分配1字节占位,表示对象存在过。
  2. 一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。

4.2 对象的初始化和清理

 类的6个默认成员函数

在我们前面学习的类中,我们会定义成员变量和成员函数,这些我们自己定义的函数都是普通的成员函数,但是如若我们定义的类里什么也没有呢?是真的里面啥也没吗?

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成6个默认成员函数

4.2.1 构造函数和析构函数

//对象的初始化和清理


class Person
{
public:

	//1、构造函数  进行初始化
	Person()
	{
		cout << "Person的构造函数的调用" << endl;
	}

	//2、析构函数 进行清理的操作
	~Person()
	{
		cout << "Person的析构函数的调用" << endl;
	}

};

//构造和析构都是必须有的实现,入宫我们自己不提供,编译器会提供一个空实现的构造和析构
void test01()
{
	Person p;//在栈上的数据,test01执行完毕后,释放这个对象
}

int main()
{
	//test01();

	Person p;//只有构造没有析构,在按任意键之后瞬间会有一个析构的调用

	system("pause");
	return 0;
}

         

4.2.2 构造函数的分类及调用

 

//构造函数的分类及调用

//分类
//按照参数分类  无参构造(默认构造) 和 有参构造
//按照类型分类  不同构造函数  和 拷贝构造
class Person
{
public:
	//构造函数
	Person()
	{
		cout << "Person的构造函数调用" << endl;
	}
	//有参构造函数
	Person(int a)
	{
		age = a;
		cout << "Person的有参构造函数调用" << endl;
	}
	//拷贝构造函数(拷贝一份一模一样的数据出来)
	Person(const Person &p)
	{
		//将传入的人身上的所有属性,拷贝到我身上。
		age = p.age;
		cout << "Person的拷贝构造函数调用" << endl;
	}
	//析构函数
	~Person()
	{
		cout << "Person的析构函数调用" << endl;
	}

	int age;
};

//调用
void test01()
{
	//1、括号法
	//Person p1;//默认构造函数调用
	//Person p2(10);//有参构造函数
	//Person p3(p2);//拷贝构造函数

	//注意事项1:
	//在调用默认构造函数时不要加小括号()。
	//因为下面这行代码,编译器会认为时一个函数的声明(Person为返回值,p1为函数名),不会认为在创建对象。
	//Person p1();//错误的,不创建对象了。

	//cout << "p2的年龄为: " << p2.age << endl;//10
	//cout << "p3的年龄为: " << p3.age << endl;//10

	//2、显示法
	Person p1;//默认构造
	Person p2 = Person(10);//有参构造
	Person p3 = Person(p2);//拷贝构造

	//Person(10)、Person(p2)等号右侧为一个匿名对象,左侧的p2、p3才为对象的名字
	//匿名对象   特点:当前行执行结束后,系统会立即回收掉匿名对象,即马上析构。
	
	//Person(10);
	//cout << "aaaaa" << endl;//先构建销毁在打印aaaaa

	//注意事项
	// 不要利用拷贝构造函数,初始化匿名对象。编译器会认为Person (p3)==Person p3;认为是对象的声明。
	// Person(p3);//错误:person3是一个重定义
	
	//3、隐式转换法
	Person p4 = 10;//相当于 写了 Person p4=Person(10),
}


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

构造函数再理解

如下日期类:

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Init(2022, 5, 17);
	d1.Print();
	return 0;
}

正常情况下,我们写的这个日期类,首先初始化,其次打印。但如果说你突然忘记初始化了,直接就开始访问会怎么样呢?

没初始化直接访问输出的是随机值。 忘记初始化其实是一件很正常的事情,C++大佬在这一方面为了填补C语言的坑(必须得手动初始化)。因而就设计出了构造函数。

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。其目的就是为了方便我们不需要再初始化。
构造函数特性:

构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象

其特征如下:

  1. 函数名和类名相同;
  2. 无返回值;
  3. 对象实例化时编译器自动调用对应的构造函数;
  4. 构造函数可以重载;
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成;
  6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

如下即为构造函数:

Date()
{
	_year = 1;
	_month = 1;
	_day = 1;
}

特性3解释:对象实例化时编译器自动调用对应的构造函数。

也就是说我们在实例化一个对象后,它会自动调用这个构造函数,自动就初始化了,我们可以通过调试来看:

特性4解释: 构造函数支持重载

Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

像这个重载函数是明确了我们要传参的,所以在实例化对象后就必须把参数写上去(虽然看着奇奇怪怪,但是没有办法,毕竟普通的调用,参数都是在函数名后面,而这个参数在实例化对象后面):Date d2(2022, 5, 17);

输出和之前的构造函数对比:

注意:没有参数时我在调用的时候不能加上括号(),切忌!!构造函数尤为特殊。

如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明

 无参的情况下必须要像我们刚开始实例化的d1那样:

 构造函数的重载我们推荐写成全缺省的样子。

//普通的构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
//全缺省的构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

首先,普通的构造函数和全缺省的构造函数在不调用的情况下可以同时存在,编译也没有错误。但是在实际调用的过程中,会存在歧义。如下的调用:

class Date
{
public:
//普通的构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
//全缺省的构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
}

此时我实例化的d1到底是调用普通的构造函数?还是调用全缺省的构造函数?并且此段代码编译出现错误。何况我在没有调用函数的情况下编译是没错的。

由此可见:它们俩在语法上可以同时存在,但是使用上不能同时存在,因为会存在调用的歧义,不知道调用的是谁,所以一般情况下,我们更推荐直接写个全缺省版的构造函数,因为是否传参数可由你决定。传参数数量也是由你决定。

特性5解释:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

class Date
{
public:
	
	// 我们不写,编译器会生成一个默认无参构造函数
	/*Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
	
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
	Date d;
	d.Print();
}

不是不自己写构造函数,编译器会默认生成吗?为什么到这又是随机值了?这难道也算初始化?别急,搞清楚这个得先明白默认构造函数:

注意:C++把变量分成两种

内置类型/基本类型:int、char、double、指针……
自定义类型:class、struct去定义的类型对象
C++默认生成的构造函数对于内置类型成员变量不做处理,对于自定义类型的成员变量才会处理,这也就能很好的说明了为什么刚才没有对年月日进行处理(初始化),因为它们是内置类型(int类型的变量)

让我们来看看自定义类型是如何处理的。

class A
{
public:
	A()
	{
		cout << "A()" << endl;
		_a = 0;
	}
private:
	int _a;
};

首先,这是一个名为A的类,有成员变量_a,并且还有一个无参的构造函数,对_a初始化为0。接着:

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	A _aa;
};
int main()
{
	Date d;
	d.Print();
}

日期类里有三个内置类型,一个自定义类型(A_aa),我们编译运行看看结果:

 

通过运行结果以及调试,也正验证了默认构造函数对自定义类型才会处理。这也就告诉我们,当出现内置类型时,就需要我们自己写构造函数了。

总结:

  1. 如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数
  2. 如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。

特性6解释:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。。

特性6可以简单总结为不用传参就可以调用的即为默认构造函数。

既然我默认构造函数只对自定义类型才会处理,那如果我不想自己再写构造函数也要对内置类型处理呢?我们可以这样做:(后续会仔细说明)

析构函数再理解

概念:通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

构造函数特性:

析构函数是特殊的成员函数。

其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值。
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,C++编译系统自动调用析构函数。
  5. 编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。

实例化出的对象会调用它的默认构造函数进行初始化,其次,出了作用域后又调用其析构函数。析构的目的是为了完成资源清理,什么样的才能算是资源清理呢?像我这里定义的成员变量就不需要资源清理,因为出了函数栈帧就销毁,真正需要清理的是malloc、new、fopen这些的,就比如清理栈里malloc出的空间。

class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st;
}

C++的构造函数就像先前C语言常写的Init,而析构函数就像Destroy

看如下的题目:

int main()
{
	Stack st1;
	Stack st2;
}

现在我用类实例化出st1和st2两个对象,首先,st1肯定先构造,st2肯定后构造,这点毋庸置疑,那关键是谁先析构呢?

答案:st2先析构,st1后析构

解析:这里st1和st2是在栈上的,建立栈帧,其性质和之前一样,后进先出,st2后压栈,那么它肯定是最先析构的。所以栈里面定义对象,析构顺序和构造顺序是反的。

解释特性3:一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

若自己没有定义析构函数,虽说系统会自动生成默认析构函数,不过也是有要求的,和构造函数一样,内置类型不处理,自定义类型会去调用它的析构函数,如下:

class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		cout << "~Stack():" << this << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
 
};
class MyQueue
{
public:
	//默认生成的构造函数可以用
	//默认生成的析构函数也可以用
	void push(int x)
	{}
 
	int pop()
	{}
private:
	Stack _S1;
	Stack _s2;
};
int main()
{
	MyQueue q;
}

对于MyQueue而言,我们不需要写它的默认构造函数,因为编译器对于自定义类型成员(_S1和_S2)会去调用它的默认构造,Stack提供了默认构造,出了作用域,编译器会针对自定义类型的成员去默认调用它的析构函数,因为有两个自定义成员(_S1和_S2),自然析构函数也调了两次,所以会输出两次Stack()……

4.2.3 拷贝构造函数调用时机

//4.2.3 拷贝构造函数调用时机

class Person
{
public:
	Person()
	{
		cout << "Person默认构造函数调用" << endl;
	}

	Person(int age)
	{
		m_Age = age;
		cout << "Person有参构造函数调用" << endl;
	}

	Person(const Person &p)
	{
		m_Age = p.m_Age;;
		cout << "Person拷贝构造函数调用" << endl;
	}
	~Person()
	{
		cout << "Person析构函数调用" << endl;
	}
	int m_Age;
};

//1、使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
	Person p1(20);
	Person p2(p1);

	cout << "p2的年龄: " <<p2.m_Age<<endl;
}

//2、值传递的方式给函数参数传值
void doWork(Person p)
{

}
void test02()
{
	Person p;
	doWork(p);
}

//3、值方式返回局部对象
Person doWork2()
{
	Person p1;//调用默认构造
	cout << (int*)&p1 << endl;
	return p1;//调用拷贝构造(这一行执行时,会拷贝一个新的对象传给外面)
}
void test03()
{
	Person p = doWork2();
	cout << (int*)&p << endl;
}

int main()
{
	/*test01();
	test02();*/

	test03();
	system("pause");
	return 0;
}

  2、

 3、 ​​​​​​

拷贝构造函数再理解

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

拷贝构造函数特性:

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
  3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

如下即为拷贝构造函数:

Date(Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

解释特性2:拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

为什么传值传参会引发无穷递归呢?

我们先举一个普通的func函数作为例子:

//传值传参
void Func(Date d)
{}
int main()
{
	Date d1(2022, 5, 18);
	Func(d1);
	return 0;
}

此函数调用传参是传值传参。在C语言中,把实参传给形参是把实参的值拷贝给形参,而我的实参d1是自定义类型的,需要调用拷贝构造,传值传参是要调用拷贝构造的,但是我如果不想调用拷贝构造呢?就需要引用传参,因为此时d就是d1的别名。

void Func(Date& d) {}

此时再回到我们刚才的例子:如若我不传引用传参,就会疯狂的调用拷贝构造:

Date(Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

为了避免出现无限递归调用拷贝构造,所以要加上引用,加上引用后,d就是d1的别名,不存在拷贝构造了。同类型的传值传参是要调用拷贝构造的。

Date(const Date& d) {} 
//最好加上const,对d形成保护

解释特性3:若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

class Date
{
public:
//构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
//拷贝构造函数
    /*
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
    */
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Func(Date& d)
{
	d.Print();
}
int main()
{
	Date d1(2022, 5, 18);
	Date d2(d1);
	Func(d1);
	d2.Print();
}

为什么我这里没有写拷贝构造函数,它也会自动完成拷贝构造呢?由此我们要深思,拷贝构造与构造和析构是不一样的,构造和析构都是针对自定义类型才会处理而内置类型不会处理,而默认拷贝构造针对内置类型的成员会完成值拷贝,浅拷贝,也就是像把d1的内置类型成员按字节拷贝给d2。

由此得知,对于日期类这种内置类型的成员是不需要我们写拷贝构造的,那是不是所有的类都不需要我们写拷贝构造呢?来看下面的栈类。

class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
//不写拷贝构造,编译器调用默认拷贝构造
	/*
	Stack(const Stack& st)
	{
		_a = st._a;
		_top = st._top;
		_capacity = st._capacity;
	}
	*/
	//析构函数
	~Stack()
	{
		cout << "~Stack():" << this << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1(10);
	Stack st2(st1);
}

我们通过调试看到运行崩溃了,可见栈的拷贝构造函数不能像日期类一样不写而让编译器去调用默认拷贝构造(就是按照日期类的模式写了拷贝构造也会出错),因为此时的st1(指针)和st2(指针)指向的就是同一块空间,通过调试可以看出:

 

st1和st2指向同一块空间会引发一个巨大的问题:析构函数出错,因为我st2会先析构,析构完后我st1再析构,不过我st1指向的空间已经被st2析构过了,因为它俩指向同一块空间,同一块空间我释放两次就会有问题。 出了析构存在问题,增删查改那也会有问题,这个后续会谈到。其实刚才写的栈的拷贝构造就是浅拷贝,真正栈的拷贝构造应该用深拷贝来完成。

综上,我们可以得知,浅拷贝针对日期类这种是没有问题的,而类的成员若是指向一块空间的话就不能用浅拷贝了。

如果是自定义类型呢?让编译器自己生成拷贝构造会怎么样呢?

class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
//不写拷贝构造,编译器调用默认拷贝构造
	/*
    浅拷贝
	Stack(const Stack& st)
	{
		_a = st._a;
		_top = st._top;
		_capacity = st._capacity;
	}
	*/
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue
{
//默认拷贝构造针对内置类型的成员会完成值拷贝,浅拷贝
//自定义类型的成员,去调用这个成员的拷贝构造。
private:
	int _size = 0;
	Stack _S1;
	Stack _s2;
};
int main()
{
	MyQueue mq1;
	MyQueue mq2(mq1);
}

其实这里同样是发生了浅拷贝,归根结底在于我栈的深拷贝还没写。

 仔细看调试,mq2调用了其成员栈的拷贝构造,而我mq1和mq2的两个栈又发生了浅拷贝,它们对应的_S1和_S2指向的地址都是一样的,这也就意味着析构时同一块空间又会析构两次,出错。这里套了两层。

一般的类,自己生成拷贝构造就够用了,只有像Stack这样自己直接管理资源的类,需要自己实现深拷贝。

补充:void TestDate2()
{
    Date d1(2022, 5, 18);
    Date d3 = d1; //等价于 Date d3(d1);
}

Date d3 = d1 是拷贝构造,不是赋值,拿一个对象初始化另一个对象是拷贝构造。Date d3 = d1 是拷贝构造,不是赋值,拿一个对象初始化另一个对象是拷贝构造。

4.2.4 构造函数调用规则

自己写了拷贝构造函数:

 没写拷贝构造函数,使用系统给的:只做了值拷贝

4.2.5  深拷贝与浅拷贝

//深拷贝和浅拷贝
class Person
{
public:
	Person()
	{
		cout << "Person的默认构造函数的调用" << endl;
	}
	Person(int age,int height)
	{
		m_Age = age;
		m_Height =new int(height);
		cout << "Person的有参构造函数调用" << endl;
	}
	//自己实现拷贝构造函数,解决浅拷贝带来的问题
	Person(const Person &p)
	{
		cout << "Person拷贝构造函数调用" << endl;
		m_Age = p.m_Age;
		//m_Height = p.m_Height;//编译器默认实现的就是这行代码
		//深拷贝操作
		m_Height = new int(*p.m_Height);//重新在堆区开辟一个区域
	}
	~Person()
	{
		//析构代码,将堆区开辟数据做释放操作
		if (m_Height != NULL)
		{
			delete m_Height;
			m_Height = NULL;
		}
		cout << "Person的默认析构函数的调用" << endl;
	}
	int m_Age;//年龄
	int *m_Height;//身高,在堆区上开辟一个指针
}; 

void test01() {
	Person p1(18,160);
	cout << "p1的年龄为: " << p1.m_Age<<"身高为:"<< *p1.m_Height << endl;
	Person p2(p1);//编译器提供了一个默认的拷贝构造函数,做了浅拷贝操作
	cout << "p2的年龄为: " << p2.m_Age<<"身高为:" << *p2.m_Height <<endl;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

4.2.6 初始化列表参数

构造函数可以给属性进行初始化操作,初始化列表是另一种方式。

//初始化列表
class Person
{
public:
	//传统初始化操作(构造函数初始化参数)
	/*Person(int a, int b, int c)
	{
		m_A = a;
		m_B = b;
		m_C = c;
	}*/

	初始化列表初始化属性
	//Person() :m_A(10), m_B(20), m_C(30)//缺点:只能固定值,10,20,30
	//{

	//}
	
	//改进
	Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c)
	{

	}
	int m_A;
	int m_B;
	int m_C;
};
void test01()
{
	//Person p(10, 20, 30);
	
	//Person p;

	Person p(30, 20, 10);
	cout << "m_A=" <<p.m_A<< endl;
	cout << "m_B=" << p.m_B<<endl;
	cout << "m_C=" << p.m_C<<endl;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

 注意:冒号是在构造函数形参列表括号之后。

4.2.7 类对象作为类成员


//类对象作为类成员
#include<string>
//手机类
class Phone
{
public:

	Phone(string pName)
	{
		cout << "Phone的构造函数调用" << endl;
		m_PName = pName;
	}
	~Phone()
	{
		cout << "Phone的析构函数调用" << endl;
	}
	//手机品牌名称
	string m_PName;
};

//人类
class Person
{
public:
	//Phone m_Phone=pName  隐式转换法(利用字符串非对象进行初始化操作)
	Person(string name, string pName):m_Name(name),m_Phone(pName)
	{
		cout << "Person的构造函数调用" << endl;
	}
	//姓名
	string m_Name;
	//手机
	Phone m_Phone;
	~Person()
	{
		cout << "Person的析构函数调用" << endl;
	}
};

//当其他类的对象作为本类的成员,构造时候先构造对象,在构造自身
void test01()
{
	Person p("张三", "苹果MAX");
	cout << p.m_Name << "拿着:" << p.m_Phone.m_PName << endl;//张三拿着:苹果MAX
}
int main()
{
	test01();
	system("pause");
	return 0;
}

 

4.2.8 静态成员

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

静态成员分为:

  • 静态成员变量:

                       所有对象共享一份数据;

                       在编译阶段分配内存;

                       类内声明,类外访问。   

  • 静态成员函数:

                       所有对象共享同一个函数;

                       静态成员函数只能访问静态成员变量。(静态成员函数没有隐藏的this指针,不能                                                                                          访问任何非静态成员

静态成员变量案例:

静态成员变量不属于某个对象上,所有对象都共享同一份数据。静态的成员变量一定要在类外进行初始化。

因此静态成员变量有两种访问方式:1、通过对象进行访问;2、通过类名进行访问。3.通过匿名对象突破类域进行访问。

   (非静态成员变量只能通过对象访问变量进行读或写)

class Person
{
public:
	//1、所有对象都共享同一份数据
    //2、编译阶段就分配内存
	//3、类内声明,类外初始化
	static int m_A;

	//静态成员变量也是有访问权限的
private://类外就不可以访问了
	static int m_B;
};

//类外初始化
int Person::m_A = 100;//类外初始化
int Person::m_B = 200;

void test01()
{
	Person p;
	cout << p.m_A << endl;//100

	Person p2;
	p2.m_A = 200;//用p2对象将其改为两百

	cout << p.m_A << endl;//200
	cout << p2.m_A << endl;//200,共享同一份数据
}

void test02()
{
	//静态成员变量 不属于某个对象上,所有对象都共享同一份数据
	//因此静态成员变量有两种访问方式:

	//1、通过对象进行访问
	Person p;
	cout<< p.m_A << endl;//100

	//2、通过类名进行访问
	cout << Person::m_A << endl;//100

	//cout << Person::m_B << endl;//类外访问不到私有静态成员变量
    
    //3.通过匿名对象访问
    cout<<Person().m_A<<endl;
}
int main()
{
	//test01();
	test02();
	system("pause");
	return 0;
}

静态成员函数案例:

也有两种访问方式:1、通过对象访问;2、通过类名访问。

//静态成员函数
//所有对象共享一个函数
//静态成员函数只能访问静态成员变量
class Person
{
public:
	static void func()
	{ 
		m_A = 100;//静态成员函数可以访问 静态成员变量(m_A不属于某一个对象)
		//m_B = 200;//静态成员函数 不可以访问 非静态成员变量(无法区分到底是那个对象的m_B)
		//函数体无法区分到底是那个对象的m_B.
		cout << "static void func调用" << endl;
	}

	//静态成员函数也是有访问权限的
private:
	static void func2()
	{
		cout << "static void fun2调用" << endl;
	}

	static int m_A;//静态成员变量
	int m_B;//非静态成员变量
};

int Person::m_A = 0;
//int Person::m_B = 0;非静态变量不需要在类外进行定义初始化

void test01()
{
	//1、通过对象访问
	Person p;
	p.func();

	//2、通过类名访问
	Person::func();

	//Person::func2();类外访问不到私有静态成员函数
}
int main()
{
	test01();
	system("pause");
	return 0;
}

静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值。

  • 静态成员函数可以调用非静态成员函数吗?

不可以,因为静态成员函数是没有this指针的,无法调用非静态成员函数。

  • 非静态成员函数可以调用类的静态成员函数吗?

可以,因为静态成员为所有类对象所共享,不受访问限制

  • 面试题:实现一个类,计算中程序中创建出了多少个类对象。

假设命名该类为A,那么A类型的对象一定是经过构造函数或拷贝构造的,那么我们就可以分别定义两个静态成员变量,在构造函数和拷贝构造里++变量,这样,每创建一次对象,变量就++一次,自然就好求了。如下:

class A
{
public:
	A()
	{
		++_count1;
	}
	A(const A& aa)
	{
		++_count2;
	}
	static int GetCount1()
	{
		return _count1;
	}
	static int GetCount2()
	{
		return _count2;
	}
private:
	static int _count1; 
	static int _count2;
};
int A::_count1 = 0;
int A::_count2 = 0;
A Func(A a)
{
	A copy(a);
	return copy;
}
int main()
{
	A a1;
	A a2 = Func(a1);
 
	cout << a1.GetCount1() << endl; // 1
	cout << a2.GetCount2() << endl; // 3
	cout << A::GetCount1() + A::GetCount2() << endl; // 4
}

这里用全局变量(count1和count2)也是可以的,但不推荐。在简单的程序里可以使用没问题,但是在项目中不推荐用全局的,因为可能会出现链接冲突的问题,还是用静态成员变量为优。

4.3 C++对象模型和this指针

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

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

只有非静态成员变量才属于类的对象上。(求类所占内存空间时,仅算非静态成员函数所占空间大小)

空对象占用内存空间为:1个字节。因为C++编译器会给每个空对象也分配一个字节空间,为了区分空对象占内存的位置。每个空对象也应该有一个独一无二的内存地址。

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

class Person 
{
	int m_A;//非静态成员变量  属于类的对象上  4

	static int m_B;//静态成员变量  不属于类的对象上 4
	 
	void func() {};//非静态成员函数 不属于类的对象上  4

	static void func2() {};//静态成员函数 不属于类的对象上 4
};

 int Person::m_B=0;

void test01()
{
	Person p;
	//空对象占用内存空间为:1
	//因为C++编译器会给每个空对象也分配一个字节空间,为了区分空对象占内存的位置。
	//每个空对象也应该有一个独一无二的内存地址
	cout << "size of p=  " << sizeof(p) << endl;
}

void test02()
{
	Person p;

	cout << "size of p=  " << sizeof(p) << endl;
	//仅含非静态成员变量所占内存为:4
	//非静态在加上静态成员变量所占内存仍为:4.(因为静态成员变量不属于类的对象上)
	//加上函数仍为4
	//加上静态成员函数仍为4

}

int main()
{
	//test01();
     test02();
	system("pause");
	return 0;
}

4.3.2 this指针概念

C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。 

this指针引出:

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1, d2;
	d1.Init(2022, 5, 15);
	d2.Init(2022, 5, 16);
	d1.Print();
	d2.Print();
	return 0;
}

上述定义的日期类中,我们定义了d1和d2两个对象,并且初始化后将其打印。首先一点我们都清楚,我d1和d2调用的Print是同一个函数。

  • 但是现在有一个问题?它怎么知道我第一个Print调用的就是5月15号那个,而第二个Print调用的就是5月16号那个呢?调用的Print都是在公共代码段里找的,怎么就能区分出呢?

上述问题就设计到了C++的隐含this指针。this指针是一个隐含的形参。也就意味着我Print函数和Init函数其实是被编译器处理过了,并且实参也会多出传地址,如下:

class Date
{
public:
	//Print处理前
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
 
	//Print处理后
	void Print(Data* const this) //注意const修饰,指针不能修改,指向的内容可以修改
	{
		cout << this->_year << "-" << this->_month << "-" << this > _day << endl;
	}
 
	//Init处理前
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//Init处理后
	void Init(Date* const this, int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;
	Date d2;
	Date d3;
	d1.Init(2022, 5, 16);
	d2.Init(2022, 5, 17);
 
	//实参改变
	d1.Init(&d1, 2022, 5, 16);
	d2.Init(&d2, 2022, 5, 17);
 
	d1.Print();
	d2.Print();
 
	//实参改变
	d1.Print(&d1);
	d2.Print(&d2);
 
	return 0;
}

 

谁调用就把谁的地址传过来,此时的this就是形参,整个过程就叫做隐含的this指针

为了更加方便且直观的看出调用的函数区别,我们通过打印地址来看看:

 注意:实参和形参的位置不能显示的写出来,就比如this和&都不能明着在实参和形参的位置写出来,编译器会自己帮我们完成,我们无需操作 ,你不能抢了编译器的饭碗。但是我们可以在函数里面显示出this指针,当然你也可以不写,因为编译器会帮你完成。

 this指针本身不能修改,因为它是const修饰的,但是this指针指向的对象可以被修改,这点无需强调了。

this指针的特性:

  1. this指针的类型:类类型* const
  2. 只能在“成员函数”的内部使用
  3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。

1、解决名称冲突案例:

class Person
{
public:
	Person(int age)
	{
		//age = age;//认为这三个为同一份数据,与成员变量无关。
		this->age = age;//this指向p1对象
	}

	int age;
};

//1、解决名称冲突
void test01()
{
	Person p1(18);
	cout << "p1的年龄为: " << p1.age << endl;
}
//2、返回对象本身用*this
 
 
int main()
{
	test01();
	system("pause");
	return 0;
 }

 

 加上this指针:指向的是被调用的成员函数所属的对象。

 

 2、在类的非静态成员函数中返回对象本身

创建两个对象p1,p2,非静态成员函数的功能为p1加p2的成员变量相加。如果想一直追加,函数体中就应返回对象本身(即*this),函数以引用的方式返回。

class Person
{
public:
	Person(int age)
	{
		//age = age;//认为这三个为同一份数据,与成员变量无关。
		this->age = age;//this指向p1对象
	}

	Person& PersonAddAge(Person &p)//引用的方式做返回,—一直返回的是p2本体
	{
		this->age +=p.age;
		 
		//this指向p2的指针,而*this指向的就是p2这个对象本体。
		return *this;
	}
	int age;
};

//1、解决名称冲突
void test01()
{
	Person p1(18);
	cout << "p1的年龄为: " << p1.age << endl;
}
//2、返回对象本身用*this
void test02()
{
	Person p1(10);

	Person p2(10);

	//链式编程思想
	p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);//40

	cout << "p2的年龄为:" << p2.age << endl;
 }
 
int main()
{
	//test01();
	test02();
	system("pause");
	return 0;
 }

 

函数以值得方式返回:

调用完一次之后,p2加了十岁,返回的不再是p2本体,而是本体创建了一个新的对象数据,调用了拷贝构造函数(用值的方式返回会调用一份新的数据)。

Person PersonAddAge(Person &p)//引用的方式做返回
	{
		this->age +=p.age;
		 
		//this指向p2的指针,而*this指向的就是p2这个对象本体。
		return *this;
	}
	

  

4.3.3 空指针访问成员函数

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

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

//4.3.3 空指针访问成员函数
class Person
{
public:
	void showClassName()
	{
		cout << "this is Person class" << endl;
	}

	void showPersonAge()
	{
		//报错的原因是因为传入的指针是NULL
		if (this == NULL)
		{
			return;
		}
		cout << "age  " << this->m_Age << endl;//this没有指向一个确切的对象
	}

	int m_Age;
};
 
void test01()
{
	Person  * p = NULL;//,创建一个空指针,仅创建指针,没有创建对象

	p->showClassName();//空指针访问成员函数
	//p->showPersonAge();//出错,代码崩溃
}

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

面试题

1.下面程序编译运行结果是?

A、编译错误        B、运行崩溃        C、正常运行

class A
{
public:
	void Show()
	{
		cout << "Show()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->Show();
}

答案:C

解析:首先要明确,空指针的解引用不会报编译错误,可能有人会觉得这里p不是空指针嘛,空指针解引用不应该会出错?别急,我们把私有的成员变量放出来,再解引用对比看看:

其实第130行才是真正的解引用,而第129行不是解引用,还是那句话,对象里面只有成员变量,那我访问成员变量就是到指针指向的空间取访问_a,但是指针指向的空间是空指针,当然就会报错,而show函数不在对象里面,它在公共代码段里头,它就跟普通的函数调用一样,去找到函数的地址即可,即call地址,并且把p作为实参传到形参this指针里头,这也间接证明了this指针可以为空。所以这段程序运行正常。
2.下面程序编译运行结果是?

A、编译错误        B、运行崩溃        C、正常运行

class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->PrintA();
}

答案:B

解析:此题就会出现运行崩溃了。首先,我调用PrintA函数,实参p传到形参this指针,此时编译器对_a进行访问,此时就是空指针的解引用,因为编译器会对改行代码处理如下:

 此时的this->_a就是空指针的解引用,当然会运行崩溃。

3.this指针存在哪里?

A、栈        B、堆        C、静态区        D、常量区

答案:A

解析:this指针是把对象的地址传递过去,它是一个形参,参数的传递是在函数的栈帧,也就是说this指针存在栈里,当然有些编译器会使用寄存器优化。

4、this指针可以为空吗?

答案:可以。

4.3.4 const修饰成员函数

只读属性

 

隐含在每个成员函数中均有一个this指针。this指针指向的是创建的对象,当对象调用成员函数、变量时,this指向函数或者变量。

//4.3.4 常函数和常对象
//常函数
class Person
{
public:
	//this指针的本质  是指针常量 指针的指向是不可以修改的
	//const Person *const this;
	//在成员函数后面加const,所修饰的是this指针,让指针指向的值也不可以修改
	void showPerson() const  //加了const 指针指向的值不可以修改
	{
		 this->m_B=100;//可以修改
		//this->m_A = 100;//错误,值不可修改
		//this=NULL;//this指针不可以修改指针的指向
	}


	void func()//普通函数
	{

	}
	int m_A ;
	mutable int m_B;//特殊变量,即使在常函数中,也可以修改这个值,加关键字mutable

};


//常对象
void test02()
{
	const Person p;//在对象前加const,变为常对象。
	//p.m_A = 100;//不能修改
	p.m_B = 100;//可以修改m_B是特殊值,在常对象下也可以需改。

	//常对象只能调用常函数
	p.showPerson();
	//p.func();//不能调用常函数,因为普通函数允许修改成员变量,常对象本身不允许修改属性,如果函数能调用起来,那么就侧面将属性修改了
}
void test01()
{
	Person p;
	p.showPerson();
}

int main()
{
	//test01();
    test02();
	system("pause");
	return 0;
}

const成员函数再理解 

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

假如我现在有一个日期类,并且有如下的Func函数即调用情况:

很明显,这里Func函数d的调用Print()出错了,而d1调用Print()却没出错,为何呢?

这里涉及到权限问题。我们先把实际调用过程中,隐含的this指针写出来:

Print()函数里的const修饰this本身,this不能修改,但是this可以初始化,接着我们要搞清楚&d1和&d的类型分别是:

  • &d1:Date*
  • &d:const Date*
  1. Date*传给Date* const没有问题,都是可读也可修改,所以d1调用Print()不会出错
  2. 而const Date* 指向的内容不能被修改,可是当它传给Date*时就出错了,因为Date*是可以修改的,这里传过去会导致权限放大。所以当然d调用Print()函数报错。

解决办法:

加上const去保护this指向的内容,也就是在Date*的前面加上const:

解决办法:

加上const去保护this指向的内容,也就是在Date*的前面加上const:

但是这里又不能之间加上const,因为this指针是隐含的,你不能显示的将const写出来。因此,C++为了解决此问题,允许在函数后面加上const以达到刚才的效果:

void Print() const// 编译器默认处理成:void Print(const Date* const this)
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

此时我const Date*传给const Date*就是权限不变,自然不会出错了,同样我Date*传给const Date*就是权限缩小也不会有问题。因为权限不能放大,只能缩小或不变。

  • 注意:建议成员函数中不修改成员变量的成员函数,都加上const,此时普通对象和const对象都可以调用。
  • 补充:请思考下面几个问题:

const对象可以调用非const成员函数吗?

答案:不能,const调用非const是权限放大。

非const对象可以调用const成员函数吗?

答案:可以,此时权限缩小。

const成员函数内可以调用其它的非const成员函数吗?
答案:不可以,依旧是权限放大。

非const成员函数内可以调用其它的const成员函数吗?
答案:可以,权限缩小。

4.4 友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

 全局函数做友元:

//全局函数做友元
//建筑物类
class Building
{
	//goodGay全局函数是Building好朋友,可以访问Building中私有成员
	friend void goodGay(Building * building);

public:
	Building()
	{
		m_SittingRoom = "客厅";
		m_BedRoom = "卧室";
	}

public:
	string m_SittingRoom;
private:
	string m_BedRoom;
};

//全局函数
void goodGay(Building* building)
{
	cout << "好基友全局函数 正在访问:" << building->m_SittingRoom << endl;
	cout << "好基友全局函数 正在访问:" << building->m_BedRoom << endl;
}

void test01()
{
	Building building;
	goodGay(&building);

}

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

 类做友元:

//类做友元
#include<string>
//class Building;
//房子类
class Building
{
	//告诉编译器GoodGay类是Building类的好朋友,可以访问到Building类中私有内容
	friend class GoodGay;

public: 
	Building();//构造函数(类外实现)
public:
	string m_SittingRoom;//客厅
private:
	string m_BedRoom;//卧室
};

//好基友类
class GoodGay
{
public:
	GoodGay();//构造函数—类外实现

	void visit();//参观函数访问Building中的属性

	Building *building;
};


//类外写成员函数(类外写构造函数)
Building::Building()
{
	m_SittingRoom = "客厅";//初始化
	m_BedRoom = "卧室";
}


GoodGay::GoodGay()
{
	//创建建筑物对象
	building = new Building;//让building指向new出来的对象
}

void GoodGay::visit()
{
	cout << "好基友类正在访问:" << building->m_SittingRoom << endl;
	cout << "好基友类正在访问:" << building->m_BedRoom << endl;
}

void test01()
{
	GoodGay gg;
	gg.visit();
}

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

 

 成员函数做友元:

#include<string>
//3、成员函数做友元
class Building;
class GoodGay
{
public:
	GoodGay();
  //创建两个成员函数,让其中一个可以访问building中的私有成员
	void visit();//让visit函数可以访问Building中的私有成员
	void visit2();//让visit02函数不可以访问Building中的成员函数——正常写的均不可访问
	Building * building;
};
class Building
{
	//告诉编译器 GoodGay类下的visit成员函数作为本类的好朋友,可以访问私有成员
	friend void GoodGay::visit();
public:
	Building();

public:
	string m_SittingRoom;//客厅
private:
	string m_BedRoom;//卧室

};
//类外实现成员函数
Building::Building()
{
	m_SittingRoom = "客厅";
	m_BedRoom = "卧室";
}

GoodGay::GoodGay()
{
	building = new Building;//创建一个Building

}

void GoodGay::visit()
{
	cout << "visit函数正在访问:" << building->m_SittingRoom << endl;
	cout << "visit函数正在访问:" << building->m_BedRoom << endl;
}
void GoodGay::visit2()
{
	cout << "visit2函数正在访问:" << building->m_SittingRoom << endl;
	//cout << "visit函数正在访问:" << building->m_BedRoom << endl;
}
void test01()
{
	GoodGay gg;//GoodGay创建一个gg对象(GoodGay实例化),创建对象时就会调用其构造函数。
	gg.visit();//gg对象访问GoodGay中的visit方法
	gg.visit2();
}
int main()
{
	test01();
	system("pause");
	return 0;
}

 说明:

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数;
  • 友元函数不能用const修饰,因为友元没有this指针;
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
  • 一个函数可以是多个类的友元函数;
  • 友元函数的调用与普通函数的调用和原理相同。

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。(注意声明的时候编译器会向上寻找)。

  • 友元关系是单向的,不具有交换性。
  • 友元关系不能传递。

4.5 运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其另外一种功能,以适应不同的数据类型。

4.5.1 加号运算符重载(+)

作用:实现两个自定义数据类型相加的运算。

//加号运算符重载

class Person
{
public:

	//1、成员函数重载+号
	Person operator+(Person &p)
	{
		Person temp;
		temp.m_A = this->m_A + p.m_A;
		temp.m_B = this->m_B + p.m_B;
		return temp;
	}
public:
	int m_A;
	int m_B;
};

2、全局函数的重载+号
//Person operator+(Person& p1, Person& p2)
//{
//	Person temp;
//	temp.m_A = p1.m_A + p2.m_A;
//	temp.m_B = p1.m_B + p2.m_B;
//	return temp;
//}


//函数重载的版本(实现Person + int)
Person operator+(Person& p1, int num)
{
	Person temp;
	temp.m_A = p1.m_A + num;
	temp.m_B = p1.m_B + num;
	return temp;
}
void test01()
{
	Person p1;
	p1.m_A = 10;
	p1.m_B = 10;
	Person p2;
	p2.m_A = 10;
	p2.m_B = 10;

	//成员函数重载本质调用
	Person p3 = p1.operator+(p2);


	//全局函数重载本质调用
	//Person p3 = operator+(p1, p2);
	

	//成员函数和全局函数均可以简化成这种形式
	//Person p3 = p1 + p2;//简化形式


	//运算符重载,也可以发生函数重载
	Person p4 = p1 + 100;//person+int

	cout << "p3.m_A= " << p3.m_A << endl;
	cout << "p3.m_B= " << p3.m_B << endl;

	cout << "p4.m_A= " << p4.m_A << endl;
	cout << "p4.m_B= " << p4.m_B << endl;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

 

 总结:1、对于内置的数据类型的表达式的运算符是不可能改变的

           2、不要滥用运算符重载

4.5.2 左移运算符的重载(<<)

       一般情况下,输出一个变量用cout<<即可,当定义一个类P时,封装着成员变量m_A和m_B,那么用cout<<输出P时能否将其下成员变量(内部属性)也顺便输出呢?一定是不可以的因为cout不知道内部的属性,因此对输出符号进行重载,使其对其内部属性也能够进行输出。

作用:可以输出自定义的数据类型。

//左移运算符重载
class Person
{
	friend ostream& operator<<(ostream& cout, Person& p);
public:
	Person(int a, int b)
	{
		m_A = a;
		m_B = b;
	}
//public:
	利用成员函数重载 左移运算符  p.operator<<(cout)  简化版本 p<<cout
	通常不会利用成员函数重载重载<<运算符,因为无法实现cout在左侧
	// 因为需要用成员函数去调用,故对象名一定在左侧
	//void operator<<(cout)
	//{

	//}
private:
	int m_A;
	int m_B;
};

//只能利用全局函数重载左移运算符
ostream & operator<<(ostream &out, Person &p)//本质  operator<<(cout, p)  简化 cout << p
{
	//cout对象属于ostream类型
	out << "m_A= " << p.m_A << " m_B=  " << p.m_B;
	return out;//将cout作为返回值,则后面可不停追加输出,链式编程思想
}

void test01()
{
	Person p(10,10);

	//p.m_A = 10;
	//p.m_B = 10;//也可用构造函数在类内进行赋初值
	cout << p<<"hello world"<<endl;//链式编程

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

总结:重载左移运算符配合友元可以实现输出自定义数据类型。

4.5.3 递增运算符重载(++)

     自己写出一种数据类型,实现前置后者后置的递增,并实现数据的输出。

作用:通过重载递增运算,实现自己的整形数据。

//重载递增运算符
//自定义整形
class MyInteger
{
	friend ostream& operator<< (ostream& cout, MyInteger myint);
public:
	MyInteger()
	{
		m_Num = 0;
	}

	//重载前置++运算符   返回引用是为了已知对一个数据进行递增操作
	    //若只返回MyInteger的话加加后则返回一个新的数据,我们需要的是一直对一个数据进行操作
	 MyInteger& operator++()//
	{
		 //先进行++运算
		m_Num++;
		//再将自身作为返回
		return *this;
	}

	//重载后置++运算符
	 //这个int代表占位参数,可以用于区分前置和后置递增
	//返回的是一个值,而不是引用,因为temp是一个局部变量,函数执行结束即释放返回引用是违法的
	 MyInteger operator++(int)   //++重载重定义了返回值不可以作为重载类型
	 {
        //先 记录当时结果
		 MyInteger temp = *this;
                     //记录当前本身的值,然后让本身的值加1,但返回的是以前的值,达到先返回后++
		 //后 递增++操作
		 m_Num++;
		 //最后 将记录结果做返回
		 return temp;
	 }
private:
	int m_Num;
};

//全局函数重载左移运算符
ostream& operator<< (ostream & cout, MyInteger myint)
{
	cout << myint.m_Num;
	return cout;
}
//测试++前置
void test01()
{
	MyInteger myint;
	cout <<++(++myint) << endl;
	cout << myint << endl;

}

//测试++后置
void test02()
{
	MyInteger myint;
	cout << myint++ << endl;
	cout << myint << endl;
}

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

 总结:前置递增返回引用,后置递增返回值。

4.5.4 赋值运算符重载(=)

    类中提供的默认operator= 对对象或者对象之间进行赋值操作时使用,做属性的值拷贝,会产生深拷贝和浅拷贝问题。那么如何重载赋值运算符号,以及什么时候重载。

编译器提供的拷贝是浅拷贝,如果类中的对象变量创建在堆区,就要进行深拷贝。

//赋值运算符重载
class Person
{
public:
	Person(int age)
	{
		m_Age = new int(age);//new出的对象返回int*,将年龄数据开辟到堆区
	}

	~Person()//写了析构函数之后代码崩溃了(因为堆区内容重复释放,p1、p2均要释放堆区商的m_Age)
	{
		if (m_Age != NULL)
		{
			delete m_Age;
			m_Age = NULL;
		}
	}

	//重载 赋值运算符
	Person&  operator=(Person& p)
	{
		//编译器提供浅拷贝
		//m_Age=p.m_Age;
		
		//应该判断是否有属性在堆区,如果有先释放干净,然后在进行深拷贝(p2本身就有一个堆区内存应该先释放)
		if (m_Age != NULL)
		{
			delete m_Age;
			m_Age = NULL;
		}
		//深拷贝
		m_Age = new int(*p.m_Age);//让对象指向新开辟的内存地址

		//返回对像本身
		return *this;//返回对象本身,在进行操作时将其就可以赋给p3
	}

	int * m_Age;//m_Age为指针变量,开辟在堆区
};

void test01()
{
	Person p1(18);
	Person p2(20);
	Person p3(30);

	p3 = p2 = p1; //赋值操作
	//三个连等错,p3=void为错的
	cout << "p1的年龄为: " << *p1.m_Age << endl;
	cout << "p2的年龄为: " << *p2.m_Age << endl;
	cout << "p3的年龄为: " << *p3.m_Age << endl;
}

int main()
{
	test01();

	内置数据类型
	//int a = 10;
	//int b = 20;
	//int c = 30;

	//c = b = a;//可以进行连等操作

	//cout << "a= " << a << endl;
	//cout << "b= " << b << endl;
	//cout << "c= " << c << endl;

	system("pause");
	return 0;
}

写了析构函数之后程序崩溃:

4.5.5 关系运算符重载(<,>,=,!=,大于等于,小于等于)

         进行对比操作时,对自己定义的数据进行对比要进行关系运算符号的重载。

作用:重载关系运算符,可以让两个自定义类型对象进行比较操作。

//重载关系运算符
class Person
{
public:
	Person(string name, int age)
	{
		m_Name = name;
		m_Age = age;
	}

	//重载==号
	bool operator==(Person &p)
	{
		if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
		{
			return true;
		}
		return false;
	}

	//重载!=号
	bool operator!=(Person& p)
	{
		if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
		{
			return false;
		}
		return true;
	}
	string m_Name;
	int m_Age;
};

void test01()
{
	Person p1("Tom", 18);
	Person p2("Tom", 18);
	if (p1 == p2)
	{
		cout << "p1和p2是相等的!" << endl;
	}
	else
	{
		cout << "p1和p2是不相等的!" << endl;
	}

	if (p1 != p2)
	{
		cout << "p1和p2是不相等的!" << endl;
	}
	else
	{
		cout << "p1和p2是相等的!" << endl;
	}
}
int main()
{
	test01();
	system("pause");
	return 0;
}

4.5.6 函数调用运算符重载

//函数调用运算符重载
//打印输出函数类
#include<string>

class MyPrint
{
public:

	//重载函数调用运算符
	void operator()(string test)//返回的是void类型
	{
		cout << test << endl;
	}

};

//正常函数调用
void MyPrint02(string test)
{
	cout << test << endl;
}



void test01()
{
	MyPrint myPrint;

	myPrint("hello world");//由于使用起来非常类似于函数调用,所以称仿函数

	MyPrint02("hello world");
}


//仿函数非常灵活,没有固定的写法

//加法类

class MyAdd 
{
public:
	int operator()(int num1, int num2)//返回的是int 
	{
		return num1 + num2;
	}
};
void test02()
{
	MyAdd myadd;
	int ret=myadd(100, 100);
	cout << "ret= " << ret << endl;

   //匿名函数对象
	//类名加()创建匿名对象
	cout << MyAdd()(100, 100) << endl;
}
int main()
{
	test01();
	test02();
	system("pause");
	return 0;
}

4.6 继承

4.6.1 继承的基本语法

 普通实现:(会重复很多代码)

普通实现页面----重复量大
//java实现页面
class Java
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java、Python、C++、...(公共分类列表)" << endl;
	}
	void content()
	{
		cout << "Java学科视频" << endl;
	}
};

//Python页面
class Python
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java、Python、C++、...(公共分类列表)" << endl;
	}
	void content()
	{
		cout << "Python学科视频" << endl;
	}
};


//C++页面
class CPP
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java、Python、C++、...(公共分类列表)" << endl;
	}
	void content()
	{
		cout << "C++学科视频" << endl;
	}
};

继承实现:

//继承的好处:减少重复代码
// 语法:class 子类:继承方式 父类
// 子类也称为派生类  父类也成为基类
// 
//继承实现页面
//公共页面
class BasePag
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java、Python、C++、...(公共分类列表)" << endl;
	}
};

//Java 页面
class Java :public BasePag
{
public:
	void content() 
	{
		cout << "Java学科视频" << endl;
	}
};

//Python 页面
class Python :public BasePag
{
public:
	void content()
	{
		cout << "Python学科视频" << endl;
	}
};
//C++页面
class CPP :public BasePag
{
public:
	void content()
	{
		cout << "CPP学科视频" << endl;
	}
};
void test01()
{
	cout << "Java下载视频页面如下: " << endl;
	Java ja;
	ja.header();
	ja.footer();
	ja.left();
	ja.content();

	cout << "-------------------------" << endl;
	cout << "Python下载视频页面如下: " << endl;
	Python py;
	py.header();
	py.footer();
	py.left();
	py.content();

	cout << "-------------------------" << endl;
	cout << "C++下载视频页面如下: " << endl;
	CPP cpp;
	cpp.header();
	cpp.footer();
	cpp.left();
	cpp.content();
}
int main()
{
	test01();
	system("pause");
	return 0;
}

总结:

4.6.2 继承方式

 

class Base1
{
public:
	int m_A;
protected:   //类内可以访问,类外不可以访问
	int m_B;
private:
	int m_C;

};

//公共继承
class Son1 :public Base1
{
public:
	void func()
	{
		m_A = 10;//父类中的公共权限成员 到子类中依然是公共权限
		m_B = 20;//父类中的保护权限成员 到子类中依然是保护权限
		//m_C = 30;//父类中的私有权限成员 子类访问不到
	}
};


void test01()
{
	Son1 s1;
	s1.m_A = 100;
	//s1.m_B = 200;//到了Son1中m_B保护权限,类外不可以访问
}

//保护继承
class Base2
{
public:
	int m_A;
protected:   //类内可以访问,类外不可以访问
	int m_B;
private:
	int m_C;

};
class Son2 :protected Base2
{
public:
	void func()
	{
		m_A = 10;//父类中的公共权限成员 到子类中变为保护权限
		m_B = 20;//父类中的保护权限成员 到子类中是保护权限
		//m_C = 30;//父类中的私有权限成员 子类访问不到
	}
};
void test02()
{
	Son2 s1;
	//s1.m_A;//在Son2中 m_A变为保护权限,因此类外访问不到
	//s1.m_B;//在Son中m_B为保护权限,不可以访问

}

//私有继承
class Base3
{
public:
	int m_A;
protected:   //类内可以访问,类外不可以访问
	int m_B;
private:
	int m_C;

};
class Son3 :private Base3
{
public:
	void func()
	{
		m_A = 10;//父类中的公共权限成员 到子类中变为私有权限成员
		m_B = 20;//父类中的保护权限成员 到子类中变为私有权限
		//m_C = 30;//父类中的私有权限成员 子类访问不到
	}
};

class GrandSon :public Son3
{
public:
	void func()
	{
		//m_A = 10;//到了Sons中,m_A变为私有,即使是儿子,即使是公共继承也不可以访问
		//m_B = 20;
	}
};
void test03()
{
	Son3 s1;
	//s1.m_A;//在Son3中 m_A变为私有权限,因此类外访问不到
	//s1.m_B;//在Son中m_B为私有权限,不可以访问

}
int main()
{
	test01();
	test02();
	test03();
	system("pause");
	return 0;
}

4.6.3 继承中的对象模型

//继承中的对象模型

class Base
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

class Son :public Base
{
public:
	int m_D;
};

void test01()
{
	//父类中所有非静态成员属性都会被子类继承下去
	//父类中私有成员属性是被编译器给隐藏了,因此是访问不到的,但是确实被继承了
	cout << "size of Son= " << sizeof(Son) << endl;//12
}

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

 查看具体的继承方式:

 结论:父类中的私有成员也是被子类继承下去,只是由编译器给隐藏最后访问不到。

4.6.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 test01()
{
	//Base b;

	//继承中的构造和析构顺序如下
	//先构造父类,在构造子类,析构的顺序与构造的顺序相反
	Son s;
}

int main()
{
	test01();
	system("pause");
	return 0;

}

 总结:继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。

4.6.5 继承同名成员处理方式

//继承中同名成员的处理方式
class Base
{
public:
	Base()
	{
		m_A = 100;
	}

	void func()
	{
		cout << "Base - func() 调用" << endl;
	}

	void func(int a)//函数重载
	{
		cout << "Base - func(int a) 调用" << endl;
	}
	int m_A;
};

class Son:public Base
{
public:
	Son()
	{
		m_A = 200;

	}
	void func()
	{
		cout << "Son - func() 调用" << endl;
	}
	int m_A;
};

//同名成员属性处理方式
void test01()
{
	Son s;
	cout << "Son 下 m_A= " << s.m_A << endl;
	//如果通过子类对象 访问到父类中同名成员,需要加作用域
	cout << "Base 下 m_A= " << s.Base::m_A << endl;
}

//同名成员函数处理方式
void test02()
{
	Son s;

	s.func();//直接调用 调用的是子类中的成员

	//如何调用父类中的成员函数,加作用域
	s.Base::func();

	//如果子类中出现和父类中同名的成员函数,子类的同名成员函数会隐藏掉父类中所有同名成员函数
	//如果想访问到父类中被隐藏的同名成员函数,需要加作用域
	s.Base::func(100);
}
int main()
{

	//test01();
	test02();
	system("pause");
	return 0;
}

总结:

4.6.6 继承同名静态成员处理方式


//继承同名静态成员的处理方法
class Base
{
public:
	static int m_A;

	static void func()
	{
		cout << "Base -static void func()的调用" << endl;
	}

	static void func(int a)
	{
		cout << "Base -static void func(int a)的调用" << endl;
	}
};
int Base::m_A = 100;

class Son :public Base
{
public:
	static int m_A;

	static void func()
	{
		cout << "Son -static void func()的调用" << endl;
	}
};
int Son::m_A = 200;

//同名静态成员属性
void test01()
{
	//1、通过对象访问
	cout << "通过对象访问:" << endl;
	Son s;
	cout << "Son  下 m_A= " << s.m_A << endl;
	cout << "Base 下 m_A= " << s.Base::m_A << endl;

	//1、通过类名访问
	cout << "通过类名访问:" << endl;
	cout << "Son 下 m_A= " << Son::m_A<<endl;
	//第一个::代表通过类名访问,第二个::代表访问父类作用域下
	cout << "Base 下 m_A= " <<Son::Base::m_A<< endl;

}
//同名静态成员函数
void test02()
{
	//1、通过对象调用
	cout << "通过对象访问" << endl;
	Son s;
	s.func();
	s.Base::func();

	//2、通过类名调用
	cout << "通过类名访问" << endl;
	Son::func();
	Son::Base::func();

	//子类出现和父类同名静态成员函数,也会隐藏父类中所有同名成员函数
	//Son::func(100);//直接调用失败:父类中被隐藏
	//如果想访问父类中被隐藏同名成员函数,需要加作用域
	Son::Base::func(100);
}
int main()
{
	//test01();
	test02();
	system("pause");
	return 0;
}

 总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问方式(通过对象和通过类名)。

4.6.7 多继承语法

//多继承语法
class Base1
{
public:
	Base1()
	{
		m_A = 100;
	}

	int m_A;
 };

class Base2
{
public:
	Base2()
	{
		m_A = 200;
	}
	int m_A;
};

//子类 需要继承Base1和Base2
class Son :public Base1, public Base2
{
public:
	Son()
	{
		m_C = 300;
		m_D = 400;
	}
	int m_C;
	int m_D;
};

void test01()
{
	Son s;
	cout << "sizeof Son= "<< sizeof(s) << endl;//16
	//当父类中出现同名成员,需要加作用域区分
	cout << "Base1::m_A=  " << s.Base1::m_A << endl;
	cout << "Base2::m_A=  " << s.Base2::m_A << endl;
}

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

  总结:多继承中如果出现同名情况,子类使用时候加作用域区分。

4.6.8 菱形继承

// 菱形继承
//动物
class Animal
{
public:
	int m_Age;
};

//利用虚继承 解决菱形继承的问题
// 在继承之前 加上关键字 virtual 变为虚继承
// Animal类称为 虚基类
//羊类
class Sheep :virtual public Animal{};

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

//羊驼类
class SheepTuo :public Sheep, public Tuo{};

void test01()
{
	SheepTuo st;

	st.Sheep::m_Age=18;
	st.Tuo::m_Age = 28;
	//当菱形继承,两个父类拥有相同数据,需要加以作用域区分
	cout << "st.Sheep::m_Age= " << st.Sheep::m_Age << endl;
	cout << "st.Tuo::m_Age= " << st.Tuo::m_Age << endl;
	//这份数据我们知道,只有一份就可以,菱形继承导致数据有两份,资源浪费
	cout << "st.m_Age=" << st.m_Age << endl;//自身访问 
}
int main()
{
	test01();
	system("pause");
	return 0;
}

 继承和虚继承实质区别:

 虚基类继承:

 

4.7 多态

4.7.1 多态的基本概念

函数重载让函数名以多种形态展现。

//4.7 多态
//动物类
class Animal
{
public:
	//虚函数
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};

//猫类
class Cat:public Animal
{
public:
	//重写:函数返回值类型 函数名 参数列表 完全相同  (virtual 可写可不写)
	void speak()   //virtual 关键字可写可不写
	{
		cout << "小猫在说话" << endl;
	}
};


//狗类
class Dog:public Animal
{
public:
	void speak()
	{
		cout << "小狗在说话" << endl;
	}
};

//地址早绑定 在编译阶段确定函数地址
//如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,地址晚绑定

//执行说话的函数
//我们希望传入什么对象,那么就调用什么对象的函数
void doSpeak(Animal & animal)//Animal & animal =cat;用父类的引用指向子类的对象
{
	animal.speak();
}

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

//动态多态使用
//父类的指针或者引用 指向子类的对象

void test01()
{
	Cat cat;
	doSpeak(cat);

	Dog dog;
	doSpeak(dog);
}
int main()
{
	test01();
	system("pause");
	return 0;
}

4.7.2 多态案例—计算器类

//4.7.2多态案例——计算器类
//分别利用普通写法和多态技术实现计算器类

//普通写法
#include<string>
class Calculator
{
public:
	int getResult(string oper)
	{
		if (oper == "+")
		{
			return m_Num1 + m_Num2;
		}
		else if (oper == "-")
		{
			return m_Num1 - m_Num2;
		}
		else if(oper=="*")
		{
			return m_Num1 * m_Num2;
		}
		//如果想扩展新的功能,需要修改源码
		//在真正开发中,提倡 开闭原则
		//开闭原则:对扩展进行开放,对修改进行关闭

	}

	int m_Num1;//操作数1
	int m_Num2;//操作数2
};

void test01()
{
//创建计算器对象
	Calculator c;
	c.m_Num1 = 10;
	c.m_Num2 = 10;

	cout << c.m_Num1 << "+" << c.m_Num2 << "=" << c.getResult("+") << endl;

	cout << c.m_Num1 << "-" << c.m_Num2 << "=" << c.getResult("-") << endl;

	cout << c.m_Num1 << "*" << c.m_Num2 << "=" << c.getResult("*") << endl;
}


//利用多态实现计算器
//多态好处:
// 1、组织结构清晰
// 2、可读性强
// 3、对于前期和后期扩展以及维护性高


//实现计算器抽象类
class AbstractCalculator
{
public:
	virtual int getResult()//虚函数
	{
		return 0;
	}

	int m_Num1;
	int m_Num2;
};

//加法计算器类
class AddCalculator :public AbstractCalculator
{
public:

	int getResult()
	{
		return m_Num1 + m_Num2;
	}
};

//减法计算器类
class SubCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 - m_Num2;
	}
};

//乘法计算器类
class MulCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 * m_Num2;
	}
};


void test02()
{
	//多态的使用条件
	//父类指针或者引用指向子类对象

	//加法运算
	AbstractCalculator * abc = new AddCalculator;  //new出来的对象存放在堆区,需要手动释放
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;

	cout << abc->m_Num1 << "+" << abc->m_Num2 << "=" << abc->getResult() << endl;
	//用完后记得销毁(释放的是数据,指针还是父类的指针)
	delete abc;

	//减法运算
	abc = new SubCalculator;
	abc->m_Num1 = 100;
	abc->m_Num2 = 100;

	cout << abc->m_Num1 << "+" << abc->m_Num2 << "=" << abc->getResult() << endl;

	//乘法运算
	abc = new MulCalculator;
	abc->m_Num1 = 100;
	abc->m_Num2 = 100;

	cout << abc->m_Num1 << "*" << abc->m_Num2 << "=" << abc->getResult() << endl;
}
int main()
{
	//test01();
	test02();
	system("pause");
	return 0;
}

总结:C++开发提倡利用多态设计程序架构,因为多态优点很多。

4.7.3 纯虚构函数和抽象类

4.7.4 多态案例二——制作饮品

//4.7.4多态案例二—制作饮品
class AbstractDrinking
{
public:
	//煮水
	virtual void Boil() = 0;
	
	//冲泡
	virtual void Brew() = 0;
	
	//倒入杯中
	virtual void PourInCup() = 0;
	
	//加入辅料
	virtual void PutSomething() = 0;
	
	//制作饮品
	void makeDrink()
	{
		Boil();
		Brew();
		PourInCup();
		PutSomething();

	}
};

//制作咖啡
class Coffee :public AbstractDrinking
{
public:
	//煮水
	virtual void Boil()
	{
		cout << "煮农夫山泉" << endl;
	}

	//冲泡
	virtual void Brew()
	{
		cout << "冲泡咖啡" << endl;
	}

	//倒入杯中
	virtual void PourInCup()
	{
		cout << "倒入杯中" << endl;
	}

	//加入辅料
	virtual void PutSomething()
	{
		cout << "加入糖和牛奶" << endl;
	}
};

//制作茶水
class Tea :public AbstractDrinking
{
public:
	//煮水
	virtual void Boil()
	{
		cout << "煮矿泉水" << endl;
	}

	//冲泡
	virtual void Brew()
	{
		cout << "冲泡茶叶" << endl;
	}

	//倒入杯中
	virtual void PourInCup()
	{
		cout << "倒入茶杯" << endl;
	}

	//加入辅料
	virtual void PutSomething()
	{
		cout << "加入柠檬" << endl;
	}
};

//制作函数
void doWork(AbstractDrinking* abs)//AbstractDrinking *abs=new Coffee(父类的指针指向子类的对象)
{
	abs->makeDrink();
	delete abs;//释放堆区指针
}

void test01()
{
	//制作咖啡
	doWork(new Coffee);
	cout << "----------------" << endl;
	//制作茶叶
	doWork(new Tea);//根据不同对象的传入,来具体看走哪一个
}
int main()
{
	test01();
	system("pause");
	return 0;
}

4.7.5 虚析构和纯虚析构

 

#include<string>
class Animal
{
public:
	Animal() {
		cout << "Animal的构造函数调用" << endl;
	}

	~Animal() {
		cout << "Animal的析构函数调用" << endl;
	}
	//纯虚析构
	virtual void speak() = 0;
	
};

class Cat :public Animal
{
public:

	Cat(string name)
	{
		cout << "Cat的构造函数调用" << endl;
		m_Name = new string(name);//用指针维护创建出来的堆区猫名
	}
	virtual void speak()
	{
		cout <<*m_Name<< "小猫在说话" << endl;
	}

	~Cat()
	{
		if (m_Name != NULL)
		{
			cout << "Cat析构函数调用" << endl;
			delete m_Name;
			m_Name = NULL;
		}
	}
	string* m_Name;//猫的名字用指针维护
};

void test01()
{
	Animal* animal = new Cat("Tom");
	animal->speak();
	delete animal;

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

 

 没有走子类的析构函数,即没有释放堆区数据,导致内存泄露。

解决办法1:将父类中的析构函数改为虚析构函数。

//利用虚析构可以解决 父类指针释放子类对象时不干净的问题
virtual ~Animal() {
		cout << "Animal的析构函数调用" << endl;
	}

 解决方法:将父类中的析构函数改为下面的纯虚析构

//纯虚析构
	virtual ~Animal() = 0;

在父类外实现,需要有函数体内具体实现。(需要声明也需要实现)

Animal:: ~Animal()
{
	cout << "Animal的纯析构函数调用" << endl;
}

 具体代码如下:

//4.7.5虚析构和纯虚析构
#include<string>
class Animal
{
public:
	Animal() {
		cout << "Animal的构造函数调用" << endl;
	}

	利用虚析构可以解决 父类指针释放子类对象时不干净的问题
	//virtual ~Animal() {
	//	cout << "Animal的虚析构函数调用" << endl;
	//}

	//纯虚析构  需要有声明也需要实现
	//有了纯虚析构之后,这个类也属于抽象类,无法实现实例化对象
	virtual ~Animal() = 0;

	//纯虚函数
	virtual void speak() = 0;
	
};

Animal:: ~Animal()
{
	cout << "Animal的纯析构函数调用" << endl;
}

class Cat :public Animal
{
public:

	Cat(string name)
	{
		cout << "Cat的构造函数调用" << endl;
		m_Name = new string(name);//用指针维护创建出来的堆区猫名
	}
	virtual void speak()
	{
		cout <<*m_Name<< "小猫在说话" << endl;
	}

	~Cat()
	{
		if (m_Name != NULL)
		{
			cout << "Cat析构函数调用" << endl;
			delete m_Name;
			//父类指针在析构时候 不会调用子类中析构函数,导致子类中如果有堆区属性,出现内存泄露
			//解决办法:给基类增加一个虚析构函数
			//虚析构函数就是用来解决通过父类指针释放子类对象
			m_Name = NULL;
		}
	}
	string* m_Name;//猫的名字用指针维护(有属性开辟到堆区)
};

void test01()
{
	Animal* animal = new Cat("Tom");
	animal->speak();
	delete animal;

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

 

4.7.6 多态案例三—电脑组装

 案例需求分析:

//4.7.6 多态案例三—电脑组装
//抽象不同零件类
//抽象CPU类
class CPU
{
public:
	//抽象的计算函数
	virtual void calculate() = 0;
};

//抽象显卡类
class VideoCard
{
public:
	//抽象的显卡函数
	virtual void display() = 0;
};

//抽象内存条类
class Memory
{
public:
	//抽象的存储函数
	virtual void storage() = 0;
};

//电脑类
class Computer
{
public:
	Computer(CPU* cpu, VideoCard* vc, Memory* mem)
	{
		m_cpu = cpu;
		m_vc = vc;
		m_mem = mem;
	}

	//提供工作的函数
	void work()
	{
		m_cpu->calculate();

		m_vc->display();

		m_mem->storage();
	}

	//提供析构函数 释放3个电脑零件
	//释放CPU
	~Computer()
	{
		//释放CPU
		if (m_cpu != NULL)
		{
			delete m_cpu;
			m_cpu = NULL;
		}

		//释放显卡零件
		if (m_vc != NULL)
		{
			delete m_vc;
			m_vc = NULL;
		}

		//释放内存条零件
		if (m_mem != NULL)
		{
			delete m_mem;
			m_mem = NULL;
		}
	}


private:

	CPU* m_cpu;//CPU的零件指针
	VideoCard* m_vc;//显卡零件指针
	Memory* m_mem;//内存条零件指针

};

//具体厂商
//Inter厂商
class IntelCPU :public CPU
{
public:
	virtual void calculate()
	{
		cout << "Intel的CPU开始计算了!" << endl;
	}
};

class IntelVideoCard :public VideoCard
{
public:
	virtual void display()
	{
		cout << "Intel的显卡开始显示了!" << endl;
	}
};

class IntelMemory :public Memory
{
public:
	virtual void storage()
	{
		cout << "Intel的内存条开始存储了!" << endl;
	}
};

//Lenovo厂商
class LenovoCPU :public CPU
{
public:
	virtual void calculate()
	{
		cout << "Lenovo的CPU开始计算了!" << endl;
	}
};

class LenovoVideoCard :public VideoCard
{
public:
	virtual void display()
	{
		cout << "Lenovo的显卡开始显示了!" << endl;
	}
};

class LenovoMemory :public Memory
{
public:
	virtual void storage()
	{
		cout << "Lenovo的内存条开始存储了!" << endl;
	}
};


void test01()
{
	cout << "第一台电脑开始工作" << endl;
	//第一台电脑零件
	CPU * intelCpu = new IntelCPU;
	VideoCard* intelCard = new IntelVideoCard;
	Memory* intelMem = new IntelMemory;

	//创建第一台电脑
	Computer* computer1 = new Computer(intelCpu, intelCard, intelMem);
	computer1->work();
	delete computer1;

	cout << "-------------------" << endl;
	cout << "第二台电脑开始工作" <<endl;
	//第二台电脑组装
	Computer* computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);
	computer2->work();
	delete computer2;

	cout << "-------------------" << endl;
	cout << "第三台电脑开始工作" << endl;
	//第三台电脑组装
	Computer* computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);
	computer3->work();
	delete computer3;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

5 文件操作

5.1 文本文件

 5.1.1 写文件

 

//5、文本文件 写文件
#include <fstream>
void test01()
{
	//1、包含头文件 fstream

	//2、创建流对象
	ofstream ofs;

	//3、指定打开方式
	ofs.open("test.txt", ios::out);//默认在项目文件的路径下

	//4、写内容
	ofs << "姓名:张三" << endl;
	ofs << "性别:男" << endl;
	ofs << "年龄:18" << endl;

	//5、关闭文件
	ofs.close();

}

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

 

 5.1.2 读文件 


//5.2 读文件
//文本文件 读文件
#include <fstream>
#include <string>
void test01()
{
	//1、包含头文件
	

	//2、创建流对象
	ifstream ifs;

	//3、打开文件 并且判断是否打开成功
	ifs.open("test.txt", ios::in);

	if ( !ifs.is_open())
	{
		cout << "文件打开失败" << endl;
		return;
	}

	//4、读数据
	//第一种 
	//char buf[1024] = { 0 };//创建一个字符数组
	//while (ifs >> buf)//一行一行读取,直到读不到输出假,则循环结束
	//{
	//	cout << buf << endl;
	//}

	第二种
	//char buf[1024] = { 0 };//将读取的数据放到数组中
	// getline(放到那个地方,最多读取的大小)——为成员函数(ifs对象的)
	//while (ifs.getline(buf, sizeof(buf)))
	//{
	//	cout << buf << endl;
	//}

	//第三种
	//string buf;//将所有数据放到字符串中
	// 全局函数getline(基础输入流ifs,准备好的字符串)
	//while (getline(ifs, buf))
	//{
	//	cout << buf << endl;
	//}

	//第四种(一个字符一个字符读)——不推荐使用效率较慢
	char c;
	while ((c = ifs.get()) != EOF)//如果没读到文件尾部则循环一直 EOF:end of file
	{
		cout << c;
	}
}
int main()
{
	test01();
	system("pause");
	return 0;

 

5.2 二进制文件

 

5.2.1 写文件

//5.2.1、写文件
#include<fstream>

class Person
{
public:
	char m_Name[64];//姓名
	int m_Age;//
};

void test01()
{
	//1、包含头文件

	//2、创建流对象
	ofstream ofs("person.txt", ios::out | ios::binary);

	//3、打开文件
	//ofs.open("person.txt",ios::out|ios::binary);
	// 
	//4、写文件
	Person p = { "张三",8 };
	ofs.write((const char*)&p, sizeof(Person));//write(const char*  ,std::streamsize)
	//5、关闭文件
	ofs.close();
}
int main()
{
	test01();
	system("pause");
	return 0;
}

总结:文件输出流对象 可以通过write函数,以二进制方式写数据。

5.2.2 读文件

//5.2.2 二进制度文件
#include<fstream>
class Person
{
public:
	char m_Name[64];//姓名
	int m_Age;//年龄
};

void test01()
{
	//1、包含头文件

	//2、创建流对象
	ifstream ifs;

	//3、打开文件  判断文件是否打开成功
	ifs.open("person.txt", ios::in | ios::binary);

	if (!ifs.is_open())
	{
		cout << "文件打开失败" << endl;
		return;
	}
	//4、读文件 
	Person p;

	ifs.read((char*)&p, sizeof(Person));

	cout << "姓名:   " << p.m_Name << " 年龄:  " << p.m_Age << endl;

	//5、关闭文件
	ifs.close();
}
int main()
{
	test01();
	system("pause");
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值