| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> C++类和对象 -> 正文阅读 |
|
[C++知识库]C++类和对象 |
这篇博客是对C++中类和对象的详细讲解(写了半个多月的时间,博主已经被榨干),类和对象对于C++是重中之重,学好了类和对象,C++也就有了好的基础,但类和对象的细节实在是太多,这篇博客用将近两万字,只为了清楚的表述其中的细节点,虽然内容很多,但全是我认真总结,希望能对你有所帮助 前言在C中变量类型分为内置类型和自定义类型,在自定义类型中有一个关键词struct结构体,结构体中能定义不同类型的变量(无论是自定义类型还是内置类型)。而C++是兼容struct的,但C++对struct进行了改进,struct在C++中不仅能定义变量,还能定义函数。 不过C++很少使用struct类型,而是用class类代替struct结构体(它们之间的区别下面会说到) class定义的类能直接用类名做类型名,不用像struct定义的结构体那样,定义结构体时还要加上struct
类的定义和struct一样,类只是将struct改成了class
ClassName是自定义的类名,class是类的关键字。类的定义有两种方式,一种是在类里面实现类的函数(这样的函数会被编译器当成内联函数),另外一种就是只在类中声明类的函数,其定义在类外实现。 而我更倾向于将类的函数实现在类外(当函数数量很多且函数代码量大时),这样使代码看起来更简洁,但当类的函数不多,且函数短小时就将函数的定义实现在类里面。 类的访问限定符和封装类的访问限定符限定的是用户访问类中成员的权限。访问限定符有三个:public(共有),private(私有),protected(保护)。public修饰的成员在类外就能访问,private和protect修饰的成员不能在类外被访问。使用方式:在访问限定符后加上冒号: 接着换行写类的成员。举个例子
(将成员变量定义在任意位置都行,不一定要定义在成员函数的上面。那有个问题,当成员函数访问类的成员变量时,不会出现找不到成员变量的情况吗?因为类在C++中是一个整体,一个新的作用域,当成员函数要访问成员变量时,函数会在整个类中找这个成员变量是否存在,而不是像普通代码一样,只会向上找) 先明白访问限定符的作用范围:从访问限定符开始到下一个访问限定符结束(上面代码public的作用范围:从public开始到private结束),或者从访问限定符开始到类的结束(上面代码中private作用范围:从private开始到类的最后)。在限定符作用范围内定义的成员都是该限定符修饰的。 上面代码定义了一个Person类,类的成员函数有初始化Init,还有打印Print,还有成员对象_name和_age,由于Init和Print是public修饰的,所以在类外(main函数中)也能使用这两个函数。而_name和_age是用private修饰的,在类外则不能访问 而C++的类中没有写访问限定符,默认成员是私有的。C的结构体则默认成员是公有的。 封装C++三大特性:封装,继承,多态;这里介绍特性之一的多态:
刚刚说到,用private修饰的成员可以被很好的保护起来,这种保护在C++中叫做封装,而将用public修饰的几个函数提供给外界使用,这些函数接口保证了类中成员能被合理的使用,访问。 总结:封装本质上是一种管理,封装过程,隐藏细节,这样的管理极大的提高了代码的安全性并且实现了代码的低耦合。 类的作用域当定义一个类时,一个新的作用域也随之被定义,也就是说,两个不同的类可以有相同的成员名,因为类的作用域不同,变量作用域也不同。 而一个函数在类中声明,想在类外定义时,就必须加上类名和作用域解析符:: 正确的写法:在函数名前加上类名和域作用限定符::(别加在函数返回类型前面,我第一次就写错了)。这里解释下为什么这个函数能访问类的私有成员,不是说私有成员不能在类外访问吗?道理很简单,加上了类名,这个函数也就是这个类中的函数,访问限定符限制类外的访问,函数在类中(不是类外),不会受到限制,当然能访问私有成员。 类的实例化类的储存空间大小先抛出一个问题,当一个类中只有成员函数时,sizeof求出的大小是多少?空类的大小又是多少?
类的实例化:类的声明就像一个模板,限定了类的成员,而一个类被声明时,系统是不会分配内存空间给声明的,只有类被定义时,才会分配内存空间储存类的信息。这个分配内存空间的过程叫做类的实例化。 例如下面的Person p就是类的实例化,分配了内存空间 再做一个实验,将Test1类加上一个int变量 总结一下:由于同一类的实例化对象的成员函数总是一样的,当实例化对象时,保存相同的代码显然是多余的。所以为了不占用更多的储存空间,这些成员函数被储存到一个公共代码区,当要调用这些函数时才去这块代码区去找。而同一类的实例化对象的成员变量总是会有不一样的数据出现,这时就要对这些数据进行存储。所以计算一个类的大小时,只用计算类中成员变量的大小,这个计算规则遵守内存对齐,关于内存对齐可以看我之前写的结构体总结,里面有详细的内存对齐规则说明以及练习题。 this指针前面提到类的成员函数会放到一个公共代码区,但有一个问题
类的成员函数的形参中其实还隐藏了一个this指针,这个指针只有在编译后才会被加上(这个是编译器要做的事),传参时给了实体的地址,函数就能通过地址该实体进行修改了。 (函数中出现的参数会优先去形参中找,如果形参中没有这个参数,编译器就会去成员变量中找,找到了编译器会为其加上this->,找不到编译器报错。) this指针不能在形参中显式的写出,但this指针能被函数使用,看一个例子
p->Init()这行代码是对的吗?p是一个空指针,而对空指针解引用,很明显是错的。但解引用的前提是:需要解引用访问某个对象,而p中有存储Init函数吗?p中只存储成员变量,不存储类的函数。所以这行代码实际上没有解引用。既然没有对空指针解引用,这行代码就能正常运行。而p->_age,_age是类的成员变量,存储在p中,所以这时需要通过解引用来访问p的元素,而p是空指针,对空指针解引用会出现非法访问,导致程序崩溃。 this指针特性
空指针访问类的成员
如果p是一个Date类的空指针,可以通过p访问类的Print函数吗? 之前说过,类的成员函数不属于任何一个对象,成员函数放在一个公共代码区,通过p调用Print,本质是将p指向的地址传给函数,函数用this指针接收,但是Print函数没有解引用这个地址(没有去访问),程序就不会报错。 类的6个默认成员函数刚刚说一个空类的大小为1字节,但空类没有成员函数,也没有成员变量。虽然表面上没有成员函数,但编译器会默认生成6个成员函数,这里介绍一些重要的成员函数 构造函数也许是翻译问题,构造函数不是字面意思,去构造或是创建一个对象,其准确的名称应该是“初始化”函数,构造函数的主要功能是将类的成员变量赋初值。 构造函数特性1.函数名与类名相同假设有一个日期类,名字是Date,则该类的构造函数的函数名也是Date 2.构造函数没有返回值构造函数是进行初始化的函数,一个进行初始化的函数当然不需要返回值了。而普通函数可能有返回值可能没有,所以普通函数中才用上了void返回类型来表示该函数没有返回值。而构造函数是一个特殊的函数,它的功能决定了它一定没有返回值,既然一定没有返回值,C++就做的干脆了些,连void都不用写,这样的函数看着也更加简洁,同时也能一眼看出这是个构造函数。 3.构造函数可以重载先写一个日期类,成员变量有年,月,日。手动给该类写构造函数,一个构造函数无参,什么都不干,一个有参,接收输入的年月日,并将其赋值给成员变量。这两个构造函数构成函数重载。
调用构造函数后d1,d2里的数据。由于无参构造函数什么都不干,所以d1中的值都是随机值。 而main函数中的Data d1怎么就调用无参构造函数了?这行代码连函数调用符()都没有,看起来就是定义了一个日期对象d1啊。其实C++语法规定,这行代码就该怎样写,调用无参构造函数初始化对象时不用加上(),如果加上,则代码变成Date d1(),这行代码是调用函数吗?很显然这是一行声明函数的代码,d1是函数名,该函数无参,返回值是Date日期类。所以加上函数调用符不是在调用函数而是在声明函数,无法达到我们的目的,因此要调用无参构造函数千万不能加()。 4.对象实例化时,编译器自动调用构造函数,并且在该对象的生命周期内,构造函数只调用一次通过他调试观察对象在初始化时会自动调用构造函数 5.如果类中没有显式的构造函数,编译器会自动生成一个默认构造函数,该函数无参当屏蔽显示构造函数,再去实例化一个日期对象时,编译器就会生成一个默认构造函数,并且去调用该函数,但通过观察d1的值发现,生成的默认构造函数并不会修改对象的值(里面的值还是随机值)。 先看结论:对于内置类型,编译器生成的默认构造函数不会对数据进行处理;对于自定义类型,默认构造函数会去调用自定义类型中的内置类型的默认构造函数(这里注意区分默认构造函数和构造函数,具体看第6点)。 例如现在有一个Queue类,其成员变量有一个属于Stack类的st1和st2
此时创建一个Queue类的变量q,由于没有显式定义的构造函数,所以编译器会自动生成默认构造函数,又因为Queue中有两个Stack类,属于自定义类型,所以默认构造函数会去调用Stacke的默认构造函数。要是Stack的构造函数也是编译器默认生成的就相当于q这个变量里存储的还是随机值。 结论:当一个类中有内置类型时,最好显式地写上构造函数(只要有就写),而当一个类中只有自定义类型时(这里是只有),可以不用写这个类的构造函数,编译器会自动调用默认构造函数,这个默认构造函数会去调用你为内置类型显式定义的构造函数。 Stack中有三个私有成员,如果Queue的构造函数由我们显式定义,赋值Stack的私有成员时很显然程序会报错。所以对于有自定义类型的类,有时是不能初始化自定义类型的。 6. 默认构造函数有三种:我们不写,编译器默认生成的;我们写的无参数的;我们写的全缺省的简单说默认构造函数的特点就是:不用传参数就可以调用的 但默认构造函数只能存在一个。这就有一个问题,当我们写了无参的构造函数和全缺省的构造函数,在最后程序运行时,程序是要调用无参的还是全缺省的?
为了防止歧义的出现,定义构造函数时尽量不要定义一个无参构造和一个全缺省构造。推荐只定义一个全缺省的函数。 7.C++11的补丁最后C++11为构造函数新增了一条语法,比如Queue这样的类中除了自定义类型还有内置类型,通常是让编译器自己生成默认构造函数去调用Stack的默认构造函数,这样虽然对st1和st2进行了初始化,_size的值却是随机值,C++11可以这样用 析构函数析构函数不是完成对象的销毁,与构造函数相反,析构函数完成对象的清理工作:释放申请的资源。 比如一个日期类是否要手动写一个析构函数?日期类的成员变量都是局部变量,在函数调用完后自动销毁,没有写析构的必要。如果是链表类或者栈类,有动态开辟内存空间的类呢,就需要进行资源的清理。 析构函数的特性
这段代码中,程序会先调用st1的构造函数再去调用st2的构造函数,当两者的生命周期快结束时,那么谁的析构函数先调用呢?由于st1和st2是存储在栈中的,栈满足先进后出的特点,所以st2的析构先调用,st1的后调用。 而析构函数要做的事就是释放内存空间,像上面的Stack一样,构造函数为_data申请了堆空间,而析构函数要做的就是释放申请的堆空间。 还有一点,和构造函数一样,若类中没有析构函数的显式定义,则编译器会生成一个默认析构函数并且调用它。 对于内置类型,析构函数并不会释放申请的内存;对于自定义类型,不写析构函数,编译器生成的默认析构函数会去调用该类型的析构函数(这里又和构造函数不一样,构造函数对于自定义类型生成的默认构造函数会去调用它的默认构造函数,注意构造是默认构造调用默认构造,而析构是默认析构调用析构) 假设类中有一个指针,该指针可能是new的,也可能是malloc,还可能是一个文件指针,编译器若处理内置类型,释放指针指向的空间。首先new和malloc得到的指针要用delect和free释放,编译器要怎么知道指针该用什么释放。其次编译器不知道 学习这几个函数时只要把握两个方面:
拷贝构造函数拷贝构造作为构造函数的一种重载形式,本质上是一种构造函数。定义语法和构造函数一样。 要怎么使用:比如定义了一个Date d1,想用d1来初始化d2,首先是要定义d2,这样写:Date d2,然后加上括号表示调用构造函数,括号里是要被拷贝的对象,Date d2(d1),可以看下面的main函数
这里要注意:拷贝构造函数的参数有且只有一个,并且该参数必须是对本类类型变量的引用,否则会出现无穷递归调用。 像上面那样写拷贝构造就是错的。解释:先理解一个点,当函数形参是自定义类型时,给函数传参,形参会去复制实参的值进行拷贝,但形参如果是自定义类型呢?语法规定这个拷贝工作要交给自己做。所以当函数的形参是自定义类型时,形参为了拷贝实参会去调用该实参类型的拷贝构造函数。 上面的代码中 Date(Date d)是一个拷贝构造函数,然而该函数的形参是一个Date类对象,要生成形参d就要对实参进行拷贝,拷贝就会调用Date类的拷贝构造函数,但是Date类的拷贝构造函数却是它自己,自己去调用自己,但是没有停下来的条件,会导致程序不断的调用拷贝构造函数,陷入死循环。 因此拷贝构造函数的形参必须是该类型的引用 正确的写法:Date(Date& d),形参作为实参引用时,不会拷贝实参,就不会出现无线递归的问题 浅拷贝导致的问题
上面的拷贝构造函数只是简单的将st1的数据赋值到了st2中,但Stack类中有一个_data指针,指向在堆中开辟的空间。所以这样拷贝就会有一个问题,st1的_data值和st1相同,即两者指向同一块内存空间。很明显,两者共用一块内存空间会导致许多的问题,如数据的存储会相互影响,但更严重的问题是:st1和st2在销毁前调用析构函数会使_data释放两次,第二次释放,即释放一个已经被释放的指针,导致程序崩溃。 还有一点:对于内置类型,不写拷贝构造编译器生成的默认拷贝构造是浅拷贝,对于自定义类型,不写拷贝构造编译器生成的默认拷贝构造函数会自动调用自定义类型的拷贝构造(不是默认的拷贝构造)。 综上总结:要直接管理类中的资源时,需要自己写拷贝构造函数,间接管理类中的资源时不需要写拷贝构造,使用编译器生成的浅拷贝就够用了。
就像上面的例子,Queue是自定义类型,里面有两个Stack类的自定义成员,Stack会申请堆中的资源,属于直接管理资源,拷贝构造要自己实现(深拷贝)。而Queue属于间接管理资源,编译器生成的默认拷贝构造函数会去调用Stack的拷贝构造函数,Stack的拷贝构造已经实现好了,所以不用自己写Queue的拷贝构造。 (拷贝构造函数更规范的写法:Stack(const Stack& st),因为拷贝构造函数不修改被拷贝对象的数据,所以用const修饰引用的对象,表示其不能被修改) 不那么重要的默认成员函数还有个默认函数,这里只需要稍微提一下,就是取地址和取const地址的函数重载
这两个&重载基本不需要自己写,大多数情况下是用编译器默认生成的函数。 运算符重载对于一个日期类对象d,能不能实现d + 100这种操作呢?由于编译器只支持操作数为内置类型的运算,d为自定义类型,所以d + 100是不能被编译器识别的。要实现这样的操作,就需要重新定义+操作符,用一个函数定义这个操作符的行为,这个函数叫做运算符重载。 和函数重载不同,函数重载指的是参数不同,函数名相同的几个函数构成重载,运算符重载是重新定义这个运算符的意义与功能,这里要注意别混淆。 使用:返回值operator运算符(参数列表),比如我想知道两个日期是否相等,这时就需要重载==这个运算符。
但operator==(d1, d2)这行代码比起d1 == d2,像是一个函数调用,难以体现这是两个操作数作比较,所以不推荐这种写法,用d1 == d2不仅简洁还提高了程序的可读性。 但有一个问题,可以看到我屏蔽了Date类中的private,将成员变量设为公有,但这也违法了C++的封装特性,使外界访问成员变量。如果成员变量是私有的,运算符重载函数却在类外定义,这时就不能访问类的私有成员,运算符无法重载。所以更适合的写法是将运算符重载写在类里面。 但类里面的每个函数都隐藏了一个形参:this指针。比如==运算符有两个操作数,但由于this指针的存在重载函数的形参只要有一个,另一个为隐藏的this。 (由于this存在,==的操作数应该为1个,2个操作数程序会报错)
运算符重载函数的特性
赋值运算符重载
上面的赋值运算符重载函数:首先是将形参d的数据赋值给this指针指向对象,这没什么好说的。但平常的赋值运算符支持连等,即i = j = k,并且赋值运算符的优先级是从右向左,先将k赋值给j,再把j的值赋值给i。所以赋值函数返回值是左操作数的值。而函数中只有左操作数的地址,this指针,所以返回值是对this的解引用,但返回值是Date的话在返回时会去调用一次构造拷贝函数,显然是麻烦的,所以返回值应该是Date的引用。 关于更多的运算符重载可以看我写的日期类的实现,里面详细介绍了重载运算符的实现。 赋值和拷贝构造了解了赋值运算符的重载,现在来看这一段
代码中的1和2有什么区别?1是拷贝构造,2是对象的赋值。拷贝构造和赋值又有什么区别?**用存在的对象去初始化不存在的对象叫拷贝构造,用存在的对象去初始化存在的对象叫赋值。**首先d2是不存在的,在创建d2对象时,用d1的值去初始化d2,这样就叫拷贝构造,而d3是之前已经创建的,将d1的值赋值到d3中,这样叫赋值。 初始化列表初始化列表的意义
这些变量真正的定义是在类的对象创建时,比如Date d1,创建了一个Date类的d1对象,此时d1的所有成员变量都有一块自己的内存空间,但还未赋初始值,因此构造函数被引入C++,构造函数将成员变量初始化,也就是赋初值。但总有一些变量不能在定义之后赋值,比如const修饰的常变量为只读属性,定义过后不能写入。引用,引用在定义时必须有一个明确的引用实体。 刚才说的,Date d1是为Date类中的所有成员变量开辟内存空间,如果Date类有一个引用或者const修饰的常变量,在定义时必须赋初值,而构造函数的赋初值是在变量定义后赋初值,想要在变量定义时就被赋初值,就要用到初始化列表。
在构造函数后加上冒号,用括号里的变量的值去初始化括号前变量的值(所以value是一个全局变量的引用,如果是一个局部变量的引用,出了构造函数,可能会导致非法访问的问题)。 总结一下就是:Date类的对象d1定义的过程:先去调用Date类的初始化列表(初始化列表是每个类都有的,就算没有显式的写出,因为初始化列表是变量定义的地方,一个变量要先定义才能使用是吧。初始化列表干的事就是为变量开辟内存空间),在类中的成员变量定义之后(调用完初始化列表之后)才会去调用构造函数, 除了const和引用要在初始化列表初始化,没有默认构造函数的自定义变量也要在初始化列表初始化
比如声明了一个ATest类,这个类的构造函数不是无参,不是全缺省,不是我们不写构造函数编译器自动生成的,所以ATest的构造函数不是默认构造函数,而Date类中有一个ATest类的对象_a,当定义一个Date类的对象时,需要通过初始化列表定义该对象(初始化列表是对象定义的地方,如果没有显式地在初始化列表中定义_a,编译器会自动定义_a,同时调用_a的默认构造函数,而_a没有默认构造函数,程序报错),此时就需要用初始化列表定义_a,并传参给 _a的构造函数 explici关键字
以上main函数中的ATest a = 10,存在隐式类型转换,表示先用10构造一个ATest类对象,再用该对象拷贝构造一个a。 成员初始化顺序
以上程序输出结果是什么?两个1吗? 静态成员静态成员变量假设我写了一段程序,要统计一个类的构造函数调用了多少次。那么可以创建一个全局变量count,在构造函数中写上++count。这种做法虽然可行,但全局变量破坏了程序的封装性,也让程序变得不安全,因为修改全局变量很容易。所以C++很少使用全局变量,要想统计一个类的构造函数调用了几次,可以创建一个静态的成员变量。
但在类中声明了静态成员变量还不能使用,这里理解一个点:静态成员变量不属于该类的任何一个对象(因此sizeof(对象名),不会将静态成员变量的大小算入其中),但属于所有该类的对象,是属于整个类的,生命周期是整个程序(在main函数创建栈帧之前,静态变量已经创建好了,然后根据main函数中的初始化语句进行初始化),作用域是该类,存储的区域是静态区。因此构造函数不会为静态成员变量赋初值,初始化列表也不会为静态成员变量开辟空间。 要为静态成员开辟空间,需要我们手动定义
如何访问静态成员变量?因为上面声明的静态成员变量是私有的,不能在类外直接访问,所以需要在类中定义一个公有函数,该公有函数返回_count的值。
静态成员函数以此引出静态成员函数,在定义函数前加上static,将函数定义成静态的,在访问该函数时,可以通过普通的对象访问,也能通过类来访问。 总结
友元友元分为友元类和友元函数,友元为我们提供了便利,但同时友元也破坏了程序的封装性,所以友元要尽量少用。 友元函数友元函数能直接访问一个类的私有成员,但不属于该类,在日期的实现中,友元函数在重载cout运算符时被应用,可以去看下这篇博客。 友元函数的声明可以放在类的如何一个地方,不过正常都放在类的最开始处(声明很简单,只要在函数名前加上friend)
假设有这样的需求:上面的Func需要访问两个类的私有成员,这时就需要将Func声明成Date和Time的友元函数,使Func函数能访问这两个类的私有成员。 友元类若将Date声明成Time的友元类,则Date的所有函数可以访问Time的私有成员,可以理解为Date类的所有函数都是Time的友元函数。
总结友元的访问关系:我想访问你的私有成员,我就要成为你的友元。且友元是单向的,若A是B的友元,A能访问B的私有,B不能访问A的私有;友元不能传递,A是B的友元,A能访问B的私有,C是A的友元,C能访问A的私有,但C不能访问B的私有,友元不具有传递性。 内部类内部类的概念及特性一个类定义在另一个类中,前者就是一个内部类,后者为外部类。内部类是一个独立的类,它不属于外部类,外部类不能访问内部类的任何对象 并且内部类可以定义在外部类的任何地方,但受访问限符private,public,protect的限制。当内部类是public时,作用域被限制在外部类中,所以定义内部类对象要写外部类名::内部类名。如果内部类是private的,则不能在外部类外创建内部类的对象,即函数或其他函数中,只能在外部类里面创建内部类对象,此时的内部类是外部类专属的。
编译器的优化最后看一段程序
(在类名后加上一对括号,表示创建一个该类的匿名对象,匿名对象的性质和临时变量相似,具有临时性,匿名对象被const修饰,因此当匿名对象作为函数形参时,不能用引用做形参,若用引用做形参需要加const) 对于这样连续构造的情况,现在的编译器大多会进行优化。 |
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/23 16:30:13- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |