【C++】 初阶篇 类与对象(上) 类和this指针
前言
从本篇开始,我们将正式踏入 C++ 面向对象编程的核心。讲解类与对象的基础知识,涵盖类的定义、访问控制、对象内存布局以及this指针等关键概念,同时初次接触面向对象三大特性之一——封装,为后续的深入学习打下坚实基础。
一、 面向对象VS面向过程
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
1.1 面向过程与面向对象的区别
比如 “洗衣服” 这个需求,C 语言的思路是拆成 “线性步骤”,用函数一步步实现:
// 伪代码:面向过程的洗衣服void拿盆子(){...}void放水(){...}void放衣服(){...}void手搓(){...}void拧干(){...}void晾衣服(){...}// 主逻辑:按顺序调用函数intmain(){拿盆子();放水();放衣服();手搓();拧干();晾衣服();return0;}核心是 “关注过程”,函数是操作的载体,数据(比如衣服、水)和函数是分离的。
而++ 会先拆分出对象(衣服、洗衣机、洗衣粉),再定义每个对象的 “属性” 和 “行为”,最后靠对象交互完成需求:
衣服对象:属性(脏污程度、材质),行为(被清洗、被晾干)
洗衣机对象:属性(容量、转速),行为(注水、洗衣、甩干)
洗衣粉对象:属性(类型、用量),行为(溶解、去污)
核心是 “关注对象”,把数据和操作数据的方法 “打包” 到对象里,逻辑更贴近现实世界,也更适合复杂系统。
二、 类的定义
2.1引入
C 语言的结构体只能放变量,但 C++ 的结构体(甚至更常用的class)能同时放变量和函数—— 这是 C++ 实现 “数据 + 方法封装” 的基础。比如:之前用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数。
typedefintDataType;structStack{// 成员函数:操作栈的方法voidInit(size_t capacity){_array=(DataType*)malloc(sizeof(DataType)*capacity);if(nullptr==_array){perror("malloc失败");return;}_capacity=capacity;_size=0;}voidPush(constDataType&data){// 扩容逻辑(简化)_array[_size]=data;++_size;}DataTypeTop(){return_array[_size-1];}voidDestroy(){if(_array){free(_array);_array=nullptr;_capacity=0;_size=0;}}// 成员变量:栈的数据DataType*_array;size_t _capacity;size_t _size;};// 使用时直接调用对象的方法,不用传结构体指针!intmain(){Stack s;s.Init(10);// 直接调用成员函数s.Push(1);s.Push(2);cout<<s.Top()<<endl;// 输出2s.Destroy();return0;}不过在 C++ 里,更推荐用class代替struct定义类 —— 两者的核心区别是默认访问权限
2.2类访问限定符
C++ 一种实现封装的方式,用类将对象的属性与方法结合在一起, 一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。
| 限定符 | 作用域内访问 | 类外访问 | 说明 |
|---|---|---|---|
public | ✅ | ✅ | 对外暴露的接口(如成员函数) |
protected | ✅ | ❌ | 目前和private一样,到继承那会有区别 |
private | ✅ | ❌ | 隐藏的内部细节(如成员变量) |
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
2.3定义
类的定义和和C语言中的结构体非常相似,但是C++中的类除了定义变量外还可以定义函数。class为定义类的关键字,{}中为类的主体。一定要注意后面的分号。
类体中的内容称为类的成员:
- 类中的变量称为成员变量。
- 类中的函数称为成员函数。
2.3.1类域
- 类定义了新的作用域,名为类域。同命名空间类般能解决命名冲突。类的所有成员都在类的作用域中。在类体外定义成员时,需要使用
::作用域操作符指明成员属于哪个类域进行优先查找。
定义一个队列,定义一个栈,他们都有相同名称的函数方法,如果没有类域的隔离,就会发生冲突。
classQueue{voidPush(intx){}};classStack{voidPush(intx){}类域和命名空间域只是名称隔离,不影响声明周期。
2.3.2定义类的两种写法
定义类一般有两种写法,关键看 “成员函数的定义位置”。
- 声明 + 定义全在类里
#include<iostream>usingnamespacestd;classDog{public://公有intAge(){returnage;}private://一般数据都很私密像我们的身份证那些都不想给别人看到charname[5];intage;};类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:在声明的地方成员函数名前需要加类名::
此时定义在类里面的成员函数默认为inline。如果不希望自己的成员函数是内联函数,为了解决这个问题:对同文件中的函数声明定义分离。因为内联函数不能声明定义分离。
Dog.h(声明)
#definePERSON_H// 防止头文件重复包含#include<iostream>usingnamespacestd;classDog{public:intAge(){};private:charname[5];intage;};Dog.cpp(实现)
classDog{public:intDog::Age()//{returnage;//实现}private:charname[5];intage;};2.3.3定义习惯
- 成员变量命名规则的建议:推荐加前缀(如_)或后缀区分
- 相对位置:各种公司中习惯将类的方法定义在上面,把类的属性定义在下面。
一定要区分 “成员变量” 和 “函数形参”,避免像下面这样写 “无效代码”:
classDate{public:voidInit(intyear){year=year;// 到底是给成员变量赋值,还是形参自己赋值?}private:intyear;// 成员变量};所以我们一般这么写
classDate{public:voidInit(intyear){_year=year;// 清晰:_year是成员变量,year是形参}private:int_year;// 加前缀_// 或 int mYear; // 加前缀m(成员的意思)};2.4类的实例化
用类类型创建对象的过程,称为类的实例化
类只是 “对象的设计图”,比如 “学生信息表” 是类,“张三的信息” 才是对象 ——实例化就是用类创建对象的过程。**类没有内存,对象才有**
类是 “建筑设计图”,对象是 “按图盖的房子”—— 设计图没有实体,房子才有实际空间。一个设计图也可以盖很多房子
2.5如何计算类实例化出对象的大小
一个类实例化出的对象可以分为2部分,成员变量,成员函数,那我们计算大小的时候就可以分为3种情况看以下的代码
// 类中既有成员变量,又有成员函数classA1{public:voidf1(){}private:int_a;};// 类中仅有成员函数classA2{public:voidf2(){}};// 类中什么都没有---空类classA3{};intmain(){cout<<"A1:"<<sizeof(A1)<<endl<<"A2:"<<sizeof(A2)<<endl<<"A2:"<<sizeof(A3)<<endl;return0;}类里既有成员变量又有成员函数,但计算对象大小时,我们发现只算成员变量的大小,那么成员函数在哪里呢?空类为什么是1呢
2.5.1. 为什么不算成员函数&&空类为1?
- 如果每个对象都存一份成员函数,100 个对象就有 100 份相同的代码,会浪费内存 —— 所以成员函数统一存在 “公共代码区”,所有对象共享。
- 空类为1,如果一个字节都不给,怎么表示类存在过呢!所以这里给1字节,纯粹是为了占位标识对象存在。
2.5.2.成员变量的计算方式
对于多个成员变量的函数我们一般采用内存对齐的方式(和C语言的结构体一样)进行计算的
- 结构体的内存对齐规则:
- 第一个成员在偏移量 0 的位置;
- 其他成员对齐到 “对齐数” 的整数倍(对齐数 = 编译器默认对齐数(VS 默认是 8)和成员大小的较小值);
- 整个对象的大小是 “最大对齐数” 的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
这里举一个例子
classA{private:char_a;// 1字节,对齐数1int_b;// 4字节,对齐数4(min(8,4))double_c;// 8字节,对齐数8(min(8,8))};// 计算:1(_a) + 3(填充) + 4(_b) + 8(_c) = 16字节(最大对齐数8,16是8的整数倍)cout<<sizeof(A)<<endl;// 输出162.5.3.一些小问题
问题 1:结构体怎么对齐?为什么要进行内存对齐?
- 对齐规则: 上面有
- 对齐原因:
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
- 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。
问题 2:如何让结构体按照指定的对齐参数进行对齐?能否按照 3、4、5 即任意字节对齐?
- 指定对齐方式:
编译器指令:如#pragma pack(n)(n为对齐字节数,如#pragma pack(4)指定 4 字节对齐);
C++11 属性:[[gnu::aligned(n)]](GCC 扩展)或alignas(n)(标准语法)。(后面学的可以先了解) - 能否任意对齐:不能。对齐参数n需是 2 的幂(如 1、2、4、8 等),因为硬件访问机制基于 2 的幂对齐,非 2 幂(如 3、5)的对齐参数编译器可能不支持或引发性能 / 兼容性问题。
问题 3:什么是大小端?如何测试某台机器是大端还是小端.
- 大小端定义:
大端():数据高位字节存低地址,如0x12345678存为0x00(低地址)、0x0003(高地址)
小端():数据低位字节存低地址,如0x1234存为0x34(低地址)、0x12(高地址)。 - 测试方法:
inta=1;// 二进制0x00000001if(*(char*)&a==1)printf("小端\n");elseprintf("大端\n");三、this 指针
3.1 this 指针的引出
日期类例子:
classDate{public:voidInit(intyear){_year=year;}voidprint(){cout<<"printf()"<<endl;}int_year;};intmain(){Date d1;Date d2;return0;}上面的代码中,Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这里就要看到C++给了一个隐含的this指针解决这里的问题
3.2定义
编译器编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类类型的指针,叫做this指针,this传递的是调用这个函数的对象的指针,(在调用的时候他会悄悄的把调用者的地址传过去。)比如Date类的Init的真实原型为:
// 编译器的隐藏操作(用户不用写)voidInit(Date*this,intyear,intmonth,intday){this->_year=year;// 通过this指针访问当前对象的成员this->_month=month;this->_day=day;}所以你的函数的第一个参数有没有参数,他们都有一个隐含的参数this,所有的非静态的成员函数都会增加这么一个隐含的this指针。
以d1举例,this传过去的是d1的指针。 那么怎么知道后面的参数是d1的年月日呢?
this指向调用对象,类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值:
this->_year=year;注意事项
C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。
3.3特性
- 类型:类型* const,即指向当前类对象的常量指针, this的值是不可以修改的,this的指向的内容是可以被修改。
- 存储与传递:通过寄存器隐式传递(经常使用,利用寄存器存储快),是成员函数的隐式形参,不存储在对象内部;
- 空指针场景:是否崩溃取决于是否解引用 this。若成员函数不访问成员变量(未解引用 this),this 为空时可正常运行;若访问成员变量(解引用 this),this 为空则会触发未定义行为(通常崩溃)。
// 例1:不访问成员变量,this为空也运行classA{public:voidPrint(){cout<<"Print()"<<endl;}private:int_a;};intmain(){A*p=nullptr;p->Print();// 正常运行:Print()不访问_a,不用解引用thisreturn0;}// 例2:访问成员变量,this为空崩溃classA{public:voidPrintA(){cout<<_a<<endl;}// 要访问_a,需解引用thisprivate:int_a;};intmain(){A*p=nullptr;p->PrintA();// 崩溃:this是nullptr,解引用报错return0;}cout<<_a<<endl实际上是cout<_a<<endl(解引用了)。