【C++学习】类和对象 | 类的成员函数存放在哪里?| this指针 | 构造函数 | 析构函数 | 探索构造和析构函数的更多细节

简介: 【C++学习】类和对象 | 类的成员函数存放在哪里?| this指针 | 构造函数 | 析构函数 | 探索构造和析构函数的更多细节

写在前面:

上一篇文章开始学习类和对象了,结尾还留了一个疑问,


类的成员函数究竟存放在哪里?


如果有兴趣可以去看看:http://t.csdn.cn/JilEt


这篇文章先解答这个问题然后继续学习类和对象的内容。


目录


写在前面:


1. 类的成员函数存放在哪里?


2. this指针


3. 构造函数


4. 析构函数


5. 探索构造和析构函数的更多细节


写在最后:


1. 类的成员函数存放在哪里?

实际上,类的成员函数是存放在公共代码区。


你可能会问,那这个公共代码区是在哪里呢?


这个其实我们不用关心,因为我们在调用的时候编译器会帮我们找到,


最重要其实是得理解为什么成员函数要单独放在一个区域,


而类的成员变量就有多份,每个实例化出来的类对象都有一份。


再来看一个例子:(如果这个类没有成员变量呢?)


#include 
using namespace std;
class A {
  void f() {}
};
int main()
{
  cout << sizeof(A) << endl;
  return 0;
}

输出:


1

为什么是1,没有成员变量,类的大小难道不是0吗?


实际上,没有成员变量的类保留一个字节的大小是为了占位,


表示对象存在,不存储有效数据。


2. this指针

我们来看这样一段代码:

#include 
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()
{
  Date d1, d2;
  d1.Init(2022, 1, 11);
  d2.Init(2022, 1, 12);
  d1.Print();
  d2.Print();
  return 0;
}


/


我们定义了一个存放日期的类,


实现了一个初识化的函数和一个打印函数,


可是我们在调用打印函数的时候,都是直接调用 Print(),


编译器是怎么区分我们的成员变量究竟是用那一份数据的呢?


实际上,类的成员函数存在着这一个隐藏的参数,


我们通常把它称作隐藏的this指针:


还是这份代码:(以Print函数为例)

#include 
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;
  }
  //实际上编译器是这样操作的:
  //void Print(Date* const this)
  //{
  //  cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
  //}
private:
  int _year; 
  int _month; 
  int _day; 
};
int main()
{
  Date d1, d2;
  d1.Init(2022, 1, 11);
  d2.Init(2022, 1, 12);
  d1.Print();
  d2.Print();
  return 0;
}


我们不能在形参或者实参显示传递this指针 ,


但是我们可以在函数里面使用this指针的,


像这样是允许的:

//#include 
//using namespace std;
//
//class A {
//  void f() {}
//};
//
//int main()
//{
//  cout << sizeof(A) << endl;
//  return 0;
//}
#include 
using namespace std;
class Date
{
public:
  void Init(int year, int month, int day)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print()
  {
  cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
  }
  //实际上编译器是这样操作的:
  //void Print(Date* const this)
  //{
  //  cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
  //}
private:
  int _year; 
  int _month; 
  int _day; 
};
int main()
{
  Date d1, d2;
  d1.Init(2022, 1, 11);
  d2.Init(2022, 1, 12);
  d1.Print();
  d2.Print();
  return 0;
}

不过我们是不能修改this指针的,


看到那个const了吗,他修饰this,所以this不能被修改,


但是this指针指向的内容是可以被改变的。


这时候问题来了,this指针是存在哪里的?


看清楚了,this指针是一个形参啊,他就跟普通的参数一样存在函数调用的栈帧里面。


这个时候来一道紧张刺激的题目试试水,看看对this指针的理解如何:


// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行

class A
{
public:
  void Print()
  {
  cout << "Print()" << endl;
  }
private:
  int _a;
};
int main()
{
  A* p = nullptr;
  p->Print();
  return 0;
}


答案选C,


先来说A选项,这段代码没有语法错误。


再来看B选项,p调用Print函数的时候不会发生解引用,


因为Print的地址不在对象中,在调用前已经找到该函数了,所以这里没有访问空指针,


其它地方也没有访问空指针,所以也不会运行的时候崩溃,


所以答案选C,代码正常运行。


那我们再来看这一道题:


// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行

class A
{
public:
  void PrintA()
  {
  cout << _a << endl;
  }
private:
  int _a;
};
int main()
{
  A* p = nullptr;
  p->PrintA();
  return 0;
}


答案选B,


做完第一道题目,这道题其实就已经很清楚了,


调用PrintA这个函数的时候,通过this指针调用_a,就会导致访问空指针,然后崩溃。


3. 构造函数

我们在使用C语言实现数据结构或者说使用数据结构的时候,


都必须先Init初始化一下,使用完了之后又得Destory销毁,不然会内存泄漏,


怎么说呢,用个一两次到还好,要是频繁创建销毁实在是太不方便了,


这个时候,祖师爷就想了个办法,要是能自动初始化和销毁就好了,


然后,祖师爷就设计了构造函数,专门用来做初始化工作。


构造函数的特征:


1. 函数名与类名相同


2. 没有返回值(也不需要写void)


3. 对象实例化的时候编译器自动调用对应的构造函数


4. 构造函数可以重载


为什么构造函数这么特殊?别问,问就是他是祖师爷的亲儿子。


比如说我们写一个构造函数:

#include 
using namespace std;
class Stack {
public:
  //构造函数
  Stack(int capacity = 4) {
  _arr = (int*)malloc(sizeof(int) * capacity);
  if (malloc == nullptr) {
    perror("Stack::malloc::fail");
    return;
  }
  _capacity = capacity;
  _size = 0;
  }
  //不需要这个了
  //void Init() {
  //  //...
  //}
  void Push() {
  //...
  }
  void Destory() {
  //...
  }
private:
  int* _arr;
  int _size;
  int _capacity;
};
int main()
{
  Stack st;
  st.Push();
  return 0;
}

这个时候,我们不需要Init就可以直接使用这个对象而不会报错了,


因为在实例化类对象的时候就自动调用了构造函数。


4. 析构函数

析构函数与构造函数的功能正好相反,


其实就是我们前面讲的,完成Destroy的功能,


对象在销毁的时候会自动调用析构函数,完成对象中资源的清理工作。


析构函数的特征:


1. 析构函数名是在类名前加上字符 "~"


2. 无参数和返回值


3. 一个类只有一个析构,如果没有定义,系统会生成一个默认的析构函数(析构函数不能重载)


4. 对象生命周期结束时,编译器会自动调用析构函数


有了析构函数,Destroy自然也不需要了,


还是这段代码:

#include 
using namespace std;
class Stack {
public:
  //构造函数
  Stack(int capacity = 4) {
  _arr = (int*)malloc(sizeof(int) * capacity);
  if (malloc == nullptr) {
    perror("Stack::malloc::fail");
    return;
  }
  _capacity = capacity;
  _size = 0;
  }
  //不需要这个了
  //void Init() {
  //  //...
  //}
  void Push() {
  //...
  }
  //不需要这个了
  //void Destory() {
  //  //...
  //}
  //析构函数
  ~Stack() {
  free(_arr);
  _arr = nullptr;
  _size = 0;
  _capacity = 0;
  }
private:
  int* _arr;
  int _size;
  int _capacity;
};
int main()
{
  Stack st;
  st.Push();
  return 0;
}

有了构造和析构函数,以后就不用再用Init和Destroy了,解放双手。


5. 探索构造和析构函数的更多细节

来看这段代码:

class Date {
public:
  void Print()
  {
  cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
  }
private:
  int _year; 
  int _month; 
  int _day; 
};
int main()
{
  Date d1;
  d1.Print();
  return 0;
}

我们并没有自己实现构造函数,


那类里面有构造函数吗?


根据我们刚刚学习的构造函数的特性,编译器会给我们自动生成一份默认的构造函数,


那默认的构造函数有做什么事情吗?


来看这段代码的输出:


-858993460--858993460--858993460

是的,一堆随机值,


默认生成的构造函数看起来啥也没干。


实际上,编译器默认生成的构造函数,内置类型不做处理,


而自定义类型会去调用他们自己的默认构造函数。


(自定义类型:使用struct/class定义的类型)


这里补充一下:有些编译器可能会处理内置类型(C++没有规定,所以我们默认没有处理)


默认构造函数啥都不干好像不太好,所以C++就打了个补丁:


来看例子:

#include 
using namespace std;
class Date {
public:
  void Print()
  {
  cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
  }
private:
  int _year = 2023; 
  int _month = 6; 
  int _day = 26; 
};
int main()
{
  Date d1;
  d1.Print();
  return 0;
}

输出:


2023-6-26

可以在成员函数的声明那里给缺省值,


这个是C++11添加的新语法。


这里还有一种情况,


来看代码:

#include 
using namespace std;
class Date {
public:
  Date() {
  _year = 2023;
  _month = 1;
  _day = 1;
  }
  Date(int year = 2023, int month = 1, int day = 1) {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print()
  {
  cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
  }
private:
  int _year; 
  int _month; 
  int _day; 
};
int main()
{
  Date d1;
  d1.Print();
  return 0;
}

我们重载了构造函数,一个无参,一个全部参数带着缺省值,


这样的情况是报错的,为什么呢?


因为无参调用会出现歧义,两个构造函数都能进行无参调用,导致错误。


这里补充一个知识点,什么是默认构造函数?


不传参就调用的就是默认构造函数,无论是我们自己写的还是编译器自己生成的。


而默认构造函数只能有一个,如果我们写了,编译器就不会生成,


而上面那种情况就是右两个默认构造函数,这个规则的原理刚刚我们也分析了,


其实就是会出现歧义这个问题。


这里就再说一句:


析构函数跟构造函数差不多,如果没写析构函数,编译器会自动生成默认的析构函数,


(当然它也啥都不干)而在类内的定义的自定义类型会调用他们自己的析构函数。


写在最后:

以上就是本篇文章的内容了,感谢你的阅读。


如果感到有所收获的话可以给博主点一个赞哦。



相关文章
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
108 4
|
2月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
51 2
|
2月前
|
C语言
学习——理解指针(4)(指针学习最后一节)
学习——理解指针(4)(指针学习最后一节)
|
2月前
|
存储 C++
学习——理解指针(3)
学习——理解指针(3)
|
2月前
|
编译器
学习——理解指针(2)
学习——理解指针(2)
|
2月前
|
存储
学习——理解指针(1)
学习——理解指针(1)
|
4月前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
104 4
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
120 13
|
2月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
37 0
|
3月前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
141 4