C++ 类与对象(上)笔记(整理与补充)
C++ 类与对象核心笔记(整理与补充)
一、引言:类与对象的核心地位
C++ 作为面向对象编程语言(OOP),其核心思想围绕 “类” 与 “对象” 展开。类是对现实世界中一类事物的抽象描述(模板 / 图纸),对象是类的具体实例(实体)。本笔记系统梳理类与对象的定义、封装、内存模型、访问控制等核心知识点,结合代码示例与细节补充,夯实 OOP 基础。
二、类的基础定义与文件组织规范
2.1 类的构成要素
类是 “成员变量”(描述对象属性 / 状态)与 “成员方法”(描述对象行为 / 操作)的集合,是封装思想的直接体现。
2.2 类的声明与定义的文件拆分原则
为保证代码模块化与可维护性,C++ 中类的声明与定义通常拆分到不同文件:
- 声明(Declaration):放在
.h
(头文件)中,仅告知编译器 “类的结构”(包含哪些成员变量、成员方法的签名),不涉及具体实现。 - 定义(Definition):放在
.cpp
(源文件)中,实现.h
中声明的成员方法(填充函数逻辑)。
补充:头文件保护(防止重复包含)
用户未提及头文件重复包含问题,此处补充:.h
文件需添加 “头 guard” 或#pragma once
,避免因多次包含导致编译错误。示例如下:
// Person.h(头文件,类的声明)
#ifndef PERSON_H // 头 guard 开始(也可使用 #pragma once)
#define PERSON_H#include <string> // 包含成员变量/方法依赖的头文件// 类的声明:仅描述结构,不实现成员函数
class Person {
public:// 成员变量声明(属性:姓名、性别、年龄)std::string _name;std::string _gender;int _age;// 成员方法声明(行为:打印个人信息)void PrintPersonInfo();
};#endif // PERSON_H // 头 guard 结束
// Person.cpp(源文件,类的定义)
#include "Person.h" // 包含对应头文件,关联类声明
#include <iostream> // 依赖的输入输出头文件// 成员方法的类外定义:通过::(作用域解析符)指定所属类
void Person::PrintPersonInfo() {// 访问类的成员变量,实现打印逻辑std::cout << "姓名:" << _name << ",性别:" << _gender << ",年龄:" << _age << std::endl;
}
三、封装思想与访问限定符
3.1 封装的核心作用
封装是 OOP 三大特性(封装、继承、多态)之首,核心实现两大目标:
- 数据与行为的整合:将描述对象的 “数据(成员变量)” 与操作数据的 “行为(成员方法)” 绑定在类内部,使对象成为逻辑完整的独立单元。
- 选择性暴露接口:通过访问控制,隐藏类的内部实现细节(如核心数据),仅对外暴露安全的操作接口(如数据查询 / 修改方法),保证数据安全性与使用规范性。
3.2 三种访问限定符的功能
C++ 通过public
、protected
、private
三个关键字控制类成员的 “对外可见性”,具体功能如下:
访问限定符 | 访问范围 | 核心用途 |
---|---|---|
public | 类内、类外、子类均可访问 | 定义对外暴露的接口(如成员方法、公共常量) |
protected | 类内、子类可访问,类外不可访问 | 定义子类需继承的成员(兼顾隐藏与继承需求) |
private | 仅类内可访问,类外、子类均不可访问 | 定义类的核心隐私数据(如成员变量) |
说明:
- 通常成员变量默认设为
private
(避免类外直接修改,保证数据完整性),通过public
成员方法间接操作(如SetAge(int age)
设置年龄,GetAge()
获取年龄)。 protected
的特殊性需结合 “继承” 理解:子类可访问父类的protected
成员,但外部代码无法访问,后续继承章节会详细展开。
3.3 访问限定符的生效范围
访问限定符的生效规则:从限定符出现的位置开始,直到下一个访问限定符出现或类定义结束为止,期间所有成员均遵循当前限定符的访问规则。
示例代码(含注释):
class Student {
// 第一个访问限定符:public
public:// 以下成员均为public(直到下一个限定符private出现)std::string _studentId; // 学号(对外可访问,如查询学号)void ShowStudentId() { // 打印学号(对外接口)std::cout << "学号:" << _studentId << std::endl;}// 第二个访问限定符:private
private:// 以下成员均为private(直到类定义结束)std::string _name; // 姓名(核心数据,类外不可直接访问)int _score; // 分数(核心数据,类外不可直接修改)// private成员方法:仅类内调用(如校验分数合法性)bool IsScoreValid(int score) {return score >= 0 && score <= 100;}// 第三个访问限定符:public(若后续有成员,需重新指定)
public:// 对外接口:设置分数(通过private方法校验,保证数据合法)void SetScore(int score) {if (IsScoreValid(score)) {_score = score;} else {std::cout << "分数非法!" << std::endl;}}
};
四、C 与 C++ 的 struct 差异及 C++ 中 class 与 struct 的区别
4.1 C 语言的 struct 特性
C 语言的 struct 仅作为 “纯数据容器”,不支持封装行为,核心特性如下:
- 仅能包含成员变量(如 int、指针等),不能包含函数。
- 使用自身类型的指针时,必须显式添加
struct
关键字(C 中 struct 名不能单独作为类型名)。
示例代码(C 语言 struct,含注释):
// C语言的struct:仅存储数据,无成员函数
struct ListNode_C {int _val; // 数据域:节点值struct ListNode_C* _next; // 指针域:下一个节点地址(必须加struct)struct ListNode_C* _prev; // 指针域:上一个节点地址(必须加struct)
};// C语言中使用struct:需通过struct ListNode_C定义变量
void C_Struct_Example() {struct ListNode_C node1; // 定义struct变量,必须加structnode1._val = 10; // 直接访问成员变量(C中struct成员默认公开)node1._next = NULL;
}
4.2 C++ 的 struct 特性
C++ 的 struct 兼容 C 语言用法,同时具备 “类(class)的部分能力”,核心特性如下:
- 兼容 C:可作为纯数据容器,仅包含成员变量。
- 扩展功能:支持包含成员函数(实现数据与行为的封装),可直接用 struct 名作为类型名(无需加 struct)。
示例代码(C++ 的 struct,含注释):
// C++的struct:支持成员变量+成员函数,实现封装
struct ListNode_CPP {int _val; // 数据域:节点值ListNode_CPP* _next; // 指针域:直接用struct名作为类型名(无需加struct)ListNode_CPP* _prev; // 指针域:无需加struct// 成员函数:初始化节点(封装操作数据的行为)void InitNode(int val) {_val = val; // 成员函数内部访问成员变量_next = NULL;_prev = NULL;}// 成员函数:打印节点值(对外暴露的接口)void PrintVal() {std::cout << "节点值:" << _val << std::endl;}
};// C++中使用struct:直接用struct名定义变量,无需加struct
void CPP_Struct_Example() {ListNode_CPP node2; // 无需加struct,直接作为类型名node2.InitNode(20); // 调用成员函数初始化node2.PrintVal(); // 调用成员函数打印(输出:节点值:20)
}
4.3 C++ 中 class 与 struct 的核心差异
C++ 中 class 与 struct 均能定义 “类”,唯一核心差异是默认访问权限,其他特性(如继承、多态)完全一致:
关键字 | 默认访问权限 | 成员变量 / 函数默认可见性 | 适用场景 |
---|---|---|---|
class | private | 类外不可直接访问默认成员 | 定义需严格隐藏内部细节的类(如业务对象) |
struct | public | 类外可直接访问默认成员 | 定义简单数据结构(如链表节点)或兼容 C 代码 |
补充:
- 无论使用
class
还是struct
,均可显式添加public
/protected
/private
来控制访问权限,默认权限仅影响未指定限定符的成员。 - 示例:用 struct 定义类并显式指定权限,与 class 行为一致:
struct Person_Struct { private: // 显式指定private,成员变量默认不可访问std::string _name; public: // 显式指定public,成员方法对外可见void SetName(const std::string& name) {_name = name;} };
五、声明与定义的区别(含类成员函数)
5.1 声明(Declaration)的本质
声明是 “向编译器承诺某个实体的存在”,仅告知实体的 “名称与签名”,不分配内存或填充实现逻辑。核心特点:
- 不包含具体实现(如函数无函数体,变量无初始化)。
- 可多次声明(如函数声明可在多个文件中出现,只要签名一致)。
示例(函数声明与变量声明):
// 函数声明:仅告知函数名、参数类型、返回值类型,无实现
void add(int a, int b); // 声明“存在一个add函数,接收两个int,返回void”// 变量声明:用extern告知“变量在其他文件定义”,不分配内存
extern int global_num; // 声明“global_num是int类型,在其他文件定义”
5.2 定义(Definition)的本质
定义是 “将声明的承诺落地”,分配内存(变量)或填充实现逻辑(函数),核心特点:
- 包含具体实现(函数有函数体,变量有内存分配,可初始化)。
- 同一作用域内仅能定义一次(避免重复分配资源,即 “单定义规则 ODR”)。
示例(函数定义与变量定义):
// 函数定义:实现add函数的逻辑,分配代码区内存
void add(int a, int b) {std::cout << "两数之和:" << a + b << std::endl; // 具体实现
}// 变量定义:分配内存(栈/全局区),可初始化
int global_num = 10; // 定义global_num,分配内存并初始化为10
5.3 类成员函数的两种定义方式
类成员函数的定义分为 “类内定义” 与 “类外定义”,均需先在类内声明(或直接在类内定义即包含声明)。
方式 1:类内定义(声明 + 定义合一)
- 直接在类的 {} 内部编写成员函数的实现,此时函数默认被编译器视为
inline
(内联函数,适合简单逻辑,避免函数调用开销)。 - 示例(Person 类内定义成员函数):
class Person { public:std::string _name;int _age;// 类内定义成员函数:声明与定义合一,默认inlinevoid ShowInfo() {// 直接访问类内成员变量,实现打印逻辑std::cout << "姓名:" << _name << ",年龄:" << _age << std::endl;} };
方式 2:类外定义(声明与定义分离)
- 类内仅声明成员函数(告知函数签名),类外通过
::
(作用域解析符)指定函数所属类,补充实现逻辑。 - 必须包含对应类的头文件,确保编译器能关联声明与定义。
- 示例(Person 类外定义成员函数,与 2.2 节文件组织一致):
// Person.h(类内声明) class Person { public:std::string _name;void PrintPersonInfo(); // 仅声明,无实现 };// Person.cpp(类外定义) #include "Person.h" #include <iostream>// 类外定义:通过Person::指定PrintPersonInfo属于Person类 void Person::PrintPersonInfo() {std::cout << "姓名:" << _name << std::endl; // 实现逻辑 }
补充:成员函数声明与定义的一致性要求
- 函数签名(参数类型、参数个数、参数顺序、const 修饰(若有))必须完全一致,否则编译器视为 “重载函数”,若未找到匹配声明则报错。
- 示例:若类内声明为
void SetAge(int age)
,类外定义为void Person::SetAge(double age)
,则编译报错(签名不匹配)。
六、类与对象的关系(抽象与实例)
6.1 类与对象的本质区别
类与对象是 “抽象模板” 与 “具体实例” 的关系,类比现实世界:
- 类(Class):抽象描述一类事物的 “共性”(属性 + 行为),无具体实体,不占用内存。例如 “汽车类” 描述汽车的属性(品牌、颜色)和行为(启动、刹车)。
- 对象(Object):类的 “实例化产物”,具备类描述的所有属性与行为,占用实际内存。例如 “张三的红色特斯拉 Model 3” 是 “汽车类” 的一个对象。
6.2 代码层面的类与对象实现
步骤 1:定义类(抽象模板)
#include <string>
#include <iostream>// 定义Person类(抽象模板):描述“人”的共性
class Person {
public:// 成员变量(属性):描述对象的状态std::string name; // 姓名int age; // 年龄// 成员函数(行为):描述对象的操作void show() {// 打印对象的属性,所有Person对象共用此逻辑std::cout << name << " " << age << std::endl;}
};
步骤 2:创建对象(实例化)
通过 “类名 + 对象名” 在栈上创建对象,或通过new
在堆上创建对象(用户未提及堆上创建,补充此方式)。
int main() {// 方式1:栈上创建对象(p1):自动分配/释放内存,作用域结束后销毁Person p1; // p1是Person类的对象(实例)p1.name = "张三"; // 给p1的成员变量赋值(每个对象的属性独立)p1.age = 20;p1.show(); // 调用p1的成员函数(执行show逻辑,输出:张三 20)// 方式2:堆上创建对象(p2):手动分配/释放内存,需用delete销毁Person* p2 = new Person(); // 用new创建对象,返回指向对象的指针p2->name = "李四"; // 堆对象通过->访问成员(指针访问语法)p2->age = 25;p2->show(); // 调用堆对象的成员函数(输出:李四 25)delete p2; // 手动释放堆内存,避免内存泄漏p2 = NULL; // 指针置空,避免野指针return 0;
}
6.3 核心特性:成员函数的共用性
- 成员变量:每个对象独立存储,占用对象的内存(如 p1 的 name 和 p2 的 name 存储在不同地址,值可不同)。
- 成员函数:所有同类型对象共用一份代码,存储在代码区(而非对象内存中),避免重复存储导致的内存浪费。
- 补充:通过
sizeof(对象)
可验证:对象大小仅包含成员变量的大小(含内存对齐填充),与成员函数无关。例如sizeof(Person)
=sizeof(std::string)
+sizeof(int)
(需考虑内存对齐)。
七、对象的内存模型(含内存对齐)
7.1 对象的内存构成
对象的内存仅存储非静态成员变量,成员函数、静态成员(静态变量、静态函数)均存储在全局代码区 / 数据区,不占用对象内存。核心原因:
- 成员变量是 “对象专属”:每个对象的成员变量值不同(如学生 A 和 B 的分数不同),需独立存储。
- 成员函数是 “类共用”:所有对象的成员函数逻辑一致(如计算分数的逻辑),共用一份代码即可。
补充:空类的内存大小
用户未提及空类(无任何成员变量 / 成员函数的类),补充:空类的大小为1 字节(编译器为其分配 1 字节内存,用于区分不同空类对象的地址,避免所有空类对象地址相同)。
// 空类示例
class EmptyClass {};int main() {std::cout << sizeof(EmptyClass) << std::endl; // 输出:1(而非0)return 0;
}
7.2 内存对齐:编译器的地址调整策略
7.2.1 内存对齐的定义
内存对齐是编译器对变量存储地址的强制调整规则:变量的起始地址必须是其 “自身大小” 的整数倍(如 int(4 字节)的起始地址必须是 4 的倍数,double(8 字节)的起始地址必须是 8 的倍数)。
7.2.2 内存对齐的核心原因
- 提升硬件访问效率:CPU 读取内存时按 “固定块大小”(如 4 字节、8 字节)读取。若变量对齐,CPU 仅需 1 次读取即可获取数据;若未对齐,需 2 次读取 + 数据拼接,效率显著降低。
- 兼容硬件限制:部分嵌入式芯片、旧 CPU 不支持 “非对齐地址访问”,强制非对齐会导致程序崩溃。
7.2.3 类对象内存对齐的计算步骤
类对象的大小 = 所有非静态成员变量大小之和 + 填充字节(padding)(编译器自动插入,用于满足对齐规则),计算分两步:
步骤 1:单个成员变量的 “偏移量对齐”
每个成员变量的 “偏移量”(相对于对象起始地址的字节数)必须是其自身大小的整数倍,若不满足则插入填充字节。
步骤 2:整个类对象的 “总大小对齐”
类对象的总大小必须是 “所有成员变量中最大大小” 的整数倍,若不满足则在末尾插入填充字节。
示例:类 A 的内存对齐计算(含注释)
// 定义类A,包含char、int、double三种成员变量
class A {
public:char c; // 大小1字节,自身大小=1int i; // 大小4字节,自身大小=4double d; // 大小8字节,自身大小=8
};int main() {// 计算类A对象的大小,按内存对齐规则分析:// 步骤1:单个成员偏移量对齐// - c的偏移量:0(0是1的整数倍,无需填充),占用0~0字节// - i的自身大小=4,偏移量需是4的整数倍:当前偏移量为1(c占1字节),需插入3字节填充(1→4),i占用4~7字节// - d的自身大小=8,偏移量需是8的整数倍:当前偏移量为8(i占4字节,4+4=8),无需填充,d占用8~15字节// 步骤2:总大小对齐// 所有成员最大大小=8(double),当前总大小=1(c)+3(填充)+4(i)+8(d)=16字节,16是8的整数倍,无需末尾填充std::cout << sizeof(A) << std::endl; // 输出:16(而非1+4+8=13)return 0;
}
补充:内存对齐的编译器控制
用户未提及如何调整对齐规则,补充:可通过编译器选项或预处理指令修改默认对齐系数(如默认对齐系数为 8,可改为 4):
- GCC 编译器:通过
-fpack-struct=n
选项(n 为对齐系数,如g++ -fpack-struct=4 test.cpp
)。 - VS 编译器:通过
#pragma pack(n)
指令(如#pragma pack(4)
设置对齐系数为 4,#pragma pack()
恢复默认)。
示例(修改对齐系数为 4):
#pragma pack(4) // 设置对齐系数为4
class A {
public:char c; // 1字节double d; // 8字节(自身大小8,但对齐系数为4,偏移量需是4的整数倍)
};
#pragma pack() // 恢复默认对齐int main() {// 分析:// c偏移量0(1的倍数),占用0~0// d的偏移量需是4的倍数:当前偏移量1,插入3字节填充(1→4),d占用4~11// 总大小=1+3+8=12,最大成员大小8,但对齐系数4,12是4的倍数,总大小12std::cout << sizeof(A) << std::endl; // 输出:12return 0;
}
八、this 指针:成员函数的隐式参数
8.1 this 指针的本质
this 指针是编译器为非静态成员函数隐式添加的 “隐藏参数”,其类型为 “类名* const
”(指向当前类对象的常量指针,不能修改指向,但可修改指针指向的对象内容),核心作用:标识 “当前调用成员函数的对象”,使成员函数能精准访问当前对象的成员变量。
补充:this 指针的 const 属性
- 普通成员函数的 this 指针:
类名* const this
(指向不可改,对象内容可改)。 - const 成员函数的 this 指针:
const 类名* const this
(指向不可改,对象内容也不可改,用于保证函数不修改对象状态),用户未提及,补充示例:class Person { public:std::string _name;int _age;// const成员函数:this指针为const Person* const,不能修改对象成员void ShowInfo() const {std::cout << _name << " " << _age << std::endl; // 仅访问,不修改,合法// _age = 30; // 错误:const成员函数不能修改对象成员} };
8.2 this 指针的工作机制(编译器隐式处理)
程序员无需显式声明或传递 this 指针,编译器在编译阶段自动完成以下操作:
步骤 1:成员函数参数隐式添加 this 指针
程序员编写的成员函数:
void Date::Init(int year, int month, int day) {_year = year;_month = month;_day = day;
}
编译器处理后(隐式添加 this 参数):
// 编译器自动添加this指针参数,程序员不可见
void Date::Init(Date* this, int year, int month, int day) {this->_year = year; // 隐式通过this访问成员变量this->_month = month;this->_day = day;
}
步骤 2:调用成员函数时隐式传递对象地址
程序员编写的调用代码:
Date d1;
d1.Init(2024, 5, 20); // 调用d1的Init函数
编译器处理后(隐式传递 d1 的地址给 this):
Date d1;
// 隐式将&d1作为this参数传递
d1.Init(&d1, 2024, 5, 20);
步骤 3:成员访问隐式转换为 this-> 成员
程序员编写的成员访问:
void Date::Print() {std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
编译器处理后(隐式添加 this->):
void Date::Print(Date* this) {// 隐式通过this指针访问当前对象的成员变量std::cout << this->_year << "-" << this->_month << "-" << this->_day << std::endl;
}
8.3 this 指针的存储位置
- 理论上:this 指针作为函数形参,应存储在栈区(函数形参默认在栈上分配)。
- 实际实现:为提升效率,主流编译器(如 VS)通过寄存器(ecx 寄存器) 传递 this 指针,而非栈区(减少栈操作开销)。
- 注意:this 指针不能被用户显式修改(因类型是
类名* const
),也不能在成员函数中显式声明 this 指针。
九、空指针调用成员函数的行为分析
空指针(NULL)指向 “无有效内存的地址”,调用成员函数时的行为取决于函数是否 “访问成员变量”(即是否依赖 this 指针)。
9.1 示例代码(含详细注释)
#include <iostream>
using namespace std;class A {
public:// 成员函数1:访问成员变量(依赖this指针)void PrintA() {// 编译器会转换为:cout << this->_a << endl;// 若this为NULL,解引用NULL会导致程序崩溃cout << _a << endl;}// 成员函数2:不访问成员变量(不依赖this指针)void Show() {// 仅执行输出逻辑,无需访问对象成员,不依赖this指针cout << "Show()" << endl;}private:int _a; // 成员变量:PrintA函数依赖此变量
};int main() {A* p = NULL; // 定义空指针p,不指向任何有效对象// 情况1:p->PrintA(); —— 程序崩溃// 原因:PrintA访问成员变量_a,需通过this指针(this = p = NULL)// 解引用NULL指针(this->_a)是未定义行为,导致程序崩溃// p->PrintA(); // 取消注释后运行,程序崩溃// 情况2:p->Show(); —— 正常运行// 原因:Show函数不访问任何成员变量,无需依赖this指针// 成员函数存储在代码区,调用时仅需找到函数地址,无需访问对象内存p->Show(); // 输出:Show(),正常运行return 0;
}
9.2 核心结论与补充注意事项
- 崩溃条件:若成员函数访问成员变量(依赖 this 指针),空指针调用会因 “解引用 NULL 指针” 导致程序崩溃(未定义行为)。
- 正常条件:若成员函数不访问成员变量(不依赖 this 指针),空指针调用可能正常运行(但仍不推荐)。
- 补充注意事项:
- 空指针调用成员函数属于 “未定义行为”(C++ 标准未规定其行为),即使当前正常运行,也可能因编译器版本、平台变化导致崩溃。
- 实际开发中应避免空指针调用成员函数,需先判断指针是否非 NULL(如
if (p != NULL) p->Show();
)。
十、补充:类的静态成员
用户未提及静态成员,静态成员是类的重要组成部分,补充如下:
10.1 静态成员的定义
静态成员(静态成员变量、静态成员函数)属于 “类本身”,而非单个对象,所有对象共用静态成员,存储在全局数据区(而非对象内存)。
10.2 静态成员变量
特性:
- 需在类内声明(加
static
关键字),类外定义(分配内存,仅一次)。 - 所有对象共用一份,修改一个对象的静态成员变量,所有对象均可见。
- 可通过 “类名::静态变量名” 或 “对象。静态变量名” 访问(推荐前者,明确属于类)。
- 需在类内声明(加
示例代码:
class Student { public:std::string _name;static int _studentCount; // 类内声明静态成员变量:学生总数 };// 类外定义静态成员变量:必须加类名::,分配全局数据区内存 int Student::_studentCount = 0;int main() {Student s1;Student s2;Student::_studentCount++; // 通过类名访问,学生总数+1(变为1)s2._studentCount++; // 通过对象访问,学生总数+1(变为2)// 所有对象共用静态变量,输出均为2cout << Student::_studentCount << endl; // 输出:2cout << s1._studentCount << endl; // 输出:2return 0; }
10.3 静态成员函数
特性:
- 类内声明(加
static
关键字),类外定义(无需加static
)。 - 无 this 指针(因属于类,不依赖对象),故不能访问非静态成员变量 / 函数(需通过对象指针 / 引用访问)。
- 可通过 “类名::静态函数名” 或 “对象。静态函数名” 访问。
- 类内声明(加
示例代码:
class Student { public:std::string _name;static int _studentCount;// 静态成员函数:无this指针,不能访问非静态成员static void ShowStudentCount() {cout << "学生总数:" << _studentCount << endl; // 可访问静态成员// cout << _name << endl; // 错误:不能访问非静态成员(无this指针)} };int Student::_studentCount = 0;int main() {Student::_studentCount = 3;Student::ShowStudentCount(); // 通过类名调用,输出:学生总数:3Student s;s.ShowStudentCount(); // 通过对象调用,输出:学生总数:3return 0; }
十一、总结
本笔记围绕 C++ 类与对象的核心知识点展开,涵盖类的定义与文件组织、封装与访问控制、class 与 struct 差异、声明与定义、类与对象的抽象实例关系、对象内存模型与内存对齐、this 指针机制、空指针调用问题及静态成员补充。核心逻辑遵循 “从抽象到具体、从语法到原理”,结合代码示例与细节补充,可作为 OOP 基础的系统参考。后续需进一步学习继承、多态等特性,以完善面向对象编程知识体系。