C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++类与对象

C++深入刨析类与对象的使用

作者:乔乔家的龙龙

类和对象是两种以计算机为载体的计算机语言的合称。对象是对客观事物的抽象,类是对对象的抽象。类是一种抽象的数据类型;变量就是可以变化的量,存储在内存中—个可以拥有在某个范围内的可变存储区域

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;
}

结果如你所想:

那么问题来了,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指针这个参数,所以类的静态成员函数也就无法调用类的非静态成员变量。

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;
}

结果如图:

没错,程序崩溃辣!但是很明显,hello 打印出来了就说明函数的访问是没有问题的,但是是没有办法访问成员变量的。

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

空指针 nullptr 其实并不是真的“空”,实际上是真实存在的,他指向虚拟进程空间里面地址为 0 的地方,这个 0 地址处是用来程序初始化的,是预留出来的,并不是用来存储数据的。因此空指针一旦指向数据,这个数据就是不被认可的,没有意义的。

类的默认成员函数

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

构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名叫构造,但他的主要任务并不是开辟空间或者创建对象,而是将对象初始化。我们不写,编译器也会生成一个默认无参数的构造函数,但这个默认的构造函数不一定有用,而 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;
}

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

析构函数

如果构造函数高速了我们对象是怎么来的,那么析构函数就是在告诉我们对象是怎么走的。析构函数与构造函数功能相反,他并不是完成了对象的销毁,因为局部对象销毁是由编译器来完成,而析构函数是完成对象的一些资源清理工作,他是在对象销毁时自动调用。

再三强调不是销毁对象本身!不是销毁对象本身!所谓的资源清理针对的对象是 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++ 内置的运算符,这样就能使用自定义类型的运算符。重载的运算符是带有特殊名称的函数,函数名是由关键字 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)),这里编译器时很聪明的,你写的全局他会去调用全局,你写的成员他会去调用成员。而且遇到运算符重载他会在优先去类里面找,没有才回去类外面找,也就是说类的里外同时存在运算符重载函数是可以编译过去的。

注意一下

::

sizeof

?:

.

.*

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

到此这篇关于C++深入刨析类与对象的使用的文章就介绍到这了,更多相关C++类与对象内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文