c++ 类和对象(全)

本文只是把之前上中下三篇文章集合了起来,后面跟着补充一点示例代码,也只是为了方便大家一下子全部观看。

类和对象(上)

一.类的定义

1.类定义格式

我们可以先看一个类的例子(栈):

cpp 复制代码
class Stack
{
private:
    int* a;
    int top;
};

class 为定义类的关键字,Stack为类的名字,{ }里的内容是类的主体,需要注意的是类定义结束后的 } 后面要跟着分号。类里面的变量称之为类的属性或者是成员变量,类里面的函数称为类的方法或成员函数。

为了区分成员变量,一般习惯上成员变量会有一个特殊的标识,一般习惯上在成员变量前面加上_或者是m开头,这个只是建议,并不是强制要求。

cpp 复制代码
class A
{
    int a;
    int _b;
    int mc;
};
//三种都可以

在c++ 里,struct 也可以定义类,c++ 兼容c 语言里的struct 用法,同时struct 升级成了类,即struct 里面也可以定义函数了,但是一般还是用class 来定义类。

定义在类里面的成员函数都默认是inline 的,不了解inline 的可以看c 到c++ 过渡里面提到的。

2.访问限定符

c++ 里一种实现封装的方式,类的三大特性封装,继承和多态,后面会专门出文章。用类将对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

第一个举的例子里面有private ,除此之外还有public和protected。public 修饰的成员在类的外面可以直接被访问,private 和protected 修饰的在类外面不能直接访问。而这两个的差距是在后面的继承里面体现的。需要注意虽然被private 和protected 修饰的成员变量不可以在类外直接被使用,但是在类里面的成员函数可以直接使用改变。

访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止,如果后面没有访问限定符了,那么就会到 } 也就是类结束了再停下来。

class 定义成员没有被访问限定符修饰的默认是private ,即私有的,struct 里面默认的是public。

一般成员变量都会被限制为private 或者 protected ,需要让别人使用的成员函数会被放在public 里。

3.类域

类定义了一个新的作用域,类的所有成员都在类的作用域里,在类外面定义成员时,需要用:: 作用域操作符指明成员是属于哪个类域。

类域影响的是编译器的查找规则,一个定义在类里面的函数在外面想要使用时如果不加 :: 就会被编译器默认为全局里的函数,然后就会报错找不见这个函数的定义和声明。使用了:: 之后,编译器就会明白这个函数是类里面的函数,会自行去类域里面找这个函数并使用。

二.实例化

1.实例化概念

用类类型在物理内存里创建对象的过程,结束类实例化出对象。类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,并没有开辟空间存放有效数据,用类实例化出对象时,才会分配空间。就像c 语言里面的创建变量时的那样,创建变量就像实例化对象一样,在内存里开辟空间存放你需要的数据。

一个类可以实例化多个对象,实例化出的对象,占用的是实际的物理空间,物理上的"内存",存储类成员变量。这里其实和struct 很像,或者可以想象成创建的类就是一个新的我们自己定义的类型,就像那些整数类型,浮点数类型一样,我们可以用这个类型创建不同的变量,区别在于我们自己定义的类型可以使用类里面自己定义的函数,即我们定义的类的每个变量都是可以使用我们写的成员函数的,这些成员函数就是用来"服务"类变量的。

2.对象大小

不需要特殊的计算方法,函数的大小,函数会被放在一个公共的区域里,而计算成员的大小,当对类创建对象时,不同的对象引用同一个函数会去那个公共地址,函数指针是一个地址,调用函数被编译成汇编指令[call地址],其实编译器在编译链接时,就要找到函数的地址,不是在运行时找只有动态多态是在运行时找,就需要存储函数地址。

内存对齐规则:

内存对齐上 struct 与 class 相同。第一个成员在与结构体偏移量为0的地址处。其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。这里的对齐数在之前的文章里结构体与位段详解有提到。是编译器默认对齐数与成员大小的较小值,不同的软件默认对齐数会不同。

需要注意结构体的总大小必须是所有对齐数里面最大的那一个的整数倍,就算会有空间浪费,也要取整数倍。如果是嵌套类结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

特别对于只有函数的类或者什么都没有的类,类的变量大小为1,因为如果 1 个字节都没有无法证明对象存在,所以会给 1 个字节用于占位标识对象存在。下面是一个计算类的大小的例子:

cpp 复制代码
class PersonBirthday
{
    private:
        char name[15];
        int year;
        int month;
        int day;
};

这是一个人的生日的类,下面是计算该类的对象的大小:

3.this指针

this 指针一般存储在栈里面,有些寄存器优化会存在寄存器里面。

在编译器编译过程中,类的成员函数默认都会在形参的第一个位置,增加一个当前类型的指针,就是 this 指针,也就是说函数会用默认传过去的 this 指针来确定并访问对应的类变量。当我们创建类变量后,调用该类变量的成员函数时,可能会有人疑惑一个" . "是如何让那个类变量确定是 a 这个类变量不是 b 这个类变量,类的成员函数访问成员变量是通过 this 指针访问的。c++ 规定不能在实参和形参的位置显示的写 this 指针(编译时编译器会自行添加默认的 this 指针),但是在函数体内部可以直接使用 this 指针。

4.c++和c语言对比

面向对象具有三大特性:封装,多态和继承。下面初步了解一下封装:

c++ 中数据和函数都放在了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是c++的封装的一种体现,这里的封装的本质是一种更严格的规范的管理,避免出现乱访问的修改的问题,当然封装不仅仅是这样的。

c++ 中有一些相对方便的语法,比如缺省参数会方便很多,成员函数每次不需要传地址对象,因为this 指针隐含的传递,方便了很多,使用类型不再需要typedef,用类名就很方便。

类和对象(中)

类的默认成员函数

默认成员函数是用户没有自己实现,编译器会自动实现的成员函数,这些函数被称为默认成员函数。默认成员函数有以下 6 种,前四种是比较重要的。c++11之后还新增了两种默认成员函数,移动构造和移动赋值。下面介绍这 6 种函数:

1.构造函数

构造函数可以理解为初始化函数,是特殊化的成员函数,需要注意的是,构造函数虽然叫做构造,但并不是开辟空间创建变量(我们经常使用的局部对象是栈帧创建时,空间就开好了),它仅仅是对已经存在的变量进行初始化。构造函数会在对象创建时自动调用。

在了解构造函数之前需要确定什么是内置类型,什么是自定义类型,内置类型就是系统已经定义好的变量的类型,像int,char,double,float等类型就是内置类型,而我们自己定义的类,结构体等自己定义的类型。

构造函数有以下特点:

1.函数名与类名相同,构造函数的函数名就是类名。

2.没有返回值,构造函数不用写 void int double 等表明返回类型,那一块直接空下。

3.对象实例化时系统会自动调用对应的构造函数。

4.构造函数可以重载

5.如果类中没有显示定义构造函数,则c++ 编译器会自动生成一个无参的默认函数,一旦用户显式定义,编译器将不再生成,即码农觉得编译器默认的构造函数不合心意可以自己写构造函数,不写也可以(有部分特殊情况必须写)。

6.无参构造函数,全缺省构造函数,编译器默认生成的构造函数都叫做默认构造函数,但是这三个只能存在一个,不能同时存在(无参构造函数和全缺省构造函数同时存在会导致混乱,因为两者都是没有参数的函数,编译器不能确定你要调用哪个)。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义,可以认为不传递参数的构造函数就称为默认构造函数。

7.我们不写,编译器默认生成的构造函数对内置类型成员变量并没有要求,也就是说是否初始化这个要看编译器,对于自定义的成员变量,编译器会调用该变量的默认构造函数,如果没有默认构造函数就会报错。这里需要注意的是,不是所有的自定义类型都需要我们自己去写构造函数,如果你定义的自定义类型里面只有内置类型,那么也是不用写默认构造函数的,可以依靠编译器自行处理,但是有其他的变量比如动态开辟内存的变量等就需要我们自己写。我们初始化这个自定义变量需要初始化列表,这部分内容和一部分构造函数的补充会放在后面的一篇文章 类和对象(下) 里面说明。下面是一个简单的我们自己手写默认构造函数的例子:

cpp 复制代码
class PersonBirthday
{
    private:
        char name[15];
        int year;
        int month;
        int day;
    public:
        PersonBirthday()
        {
            std::strcpy(name, "");
            year=0;
            month=0;
            day=0;
        }
        //两种手写的默认构造函数如果实际中写的话只选其中一种就行,这里只是为了列出来
        PersonBirthday(const char* n = "", int y = 0, int m = 0, int d = 0)
        {
            std::strcpy(name, n);
            year = y;
            month = m;
            day = d;
        }
};

2.析构函数

析构函数与构造函数功能相反,析构函数不是对对象本身的销毁,比如局部对象是存在栈帧的,函数结束后栈帧就会销毁,就会自动释放,不需要我们管,c++规定对象在销毁时会自动调用析构函数完成对象中资源的清理释放。下面是析构函数的特点:

1.析构函数的函数名是类名前加 ~ 。

2.与构造函数相同,析构函数没有返回值,同时也没有参数。

3.一个类只能有一个析构函数,若为显式定义,系统会自动生成默认的析构函数。

4.对象生命周期结束时,系统会自动调用析构函数,不需要我们手动调用。

5.跟构造函数类似,我们不写析构函数时,编译器对内置类型成员不做处理,自定义类型成员会调用它的析构函数。

6.自定义类型成员无论我们写不写析构函数它都会自动调用。

7.如果类中的对象没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,但是如果有申请资源就必须写析构函数,否则会造成资源泄漏。也就是说大部分的析构函数其实都不需要我们自己写,仅仅靠编译器生成的就可以,只有申请资源需要我们特殊关注之外。

8.对于多个对象的析构顺序,c++规定先定义的后析构,后定义的先析构。类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象。全局对象先于局部对象进行构造,局部对象按照出现的顺序进行构造,无论是否为static。

3.拷贝构造函数

拷贝构造函数可以理解为把一个已有的数据类拷贝一份给新的变量,如果一个构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认值,则此构造函数也叫拷贝构造函数,即拷贝构造函数是一种特殊的构造函数。下面是拷贝构造函数的特点:

1.拷贝构造函数是构造函数的一个重载。

2.拷贝构造函数的第一个参数必须是类类型对象的引用(一般加上const,避免产生对原来数值的改变),使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用,拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。c++ 规定,函数里参数的传值传参传类对象的时候要调用拷贝构造,可以理解为开辟一个空间存放要传参对象的数据,然后用这个新开辟的空间作为临时变量去执行函数体,c++ 相当于把这一部分直接调用拷贝构造函数实现。

3.c++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成,大部分人会觉得这种行为有些浪费空间,有些麻烦,因此在后面会有对编译器的优化。

4.若未显式定义拷贝构造,编译器会自动生成拷贝构造函数,自动生成的拷贝构造函数对内置类型成员变量会完成值拷贝(浅拷贝,一个一个字节的拷贝),对自定义类型的成员变量会调用他的拷贝构造。

5.对于类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造函数就已经完全够用了,不需要我们显式实现。但是像Stack(栈)这样的类,虽然也有内置类型,但是对于指向资源编译的变量,编译器自动生成的拷贝构造完成的值拷贝(浅拷贝)就不符合我们的需求,就需要自己写拷贝构造函数。

6.传值返回会产生一个临时对象调用拷贝构造,传值引用返回的是返回对象的别名(引用)没有产生拷贝,但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的。这时的引用相当于一个野引用,类似一个野指针一样,传引用返回可以减少拷贝,但是一定要确保返回对象在当前函数结束后还在,才能引用返回。

4.赋值运算符重载

1.运算符重载

当运算符被用于类类型的对象时,c++ 允许我们通过运算符重载的形式来指定新的含义。举个例子,创建一个日期的类,类成员变量是年月日,然后我们可以重载加号,使得类对象加一个数字返回的是一个新的日期对象。c++ 规定类类型的对象使用运算符的时候必须转换成调用对应的运算符重载,如果你没有写这个运算符重载,那么就会报错。

运算符重载可以看成有特殊名字的函数,他的名字是operator + 要重载的运算符,按之前的例子来说,就是operator+,这里加号就是指我们要重载的就是加号运算符,和其他函数一样,运算符重载也具有返回类型和参数列表和函数体。还是按加号为例,日期加数字返回一个新的日期,所以这个函数的返回值是日期类。

重载运算符函数的参数个数和该运算符作用的运算对象数量一样多,一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。还是按加号为例,加号是有左右两个参数的,所以括号里的参数列表就要写两个,一个写日期类,一个写整数数字,顺序就是按从左到右即可。

如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this 指针,也就是说这个时候传参个数比运算对象个数少一个。还是按加号为例,我们把加号重载写在类里面的时候,就可以省略第一个日期类对象的参数,只写第二个数字的参数。

运算符重载之后并不会改变它的优先级,依旧是按照重载之前的那个符号的优先级。

不能凭空创建新的符号,只能重载已经存在的运算符。

重载运算符至少需要一个类类型的参数,不能全部都是内置类型,比如int operator + (int x, int y)

有五个运算符不能进行重载,..*sizeof? :: :

重载++ 运算符的时候,需要区分前置++和后置++,c++ 规定后置++增加一个int 形参,用于区分,前置: int operator ++() 后置: int operator ++( int )。

重载 >> 和 << 时要重载到全局函数,因为类函数默认第一个参数是 this 指针,是符号左侧的那个参数,但是一般这两个用到的是 cin 和 cout ,并且都是在左边,因此需要写成全局函数,第二个参数就可以写类类型对象。

2.赋值运算符重载

赋值运算符重载也是一个默认函数,用于完成已经存在的两个对象直接的拷贝赋值,这里需要注意和拷贝构造的区别。拷贝构造是用于创建对象时的初始化,我们可以新创建一个对象并将一个已经存在的类对象赋值给这个新创建的对象,这个时候就是拷贝构造。而赋值运算符重载则是两个已经存在的类对象A B,把 A 的值赋值给 B ,就是 B = A 。下面是赋值运算符重载的特点:

赋值运算符重载在没有显式实现的时候,编译器会自动生成一个默认的赋值运算符重载,默认的赋值运算符重载跟默认的拷贝构造函数类似,对内置类型变量会完成值拷贝,也就是浅拷贝(用一个字节一个字节的拷贝) ,对自定义类型成员变量会调用其赋值重载函数。需要注意的是没有指向资源的变量默认的已经够用,但是如果有变量是我们申请了资源的(动态开辟内存的)就需要自己写。

赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算符重载的参数建议写成const 当前类型引用,否则传值传参会有临时拷贝造成空间浪费。

赋值运算符重载有返回值,并且建议写成当前类类型引用,写成引用可以提高效率,写返回值则是为了连续赋值。

5.取地址运算符重载

取地址运算符重载函数包括了两种,一个是普通默认取地址运算符重载,另一个是 const 取地址成员函数。

1.const成员函数

将 const 修饰的成员函数称之为 const 成员函数,修饰成员函数放到成员函数参数列表的后面。

const 实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员变量进行修改。

2.取地址运算符重载

取地址运算符重载分为普通和const 取地址重载,一般这两个函数编译器自动生成的就可以够我们使用,不需要去显式实现。

类和对象(下)

一.构造函数补充

在之前我们实现构造函数时,初始化成员变量主要用在构造函数体内赋值,而构造函数初始化还有另一种方式就是使用初始化列表,初始化列表的使用方式是以一个冒号开头,接着一个以逗号分隔的数据成员列表,每个成员列表后面跟一个放在括号里的初始化值或者表达式。

cpp 复制代码
class Stack
{
private:
    int* arr;
    int top;
    int capacity;
public:
    Stack()
        :arr(new int[4])
        ,top(0)
        ,capacity(4)
    {
        //可能进行的操作
    }
}

这里开辟动态内存用的是 new ,这点会在后面内存管理的地方讲,现在可以简单的理解为开辟动态内存空间并且初始化了一下。

每个成员变量在初始化列表中只出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。引用成员变量, const 成员变量,没有默认构造函数的类类型变量都必须放在初始化列表里面初始化,否则会编译报错。内置类型可以自己选择是在初始化列表还是在函数里面初始化。

c++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。一般都尽量用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果你给了缺省值,那么初始化列表就会用这个初始化,如果没有给,则是否初始化就会取决于编译器,类似于默认构造函数那里的。

初始化列表中按照成员变量在类中声明(private)顺序进行初始化,跟成员在初始化列表出现的先后顺序无关,建议两者的顺序保持一致。

二.static成员

用 static 修饰的成员变量称为静态成员变量,也是属于类的成员,静态成员变量必须在类的外面初始化。静态成员变量是被所有的该类对象所共享的,不属于单独的类对象,存放在静态区。并且该静态成员变量是唯一的,不会随着类变量的创建或销毁,增加或减少而变动,只会随着我们专门访问和改变时才会改变。

用 static 修饰的成员函数称之为静态成员函数,静态成员函数没有 this 指针。静态成员函数可以访问其他的静态成员变量,但是不能访问非静态成员变量,因为静态成员函数不像其他成员函数的参数中有默认的 this 指针。而非静态的成员函数就可以访问任意的静态成员变量和静态成员函数。

访问静态成员变量或者静态成员函数的时候就是像正常访问成员函数一样用类变量名.静态成员或者类变量名.静态成员函数来访问。亦或者是用类名:: 的方式访问。

需要注意静态成员变量不能在声明位置给缺省值初始化(const static 可以给缺省值,因为 const 修饰表明该变量无法改变),缺省值初始化是我们正常创建类变量初始化的一种,是按照初始化列表的步骤进行的,但是静态成员变量是公共的,不属于单独对象的,不会参与对象的初始化列表过程。局部静态成员变量是在代码运行到那里的时候才会创建初始化,静态变量的生命周期是全局的,可以理解生命周期类似于全局变量,在程序结束前才会销毁。

三.友元

我们之前说到的类和对象的成员变量一般是在private限定符下面的,用于防止类以外的函数使用或者修改成员变量,这也是封装的一种,可以保证数据的安全。而我们想要让非类成员函数也可以访问类成员变量时,就需要友元了,友元分为了友元函数和友元类,在函数声明或者类声明的前面加 friend ,并且把友元声明放在你想要访问的类里面就可以,然后这个友元类或者友元函数就可以访问想访问类里面的 private 和 protected 修饰的成员变量了。下面是示例

cpp 复制代码
class people
{
private:
    char* name;
    int age;
    char* sex;
    friend void printpeo( people& a );
    friend class student;
}
void printpeo( people& a )
{
    cout << a.name << a.age << a.sex << endl;
}
class student
{
private:
    char* school;
    char* studentnum;
}

这里我并没有写引用头文件和类里面的函数什么的,仅仅是为了展示一下友元的写法。

下面我用友元函数来泛指友元函数和友元类。友元函数可以在类里面的任何限符下面声明,不会受到限定符的影响。同一个函数可以是多个类的友元函数。友元类中的成员函数都是那个类的友元函数,都可以访问那个类的私有和保护成员。

友元关系不能传递,A是B的友元,B是C的友元,A不是C的友元。并且友元关系单向,不能相互交换A是B的友元,A可以访问B的,但是B不能访问A的。

虽然友元让人觉得便利,但是友元实际上会破坏封装,不建议多用。

四.内部类

如果一个类A定义在另一个类B的内部,那么A就是B的内部类,A可以访问B的私有和保护的成员变量(这里就像是友元类一样)。需要注意A是一个独立的类,受外部类类域限制和访问限定符限制,B的成员变量并不包括A这个类。

内部类默认是外部类的友元类。内部类也是一种封装,还是A类是B类的内部类,如果A是放在 private 或者protected 的限定符下,那么A就是B的专属内部类,B类外面就不能使用A类的成员变量了(B类外面的友元也不可以)。一般B类和A类是紧密相关的,就拿生物里的界门纲目科属种来举例子,B是目,A是科,A有着B的特点并且A又独自发展出独属于A的特点。

五.匿名对象

用类型(实参)直接定义出来的对象就叫做匿名对象,我们之前的类型 对象名(实参)定义出来的就叫有名对象。匿名对象主要是为了方便,生命周期只有创建出来对象的当前一行,就是为了即用即弃。下面是定义示例

六.对象拷贝时的编译器优化

在正常用等于号拷贝变量时过程其实是比较复杂的。还是例子A和B,A和B都是是已经创建好初始化完的类对象,类的名字叫做 student 然后我们想要B变量和A一样的数据就要用等于号。那么就是B = A(这里一般类都需要重载等于号),这里编译器会先创建一个临时变量(视编译器,不一定),存储A的数据,然后再把临时变量的数据传给B里面。这里就多了一个临时变量的消耗。或者是传值传参的时候,编译器会先对参数创建一个临时变量,然后用这个临时变量来运行函数体(因此我们传参一般采用引用,可以直接改变参数数据的同时也不会有临时变量的消耗),而返回值返回的时候也是如此 int a = func( 1 ),func 的返回值会先创建一个临时变量,然后再赋给 a 这些临时变量如果数据小点还好说,一旦数据大或者多次调用函数,那么浪费的空间是非常恐怖的,因此编译器会进行优化。

现代编译器为了尽可能提高效率,在不影响正确性的情况下,会尽可能减少一些传参和返回值的过程中可以省略的拷贝。对于如何优化c++ 并没有严格规定,因此不同的编译器优化的程度,方法会有不同。大部分的是合并优化,减少临时变量或者其他不必要的变量的创建,减少空间使用,提高效率。

代码示例

后面这里作者添加了点模拟栈类的代码,做个示例

cpp 复制代码
//这里是Stack.h文件里的
#pragma once
//类栈
#include<iostream>
#include<assert.h>
using namespace std;

class Stack
{
private:
	char* _arr;
	size_t _top;   //依旧指向栈顶元素的下一个位置(一开始没有元素指向0)
	size_t _capacity;

public:
	//迭代器前提
	typedef char* iterator;
	typedef const char* const_iterator;
	iterator begin()
	{
		return _arr;
	}
	iterator end()
	{
		return _arr + _top;
	}
	//默认构造
	Stack(char* arr = nullptr, size_t top = 0, size_t capacity=0)
		:_arr(new char[capacity ])
		,_top(top)
		,_capacity(capacity)
	{
		if (arr != nullptr)
		{
			for (int i = 0;i < top;i++)
			{
				_arr[i] = arr[i];
			}
		}
	}
	//拷贝构造
	Stack(Stack& s1)
		:_arr(new char[s1._capacity ])
		, _top(s1._top)
		, _capacity(s1._capacity)
	{
		int num = 0;
		for (auto ch : s1)
		{
			_arr[num++] = ch;
		}
	}
	//析构函数
	~Stack()
	{
		delete[] _arr;
		_top = 0;
		_capacity = 0;
	}
	//判断空间是否足够并按需扩容(按照1.5倍扩)
	void JudgeTop();
	//插入数据
	void Push_Back(char a);
	//删除数据
	void Pop_Back();
	//返回栈顶
	char RetTop();
	//栈的遍历
	void PrintStack();
	//重载输入输出
	friend istream& operator >> (istream& in, Stack& s);
	friend ostream& operator << (ostream& out, Stack& s);
	//重载赋值运算符,返回值用于确保连续赋值的可能性
	Stack& operator = (const Stack& s);
};

istream& operator >> (istream& in, Stack& s);
ostream& operator << (ostream& out, Stack& s);
cpp 复制代码
//这里是Stack.cpp文件里的
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
//判断空间是否足够并按需扩容(按照1.5倍扩)
void Stack::JudgeTop()
{
	if (_top == _capacity && _top!=0)
	{
		char* narr = new char[_capacity + _capacity / 2];
		int num = 0;
		for (auto ch : (*this))
		{
			narr[num++] = ch;
		}
		_capacity += _capacity / 2;
		delete[] _arr;
		_arr = narr;
	}
	else if (_top == 0)
	{
		char* narr = new char[4];
		delete[] _arr;
		_arr = narr;
		_capacity = 4;
	}
}
//插入数据
void Stack::Push_Back(char a)
{
	JudgeTop();
	_arr[_top++] = a;
}
//删除数据
void Stack::Pop_Back()
{
	assert(_top != 0);
	_top--;
}
//返回栈顶
char Stack::RetTop()
{
	assert(_top != 0);
	return _arr[_top - 1];
}
//栈的遍历
void Stack::PrintStack()
{
	int num = 0;
	for (auto ch : (*this))
	{
		cout << ch;
	}
	cout << endl;
}

//重载输入输出
istream& operator >> (istream& in, Stack& s)
{
	char* arr = new char[256]; //有输入字符串限制长度(正常来说一般不会太长)可以后面在学了string 之后改进
	in >> arr;
	int top = strlen(arr);
	Stack ns(arr, top, top);
	s = ns;
	delete[] arr;
	return in;
}
ostream& operator << (ostream& out, Stack& s)
{
	s.PrintStack();
	return out;
}

//重载赋值
Stack& Stack::operator = (const Stack& s)
{
	if (this != &s)
	{
		delete[] this->_arr;
		this->_arr = new char[s._capacity];
		for (int i = 0;i < s._top;i++)
		{
			_arr[i] = s._arr[i];
		}
		this->_top = s._top;
		this->_capacity = s._capacity;
	}
	return *this;
}

作者这里仅仅是做了一个简单的大概的模拟,没有模拟太多方法,可能代码有误,希望及时指出。

相关推荐
echome8882 小时前
Python 异步编程实战:async/await 从入门到精通
开发语言·python·php
liuccn2 小时前
GeoTools跟GDAL 库的关系与区别以及应用场景
java·arcgis
为美好的生活献上中指2 小时前
*Java 沉淀重走长征路*之——《MyBatis与MyBatis-Plus一文打尽!》
java·jvm·maven·mybatis·mybatis-plus
Morwit2 小时前
【力扣hot100】 85. 最大矩形
c++·算法·leetcode·职场和发展
brave_zhao2 小时前
javafx中能有异步调用业务方法吗
java
王夏奇2 小时前
python中的深浅拷贝和上下文管理器
java·服务器·前端
小杍随笔2 小时前
【Rust 语言编程知识与应用:自定义数据类型详解】
开发语言·后端·rust
m0_528174453 小时前
C++中的代理模式变体
开发语言·c++·算法
皙然3 小时前
深入理解 Java HashMap:从底层原理、源码设计到面试考点全解析
java·开发语言·面试