C++——类和对象(了解面向过程和面向对象、初步认识类和对象、类大小的计算、this指针)

简介: C++——类和对象(了解面向过程和面向对象、初步认识类和对象、类大小的计算、this指针)

类和对象


1. 面向过程和面向对象

在学习C++类和对象之前,我们首先需要搞清楚什么是面向过程,什么是面向对象

1.1 面向过程

我们以前学的C语言就是典型的面向过程的语言

面向过程编程是一种以过程为中心的编程方法。在这种范式下,程序被划分为一系列函数或过程,这些函数用于解决特定的问题

例如:我们可以将用手洗衣服看作是面向过程的:

  • 通过”放水“”手搓“”拧干“等一系列过程来达到将衣服洗干净的目的。

面向过程的语言有如下特点:

  • 数据和函数之间通常是分离的,函数对数据进行操作,数据可以是全局的或局部的。
  • 面向过程的编程语言常常使用顺序、条件语句和循环来执行任务。

1.2 面向对象

C++、Java、Python等语言都是面向对象的语言。

面向对象编程是一种以对象为中心的编程方法。在这种范式下,程序被组织成一组对象,每个对象包含数据和与之相关的方法

例如,我们可以将用洗衣机洗衣服看作是面向对象的:

  • ”衣服“”洗衣粉“是我们要关注的对象,我们只需要将要处理的对象放入“洗衣机”中,让洗衣机处理即可。
  • 而不要关心”洗衣机“具体干了什么和它的工作原理。

面向对象的语言有如下的特点:

  • 对象是类的实例,类是定义了对象的属性和方法的蓝图。
  • 面向对象编程强调数据封装、继承和多态,这些概念有助于组织和管理复杂的程序。

2. 类和对象

2.1 什么是类

在C语言中,我们有struct类型,我们称之为结构体。例如:

struct Stack
{
  int* st;
  int top;
};

但是,C语言的结构体有如下的局限性,这使得我们在使用时很不方便:

  1. 定义结构体变量时,类型名太长。例如我们要定义上面的结构体类型的变量st1
struct Stack st1;
  1. 结构体内只能声明变量,而不能声明和定义函数

为了解决这些问题,C++规定:可以在struct里面声明和定义函数

例如,在C++中,我们可以这样实现一个栈:

struct Stack
{
  void Init(int capacity)
  {
    _capacity = capacity;
    _st = (int*)malloc(sizeof(int) * _capacity);
    _top = 0;
  }
  void Push(int val)
  {
    if (_top == _capacity)
    {
      _capacity *= 2;
      int* tmp = (int*)realloc(_st, sizeof(int) * _capacity);
      if (nullptr == tmp)
        exit(-1);
      _st = tmp;
    }
    _st[_top++] = val;
  }
  //仅为了展示C++struct里面可以声明和定义函数
  //故其他功能不做展示
  int* _st;
  int _top;
  int _capacity;
};
  • 在C++中,我们就称用struct关键字修饰的结构为
  • 同时,C++更喜欢用class来声明类,而不是struct

C++的类由这样的特点:

  1. 类名就是类型名。例如:有一个类为class Stack,那么就可以用这个类名定义一个变量st1:Stack st1
  2. 类整体定义的是一个作用域(由一对花括号{}包裹起来的就是一个作用域)
  3. C++兼容C语言的绝大多数语法,可以说C++的类是C语言struct的升级

2.2 类的定义

类的定义方法为:

//class也可以换为struct
class className
{
    //类体:由成员函数和成员变量组成
};  //注意这个分号
  • class/struct为定义类要用到的关键字,className为类名
  • 类体中的变量称为类的属性或者成员变量,类体中的函数称为类的方法或者成员函数

2.2.1 声明和定义类中函数的两种方法

方法一——将声明和定义放在一起

例如:

class Date
{
public:
  void Init(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  int _year;
  int _month;
  int _day;
};

如果将成员函数的声明和定义都放到一起,那就需要注意:该函数可能被编译器认定为inline内联函数

注:如果对inline内联函数不太了解,建议看看👉C++特性——inline内联函数

方法二——将声明和定义分离

如果采用”在类里面声明函数,在类外面定义函数“的方法,那就需要通过域作用限定符:,将类名和函数名连接起来,用来说明定义的函数是这个类里面的。

例如:


在日常写代码中,我们可以将方法一和方法二结合来定义类:将复杂的、代码量大的函数定义在类外,将频繁使用的、代码简单的函数定义在类里面。这样不仅可以提高效率,而且可以提高代码的可阅读性。

2.2.2 声明成员变量的小细节

我们来看一个Date类的声明:

class Date
{
  void Init(int year = 1, int month = 1, int day = 1)
    {
        year = year;
        month = month;
        day = day;
  }
    void Print()
  {
    cout << "Date-> " << year << ':' << month << ':' << day << endl;
  }
  int year;
  int month;
  int day;
};

我们声明的类成员变量为year, month, day,成员函数Init的三个形参也同样为year, month, day,当我们进行赋值语句的时候,是否可以得到正确的结果呢?

我们对其初始化,并打印:

int main()
{
  Date d1;
  d1.Init(2023, 10, 23);
  d1.Print();
  return 0;
}

output:

Date-> -858993460:-858993460:-858993460

可以看到,并没有得到我们想要的结果。

  • 因此,为了防止类似错误的出现,并提高代码的可阅读性
  • 在C++中,我们一般将内里面的成员变量的名字前加下划线_
class Date
{
  int _year;
  int _month;
  int _day;
};

2.3 访问限定符

我们同样以stack类为例子:

class Stack
{
  void Init(int capacity)
  {
    _capacity = capacity;
    _st = (int*)malloc(sizeof(int) * _capacity);
    _top = 0;
  }
  void Push(int val)
  {
    if (_top == _capacity)
    {
      _capacity *= 2;
      int* tmp = (int*)realloc(_st, sizeof(int) * _capacity);
      if (nullptr == tmp)
        exit(-1);
      _st = tmp;
    }
    _st[_top++] = val;
  }
  //仅为了展示C++struct里面可以声明和定义函数
  //故其他功能不做展示
  int* _st;
  int _top;
  int _capacity;
};

我们将储存数据的数组st,栈顶指针top,栈的最大容量capacity及其相关方法(成员函数)放入stack类后,

  • 一般来说,我们并不希望用户能直接修改sttopcapacity的内容,
  • 而是希望用户能够调用内里面的方法(成员函数)来间接地改变sttopcapacity,来实现栈的功能
  • 就像C语言是通过调用函数来操作栈,而不是直接操作栈的相关参数。

因此为了限制用户访问类成员的权限,C++有了关键字——访问限定符

访问限定符有以下三类:

  • public(公有):public修饰的成员可以在类外直接被访问
  • protected(保护)、private(私有):现阶段我们认为protectedprivate没有区别的。被他们修饰的类成员不能在内外访问

2.3.1 访问限定符的作用范围

访问限定符的作用范围:

  • 从该访问限定符出现开始
  • 到下一个访问限定符出现结束

例如:

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

Date类里面,成员函数Initpublic修饰,可以在类外被访问,成员变量_year_month_dayprivate修饰,不能在类外被访问。

2.3.2 class类和struct类的默认访问权限

需要清楚,如果不在类里面加访问限定符

  • class类的默认访问权限是private
  • struct类的默认访问权限是public

例如:

class Date
{
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1._year = 1;
  return 0;
}
//会报错:无法访问 private 成员(在“Date”类中声明)
//这就说明了:class类的默认访问权限就是private
//而如果将class改为struct,那么就可以正常运行

2.4 类的实例化

用类定义一个对象的过程就叫做类的实例化

需要注意:

  • 当我们只是声明一个类时,这个类是并不会占据任何空间的。因为类里面只是对各成员的声明,而没有开辟任何空间
  • 只有当我们用类实例化出一个对象,我们才可以对类成员进行引用等操作
  • 一个类可以实例化多个对象

例如:

struct Date
{
  int _year = 1;
  int _month;
  int _day;
};
int main()
{
  Date._year = 1;
    //会报错:error C2059: 语法错误:“.”
  return 0;
}

我们也可以将类比作是构造图,将对象比作是房子,来理清二者之间的关系:

  • 构造图只是一张图纸,不会占据土地空间——类只是对成员的声明,不会开辟空间
  • 由构造图建造出的房子会占用实际的土地空间——由类实例化出的对象会开辟空间来存储各成员
  • 一张构造图可以建造出许多房子——一个类可以实例化多个对象

2.5 类大小的计算

当类中没有成员函数时,类所占空间的大小遵循C语言结构体大小的计算规则

  • 结构体的第一个成员永远放在相较于结构体变量起始位置偏移量为0的位置
  • 从第二个成员开始,往后的每个成员都要对齐到某个对齐数的整数倍处
    - 对齐数:结构体成员自身大小和默认对齐数的较小值
  • 结构体的总大小必须是最大对齐数的整数倍
    - 最大对齐数:所有成员的对齐数中的最大值

例如:

class Grade
{
  int _number;
  double _math;
  float _chinese;
};
int main()
{
  cout << sizeof(Grade) << endl;
  return 0;
}

output:

24

注:如果对于结构体大小的计算不了解,建议看看👉C语言结构体详解

但是,如果类里面有成员函数呢?这个类的大小又是多少呢?

例如:

struct Date
{
  void Init(int year = 1, int month = 1, int day = 1);
  int _year;
  int _month;
  int _day;
};
int main()
{
  cout << "sizeof(Date) -> " <<  sizeof(Date) << endl;
  return 0;
}

要搞清楚C++类的大小到底怎么计算,我们首先就要搞清楚类对象的存储方式到底是怎么样的。

2.5.1 类对象的存储方式

让我们来思考一个问题:

用一个类创建多个对象时,类中的成员变量需要多开辟一份吗?类中的成员函数需要多开辟一个吗?

如果想不清楚,我们仍可以用建房子来类比

  • 将房子中的卧室、厕所、厨房等私用设施比作是成员变量;将房子外的公园、亭子等公用设施比作是成员函数
  • 显然,当我们用一份构造图建造多个房子时,房子的厕所、卧室肯定是要重新新建的,而房子外的公园、亭子用原来的就好

用类实例化多个对象也是如此:不同的对象所包含的成员变量为各自所有,需要重新开辟,而成员函数是这些对象共有的,不要开辟

因此,类对象的存储方式应该是这样的:

类对象只存储成员变量,而成员函数放在公共代码区


既然类对象的成员变量并不和成员函数存放在一起,那么自然计算类的大小时,也就不需要考虑成员函数了。

所以:

struct Date
{
  void Init(int year = 1, int month = 1, int day = 1);
  int _year;
  int _month;
  int _day;
};
//Date类所占大小就是12

那么,空类的大小又是多少呢?

class Test
{
};

C++规定:空类的大小为1,这个字节不存储有效数据。用来标识定义的对象存在过


可以总结:

  • 空类的大小为1个字节
  • 类的大小实际上是“成员变量的大小之和”(注意内存对齐)
  • 成员函数存放在公共代码区,不用计算

2.6 this指针

看下面的代码:

class Date
{
public:
  void Init(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1, d2;
  d1.Init(2023, 10, 23);
  d2.Init();
  return 0;
}

上面的代码中,我们定义了类Date,同时示例化了两个对象d1, d2

现在就要问大家一个问题:既然这两个对象用的都是同一个Init()函数,那编译器是怎么知道他要处理的是d1的成员变量还是d2的成员变量?

C++通过引入this指针来处理这个问题:

C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

上面的Init()函数和d1.Init(2023, 10, 23);实际上等价于:

void Init(Date* this, int year = 1, int month = 1, int day = 1)
{
    this->_year = year;
    this->_month = month;
    this->_day = day;
}
d1.Init(&d1, 2023, 10, 23);

2.6.1 this指针的特性

  1. this指针被*const修饰:* const this。表示:不能修改this指针的指向this至指向当前对象),但是可以修改this指针指向空间的值(可以通过this指针访问成员变量)
  2. 不能写this相关的形参或实参,但是可以在类的成员函数里面显示的使用
  3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
  4. this指针可以为空

最后,我们用一道题来结束本篇文章:

#include <iostream>
using namespace std;
class Test
{
public:
  void Print1()
  { 
    cout << "Print()" << endl;
  }
  void Print2()
  {
    cout << _a << endl;
  }
private:
  int _a;
};
int main()
{
  Test* t1 = nullptr;
    t1->Print1(); //Yes or Not ?
  t1->Print2(); //Yes or Not ? 
  return 0;
}

大家认为这个程序会得到什么结果呢?

我们来进行调试:

为什么会出现这种情况呢?

  • 我们定义了一个指向Test类对象的指针t1,但将其赋为空指针nullptr
  • 函数Print1()并没有访问类成员变量,而且成员函数存放在公共代码区,因此尽管this指针为空,我们也可以正常使用Print1()
  • 函数Print2()实际上可以写为:
void Print2(Test* this)
{
    cout << this->_a << endl;
}
  • 由于this指针为空,空指针并不指向任何有效数据,显然就会发生错误。

3. 总结

本次我们对类和对象进行了初步的了解和学习:知道了面向过程和面向对象的基本概念,知道了类的定义和类的实例化等相关概念和操作。

但C++类和对象的知识远不止于此。后面我们将继续学习关于类和对象的构造函数、析构函数、拷贝函数、运算符重载的知识,感兴趣的小伙伴可以订阅此专栏。

👉C++教程

相关文章
|
15天前
|
存储 C++ 容器
C++入门指南:string类文档详细解析(非常经典,建议收藏)
C++入门指南:string类文档详细解析(非常经典,建议收藏)
28 0
|
15天前
|
存储 编译器 C语言
C++入门: 类和对象笔记总结(上)
C++入门: 类和对象笔记总结(上)
30 0
|
6天前
|
存储 算法 C语言
【C++初阶】8. STL初阶 + String类
【C++初阶】8. STL初阶 + String类
44 1
|
6天前
|
C语言 C++
【C++初阶】9. string类的模拟实现
【C++初阶】9. string类的模拟实现
33 1
|
13天前
|
存储 安全 编译器
【C++】类的六大默认成员函数及其特性(万字详解)
【C++】类的六大默认成员函数及其特性(万字详解)
31 3
|
15天前
|
C++
4. C++类的组合
4. C++类的组合
25 0
|
15天前
|
编译器 C语言 C++
【c++】类和对象(三)构造函数和析构函数
朋友们大家好,本篇文章我们带来类和对象重要的部分,构造函数和析构函数
|
15天前
|
存储 编译器 C语言
【c++】类和对象(二)this指针
朋友们大家好,本节内容来到类和对象第二篇,本篇文章会带领大家了解this指针
【c++】类和对象(二)this指针
|
16天前
|
存储 编译器 C语言
【c++】类和对象(一)
朋友们,大家好,本篇内容我们来对类和对象进行初步的认识
|
16天前
|
算法 C++ 容器
【C++练级之路】【Lv.10】【STL】priority_queue类和反向迭代器的模拟实现
【C++练级之路】【Lv.10】【STL】priority_queue类和反向迭代器的模拟实现