欢迎光临
我们一直在努力

类和对象(上)

一 、类的定义

1 .1 类定义格式

class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。

class A
{
int _a;
};

struct B
{
int _b;
};

为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_或者m开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求。

class{

private:
string _name;
int _age;
char _sex;
};

C++struct也可以定义类,C++兼容Cstruct的用法,同时struct升级成了类,明显的变化是 struct中可以定义函数,一般情况下我们还是推荐用class定义类。

定义在类面的成员函数默认为inline

我们可以通过以前的知识来构建一个日期类:

class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}

private:
// 为了区分成员变量,⼀般习惯上成员变量
// 会加⼀个特殊标识,如_ 或者 m开头
int _year; // year_ m_year
int _month;
int _day;
};

1 .2 访问限定符

C++一种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

public修饰的成员在类外可以直接被访问;protectedprivate修饰的成员在类外不能直接被访问,protectedprivate是一样的,以后继承章节才能体现出他们的区别。

访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到}即类结束。

class定义成员没有被访问限定符修饰时默认为privatestruct默认为public

一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public

1 .3 类域

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

类域影响的是编译的查找规则,下面程序中In it如果不指定类域Stack,那么编译器就把In it当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知In it是成员函数,当前域找不到的array等成员,就会到类域中去查找。

#include<iostream>
using namespace std;

class Stack
{
public:
// 成员函数
void Init(int n =4);
private:
// 成员变量
int* array;
size_t capacity;
size_t top;
};

// 声明和定义分离,需要指定类域
void Stack ::Init(int n)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}

int main()
{
Stack st;
st .Init();

return 0;
}

二、实例化

2 .1 实例化概念

用类类型在物理内存中创建对象的过程,称为类实例化出对象。

类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。

一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。打个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了有多少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。

class A
{
public:
A(int a,int b)
:_a(a)
,_b(b)
{}

private:
int _a;
int _b;
}

int main()
{

return 0
}

2 .2 对象大小

内存对齐规则

第一个成员在与结构体偏移量为0的地址处。

其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

注意:对齐数=编译器默认的一个对齐数与该成员大小的较小值。

VS中默认的对齐数为8

结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

#include<iostream>
using namespace std;

// 计算一下A/B/C实例化的对象是多大?
class A
{
public :
void Print()
{
cout << _ch << endl;
}
private :
char _ch;
int _i ;
};

class B
{
public :
void Print()
{
//…
}
};

class C
{};

int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;

return 0;
}

三、this指针

3 .1  什么是 this 指针

C++ 中,类的成员函数在调用的时候,编译器会自动传入一个隐藏参数 this,它是当前对象的地址。

一句话:this = 当前正在调用这个成员函数的对象的地址

class A
{
public:
void func(int x)
{
cout << this << endl;
cout << _a << endl;
}
private:
int _a = 10;
};

int main()
{
A obj;
obj.func(20);
}

这个简单的代码就会在他的底层转化成为:

// 编译器自动加的隐藏参数 A* this
void func(A* this, int x)
{
cout << this << endl;
cout << this->_a << endl;
}
// 调用时自动传对象地址:func(&obj,20);

这里的 _a,本质是 this->_a,this 指针是编译器自动加上的,你不用手动传对象地址。

3. 2 this 指针的特性

  • this 指针是成员函数的形参,存在于栈上(函数调用栈),不是对象的成员,不会占用对象的存储空间。 对象内存里只有成员变量,没有 this。
  • this 指针的类
    • 普通成员函数:类名* const this → 指针本身不能改,不能让 this 指向别的对象;可以修改对象成员
    • const 成员函数:const 类名* const this → 指针不能改,对象内容也不能修改

            3.this 指针是隐式参数,不能手动写在函数形参列表;调用函数时也不能手动传对象地址。

            4.在成员函数内部,可以显式写 this-> 成员,用来区分【成员变量】和【同名局部变量 / 参数】。

    经典场景:同名变量,必须用 this 区分

    class A
    {
    public:
    void set_a(int _a)
    {
    // 形参名字和成员变量重名,不加this就会变成自己赋值给自己
    this->_a = _a;
    }
    private:
    int _a;
    };

    3. 3 const 成员函数 和 this 的关系

    void show() const
    {
    // this 类型变成 const A* const this
    cout << _a << endl;
    // _a = 100; // 报错!不能修改成员
    }

    • 普通成员函数:A* const this
    • const 成员函数:const A* const this

    const 修饰的是this 指向的内容,不是 this 指针本身。

    this 指针能不能是空指针:

    class A
    {
    public:
    void func()
    {
    cout << "hello" << endl;
    //cout << _a << endl; // 如果打开这行,空指针解引用,程序崩溃
    }
    private:
    int _a=10;
    };

    int main()
    {
    A* p = nullptr;
    p->func(); // 可以执行!func没有访问任何成员变量
    }

    可以!this 可以是 nullptr,但是访问成员就会崩溃

    3. 4 this 指针的用途总结(4 个)

  • 区分成员变量与同名函数参数(最常用)

    this->_val = val;

  • *成员函数返回 this,实现连续调用(链式调用)

    class Date
    {
    public:
    Date& AddDay(int d)
    {
    _day += d;
    return *this; //返回当前对象本身的引用
    }
    private:
    int _year,_month,_day;
    };
    //链式调用
    Date d;
    d.AddDay(1).AddDay(2).AddDay(3);

  • 在成员函数内部,拿到当前对象地址,传给别的函数。
  • 用来判断,防止自己给自己赋值(拷贝赋值运算符)

    Date& operator=(const Date& d)
    {
    if(this != &d) //防止自赋值
    {
    //拷贝逻辑
    }
    return *this;
    }

  • 四、C++和C语言实现Stack对比

    面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解一下封装。

    通过下面两份代码对比,我们发现C++实现Stack形态上还是发生了挺多的变化,底层和逻辑上没啥变化。

    C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的一种体现,这个是最重要的变化。这里的封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习。

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

    在我们这个C++入门阶段实现的Stack看起来变了很多,但是实质上变化不大。等着我们后面看STL中的用适配器实现的Stack,大家再感受C++的魅力。

    我们之前学过了c语言的栈的实现,我们直接来开始吧!

    先来定义一些成员参数

    class stack
    {
    public:
    //初始化
    void Init(int n = 4);

    //扩容
    void push(int x);

    //取栈顶
    int Top();

    //删除栈顶元素
    void Pop() { assert(top > 0); top–; }

    //判空
    bool Empty() { return top == 0; }

    //销毁栈
    void Destroy();

    private:
    int* array;

    size_t capacity = 0;
    size_t top = 0;
    }

    4.1初始化

    我们通过之前学习的缺省参数来用在这里,就算不需要传值都可以拿到初始为4的空间,比较于数据结构c语言的写法来看省去了一个三目运算符。

    // 声明和定义分离,需要指定类域
    void Stack::Init(int n)
    {
    array = (int*)malloc(sizeof(int) * n);
    if (nullptr == array)
    {
    perror("malloc申请空间失败");
    return;
    }
    capacity = n;
    top = 0;
    }

    4.2插入元素

    这里的思路就是如果内存满了就扩容,否则就直接在栈顶插入元素,栈顶的位置就是栈区的元素,下标是从0开始,下标+1就是元素的个数,而且top始终在下标+1的位置,所以top可以表示为元素的个数。

    void Stack::push(int x)
    {
    if (capacity == top)
    {
    //就应该扩容
    int* tmp = (int*)realloc(array, 2 * sizeof(int) * capacity);
    if (!tmp)
    {
    perror("realloc fail");
    exit(0);
    }
    capacity *= 2;
    array = tmp;
    }
    array[top++] = x;
    }

    4.3取栈顶的元素

    刚刚我们知道了top是元素的个数,永远是指向pos+1,那么最后一个值的下标自然就是top-1了。

    //取栈顶
    int Stack::Top()
    {
    assert(top > 0);

    return array[top – 1];
    }

    4.4删除栈顶的元素

    这一行代码为什么写在.h文件里呢,因为在c++里面引入了一个叫内联函数的家伙。

    void Pop() { assert(top > 0); top–; }

    重点知识点:类内直接定义的函数,C++ 会隐式变成 inline 内联函数,所以写在.h 头文件不会报重复定义的链接错误。

    关于内联是什么意思呢?

    简单来讲内联就是为了防止频繁调用函数,编译阶段,把函数的代码直接原地展开,不产生函数调用跳转,省去调用开销。

    4.5判空

    这个判空就是根据元素个数来看的,而top就是元素个数。

    bool Empty() { return top == 0; }

    4.6栈销毁

    这个销毁直接取释放了这个数组就好,因为就是一个简单的指针,最后置为空。

    //消除栈
    void Stack::Destroy()
    {
    assert(array);

    free(array);
    array = nullptr;
    top = capacity = 0;
    }

    赞(0)
    未经允许不得转载:171主机测评 » 类和对象(上)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址