C++——类与对象(三)

目录

引言

类型转换

1.类型转换

2.explicit 关键字

static 成员

1.static 静态成员变量

2.static静态成员函数

3.访问静态成员变量的方法

3.1 静态成员变量为公有

3.2 静态成员变量为私有

友元

1.友元函数

2.友元类

内部类

匿名对象

对象拷贝时的编译器优化

1.类型转换

2.传值传参

3.传值返回

结束语


引言

前面两篇:

C++——类与对象(一)

C++——类与对象(二)

本篇是类与对象的第三篇,也是最后一篇。

求点赞收藏评论关注!!!

类型转换

1.类型转换

我们在学习C语言的时候知道,不同的数据类型在赋值的时候会发生类型转换。C++也是如此。

来看个简单的例子,如下所示:

#include<iostream>
using namespace std;

int main()
{
	int a = 3.14;
	cout << a << endl;
	return 0;
}

输出结果如下:

由于 3.14 是一个浮点数,而变量 a 是int类型的,所以在赋值给 a 之前,中间生成一个临时对象,将右操作数强制类型转换为左操作数的类型,再将临时对象的值赋值给 a,3.14 会被隐式转换为 int类型。

再来看这段代码:

class A
{
public:
	A(int a1)
		: _a1(a1)
	{
		// ...
	}

	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 aa1(10);
	aa1.Print();

	A aa2 = 100;
	aa2.Print();

	return 0;
}

A aa1(5);

通过调用接受单个 int 参数的构造函数被正确创建,_a1 被初始化为 10,而 _a2 保持其默认值 2。

A aa2 = 100;

使用了 A 类的接受一个 int 参数的构造函数来创建一个 A 类型的对象,并通过拷贝构造函数将这个对象的值传递给 aa2。而整个过程中,10 作为一个整型值被用来作为构造函数的参数,从而实现了从整型到 A 类型的转换。

编译器会对上面的过程进行优化,优化为直接构造。

再来看看这个例子:

int main()
{
	A aa1(10);
	aa1.Print();

	A& aa2 = 10;

	return 0;
}

能不能这样子使用引用呢?

答案是否定的。

我们需要修改成:

int main()
{
	A aa1(10);
	aa1.Print();

	const A& aa2 = 10;

	return 0;
}

由于类型转换会产生一个临时对象。临时对象是常量的。const 引用可以绑定到这个临时对象上,确保不会修改这个临时对象。这里的 const 引用表明你不会修改这个对象,并且临时对象的生命周期足够长以保证引用在使用期间有效。

C++11后支持多参数的类型转换,如下所示:

int main()
{
	A aa3 = { 1,1 };
	aa3.Print();

	return 0;
}

2.explicit 关键字

在C++中,explicit 关键字用于修饰只有一个参数的构造函数,或者所有参数都有默认值的构造函数,以防止这些构造函数被用于隐式类型转换。当构造函数被声明为 explicit 时,它只能被显式地用于构造对象,而不能在需要该类类型对象的表达式中隐式地通过其他类型(特别是基本数据类型)来构造对象。

来看个简单的例子:

class A
{
public:
	explicit A(int a1)
		: _a1(a1)
	{
		// ...
	}

	explicit 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 a = 10;
	return 0;
}

错误信息:

static 成员

1.static 静态成员变量

(1)静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

举个例子:

class A
{
private:
	static int _a;
	int _x;
	char _y;
};

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

输出结果为:8

这里的计算规则是按照C语言计算结构体大小的规则。并没有把静态成员变量_a考虑进去,这是因为静态成员变量属于整个类,是类的所以对象,所以静态变量成员不计入总大小。

(2)用static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。

像这样:

class A
{
private:
	//声明
	static int _x;
	static int _y;
};

//定义
int A::_x = 0;
int A::_y = 0;

2.static静态成员函数

⽤static修饰的成员函数,称之为静态成员函数。

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

class A
{
public:
	static void Func()
	{
		cout << x << endl;  // 错误,访问了非静态成员,因为无this指针
		cout << _a << endl; // 正确
	}
private:
	//声明
	int x = 0;
	static int _a;
};
//定义
int A::_a = 0;

(2)非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

3.访问静态成员变量的方法

3.1 静态成员变量为公有

当静态成员变量为公有时,有如下三种方法进行访问:

(1)通过对象.静态成员来访问

(2)通过类名::静态成员来行访问

(3)通过匿名对象突破类域进行访问

代码演示如下:

class A
{
public:
	// 声明
	static int _x;
};

// 定义
int A::_x = 0;

int main()
{
	A a;
	cout << a._x << endl;  //通过对象.静态成员来访问
	cout << A::_x << endl; //通过类名::静态成员来行访问
	cout << A()._x << endl;//通过匿名对象突破类域进行访问
	return 0;
}

输出结果:

3.2 静态成员变量为私有

当静态成员变量为私有时,有如下三种方法进行访问:

(1)通过对象.静态成员函数来访问

(2)通过类名::静态成员函数来行访问

(3)通过匿名对象调用成员函数进行访问

代码演示如下:

class A
{
public:
	static int Get_x()
	{
		return _x;
	}
private:
	static int _x;
};

int A::_x = 0;

int main()
{
	A a;
	cout << a.Get_x() << endl; //通过对象.静态成员函数来访问
	cout << A::Get_x() << endl;//通过类名::静态成员函数来行访问
	cout << A().Get_x << endl; //通过匿名对象调用成员函数进行访问
	return 0;
}

友元

友元(Friend)在C++中是一种特殊的关系,它允许一个函数或另一个类的成员函数访问某个类的私有(private)成员和保护(protected)成员。这种机制打破了类的封装性,但在某些情况下,它提供了必要的灵活性和便利性。

1.友元函数

友元函数是定义在类外部,但在类内部通过friend关键字声明的函数。它不是类的成员函数,因此没有this指针,也不能直接访问类的非静态成员变量(除非通过对象或引用作为参数)。然而,友元函数可以访问类的所有私有成员和保护成员。

声明方式如下:

class MyClass 
{  
    // ...  
    friend void friendFunction(const MyClass& obj); // 声明友元函数  
};  
  
void friendFunction(const MyClass& obj) 
{  
    // 可以访问MyClass的私有和保护成员  
}

来看个简单的例子,如下所示:

class Date
{
	friend void Print(const Date& d);//友元函数
public:
	Date(int year = 1, 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;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Print(const Date& d)
{
	cout << d._year << "-" << d._month << "-" << d._day << endl;
}

int main()
{
	Date d(2024, 9, 1);
	Print(d);

	return 0;
}

友元函数有以下几点注意事项:

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

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

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

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

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

2.友元类

友元类是指某个类被另一个类声明为友元。这意味着友元类的所有成员函数都可以访问另一个类的私有成员和保护成员。这种关系是对称的(在声明上),但并不意味着两个类互相拥有对方的全部访问权限(因为友元关系不是传递的)。

声明方式如下:

class MyClass 
{  
    // ...  
    friend class FriendClass; // 声明友元类  
};  
  
class FriendClass 
{  
public:  
    void accessMyClass(MyClass& obj) 
    {  
        // 可以访问MyClass的私有和保护成员  
    }  
};

来看个简单的例子:

// Time 类,表示时间,包含小时、分钟和秒  
class Time
{
	// 声明 Date 类为 Time 类的友元类,
	// 允许 Date 类直接访问 Time 类的私有成员  
	friend class Date;
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{

	}
private:
	int _hour; 
	int _minute;
	int _second; 
};

// Date 类,表示日期,并包含一个 Time 类型的成员变量来存储时间  
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year), _month(month), _day(day)
	{
    
    }

	// 设置日期的时间部分  
	// 由于 Date 是 Time 的友元类,所以可以直接访问 Time 类的私有成员  
	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; 
};

(1)友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

(2)友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。

(3)友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是B的友元。

内部类

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

如下所示:

class A
{
	class B
	{
	private:
		int _x;
	};
private:
	int _a;
	int _b;
};

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

在上述的代码中:B就是A的内部类。

输出结果为:8

我们可以看到计算A的大小并不包括B。

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。

由于内部类受到外部类的限制,因此我们访问内部类B需要指定类域。

同时, B 它也受 A 访问限定符的限制,如果 B 定义在 A 的私有,那么外面就无法访问到 B 了。

内部类默认是外部类的友元

内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考
虑把A类设计为B的内部类,如果放到private / protected位置,那么A类就是B类的专属内部类,其他地方都用不了。

匿名对象

匿名对象与我们学习C语言时学过的匿名结构体类似。

匿名对象只有类名,作用域只在匿名对象声明的一行。

在之前我们实例化对象时不传参是这样子的:

int main()
{
	A aa1;
	
	//不能这样
	//因为分不清是实例化还是函数声明
	A aa2();

	return 0;
}

C++使用匿名对象:

int main()
{
    //传参
	A();
    //不传参
    A(1)
	return 0;
}

这样子实例化的对象称之为匿名对象,之前我们使用的则称之为有名对象。

匿名对象的声明周期只有当前这一行。一般临时定义一个对象当前用一下即可,就可以定义匿名对象。

我们还是使用一个简单的日期类作为例子:

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		cout << "Date" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	// 析构函数
	~Date()
	{
		cout << "~Date()" << endl;
		_year = _month = _day = 0;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date();		// 匿名对象
	Date d;		// 有名对象
	return 0;
}

输出结果为:

对匿名对象进行引用可以延长匿名对象的生命周期

像这样:

int main()
{
	const Date& dc = Date();//匿名对象也具有常性
	Date d;
	return 0;
}

对象拷贝时的编译器优化

现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传参过程中可以省略的拷贝。

如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译还会进行跨行跨表达式的合并优化。

我们来看看这个例子:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		cout << "Date" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
    // 析构函数
	~Date()
	{
		cout << "~Date()" << endl;
		_year = _month = _day = 0;
	}

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

1.类型转换

int main()
{
	Date d = 2024;

	return 0;
}

从语法上来说,上述代码会发生隐式类型转换。

先用 2024 构造一个临时对象 Date 类;再调用拷贝构造,将临时对象赋值给 d

运行结果:

但是当我们运行程序之后会发现实际运行结果只有构造,并没有调用拷贝构造。

因为编译器认为构造一个临时对象,马上进行拷贝,中间的临时对象什么都没做,效率太低了。

2.传值传参

void fun(Date d)
{
	// ...
}

int main()
{
	Date d(2024);
	fun(d);

	return 0;
}

对于自定义类型,传值传参要先调用拷贝构造,那上述代码有发生优化吗?

输出结果为:

我们发现并没有对其进行优化。

这是因为:构造与拷贝构造并没有发生在一个连续的步骤中。

但是当我们使用匿名对象时:

这个时候编译器就会进行优化了。

3.传值返回

从语法上来说,传值返回会先将返回值拷贝到一个临时对象中,再将临时对象拷贝给函数外接收的变量。

Date fun()
{
	Date d(1);
	return d;
}
int main()
{
	fun().Print();

	return 0;
}

在不同的编译器下,输出的结果是不同的:以vs2019与vs2022为例,vs2022的优化更为激进。

在vs2022中,拷贝构造函数由于编译器的优化而未被执行。

不过嘛,毕竟不同的编译器在优化可能都会有些差异,还是得具体情况具体分析。

结束语

C++类与对象的第三篇。

写的有点慢。。。

总之,还是感谢各位大佬的支持!!!

求点赞收藏评论关注!!!

猜你喜欢

转载自blog.csdn.net/2301_80401288/article/details/142027888