当前位置: 首页 > news >正文

类和对象万字详解

目录

一、面向对象与面向过程的区别

面向过程:

面向对象:

二、类的引入

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是我们自己设置的类名,{}中就是类的主体,也是类域,注意括号后的‘;’不可省略。

类体中的内容称为类的成员,变量被称为类的属性成员变量;类中的函数被称为类的方法或成员函数。

类的两种定义方法:

  1. 声明和定义全在类体中,需注意:成员函数在类中定义可能被编译器当成内联函数处理。

  1. 类声明放在.h文件中,成员函数定义放在.cpp中,注意,成员函数前需要加上类名::

为什么要将声明与定义分开存放呢?

第一为了方便阅读,你想想,当你将声明与定义一起存放时,你在阅读一个类的函数声明时,你本来只想看看这个类封装了什么方法,但是你看到的却是大段大段的函数定义查找起来并不方便,所以如果将它们分开,我们只会看到方法整整齐齐的放在那里。

第二是为了如果你不想泄露源代码,可以只将.cpp文件转换成二进制文件发送出去,不需要将头文件与方法源码一起发送出去。不过这都是后话了。

那我们为什么需要在.cpp文件中对成员函数加上类名::?

还是以上面那个洗衣机为例:

如果不加上类名::就会出现如上情况:根本分不清哪个函数是哪个类的成员函数。因此我们需要将类名::加上去,表明这个成员函数是属于哪一个类域的。

不过在博客中,为了方便讲解,会将声明与定义放在一起。

在定义类的成员变量时有一个小细节:在成员变量前加上一个_,这是一种编程习惯,为了区分参数与成员变量。就比如:

class Date
{void Init(int year,int month,int day){year=year;month=month;day=day;}int year;int month;int day;
};

在上面这个函数里面year是谁的year容易误解,所以干脆成员变量前面都加上_

class Date
{void Init(int year,int month,int day){_year=year;_month=month;_day=day;}int _year;int _month;int _day;
};

类的限定访问符

其实上面这一段中的方法在外界是无法使用的

class Date
{void Init(int year,int month,int day){year=year;month=month;day=day;}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){year=year;month=month;day=day;}
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。注意:在继承和模板参数列表位置,structclass也有区别,后序给大
家介绍。

类的实例化

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 *。

在每次我们调用函数的时候,函数的参数不只是我们输入的那些,就像上面的(year,month,day)每次都会有一个编译器偷偷帮我买加上的一个参数: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 *ptr=nullptr;
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);//自动调用有参的构造函数

构造函数一个优势就在于,我们在创建对象的时候就会自动调用构造函数,而不用我们手动的敲写:Init(1,1,1);这样的代码,其实多两行也没关系,但是怕就是怕我们自己忘记调用了。这个好处更容易体现在下面的析构函数中。

现在要讨论一下构造函数自动调用的相关细节

我们将我们自己写出来的构造函数先屏蔽一下,看看系统默认的构造函数效果是什么

#include<iostream >
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,其实是内置数据类型(语言本身就有的int,char,long之类的),与之相对的是自定义类型(class,struct,Union之类的)。

编译器对内置类型的初始化————不会对他进行赋值。

编译器对自定义类型的初始化————调用自定义类型自己本身的构造函数初始化。

注意:

编译器自动生成的条件是我们不写编译器才会自动生成默认的,但我们一旦实现了任意一种构造函数,编译器就不会自动生成了。

我们不能这样去调用一个构造函数或者建立对象: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 d3=d2;//这个属于拷贝构造函数而不是复制重载

运算符重载

在讲复制重载前,我们需要先讲运算符重载,因为复制重载就属于运算符重载。

c++为了增强代码的可读性,引入了运算符重载,运算符重载是具有特殊函数名的函数,也拥有返回类型,以及参数,其返回值与参数列表与普通函数相似

函数名字为:operator后面接需要重载的运算符符号

函数原型:返回值类型operator操作符参数列表

举个例子:int operator==Date a,Date b);

我们就可以这样调用函数:a==b.

就可以很简便的计算出两个日期是否相等。

注意:
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,因此我们不需要第一个参数。 

这样程序就没有错误了。

 调用方式: d1==d2;或者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的原因一是需要保护对象的成员属性,二是为了不让参数权限放大导致传参失败。

返回值这样设计的好处就是如果

d1=d2=d3;

如果你不给这个函数一个返回值,是编译不过去的,因为程序先操作d2=d3,操作完发现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.

实现方法如下:

#include<iostream>
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. 静态成员也是类的成员,受publicprotectedprivate 访问限定符的限制
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友元关系不能传递
   如果CB的友元, BA的友元,则不能说明CA的友元。
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. 内部类可以定义在外部类的publicprotectedprivate都是可以的。
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++
JavaPython)将洗衣机用类来进行描述,并输入到计算机中
3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才
能洗衣机是什么东西。
4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象

相关文章:

类和对象万字详解

目录 一、面向对象与面向过程的区别 面向过程&#xff1a; 面向对象&#xff1a; 二、类的引入 class与struct爱恨情仇 class的语法 类的定义&#xff1a; 类的限定访问符 类的实例化 类对象模型 this指针的应用 三、封装 四、类的六个默认成员函数 构造函数 再谈…...

如何使用码匠连接 CouchDB

目录 在码匠中集成 CouchDB 在码匠中使用 CouchDB 关于码匠 CouchDB 是一种开源的 NoSQL 数据库服务&#xff0c;它使用基于文档的数据模型来存储数据。CouchDB 的数据源提供了高度可扩展性、高可用性和分布式性质。它支持跨多个节点的数据同步和复制&#xff0c;可以在多个…...

MySQL对表操作

结束了上一章内容&#xff0c;我们对数据库的操作有一定的了解&#xff0c;本章内容就是针对表中的数据进行操作的。 针对表中数据的操作绝大部分都是增删改查&#xff08;CRUD&#xff09;&#xff0c;CRUD也就是四个单词的缩写&#xff1a; 增加(Create)、查询(Retrieve)、…...

springboot3整合mybatis遇到的坑

本人不经常写java&#xff0c;本文仅作问题记录&#xff0c;如有问题请把不吝赐教。 坑1、Property sqlSessionFactory or sqlSessionTemplate are required Caused by: java.lang.IllegalArgumentException: Property sqlSessionFactory or sqlSessionTemplate are required…...

SpringBoot+Spring常用注解总结

1. SpringBootApplication 这里先单独拎出SpringBootApplication 注解说一下&#xff0c;虽然我们一般不会主动去使用它。 SpringBootApplication public class SpringSecurityJwtGuideApplication {public static void main(java.lang.String[] args) {SpringApplication.ru…...

优化UnRaid容器的WebUI端口设置实现应用快捷访问的方法

文章目录前言详细流程前言 自从入了UnRaid的坑&#xff0c;发现Docker真是个好东西&#xff0c;各种各样的应用工具层出不穷&#xff0c;可以大大提高生产效率。然而在安装Docker应用后&#xff0c;对于如何方便的访问该应用&#xff0c;各个应用服务提供者给出的解决方案不是…...

Android Framework-管理Activity和组件运行状态的系统进程—— ActivityManagerService(AMS)

ActivityManagerService&#xff08;AMS&#xff09;是Android提供的一个用于管理Activity&#xff08;和其他组件&#xff09;运行状态的系统进程 AMS功能概述 和WMS一样&#xff0c;AMS也是寄存于systemServer中的。它会在系统启动时&#xff0c;创建一个线程来循环处理客户…...

【C语言】结构体和共用体

目录一、结构体&#xff08;一&#xff09;结构体声明&#xff08;二&#xff09;结构体变量定义&#xff08;三&#xff09;结构体变量的初始化&#xff08;四&#xff09;结构体的引用&#xff08;五&#xff09;结构体数组二、共用体&#xff08;一&#xff09;共用体定义&a…...

微搭低代码从入门到实战

低代码从21年起开始成为热点&#xff0c;至今已经发展了两年多的时间。微搭作为腾讯云旗下的低码产品也历经多轮优化。 不同人选择低代码有不同的理由&#xff0c;有的是初创企业希望低代码来提升运营效率的。有的是传统企业&#xff0c;希望借助低代码来改造现有系统提供移动…...

AM5728(AM5708)开发实战之安装Debian 10桌面操作系统

一 环境搭建 准备一个SD卡启动卡&#xff0c;能够正常引导板卡启动&#xff0c;后续会把Debian 10镜像安装到SD卡ext4分区 准备两个U盘&#xff0c;一个格式化成fat32文件系统&#xff0c;另一个格式化成ext4文件系统 下载Debian 10镜像&#xff0c;镜像名字为debian-10.4.0-a…...

ip-guardip-guard如何通过准入网关对指定的服务器进行通讯加密保护?

1、准入网关在高级配置设置受保护服务器; WEB管理界面【系统工具】,点击【配置管理】,点击参数设置,进入高级配置界面,输入配置内容即可。 [ControlServer]...

JavaScript基础语法

目录 1.初识JavaScript 1.1背景知识 1.2JS的三种书写方式 行内式 内嵌式 外部式 2.语法简介 2.1变量的使用 变量创建方法 动态类型 2.2基本数据类型 2.3数组 js数组创建方式 遍历方式 添加元素:尾插 ​编辑删除元素:splice 2.4函数 格式 函数表达式 作用域…...

《SQL基础》17. InnoDB引擎

InnoDB引擎InnoDB引擎逻辑存储结构架构内存结构磁盘结构后台线程事务原理事务基础redo logundo logMVCC基本概念隐式字段undo log版本链readView原理分析InnoDB引擎 逻辑存储结构 InnoDB的逻辑存储结构如下图所示&#xff1a; 表空间 表空间是InnoDB存储引擎逻辑结构的最高层…...

api接口详解大全

api接口详解大全?优秀的设计是产品变得卓越的原因设计API意味着提供有效的接口&#xff0c;可以帮助API使用者更好地了解、使用和集成&#xff0c;同时帮助人们有效地维护它每个产品都需要使用手册&#xff0c;API也不例外在API领域&#xff0c;可以将设计视为服务器和客户端之…...

为什么要用VR全景?5个答案告诉你

看中了刚上市的一款新车&#xff0c;再也不用等车展、去4s店才能仔细观赏&#xff0c;点开手机就能“置身”车内近距离观看每一处细节&#xff0c;点击关灯开灯、关门关门&#xff0c;除了摸不到&#xff0c;和在现场几乎没有区别&#xff1b; 准备买房的时候&#xff0c;没人愿…...

常用的深度学习优化方式

全连接层 PyTorch中的全连接层&#xff08;Fully Connected Layer&#xff09;也被称为线性层&#xff08;Linear Layer&#xff09;&#xff0c;是神经网络中最常用的一种层。全连接层将输入数据的每个元素与该层中的每个神经元相连接&#xff0c;输出结果是输入数据与该层的…...

全面吃透Java Stream流操作,让代码更加的优雅

文章目录1 认识Stream流1.1 什么是流1.2 流与集合1.2.1 流只能遍历一次1.2.2 外部迭代和内部迭代1.3 流操作1.3.1 中间操作1.3.2 终端操作1.3.3 使用流2 学会使用Stream流2.1 筛选和切片2.1.1 用谓词筛选2.1.2 筛选各异的元素2.1.3 截短流2.1.4 跳过元素2.2 映射2.2.1 map方法2…...

机器学习学习记录1:假设空间

我们可以把学习过程看作一个在所有假设组成的空间中进行搜索的过程&#xff0c;搜索目标是找到与训练集"匹配" 的假设&#xff0c;即能够将训练集中的瓜判断正确的假设.假设的表示一旦确定&#xff0c;假设空间及其规模大小就确定了.对于西瓜问题&#xff0c;这里我们…...

开源工具系列5:DependencyCheck

Dependency-Check 是 OWASP&#xff08;Open Web Application Security Project&#xff09;的一个实用开源程序&#xff0c;用于识别项目依赖项并检查是否存在任何已知的&#xff0c;公开披露的漏洞。 DependencyCheck 是什么 Dependency-Check 是 OWASP&#xff08;Open Web …...

JDBC知识点全面总结2:JDBC实战编写CRUD

二.JDBC知识点全面总结1&#xff1a;JDBC实战编写CRUD 1.JDBC重要接口&#xff1f; 2.Driver和DriverMangement的关系&#xff1f; 3.JAVA与数据库连接 4.JAVA中使用statement来执行sql语句时&#xff0c;拼接字符串的sql注入问题&#xff1f; 5.使用preparedstatement进行…...

java - 数据结构,算法,排序

一、概念 1.1、排序 排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 平时的上下文中&#xff0c;如果提到排序&#xff0c;通常指的是排升序&#xff08;非降序&#xff09;。 通常意义上的排序&#…...

二叉树经典14题——初学二叉树必会的简单题

此篇皆为leetcode、牛客中的简单题型和二叉树基础操作&#xff0c;无需做过多讲解&#xff0c;仅付最优解。有需要的小伙伴直接私信我~ 目录 1.二叉树的节点个数 2.二叉树叶子节点个数 3.二叉树第K层节点个数 4.查找值为X的节点 5.leetcode——二叉树的最大深度 6.leetc…...

基于NMOSFET的电平转换电路设计

一、概述&#xff1a; 在单片机系统中&#xff0c;5V、3.3V是芯片常用的电平。而在传输协议中(如IIC、SPI等协议)&#xff0c;存在芯片与芯片的高电平和低电平定义的范围不一样&#xff0c;所以需要存在一个电平转换电路&#xff0c;来使芯片与芯片之间顺利的传输。 二、前置…...

mongoDB搭建集群

(学习自黑马)下载对应linux版本MongoDB源码下载地址&#xff1a;https://www.mongodb.com/download-center#community目前在一台服务器开三个端口模拟三个mongodb, 配置一个主节点27017,一个从节点27018,一个仲裁者27019配置主节点,副节点,仲裁节点(下面的创建文件一共有三份,通…...

[深入理解SSD系列 闪存2.1.5] NAND FLASH基本读操作及原理_NAND FLASH Read Operation源码实现

前言 上面是我使用的NAND FLASH的硬件原理图,面对这些引脚,很难明白他们是什么含义, 下面先来个热身: 问1. 原理图上NAND FLASH只有数据线,怎么传输地址? 答1.在DATA0~DATA7上既传输数据,又传输地址 当ALE为高电平时传输的是地址, 问2. 从NAND FLASH芯片手册可知,要…...

最新 JVM 面试经典问题

文章目录 说说JVM的内存布局?知道new一个对象的过程吗?知道双亲委派模型吗?说说有哪些垃圾回收算法?标记-清除复制算法标记-整理那么什么是GC ROOT?有哪些GC ROOT?垃圾回收器了解吗?年轻代和老年代都有哪些垃圾回收器?G1的原理了解吗?什么时候会触发YGC和FGC?对象什么…...

HTML5 和 CSS3 的新特性

目标能够说出 3~5 个 HTML5 新增布局和表单标签能够说出 CSS3 的新增特性有哪些HTML5新特性概述HTML5 的新增特性主要是针对于以前的不足&#xff0c;增加了一些新的标签、新的表单和新的表单属性等。 这些新特性都有兼容性问题&#xff0c;基本是 IE9 以上版本的浏览器才支持&…...

Vulnhub系列:FristLeaks

一、配置靶机环境以往的靶机&#xff0c;本人是在virtual box中&#xff0c;去配置&#xff0c;和vm上的kali进行联动&#xff0c;但是这个靶机需要DHCP&#xff0c;以往的方式可能不太行了&#xff0c;或者可以在virtual box中桥接成统一网卡。下面介绍下本人最有用的方法&…...

XWiki Annotation Displayer 存在任意代码执行漏洞(CVE-2023-26475)

漏洞描述 XWiki 是一个开源的企业级 Wiki 平台&#xff0c;Annotation Displayer 是 XWiki 中的一个插件&#xff0c;用于在 XWiki 页面上显示注释和其他相关内容。 该项目受影响版本存在任意代码执行漏洞&#xff0c;由于Annotation Displayer 对 Groovy 宏的使用没有限制&a…...

数字孪生GIS智慧风场Web3D可视化运维系统

随着国家双碳目标的实施&#xff0c;新能源发电方式逐渐代替了污染大气层的火力发电&#xff0c;其中风力发电相比于光伏发电具有能量密度高、发电小时数长、生命周期达20-25年之久等独特的优势。风能取之不尽、用之不竭&#xff0c;在新型能源互联网下&#xff0c;风力发电有可…...