【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++程序的内存布局(从低地址到高地址)大致可分为:①代码段;②数据段(又可细分为已初始化数据和未初始化数据);③堆区;④共享区(堆或栈可拓展至此);⑤栈区;⑥内核空间(操作系统专属)。
图示如下
下面详细介绍每个区域的内容与特性
-
代码段
-
存放内容:编译后的机器指令(你的函数代码)、字符串常量(如
"Hello"
)。 -
特性:通常是只读的与共享的,多个相同进程可以共享同一份代码段以节省内存。
-
-
已初始化数据段
-
存放内容:已初始化的全局变量和静态变量(包括静态局部变量、静态成员变量)。
-
特性:在程序开始前就分配好空间并初始化,生命周期贯穿整个程序。
-
-
未初始化数据段
-
存放内容:未初始化的全局变量和静态变量。
-
特性:在程序开始前由操作系统将其内容全部初始化为零。这也是为什么未初始化的全局变量默认是0的原因。
-
-
堆区Heap
-
存放内容:由
malloc
、new
、new[]
等动态分配的内存。 -
特性:由程序员手动管理其生命周期(
free
、delete
)。向上增长(向共享区)。分配速度较慢,可能会产生内存碎片。
-
-
栈区Stack
-
存放内容:局部变量、函数参数、返回值等。
-
特性:由编译器自动管理。每个函数调用都会在栈上创建一个新的栈帧。向下增长(向共享区)。分配和释放速度极快。
-
-
内核空间
-
存放内容:操作系统内核的代码和数据。
-
特性:用户程序无法直接访问。
-
三、静态成员与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++程序的内存布局,再然后介绍了静态成员与动态成员,最后讨论了模板相关。
整理不易,希望对你有所帮助。
读完点赞,手留余香。