☰
C++类和对象(上):从面向对象基础到核心成员函数
2026/10/7 8:48:51 网站建设 项目流程

在C++中,类和对象是面向对象编程(OOP)的基石。本文将从面向对象思想引入、类的定义与封装,一直深入到构造函数、析构函数、拷贝构造以及运算符重载等核心机制。


一、 面向过程 vs 面向对象

  • C语言(面向过程):关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。(例如:洗衣服 -> 拿盆子 -> 放水 -> 放衣服 -> 放洗衣粉 -> 揉搓 -> 拧干 -> 晾晒)

  • C++(面向对象):关注的是对象,将一件事拆分成不同的对象,靠对象之间的交互完成。(例如:人、衣服、洗衣机、洗衣粉四个对象,人只需将衣服放进洗衣机,启动按钮即可,不需关注洗衣机内部如何运转)。


二、 类的引入与定义

C语言中,结构体只能定义变量;而在C++中,结构体(struct)和类(class)不仅可以定义变量,还可以定义函数。

1. 类的两种定义方式

方式一:声明和定义全部放在类体中

这种方式比较直接,适合代码量较少的情况。需要注意的是,如果成员函数在类体内部定义,编译器可能会将其视为内联函数(inline)来处理。

// 全部写在类里面 class Person { public: // 成员函数在类内部定义 void showInfo() { cout << _name << "-" << _sex << "-" << _age << endl; } private: char* _name; // 姓名 char* _sex; // 性别 int _age; // 年龄 };

方式二:声明放在 .h 文件,定义放在 .cpp 文件

在实际工程开发中,更推荐使用这种方式。这能让代码结构更清晰,也利于分离编译。
注意:在.cpp文件中定义成员函数时,函数名前必须加上类名::,以指明该函数属于哪个类域。

---------- person.h (头文件:类声明) ----------
class Person { public: // 成员函数声明 void showInfo(); private: char* _name; // 姓名 char* _sex; // 性别 int _age; // 年龄 };
---------- person.cpp(源文件:类定义)----------
#include "person.h" // 成员函数定义,必须加 Person:: void Person::showInfo() { cout << _name << "-" << _sex << "-" << _age << endl; }

💡建议:日常练习时为了方便,可以使用方式一。但在实际工作项目中,要使用方式二,做到声明与实现分离。

2.struct和class的区别

在C++中,struct和class都可以用来定义类,区别在于默认的访问权限:

  • struct的默认访问权限是public(公有)。

  • class的默认访问权限是private(私有)。


三、 类的访问限定符及封装

1. 访问限定符

限定符含义特点
public(公有)类外可以直接访问暴露给用户的接口
protected(保护)类外不能直接访问继承体系中会用到
private(私有)类外不能直接访问隐藏内部实现细节

2. 封装

封装的核心目的:隐藏实现细节,只暴露操作接口。
将数据和操作数据的方法进行有机结合,通过访问权限来隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。就像电脑一样,用户只需要知道如何开机、使用键盘鼠标,而不需要关心CPU和显卡的内部工作原理。


四、 类的作用域、实例化与对象大小

1. 类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。

2. 类的实例化

  • 类:是设计图,只定义有哪些东西,不占用实际物理内存。

  • 对象:是房子,根据设计图建造出来的实体,占用物理内存。

  • 注意:不能直接访问类中的成员变量,必须通过实例化后的对象来访问。

3. 对象大小的计算

核心规则:对象中只存储成员变量,不存储成员函数。成员函数存放在代码段,属于同类所有对象共享。

内存对齐规则(同结构体):

  1. 第一个成员在与结构体偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍地址处。对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(VS中默认8,Linux中默认4)。

  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

  4. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

特殊情况:空类

class A {}; // 空类 int main() { cout << sizeof(A) << endl; // 输出 1 }
  • 为什么空类是1?因为编译器需要一个独一无二的地址来标识这个类的不同对象,所以会分配1个字节作为占位符。


五、 类成员函数的 this 指针

1. this 指针的本质

  • this指针是隐藏的成员函数形参,指向当前调用该函数的对象。在 x86 平台上,this通常通过ecx寄存器传递;在 x64 平台上,则作为第一个参数通过rcx寄存器传递。

  • 在类中定义成员函数时,编译器会自动加上this指针。例如void Print()实际上是void Print(Date* const this)。

2. 经典面试题:空指针调用成员函数

class A { public: void Print() { cout << "Print()" << endl; } void PrintA() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); // 正常运行!因为Print()函数没有访问成员变量,不需要解引用this p->PrintA(); // 崩溃!因为PrintA()访问了_a,本质是 this->_a,对空指针解引用导致崩溃 return 0; }

3. const 成员函数

将const修饰的成员函数称之为const成员函数。const修饰类成员函数,实际修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

  • 权限问题:const对象只能调用const成员函数;非const对象既可以调用const成员函数,也可以调用非const成员函数。

  • 权限放大:权限可以缩小(非const对象调用const函数),但不能放大(const对象调用非const函数)。


六、 类和对象(中):六大默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。

1. 构造函数 (Constructor)

主要任务:初始化对象,而不是开空间创建对象。

特性说明
函数名与类名相同
返回值无返回值(不需要写void)
调用时机对象实例化时由编译器自动调用
重载可以重载
默认生成如果类中没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数。一旦用户显式定义,编译器将不再生成。

C++11 补丁:针对编译器默认生成的构造函数对内置类型不做处理(是随机值)的问题,C++11允许在成员声明时给缺省值。

class Date { private: int _year = 1; // 声明时给缺省值 int _month = 1; int _day = 1; };

2. 析构函数 (Destructor)

主要任务:完成对象中资源的清理工作,而不是销毁对象本身(对象销毁由编译器完成)。

特性说明
函数名类名前加~
参数与返回值无参数,无返回值
重载一个类只能有一个析构函数,不能重载
调用时机对象生命周期结束时,C++编译系统自动调用
默认生成若未显式定义,系统会自动生成默认的析构函数

什么时候需要自己写析构函数?

  1. 有动态申请资源(malloc/new),需要显式释放资源。

  2. 没有动态申请资源,不需要写析构。

  3. 需要释放资源的成员都是自定义类型,不需要写析构(会自动调用成员对象的析构)。

3. 拷贝构造函数 (Copy Constructor)

主要任务:用已存在的对象创建新对象。

特性说明
函数名与类名相同
参数只有一个参数,且必须是类类型对象的引用(一般用const修饰)。使用传值方式会引发无穷递归调用。
调用场景使用已存在对象创建新对象;函数参数类型为类类型对象;函数返回值类型为类类型对象。

浅拷贝 vs 深拷贝:

  • 浅拷贝(默认生成):内置类型成员完成值拷贝,自定义类型成员会调用他的拷贝构造。对于指针成员,只拷贝指针地址,会导致两个对象指向同一块内存(二次析构,互相影响)。

  • 深拷贝(必须自己实现):对于Stack这种有动态资源的类,必须自己实现拷贝构造,重新分配内存并拷贝数据。

// 深拷贝示例 Stack(const Stack& st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (nullptr == _a) return; memcpy(_a, st._a, sizeof(int) * st._top); _top = st._top; _capacity = st._capacity; }

4. 赋值运算符重载 (operator=)

主要任务:两个已经存在的对象之间进行复制拷贝。

  • Date d4 = d2;是拷贝构造(初始化)。

  • d4 = d1;是赋值运算符重载(已经存在了)。

格式与要点:

Date& operator=(const Date& d) { if (this != &d) { // 1. 检测是否自己给自己赋值 _year = d._year; _month = d._month; _day = d._day; } return *this; // 2. 返回*this,支持连续赋值 (d5 = d4 = d1) }
  • 参数:const T&,传递引用提高传参效率。

  • 返回值:T&,返回引用提高返回效率,有返回值目的是为了支持连续赋值。

  • 注意:赋值运算符只能重载成类的成员函数,不能重载成全局函数。

  • 默认生成的赋值重载行为跟拷贝构造一样(浅拷贝),有动态资源的类需要自己实现深拷贝。

5. 取地址及 const 取地址操作符重载

这两个默认成员函数一般不用重新定义,编译器默认会生成。

Date* operator&() { return this; } const Date* operator&() const { return this; }

七、 运算符重载进阶

1. 运算符重载基本规则

  • 不能通过连接其他符号来创建新的操作符(如operator@)。

  • 重载操作符必须有一个类类型参数。

  • 用于内置类型的运算符,其含义不能改变。

  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。

  • 不能重载的运算符(5个):.*、::、sizeof、?:、.。

  • 补充:operator=、operator()、operator[]、operator->只能重载成类的成员函数,不能重载成全局函数。

2. 前置++ 与 后置++ 的重载

为了区分前置和后置,后置++重载时增加了一个int参数,但这个参数并不接收具体的值,仅仅是占位,跟前置++构成重载。

// 前置++ Date& operator++() { *this += 1; return *this; } // 后置++ Date operator++(int) { Date tmp = *this; *this += 1; return tmp; }
  • 前置++:返回++之后的对象(引用)。

  • 后置++:返回++之前的对象(值)。

3. 流插入<<与流提取>>的重载

cout << d1;会转换为operator<<(cout, d1);


不能写成成员函数!因为成员函数的第一个参数永远是this指针,即d1.operator<<(cout),这不符合我们的使用习惯。
必须写成全局函数,并在类中声明为友元(friend)。

class Date { // 友元函数声明 friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); }; // 全局定义 ostream& operator<<(ostream& out, const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日"; return out; // 支持连续输出 }

八、 总结

学习类和对象,核心在于理解封装、内存分布、以及编译器默认生成的六大函数的行为。

  1. 基础:区分面向过程和面向对象,掌握类的作用域、实例化与大小计算(内存对齐)。

  2. 核心工具:熟练使用this指针,理解const成员函数的权限缩小原则。

  3. 生命周期管理:深刻理解构造函数(初始化)、析构函数(清理)、拷贝构造(深/浅拷贝)的调用时机和底层行为。

  4. 运算符重载:掌握赋值运算符、前后置++、流插入提取的重载规则。

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

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

立即咨询