腾云网建设网站,苏州网站建设网,网站模块建设,南宁建设工程质量网站目录 一、面向对象与面向过程的区别
面向过程#xff1a;
面向对象#xff1a;
二、类的引入
class与struct爱恨情仇
class的语法
类的定义#xff1a;
类的限定访问符
类的实例化
类对象模型
this指针的应用
三、封装
四、类的六个默认成员函数
构造函数
再谈…目录 一、面向对象与面向过程的区别
面向过程
面向对象
二、类的引入
class与struct爱恨情仇
class的语法
类的定义
类的限定访问符
类的实例化
类对象模型
this指针的应用
三、封装
四、类的六个默认成员函数
构造函数
再谈构造函数
初始化列表
析构函数
拷贝构造函数
拷贝构造函数的无限递归问题 运算符重载
赋值重载
前置和后置的重载
五、const成员
六、static成员
七、友元
友元函数
友元类
八、内部类
九、匿名对象
十、拷贝对象是的一些编译器优化
十一、再次理解类和对象 一、面向对象与面向过程的区别
很多同学都听说过、c语言是一个面向过程而c是一个面向对象的语言。这里的对象与我们接下来要讲的类和对象是同一个东西。
那么什么是面向过程呢举个例子
洗衣服的例子。 面向过程
我们洗衣服需要进过拿盆子——放水——放衣服——放洗衣粉——手搓——换水等等步骤 如果将这个抽象成编程我们写程序也会按照这个步骤一步步往下写。
面向对象 假设我们有一个洗衣机。我们将洗衣服那些繁琐的步骤交给洗衣机来做那么我们只需要让衣服、洗衣服、洗衣机三个物品进行相互作用就可以实现洗衣服这项工程。并不需要关心衣服到底是如何洗的。 上面提到的人、洗衣机、洗衣粉就是我们要讲到的类与对象中的对象。
那么上面那种一步步繁琐的过程是怎么实现的呢我们将这些动作是怎么做的教会给洗衣机然后每次洗衣服的时候让洗衣机进行操作就行了我们不再需要关心底层是如何实现的了只需要按时取衣服就行了。
将洗衣服的动作交给洗衣机这一步就是————封装。我们下面会详细讲先感性的认识一下就像将一些方法教给机器人然后我们每次需要的时候只需要命令就好了不需要关心底层到底是什么步骤。 二、类的引入
class与struct爱恨情仇
上文说到的封装的机器人其实就指的是类方法可以看作是函数封装这个动作就是将一些函数写进类中。
类是c区别于c独有的一个数据类型。但巧的是在c语言我们也有类似的数据类型——结构体。
回忆一下结构体它的作用就是将许多不同的数据类型打包进一个结构体中。比如
struct Date
{int year;int month;int day;
};
在c语言中的结构体我们只能将一些数据打包起来但是在此基础上进行一个升级打包数据的同时我们也允许将一些函数打包进去就形成了——类这个数据类型。 类其实就是c结构体的升级版。也是因为这个关键的数据类型我们才实现了面向过程。 在vs2022下编写这段程序我们发现它不会报错。 同时我们发现如果使用struct也同样不会报错。 因为struct在c也被升级了那都升级了为什么还要产生一个class来使用并且还成为了类似正宫的地位
因为struct与class其实还是存在着一些不同。等会再讲先了解一下class的语法。
class的语法
类的定义
class classname
{};
class是定义类的关键字classname是我们自己设置的类名{}中就是类的主体也是类域注意括号后的‘’不可省略。
类体中的内容称为类的成员变量被称为类的属性或成员变量类中的函数被称为类的方法或成员函数。
类的两种定义方法
声明和定义全在类体中需注意成员函数在类中定义可能被编译器当成内联函数处理。类声明放在.h文件中成员函数定义放在.cpp中注意成员函数前需要加上类名::
为什么要将声明与定义分开存放呢
第一为了方便阅读你想想当你将声明与定义一起存放时你在阅读一个类的函数声明时你本来只想看看这个类封装了什么方法但是你看到的却是大段大段的函数定义查找起来并不方便所以如果将它们分开我们只会看到方法整整齐齐的放在那里。
第二是为了如果你不想泄露源代码可以只将.cpp文件转换成二进制文件发送出去不需要将头文件与方法源码一起发送出去。不过这都是后话了。
那我们为什么需要在.cpp文件中对成员函数加上类名::
还是以上面那个洗衣机为例 如果不加上类名::就会出现如上情况根本分不清哪个函数是哪个类的成员函数。因此我们需要将类名::加上去表明这个成员函数是属于哪一个类域的。 不过在博客中为了方便讲解会将声明与定义放在一起。 在定义类的成员变量时有一个小细节在成员变量前加上一个_这是一种编程习惯为了区分参数与成员变量。就比如
class Date
{void Init(int year,int month,int day){yearyear;monthmonth;dayday;}int year;int month;int day;
};
在上面这个函数里面year是谁的year容易误解所以干脆成员变量前面都加上_
class Date
{void Init(int year,int month,int day){_yearyear;_monthmonth;_dayday;}int _year;int _month;int _day;
}; 类的限定访问符
其实上面这一段中的方法在外界是无法使用的
class Date
{void Init(int year,int month,int day){yearyear;monthmonth;dayday;}int year;int month;int day;
};
因为类拥有限定访问符的约束 限定访问符的说明
1.public修饰的成员在类外可以直接被访问。
2.protect和private修饰的成员在类外不能直接被访问因为我们没有讲到继承在这里protect与private的作用是类似的。
3.访问限定符的作用域为它出现的位置开始到下一个限定符的出现为止。
class Date
{
public:void Init(int year,int month,int day){yearyear;monthmonth;dayday;}
private:int year;int month;int day;
}; 就像这里public的作用域到private为止而private的作用域到}为止 4.如果后面没有限定访问符作用域到}为止。
5.class的默认访问权限是private而struct的默认访问限定符是public。
这就是上面讲到的struct与class的一点区别而且这也是上面那段代码无法运行的原因。 解答C需要兼容C语言所以C中struct可以当成结构体使用。另外C中struct还可以用来 定义类。和class定义类是一样的区别是struct定义的类默认访问权限是publicclass定义的类 默认访问权限是private。注意在继承和模板参数列表位置struct和class也有区别后序给大 家介绍。 类的实例化
class Date
{
public:void print(){cout _year endl;cout _month endl;cout _day endl;}void fun(){cout this endl;}
private:int _year;int _month;int _day;
};
那么我们在敲下上面这段代码是不是就意味着我们可以在Date里面存放数据了吗
提出一个问题Date里面的_year、_month、_day是声明还是定义
上面的这一段代码其实只是告诉编译器我们有这几个数据可以使用并没有分配内存给它们存放数据因此我们只是将它们声明是无法使用的。 打个比方:上面这个现象就相当于我没有一张别墅的图纸我们能拿着这张图纸直接住进去吗显然不行我们需要用这张图纸将房子建出来我们才可以真正使用居住进去。 接下来我们造房子。
class Date
{
public:Date(int year, int month, int day){_year year;_month month;_day day;}void print(){cout _year endl;cout _month endl;cout _day endl;}void fun(){cout this endl;}
private:int _year;int _month;int _day;
};
void test1()
{Date p1(2023,3,2);p1.print();
} 于是我们就将Date实例化为p1可以将p1的数据初始化并且调用其函数。就像图纸一样我们这个图纸可以用来建造很多栋房子我们这个Date类也可以进行多次实例化并产生多个不同的对象比如p1 p2 p3。
这就是我们讲的类与对象中的对象。
类对象模型
class Date
{
public:Date(int year, int month, int day){_year year;_month month;_day day;}void print(){cout _year endl;cout _month endl;cout _day endl;}void fun(){cout this endl;}
private:int _year;int _month;int _day;
};
在c语言中我们很容易算出来这个类的大小为12但是到了c中呢这里面还包含了成员函数。 复习一下结构体内存对齐 1. 第一个成员在与结构体偏移量为0的地址处。 2. 其他成员变量要对齐到某个数字对齐数的整数倍的地址处。 注意对齐数 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8 3. 结构体总大小为最大对齐数所有变量类型最大者与默认对齐参数取最小的整数倍。 4. 如果嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整 体大小就是所有最大对齐数含嵌套结构体的对齐数的整数倍。 看看结果 在c的语法体系下我们得出的内存大小还是12字节因此我们大胆猜测一下也许类的成员函数并没有放在对象的内存空间之中。
其实仔细想想也很合理如果我们定义了多个对象那岂不是在每个对象下都会存放相同的代码太浪费空间了。这些代码都统一存放在代码段里面当我们调用的时候才回去查找。 检测一下有没有理解上面知识点
// 类中既有成员变量又有成员函数
class A1 {
public:void f1(){}
private:int _a;
};
// 类中仅有成员函数
class A2 {
public:void f2() {}
};
// 类中什么都没有---空类
class A3
{};
这三个类的对象各需要分配多大的内存
答案是4、1、1
第一个我们可以理解但是后面明明就没有成员变量为什么还需要占用内存呢
我们定义一个变量不可能不分配任何内存给它所以至少会占用一个字节 而这个字节叫做占位符
this指针的应用
Date(int year, int month, int day){_year year;_month month;_day day;}
Date的每个对象都有一个共同的类模板而不同的对象调用的都是同一个函数那么我们我们怎么知道上面这个函数里的_year _month _day就是p1的成员变量而不是p2、p3的呢
这里就要提一个this指针了这个指针的类型就是Date *。
在每次我们调用函数的时候函数的参数不只是我们输入的那些就像上面的yearmonthday每次都会有一个编译器偷偷帮我买加上的一个参数this
这个this就是为了区分各个不同的变量而存在的。
将函数的参数完整的写出来就是Date(Date *this,int year,int month,int day)
包括我们的函数也可以像下面这样声明
Date(int year, int month, int day){this-_year year;this-_month month;this-_day day;}
这样就明确的指出了这是哪个对象的成员变量。
下面问两个面试题
1.this指针存放在哪里 1. this指针的类型类类型* const即成员函数中不能给this指针赋值。 2. 只能在“成员函数”的内部使用 3. this指针本质上是“成员函数”的形参当对象调用成员函数时将对象地址作为实参传递给this形参。所以对象中不存储this指针存储在栈上。 4. this指针是“成员函数”第一个隐含的指针形参一般情况由编译器通过ecx寄存器自动传递不需要用户传递 2.this指针可以为空吗
先来看看下面这个代码
Date *ptrnullptr;
pyr-fun();
猜测一下它的运行结果是什么程序错误、程序崩溃、正常运行 很多同学没想到的是居然正常运行了ptr虽然没有指向一个具体的类对象但是我也没有做什么坏事呀第一我没有调用一个没有内存空间的成员函数第二我调用的成员函数也不是存放在类对象里面的而是存放在代码共享段我自己去代码段粒查找就行了。
但是需要注意的是我调用的这个函数也没有访问类的成员变量。因此也会去访问一个不纯在的变量。 三、封装
在类和对象阶段主要是研究类的封装特性那什么是封装呢 封装将数据和操作数据的方法进行有机结合隐藏对象的属性和实现细节仅对外公开接口来 和对象进行交互。 封装本质上是一种管理让用户更方便使用类。比如对于电脑这样一个复杂的设备提供给用 户的就只有开关机键、通过键盘输入显示器USB插孔等让用户和计算机进行交互完成日 常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。这里也显示是出了编程并不是一个从虚无开始创建程序的一个过程它同时野需要我们对生活的观察与总结因为我们最终还是需要解决生活中的问题回归到生活中来的。 四、类的六个默认成员函数
如果一个类什么成员都没有就简称为空类。
空类里面真的什么都没有吗其实我们的编译器会自动默认生成六个默认成员函数。
默认成员函数用户没有自己实现的话编译器会自动生成的函数。 为什么要生成呢
c这个语言是经历过对c的总结与升级而形成的所以我们在这里至少也可以知道这几个默认成员函数的存在是为我们程序员减负而存在的。
这里我们只讲解前面四个函数后面两个用的少。
构造函数
构造函数虽然听起来像是开空间创建对象但其实很贴切的名字应该是初始化函数。
它的作用是初始化对象。
其特征如下 1. 函数名与类名相同。 2. 无返回值。 3. 对象实例化时编译器自动调用对应的构造函数。 4. 构造函数可以重载。 5. 如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦 用户显式定义编译器将不再生成。 6. 关于编译器生成的默认成员函数很多童鞋会有疑惑不实现构造函数的情况下编译器会 生成默认的构造函数。 7. 无参的构造函数和全缺省的构造函数都称为默认构造函数并且默认构造函数只能有一个。 注意无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数都可以认为是默认构造函数 class Date
{
public:Date(){ _year 1;_month 1;_day 1;}Date(int year, int month, int day){_year year;_month month;_day day;}void print(){cout _year endl;cout _month endl;cout _day endl;}void fun(){cout this endl;}
private:int _year;int _month;int _day;
};
上面这一段代码其实就用到了构造函数,而且有两个因为是允许重载的而且一个有参一个无参他们的调用方式分别如下
Date d1;//创建对象的时候就自动调用构建函数进行初始化而且是无参的那个函数
Date d2(2023.3.4);//自动调用有参的构造函数
构造函数一个优势就在于我们在创建对象的时候就会自动调用构造函数而不用我们手动的敲写Init1,1,1这样的代码其实多两行也没关系但是怕就是怕我们自己忘记调用了。这个好处更容易体现在下面的析构函数中。 现在要讨论一下构造函数自动调用的相关细节
我们将我们自己写出来的构造函数先屏蔽一下看看系统默认的构造函数效果是什么
#includeiostream
using namespace std;
class Date
{
public://Date()//{// _year 1;// _month 1;// _day 1;//}//Date(int year, int month, int day)//{// _year year;// _month month;// _day day;//}void print(){cout _year endl;cout _month endl;cout _day endl;}void fun(){cout this endl;}
private:int _year;int _month;int _day;
};
void test1()
{Date d1;d1.print();
}
int main()
{test1();return 0;
} 我们可以看出系统自动给我们的年月日是随机数这其实就是系统自动分配内存时本来就有的数据编译器在初始化时并没有对我们的数据进行赋值。
我们定义的_year,_month,_day其实是内置数据类型语言本身就有的intcharlong之类的与之相对的是自定义类型classstructUnion之类的。
编译器对内置类型的初始化————不会对他进行赋值。
编译器对自定义类型的初始化————调用自定义类型自己本身的构造函数初始化。
注意
编译器自动生成的条件是我们不写编译器才会自动生成默认的但我们一旦实现了任意一种构造函数编译器就不会自动生成了。
我们不能这样去调用一个构造函数或者建立对象Date d1()。这样程序会报错谁知道你是不是在声明一个返回值为Date类型的函数呢这样的问题也被称之为——二义性。 再谈构造函数
Date(int year, int month, int day){_year year;_month month;_day day;} 虽然上述构造函数调用之后对象中已经有了一个初始值但是不能将其称为对对象中成员变量的初始化构造函数体中的语句只能将其称为赋初值而不能称作初始化。因为初始化只能初始化一次而构造函数体内可以多次赋值。 初始化列表
初始化列表的书写格式以一个冒号开始接着是一个以逗号分隔的数据成员列表每个“成员变量”后面跟一个放在括号中的初始值或表达式。
class Date
{
public:Date(int year,int month,int day):_year(year),_month(month),_day(day){}
private:int _year;int _month;int _day;
} 1每个成员变量再初始化列表中只能出现一次初始化列表只能初始化一次 2类中包含以下成员必须放在初始化列表初始化 引用、const、自定义成员变量 const成员变量、引用成员变量、没有默认构造函数的自定义类型成员变量不能先定义再初始化它们在初始化列表内定义并且必须在定义时就初始化因此必须在初始化列表内初始化。 3尽量使用初始化列表因为不管你用不用对于自定义类型的成员变量一定会优先于初始化列表初始化。 析构函数
通过刚刚的构造函数的学习我们知道了一个对象是怎么来的那么一个对象要怎么销毁呢
析构函数与构造函数相反析构函数不是完成对对象本身的销毁局部对象销毁工作是由编译器完成的而对象在销毁时会自动调用析构函数完成对象中资源的清理工作。
析构函数的特征 1. 析构函数名是在类名前加上字符 ~。 2. 无参数无返回值类型。 3. 一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。注意析构函数不能重载 4. 对象生命周期结束时C编译系统系统自动调用析构函数。 class stack
{
public:stack(){_a nullptr;_capacity 0;_top 0;}~stack(){free(_a);_a nullptr;}
private:int * _a;int _capacity;int _top;
};
这里我就写了一个简单的栈类这里面我就实现了析构函数~符号的意思就是“取反”所以这里也与构造函数相对应。
我们在c语言实现栈的时候我们建立一个栈就需要手动调用初始化函数自己写的Init函数销毁一个栈的时候就需要手动调用一个销毁函数自己写的destory函数如果我们忘记了调用销毁函数那么这个我创建的栈就无人销毁造成了内存泄漏非常严重的问题。
但是在c的语法体系下下我们不需要自己调用销毁函数这个栈自己走到了生命周期尽头就会被编译器自己调用的析构函数销毁。这里也展示出了封装的好处将繁琐的细节封装起来方便用户使用。 拷贝构造函数
但我们知道这来给你个都是类出了生命周期是要析构的如果同一个空间被析构两次就有问题了。 因此我们编译器在这里合理的拷贝方式是——重新申请一块空间然后赋值这就是深拷贝。 那么是不是所有的指针类型或者数组都要重新申请空间呢并不是如果是其他体量很庞大不适合这样开辟空间的数据结构就应该用到直接拷贝地址的方式了这就是浅拷贝。
那么这样说的话编译器怎么知道要怎么拷贝呢因为不论它怎么选你总能挑出编译器的刺它就说干脆你自己选择要怎么拷贝算了。
于是编译器的拷贝规则是内置类型的就由我来完成但是自定义类型struct、class之类的就由用户自己定义如何拷贝——通过拷贝构造函数。 拷贝构造函数的无限递归问题
回到上面的话题为什么不能用传值作为参数而必须要用引用作为参数呢
这是一个传值为参数的拷贝构造函数声明以及调用时的代码
Date(Date d); Date d2(d1);
我们要知道在传值的过程我们时需要将d1的值先拷贝进d就是在进行函数本体操作前参数需要先拷贝进来。 在将d1拷贝给d的时候就会调用Date的内部拷贝构造函数——套娃开始了。在调用第二层拷贝构造时发现参数还需要拷贝那就再接着调用第三层拷贝构造函数。 这就是形成无限递归的原因。
那要怎么解决这个问题呢很简单不是要拷贝递归嘛我直接用引用传参这样你就不会在拷贝前对参数拷贝了。
但是我们要注意一点函数声明定义的时候我们尽量在前面加上const不只是这里而是每次使用引用的时候到加上一个const因为权限能平移、缩小但是不能放大。
如Date (const Date d) 拷贝函数的用法
Date d2(d1);
Date d3d2;//这个属于拷贝构造函数而不是复制重载 运算符重载
在讲复制重载前我们需要先讲运算符重载因为复制重载就属于运算符重载。
c为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数也拥有返回类型以及参数其返回值与参数列表与普通函数相似。
函数名字为operator后面接需要重载的运算符符号。
函数原型返回值类型operator操作符参数列表
举个例子int operatorDate a,Date b
我们就可以这样调用函数ab.
就可以很简便的计算出两个日期是否相等。 注意 1.不能通过连接其他符号来创建新的操作符比如operator 2.重载操作符必须有一个类类型参数用于内置类型的运算符其含义不能改变例如内置的整型不 能改变其含义 3.作为类成员函数重载时其形参看起来比操作数数目少1个因为成员函数的第一个参数为隐藏的this 4.“.* :: sizeof ?: .” 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。 我们先试着在类外定义一个运算符重载
这个运算符重载函数有多少个参数由这个是几目的运算符下面这个就是双目的。
bool operator(const Date d1, const Date d2)
{return d1._year d2._year d1._month d2._month d1._day d2._day;
}
你会发现程序会报错 因为你在类里面的声明是privqate的类型在类外是无法访问的。
解决办法就是在类里面定义一个重载函数
class Date
{
public:Date(){_year 1;_month 1;_day 1;}Date(Date d){}Date(int year, int month, int day){_year year;_month month;_day day;}void print(){cout _year endl;cout _month endl;cout _day endl;}void fun(){cout this endl;}bool operator(const Date d1, const Date d2){return d1._year d2._year d1._month d2._month d1._day d2._day;}private:int _year;int _month;int _day;
};我们发现它又报错了说是参数太多了双目运算符刚好两个参数呀但是我们不要忘记了在类里面定义的函数有一个隐形参数this因此我们不需要第一个参数。 这样程序就没有错误了。 调用方式 d1d2;或者operator(d1,d2);
第一种调用方式就体现出来了运算符重载的优势——可读性强。 接下讲讲第四个默认成员函数
赋值重载
赋值重载就属于运算符重载但是这是运算符重载函数里面唯一一个默认成员函数。是可以被编译器默认生成的。
Date operator(const Date d)//返回值支持连续连必须赋值
{if (this d)//避免自己自己给自己赋值{_year d_year;_month d_month;_day d_day;}return *this;
}
这就是赋值重载的定义形式。
以下是格式 参数类型const T传递引用可以提高传参效率 返回值类型T返回引用可以提高返回的效率有返回值目的是为了支持连续赋值 检测是否自己给自己赋值 返回*this 要复合连续赋值的含义 参数要加上const的原因一是需要保护对象的成员属性二是为了不让参数权限放大导致传参失败。
返回值这样设计的好处就是如果
d1d2d3;
如果你不给这个函数一个返回值是编译不过去的因为程序先操作d2d3操作完发现d1__
这个后面没有东西给它传参。所以我们需要给这个函数一个返回值。 赋值重载不能重载为全局函数
class Date
{
public:Date(int year 1900, int month 1, int day 1){_year year;_month month;_day day;}int _year;int _month;int _day;
};
// 赋值运算符重载成全局函数注意重载成全局函数时没有this指针了需要给两个参数
Date operator(Date left, const Date right)
{if (left ! right){left._year right._year;left._month right._month;left._day right._day;}return left;
}
以上的代码会编译失败如果不在类里面弄显示实现编译器就会自己在类里面实现一个默认赋值重载两者就会产生冲突。
用户没有显示实现编译器默认生成一个重载函数是以值的逐字节拷贝。 前置和后置的重载
在写这类函数的时候我们会发现不论怎么写都会是下面的形式
Date operator();
不论是前置还是后置。
因此我们就需要有个标识符来区别这两个东西。
c规定后置需要增加一个int类型的参数但是调用函数的时候该参数不需要传递编译器自动传递。
定义如下 Date operator(int){Date temp(*this);_day 1;return temp;}
后置是要将1前的值返回然后再对值1所以我们要先将1前的值保存然后再1。
一下是前置的函数定义 Date operator(){_day 1;return *this;} 五、const成员
既然类成员函数中的this参数是隐式的我们想要对它加const限定要加呢
在函数之后加上一个const就可以实现这个功能 右边的书写形式只是为了让大家更好的理解编写是是不能这样书写的。
加上这个的作用就是在这个函数内无法对类本身进行修改。 六、static成员
先提出一个面试题创建一个类计算在这个程序中创建了多少分个对象。
我们第一个想到的应该是对构造函数下手——每每创建一个对象就给计数加1那么这个计数要怎么定义呢因为你创建的不同的对象都会调用一个成员函数——构造函数我们就可以定义一个全局变量每次创建对象都给这个变量加1.
实现方法如下
#includeiostream
using namespace std;int scount 0;//全局变量
class stack
{
public:stack(){_a nullptr;_capacity 0;_top 0;scount;//给全局变量}~stack(){free(_a);_a nullptr;}
private:int* _a;int _capacity;int _top;
};void test1()
{stack s1;stack s2;stack s3;stack s4;stack s5;stack s6;cout scount endl;}
int main()
{test1();return 0;
} 但如果我手贱呢
void test1()
{stack s1;stack s2;stack s3;stack s4;stack s5;stack s6;sount;sount;cout scount endl;}
这样就失效了。
我们还有另外一种方式static成员变量这个成员变量厉害就立在在于这里面的数据是全体对象共享的而且我们将它封装进了类里面。 1. 静态成员为所有类对象所共享不属于某个具体的对象存放在静态区 2. 静态成员变量必须在类外定义定义时不添加static关键字类中只是声明 3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问 4. 静态成员函数没有隐藏的this指针不能访问任何非静态成员 5. 静态成员也是类的成员受public、protected、private 访问限定符的限制 class A
{
public:
A() { _scount; }
A(const A t) { _scount; }
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A::_scount 0;
void TestA()
{
cout A::GetACount() endl;
A a1, a2;
A a3(a1);
cout A::GetACount() endl;
}
七、友元 友元提供了一种突破封装的方式有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多用。 友元分为友元函数和友元类 友元函数 问题现在尝试去重载operator然后发现没办法将operator重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象才能正常使用。所以要将operator重载成 全局函数。但又会导致类外没办法访问成员此时就需要友元来解决。operator同理。 class Date
{friend ostream operator(ostream _cout, const Date d);friend istream operator(istream _cin, Date d);
public:Date(int year 1900, int month 1, int day 1): _year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
ostream operator(ostream _cout, const Date d)
{_cout d._year - d._month - d._day;return _cout;
}
istream operator(istream _cin, Date d)
{_cin d._year;_cin d._month;_cin d._day;return _cin;
}
int main()
{Date d;cin d;cout d endl;return 0;
} 1友元函数可访问类的私有和保护成员但不是类的成员函数 2友元函数不能用const修饰 3友元函数可以在类定义的任何地方声明不受类访问限定符限制 4一个函数可以是多个类的友元函数 5友元函数的调用与普通函数的调用原理相同 友元类 1友元关系是单向的不具有交换性。 比如在Time类中声明Date类为其友 元类那么可以在Date类中直接访问Time类的私有成员变量但想在Time类中访问Date类中私有的成员变量则不行。 2友元关系不能传递 如果C是B的友元 B是A的友元则不能说明C时A的友元。 3友元关系不能继承在继承位置再给大家详细介绍。 class A
{
private:static int k;int h;
public:class B // B天生就是A的友元{public:void foo(const A a){cout k endl;//OKcout a.h endl;//OK}};
};
int A::k 1;
int main()
{A::B b;b.foo(A());return 0;
}
友元的方式尽量少用因为这破坏了函数的封装性。 八、内部类 概念如果一个类定义在另一个类的内部这个内部类就叫做内部类。内部类是一个独立的类它不属于外部类更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。 注意内部类就是外部类的友元类参见友元类的定义内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。 特性 1. 内部类可以定义在外部类的public、protected、private都是可以的。 2. 注意内部类可以直接访问外部类中的static成员不需要外部类的对象/类名。 3. sizeof(外部类)外部类和内部类没有任何关系。 九、匿名对象
class A
{
public:A(int a 0):_a(a){cout A(int a) endl;}~A(){cout ~A() endl;}
private:int _a;
};
class Solution {
public:int Sum_Solution(int n) {//...return n;}
};
int main()
{A aa1;// 不能这么定义对象因为编译器无法识别下面是一个函数声明还是对象定义//A aa1();// 但是我们可以这么定义匿名对象匿名对象的特点不用取名字// 但是他的生命周期只有这一行我们可以看到下一行他就会自动调用析构函数A();A aa2(2);// 匿名对象在这样场景下就很好用当然还有一些其他使用场景这个我们以后遇到了再说Solution().Sum_Solution(10);return 0;
}
上面指的好用就是——如果只是为了调用一次成员函数就可以这样使用。 十、拷贝对象是的一些编译器优化
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)
{}
A f2()
{A aa;return aa;
}
int main()
{// 传值传参A aa1;f1(aa1);cout endl;// 传值返回f2();cout endl;// 隐式类型连续构造拷贝构造-优化为直接构造f1(1);// 一个表达式中连续构造拷贝构造-优化为一个构造f1(A(2));cout endl;// 一个表达式中连续拷贝构造拷贝构造-优化一个拷贝构造A aa2 f2();cout endl;// 一个表达式中连续拷贝构造赋值重载-无法优化aa1 f2();cout endl;return 0;
}
十一、再次理解类和对象 现实生活中的实体计算机并不认识计算机只认识二进制格式的数据。如果想要让计算机认识现 实生活中的实体用户必须通过某种面向对象的语言对实体进行描述然后通过编写程序创 建对象后计算机才可以认识。比如想要让计算机认识洗衣机就需要 1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识洗衣机有什么属性有那些功能即对洗衣机进行抽象认知的一个过程 2. 经过1之后在人的头脑中已经对洗衣机有了一个清醒的认识只不过此时计算机还不清楚想要让计算机识别人想象中的洗衣机就需要人通过某种面相对象的语言(比如C、 Java、Python等)将洗衣机用类来进行描述并输入到计算机中 3. 经过2之后在计算机中就有了一个洗衣机类但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的通过洗衣机类可以实例化出一个个具体的洗衣机对象此时计算机才 能洗衣机是什么东西。 4. 用户就可以借助计算机中洗衣机对象来模拟现实中的洗衣机实体了。 在类和对象阶段大家一定要体会到类是对某一类实体(对象)来进行描述的描述该对象具有那些属性那些方法描述完成后就形成了一种新的自定义类型才用该自定义类型就可以实例化具体的对象。