news 2026/10/31 1:35:27

【C++类和对象下篇:深入理解面向对象的核心进阶特性】

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++类和对象下篇:深入理解面向对象的核心进阶特性】

C++类和对象下篇:深入理解面向对象的核心进阶特性

在C++面向对象编程中,类和对象是核心基石。继上篇掌握类的基本定义、对象实例化和普通成员函数的使用后,本文将聚焦于构造函数进阶、静态成员、友元、内部类等关键特性,这些知识点是突破C++面向对象编程的重要关口,也是面试高频考点。

引言

当你已经熟悉了类的基本定义和对象的使用后,是否曾有过这样的困惑:

  • 为什么构造函数体里的赋值不叫"初始化"?
  • 如何统计一个类创建了多少个对象?
  • 为什么cout << obj不能直接作为成员函数重载?
  • 内部类和外部类到底是什么关系?

本文将结合实际代码案例,逐一拆解这些核心概念,帮你彻底理解并灵活运用。


一、再谈构造函数:从赋初值到真正的初始化

构造函数的核心作用是创建对象时为其成员变量设置初始值,但普通的构造函数体赋值并非真正的"初始化"。这一章节的重点就是理解初始化列表的意义,以及explicit关键字的使用。

1.1 构造函数体赋值的局限

构造函数体中的语句本质是赋初值,而非初始化。因为初始化只能对变量执行一次,而构造函数体内可以对同一个成员变量进行多次赋值:

classDate{public:Date(intyear,intmonth,intday){_year=year;// 赋初值,而非初始化_year=2024;// 可多次赋值_month=month;_day=day;}private:int_year;int_month;int_day;};

上述代码中,_year被先后赋值两次,这说明构造函数体只是完成了值的修改,而非对成员变量的初始化。

1.2 初始化列表:真正的初始化方式

初始化列表是C++中对类成员变量进行真正初始化的唯一方式,它以冒号:开头,后跟逗号分隔的成员变量初始化项,每个成员变量后用括号指定初始值:

classDate{public:// 初始化列表初始化Date(intyear,intmonth,intday):_year(year),_month(month),_day(day){}private:int_year;int_month;int_day;};
初始化列表的四大核心注意事项

注意1:唯一性

每个成员变量在初始化列表中只能出现一次,因为初始化只能执行一次。

注意2:强制性

以下三类成员变量必须在初始化列表中初始化,无法在构造函数体中赋初值:

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类无默认构造函数时)
classA{public:A(inta):_a(a){}// 无默认构造函数private:int_a;};classB{public:// 必须在初始化列表初始化A对象、引用、const变量B(inta,intref):_aobj(a),_ref(ref),_n(10){}private:A _aobj;// 自定义类型,无默认构造int&_ref;// 引用constint_n;// const变量};

为什么这三类成员必须在初始化列表初始化?

  • 引用成员:C++语法规定,引用必须在定义时初始化,且一旦初始化不能改变指向;构造函数体是在成员定义(内存分配)后执行,无法完成引用的初始化。
  • const成员:C++中const常量必须在定义时初始化,后续不能修改其值,构造函数体无法满足"定义时初始化"的要求。
  • 无默认构造的自定义类型成员:自定义类型成员创建时,编译器会自动调用其构造函数;若该类无默认构造(无参/全缺省构造),编译器无法自动推导,必须在初始化列表中显式调用其带参构造完成初始化。

注意3:优先性

无论是否显式使用初始化列表,自定义类型成员变量一定会先通过初始化列表完成初始化。即使构造函数体为空,自定义类型成员也会调用其默认构造函数初始化。

注意4:顺序性

成员变量的初始化顺序由其在类中的声明次序决定,与初始化列表中的书写顺序无关。这是一个极易踩坑的点:

classA{public:A(inta):_a1(a),_a2(_a1){}voidPrint(){cout<<_a1<<" "<<_a2<<endl;}private:int_a2;// 先声明int_a1;// 后声明};intmain(){Aaa(1);aa.Print();// 输出:1 随机值}

上述代码中,_a2先声明,因此先被初始化,此时_a1尚未初始化,_a2(_a1)会取到随机值,后续_a1被初始化为1,最终打印结果为1 随机值。

避坑技巧:编写初始化列表时,让书写顺序与类内声明顺序一致,避免逻辑错误。

1.3 explicit关键字:禁止构造函数的隐式类型转换

构造函数除了初始化对象,单个参数或除第一个参数无默认值,其余均有默认值的构造函数,会具备隐式类型转换的能力。这种特性虽然灵活,但会降低代码可读性,explicit关键字可禁止该特性。

隐式类型转换的场景
classDate{public:// 单参构造函数,未加explicit,支持隐式转换Date(intyear):_year(year){}Date&operator=(constDate&d){if(this!=&d){_year=d._year;}return*this;}private:int_year;};voidTest(){Dated1(2022);d1=2023;// 隐式转换:2023构造无名Date对象,再赋值给d1}

上述代码的执行过程是两步操作:

  1. 编译器用2023调用Date(int year)构造一个临时匿名Date对象
  2. 调用赋值运算符operator=,将临时对象赋值给d1
explicit的作用

在构造函数前添加explicit,即可禁止上述隐式转换:

explicitDate(intyear):_year(year){}// 此时d1 = 2023; 编译报错,禁止了隐式类型转换

扩展要点:

  • explicit仅对单参构造或除第一个参数无默认值,其余全有默认值的多参构造有效
  • C++11中,explicit也可修饰拷贝构造,禁止拷贝构造的隐式转换
  • 显式转换不受影响:d1 = Date(2023)是显式构造临时对象,即使加了explicit也能编译通过

二、static成员:类的共享成员

在类中,用static修饰的成员变量和成员函数,称为静态成员,其核心特性是属于类,而非某个具体的对象,为所有类对象所共享,是实现类级别的数据共享和函数调用的关键。

2.1 静态成员的核心概念

  1. 静态成员变量:类内声明,必须在类外初始化,初始化时不添加static关键字
  2. 静态成员函数:无隐藏的this指针,不能访问非静态成员
  3. 访问方式:类名::静态成员或对象.静态成员

2.2 经典面试题:统计类对象的创建个数

静态成员的经典应用场景是统计类对象的创建/销毁个数,因为静态成员变量为所有对象共享,不会随对象销毁而释放:

classA{public:// 构造函数:创建对象,计数+1A(){++_scount;}// 拷贝构造:拷贝对象,计数+1A(constA&t){++_scount;}// 析构函数:销毁对象,计数-1~A(){--_scount;}// 静态成员函数:获取当前对象个数staticintGetACount(){return_scount;}private:staticint_scount;// 类内声明静态成员变量};intA::_scount=0;// 类外初始化,必须写!voidTestA(){cout<<A::GetACount()<<endl;// 输出0,无对象A a1,a2;Aa3(a1);cout<<A::GetACount()<<endl;// 输出3,3个对象}

2.3 静态成员的五大特性

  1. 静态成员存放在静态区,为所有类对象共享,不属于任何单个对象
  2. 静态成员变量类内声明,类外定义,定义时不添加static
  3. 访问方式:类名::静态成员(推荐)或对象.静态成员
  4. 静态成员函数无this指针,因此不能访问任何非静态成员(非静态成员需要this指针指向具体对象)
  5. 静态成员受访问限定符(public/protected/private)限制

验证:sizeof(类名)的结果不包含静态成员变量,因为静态成员不属于对象,仅计算非静态成员的内存大小。

2.4 高频面试问题

问题1:静态成员函数可以调用非静态成员函数吗?

答案:不能

非静态成员函数有一个隐藏的this指针参数(*this),用于指向调用该函数的具体对象;而静态成员函数属于类本身,无this指针,无法确定要访问哪个对象的非静态成员,因此编译器会报错。

问题2:非静态成员函数可以调用静态成员函数吗?

答案:可以

静态成员属于类,为所有对象共享,其访问不依赖于具体对象;非静态成员函数有this指针,调用静态成员函数时,编译器会自动忽略this指针,直接通过类名访问静态成员,无需传递对象。


三、友元:突破封装的特殊机制

封装是C++面向对象的三大特性之一,而友元是一种突破封装的特殊方式,允许类外部的函数/类直接访问类的私有和保护成员。友元虽带来便利,但会增加代码耦合度、破坏封装,因此不宜多用。

友元分为友元函数和友元类两类。

3.1 友元函数

友元函数的使用场景

最典型的场景是重载流插入operator<<和流提取operator>>。因为这两个运算符的左操作数是ostream/istream对象(如cout/cin),而类的成员函数的第一个参数是隐藏的this指针,无法将cout作为左操作数,因此需要将其重载为全局函数,再通过友元让全局函数访问类的私有成员。

友元函数的实现
#include<iostream>usingnamespacestd;classDate{// 声明友元函数:无需加访问限定符,可写在类内任意位置friendostream&operator<<(ostream&_cout,constDate&d);friendistream&operator>>(istream&_cin,Date&d);public:Date(intyear=1900,intmonth=1,intday=1):_year(year),_month(month),_day(day){}private:int_year;int_month;int_day;};// 全局友元函数:直接访问Date的私有成员ostream&operator<<(ostream&_cout,constDate&d){_cout<<d._year<<"-"<<d._month<<"-"<<d._day;return_cout;}istream&operator>>(istream&_cin,Date&d){_cin>>d._year>>d._month>>d._day;return_cin;}intmain(){Date d;cin>>d;// 调用友元函数operator>>cout<<d;// 调用友元函数operator<<return0;}

核心问题:为什么operator<>不能重载为成员函数?

C++中运算符重载的左操作数是函数的第一个参数:

  • 若将operator<<重载为Date的成员函数,其第一个参数是隐藏的this指针(指向Date对象),调用形式为d1 << cout,与实际使用的cout << d1相反,不符合编程习惯
  • 若强行按成员函数重载,会导致使用方式怪异,且无法与标准输入输出流兼容

解决方案:将其重载为全局函数,让cout(ostream对象)作为第一个参数,Date对象作为第二个参数;再通过友元让全局函数访问Date的私有成员。

友元函数的特性
  1. 友元函数是全局普通函数,不属于任何类
  2. 可直接访问类的私有/保护成员,无需通过对象/类名
  3. 不能用const修饰(const修饰的是成员函数的this指针,友元函数无this)
  4. 类内声明时不受访问限定符限制,可写在public/private/protected任意位置
  5. 一个函数可作为多个类的友元函数

3.2 友元类

友元类的所有成员函数,都可以作为另一个类的友元函数,直接访问其私有/保护成员。

友元类的实现
classTime{// 声明Date为Time的友元类:Date的所有成员函数可访问Time的私有成员friendclassDate;public:Time(inthour=0,intminute=0,intsecond=0):_hour(hour),_minute(minute),_second(second){}private:int_hour;int_minute;int_second;};classDate{public:Date(intyear=1900,intmonth=1,intday=1):_year(year),_month(month),_day(day){}voidSetTimeOfDate(inthour,intminute,intsecond){// 直接访问Time的私有成员_t._hour=hour;_t._minute=minute;_t._second=second;}private:int_year;int_month;int_day;Time _t;};
友元类的三大核心特性
  1. 单向性:友元关系是单向的,不具有交换性。比如上述Date是Time的友元,Date可访问Time的私有成员,但Time不能访问Date的私有成员
  2. 不可传递性:若C是B的友元,B是A的友元,不能推出C是A的友元
  3. 不可继承性:友元关系无法被继承,子类不能继承父类的友元关系

四、内部类:类中的独立类

内部类是指定义在另一个类内部的类,它是一个完全独立的类,不属于外部类,外部类也无法直接访问内部类的成员。但核心特性是:内部类天生是外部类的友元类。

4.1 内部类的核心特性

  1. 内部类可定义在外部类的public/protected/private任意位置
  2. 内部类可直接访问外部类的static静态成员,无需外部类的对象或类名
  3. 内部类访问外部类的非静态成员,需要通过外部类的对象参数
  4. sizeof(外部类)的结果与内部类无关,因为内部类是独立的类

4.2 内部类的代码实现

classA{private:staticintk;// 静态成员inth;// 非静态成员public:// 内部类B,天生是A的友元类classB{public:voidfoo(constA&a){cout<<k<<endl;// 直接访问外部类的static成员cout<<a.h<<endl;// 通过外部类对象访问非静态成员}};};intA::k=10;// 外部类静态成员类外初始化intmain(){// 访问内部类:外部类名::内部类名A::B b;b.foo(A());// 传入外部类匿名对象return0;}

五、匿名对象:无名字的临时对象

匿名对象是指创建时不指定对象名的临时对象,其核心特性是生命周期仅在当前行,执行完当前行后会立即调用析构函数销毁,适合临时使用的场景。

5.1 匿名对象的定义和使用

classA{public:A(inta=0):_a(a){cout<<"A(int a)"<<endl;}~A(){cout<<"~A()"<<endl;}private:int_a;};classSolution{public:intSum_Solution(intn){returnn*(n+1)/2;}};intmain(){Aaa1(1);// 普通对象A();// 匿名对象:当前行执行完立即析构Aaa2(2);// 普通对象// 匿名对象的经典使用:调用类的成员函数,无需定义命名对象cout<<Solution().Sum_Solution(10)<<endl;return0;}

5.2 注意点

不能用A aa1()的方式定义对象,编译器会将其识别为函数声明(返回值为A,无参数的函数),而非对象定义。


六、编译器优化:拷贝对象的性能提升

在C++中,传参和传返回值的过程中,编译器会对对象的拷贝进行优化,减少不必要的拷贝构造,提升程序性能。这是编译器的特性,不同编译器的优化策略基本一致。

classA{public:A(inta=0):_a(a){cout<<"A(int a)"<<endl;}A(constA&aa):_a(aa._a){cout<<"A(const A& aa)"<<endl;}A&operator=(constA&aa){cout<<"A& operator=(const A& aa)"<<endl;if(this!=&aa)_a=aa._a;return*this;}~A(){cout<<"~A()"<<endl;}private:int_a;};voidf1(A aa){}// 传值传参Af2(){A aa;returnaa;}// 传值返回

6.1 核心优化场景

场景1:隐式类型转换优化

f1(1);// 1构造A对象+拷贝构造给f1的形参 → 优化为直接构造形参

场景2:单个表达式中的优化

f1(A(2));// 构造匿名对象+拷贝构造给形参 → 优化为直接构造形参

场景3:返回值优化

A aa2=f2();// f2返回值拷贝构造+赋值给aa2 → 优化为一次拷贝构造

场景4:无法优化的场景

A aa1;aa1=f2();// f2返回值拷贝构造临时对象+赋值重载给aa1 → 无优化

七、再次理解封装:从现实到计算机的映射

C++的类和对象本质是对现实世界实体的抽象和建模,而封装是实现这种建模的核心手段,封装将实体的属性(成员变量)和行为(成员函数)包裹在一起,同时通过访问限定符控制外部对成员的访问,实现了数据隐藏和代码复用。

从现实世界到计算机世界的映射过程:

  1. 抽象:对现实实体(如洗衣机)进行分析,提取其属性(容量、品牌)和行为(洗衣、脱水)
  2. 描述:用C++类将抽象后的属性和行为进行定义,形成自定义类型
  3. 实例化:通过类创建具体的对象,计算机才能够识别并处理该实体
  4. 模拟:通过调用对象的成员函数,模拟现实实体的行为

简单来说:类是对现实实体的抽象描述,对象是抽象描述的具体实例,而封装则保证了抽象描述的完整性和安全性。


总结

本文详细讲解了C++类和对象下篇的七大核心知识点,其中初始化列表、static成员、友元是面试和开发中的高频考点,需要重点掌握:

知识点核心要点
初始化列表是类成员真正的初始化方式,三类成员(引用/const/无默认构造的自定义类型)必须在其中初始化,且初始化顺序与类内声明一致
static成员属于类,为所有对象共享,静态成员函数无this指针,不能访问非静态成员
友元是突破封装的特殊方式,分为友元函数和友元类,使用时需权衡耦合度
内部类是独立的类,天生是外部类的友元,sizeof(外部类)与内部类无关
匿名对象生命周期仅一行,适合临时调用成员函数的场景
编译器优化会对拷贝对象进行优化,减少不必要的拷贝构造,提升性能
封装是C++面向对象的核心,实现了从现实实体到计算机对象的映射

这些知识点是C++面向对象编程的基础,后续的继承、多态等特性都基于此,建议结合代码案例反复练习,加深理解。


写在最后

C++面向对象编程的世界博大精深,每一个知识点背后都有其设计的历史原因和实际应用场景。希望本文能帮助你建立起对类与对象进阶特性的系统认知。如果你在学习过程中有任何问题,欢迎在评论区留言讨论。

下一篇文章我们将探讨C++的继承特性,敬请期待!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 23:23:44

从零开始使用轻量级SQL解析器:纯JS实现的前端集成方案

从零开始使用轻量级SQL解析器&#xff1a;纯JS实现的前端集成方案 【免费下载链接】sql-parser A SQL parser written in pure JS 项目地址: https://gitcode.com/gh_mirrors/sqlpar/sql-parser 在现代Web应用开发中&#xff0c;SQL解析技术正成为数据可视化、查询构建器…

作者头像 李华
网站建设 2026/10/12 23:17:35

AI伦理审查中的“反人类代码”误判机制及测试应对策略

第一章 概念解构&#xff1a;AIHR系统的判定逻辑漏洞graph LR A[AIHR扫描维度] --> B[代码暴力性特征] A --> C[决策黑箱化指数] A --> D[社会偏见关联度] B --> E[死循环强制控制] C --> F[不可解释决策树] D --> G[性别/种族敏感词]测试视角的误判根源特征…

作者头像 李华
网站建设 2026/10/13 1:08:23

STM32CubeMX新手必看:解决SWD/JTAG Communication Failure的3种实用方法

STM32CubeMX调试接口配置实战&#xff1a;从“Communication Failure”到稳定下载的深度解析 刚拿到一块崭新的STM32开发板&#xff0c;满心欢喜地打开STM32CubeMX配置好工程&#xff0c;在Keil里点击下载&#xff0c;却弹出一个冰冷的“SWD/JTAG Communication Failure”对话框…

作者头像 李华
网站建设 2026/10/13 1:19:34

Windows10下YOLO深度学习环境配置全攻略:从CUDA到torch

1. 环境准备&#xff1a;理清思路&#xff0c;打好地基 想在Windows 10上玩转YOLO&#xff0c;第一步不是急着下载安装包&#xff0c;而是要把思路理清楚。很多新手朋友一上来就照着教程一通操作&#xff0c;结果不是CUDA版本对不上&#xff0c;就是torch装不上GPU版本&#xf…

作者头像 李华
网站建设 2026/10/13 4:57:06

GIS开发薪资和哪些因素有关?学历?工作经验?行业?

通常来说&#xff0c;GIS开发薪资和很多方面的因素有关&#xff0c;公司所在城市、行业&#xff1b;面试者的学历、工作经验、项目经验等。尤其是首次找工作&#xff0c;很多人反馈对GIS开发薪资最大的一个影响因素就是工作经验和项目经验。今天我们来看下&#xff0c;不同因素…

作者头像 李华