C++ —— 类和对象

目录

介绍类和对象

[一. 类和对象------类的定义](#一. 类和对象——类的定义)

1.访问限定符

2.类域

作用操作符::

3.对象大小

类的实例化

内存对齐规则

4.this指针

this指针会出现的问题

5.C语言结构体与C++类对比

封装的本质

C++类的优点

[二 .类和对象------关于成员](#二 .类和对象——关于成员)

1.类的默认成员函数

I.构造函数

构造函数------初始化列表

初始化列表注意事项

II.析构函数

III.拷贝构造函数

IV.赋值运算符重载

V.取地址运算符重载(普通对象)

VI.取地址运算符重载(const对象)

const成员函数

类的默认成员函数总结

2.static成员

3.友元和内部类

友元函数

友元类和内部类

4.匿名对象

5.类型转换与对象拷贝时的编译器的优化


介绍类和对象

类是对象一种抽象的描述,是定义同一类所有对象的变量和方法的蓝图或模型,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它,因为这些成员变量只是声明,只有在实例化出对象的时候,才会分配空间。

所以,用类类型在物理内存中创建对象的过程,就是类实例化出对象。(一个类可以实例化出多个对象,实例化出的对象,会占用实际的物理空间,用来存储类成员变量 )

**类和对象的关系是类是对象的抽象,而对象是类的具体实例。**‌

一. 类和对象------类的定义

C语言中,我们定义一个结构体是用的struct

cpp 复制代码
//定义一个日期类型
struct Date
{
    int year;
    int month;
    int day;
};

C++是兼容C语言的,也可以用struct定义,同时struct升级成了类,C++中还有一个定义类的关键字是class,如下所示:

cpp 复制代码
class Date
{

//类的成员:
public:

//成员函数
//后面会补充构造和析构函数
	Date()        //默认构造(构造函数)
	{}
	Date(const Date& d)   //拷贝构造(构造函数)
	{}
	~Date()    //析构函数
	{}


private:
//成员变量(或叫类的属性)
	int _year;
	int _month;
	int _day;
};   //这个分号不能省略

在{}中的为类的主体,与struct一样不能省去结束时的分号,类里面的内容是类的成员 ,类中的函数叫做类的成员函数(类的方法) (C++中struct里也可也定义函数),定义在类中的函数默认是内联inline ,类中的变量可以称为成员变量(类的属性)。

1.访问限定符

从上面两个代码相比看出,C++定义类多出了两个关键字:public和private,这两个是访问限定符, 访问限定符有三个:public(公有:修饰的成员在类外可以直接被访问)、private(私有)、protected(保护),private和protected修饰的成员都不能直接在类外被访问。

访问权限作用域:从该访问限定符的出现的位置开始直到下一个访问限定符出现为止,若后面没有访问限定符,就到 } 结束(即类结束)

class和struct都可以定义类,但是它们之间也是有区别的:struct定义的成员默认都是public,而class默认的都是private,一般用class定义类较多。

2.类域

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

作用操作符::

用来指定访问某一空间(或作用域)中的成员的

使用方式如下:

cpp 复制代码
//命名一个叫FFDUST的空间
namespace FFDUST
{
	//可以在这个里定义变量/函数/类型
	int a = 1;
	void func()
	{
		cout << "func()" << endl;
	}
	struct Date
	{
		int year;
		int month;
		int day;
	};
}

int main()
{
	//作用域限定符,就可以访问指定在某一空间的成员
    //指定FFDUST命名空间里的变量a
	cout << FFDUST::a << endl;
    //指定FFDUST命名空间里的func函数
	FFDUST::func();
    //用指定FFDUST命名空间里的Date类型定义一个变量a
	FFDUST::Date a;
	cout << sizeof(a) << endl;

	return 0;
}

还可以用于命名空间嵌套时候

cpp 复制代码
//命名一个叫FFDUST的空间
namespace FFDUST
{
	//命名一个叫FF的空间
	namespace FF
	{
		int FF = 10;

		int Add(int x, int y)
		{
			return x + y;
		}
	}
	//命名一个叫DUST的空间
	namespace DUST
	{
		int DUST = 1;
		int Add(int x, int y)
		{
			return (x + y) * 100;
		}
	}
}

int main()
{
	cout << FFDUST::FF::FF << endl;
	cout << FFDUST::DUST::DUST << endl;

	cout << FFDUST::FF::Add(1, 1) << endl;
	cout << FFDUST::DUST::Add(1, 1) << endl;

	return 0;
}

类域影响的是编译的查找规则,若定义了一个类,类的成员的声明和定义分离(如成员函数),就需要指定类域, 否则编译器会认为全局的或者当前某个命名空间的,编译时就导致找不到成员的声明/定义在哪

3.对象大小

类的实例化

用类类型在物理内存中创建对象的过程,称为类实例化出对象。一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间来存储类成员变量。

只有用类实例化出对象时,才会分配空间(没有实例化时,这些成员变量只是声明,不会分配空间)。

如何实例化,如下测试:

cpp 复制代码
//简单定义一个日期类测试
class DateTest
{
public:
	//默认构造
	Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "Date()" << endl;
	}
	//拷贝构造
	Date(const Date& d)	
		:_year(d._year)
		, _month(d._month)
		, _day(d._month)
	{

		cout << "Date(const Date& d)" << endl;
	}
	//析构
	~Date()
	{
		cout << "~Date()" << endl;
	}

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

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

int main()
{
    //这里就是实例化出了d1,d2,d3
	DateTest d1;
	DateTest d2(2024, 10, 12);
	DateTest d3(d1);

	d1.Print();
	d2.Print();
	d3.Print();

    cout << sizeof(d1) << endl << sizeof(d2) << endl << sizeof(d3) << endl;
	return 0;
}

我们看一下输出结果

实例化之后就可以来分析对象的大小了,C++中也有内存对齐规则与C语言结构体一致

内存对齐规则

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

拿上述测试程序来解释:

不过C++类中有特殊情况:

空类和只有成员函数的类的大小该是什么呢?

cpp 复制代码
class A
{
public:
	void Print()
	{
		//...
	}
};
class B
{};
int main()
{
	A a;
	B b;
	cout << sizeof(a) << endl;
	cout << sizeof(b) << endl;
	return 0;
}

它们大小结果是1,这里给1个字节是为了占位标识对象的存在,若一个字节都不给,无法表示对象存在过。

4.this指针

this指针就是当前类类型的指针,编译器编译后,类的成员函数默认都会放在形参第一个位置,它的作用就是类的成员函数中访问成员变量的(本质都是通过this指针访问的)。

注意:C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但可以在函数体内显示使用this指针。

cpp 复制代码
class Date
{
public:
	//默认构造
	Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	//拷贝构造
	Date(const Date& d)	
    //本质是	Date(Date* const this , const Date& d) 但this这里不能显示写	
	{
        //这里可以显示写this
        this->_year = year;     //_year=year;
        this->_month = month;   //_month = month;
        this->_day = day;       //_day = day;
	
	}
	~Date()
	{}

	void Print()    //	void Print(Date* const this)
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

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

注:这里const在*之后修饰的是指针本身,若const在*之前就相当于const (*this) 修饰的是指针指向的内容。

this指针会出现的问题

  • this指针的类型:类类型* const,所以不能给this赋值,并且只能在该类的成员函数的内部使用。
  • this指针本质上是成员函数的形参,当对象调用成员函数时,对象将地址作为实参传递给this形参,所以对象中不存储this指针,(形参存在栈帧里)所以this指针默认存在栈,而不是静态区。
  • this指针一般由编译器通过ecx寄存器自动传递,不需要用户传递。

就比如下代码,这种情况不会报错

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	void Print()
	{
		cout << "A::Print()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

并且可以正常运行:

调试监视窗口来看,这个p就是个空

但这里p->调用本质上不是解引用操作,是通过底层汇编上跳转至那个函数

但下面这种情况就是对空解引用会崩溃

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	void Print()
	{
		cout << _a << endl;  //这里就相当于this->_a,_a是存到对象里的,这就会导致对空指针解引用
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

5.C语言结构体与C++类对比

封装的本质

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

封装是一种更严格规范的管理,避免出现乱访问修改的问题,也让用户更方便使用类。

C++的类就是封装的一种体现,C++的类中不仅有数据还有成员函数,通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

C++类的优点

C++语法相对C语言的方便,比如给函数参数缺省值也可以完成初始化,并且成员函数的参数中隐含this指针,就不需要每次传对象的地址,还可以直接使用类名当类型,不需要typedef。

cpp 复制代码
class A
{
private:
	int _a;
};

struct B
{
	int b;
};
typedef struct B B;

int main()
{
	A a;  //C++类名可以直接当类型

	struct B b1;  //C语言需要这样写或者用typedef
	B b2;
	return 0;
}

二 .类和对象------关于成员

1.类的默认成员函数

类的默认成员函数就是用户没有显示实现的,编译器会自动生成的成员函数。

编译器会默认生成6个默认成员函数

I.构造函数

构造函数主要任务就是对象实例化时初始化对象,会自动调用。(注意:构造函数虽叫构造,但主要并不是开空间创建对象,我们常用2的局部对象是栈帧创建时,空间就开好了)。

构造函数的特点:

  1. 函数名与类名相同

  2. 无返回值(啥也不要写,甚至也不写void)

  3. 对象实例化时系统会自动调用对应的构造函数

  4. 构造函数可以重载

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

  6. 默认构造函数就是不传实参的构造函数,有:无参构造函数、全缺省参数的构造函数、不写构造时编译器默认生成的构造函数。(注意:三个函数只能存在一个,不能同时存在,无参构造函数和全缺省参数构造函数可以构成函数重载,但是当不传参数的时候,二者会存在歧义)

  7. 不显示实现,编译器默认生成的构造,对内置类型成员变量的初始化是没有要求的(也就是说,取决于编译器,有些编译器会给它们初始化),对于自定义类型成员变量,要求调用这个成员变量默认构造函数初始化(比如用两个stack实现一个queue,前提是stack要有默认构造,queue就可以不用写,默认调用它的成员变量stack里的),如果这个成员变量没有默认构造,就会报错。

  8. 可以用初始化列表初始化成员变量。

    cpp 复制代码
    class Date
    {
    public:
    
    	//默认构造 -- 无参构造函数
    	Date()
    	{
    		_year = 1900;
    		_month = 1;
    		_day = 1;
    	}
    	//带参构造 --- 用于传参的
    	Date(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	//注意:全缺省构造函数与无参构造函数不能同时存在,这里写一起是为了展示它们
    	//默认构造 -- 全缺省构造函数
    	Date(int year = 1900, int month = 1, int day = 1)
    		:_year(year)                      //初始化列表写法
    		, _month(month)
    		, _day(day)
    	{}
    	//默认构造 -- 全缺省构造函数
    	Date(int year = 1900, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };

    注意:构造函数是一定要写的,因为我们要定义这个类的初始化方式。

构造函数------初始化列表

我们上面介绍构造函数初始化还可以用初始化列表,它的使用方式以:开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。

初始化列表是每个成员变量定义初始化的地方 ,不在初始化列表初始化的成员也会走初始化列表,所以尽量使用初始化列表初始化,每个成员变量在初始化列表中只能出现一次。(引用成员变量、const成员变量和没有默认构造的类类型变量,是必须要在初始化列表位置进行初始化,否则会报错)。

也可以写成下面形式:

cpp 复制代码
class Date
{
public:

	//默认构造
	Date()
	{}
	//带参构造 --- 用于传参的
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
    //也可以在这里给缺省值,成员变量声明位置给缺省值
    //这里也是给初始化列表初始化成员用的
    //函数参数的缺省值,给显示传不传参用的
	int _year = 1900;
	int _month = 1;
	int _day = 1;
};

int main()
{
	Date d1;
	d1.Print();

	return 0;
}

注意: 初始化列表中是按照成员变量在类中声明的顺序进行初始化,跟成员在初始化列表出现的先后顺序无关,建议是声明顺序和定义顺序一致。

cpp 复制代码
class test
{
public:
	test(int a)
		:_a1(a)
		,_a2(_a1)
	{}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 1;
	int _a1 = 1;
};

int main()
{
	test a(10);
	a.Print();

	return 0;
}

可以看到输出_a2时是一个随机值,这就是因为_a2比_a1先声明,_a2就先用_a1初始化, 但是这时_a1并没有被10初始化,是一个随机值,就导致了上面的情况。

初始化列表注意事项
  • 无论是否显示写初始化列表,每个构造函数(默认构造函数也有,函数参数缺省值在初始化列表走的)都有初始化列表
  • 无论是否在初始化列表显示初始化,每个成员变量都要走初始化列表初始化

每个成员变量都要走初始化列表的情况总结:

1. 在初始化列表初始化的成员(显示写的)

2. 没有在初始化列表的成员(不显示写)

a. 声明的地方有缺省值就用缺省值

b. 没有缺省值

x:内置类型,不确定是否初始化,取决于编译器

y:自定义类型,调用默认构造,没有默认构造就报错

3. const、引用、没有默认构造的自定义类型,必须在初始化列表初始化

II.析构函数

与构造函数相反,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。

注意:析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要管。

析构函数的特点:

  1. 析构函数名是在类名前加上字符~
  2. 无参数无返回值。(与构造类似)
  3. 一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数
  4. 对象生命周期结束时,会自动调用析构函数
  5. 若不显示写,编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数(与构造类似)
  6. 自定义类型成员无论声明情况都会自动调用析构函数(当显示写析构函数时,对于自定义类型成员也会调用它的析构)
  7. C++规定,一个局部域的多个对象,后定义的先析构(相当于栈--后进先出)
  8. (与构造类似)若类中没有申请资源,析构函数可以不用写,直接使用编译器生成的默认析构函数(我们实现的Date类,成员变量类型是内置类型,并且没有空间资源的申请,就可以不用写)。若默认生成的析构可以用,也不需要显示写析构(如,自定义类类型成员调用它的析构,两个Stack实现一个MyQueue,前提是Stack要有显示实现的析构,因为它有资源的申请,否则会造成资源泄露)
cpp 复制代码
class Date
{
public:
	//默认构造
	Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "Date()" << endl;
	}
	//析构 用于清理释放资源
	~Date()
	{
		cout << "~Date()" << endl;
	}

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

int main()
{
	Date d1;
	return 0;
}

这里可以不用显示写析构, 上面介绍了,我们看一下要显示写析构的情况:

cpp 复制代码
class Stack
{
public:
	//默认构造
	Stack(int n = 4)
	{
		_a = new int[n] {0};   //申请n个连续空间,并都初始化为0
		//new可以不用显示写检测异常,若出现异常自己会抛异常
		_capacity = n;
		_top = 0;
		cout << "Stack(int n = 4)" << endl;
	}
	//析构函数 这里要显示写,因为上面构造时候申请了空间
	~Stack()
	{
		delete[] _a;      //释放_a中n个连续空间
		_a = nullptr;    //C++中nullptr代表空指针
		_top = _capacity = 0;
		cout << "~Stack()" << endl;
	}
private:
	int* _a;
	size_t _capacity;
	size_t _top;
};

int main()
{
	Stack s1;
	return 0;
}

这里是必须要写的,否则会造成资源泄露。

III.拷贝构造函数

拷贝构造函数,是一种特殊的构造函数,通俗来说是想用自己的类型的对象来初始化自己,

拷贝构造函数,它的第一个参数是自身类类型的引用,且任何额外的参数必须都要有默认值(或无额外参数,这个用的最多)

C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

拷贝构造写法:

cpp 复制代码
class Date
{
public:
	//默认构造
	Date()
	{}

	//构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造函数//
    //参数类型必须是自身类类型的引用
	Date(const Date& d)  //const是为了保护形参不被改变
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	//析构函数
	~Date()
	{}

private:
	int _year = 1900;
	int _month = 1;
	int _day = 1;
};

拷贝构造函数特点:

  1. 拷贝构造函数与构造函数构成重载
  2. 拷贝构造第一个参数必须是自身类类型对象的引用,使用传值会报错,会引发无穷递归。拷贝构造也可以多个参数,但第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
  3. 若不显示定义拷贝构造,编译器会自动生成拷贝构造函数(浅拷贝),会对内置类型成员变量完成值拷贝/浅拷贝(一个字节一个字节的拷贝,类似于memcpy,所以日期类这种可以不用写)。对自定义类型成员会调用它的拷贝构造。
  4. 像日期类成员变量都是内置类型且没有指向什么资源的,编译器自动生成的拷贝构造函数就可以完成需要的拷贝,所以可以不用显示实现拷贝构造。若指向了资源(如Stack,虽然都是内置类型,但它的_a成员指向了资源,就需要自己实现深拷贝(对指向的资源也进行拷贝,浅拷贝只会让它们指向同一块资源)。若类内部成员有自定义类型成员,编译器自动生成的拷贝构造就会调用这个自定义类型成员的拷贝构造,也不需要实现(比如两个Stack实现一个queue,Stack前提要有拷贝构造,这样就会自动调用Stack的拷贝构造)
  5. 传值返回引用会产生一个临时对象,调用拷贝构造。传引用返回,返回的是返回对象的别名(引用),没有产生拷贝(不会调用拷贝构造,因为没有产生临时对象,返回的就是它本身)。注意:若返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,这时候使用引用返回时有问题的,相当于野引用(类似野指针)。所以要保证返回对象在当前函数结束还在,来使用引用。

拿栈来举例,对比一下浅拷贝,深拷贝

cpp 复制代码
class Stack
{
public:
	//默认构造
	Stack(int n = 4)
	{
		_a = new int[n] {0};
		_capacity = n;
		_top = 0;
	}
	//拷贝构造
	Stack(const Stack& s)
	{
		_a = new int[s.capcaity()] {0};
		memcpy(_a, s._a, sizeof(int) * s.top());
		_capacity = s.capcaity();
		_top = s.top();
	}
	~Stack()
	{
		delete[] _a;
		_a = nullptr;
		_top = _capacity = 0;
	}

	void Push(int x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			int* tmp = (int*)realloc(_a, newcapacity *sizeof(int));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}

	size_t top()const
	{
		return _top;
	}

	size_t capcaity()const
	{
		return _capacity;
	}

private:
	int* _a;
	size_t _capacity;
	size_t _top;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	Stack s2(s1);
	//拷贝构造也可以写成
	//Stack s2 =s1;
	return 0;
}

可以看到它们两个指向不同空间,但数据相同

若是浅拷贝,可以看出它们指向同一块空间,并且程序会报错。

IV.赋值运算符重载

赋值运算符重载也是一个默认成员函数,用于两个已经存在的对象直接的拷贝赋值(拷贝构造是用一个存在的对象去初始化另一个要创建的对象)

cpp 复制代码
Date d1(2024,10,1);
Date d2(d1);  //拷贝构造

Date d3(2024,10,2);
Date d4(2024,10,1);
d3=d4;    //赋值运算符重载

赋值运算符重载特点与拷贝构造类似

赋值运算符的特点:

  1. 是一个运算符重载,并且要必须重载为成员函数,为了减少拷贝且不想对象被修改,参数建议写成const当前类类型(与拷贝构造一样)
  2. 它有返回值,目的是为了支持连续赋值的场景,返回类型建议写成当前类类型的引用,引用返回可以提高效率(不会产生临时对象,减少拷贝),返回的就是对象的别名(就是它本身,可以直接修改)。
  3. 没有显示实现时,编译器会自动生成一个默认赋值运算符重载,它与拷贝构造类似,对内置成员变量完成浅拷贝,对自定义类型成员变量会调用它的赋值运算符重载。
  4. 与拷贝构造类似,像Date类的成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就够用了,所以可以不显示实现。若像stack类,虽然都是内置类型,但它指向了资源,(情况与拷贝构造类似)就需要实现深拷贝。若类型内部主要是自定义类型成员,编译器自动生成的赋值重载就会自动调用自定义类型成员的赋值运算符重载,也可以不用显示实现(如两个stack实现一个queue)。

结合上面的,就可以把日期类主要用的最多的默认成员函数实现了:

cpp 复制代码
class Date
{
public:
	//构造函数(可以传参构造,不传参就是默认构造)
	Date(int year = 1900, 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;
	}

	//赋值运算符重载 --- d1 = d2
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		//cout << "Date& operator=(const Date& d)" << endl;
		//d1 = d2 --> d1.operator=(d2) 表达式返回的对象应该是d1本身,所以要对this指针解引用。
		// 用引用返回*this就是d1的别名 
		return *this;   //this指针可以在这里显示调用,指向当前成员
	}

	//析构函数内置类型也不用显示写,编译器会自动生成,且Date类没有申请资源
	~Date()
	{}

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

private:
	int _year;
	int _month;
	int _day;


};
//
int main()
{
	Date d1(2024, 10, 14);
	Date d2(1900, 1, 2);
	Date d3(d1);   //拷贝构造
	d1.Print();
	d2.Print();
	d3.Print();
	d1 = d2;      //赋值运算符重载
	cout << endl;
	//d1 = d2 --> d1.operator=(d2) 
	d1.operator=(d2);
	d1.Print();
	d2.Print();
	d3.Print();
	cout << endl;
	//有返回值就可以连续赋值
	d1 = d2 = d3;
	d1.Print();
	d2.Print();
	d3.Print();
	return 0;
}

输出结果:

V.取地址运算符重载(普通对象)

取地址运算符重载普通对象和const对象用的都比较少,一般编译器这两个函数自动生成就够用了,不需要显示实现,除非特殊场景(不想让别人取到当前类对象的正确地址,可以实现一份返回一个假地址。

cpp 复制代码
class Date
{
public:
	//构造函数(可以传参构造,不传参就是默认构造)
	Date(int year = 1900, 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;
	}

	//赋值运算符重载 --- d1 = d2
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		//cout << "Date& operator=(const Date& d)" << endl;
		//d1 = d2 --> d1.operator=(d2) 表达式返回的对象应该是d1本身,所以要对this指针解引用。
		// 用引用返回*this就是d1的别名 
		return *this;   //this指针可以在这里显示调用,指向当前成员
	}

	//析构函数内置类型也不用显示写,编译器会自动生成,且Date类没有申请资源
	~Date()
	{}

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

	Date* operator&()
	{
		//return this;   //this指针指向该对象,这样是返回正确的地址

		//返回空
		//return nullptr;
		//返回一个乱输的地址
		return (Date*)0x1546652; 
		//两者目的都一样
	}
	const Date* operator&()const       //const对象要调用const成员函数且返回const Date*
	{
		//return this;   //this指针指向该对象,这样是返回正确的地址
		//return nullptr;

		return (Date*)0x1546600;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024, 10, 14);
	const Date d2(1900, 1, 2);
	cout << &d1 << endl;
	cout << &d2 << endl;
	return 0;
}

可以看出,实际地址与打印的地址明显不同,这里const成员和普通成员的取地址运算符重载函数也构成了函数重载。

自定义类型用运算符都需要重载,来自定义它的行为,系统不提供(除赋值重载和取地址重载外)

VI.取地址运算符重载(const对象)

const对象取地址运算符重载,与上面类似,就是需要强调的是const成员函数

const成员函数

就是const修饰的成员函数,注意const修饰的成员函数要放到成员函数参数列表之后

const修饰该成员函数隐含的this指针,就拿上面Print成员函数,Print隐含的this指针实质就是

const Date* const this; //第一个const修饰指针指向的内容的

往后不想修改成员变量的建议加const。

(const修饰的对象,是需要调用const修饰的成员函数的,不能调用普通成员函数;普通对象可以调用普通成员函数和const修饰的成员函数)。

类的默认成员函数总结

一般情况下,类的构造是需要自己写的(因为要自己去定义类的初始化方式),拷贝构造、赋值重载、析构是不需要写的,编译器会自动生成调用,若需要深拷贝的情况,这些则都需要自己实现。

如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则不需要。

如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则不需要。

2.static成员

  1. static修饰的成员变量,是静态成员变量,它是必须要在类外初始化。
  2. static修饰的成员函数,是静态成员函数,它没有this指针。
  3. 静态成员也是类的成员,也受访问限定符限制。
  4. 静态成员函数中可以访问其他静态成员,但不能访问非静态成员(非静态成员含this指针);非静态成员函数是可以访问任意静态成员变量和静态成员函数的。
  5. 突破类域就可以访问静态成员,可以通过 类名::静态成员( 静态成员可以通过类名直接访问,而不需要创建类的实例。) 或者 对象.静态成员 来访问静态成员变量 和静态成员函数,不过前提是要在公有的情况下。
cpp 复制代码
class A
{
public:
	A()
	{
		++_count;
	}
	A(const A& t)
	{
		++_count;
	}
	~A()
	{
		--_count;
	}
	static int GetAcount()   //没有默认自带this指针
	{
		return _count;
	}
private:
	static int _count;    //要在类里面声明
    //注意:不能给缺省值,因为它不是存在静态区中的,不会走初始化列表
};

//在类外面初始化
int A::_count = 0;    //声明已经有static修饰了,定义就不需要加了

int func()
{
	static int _n = 0;
	return (++_n);
}

int main()
{
	cout << A::GetAcount() << endl;   //类名::静态成员函数
	A a1, a2;
	A a3(a1);
	cout << A::GetAcount() << endl;
	cout << a1.GetAcount()<< endl;   //对象.静态成员函数
    cout << endl;
    cout << func() << endl;
    cout << func() << endl;
    cout << func() << endl;

    //这里不能直接访问_count,因为它被private修饰了。

	return 0;
}

从输出结果看出,static修饰的成员变量与static修饰局部变量,都可以被保存。

所以,静态成员变量为所有类对象所共有,它不属于某个具体的对象,不存在对象中,而存在静态区中(在内存中只有一份拷贝,被类的所有实例共享)。

静态区:存储全局变量和静态变量的。

静态成员变量不存在对象中,它就不会走构造函数初始化列表,所以静态成员变量不能在声明的位置给缺省值初始化(缺省值是为了构造函数走初始化列表的)。

如下例题:

cpp 复制代码
//设已经有A,B,C,D 4个类的定义,程序中A,B,C,D构造函数调⽤顺序为?
//设已经有A,B,C,D 4个类的定义,程序中A,B,C,D析构函数调⽤顺序为?
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;    
};

class B
{
public:
	B()
	{
		cout << "B()" << endl;
	}
	~B()
	{
		cout << "~B()" << endl;
	}
private:
	int _b;
};

class C
{
public:
	C()
	{
		cout << "C()" << endl;
	}
	~C()
	{
		cout << "~C()" << endl;
	}
private:
	int _c;
};

class D
{
public:
	D()
	{
		cout << "D()" << endl;
	}
	~D()
	{
		cout << "~D()" << endl;
	}
private:
	int _d;
};
C c;    //全局变量在main函数建立前就创建了

int main()
{
	A a;
	B b;
	static D d;  /*静态无论是内置类型或自定义类型,是在第一次走到定义的这个地方时才会初始化,只有全局的静态才会在main函数建立之前初始化。*/
    //d声明周期时全局的,且作用域时局部的,所以会先析构D,再C
	return 0;
    //析构是后进的先析构
}

输出结果:

3.友元和内部类

‌friend是友元的关键字,友元分为两类:友元函数和友元类,用的时候需要在函数声明或者类声明前面加friend,它提供了一种突破类访问限定符封装的方式。

友元函数

外部友元函数可以访问到类的私有和保护成员(需要访问类的私有,会涉及到友元),友元函数是一种声明,不是类的成员函数。 它也可以是多个类的友元函数。

cpp 复制代码
//要注意在这里先声明B类,因为编译器是向上找的
//否则A的友元函数声明时编译器不认识B
class B;


//A和B都有func的友元声明,func是它们俩类的友元函数
class A
{
	friend void func(const A& a, const B& b);
private:
	int _a = 100;
};

class B
{
	friend void func(const A& a, const B& b);
private:
	int _b = 120;
};

void func(const A& a, const B& b)
{
	cout << a._a << endl;     //在类中友元声明func就可以访问它们俩的私有了
	cout << b._b << endl;
}

int main()
{
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}

可以看到,A和B的私有成员变量被访问到了。

友元类和内部类

内部类默认是它外部类的友元类,它具有友元类的特点,内部类本质是一种封装,它是一个类定义到另一个类的内部,但它是一个独立的类 ,外部类定义的对象中不包含内部类,它只受外部类类域限制访问限定符限制的。

友元类的特点:

  • 友元类中的成员函数,都可以是另一个类的友元函数,也都能访问那个类私有和保护成员。
  • 友元类的关系是单向的(A是B的友元,但B不是A的友元)。
  • 友元类的关系不能传递(A是B的友元,B是C的友元,但A不是C的友元)。
cpp 复制代码
//内部类

class A
{
public:
	class B  //B是A的友元类,不是A的成员
	{
	public:
		void BprintA(const A& a)
		{
			cout << a._a1 << endl;
			cout << a._a2 << endl;
		}
	private:
		int _b1 = 120;
		int _b2 = 12;
	};

private:
	int _a1 = 100;
	int _a2 = 10;
};

int main()
{
	cout << sizeof(A) << endl; 
	A aa;

	//这样定义B是不行的,B是在A这个类的类域里
	//B bb;
	//需要指定类域访问,B属于A
	A::B bb;

	bb.BprintA(aa);

	return 0;
}

A类的大小是8字节,根据内存对齐规则,也就相当于A类中两个int类型所占字节大小,B类虽然也有俩int类型,但B不是它的成员,只是它的友元类,受A类的类域限制。下面输出100和10就可以看出,内部类就是友元类,可以访问另一个类的私有和保护。

cpp 复制代码
//声明友元类 

class A
{
	friend class B;    //友元声明 B是A的友元类
private:
	int _a1 = 100;
	int _a2 = 10;
};

class B
{
public:
	void printAB1(const A& a)
	{
		cout << a._a1 << endl;
		cout << _b1 << endl;
	}
	void printAB2(const A& a)
	{
		cout << a._a2 << endl;
		cout << _b2 << endl;
	}
private:
	int _b1 = 120;
	int _b2 = 12;
};

int main()
{
	A aa;
	
	//不需要访问类域找B,因为B只是A的友元
	//并且B不属于A,不受A类域的限制
	B bb;

	bb.printAB1(aa);
	bb.printAB2(aa);

	return 0;
}

输出结果:

4.匿名对象

上面我们用:类型+对象名(实参) 定义出来的叫有名对象 ,匿名对象就是不加对象名,直接就是:类型(实参)

匿名对象生命周期只在当前一行。

cpp 复制代码
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	A(int a)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
	void Print()
	{
		cout << "xxxxxxx" << endl;
	}
private:
	int _a = 100;
};

int main()
{
	//有名对象不传实参是需要这样定义
	A a1;
	//不能这样定义,这样定义就类似于一个函数声明(如:int func() ),编译器会分不清
	//A a1();
	
	//有名对象传实参
	A a2(2);

	//匿名对象不传实参可以这样定义,不用取名字
	A();   //它声明周期只有这一行,下一行它会自动调用析构

	//匿名对象传实参
	A(1);

	//匿名对象的一种用法
	A().Print();    //这样可以更方便
	return 0;
}

5.类型转换与对象拷贝时的编译器的优化

C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。

类类型对象之间也可以隐式类型转换,需要相应的构造函数支持。

(通俗说:C++是支持隐式类型转换是要借助构造函数支持的。

若不想让其支持隐式类型转换,就在构造函数前面加一个关键字explicit 即可。

cpp 复制代码
class A
{
public:
	A(int a)
		:_a1(a)
	{}

	A(int a1, int a2)
		:_a1(a1)
		,_a2(a2)
	{}

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}

private:
	int _a1 = 1;
	int _a2 = 2;
};



int main()
{
    
	A a1(100);   //调用构造函数(传参构造)
	a1.Print();

    //这里编译器是会优化的
    //连续的 构造+拷贝构造 -> 优化为直接构造
	A aa1 = 10;  //这里是一个隐式类型转换
	aa1.Print();

    //C++11之后支持多参数转化
    A aa2 = { 11 , 22 };
	aa2.Print();

	return 0;
}

注意: 临时对象都具有常性,不能对其修改(权限放大问题)。

cpp 复制代码
int i = 0;
float& a = i;   //这里会发生隐式类型转化
                //i会产生一个float类型的临时对象(变量)
                //但这里a是float引用类型,就是这个临时对象的别名
                //这里会报错,就是因为临时对象具有常性
                //不加const修饰会使权限放大,导致报错

//需要这样写
const float& a = i;

用explicit修饰:

cpp 复制代码
explicit A(int a)  //这样构造函数不再支持隐式类型转换
	:_a1(a)
{}

类类型隐式类型转换还需要注意一点:

cpp 复制代码
class A
{
public:
	A(int a)
		:_a1(a)
	{}

	A(int a1, int a2)
		:_a1(a1)
		,_a2(a2)
	{}

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}

	int GetA() const    //这里给B类提供值的成员函数也要用const修饰
	{
		return _a1 + _a2;
	}

private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
public:
	B(const A& a)     //若这里拷贝构造用的const修饰
		:_b(a.GetA())
	{}
	void Print()
	{
		cout << _b << endl;
	}
private:
	int _b;
};

int main()
{
	A aa2 = { 11,22 };
	aa2.Print();

	B b1 = aa2;
	b1.Print();
	return 0;
}

可以看出成功的隐式类型转换了。

编译器为了提高程序效率,会进行一些优化(取决于编译器,并且不能影响正确性)

cpp 复制代码
//这里vs编译器是会优化的
    //连续的 构造+拷贝构造 -> 优化为直接构造
	A aa1 = 10;    //减少这里临时变量的构造和对aa1的拷贝构造
                   //相当于直接让10构造aa1
	aa1.Print();
cpp 复制代码
//定义一个A类用来观测VS优化的情况
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	A(int a1, int a2)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A(int a1, int a2)" << endl;
	}
	A(const A& a)
		:_a1(a._a1)
		,_a2(a._a2)
	{
		cout << "A(const int& a)" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 0;
	int _a2 = 0;
};

这是一个正常的带参构造:

用一个匿名对象拷贝构造一个A类型对象: (可以看出这里被优化成直接构造了)

继续用一个匿名对象连续两次的拷贝构造: (直接成构造了)

这种情况就优化不了了

总结:(vs下)

  • 隐式类型转换,连续的构造+拷贝构造 -> 优化为直接构造
  • 一个表达式中,连续的构造+拷贝构造 -> 优化为一个构造
  • 连续的拷贝构造+拷贝构造 -> 优化成构造
  • 一个表达式中,连续的拷贝构造 + 赋值重载 -> 无法优化

通过对类和对象的深入理解和运用,我们能够更加高效地组织代码、提高代码的可维护性和可扩展性。类和对象对我们以后的应用和学习中都发挥着至关重要的作用。

制作不易,若有不足之处或出问题的地方,请各位大佬批评指正 ,感谢大家的阅读支持!!!

相关推荐
Ajiang282473530411 分钟前
对于C++中stack和queue的认识以及priority_queue的模拟实现
开发语言·c++
幽兰的天空16 分钟前
Python 中的模式匹配:深入了解 match 语句
开发语言·python
Theodore_10223 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
‘’林花谢了春红‘’5 小时前
C++ list (链表)容器
c++·链表·list
----云烟----5 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024065 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic6 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it6 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康6 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神6 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式