
| 🍕阿i索 | 个人主页 |
|---|---|
| 《C语言专栏》 | 《C++专栏》 |
| 《数据结构专栏》 | 《LaTeX专栏》 |
| 待更新... |
这一篇,我们正式开启 C++ 学习的第一章:类与对象(上)。
一、类的定义
1. 类的定义格式
class 为定义类的关键字,后接类名,{ } 内为类的主体,类定义结束后分号不能省略 。类中的变量称为属性 / 成员变量;类中的函数称为方法 / 成员函数。定义在类内的成员函数默认为 inline 内联函数。
为区分普通变量,成员变量通常加特殊标识(如前缀_、后缀_),C++ 无强制要求。
2. struct 与 class 的关系
- C++ 兼容 C 中
struct的用法,同时将struct升级为类 ,支持在struct内定义成员函数。 - 访问权限默认值不同:
class未指定访问限定符时,成员默认为private;struct未指定时,成员默认为public。 - 使用场景:一般推荐用
class定义类;无需私有成员的简单场景,可使用struct。 - 类型标识:C++ 中
struct的类名可直接作为类型使用,无需像 C 语言那样通过typedef重定义。
3. 类的访问限定符
C++ 通过访问限定符实现封装,将对象的属性与方法结合,选择性对外提供接口,访问权限作用域从限定符出现位置开始,到下一个限定符或者类结束为止。
| 访问限定符 | 类外访问性 | 适用场景 |
|---|---|---|
public |
可直接访问 | 对外提供的接口(如成员函数) |
protected |
不可直接访问 | 继承场景使用(与 private 暂无区别) |
private |
不可直接访问 | 类的内部属性(成员变量) |
通用规范 :成员变量设为private/protected,需外部调用的成员函数设为public。 |
4. 类域
- 类定义了一个新的作用域,类的所有成员均属于该类域。
- 类的声明与定义分离时,类体外定义成员函数必须通过
::作用域操作符指明所属类域,否则编译器会将其视为全局函数,导致无法找到类内成员。 - 不同类域的函数无重载关系:重载要求函数在同一作用域,不同类的同名函数因作用域不同,不构成重载。
类的声明与定义分离示例
#include<iostream>
using namespace std;
class Stack
{
public:
// 类内声明成员函数
void Init(int n = 4);
private:
int* _a;
size_t _capacity;
size_t _top;
};
// 类外定义,通过Stack::指明类域
void Stack::Init(int n)
{
_a = (int*)malloc(sizeof(int) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
二、类的实例化
1. 实例化的概念
- 定义:用类类型在物理内存中创建对象的过程,称为类实例化出对象。
- 类与对象的关系:类是对对象的抽象描述 / 模型 ,仅声明成员变量,不分配实际内存;对象是类的具体实例,实例化时才会为成员变量分配物理内存,存储实际数据。
- 一个类可实例化多个对象,每个对象拥有独立的成员变量空间,存储各自的数据。
2. 对象的大小计算
(1)对象的存储规则
- 对象中仅存储成员变量 ,成员函数编译后为指令,存储在公共代码段,所有对象共享同一套成员函数指令。
- 原因:若每个对象存储成员函数指针,会造成内存冗余(如 100 个对象会重复存储 100 次相同指针);编译器编译链接时已确定成员函数地址,无需运行时存储(动态多态除外)。
(2)内存对齐规则
C++ 类实例化的对象,其成员变量布局遵循结构体内存对齐规则,具体如下:
- 第一个成员在与对象偏移量为 0 的地址处。
- 其他成员变量对齐到对齐数的整数倍地址处;
嵌套结构体时:嵌套的结构体对齐到自身的最大对齐数的整数倍处;
整个对象的总大小为所有最大对齐数(含嵌套结构体)的整数倍。
对齐数 = 编译器默认对齐数 与 成员变量大小的较小值(VS 中默认对齐数为 8)。
对象的总大小为最大对齐数(所有成员的对齐数的最大值)的整数倍。
(3)特殊情况:无成员变量的类
无成员变量的类(或仅含成员函数的类),实例化的对象大小为1 字节。
该字节仅用于占位标识对象的存在,不存储任何实际数据。
(4)大小计算示例
#include<iostream>
using namespace std;
class A // 含char和int成员
{
public:
void Print() { cout << _ch << endl; }
private:
char _ch; // 大小1,对齐数1
int _i; // 大小4,对齐数4(VS默认8,取较小值4)
};
class B // 仅含成员函数
{
public:
void Print() {}
};
class C // 无任何成员
{};
int main()
{
cout << sizeof(A) << endl; // 8(char占1字节,补3字节对齐,int占4字节,总大小8=4*2)
cout << sizeof(B) << endl; // 1(占位字节)
cout << sizeof(C) << endl; // 1(占位字节)
return 0;
}
三、this 指针
1. this 指针的引入原因
类的成员函数为所有对象共享,函数体中无对象区分标识,this 指针用于解决成员函数如何识别操作的是哪个对象的问题。
2. this 指针的特性
- 编译器自动添加:类的成员函数编译后,形参第一个位置会自动增加当前类类型的 const 指针
this,无需程序员显式写在形参 / 实参位置。 - 实际函数原型:如
Date类的Init函数,声明为void Init(int year, int month, int day),真实原型为void Init(Date* const this, int year, int month, int day)。 - 成员访问本质:成员函数中访问成员变量 / 成员函数,本质是通过
this指针访问 ,如_year = year;等价于this->_year = year;。 - 显式使用:C++ 禁止在形参 / 实参位置显式写
this,但可在成员函数体内显式使用。 - 不可修改:
this指针为const指针,指向不可修改(如this = nullptr;会编译报错)。
3. this 指针的调用机制
对象调用成员函数时,编译器会自动将对象的地址作为实参传递给 this 指针 ,如:d1.Init(2024,3,31);等价于d1.Init(&d1,2024,3,31);。
4. this 指针的经典例题分析
例题 1
class A
{
public:
void Print() { cout << "A::Print()" << endl; }
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print(); // 结果:正常运行
return 0;
}
分析 :Print函数为成员函数,存储在公共代码段,调用时仅需函数地址,无需解引用this指针(p),因此空指针调用无崩溃。
例题 2
class A
{
public:
void Print() { cout << "A::Print()" << endl; cout << _a << endl; }
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print(); // 结果:运行崩溃
return 0;
}
分析 :Print函数中访问_a,本质是this->_a,this指针为nullptr,解引用空指针导致运行崩溃。
例题 3:this 指针的存储区域
问题 :this 指针存在内存哪个区域?(答案:A 栈)分析 :this 指针是成员函数的形参,形参存储在函数的栈帧中,因此 this 指针位于栈区。
四、C 与 C++ 实现栈(Stack)的对比
C++ 基于 C 的底层逻辑,通过面向对象的封装特性重构栈的实现,核心变化体现在语法和管理规范上,底层内存操作逻辑一致。
1. 差异对比
| 特性 | C 语言实现 | C++ 实现 |
|---|---|---|
| 数据与函数关系 | 数据(结构体)与函数分离,函数需显式传递结构体地址 | 数据(成员变量)与函数(成员函数)封装在类内,通过 this 指针隐式传递对象地址 |
| 访问控制 | 无访问限定符,结构体成员可随意修改,安全性低 | 通过 public/private 实现访问控制,成员变量私有化,仅通过接口操作,安全性高 |
| 类型使用 | 需通过 typedef 重定义结构体为新类型 | 类名直接作为类型使用,无需 typedef |
| 函数调用 | 函数名需加前缀(如 STInit),区分模块 | 直接通过对象调用成员函数(如 s.Init ()),语法更简洁 |
| 缺省参数 | 不支持缺省参数,需手动传递所有参数 | 成员函数支持缺省参数(如 Init (int n=4)),使用更灵活 |
C 语言:结构体 + 全局函数
typedef struct Stack {
int* a;
int top, capacity;
} ST;
void STInit(ST* ps);
void STPush(ST* ps, int x);
C++:类 + 成员函数
class Stack {
public:
void Init(int cap = 4);
void Push(int x);
private:
int* _a;
int _top, _capacity;
};
封装的本质
C++ 封装的本质是对数据和方法进行更严格的规范管理,将数据私有化,避免外部随意修改,仅通过对外暴露的公共接口操作数据,降低程序出错概率。
五、面向对象三大特性(基础)
C++ 面向对象的三大核心特性为:封装、继承、多态 ,本章节核心讲解封装的基础实现:
- 封装的第一层:将数据(成员变量)和操作数据的方法(成员函数)封装在类的同一作用域内;
- 封装的第二层:通过访问限定符(public/private/protected)控制类成员的对外访问权限,实现数据的隐藏和接口的暴露。
六、编程命名规范(惯例)
- 驼峰命名法:
- 类名、函数名:大驼峰(单词首字母大写,如
StackInit、Date); - 普通变量:小驼峰(第一个单词首字母小写,后续单词首字母大写,如
stackSize);
- 类名、函数名:大驼峰(单词首字母大写,如
- 下划线命名法:单词间用下划线分隔(如
stack_init),适用于 C/C++ 的函数 / 变量命名; - 成员变量:在驼峰 / 下划线基础上加特殊标识(如
_a、m_capacity),区分普通变量。
下一篇再见🍕