三部曲深剖C++类与对象——中篇

目录

    • 传统艺能
    • this指针
    • this指针存放在哪
    • nullptr与类
  • 类的默认成员函数
    • 构造函数
    • 意义
    • 析构函数
    • 拷贝构造
    • 运算符重载

传统艺能

小编是双非本科大一菜鸟不赘述,欢迎大佬指点江山(QQ:1319365055)
此前博客点我!点我!请搜索博主 【知晓天空之蓝】

非科班转码社区诚邀您入驻
小伙伴们,打码路上一路向北,背后烟火,彼岸之前皆是疾苦
一个人的单打独斗不如一群人的砥砺前行
这是我和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我

倾力打造转码社区微信公众号
在这里插入图片描述


在这里插入图片描述

this指针

现在给出一段代码,实现一个普通的日期 date 的打印:

class date
{
public:
	void print()
	{
		cout << year<<' ' << month <<' '<< day << endl;
	}
	void init(int y, int m, int d)
	{
		year = y;
		month = m;
		day = d;
	}
private:
	int year;
	int month;
	int day;
};
int main()
{
	date d1;
	date d2;
	date d3;
	d1.init(2022, 5, 15);
	d2.init(2022, 5, 14);
	d3.init(2022, 5, 13);
	d1.print();
	d2.print();
	d3.print();

	return 0;
}

结果如你所想:
三部曲深剖C++类与对象——中篇_第1张图片
那么问题来了,d1,d2,d3在调用类里面的 print 函数时,并没有指明对象或者给出形参,最后结果却能打印出不同的三个结果,这是为什么呢?、

这就是C++语法中的隐藏的 this 指针这里的 this 指针其实就是隐含的形参,这个形参会对函数进行处理,比如刚刚的 print 函数以及他的调用,他们的真面目其实是这样:

void print(date* this)
{
cout<< this->year << ’ ’ << this->month << ’ ’ << this->day << endl;
}

d1->init(&d1,2022,5,15);

也就是说所有成员变量前面都会有一个 this 指针修饰(实际上 this 指针是个 const 类型,指针不能修改但指向内容可以修改),使得形参和实参之间架起一座无形的桥梁,进行连结。值得注意的是,这个过程是编译器在捣鼓实现的,不需要我们自己去搞,要是写的时候强行带上 this 指针反而还会报错
在这里插入图片描述

那数据是如何对应上的呢?换个问题就是 print 每次是怎么样对应上 d1,d2,d3的,其实访问成员变量年月日,并不是在访问 private 里的年月日,private 里只是声明并不存在空间的开辟,访问但是同一个类里面的 init 函数从而访问到成员变量。

this指针存放在哪

this 指针存在寄存器里面

其实编译器在生成程序时加入了获取对象首地址的相关代码。并把获取的首地址存放在了寄存器ECX中(VC++编译器是放在ECX中,其它编译器有可能不同)。也就是成员函数的其它参数正常都是存放在栈中,而this指针参数则是存放在寄存器中。

类的静态成员函数因为没有this指针这个参数,所以类的静态成员函数也就无法调用类的非静态成员变量。

三部曲深剖C++类与对象——中篇_第2张图片

nullptr与类

如果我定义了一个空值指针,让空值指针分别去访问我类里面的函数与成员变量会怎么样:

class Data
{
public:
	void print()
	{
		cout << "hello" << endl;
	}
	void printa()
	{
		cout << a << endl;
	}
private:
	int a;
};
int main()
{
	Data* n = nullptr;
	n->print();//访问函数
	n->printa();//访问成员变量
	return 0;
}

结果如图:
三部曲深剖C++类与对象——中篇_第3张图片
没错,程序崩溃辣!但是很明显,hello 打印出来了就说明函数的访问是没有问题的,但是是没有办法访问成员变量的。

我们说类里面用空指针访问函数,成员变量结果会不同,原因就是函数在公共代码区,不需要解引用,直接找到函数地址变成 call 地址即可,而成员变量的访问需要解引用自然空指针就会寄。

空指针 nullptr 其实并不是真的“空”,实际上是真实存在的,他指向虚拟进程空间里面地址为 0 的地方,这个 0 地址处是用来程序初始化的,是预留出来的,并不是用来存储数据的。因此空指针一旦指向数据,这个数据就是不被认可的,没有意义的。
三部曲深剖C++类与对象——中篇_第4张图片

类的默认成员函数

类里面什么都没有,就称它为空类,实际上空类中真的什么都没有吗?答案是 NO!任何一个类在默认情况下都会生成 6 个成员函数。

三部曲深剖C++类与对象——中篇_第5张图片

构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名叫构造,但他的主要任务并不是开辟空间或者创建对象,而是将对象初始化。我们不写,编译器也会生成一个默认无参数的构造函数,但这个默认的构造函数不一定有用,而 C++11打的补丁,针对编辑器自己生成的默认成员函数不初始化的问题,给了缺省值来供默认构造函数使用。

需要注意的是:

  1. 类名与函数名保持一致
  2. 可以不用传参,没有返回值
  3. 对象实例化时编辑器自动调用对应的构造函数
  4. 构造函数支持函数重载
  5. 如果类中没有显式定义构造函数,C++编译器会自动生成一个无参的默认构造函数,如果我们自己显式定义了就不会给出了
  6. 无参的构造函数和全缺省的构造函数都被称为默认构造函数,并且默认构造函数只能有一个

意义

C++将变量分为两种:内置类型(int,char,指针类型等等)自定义类型(struct/class 去定义的类型对象),默认生成的构造函数对于内置类型成员变量不做处理,对自定义类型才进行处理。而这正好就是 C++ 语法设计的一个败笔,他会导致

如果有内置类型的成员就得自己写构造函数,比如:

class Stack
{
public:
     void push(int x){
     }
     void pop(){
     }
private:
     Stack stackpush;
     Stack stackpop;
}

该类里面只有自定义类型成员变量,就不需要去写构造函数,默认的构造函数就可以完成了。总结一下就是如果类里面只有自义定类型,就可以用默认构造函数,如果存在内置类型或者需要显示传参初始化就需要自己写构造函数。

析构函数

如果构造函数高速了我们对象是怎么来的,那么析构函数就是在告诉我们对象是怎么走的。析构函数与构造函数功能相反,他并不是完成了对象的销毁,因为局部对象销毁是由编译器来完成,而析构函数是完成对象的一些资源清理工作,他是在对象销毁时自动调用
三部曲深剖C++类与对象——中篇_第6张图片

再三强调不是销毁对象本身!不是销毁对象本身!所谓的资源清理针对的对象是 malloc,new 或者 fopen 这类的操作进行清理收尾,其实本质上就相当于我们之前的 destroy 函数。

其特征如下:

  1. 析构函数名是在类名前面加上字符 -
  2. 无参数也无返回值
  3. 一个类有且仅有一个析构函数,如果没有显式定义,系统会自动生成析构函数
  4. 对象声明周期结束时,C++编译系统自动调用析构函数

但是注意一个顺序问题

int main()
{
Stack st1;
Stack st2;

return 0;
}

st1 相比 st2 先构造这没什么问题,但 st2 却比 st1 先析构,析构和构造的顺序是相反的。但他和构造函数一样,对内置类型不处理但是对于自定义类型会去调用。

拷贝构造

有没有可能你会想搞一个和自己一样的对象出来呢?如果想那就该我拷贝构造登场辣!

int main()
{
     Date d1(2022,5,16);
     Date d2(d1);

     return 0;
}

这里的 d2 就是拿 d1 来初始化,所以拷贝构造只有单个形参,该形参是对类类型对象的引用,一般由 const 修饰,再用已存在的类类型对象创建新对象时由编译器自动调用。他实际上是构造函数的一种函数重载,他的参数只要一个,而且必须使用引用传参,因为使用传值会引发无穷递归调用。

在什么情况下系统会调用拷贝构造函数:

(1)用类的一个对象去初始化另一个对象时

(2)当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用

(3)当函数的返回值是类的对象或引用时

C++规定了自义定类型对象,拷贝初始化要调用拷贝构造完成。这就引出来一个问题,比如我们拷贝栈,定义了两个对象 st1 和 st2,假如我写成下面的样子就会出乱子:

Stack st1(1);
Stack st2(st1);

因为是拷贝构造, st2 指向地址和 st1 是一样的,这并不是我们想要的结果,我们传统拷贝是指向同一块空间,而且这里拷贝构造会崩,原因很简单,这里原理上进行的是浅拷贝,默认构造函数会对类里面内置类型进行浅拷贝,值拷贝,对于自义定类型,编译器不知道自义定类型的行为,如何拷贝,什么规则,像 stack 这种类型就需要深拷贝实现,我们后面再去学习。

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

st1 构建完成变成 st2 参数去初始化 st2,st2 一旦拷贝完成就要进行析构,而析构会先执行 st2 ,st2 一旦被清理这块空间就会被销毁,而此时 st1 还在使用这块空间,就会导致内存错误。

三部曲深剖C++类与对象——中篇_第7张图片

运算符重载

说到运算符重载,这里面有几分门道嗷。他是个啥呢?

您可以重定义或重载大部分 C++ 内置的运算符,这样就能使用自定义类型的运算符。重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

说的比较玄学,其实你设想一下这个场景,假如我们给出的日期类有两个成员 d1,d2,我们如果去比较他们能用运算符 ==,<,> 来比较吗,或者我给日期 +、- 一个数可以吗,很明显不行,因为内置类型可以直接使用运算符,但是自义定类型不可以,因此就引入了我们的运算符重载。

运算符重载——函数,函数名:operator +运算符,参数是运算符的操作数,如下:

operator==(Date d1,Date d2)
{
      return d1.year == d2.year
      && d1.month == d2.month
      && d1.day == d2.day
}

细心的你可能会问,内置类型我定义在私有域里面该怎么访问呢?这就有三种方法:

  1. 从根源解决,改成公有类型public(尬活了属于是)
  2. 再写一个公有域的函数来调用私有域的内置类型成员;
  3. C++ 友元(后续会学习到)

先别觉得ez,因为这里他会报错
在这里插入图片描述

==是要求的两个参数,我这里也是两个参数没错啊,为什么会参数太多啊?别忘了,操作符还有 默认的形参 this,他被限定为第一个形参,因此我们只需要写一个参数即可:

operator==(Date d)
{
      return year == d.year
      && month == d.month
      && day == d.day
}

编译器遇到 if(d1 == d2) 这样的语句,就会去处理成对应的重载运算符调用 if(d1.operator(d2)),这里编译器时很聪明的,你写的全局他会去调用全局,你写的成员他会去调用成员。而且遇到运算符重载他会在优先去类里面找,没有才回去类外面找,也就是说类的里外同时存在运算符重载函数是可以编译过去的。

注意一下

  1. ::
  2. sizeof
  3. ?:
  4. .
  5. .*

这五个操作符是不能进行重载的,在选择题中会经常出现。

今天就先到这里吧,润了家人们。

你可能感兴趣的:(C++,c++,类与对象)