类和对象(1)

类和对象

1.面向对象和面向过程的初步认知(面向对象和面向过程是需要在学习的过程中加深认知的):

比如一个外卖系统:

面向对象是重点关注商家,客户,骑手。重点分析三者之间类的关系。将一件事拆分成不同的对象,靠对象之间的交互完成。

面向过程是重点关注如何实现:上架商品,点外卖,通知商家,联系骑手,派送,点评。如何通过函数实现这些功能。

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

2.类的引入

c++最开始是通过struct进行引入的,c++兼容C语言,而且c++对struct进行升级。将struct升级成了类。

具体表现为:1.结构体名称可以作类(创建结构体变量不需要加struct了) 2.里面可以定义函数。

c++中结构体的定义:

struct ListNode

{

    int a;

    ListNode* next;

}就不会报错了。

定义函数:

struct student

{

    void Init(const char* name, const char* sex, int age)

    {

        strcpy(_name, name);

        strcpy(_sex, sex);

        _age = age;

    }

    void Print()

    {

        cout << _name << " " << _sex << " " << _age << endl;

    }

    char _name[20];

    //_不是必须要加上的,只是习惯加上,用来表示成员变量。

    char _sex[5];

    int _age;

    //编译器只会向上查找,但类是一个整体,找类的成员变量会在类这个整体范围内找。

};

int main()

{

    struct student s1

    //student s1也可以

    s1.Init("zhangsan", "male", 18);

    //使用类的情况

    s1.Print();

    return 0;

}

结果是打印出:zhangsan male 18

虽然struct也对,但是c++还是喜欢用新的关键字:class 来定义类。

从c++的这里开始,就不叫变量了,更喜欢叫对象。student s1,student是类,s1是类定义的对象。

看面向过程的代码就是看每个功能实现的过程;

看面向对象的的代码就是看商家在完成订单,骑手在配送订单,用户在提交订单。很多对象在完成这样一个多程。

3.类的定义

struct和class定义类是存在区别的。

class定义类

class classname

{

    //类体:由成员函数和成员变量组成

};

除此之外还有一个叫做访问限定符,访问限定符:public(公有),protected(保护),private(私有)

c++有一个概念叫做封装。由此提出了访问限定符,访问限定符的意义在于,类中定义的类体,不一定都要给你使用。想给你用的,定义为公有:

class student

{

public://public是一个新的关键字,代表public直到最后都是公有的

    void Init(const char* name, const char* sex, int age)

    {

        strcpy(_name, name);

        strcpy(_sex, sex);

        _age = age;

    }

    void Print()

    {

        cout << _name << " " << _sex << " " << _age << endl;

    }

    char _name[20];

    char _sex[5];

    int _age;

};

int main()

{

    struct student s1

    s1.Init("zhangsan", "male", 18);//如果将class类中的public去掉,会编译报错

    s1.Print();

    return 0;

}

报错的原因是,class中的空间默认是私有的,要想使用必须加上public,而struct的空间默认是公有的。

公有的就是可以在类外面访问,私有的就是类在外面访问不了。struct也是可以加访问限定符的。

如果加上了私有的访问限定符,那么包括打印,都是不能在类空间外面进行的。现阶段,认为protected和private是一样的。

4.访问限定符以及封装

4.1访问限定符说明:

    1. public修饰的成员在类外可以直接被访问

    2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)

    3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止

    4. class的默认访问权限为private,struct为public(因为struct要兼容C)

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

4.2封装

面向对象的三大特性:封装,继承,多态

要理解封装就要和C语言比较,比如数据结构栈。

C语言要实现:

struct  Stack

{

    int* _p;

    int _top;

    int _capacity;

};

void StackInit(struct Stack* p1);

void StackPush(struct Stack* p1, int x);

int StackTop(struct Stack* p1);

数据和方法是分离的。

分离的最大问题在于太过自由。比如在使用时,取栈顶元素。

printf("%d ", StackTop(&st));这是一种方法,还存在一种方法:

printf("%d ", st._p[st._top]);那么问题来了,这个top是栈顶元素的位置还是栈顶元素的下一个位置?具体取决于栈的初始化。

也就是说,如果把栈的代码改了,这里就出问题了。

c++就不会出现上面情况的误用:

c++把数据和方法封装到一起,也就是类的里面;想给你使用的设计成公有,不想访问的设计成私有。

class Stack

{

public:

    void Init();

    //函数名也可以简化,因为已经确定是栈的函数了。参数也可以简化,因为在类的空间中可以直接访问类中的对象。

    void Push(int x);

    int Top();

private:

    int* _p;

    int _top;

    int _capacity;

}

这样c++的代码就规范化了,想要访问栈顶元素只有一条途径。

一般情况,设计类,成员变量都是私有或者保护。给访问的函数设计成公有,不给访问的函数设计成私有。

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

封装的本质就是更严格的管理设计。

5.类的作用域

类的作用域是类的整体,类定义了一个新的作用域,叫类域。和命名空间的作用域命名空间域一样,是一个新的域。

class Stack

{

public:

    void push(int x)

    {}

};

class Queue

{

public:

    void push(int x)

    {}

};

问:这两个push可不可以同时存在?

可以,首先不构成函数重载,函数重载要求函数名相同,参数不同,而且函数重载要在同一作用域。所以他们能同时存在的唯一原因是它们在不同的作用域。

类函数的声明定义

C语言中我们会将函数的声明和定义分开,用来看函数实现的结构。

c++中会这样声明:

定义stack.h头文件,在其中写上:

class Stack

{

public:

    void Init();

    void Push(int x);

    void Top();

private:

    int* _a;

    int _top;

    int _capacity;

}

定义:

#include"stack.h"

void Stack::Init()//必须声明对应的类域

{

    _a = nullptr;//访问限定符限定的是从类外面去访问

    _top = 0;

    _capacity = 0;

}

……

在类里面定义的函数,编译器默认是inline函数。

所以实际中一般情况,短小函数可以直接在类里面定义;长一点的函数,声明和定义分离。

6.函数的实例化

用类的类型创建对象的过程,叫类的实例化

类不能存储数据,类定义的对象才能存储数据;

就像变量类型一样,变量类型不能存储数据,变量类型定义的变量才能存储数据。

class Stack

{

public:

    void Init();

    void Push(int x);

    void Top();

private:

    int* _a;

    int _top;

    int _capacity;

}

提问:private下面的部分是声明,还是定义?

声明,变量声明和定义的区别在于是否开辟空间。

int main()

{

    Stack st;

    st.Init();

    cout <<sizeof(st) << endl;

    //类的大小是多少?

    return 0;

}

栈里面有三个成员,三个函数,按理解,函数应该会存函数指针,函数调用时,还要建立栈帧,函数在类的类域中,所以计算类的大小可能还要考虑函数栈帧的大小。

先公布结果:输出12

按实际结果来看,应该是只储存了类的成员。为什么只储存类的成员,不存储类的函数呢?

类可以实例化多个对象,每个对象成员的值不一样。但是每个对象调用类中的函数,调用的函数是同一个位置的函数。有必要在对象中存一份函数吗?没有,所以函数的指针没有被存在对象中。函数实际存储在内存中的公共区域,叫代码段。如图:

int main()

{

    Stack st1;

    st1._top;//假设这里的_top没有被定义成私有

    st1.Init();

    Stack st2;

    st2._top;

    st2.Init();

    return 0;

}

两个_top分别在相同的类创建的不同的对象中,调用_top是在对象中找;而调用Init则是从同一个地方调用。Init没有存储在对象中。计算类的大小时,就不考虑成员函数。

class是从struct中进化出来的,class中的成员也满足内存对齐。规则和struct的内存对齐一样。

结构体对齐规则:

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

2.其他成员变量要对齐到对齐数的整数倍地址处

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

    vs的默认对齐数为8

    linux环境下没有默认对齐数,对齐数就是成员自身大小

3.结构体总大小为最大对齐数 (每一个变量都有一个对齐数) 的整数倍

4.如果出现结构体嵌套结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有对齐数中(包括嵌套结构体的对齐数)最大对齐数的整数倍。

所以

class A1

{

public: 

    void f();

private:

    int _a;

    char _b;

};

A1的大小为8

class A2

{

public: 

    void f();

};

class A3//类中什么都没有,称为空类

{};

A2,A3的大小呢?

严格来讲A2,A3是一样的。是0吗?如果是0,那么就存在一个问题A2 aa;以A2为类创建的对象大小是多大?如果是0,那么aa的地址是多少?如何证明这个对象存在,如果不存在,怎么调用类中的函数?所以这个地方,aa的大小为1,也就是说没有成员变量的类,至少会开辟1字节,不是为了存储数据,仅仅表示对象存在。

隐含的this指针

取一个日期类(之后会重点学习日期类):

class Date

{

public:

    void Print()

    {

        cout << _year << '-' << _mouth <<'-' << _day << endl;

    }

    void Init(int day, int mouth, int year)

    {

        _year = year;

        _mouth = mouth;

        _day = day;

    }

private:

    int _year;

    int _mouth;

    int _day;

};

int main()

{

    Date d1;

    d1.Init(2022, 5, 26);

    Date d2;

    d2.Init(2022, 5, 27);

    d1.Print();

    d2.Print();

    //定义了两个对象,但是Print函数并不在对象中,那么Print函数是如何找到对应的空间来打印对应的数据呢?

    return 0;

}

Print函数的反汇编,可以看出调用的Print函数是同一个函数。

这就要所到c++编译器做了一件事,将Print函数进行处理,经过编译器编译后处理成:

void Print(Date* this)//this是新增的关键字,是形参

{

    cout << this->_year << '-' << this->_mouth <<'-' << this->_day << endl;

}

调用的地方也会被处理成这样:d1.Print(&d1);

隐含的this指针是编译器做的,不能添加到Print函数中去。但是存在使用this指针的情况,也就是说this指针是可以使用的。

比如:

void Print()//this可以使用但是不能在实参和形参的位置显示的写

{

    cout << this << endl;

    //this是Date* const类型,不可以修改 

    cout << this->_year << '-' << this->_mouth <<'-' << this->_day << endl;

    //自己可以加,但是编译器会自动加上,所以正常情况下都不会加

}

下面的程序如果运行,结果是什么?a.编译错误b.程序崩溃c.正常运行

class A

{

public:

    void Show()

    {

        cout<<"Show()"<<endl;

    }

private:

    int _a;

};

int main()

{

    A* p = nullptr;

    p->Show();

}

首先肯定不是a,空指针问题只有在运行时,访问到才会发现。也就是说空指针肯定不会在编译阶段报错。

其次补充一个知识:

A d1;

A* p = &d1;

p->Show();//编译器会将p->Show()处理成p->Show(p)

这段代码会正常运行。

原因在于,Show函数并不在d1中,所以p->Show()没有对p进行解引用。p->Show()就像函数调用一样。this指针也只是被初始化成00000000。如果加上p->_a = 0;就会运行崩溃。

this指针一般作函数形参,所以存在于栈区。视编译器的不同,也有可能在寄存器中,比如下图的情况。

指针是对内存编号的结果,从0x00000000到0xFFFFFFFF,从小往大编,空指针就是第一个字节空间的地址,第一个字节空间通常会被预留出来,不存储数据,所以空指针是不可访问的。空指针是虚拟地址,物理地址不存在不能访问的地址。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值