C++类和对象

目录

一、面向过程和面向对象初步认识

二、类的引入

三、类的定义

四、类的访问限定符及封装

1 访问限定符

2 封装

五、类的作用域与实例化

六、类对象模型

1 类对象大小的计算

2 结构体内存对齐 

七、this指针 

1 this指针的引入

2 this指针的特性

八、类的6个默认成员函数 

1 构造函数 

1.1 概念

1.2 特性

2 析构函数

2.1 概念

2.2 特性

3 拷贝构造 

3.1 概念

3.2 特性 

4 赋值运算符重载

4.1 运算符重载

4.2 赋值运算符重载

补充概念:const修饰成员函数

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

6 综合训练 

九、再谈构造函数 

1 构造函数体赋值

2 初始化列表

十、explicit关键字

1 自定义类型的隐式类型转换 

2 explicit关键字

十一、static成员

1 概念

2 特性

 十二、C++11的成员初始化新玩法

十三、友元

 1 友元函数

2 友元类 

 十四、内部类

十五、相关练习题


一、面向过程和面向对象初步认识

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

 例如我们手洗衣服时,我们会这样做:

  • C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完 成。

例如我们机洗衣服时,在整个过程中,会存在四个对象:人、衣服、洗衣粉和洗衣机。整个洗衣服的过程是:人将衣服放进洗衣机、倒入洗衣粉,启动洗衣机,洗衣机就会完成洗衣过程并且甩干。整个过程是人、衣服、洗衣粉和洗衣机四个对象之间交互完成的,人不需要关心洗衣机具体是如何洗衣服的,是如何甩干的。 

二、类的引入

 C语言结构体中只能定义变量在C++中,结构体内不仅可以定义变量,也可以定义函数。比如: 我们之前在讲数据结构的时候,用C语言方式实现了一个栈,在这个栈的结构体中只能定义变量;现在以C++方式实现, 会发现struct中也可以定义函数。

C语言:

#include<stdio.h>
#include<stdlib.h>
typedef int StackDataType;
struct Stack
{
	StackDataType* _array;//为了区分普通变量与成员变量,我们可以在成员变量前面加一个_ ,成员变量后面会讲
	int _top;
	int _capacity;
};
void StackInit(struct Stack* pst)
{
	pst->_array = (StackDataType*)malloc(sizeof(StackDataType) * 4);
	pst->_top = 0;
	pst->_capacity = 4;
}
int main()
{
	struct Stack st;
	StackInit(&st);
	return 0;
}

C++:

#include<iostream>
using namespace std;
typedef int StackDataType;
struct Stack
{
	void StackInit()
	{
		_array = (StackDataType*)malloc(sizeof(StackDataType)*4);
		_capacity = 4;
		_top = 0;
	}
	StackDataType* _array;
	int _capacity;
	int _top;
};
int main()
{
	Stack s;
	s.StackInit();
	return 0;
}

我们上面结构体的定义,在C++中更喜欢用class来代替

三、类的定义

class ClassName
{
	//类体
};

我们可以看出,对于在C++中定义一个类,需要包括一个关键字 class 然后后面接上类的名字,然后就是 {} 和 了。在 {} 里面的是类体(也就是类的主体)。类体中内容称为类的成员:类中的变量称为类的属性成员变量; 类中的函数称为类的方法或者成员函数。比如,我们定义一个学生类。

class Student
{
	//方法(成员函数)
	void Print()
	{
		// …
	}
	//属性(成员变量)
	char _name[5];
	char _sex;
	int _age;
	// …
};

类的两种定义方式:

  1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内 联函数处理。上面的学生类就是这一种。
  2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
//Student.h

class Student
{
	//成员方法
	void Print();
	//成员变量
	char _name[5];
	char _sex;
	int _age;
	// …
};
//Student.cpp

#include"Student.h"
//成员方法
void Student::Print()
{
	// …
}

一般情况下,项目中更期望采用第二种方式。我们日常编写代码,有的时候为了方便会采用第一种方式。

四、类的访问限定符及封装

1 访问限定符

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

访问限定符说明: 

  1. public修饰的成员在类外可以直接被访问;
  2. protected和private修饰的成员在类外不能直接被访问(我们这里暂且认为protected和private是类似的,讲继承的时候再讲它们的区别)。在类里面不受访问限制;
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;
  4. 如果后面没有访问限定符,作用域就到 } 即类结束;
  5.  C++中,class和struct都可以用来定义类,class的默认访问权限为privatestruct为public(因为struct要兼容C) ;
  6. 一般情况下,成员变量都是比较隐私的,都会被定义成私有或者保护。 
#include<iostream>
using namespace std;
class Student
{
public:
	void ShowInfo()
	{
		cout << "ShowInfo()" << endl;
	}
private:
	char _name[5];
	char _sex;
	int _age;
};
int main()
{
	Student s;
	s.ShowInfo();
	return 0;
}

当我们定义一个学生类型的变量 s 时,我们会发现我们只能在类外面访问该类里面的公有成员。

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

C++中struct和class的区别:

C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来 定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类 默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后面我们再说。

2 封装

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

我们这里先讲封装,继承和多态后面再讲。

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

也就是说,对于封装,我们是把数据(成员变量)和方法(成员函数)定义在一起的,然后,把想给你看的给你看,不想给你看的封装起来。比如,如果不想把成员变量给其他人看见就定义成private或protected,成员函数想给其他人看见就定义成public。一般情况下,成员变量都是private或protected的,我们如果要改它们的值需要通过类里面的一系列接口(可以理解成为成员函数)来进行修改。

class Stack
{
public:
	void Push();
	void Pop();
	void Empty();
private:
	int* _array;
	int _top;
	int _capacity;
};

 比如,我们这里通过出入栈来控制栈顶的值,而不是直接去改栈顶的值。

封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用 户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日 常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。

对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如 何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计 算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以 及键盘插孔等,让用户可以与计算机进行交互即可

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

五、类的作用域与实例化

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

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

类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;

一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量

对于类的实例化,我们这里先讲一下声明和定义的区别:

简单来说,声明就是一种承诺,承诺要做某事,但是还没有做(还没有开空间)。定义就是把这件事做了(开空间了)。

做个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。

//定义一个Stack类,但是没有为里面的成员变量开空间
class Stack
{
public:
	//声明
	void Push(int x);
	void Pop();
	void Empty();
private:
	//声明
	int* _array;
	int _top;
	int _capacity;
};
//在类外面定义
void Stack::Push(int x)
{
    //…
}
int main()
{
    //类实例化出对象,相当于定义出了类的成员变量,开辟了空间
    Stack s1;
    Stack s2;
    return 0;
}

六、类对象模型

1 类对象大小的计算

测试代码:

class Stack
{
public:
	//声明
	void Push();
	void Pop();
	void Empty();
private:
	//声明
	int* _array;
	int _top;
	int _capacity;
};
int main()
{
	Stack s1;
	cout << sizeof(s1) << endl;
	return 0;
}

测试结果:这里是x64的平台。

我们会发现:一个类的大小,实际就是该类中“成员变量”之和,当然要注意内存对齐

那为什么没有算成员函数的大小呢?

因为一个类实例化出n个对象后,每个对象的成员变量都可以存储不同的值,但是它们调用的函数却都是同一个。如果每个对象里面都放有成员函数,而这些成员函数都是一样的,就会浪费空间。所以,在实例化出的对象里面不存放成员函数,而是把成员函数存放到公共代码段

从上面的汇编代码可以看出,两个实例化出的对象调用的都是同一个成员函数。

注意:空类(没有成员变量)比较特殊,编译器给了空类一个字节来唯一标识这个类的对象(不是为了存储数据,而是用来占位,表示它存在)

2 结构体内存对齐 

这里可以看我发的另一篇文章:

结构体内存对齐

七、this指针 

1 this指针的引入

 我们这里先定义一个日期类Date。

#include<iostream>
using namespace std;
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
    Date d2;
	d1.Init(2024, 7, 14);
    d2.Init(2024, 7, 15);
	d1.Print();
	d2.Print();
	return 0;
}

测试结果:

对于上述类,有这样的一个问题:

Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当 d1 调用 Init 函数时,该函数是如何知道应该设置 d1 对象,而不是设置d2对象呢? 

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

但是,上述改变后的代码在编译器上运行会报错,因为编译器会处理成带this指针的 ≠ 我们自己能够这么写,我们自己写的代码最多只能在成员变量前加 this-> 。同时,编译器在自动加this指针作为函数参数时,都是把它作为第一个参数。我们一般都不加this,反正编译器会自己加。

class Date
{
public:
	void Init(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	void Print()
	{
		cout << this->_year << '-' << this->_month << '-' << this->_day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Init(2024, 7, 14);
	d1.Print();
	return 0;
}

2 this指针的特性

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

八、类的6个默认成员函数 

  • 如果一个类中什么成员都没有,简称为空类。
  • 空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
  • 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

1 构造函数 

1.1 概念

#include<iostream>
using namespace std;
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(2024, 7, 14);
	d1.Print();
	Date d2;
	d2.Init(2024, 7, 15);
	d2.Print();
	return 0;
}

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置 信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

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

1.2 特性

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

其特征如下:

1. 函数名与类名相同。

2. 无返回值类型。

3. 对象实例化时编译器自动调用对应的构造函数。

4. 构造函数可以重载。

#include<iostream>
using namespace std;
class Date
{
public:
    // 1.无参构造函数
    Date()
    {}

    // 2.带参构造函数
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

void TestDate()
{
    Date d1; // 调用无参构造函数
    Date d2(2015, 1, 1); // 调用带参的构造函数

    // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
    // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
    Date d3();
}

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

#include<iostream>
using namespace std;
class Date
{
public:
    /*
    // 如果用户显式定义了构造函数,编译器将不再生成
    Date(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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
    // 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
    // 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
    Date d1;
    d1.Print();
    return 0;
}

关于编译器生成的默认成员函数,很多读者可能会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d1对象调用了编译器生成的默认构造函数,但是d1对象_year/_month/_day,依旧是随机值。也就是说,在这里编译器生成的默认构造函数并没有什么用?

解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。 

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

 我们得出结论:

C++默认生成的无参构造函数,对内置类型的成员变量不会做处理(初始化),对自定义类型的成员变量,会自动去调用它的构造函数初始化。(双标)

注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型),声明时给默认值
	int _year = 2003;
	int _month = 1;
	int _day = 23;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

注意:无参构造函数、编译器默全缺省构造函数、我们没写认生成的构造函数,都可以认为是默认构造函数。

#include<iostream>
using namespace std;
class Date
{
public:
	Date()
	{
		_year = 2003;
		_month = 1;
		_day = 23;
	}
	Date(int year = 2003, int month = 1, int day = 23)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Test()
{
	Date d1;//编译不通过,存在歧义,到底是调用无参的构造函数还是全缺省的构造函数?
}
int main()
{
    Test();
    return 0;
}

2 析构函数

2.1 概念

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

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

2.2 特性

1. 析构函数名是在类名前加上字符 ~。

2. 无参数无返回值类型。  

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

注意:析构函数不能重载。

4. 对象生命周期结束时,C++编译系统自动调用析构函数。

#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
	Stack(int capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
		cout << _capacity << ' ' << _size << endl;
	}
	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	// 其他方法...
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
		cout << "~Stack()" << endl;
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
void TestStack()
{
	Stack s;
	s.Push(1);
	s.Push(2);
}
int main()
{
	TestStack();
	return 0;
}

 

5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。 

#include<iostream>
using namespace std;
typedef int DataType;
class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year =2000;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是
内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数(_t是私有的),实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数 。

6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

3 拷贝构造 

3.1 概念

在现实生活中,我们可能需要创建一个跟已存在对象一摸一样的新对象,拷贝构造就是完成这项任务的。

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

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//Date d2(d1);
	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;
};
int main()
{
	Date d1(2024, 7, 15);
	Date d2(d1);//拷贝构造
	d2.Print();
	return 0;
}


3.2 特性 

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

1. 拷贝构造函数是构造函数的一个重载形式

2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。

传引用可以解决上述问题。

有的小伙伴可能会想:那既然是传引用,大家都用同一个空间,是不是我d2里面成员变量的值发生变化,d1里面的成员变量的值也会发生变化?其实这是不对的,因为d才是d1的引用,不是d2是d1的引用,d1和d2除了里面的成员变量的值是一样的,其它其实没有太大联系。

因为在拷贝构造传参时,不会对形参进行修改,为了防止程序员误操作影响程序结果,我们一般在类类型前面加上const,让形参的值不会改变,自然也就不会影响其它对象。比如:如果我们没有在Date& d 前面加const,如果程序员在里面写了一句“day=_day”,则最后会使d1与d2的成员变量中的_day的值都为随机值。【C++在很多地方都会使用const和引用来减少变量值意外修改问题与减少内存消耗

除此之外,拷贝构造还有另一种表达方式。 

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024, 7, 15);
	//表达方式1
	Date d2(d1);
	//表达方式2
	Date d3 = d1;
	return 0;
}

3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

#include<iostream>
using namespace std;
class Time
{
public:
    Time()
    {
        _hour = 1;
        _minute = 1;
        _second = 1;
    }
    Time(const Time& t)
    {
        _hour = t._hour;
        _minute = t._minute;
        _second = t._second;
        cout << "Time::Time(const Time&)" << endl;
    }
private:
    int _hour;
    int _minute;
    int _second;
};
class Date
{
private:
    // 基本类型(内置类型)
    int _year = 2000;
    int _month = 1;
    int _day = 1;
    // 自定义类型
    Time _t;//自定义类型会调用其拷贝构造函数完成拷贝      
};
int main()
{
    Date d1;
    // 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
    // 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
    Date d2(d1);
    return 0;
}

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 当然像日期类这样的类是没必要的。但是下面这种类就需要自己实现了。

#include<iostream>
using namespace std;
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1(10);
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

 上面的代码会存在浅拷贝问题。当我们创建一个栈类型的对象s1并为其动态开辟10个空间后,我们再将1、2、3、4入栈,此时将s1拷贝构造给新对象s2。由于我们自己没实现拷贝构造,所以编译器会去调用默认的拷贝构造函数,实现浅拷贝。此时,s2的成员变量的值也会和s1的成员变量的值一样。

最后,我们会发现s1的成员变量_array与s2的成员变量_array是一样的,说明它们都指向同一块空间(malloc动态开辟的空间),这里就会存在问题。首先,我们两个对象里的指针变量都指向同一块空间,当s1与s2都执行入栈操作的时候,一个对象会影响另一个对象。(这个不是我们这里的重点,以后再讲)其次,这里引发崩溃的根本原因在于对象最后执行的析构函数。由于先创建的是s1,然后是s2,所以,会先析构s2再析构s1。我们知道,析构的时候会去释放对象开辟的空间(free(_array)),所以,当s2析构后,这块空间就被释放了,但是s1又会去调它自己的析构函数,再来释放这一块空间,这就会出问题,同一块空间不能释放两次。这就需要我们自己实现深拷贝去解决,我们后面会专门讲,这里先不说。

4 赋值运算符重载

4.1 运算符重载

当自定义类型需要判断是否相等(或其它操作)怎么办?我们以前也许会写一个判断是否相等的函数,但是它肯定没有直接“对象1==对象2”好理解,为了解决这个问题,C++引入了运算符重载。

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

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

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

其中,参数列表的个数由运算符的操作数决定,比如++只有一个操作数,参数列表就只有一个参数。

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
#include<iostream>
using namespace std;
// 全局的operator==
//如果成员变量是private的,会报错,因为类外面不能访问private的成员变量
class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//private:
	int _year;
	int _month;
	int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}
void Test()
{
	Date d1(2024, 7, 15);
	Date d2(2018, 7, 16);
    //自定义类型是不能用运算符的,要用就得实现重载函数,自定义类型用的时候等价于调用这个重载函数
	cout << (d1 == d2) << endl;//d1==d2会被编译器转换成operator==(d1,d2),d1==d2相当于一个函数调用
    //d1==d2要加括号,因为<<优先级更高,不加括号,d1会先跟cout重载
	//我们自己写也可以写成:
	cout << operator==(d1, d2) << endl;//但我们一般不这么写,因为这样可读性不好
}
int main()
{
	Test();
	return 0;
}

为了保证类的封装性,必须让成员变量是private的,运算符重载如果要访问这些成员变量,就需要把这个函数定义在类里面,让它变为成员函数。所以,此时该函数就需要少传一个参数,因为编译器会自动传一个this指针。 

#include<iostream>
using namespace std;
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
         _year = year;
         _month = month;
         _day = day;
    }
        // bool operator==(Date* this, const Date& d2)
        // 这里需要注意的是,左操作数是this,指向调用函数的对象
    bool operator==(const Date& d2)
    {
        return _year == d2._year
            && _month == d2._month
            && _day == d2._day;
    }
private:
    int _year;
    int _month;
    int _day;
};
void Test()
{
    Date d1(2024, 7, 15);
    Date d2(2024, 7, 15);
    //两种调用方式
    cout << (d1 == d2) << endl;
    cout << d1.operator==(d2) << endl;
}
int main()
{
	Test();
	return 0;
}

4.2 赋值运算符重载

1. 赋值运算符重载格式:

  • 参数类型:const T&,传递引用可以提高传参效率(T是指类模版,以后会讲);
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值;
  • 返回*this :要符合连续赋值的含义。
#include<iostream>
using namespace std;
class Date
{
public:
    //构造函数
    Date(int year = 2000, 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;
    }
    //赋值运算符重载
    Date& operator=(const Date& d)
    {
        if (this != &d)//防止自己给自己赋值,自己给自己赋值没有意义
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }
    void Print()
    {
        cout << _year << '-' << _month << '-' << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
void Test()
{
    Date d1(2024, 7, 15);
    Date d2;
    Date d3;
    d3 = d2 = d1;//从右往左依次赋值,d1赋值给d2后,会有一个返回值(*this),它就是d2,然后再将其赋值给d3
    d1.Print();
    d2.Print();
    d3.Print();
}
int main()
{
	Test();
	return 0;
}

 赋值运算符重载与拷贝构造的区别:

#include<iostream>
using namespace std;
class Date
{
public:
    //构造函数
    Date(int year = 2000, 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;
    }
    //赋值运算符重载
    Date& operator=(const Date& d)
    {
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }
    void Print()
    {
        cout << _year << '-' << _month << '-' << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
void Test()
{
    Date d1(2024, 7, 15);
    d1.Print();
    Date d2(2023, 7, 17);
    d2.Print();
    //赋值运算符重载
    d2 = d1;//两个对象都已经存在并且都初始化好了,被赋值对象(d2)可能已
           //经执行了一些操作,后来想要将一个对象(d1)赋值(拷贝)给它(d2)
    
    //拷贝构造
    Date d3(d1);//一个对象(d3)还不存在,构造该对象(d3)时用另一个对象(d1)去初始化

    Date d4 = 41;//这里是拷贝构造,不是赋值运算符重载
}
int main()
{
	Test();
	return 0;
}

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

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

// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
	if (&left != &right)
	{
		left._year = right._year;
		left._month = right._month;
		left._day = right._day;
	}
	return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
// E0341“operator=”必须是成员函数

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time& operator=(const Time& t)
	{
		if (this != &t)
		{
			_hour = t._hour;
			_minute = t._minute;
			_second = t._second;
		}
		cout << "Time& operator=(const Time& t)" << endl;
		return *this;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 2000;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	Date d2;
	d1 = d2;
	return 0;
}

输出结果:

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。但是下面这种类就需要自己实现了。

#include<iostream>
using namespace std;
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2;
	s2 = s1;
	return 0;
}

程序崩溃的原因和上面的拷贝构造是一样的,都是因为浅拷贝。

注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

补充概念:const修饰成员函数

我们上面说过:C++在很多地方传参的时候都会使用const。但是存在下面一种情况:

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	/*void Print()//this的类型为Date
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}*/
    void Print()const
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void func(const Date& d)
{
	d.Print();//报错: d 的类型为 const Date
}
int main()
{
	Date d1(2024, 7, 27);
	func(d1);
	return 0;
}

当我们调用 func 函数,对象 d 的类型为 const Date 类型,是只读的。然而,当调用 Print 函数时,Print 函数包含的隐含指针 this 是 Date* 类型的,是可读可写的。这里就存在权限的放大。那么,怎么做呢?

C++于是就在该函数的()后面加上 const ,使 this 为只可读的,这里相当于 const Date* this。

但是,由于这里 const 修饰的是 *this 这个对象(不是指针),那么,这个对象的内容就没法改变了。

这里是对象调用const成员函数,我们再看看成员函数调用const成员函数

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()const
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	} 
	void func1() //void func1(Date* this)
	{
		func2(); //this->f2(this) //权限的缩小,可以
	}
	void func2()const
	{

	}
	void func3() //void func3(Date* this)
	{
		
	}
	void func4()const //void func4(const Date* this)
	{
		func3(); //报错 //this->func3(this) //权限的放大,不可以
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 7, 27);
	return 0;
}

总结:

  • const对象不可以调用非const成员函数;
  • 非const对象可以调用const成员函数吗;
  • const成员函数内不可以调用其它的非const成员函数;
  • 非const成员函数内可以调用其它的const成员函数。

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

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

默认的取地址操作符重载:

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()const
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 7, 27);
	cout << &d1 << endl;//取地址重载
	return 0;
}

如果我们自己实现:

	Date* operator&()
	{
		return this;
	}

但是编译器默认生成的取地址重载已经够用了,不需要自己实现。

默认的const取地址重载:

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()const
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
	Date* operator&()
	{
		cout << "Date* operator&()" << endl;
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 7, 27);
	Date d2;
	const Date d3;

	cout << &d1 << endl; //调用自己实现的取地址重载
	cout << &d2 << endl; //调用自己实现的取地址重载
	cout << &d3 << endl; //调用默认的const取地址重载

	return 0;
}

如果我们自己实现:

	const Date* operator&()const
	{
		return this;
	}

 同样地,这个也不需要自己实现。

但是,如果不想让别人取到对象的地址,我们就需要自己实现了。

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()const
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
	Date* operator&()
	{
		cout << "Date* operator&()" << endl;
		return nullptr;
	}
	const Date* operator&()const
	{
		cout << "const Date* operator&()const" << endl;
		return nullptr;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 7, 27);
	Date d2;
	const Date d3;

	cout << &d1 << endl; 
	cout << &d2 << endl; 
	cout << &d3 << endl; 

	return 0;
}

6 综合训练 

这里以一个Date类为例,实现与该类有关的一系列成员函数,来实现各种操作。

日期类的实现

九、再谈构造函数 

1 构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。(我们上面讲的就是这种)

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 7, 28);//调用构造函数
	return 0;
}

2 初始化列表

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

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 7, 28);
	return 0;
}

既然都有函数体赋值了,那为什么还有初始化列表呢?

因为有几类成员必须在初始化列表初始化。

它们分别是引用成员const成员自定义类型成员(且该类没有默认构造函数时)

class A
{
public:
	A(int a) // 有一个参数,则不生成默认构造函数
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
		:_aobj(a)
		, _ref(ref)
		, _n(10)
	{}
private:
	A _aobj;  // 没有默认构造函数(不用传参就可以调用的构造函数)
	int& _ref;  // 引用
	const int _n; // const 
};

那为什么它们必须在初始化列表初始化呢?

我们以上述代码为例:

class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)//可以理解成初始化列表是 对象的成员变量定义的地方
		:_aobj(a)
		, _ref(ref)
		, _n(10)
	{}
private:
    // 成员变量声明的地方
	A _aobj; 
	int& _ref; 
	const int _n;
};

int main()
{
    B b(1, 2); // 对象定义的地方
    return 0;
}

由于初始化列表是对象的成员变量定义的地方,而这三类成员都必须在定义的时候初始化。(对象定义时,会去调用构造函数初始化,假设没有传参,就会去调用默认的无参构造函数,由于这里没有默认的构造函数,所以就必须传参)

注意:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次);
  2. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化;
  3. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
#include<iostream>
using namespace std;
class A
{
public:
    A(int a)
        :_a1(a)
        , _a2(_a1)
    {}

    void Print() {
        cout << _a1 << " " << _a2 << endl;
    }
private:
    int _a2;
    int _a1;
};
int main() 
{
    A aa(1);
    aa.Print();
}

 输出结果:

所以,我们在实际中,最好让声明次序与初始化列表的次序一致。 

十、explicit关键字

1 自定义类型的隐式类型转换 

我们前面已经学了构造函数与拷贝构造函数,其实还存在一种隐式类型转换。

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0)
		:_year(year)
	{}
private:
	int _year;
};
int main()
{
	Date d1(1);//构造
	Date d2 = 2;//隐式类型转换 构造出临时的tmp(2),再用tmp(2)拷贝构造d2(tmp),最后优化成直接构造(不一定发生)
	Date d3 = d1;//拷贝构造
	return 0;
}

同样地,这里的临时对象也具有常性。 

include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0)
		:_year(year)
	{}
	Date(const Date& d)
	{
		_year = d._year;
	}
private:
	int _year;
};
int main()
{
	Date d1(1);//构造
	const Date& d2 = 2;//优化不会产生,因为引用的是中间产生的临时对象
	Date d3 = d1;//拷贝构造
	return 0;
}

但是,如果我们不想上面的隐式类型转换发生怎么办?

这里就需要在构造函数前面加一个 explicit 关键字。

2 explicit关键字

上面是C++98当中的,它只针对单个参数的情况,C++11还有一种针对多参数的情况。

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, 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;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024, 8, 1);//构造
	//C++11才支持
	const Date& d2 = {2024, 8, 2};//隐式类型转换,要用{}

	Date d3 = d1;//拷贝构造
	return 0;
}

同样地,如果不想隐式类型转换,也可以在构造函数前加 explicit 关键字。

十一、static成员

1 概念

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

这里有一个面试题:

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

#include<iostream>
using namespace std;
int n = 0;
class A
{
public:
	A()//构造
	{
		++n;
	}
	A(const A& a)//拷贝构造
	{
		++n;
	}
};
//A& f1(A& a)    //如果这样,最终n = 2
A f1(A a)
{
	return a;
}
int main()
{
	A a1;//一个
	A a2;//一个
	f1(a1);//两个,传参一个,返回一个
	f1(a2);//两个
	cout << n << endl;//总共六个
    return 0;
}

重点:对象一定是由我们的构造或拷贝构造产生的。

但是上面的代码会存在问题:

int n = 0;
class A
{
public:
	A()//构造
	{
		++n;
	}
	A(const A& a)//拷贝构造
	{
		++n;
	}
};
A f1(A a)
{
	return a;
}
int main()
{
	A a1;
	A a2;
	f1(a1);
	n = 1;
	f1(a2);
	cout << n << endl;//n = 3
    return 0;
}

我们会发现谁都可以对 n 进行修改,所以就没有封装性。也就是说,上面的 n 跳出了类的管理。要解决这样的问题,首先要让 n 变成私有的成员变量,然后,由于 n 成为了成员变量,所以,每个对象里面都有一个 n ,当让 n 自加时,是加的自己这个对象里面的 n ,而我们要做的是加到同一个 n 上,所以,要将这个 n 定义成 static 的,这样,全局就只有一个 n 了。但是,在类里面的 n 还只是声明,定义还是需要放在类外面。

#include<iostream>
using namespace std;
class A
{
public:
	A()//构造
	{
		++n;
	}
	A(const A& a)//拷贝构造
	{
		++n;
	}
	static int GetN()//static修饰成员函数,没有this指针,函数中不能访问非静态的成员
	{
		/*_a = 10;*///报错
		return n;
	}
private:
	static int n;//声明  n不是属于某个对象,是属于类的所有所有对象,是属于这个类
	//n不在对象中,n在静态区
	int _a;
};
int A::n = 0;//定义
A f1(A a)
{
	return a;
}
int main()
{
	A a1;
	A a2;
	f1(a1);
	f1(a2);
	cout << a1.GetN() << endl;
	cout << A::GetN() << endl;//当成员函数为static时,可以这样调用
	return 0;
}

2 特性

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

 十二、C++11的成员初始化新玩法

我们知道,对于成员变量的定义,如果是非静态的就在构造函数处定义,对于静态的就在类外面定义。但是C++11支持在声明时给缺省值。

对于声明时给参数值,如果我们在构造函数处给变量赋值了,就不使用原来的缺省值。

但是,对于静态的成员变量,不能在声明时给缺省值,必须在类外面定义。 

十三、友元

友元分为:友元函数友元类
友元提供了一种突破封装的方式(即在类外面可以访问 private 与 protected 的成员),有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

#include<iostream>
using namespace std;
class Date
{
public:
	friend void func(Date& d);//友元函数  声明
private:
	int _year = 0;
	int _month = 1;
	int _day = 1;
};
void func(Date& d)//定义
{
	d._year = 10;
	cout << d._year << endl;
}
int main()
{
	Date d1;
	func(d1);
	return 0;
}

 1 友元函数

对于友元,我们可能会想:既然要在类外面访问 private 和 protected 的成员,那为什么不将这个函数直接写在类里面,而偏偏要写成友元呢?

因为在某些特定场合下,必须用友元。

就比如,我们想用 cout 直接输出自定义类型的值,以往我们都是写一个 Print 函数来实现,但是,现在我们想像内置类型一样直接使用 cout 来输出,这就需要使用运算符重载与友元函数。

假设我们没有使用友元,我们的代码应为:

#include<iostream>
using namespace std;
class Date
{
public:
    //cout是标准库命名里面的一个ostream类型的全局对象
	void operator<<(ostream& out)
	{
		out << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year = 0;
	int _month = 1;
	int _day = 1;
};

int main()
{
	Date d1;
	cout << d1;//报错  E0349  没有与这些操作数匹配的“<<”运算符
	return 0;
}

这里使用 cout 输出会报错。

#include<iostream>
using namespace std;
class Date
{
public:
	void operator<<(ostream& out)
	{
		out << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year = 0;
	int _month = 1;
	int _day = 1;
};

int main()
{
	Date d1;
	d1.operator<<(cout);//0-1-1
    d1 << cout;//0-1-1
	return 0;
}

可是,当我们使用对象来显示调用时,却没有报错;并且,如果我们将 d1 与 cout 的位置换一下却也可以输出了,这是为什么呢?

因为运算符重载已经规定了,第一个参数必须是隐含的 this 指针。

那么,如果是 d1<< cout,d1 就会是 *this ,cout 就会是 out。这也就没问题了。

虽然可以按照上面这么写,但是,为了提升程序的可读性,我们还是要使用 cout << d1 。那么,这个运算符重载就不能写成成员函数,因为第一个参数已经被 this 占有了。这就需要将该函数写在类外面,并使用友元了。

#include<iostream>
using namespace std;
class Date
{
public:
	friend void operator<<(ostream& out, Date& d);
private:
	int _year = 0;
	int _month = 1;
	int _day = 1;
};
void operator<<(ostream& out, Date& d)
{
	out << d._year << '-' << d._month << '-' << d._day << endl;
}
int main()
{
	Date d1;
	cout << d1;//0-1-1
	return 0;
}

实现连续输入输出:

#include<iostream>
using namespace std;
class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 2000, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << '-' << d._month << '-' << d._day;
	return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}
int main()
{
	Date d1;
	Date d2;
	cin >> d1 >> d2;
	cout << d1 << '/' << d2 << endl;
    return 0;
}

总结: 

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

2 友元类 

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

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

比如下面的Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

  • 友元关系不能传递

如果C是B的友元, B是A的友元,则不能说明C时A的友元。

  • 友元关系不能继承,在学习继承时我们再具体讲解。
class Time
{
    friend class 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)
    {
        // 直接访问时间类私有的成员变量
        _t._hour = hour;
        _t._minute = minute;
        _t._second = second;
    }

private:
    int _year;
    int _month;
    int _day;
    Time _t;
};

 十四、内部类

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

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

特性:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
 static int k;
 int h;
public:
 class B // B天生就是A的友元
 {
 public:
 void func(const A& a)
 {
 cout << k << endl;//OK
 cout << a.h << endl;//OK
 }
 };
};
int A::k = 1;
int main()
{
    A::B b;
    b.func(A());
    
    return 0;
}

 内部类在C++很少使用,了解一下即可。

十五、匿名对象

我们想要调用类里面的公有成员都是需要先创建一个对象,然后再用该对象去调用对应的类里面的公有成员。

class Date
{
public:
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	d.Print();
	return 0;
}

这里我们可以用另一种方式创建对象,此时,创建出来的对象被叫做匿名对象,该对象没有名字。

class Date
{
public:
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	d.Print();
	//Date();//匿名对象
    Date().Print();//可适用于:只有这一行会使用这个创建的对象,其它地方不需要使用
	return 0;
}

两种方式创建出来的对象有一个区别:

前一种对象的生命周期在main函数中,后一种对象的生命周期只在对应的那一行。

十六、相关练习题

JZ64 求1+2+3+...+n

HJ73 计算日期到天数转换

KY111 日期差值

KY222 打印日期

KY258 日期累加

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值