目录
- 一、类
- 1.类定义格式
- (1)struct定义类
- 2.访问限定符
- 3.类域
- 二、实例化
- 1.成员变量和成员函数的存储
- 2.编译器编译成员函数
- 三、this指针
- 四、类的默认成员函数
- 1.构造函数
- 2.析构函数
- 3.拷贝构造函数
一、类
类是对一类事物的抽象描述,是模板、蓝图
类定义了一类事物有哪些属性、哪些行为
1.类定义格式
类的定义和结构体类似,但区别是类中可以定义函数
classStack{//成员变量(属性)//成员函数(方法)};class:定义类的关键字Stack:类的名字{}之间:类的主体- 类体内容统称为类的成员
- 类中的变量:成员变量 / 属性
- 类中的函数:成员函数 / 方法
Tips:类定义结束,大括号后面的分号千万不要忘记!
C++ 为了区分成员变量和函数形参,编码习惯上成员变量会增加标识:比如变量前后加下划线_,或者以m开头,例如_year、m_year
写在类内部的成员函数,默认是 inline 内联函数
成员函数支持声明与定义分离:类内只写函数声明,函数实现写在类外部
由于外部属于全局作用域,全局函数只会在全局寻找,因此必须使用类名::指定所属类域
classData{public://类内声明voidInit(intyear,intmonth,intday);};//类外定义,必须加类域Data::voidData::Init(intyear,intmonth,intday){_year=year;_month=month;_day=day;}(1)struct定义类
C++ 中struct兼容 C 语言结构体用法,同时升级成为类,内部也可以定义成员函数。
日常开发中,我们优先使用class来定义类
//C语言写法structListNodeC{intval;structListNodeC*next;//C中struct+结构体名称代表类型};typedefstructListNodeC1{intval;structListNodeC1*next;}LTNodeC;//C++ struct,不需要重复写structstructListNodeCPP{intval;ListNodeCPP*next;};2.访问限定符
封装是面向对象三大特性之一,访问限定符是C++的一种实现封装的方式。C++ 通过类把属性和方法整合在一起,再通过访问限定符选择性对外提供接口
访问限定符 { 在类外可以直接被访问: p u b l i c 在类外不能直接被访问: p r o t e c t e d 、 p r i v a t e 访问限定符\begin{cases} 在类外可以直接被访问:public\\ 在类外不能直接被访问:protected、private \end{cases}访问限定符{在类外可以直接被访问:public在类外不能直接被访问:protected、private
访问权限作用域:从该限定符出现位置开始,直到下一个访问限定符出现;如果后面没有其他限定符,则作用域到类结束的}
定义的成员没有被访问限定符修饰:
class定义的类,默认访问权限为 privatestruct定义的类,默认访问权限为 public
编码规范:
成员变量一般设置为private/protected做封装;需要外部调用的接口函数设置为public
3.类域
类定义了一个全新的作用域,类的所有成员都属于这个类域。
如果在类体外实现成员函数,必须使用::作用域解析运算符指明该成员属于哪一个类
类域的作用:影响编译器标识符查找规则
二、实例化
实例化:使用类类型,在物理内存上创建对象的过程。
- 对象:类的实例,根据模板生产出来的具体实体。
- 关系:一个类可以实例化出多个对象
内存规则:
类只是模板,类里面的成员变量仅仅是声明,不会分配内存空间;
实例化出来的对象才会占用物理内存,用来存储成员变量。
1.成员变量和成员函数的存储
- 成员变量:每个对象各自独立,每个对象单独存储自己的成员变量。
- 成员函数:所有对象调用的是同一份函数代码,全部存放在公共代码区,避免内存重复浪费。
函数指针只是地址,不会存放在对象内存中;调用成员函数时,会被编译成汇编指令,编译器编译链接阶段直接找到函数地址
#include<iostream>usingnamespacestd;classA{public:voidPrint(){}};classB{};intmain(){A a;B b;cout<<&a<<endl;cout<<sizeof(a)<<endl;cout<<sizeof(b)<<endl;return0;}a、b大小运行结果:
运行程序后我们发现成员变量A、B类的大小不为0
空类:没有任何成员变量的类
空类的大小为 1 字节。这 1 字节仅仅做占位,用来标识这个对象是唯一存在的,不存储有效数据
2.编译器编译成员函数
#include<iostream>usingnamespacestd;classA{public:voidPrint(){cout<<"A::Print()"<<endl;}};intmain(){A*p=nullptr;p->Print();return0;}成员函数地址编译阶段就已经确定,和对象本身没有关系;这里不会对空指针做解引用操作,因此p->Print();并没有因为解引用空指针报错
但语法上仍然要求通过对象 / 对象指针调用成员函数,两个原因:
- 需要通过对象类型确定对应的类域,编译器去类里面查找这个成员函数。
- 需要传递隐含的
this指针作为函数参数。
三、this指针
不同对象调用同一个成员函数,函数能够区分当前操作的是哪一个对象,底层依靠隐含的this 指针
this指针:编译器处理成员函数时,会自动在成员函数形参第一个位置增加当前类类型的指针。this 指针指向调用该函数的对象。
#include<iostream>usingnamespacestd;classDate{public://编译器底层等价于 void Init(Date* const this,int year,int month,int day)voidInit(intyear,intmonth,intday){_year=year;//等价 this->_year=year;_month=month;//等价 this->_month=month;_day=day;//等价 this->_day=day;}//等价 void Print(Date* const this)voidPrint(){cout<<_year<<"/"<<_month<<"/"<<_day<<endl;}private:int_year;int_month;int_day;};intmain(){Date d1;//编译器底层转换 d1.Init(&d1,2026,9,20);d1.Init(2026,9,20);//编译器底层转换 d1.Print(&d1);d1.Print();return0;}- 在成员函数内部访问成员,本质全部是通过
this->访问。 - ❗不能在实参、形参位置显式写 this;但是函数体内可以显式写
this->。 this指针本身是Date* const,指针本身不能被修改,但是指针指向对象的内容可以修改。- this 属于函数形参,一般存放在栈帧;部分编译器会做优化,放到寄存器提高运行效率。
面向对象三大特性:封装、继承、多态
C++ 把数据和函数收纳到类中,通过访问限定符管控访问权限,这是封装的基础体现
四、类的默认成员函数
默认成员函数:用户没有显式写,编译器会自动帮我们生成的成员函数
默认成员函数 { 初始化和清理 { 构造函数:主要完成初始化工作 析构函数:主要完成清理工作 拷贝复制 { 拷贝构造:使用同类对象初始化创建对象 赋值重载:主要是把一个对象赋值给另一个对象 取地址重载:主要是对普通对象和const对象取地址,这两个很少会自己实现 默认成员函数\begin{cases} \text{初始化和清理}\begin{cases} \text{构造函数:主要完成初始化工作}\\ \text{析构函数:主要完成清理工作} \end{cases}\\ \text{拷贝复制}\begin{cases} \text{拷贝构造:使用同类对象初始化创建对象}\\ \text{赋值重载:主要是把一个对象赋值给另一个对象} \end{cases}\\ \text{取地址重载:主要是对普通对象和const对象取地址,这两个很少会自己实现} \end{cases}默认成员函数⎩⎨⎧初始化和清理{构造函数:主要完成初始化工作析构函数:主要完成清理工作拷贝复制{拷贝构造:使用同类对象初始化创建对象赋值重载:主要是把一个对象赋值给另一个对象取地址重载:主要是对普通对象和const对象取地址,这两个很少会自己实现
1.构造函数
构造函数是特殊成员函数,在对象实例化的时候自动调用,完成对象初始化,替代我们手动写的Init初始化函数
构造函数的特点:
- 函数名和类名完全相同
- 没有返回值,不能写 void
- 创建对象实例时,编译器自动调用
- 支持函数重载
#include<iostream>usingnamespacestd;classDate{public://无参构造Date(){_year=1;_month=1;_day=1;}//带参构造Date(intyear,intmonth,intday){_year=year;_month=month;_day=day;}voidPrint(){cout<<_year<<"/"<<_month<<"/"<<_day<<endl;}private:int_year;int_month;int_day;};intmain(){Date d1;d1.Print();Dated2(2026,9,21);d2.Print();return0;}⚠️注意:调用无参构造不要写括号!Date d1();这会被编译器识别成函数声明,编译器无法识别会报警告,不会创建对象
- 如果我们没有写任何构造函数,编译器自动生成一份无参默认构造;只要用户显式实现任意构造函数,编译器就不再生成默认构造
默认构造函数:不需要传递实参就可以调用的构造函数。
包括三种:
- 无参构造函数
- 全缺省构造函数
- 编译器自动生成的无参构造
Tips:这三者有且只能存在一个,不能同时共存
C++ 类型划分:
- 内置类型(基本类型):语言原生类型,int、char、double、指针等
- 自定义类型:class /struct 我们自己写的类
编译器自动生成的默认构造行为:
- 内置类型成员:没有强制初始化要求,值随机,看编译器实现。
- 自定义类型成员:自动调用该类型的默认构造函数初始化;如果该类没有默认构造,直接编译报错,此时需要初始化列表解决。
默认的无参的显式初始化需要传参数、如果有内置类型需要我们自行处理…所以构造函数大多数情况下都需要自己实现,小部分类似MyQueue、Stack有默认构造情况下可以依靠编译器自动生成
大部分场景构造函数建议自己实现,遵循「构造函数应写尽写」
2.析构函数
析构函数和构造函数功能相反,对象生命周期结束的时候自动调用,完成对象资源释放清理,等价于之前数据结构写的Destroy销毁函数
析构函数的特点:
- 析构函数名是在类名前加上字符~,函数名:
~类名 - 无返回值、无参数
- 一个类只能有一个析构函数,不手动写编译器自动生成默认析构
- 对象生命周期结束,系统自动调用
- 编译器生成默认析构:内置类型成员什么都不处理;自定义类型成员会自动调用它的析构函数。
- 就算自己手写析构,类里面的自定义类型成员依然会自动调用析构
编码判断:
- 类内部没有向堆申请资源(如Date 日期类):不需要手写析构,使用编译器默认生成即可。
- 默认生成的析构可用(如MyQueue):不需要手写析构
- 类中有 malloc/new 申请堆内存资源(如Stack 栈类):必须手动写析构释放资源,否则内存泄漏
classStack{public:Stack(intn=4){_a=(int*)malloc(sizeof(int)*n);if(nullptr==_a){perror("malloc fail");return;}_capacity=n;_top=0;}~Stack(){free(_a);_a=nullptr;_top=_capacity=0;}private:int*_a;int_top;int_capacity;};intmain(){Stack st1;Stack st2;return0;}如果有多个对象,对象销毁顺序:后定义的对象先析构
3.拷贝构造函数
拷贝构造是特殊的构造函数:使用一个已经存在的同类对象,去初始化创建另一个新对象
拷贝构造函数的第一个参数必须是自身类类型的引用,且其余参数要有缺省默认值
#include<iostream>usingnamespacestd;classDate{public:Date(intyear=1,intmonth=1,intday=1){_year=year;_month=month;_day=day;}//拷贝构造函数Date(constDate&d){_year=d._year;_month=d._month;_day=d._day;}voidPrint(){cout<<_year<<"/"<<_month<<"/"<<_day<<endl;}private:int_year;int_month;int_day;};拷贝构造关键特点
- 拷贝构造属于构造函数的重载。
- 第一个参数必须是类的引用,不能传值!因为语法逻辑上拷贝构造传值传参会引发无穷递归调用
- C++ 规定:自定义类型发生拷贝行为,必须调用拷贝构造。对象传值传参、传值返回,都会调用拷贝构造。
- 用户不显式写拷贝构造,编译器自动生成默认拷贝构造:
- 内置类型:逐字节拷贝(浅拷贝 / 值拷贝)
- 自定义类型成员:调用该类型的拷贝构造函数。
Tips:参数建议加上const保护源对象不被修改
引发无穷递归调用原因:
C++规定传值传参要调用拷贝构造
传值传参要调用拷贝构造,调用拷贝构造要先传参,如果拷贝构造完成了再继续调用原来的函数
但是如果本身调用的就是拷贝构造,调用这个拷贝构造要传值传参,传值传参会形成一个新的拷贝构造,就会造成无穷递归
但是如果第一个参数是类类型的引用:
引用传参,传入的参数是别名,不会形成新的拷贝构造
#include<iostream>intmain(){Dated1(2026,9,20);//普通构造d1.Print();Dated2(d1);//拷贝构造d2.Print();//两种写法,都会调用拷贝构造return0;}值拷贝/浅拷贝:按字节复制,一个字节一个字节的拷贝
深拷贝:不仅拷贝对象本身,对象指向的堆资源也要重新开辟一份拷贝
如果一个类显式实现了析构并释放资源,那么它就需要显式写拷贝构造,否则就不需要
⚠️Tips:
如果类中管理堆资源(Stack 栈),默认生成的浅拷贝会让多个对象指向同一块堆空间;对象析构时,同一块内存会被 free 多次,程序直接崩溃,这种情况必须手写拷贝构造,实现深拷贝
Stack 深拷贝示例:
Stack(constStack&st){_a=(int*)malloc(sizeof(int)*st._capacity);if(nullptr==_a){perror("malloc fail");return;}memcpy(_a,st._a,sizeof(int)*st._top);_top=st._top;_capacity=st._capacity;}调用拷贝构造:
#include<iostream>intmain(){Stack st1;st1.Push(1);//方式一Stackst2(st1);//方式二Stack st3=st1;return0;}传值返回:
Stackfunc1(){Stack st;returnst;}intmain(){Stack ret=fun1();return0;}传值返回会产生一个临时对象调用拷贝构造。传值引用返回返回的是返回对象的别名(引用),没有产生拷贝
传引用返回局部对象:
Stack&func2(){Stack st;returnst;}intmain(){Stack ret=func2();return0;}如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,这时的引用相当于一个野引用,类似一个野指针
传引用返回可以避免拷贝,前提:返回的对象生命周期不会随着函数结束销毁