C++ 类和对象

对面向对象(OOP)的初步认识

  • C语言是面向过程 的,关注是处理数据的过程,分析出求解问题的步骤,通过函数调用逐步解决问题。数据 和 处理数据的方法是分离的。
  • C++是 基于面向对象 的,关注的是 对象 ,将一件事情拆分成不同的对象,靠对象之间交互完成。而C++ 将数据 和 处理数据的方法封装在一起,包含数据完整的生命周期。 但是C++也不是纯面向对象的语言,C++由于向下兼容C语言使得其也有面向对象的特性。

类的引入

C语言中我们学过 一种自定义类型——结构体 (可以参考博文:自定义类型详解
C语言中结构体是一种数据类型,可以表示不同数据类型的一种集合,在C++中对struct的作用进行了延申,struct 里面不仅可以定义 数据 还可以定义函数!


struct Student
{
	void SetStudentInfo(const char* s, int a)
	{
		strcpy(name, s);
		age = a;
	}
	void print()
	{
		cout << name << "    " << age << endl;
	}
	char name[20];
	int age;
};
int main()
{
	Student s;
	s.SetStudentInfo("Peter", 20);
	s.print();
	return 0;
}

这里就要引出我们在C++中更喜欢用class来代替struct来表示这种新的数据类——

类的定义

由上面的引入 得出了 组成类的成员

  • 类中的数据——成员变量
  • 类中的函数——成员函数

下面是类的定义方式:


class classname
{

	类的体:由成员函数 和 成员变量 组成

};   注意这里的分号

class ——定义类的关键字 ,classnaeme——类的名字,{}里包含的是类的主体。

类的定义方式:

  • 1.(函数)声明和定义放在一起(这里的定义指的是函数,成员变量在类中只是声明!)
class Student
{
	void SetStudentInfo(const char* s, int a)
	{
		strcpy(name, s);
		age = a;

	}
	void print()
	{
		cout << name << "    " << age << endl;
	}
	char name[20];
	int age;
};

注意: 这里在类里面定义函数 编译器一般会把其当成内联函数处理,所以小一点的函数可以在内里面定义,但是大一点的函数定义和声明还是分离比较好

  • 2.(函数)声明和定义分开

可以声明在头文件 Student.h中


class Student
{
public:
	void SetStudentInfo(const char* s, int a);
	void print();
	char name[20];
	int age;
};

定义放在 Student.cpp中,使用:: 运算符

#include<Student.h>
void Student::SetStudentInfo(const char* s, int a)
{
	strcpy(name, s);
	age = a;

}
void Student::print()
{
	cout << name << "    " << age << endl;
}

类的访问限定符及封装

访问限定符

kk
说明

  1. public修饰的成员可以被类外直接访问
  2. proteced和private修饰的成员在在类外不可以直接被访问
  3. 访问权限的作用域 是从该访问限定符出现的位置到下一个访问出现的位置之前
  4. class的默认访问权限 为private,struct为public(因为要兼容C,这也是class和struct在表示类时的唯一区别)

问题:class和struct 有什么区别?
在C语言中struct可以表示成结构体去使用。C++由于兼容了C语言的特性,所以struct既能表示结构体,又能表示类并且和class作用一样,唯一不同的是:class的默认访问权限 为private,struct为public

封装

首先我们要了解一下面向对象的三大特性:封装、继承、多态
什么是封装呢?
封装是将数据和操作数据的方法进行有机的结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
说大白话:就是想让你访问的就是公有,不想让你访问的就设成私有,你必须通过成员函数才能与数据交互

总结
封装实际上是一种对数据的管理,防止乱访问数据造成的修改

类的作用域

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

class Student
{
public:
	void SetStudentInfo(const char* s, int a);
	void print();
	char name[20];
	int age;
};
//这里要指明setStudentinfo是来自Student这个类域
void Student::SetStudentInfo(const char* s, int a)
{
	strcpy(name, s);
	age = a;
}

void Student::print()
{
	cout << name << "    " << age << endl;
}

类的实例化

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

  1. 类只是一个模型,和struct表示的结构体一样是一个类型集合,定义一个类并没有实际给其分配内存空间来储存
  2. 一个类可以实例化多个对象,对象是类似于定义的变量,占有内存

打个比方:一个类定义出来就类似于一个图纸,类实例化出的对象就类似于按照图纸造出的房子,有了图纸你就可以造出房子,但图纸并没有实际存在的房子
在这里插入图片描述


class Student
{
public:
	void SetStudentInfo(const char* s, int a);
	void print();


	char name[20];   //变量声明 不开辟空间
	int age;
};

void Student::SetStudentInfo(const char* s, int a)
{
	strcpy(name, s);
	age = a;

}
void Student::print()
{
	cout << name << "    " << age << endl;
}


int main()
{
	Student s;   // 实例化的对象
	s.SetStudentInfo("Peter", 20);
	s.print();
	return 0;

}

类的大小的计算

如何计算一个类的大小呢?

  1. 解决这个问题首先要知道如何处理类中成员函数所占的空间
    这里成员函数其实是不存储在类里的,而是存储在内存分区中的 代码区。代码区存的都是在编译后 代码转换成的指令。而类的实例化是在堆栈上开辟的空间,所以在计算内存中无需考虑成员函数的大小,只 需要考虑成员变量。
  2. 然后类的大小分配原则和结构体的一模一样——都是按照内存对齐
    具体 可以参考博客:结构体的内存对齐规则

class Student
{
public:
	void SetStudentInfo(const char* s, int a);
	void print();
	char name[20];
	int age;
};

void Student::SetStudentInfo(const char* s, int a)
{
	strcpy(name, s);
	age = a;

}
void Student::print()
{
	cout << name << "    " << age << endl;
}


int main()
{
	cout << sizeof(Student) << endl;
}

由内存对齐规则可以知道结果是24

特殊的类的大小

class A
{
public:
	void f2();
};


class B
{};

int main()
{

	cout << sizeof(Student) << endl;
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
}

在这里插入图片描述

这里两个类 A 和 B并没有成员变量,按照上面的计算 内存应该为0,但是这里 人为的规定其大小为1 ,这里给一个字节是为了占位,表示对象存在,但是不存储任何有效数据!

类成员的this指针

我们先定义一个Data类:


class Data
{
public:
	void print()
	{
		cout << "year: " << _year << " month: " << _month << " day: " << _day << endl;
	}


	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Data d1;
	Data d2;
	d1.SetDate(2021, 10, 12);
	d2.SetDate(2020, 10, 12);
}

对于上述类,有一个问题:
Data类中由SetData与Display两个成员函数,函数体中没有关于不同对象的区分,那当d1调用SetData函数时,该函数是如何区分应该设置d1对象还是设置d2对象呢?

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

this指针的特性

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

所以这里d1.SetDate(2021, 10, 12); 这句函数调用也就相当于 SetDate(&d1,2021,10,12);

在这里插入图片描述

注意:

  1. this指针存储在哪里?
    this指针不是存储在对象里面!!this指针是形参,形参和函数中的局部变量都是存储在函数栈帧里面的,实际上是由ecx寄存器传入
  2. this指针不能为空

下面这段代码能让我们更深刻的了解 成员函数 和 this指针


class A
{
public:
	void printA()
	{
		cout << _a << endl;
	}

	void show()
	{
		cout << "show()" << endl;
	}
private:
	int _a;
};


int main()
{
	A* p = nullptr;
	p->printA();  //语句1
	p->show();    //语句2
}

问题:

  1. 这段代码能通过编译吗?能正常运行吗?
  2. 单独运行语句2 能正常运行吗?

首先这段代码是可以通过编译的,但是会在运行阶段挂掉,且中断在printA函数的_a调用上。

一部分同学会认为编译无法通过的原因是 :p是一个空指针,对空指针解引用调用函数不是瞎搞吗?所以这里无法通过编译。这是对成员函数的存储位置还不是很了解,前面讲过成员函数是不储存在对象里面的,而是储存在内存上的代码区上,这里调用成员函数不回去访问p指向的空间,也就不存在对空指针解引用。
实际上:这里进行的操作是把 p(NULL)的值传给this指针!!

后面的现象也就很好解释了,传给this指针 show函数并没有调用类里面的变量,而printA函数调用了变量_a,而我们知道实际上这里调用的是this->_a,所以这里才出现了对空指针的解引用。

类的6个默认成员函数

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

在这里插入图片描述
注意:

  1. 默认成员函数在类定义之时,就会生成,但是如果自己定义了就不会再生成
  2. 默认成员函数也不是必须自己写,当默认成员函数能完成功能就不用自己写,如果不能完成功能 例如下面会讲到的 stack类,构造、析构、拷贝构造、赋值重载都要自己写!

构造函数

在这里插入图片描述

class Data
{
public:
	void print()
	{
		cout << "year: " << _year << " month: " << _month << " day: " << _day << endl;
	}


	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Data d1;
	Data d2;
	d1.SetDate(2021, 10, 12);
	d2.SetDate(2020, 10, 12);
}

前面在实现日期类的时候,写过一个函数是void SetDate(int year, int month, int day) 这个函数的主要目的是对成员变量进行初始化,这里其实的功能和构造函数一模一样,都是对类成员变量进行初始化,但是缺点也是很明显定义与初始化是分离的,每次定义完要调用函数初始化。构造函数直接在定义的时候就可以初始化了。

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

  • 默认构造函数——不用传参就可以调用的构造函数

特性

注意构造函数是初始化对象,而不是定义构造函数(开辟空间)
其特征如下:

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载——提供多种初始化对象的方式。

class Data
{
public:
	Data()  //无参的构造函数
	{
		;
	}

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

int main()
{
	Data d1;       
	Data d2(2021,7,26);
	
	Data d3(); //注意无参构造函数初始化对象时,对象后面不用跟括号,否则就成函数声明了
	//这里就是一个无参返回值是类Data的函数名为d3的函数
}


  1. 如果没有显示的定义构造函数,则C++编译器会自动生成一个无参默认构造函数,一旦用户显示定义编译器就不会再生成
class Data  //这里没有定义构造函数,编译器就会自己生成一个
{
private:
	int _year;
	int _month;
	int _day;
};

那么编译器生成的构造函数能完成什么功能呢?

  • 对待内置类型例如:int ,double ,指针,long…默认构造函数是不会初始化的
  • 对待自定义类型(class、struct)会调用它的 默认 构造函数(不用传参数的构造函数)

class A
{
public:
	A(int a = 100)
	{
		_a = a;
	}
	int _a;
};


class Data
{
	void print()
	{
		cout << ps._a <<endl<<_year<<endl<<_month<<endl<<_day<< endl;
	}
private:
	int _year;
	int _month;
	int _day;
	A ps;
};

int main()
{
	Data d1;
	d1.print();
}

在这里插入图片描述

这里在类Data成员变量中定义了 三个int类型和一个类A类型的变量,最后只有自定义类型的变量被初始化了


  1. 无参构造函数和全缺省构造函数 都称为默认构造函数,并且默认构造函数只能有一个! 注意:无参构造函数全缺省构造函数编译器自己生成的构造函数 统称为 默认构造函数

误区: 只有编译器自己生成的构造函数才是默认构造函数


class Data
{
public:
	Data()
	{
		_year = 0;
		_month = 1;
		_day = 1;
	}

	Data(int year=0 , int month=1 , int day=1 ) //两个默认构造函数不能同时存在
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

不能存在两个默认构造函数,如果在类的实例化时不传参,编译器不知道调用哪一个构造函数。
注意
这样写构造函数是完全错误的,这不是默认构造函数(然而我一直是这么认为的),这是对构造函数的声明没有定义。

class A
{
public:
    A();
};

正确的写法是如下,或者干脆不写,上面的写法会报链接错误

class A
{
public:
    A()
    {
    }
};

总结:
大多数情况下构造函数都要自己去写,因为初始化出来的变量才会符合要求。一般情况下建议写一个全缺省的构造函数,这样可以应对各种场景。

参数列表

上面我们介绍了构造函数,其中有一种特殊的构造函数:默认构造函数。默认构造函数对待内置类型是不处理的,对待自定义类型是调用自定义类型的 默认构造函数 (注意是 默认构造函!!!!)
但是这里遗留了一个问题,如何对自定义类型里面的变量赋值?

class B
{
public:
	B(int x=1,int y=2)
	{
		_x = x;
		_y = y;

	}

	const B& operator=(const B& d1)
	{
		_x = d1._x;
		_y = d1._y;

		return *this;
	}
private:
	int _x;
	int _y;

};

class A
{
public:
	A(int a,int b,int c)
	{
		B b2(b,c);
		b1 = b2;
		// b1= B(b,c)  使用匿名对象:生命周期只有这一行!
		_a = a;
	}
	
private:
	int _a;
	B b1;
};

int main()
{
	A a1(100,100,100);
}

如果想要把值赋给类A中的类B成员变量,只能先创建一个临时变量赋值,然后再用赋值运算符重载拷贝给成员变量。这样定义类中的 类对象会十分麻烦。

这里我们就要介绍一下初始化列表:
初始化列表: 以一个冒号开始,接着是以一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式

	A(int a,int b,int c)         //未显示定义参数列表,但是参数列表依然存在                            
	{
		B b2(b,c);
		b1 = b2;
		_a = a;
	}
	
    A(int a,int b,int c)//使用初始化列表
		:_a(a)
		,b1(b,c)
	{}

之所以参数列表能解决上面的问题,实际上是自定义类型会在参数列表处初始化(而自定义的 初始化 和 变量赋值是绑定在一起的),如果我们能在初始化的时候赋值就可以调用非默认构造函数了,而不是通过创建临时变量赋值重载。

参数列表不管你是否显示的写出来一直是存在的,而且一切变量都会在参数列表处初始化(如果我们对未显示定义参数列表的构造函数按f11一步一步的调试,会发现实际上在进入构造函数之前会跳到类B的默认构造函数)

注意:

  • 每个成员只能在初始化列表上出现一次

  • 类中包含以下成员必须在初始化列表处初始化:

    1. 引用成员变量
    2. const成员变量
    3. 自定义类型成员变量(该类没有默认构造函数)

可以发现必须在初始化列表初始化的成员变量有一个共性:定义的时候就必须赋初值,赋初值是和定义是绑定在一起的。
在这里插入图片描述

易错点



class A
{
public:

	A(int x)
		:_a1(x)
		, _a2(_a1)
	{}
	
	void print()
	{
		cout << _a1 << "    " << _a2 << endl;
	}

private:
	int _a2;
	int _a1;
};

int main()
{
	A a(1);
	a.print();
	return 0;
}

结果:很多人会认为结果是 1 1,但实际上是:
在这里插入图片描述
成员变量在类中声明次序就是其在初始化列表中初始化顺序,与其在初始化列表中的先后次序无关。

析构函数

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

特性

  1. 析构函数名是在类名前加上字符~
  2. 无参数无返回值
  3. 一个类有且只有一个析构函数。若未显示定义,系统会自动生成默认的析构函数
  4. 对象生命周期结束时,C++编译器会自动调用析构函数
class stack
{
public:
	stack(int capacity=4)
	{
		a = (int*)malloc(capacity * sizeof(int));
		assert(a);
		_capacity = capacity;
		_sz = 0;
	}
	~stack()          //析构函数
	{
		if (a)
		{
			free(a);        //在堆上释放空间
			a = NULL;
			_capacity = 0;
			_sz = 0;
		}
	}

private:
	int* a;
	int _capacity;
	int _sz;
};
  1. 关于编译器自动生成的默认析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,会调用自定义类型的析构函数

class String
{
public:
	String(const char *str="jack ma")
	{
		p = (char*)malloc((strlen(str)+1) * sizeof(char));
		strcpy(p, str);
	}

	~String()
	{
		if (p)
		{
			free(p);
			p = NULL;
		}

	}
private:
	char* p;
};

class Person
{
public:
	Person(int age = 20)
	{
		_age = age;
	}
private:
	int _age;
	String name;   //string类是一个自定义类型

};

int main()
{
	Person a1;
	return 0;
}

我们按下f10打开调试界面
在这里插入图片描述

拷贝构造函数

概念:
拷贝构造函数:在对象创建的时候,将另一个对象的全部内容复制给该对象

特征:

  1. 拷贝构造函数是构造函数的一种重载
  2. 拷贝构造函数的参数只有一个且必须引用传参,使用传值的方式会引发无穷递归
class Data
{
public:
	Data(int year = 0, int month = 1, int day = 1) 
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Data(const Data& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

为什么不能使用传值传参?
在这里插入图片描述
传值在传参的过程中要拷贝给临时变量,这个过程又要调拷贝构造,而拷贝构造又是传值传参…

  1. 若未显示定义,系统生成默认的拷贝构造函数。默认拷贝构造函数按对象字节序拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
    这种拷贝和memcpy函数的拷贝实现是相同的

class Data
{
public:

	Data(int year = 0, int month = 1, int day = 1) 
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout  << endl << _year << endl << _month << endl << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Data d1(2021, 1, 1);
	Data d2(d1);
	d2.print();
	return 0;
}

在这里插入图片描述

  1. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的拷贝,我们还需要自己实现拷贝构造函数吗?上面的Data日期类似乎并不需要,但是有些类就需要了
class String
{
public:
	String(const char *str="jack ma")
	{
		p = (char*)malloc((strlen(str)+1) * sizeof(char));
		strcpy(p, str);
	}

	~String()
	{
		if (p)
		{
			free(p);
			p = NULL;
		}
	}
private:
	char* p;
};


int main()
{
	String s1("love");
	String s2(s1);
	return 0;
}

这个程序如果运行就会崩溃原因如下:
在这里插入图片描述
由于浅拷贝使得s1.p和s2.p指向堆上同一块空间,在作用域结束时候调用析构函数时,由于s2后创建先析构,s2.p所指向的空间还给了操作系统,但是s1析构的时候,s1.p就会重复释放已经被释放的空间,所以会报错!
这里就体现了浅拷贝的坏处了,浅拷贝并没有拷贝实际的内存,而是拷贝了指向那块内存的地址!

赋值操作函数

运算符重载

内置类型是支持运算符的,但是自定义类型是不支持的,运算符重载使得自定义类型也能使用操作符

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

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

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

注意

  • 不能通过连接其他符号来创建新的操作符 例如operator @
  • 重载操作符必须有一个类类型或者枚举类型的操作数(内置类型不能重载,参数类型不能都是内置类型!)

当运算符有两个操作数时,第一个参数是左操作数,第二个参数是右操作数

  • 用于内置类型的操作符,其含义不能改变,例如:内置的整形+,不能改变其含义
  • 作为类成员的重载函数,其参数看起来比操作数数目少1,成员函数的操作符有一个默认的形参this,限定为第一个形参
  • 运算符重载函数一般作为成员函数放在类里面,如果在类外定义,就无法访问类里面的private成员
  • .*.::?:sizeof注意以上五个运算符不能重载

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

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

	bool operator==(const Data d)
	{
		if (_year == d._year && _month == d._month && _day == d._day)
			return true;
		else
			return false;
	}

	void print()
	{
		cout  << endl << _year << endl << _month << endl << _day << endl;
	}
	
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Data d1(2021, 10, 12);
	Data d2;
	Data d3(d1);

	cout << (d1 == d2) << endl << (d1 == d3) << endl;  //注意这里(一定要加括号)运算符的优先级问题!!!!!!
	return 0;
}

在这里插入图片描述

赋值运算符重载

我们通过上面的学习可以很简单的写出赋值运算符重载函数:


class Data
{
public:
	Data(int year = 0, int month = 1, int day = 1)  //构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Data(const Data& d)  //拷贝构造函数
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void operator=(const Data& d)     //赋值运算符重载函数
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void print()
	{
		cout  << endl << _year << endl << _month << endl << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

但是这里的赋值运算符是不完全正确的,在一些情况下例如:连续赋值的时候就无法使用
赋值运算符主要有四点:

  1. 参数类型,参数个数只有一个一般为const类
  2. 返回值,返回值一般返回引用,减少返回时的拷贝
  3. 检测是否给自己赋值
  4. 返回*this
  5. 一个类如果没有 显示的定义赋值重载运算符,编译器会生成一个按字节序拷贝的赋值重载

上面编写的赋值重载每有考虑到 例如d1=d2=d3; 这种连续赋值的情况,下面做出改进:

class Data
{
public:
	Data(int year = 0, int month = 1, int day = 1)  //构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Data(const Data& d)  //拷贝构造函数
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	bool operator==(const Data& d)
	{
		if (_year == d._year && _month == d._month && _day == d._day)
			return true;
		else
			return false;
	}

	Data& operator=(const Data& d)     //赋值运算符重载函数
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		
		return *this;
	}

	void print()
	{
		cout  << endl << _year << endl << _month << endl << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Data d1(2021, 10, 12);
	Data d2;
	Data d4;
	
	d2 =d4= d1;
	d4.print();
	d2.print();
	return 0;
}

使用引用返回的原因:首先是这里this指针传进来的是类的地址,所以*this指向的就是主函数里面定义的类的空间,而我们知道在传值返回的时候,是先将返回值传给一个中间变量(调用拷贝构造函数),再将中间变量传回主函数(赋值重载)。但是这里用传引用就不用拷贝,而且引用的变量由于是全局变量不会出函数就会被销毁。

默认拷贝构造与赋值运算符重载的问题

很多人学到这有一个问题:赋值运算符重载和 拷贝构造函数实现的内容都差不多,为什么还要学习赋值重载运算符?
搞清楚两个不同点:

  1. 重载的对象不同:
    - 拷贝构造函数的重载指的是函数的重载,对构造函数的重载
    - 赋值运算符的重载指的是运算符的重载,对运算符的重载

  2. 实现的对象不同:
    拷贝构造函数 是在类变量初始化的时候调用的,而拷贝运算符是对两个已经初始化过的类变量使用的,且注意:Date d1=d2; 这种情况不是 赋值 而是 拷贝构造!

实现一个日期类

下面运用上面的知识实现一个日期类:
Date.h文件

#include<iostream>
#include<assert.h>

using namespace std;

class Date

{

public:

	// 获取某年某月的天数

	int GetMonthDay(int year, int month);

	// 全缺省的构造函数

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



	// 拷贝构造函数

  // d2(d1)

	Date(const Date& d);



	// 赋值运算符重载

  // d2 = d3 -> d2.operator=(&d2, d3)

	Date& operator=(const Date& d);



	// 析构函数

	//~Date();



	// 日期+=天数

	Date& operator+=(int day);



	// 日期+天数

	Date operator+(int day);



	// 日期-天数

	Date operator-(int day);



	// 日期-=天数

	Date& operator-=(int day);



	// 前置++

	Date& operator++();



	// 后置++

	Date operator++(int);



	// 后置--

	Date operator--(int);



	// 前置--

	Date& operator--();

	// >运算符重载

	bool operator>(const Date& d)const;



	// ==运算符重载

	bool operator==(const Date& d)const;



	// >=运算符重载

	inline bool operator >= (const Date& d)
	{
		return *this - d >= 0;
	}



	// <运算符重载

	bool operator < (const Date& d)const;



	// <=运算符重载

	bool operator <= (const Date& d)const;



	// !=运算符重载

	bool operator != (const Date& d)const;



	// 日期-日期 返回天数

	int operator-(const Date& d);




	void print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:

	int _year;

	int _month;

	int _day;

};

Date.cpp文件


#include"Date.h"

int Date::GetMonthDay(int year, int month)
{
	static int a[12] = { 31,0,31,30,31,30,31,31,30,31,30,31 };
	if (month == 2)
	{
		if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
			return 29;
		else
			return 28;
	}
	else
		return a[month-1];
}


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

Date& Date::operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}


Date& Date::operator+=(int day)
{
	_day += day;

	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;

		if (_month > 12)
		{
			_year++;
			_month = 1;
		}

	}

	return *this;
}



Date Date::operator+(int day)
{
	Date d1(*this);

	d1 += day;
	return d1;
}

// 日期-天数

Date Date::operator-(int day)
{
	Date d1(*this);
	d1 -= day;
	return d1;
}



// 日期-=天数

Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day < 0)
	{
		_day += GetMonthDay(_year, _month);
		_month--;

		if (_month < 0)
		{
			_year--;
			_month = 12;
		}

	}
	return *this;
}



// 前置++

Date& Date::operator++()
{
	_day += 1;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;

		if (_month > 12)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}



// 后置++

Date Date::operator++(int)
{
	Date d1(*this);
	++(*this);
	return d1;
}



// 后置--

Date Date::operator--(int)
{
	Date d1(*this);
	--(*this);
	return d1;
}



// 前置--

Date& Date::operator--()
{
	_day -= 1;
	while (_day < 0)
	{
		_day += GetMonthDay(_year, _month);
		_month--;

		if (_month < 0)
		{
			_year--;
			_month = 12;
		}

	}
	return *this;
}



bool Date::operator>(const Date& d)const
{
	if (_year > d._year)
		return true;
	else if (_year == d._year && _month > d._month)
		return true;
	else if (_year == d._year && _month == d._month && _day > d._day)
		return true;
	else
		return false;
}



// ==运算符重载

bool Date::operator==(const Date& d)const
{
	return _year==d._year&&_month==d._month&&_day==d._day;

}

// <运算符重载

bool Date::operator < (const Date& d)const
{
	if (*this <= d && *this != d)
		return true;
	else
		return false;

}

// <=运算符重载

bool Date::operator <= (const Date& d)const
{
	return !(*this>d);

}

// !=运算符重载

bool Date::operator != (const Date& d)const
{
	return !(*this==d);
}

int Date::operator-(const Date& d)
{
	int ret = 1;
	int count = 0;
	if (*this < d)
		ret = -1;
	Date max = (*this) > d ? *this : d;
	Date min = (*this) < d ? *this : d;

	while (min<max)
	{
		min++;
		count++;
	}
	return ret * count;
}

这里许多成员的函数的实现是借助其他成员函数来实现的,这样函数的复用性就会大大提高,便于以后代码的修改。

const成员函数

const修饰类的成员函数

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

例如上面的 日期打印函数:

void print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

为了防止在print成员函数中对类成员变量进行修改,所以这里我们最好把print函数写成const成员函数

void print() const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

注意:

  1. const对象是不能调用非const成员函数,防止通过非const成员函数对const进行修改
  2. 非const函数是可以调用const对象的
  3. const成员函数是不能调用非const成员函数
  4. 非const函数是可以调用const成员函数的
  5. const只能修饰非静态成员函数

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

这两个函数写不写都无所谓,因为编译器会自动生成

class A
{
public:
	A* operator&()
	{
		return this;
	}
	
	const A* operator&()const
	{
		return this;
	}
};

static成员

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



class A
{
public:
	A(int x=1)
		:_a1(x)
	{
		count++;
	}

	static int Get_count()
	{
		return count;
	}

private:
	int _a1;
	static int count;
};

int A::count = 0;

int main()
{
	A a1(1);
	A a2(2);
	A a3[3];

	cout << A::Get_count() << endl;;
	return 0;
}

在这里插入图片描述

特性:

  1. 静态成员为所有类对象所共享,不属于某个具体实例,属于整个类
  2. 静态成员变量必须在类外定义,定义时不添加static关键字
  3. 类静态成员即可用 类名:: 静态成员 或者 对象.静态成员来访问
  4. 静态成员函数没有隐藏this指针,不能访问任何非静态成员
  5. 静态成员和类的普通成员一样,也有public,protected,private三种访问级别,也可以具有返回值
  6. static成员是在静态区上开辟空间,所以计算sizeof(类)的时候是不计算static成员的

友元

友元函数

友元函数可以直接访问类的私有成员,他是定义在类外部的普通函数(不是成员函数),不属于任何类,但需要在类的内部声明,声明时要加friend 关键字

特性:

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

友元类

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

注意:

  • 友元关系是单向的,不具有交换性(例如:类A是类B的友元,那么B就可以访问类B里的所有成员,但是反过来却不行。)
  • 友元关系不能传递(A是B的友元,B是C的友元,则不能说明A是C的友元)

内部类

概念:
如果一个类定义在另一个类的内部,就叫做内部类。

特性:

  • 内部类是一个独立的类,他不属于外部类,更不能通过外部类的对象去调用内部类。
  • 外部类对内部类没有访问权限的 !
  • 内部类可以直接访问外部类中的任意成员,不需要外部类的对象/类名
  • sizeof(外部类)=外部类 内部类不参与外部类大小计算
  • 内部类和友元类没啥区别,只不过收到外部类的类域的限制(不同的访问限定符决定不同的访问权限)

class A
{
public:
	A(int x = 1)
		:_a1(x)
	{
	}

	class B
	{
	public:
		B(int b=1)
		{
			_b1 = b;
		}
	private:
		int _b1;
	};

private:
	int _a1;
};

int main()
{
	A a1;
	//B b1;  //由于收到A的类域的限制,无法 在全局直接访问类B
	A::B b1;   //必须通过A来访问B
}

关于类的两个问题

编译器优化拷贝构造函数


class A
{
public:

	A(const A& d)
	{
		cout << "拷贝" << endl;
	}

	A& operator = (const A& a)
	{
		cout << "=" << endl;
		_a = a._a;
		return *this;
	}

private:
	int _a;
};
A fun(A a)
{
	return a;
}

void  fun1(A a)
{

}

例一:

int main()
{
	A a1;

	fun1(a1);
}

众所周知:函数传值是传的实参的一份拷贝,所以这里会调用一次拷贝构造函数

例二:

int main()
{
	A a1;
	A a2 = fun(a1);

}

前面传值给函数函数 有一次拷贝构造
这里是把函数的返回值拷贝构造a2(注意这里不是赋值重载!!),函数传返回值我们知道是先传给一个构造的中间变量,再由中间变量传给主函数的值。所以这里单纯传返回值就应该有两次拷贝构造
所以这里总共应该有三次可拷贝构造!!!!
在这里插入图片描述
但实际上:只有两次拷贝构造
在这里插入图片描述
这里对传返回值做了简化:
在这里插入图片描述

构造、析构函数的调用先后问题

class A
{
public:
	A(int x=1)
		:_a1(x)
	{
		cout << "A()" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}

private:
	int _a1;
};

A a;
int main()
{
	A b;
	A c;
	static A d;
}

这里构造函数的调用顺序是:a b c d
析构函数的调用顺序是:c b d a

注意:

  1. 全局变量永远是最先构造,在进入main函数之前就会完成构造
  2. 函数内的静态变量由于存在静态区,所以出函数不会被销毁,顾析构函数调用顺序在形参的后面
  3. 静态变量 比全局变量先析构
  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值