【C++】函数重载、引用、类和对象

目录

一、函数重载
   1.1 函数重载概念
   1.2 C++支持函数重载的原理

二、引用
   2.1 引用概念
   2.2 引用特性
   2.3 常引用
   2.4 引用的使用场景
   2.5 传值、传引用效率比较
   2.6 引用与指针的区别

三、类和对象
   3.1 面向过程和面向对象
   3.2 类的引入和定义
   3.3 类的访问限定符和封装及其作用域
   3.4 类的实例化
   3.5 类对象大小的计算
   3.6 类成员函数的this指针

   3.7 类的6个默认成员函数
     3.7.1 构造函数和析构函数
     3.7.2 拷贝构造函数
     3.7.3 赋值运算符重载
     3.7.4 取地址及const取地址操作符重载

   3.9 static成员
   3.10 友元、内部类
   3.11 匿名对象及拷贝对象时的一些编译器优化


函数重载

1.函数重载概念

  是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的(形参列表参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

#include<iostream>
using namespace std;

// 1、参数类型不同
int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}
double Add(double left, double right)
{
	cout << "double Add(double left, double right)" << endl;
	return left + right;
}

// 2、参数个数不同
void Func()
{
	cout << "Func()" << endl;
}
void Func(int a)
{
	cout << "Func(int a)" << endl;
}

// 3、参数类型顺序不同
void Func(int a, char b)
{
	cout << "Func(int a,char b)" << endl;
}
void Func(char b, int a)
{
	cout << "Func(char b, int a)" << endl;
}

int main()
{
	Add(10, 20);
	Add(10.1, 20.2);
	Func();
	Func(10);
	Func(10, 'a');
	Func('a', 10);
	return 0;
}


2.C++支持函数重载的原理--名字修饰(name Mangling)

  为什么C++支持函数重载,而C语言不支持呢?

在C/C++中,一个程序要运行起来,需要经历预处理、编译、汇编、链接四个过程。
在这里插入图片描述
在这里插入图片描述

  1. 实际项目通常是由多个头文件和多个源文件构成,而通过C语言阶段学习的编译链接,我们可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么怎么办呢?
  2. 所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。
  3. 那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。
  4. 由于Windows下vs的修饰规则过于复杂,Linux下修饰规则相对简单,因此我们以Linux下为例。
  5. 通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】

对于同一段代码在Linux上编译

在这里插入图片描述
C语言编译器的结果
在这里插入图片描述
不难发现,gcc编译完成后,函数名并没有发生修改

C++编译器编译的结果
在这里插入图片描述
而g++编译完成后,对函数名做出了修饰,并将函数的参数类型添加到了函数名后

以下是Windows下函数名修饰规则
在这里插入图片描述
根据以上结果,我们便可以得出结论

C++通过函数修饰规则来区分同名函数,从而支持函数重载,条件是函数参数必须不同,如果仅仅是返回值不同,并不能构成函数重载。


引用

1.引用概念

  引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。(就如同“你的名字”和“你的外号”一样)

类型& 引用变量名(对象名) = 引用实体

#include<iostream>
using namespace std;

int main()
{
	int a = 10;
	int& b = a;
	//b就是a的引用
	
	//引用类型必须和引用实体是同种类型的!!!
}


2.引用特性
  • 引用在定义时必须初始化
  • 一个变量可以有多个引用(也就是可以有多个“别名”)
  • 引用一旦引用一个实体,就不能引用其他实体
#include<iostream>
using namespace std;

int main()
{
	//1.引用必须初始化
	int a = 10;
	int& b = a;//allowed
	int& c;//not allowed
	
	//2.一个变量可以有多个引用
	int i = 20;
	int& j = i;//allowed
	int& m = i;//allowed
	int& n = i;//allowed
	
	//3.引用一旦引用一个实体,就不能引用其他实体
	int p = 30;
	int q = 40;
	int& r = p;//allowed
	r = q;//not allowed 
}


3.常引用
#include<iostream>
using namespace std;

int main()
{
	int a = 10;
	int& b = a;//allowed
	const int& c = a;//allowed
	
	const int d = 20;
	const int& m = d;//allowed
	int& n = d;//not allowed
}

权限可以缩小,但不能放大,如果引用实体为常量(即该实体的值不可被修改),那么只能对其使用常引用,但如果引用实体为变量,那么既可以对其使用常引用(但不能通过常引用取修改引用实体的值),也可以对其引用。

4.引用的使用场景
  • 作参数
void Swap(int& i, int& j)
{
	int tmp = i;
	i = j;
	j = tmp;
}
  • 作返回值
int& Count()
{
	static int n = 0;
	n++;
	return n;
}

这里需要注意的是:将引用作为返回值时,返回的对象必须还在(即该空间还没有还给操作系统),如果返回了一个函数结束后会被销毁的对象(如局部变量)的引用,一种情况是该对象没有被立即销毁,能够正确地返回值,另一种情况是该对象立即被销毁,则函数会返回一个随机值。因此,对于返回局部变量这种对象,应该使用返回值,而不是返回引用

5.传值、传引用效率比较

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

传值和传引用的效率对比
在这里插入图片描述
值返回和引用返回的效率对比
在这里插入图片描述


6.引用与指针的区别

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间

在这里插入图片描述
底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

在这里插入图片描述
引用和指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。

  2. 引用在定义时必须初始化,指针没有要求(但不初始化指针会造成野指针的问题)

  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

  4. 没有NULL引用,但有NULL指针

  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)

  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

  7. 有多级指针,但是没有多级引用

  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

  9. 引用比指针使用起来相对更安全


类和对象

1.面向过程和面向对象

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

在这里插入图片描述
在这里插入图片描述
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

在这里插入图片描述

2.类的引入和定义

C语言中的结构体中只能定义变量,而在C++中,结构体中不仅能够定义变量,也可以定义函数。

//在C++中更习惯用class代替struct
struct Student
{
	void Print()
	{
		cout << name << endl;
		cout << age << endl;
		cout << grade << endl;
	}
	
	char name[20];
	int age;
	int grade;
};
/*
class ClassName(class为关键字,ClassName为类的名字)
{

	类体:由成员函数和成员变量组成
	
};(分号不能掉!)
*/

类的两种定义方式:

  1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。(如同上述代码)

  2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
    在这里插入图片描述

成员变量名命名建议:一般为变量名加一个前缀或后缀,不然容易造成变量名和函数参数名相同的情况(如 age = age / grade = grade)

3.类的访问限定符和封装及其作用域

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

在这里插入图片描述
【访问限定符说明】

  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. 如果后面没有访问限定符,作用域就到 } 即类结束。
  5. class的默认访问权限为private,struct为public(因为struct要兼容C)

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

封装

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

在类和对象中,主要研究的是封装。那什么是封装呢?

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理,让用户更方便使用类。

在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用

类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 “::” 作用域操作符指明成员属于哪个类域。如上述代码中的Print函数在.c文件中定义时,必须要加上Student::


4.类的实例化
  1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没
    有分配实际的内存空间来存储它
    ;比如:入学时填写的学生信息表,表格就可以看成是一个
    类,来描述具体学生信息。
  2. 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
struct Student
{
	char name[20];
	int age;
	int grade;
};
int main()
{
	//我们可以实例化多个Student类对象
	Student s1;
	Student s2;
	Student s3;
	return 0;
}

3.类就如同设计图纸,类的实例化就是在制造出图纸中的内容,我们可以由一张设计图纸制造出多个内容。


5.类对象大小的计算

我们说过,类中既可以包含成员变量,也可以包含成员函数,那我们如何计算一个类的大小呢?

猜测:

  • 对象中包含各个类的成员

在这里插入图片描述不难想到,每个对象中的成员变量是不同的,但它们都调用同一份函数,如果按照这种方式存储,每个对象中都保存一份代码,相同的代码保存多次,就会造成空间的浪费。那应该如何解决呢?

  • 代码只保存一份,在对象中保存存放代码的地址

在这里插入图片描述

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

在这里插入图片描述
我们通过分别获取不同对象的大小能够得出结论
在这里插入图片描述
很显然,这种结果符合第三种猜测

结论:一个类的大小,就是这个类中“成员变量”之和(要注意内存对齐,与结构体中的规则相同)而对于空类,编译器给了空类一个字节来唯一标识这个类的对象


6.类成员函数的this指针

以下内容均以Date日期类为例

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, d2;
 	d1.Init(2023, 2, 5);
 	d2.Init(2022, 2, 6);
 	d1.Print();
	d2.Print();
 return 0;
}

我们不禁疑问,Date类中有Init和Print两个成员函数,但并没有它们对于不同对象的区分,当d1和d2同时调用这两个函数时,它们该如何区分呢?

C++中通过引入this指针解决该问题

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

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

7.类的6个默认成员函数

“空类”实际上并不是空类,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现时,编译器生成的成员函数称为默认成员函数。

在这里插入图片描述

7.1构造函数和析构函数

构造函数

构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

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

构造函数的特性:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
  6. 默认生成的构造函数对内置类型不作处理,只处理自定义类型

内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
    注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
class Date
{
//private:
public:
	//成员变量
	int _year;
	int _month;
	int _day;

public:

	//构造函数(不是创建了对象,而是初始化对象)
	//默认生成的构造函数对内置类型不处理,只处理自定义类型
	
	//无参
	Date()
	{}
	
	//含参
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	//全缺省构造函数
	/*
	Date(int year = 1970, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	*/
};
void TestDate()
{
      Date d1; // 调用无参构造函数
      /*
      	注意!调用无参构造函数时,对象名后不能带"()"!
      */
      Date d2()//not allowed
      
      Date d3(2023, 2, 6); // 调用带参的构造函数
      
      /*
      调用全缺省构造函数
      Date d4;//不传参(与调用无参构造函数相同,对象名后不能带"()")
      Date d5(2023, 2);//传参
      */
}

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化因为初始化只能初始化一次,而构造函数体内可以多次赋值。这列我们就要简单介绍一下初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式

class Date
{
public:
	 Date(int year, int month, int day)
     	: _year(year)
     	, _month(month)
    	 , _day(day)
 		{}
private:
	int _year;
	int _month;
	int _day;
};

注意:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)
  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
  2. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

另外,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数还具有类型转换的作用,并且可以通过explicit关键字修饰构造函数,禁止类型转换。

class Date
{
public:

 // 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
 // explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
 explicit Date(int year)
 		  :_year(year)
 		  {}
 /*
 // 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具
有类型转换作用
 // explicit修饰构造函数,禁止类型转换
 explicit Date(int year, int month = 1, int day = 1)
 		  : _year(year)
 		  , _month(month)
 		  , _day(day)
 		  {}
 */
 Date& operator=(const Date& d)
 {
 	if (this != &d)
 	{
 		_year = d._year;
 		_month = d._month;
 		_day = d._day;
 	}
 	return *this;
 }
private:
 	int _year;
 	int _month;
 	int _day;
};
void Test()
{
 	Date d1(2022);
 	// 用一个整形变量给日期类型对象赋值
 	// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
 	d1 = 2023;
 	// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用
}


析构函数

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作(并不是销毁了该对象所占的内存空间)

析构函数的特性:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
  5. 析构函数会默认处理自定义类型。
class Time
{
public:
 ~Time()
{
 	cout << "~Time()" << endl;
}
private:
 	int _hour;
 	int _minute;
 	int _second;
};
class Date
{
private:
 // 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	
 // 自定义类型
 	Time _t;
};
int main()
{
 	Date d;
 	return 0;
}
/*main方法中创建了Date对象d,而d中包含4个成员变量

其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可

而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数

但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象

所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数

目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁

main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数

总而言之:创建哪个类的对象就会调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

(main函数销毁Date类对象时调用Date类的析构函数,而该析构函数内部调用了Time类对象的析构函数)


7.2 拷贝构造函数

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

拷贝构造函数的特性:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用使用传值方式编译器直接报错,
    因为会引发无穷递归调用
  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
    字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
  4. 编译器生成的默认拷贝构造函数通过值拷贝可以直接处理内置类型,但对于自定义类型(该类型对象(如动态数组)在内存中开辟了空间)我们必须要自己显示实现
  5. 拷贝构造函数典型调用场景:
    • 使用已存在对象创建新对象
    • 函数参数类型为类类型对象
    • 函数返回值类型为类类型对象
class Date
{
public:
	//Date(const Date date)错误写法,会引发无穷递归
	Date(const Date& date)//加上const防止传入的数据
	{
		_year = date._year;
		_month = date._month;
		_day = date._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d2);
}
//在传参和返回时,如果能使用引用,尽量使用引用,能提高效率

在这里插入图片描述


7.3 赋值运算符重载

运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号

函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .*  ::sizeof?:.  注意以上5个运算符不能重载。

以 == 为例

#include<iostream>
using namespace std;

class Date
{
private:
	int _year;
	int _month;
	int _day
public:
	Date(int year = 1970, int month = 1, int day = 1)
	{
		_year = year;
        _month = month;
        _day = day;
    }
    //如果运算符重载全局,那么成员变量必须是公有,后面可以通过友元解决
    //这里我们直接重载成成员函数
	//(有多少个操作数,就有多少个参数,顺序由左往右)
	//d1 == d2
	bool operator==(/*const Date* this,*/const Date& d)//传引用效率更高
	{
		//左操作数是this,指向函数调用的对象
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
};
int main()
{
	Date d1;
	Date d2(2023, 2, 6);
	cout << d1 == d2 << endl;
}



赋值运算符重载

  1. 赋值运算符重载格式
  • 参数类型:const ClassName&,传递引用可以提高传参效率
  • 返回值类型:ClassName&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要复合连续赋值的含义
#include<iostream>
using namespace std;

class Date
{
private:
	int _year;
	int _month;
	int _day
public:
	Date(int year = 1970, int month = 1, int day = 1)
	{
		_year = year;
        _month = month;
        _day = day;
    }
    
	//d1 = d2
	Date&(/*const Date* this,*/const Date& d)//传引用效率更高
	{
		if(this != &d)//检测是否给自己赋值
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;//返回一个Date对象,以便连续赋值
	}
};
int main()
{
	Date d1;
	Date d2 = d1;
}
  1. 作为默认成员函数之一,赋值运算符如果不显式实现,编译器会默认生成一个,如果用户在类外也实现了一个赋值运算符重载,那么就会与默认生成的发生冲突,因此赋值运算符重载只能是类的成员函数
  2. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
  3. 与拷贝构造函数相同,如果类中未涉及内存空间管理,可以使用编译器默认生成的,但如果涉及到了内存空间管理(如动态数组的拷贝),用户必须自己实现该类的赋值运算符重载

日期类的实现:C++简易万年历

7.4 取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date
{ 
 public :
 	Date* operator&()
	{
 		return this ;
 	}
	const Date* operator&()const
 	{
 		return this ;
 	}
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容


8. const成员函数

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

在这里插入图片描述

9. static成员

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。

例题:求1+2+3+…+n

//这列我们可以通过实现一个类,并且在类中加入static成员,计算类中创建了多少对象,就能求出答案
class Sum
{
public:
    Sum()
    {
    	//每创建一个对象,就会调用一次构造函数
        _sum+=_i;
        ++_i;
    }
    static int GetSum()
    {
        return _sum;
    }
private:
    static int _i;
    static int _sum;
};

//static成员变量必须在类外初始化
int Sum::_i=1;
int Sum::_sum=0;

class Solution {
public:
    int Sum_Solution(int n) {
        Sum*  ptr=new Sum[n];//动态开辟(也可以创建一个变长数组Sum[n],但有的编译器可能不支持)
        return Sum::GetSum();
    }
};

特性:

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
10. 友元、内部类

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

友元分为:友元函数友元类

友元函数

  前面在实现日期类中的输入输出时我们用到了友元函数,友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
 Date(int year = 1900, int month = 1, int day = 1)
	 : _year(year)
	 , _month(month)
	 , _day(day)
	 {}
private:
	 int _year;
	 int _month;
	 int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
	 out << d._year << "/" << d._month << "/" << d._day;
	 return out; 
}
istream& operator>>(istream& in, Date& d)
{
	 in >> d._year;
	 in >> d._month;
	 in >> d._day;
	 return in;
}
int main()
{
	 Date d;
	 cin >> d;
	 cout << d << endl;
	 return 0;
}

注意:

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

友元类

class Time
{
   friend class Date;   // 声明日期类为时间类的友元类,则在Date类中就直接访问Time类中的私有成员变量
public:
 	Time(int hour = 0, int minute = 0, int second = 0)
		 : _hour(hour)
		 , _minute(minute)
		 , _second(second)
		 {}
   
private:
	   int _hour;
	   int _minute;
	   int _second;
};
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
       : _year(year)
       , _month(month)
       , _day(day)
   	   {}
   
   void SetTimeOfDate(int hour, int minute, int second)
   {
       // 直接访问Time类私有的成员变量
       _t._hour = hour;
       _t._minute = minute;
       _t._second = second;
   }
   
private:
	   int _year;
	   int _month;
	   int _day;
	   Time _t;
};

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

  • 友元关系是单向的,不具有交换性。
  • A是B的友元,A就可以访问B,但B不能访问A
  • 友元关系不能传递
  • 如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  • 友元关系不能继承

内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

#include<iostream>
using namespace std;
class A
{
private:
	int _a;
	static int _k;
	
	class B//B就是A的内部类
	{
	private:
		int _b;
		
	public:
		void Func(const A& a)
		{
			//B天生就是A的友元类,因此可以直接访问
			cout << a._a << endl;
			cout<< k << endl;
		}
	};
	
};
int A::_k = 10;
int main()
{
	A::B b;//创建B的对象
	b.Func(A());//传参时用到了匿名对象
	//匿名对象的特点是不用取名字,但他的生命周期只有它所在的那一行
	return 0;
}

特点:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
11. 拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
class A
{
public:
 	A(int a = 0)
 	 :_a(a)
 	 {
 	 	cout << "A(int a)" << endl;
 	 }
 	A(const A& aa)
 	 :_a(aa._a)
 	 {
	 	cout << "A(const A& aa)" << endl;
	 }

 	A& operator=(const A& aa)
 	 {
 		cout << "A& operator=(const A& aa)" << endl;
 		if (this != &aa)
 		{
	 		_a = aa._a;
 		}
 		return *this;
 	 }
 	~A()
 	 {
 		cout << "~A()" << endl;
 	 }
private:
 	int _a;
};
void f1(A aa)
{}

A f2()
{
 	A aa;
 	return aa;
}
int main()
{
	 // 传值传参
	 A aa1;
	 f1(aa1);
	 cout << endl;
	 // 传值返回
	 f2();
	 cout << endl;
	 // 隐式类型,连续构造+拷贝构造->优化为直接构造
	 f1(1);
	 // 一个表达式中,连续构造+拷贝构造->优化为一个构造
	 f1(A(2));
	 cout << endl;
	 // 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	 A aa2 = f2();
	 cout << endl;
	 // 一个表达式中,连续拷贝构造+赋值重载->无法优化
	 aa1 = f2();
	 cout << endl;
	 return 0;
}



小结:类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。

在这里插入图片描述
感谢阅读!由于作者水平有限,对上述内容只能做简单的介绍,如有问题,请多指教!
Thanks♪(・ω・)ノ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值