一、类入门
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如: 之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现, 会发现struct中也可以定义函数
C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。
和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。
注意:在继承和模板参数列表位置,struct和class也有区别
1.类的定义
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数
类内的函数叫做类的成员函数,在类内定义的成员函数一般会被编译器做内联函数处理
类声明放在.h文件中,成员函数定义放在.cpp文件中
#include<iostream>
using namespace std;
class person
{
public:
//成员函数
void set(string name,int age,int sex)
{
m_name = name;
m_age = age;
m_sex = sex;
}
private:
//成员变量
string m_name;
int m_age;
int m_sex;
};
int main()
{
person p1;
p1.set("rxy", 18, 1);
//对象的大小
//一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
//注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
cout << "p1大小为"<<sizeof(p1) << endl;
cout << sizeof(string);
cout << endl;
person p2;
p2.set("xxx", 6, 0);
return 0;
}
2.类的访问限定符及封装
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选 择性的将其接口提供给外部的用户使用
将类内分为公共权限(public)、保护权限(protected)、私有权限(private)
公共权限:类内可以访问,类外也可以访问
保护权限:类内可以访问,类外不能访问
私有权限:类内可以访问,类外不能访问
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
面向对象的三大特性:封装、继承、多态
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来 和对象进行交互。 封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用 户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件
3.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域
#include<iostream>
#include<string>
using namespace std;
class person
{
public://将成员属性私有化,需要在公共权限提供接口
//写姓名
void setname(string name)
{
m_name = name;
}
//读姓名
string getname()
{
return m_name;
}
int getage()
{
int m_age = 16;
return m_age;
}
void setlover(string lover)
{
m_lover = lover;
}
//输入财产
void setmoney(int money)
{
m_money = money;
}
//输出财产
int getmoney()
{
if (m_money < 0 ||m_money>1000)
{
cout << "财产不正常" << endl;
return 0;
}
return m_money;
}
private:
string m_name;//姓名 可读可写
int m_age;//年龄 只读
string m_lover;//爱人 只写
int m_money;//财产 可读可写
};
int main()
{
person d1;
d1.setname("张三");
//d1.m_name = "张三"; 不可以使用这条语句,因为m_name属于私有权限
cout << "姓名:" << d1.getname() << endl;
//d1.getage = 18;
//d1.getage("18");
//这两种都是不可以的,因为getage没有形参也就没有输入,不可以赋值
cout << "年龄:" << d1.getage() << endl;
d1.setlover("xx");
//cout << "爱人" << d1.setlover << endl;//报错因为函数无返回值
d1.setmoney(100);
cout << "财富:" << d1.getmoney() << endl;
system("pause");
return 0;
}
4.内存对齐
(1)类的大小
类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算 一个类的大小?
猜测一:如果一个对象包含类内的所有成员变量和成员函数
每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一 个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。
猜测二:代码只保存一份,在对象中保存存放代码的地址
即不同对象保存不同的成员变量,但是保存同一个成员函数的地址。
猜测三:只保存成员变量,成员函数存放在公共的代码段
c++是兼容c语言的,在c语言struct结构体中是没有成员函数的,因此c++也不会为对象保存各自的成员函数。一个对象调用函数的时候,可以等价为普通函数的调用
结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象
(2)内存对齐
理论上,32位系统下,int占4byte,char占一个byte,那么将它们放到一个结构体中应该占4+1=5byte;但是实际上,通过运行程序得到的结果是8 byte,这就是内存对齐所导致的。
//32位系统
#include<stdio.h>
struct{
int x;
char y;
}s;
int main()
{
printf("%d\n",sizeof(s); // 输出8
return 0;
}
现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐
尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,将这些存取单位称为内存存取粒度
①假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作
②现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率
内存对齐规则:
1. 第一个成员在与结构体偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。 VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
//32位系统
#include<stdio.h>
struct
{
int i;
char c1;
char c2;
}x1;
struct{
char c1;
int i;
char c2;
}x2;
struct{
char c1;
char c2;
int i;
}x3;
int main()
{
printf("%d\n",sizeof(x1)); // 输出8
printf("%d\n",sizeof(x2)); // 输出12
printf("%d\n",sizeof(x3)); // 输出8
return 0;
}
5.this指针
对于两个不同的对象,c++通过this指针实现区分C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数。让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
2. 只能在“成员函数”的内部使用
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递5.this指针存放在栈中,作为隐式形参,在vs中存储与ecx寄存器
二、默认成员函数
对于一个空类,编译器会默认自动生成六个默认成员函数:构造函数,析构函数,拷贝构造函数,赋值重载函数,取地址重载函数(包括普通对象和const对象)
默认生成的构造函数,会对自定义类型(struct,class等)调用,对于内置类型(int,double等)不调用
#include<iostream>
using namespace std;
class Date
{
public:
//无参构造函数
//Date()
//{
// cout << "无参构造函数调用" << endl;
//}
//有参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
cout << "有参构造函数调用" << endl;
}
//拷贝构造函数,需要采用引用传参,如果使用值传参会出现浅拷贝问题
//浅拷贝可能会导致野指针问题,数据覆盖问题,
Date(const Date& d2)//Date(Date& d2)这样是错误的,会无限递归
{
_year = d2._year;
_month = d2._month;
_day = d2._day;
cout << "拷贝构造调用" << endl;
}
//析构函数
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
void Init(int year, int month, int day)
//每次创建一个新的对象,就需要调用Init进行初始化,比较麻烦
//因此引入构造函数,使得每次创建新对象时就完成初始化操作
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//Date d1;
//d1.Init(2022, 7, 3);
//Date d2;//无参构造
Date d3(2000, 12, 6);//有参构造
Date d2(d3);
}
1.构造函数
构造函数:主要作用在创建对象时,为对象赋初值
对象的初始化与清除也是十分重要的问题,在对象的开始默认使用构造函数,完成对对象的初始化工作,如果不主动提供构造函数,则编译器会自动强制使用编译器自带的构造函数,为空实现
①构造函数没有返回值,也不写void
②函数名称与类名相同
③构造函数可以有参数,因此也可以函数重载
④程序在调用时会自动调用构造函数,无需手动调用,且程序允许一次只会调用一次
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
2.析构函数
析构函数:主要作用在销毁对象是,执行清理
对象的初始化与清除也是十分重要的问题,在对象的结束默认使用析构函数,完成对对象的清理工作,如果不主动提供析构函数,则编译器会自动强制使用编译器自带的析构函数,为空实现
①析构函数没有返回值,也不写void
②函数名称与类名相同,并在名称前面加~
③析构函数不可以有参数,无法发生重载
④程序在对象销毁之前会自动调用析构函数,无需手动调用,且程序结束一次只会调用一次
内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可。对于自定义类型成员的释放需要使用析构函数
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数
有资源申请时,一定要写,否则会造成资源泄漏
3.拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用
用法:①使用一个已经创建完毕的对象来初始化一个新的对象②以值传递的方式给函数参数传值③以值方式返回局部对象
1. 拷贝构造函数是构造函数的一个重载形式
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
4. 拷贝构造函数典型调用场景: ①使用已存在对象创建新对象②函数参数类型为类类型对象 ③函数返回值类型为类类型对象
5.编辑器默认的拷贝构造函数是一个浅拷贝,直接进行值传递拷贝。类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝
深拷贝与浅拷贝的区别在于浅拷贝直接进行值复制传递,两次传递共用一个地址,所以在析构函数的释放过程中会重复释放堆区内存
浅拷贝地址相同而深拷贝地址不同
三、运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.* :: sizeof ?:(三目运算符) . 以上5个运算符不能重载
//通过运算符重载,实现自定义类型的
//内置类型可以直接使用运算符,自定义类型也需要自定义运算符
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator<(const Date& d2)
{
if (_year < d2._year)
{
return true;
}
else if (_year == d2._year)
{
if (_month < d2._month)
{
return true;
}
else if (_month == d2._month)
{
if (_day < d2._day)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
else
{
return false;
}
}
bool operator>=(const Date& d2)//大于相当于<直接取反,代码复用
{
return !(*this < d2);
}
Date& operator=(const Date& d2)//赋值运算符重载
/* 参数类型:const T& ,传递引用可以提高传参效率
返回值类型:T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回* this :要复合连续赋值的含义*/
//赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
//一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了
//故赋值运算符重载只能是类的成员函数
{
_day = d2._day;
_month = d2._month;
_year = d2._year;
return *this;
}
//前置++
Date& operator++()
{
_day += 1;
return *this;
}
//后置++
Date& operator++(int)//占位参数int的目的是为了区分前后++
//后置++是先拷贝,然后对拷贝的对象进行运算,并返回拷贝值
//后置++是先使用后+1,因此需要返回+1之前的旧值
//故需在实现时需要先将this保存一份,然后给this + 1
{
Date temp(*this);
_day += 1;
return temp;
}
//private:
int _year;
int _month;
int _day;
};
//全局的operator==
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test()
{
Date d1(2018, 9, 26);
Date d2(2020, 10, 25);
cout << (d1 == d2) << endl;
cout << (d1.operator<(d2)) << endl;
d1.operator=(d2);
cout << d1._year <<"\t" << d1._month <<"\t" << d1._day << endl;
Date d3;
d3.operator=(d2);
cout << d3._year << "\t" << d3._month << "\t" << d3._day << endl;
}
int main()
{
Test();
system("pause");
return 0;
}
赋值运算符只能重载成类的成员函数不能重载成全局函数:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现
四、const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Display()const //编译器处理为 void Display(const Date* this)
{
cout << "Display()const 调用" << endl;
cout << _year << endl;//编译器处理为 cout<<this->_year<<endl;
cout << _month << endl;
cout << _day << endl;
}
void Display()
{
cout << "Display() 调用" << endl;
cout << _year << endl;
cout << _month << endl;
cout << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 3, 29);
d1.Display();
const Date d2(2023, 3, 29);//常对象调用常函数
d2.Display();
system("pause");
return 0;
}
(1)非const->const是权限缩小,是允许的
const->非const是权限放大,是不允许的
本质上const修饰this指针,this的类型为const 类名*
非const对象的this指针类型为 类名*(2)const对象不可以调用非const成员函数
非const对象可以调用const成员函数,也可以调用非const成员函数(3)const成员函数不能调用其他非const成员函数
非const成员函数可以调用其他const成员函数
五、构造函数进阶
1.初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式
C++采用初始化列表的方式对对象的属性进行初始化
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量 const成员变量 自定义类型成员(且该类没有默认构造函数时)
初始化列表的初始化顺序取决于对象属性的声明顺序,与属性在初始化列表中的位置无关
不管是否使用初始化列表,对于自定义类型成员变量,一定要使用初始化列表初始化
2.explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用
用explicit修饰构造函数,将会禁止构造函数的隐式转换
C++中,一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色:① 是个构造②是个默认且隐含的类型转换操作符。然而某些情况下并不需要进行隐式类型转换,因此使用explicit
#include<iostream>
using namespace std;
//通过构造函数给对象中的各个成员变量一个合适的初始值
//虽然对象经过构造函数已经有了一个初始值,但是这并不是对象的初始化,只能认为是赋初值
//对象的初始化只能有一次,而构造函数体内可以有多次赋值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
class Date_2
{
public:
Date_2(int year,int month,int day,int a ,int b)
:_year(year), _month(month), _day(day),aaa(a),bbb(b)//初始化列表 属性实现
{
}
private:
int _year;//属性声明
int _month;
int _day;
int& aaa;//引用
const int bbb;//const
};
//构造函数不仅可以构造和初始化对象
//对于①单个参数②除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换作用
class Date_3
{
public:
//对于单个参数构造函数,使用explicit修饰构造函数,禁止类型转换--explicit去掉之后具有类型转换作用
/*explicit*/ Date_3(int year)
:_year(year)
{
}
虽然有多个参数,但是除了第一个元素没有默认值,其余属性均有默认值,创造对象时后两个参数可以不传递
//explicit Date_3(int year,int month=1,int day=1)
// :_year(year),_month(month),_day(day)
//{
//}
Date_3& operator=(const Date_3& d)
{
if (this != &d)//this是指针,指向当前对象
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date_3 d3(2023);
//如果构造函数有explicit标识,则不能进行隐式类型转化,下面的赋值语句报错
d3 = 100;
//如果构造函数中没有explicit标识,则能进行隐式类型转换
//编译器自动使用100创建一个无名对象,然后通过无名对象对d3进行赋值
}
六、静态成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量
用 static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
静态成员变量:①所有对象共享一份数据②在编译阶段分配内存③类内声明类外初始化
静态成员函数:①所有对象共享同一个函数②静态成员函数只能访问静态成员变量
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
#include<iostream>
// 1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
//2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
//3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
//4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
//5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
using std::cout;
using std::endl;
//实现一个类,用于计算程序中创建了多少个类变量
class A
{
public:
A(int a = 0)
{
++count;
}
A(const A& a)
{
++count;
}
int GetCount()
{
return count;
}
static int GetCount_2()
{
//_num = 1;//静态成员函数不可以访问成员变量,只能访问静态成员变量
return count;
}
private:
//声明为static的类成员称为类的静态成员;用static修饰的成员变量,称之为静态成员变量;用
//static修饰的成员函数,称之为静态成员函数。
int _num;
static int count;//静态成员变量,属于所有对象,属于整个类
};
int A::count = 0; //静态成员变量一定要在类外进行初始化
int main()
{
A a1;
A a2(a1);
//如果count属于public作用域,那么可以通过一下方式在类中找到count
//cout << a1.count << endl;
//cout << A::count << endl;
//A* p1 = nullptr;
//cout << p1->count << endl;
//如果count属于private作用域,需要通过成员函数获得
A a3;
cout << a3.GetCount() << endl;
cout << A::GetCount_2() << endl;
system("pause");
return 0;
}
七、友元
1.友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
需要提前声明要使用的类:必须将先定义的类的成员函数作为后定义的类的友元函数,如果调换顺序会出现语法错误
2.友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
友元类,实际上就是把这个类定义为另一个类的友元(即A类是B类的友元,A可以访问B中的私有成员变量以及保护成员变量)
注意:
(1)友元关系不具有传递性
(2)友元关系不具备双向性(我是你的友元类,我可以访问你,但你不能访问我)
(3)若要相互访问对方类的成员,则要双方都要定义对方为自己的友元
(4)内部类就是外部类的友元类(内部类:如果一个类定义在另一个类的内部,这个内部类就叫做内部类)
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
using std::ostream;
using std::istream;
//友元分为友元函数、友元类
//友元类的所有成员函数都可以是另一个类的友元函数
//都可以访问另一个类中的非公有成员
//①友元关系具有单向性,不具有交换性②友元关系不能传递③友元不能继承
class Time
{
// 声明日期类为时间类的友元类
//则在日期类中就直接访问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;
};
//如在运算符重载中,重载赋值运算符出现的问题
//无法对私有作用域下的属性进行处理,因为没有访问权限
class Date
{
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year=2023, int month=3, int day=29)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
void Print()
{
cout << _year << endl;
cout << _month << endl;
cout << _day << endl;
}
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;
};
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d1;
d1.Print();
cin >> d1;
d1.Print();
return 0;
}
八、内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类
内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访 问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
3. sizeof(外部类)=外部类,和内部类没有任何关系
#include<iostream>
using namespace std;
class Out
{
private:
static int i;
int j;
public:
class In//内部类的空间是独立的,但是受到外部类访问空间的限制
{
public:
void p(const Out& a)
{
cout << i << endl;
cout << a.j << endl;
}
};
};
int main()
{
Out o1;
Out::In I1;//如果Out是私有就会报错
}
九、匿名对象
匿名对象指的就是 没有名字的对象 ,在使用中理解为实例化一个类对象,但是并不把它赋给一个对应的类变量,而是直接使用
我们知道在C++的创建对象是一个费时,费空间的一个操作。有些固然是必不可少,但还有一些对象却在我们不知道的情况下被创建了。通常以下三种情况会产生临时对象:
1.以值的方式给函数传参:按值传递时,首先将需要传给函数的参数,调用拷贝构造函数创建一个副本,所有在函数里的操作都是针对这个副本的,也正是因为这个原因,在函数体里对该副本进行任何操作,都不会影响原参数
2.类型转换:我们在做类型转换时,转换后的对象通常是一个临时对象。编译器为了通过编译会创建一起我们不易察觉的临时对象
3.函数需要返回一个对象时:当函数需要返回一个对象,他会在栈中创建一个临时对象,存储函数的返回值
class Cat
{
public:
Cat()
{
cout<<"Cat类 无参构造函数"<<endl;
}
Cat(Cat& obj)
{
cout<<"Cat类 拷贝构造函数"<<endl;
}
~Cat()
{
cout<<"Cat类 析构函数 "<<endl;
}
};
void playStage() //一个舞台,展示对象的生命周期
{
Cat(); /*在执行此代码时,利用无参构造函数生成了一个匿名Cat类对象;执行完此行代码,
因为外部没有接此匿名对象的变量,此匿名又被析构了*/
Cat cc = Cat(); /*在执行此代码时,利用无参构造函数生成了一个匿名Cat类对象;然后将此匿名变 成了cc这个实例对象*/
}
int main()
{
playStage();
system("pause");
return 0;
}
十、拷贝对象优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还 是非常有用的
结论:
①多使用const &传参
②接收返回值对象,尽量拷贝构造方式接收,而不是赋值接收
③函数中返回对象时,尽量返回匿名对象
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
void f2(const A& aa)//引用传参尽量加const,从而即可以接收变量也可以接收常量
{}
A f3()
{
A aa;//构造函数
return aa;//拷贝构造,aa出作用域就销毁了,所以会拷贝一个新的传递
}
A f4()
{
return A();//匿名函数,构造+拷贝构造-》优化为直接构造
}
int main()
{
//值传参
A aa1 = 1;//构造+拷贝构造-》编译器优化为直接构造
f1(aa1);//拷贝构造
f1(2);//构造+拷贝构造-》优化为直接构造
f1(A(3));//构造+拷贝构造+拷贝构造-》优化为直接构造
cout << endl;
//引用传参
f2(aa1);//无优化
f2(2);//无优化
f2(A(3));//无优化
//返回
f3();//构造+拷贝构造,无优化
A aa2 = f3();//构造+拷贝构造+拷贝构造-》优化为构造+拷贝构造
cout << endl;
f4();
A aa3 = f4();//直接构造+拷贝构造-》优化为直接构造
return 0;
}