超级详细的C++教程5:类和对象(上)

上期C++教程:超级详细的C++教程4:内联函数、auto关键字、范围for、nullptr


1.初步认识面向过程和面向对象

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

下面是用C语言的思想来解决洗衣服的过程:

6b0d041e8283420b9293e15ec61afb15.png

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

C++实现洗衣服总共分为4个对象:人,衣服,洗衣粉,洗衣机;人将衣服和洗衣粉放入到洗衣机里面去。洗衣机是如何去洗衣服的过程我们不需要关注,只需要调用洗衣机这个对象即可。

5ea5ed3d11c1475e949c2922d1f94cb4.png

总结:

面向过程关注的是过程,分析出求解问题的步骤,提供函数的调用逐步解决问题;

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


2.C++结构体的升级

C++兼容C语言结构体的所有用法同时还将结构体升级成了类

类有两大特点:1.类名就是类型,2.类里可以定义函数

1.类名就是类型

假设我们要定义一个链表的节点,C语言的方法,例如:

struct ListNode
{
    struct ListNode* next;
    int a;
};

int main()
{
    struct ListNode* s;
    return 0;
}

C++将结构体升级后,可以将struct省略不写,因为类名就是类型,C++定义链表节点的方法,例如:

struct ListNode
{
    ListNode* next;
    int a;
};

int main()
{
    ListNode* s;
    return 0;
}

2.类里面可以定义函数

C++里结构体里面是可以写函数的,例如:

struct stack
{
    int* data;
    int top;
    int capacity;

    void Init()
    {
        data = nullptr;
        top= 0;
        capacity = 0;
    }
};

C++结构体 VS C语言结构体

假如我们要实现一个栈,C语言的方法是这样的,例如:

//数据
struct stack
{
    int* data;
    int top;
    int capacity;
};

//方法
void StackInit(struct stack* ps)
{
    ps->data = NULL;
    ps->capacity = 0;
    ps->top= 0;
}

void StackPush(struct stack* ps,int x)
{
    if(ps->capacity == ps->top)//扩容
    {
        //....
    }
    ps->data[ps->top] = x;
    ps->top++;
}

C语言面向的是过程,它关注的是过程,所以它的数据和方法是分离的。

如果要使用上面C语言实现的栈,例如:

int main()
{
    struct stack s;
    StackInit(&s);
    StackPush(&s,10);
    StackPush(&s,20);
    StackPush(&s,30);
    return 0;
}

C++实现栈的方法,例如:

struct stack
{
    int* data;
    int top;
    int capacity;

    void Init()
    {
        data = nullptr;
        top= 0;
        capacity = 0;
    }

    void Push(int x)
    {
        if(capacity == top)//扩容
        {
            //....
        }
        data[top] = x;
        top++;
    }
};

使用上面C++所实现的栈,例如:

int main()
{
    stack s;
    s.Init();
    s.Push(1);
    s.Push(2);
    s.Push(3);
    return 0;
}

3.类的定义

虽然C++将struct升级成了类,但是C++喜欢用class定义类,例如:

class CalssName
{
    //类体:由成员函数和成员变量组成
};//后面一定要有分号,和结构体一样
class为定义类的关键字ClassName为类的名字{}中为类的主体,注意类定义结束时后面分号不能省略;
 
类体中内容称为类的成员:类中的变量称为成员变量; 类中的函数称为成员函数(需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理);

使用class定义类和struct定义类不同,程序会出现变化,这里就引入了新的东西---->访问限定符

1.访问限定符

类的访问限定符有3个,分别是:public(公有),protected(保护),private(私有);

a61b6391b3a84b128b1a1e8a35380a60.png

1. public修饰的成员在类外可以直接被访问,例如:
class Date
{
public://下面的成员在类外面可以被访问
    void PrintfDate()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d;
    d._year = 2024;
    d._month = 11;
    d._day = 13;
    d.PrintfDate();
    return 0;  
}
2. protected和private修饰的成员在类外不能直接被访问(protected和private现阶段我们将他们认为是一样的,只有到继承部分才会有区别),例如:
#include <iostream>
using namespace std;

class Date
{
private://下面的成员在类外面不可以被访问
    void PrintfDate()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d;

    //错误代码///
    d._year = 2024;//不能在类外面被访问
    d._month = 11;//不能在类外面被访问
    d._day = 13;//不能在类外面被访问
    d.PrintfDate();//不能在类外面被访问
    ///

    return 0;  
}
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,例如: 
#include <iostream>
using namespace std;

class Date
{
public://下面的成员在类外面可以被访问
    void PrintfDate()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private://下面的成员在类外面不可以被访问
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d;
    d.PrintfDate();//可以在类外面访问
    //错误代码///
    d._year = 2024;//不能在类外面被访问
    d._month = 11;//不能在类外面被访问
    d._day = 13;//不能在类外面被访问
    ///
    return 0;  
}

d43f65e4c27a42b99458f992cd65977c.png

4. 如果后面没有访问限定符,作用域就到 } 即类结束,例如:
 
01f7e585708d4518baa4e62ce736c0e4.png
5. class的默认访问权限为private,struct为public(因为struct要兼容C),例如:
 
#include <iostream>
using namespace std;

struct LsitNode
{
    int a;
    LsitNode* next;
};

class stack
{
    int* a;
    int top;
    int capacity;
};

int main()
{
    //struct的默认权限是公有,可以在类外面访问
    LsitNode a;
    a.a = 10;
    a.next = nullptr;

    //class的默认权限是私有,不可以在类外面访问
    stack s;
    s.a = nullptr;//错误代码,a是私有成员,不可以在类外面访问
    s.top= 0;//错误代码,top是私有成员,不可以在类外面访问
    s.capacity = 0;//错误代码,capacity是私有成员,不可以在类外面访问
    return 0;
}

struct里面也可以使用访问限定符来限制成员的访问权限,例如:

class stack
{
public:
    void Init()
    {
        a = nullptr;
        size = 0;
        capacity = 0;
    }
private:
    int* a;
    int size;
    int capacity;
};

总结:

1.在类里面,如果我们想别人使用我们就定义成public(公有),不想让别人访问就定义成protected(保护)和private(私有),如果我们不写访问限定符,class里面默认是private(私有);

2.struct里面也可以去使用访问限定符,如果我们不写访问限定符,struct里面默认是public(公有);

3.struct和class没有区别,只是默认访问限定符有区别,一般情况定义类都是使用class,只有极少数的情况下才会用struct。

2.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中 在类体外定义成员时,需要使用 ::
作用域操作符指明成员属于哪个类域,例如:
#include <iostream>
using namespace std;

class person
{
public:
    void PrintfPerson();
private:
    char name[20];
    int age;
    char id[15];
};

void person::PrintfPerson()
{
    cout << name << " ";
    cout << age << " ";
    cout << id << endl;
}

上面代码中,在PrintfPerson这个函数名前面加上person:: 是告诉编译器PrintfPerson这个函数是person这个类作用域里面的。

成员函数如果在类中定义,编译器可能会将其当成内联函数处理(我们可以认为前面有一个隐藏的inline),例如:

class person
{
public:
    void PrintfPerson();
    int GetAge()//编译器会将类里面定义的函数当成内联函数处理
    {
        return age;
    }
private:
    char name[20];
    int age;
    char id[15];
};

void person::PrintfPerson()
{
    cout << name << " ";
    cout << age << " ";
    cout << id << endl;
}

虽然成员函数如果在类中定义,编译器可能会将其当成内联函数处理,但是它不一定会直接展开,还是得看编译器的最终决定(C++的语法细节超级多)。

C++类的规范写法:

1.短小的函数可以在类里面直接定义;

2.递归或者长函数在类里面声明,类外面定义;

3.成员变量的前面或者后面最好加上一个_来和函数参数区分(这是一个良好的惯例);

根据上述,所演示的person类规范的写法,例如:

#include <iostream>
using namespace std;

class person
{
public:
    void PrintfPerson();
    int GetAge()//编译器会将类里面定义的函数当成内联函数处理
    {
        return age;
    }
private:
    char _name[20];
    int _age;
    char _id[15];
};

void person::PrintfPerson()
{
    cout << _name << " ";
    cout << _age << " ";
    cout << _id << endl;
}

3.类的实例化

我们先来定义一个日期类Date,例如:

class Date
{
public:
    void init(int year,int month,int day);
private:
    int _year;
    int _month;
    int _day;
};

void Date::init(int year,int month,int day)
{
    _year = year;
    _month = month;
    _day = day;
}

上面代码中,Date类的成员_year、_month、_day这3个变量是声明还是定义????

函数的声明和定义很好区分,如上面的Date类的init函数:

a89686a2244d466e8e7292fc6c3453b1.png

而上面代码中,Date类中_year、_month、_day这三个成员变量是声明,而不是定义。因为这三个成员变量没有在内存中开辟空间,它只告诉你这三个变量的名字和类型分别是什么。

只有定义后才会在内存中开辟空间,例如:

int main()
{
    Date d;
    return 0;
}

上面代码中d就是个对象(d既可以叫对象,也可以叫变量,C++面向对象,更喜欢叫对象),而在d这个对象被定义出来后_year、_month、_day这三个成员变量作为d这个对象内的成员才会定义出来。

33d4ca15e36c420bb39263cd00a85e2a.png

这句代码就是对象的定义,也叫做对象的实例化。

类和对象的关系是一对多,一个类可以有多个对象。我们可以把类看作是房子的设计图,把对象看作是房子,一张房子的设计图是可以造出很多的房子。

3553038fdefb48419f8a96f3f1cb664c.png


4.类对象模型

类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算
一个类的大小?例如:
class A
{
public:
    void init(int a, char b, int c)
    {
        _a = a;
        _b = b;
        _c = c;
    }
    void print()
    {
        cout << _a << " " << _b << " " << _c <<endl;
    }
private:
    int _a;
    char _b;
    int _c;
};

成员函数不在类里面,如果要计算一个类的大小,只计算成员变量的大小,还要考虑内存对齐。所以上面代码中A这个类的大小为12个字节。

不同对象使用的是同一个成员函数,如下图所示:

a112895b1b59440d8b5e93724360ff61.png

从上图中可以看出,a1和a2这两个对象分别调用的init和print这成员函数都是同一个成员函数,因为函数的地址都是一样的。

所以成员函数不在对象里,而是在公共代码区。而成员变量a1和a2各自都有一份,因为用同一个成员函数所打印出来的结果不同。

所以对象的模型是这样的,如下图所示:

a2fa16581c54425992f81eaa0aa0c946.png

一个类所实例出来的多个对象,成员变量每个对象各自都有一份,每个对象共同一起使用同一个成员函数。

一个没有成员的类叫空类,它的大小为1个字节这个字节不存储有效数据,它标识定义的对象存在过,例如:

#include <iostream>
using namespace std;

class B
{};

int main()
{
    cout << sizeof(B) << endl;
    return 0;
}

只有成员函数,没有成员变量的类的大小也是一个字节,例如:

#include <iostream>
using namespace std;

class C
{
public:
    void func()
    {}
};

int main()
{
    cout << sizeof(C) << endl;
    return 0;
}

5.this指针

我们先来定义一个日期类Date,例如:

#include <iostream>
using namespace std;

class Date
{
public:
    void init(int year,int month,int day);
    void print();
private:
    int _year;
    int _month;
    int _day;
};

void Date::init(int year,int month,int day)
{
    _year = year;
    _month = month;
    _day = day;
}

void Date::print()
{
    cout << _year << "-";
    cout << _month << "-";
    cout << _day << endl;
}

int main()
{
    Date d1;
    Date d2;

    d1.init(2024,11,4);
    d2.init(2024,11,5);

    d1.print();
    d2.print();
    return 0;
}
Date类中有init与 print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 init 函
数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
 
答案是 成员函数有一个隐藏的参数,这个参数是this指针,上面的代码最终会被编译器处理成下图中的这个样子,如图所示:
6793b89c3fb34307a03ad46ca35f5d52.png

对象调用成员函数会被编译器处理成下图中的这个样子,如图所示:

300723fdf7e644a991c4312a73d82db0.png

所以这就是为什么d1调用 init 函数时,该函数设置的是d1对象,而不是设置d2对象,如下图所示:

b1674dd995a74bbf9fe19c8659681885.png

也就是说哪个对象调用成员函数,this指针就会指向那个对象。

学到这里,回头再去看C++和C语言所实现的栈,其实底层是一样的,只是编译器默默为我们做了处理,如下图所示:

7b979c1591e84eacbb0be2a351a3a97e.png

类里面的成员函数,每一个函数的第一个参数都是this指针(除了后面学的静态成员,成员函数的实际参数个数会比看到参数个数会多一个),哪个对象调用成员函数,this指针就会指向那个对象,this指针它是隐藏起来的,我们不能显示的去写this指针的形参和实参,否则编译器会报错,但是我们可以在类里面显示的去使用(一般不建议),例如:

class Date
{
public:
    void init(int year, int month, int day);
    void init(int year,int month,int day);
    void print();
private:
    int _year;
    int _month;
    int _day;
};

void Date::init(int year,int month,int day)
{
    this->_year = year;
    this->_month = month;
    this->_day = day;
}

int main()
{
    Date d1;
    d1.init(2024,11,4);//实际上的调
    d1.print();
    return 0;
}

this指针后面我们会用到它,一般都不会显示的去使用(只有新手才会显示的去写,因为这样代码会更清晰)。

this指针是不能被改变,但是它指向的内容可以被修改;

this指针没有存在对象里面,this是一个形参,它是存在栈帧上;


4.封装

面向对象的三大特性: 封装、继承、多态
在类和对象阶段,我们主要是研究类的封装特性,那什么是封装呢?
 
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来 和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标、键盘插孔等,让用户可以与计算机之间进行交互即可。
 

在C语言中,就没有访问限定符来对结构体进行一个封装,如果使用不规范会造成问题,例如有些网上的代码和书上的代码中C语言所实现的栈,明明提供了判断栈是否为空的函数,但是他们直接使用里面的成员top来对栈进行判空,例如:

struct Stack
{
    int* a;
    int top;
    int capacity;
};

int main()
{
    Stack a;
    //有问题的代码,使用不规范
    if(a.top == 0)//判断栈是否为空
    {
        //...
    }
    return 0;
}
直接成员top来对栈进行判空,这是一个不规范的行为,万一实现栈的人设置top为-1才为空的话会导致程序有问题(并没有人规定top是从0开始还是从-1开始)。这就相当于计算机厂商在出厂时,在外部套上壳子,和提供了开机键,但是用户把开机键位置的壳子给拆了,直接使用两根电线来连接电路来使计算机开机。
 
在C++语言中实现封装,可以 通过类将数据以及操作数据的方法进行有机结合,通过访问权限来
隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值