一、前言
C/C++作为经典的编程语言,二者并不是相互独立,C/C++语法之间相互渗透,二者之间相辅相成,可以这么认为,C++是在C的基础上对C的语法进行了优化,在C中有结构体、枚举等自定义类型,C++在C结构体的基础上引入了类和对象的相关概念,类和对象与结构体结构特点相似,不同在于C语言的结构体只能定义成员变量,而C++的类除了可以定义成员变量,还可以定义成员函数,这是C++类与结构体的一大不同点,本文将围绕C++的类与对象展开介绍,主要介绍类和对象的结构特点、以及成员函数的定义和相关用法、类中的构造函数、析构函数、运算符重载等相关内容,类和对象作为C++入门的基础,同时也是C++的重点内容,学好这一模块,可以为我们后续学习面向对象编程、动态内存管理、STL等相关内容打下坚实的基础。
二、类
1、类的定义
C++的类与C中的结构体在结构上相似,它们都属于自定义类型,C中定义结构体的关键字为struct,C++定义类的关键字为class,当然C++也兼容C,因此在C++中struct也可以用来定义类,但一般使用class来定义类,类与C的结构体不同在于类中能定义成员函数,而结构体只能定义成员变量,不能定义成员函数,此外,结构体中的成员默认都是允许被外界访问,而在类中的成员变量、函数默认不允许被外界访问,为类私有,类中的成员可分为public、private、protected三大类型,public的成员变量、函数允许被外界访问,private、protected类型的成员变量、函数为类私有,不能被外界直接访问。每个类型的作用范围从该类型的当前位置开始,到下一个类型开始前结束,若没有下一类型,则一直作用到类结束。

(1)
如图(1),我们定义了一个类class,类名为date,其成员函数Init为public类型,允许外界直接访问,Init函数用于类date的初始化,即分别初始化成员变量_year、_month、_day,成员变量_year、_month、_day为private类型,成员变量_time、_miniutes、_seconds为protected类型,二者均不允许外界直接访问。
此外,类中的成员函数也可在类外定义,即成员函数在类中声明,类外定义,但类外定义时需指明该成员函数所在的类域,即需要使用域操作符::

(2)
如图(2),在类date中声明了Init函数,但Init函数的定义在类外,这时就需使用域操作符::,即date::Init。
2、类名
C的结构体若想对它进行命名,一般需要使用typedef关键字对其命名,而在C++中,则无需使用typedef来对类进行命名,class后面的类名即可表示该类的类型,也就是该类的类名。

(3)
在实现链表结构时,我们定义了链表结点的结构体,对结点进行命名时,用C的语法,一般使用typedef来对结构体命名,在C++中,则无需typedef来对结构体进行命名,struct后面的类名ListnodeCpp即可代表该结构体类型,可以看出,C++的命名表示方式更加方便。
3、this指针
C++类的成员函数参数与普通函数的函数参数有所不同,类的成员函数参数多了一个隐藏的this指针,this指针指向该对象的地址,对象是类的实例化,如果将对象比作房子,那么类就是设计该房子的图纸,类的成员变量都只是声明,编译器并没有为类的成员变量开空间,而对象的成员变量都有空间大小,是类的实例化。

(4)

(5)
如图(4)(5)所示,date类中的成员函数Init与print函数参数实际隐藏了一个指向对象的this指针,this指针具体可表示为date*const this,我们在实际书写时,this指针不能显式表示在成员函数的函数参数中,但可以显式表示在成员函数的内部。

(6)

(7)
如图(6)(7)所示,this指针显式表现在成员函数Init、print内部,当然,this指针也可以不显式在成员函数内部,需要注意的是this指针不能显式写在成员函数的函数参数中,this指针指向该对象的地址,使得之后调用该成员函数不需要再手动传该对象的地址。

(8)
图(8)中定义了两个date类型的对象d1、d2,并对d1、d2进行初始化,Init函数的this指针指向d1、d2的地址,因此调用Init函数无需传d1、d2的地址,只需传实参即可完成d1、d2的初始化。

(9)
如图(9)所示,d1、d2调用Init函数只需传实参即可,即d1.Init(2025,9,24),d2.Init(2025,9,25),即可完成d1、d2的初始化,d1.print()、d2.print(),调用print打印d1、d2,同理print成员函数也有指向d1、d2的this指针,调用时也无需传d1、d2的地址,直接调用即可。
4、内存对齐
与C的结构体一样,C++的类也有内存对齐规则,内存对齐要求类的第一个成员在结构体偏移量为0的地址处,其他成员变量要对齐到自身对齐数的整数倍位置,对齐数为编译器默认的对齐数与该成员大小的较小值,在VS环境中,编译器默认的对齐数为8,类的大小为所有成员变量最大对齐数的整数倍,若类中还嵌套了其他类,则嵌套的类先对齐到自身最大对齐数的整数倍处,类的总大小就是最大对齐数(含嵌套类的最大对齐数)的整数倍。
需要注意的是类的成员函数并没有存放在类中,即成员函数指针并没有在类中,因为不同对象的成员变量可能不相同,但它们的成员函数是一样的,即它们的函数指针是一样的,如果成员函数存放在类中,即函数指针存放在类中,那么如果类要实例化100个对象,那么函数指针就要存储100次,显然函数指针存储在对象中就浪费了,但实际上函数指针是不需要存储的,函数指针是个地址,调用函数时会被转化为汇编指令,call地址,这是在编译链接过程就要完成的,并不是在程序运行时才完成,因此函数指针并不存放在类中。

(10)
(10)中定义了一个类A,求A的大小,根据内存对齐规则,A的成员函数Print并没有存放在A中,因此在计算A的大小时不计算函数print的大小,只需计算成员变量的大小即可,第一个成员变量_ch大小为1个字节,直接存放在结构体偏移地址为0的位置,第二个成员变量_i大小为4个字节,需存放在自身对齐数的整数倍位置,VS的默认对齐数为8,_i的大小为4,取较小值4,故_i的对齐数为4,需对齐到4的整数倍位置,如(10)中所示,_ch与_i中间空了3个字节,大小为1+3+4=8个字节,也恰好为最大对齐数4的整数倍,故A的大小为8个字节。

(11)
一些特殊情况,如(11)所示,当类中无成员变量或函数时,类的大小并不为0,因为若类的大小为0,与类不存在有歧义,因此规定类的大小不为0,大小为1,1的作用仅仅是为了证明类的存在,起占位作用,同理当类只有成员函数没有成员变量时,由于成员函数指针并没有存放在类中,但类的大小也不为0,大小也为1,这个1的作用与上面类似,用于证明类的存在,起占位作用。
三、类的默认成员函数
类的默认成员函数是指用户没有显式实现,编译器会默认自动生成的成员函数,编译器默认生成的成员函数有6个,默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符,本文将着重介绍默认构造函数与析构函数,它们也是较为重要的两个默认成员函数。
1、构造函数
构造函数是特殊的成员函数,主要用于对象的初始化。构造函数的函数名与类名相同,构造函数无返回值,对象实例化时系统会自动调用对应的构造函数,构造函数可以重载,如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

(12)

(13)
如(12)、(13)所示,构造函数有3大类型,无参构造函数、带参构造函数、全缺省构造函数,无参构造函数、全缺省构造函数、不写构造函数时编译器默认生成的构造函数,都是默认构造函数,这3个函数有且只有一个存在,不能同时存在,无参构造函数和全缺省构造函数虽然构成函数重载,但调用时存在歧义,如下图(14)所示。


(14)
C++把类型分为内置类型和自定义类型,内置类型就是语言提供的原生数据类型,如int/char/double/指针等,自定义类型就是class/struct等关键字自己定义的类型,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么编译器就会报错。
了解了构造函数,我们就可以利用构造函数来初始化对象,在绝大多数情况下,我们不写构造函数时,编译器默认生成的构造函数是不靠谱的,因此对于构造函数,我们应写尽写。


(15)
如图(15)所示,利用构造函数实现对栈Stack的初始化,构造函数的函数名与类名Stack相同,构造函数同样也具有this指针,指向对象的地址,因此构造函数也无需传对象的地址,初始化栈我们以栈的空间大小n=4为缺省值,实现对栈的初始化。
2、析构函数
析构函数与构造函数功能相反,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的函数名是在类名前加上字符~,与构造函数相似,析构函数无参数无返回值,一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数,当对象生命周期结束时,系统会自动调用析构函数,与构造函数相似,我们不写,编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用他的析构函数,还需要注意的是我们显式写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。


(16)
如(16)所示,我们利用类实现栈Stack,并调用Stack构造函数进行了初始化,~Stack为Stack的析构函数,用于栈空间资源清理释放工作,free释放_arr的空间,并将_arr置为nullptr,_capacity与_top置为0,完成对Stack的析构。

(17)
有了前面的Stack栈结构,我们可以借助双栈结构来实现队列,其中一个栈用于入数据,模拟队列的入数据,另一个栈用于出数据,模拟队列的出数据,两个栈正好互补,就实现了队列的先进先出结构,需要注意的是,在对队列对象进行销毁时,即使用户显式写析构函数~Myqueue,由于成员变量是自定义类型,所以编译器也会自动调用Stack的析构函数~Stack。
四、运算符重载
当运算符被用于类类型的对象时,C++允许我们通过运算符重载的形式指定新的含义,C++规定类类型对象使用运算符时,必须转化成调用对应的运算符重载,若没有对应的运算符重载,则会编译报错。运算符重载是具有特定名字的函数,由operator关键字与后面要定义的运算符共同构成,与其他函数一样,它也具有其返回类型和参数列表以及函数体。重载运算符函数的参数个数与该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。

(18)

(19)
如图(18)、(19)所示,(18)定义了一个类Date,用于表示日期,我们对运算符==进行重载,来判断日期是否相等,operator==为运算符重载函数,二元运算符函数的函数参数为2个,Date类型的变量d1、d2,返回类型bool类型,用来判断真假,日期相等即年、月、日相同。

(20)
(20)中定义了两个Date类型的对象d3、d4,并对它们进行了初始化,调用运算符重载函数operator==来判断d3、d4是否相等,ret来接收operator==函数的返回值。

(21)
d3与d4相等,故ret=1,运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
需要注意的是如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。


(22)
如(22)所示,operator==运算符重载函数是类Date的成员函数,这时该函数的第一个运算对象默认传给隐式的this指针,参数比运算对象少一个。

(23)
如图(23)所示,比较d3、d4,只需调用d3中的operator==运算符重载函数进行比较,即d3.operator==(d4),ret接收operator==函数的返回值,运算符重载函数operator==也可简化表示为==,即d3==d4,此时==就相当于operator==函数。

(24)
五、结语
类和对象作为C++的入门基础,同时也是C++的重点内容,是之后面向对象编程、动态内存管理、STL等内容的学习基础,本文着重介绍了C++类和对象中有关类的定义、类名、以及成员函数中this指针的相关问题,类的内存对齐方式与C的结构体内存对齐方式一致,类与结构体最大的不同在于类中可以定义成员函数,此外,类还具有默认的成员函数,本文重点介绍了构造函数与析构函数,类的构造函数与析构函数是类的特殊成员函数,构造函数与析构函数在用户没有显式定义时,编译器也会默认生成,此外,C++还支持运算符重载,运算符重载主要用于自定义类型的比较,运算符重载函数的函数参数个数与参与该运算符的运算对象个数一致,当运算符重载函数作为成员函数时,这时该函数的第一个运算对象将默认传给隐式的this指针,这时函数参数个数会比参与运算的对象个数少一个。概括地说,类是蓝图,对象是实例,这是C++面向对象编程的基石,但这仅仅只是开始,后面还有封装、继承、多态三大特性,它们同样以类和对象为基础,这是C++面向对象编程的一大特点,面向对象,不仅是技术,更是艺术!


被折叠的 条评论
为什么被折叠?



