【C++】类与对象一
前言因为类与对象的内容比较多所以我打算将类与对象分为好几篇文章。本文主要是给类与对象开个头介绍一下类和对象的定义与实例化最后与C语言做下对比算是在类与对象章节中比较简单的一篇本章所以也相应的会比较短后面才是重头戏总感觉肝有点小痛1.类与对象的定义与语法1.1类的介绍与语法格式定义类就像是我们在C语言阶段学过的结构体的升级事实上C也会C语言的结构体进行了升级后面会有介绍在以前学习的C语言的结构体中我们只能在结构体中定义成员变量而不能在里面定义函数。而C又是一个面向对象的语言比如我想将洗衣机定义为一个对象的话我们不仅仅需要成员变量来描述这个洗衣机的参数我们还需要各种函数来实现洗衣机的功能用户才能通过接口来使用这个洗衣机。为了解决这个痛点实现面向对象C引入了类的概念。类不仅可以在里面定义成员变量还可以在里面定义出成员函数。但我们需要注意以下类与命名空间的区别类是可以实例化创建多个实体的但命名空间不能而且类有自己的域叫做类域命名空间有自己的域叫做命名空间域二者都不会影响变量的生命周期。下面我就定义一个栈类来为大家讲解类classStack// 也可以写成struct Stack但是会变成默认公开{public://修饰符下面会介绍//成员函数类的方法voidInit(){aNULL;capacity0;top0;}voidDestroy(){free(a);aNULL;capacity0;top0;}voidPush(intx){...}voidPop(){...}private://同样的修饰符//成员变量属性int*_a;int_top;int_capacity;};class就是C提供的一个关键字旁边的是类名这里的用法和命名空间是不是很像。在类中我们分为两个部分一个为成员函数类方法和成员变量属性一般来所我们一般都把成员变量放在成员函数的后面当然你要放在上面的话语法也没什么问题主要看你。你可能注意到我统一在成员变脸前加上了一个_这里我只要是想要声明一下该变量为类的成员变量这里比较的自由不加也是可以的还有加的方法在C中也并没有一个固定的标准可能在不同的公司都有各种的标准但在这里我就统一在前面加上一个_这是我的个人习惯。前面也有提到过C同样对C语言的结构体struct进行了升级和class的是几乎一致的但是有一个小差别在C中用class定义类里面的变量或者函数默认来所是私有的但如果用struct来定义类的话就默认都是共有的可以随便调用关于私有公有我会在后面介绍访问限定符中详细介绍我这里先点到为止在类里里面的成员函数都默认为inline函数只是隐藏起来了其实都会被inline修饰但其实也不同担心代码量过大因为inline毕竟只是对编译器的建议该不展开还是不会展开的1.2 访问限定符很明显在上面的类中我还有private和public关键字这些关键字的作用是什么呢下面我将依依介绍C的类是一种实现封装的方式将对象的属性与方法给结合到一起但在有些情况下我们并不希望属性或者方法被外部类外被调用修改为了解决这个问题在C中提供了访问限定符来修饰类里的属性或者方法这样我们就可以选择性的把接口给外部的用户来使用。访问限定符一共有三种在现阶段我们就认为private和protected是一样的作用这两个的差别只有在后面介绍继承才能给说清楚被public修饰的成员在类外是可以直接的访问的但是被private和protected修饰的成员在类外是不能被直接访问的但如果是在类里的话那它们彼此之间就可以随意的访问访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌如果后⾯没有访问限定符作⽤域就到 }即类结束 比如在上面的类中成员函数都被public给修饰而成员变量都被private给修饰了class定义成员没有被访问限定符修饰时默认为privatestruct默认为public1.3 类域和命名空间一样类也会自己的域叫做类域。既然有了域的概念类能不能像命名空间一样做到声明与定义隔离呢答案是可以的我们同样可以通过::域作用限定符指定该成员属于哪个类域。classStack{public://声明voidInit();voidDestroy();private:int*_a;int_top;int_capacity;};//定义voidStack::Init(){//....}voidStack::Destroy(){//....}类域影响的是编译时的查找规则如果不指定的话编译器会认为这是一个全局的函数找不到里面的函数变量时就会报错如果指定了类域的话编译器就会知道这个函数是一个成员函数当前域里找不到函数变量就会去指定的域里面找2.类的实例化上面的内容都是教大家画蓝图下面来介绍如何用蓝图来盖房子2.1实例化概念前面我们定义的类其实并没有在物理内存中开辟一块内存我们之前所做的更像是声明有这么一个类就像是盖房子之前画的那个蓝图一样。类只是一个比较抽象的描述相当于是一种模型限定了里面有哪些成员变量有哪些成员函数我们并没有使用这些变量或者函数。那我们该如何实例化并使用这些类呢下面定义一个日期类来举例子classDate{private:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}voidPrint(){cout_year/_month/_dayendl;}public:int_year;int_month;int_day;};把上面的类实例化为两个实体d1 d2并使用intmain(){//实例化出两个实体Date d1;Date d2;//初始化两个实体d1.Init(2026,6,1);d2.Init(2026,6,2);d1.Print();d2.Print();return0;}// 打印结果// 2026/6/1// 2026/6/2通过打印的结果我们可以知道通过这个类实例化的这两个实体是各种独立的它们都分别在内存中独立的占有自己的空间相当于我们用这个类作为蓝图去建造多个房子但各个房子里面住的人又是不同的:2.2对象的内存大小类的实例化出的实体叫做对象而对象肯定是要在堆区上开辟内存的。那么对象的大小我们该如何计算呢对象里的成员函数有些特殊不占用对象的内存后面我们会讨论这里的分配内存机制和C语言结构体的内存对齐机制是一模一样的,在我以前写过的文章【C语言】结构体全方面详细解析-CSDN博客中有介绍过这里我就不再赘述了#includeiostreamusingstd::cout;usingstd::endl;classDate{public:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}voidPrint(){cout_year/_month/_dayendl;}private:int_year;//在内存偏移量 0 - 3;int_month;// 在内存偏移量 4 - 7;int_day;// 在内存偏移量 8 - 11;};intmain(){Date d1;// 在vs中默认对齐数是8 ,成员变量的最大字节为4// 结果取二者的最小值的整数倍// 存放三个成员变量需要11字节// 所以结果应该是4的整数倍12coutsizeof(d1)endl;return0;}// 打印结果// 12看到这个结果你可能会产生出两个疑问类的定义会占用空间吗为什么成员函数不占用对象的空间这里我先解答第一个问题我们先来看下面这段程序classA{};classB{intx;};intmain(){std::coutsizeof(A)std::endl;std::coutsizeof(B)std::endl;return0;}// 打印结果// 1// 4第一个A我们通过sizeof运算符计算出A的大小为1字节那么B不应是4 1 5字节吗为什么还是通过内存对齐机制算出来的四个字节呢首先在C 标准规定任何完整类型的对象都必须有非零大小如果没有这个1的话那么我们该如何判断这个类是否存在过呢所以这个1纯粹是作为一个占位标识这个类的存在而B不是一个空类所以只需要正常的通过内存对齐的机制来计算大小即可A与B是相互独立的两个类。这里我们再解答第二个问题就是为什么成员函数不占用对象的空间。比如我们要为一个存在成员函数的类创建很多个对象那难道也要跟着创建很多个函数吗这些函数的功能都是一样的如果同样要创建很多个的话就会造成很严重的空间浪费问题。编译器很明显不会傻乎乎的这么干在编译阶段对应的成员函数的地址会被保存下来不同的对象在调用时调用的其实是同一个函数这个函数早在编译阶段就被编译成了一段汇编指令使用时直接通过保存的地址来找到这个函数下面我们可用通过汇编代码来看看验证我们的猜想classA{public:voidPrint(){std::couthellostd::endl;}};intmain(){A a1;A a2;a1.Print();a2.Print();return0;}转到反汇编你会发现这两个对象调用成员函数时会通过call跳转到同一个函数中这个函数相当于变成了公共代码区的函数。所以自然不会占用对象里的空间。2.3this指针前面我们知道编译器调用的成员函数实际上是同一个函数那么当我们为一个类创建多个对象时编译器是怎么知道该传哪个对象的值的呢这里就需要this指针来发挥作用了。编译器编译后类的成员函数默认都会在形参第⼀个位置增加⼀个当前类类型的指针叫做this指针这样这个函数就知道传的是哪个对象的值同样的我们通过成员函数访问成员变量时也同样需要this指针来访问成员变量。但是C规定不能在实参和形参的位置显式的写this指针(编译时编译器会处理)但是可以在函数体内显式的使用指针。下面我就以这个程序来作为例子(原版)classDate{public:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}voidPrint(){cout_year/_month/_dayendl;}private:int_year;int_month;int_day;};intmain(){Date d1;Date d2;return0;}本质classDate{public:// 语法不允许我这里只是演示voidInit(Date*constthis,intyear,intmonth,intday){this-_yearyear;this-_monthmonth;this-_dayday;}// 语法不允许我这里只是演示voidPrint(Date*constthis){coutthis-_year/this-_month/this-_dayendl;}private:int_year;int_month;int_day;};intmain(){Date d1;Date d2;d1.Init(d1,2026,6,1);d1.Print(d1);d2.Init(d2,2026,6,2);d2.Print(d2);return0;}this指针是调用函数成员时产生的临时指针变量函数执行完就自动销毁了。this指针我们一般都认为和函数里的局部变量一样存放在栈区但因为用的比较多有些编译器会优化为了寄存器这个并没有一个明确的标准各个编译器都有各自的方案。有了this指针我们在封装一个类时就不必要像在C语言阶段那样苦哈哈的传地址通过this指针隐含的传递我们在写代码时就会方便很多。完当然现阶段我们对C的了解还很少要等到后面深入的学习C才能感受到C面向对象三大特性封装、继承、多态的魅力。 ps: 终于通宵肝完了好累等会还要去上早八的水课/(ㄒoㄒ)/~~