当前位置: 首页 > news >正文

【C++闯关笔记】封装②:友元与模板

系列文章目录

从C到C++入门:C++有而C语言没有的基础知识总结-CSDN博客

【C++闯关笔记】封装①:类与对象-CSDN博客

【C++闯关笔记】封装②:友元与模板-CSDN博客


目录

系列文章目录

一、什么是友元

1.友元函数

2.友元类

内部类

二、C++程序的内存布局

三、静态成员与const成员

1.静态成员

2.const成员

3.静态成员和count成员的区别与联系

四、模板

1.函数模板

函数模板的实例化

2.类模板

总结



一、什么是友元

友元提供了一种突破封装的方式,在特定情况下提供了便利。但友元破坏了封装,并友元会增加耦合度,所以友元不宜多用

友元大致可分为友元函数与友元类。

1.友元函数

概念

友元函数是定义在类外部的普通函数,可以直接访问类的私有成员。友元函数不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

示例

class Date
{friend void test_display(const Date& d);public:private:int _year;int _month;int _day;
};void test_display(const Date& d)
{cout << d._year << d._month << d._day << endl;
}

友元函数细节

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

2.友元函数不能用const修饰;

3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制 一个函数可以是多个类的友元函数;

4.友元函数的调用与普通函数的调用原理相同;

什么情况下会用到友元函数呢?

假如我们有一个Date日期类,我们想通过cout直接输出Date类的对象,如:

class Date
{
public:Date(int year = 2025, int month = 8, int day = 20):_year(year),_month(month),_day(day){}
private:int _year;int _month;int _day;
};
Date d1(2025,2,1);
cout<<d1<<endl;

为了达到目标,我们可以通过重载“<<”操作符实现,问题是:是以成员函数的方式实现呢,还是以全局普通函数加友元的方式实现?

先本能的试试成员函数,毕竟重载目标就是专用于输出Date类,

友元函数实现:

	ostream& operator<<(ostream& out){out << _year << _month << _day;return out;}

解释:

①我们平常使用的cout(std::cout)是std::ostream类的一个预定义好的全局对象,该种被关联到标准输出(通常是终端屏幕)。形象的理解:编译器在一开始就为我们创建好了一个对象cout供我们使用输出(cin同理)。

②“<<”运算符,本质上是C语言中的移位运算符,在“iostream库”中重载了“<<”运算符并作为实现为 ostream 类的成员函数,功能就是输出(“>>“同理)。

③所以,cout是ostream类的对象,"<<"是ostream的成员函数 。故当我们想要重载"<<"时返回值应该ostram类型,形参也为ostream类型(用于接受调用”cout<<“时传入的cout)。

OK,到这里重载没有问题,可仔细一想这成员函数该怎么调用呢?

Date d(1000,11,2);
d.operator<<(cout);

这样的调用方式明显不符合日常使用,故”operator<<“和”operator>>“的重载,应该用普通全局函数加友元实现。

全局函数实现加友元实现:

class Date
{friend ostream& operator<<(ostream& out, const Date& d);
public:Date(int year = 2025, int month = 8, int day = 20):_year(year),_month(month),_day(day){}
private:int _year;int _month;int _day;
};ostream& operator<<(ostream& out,const Date& d)
{out << d._year << d._month << d._day;return out;
}

当想要输出Date类对象时,直接可以使用"<<"运算符输出,如下。

Date d(2000,1,3);
cout<<d;

通过上面有关哦ostraem的解释,我们现在可以明白:实际上cout<<d,在编译器执行时应该是cout.<<(this,d)或者<<(cout,d),cin同理。

2.友元类

概念

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

示例

通过定义一个Time类,将Date设置为Time的友元类,可以实现Date的成员函数访问Time的私有成员:

class Time
{friend class Date;
public:Time(int h=1, int m=1, int s=1) :_hour(h), _min(m), _sec(s){ }
private:int _hour;int _min;int _sec;
};class Date
{public:Date(int year = 2025, int month = 8, int day = 20,const Time& t = Time()) :_year(year), _month(month), _day(day), _t(t){}void Display(){cout << "year:" << _year << " month:" << _month << " day:" << _day;cout << " hour:" << _t._hour << "min: " << _t._min << " sec:" << _t._sec << endl;}private:int _year;int _month;int _day;Time _t;
};

友元类的特性

1.友元关系是单向的,不具有交换性。 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

2.友元关系不能传递。如果C是B的友元, B是A的友元,不能得出C是A的友元。

3.友元关系不能继承。

内部类

概念

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

内部类特性

1.内部类是一个独立的类, 它不属于外部类。更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

2.内部类就是外部类的友元类。内部类可以通过外部类的对象参数来访 问外部类中的所有成员。但是外部类不是内部类的友元。

3. 内部类可以任意定义在外部类的public、protected、private。

4. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。

5. sizeof(外部类)=外部类,和内部类没有任何关系。

示例

class Date
{public:class Time{friend class Date;public:Time(int h = 1, int m = 1, int s = 1) :_hour(h), _min(m), _sec(s){}void Display(const Date& d){cout << "year:" << d._year << " month:" << d._month << " day:" << d._day;cout << " hour:" << _hour << "min: " << _min << " sec:" << _sec << endl;}private:int _hour;int _min;int _sec;};Date(int year = 2025, int month = 8, int day = 20) :_year(year), _month(month), _day(day){}private:int _year;int _month;int _day;
};


二、C++程序的内存布局

若想真正探究清楚C++程序的具体运行逻辑,只写代码肯定是流于表面的,需要深入探究程序的运行机制,这是成为优秀程序员的必经之路。

C++程序的内存布局(从低地址到高地址)大致可分为:①代码段;②数据段(又可细分为已初始化数据和未初始化数据);③堆区;④共享区(堆或栈可拓展至此);⑤栈区;⑥内核空间(操作系统专属)。

图示如下

下面详细介绍每个区域的内容与特性

  1. 代码段 

    • 存放内容:编译后的机器指令(你的函数代码)、字符串常量(如 "Hello")。

    • 特性:通常是只读的与共享的,多个相同进程可以共享同一份代码段以节省内存。

  2. 已初始化数据段 

    • 存放内容已初始化全局变量静态变量(包括静态局部变量、静态成员变量)。

    • 特性:在程序开始前就分配好空间并初始化,生命周期贯穿整个程序。

  3. 未初始化数据段 

    • 存放内容未初始化全局变量静态变量

    • 特性:在程序开始前由操作系统将其内容全部初始化为零。这也是为什么未初始化的全局变量默认是0的原因。

  4. 堆区Heap

    • 存放内容:由 mallocnewnew[] 等动态分配的内存。

    • 特性:由程序员手动管理其生命周期(freedelete)。向上增长(向共享区)。分配速度较慢,可能会产生内存碎片。

  5. 栈区Stack

    • 存放内容局部变量函数参数返回值等。

    • 特性:由编译器自动管理。每个函数调用都会在栈上创建一个新的栈帧向下增长(向共享区)。分配和释放速度极快。

  6. 内核空间

    • 存放内容:操作系统内核的代码和数据。

    • 特性:用户程序无法直接访问。


三、静态成员与const成员

1.静态成员

静态成员概念

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

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

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

示例

class Date
{public:private:int _year;int _month;int _day;static int count;
};int Date::count = 0;

静态成员特性

1. 静态成员为所有类对象所共享,存放在静态区(数据段);

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

3. 类静态成员即可用:类名::静态成员,或者 对象.静态成员 来访问;

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

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

通过对静态成员特性的分析,我们可以得到两个有价值的结论:

1. 静态成员函数可以调用非静态成员函数吗?不能。

静态成员函数没有this指针,不能调用非静态成员函数;

2. 非静态成员函数可以调用类的静态成员函数吗?可以。

静态成员函数的调用不需要对象实例。

2.const成员

const成员概念

声明为static的类成员称为类的常态(const)成员:

const修饰的成员变量的值,在实例化出的对象中不可更改;

const修饰类成员函数,实际修饰该成员函数的this指针,表明在该成员函数中不能对类的任何成员进行修改。

示例

class Date
{public:Date(int year = 2025, int month = 8, int day = 20 ,int cnt=0) :_year(year), _month(month), _day(day),_cnt(cnt){}Date(const Date& d):_cnt(d._cnt){cout << "const Date& d" << endl;this->_year = d._year;this->_month = d._month;this->_day = d._day;}private:int _year;int _month;int _day;const int _cnt;
};

值得注意的是,具有const成员变量的类在设计拷贝构造函数时,必须用初始化列表显式地用源对象的值来初始化新对象的const成员。

const成员特性

1.const对象不能调用非const成员函数;

2. 非const对象可以调用const成员函数;

3. const成员函数内不可以调用其它的非const成员函数;

4. 非const成员函数内可以调用其它的const成员函数。

3.静态成员和count成员的区别与联系

首先来看它们的区别:

特性static 成员 (静态成员)const 成员 (常量成员)
核心语义“属于类”而非对象“不可修改”
内存中的副本数唯一 一份,所有类对象共享每个对象都拥有自己的一份
生命周期程序开始时创建,结束时销毁,生命周期随程序随着对象的创建而创建,销毁而销毁,生命周期随对象
内存位置数据段取决于对象本身(对象在栈,它就在栈;对象在堆,它就在堆)
初始化方式必须在类外单独定义和初始化(除了整型静态常量)必须在类的构造函数的初始化列表中初始化
访问方式既可以通过对象访问(obj.staticVar),也可以通过类名访问(Class::staticVar)只能通过对象实例访问(obj.constVar)

静态成员和count成员的联系——组合为“静态常量成员”

连着的组合创建了一个所有对象共享的、不可修改的常量。它就像是属于这个类的“全局常量”。

static const组合在C++中非常常见,主要用于:

①定义类相关的常量:比如数学类中的π值。

②定义数组大小:这是非常经典的用法。


四、模板

在编程中我们可能会遇到这种场景:两个不同类型的变量,需要经过同样的处理操作。如果根据数据类型定义两个重载函数感觉有些麻烦,能否有种“方便的模具函数”,使得需要经过同样处理操作的变量都可以调用?

C++为实现泛型编程,引入了模板概念,即告诉编译器一个模具,让编译器根据不同的类型利用该模子来生成代码。

1.函数模板

函数模板概念

函数模板代表了同种类型的函数的抽象模板,该函数模板与类型无关,在使用时通过传入的参数类型产生函数的特定类型版本。

函数模板语法

template <typename T1,typename T2,……>

返回值类型 函数名(参数列表){}

注意:两条语句需要按顺序写在一起

示例

我们可以定义一个swap函数的模板,在调用swap时,编译器自动根据传入参数的类型推断T1的类型,之后不管是什么类型的数据需要交换都可以通过swap函数。

template<typename T1>
void swap(T1& a, T1& b)
{T1 temp = a;a = b;b = temp;
}

细节注意:typename是用来定义模板参数关键字,也可以使用class,为了区分通常使用typename(不能使用struct代替class)。

函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化可以分为:隐式实例化和显式实例化。

隐式实例化:让编译器根据实参确定模板参数的实际类型。

就比如上述的swap函数。

但有一点需要注意:编译器一般不会进行类型转换操作,也就是说如果实参是两种类型,那么编译器就会报错。如下:

template<typename T2>
T2 Add(const T2& a, const T2& b)
{return a + b;
}int main()
{int a = 2, b = 3;double c = 1.1, d = 2.1;//编译器报错,实参类型不同cout << Add(a, d) << endl;return 0;
}

解决办法有两种:

一种是在传入参数是就将它们强制转换成同一类型,如

cout << Add(a, (int)d) << endl;

第二种方法就是显示实例化。

显式实例化:在函数名后添加<>,<>中指明模板参数的实际类型。

cout << Add<int>(a, d) << endl;

如果传入参数的类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器则会报错。

函数模板的匹配原则

1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函。

template<typename T,typename T1>
T Add(const T& a, const T1& b)
{return a + b;
}//专用加法函数
int Add(const int a,const int b)
{return a + b;
}

2.如果同时满足模板函数与专用函数,在调动时会优先调用非模板函数。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板函数。

3. 模板函数不会自动的类型转换,但普通函数可以进行自动类型转换

2.类模板

在函数模板之后同理可得类模板概念。

具体看类模板的语法

template <typename T1,typename T2,……>

class 类模板名 { // 类内成员定义 };

注意:两条语句需要按顺序写在一起

示例

template<typename T>
class DateType
{
public:DateType(T& date) :_date(date){}void Display();
private:T _date;
};

注意:类模板中函数放在类外进行定义时,需要加模板参数列表,如

template<typename T>
void DateType<T>::Display()
{cout << _date;
}

类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,实例化的类型放在<> 中。类模板名字不是真正的类型,而实例化的结果才是真正的类型。如

DateType<int> a(1);//DateType<int>才是类型
DateType<double> b(3.14);//DateType<double>才是类型


总结

本位为【C++闯关】系列的第三篇内容,先是介绍了友元相关内容,紧接着补充了C++程序的内存布局,再然后介绍了静态成员与动态成员,最后讨论了模板相关。

整理不易,希望对你有所帮助。

读完点赞,手留余香。

http://www.dtcms.com/a/344079.html

相关文章:

  • git新建项目如何推送到远程仓库
  • 深度学习②【优化算法(重点!)、数据获取与模型训练全解析】
  • 医疗AI中的电子病历智能化:Model Context Protocol使用从规则编码到数据涌现
  • 齐次变换矩阵的逆变换:原理与SymPy实现
  • 零音乐基础想创作?通过cpolar,ACE-Step远程编曲如此简单
  • Gauth-字节在海外推出的AI学习辅助应用
  • FFmpeg添加水印
  • 学习嵌入式第三十五天
  • PCB电路设计学习2 元件原理图封装的添加 手工设计元件封装
  • LeetCode100 -- Day4
  • webpack开发模式与生产模式(webpack --mode=development/production“, )
  • 如何修复“DNS服务器未响应”错误
  • OpenHarmony子系统介绍
  • LLM实践系列:利用LLM重构数据科学流程01
  • 数据分析专栏记录之 -基础数学与统计知识 2 概率论基础与python
  • OpenHands:开源AI软件开发代理平台的革命性突破
  • 密码管理中Null 密码
  • 第七章 愿景22 超级丹项目汇报
  • 算法第五十三天:图论part04(第十一章)
  • Spring Boot+Docker+Kubernetes 云原生部署实战指南
  • LLM实践系列:利用LLM重构数据科学流程03- LLM驱动的数据探索与清洗
  • Windows应急响应一般思路(一)
  • [激光原理与应用-317]:光学设计 - Solidworks - 零件、装配体、工程图
  • VTK学习笔记3:曲线与曲面源
  • 闲置笔记本链接硬盘盒充当Windows NAS 网易UU远程助力数据读取和处理
  • 全球电商业财一体化趋势加速,巨益科技助力品牌出海精细化运营
  • 数字隔离器:新能源系统的安全与效能革命
  • JavaWeb前端04(Vue生命周期,钩子函数)
  • Jmeter自动化性能测试常见问题汇总
  • 什么是Jmeter?Jmeter使用的原理步骤是什么?