目录
一、类的定义
1.定义与声明放一起
2.定义与声明分开
二、类的访问限定符及封装
1.类的访问限定符
2.类的封装
三、类的实例化
四、类对象
1.类对象的存储方式
2.计算类对象的大小
面试题
1.结构体怎么对齐? 为什么要进行内存对齐?
2.如何让结构体按照指定的对齐参数进行对齐?
3.什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景?
五、this指针
1.this指针的含义
2.this指针的性质
面试题
①this指针存在哪里?
②this指针可以为空吗?
六、类的默认成员函数
1.构造函数
①构造函数的含义
②构造函数赋初值而非初始化
③列表初始化
④C++11 新成员初始化
⑤构造函数的特性
⑤explicit关键字
2.析构函数
①构造函数的概念
②析构函数特性
3.拷贝构造函数
①拷贝构造函数的概念
②拷贝构造函数的特性
4.运算符重载
①运算符重载的意义
②运算符重载的性质
③运算符重载的使用
④赋值运算符重载
5.const成员函数
①const修饰类的成员函数
②const修饰类的对象和成员函数
6.取地址及const取地址操作符重载
七、static的类成员
1.static的类成员概念
2.static的类成员特性
八、友元函数和友元类
1.友元函数及友元类的简介
2.友元函数
①引申问题
②友元函数的使用
3.友元类
九、内部类
1.内部类的概念
2.内部类的特性
3.内部类的实现
一、类的定义
在C++中,类的声明和定义可以分开写,也可以放在一起写。但是,推荐将类的声明和定义分开写,即将类的声明放在头文件中,将类的定义放在源文件中。这样做的好处是可以提高代码的可读性和可维护性。如果将类的声明和定义都放在头文件中,那么头文件就会变得很长,不利于代码的阅读和维护。如果将类的声明和定义都放在源文件中,那么就需要在头文件中声明类,否则其他文件无法使用该类。
1.定义与声明放一起
class Stack
{
public:void StackInit(int InitSize = 4){a = (int*)malloc(sizeof(int) * InitSize);if (a == NULL){perror("malloc");exit(-1);}capacity = InitSize;size = 0;}void StackPush(int x){if (size == capacity){int new_capacity = capacity * 2;int* new_a = (int*)realloc(a, sizeof(int) * new_capacity);if (new_a == NULL){perror("realloc");exit(-1);}a = new_a;capacity = new_capacity;}a[size] = x;size++;}private:int* a;int capacity;int size;
};
2.定义与声明分开
class Stack
{
public://成员函数的声明void StackInit(int InitSize = 4);
private://成员变量的定义int* a;int capacity;int size;
};//成员函数的定义
//作用域符号::表示StackInit成员函数属于Stack域
void Stack::StackInit(int InitSize = 4)
{a = (int*)malloc(sizeof(int) * InitSize);if (a == NULL){perror("malloc");exit(-1);}capacity = InitSize;size = 0;
}
注意成员函数在类外定义,需要使用::作用域解析符,指明成员属于哪个类域。
二、类的访问限定符及封装
1.类的访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
public表示公有成员,可以被任何函数访问;protected表示保护成员,只能被类内部和子类访问;private表示私有成员,只能被类内部访问。
实例化出来的对象无法直接去访问私有成员变量,只能通过公有成员函数访问。
如下:
Stack s1;
s1.capacity = 0; //err 无法直接去访问私有成员变量
s1.StackInit(); //只能通过公有成员函数访问
【访问限定符说明】
- public修饰的成员在类外可以直接被访问。
- protected和private修饰的成员在类外不能直接被访问。
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
- class的默认访问权限为private,struct为public(因为struct要兼容C)。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private。
2.类的封装
【面试题】 面向对象的三大特性:封装、继承、多态。
在类和封装阶段,我们只研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装是面向对象编程的三大特性之一,也是一种管理。它将零散的数据和算法放到一个集合里,方便管理和使用。
三、类的实例化
类的实例化是指创建一个对象的过程。在C++中,类的实例化有两种方式——在栈中实例化和在堆中实例化。
Stack st;
四、类对象
1.类对象的存储方式
2.计算类对象的大小
一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员(只计入指针变量大小[动态数据])都是不占用类对象的存储空间的。
当类不包含虚函数和非静态数据成员时,其对象大小为1。
【符合结构体的内存对齐规则】
类对象的大小 = 各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和 + vfptr指针(多继承下可能不止一个)+vbptr指针(多继承下可能不止一个)+编译器额外增加的字节。
面试题
1.结构体怎么对齐? 为什么要进行内存对齐?
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。
2.如何让结构体按照指定的对齐参数进行对齐?
#pragma pack(n) 强制使对齐模数为n(不管VS和Linux)
3.什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景?
大端存储模式:就是内存的低地址上存着数据的高位,高地址上存着数据的低位。
小端存储模式:就是内存的低地址上存数据的低位,而高地址上存数据的高位。
大小端场景:代码移植和网络通信。
五、this指针
1.this指针的含义
C++编译器给每个非静态的成员函数(静态成员函数没有this指针)增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
例如:
Stack s1;
Stack s2;
s1.StackInit();
通过this指针,s1.StackInit()函数指定去初始化对象s1而非对象s2。
2.this指针的性质
- 对象的this指针不是对象本身的一部分。
- 它是一个隐藏的指针形参,传递给非静态成员函数。一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
- 在类X的成员函数中,它的类型是X*(指向X的指针)。
例如:
class MyClass {
private:int x;
public:void setX(int value);int getX();
};//相当于传入Myclass* this的参数
void MyClass::setX(int value) {x = value;//相当于this->x = value
}int MyClass::getX() {return x;//相当于return this->x
}int main() {MyClass obj;obj.setX(10);cout << obj.getX() << endl; // 输出 10return 0;
}
面试题
①this指针存在哪里?
其实编译器在生成程序时加入了获取对象首地址的相关代码。并把获取的首地址存放在了寄存器ECX中(VC++编译器是放在ECX中,其它编译器有可能不同)。也就是成员函数的其它参数正常都是存放在栈中。而this指针参数则是存放在寄存器中。类的静态成员函数因为没有this指针这个参数,所以类的静态成员函数也就无法调用类的非静态成员变量。
②this指针可以为空吗?
可以为空,当我们调用函数时,如果函数内部不需要使用到this,也就是不需要通过this指向当前对象并对其进行操作时才可以为空(当我们在其中什么都不放或者在里面随便打印一个字符串),如果调用的函数需要指向当前对象,并进行操作,则会发生错误(空指针引用)就跟C中一样不能进行空指针的引用。
例如:
class A
{
public:void PrintA(){std::cout << _a << std::endl;}void Show(){std::cout << "Show()" << std::endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->PrintA();p->Show();
}
PrintA()由于this指针取到nullptr,nullptr->_a产生非法访问,程序崩溃。而Show()由于没有指向相关对象,因而可以正常运行。
六、类的默认成员函数
空类自动生成下列6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数(构造函数的重载)
- 赋值运算符重载
- 取地址运算符重载(普通对象)
- 取地址运算符重载(const对象)
1.构造函数
①构造函数的含义
构造函数是一种特殊的成员函数,它会在对象创建时自动调用,用于初始化对象的数据成员。它可以在对象创建时给成员变量赋初值,但构造函数本身并不是初始化的全部。
<1>在类内定义构造函数
class Date
{
public:Date(int year = 1970, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Display(){std::cout << _year << " ";std::cout << _month << " ";std::cout << _day << " " << std::endl;}
private:int _year;int _month;int _day;
};
//...
Date d1;
d1.Display();
Date d2(2003, 9, 17);
d2.Display();
<2>在类外定义构造函数
class Date
{
public:Date(int year = 1970, int month = 1, int day = 1);void Display(){std::cout << _year << " ";std::cout << _month << " ";std::cout << _day << " " << std::endl;}private:int _year;int _month;int _day;
};
Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;
}
//...
Date d1;
d1.Display();
Date d2(2003, 9, 17);//调用有参构造函数
d2.Display();
②构造函数赋初值而非初始化
虽然构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
如下:
class Date
{
public:Date(int year, int month, int day) //构造函数{_year = year;_year = 2021; //第二次赋值_month = month;_month = 6; //第二次赋值_day = day;_day = 10; //第二次赋值}void Display(){std::cout << _year << " " << _month << " " << _day << std::endl;}
private:int _year;int _month;int _day;
};
输出结果:
2021 6 10
③列表初始化
列表初始化————使用花括号初始化器
【注意】
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
- 类中包含以下成员,必须放在初始化列表位置进行初始化。
1.引用成员变量
2.const成员变量
3.自定义类型成员(该类没有默认构造函数)
对于自定义类型成员变量,一定会优先使用初始化列表初始化。
列表初始化(花括号初始化器)
Date(int year = 1970, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
class A
{
public:A(int a):_a1(a), _a2(_a1){}void Print() {std::cout << _a1 << " " << _a2 << std::endl;}
private:int _a2;int _a1;
};
int main() {A aa(1);aa.Print();return 0;
}
_a2初始化时_a1还未初始化,最后_a1初始化。
④C++11 新成员初始化
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。初始化是在初始化列表中初始化的。
class A
{
public:void Print(){cout << a << endl;cout << b._b << endl;cout << p << endl;}
private:// 非静态成员变量,可以在成员声明时给缺省值。int a = 10;B b = 20;int* p = (int*)malloc(4);
};
【与使用缺省参数相比】
优点:可以使得代码更加清晰易懂,可以直接看出成员变量的初始值。
缺点:是如果类的成员变量比较多,可能会使得声明变得复杂和冗长。
利用缺省参数进行初始化赋值的方式可以减少代码量,使得代码更加简洁明了。但是这种方式需要依赖于默认参数的特性,在使用时需要特别注意参数顺序和默认值的设置,否则容易出现错误。
因此,对于非静态成员变量的初始化赋值,要根据具体情况来选择合适的方式。如果成员变量比较少且有明确的初值,可以采用声明时初始化赋值的方式;如果成员变量比较多或者初值比较复杂,可以考虑利用缺省参数进行初始化赋值。
⑤构造函数的特性
- 构造函数的名称必须与类名相同,没有返回类型(包括void),能被重载。
- 构造函数可以有参数,这些参数可以用于初始化数据成员。
- 如果没有定义构造函数,则编译器会提供一个默认的构造函数。
- 类的构造函数可以分为默认构造函数、带参数的构造函数和拷贝构造函数三种类型。
- 默认情况下,如果没有定义任何构造函数,则编译器会自动生成一个默认的无参构造函数。如果定义了带参数的构造函数,则编译器不会再生成默认的无参构造函数。
对于默认构造函数:
- 针对内置的类型的成员变量不做处理。
- 对于自定义类型的成员变量,调用它的构造函数初始化。
⑤explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。而explicit可以屏蔽这种类型转换。
构造函数对于单个参数的构造函数,具有类型转换的作用。例如下列代码:
class Date
{
public:Date(int year = 1970) : _year(year) {}void Display();
private:int _year;
};
void Date::Display()
{std::cout << _year << std::endl;
}
int main()
{Date d1(2016);d1.Display();d1 = 2019;d1.Display();
}
输出结果:
2016
2019
在这里同样打印出了d1,而且d1的值被改成了2019,这就是单参构造函数的隐式转换。语法上的d1=2019是先构造,在拷贝构造:
Date tmp(2019);
Date d1(tmp);
加上explicit屏蔽类型转换
class Date
{
public:explicit Date(int year = 1970) : _year(year) {}void Display();
private:int _year;
};
void Date::Display()
{std::cout << _year << std::endl;
}
2.析构函数
①构造函数的概念
析构函数是一种特殊的成员函数,当对象的生命周期结束时调用,用于释放对象获取的资源。在C++中,当对象超出范围或被显式删除时,会自动调用析构函数。
②析构函数特性
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 析构函数不能重载。
- 先构造的后析构,后构造的先析构。(类似栈)
析构函数代码如下:
class SeqList
{
public:SeqList(int default_capacity = 10);~SeqList();//析构函数完成对类的资源清理
private:int* a;int capacity;int size;
};
//构造函数
SeqList::SeqList(int default_capacity)
{a = (int*)malloc(sizeof(SeqList) * default_capacity);assert(a);capacity = default_capacity;size = 0;
}
//析构函数
SeqList::~SeqList()
{free(a);a = nullptr;capacity = 0;size = 0;
}
3.拷贝构造函数
①拷贝构造函数的概念
拷贝构造函数是一个特殊的构造函数,用于创建一个对象的副本。它通常是通过另一个对象的引用(一般常用const修饰)作为参数来定义的。拷贝构造函数的作用是创建一个新的对象,该对象与原始对象具有相同的值和属性。
例如:
class Date
{
public:Date(int year = 1970, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {};//构造函数Date(const Date& copy);//拷贝构造函数void Display();
private:int _year;int _month;int _day;
};
Date::Date(const Date& copy)
{_year = copy._year;_month = copy._month;_day = copy._day;
}
void Date::Display()
{std::cout << _year << " ";std::cout << _month << " ";std::cout << _day << " " << std::endl;
}
//...
Date d1(2003, 9, 17);
Date d2(d1);
d1.Display();
d2.Display();
输出结果:
2003 9 17
2003 9 17
②拷贝构造函数的特性
-
拷贝构造函数的参数有且仅有一个且必须使用引用传参,使用传值会引发无穷递归调用。
使用传值方式时,形参是实参的一份拷贝,会引起拷贝的递归调用。
-
当创建一个对象时,如果没有指定拷贝构造函数,则会使用编译器自动生成的默认拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
-
如果一个类包含指针成员变量,则需要手动实现拷贝构造函数(默认拷贝会指向同一个地址),以确保在拷贝对象时,指针所指向的内存地址也得到复制。
-
拷贝构造函数的作用是在创建新对象时对其进行初始化,因此它通常被定义为公有函数。
-
如果不希望某个类的对象被拷贝,可以将拷贝构造函数声明为私有或删除它,从而禁止该类对象的复制。
4.运算符重载
①运算符重载的意义
运算符重载的意义是为了让程序员能够自定义类型的运算符,使得这些类型的对象可以像内置类型一样进行运算。
定义的语法:返回值类型 operator+操作符(参数列表)
例如实现以下功能:
Date d1(2003, 9, 17);
Date d2;
d1 = d2;//像内置类型一样进行运算
②运算符重载的性质
- 运算符重载不能新定义运算符,只能对已有的运算符进行重载。比如:operator@。
- 重载操作符必须有一个类类型或者枚举类型的操作数。
- 用于内置类型的操作符,其含义不能改变。例如:内置的整型+,不能改变其含义。
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的。
(因为左操作数[this指向的对象]被隐藏) - 操作符有一个默认的形参this,限定为第一个形参。
- .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
③运算符重载的使用
【关于把运算符重载定义在类外】
运算符重载可以在类外进行定义和实现,但是必须是类的友元函数或者全局函数。类的友元函数可以访问类的私有成员,因此可以方便地进行运算符重载。需要注意的是,对于某些运算符重载,例如赋值运算符,建议在类内进行实现,以确保正确性和安全性。
代码如下:
class Date
{
public:Date(int year = 1970, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}Date(const Date& copy);constexpr bool operator==(Date& rhs) const{return _year == rhs._year && _month == rhs._month && _day == rhs._day;}
private:int _year;int _month;int _day;
};
Date::Date(const Date& copy)
{_year = copy._year;_month = copy._month;_day = copy._day;
}
int main()
{Date d;Date d1(2003, 9, 17);Date d2(d1);cout << (d == d1) << " ";cout << (d1 == d2);return 0;
}
输出结果:
0 1
④赋值运算符重载
赋值运算符主要有五点:
- 参数类型。
- 返回值。
- 检测是否自己给自己赋值。
- 返回*this。
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。注意默认赋值运算符重载为浅拷贝,无法深拷贝。
代码如下:
class Date
{
public:Date(int year = 1970, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}Date(const Date& copy);Date& operator=(const Date& rhs){if (this != &rhs){_year = rhs._year;_month = rhs._month;_day = rhs._day;}return *this;}void Display();
private:int _year;int _month;int _day;
};
Date::Date(const Date& copy)
{_year = copy._year;_month = copy._month;_day = copy._day;
}
void Date::Display()
{std::cout << _year << " ";std::cout << _month << " ";std::cout << _day << " " << std::endl;
}
int main()
{Date d1(2003, 9, 17);Date d2;d2 = d1;d1.Display();d2.Display();return 0;
}
输出结果:
2003 9 17
2003 9 17
5.const成员函数
①const修饰类的成员函数
const放置末尾 修饰的是隐藏参数this指向的对象
附:构造函数和析构函数无法使用const修饰
若用const修饰定义于类外的成员函数,需要类内的声明和类外的定义都加上const。
代码如下:
class Date
{
public:Date(int year = 1970, int month = 1, int day = 1): _year(year), _month(month), _day(day) {}Date(const Date& copy);Date& operator=(Date& rhs){if (this != &rhs){_year = rhs._year;_month = rhs._month;_day = rhs._day;}return *this;}void Display() const;
private:int _year;int _month;int _day;
};
Date::Date(const Date& copy)
{_year = copy._year;_month = copy._month;_day = copy._day;
}
void Date::Display() const
{std::cout << _year << " ";std::cout << _month << " ";std::cout << _day << " " << std::endl;
}
②const修饰类的对象和成员函数
思考题:
- const对象可以调用非const成员函数吗?
不可以,因为const修饰的对象为只读类型,若调用非const非成员函数,属于权限放大行为,只读权限变成既可以只读又可以可写。 - 非const对象可以调用const成员函数吗?
可以,因为非const对象权限拥有可读、可写权限,调用const成员函数属于权限缩小问题,权限变为只读。 - const成员函数内可以调用其它的非const成员函数吗?
不可以,因为const修饰的成员函数为只读类型,若调用非const非成员函数,属于权限放大行为,只读权限变成既可以只读又可以可写。 - 非const成员函数内可以调用其它的const成员函数吗?
可以,因为非const成员函数权限拥有可读、可写权限,调用const成员函数属于权限缩小问题,权限变为只读。
还是那句话,权限只可以缩小,不能够放大。也就是我本身只能是可读的(const),不能传过去编程可读可写的了(非const)。
6.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
代码如下:
#include<iostream>
using namespace std;
class Date
{
public:Date* operator&(){return this;}const Date* operator&()const{return this;}
private:int _year; // 年int _month; // 月int _day; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
七、static的类成员
1.static的类成员概念
在C++中,静态类成员是与类本身关联的成员,而不是与类的任何实例或对象关联的成员。它们可以通过类名和作用域解析运算符(::)来访问,而不是通过对象或实例来访问。
静态成员变量必须在类定义之外进行初始化,可以在定义类的源文件中进行初始化,或者在单独的源文件中进行初始化。这通常使用作用域解析运算符来完成。
2.static的类成员特性
1.静态成员在静态区存放,为所有类对象所共享,不属于某个具体的实例。
代码如下:
//...
class Test
{
private:static int _n;int a;
};
int main()
{std::cout << sizeof(Test) << std::endl;return 0;
}
输出结果:
4
2.静态成员变量必须在类外定义,定义时不添加static关键字。
class Test
{
public:
private:static int _n; //静态成员的声明int a;
};
int Test::_n=10;
3.类静态成员即可用类名::静态成员或者对象.静态成员来访问。
①静态成员变量公有
公有1.通过类实例化对象突破类域进行访问
公有2.通过匿名对象突破类域进行访问
公有3.通过类名突破类域进行访问
//...
class A
{
public:static int _n;
};
//...
A a;对类A实例化
//...
a._n //公有1.通过类实例化对象突破类域进行访问
A()._n //公有2.通过A()创建的匿名对象来突破类域进行访问
A::_n //公有3.通过类名突破类域进行访问
②静态成员变量私有
私有1.通过实例化的对象调用成员函数进行访问
私有2.通过匿名对象调用成员函数进行访问
私有3.通过类名调用静态成员函数进行访问
// ...
class A
{
public:static int GetN(){return _n;}
private:static int _n;
};
// 静态成员变量的定义初始化
int A::_n = 10;
//...
a.GetN() //私有1.通过实例化的对象调用成员函数进行访问
A().GetN() //私有2.通过匿名对象调用成员函数进行访问
A::GetN() //私有3.通过类名调用静态成员函数进行访问
4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
无this指针指定
【问题】
静态静态成员函数可以调用非静态成员函数吗?
不可以,静态成员函数没有隐藏的this指针,不能访问任何非静态成员。非静态成员函数可以调用类的静态成员函数吗?
可以,非静态成员函数和静态成员函数都在类中,在类中不受访问限定符的限制。
八、友元函数和友元类
1.友元函数及友元类的简介
友元函数和友元类可以访问类的私有成员和保护成员。
友元函数不是类的成员函数,但可以访问类中的所有成员,而一般函数只能访问类中的公有成员。
友元类是指在类定义中使用friend关键字声明的另一个类,该类可以访问声明它为友元的类的私有成员和保护成员。
优点:
- 友元函数和友元类可以访问类的私有成员和保护成员,从而增加了程序的灵活性。
- 友元函数和友元类可以减少代码量,提高代码复用性。
缺点:
- 友元函数和友元类破坏了类的封装性和隐藏性,降低了程序的安全性。
- 友元函数和友元类增加了程序的复杂度,使程序难以维护。
2.友元函数
①引申问题
问题:试图重载cout函数为成员函数,但是无法进行。
因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。 但是实际使用中cout需要是第一个形参对象,才能正常使用。
【扩展】
>>
是 C++ 中的流提取运算符(stream extraction operator),它和流插入运算符<<
一样都是用于输入/输出流的运算符。
>>
运算符用于从输入流中提取数据,将数据存储到变量中。它的语法如下:istream& operator>>(istream& is, T& obj);
其中,
is
是一个输入流对象,T
是要提取数据的变量类型,obj
是要存储数据的变量。
这时候可以将cout重载为全局函数,但是同时也失去了访问private成员的权限,这时候可以使用友元函数来解决。
②友元函数的使用
friend关键字 + 函数声明
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数。
- 友元函数不能用const修饰。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用与普通函数的调用和原理相同。
代码如下:
//友元函数的使用
class Date
{friend std::ostream& operator<<(std::ostream& _cout, const Date& d);friend std::istream& operator>>(std::istream& _cin, Date& d);
public:Date(int year, int month, int day): _year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
//友元函数的定义
std::ostream& operator<<(std::ostream& _cout, const Date& d)
{_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}
std::istream& operator>>(std::istream& _cin, Date& d)
{_cin >> d._year;_cin >> d._month;_cin >> d._day;return _cin;
}
int main()
{Date d(2021, 6, 8);cin >> d;cout << d << endl;return 0;
}
输入:
2003 9 17
输出:
2003-9-17
3.友元类
特点:
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
- 比如下述A类和B类,在A类中声明B类为其友元类,那么可以在B类中直接访问A类的(私有/保护)成员变量,但想在A类中访问B类中私有的成员变量则不行。
- 友元关系不能传递,如果B是A的友元,C是B的友元,则不能说明C时A的友元。
class B; // 前置声明class A {friend class B; // 声明B为友元类private:int private_member_;protected:int protected_member_;
};class B {
public:void AccessPrivateMember(A& a){a.private_member_ = 1; // 可以访问A的私有成员}void AccessProtectedMember(A& a){a.protected_member_ = 2; // 可以访问A的保护成员}
};
九、内部类
1.内部类的概念
C++中的内部类是在一个类中定义的另一个类。内部类可以访问外部类的私有成员和方法,但是外部类不能直接访问内部类的私有成员和方法。内部类可以像普通类一样拥有成员变量、成员函数、构造函数和析构函数等。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
2.内部类的特性
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
- sizeof(外部类) = 外部类,和内部类没有任何关系。
3.内部类的实现
内部类的定义通常在外部类的声明中,例如:
class OuterClass {
public:// 外部类的公共成员和方法class InnerClass {public:// 内部类的公共成员和方法private:// 内部类的私有成员和方法};private:// 外部类的私有成员和方法InnerClass inner; // 内部类的对象
};
可以通过以下方式创建内部类的对象:
OuterClass outer; // 外部类的对象
OuterClass::InnerClass inner; // 内部类的对象
outer.innerMethod(); // 调用外部类的方法访问内部类的成员