您现在的位置是:首页 >技术杂谈 >C++好难(4):类和对象(下)网站首页技术杂谈

C++好难(4):类和对象(下)

小羊在摸鱼 2024-06-14 17:17:00
简介C++好难(4):类和对象(下)

okk我们终于来到了C++类和对象的最后一节,大多都是对之前学习的内容做的补充

所以加油继续冲啦!

       ∧_∧::
   (´・ω・`)::
  /⌒  ⌒)::
 /へ__  / /::
(_\\  ミ)/::
  | `-イ::
  /  y  )::
 //  /::
/ /::
( く:::
|\ ヽ:::


【本节目标】

  1. 再谈构造函数
  2. Static成员
  3. 友元
  4. 内部类
  5. 匿名对象

目录

【本节目标】

1.再谈构造函数

1.1构造函数体赋值

1.2 初始化列表

初始化列表的特性

2. static 成员

2.1概念

2.2特性

3.友元

3.1友元函数

友元函数的特点:

3.2 友元类

4.内部类

4.1 概念

5.匿名对象


1.再谈构造函数

1.1构造函数体赋值

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

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

这里需要注意:

上述代码再构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对象成员变量的初始化,

构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

理由如下代码:

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year; // 第一次赋值
		_year = 2022; // 第二次赋值
		//...还可以赋值很多次

		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

这段代码中,构造函数对_year的多次复制,其编译过程是被允许的。

那么既然构造函数里面进行的是赋值的话,什么时候才是成员变量的初始化?

这就要引出我们真正的初始方式:初始化列表

1.2 初始化列表

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

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

int main()
{
	Date d1;// 类对象的整体初始化
//(初始化了类对象,但没有初始化成员变量),成员变量放到了初始化列表里面解决

	return 0;
}

初始化列表是对成员函数的初始化,他直接跟在析构函数参数括号的后面

初始化列表的特性

特性一:
解决一些必须在声明时就要初始化的成员变量

(1)const修饰的成员变量

class Date
{
private:
	const int _year; // const修饰的成员变量只能在定义时初始化
};

const 修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。

(2)引用成员变量

class Date
{
private:
	int& _year; // 引用成员变量只能在定义时初始化
};

引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。

(3)自定义类型成员(该类没有默认构造函数)

class A 
{
public:
	A(int val) //注:这个不叫默认构造函数(因为需要传参调用)
	{
		_val = val;
	}
private:
	int _val;
};

class B
{
public:
	B()
		:_a(2023) //所以必须使用初始化列表对其进行初始化
	{}
private:
	A _a; //自定义类型成员(该类没有默认构造函数)⬆
};

在初始化列表:_a(2023)也就是对A类的构造函数进行的一次调用

肯定有人会有这样的疑惑:   为什么不直接在类中声明时就给其初始化?

像这样:,但这样的写法,放在现在是可以的,但在C11发布之前是不行的

class A
{
public:
	A()
	{}

private:
	int a;
	const int a1 = 1;
	int& b = a;
};

int main()
{
	A d1;

	return 0;
}

要注意:

在类里面声明时给值,不叫做初始化,而是缺省参数,在C11发布之前不允许在类里面直接定义,所以才会有初始化列表的应用

特性二:
每个成员变量在初始化列表中,只能出现一次,(只能初始化一次)

因为初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现

特性三:
在你不写初始化列表时,系统自动生成的初始化列表,对内置类型不处理,对自定义类型也是去调用它的初始化类别,

和构造函数、析构函数的特点很像

因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

所以,在我们写构造函数的时候,尽量使用初始化列表进行初始化,能提高效率

如下:

// Date类
class Date
{
public:
	Date(int day = 0)
	{
		_day = day;
	}
private:
	int _day;
};

// Test类
class Test
{
public:
	Test(int day)
		:_d(12)//调用Date的构造函数
	{}
	//这样用初始化列表,只用调用一次Date类的构造函数


	Test(int day)
	{ 
		Date t(day);
		_d = t;
	}
	//这样写的话,会调用两次Date类的构造函数
	//因为系统在自动生成初始化列表时就会调用一次,之后在{ }里面又会调用一次

private:
	Date _d;
};

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

int i = 0;

class Test
{
public:
	Test()
		:_b(i++)
		, _a(i++)
	{}
	void Print()
	{
		cout << "_a:" << _a << endl;
		cout << "_b:" << _b << endl;
	}
private:
	int _a;
	int _b;
};

int main()
{
	Test test;
	test.Print();
	return 0;
}

结果:

可以看到代码中,在初始化列表里面,我们先初始化的_b,然后是_a,但是结果却不按这个顺序走,因为初始化顺序是按照声明时的顺序走的

2. static 成员

2.1概念

声明为static的类成员称为类的静态成员

static修饰的成员变量,称之为静态成员变量
static修饰的成员函数,称之为静态成员函数

静态成员变量一定要在类外进行初始化

2.2特性

特性一

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

先看下面的代码

我们发现 Test2 的类里面比 Test1 类多定义了一个静态变量 _aaa 但是其空间并没有增加

因为静态成员 _aaa 是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和。

特性二

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

class Date
{
private:
    //静态成员变量的声明
	static int _year;
	static int _month;
	static int _day;
};

// 静态成员变量的定义初始化
int Date::_year = 2023;
int Date::_month = 5;
int Date::_day = 8;

特性三

(3)类静态成员即可用 类名::静态成员函数 或者 对象.静态成员函数 来访问

class Date
{
public:
	static void Print()
	{
		cout << _year << "-" << _month << endl;
	}
private:
	static int _year;
	static int _month;
};

int Date::_year = 2022;
int Date::_month = 10;

int main()
{
	Date d1;

	d1.Print(); // 对象.静态成员函数

	Date::Print(); // 类名::静态成员函数
	return 0;
}

应为static定义的成员函数是所有类对象所共享的,所以它既可以用类域::来定位,也可以用对象.来定位

特性四

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

class Date
{
public:
	static void Print()
	{
		cout << _year << endl; //静态的成员可以访问
		cout << _month << endl; //不能访问非静态成员
	}
private:
	static int _year;
	int _month;
};

int Date::_year = 2022;

int main()
{
	Date d1;

	d1.Print(); // 对象.静态成员

	return 0;
}

 含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。

特性五

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

在静态成员变量设为private时,经过我们通过类域进行访问,也是不行的

特性六

 (6)访问静态成员变量的方法:

class Date
{
public:
	static int GetMonth()
	{
		return _month;
	}
	static int _year;
private:
	static int _month;
};

//静态成员变量的定义初始化
int Date::_year = 2023;
int Date::_month = 5;


int main()
{
	Date d1;

	//对公共属性的静态成员进行访问
	cout << d1._year << endl; // 1.通过类对象突破类域进行访问
	cout << Date()._year << endl; // 2.通过匿名对象突破类域进行访问
	cout << Date::_year << endl; // 3.通过类名突破类域进行访问

	//对私有属性的静态成员进行访问
	cout << d1.GetMonth() << endl; // 1.通过类对象突破类域进行访问
	cout << Date().GetMonth() << endl; // 2.通过匿名对象突破类域进行访问
	cout << Date::GetMonth() << endl; // 3.通过类名突破类域进行访问

	return 0;
}

3.友元

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

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

3.1友元函数

问题:现在尝试去重载operator<<,然后会发现没办法将operator<<重载成成员函数。

如下展示:

#include <iostream>
using std::cin;
using std::cout;
using std::endl;

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

    std::ostream& operator<<(std::ostream& out)
    {
        out << _year << "-" << _month << "-" << _day;
        return out;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d(2017, 12, 24);
    cout << d;

    return 0;
}

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

用友元来解决如下:

class Date
{
	//友元函数
	// 标准流输出 --> printf
	friend std::ostream& operator<<(std::ostream& out, const Date& d); 
	// 标准流插入 --> scanf
	friend std::istream& operator>>(std::istream& in, Date& d); 
public:
	Date(int year = 2023, int month = 5, int day = 8)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

// <<运算符重载---输入
ostream& operator<<(std::ostream& out, const Date& d) 
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
	return out;
}

// >>运算符重载---输出
istream& operator>>(std::istream& in, Date& d) 
{
	in >> d._year >> d._month >> d._day;
	return in;
}

int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

注意:其中 cout ostream 类的一个全局对象cin istream 类的一个全局变量
<< 和 >> 运算符的重载函数具有返回值是为了实现连续的输入和输出操作。

友元函数的特点:

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

3.2 友元类

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

class Time
{
	friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
	Time(int hour = 12, 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 = 2023, 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;
};
  • 友元关系是单向的,不具有交换性。

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

  • 友元关系不能传递

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

  • 友元关系不能继承

4.内部类

4.1 概念

如果一个类定义在另一个类的内部,这个内部类就叫做内部类

如下:

class A
{
public:
	// 内部类
	class B // B天生就是A的友元,在B里面可以直接访问A的私有成员
	{
	public:
		void Print(const A& a)
		{
			cout << y << endl; // 可以直接访问静态成员
			cout << a.h << endl; // 也可以访问普通成员
		}
	private:
		int _b;
	};

private:
	static int y;
	int h;
};

int A::y = 1;

int main()
{
	A a; // 定义a对象

	A::B bb; // 定义bb对象

	bb.Print(a); // 把a对象传给bb对象,打印
	return 0;
}

上述代码中,我们在A类里面定义了一个B类,B就叫做A的内部类

B天生就是A的友元,B可以访问A的私有,但不能访问B的私有

注意:

  • 1. 内部类可以定义在外部类的public、protected、private都是可以的。
  • 2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  • 3. sizeof(外部类)=外部类,和内部类没有任何关系。

5.匿名对象

对于一个类 class  A{ };

通过前面的学习,我们知道,在定义类对象的时候不能这样写:A  aa1();

但是我们可以写成这样: A();

匿名对象的特点:

(1)匿名对象的声明周期只有写匿名周期的那一行

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}

private:
	int _a;
};

int main()
{
    A aa1;    

	A();

    return;
}

通过调试我们可以看到,匿名对象在离开他那一行的时候,他的生命周期就结束了

这样写的方式我们就称为匿名对象,那么匿名对象有什么用呢?

看看下面这段代码:

class Solution {
public:
	int Sum_Solution(int n) 
	{
		//...
		return n;
	}
};
int main()
{
	Solution aaa;
	cout << aaa.Sum_Solution(10) << endl;//一般调用

	cout << Solution().Sum_Solution(10) << endl;//匿名对象调用

	return 0;
}

如果我们要用一个类里面的某一个成员函数,我们可能需要真没为其定义一个实例对象 aaa 出来,然后再去调用该类里面的成员函数

比较麻烦,

所以我们可以直接用过匿名对象来进行解决,匿名对象再其调用完成后会直接销毁,不会占用资源

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。