C++之封装

C++的三大特性就是封装、继承、多态。今天说说第一个封装。
封装肯定就是说类喽。C++中的类用class定义。

class A  //定义了一个类,类名是A
{

};

上面的就是类,我们可以用类去创建对象。就像用int定义一个变量一样。string就是一个类。

在class类定义中,我们可以自己控制所谓的权限访问。主要有3个,publicprivateprotected,其中protected主要在继承中使用,后续再说

访问属性属性对象内部对象外部
public公有可访问可访问
private私有可访问不可访问

一般把成员变量设为private,而成员函数设为public,可以更好的进行权限控制。

成员函数实现的时候,如果全部写在类内部,那没什么问题,正常些就行了,但是要是声明在类内部,实现在类外部的时候,这时候就好注意了,要这样写:

bool Stack::isFull()   //成员函数  在类外写要写上类名::
{
    return top == spaceSize;
}

在我们多文件编程的时候,一般都是一个类,对应一个.h文件和一个.cpp文件,我们把声明放在.h里,实现放在.cpp里。成员函数的实现写在.cpp里边。

1、构造器

有的人管构造器就构造函数,说的都是这个。

与类名同的无返回值函数, 在类对象创建时(堆/栈对象), 自动调用, 完成类对象的初始化。 尤其是动态堆内存的申请。

当我们创建一个类对象的时候,会首先调用,并且完成对类对象的初始化操作。

语法

class 类名
{
    类名(形式参数)
    构造体
};
//实例就像下面这样,A为类名
class A
{
    A(形参)
    {}
};

构造的规则

1 与类名同, 在对象创建时自动调用,完成初始化相关工作。

2 无返回值, 可带参数, 可以重载, 可默认参数。

3 默认无参空实现体, 一经自实现, 默认不复存在

注意:

构造器在类创建的时候默认存在一个系统提供的无参空体构造器,一旦我们实现了一个构造器(不管我们实现的有参数还是没有参数),那么默认的将不复存在。(有了有参构造器后, 空构造器不复存在 )

默认无参构造函数, 常作作为生成类对象的一种"标配"存在, 所在我们在实现重载或默认参数的构造的时候, 将默认无参构造函数, 纳入其中

构造器也是支持重载和默认参数的。所以构造器可以存在多个。创建对象的时候自动调用适配的哪一个。

#include <iostream>
#include <string.h>
using namespace std;

class Stack
{
public:
    Stack()                         //构造器
    {
        top = 0;
        space = new char[1024];
        memset(space,0,1024);
        spaceSize = 1024;
    }
    
    Stack(int size)                    //构造器重载,这里这种情况不能写为默认参数,
                                    //不然用stack s创建对象的时候,会报错,二义性
                                    //如果没有上边的那个构造器,就应该写上默认参数
    {
        top = 0;
        space = new char[size];
        memset(space,0,size);
        spaceSize = size;
    }
    
    //initial list  初始化列表 完成初始化(数据成员的)  效率很高
    //初始化列表实始化顺序, 跟列表中的顺序无关.实始化顺序和private中写的顺序一样
    //此列表不要拿被初始化的成员去初始化其它
    Stack(int size = 1024)
        :top(0),
         spaceSize(size),
         space(new char[size]{0})
    {
    }

    bool isFull();

private:
    int  top;
    int  spaceSize;
    char *space;
};

bool Stack::isFull()   //成员函数  在类外写要写上类名::
{
    return top == spaceSize;
}
int main()
{
    Stack s;          //创建对象
    cout<<s.isFll()<<endl;//成员函数的调用,用.

    Stack s2(10);
    cout<<s2.isFull()<<endl;
    return 0;
}

2、析构器

也有人管这个叫析构函数

以"~"开头与类名相同,无参无返回的函数, 在类对象销毁时(栈/堆对象), 自动调用, 完成对象的销毁。 尤其是类中己申请的堆内存的释放。

class 类名
{
    ~类名()
    析构体
}

析构的规则

1 、对象销毁时, 自动调用, 完成销毁的善后工作。

2 、无返值, 与类名同, 无参, 不可以重载与默认参数。

3 、系统提供默认空析构器, 一经自实现, 不复存在。

我们在构造器中new出来的空间,要在析构器中delete掉。比如上面的代码中的stack类中的分配,我们完善一下

class Stack
{
public:
    Stack()                         //构造器
    {
        top = 0;
        space = new char[1024];
        memset(space,0,1024);
        spaceSize = 1024;
    }
    
    ~Stack()
    {
        delete []space;
    }
};

3、拷贝构造器

由己存在的对象,创建新对象。也就是说新对象,不由构造器来构造,而是由拷贝构造器来完成。拷贝构造器的格式是固定的。

class 类名
{
    类名(const 类名 & another)
    {
        拷贝构造体
    }

};

拷贝构造规则

1、系统提供默认的拷贝构造器。一经实现,不复存在。

2、系统提供的是等位拷贝,也就是所谓的"浅浅"的拷贝。

3、要实现深拷贝,必须要自定义。

4、发生时机:制作对象的副本,以对象作为参数和返回值。

浅(shallow)/深(deep)拷贝

系统提供默认的拷贝构造器,一经定义不再提供。但系统提供的默认拷贝构造器是等位拷贝,也就是通常意义上的浅(shallow)拷贝。如果类中包含的数据元素全部在栈上,浅拷贝也可以满足需求的。但如果堆上的数据,则会发生多次析构行为。

浅拷贝

class Date
{
public:
    Date(int y = 2016,int m =6 ,int d = 6)
            :year(y),month(m),day(d){}    //构造器,初始化列表
            
    Date(const Date &another)   //拷贝构造器,浅拷贝
    {
        year = another.year;
        month = another.month;
        day= another.day;
    }
    void dis()
    {
        cout<<"year:"<<year
            <<"month"<<month
               <<"day"<<day<<endl;
    }
private:
    int year;
    int month;
    int day;
};

int main()
{
    Date d(2016,8,8);  //调用构造器
    d.dis();
    
    Date dd(d);            //调用的是拷贝构造器
    return 0;
}

深拷贝示例

本例中典型的特点,就是数据包含在栈上的空间,必须要自实现拷贝构造,达到深拷贝的目的,不然则会造成重析构,double free 的现象。使用浅拷贝,就会发生如下图所示:

这两段堆上的共享一段内存空间。正确的做法我们应该使用深拷贝,像下面的一样:

#include <iostream>
#include <string.h>
using namespace std;
class DataStr
{
public:
    DataStr()                          //构造器
    {
        cout<<"constructor"<<endl;
        _str = new char[100];
        strcpy(_str,"C++ is the best language in the World");
    }
    
    DataStr(const DataStr & another)  //深拷贝构造器
    {
        cout<<"copy constructor"<<endl;
        _str = new char[strlen(another._str)+1]; //创建新的堆空间,分配内存 注意strlen求的长度
        strcpy(_str,another._str);               //将传入的another对象内容拷贝一份给新的
    }
    
    ~DataStr()                    //析构器
    {
        cout<<"destructor"<<endl;
        delete []_str;
    }
    
    void dis()
    {
        cout<<_str<<endl;
    }
    
private:
    char *_str;
};
int main()
{
    DataStr ds;
    ds.dis();
    
    DataStr ds2 = ds; //调用深拷贝构造器
    return 0;
}

4、this 指针

this指针就是指向调用对象的指针。系统在创建对象时,默认生成的指向当前对象的指针。这样作的目的,就是为了带来使用上的方便。

this 指针的使用规则

1、指向当前对象,可用于用所有的成员函数,但不能应用于初始化列表。

2、this 是以隐含参数的形式传入,而非成员的一部分,所以不会影响 sizeof(obj)对象的大小。

3、this 指针本身是不能更改指向的,即,是 const 类型修饰的。

this 作用

1、指向当前对象,避免入参与成员名相同

2、支持基于返回 this 引用的多重串联调用的函数(连续赋值)。

#include <iostream>
using namespace std;
class Stu
{
public:
    Stu(string name, int age)
    {
        this->name = name;//name = name;错误写法
        this->age = age;
    }
    
    Stu & growUp()            //注意这里返回引用
    {
        this->age++;
        return *this;    //这行代码可以实现串联调用,例如下边的     
                        //s.growUp().growUp().growUp().growUp().growUp();这样使用
    }
    
    void display()
    {
           cout<<name<<" : "<<age<<endl;
    }
    
private:
    string name;
    int age;
};

int main()
{
    Stu s("www.mengchao.xyz",18);
    s.growUp().growUp().growUp().growUp().growUp();
    s.display();
    return 0;
}

5、运算符重载

前面用到的运算符<<,本身在 C 语言中,是位操作中的左移运算符。现在又用作流插入运算符,这种一个运算符多种用处的现像叫作运算符重载。在 C 语言中本身就有重载的现像,比如 & 既表示取地址,又表示位操作中的与运算。*既表示解引用,又表示乘法运算符。只不过此类功能,C 语言并没有对开发者开放这类功能。C++提供了运算符重载机制。可以为自定义数据类型重载运算符。实现构造数据类型也可以像基本数据类型一样的运算特性。比如,我们可以轻松的实现基本数据类型 3 + 5 的运算,确实不能实现两个结构体类型变量相加的运算。

struct Comp
{
    float real;
    float image;
};
Comp operator+(Comp one, Comp another) //重载了+运算符
{
    one.real += another.real;
    one.image += another.image;
    return one;
}
int main()
{
    Comp c = {1.0,2.0};
    Comp c2 = {3,4};
    Comp sum = c1+c2; //operator+(c1,c2)可以直接使用重载的+号
    return 0;
}

类中重载赋值运算符

用一个己有对象,给另外一个己有对象赋值。两个对象均己创建结束后,发生的赋值行为,就会涉及到赋值运算符重载的问题。

语法

class 类名
{
    类名& operator=(const 类名& 源对象)
    {
        拷贝体
    }

};
class A    
{
    A& operator=(const A& another)
    {
        //函数体
        return *this;
    }
};

operator=的特性

1、系统提供默认的赋值运算符重载,一经自实现,不复存在。

2、系统提供的也是等位拷贝,也就浅拷贝,会造成内存泄漏,重析构。要实现"深深"的赋值,必须自定义。

3、自实现需要解决的问题有 3 个:1 重析构 (老问题) 2 内存泄漏 3 自赋值

4、返回引用,通常不能用 const 修饰。string a,b,c; (a= b) = c; (a+b) = c

浅赋值运算符重载

class Date
{
public:
    Date(int y = 2016,int m =6 ,int d = 6)
           :year(y),month(m),day(d){}
    
    Date& operator=(const Date& another) //浅重载赋值运算符
    {
        this->year = another.year;
        this->month = another.month;
        this->day = another.day;
        return *this;
    }
private:
    int year;
    int month;
    int day;
};

深赋值运算符重载

mystring & operator =(const mystring & another)
{
    if(this == &another)
          return *this;            //实现自赋值
    
    delete []this->_str;        //解决内存泄露,先销毁掉原来的空间
    
    int len = strlen(another._str);//解决重析构double free问题
    _str = new char [len+1];
    strcpy(_str,another._str);
    
    return *this;   //实现连续赋值
}

6、栈对象返回

C++中的栈对象可以返回,但是不可以返回栈对象的引用。

我们说返回对象的引用是更好的,不过并不是什么时候都可以返回对象的引用,就是上面说的栈对象的引用。下面看看返回栈对象的引用。

  返回对象会涉及到生成返回对象的副本,这事调用函数的程序可以使用的副本,因此,返回对象的时间成本包括了调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间。返回引用可以节省时间和内存。直接返回对象与按值传递对象类似,他们都生成临时副本。同样,返回引用与按引用传递对象类似,调用和被调用的函数对同一个对象进行操作。

并不是总是可以返回引用的,当函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用是非法的,在这种情况下,应返回对象,以生成一个调用程序可以使用的副本

7、对象数组

不管是栈上的对象数组还是堆上的对象数组,如果生成的数组,未初始化,则必调用无参构造器。或手动调用带参构造器。构造器无论是重载还是默认参数,一定要把系统默认的无参构造器包含进来。不然生成数组的时候,可能会有些麻烦。

#include <iostream>
using namespace std;
class Stu
{
public:
    Stu(string n = ""):_name(n){}
    void dis()
    {
        cout<<_name<<endl;
    }
    
private:
    string _name;
};
int main()
{
    Stu s1[5]= {Stu("zhangsan"),Stu("lisi")}; 
    Stu s2[]= {Stu("zhangsan"),Stu("lisi")};
    Stu * ps = new Stu[1]{Stu("zhangsan")};
    return 0;
}
Last modification:October 6th, 2019 at 02:11 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment