c++基础篇(二)——类与对象入门(下)

作者介绍:

关于作者:东条希尔薇,一名喜欢编程的在校大学生
主攻方向:c++和linux
码云主页点我
本系列仓库直通车
作者CSDN主页地址


目录

  • 友元和友元类
    • 问题引入
    • 友元的定义及使用
    • cout<<重载的完善
  • 初始化列表
  • static成员
  • const成员函数和内部类

友元和友元类

问题引入

在前面我们这个日期类中:

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

 void Display()
 {
 cout <<_year<< "-" <<_month << "-"<< _day <<endl;
 }
private:
 int _year;
 int _month;
 int _day;
};

我们想要打印出这个日期,只能调用我们的函数DisPlay来实现

但却不能用cout来实现

cout<<d1;//报错,因为cout不认识自定义类型

我们如果要实现cout输出自定义类型该怎么办呢?我们可以考虑运算符重载

我们在类里面重载一个<<符号,我们可以查阅cplus参考来看看cout的原型

c++基础篇(二)——类与对象入门(下)_第1张图片
cout可以当做一个outstream类型的形参,所以我们的函数可以这样设计

void operator<<(ostream&out)//注意:这个函数是在类里面定义的
{
	cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}

之后,我们的类就是这个样子

但是,我们这个函数定义完了后,却还是不能使用

c++基础篇(二)——类与对象入门(下)_第2张图片
为什么呢?

还记得我们之前的this指针吗?

我们之前说过,只要在类里面定义的函数,函数的形参都会自动的带一个指向对象本身的this指针

但是,这个指针,是会默认占据形参的第一个位置的

而我们的运算符重载中,只要是双目操作符,左操作数就是参数列表的第一个参数,右操作数就是参数列表的第二个参数

我们可以验证一下

class Test
{
public:
	Test(int a = 0, int b = 0)
	{
		_a = a;
		_b = b;
	}

	bool operator>(const Test& t2)//这里的this实际上就是第一个参数
	{
		return this->_a > t2._a;
	}
private:
	int _a;
	int _b;
};

int main()
{
	Test t1(2, 3);
	Test t2(3, 4);
	int res = t1 > t2;
	cout << res << endl;
	return 0;
}

如果是这样,那么结果应该就为0(false)

在这里插入图片描述

我们验证了操作数顺序的问题,所以,我们应该这么使用这个重载符号

	Date d1(2022, 1, 20);
	d1 << cout;

程序能正常运行了

在这里插入图片描述
但是,这么使用函数不符合我们的使用习惯,所以我们怎么才能让这个函数不带this呢?

当然,我们自然能想到,把它定义在类外就行了嘛!这样我们的函数就不会带this了,这种想法没错,但是定义在类外我们就不能访问我们封装好的元素了,所以我们就要想的办法怎么能让类外的某些函数访问某个类的私有元素

友元就能有效解决这个问题

友元的定义及使用

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

我们上面问题的引入其实就涉及了友元函数的知识

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

友元虽然能访问类的私有成员,但是仍然属于全局函数,不属于成员函数

定义方式:

我们可以在类域内的任意位置声明友元函数,需要在函数最前面加上friend关键字

class Date
{
	friend void operator<<(outstream&out,const Date&d1);
	//...
}

然后在全局位置定义函数,友元不需要在函数名前加上::类域符

这样我们就能以正常习惯使用我们的cout函数了

而友元类与同理,友元类中的成员函数都可以访问另一个类的私有成员

class A
{
	friend classB;//B可以访问A的私有成员
	//..
}

注意友元是单向的,如果声明了B是A的友元,那么B可以访问A的私有成员,但A却不能访问B的私有成员

cout<<重载的完善

我们上面的函数虽然是完成了任务,但是遇到这样的情况却还是不能解决

cout<<d1<<d2;

操作符是从左到右进行的,左边的操作符返回了一个void类型,这样第二个<<就匹配不上类型了

所以我们还需要让函数返回cout才能实现任务

//最终的cout<<重载
outsream& operator<<(ostream&out,const Date& d)
{
	cout<<d._year<<"-"<<d._month<<"-"<<d._day<<endl;
	return out;
}

初始化列表

在构造函数的末尾打上冒号,然后是以成员变量(参数)为格式,以逗号分开的列表

class Date
{
public:
 Date(int year=1991, int month=1, int day=1)
:_year(year),_month(month),_day(day)
 {
	//注意,这里的花括号不能省去
 }
private:
 int _year;
 int _month;
 int _day;
};

这样就能完成对象的初始化

那么,这种方式和我们之前的构造函数有什么区别呢?

我们先了解一下声明,初始化,和赋值

下面这种,只声明这个变量的存在,而不赋值的操作,叫声明

int a;//这是声明,如后续不赋值,a将是随机值

用=操作,叫做赋值

a=10;//赋值操作

而我们这种把声明和赋值结合起来的操作,叫初始化

int a=10;//初始化

延伸在类中,我们的成员变量,其实是一种声明

而我们在本节前面的定义方式其实是一种赋值操作

虽然看着没有什么区别,但是赋值和初始化的效率是不一样的,因为我们知道在赋值操作中,类会多进行一次赋值的拷贝构造,而在初始化仅需进行一次拷贝构造

我们可以通过这个程序来验证一下

class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}

	void operator=(A& aa)
	{
		cout << "=" << endl;
	}
};

class Test
{
public:
	//Test(A& aa)//1
	//{
	//	a = aa;
	//}

	//Test(A& aa):a(aa)/2
	//{
	//	
	//}
private:
	A a;
};

int main()
{
	A a;
	Test t1(a);
	return 0;
}

1是赋值操作,2是初始化操作,分别运行

这是1运行(赋值)
在这里插入图片描述

这是2运行(初始化)

在这里插入图片描述
我们可以观察到,1多调用了一次构造和赋值操作,因为1的声明和赋值是分离的,在赋值那里多了一次构造

所以,初始化能在一定程度上提升我们程序的效率

另外,我们的有些成员变量是必须要初始化的

  1. const成员变量,因为const一旦被声明后,再不能被赋值,所以需要初始化赋值
  2. 引用,因为引用必须要初始化
  3. 没有默认构造函数的自定义类型

正因为以上原因,初始化列表得到了广泛的应用

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

例如:

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

这个函数会先初始化a2,而不是a1,所以a2不能被正常初始化

static成员

在成员的定义前加上static关键字的称作静态成员变量,它具有以下的特性:

一定要在类外初始化,但可以不借助对象或匿名对象访问,可以直接通过访问限定符访问,但是只有公有的静态函数才能在类外直接使用

class A
{
public:
	static int a;
	static void func()
}

int A::a=0;

A::func();

生命周期在整个程序运行期间,并且被此类中的所有对象共用

例如:

class A
{
public:
	A()
	{
		_i++;
		_res += _i;
	}

	int getans()
	{
		return _res;
	}
private:
	static int _i;
	static int _res;

};

int A::_i = 1;
int A::_res = 0;

int main()
{
	A arr[10];
	cout << A().getans() << endl;
	return 0;
}

原理:我们这里包含10个A类,A类会被调用10次构造函数,而因为i和res被所有对象所共用,所以i和res将会保留它的值,不会被销毁,并且所有类都可以访问

在这里插入图片描述
静态函数只能访问静态成员,且不会自带this指针

const成员函数和内部类

比如我们想访问日期有一个display函数,但我们为了防止日期不会被意外修改,我们或许可以这样

int main()
{
	const Date d2(2022,1,17);
	d2.Display();
	return 0;
}

但是程序会报错,因为我们的date是const,而传入this后却没有了const属性,这属于权限的放大

所以,为了防止被意外修改,我们可以这样

void disPlay()const

用const修饰的函数,本质是修饰this它保证了this指向的内容不会被修改

而内部类,顾名思义,就是定义在某一个类里面的类

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

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

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

内部类可以定义在外部类的public、protected、private都是可以的,private同样不能被外界访问到

你可能感兴趣的:(c++基础及其STL,c++,开发语言,后端)