目录
1.引出
C与C++的区别在于,C是面向过程的,C++是面向对象的。面向过程大概就是当我们遇到问题需要处理,优先想到的是需要哪些变量变成抽象结构,然后一次应对结构来实现代码解决问题,好比我如果想要造一个火箭,我需要考虑螺丝怎么造,螺丝跟其他构建连接怎么实现等等,这种思考角度可能过于全面了,一整个全部考虑难免会乱;而C++面向对象的意思其实就像分包,造火箭,分开考虑,螺丝就流水线造,什么发射器设计由专门的技术团队造。会发现C++的面向对象通过对对象的分离,以对象为基本单位实现对象所能实现的一部分问题。之所以C++能够实现面向对象的设计是因为它的一些内部实现。而这里要讲的是类和对象。
2.C++的升级,类的定义
1.对原来的struct进行升级
C++保有原来struct的语言逻辑,在此基础上加上了可以把函数一起分装到结构体中变成类。
左图:是C语言会写出的代码,结构体只能定义变量,函数的实现跟结构体是分开的,要实现函数需要在构建的结构体中实现,也就是说,实现函数还得把结构体的参数给带上。
右图:是C++中结构体的升级,即为类。它在结构体中可以把函数也写入其中。在类中,类的属性叫做成员变量,里面的函数称为成员方法。而这个类使用起来也方便不少,调用函数也不用单独调用了,直接在结构体中使用便可。
需要注意的是,C语言如果对结构体进行重命名,结构体内的名字依然不可以直接用,因为重命名生效在重命名之后;但是C++可以直接使用。
2.类的定义
struct升级后还不满意,需要再做一个新的附和类的形式,这样可以彻底和struct分离开来。其实很struct没什么实现上区别,只是在变量的访问限定默认设置有区别。
class classname
{
//类的变量和函数
};
3.类的访问限定符
类中的成员变量在类的外面是否能被访问修改是访问限定符的功能。仔细想想,如果一个类是需要调用Push函数推入才能加数据,但是如果我对类的变量不加以限制,任凭外面对类中的变量进行直接访问操作,这样岂不是使得这个类内容被改的面目前方,虽然这不是类的责任了,但是设计者在设计角度就杜绝这种行为而提出使用访问限制符。
种类:1.public(公有)2.protected(保护)3.private(私有)
1.public类的外面可以直接访问
2.protected和private类似
3.访问限定符的范围为定义开始到下一个定义结束,或者直到类定义结束
4.类中对类的变量是可以直接访问的,但是类外需要根据访问限定符来决定
5.class默认变量的访问限定符是private;struct默认变量的访问限定符是public
4.类的声明和定义
需要知道的是,类里面的范围被称为类域,类域就跟传统的域一样功能的实现。
1.类的定义中成员变量的本质
类中的成员变量属于一种声明,即不是实际存在的实体变量,它是一种标记,让我们知道原来这个类中的变量是这样的。所以不能直接将类的成员变量赋值,它根本没有空间开辟让我们存。
错误写法:
该代码有两个错误;1.类确认了类域,但是类的成员变量是一种声明。我们想要用域的方法访问本身就是错误,因为它没有开辟空间只是一种声明,2.就算类域错误撇开不管,该数组权限是私有的,类的外面不能直接访问。
2.类的声明与定义分离
开发软件或者办理业务,声明及其重要,它不仅是为了防止别人抄袭,还要在我们自己重新读取代码时清楚这个类的成员函数实现到底是干什么的,所以函数的声明是必要的。
头文件定义声明不需要把成员函数写出来,在源文件中实现成员函数的定义,需要注意的是:类是一种域,需要域作用限定符在源文件中实现连接起头文件中的声明。(函数如果有重载,那么也要在声明里实现,定义中不写。)如果成员函数写在声明里,那么函数默认是内联函数。
3.声明成员变量定义
一般地,成员变量只要名字在域中不重复定义就可以了。但是考虑到实现程序能让人读懂,所以需要对成员变量跟普通变量做区分,一半会在成员变量中加一些加以区分的符号以达到定义命名分离的作用。
3.面向对象的三大特称之一封装
三大特征:封装、继承、多态
封装:C语言中的结构体的数据跟方法是分离开来的;C++将数据跟方法封装到类中。这样做其实避免了我们直接访问结构中的数据,所以想要增删查改数据就需要调用函数来操作数据
4.类的大小
类的大小计算和C语言的结构体大小计算一致。并没有把函数也算到大小中去。所以计算大小不需要看成员函数。为什么这样设计,成员变量算入大小理所当然,成员函数专门存储在代码段(常量区)中,因为编译器会自己去找对应函数,所以没有必要把他算入内存大小来浪费空间。
如果没有类中没有成员,那么这个类的大小是1beyt,原因是需要有内存去占空间。
5.this指针
1.问题引入
抛开private把成员变量变成私有的问题,假设成员变量是公开的。右边的类域访问是否合理呢?答案是都不合理。对于调用成员变量来说,其实成员变量只是声明,没有开辟空间使得变量被赋值,所以这里是有问题的。其二对于函数调用来说,这里就算this指针的问题。其实本质是参数没有传其导致无法调用。下面我们进一步解释this指针。
2.this指针
函数调用默认会传入调用该函数的结构体变量的指针,所以我们还能解决前面的问题,为什么不能直接类域访问,因为没有对应的变量指针。当然this指针是默认的,人为不能定义,下面的代码虽然是本质,但是在实际中是不能实现的。即this指针的定义和传递不能人为用,但是我们可以在类里面用this。
下面这样的this指针调用是被允许的,但是简单的代码一般不用。
void Print()
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
那么this指针存在哪里呢?this指针是形参,存在栈帧里。特别的,vs中this存在ecx中。
6.类中的默认成员函数
六大默认函数:构造函数、析构函数、拷贝构造、赋值重载、普通对象取地址、const对象取地址
系统是这样的:如果函数中没有写默认函数,那生成结构体自动调用默认函数时,是根据系统自动生成的函数进行;如果我们写了默认函数的使用规则,那么自动调用默认函数,系统不会默认函数,直接调用我们写好的函数运行。
1.初始化和清理
在使用结构体时,需要对结构体可能要进行初始化和销毁。不初始化可能会造成运行崩溃,不销毁会造成内存泄漏。没有初始化可能会报错,但是内存泄漏就没有直观的提示,所以忘记以后很难被发现。由此引出两个默认函数。
1)构造函数
功能为:初始化,意思为在类外已经构造好一个结构体,而这个函数的功能就是对已经有的结构体进行初始化。
特征:
1.函数名跟类名相同 2.没有返回值 3.结构体生成后自动调用 4.可支持重载
当然也不是必须传入参数才能算初始化的嘛,我们只想定义一个d3使得它Date d3就行,那么好。类中的成员函数是支持重载的。也就是说,编译器支持多种类的初始化。但是不能出现Date d3()这样的操作。
优化:加入缺省,可以省略一些不必要的重载。
默认构造函数,无参和全缺省的构造函数都称为默认构造函数,但是他们不能同时出现。当然系统自动生成的构造函数也是一种默认构造。
2)析构函数
功能:不是对对象本身的销毁,而是对对象开辟的资源进行释放和清理。
特征:
1.类名前加“~”就是析构函数 2.无参数无返回值,所以不能重载 3.一个类只有一个析构函数 4.类的生命周期结束,系统自动调用析构函数
以下的析构过程,类的生命周期结束时,即return 0之前,系统自己调用析构函数使得内存释放。
析构的顺序是根据栈的特征所决定的,即先进后出,所以最后构造的先析构。
3)默认生成的构成函数和析构函数
自动生成的构造函数
系统会自动区分类中的成员变量的类型,分为两类:内置类型和自定义类型。内置类型就是语言自己提供的类型,自定义的类型是我们写的类型。对于内置类型,系统生成的构造函数仅仅会给内置类型进行初始化随机赋值;只有自定义类型才会在构造函数中调用自定义类型规定的默认初始函数。也就是说,其实不管是内置的类型还是自定义的类型,都是由内置类型组合而来的,那么唯一区别就是,当构造函数中是内置类型,初始化基本上等于无效(因为其随机赋值的特性),而自定义类型也是我们在自定义的结构体中对它的内置类型进行了准确的赋值。析构函数同理,不敢对内置的成员变量释放掉,但是写了自定义的析构函数会自动默认使用原来写的析构函数。
可是,如果我定义了一个类包括自定义和内置类型,这时出现了这样的现象:自定义的成员变量被定义了,但是内置类型的成员变量还是没有被定义的。所以这时,需要重新解决该问题,解决方法有以下两种:1.在声明时对成员变量进行缺省操作 2.在构造函数和域中间的部分是初始化列表,该功能能够实现(下面再介绍)
缺省加的操作,当然构造函数中没有对该缺省成员变量进行初始化,那么就按照缺省的来;如果有对该缺省成员变量进行初始化,那么初始化的结果根据构造函数来,此时缺省失效。优先级:自己定义的构造函数>缺省>系统生成的默认构造
2.类的拷贝,默认函数的卡卡西
1.拷贝构造
功能实现:构造一个结构体,并且把另一个结构体数据拷贝给该结构体
特征:
1.拷贝构造也是构造,是构造函数的重载
2.拷贝构造有且只有一个参数,即为该类类型的引用。不用引用会出现循环定义
为什么必须传引用而不是类的类型?那么我们假设传入的是类的类型,下面是产生错误的原因解释步骤 1.首先我们要清楚传入的类不是原来的类,是原来类的一份临时拷贝。2.它是拷贝,那就触发了我们对于拷贝构造的定义啊,即拷贝一个类到另外一个类 3.我要写拷贝函数来拷贝这个类需要调用通过拷贝构造得到的临时参数。这样不就是循环定义吗,我解释我自己。换个更加直观的说法:我需要钥匙打开我家的门以达到拿回被我落在家里的钥匙,可是我没有钥匙,所以我要去拿钥匙,可是我钥匙在门里,我需要用钥匙打开门。。。不断循环。
正确的演示
不加const可能我们会把拷贝和被拷贝关系搞反,使得没有达到拷贝任务并且丢失被拷贝的数据。
自动生成的拷贝构造
默认的拷贝构造生成规则是将原来的类一个单位一个单位的拷贝到构造的类中。它对于自定义类型和内置类型都进行拷贝,对自定义类型会调用它的拷贝构造。那么这里存在一个问题:默认的拷贝构造真的够用吗?答案是否定的。有些拷贝只需要数据进去即可,那么默认的拷贝刚好够用;但是有些拷贝仅仅是拷贝了其地址,对于指针指向的内容没有开辟空间,使得拷贝后同时存在多个指针指向同一个空间,这种潜拷贝无法满足要求,需要的是深拷贝,那么这些要自己写;当然有些有些类要深拷贝,但是自定义类型已经实现深拷贝了,所以又可以不需要了。
2.运算操作符重载的引入,复制构造的前调
运输符重载:对运算符号的重新定义使其支持新的运算。
举个例子:减法是用来对数进行运算的,那么我们在生活里常常要计算下一次的考试到现在还有几天,那么减法这个运算规则在现实生活里是运行的,但是计算机不知道这个减对于日期的操作,所以此时我们需要重新对减法进行重载使得其可以对自定义类型对象用减法。
特征:
1.写重载格式为operator运算符(参数)
2.传入的参数必须有一个类的类型,不能对内置类型进行运算重载
3. " .* "," sizeof "," ?: "," :: "," . "不能重载
放在类中时,需要注意有隐藏的this指针,所以注意传入的参数是否匹配。
3.赋值重载
功能:使得已经存在的类得到拷贝类的数据
特征:本质也是一种运算重载。即operator=(参数)
演示:
不过还有一些要优化的地方,首先,拷贝传入的参数进行作为临时变量调用拷贝构造需要时间,所以我们可以传入参数的别名使得其省去拷贝的时间。此外,赋值在内置类型中可以进行链式赋值即a=b=c;但是上面的代码不能实现,原因是没有参数传回去;那么因为需要被拷贝的类已经开辟空间了,所以传回别名即可,当然指针也可以但是赋值时把地址传入进行赋值的操作过于生涩,故不考虑。以及自己给自己赋值,可以直接退出函数。
4.自动生成的默认拷贝
拷贝构造和赋值拷贝自动生成的函数对于自定义类型和内置类型都进行拷贝,原理是把全部内存按单位bit进行复制。但是这种形式有些时候是不能完全实现我们对于类的需求的,运用默认不当可能会造成运行崩溃。