【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语言中则需要用户自己维护。



相关文章
|
2天前
|
编译器 C++
C++ 类构造函数初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。
43 30
|
17天前
|
C++
C++(十一)对象数组
本文介绍了C++中对象数组的使用方法及其注意事项。通过示例展示了如何定义和初始化对象数组,并解释了栈对象数组与堆对象数组在初始化时的区别。重点强调了构造器设计时应考虑无参构造器的重要性,以及在需要进一步初始化的情况下采用二段式初始化策略的应用场景。
|
17天前
|
存储 编译器 C++
C ++初阶:类和对象(中)
C ++初阶:类和对象(中)
|
17天前
|
C++
C++(十六)类之间转化
在C++中,类之间的转换可以通过转换构造函数和操作符函数实现。转换构造函数是一种单参数构造函数,用于将其他类型转换为本类类型。为了防止不必要的隐式转换,可以使用`explicit`关键字来禁止这种自动转换。此外,还可以通过定义`operator`函数来进行类型转换,该函数无参数且无返回值。下面展示了如何使用这两种方式实现自定义类型的相互转换,并通过示例代码说明了`explicit`关键字的作用。
|
17天前
|
存储 设计模式 编译器
C++(十三) 类的扩展
本文详细介绍了C++中类的各种扩展特性,包括类成员存储、`sizeof`操作符的应用、类成员函数的存储方式及其背后的`this`指针机制。此外,还探讨了`const`修饰符在成员变量和函数中的作用,以及如何通过`static`关键字实现类中的资源共享。文章还介绍了单例模式的设计思路,并讨论了指向类成员(数据成员和函数成员)的指针的使用方法。最后,还讲解了指向静态成员的指针的相关概念和应用示例。通过这些内容,帮助读者更好地理解和掌握C++面向对象编程的核心概念和技术细节。
|
30天前
|
存储 算法 编译器
c++--类(上)
c++--类(上)
|
1月前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
1月前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
|
17天前
|
存储 C++
C++(五)String 字符串类
本文档详细介绍了C++中的`string`类,包括定义、初始化、字符串比较及数值与字符串之间的转换方法。`string`类简化了字符串处理,提供了丰富的功能如字符串查找、比较、拼接和替换等。文档通过示例代码展示了如何使用这些功能,并介绍了如何将数值转换为字符串以及反之亦然的方法。此外,还展示了如何使用`string`数组存储和遍历多个字符串。
|
25天前
|
存储 C++
C++ dll 传 string 类 问题
C++ dll 传 string 类 问题
16 0