完整教程:C++中关于类和对象

最近学了C++中关于类和对象的知识,特此写下此篇加深自己的印象。

本篇目录

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

2.类的引入

3.类的定义

类的两种定义方式:

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

4.1访问限定符

【访问限定符说明】

【面试题】

4.2封装

【面试题】

5.类的作用域

6.类的实例化

7.类对象模型

7.1如何计算类对象的大小

7.2 类对象的存储方式的猜测

7.3 结构体内存对齐规则

【面试题】

1.结构体怎么对齐?为什么要进行内存对齐?

2.如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?

3.什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景?

8.this指针

8.1 this指针的引出

8.2 this指针的特性

【面试题】

1.this指针存在哪里?

2.this指针可以为空吗?

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

10.构造函数

10.1 概念

10.2 特性

11.析构函数

11.1 概念

11.2 特性

12. 拷贝构造函数

12.1 概念

12.2 特征

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

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

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

4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,那么还需要自己显式实现吗?

5.拷贝构造函数典型调用场景:

13. 赋值运算符重载

13.1运算符重载

13.2 赋值运算符重载

1.赋值运算符重载格式

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

13.3 前置++和后置++重载

14.const成员

15.取地址以及取地址操作符重载

16.再谈构造函数

16.1构造函数的赋值

16.2初始化列表

【注意】

16.3explicit关键字

17.Static成员

17.1概念

【面试题】

17.2 特性

【面试题】

18.友元

18.1友元函数

18.2友元类

19.内部类

20. 再次理解类和对象



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

什么是面向过程,什么是面向对象?下面我们可以来建立一个初步的认识。大家肯定都知晓C语言是面向过程的语言,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

咱们可以拿洗衣服举例子。面向过程是把洗衣服的这件事情,分成一个一个过程,逐步解决问题。

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

总共有四个对象人,衣服,洗衣粉,洗衣机

整个洗衣服的过程:人将衣服放进洗衣机、倒入洗衣粉,启动洗衣机,洗衣机就会完成洗衣过程并且甩干。

整个过程主要是:人,衣服,洗衣粉,洗衣机四个对象之间交互完成的,人不需要关心洗衣机具体是如何洗衣服的,是如何甩干的。

2.类的引入

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

typedef int DataType;
struct Stack
{
	void Init(size_t capacity)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(const DataType& data)
	{
		//扩容...
		_array[_size] = data;
		++_size;
	}
	DataType Pop()
	{
		//....
		return _array[_size - 1];
	}
	DataType* _array;
	size_t _capacity;
	size_t _size;
};

但是对于上述的结构体的定义,在C++中更喜欢用class来代替使用。

3.类的定义

class classname
{
	//类体:由成员函数和成员变量组成
};  //这里要注意有分号

class为定义类的关键字,classname为类的名字,{}中为类的主体,注意类定义结束时,后面的分号不能省略

类体中内容称为类的成员:类中变量称为类的属性或者成员变量;类中的函数成为类的方法或者成员函数。

类的两种定义方式:

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

2.类声明放在.h文件中,成员函数定义放在.cpp中,注意:成员函数名前需要加类名::

一般情况下,更期望采用第二种方式。还有,关于成员变量的命名规则的小建议:我们在给成员变量去名称的时候,为了不引发歧义,我们一般习惯给类内成员变量的前面或者后面加一个"_"标识符,无特殊含义,只是为了区分,不造成歧义,同时,也是一种很好的编码风格。如下:

//一般都建议这样命名
class Date
{
public:
	void Init(int year)
	{
		_year = year;
	}
private:
	int _year;
};
// 或者这样
class Date
{
public:
	void Init(int year)
	{
		mYear = year;
	}
private:
	int mYear;
};
//或者其他方式可以的。

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

4.1访问限定符

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

【访问限定符说明】

1.public修饰的成员在类外可以直接被访问。

2.protected和private修饰的成员在类外不能直接被访问。

3.访问权限作用域是从该访问限定符出现的位置开始知道下一个访问限定符出现为止

4.如果后面没有访问限定符,作用域就到 } 即类结束。

5.class的默认访问权限为private,struct为public(因为struct要兼容C)。

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


【面试题】

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

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


4.2封装


【面试题】

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


那何为封装呢?

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

封装本质上就是一种管理,让用户更方便使用类。

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

5.类的作用域

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

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

注意:只有局部域和全局域会影响生命周期,类域和命名空间域不会影响生命周期

6.类的实例化

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

1.类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。比如:入学时填写的学生信息表,表格就可以看成一个类,来描述具体学生信息

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

int main()
{
 Person._age = 100;   // 编译失败:error C2059: 语法错误:“.”
 return 0;
}

注意:这里Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。

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

7.类对象模型

7.1如何计算类对象的大小

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

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

7.2 类对象的存储方式的猜测

1.对象中包含类的各个成员

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

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

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

问题:对于上述三种存储方式,那计算机到底是按照哪种方式来存储的呢?

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

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

结论:一个类的大小,实际就是该类中“成员变量”之和,当然,这里要注意一下内存对齐。

注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象

7.3 结构体内存对齐规则

上述类的内存对齐规则就和结构体内存规则相似的,这里来对结构体内存对齐规则做一个补充说明。

1.第一个成员在与结构体偏移量为0的地址处。

2.其他成员变量要对齐到某个数字(对齐数) 的整数倍的地址处。

注意:对齐数=编译器默认的一个对齐数 与 该成员的大小的较小值。

visual studio 中默认的对齐数为8

3.结构体总大小为:最大对齐数(每一个成员变量都有一个对齐数)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

【面试题】
1.结构体怎么对齐?为什么要进行内存对齐?

解答:如何进行结构体对齐的解答在上面。下面回答为什么要进行内存对齐:

1)平台原因(移植问题):

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2)性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总的来说:结构体的内存对齐就是一种用空间换取时间的做法,为了提高访问速率。

2.如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?

解答:可以按照指定的对齐参数进行对齐。在visual studio编译器下,可以利用  #pragma 这个预处理指令,使用方式:#pragma pack (1)  -------就是设置默认对齐数为1

#pragma pack ()  ---------取消设置的默认对齐数,还原为默认对齐数。

3.什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景?

解答:就是数据在内存中存储的时候会按照不同的存储顺序,通常就会有两种策略。一种为大端字节序存储,一种是小端字节序存储。

大端(存储)模式:是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处。

小端(存储)模式:是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处。

接下来给两种方案测试一台机器是大端还是小端:

方案1:直接利用大小端的特性。

方案2:利用联合体共用内存的特性。

有些场景下是需要考虑大小端的,比如:网络编程中,要对主机字节序<---->网络字节序 进行相互转化使用。嵌入式系统开发中,与硬件寄存器交互的时候,也需要进行大小端转化。还有解析网络协议啦,数据序列化啦(自定义二进制协议),跨平台数据交换(处理不同架构的数据)啦.....等等场景。

8.this指针

8.1 this指针的引出

关于this指针的问题,我们先来定义一个日期类Date

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		std::cout << _year << "-" << _month << "-" << _day << std::endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1, d2;
	d1.Init(2025, 2, 11);
	d2.Init(2025, 1, 22);
	d1.Print();
	d2.Print();
	return 0;
}

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

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

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

8.2 this指针的特性

1.this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。

2.只能在“成员函数”内部使用this指针。

3.this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。

4.this指针是“成员函数”第一个隐含的指针形参,一般情况都编译器通过ecx寄存器自动传递,不需要用户传递

【面试题】
1.this指针存在哪里?

this指针是类“成员函数”的第一个隐藏的形参指针,故this指针是跟普通参数一样存在函数调用的栈帧里面。

2.this指针可以为空吗?

一般来讲,不能为空,但是还是得看类中“成员函数”有没有利用this指针访问成员变量

下面代码帮助大家理解:

class A
{
public:
	void Print()
	{
		std::cout << "Print()" << std::endl;
	}
private:
	int _a;
};
int main()
{
	A* pa = nullptr;
	pa->Print();
	return 0;
}

pa调用Print(),不会发生解引用,因为Print()的地址不在对象中。pa会作为实参传递给this指针。此时this指针虽然为空的,但是成员函数内并没有对this指针进行解引用。

class A
{
public:
	void Print()
	{
        //通过this指针访问成员变量
		std::cout << _a << std::endl;
	}
private:
	int _a;
};
int main()
{
	A* pa = nullptr;
	pa->Print();
	return 0;
}

pa调用Print(),不会发生解引用,因为Print()的地址不在对象中。pa会作为实参传递给this指针。this指针是空的,但是成员函数内访问了_a成员变量,本质上是this->_a,此时nullptr解引用,固然程序会运行崩溃。

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

如果一个类中什么成员都没有,简称为空类。

但是类中真的什么都没有吗?显然并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

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

10.构造函数

10.1 概念

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

10.2 特性

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

其特征如下:

1.函数名与类名相同。

2.无返回值。

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

4.构造函数可以重载。

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

6.关于编译器生成的默认成员函数,很多人会有困惑:不实现构造函数的情况下,编译器会生成默认的构造函数,但是看起来默认构造函数并没有什么用?

解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char.....,自定义类型就是我们使用class/struct/union等自己定义的类型。

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

7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,这三个可以认为是三个默认构造函数。----注意区分6大默认成员函数!

这里需要注意:当类中没有写构造函数,在编译器自己生成的构造函数中,不会对内置类型/基本类型(int/char...)做处理,对于自定义类型(class/struct....)会调用它自身的默认构造函数。所以建议:如果有内置类型的情况下,就需要自己写构造函数,最好不用编译器自己生成的。如果全部都是自定义类型,那可以考虑让编译器自己生成。

class A
{
public:
	A()
	{
		_a = 0;
	}
private:
	int _a;
};
class Date
{
public:
	//如果类中显式定义了构造函数,编译器将不再生成
	////1.无参构造函数
	//Date()
	//{
	//}
	////2.带参构造函数
	//Date(int year,int month,int day)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
private:
	//基本类型(内置类型)
    //C++11之后打了补丁,内置类型成员变量在类中声明时可以给默认值
	int _year=1970;
	int _month=10;
	int _day=10;
	//自定义类型
	A _A;
};
void TestDate()
{
	//Date d1;//调用无参构造函数-----这里需要注意的是,
    //类中不能有无参构造和全缺省构造同时存在,否则调用此句就会引发歧义。
	//Date d2(2025, 10, 10);//调用带参的构造函数
	//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
	// Date d3();
	//----------------
	//将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
	//将Date类中构造函数放开,一旦自己显式定义任何构造函数之后,编译器将不再生成
}

11.析构函数

11.1 概念

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

11.2 特性

析构函数是特出的成员函数,其特征如下:

1.析构函数名时在类名前面加上字符 ~ 

2.无参无返回值类型。

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

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

5.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。

所以:1.一般情况下,有动态申请资源,就需要显式写析构函数释放资源。

2.没有动态申请的资源,不需要写析构函数。

3.需要释放资源的成员如果都是自定义类型,则不需要写析构函数。

12. 拷贝构造函数

12.1 概念

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

12.2 特征

1.拷贝函数构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归。
class Date
{
public:
	Date(int year = 1900, int month = 10, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//Date(Date d)---错误写法
	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++规定,内置类型传参时是直接拷贝,自定义类型传参时必须调用拷贝构造完成拷贝。

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

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

4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,那么还需要自己显式实现吗?

当然,像是Date日期类这样的类是没有必要自己实现的。如果涉及到有动态内存开辟空间的类就需要我们自己进行显式实现拷贝构造函数啦,否则会出现程序崩溃

typedef int DataType;
class Stack
{
public:
	Stack(int capacity=10)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		//扩容...
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_size = 0;
			_capacity = 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(s1);
	return 0;
}

所以:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数一定要写,否则就是浅拷贝。

5.拷贝构造函数典型调用场景:

1)使用已存在对象创建新对象。

2)函数参数类型为类类型对象。

3)函数返回值类型为类类型对象。

class Date
{
public:
	Date(int year, int minute, int day)
	{
		cout << "Date(int,int,int):" << this << endl;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d):" << this << endl;
	}
	~Date()
	{
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date Test(Date d)
{
	Date temp(d);
	return temp;
}
int main()
{
	Date d1(2022, 1, 13);
	Test(d1);
	return 0;
}

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量用引用。

13. 赋值运算符重载

13.1运算符重载

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

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

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

注意:1.不能通过连接其他符号来创建新的操作符:比如operator@

2.重载操作符必须有一个类类型参数。

3.用于内置类型的运算符,其含义不能改变,例如:内置类型+,不能改变其含义。

4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针。

5.    .*     ::      sizeof      ?:       .        注意以上5个运算符不能重载。这个经常在笔试选择题中出现。一定要注意!!!

class Date
{
public:
	Date(int year=1900,int month=10,int day=10)
	{
		_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;
};

13.2 赋值运算符重载

1.赋值运算符重载格式

1)参数类型: const T&,传递引用可以提高传参效率

2)返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

3)检测是否自己给自己赋值。

4)返回*this :要复合连续赋值的含义

class Date
{
public:
	Date(int year = 1900, int month = 10, int day = 10)
	{
		_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;
	}
private:
	int _year;
	int _month;
	int _day;
};
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 =”必须是非静态成员

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

3.用户没有显式实现时,编译器会生成一个默认的赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量时直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

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;
		}
		return *this;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	Date d2;
	d1 = d2;
	return 0;
}

既然编译器生成的默认运算符重载就已经可以完成字节序的值拷贝了,那么还需要自己来实现吗?

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

13.3 前置++和后置++重载

注意一下:后置++,C++作了规定:后置++重载时要多增加一个int类型的参数,但调用函数时该参数不用传递,编译器会自动传递。

还有小细节:前置++中是先+1,后使用,返回的是+1后的结果。所以要返回this指向的对象,this指向的对象函数结束后不会销毁,故可以采用引用的方式返回来提高效率。后置++就不可以,因为后置++是先使用,后+1,返回的是+1前的结果。所以必须有临时对象,返回临时对象,因此返回值不可以用引用返回。

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//前置 ++:返回+1之后的结果。
	//注意:this指向的对象函数结束后不会销毁,故以引用方式返回以此提高效率。
	Date& operator++()
	{
		_day += 1;
		return *this;
	}
	//后置++:
	// 前置++和后置++ 都是一元运算符,为了让前置++与后置++能形成正确重载
	// C++规定:后置++重载时多增加一个int类型的参数,
    //但是调用函数时该参数不会传递,编译器自动传递。
	//注意:后置++是先使用,后+1,因此需要返回+1,之前的旧值,
    //故需在实现时需要先讲this保存一份,
	//然后给this+1
	// 因为temp 是临时对象,因此只能以传值的方式返回,不能返回引用
	Date operator++(int )
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}
private:
	int _year;
	int _month;
	int _day;
};

下面我来写一个日期类-----Date的.h和.cpp分开

// ----Date.h--------
#pragma once
#include 
class Date
{
	friend std::ostream& operator<<(std::ostream& out, const Date& d);
	friend std::istream& operator>>(std::istream& in, Date& d);
public:
	//构造函数
	Date(int year = 1900, int month = 10, int day = 10);
	//拷贝构造
	Date(const Date& d);
	Date& operator=(const Date& d);
	bool operator<(const Date& d);
	bool operator==(const Date& d);
	bool operator<=(const Date& d);
	bool operator>(const Date& d);
	bool operator>=(const Date& d);
	bool operator!=(const Date& d);
	static int GetMonthDay(int year, int month)
	{
		static int arrayDay[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month == 2 && ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
		{
			return 29;
		}
		else
		{
			return arrayDay[month];
		}
	}
	Date& operator+=(int day);
	Date operator+(int day);
	Date& operator-=(int day);
	Date operator-(int day);
	void Print()
	{
		std::cout << *this;
	}
	int operator-(const Date& d);
private:
	int _year;
	int _month;
	int _day;
};
//---------Date.cpp---------
#include "Date.h"
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
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;
}
bool Date::operator<(const Date& d)
{
	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;
	}
	return false;
}
bool Date::operator==(const Date& d)
{
	return _year == d._year && _month == d._month && _day == d._day;
}
bool Date::operator<=(const Date& d)
{
	return (*this < d) || (*this == d);
}
bool Date::operator>(const Date& d)
{
	return !(*this <= d);
}
bool Date::operator>=(const Date& d)
{
	return (*this > d) || (*this == d);
}
bool Date::operator!=(const Date& d)
{
	return !(*this == d);
}
Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		++_month;
		if (_month == 13)
		{
			_month = 1;
			_year += 1;
		}
		_day -= GetMonthDay(_year, _month);
	}
	return *this;
}
Date Date::operator+(int day)
{
	Date tmp(*this);//拷贝构造
	tmp += day;
	return tmp;
}
Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day < 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}
Date Date::operator-(int day)
{
	Date tmp(*this);
	tmp -= day;
	return tmp;
}
int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (max < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int n = 0;
	while (min != max)
	{
		++n;
		min += 1;
	}
	return flag * n;
}
std::ostream& operator<<(std::ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << std::endl;
	return out;
}
std::istream& operator>>(std::istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

对于上述日期类代码,有许多小细节需要注意,譬如:关于下列是采用第一种方案还是第二种方案呢?明显是第一种的拷贝构造少,故效率肯定比第二种效率要高。

14.const成员

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

请思考下面几个问题:

1.const对象可以调用非const成员函数吗?----不可以

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

3.const成员函数内可以调用其他的非const成员函数吗?---不可以

4.非const成员函数内可以调用其他的const成员函数吗?---可以

总结:const对象--->只能调用const成员函数。

非const对象--->可以调用const和非const成员函数。

const成员韩式内--->只能调用const成员函数。

非const成员函数内--->可以调用const和非const成员函数。

15.取地址以及取地址操作符重载

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

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

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

16.再谈构造函数

16.1构造函数的赋值

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

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

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

16.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);
}

4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

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();
}

所以上述代码输出的是 1  和 随机值。因为初始化顺序看的是声明顺序,而不是初始化列表中的顺序。

16.3explicit关键字

构造函数不仅可以构造与初始化对象,对于接受单个参数的构造函数,还具有类型转换的作用。接收单个参数的构造函数具体表现:

1.构造函数只有一个参数。

2.构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值。

3.全缺省构造函数。

class A
{
public:
	A(int a)
		:_a(a)
	{
		std::cout << "A(int a)" << std::endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
		std::cout << "A(cosnt A& aa)" << std::endl;
	}
private:
	int _a;
};
int main()
{
	A aa1(1);
	A aa2 = 2; //隐式类型转换,整型转换成自定义类型
	//2 构造一个A的临时对象,临时对象再拷贝构造aa2
	return 0;
}

用explicit修饰构造函数,将会禁止构造函数的隐式转换。

17.Static成员

17.1概念

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

【面试题】

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

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

17.2 特性

1.静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区

2.静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

3.类静态成员即可用 类名::静态成员 或者对象.静态成员 的方式来访问。

4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员。

5.静态成员也是类的成员,受public、protected、private访问限定符的限制。

【问题】

1.静态成员函数可以调用非静态成员函数吗?---不可以,因为静态成员函数没有this指针。

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

【面试题】

设计一个类,在类外面只能在栈上创建对象

设计一个类,在类外面只能在堆上创建对象

class A
{
public:
	static A GetStackObj()
	{
		A aa;
		return aa;
	}
	static A* GetHeapObj()
	{
		return new A;
	}
private:
	A(){}
private:
	int _a1 = 1;
	int _a2 = 2;
};
int main()
{
	A::GetStackObj();
	A::GetHeapObj();
	return 0;
}

18.友元

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

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

18.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)
	{
	}
	// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
	// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, 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& _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;
}

说明:

1)友元函数可访问类的私有和保护成员,但不是类的成员函数

2)友元函数不能用const修饰

3)友元函数可以在类定义的任何地方声明,不受类访问限定符限制

4)一个函数可以时多个类的友元函数。

5)友元函数的调用与普通函数的调用原理相同。

18.2友元类

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

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

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

2.友元关系不能传递

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

3.友元关系不能继承

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;
};

19.内部类

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

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

特性:

1.内部类可以定义在外部类的public、protected、private都是可以的。

2.注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名

3.sizeof(外部类)=外部类,和内部类没有任何关系。

class A
{
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
	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;
}

20. 再次理解类和对象

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的 实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以 认识。

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

posted on 2025-12-26 11:13  ljbguanli  阅读(46)  评论(0)    收藏  举报