网站建设方法氵金手指排名27,网页设计教程ppt封面图片,iis 多网站,宁波网红打卡地类和对象目录#xff1a;
一、面向过程和面向对象初步认识
二、类的引入定义#xff08;struct class#xff09;
2.1自定义类型 struct 和 class 的区别
2.2类放在内存中的什么存储区#xff1f;
2.3类中函数定义的方式
2.3.1声明和定义分离#xff08;增强代…类和对象目录
一、面向过程和面向对象初步认识
二、类的引入定义struct class
2.1自定义类型 struct 和 class 的区别
2.2类放在内存中的什么存储区
2.3类中函数定义的方式
2.3.1声明和定义分离增强代码可读性强烈推荐
2.3.2声明和定义一起隐式内联函数
2.3.3类中变量的声明方式
三、类的访问限定符封装作用域
3.1访问限定符
3.2封装
3.3类作用域
四、类的实例化类 类型创建对象的过程计算类的大小考虑内存对齐
4.1什么是类什么是对象
4.2类大小计算的疑惑点
4.3类对象的存储方式
4.4结构体内存对齐规则必须会面试常问
4.5空类大小的计算面试常考
五、隐藏的this指针
5.1非静态成员函数的this指针
5.2this指针的特点
5.3this指针存在哪里
5.4关于this指针为空的问题
六、类的六个默认成员函数
6.1构造函数初始化和对象内的资源申请不是对象的创建
6.1.1特性分析 - 自动生成特性5
6.1.2特性分析 - 选择处理
6.1.3特性分析 - 默认构造特性7
6.1.4C11补丁 - 成员变量的缺省值是声明不是初始化
6.2析构函数是对象的销毁不是对象中资源的清理
6.3拷贝构造已经存在的对象初始化创建出一个对象
6.3.1拷贝构造函数是什么
6.3.2拷贝构造函数怎么写
6.3.3拷贝构造参数为什么加const
6.3.4深浅拷贝简单讲解
6.3.5调用拷贝构造的场景
6.3.6怎么提高程序效率
6.4运算符重载不是默认成员函数
6.4.1引入
6.4.2运算符重载函数的位置
6.4.3运算符重载的特性
6.4.4常见的运算符重载
6.5赋值重载是默认成员函数
6.5.1基础知识
6.5.2特性分析 - 函数格式
6.5.3特性分析 - 重载为成员函数避免冲突
6.5.4特性分析 - 深浅拷贝 6.6取地址及const取地址重载这俩个函数不重要但是const成员重要
6.6.1const成员函数
6.6.2取地址重载
6.6.3const取地址重载
6.6.4 流插入 和 流提取 流插入与流提取的运算符重载
6.7总结
七、初始化列表声明远远不够还得定义对象
7.1什么声明是初始化列表
7.2 初始化变量的小细节
7.3什么时候必须使用初始化变量重点
7.4初始化列表使用建议
7.5成员变量在类中声明次序就是其在初始化列表中的初始化顺序与其在初始化列表中的先后次序无关
八、explict关键字
8.1关于类型转换
8.2如何防止隐式类型转换
九、static成员
9.1概念
九、static成员变量与函数
9.1概念
面试题实现一个类计算程序中创建出了多少个类对象
9.2static 成员变量
9.2.1特性1
9.2.2特性2类内声明类外初始化
9.2.3特性3 9.3static成员函数有点成也萧何败也萧何的感觉
十、友元
10.1输入输出的重载细节在上方运算符重载最后一条
10.2友元函数
十一、内部类
十二、匿名对象偷懒专用
十三、编译器优化在static中就已经有所提及 一、面向过程和面向对象初步认识
C语言是面向过程的关注的是过程分析出求解问题的步骤通过函数调用逐步解决问题而C是基于面向对象的关注的是对象将一件事情拆分成不同的对象靠对象之间的交互完成。我们以洗衣服为例
面向过程 – 逐步求解问题 面向对象 – 通过对象之间的交互解决问题 又比如我们的外卖系统面向过程关注的是顾客应该如何下单、商家应该如何做出菜品、骑手应该如何将外卖送达而面向对象关注的是顾客、商家、骑手这四个对象之间的交互比如顾客下单后商家出餐然后骑手送餐而不必关心顾客如何下单、商家如何出餐、骑手如何送达这类面向过程的问题 二、类的引入定义struct class
2.1自定义类型 struct 和 class 的区别
C语言中的struct只能定义变量但C中的struct不仅可以定义变量也可以定义函数他和class关键字的作用是相同的都可以定义类当然这是C为了兼容C语言对struct做出的改变所以struct所定义的类的成员默认是公有的就是无论谁都可以使用因为在C语言中我们是可以直接获取到结构体里面的所有数据的
C中class定义的类的成员默认是私有的类内不被限制类外不可以直接获取到类中的变量也就是类中的数据,这也正是面向对象语言的一大特性封装你只能通过类中的函数来访问数据不可以直接访问到类里面的数据
//C语言
struct Student
{char name[20];char id[11];int weight;int height;
};int main()
{struct Student stu1 { zhangsan, 2202101001, 60, 180 };
}//C
struct Stack
{//类体由成员函数和成员变量组成void Init(int N 4){top 0;capacity 0;// 访问限定符限制的是类外面的类里面不会被限制}void Push(int x){}int* array;int top;int capacity;//C把类看作一个整体编译器搜索的时候会在整个类里面去搜索。C语言为了编译的效率只会向上搜索。
};//不要丢掉分号这里的struct是C中的用法不仅可以定义函数还可以定义变量
小羊注C结构体直接使用 structName 代表类而不用加 struct 关键字但是C兼容C语言结构体的全部用法使用我们之前使用 struct structName 的方式定义变量也是没问题的
typedef struct ListNode
{/*struct*/ListNode* next; //SListNode 可以直接代表这个类所以此处可以不用加 structint data;
}SL;int main()
{//C语言用法 加tructstruct ListNode* sl1;SL* sl2;//C用法 直接结构体名字上ListNode* sl3;
} 最后在C中更喜欢用 class 来代替 struct并且把变量称为属性/成员变量把函数称为成员函数/成员方法
2.2类放在内存中的什么存储区
a. 首先大家要知道类只是一个抽象的描述对象才是一个具体的东西就像intdoublechar等类型他们也只是一个描述他们所定义出来的变量才是具体的东西所以程序中是不存在类这样的东西的因为它不是具体化的东西自然类是不占用内存的。
b. 类的描述信息程序在编译的时候只要其语义运行时是不需要他的因为到运行阶段的时候代码都已经转换为二进制指令了二进制指令命令电脑去做对象与对象之间的交互哪还有类这样的概念。当然如果要用类中的成员函数成员函数是在代码段的所以光对于类来说他仅仅是方便程序猿写程序的一个抽象化描述
c. 在C的程序中编译链接之后的指令宏观角度来说其实是对象之间利用接口进行交互以及单个对象和接口之间的交互这种宏观的概念里面哪还有类啊只要编译器了解类的语义之后类就没用了所以根本没有必要分配给它内存。
总的来说类只是方便人来解决问题的对于编译器该怎么走怎么走所以不分配内存给类
2.3类中函数定义的方式
class className
{//...
};类体中的内容称为类的成员
类中的变量称为类的属性或成员变量
类中的函数称为类的方法或者成员函数
2.3.1声明和定义分离增强代码可读性强烈推荐
a. 声明和定义分离的话那函数就会被当作正常函数对待在调用的地方建立函数栈帧分配堆栈的空间。 b. 函数定义时的函数名前要加上类域名
//使用时要指定类域
class Person
{
public:void PrintPersonInfo();
private:char _name[20];char _gender[3];int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{cout _name _gender _age endl;
}甚至为了更加的模块化还可以这样做类声明放在.h文件中成员函数定义放在.cpp文件中 (注意成员函数名前需要使用类名域限定符) 2.3.2声明和定义一起隐式内联函数
class Person
{
public:void PrintPersonInfo() //inline void PrintPersonInfo()
{cout _name _gender _age endl;
}
private:char _name[20];char _gender[3];int _age;
}; void PrintPersonInfo() //inline void PrintPersonInfo()
如果声明和定义都放在类里面函数会向编译器发出内联请求是否同意这取决于编译器这个知识了解一下就好
2.3.3类中变量的声明方式
推荐加下划线的方式来声明成员变量
class Person
{
public:void PrintPersonInfo();
private:char _name[20];char _gender[3];int _age;
};三、类的访问限定符封装作用域
3.1访问限定符 public 修饰的成员在类外可以直接被访问protected 和 private 修饰的成员在类外不能直接被访问 (此处 protected 和 private 是类似的)访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止如果后面没有访问限定符作用域就到 } 即类结束class 的默认访问权限为 privatestruct 为 public (因为struct要兼容C)
注意访问修饰限定符限定的只是类外的访问权限类内可以随意访问并且访问限定符只在编译时有用当数据映射到内存后没有任何访问限定符上的区别 3.2封装
将数据和操作数据的方法进行有机结合隐藏对象的属性和实现细节仅对外公开接口来和对象进行交互。
封装本质上是一种管理让用户更方便使用类在数据结构初阶时我们曾用C语言来实现栈其中关于返回栈顶元素的函数接口 – Top就很好的体现了封装的作用 3.3类作用域
类定义了一个新的作用域类的所有成员都在类的作用域中。在类体外定义成员时需要使用 :: 作用域操作符指明成员属于哪个类域 注意类域和我们之前学习的命名空间域不同命名空间域中存放的是变量和函数的定义而类域中虽然可以定义函数但对于变量来说仅仅只是声明并没有为变量开辟空间只有用这个类实例化出的对象才会开辟空间这也就是为什么结构体和类中的成员变量都不能直接初始化而是必须先定义出变量的原因
所以类域调用函数是非法的必须创建对象 运算符类是.结构体是- 四、类的实例化类 类型创建对象的过程计算类的大小考虑内存对齐
4.1什么是类什么是对象
声明是对成员变量private中写的内容就叫做声明也就是告知有哪些家庭成员
定义对象因为类的本质就是图纸模板框架所以叫做定义对象
创建对象就是造房子有了类创建好了对象
一个最形象的例子如果类是图纸对象就是房子类中的成员变量就是一个个房子中的成员函数就像是小区中的公共健身器材函数怎么存储下面4.3内容会有详细讲解不要错过 1、类是对对象进行描述的是一个模型一样的东西限定了类有哪些成员定义出一个类并没有分配实际的内存空间来存储它比如入学时填写的学生信息表表格就可以看成是一个类来描述具体学生信息 2、一个类可以实例化出多个对象实例化出的对象占用实际的物理空间存储类成员变量
类实例化出对象就像现实中使用建筑设计图建造出房子类就像是设计图只设计出需要什么东西但是并没有实体的建筑存在同样类也只是一个设计实例化出的对象才能实际存储数据占用物理空间
4.2类大小计算的疑惑点
在C语言阶段我们学习了如何计算一个结构体类型的大小那么对于升级版的结构体 – 类来说类中既可以有成员变量又可以有成员函数那么一个类的对象中包含了什么我们又如何计算一个类的大小
class A
{
public:void PrintA()// 从操作系统角度来看不可能把所有的指令都存下来只存函数的地址指针大小4个字节所以大小应该是5个字节内存对齐就是8个字节{cout _a endl;}
private:int _a;
};
int main()
{cout sizeof(A) endl;
}在类中甚至还有成员函数需要计算那么类的大小看起来非常复杂但我认为读者们看到结构体内存对齐的时候就一定可以豁然开朗了请看我慢慢分解~
4.3类对象的存储方式
a.
为了节省实例化对象所占空间我们将每个对象的成员函数抽离出来放在公共代码段这样在使用函数时每个对象只要去公共代码段里面调用就可以了里面放着该类所有成员函数的有效地址
b.
函数经过编译后形成的指令是由编译器放置到代码段中去的所以编译器在调用该函数时也能轻松的找到指令在代码段中所处的位置并且编译器并也不会将不同类中成员函数所形成的指令混淆
即一个类的大小实际就是该类中 ”成员变量” 之和
验证
class A
{
public:void PrintA(){cout _a endl;}
private:char _a;
};int main()
{cout sizeof(A) endl;
}我们看到类A的大小是1说明只存储了成员变量 _a 的地址而并没有存储成员函数 PrintA 的地址
4.4结构体内存对齐规则必须会面试常问
类大小的计算方式和结构体大小的计算方式完全相同都需要进行内存对齐
点击进入结构体内存对齐规则http://t.csdn.cn/5KtHd
4.5空类大小的计算面试常考
上面我们探讨的是普通类的大小那么对于一些特殊的类比如空类或者是只有成员函数没有成员变量的类他们的大小是多少呢是0还是别的值
class B
{
};class C
{void PrintC(){cout PrintC() endl;}void InitC(){cout InitC() endl;}
};int main()
{cout sizeof(B) endl;cout sizeof(C) endl;
}首先函数体不占用空间这个我们都知道那么为什么空类的大小是1原因1即使是空类也可以创建对象所以赋予大小。原因2是为了占位 五、隐藏的this指针
5.1非静态成员函数的this指针
a. 我们知道一个成员函数是可以被多个对象所调用的那成员函数怎么知道它现在作用的是哪个对象呢万一有10个对象都调用同一个函数函数只作用于第一个对象这可怎么办啊无法满足我们的需求啊
b. 所以C编译器给每个“非静态成员函数”增加了一个隐藏的this指针来作为函数的形参 并且规定该参数必须在函数形参的最左边位置这个指针中存储的就是对象的地址在函数体中所有访问对象成员变量的操作都是通过this指针来完成的只不过这些操作对于用户是透明的用户不需要手动传递对象地址编译器可以自动完成哪个对象调用就传递哪个对象的地址给this
c. 我们不可以手动去传递this指针这是编译器的工作我们不能去抢但是我们可以在函数体内部使用这个this指针
验证
#include stdio.h
#includeiostream
using namespace std;
class Date
{
public:void Init(int year, int month, int day){_year year;//使用this指针访问到对象的成员变量_year_month month;_day day;}void Print()// 在函数体内部我们可以使用this指针。{cout this endl;cout this-_year - this-_month - _day endl;//我们加了this编译器就不加了我们不加编译器就会加}
private:int _year; int _month; int _day;
};
int main()
{Date d1;d1.Init(2023, 9, 9);Date d2;d2.Init(2023, 9, 10);d1.Print();//d1调用访问的就是d1的成员d2.Print();//d2调用访问的就是d2的成员cout d1 endl;cout d2 endl;return 0;
}由结果可以看到this指针的的确确就是对象的地址 5.2this指针的特点 this 指针只能在 “成员函数” 的内部使用this 指针使用 const 修饰且 const 位于指针*的后面即 this 本身不能被修改但可以修改其指向的对象 (我们可以通过 this 指针修改成员变量的值但不能让 this 指向其他对象)this 指针本质上是“成员函数”的一个形参当对象调用成员函数时将对象地址作为实参传递给 this 形参所以对象中不存储this 指针this 指针是“成员函数”第一个隐含的指针形参一般情况由编译器通过建立“成员函数”的函数栈帧时压栈传递不需要用户主动传递。(注由于this指针在成员函数中需要被频繁调用所以VS对其进行了优化由编译器通过ecx寄存器传递) 有关const和指针的三种写法 const Date* p1;
Date const* p2; 上面这两种写法都是等价的const放在星号的左边修饰的就是 * p1和* p2也就是指针指向的对象
Date* const p3;// 下面这种写法修饰的是p3const放在*右边修饰的是指针变量p3本身
我们的this指针用的就是第二种方法因为this指针的所指不可以改
5.3this指针存在哪里
我们的第一反应都是存在对象中那么问题来了我们计算类的大笑的时候难道还计算this指针了吗说明this指针一定不在对象中存放
答案this 指针作为函数形参存在于函数的栈帧中而函数栈帧在栈区上开辟空间所以 this 指针存在于栈区上不过VS这个编译器对 this 指针进行了优化使用 ecx 寄存器保存 this 指针
小羊注如果成员函数被当成内联处理this就不存在也不会存在存在哪里的问题了也就是说调用的位置直接展开了比如内部的_str直接替换为s._str这样不需要this指针也可以进行访问
5.4关于this指针为空的问题
this 指针作为参数传递时是可以为空的但是如果成员函数中使用到了空的this 指针那么就会造成对空指针的解引用
//下面两段程序编译运行结果是 A、编译报错 B、运行崩溃 C、正常运行
class A //程序1
{
public:void Print(){cout Print() endl;}
private:int _a;
};int main()
{A* p nullptr;p-PrintA();return 0;
}
//***********************************//
class A //程序2
{
public:void PrintA(){cout _a endl;}
private:int _a;
};int main()
{A* p nullptr;p-Print();return 0;
}答程序1正常运行。原因如下
第一虽然我们用空指针A访问了成员函数Print但是由于成员函数并不存在于对象中而是存在于代码段中所以编译器并不会通过类对象p去访问成员函数即并不会对p进行解引用
第二当对象是指针类型时编译器会直接把这个指针作为形参传递给Print函数的 this 指针而 this 作为参数传递是时可以为空的在Print函数内部我们也并没有对 this 指针进行解引用 程序2运行崩溃。原因如下
程序2在 p-Print 处虽然可以正常运行但是在Print函数内部_a 会被转化为 this-_a发生了空指针的解引用this是p传递的p是空所以存在空指针的解引用 六、类的六个默认成员函数
我们上面提到过类型占一个字节的空类空类中什么都没有吗还是他有但是我们看不到 其实空类中是有东西的他有编译器默认生成的6个成员函数如果我们不主动去写默认成员函数编译器是会自动生成他们的
在使用C语言练习初阶数据结构即线性表、链表、栈、队列、二叉树、排序等内容时大家可能会经常犯两个错误特别是第二个错误可以说是十分普遍 在使用数据结构创建变量时忘记对其进行初始化操作而直接进行插入等操作在使用完毕后忘记对动态开辟的空间进行释放而直接返回 而C是在C语言的基础上生长起来的 – 修正C语言中的一些不足并加入面向对象的思想面对上面C语言存在的问题C设计出了默认成员
默认成员函数当用户没有显式实现时编译器会自动生成的成员函数称为默认成员函数 6.1构造函数初始化和对象内的资源申请不是对象的创建
构造函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的任务并不是创建对象而是当对象被创建之后完成对象的初始化工作同时构造函数不能由用户调用而是在创建类类型对象时由编译器自动调用并且在对象整个生命周期内只调用一次
构造函数有如下特性 函数名与类名相同无返回值对象实例化时编译器自动调用对应的构造函数构造函数支持重载与缺省参数如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数但一旦用户显式定义编译器将不再自动生成构造函数对内置类型不处理对自定义类型调用它自身的默认构造无参的构造函数和全缺省的构造函数都称为默认构造函数并且默认构造函数只能有一个容易晕 6.1.1特性分析 - 自动生成特性5
如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数但一旦用户显式定义编译器将不再自动生成现在让我们验证
class Date
{
public:void Print(){cout _year - _month - _day endl;}private:int _year;int _month;int _day;
};可以看到上面的日期类中我们是没有显式的去自己实现构造函数的所以编译器应该会自己生成一个无参的默认构造函数完成初始化工作 但是我们发现一个问题默认的构造函数好像并没有完成初始化工作即d1对象中的_year,_month,_day仍然是随机值那么是因为这里编译器生成的默认构造函数并没有什么用吗这个问题我们需要构造函数的第六个特性来回答
6.1.2特性分析 - 选择处理 构造函数的第六点特性如下构造函数对内置类型不处理对自定义类型调用它自身的默认构造
对于这个特性我们使用 Date、Stack 和 Myqueue 三个类来对比理解
Myqueue是232. 用栈实现队列 - 力扣LeetCode中的题目
注意在vscode中调试的时候一定得先打断点再调试
Date
class Date
{
public:void Print(){cout _year - _month - _day endl;}private:int _year;int _month;int _day;
};Stack
class Stack
{
public:Stack(int capacity 4){_a (int*)malloc(sizeof(int) * capacity);if (_a nullptr){perror(malloc fail\n);exit(-1);}_top 0;_capacity capacity;cout Stack 构造 endl;}void Push(int x){_a[_top] x;}private:int* _a;int _top;int _capacity;
};Queue
class MyQueue
{
public:void Push(int x){_pushST.Push(x);}Stack _pushST;Stack _popST;
};分析1.Stack 的成员变量全部为内置类型所以当我们不显式定义构造函数时编译器自动生成一个默认构造函数但默认生成的构造函数并不会对内置类型进行处理所以这里我们看到的是随机值Date 类的情况也是如此也就是说编译器自动生成的构造函数不能满足我们的需求所以我们需要手动定义构造函数 2.而对于MyQueue来说它的成员变量全部为自定义类型所以即使我们不提供构造函数时编译器自动生成的构造函数也会去调用自定义类型的默认构造满足需求 3.那么到底什么时候需要我们自己提供构造函数什么时候使用编译器默认生成的构造函数呢难道是内置类型全部自己定义自定义类型全部使用默认生成的吗答案是面向需求 – 当编译器默认生成的构造函数就能满足我们的需求时我们就不需要自己提供构造函数如MyQueue当编译器提供/的构造函数不能满足我们的需求时就需要我们自己定义如Date/Stack
6.1.3特性分析 - 默认构造特性7
这里有两个需要注意的地方
1、构造函数虽然支持重载和缺省参数但是无参构造和有参全缺省构造不能同时出现因为在调用时会产生二义性注意我这里说的是全缺省 同时当参数有多个时可以构成很多个重载使得构造函数变得十分冗余所以一般我们只会显式定义一个全缺省的构造函数因为这一种就可以构造就可以代表很多种参数情况 2、当我们调用无参构造或者全缺省构造来初始化对象时不要在对象后面带括号这样使得编译器分不清这是在实例化对象还是函数声明一定要注意 好的现在让我们继续讲解特性7 构造函数的第七点特性如下无参的构造函数和全缺省的构造函数都称为默认构造函数并且默认构造函数只能有一个 上面这句话的意思就是当我们使用无参的方式实例化一个对象时编译器会自动去调用该对象的默认构造函数而默认构造函数有三种编译器自动提供的无参构造函数、显式定义的无参构造函数、显式定义的全缺省的构造函数 如果类中没有默认构造函数那么我们实例化对象时就必须传递参数 Date d1(2023,9,28); //这样就可以通过编译
6.1.4C11补丁 - 成员变量的缺省值是声明不是初始化
本质上是为了弥补编译器自己生成的默认构造函数是个废物的漏洞我创建对象不写构造函数确实编译器自己有但是我开了还没有初始化这开的个屁
经过上面的学习我们发现自动生成的默认构造函数对内置类型不处理对自定义类型要处理的特性使得构造函数变得很复杂因为一般的类都有需要初始化的内置类型成员变量这就使得编译器默认生成的构造函数看起来没什么作用
C11 中针对内置类型成员不初始化的缺陷又打了补丁即内置类型成员变量在类中声明时可以给缺省值缺省值的意思就是如果构造函数没有对该变量进行初始化那么该变量就会使用缺省值 总结这里对成员变量给定缺省值并不是对其初始化因为类中的成员变量只是声明只有当实例化对象之后它才具有物理空间才能存放数据而缺省一块动态内存也不难理解相当于我设计了一份房屋的图纸我知道某个房间具体要多大所以我可以在图纸上可以进行标注当实际建造房屋的时候根据标注给定大小即可 6.2析构函数是对象的销毁不是对象中资源的清理
特性 析构函数名是在类名前加上字符 ~。无参数无返回值类型。一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。注意析构函数不能重载。对象生命周期结束时也就是对象即将被销毁的时候一般随着栈帧的销毁对象生命也会结束这时候C编译系统会自动调用析构函数 a. 编译器默认生成的析构函数对于内置类型并不会处理在对象生命结束时操作系统会自动回收内置类型的内存但对于自定义类型编译器默认生成的析构函数会调用该类类型的析构函数。
b. 值得注意的是由于编译器默认生成的默认析构不会处理内置类型这也就为它功能的缺陷埋下了隐患例如内置类型中声明了malloc开辟在堆上的空间呢这种情况下继续依靠编译器默认生成的析构显然无法满足资源的清理工作这时候就需要我们手动去将申请的空间还给操作系统。例如栈类的析构函数就需要我们自己来写他的构造函数同样也需要我们自己来写因为编译器提供的默认构造无法满足我们的要求所以写不写析构函数依然是面向需求的一个问题
~Stack(){free(_array);_array nullptr;_top _capacity 0;} 6.3拷贝构造已经存在的对象初始化创建出一个对象
特性 拷贝构造函数是构造函数的一个重载形式当我们使用拷贝构造实例化对象时编译器不再调用构造函数拷贝构造函数的参数只有一个且必须是类类型对象的引用使用传值方式编译器直接报错因为会引发无穷递归调用若未显式定义编译器会生成默认的拷贝构造函数默认的拷贝构造函数对内置类型以字节为单位直接进行拷贝 – 浅拷贝对自定义类型调用其自身的拷贝构造函数 6.3.1拷贝构造函数是什么
它其实是构造函数的一个重载形式
6.3.2拷贝构造函数怎么写
拷贝构造函数的参数只有一个且必须是类类型对象的引用使用传值方式编译器直接报错因为会引发无穷递归调用 Date(const Date d) // 错误写法编译报错会引发无穷递归{_year d._year;_month d._month;_day d._day;} 用传值作为拷贝构造的形参会出现无穷调用的原因如下
这里有一个潜在的知识盲区就是传值会进行传值拷贝产生的临时变量就又会使用拷贝构造总的来说传值会对下面造成影响但是传引用就会实形一体不会有影响 你像递归里面也是呀有些地方你就需要传引用或者传指针这种它是同一个有些地方就要传拷贝你下一层的改变不影响上一层就是实践当中的逻辑需要等你做题做的更多时候写代码写的更多了才能体会。 下面这个图放大网页才可以看清楚 6.3.3拷贝构造参数为什么加const
拷贝构造函数的参数通常使用 const 修饰这是为了避免在函数内部拷贝出错类似下面这样 6.3.4深浅拷贝简单讲解
若未显式定义编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝这种拷贝叫做浅拷贝或者值拷贝
为什么要存在深拷贝
在Stack类中他的内置类型里面有指针此时一旦发生拷贝构造两个对象中的指针指向的就是同一块空间因为默认的不会开空间那么在在两个对象生命结束时所调用的析构函数就会讲相同指针所指向的空间释放两次第二次释放的地址就是一个无效的地址这块地址根本没有指向一块儿有效的空间自然程序就会出现错误 总结
如果类中没有资源申请则不需要手动实现拷贝构造直接使用编译器自动生成的即可如果类中有资源申请就需要自己定义拷贝构造函数否则就可能出现浅拷贝以及同一块空间被析构多次的情况
其实拷贝构造和函数析构函数在资源管理方面有很大的相似性可以理解为需要写析构函数就需要写拷贝构造不需要写析构函数就不需要写拷贝构造
6.3.5调用拷贝构造的场景 使用已存在对象创建新对象函数参数类型为类类型对象函数返回值类型为类类型对象 6.3.6怎么提高程序效率
为了提高程序效率一般对象传参时尽量使用引用类型返回时根据实际场景能用引用尽量使用引用 6.4运算符重载不是默认成员函数
6.4.1引入
为了增强代码的可读性C为自定义类型引入了运算符重载运算符重载是具有特殊函数名的函数 – 其函数 - 名为关键字operator需要重载的运算符符号也具有其返回值类型函数名字以及参数列表其返回值类型与参数列表与普通的函数类似换句话说运算符重载函数只有函数名特殊其他方面与普通函数一样
void operator(Date d, int day)
{d._day day;while (d._day GetMonthDay(d._year, d._month)){d._day - GetMonthDay(d._year, d._month);d._month;if (d._month 12){d._month - 12;d._year;}}
}6.4.2运算符重载函数的位置
如果大家实际上手编写我们上面的 AddDay 和 operator 函数就会发现一个问题类中的成员函数 _year、_month、_day 都是私有的我们在类外并不能直接修改它们 但是我们又不能直接把成员变量设为共有这样类的封装线得不到保证那么如果我们把函数放到类里面呢 上面这种情况是由我们在上方第五点 this指针提到的 this 指针引起的 – 类的每个成员函数的第一个参数都是一个隐藏的 this 指针它指向类的某一个具体对象且 this 不能显示传递也不能显示写出但是可以在函数内部显示使用
也就是说本来 这个操作符只能有两个操作数所以使用 operator 重载 得到的函数也只能有两个参数但是由于我们为了使用类的成员变量将函数放在了类内部所以编译器自动传递了对象的地址并且在函数中使用一个 this 指针来接收导致函数参数变成了三个所以出现了 “operator 的参数太多” 这个报错
那么为了解决这个问题我们在定义 operator 函数时就只显式的传递一个参数 – 右操作数而左操作数由编译器自动传递当我们在函数内部需要操作左操作数时也直接操作 this 指针即可 void operator(int day)
{_day day;while (_day GetMonthDay(_year, _month)){_day - GetMonthDay(_year, _month);_month;if (_month 12){_month - 12;_year;}}
} 总代码如下
#includeiostream
using namespace std;
class Date
{
public:Date(int year 1900, int month 1, int day 1){_year year;_month month;_day day;}
int GetMonthDay(int year,int month)
{static int day[13]{0,31,28,31,30,31,30,31,31,30,31,30,31};if((month2) ((year%40 year%100!0) || (year%4000)))return 29;return day[month];
}
void Print(){cout _year / _month / _day endl;}void operator(int day)
{_day day;while (_day GetMonthDay(_year, _month)){_day - GetMonthDay(_year, _month);_month;if (_month 12){_month - 12;_year;}}
}
private:int _year;int _month;int _day;
};int main()
{Date d1;d1100;d1.Print();return 0;
} 注意
1、当我们将函数放在类内部时不管操作数有几个this 默认指向第一个操作数
2、对于在类外部无法访问类的私有成员变量的问题其实也可以使用友元解决我们后面再学习
6.4.3运算符重载的特性 不能通过连接其他符号来创建新的操作符比如operator重载操作符必须有一个类类型参数 (因为运算符重载只能对自定义类型使用)用于内置类型的运算符其含义不能改变即不能对内置类型使用运算符重载作为类类的成员函数重载时其形参看起来比操作数数目少1是因为成员函数的第一个参数为隐藏的 this以下5个运算符不能重载 .* :: sizeof . ?: 注意这个经常在笔试选择题中出现特别是 .* 操作符希望大家记住是.* 不是* 6.4.4常见的运算符重载 常见的运算符重载有operator ()、operator- (-)、operator* (*)、operator/(/)、operator ()、operator- (-)、operator ()、operator ()、operator ()、operator ()、operator ()、operator ()、operator! (!)、operator ()、operator-- (–)等 其中对于 operator 和 operator-- 来说有一些不一样的地方 – 因为 和 – 分为前置和后置二者虽然都能让变量自增1但是它们的返回值不同但是由于 和 – 只有一个操作数且这个操作数还会由编译器自动传递所以正常的 operator 和 operator-- 并不能对二者进行区分最终C规定后置/–重载时多增加一个int类型的参数此参数在调用函数时不传递由编译器自动传递
其次上面重载函数中的 operator 就是默认成员函数之一 – 赋值重载函数 6.5赋值重载是默认成员函数
6.5.1基础知识
赋值重载函数是C的默认六个成员函数之一它也是运算符重载的一种它的作用是两个已存在的对象之间的赋值其特性如下 赋值重载的格式规范赋值运算符只能重载成类的成员函数不能重载成全局函数若未显式定义编译器会生成默认的赋值重载函数默认的赋值重载函数对内置类型以字节为单位直接进行拷贝 – 浅拷贝对自定义类型调用其自身的赋值重载函数 其中赋值重载和拷贝构造有一个很恶心的小题目
d1d2是复制拷贝 因为是已经存在的俩个对象
Date d3d2是初始化 因为一个存在一个不存在
那么Date d1d2 是拷贝构造还是赋值重载 答案是拷贝构造因为创建了d1对象
6.5.2特性分析 - 函数格式
赋值重载函数的格式一般有如下要求
Tip1使用引用做参数并以 const 修饰
我们知道使用传值传参时函数形参是实参的一份临时拷贝所以传值传参会调用拷贝构造函数而使用引用做参数时形参是实参的别名从而减少了调用拷贝构造在时间和空间上的消耗另外赋值重载只会改变被赋值对象而不会改变赋值对象所以我们使用 const 来防止函数内部的误操作
void operator(const Date d);Tip2使用引用做返回值且返回值为*this
我们可以对内置类型进行连续赋值 d1d2d3 所以我们要对函数的返回值做一定的约束和限制
同时由于我们的对象是类创建的赋值重载函数的工作完成的时候对象依然存在所以可以直接引用的返回值提高效率对象不在函数空间开辟作用域在类
另外我们一般用左操作数作为函数的返回值也就是this指针指向的对象综上所述我们需要返回*this
Date operator(const Date d);
{//...return *this;
}
Tip3检查是否给自己赋值
用户在调用成员函数时有可能发生下面这种情况Date d1; Date d2 d1; d1 d2; 这种情况对于只需要浅拷贝的对象来说并没有什么大碍但对于有资源申请需要进行深拷贝的对象来说就会发生不可控的事情
if(this d) //比较两个对象的地址是否相同return *this;综上所述
//赋值重载
Date operator(const Date d)
{//自我赋值if (this d) {return *this;}_year d._year;_month d._month;_day d._day;return *this;
}6.5.3特性分析 - 重载为成员函数避免冲突 赋值运算符只能重载成类的成员函数不能重载成全局函数这是因为赋值重载函数作为六个默认成员函数之一如果我们不显示实现编译器会默认生成此时用户如果再在类外自己实现一个全局的赋值运算符重载就会和编译器在类中生成的默认赋值运算符重载冲突从而造成链接错误 6.5.4特性分析 - 深浅拷贝 赋值重载函数的特性和拷贝构造函数非常类似 – 如果我们没有显式定义赋值重载则编译器会自动生成一个赋值重载且自动生成的函数对内置类型以字节为单位直接进行拷贝对自定义类型会去调用其自身的赋值重载函数
这里的情况和 Stack 默认析构函数的情况很类似但是比它要严重一些 – 自动生成的赋值重载函数进行浅拷贝使得 st1._a 和 st2._a 指向同一块空间而 st1 和 st2 对象销毁时编译器会自动调用析构函数导致 st2._a 指向的空间被析构两次同时st1._a 原本指向的空间并没有被释放所以还发生了内存泄漏 总结
自动生成的赋值重载函数对成员变量的处理规则和析构函数一样 – 对内置类型以字节方式按值拷贝对自定义类型调用其自身的赋值重载函数我们可以理解为需要写析构函数的类就需要写赋值重载函数不需要写析构函数的类就不需要写赋值重载函数 6.6取地址及const取地址重载这俩个函数不重要但是const成员重要
6.6.1const成员函数
我们将 const 修饰的 “成员函数” 称之为 const 成员函数const 修饰类成员函数实际上修饰该成员函数隐含的 this 指针表明在该成员函数中不能对 this 指向的类中的任何成员变量进行修改 当我们定义一个只读Date对象时我们再去调用 d 的成员函数 Print 时编译器会报错
原因在于类成员函数的第一个参数默认是 this 指针而 this 指针是 Date* const this限制的是this但是Date*不受限制而我们的第一个参数即 d 的类型是 const Date*限制类型说明只读不可写将一个只读变量赋值给一个可读可写的变量时权限扩大导致编译器报错
为了解决上面这个问题C 允许我们定义 const 成员函数即在函数最后面使用 const 修饰该 const 只修饰函数的第一个参数即使得 this 指针的类型变为 const Date* const this函数的其他参数不受影响 所以当我们在实现一个类时如果我们不需要改变类的成员函数的第一个参数即不改变 *this那么我们就应该使用 const 来修饰 this 指针以便类的 const 对象在其他 (成员) 函数中也可以调用本函数
总的来说只要不希望*this成员内容改变就可以在函数后面加上const
思考
const对象可以调用非const成员函数吗-- 不可以权限扩大非const对象可以调用const成员函数吗-- 可以权限缩小const成员函数内可以调用其它的非const成员函数吗-- 不可以权限扩大非const成员函数内可以调用其它的const成员函数吗-- 可以权限缩小
6.6.2取地址重载
取地址重载函数是C的默认六个成员函数之一同时它也是运算符重载的一种它的作用是返回对象的地址
Date* operator()
{return this;
}6.6.3const取地址重载
const 取地址重载也是C的默认六个成员函数之一它是取地址重载的重载函数其作用是返回 const 对象的地址
const Date* operator() const
{return this;
}使用场景
在某些极少数的特殊情况下需要我们自己实现取地址重载与 const 取地址重载函数比如不允许获取对象的地址那么在函数内部我们直接返回 nullptr 即可
//取地址重载
Date* operator()
{return nullptr;
}//const 取地址重载
const Date* operator() const
{return nullptr;
}6.6.4 流插入 和 流提取 流插入与流提取的运算符重载 6.7总结
C的类里面存在六个默认成员函数 – 构造、析构、拷贝构造、赋值重载、取地址重载、const 取地址重载其中前面四个函数非常重要也非常复杂需要我们根据具体情况判断是否需要显式定义而最后两个函数通常不需要显示定义使用编译器默认生成的即可
构造函数 构造函数完成对象的初始化工作由编译器在实例化对象时自动调用默认构造函数是指不需要传递参数的构造函数一共有三种 – 编译器自动生成的、显式定义且无参数的、显式定义且全缺省的如果用户显式定义了构造函数那么编译器会根据构造函数的内容进行初始化如果用户没有显式定义那么编译器会调用默生成的构造函数默认生成的构造函数对内置类型不处理对自定义类型会去调用自定义类型的默认构造为了弥补构造函数对内置类型不处理的缺陷C11打了一个补丁 – 允许在成员变量声明的地方给缺省值如果构造函数没有对该变量进行初始化则该变量会被初始化为缺省值构造函数还存在一个初始化列表初始化列表的存在有着非常大的意义具体内容我们在下一个大标题讲解讲解 析构函数 析构函数完成对象中资源的清理工作由编译器在销毁对象时自动调用如果用户显式定义了析构函数编译器会根据析构函数的内容进行析构如果用户没有显示定义编译器会调用默认生成的析构函数默认生成的析构函数对内置类型不处理对自定义类型会去调用自定义类型的析构函数如果类中有资源的申请比如动态开辟空间、打开文件那么需要我们显式定义析构函数 拷贝构造 拷贝构造函数是用一个已存在的对象去初始化另一个正在实例化的对象由编译器在实例化对象时自动调用拷贝构造的参数必须为引用类型否则编译器报错 – 值传递会引发拷贝构造函数的无穷递归如果用户显式定义了拷贝构造函数编译器会根据拷贝构造函数的内容进行拷贝如果用户没有显示定义编译器会调用默认生成的拷贝构造函数默认生成的拷贝构造函数对于内置类型完成值拷贝 (浅拷贝)对于自定义类型会去调用自定义类型的拷贝构造函数当类里面有空间的动态开辟时直接进行值拷贝会让两个指针指向同一块动态内存从而使得对象销毁时对同一块空间析构两次所以这种情况下我们需要自己显式定义拷贝构造函数完成深拷贝 运算符重载 运算符重载是C为了增强代码的可读性而引入的语法它只能对自定义类型使用其函数名为 operator 关键字加相关运算符由于运算符重载函数通常都要访问类的成员变量所以我们一般将其定义为类的成员函数同时因为类的成员函数的一个参数为隐藏的 this 指针所以其看起来会少一个参数同一运算符的重载函数之间也可以构成函数重载比如 operator 与 operator(int) 赋值重载 赋值重载函数是将一个已存在对象中的数据赋值给另一个已存在的对象注意不是初始化需要自己显示调用它属于运算符重载的一种如果用户显式定义了赋值重载函数编译器会根据赋值重载函数的内容进行赋值如果用户没有显示定义编译器会调用默认生成的赋值重载函数默认生成的赋值重载函数对于内置类型完成值拷贝 (浅拷贝)对于自定义类型会去调用自定义类型的赋值重载函数赋值重载函数和拷贝构造函数一样也存在着深浅拷贝的问题且其与拷贝构造函数不同的地方在于它还很有可能造成内存泄漏所以当类中有空间的动态开辟时我们需要自己显式定义赋值重载函数来释放原空间以及完成深拷贝为了提高函数效率与保护对象通常使用引用作参数并加以 const 修饰同时为了满足连续赋值通常使用引用作返回值且一般返回左操作数即 *this赋值重载函数必须定义为类的成员函数否则编译器默认生成的赋值重载会与类外自定义的赋值重载冲突 const成员函数 由于指针和引用传递参数时存在权限的扩大、缩小与平移的问题所以 const 类型的对象不能调用成员函数因为成员函数的 this 指针默认是非 const 的二者之间传参存在权限扩大的问题同时我们为了提高函数效率以及保护对象一般都会将成员函数的第二个参数使用 const 修饰这就导致了该对象在成员函数内也不能调用其他成员函数为了解决这个问题C设计出了 const 成员函数 – 在函数最后面添加 const 修饰该 const 只修饰 this 指针不修饰函数的其他参数所以如果我们在设计类时只要成员函数不改变第一个对象我们建议最后都使用 const 修饰 取地址重载与const取地址重载 取地址重载与 const 取地址重载是获取一个对象/一个只读对象的地址需要自己显式调用它们属于运算符重载同时它们二者之间还构成函数重载大多数情况下我们都不会去显示实现这两个函数使用编译器默认生成的即可只有极少数情况需要我们自己定义比如防止用户获取到一个对象的地址 七、初始化列表声明远远不够还得定义对象
声明是对成员变量private中写的内容就叫做声明也就是告知有哪些家庭成员
定义对象因为类的本质就是图纸模板框架所以叫做定义对象
创建对象就是造房子有了类创建好了对象
7.1什么声明是初始化列表
初始化列表以一个冒号开始也就是接着是一个以逗号分隔的数据成员家庭成员列表每个成员变量后面跟一个放在括号中的初始值或表达式
构造函数函数体内执行的是赋值语句成员变量只能在初始化列表进行定义与初始化
class Date
{
public:Date(int year, int month, int day): _year(year)// 初始化列表, _month(month), _day(day){}
private:int _year;int _month;int _day;
};7.2 初始化变量的小细节
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
而且不可以用 只可以用括号
7.3什么时候必须使用初始化变量重点 const修饰引用成员变量自定义类型的类中没有合适的默认构造时必须要在初始化列表的位置进行初始化 不可以通过构造函数对成员变量进行赋初值
原因对于const修饰和引用必须使用的原因解释起来大同小异因为这二者必须在定义的时候初始化且初始化之后在被的地方不可以被修改。初始化列表最直白的定义就是定义对象所以对于这二者不可以赋值必须使用初始化列表对于没有默认构造函数的自定义类型来说我们也必须在初始化列表处对其进行初始化否则编译器就会报错。
7.4初始化列表使用建议
我们在之前C11时候打了一个补丁叫做基于缺省值它其实就是通过初始化列表来初始化的即使没有写也会走
也就是说无论我们是否使用初始化列表类的成员变量都会先使用初始化列表进行初始化
既然他每次都要走以后就尽量不在函数体内写赋值语句了直接通通写上初始化列表
7.5成员变量在类中声明次序就是其在初始化列表中的初始化顺序与其在初始化列表中的先后次序无关
class A
{
public:A(int a):_a1(a), _a2(_a1){}void Print() {cout _a1 _a2 endl;}private:int _a2;int _a1;
};由于在类中 _a2 的声明在 _a1 之前所以在初始化列表处 _a2(_a1) 语句被先被执行而此时 _a1 还是一个随机值所以最终 _a2 输出随机值 八、explict关键字
8.1关于类型转换
构造函数不仅可以构造和初始化对象对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数还具有类型转换的作用。单参数是C98就已经支持的多参数是C11才开始支持的。
8.2如何防止隐式类型转换
一旦使用explicit来修饰构造函数将会禁止构造函数的隐式转换
这看起来没有什么大用处实际在工程中是非常有必要的比如我char类型比较大小我传了俩个参数但是如果我写的是int参数的函数它就会将1.1和1.2变成俩个大小相等的数字对于这种有大问题的隐式类型转换却没有报错这是非常致命的。所以这个关键字存在的意义就是当出现不想要的隐式类型转换的时候报错。 九、static成员
9.1概念
静态函数无法访问非静态成员的根本原因就是因为他没有this指针因为对象中的函数的调用以及成员变量的访问等等其实都是通过隐形的this指针来完成的你现在没有this指针当然就无法访问这些非静态成员了。类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问第一个用类名调用很好理解但是对于第二个是因为 对象. 编译器会去找这个对象所在的类从而找到静态变量静态成员为所有类对象所共享不属于某个具体的对象存放在静态区。静态成员变量必须在类外定义定义时不添加static关键字类中只是声明全局静态局部静态类静态他们生命周期都是全局的但作用域是不同的
非static成员函数可访问static态成员函数/成员static成员函数不能访问非static成员函数/成员只能访问static成员函数/变量有点成也萧何败也萧何的感觉 九、static成员变量与函数
9.1概念
静态函数无法访问非静态成员的根本原因就是因为他没有this指针因为对象中的函数的调用以及成员变量的访问等等其实都是通过隐形的this指针来完成的你现在没有this指针当然就无法访问这些非静态成员了。类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问第一个用类名调用很好理解但是对于第二个是因为 对象. 编译器会去找这个对象所在的类从而找到静态变量静态成员为所有类对象所共享不属于某个具体的对象存放在静态区。静态成员变量必须在类外定义定义时不添加static关键字类中只是声明全局静态局部静态类静态他们生命周期都是全局的但作用域是不同的 非static成员函数可访问static态成员函数/成员static成员函数不能访问非static成员函数/成员只能访问static成员函数/变量有点成也萧何败也萧何的感觉 声明为 static 的类成员称为类的静态成员其中用 static 修饰的成员变量称之为静态成员变量用 static 修饰的成员函数称之为静态成员函数下面我们以一个面试题来引出类的静态成员的相关知识点
面试题实现一个类计算程序中创建出了多少个类对象
我们知道类创建对象一定会调用构造函数或者拷贝构造函数所以我们只需要定义一个全局变量然后在构造函数和拷贝构造函数中让其自增即可如下
int N 0;
class A
{
public:A(int i 0):_i(i){N;}A(const A a){_i a._i;N;}private:int _i;
};虽然使用全局变量的方法可以十分简便的达到我们的目的但是我们不建议使用全局变量因为全局变量可以被任何人修改十分不安全所以我们需要使用另外一种比较安全的方法 – 静态成员变量
9.2static 成员变量
静态成员变量是指用 static 关键字修饰的成员变量其特性如下 静态成员为所有类对象所共享不属于某个具体的对象存放在静态区静态成员变量必须在类外定义定义时不添加 static 关键字类中只是声明静态成员变量的访问受类域与访问限定符的约束 9.2.1特性1
由于静态成员变量在静态区 (数据段) 开辟空间并不在对象里面所以它不属于某单个对象而是所有对象共享
class A
{
public:A(int m 0):_m(m){}public:int _m;static int _n;
};int A::_n 0;可以看到当我们把类的静态成员变量设置为 public 并在类外进行定义初始化后我们可以直接通过类名域作用限定符或者通过一个空指针对象来访问这说明_n并不存在于对象里面
9.2.2特性2类内声明类外初始化
类的静态成员变量在类中只是声明必须在类外进行定义且定义时需要指定类域其不在初始化列表处进行定义初始化因为新建对象并不会改变它的值静态变量是要放在静态存储区的和对象分离开所以必须先创建出来然后创建对象的时候直接引用这样就可以做到每个对象可以访问类名可以访问如果实在理解不了可以理解为语言特性这么做就是非法的 9.2.3特性3
静态成员变量的访问受类域与访问限定符的约束我的命是静态区给的但我这个人被类所管
静态成员变量在访问时和普通的成员变量区别不大同样受类域和访问限定符的约束只是因为其不存在于对象中所以我们可以通过 A:: 来直接访问
注可以看到静态成员变量在定义声明的时候只受类域的限制而没有受到访问限定符的限制这是一个特例大家记住即可
所以我们之前创建出全局变量的那个函数就可以利用静态成员
class A
{
public:A(int i 0):_i(i){_n;}A(const A a){_i a._i;_n;}private:int _i;static int _n;
};int A::_n 0;但是现在又有了全新的问题为了保证类的封装性我们需要将成员变量设置为private但是这样又使得我们无法在类外获取到它们那该怎么办呢针对这个问题C设计出了静态成员函数 9.3static成员函数有点成也萧何败也萧何的感觉
静态成员函数是指用 static 关键字修饰的成员函数其特性如下 静态成员函数没有隐藏的this指针不能访问任何非静态成员静态成员也是类的成员同样受类域和访问限定符的约束 由于静态成员函数没有隐藏的 this 指针所以我们在调用的时候自然也就不需要传递对象的地址即我们可以通过类名域作用限定符直接调用而不需要创建对象但是相应的没有了 this 指针我们也无法去调用非静态的成员变量与成员函数因为非静态成员变量需要实例化对象来开辟空间非静态成员函数的调用则需要传递对象的地址
static int GetN(){return _n;} 注意虽然静态成员函数函数不可以调用非静态成员但是非静态成员函数是可以调用静态成员的 (调用静态成员时编译器不传递对象地址即可) 最后让我们来做一道与静态成员相关的练习题求123…n 检测检测自己哟 十、友元
10.1输入输出的重载细节在上方运算符重载最后一条
在C中我们使用 cin 和 cout 配合流插入 与流提取 符号来完成数据的输入输出并且它们能自动识别内置类型
那么它们是如何做到输入与输出数据以及自动识别内置类型的呢答案是运算符重载与函数重载
可以看到cin 和 cout 分别是 istream 和 ostream 类的两个全局对象而 istream 类中对流提取运算符 进行了运算符重载osteam 中对流插入运算符 进行了运算符重载所以 cin 和 cout 对象能够完成数据的输入输出同时istream 和 ostream 在进行运算符重载时还进行了函数重载所以其能够自动识别数据类型
那么对于我们自己定义的自定义类型我们也可以对 和 进行运算符重载使其支持输入与输出数据我们以Date为例
但是这里有一个问题如果运算符重载为类的成员函数那么运算符的左操作数必须是本类的对象因为 this 指针的类型是本类的类型也就是说如果我们要把 重载为类的成员函数那么本类的对象 d 就必须是做操作数即我们必须像下面这样调用
但是这样显然违背了我们的初衷 – 我们进行运算符重载的目的是提高程序的可读性而上面这样很可能会给函数的使用带来很大的困扰所以对于 我们只能重载为全局函数
但是重载为全局函数又会出现一个新的问题 – 在类外部无法访问类的私有数据但是我们又不可能将类的私有数据改为共有这样代价太大了那么有没有一种办法可以在类外直接访问类的私有成员呢有的它就是友元当然也可以写共有函数吸取私有区的值
10.2友元函数
友元函数可以直接访问类的私有成员它是定义在类外部的普通函数不属于任何类但需要在类的内部声明声明时需要 friend 关键字如下 #includeiostream
using namespace std;
class Date
{//友元声明 -- 可以放置在类的任意位置friend istream operator(istream in, Date d);friend ostream operator(ostream out, const Date d);public:Date(int year 1970, int month 1, int day 1):_year(year), _month(month), _day(day){}private:int _year;int _month;int _day;
};//流提取
inline istream operator(istream in, Date d)
{in d._year;in d._month;in d._day;return in;
}//流插入
inline ostream operator(ostream out, const Date d)
{out d._year / d._month / d._day;return out;
}
int main()
{Date d1;cout d1 ;return 0;
} 小羊注
1、由于流插入和流提取的重载内容较少且调用频率很高所以我们可以把其定义为内联函数
2、为了支持连续输入以及连续输出我们需要将函数的返回值设置为 istream 和 ostream 对象的引用
3、对于函数参数为什么需要加引用咧首先对于第一个参数的引用是必不可少的因为ostream类型的对象不支持拷贝对于第二个函数参数的引用是可以不加的本质上是为了提高效率
友元函数总结 友元函数可访问类的私有和保护成员但不是类的成员函数友元函数不能用 const 修饰友元函数可以在类定义的任何地方声明不受类访问限定符限制一个函数可以是多个类的友元函数友元函数的调用与普通函数的调用原理相同 十一、内部类
概念如果一个类定义在另一个类的内部这个类就叫做内部类内部类是一个独立的类它不属于外部类更不能通过外部类的对象去访问内部类的成员外部类对内部类没有任何优越的访问权限 class A
{
public:A(int a 0):_a(a){}//内部类class B{public:B(int b 0):_b(b){}private:int _b;};private:int _a;
};内部类有如下特性 内部类天生就是外部类的友元所以内部类可以通过外部类的对象参数来访问外部类中的所有成员但外部类不是内部类的友元内部类定义在外部类的 public、protected、private 处都是可以的但是内部类实例化对象时要受到外部类的类域和访问限定符的限制内部类可以直接访问外部类中的static成员不需要外部类的对象/类名内部类是一个独立的类它不属于外部类所以 sizeof (外部类) 外部类内部类在C中很少被使用在Java中使用频繁所以大家只需要了解有这个东西即可 十二、匿名对象偷懒专用
在C中除了用类名对象名创建对象外我们还可以直接使用类名来创建匿名对象匿名对象和正常对象一样在创建时自动调用构造函数在销毁时自动调用析构函数但是匿名对象的生命周期只有它定义的那一行下一行就会立马销毁 class A
{
public:A(int a 0):_a(a){cout A 构造 endl;}~A(){cout A 析构 endl;}private:int _a;
}; 十三、编译器优化在static中就已经有所提及