【C++】类和对象(重中之重)

发布时间:2023-12-16 11:00

目录

0. 前言

1.面向过程和面向对象初步认识

2.类的引入

3.类的定义

类的两种定义方式:

4.类的访问限定符及封装

4.1 访问限定符

访问限定符说明:

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

4.2 封装

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

5.类的作用域

6.类的实例化

7.类对象模型

7.1 如何计算类对象的大小

7.2 类对象的存储方式猜测

问题:

结论:

7.3 结构体内存对齐规则

8.this指针

8.1 this指针的引出

8.2 this指针的特性

面试题:

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

10. 构造函数

10.1 概念

10.2 特性

其特征如下:

11. 析构函数

11.1 概念

11.2 特性

其特征如下:

12. 拷贝构造函数

12.1 概念

12.2 特征

其特征如下:

13. 赋值运算符重载

13.1 运算符重载

注意:

13.2 赋值运算符重载

​编辑 赋值运算符主要有五点:

默认成员函数的总结

分开解释

整体总结

思考

14.日期类的实现

Date.h(类、函数的定义)

Date.cpp(类中函数的实现)

Test.cpp(测试)

15. const成员

15.1 const修饰类的成员函数

思考:

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

17. 再谈构造函数

17.1 构造函数体赋值

17.2 初始化列表

注意:

问题:

17.3 explicit关键字

18. static成员

18.1 概念

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

18.2 特性 

​编辑

问题:

19. C++11 的成员初始化新玩法

20. 友元

20.1 友元函数

问题:

注意: 

20.2 友元类

21. 内部类

概念及特性


0. 前言

        此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。

        大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~


1.面向过程和面向对象初步认识

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

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


2.类的引入

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

struct Student
{
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}

	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}

	char _name[20];
	char _gender[3];
	int _age;
};
int main()
{
	Student s;
	s.SetStudentInfo("Liyuyue", "男", 20);
	s.PrintStudentInfo();
	return 0;
}

【C++】类和对象(重中之重)_第1张图片

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


3.类的定义

class ClassName
{
	// 类体:由成员函数和成员变量组成

}; // 一定要注意后面的分号

        class为定义类的关键字ClassName为类的名字{}中为类的主体注意类定义结束时后面分号

        类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数

类的两种定义方式:

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

【C++】类和对象(重中之重)_第2张图片

         2. 声明放在.h文件中,类的定义放在.cpp文件中。

【C++】类和对象(重中之重)_第3张图片

        一般情况下,更期望采用第二种方式。

4.类的访问限定符及封装

4.1 访问限定符

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

【C++】类和对象(重中之重)_第4张图片

访问限定符说明:

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

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

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

4.2 封装

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

在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?

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

        封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们目的全封装起来,不让别人看。所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一下。 不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。


5.类的作用域

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

class Person
{
public:
	void PrintPersonInfo();
private:
	char _name[20];
	char _gender[3];
	int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
	cout << _name << " " << _gender << " " << _age << endl;
}

int main()
{
	Person p;
	p.Person::PrintPersonInfo();
	return 0;
}

【C++】类和对象(重中之重)_第5张图片


6.类的实例化

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

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

【C++】类和对象(重中之重)_第6张图片

【C++】类和对象(重中之重)_第7张图片


7.类对象模型

7.1 如何计算类对象的大小

        类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

class A
{
public:
	void PrintA()
	{
	cout << _a << endl;
	}
private:
	char _a;
	int _b;
};

int main()
{
	A a;
	int sz = sizeof(a);
	cout << sz << endl;
	return 0;
}

【C++】类和对象(重中之重)_第8张图片

7.2 类对象的存储方式猜测

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

【C++】类和对象(重中之重)_第9张图片

         缺陷 :每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?

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

【C++】类和对象(重中之重)_第10张图片

问题:

        对于上述两种存储方式,那计算机到底是按照那种方式来存储的?

我们再通过对下面的不同对象分别获取大小来分析看下:

// 类中既有成员变量,又有成员函数
class A1 {
public:
	void f1() {}
private:
	int _a;
};
// 类中仅有成员函数
class A2 {
public:
	void f2() {}
};
// 类中什么都没有---空类
class A3
{};

int main()
{
	A1 a1;
	A2 a2;
	A3 a3;
	int sz = sizeof(a1);
	cout << sz << endl;

	sz = sizeof(a2);
	cout << sz << endl;

	sz = sizeof(a3);
	cout << sz << endl;
	return 0;
}

【C++】类和对象(重中之重)_第11张图片

结论:

        一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。

7.3 结构体内存对齐规则

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8。
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是 所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

8.this指针

8.1 this指针的引出

        我们先来定义一个日期类Date:

class Date
{
public:
	void Display()
	{
		cout << _year << "-" << _month << "-" << _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()
{
	Date d1, d2;
	d1.SetDate(2018, 5, 1);
	d2.SetDate(2018, 7, 1);
	d1.Display();
	d2.Display();
	return 0;
}

【C++】类和对象(重中之重)_第12张图片

        对于上述类,有这样的一个问题:Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当s1调用SetDate函数时,该函数是如何知道应该设置s1对象,而不是设置s2对象呢?

【C++】类和对象(重中之重)_第13张图片

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

8.2 this指针的特性

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

【C++】类和对象(重中之重)_第14张图片

面试题:

  1. this指针存在哪里?
  2. this指针可以为空吗?
// 1.下面程序能编译通过吗?
// 2.下面程序会崩溃吗?在哪里崩溃
class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}

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

【C++】类和对象(重中之重)_第15张图片


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

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

class Date {};

【C++】类和对象(重中之重)_第16张图片


10. 构造函数

10.1 概念

对于以下的日期类:


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

	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.SetDate(2018, 5, 1);
	d1.Display();
	Date d2;
	d2.SetDate(2018, 7, 1);
	d2.Display();
	return 0;
}

【C++】类和对象(重中之重)_第17张图片

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

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

10.2 特性

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

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

其特征如下:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
    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;
    };
    int main()
    {
    	Date d1; // 调用无参构造函数
    	Date d2(2015, 1, 1); // 调用带参的构造函数
    
    	// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
    	// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    	Date d3();
    
    	return 0;
    }

    【C++】类和对象(重中之重)_第18张图片
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
    class Date
    {
    public:
    	/*
    	// 如果用户显式定义了构造函数,编译器将不再生成
    	Date (int year, int month, int day)
    	{
    	_year = year;
    	_month = month;
    	_day = day;
    	}
    	*/
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    int main()
    {
    	Date d;
    	return 0;
    }

    【C++】类和对象(重中之重)_第19张图片
  6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数
    // 默认构造函数
    class Date
    {
    public:
    	Date()
    	{
    		_year = 1900;
    		_month = 1;
    		_day = 1;
    	}
    
    	Date(int year = 1900, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d1;
    }

    【C++】类和对象(重中之重)_第20张图片
  7. 关于编译器生成的默认成员函数,很多小伙伴会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d 对象调用了编译器生成的默认构造函数,但 是 d 对象 _year / _month / _day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么卵用???


    解答:C++把类型分成内置类型(基本类型)自定义类型。内置类型就是语法已经定义好的类型:如 int / char...,自定义类型就是我们使用 class / struct / union 自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数
    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++】类和对象(重中之重)_第21张图片
  8. 成员变量的命名风格  
// 我们看看这个函数,是不是很僵硬?
class Date
{
public:
	Date(int year)
	{
		// 这里的year到底是成员变量,还是函数形参?
		year = year;
	}
private:
	int year;
};
// 所以我们一般都这样
class Date
{
public:
	Date(int year)
	{
		_year = year;
	}
private:
	int _year;
};
// 或者这样。
class Date
{
public:
	Date(int year)
	{
		m_year = year;
	}
private:
	int m_year;
};

【C++】类和对象(重中之重)_第22张图片


11. 析构函数

11.1 概念

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

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

11.2 特性

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

其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值(注意是无返回值,而不是返回值为空,且不能被重载)。
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
    
    typedef int DataType;
    class SeqList
    {
    public:
    	SeqList(int capacity = 10)
    	{
    		_Data = (DataType*)malloc(capacity * sizeof(DataType));
    		_size = 0;
    		_capacity = capacity;
    	}
    
    	~SeqList()
    	{
    		cout << "~Stack()析构函数" << endl;
    		if (_Data)
    		{
    			free(_Data); // 释放堆上的空间
    			_Data = nullptr; // 将指针置为空
    			_capacity = 0;
    			_size = 0;
    		}
    	}
    
    private:
    	int* _Data;
    	size_t _size;
    	size_t _capacity;
    };
    int main()
    {
    	SeqList L;
    	return 0;
    }

    【C++】类和对象(重中之重)_第23张图片
    class Stack
    {
    public:
    	Stack(int capacity = 4)
    	{
    		_a = (int*)malloc(sizeof(int) * capacity);
    		_size = 0;
    		_capacity = capacity;
    	}
    	void Push(int x)
    	{}
    	// 像Stack这样的类,析构函数具有重大意义
    	~Stack()
    	{
    		cout << "~Stack()析构函数" << endl;
    		// 清理资源
    		free(_a);
    		_a = nullptr;
    		_size = _capacity = 0;
    	}
    private:
    	int* _a;
    	int _size;
    	int _capacity;
    };
    int main()
    {
    	Stack st1;
    	st1.Push(1);
    	st1.Push(2);
    	Stack st2;
    	st2.Push(10);
    	st2.Push(11);
    	return 0;
    }

    【C++】类和对象(重中之重)_第24张图片
  5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,会对自定类型成员调用它的析构函数
    class String
    {
    public:
    	String(const char* str = "Liyuyue")
    	{
    		_str = (char*)malloc(strlen(str) + 1);
    		strcpy(_str, str);
    	}
    	~String()
    	{
    		cout << "~String()" << endl;
    		free(_str);
    	}
    private:
    	char* _str;
    };
    
    class Person
    {
    private:
    	String _name;
    	int _age;
    };
    int main()
    {
    	Person p;
    	return 0;
    }

    【C++】类和对象(重中之重)_第25张图片

        最后要声明一下,析构函数不是完成对象的销毁,这个对象是存在函数栈帧里面,函数结束,栈帧销毁,对象就销毁了

        针对这个对象有两块空间要销毁

  1. 对象本身,它是函数结束时栈帧销毁,它就销毁。
  2. 对象里的*_str指向的堆上的空间,它是析构函数清理的。

12. 拷贝构造函数

12.1 概念

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

12.2 特征

        拷贝构造函数也是特殊的成员函数。

其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个必须使用引用传参,使用传值方式会引发无穷递归调用
    class Date
    {
    public:
    	Date(int year = 2002, int month = 9, int day = 13)
    	{
    		_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;
    	Date d2(d1);
    	return 0;
    }

    【C++】类和对象(重中之重)_第26张图片
    【C++】类和对象(重中之重)_第27张图片
  3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
    class Date
    {
    public:
    	/*Date(int year = 2021, int month = 3, int day = 21)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}*/
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    int main()
    {
    	Date d1; 
    	// 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
    	Date d2(d1);
    	
    	return 0;
    }

    【C++】类和对象(重中之重)_第28张图片
  4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
    #include
    
    // 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
    class String
    {
    public:
    	String(const char* str = "Liyuyue")
    	{
    		_str = (char*)malloc(strlen(str) + 1);
    		strcpy(_str, str);
    	}
    	~String()
    	{
    		cout << "~String()" << endl;
    		free(_str);
    	}
    private:
    	char* _str;
    };
    int main()
    {
    	String s1("Hello CSDN");
    	String s2(s1);
    }

    【C++】类和对象(重中之重)_第29张图片
    【C++】类和对象(重中之重)_第30张图片

13. 赋值运算符重载

13.1 运算符重载

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

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

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

注意:

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型或者枚举类型的操作数
  3. 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
  4. 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
  5. .*::sizeof?:. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
// 全局的operator==
class Date
{
public:
	Date(int year = 2002, int month = 9, int day = 13)
	{
		_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;
}
int main()
{
	Date d1(2021, 3, 21);
	Date d2(2021, 3, 21);
	cout << (d1 == d2) << endl;
	return 0;
}

【C++】类和对象(重中之重)_第31张图片


class Date
{
public:
	Date(int year = 2002, int month = 9, int day = 13)
	{
		_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;
		}
	}
private:
	int _year;
	int _month;
	int _day;
};

【C++】类和对象(重中之重)_第32张图片

13.2 赋值运算符重载


class Date
{
public:
	Date(int year = 2002, int month = 9, int day = 13)
	{
		_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;
		}
	}
private:
	int _year;
	int _month;
	int _day;
};

【C++】类和对象(重中之重)_第33张图片 赋值运算符主要有五点:

  1. 参数类型
  2. 返回值
  3. 检测是否自己给自己赋值
  4. 返回*this
  5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
class Date
{
public:
	Date(int year = 2002, int month = 9, int day = 13)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(2021,3, 21);
	// 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。

	d1 = d2;
	return 0;
}

【C++】类和对象(重中之重)_第34张图片

        那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

// 这里会发现下面的程序会崩溃掉
class String
{
public:
	String(const char* str = "")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
	}
private:
	char* _str;
};
int main()
{
	String s1("Hello");
	String s2("CSDN");

	s1 = s2;
}

【C++】类和对象(重中之重)_第35张图片


默认成员函数的总结

分开解释

        默认成员函数:我们不写编译器会自动生成,虽然会自动生成,但是好多时候还是需要我们自己写,因为生成的那个不一定好用。

        构造函数:初始化,大部分情况下,都需要我们自己写构造函数。

        析构函数:清理内对象中资源。

        拷贝构造函数:拷贝初始化,特殊构造函数,深浅拷贝问题。

        赋值运算符重载:也是拷贝行为,但是不一样的是,拷贝构造是创建一个对象时,拿同类对象初始化的拷贝,这里的赋值拷贝时那个对象已经都存在了,都被初始化过了,现在想把一个对象,复制拷贝给另一个对象。

整体总结

1. 构造和析构的特性是类似的,我们不写编译器内置类型不处理,自定义类型调用它的构造和析构处理。

2. 拷贝构造和赋值重载特性是类似的,内置类型会完成浅拷贝,自定义类型会调用他们的拷贝构造和赋值重载。

思考

Date d5(d1):拷贝构造,拿一个已经存在的对象去构造初始化另一个要创建的对象。

d1 = d2:赋值重载,两个已经存在的对象进行拷贝。

Date d6 = d1:这是要注意的,这里是拷贝构造,不是赋值重载,因为这里符合拷贝构造的定义:拿一个已经存在的对象去构造初始化另一个要创建的对象。


14.日期类的实现

        这里为了让大家更好的理解,我已经用多文件的方式写了一份日期类的代码,大家可以通过我写的代码来巩固自己对前面知识的理解,里面写了很多注释,都很清楚,如果有兴趣的小伙伴可以尝试去写,会有很大的收获!!!

        大家如果需要以下代码可以点下面链接去我的gitee去取。Class_Date_/Class_Date_ · Liyuyuea/C++ - 码云 - 开源中国 (gitee.com)https://gitee.com/liyuyuea/CPP/tree/master/Class_Date_/Class_Date_

Date.h(类、函数的定义)

#pragma once

#include

using std::cout;
using std::endl;
using std::cin;
using std::istream;
using std::ostream;

// 析构、拷贝构造、赋值重载,可以不写,默认生成就够用; 
// 像Stack才需要自己写这三个
class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	// 全缺省的构造函数
	Date(int year = 0, int month = 1, int day = 1);


	// 获取某年某月的天数
	int GetMonthDay(int year, int month);


	void Print() const;


//	Date d5(d1); // 拷贝构造  拿一个已经存在的对象去构造初始化另一个要创建的对象
//	d1 = d2;     // 赋值重载  两个已经存在的对象->拷贝
//	Date d6 = d1; // 拷贝构造? 赋值重载? --->拷贝构造


	// 拷贝构造函数
	// 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) const;

	// 日期-=天数
	Date& operator-=(int day);

	// 日期-天数
	Date operator-(int day) const;


	// ++前置
	Date& operator++();

	// 后置++
	Date operator++(int);

	// 后置--
	Date operator--(int);

	// --前置
	Date& operator--();


	// >运算符重载
	bool operator>(const Date& d) const;

	// ==运算符重载
	bool operator==(const Date& d) const;

	// >=运算符重载
	bool operator >= (const Date& d) const;

	// <运算符重载
	bool operator < (const Date& d) const;

	// <=运算符重载
	bool operator <= (const Date& d) const;

	// !=运算符重载
	bool operator != (const Date& d) const;



	// 日期-日期 返回天数
	int operator-(const Date& d) const;

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

Date.cpp(类中函数的实现)

#include"Date.h"

// 获取当年当月的天数
// 1.定义成内联函数,因为频繁调用,为了减少函数栈帧的消耗,直接展开
// 2.这个数要频繁调用,用static放在静态区最好
// 3.month写在判断闰年之前
inline int Date::GetMonthDay(int year, int month)
{
	
	static int Dayarray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	int day = Dayarray[month];

	if (month == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0))
	{
		day = 29;
	}

	return day;
}

// 打印
void Date::Print() const
{
	cout << _year << "年" << _month << "月" << _day << "日" << endl;
}

// 全缺省的构造函数
Date::Date(int year, int month, int day)
{
	if (year > 0 && month > 0 && month < 13 && day > 0 && day < GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法日期" << endl;
		cout << year << "年" << month << "月" << day << "日" << endl;
	}

}

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


// 赋值运算符重载
Date& Date::operator=(const Date& d)
{
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	return *this;
}


Date::~Date()
{
	_year = 0;
	_month = 0;
	_day = 0;
}


// 日期+=天数
Date& Date::operator+=(int day)
{
	_day += day;
	// 天数不合法,不断进位,让日期(天数)合法
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month > 12)
		{
			_month = 1;
			_year++;
		}
	}

	return *this;
}



// 日期+天数
Date Date::operator+(int day) const
{
	//复刻+=
	Date ret(*this);
	ret += day;
	return ret;
}

// 日期-=天数
Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day < 1)
	{
		_day += GetMonthDay(_year, _month - 1);
		_month--;
		if (_month < 1)
		{
			_month = 12;
			_year--;
		}
	}
	return *this;
}

// 日期-天数
Date Date::operator-(int day) const
{
	Date ret(*this);
	ret -= day;
	return ret;
}



// 前置++和后置++都完成了++,不同的地方在于返回值不一样
// 因为他们的运算符是一样的,函数名就是一样的,
// 为了区分,对后置++做了特殊处理,加了一个参数,形成函数重载

//++前置
Date& Date::operator++()
{
	*this += 1;//复刻
	return *this;
}


//后置++
Date Date::operator++(int)
{
	Date ret(*this);
	*this += 1;
	return ret;
}


//--前置
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

//后置--
Date Date::operator--(int)
{
	Date ret = *this;
	*this -= 1;
	return ret;
}



// ==运算符重载
bool Date::operator==(const Date& d) const
{
	if (_year == d._year)
	{
		if (_month == d._month)
		{
			if (_day == d._day)
				return true;
		}
	}
	return false;
}

// >运算符重载
bool Date::operator>(const Date& d) const
{
	if (_year > d._year)
		return true;
	else if (_year == d._year)
	{
		if (_month > d._month)
			return true;
		else if (_month == d._month)
		{
			if (_day > d._day)
				return true;
			else if (_day == d._day)
				return false;
			else
				return false;
		}
		else
			return false;
	}
	else
		return false;
	
}

// >=运算符重载
bool Date::operator >= (const Date& d) const
{
	return (*this == d) || (*this > d);
}

// <运算符重载
bool Date::operator < (const Date& d) const
{
	return !(*this >= d);
}

// <=运算符重载
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) const
{
	// 效率差别不大的情况下,尽量选择写可读性强的,简单的程序
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (max != min)
	{
		++min;
		++n;
	}

	return n * flag;
}

Test.cpp(测试)

#include"Date.h"

void Test1()
{
	Date d1(2022, 6, 12);
	d1.Print();

	d1 += 100;
	d1.Print();

	d1 += 1;
	d1.Print();

	cout << endl;

	Date d2(2021, 3, 21);
	Date tmp = d2 + 20;
	tmp.Print();

	tmp = d2 + 40;
	tmp.Print();

}

void Test2()
{
	Date d1(2022, 6, 12);
	d1.Print();

	d1 -= 100;
	d1.Print();

	d1 -= 1;
	d1.Print();

	cout << endl;

	Date d2(2021, 3, 21);
	Date tmp = d2 - 20;
	tmp.Print();

	tmp = d2 - 40;
	tmp.Print();

}

void Test3()
{
	//Date d1(2022, 6, 12);
	//d1.Print();

	//d1++;
	//d1.Print();

	//d1++;
	//d1.Print();

	cout << endl;

	Date d2(2021, 3, 21);
	Date tmp1 = ++d2;
	tmp1.Print();
	d2.Print();

	Date tmp2 = d2++;
	tmp2.Print();
	d2.Print();

}

void Test4()
{
	cout << endl;

	Date d2(2021, 3, 21);
	Date tmp1 = --d2;
	tmp1.Print();
	d2.Print();

	Date tmp2 = d2--;
	tmp2.Print();
	d2.Print();

}

void Test5()
{
	Date d1(2022, 6, 12);
	
	cout << endl;

	Date d2(2021, 3, 21);
	
	int ret = d1 <= d2;
	cout << ret << endl;

}

void Test6()
{
	Date d1(2022, 3, 21);

	Date d2(2021, 3, 21);

	int tmp = d1 - d2;
	cout << tmp << endl;

}

//void Test7()
//{
//	Date d2(2021, 3, 21);
//
//	
//	cout << d2;
//
//}


int main()
{
	//Test7();
	return 0;
}

15. const成员

15.1 const修饰类的成员函数

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

【C++】类和对象(重中之重)_第36张图片

class Date
{
public:
	void Display()
	{
		cout << "Display ()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
	void Display() const
	{
		cout << "Display () const" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;
	d1.Display();

	const Date d2;
	d2.Display();
	return 0;
}

【C++】类和对象(重中之重)_第37张图片

思考:

  1. const对象可以调用非const成员函数吗?
  2. 非const对象可以调用const成员函数吗?
  3. const成员函数内可以调用其它的非const成员函数吗?
  4. 非const成员函数内可以调用其它的const成员函数吗?

【C++】类和对象(重中之重)_第38张图片


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

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

class Date
{
public:
	Date* operator&()
	{
		//return this;
		return nullptr;
	}

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

【C++】类和对象(重中之重)_第39张图片

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


17. 再谈构造函数

17.1 构造函数体赋值

        在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

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

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

        虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

17.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. 类中包含以下成员,必须放在初始化列表位置进行初始化:
    1. 引用成员变量
    2.const成员变量
    3.自定义类型成员(该类没有默认构造函数)
    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 
    };

  3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
    class Time
    {
    public:
    	Time(int hour = 0)
    		:_hour(hour)
    	{
    		cout << "Time()" << endl;
    	}
    private:
    	int _hour;
    };
    
    class Date
    {
    public:
    	Date(int day)
    	{}
    private:
    	int _day;
    	Time _t;
    };
    int main()
    {
    	Date d(1);
    	return 0;
    }

    【C++】类和对象(重中之重)_第40张图片
    【C++】类和对象(重中之重)_第41张图片
  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关(所以建议在写初始化列表的时候要按照声明顺序写)。
    //A. 输出1 1
    //B.程序崩溃
    //C.编译不通过
    //D.输出1 随机值
    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();
    }
    
    【C++】类和对象(重中之重)_第42张图片
     

问题:

构造函数中,在哪里调用的自定义类型的默认构造函数呢?我们先要明确以下三点。

  1. 对象定义的时候自动调用构造函数
  2. 构造函数的初始化列表可以认为是成员变量定义初始化的地方
  3. 初始化列表,你显示的写或者不写,都要走一遍

        内置函数如果在初始化列表没初始化,那么初始值就是随机值,自定义类型在初始化的时候要调用它的默认构造函数进行初始化,所以自定义类型是在构造函数的初始化列表调用它的默认构造函数的。

17.3 explicit关键字

         构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用

class Date
{
public:
	Date(int year)
		:_year(year)
	{}

	explicit Date(int year)
		:_year(year)
	{}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2018);

	d1 = 2019;
	return 0;
}

【C++】类和对象(重中之重)_第43张图片

         上述代码可读性不是很好,用explicit修饰构造函数,将会禁止单参构造函数的隐式转换

18. static成员

18.1 概念

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

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

class A
{
public:
	A() 
	{ 
		++_scount; 
	}
	A(const A& t) 
	{
		++_scount;
	}
	static int GetACount() 
	{ 
		return _scount;
	}
private:
	static int _scount;
};
int A::_scount = 0;
 int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
	return 0;
}

【C++】类和对象(重中之重)_第44张图片

18.2 特性 

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,他是放在静态区的
  2. 静态成员变量必须在类外定义,定义时不添加static关键字
  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值

【C++】类和对象(重中之重)_第45张图片

问题:

  1. 静态成员函数可以调用非静态成员函数吗?
  2. 非静态成员函数可以调用类的静态成员函数吗?

【C++】类和对象(重中之重)_第46张图片

19. C++11 的成员初始化新玩法

        C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值

class B
{
public:
	B(int b = 0)
		:_b(b)
	{}
	int _b;
};
class A
{
public:
	void Print()
	{
		cout << a << endl;
		cout << b._b << endl;
		cout << p << endl;
		cout << n << endl;
	}
private:
	// 非静态成员变量,可以在成员声明时给缺省值。
	int a = 10;
	B b = 20;
	int* p = (int*)malloc(4);
	static int n;
};
int A::n = 10;
int main()
{
	A a;
	a.Print();
	return 0;
}

【C++】类和对象(重中之重)_第47张图片

20. 友元

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

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

20.1 友元函数

问题:

        现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator>>重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。

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

	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day;
		return _cout;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d(2021, 3, 21);
	d << cout;
	return 0;
}

【C++】类和对象(重中之重)_第48张图片

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

class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 2021, int month = 3, int day = 21)
		: _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 d;
	cin >> d;
	cout << d << endl;
	return 0;
}

【C++】类和对象(重中之重)_第49张图片

注意: 

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

20.2 友元类

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

  1. 友元关系是单向的,不具有交换性。
            比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  2. 友元关系不能传递
           如果B是A的友元,C是B的友元,则不能说明C时A的友元。
class Date; // 前置声明
class Time
{
	friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
	Time(int hour = 1, int minute = 1, int second = 1)
		: _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;
};

【C++】类和对象(重中之重)_第50张图片

21. 内部类

概念及特性

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

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

特性:

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

	b.foo(A());
	return 0;
}

 【C++】类和对象(重中之重)_第51张图片

【C++】类和对象(重中之重)_第52张图片

         如上就是C++类和对象的所有知识,接下来本专辑会更新C++内存管理,如果大家喜欢看此文章并且有收获,可以时刻关注我,本专栏持续更新!

        感谢大家观看,感谢大家支持!

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号