类和对象(上):走进面向对象编程

个人专栏:《数据结构-初阶》《经典OJ题目》《C语言》《小白算法成长录》

欢迎大佬交流

本文代码已同步GitHub:GitHub - Stellen-z/DailyCode: pracetice · GitHub

一、类的定义

1、类定义格式

  • 类名PascalCase(如 StudentManager

  • 访问标签public:protected:private: 后加冒号,独占一行,缩进与类同级

  • 成员变量 :建议使用 m_ 前缀或尾随 _(如 m_ageage_

  • 方法定义 :小驼峰或下划线分隔(如 getValue()get_value()

  • 分号 :类定义结束后必须加分号 };

C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,那么struct中也可以定义函数

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

下面给出一个例子

cpp 复制代码
class Date
  {
  public:
      void Init(int year, int month, int day)
      {
          _year = year;
          _month = month;
          _day = day;
      }
  ​
  private:
      //区分成员变量,加上_
      int _year;
      int _month;
      int _day;
  };

接着来看struct的升级

cpp 复制代码
  #include <iostream>
  ​
  using namespace std;
  ​
  //struct升级为类
  //1.struct里面可以定义函数
  //2.名称即类型
  ​
  //兼容C语言
  typedef struct ListNodeC 
  {
      int val;
      struct ListNodeC* next;
  }ListNodeC;
  ​
  ​
  //C++名称即代表类型
  struct ListNodeCPP
  {
      void Init(int x)
      {
          next = nullptr;
          val = x;
      }
  ​
      ListNodeCPP* next;
      int val;
  };
复制代码

2、访问限定符

访问限定符是C++一种实现封装的方式,将类和对象的属性与方法结合,通过访问选择性地将其接口提供

  • public:接口部分,对外暴露(如成员函数、构造函数)。

  • private :隐藏实现细节(推荐成员变量默认为 private)。

  • protected:为派生类提供"继承但封装"的能力,常用于基类的扩展点。

  • 默认限定符class 默认 privatestruct 默认 public

  • 顺序灵活 :通常先写 public 接口,再写 protected/private 实现。

3、类域

类域指的是类定义内部所形成的作用域

a、核心特点

  • 类的成员(数据成员、成员函数、嵌套类型)位于该类独有的作用域内

  • 在类外访问成员时,必须通过 对象名.成员指针->成员类名::静态成员 的方式。

  • 在类内(成员函数体中)直接使用成员名即可,编译器会自动在当前类域中查找。

b、作用域解析符 ::

  • 用于在类外定义成员函数:ReturnType ClassName::methodName() { ... }

  • 用于区分同名的全局变量和类静态成员:ClassName::member

c、嵌套作用域

  • 类域可以嵌套(如成员函数内的局部作用域、嵌套类)。

  • 内部作用域可以访问外部类域中的成员(只要权限允许)。

类域影响的是编译的查找规则,我们通过下面例子来理解

cpp 复制代码
#include <iostream>
  ​
  using namespace std;
  ​
  class Stack
  {
  public:
      //成员函数
      void Init(int n = 4);
  ​
  private:
      //成员变量
      int* arr;
      int _top;
      int _capacity;
  };
  ​
  //没有指定类域
  void Init(int n)
  {
      ////error C2065: "arr": 未声明的标识符
      arr = (int*)malloc(sizeof(int) * n);
      if (nullptr == arr)
      {
          perror("malloc failed!\n");
          exit(1);
      }
  ​
      // error C2065:"_top": 未声明的标识符
      _top = 0;
      //error C2065:"_capacity": 未声明的标识符
      _capacity = 0;
  ​
  }
  ​
  void Stack::Init(int n)
  {
      ////error C2065: "arr": 未声明的标识符
      arr = (int*)malloc(sizeof(int) * n);
      if (nullptr == arr)
      {
          perror("malloc failed!\n");
          exit(1);
      }
  ​
      // error C2065:"top": 未声明的标识符
      _top = 0;
      //error C2065:"capacity": 未声明的标识符
      _capacity = 0;
  ​
  }
  ​
  int main()
  {
      Stack st;
      st.Init();
  ​
  ​
      return 0;
  }
复制代码

解释:函数定义时没有指定类域,在编译时就会在全局中找声明,显然找不到,就会报错

而指定类域Stack,即当全局域找不到时就回去类域中查找

二、实例化

1、实例化的概念

实例化(Instantiation)是指根据类(类型蓝图)创建具体对象(实例)的过程。

类是对象的一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量;

这些成员变量只是声明,并没与分配空间;

用类进行实例化时,才会分配空间

对象 = 类的实例,拥有独立的成员变量副本

同样,我们通过例子来讲解

cpp 复制代码
 #include <iostream>
  ​
  using namespace std;
  ​
  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:
      //成员变量在类中只是声明,没有开空间
      int _year;
      int _month;
      int _day;
  };
  ​
  int main()
  {
      //实例化对象d1,d2
      Date d1;
      Date d2;
  ​
      d1.Init(2026, 6, 7);
      d1.Print();
  ​
      d2.Init(2026, 7, 7);
      d2.Print();
  ​
      return 0;
  }

2、对象大小

实例化出来的每个对象都有独立的空间;

对象里面包含成员变量,成员函数;

a、成员变量

对象的数据空间中肯定包含成员变量;

那么成员变量按照什方式存储呢?

答案是按照内存对齐规则

内存对齐规则:

基本对齐原则

  • 每个成员 的起始偏移量必须是其自身大小的整数倍(或编译器默认对齐值的较小者)。

  • 整个结构体 的大小必须是其最大成员对齐要求的整数倍(末尾可能填充)。

默认对齐值

  • 基本类型对齐 = 其大小(char=1,short=2,int=4,double=8 等)。

  • 指针:8 字节(64位系统)。

  • 编译器可设定默认最大对齐值(如 MSVC 默认 8,GCC 默认 8/16 等)。

b、成员函数

函数名就是函数对应的地址,如果要存储函数,就需要存储函数指针;

分析一下:实例化的对象,每次调用的成员函数是否相同?

调用的成员函数是相同的!

为什么?

我们通过汇编指令来看

显然,两个对象调用函数的地址完全相同

三、this指针

1、this指针的用处

我们先来看看实例化的例子中代码运行情况

调用Initprint 函数时,函数时怎样知道访问的是d1还是d2呢?

关键就是隐含的this指针

在编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类型的 this 指针;

对于Init来说,原型为:void Init(Date* const this,int year, int month, int day)

在成员函数中访问成员变量也是通过 this指针进行访问

对于Init来说,即为

cpp 复制代码
void Init(Date* const this, int year, int month, int day)
      {
          this->_year = year;
          this->_month = month;
          this->_day = day;
      }

2、经典题目

下面来看两道经典的题目

a、Eg1

1、程序运行结果为? A、编译报错 B、运行崩溃 C、正常运行

cpp 复制代码
 #include <iostream>
  ​
  using namespace std;
  ​
  class A
  {
  public:
      void Print()
      {
          cout << "A::Print()" << endl;
      }
  ​
  private:
      int _a;
  };
  ​
  ​
  int main()
  {
      A* p = nullptr;
      p->Print();
  ​
      return 0;
  }

分析:

首先排除A选项,代码中并没有语法错误;

接着考虑 p-> 这是对 p 的解引用吗?如果是的话那就选择B选择

来看运行结果

显然程序正常运行,那就说明没有对 p 进行解引用

前面我们说过,调用的函数地址都是一样的,不同的是传给 this 指针的地址不同;

那么在汇编指令上, 首先将 p 传给 this 指针,接着call 函数地址进行调用

进入成员函数之后,直接进行打印,程序正常运行

b、Eg2

1、程序运行结果为? A、编译报错 B、运行崩溃 C、正常运行

cpp 复制代码
#include <iostream>
  ​
  using namespace std;
  ​
  class A
  {
  public:
      void Print()
      {
          cout << "A::Print()" << endl;
          cout << _a << endl;
      }
  ​
  private:
      int _a;
  };
  ​
  ​
  int main()
  {
      A* p = nullptr;
      p->Print();
  ​
      return 0;
  }

分析:这道题和上道题唯一的区别就是在成员函数内加了一句cout << _a << endl; 这会导致什么?

根据第一题分析的经验,我们知道传参是将p 传给了 this指针,那么this也就是空指针;

而访问成员变量就是通过this指针进行访问的;

那么就会造成 空指针解引用! 导致程序崩溃!

四、初探封装

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

首先来对比下面C语言实现的Stack和当前知识下C++实现的Stack

C语言版

cpp 复制代码
#include<stdio.h>
  #include<stdlib.h>
  #include<stdbool.h>
  #include<assert.h>
  ​
  typedef int STDataType;
  typedef struct Stack
  {
      STDataType* a;
      int top;
      int capacity;
  }Stack;
  ​
  ​
  // 初始化栈 
  void StackInit(Stack* ps)
  {
      assert(ps);
      ps->_a = NULL;
      ps->_capacity = ps->_top = 0;
  }
  ​
  // 入栈 
  void StackPush(Stack* ps, STDataType data)
  {
      assert(ps);
      if (ps->_capacity == ps->_top)
      {
          int newcapacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;
          STDataType* tmp = (STDataType*)realloc(ps->_a, sizeof(STDataType) * newcapacity);
          if (tmp == NULL)
          {
              perror("realloc failed!\n");
              exit(1);
          }
  ​
          ps->_a = tmp;
          ps->_capacity = newcapacity;
      }
  ​
      ps->_a[ps->_top++] = data;
  }
  ​
  // 出栈 
  void StackPop(Stack* ps)
  {
      assert(ps);
      assert(ps->_top > 0);
  ​
      ps->_top--;
  }
  ​
  // 获取栈顶元素 
  STDataType StackTop(Stack* ps)
  {
      assert(ps);
      assert(ps->_top > 0);
  ​
      return ps->_a[ps->_top - 1];
  }
  ​
  // 获取栈中有效元素个数 
  int StackSize(Stack* ps)
  {
      assert(ps);
  ​
      return ps->_top;
  }
  ​
  // 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
  bool StackEmpty(Stack* ps)
  {
      assert(ps);
      return ps->_top == 0;
  }
  ​
  // 销毁栈 
  void StackDestroy(Stack* ps)
  {
      free(ps->_a);
      ps->_a = NULL;
      ps->_capacity = ps->_top = 0;
  }
  ​
  int main()
  {
      Stack st;
      STInit(&st);
      
      StackPush(&st,1);
      StackPush(&st,2);
      StackPush(&st,3);
      StackPush(&st,4);
      
      while(!StackEmpty(&st))
      {
          printf("%d ",StackTop(&st));
          StackPop(&St);
      }
      return 0;
  }
复制代码

C++版

cpp 复制代码
#include <iostream>
  #include <assert.h>
  ​
  using namespace std;
  ​
  typedef int STDataType;
  ​
  class Stack
  {
  public:
      void Init(int n = 4)
      {
          _a = (STDataType*)malloc(sizeof(STDataType) * n);
          if (_a == NULL)
          {
              perror("malloc failed!\n");
              exit(1);
          }
  ​
          _top = 0;
          _capacity = 0;
      }
  ​
      void Push(STDataType x)
      {
          if (_top == _capacity)
          {
              int newcapacity = _capacity * 2;
              STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));
              if (tmp == NULL)
              {
                  perror("realloc fail");
                  return;
              }
              _a = tmp;
              _capacity = newcapacity;
          }
  ​
          _a[_top++] = x;
  ​
      }
  ​
  ​
      void Pop()
      {
          assert(_top > 0);
          --_top;
      }
  ​
      bool Empty()
      {
          return _top == 0;
      }
  ​
      int Top()
      {
          assert(_top > 0);
          return _a[_top - 1];
      }
  ​
      void Destroy()
      {
          free(_a);
          _a = nullptr;
          _top = _capacity = 0;
      }
  ​
  private:
      STDataType* _a;
      int _top;
      int _capacity;
  };
  ​
  int main()
  {
      Stack st;
      st.Init();
  ​
      st.Push(1);
      st.Push(2);
      st.Push(3);
      st.Push(4);
  ​
      while (!st.Empty())
      {
          cout << st.Top() << " ";
          st.Pop();
      }
      
      st.Destroy();
  ​
      return 0;
  }

对比之后,我们发现,底层逻辑依然没变

但实现形态上发生了变化

  • C++中变量和函数都放在了类里面,通过访问限定符进行限制,不能随意进行修改

  • C++中不再需要typedef来简写,直接用类型即可

如果觉得有帮助,可以关注 GitHub 项目持续更新:GitHub - Stellen-z/DailyCode: pracetice · GitHub

相关推荐
晚风叙码1 小时前
《C++面向对象进阶:static成员、友元、匿名对象与拷贝优化详解》
c++
j7~1 小时前
【C++】STL--string类--拆析解剖string以及string类的底层详解(1)
开发语言·c++·ascii编码·string类·auto和范围for
草莓熊Lotso1 小时前
【Linux网络】深入理解 TCP 协议(二):序号机制、流量控制与连接管理
linux·运维·服务器·网络·c++·tcp/ip
小欣加油12 小时前
leetcode1926 迷宫中离入口最近的出口
数据结构·c++·算法·leetcode·职场和发展
星恒随风13 小时前
C++ 类和对象入门(五):初始化列表、explicit 和 static 成员详解
开发语言·c++·笔记·学习·状态模式
浪客灿心14 小时前
项目篇:模块设计与实现
数据库·c++
牛油果子哥q14 小时前
【C++ STL vector】C++ STL vector 终极精讲:动态数组底层原理、两倍扩容机制、迭代器失效、增删查改、性能剖析与工程避坑指南
开发语言·c++
为何创造硅基生物16 小时前
独占指针的创建std::make_unique 本身自带堆出现
c++
kyle~16 小时前
ROS 2 与 Isaac Sim 联合仿真(一)体系架构、环境选型与基础通信闭环
c++·机器人·nvidia·仿真·ros2