类与对象(上)
【本节目标】
- 面向过程和面向对象初步认识
- 类的引入
- 类的定义
- 类的访问限定符及封装
- 类的作用域
- 类的实例化
- 类的对象大小的计算
- 类成员函数的 this 指针
1. 面向过程和面向对象初步认识
-
C 语言(面向过程):关注「过程」,拆解解决问题的步骤,通过函数调用逐步执行。
示例:洗衣服的过程
拿个盆子 → 放水 → 放衣服 → 放洗衣粉 → 手搓 → 换水 → 手搓 → 拧干 → 晾衣服
-
C++(基于面向对象):关注「对象」,拆分出核心对象,靠对象间交互完成任务。
示例:洗衣服的对象交互
- 核心对象:衣服、洗衣机、洗衣粉
- 核心逻辑:无需关注洗衣机内部原理,仅通过对象交互完成洗衣任务
2. 类的引入
C 语言结构体仅能定义变量,C++ 结构体支持同时定义变量 + 函数,示例(用 struct 实现栈):
cpp
typedef int DataType;
struct Stack
{
void Init(size_t capacity)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(const DataType& data)
{
// 扩容(此处省略扩容逻辑)
_array[_size] = data;
++_size;
}
DataType Top()
{
return _array[_size - 1];
}
void Destroy()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
DataType* _array;
size_t _capacity;
size_t _size;
};
int main()
{
Stack s;
s.Init(10);
s.Push(1);
s.Push(2);
s.Push(3);
cout << s.Top() << endl;
s.Destroy();
return 0;
}
!note\] 提示 C++ 中更推荐用`class`关键字替代`struct`定义类(核心逻辑一致,仅默认权限不同)。
3. 类的定义
3.1 基本格式
cpp
class className
{
// 类体:成员函数(方法) + 成员变量(属性)
}; // 注意:类定义结束必须加分号
class:定义类的关键字className:类名- 类体成员:
- 成员变量:类的属性(描述对象特征)
- 成员函数:类的方法(操作对象的行为)
3.2 类的两种定义方式
方式 1:声明 + 定义全放在类体中
- 特点:成员函数在类内定义时,编译器可能视为内联函数
cpp
class Person
{
public:
// 显示基本信息(声明+定义)
void showInfo()
{
cout << _name << "." << _sex << "." << _age << endl;
}
public:
char* _name; // 姓名
char* _sex; // 性别
int _age; // 年龄
};
方式 2:声明与定义分离(推荐)
- 声明:放在
.h头文件(仅声明函数,不写实现) - 定义:放在
.cpp文件(函数名前加类名::作用域限定符)
头文件 person.h(声明):--仅声明
cpp
class Person
{
public:
// 显示基本信息(仅声明)
void showInfo();
public:
char* _name; // 姓名
char* _sex; // 性别
int _age; // 年龄
};
实现文件 person.cpp(定义):
cpp
#include "person.h"
// 显示基本信息(实现)
void Person::showInfo()
{
cout << _name << " " << _sex << " " << _age << endl;
}
3.3 成员变量命名规则建议
为避免与函数形参同名,建议给成员变量加前缀 / 后缀标识:
cpp
// 推荐方式1:下划线前缀(最常用)
class Date
{
public:
void Init(int year)
{
_year = year; // 清晰区分成员变量 vs 形参
}
private:
int _year;
};
// 推荐方式2:m前缀(部分公司规范)
class Date
{
public:
void Init(int year)
{
mYear = year;
}
private:
int mYear;
};
!tip\] 说明 具体规则可按公司规范调整,核心是**区分成员变量与普通变量**。
4. 类的访问限定符及封装
4.1 访问限定符
C++ 通过 3 种限定符控制成员的外部访问权限,实现「封装」:
| 访问限定符 | 核心作用 |
|---|---|
public |
类外可直接访问(对外接口) |
protected |
类外不可访问(继承场景用) |
private |
类外不可访问(隐藏实现) |
关键说明
- 权限作用域:从当前限定符开始,到下一个限定符 / 类结束(
})为止; - 默认权限:
class:默认private(隐藏细节)struct:默认public(兼容 C 语言);
- 本质:仅编译期有效,编译后内存中无权限差异。
4.2 封装(面向对象三大特性之一)
!definition\] 封装定义 将数据(成员变量)和操作数据的方法(成员函数)有机结合,隐藏对象的属性和实现细节,仅对外公开接口用于交互。 \[!idea\] 封装本质 一种「管理方式」:隐藏复杂细节,只暴露简单接口,降低使用复杂度。 示例:电脑封装 → 隐藏 CPU / 显卡原理,仅暴露开关机键、键盘等接口; C++ 类封装 → 用`private`隐藏成员变量,用`public`提供操作接口。
面试题汇总
!question\] 面试题 1:C++ 中 struct 和 class 的区别? 1. 兼容性:struct 兼容 C 语言结构体,class 不兼容; 2. 默认权限:struct 默认 public,class 默认 private; 3. 其他差异:继承、模板参数列表中表现不同(后续章节讲解)。 \[!question\] 面试题 2:面向对象的三大特性? 封装、继承、多态(本节重点讲解「封装」)。
5. 类的作用域
类定义了独立的作用域,所有成员均属于该类域;类外定义成员时,需用::指定作用域:
cpp
class Person
{
public:
void PrintPersonInfo(); // 声明在Person类域中
private:
char _name[20];
char _gender[3];
int _age;
};
// 类外定义:必须加Person::
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
6. 类的实例化
!definition\] 实例化定义 用「类类型」创建「对象」的过程,称为类的实例化。
核心要点
-
类是「模型 / 图纸」:仅描述对象结构,定义类时不分配内存;
→ 类比:类 = 建筑设计图,对象 = 按图纸盖的房子。
-
对象是「具体实例」:实例化后占用物理内存,存储成员变量(成员函数存于公共代码段);
-
一个类可实例化多个对象:共享成员函数,独立存储成员变量。
错误示例(直接操作类的成员)
cpp
int main()
{
Person._age = 100; // 编译失败:类无内存,不能直接访问成员
return 0;
}
正确示例(实例化对象后操作)
cpp
void Test()
{
Person man; // 实例化对象man(分配内存)
man._name = "jack";
man._sex = "男";
man._age = 10;
man.showInfo(); // 调用成员函数
}
7. 类对象模型(对象大小计算)
7.1 核心结论
对象中仅存储成员变量(成员函数存于公共代码段),因此:
对象大小 = 成员变量之和(需遵循内存对齐规则)
7.2 验证:不同类的大小计算
cpp
// 类1:有成员变量+成员函数
class A1 {
public:
void f1(){}
private:
int _a;
};
// 类2:仅成员函数
class A2 {
public:
void f2() {}
};
// 类3:空类(无任何成员)
class A3 {};
// 计算结果(32/64位环境一致)
sizeof(A1) = 4; // 仅int _a的大小
sizeof(A2) = 1; // 空类占位字节(编译器分配)
sizeof(A3) = 1; // 空类占位字节(唯一标识对象)
7.3 结构体内存对齐规则(计算基础)
-
第一个成员:偏移量为 0 的地址处;
-
其他成员:对齐到「对齐数」的整数倍地址;
→ 对齐数 = min (编译器默认对齐数,成员大小)(VS 默认 8,Linux 默认 4);
-
总大小:最大对齐数的整数倍;
-
嵌套结构体:嵌套部分对齐到自身最大对齐数的整数倍,整体大小为所有最大对齐数的整数倍。
面试题汇总
!question\] 面试题 1:结构体怎么对齐?为什么要内存对齐? * 对齐方式:按上述 4 条规则; * 对齐原因:提高 CPU 访问效率(CPU 按固定字节数读取内存,不对齐会触发多次读取)。 \[!question\] 面试题 2:如何指定结构体对齐参数?能否任意字节对齐? * 指定对齐:用`#pragma pack(n)`(n 为对齐参数); * 限制:n 必须是 2 的幂次(1/2/4/8),不能是 3/5 等任意值。 \[!question\] 面试题 3:什么是大小端?如何测试? * 大小端定义: * 大端:高位字节存低地址,低位字节存高地址; * 小端:低位字节存低地址,高位字节存高地址(x86 架构默认小端); * 测试方法: cpp 运行 ```cpp // 方法1:union(共用体) union Test { int a; char b; } t; t.a = 1; if (t.b == 1) cout << "小端" << endl; else cout << "大端" << endl; // 方法2:指针强制转换 int a = 1; char* p = (char*)&a; if (*p == 1) cout << "小端" << endl; else cout << "大端" << endl; ```
8. 类成员函数的 this 指针
8.1 this 指针的引出
问题:同一个类的多个对象共享成员函数,函数如何区分操作哪个对象的成员?
cpp
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year, _month, _day;
};
int main()
{
Date d1, d2;
d1.Init(2022, 1, 11); // 函数如何知道操作d1?
d2.Init(2022, 1, 12); // 函数如何知道操作d2?
d1.Print();
d2.Print();
return 0;
}
!answer\] 解答 C++ 编译器为每个**非静态成员函数** 添加隐藏的`this`指针参数,该指针指向当前调用函数的对象;函数体内所有成员变量的操作,均通过`this`指针完成(用户无需手动传递,编译器自动处理)。
8.2 this 指针的特性
- 类型:
类类型* const(不能给 this 赋值,指针指向不可改); - 作用域:仅在成员函数内部使用;
- 本质:成员函数的隐含形参,对象调用函数时,自动传递对象地址给 this;
- 存储:不占用对象内存,通常由编译器通过 ecx 寄存器传递。
编译器的隐式处理(示例)
cpp
// 用户写的代码
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// 编译器处理后的代码(隐含this指针)
void Date::Print(Date* const this)
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
// 调用时的隐式转换
d1.Print(); → Date::Print(&d1); // 自动传递d1的地址给this
8.3 面试题汇总
!question\] 面试题 1:this 指针存在哪里? 答案:通常存储在寄存器(如 x86 的 ecx),而非对象 / 栈中(核心:不占用对象内存)。 \[!question\] 面试题 2:this 指针可以为空吗? 答案:可以,但需避免通过空 this 访问成员变量(否则触发空指针崩溃)。
示例 1(正常运行)
cpp
class A
{
public:
void Print()
{
cout << "Print()" << endl; // 未访问成员变量,this为空不影响
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print(); // 结果:正常输出Print()
return 0;
}
示例 2(运行崩溃)
cpp
class A
{
public:
void PrintA()
{
cout << _a << endl; // 等价于this->_a,this为空触发崩溃
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA(); // 结果:运行崩溃(空指针访问)
return 0;
}
!answer\] 解答 核心原因: 在 PrintA0) 函数内部,虽然没有显式写出 this-\>a,但是当前的 this 指针是nu∥ptr。当代码执行到 cout 的相关实现(或者是某些编译器对成员函数调用的检查机制)时,发生了对 this 指针的解引用或访问(即试图读取地址 0 处的内存)访问内存地址0是非法的,操作系统会立即拦截并抛出异常,导致程序运行崩溃(通常Access Violation或Segmentation Fault)
示例 3(正常运行)
cpp
class A
{
public:
void PrintA()
{
cout << this << endl;
cout << "PrintA()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
(*p).PrintA();
return 0;
}
!answer\] 解答 1.场景一:没有 cout\<\< this 在 Visual Studio 的 Debug 模式下,偷插入一段代码来检查 this指针。 编译器非常"尽职尽责"。它知道通过空指针调用函数是危险的,所以它会在函数刚进入的时候,偷**偷偷插入** 一段代码来检查 `this` 指针。 编译器眼中的代码是这样的:
cpp
void PrintA()
{
// <--- 编译器自动插入的"安检代码"
#ifdef _DEBUG
if (this == nullptr) {
// 发现是空指针!立即报错,中断程序
_CrtIsValidHeapPointer(this); // 导致崩溃
}
#endif
cout << "PrintA()" << endl;
}
!answer\] 续答 **结果:** 程序刚进入 `PrintA` 函数,第一行就执行了检查代码。`this` 是 0,检查不通过,**触发异常,程序崩溃** 。它甚至没机会去执行后面的 `cout`。 2.场景二:加上了 cout\<\
cpp
void PrintA()
{
// <--- Release 模式下,检查代码被删掉了,干干净净
// 下面是你真正的代码
cout << this; // 只是打印指针变量的值 0,不访问内存
cout << "PrintA()";
}
!answer\] 结果: 没有检査代码,也没有访问非法内存。程序就像开着一辆没有刹车的车在空旷的高速上跑,虽然危险(指针是空的),但只要不撞墙 (不访问成员变量),它就能一直开下去。
9. C 语言 vs C++ 实现 Stack 对比
9.1 C 语言实现(数据与操作分离)
cpp
typedef int DataType;
typedef struct Stack
{
DataType* array;
int capacity;
int size;
}Stack;
// 初始化栈
void StackInit(Stack* ps)
{
assert(ps); // 必须检测ps非空
ps->array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = 3;
ps->size = 0;
}
// 入栈
void StackPush(Stack* ps, DataType data)
{
assert(ps);
CheckCapacity(ps); // 扩容函数(省略实现)
ps->array[ps->size] = data;
ps->size++;
}
// 其他操作:StackDestroy、StackPop、StackTop等(均需传递Stack*)
int main()
{
Stack s;
StackInit(&s);
StackPush(&s, 1);
StackPush(&s, 2);
printf("%d\n", StackTop(&s));
StackDestroy(&s);
return 0;
}
C 语言实现的缺点
- 所有函数需传递
Stack*参数,且必须检测非空; - 数据(结构体)与操作(函数)分离,逻辑分散;
- 指针操作复杂,易出错。
9.2 C++ 实现(封装:数据与操作结合)
cpp
typedef int DataType;
class Stack
{
public:
void Init()
{
_array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = 3;
_size = 0;
}
void Push(DataType data)
{
CheckCapacity(); // 内部调用,无需传参
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
_size--;
}
DataType Top() { return _array[_size - 1]; }
int Empty() { return 0 == _size; }
int Size() { return _size; }
void Destroy()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
// 扩容函数(私有:外部不可访问)
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
_array = temp;
_capacity = newcapacity;
}
}
private:
DataType* _array; // 数据私有:外部不可直接操作
int _capacity;
int _size;
};
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
printf("%d\n", s.Top()); // 直接调用,无需传参
printf("%d\n", s.Size());
s.Pop();
s.Pop();
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Destroy();
return 0;
}