☰
【C++——类和对象(上)】
2026/10/3 13:43:42 网站建设 项目流程

目录

  • 一、类
    • 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定义的类,默认访问权限为 private
  • struct定义的类,默认访问权限为 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();并没有因为解引用空指针报错

但语法上仍然要求通过对象 / 对象指针调用成员函数,两个原因:

  1. 需要通过对象类型确定对应的类域,编译器去类里面查找这个成员函数。
  2. 需要传递隐含的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;}
  1. 在成员函数内部访问成员,本质全部是通过this->访问。
  2. ❗不能在实参、形参位置显式写 this;但是函数体内可以显式写this->。
  3. this指针本身是Date* const,指针本身不能被修改,但是指针指向对象的内容可以修改。
  4. 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初始化函数

构造函数的特点:

  1. 函数名和类名完全相同
  2. 没有返回值,不能写 void
  3. 创建对象实例时,编译器自动调用
  4. 支持函数重载
#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();这会被编译器识别成函数声明,编译器无法识别会报警告,不会创建对象

  • 如果我们没有写任何构造函数,编译器自动生成一份无参默认构造;只要用户显式实现任意构造函数,编译器就不再生成默认构造

默认构造函数:不需要传递实参就可以调用的构造函数。
包括三种:

  1. 无参构造函数
  2. 全缺省构造函数
  3. 编译器自动生成的无参构造

Tips:这三者有且只能存在一个,不能同时共存

C++ 类型划分:

  • 内置类型(基本类型):语言原生类型,int、char、double、指针等
  • 自定义类型:class /struct 我们自己写的类

编译器自动生成的默认构造行为:

  1. 内置类型成员:没有强制初始化要求,值随机,看编译器实现。
  2. 自定义类型成员:自动调用该类型的默认构造函数初始化;如果该类没有默认构造,直接编译报错,此时需要初始化列表解决。

默认的无参的显式初始化需要传参数、如果有内置类型需要我们自行处理…所以构造函数大多数情况下都需要自己实现,小部分类似MyQueue、Stack有默认构造情况下可以依靠编译器自动生成

大部分场景构造函数建议自己实现,遵循「构造函数应写尽写」

2.析构函数

析构函数和构造函数功能相反,对象生命周期结束的时候自动调用,完成对象资源释放清理,等价于之前数据结构写的Destroy销毁函数

析构函数的特点:

  1. 析构函数名是在类名前加上字符~,函数名:~类名
  2. 无返回值、无参数
  3. 一个类只能有一个析构函数,不手动写编译器自动生成默认析构
  4. 对象生命周期结束,系统自动调用
  5. 编译器生成默认析构:内置类型成员什么都不处理;自定义类型成员会自动调用它的析构函数。
  6. 就算自己手写析构,类里面的自定义类型成员依然会自动调用析构

编码判断:

  • 类内部没有向堆申请资源(如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;};

拷贝构造关键特点

  1. 拷贝构造属于构造函数的重载。
  2. 第一个参数必须是类的引用,不能传值!因为语法逻辑上拷贝构造传值传参会引发无穷递归调用
  3. C++ 规定:自定义类型发生拷贝行为,必须调用拷贝构造。对象传值传参、传值返回,都会调用拷贝构造。
  4. 用户不显式写拷贝构造,编译器自动生成默认拷贝构造:
    • 内置类型:逐字节拷贝(浅拷贝 / 值拷贝)
    • 自定义类型成员:调用该类型的拷贝构造函数。

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;}

如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,这时的引用相当于一个野引用,类似一个野指针

传引用返回可以避免拷贝,前提:返回的对象生命周期不会随着函数结束销毁

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询