【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++
|
1天前
|
编译器 C++
【C++】详解初始化列表,隐式类型转化,类静态成员,友元
【C++】详解初始化列表,隐式类型转化,类静态成员,友元
|
4天前
|
存储 编译器 C++
【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
|
4天前
|
编译器 C++
【C++】类和对象④(类的默认成员函数:取地址及const取地址重载 )
本文探讨了C++中类的成员函数,特别是取地址及const取地址操作符重载,通常无需重载,但展示了如何自定义以适应特定需求。接着讨论了构造函数的重要性,尤其是使用初始化列表来高效地初始化类的成员,包括对象成员、引用和const成员。初始化列表确保在对象创建时正确赋值,并遵循特定的执行顺序。
|
4天前
|
C语言 C++
【C++】日期类Date(详解)③
该文介绍了C++中直接相减法计算两个日期之间差值的方法,包括确定max和min、按年计算天数、日期矫正及计算差值。同时,文章讲解了const成员函数,用于不修改类成员的函数,并给出了`GetMonthDay`和`CheckDate`的const版本。此外,讨论了流插入和流提取的重载,需在类外部定义以符合内置类型输入输出习惯,并介绍了友元机制,允许非成员函数访问类的私有成员。全文旨在深化对运算符重载、const成员和流操作的理解。
|
4天前
|
C++
【C++】日期类Date(详解)②
- `-=`通过复用`+=`实现,`Date operator-(int day)`则通过创建副本并调用`-=`。 - 前置`++`和后置`++`同样使用重载,类似地,前置`--`和后置`--`也复用了`+=`和`-=1`。 - 比较运算符重载如`&gt;`, `==`, `&lt;`, `&lt;=`, `!=`,通常只需实现两个,其他可通过复合逻辑得出。 - `Date`减`Date`返回天数,通过迭代较小日期直到与较大日期相等,记录步数和符号。 ``` 这是236个字符的摘要,符合240字符以内的要求,涵盖了日期类中运算符重载的主要实现。
|
4天前
|
定位技术 C语言 C++
C++】日期类Date(详解)①
这篇教程讲解了如何使用C++实现一个日期类`Date`,涵盖操作符重载、拷贝构造、赋值运算符及友元函数。类包含年、月、日私有成员,提供合法性检查、获取某月天数、日期加减运算、比较运算符等功能。示例代码包括`GetMonthDay`、`CheckDate`、构造函数、拷贝构造函数、赋值运算符和相关运算符重载的实现。
|
4天前
|
编译器 C++
【C++】类和对象③(类的默认成员函数:赋值运算符重载)
在C++中,运算符重载允许为用户定义的类型扩展运算符功能,但不能创建新运算符如`operator@`。重载的运算符必须至少有一个类类型参数,且不能改变内置类型运算符的含义。`.*::sizeof?`不可重载。赋值运算符`=`通常作为成员函数重载,确保封装性,如`Date`类的`operator==`。赋值运算符应返回引用并检查自我赋值。当未显式重载时,编译器提供默认实现,但这可能不足以处理资源管理。拷贝构造和赋值运算符在对象复制中有不同用途,需根据类需求定制实现。正确实现它们对避免数据错误和内存问题至关重要。接下来将探讨更多操作符重载和默认成员函数。
|
4天前
|
存储 编译器 C++
【C++】类和对象③(类的默认成员函数:拷贝构造函数)
本文探讨了C++中拷贝构造函数和赋值运算符重载的重要性。拷贝构造函数用于创建与已有对象相同的新对象,尤其在类涉及资源管理时需谨慎处理,以防止浅拷贝导致的问题。默认拷贝构造函数进行字节级复制,可能导致资源重复释放。例子展示了未正确实现拷贝构造函数时可能导致的无限递归。此外,文章提到了拷贝构造函数的常见应用场景,如函数参数、返回值和对象初始化,并指出类对象在赋值或作为函数参数时会隐式调用拷贝构造。
|
4天前
|
存储 编译器 C语言
【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。