首页 > 编程语言 >C++——类与对象(三)

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

时间:2024-09-14 13:24:52浏览次数:3  
标签:友元 对象 成员 C++ int Date 静态

目录

引言

类型转换

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++类与对象的第三篇。

写的有点慢。。。

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

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

标签:友元,对象,成员,C++,int,Date,静态
From: https://blog.csdn.net/2301_80401288/article/details/142027888

相关文章

  • 《 C++ 修炼全景指南:九 》打破编程瓶颈!掌握二叉搜索树的高效实现与技巧
    摘要本文详细探讨了二叉搜索树(BinarySearchTree,BST)的核心概念和技术细节,包括插入、查找、删除、遍历等基本操作,并结合实际代码演示了如何实现这些功能。文章深入分析了二叉搜索树的性能优势及其时间复杂度,同时介绍了前驱、后继的查找方法等高级功能。通过自定义实现的......
  • C++实现线程池
    目录一.什么是线程池二.为什么要用线程池三.如何实现线程池这篇文章简单讨论下线程池。一.什么是线程池线程池简单来时就是维护了一组线程的池子,这组线程执行一些相似任务。是一种线程的使用方式。二.为什么要用线程池有的时候系统需要处理大量相似任务,频繁创建销......
  • c++代理类
    c++中代理类的学习https://blog.csdn.net/lcg910978041/article/details/51468680C++代理类是为了解决这样的问题: 容器通常只能包含一种类型的对象,所以很难在容器中存储对象本身。怎样设计一个c++容器,使它有能力包含类型不同而彼此相关的对象? 代理运行起来和他所代表的......
  • C++中定义一个空结构体的内存占用
    比如定义:structTest{};使用sizeof(Test)可以查看到内存占用并不是0,而是1字节。原因主要有两点:一是用于对象标识:在C++中,每个对象都需要有一个唯一的地址。如果空类或结构体的大小为0字节,那么创建两个这样的对象时,它们在内存中的地址将是相同的,这与C++的对象模型相冲突。为......
  • 内网穿透技术的思考--反向代理、TCP 隧道、 UDP 打洞--C++代码示例
    概述内网穿透是一种技术,用于在私有局域网(LAN)中的设备与外部网络(如互联网)之间建立通信通道,使得外部设备可以访问内网中的服务。由于内网设备通常位于防火墙或NAT(网络地址转换)设备之后,外部网络无法直接访问它们。因此,内网穿透技术旨在解决这一问题。本文将讨论如何使用C++实现......
  • vscode配置c/c++环境
    在VisualStudioCode(VSCode)中配置C/C++开发环境,可以帮助更加高效地编写和调试代码。以下是详细的步骤:1.安装VisualStudioCode确保已经安装了VSCode。如果还没有安装,可以从[VSCode官方网站](https://code.visualstudio.com/)下载安装程序并安装。2.安装C......
  • C++-练习-39
    题目:编写一个程序,记录捐献给"维护合法权利团队"的资金。该程序要求用户输入捐献者数目,然后要求用户输入每一个捐献者的姓名和款项。这些信息被存在一个动态分配的结构数组中。每个结构有两个成员:用来存储姓名的字符数组和用力啊存储款项的double成员。读取所有的数据后,程序将......
  • 官网下载easyx压缩包,如何在devc++配置easyx
    视频教程官网下载easyx压缩包,如何在devc++配置easyxEasyXGraphicsLibraryforC++安装指南1.访问官网官网2.下载EasyX在官网上找到下载区域,点击下载按钮以获取EasyX安装包。3.访问更多下载选项点击页面上的“more”链接,以查看更多下载选项。4.下......
  • C++一元多项式解析、计算、输出(数据结构作业),可直接运行
    //Copyright(c)[email protected]#include<bits/stdc++.h>classPolynomial{private:std::unordered_map<int,int>data_;voidzero_value_optimization(){for(autoiter=data_.begin();iter!=data_.end();){......
  • 78_JAVA_new的使用在JAVA与C++的异同之处
    Java和C++都使用new关键字来创建对象和分配内存,但它们在实现和使用上有一些重要的异同之处。以下是这两种语言中new使用的主要异同点:1. 内存管理Java:自动内存管理:Java使用垃圾回收(GarbageCollection,GC)机制来自动管理内存。对象的生命周期由垃圾回收器自动管理,......