猿创征文 |【C++】面向对象之微观部分——类的组成(上)

一、面向对象之类的抽象与封装

1.1 类与对象的定义

在这里插入图片描述
了解了类,类对象就不难理解。
举例:在现实世界中我们自己就是人类的对象。
换到计算机的角度:对象就是类在实际的内存空间中定义的变量。

1.1.1 对象的定义

现实中任何事物都可以称之为对象,有自己的独特的特点。

面向对象的思想: 就是把一切事物都看成对象,而对象一般都是由属性方法组成。
属性属于对象静态的一面,用来形容对象的一些特征。 例如:张三的身高、体重、性别。身高、体重和性别就是对象的属性。
方法属于对象动态的一面。 例如:张三会跑,会说话。跑,说话这些行为就是对象的方法。

1.1.2 类的定义

具有同种属性与行为的对象可以抽象为一个类。
例如:“人”就是一个类,其中的人名叫小明,小红等不同的人都是类在现实世界中“人”的对象。

类相当于一个模板或蓝图,他定义了它所包含的全体对象的共同的属性与行为,对象是类的实例化。
例如:我们在叫小明的时候,不会喊“人”而是说的是“小明”。

总结:类即为一些具有共有属性与行为的抽象。对象就是类的实例。

1.2 在C++中怎么表示一个类

1.2.1 类的表示与封装性

使用class来表示一个类

class + 类名
{
private://私有的
public://公有的
protected://受保护
    //1.属性
    //2.行为
    
};

类的封装性 = 属性+ 行为 + 访问权限
访问权限:用来修饰类中属性或函数的访问级别的。
public :公有的,类中或类外对象均可直接访问。
private:私有的,只有类中可以访问,类外或子类之中均不可以访问。
protected:受保护的,只有类中或子类的类中可以访问,类外是不可以访问的。

一般情况下:我们会把类中的属性设为私有的,类中方法设定为公有的。

例如封装一个人的类代码示例:

#include <iostream>
using namespace std;
class person
{
private:
    string name;
    int age;
public:
    void work()
    {
    	//cout << name << endl;//这就叫类中可以直接访问。
        cout << "正在学习C++" <<endl;
    }
};
int main()
{
    person p;
    //栈上定义对象
    p.work();

    person *p1=new person;
    //堆上定义对象
    p1->work();
    //(new Person)->work();不要使用这种方式,这种没有办法释放资源。
    delete p1;
    return 0;
}

结果展示:
在这里插入图片描述

1.2.2 C++中类和结构体有什么区别

类是由结构体演化而来的。
class在C++中表示这个类是默认的私有权限,而struct表示的类默认的是公有权限

既然类和结构体区别不大,什么时候使用结构体,什么时候使用类?
作为数据节点时,如链表的节点、树的节点时,一般多用struct
一般封装逻辑性较多时,都是用class

1.2.3 类外访问类中私有属性或方法

在类外我们不能访问类中的私有属性,所以我们可以在类中手动提供公有的set()get()方法。
代码示例:
在代码中我使用了this指针,如有疑问,请先看第二个知识点进行了解。

#include <iostream>
using namespace std;
class person
{
private:
    string name;
    int age;
public:
    void work()
    {
        cout << "正在学习C++" <<endl;
    }
    void show()
    {
        cout << "姓名:" << name << " 年龄:" << age <<endl;
    }
    //姓名属性的set与get方式
    void setname(string name)
    {
        this->name=name;
    }
    string getname()
    {
    return this->name;
    }
    //年龄属性的set与get方式
    void setage(int age)
    {
        this->age=age;
    }
    int getage()
    {
        return this->age;
    }
};
int main()
{
    person p;
    //栈上定义对象
    p.setname("yemaoxu");
    p.setage(18);
    p.show();
    p.work();
    //cout << p.age <<endl;私有不可在类外访问,要用get方法
    cout << p.getage() <<endl;
    cout << "-----------------------" <<endl;
    person *p1=new person;
    //堆上定义对象
    p1->setname("xiaoming");
    p1->setage(20);
    p1->show();
    p1->work();
    cout << p1->getname() <<endl;
    //(new Person)->work();不要使用这种方式,这种没有办法释放资源。
    delete p1;
    return 0;
}

结果展示:
在这里插入图片描述

1.2.4 类与对象的内存大小

代码展示:

#include <iostream>
using namespace std;
class A
{
private:
    int a;
    double b;
public:
    void show()
    {
        cout << "正在学习C++" << endl;
    }
};
class B
{
private:
    //int a;
    //double b;
public:
    void show()
    {
        cout << "正在学习C++" << endl;
    }
};
class C
{
private:
    //int a;
    //double b;
public:
    /*void show()
    {
        cout << "正在学习C++" << endl;
    }*/
};
class D
{
private:
    static int a;
    static double b;
public:
    void show()
    {
        cout << "正在学习C++" << endl;
    }
};
int main()
{
    A a;
    B b;
    C c;
    D d;
    cout << sizeof(a) << endl;
    cout << sizeof(b) << endl;
    cout << sizeof(c) << endl;
    cout << sizeof(d) << endl;
    return 0;
}

结果展示:
在这里插入图片描述

总结:

  1. 如果一个类是一个空类,当他去定义对象编译器为了在内存对此对象有一个内存表示,所以会在这个类中安插一个unsigned char的类型的数据。这样的话,即使是一个空类,那么在内存的空间中也有一个表示。
  2. 类对象的空间大小与非静态的属性有关。
  3. C++中class定义的类,也遵从C中结构体的内存对齐原则。

1.3 封装一个矩形类

封装一个矩形类,定义矩形的属性(宽,高)及行为(求面积)。
代码示例:

#include <iostream>
using namespace std;
class Rect
{
private:
    //抽象出矩形的属性:
    int width;
    int height;
public:
    int area()//求面积的行为
    {
        return width*height;
    }
    //通过外部参数,为类中的属性进行赋值。
    void set(int width,int height)
    {
        this->width=width;
        this->height=height;
    }
};
//定义一个函数,来比较两个矩形大小,返回最大的那个矩形。
Rect& compare(Rect& r1,Rect& r2)
{
    return r1.area()>r2.area() ? r1 : r2;
    //在C中函数的返回值默认都是一个右值。
    //在C++中如果返回值为一个引用的话,那么此函数的返回值将是一个左值。
}
int main()
{
    Rect r1,r2;
    r1.set(10,20);
    r2.set(30,40);
    cout << r1.area() << endl;
    cout << r2.area() << endl;
    cout << "-------------" << endl;
    //获取比较后大的那个矩形,并打印出此矩形的面积:
    cout << compare(r1,r2).area() << endl;
    return 0;
}

结果展示:
在这里插入图片描述

二、C++中的this指针

this指针是类中成员函数的一个隐藏形参,哪个类对象调用成员函数,this指针就指向谁。

this的类型:类名 * const this;

2.1 this指针的由来

C中实现this指针:

#include <stdio.h>
#include <stdlib.h>
typedef struct
{
    char *name;
    int age;
    void (*myfun)();
}stu;
//C++中this的原型就是这个,this就是一个指向本对象的的常指针。
//C++中的成员函数都是带有this的全局函函数。只有生成对象时才可以调用。
void show(stu *const this)
{
    printf("%s,%dn",this->name,this->age);
}
int main()
{
    stu s={"yemaoxu",18,show};//this指针的产生,随着对象产生而产生。
    //在C++中this指针隐藏在类成员函数中形参列表中的第一位(最左侧)
    s.myfun(&s);
    return 0;
}

结果展示:
在这里插入图片描述
总结:

  1. this指针,就是一个指向本对象的指针,而且是一个const修饰的常指针。
  2. this指针的产生,随着对象产生而产生。
  3. 在C++中this指针隐藏在类成员函数中形参列表中的第一位(最左侧)。
  4. 如果不加this,默认都是通过this指针来访问成员的。

2.2 this指针在C++程序底层的一些的逻辑

在这里插入图片描述
成员函数参数的入栈过程中,this指针是最后一个,并且直接放在了exc寄存器上。
在这里插入图片描述
this就是指向本对象的指针,隐藏在成员函数的最左侧,即第一位。

2.3 this指针的用法

this指针就是编译器提供给我们程序员使用的,他的用法有以下两种

2.3.1 this指针的用法一

当函数的形参变量名与类中属性变量相同冲突时,一定要使用this加以区别,也可以通过构造函数的初始化表解决。
初始化列表在第四个知识点进行讲解,感兴趣可以了解一下。

代码示例:
在代码中用到了,构造知识点,感兴趣可以去第三个知识点进行了解。

#include <iostream>
using namespace std;
class Stu
{
private:
    string name;
    int age;
public:
    Stu(string name,int age)//构造知识
    {
        this->name=name;
        this->age=age;
    }
    void show()
    {
        cout << "姓名:" << name << " 年龄:" << age <<endl;
    }
};

int main()
{
    Stu stu("yemaoxu",18);
    stu.show();
    return 0;
}

结果展示:
在这里插入图片描述

2.3.2 this指针的用法二

返回本对象。一般情况是当本对象类中的属性被修改时会这么用。

代码示例:

#include <iostream>
using namespace std;
class Stu
{
private:
    string name;
    int age;
public:
    Stu(string name,int age)//构造知识
    {
        this->name=name;
        this->age=age;
    }
    Stu& setname(string name)
    {
        this->name=name;
        return *this;
    }
    void show()
    {
        cout << "姓名:" << name << " 年龄:" << age <<endl;
    }
};

int main()
{
    Stu stu("yemaoxu",18);
    stu.show();
    cout << "-----------" << endl;
    stu.setname("xiaoming").show();
    return 0;
}

结果展示:
在这里插入图片描述

上面两个例子中为什么成员函数show可以直接访问类中的属性呢?
因为在C++的任何一个非静态成员中都隐藏了一根指向本对象的指针,这个指针就是this,由于this指针的存在,所以类中的非静态成员函数才可以访问类中的成员属性。

注意:
不能在成员函数的形参中使用this指针;
不能在构造函数的初始化表中使用this指针;
可以在成员函数的函数体中使用this指针;

三、类中的构造与析构

3.1 构造函数

3.1.1 C++中类的构造函数及意义

  1. 功能:
    在类实例化对象的过程中,给成员申请资源。如分配内存,打开文件等,完成对成员的初始化。
  2. 格式:
    构造函数与类同名
    构造函数没有返回值
    构造函数一般是public权限的

C++中定义构造函数的语法形式:

类名(形参列表)
{
    //构造函数的函数体。
    //这个函数体就应该是对类中属性进行初始化的逻辑。
}

构造的的意义就是用来初始化类中的属性的。

3.1.2 C++中类的构造函数的定义形式及调用时机

构造函数的调用时机:

栈区:
类名  对象名(构造函数的实参表);//调用构造函数
堆区:
类名  *指针名;
指针名 = new  类名(构造函数的实参表); //调用构造函数

使用new去定义对象,首先开辟空间,然后调用类中构造函数。

当去定义对象时,编译器就会根据对象后括号的实参类型,自动去调用与之相匹配的构造函数。如果在类中编译器找不到与之匹配的构造函数,将直接报错。
代码示例:

#include <iostream>
using namespace std;
class Stu
{
private:
    string name;
    int age;
public:
    Stu()
    {
        cout << "Stu的无参构造" << endl;
    }
    //构造函数也是函数,他也遵从函数重载原则。
    Stu(string name,int age)
    {
        this->name=name;
        this->age=age;
        cout << "Stu的有参构造" << endl;
    }
    void show()
    {
        cout << "姓名:" << name << " 年龄:" << age <<endl;
    }
};

int main()
{
    //在栈上定义对象:
    Stu stu;//编译器会自动调用无参的空构造。
    stu.show();
    Stu stu1("yemaoxu",18);//编译器自动调与之参数类型相匹配的有参构造函数。
    stu1.show();
    cout << "-----------" << endl;
    //在堆上定义对象
    Stu *pstu=new Stu;
    pstu->show();
    Stu *pstu1=new Stu("xiaoming",20);
    pstu1->show();
    delete pstu;
    delete pstu1;
    return 0;
}

结果展示:
在这里插入图片描述
总结:

  1. 如果类中没有提供任何构造,此种无参空构造就是编译器默认生成的,形参列表为void。如果有写任何一种构造,编译器将不再提供默认版本。所以,如果想要使用无参数的构造函数,也需要进行手动定义。
  2. 这种无参空构造也称之类中默认构造。
  3. 构造函数与普通函数还是不一样的,他没有返回值,连void都没有。
  4. 构造函数不是给你程序员调用,他是给编译器调用,用来在开辟空间同时,对类中属性的进行初始化。

3.1.3 编译器自动调用构造的形式

显式调用与隐式调用
像我们上面举例的方法调用方法就是显式调用。

隐式调用代码示例:

#include <iostream>
using namespace std;
class A
{
private:
    int a;
    int b;
    int c;
public:
    //C++11提供的关键字explicit用于修饰构造函数,以限制编译器自动进行隐式调用。
    explicit A(int a)
    {
        this->a=a;
        //cout << "隐式调用了A的有参构造" << endl;
        cout << "显式调用了A的有参构造" << endl;
    }
    A(int a,int b,int c)
    {
        this->a=a;
        this->b=b;
        this->c=c;
        cout << "另一种方式隐式调用了A的有参构造" << endl;
    }
};

int main()
{
    A a(10);//显式调用
    //A a1=10;//隐式调用
    A a2={1,2,3};//隐式调用
    return 0;
}

结果显示:
在这里插入图片描述
注: 当有explicit在构造函数前进行修饰时,编译器就不再使隐式调用的方式来调用构造函数了。这样可以提高代码可读性。

构造函数的自动调用的时机总结:

  1. 构建对象时,编译器要据给定参数的不同自动调用类中的不同参数的构造函数。
  2. 构造函数也是函数,只不过这个函数是给编译器用的,如果有多个形参不同的构造,构造函数之间也会发生重载。

3.2 析构函数

3.2.1 C++中类的析构函数及意义

  1. 作用:
    在对象消亡的时候,用来做释放空间等善后工作的。
  2. 格式:
    ~类名(void){}
    析构函数是没有参数的,所以不能重载
  3. 默认析构函数:
    如果类中没有显性定义析构函数,编译器会默认提供一个函数体为空的
    析构函数,用来消亡对象使用,如果显性定义了,默认的版本就不再提供了。

析构函数的语法形式:

~类名()
{
    //析构函数的函数体。
    //函数体就应该书写:当类中有属性指针指向堆区资源的情况,回收资源的逻辑。
    //因为在销毁对象时,首先编译器会调用类中的析构函数。
    //调用析构函数的意义就是希望你这个程序员来把有以上情况出现时进行回收资源。
}

3.2.2 C++中类的析构函数的定义形式及调用时机

析构函数的调用时机:
当对象被销毁时,编译器自动调用类中的析构函数。
被销毁分两种:
一种就是栈对象,出栈时自动被销毁,此时也将自动调用类中的析构函数。(能不能完成资源清理,那就看你程序员有没有写回收的逻辑)。

另一种就是堆上的对象:手动delete销毁,此时也调用类中的析构函数。(能不能完成资源清理,那就看你程序员有没有写回收的逻辑)。
因此,也有人说析构函数也叫清理函数。

析构函数,它与构造函数还不太一样。既可以是编译器自动调用,也可以你这个程序员也可调用。
代码示例:

#include <iostream>
using namespace std;
class Stu
{
private:
    string name;
    int age;
    int *p;
public:
    //Stu有参的构造
    Stu(string name,int age)
    {
        this->name=name;
        this->age=age;
        this->p=new int[20];
        //如果类中有属性指针指向堆区,那么当对象被销毁时,就必须把这个指针指向堆区的资源先回收。
        //不然的话就内存泄漏了。
        cout << "Stu的有参构造" << endl;
    }
    ~Stu()
    {
        cout << "Stu的析构" << endl;
        delete []p;
        //就应该清理类中有属性指针指向堆区的这种情况,在析构函数中书写回收类中属性指针指向的堆区资源的逻辑。
    }
};

int main()
{
    //在栈上定义对象:
    Stu stu("yemaoxu",18);
    //在堆上定义对象
    Stu *pstu=new Stu("xiaoming",20);
    //析构函数不仅可以编译器自动调用,也可以由你程序员在适当时机进行调用。
    //delete关键字的底层实现其实就是以下的两步完成了。先析构(清理对象的空间),再free(完成对象的销毁)。
    pstu->~Stu();
    delete pstu;
    return 0;
}

结果展示:
在这里插入图片描述
总结:

  1. 如果类中有属性指针指向堆区,那么当对象被销毁时,就必须把这个指针指向堆区的资源先回收。不然的话就内存泄漏了。
  2. 应该清理类中有属性指针指向堆区的这种情况,在析构函数中书写回收类中属性指针指向的堆区资源的逻辑。
  3. 析构函数不仅可以编译器自动调用,也可以由你程序员在适当时机进行调用。
  4. delete关键字的底层实现其实就是以下的两步完成了。先析构(清理对象的空间),再free(完成对象的销毁)。

3.3 构造函数和析构函数调用的顺序

  1. 对于堆空间的对象,他们什么时候被消亡取决于什么时候delete,先delete哪个,哪个就先消亡,所以,我们不考虑他的顺序。
  2. 对于栈空间的对象:
    构造函数调用顺序:顺序调用
    析构函数调用顺序:逆序调用
    即先构造的后析构。

代码示例:

#include <iostream>
using namespace std;

class Student{
    private:
        string name;
        int *age;
    public:
        Student(string _name, int _age):name(_name),age(new int(_age)){
            cout << this <<"  "<< this->name << "  构造函数" << endl;
        }
        ~Student(void){
            cout <<this <<"  "<< this->name << "  析构函数" << endl;
            delete age;
        }
};

int main()
{
    Student s1("小明", 18);
    Student s2("小红", 16);
    Student s3("张三", 17);
    return 0;
}

结果展示:
在这里插入图片描述

四、类中特殊属性的初始化

类的属性除了一些常用的普通变量(对象)之外,还有一些被修饰符修饰的变量(对象);

两种:
1.const修饰的成员对象、类类型对象(没有默认构造函数的对象)
2.static修饰的成员对象。

4.1 类中const修饰的成员变量及没有默认构造的类类型成员对象的初始化

在C++中const修饰的变量,必须初始化。
所以const修饰的变量,当开辟空间的同时就必须进行初始。

类中有子类的类类型(但这个类对象没有默认构造函数供其生成对象),也必须在开辟空间的同时,指定一个构造进行初始化。

如何解决这些问题呢?
构造函数的特殊语法:初始化列表

代码示例:

#include <iostream>
using namespace std;
class A
{
public:
    A(int a)
    {
        cout << "A的有参构造" << endl;
        cout << a << endl;
    }
    ~A()
    {
        cout << "A的析构" << endl;
    }
};
class stu
{
private:
    string name;
    int age;
    const int id;
    int& b;
    A a;
public:
    stu(string _name,int _age,int _id,int& _b):name(_name),age(_age),id(_id),b(_b),a(1)
    {
        cout << "stu中的有参构造" << endl;
    }
    void show()
    {
        cout << "姓名:" << name <<" 年龄:" << age << " 学号:" << id << " 引用数" << b << endl;
    }
    ~stu()
    {
        cout << "stu的析构" << endl;
    }
};
int main()
{
    int num=521;
    stu s("yemaoxu",18,1001,num);
    s.show();
    return 0;
}

结果展示:
在这里插入图片描述
总结:

  1. 初始化列表的调用时机:
    1.初始化列表,在构造函数的调用之前。
    2.初始化列表,也其实是在new的同时被调用的。
  2. 如果要全部使用初始化列表来完成对类中属性的初始化,建议大家保持与类中的属性的声明顺序相同。
  3. a(1)就是用来告诉编译器指定调用A类中的那一个构造来完成类中a对象初始化的。
  4. 必须使用初始化表的场景:
    1.构造函数的形参名和成员变量名冲突,也可以使用this指针解决;
    2.当类中有引用成员时;
    3.类中有const修饰的成员变量时;
    4.当类中有成员子对象时(有其他类的对象最为本类的成员时);

4.2 类中static修饰的类中的属性的初始化

当一个进程被加载时,系统会为这个进程分配一块4G的虚拟地址空间。
在这里插入图片描述
类中的静态属性,是所有对象所共享的一份属性。
是因为静态数据只能被加载一次。静态区的部分是属于整个进程的。

它只有一份数据。
因为静态数据,在程序加载时就已经被确定下来了。

类中的静态属性,在语法层面上,他是属性整个类,而不是某个对象。他为整个类服务,而非某一个对象。如果是public修饰的话,那么也可以直接使用域名访问符::的形式直接访问,而无需依赖某个对象调用。 所以类中的静态属性是不依赖于对象,他是属于整个类的。

当类中如果有需要定义一个为整个类而服务属性时,就可以把它升级静态属性。
这样的话这个属性就不再依赖于某个对象。

由于静态成员变量定义在静态区定义内存,而对象是存在于动态区之中,所以静态成员变量并不占用类对象的内存空间。 这个我在前面的代码中sizeof打印演示过。

静态区变量只能被初始化一次。

代码示例:

#include <iostream>
using namespace std;
class stu
{
private:
    string name;
    int age;
    const int id;
public:
    static int count;
    stu(string _name,int _age,int _id):name(_name),age(_age),id(_id)
    {
        cout << "stu中的有参构造" << endl;
        count++;
    }
    void show()
    {
        cout << "姓名:" << name <<" 年龄:" << age << " 学号:" << id << endl;
        cout << "学生人数:" << count << endl;
    }
    ~stu()
    {
        cout << "stu的析构" << endl;
    }

};
int stu::count;
int main()
{
    stu s("yemaoxu",18,1001);
    cout << "学生人数:" << s.count << endl;
    stu s1("xiaoming",20,1002);
    cout << "学生人数:" << s1.count << endl;
    s.show();
    cout << "-----------------" << endl;
    cout << &s.count << endl;
    cout << &s1.count << endl;
    return 0;
}

结果展示:
在这里插入图片描述
总结:

  1. 当类中有static修饰的成员变量时
    它是隐藏在类中作用域的一个静态变量,此变量必须在类外完成初始化才能在静态区中分配空间。 如果,对类中的静态属性没有在类外进行初始化,那么它将只是一个声明而已,没有空间的,即没有定义。
  2. 类中静态属性在类外进行初始化的方式:
    int Stu::count;
    count会放在静态区.bss段。如果给它附一个初值它就会放.data段。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>