C 语言

关注公众号 jb51net

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

C++类和对象示例详解

作者:流浪001

这篇文章给大家介绍了C++类和对象的相关知识,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

1.类的定义

1.1类的定义格式

类的定义要用到class关键字,如class Stack {};,这里的Stack为类名,也可以理解为类型,class和struct很相似,他们两个都可以用来定义类,只是在class中定义的成员默认为私有(外界不允许访问),而struct定义的成员默认为公有(在外界允许访问)。

定义在类⾯的成员函数默认为inline。

1.2访问限定符

访问限定符共有三种

1.C++⼀种实现封装的⽅式,⽤类将对象的属性与⽅法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接⼝提供给外部的⽤⼾使⽤。

2.public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访
问。

3.• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
• ⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public。
1.3 类域
类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作⽤域操作符指明成员属于哪个类域。

2.实例化

⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。(类和对象的关系)

类是对象进⾏⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,⽤类实例化出对象时,才会分配空间。

⼀个类可以实例化出多个对象,实例化出的对象占⽤实际的物理空间,存储类成员变量。打个⽐
⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多
少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房
⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。

对象大小

实例化出的每一个对象都有自己独立的空间,所以对象中肯定包含
成员变量,那么成员函数是否包含呢?⾸先函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段)(类里面的函数并不占用实例化出的空间)只是用指针将其存储起来,可以理解为实例化出的每一个对象中,它们有各自的成员变量,但是他们的函数地址是一样的。

C++中对象的大小遵循结构体的内存对齐原则

内存对⻬规则:
• 第⼀个成员在与结构体偏移量为0的地址处。
• 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
• 注意:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员⼤⼩的较⼩值。
• VS中默认的对⻬数为8
• 结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍。
• 如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。

3.this指针

形参、实参都不能显式传this指针(编译器会自动绑定),但是函数体可以任意使用。

假如一个类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调⽤Init和
Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这⾥就要看到C++给了
⼀个隐含的this指针解决这⾥的问题。

类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this
指针。⽐如Date类的Init的真实原型为, void Init(Date* const this, int year,int month, int day)

4.默认成员函数

默认成员函数就是⽤户没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,重点讲前4个。

C++中一共有6大默认成员函数它们分别为:

1.构造函数

2.析构函数

3.拷贝构造函数

4.赋值重载(拷贝赋值运算符)

5.移动构造(C++11新增)

6.移动赋值(C++11新增)

1.构造函数

构造函数的特点:
1. 函数名与类名相同。
2. ⽆返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
3. 对象实例化时系统会⾃动调⽤对应的构造函数。
4. 构造函数可以重载。

5.如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显式定义编译器将不再⽣成。(当然也不是绝对的)如:

构造函数的实现:

6.我们不写构造函数时,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决。

初始化列表

初始化列表是构造函数的另一种形式,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。

每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义
初始化的地⽅。

引⽤成员变量const成员变量没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始
化,否则会编译报错。(重点)

尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表

初始化列表总结:
1.⽆论是否显⽰写初始化列表,每个构造函数都有初始化列表;
2.⽆论是否在初始化列表显⽰初始化成员变量,每个成员变量都要⾛初始化列表初始化;

什么情况下必须自己写构造函数

1.成员变量需要自定义初始化逻辑

默认构造只会做简单的初始化(对内置类型甚至不初始化,是随机值)
如果你要按业务规则给成员赋初值,就必须手写。

2.需要在构造中申请资源(动态内存、文件、网络连接等)

默认构造不会帮你 new 内存,你不写的话 data_ 就是野指针。
后续还需要你自己实现拷贝构造和赋值重载,防止浅拷贝问题。

3.需要对成员做特殊初始化(const、引用成员)

初始化列表还有一个重点:

初始化列表是按照成员变量在类里面声明的顺序进行初始化的,跟成员在初始化列表出现的顺序无关。

2.析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作

析构函数的特点
1. 析构函数名是在类名前加上字符 ~。
2. ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。

6.如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,但是有资源申请时⼀定要⾃⼰写析构否则会造成资源泄漏

7.⼀个局部域的多个对象,C++规定后定义的先析构。(类似于栈的特性:后进先出的特性)

我们可以看到,a2是后定义的,但是析构的时候是a2先析构的。

3.拷贝构造函数

如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数 也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数

拷贝构造函数针对的是用已经存在的对象去构造另一个新的对象。

拷⻉构造的特点:
1. 拷⻉构造函数是构造函数的⼀个重载。
2. 拷⻉构造函数的
第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。 拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。
3. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型
传值传参和传值返回都会调⽤拷⻉构造完成
4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造
对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉)对⾃定义类型成员变量会调⽤他的拷⻉构造。

拷贝构造:

5. 如果一个类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完 成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。如果有资源申请,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要 我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就 需要显⽰写拷⻉构造,否则就不需要

6. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。

传值返回:

传引用返回:

还有一个赋值运算符重载,我们在下一个专题(运算符重载)中讲。

5.运算符重载

当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规 定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编 译报错。

运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其 他函数⼀样,它也具有其返回类型和参数列表以及函数体。

如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数⽐运算对象少⼀个

.*

::

sizeof

?:

.

注意以上5个运算符不能重载。

重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。

重载<<和>>时,需要重载为全局函数若重载为成员函数,this指针默认抢占了第⼀个形参位
置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。

重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对
象。(此时需要在内部类中声明其为友元函数,对于友元后面会介绍)

1.赋值运算符重载

赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值。

赋值运算符重载的特点:

1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引⽤,否则会传值传参会有拷⻉

2. 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋 值场景。

3. 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷 ⻉构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义 类型成员变量会调⽤他的赋值重载函数。

4. 对于一个类,它的成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就 可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。对于有资源指向的类,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我 们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现 了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。

区分拷贝构造和赋值拷贝终极区分口诀
新对象创建 + 用旧对象初始化 = 拷贝构造
已存在的旧对象 + 用另一个对象赋值 = 拷贝赋值

2.const成员函数

将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后 ⾯。

const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。 const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this

对于一个日期加天数的函数,我们不希望原来的日期被修改,我们可以在其函数后面加上const

对于const Date* const this,最右边的const修饰的是this的指向,代表this的原本指向为一个Date类型的对象,不能别修改了。最左边的const修饰的是this指向的内容,代表这个Date类型的对象它的内容不能被修改。这样一来就完全将这个对象限定死了,无论如何都不能被修改了。

3.取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前成员的地址,我们可以用点小手段,给他一个假地址。

6.static成员函数

⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化

静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。

⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量
和静态成员函数。

7.友元

 友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类 声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。

外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数

⼀个函数可以是多个类的友元函数。

就拿前面所说的重载<<和>>来说,如果想要重载它们,直接在类里面进行定义的话,它的第一个参数为this指针,那么语法就与我们日常所写的有很大区别,因此我们将他在类外面定义,这样就可以不用考虑this指针了。但是在类外定义就不能访问类里面的私有成员,因此友元函数就起到了很大的作用。注意,要在类里面声明为友元。

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

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