【C++】类和对象 (上篇)(3)

简介: 【C++】类和对象 (上篇)(3)

八、this 指针

1、this 指针的引出

为了引出 this 指针,我们先来简略定义一个日期类 Date:

class Date
{
public:
  void Init(int year = 1970, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1.Init(2022, 10, 3);
  d1.Print();
  Date d2;
  d2.Init(2022, 10, 4);
  d2.Print();
  return 0;
}

2020062310470442.png

对于上述类,有这样的一个问题:Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函

数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

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

2020062310470442.png

即上面的代码经过编译器处理后会变成下面这样:

class Date
{
public:
  void Init(Date* const this, int year = 1970, int month = 1, int day = 1)
  {
    this->_year = year;
    this->_month = month;
    this->_day = day;
  }
  void Print(Date* const this)
  {
    cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1.Init(&d1, 2022, 10, 3);
  d1.Print(&d1);
  Date d2;
  d2.Init(&d2, 2022, 10, 4);
  d2.Print(&d2);
  return 0;
}

但是 this 指针参数以及对象的地址都是由编译器自动传递的,当用户主动传递时编译器会报错;不过在成员函数内部我们是可以显示的去使用 this 指针的。

2020062310470442.png

2、this 指针的特性

this 指针有如下特性:

this 指针只能在 “成员函数” 的内部使用;

this 指针使用 const 修饰,且 const 位于指针*的后面;即 this 本身不能被修改,但可以修改其指向的对象 (我们可以通过 this 指针修改成员变量的值,但不能让 this 指向其他对象)

this 指针本质上是“成员函数”的一个形参,当对象调用成员函数时,将对象地址作为实参传递给 this 形参,所以对象中不存储this 指针;

this 指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过建立“成员函数”的函数栈帧时压栈传递,不需要用户主动传递。(注:由于this指针在成员函数中需要被频繁调用,所以VS对其进行了优化,由编译器通过ecx寄存器传递)

3、相关面试题

1、this指针存在哪里?


答:this 指针作为函数形参,存在于函数的栈帧中,而函数栈帧在栈区上开辟空间,所以 this 指针存在于栈区上;不过VS这个编译器对 this 指针进行了优化,使用 ecx 寄存器保存 this 指针;


2、this 指针可以为空吗?


答:this 指针作为参数传递时是可以为空的,但是如果成员函数中使用到了 this 指针,那么就会造成对空指针的解引用;


3、下面这两个程序编译运行的结果分别是什么?

//下面两段程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A  //程序1
{
public:
    void Print()
  {
    cout << "Print()" << endl;
  }
private:
  int _a;
};
int main()
{
  A* p = nullptr;
  p->PrintA();
  return 0;
}
//***********************************//
class A  //程序2
{
public:
  void PrintA()
  {
    cout << _a << endl;
  }
private:
  int _a;
};
int main()
{
  A* p = nullptr;
  p->Print();
  return 0;
}

答:程序1正常运行。原因如下:

第一,虽然我们用空指针A访问了成员函数Print,但是由于成员函数并不存在于对象中,而是存在于代码段中,所以编译器并不会通过类对象p去访问成员函数,即并不会对p进行解引用;

第二,当对象是指针类型时,编译器会直接把这个指针作为形参传递给Print函数的 this 指针,而 this 作为参数传递是时可以为空的,在Print函数内部我们也并没有对 this 指针进行解引用。

2020062310470442.png

程序2运行崩溃。原因如下:

程序2在 p->Print 处虽然可以正常运行,但是在Print函数内部,_a 会被转化为 this->_a,发生了空指针的解引用。

2020062310470442.png

九、C语言和C++实现 Stack 的对比

1、C语言实现

typedef int DataType;
typedef struct Stack
{
  DataType* array;
  int capacity;
  int top;
}Stack;
void StackInit(Stack* ps)
{
  assert(ps);
  ps->array = (DataType*)malloc(sizeof(DataType) * 4);
  if (NULL == ps->array)
  {
    perror("malloc fail\n");
    exit(-1);
  }
  ps->capacity = 4;
  ps->top = 0;
}
void StackDestroy(Stack* ps)
{
  assert(ps);
  if (ps->array)
  {
    free(ps->array);
    ps->array = NULL;
    ps->capacity = 0;
    ps->top = 0;
  }
}
void CheckCapacity(Stack* ps)
{
  if (ps->top == ps->capacity)
  {
    int newcapacity = ps->capacity * 2;
    DataType* temp = (DataType*)realloc(ps->array,newcapacity * sizeof(DataType));
    if (temp == NULL)
    {
      perror("realloc fail\n");
      exit(-1);
    }
    ps->array = temp;
    ps->capacity = newcapacity;
  }
}
void StackPush(Stack* ps, DataType data)
{
  assert(ps);
  CheckCapacity(ps);
  ps->array[ps->top] = data;
  ps->top++;
}
int StackEmpty(Stack* ps)
{
  assert(ps);
  return 0 == ps->top;
}
void StackPop(Stack* ps)
{
  if (StackEmpty(ps))
    return;
  ps->top--;
}
DataType StackTop(Stack* ps)
{
  assert(!StackEmpty(ps));
  return ps->array[ps->top - 1];
}
int StackSize(Stack* ps)
{
  assert(ps);
  return ps->top;
}
int main()
{
  Stack s;
  StackInit(&s);
  StackPush(&s, 1);
  StackPush(&s, 2);
  StackPush(&s, 3);
  StackPush(&s, 4);
  printf("%d\n", StackTop(&s));
  printf("%d\n", StackSize(&s));
  StackPop(&s);
  StackPop(&s);
  printf("%d\n", StackTop(&s));
  printf("%d\n", StackSize(&s));
  StackDestroy(&s);
  return 0;
}

可以看到,在用C语言实现时,Stack相关操作函数有以下共性:

  • 每个函数的第一个参数都是Stack*;
  • 函数中必须要对第一个参数检测,因为该参数可能会为NULL;
  • 函数中都是通过Stack*参数操作栈的;
  • 调用时必须传递Stack结构体变量的地址;

结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。

2、C++实现

typedef int DataType;
class Stack
{
public:
  void Init(int N = 4)
  {
    _array = (DataType*)malloc(sizeof(DataType) * N);
    if (NULL == _array)
    {
      perror("malloc fail\n");
      exit(-1);
    }
    _capacity = N;
    _top = 0;
  }
  void Push(DataType data)
  {
    CheckCapacity();
    _array[_top] = data;
    _top++;
  }
  void Pop()
  {
    if (Empty())
      return;
    _top--;
  }
  DataType Top()
  {
    return _array[_top - 1];
  }
  int Empty()
  {
    return 0 == _top;
  }
  int Size()
  {
    return _top;
  }
  void Destroy()
  {
    if (_array)
    {
      free(_array);
      _array = NULL;
      _capacity = 0;
      _top = 0;
    }
  }
  void CheckCapacity()
  {
    if (_top == _capacity)
    {
      int newcapacity = _capacity * 2;
      DataType* temp = (DataType*)realloc(_array, newcapacity *sizeof(DataType));
      if (temp == NULL)
      {
        perror("realloc fail\n");
        exit(-1);
      }
      _array = temp;
      _capacity = newcapacity;
    }
  }
private:
  DataType* _array;
  int _capacity;
  int _top;
};
int main()
{
  Stack s;
  s.Init();
  s.Push(1);
  s.Push(2);
  s.Push(3);
  s.Push(4);
  printf("%d\n", s.Top());
  printf("%d\n", s.Size());
  s.Pop();
  s.Pop();
  printf("%d\n", s.Top());
  printf("%d\n", s.Size());
  s.Destroy();
  return 0;
}

相比于C语言而言,C++中通过类可以将数据以及操作数据的方法进行完美结合,通过访问权限可以控制那些方法在类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。


而且每个方法不需要传递 Stack* 的参数了,编译器编译之后该参数会自动还原,即C++中 Stack* 参数是编译器维护的,而C语言中则需要用户自己维护。



相关文章
|
1月前
|
编译器 C++
C++之类与对象(完结撒花篇)(上)
C++之类与对象(完结撒花篇)(上)
35 0
|
9天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
36 4
|
10天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
16 0
|
1月前
|
编译器 C++ 数据库管理
C++之类与对象(完结撒花篇)(下)
C++之类与对象(完结撒花篇)(下)
31 0
|
1月前
|
编译器 C++
C++之类与对象(3)(下)
C++之类与对象(3)(下)
32 0
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)