目录
1.面向过程和面向对象初步认识
2. 类的引入
3. 类的定义
4. 类的访问限定符及封装
4.1 访问限定符
4.2 封装
5. 类的作用域
6. 类的实例化
7. 类对象模型
7.1 类对象的存储方式
7.2 结构体内存对齐规则
7.3 特殊情况:空类的大小
8. this 指针
8.1 this 指针的引出
8.2 this 指针的特性
9. C 语言和 C++ 实现 栈(Stack) 的对比
💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!
👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!
1.面向过程和面向对象初步认识
- 面向过程(C 语言):关注解决问题的过程,分析求解步骤,通过函数调用逐步解决问题。
- 面向对象(C++):基于面向对象编程,关注对象,将事情拆分成不同对象,依靠对象间的交互完成任务。
2. 类的引入
- C 语言结构体:只能定义变量。
- C++ 结构体:不仅能定义变量,还能定义函数。
示例:
#include <iostream>
#include <cstdlib> // 用于使用 malloc、free 和 perror 函数
#include <cassert> 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) {if (_size == _capacity) {size_t newCapacity = _capacity * 2;DataType* newArray = (DataType*)realloc(_array, sizeof(DataType) * newCapacity);if (newArray == nullptr) {perror("realloc申请空间失败");return;}_array = newArray;_capacity = newCapacity;}// 将元素存入栈中_array[_size] = data;++_size;}DataType Top() {// 确保栈不为空assert(_size > 0);// 返回栈顶元素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;// 初始化栈,指定初始容量为 10s.Init(10);s.Push(1);s.Push(2);s.Push(3);std::cout << s.Top() << std::endl;s.Destroy();return 0;
}
C++ 中,更常用 class
代替 struct
来定义类。
思考:为什么是
++_size; 而非 _size++; ?
性能方面:现代编译器中两者在性能上差异微乎其微,但理论上前置自增性能更优。后置自增需要额外内存空间保存原值,并且操作结束后还要进行一次赋值;前置自增直接对变量进行自增并返回结果,没有这些额外开销。
代码语义方面:前置自增更直观地表达“先进行自增,再更新栈元素数量”的操作顺序。
3. 类的定义
- 定义格式:
class
是定义类的关键字,ClassName
是类名,类定义结束后分号不能省略。
class className
{// 类体:由成员函数和成员变量组成}; // 一定不要忘记分号int main()
{classname 对象名;对象名.成员函数名();
}
- 类的成员:类中的变量称为类的属性或成员变量,类中的函数称为类的方法或成员函数。
- 两种定义方式:
- (1)声明和定义全放类体中:成员函数在类中定义,编译器可能将其当作内联函数处理。
- (2)类声明放
.h
文件,成员函数定义放.cpp
文件:成员函数名前需加类名::
。一般推荐这种方式。
- 成员变量命名规则建议:为避免成员变量和函数形参混淆,建议给成员变量加前缀或后缀,如
_year
或mYear
。
4. 类的访问限定符及封装
4.1 访问限定符
访问限定符 | 类外访问性 | 作用域 | 默认访问权限 |
---|---|---|---|
public(公有) | 可直接访问 | 从出现位置开始,到下一个访问限定符出现为止;若无下一个访问限定符,则到类结束 | struct 定义的类默认访问权限是 public |
protected(保护) | 不能直接访问 | 从出现位置开始,到下一个访问限定符出现为止;若无下一个访问限定符,则到类结束 | |
private(私有) | 不能直接访问 | 从出现位置开始,到下一个访问限定符出现为止;若无下一个访问限定符,则到类结束 | class 定义的类默认访问权限是 private |
注意:访问限定符主要是在编译阶段发挥作用,用于保证代码的安全性和规范性,而在内存中,对象的成员变量并没有访问限定符的区分。
【面试题】C++ 中 struct
和 class
的区别?
C++需要兼容C语言,所以C++ 中 struct
可当作结构体使用。C++ 中 struct
也可定义类,与 class
定义类类似,区别在于 struct
定义的类默认访问权限是 public
,class
定义的类默认访问权限是 private
。在继承和模板参数列表位置也有区别。
4.2 封装
- 面向对象三大特性:封装、继承、多态。类和对象阶段主要研究封装特性。
- 封装定义:将数据和操作数据的方法有机结合,隐藏对象属性和实现细节,仅对外公开接口与对象交互。
- C++ 实现封装:通过类将数据和操作方法结合,利用访问权限隐藏内部实现细节,控制类外可直接使用的方法。
5. 类的作用域
类定义了新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需用 :: 作用域操作符指明成员所属类域。
#include <iostream>
#include <string>
using namespace std;class Person
{
public:// 声明成员函数void PrintPersonInfo();void SetName(const char* name);void SetGender(const char* gender);void SetAge(int age);
private:char _name[20];char _gender[3];int _age;
};void Person::PrintPersonInfo()
{cout << _name << " " << _gender << " " << _age << endl;
}void Person::SetName(const char* name)
{// 将传入的姓名拷贝到成员变量_name中strncpy(_name, name, sizeof(_name) - 1);// 确保字符串以'\0'结尾_name[sizeof(_name) - 1] = '\0';
}void Person::SetGender(const char* gender)
{strncpy(_gender, gender, sizeof(_gender) - 1);_gender[sizeof(_gender) - 1] = '\0';
}void Person::SetAge(int age)
{_age = age;
}int main()
{Person p;p.SetName("Alice");p.SetGender("女");p.SetAge(25);p.PrintPersonInfo();return 0;
}
6. 类的实例化
- 定义:用类创建对象的过程称为类的实例化。
- 特点:
- 类是对对象的描述,是模型,定义类时未分配实际内存空间。例如学生信息表可看成类,描述具体学生信息。
- 一个类可实例化多个对象,实例化的对象占用实际物理空间,存储类成员变量。
7. 类对象模型
7.1 类对象的存储方式
在C++中,类对象的存储方式遵循以下规则:
类对象包含成员变量,但不包含成员函数。成员函数不属于某个具体的对象,而存放在公共的代码段,所有该类的对象共享同一份成员函数代码。
综上所述,类对象的大小本质上是其成员变量所占内存空间之和。但需要考虑内存对齐规则。
7.2 结构体内存对齐规则
-
第一个成员的起始位置:第一个成员变量存放在与结构体偏移量为0的地址处。
-
其他成员的对齐位置:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数等于编译器默认的对齐数和该成员大小的较小值。在Visual Studio中,默认的对齐数为8。
-
结构体的总大小:结构体总大小必须是最大对齐数(所有成员变量类型的最大者与默认对齐参数取最小)的整数倍。
-
嵌套结构体的情况:如果嵌套了结构体,嵌套的结构体要对齐到自己的最大对齐数的整数倍处,结构体的整体大小是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。
7.3 特殊情况:空类的大小
空类:没有任何成员变量和成员函数的类,或者只有成员函数而没有成员变量的类。
编译器会给空类分配1字节的空间,目的是为了让空类的对象能够拥有唯一的地址。
8. this 指针
8.1 this 指针的引出
C++ 中,当一个类有多个对象时,每个对象都有自己独立的成员变量。但是类的成员函数是所有对象共享的。
这就产生一个问题:当一个成员函数被调用时,如何知道是哪个对象在调用它呢?
#include <iostream>
using namespace std;class Date
{
public:void Init(int year, int month, int day) {_year = year;_month = month;_day = day;}void Print() {std::cout << _year << "-" << _month << "-" << _day << std::endl;}private:int _year;int _month;int _day;
};int main()
{Date d1, d2;d1.Init(2022, 1, 11);d2.Init(2022, 1, 12);d1.Print();d2.Print();return 0;
}
以上述代码为例,d1.Init(2022, 1, 11)
和d2.Init(2022, 1, 12)
被调用时,Init
函数将无法确定将这些值赋给d1
的成员变量还是d2
的成员变量。
为了解决以上问题,使用 this 指针。
C++中,每个非静态的成员函数都有一个隐藏的指针参数,这个指针就是 this 指针。它的主要作用是指向当前正在调用该成员函数的对象(不可为空)。
8.2 this 指针的特性
-
this指针的类型是
类类型 * const
,这意味着 this 指针是一个指向常量的指针,不能在成员函数中给this指针赋值。例如,对于一个Student
类,this指针的类型就是Student * const
。 -
this 指针只能在类的成员函数内部使用,不能在类的外部单独使用。
-
this 指针是成员函数的第一个隐含的指针形参,不需在调用成员函数时显式传递。当对象调用成员函数时,编译器会自动将对象的地址作为实参传递给 this 指针。例如调用
s1.display()
时,编译器会自动将其转换为Student::display(&s1)。
-
this 指针不存储于对象本身中。在函数调用期间,this 指针的值保存在函数的栈帧中。函数执行完毕后,this 指针的值就会被销毁。
9. C 语言和 C++ 实现 栈(Stack) 的对比
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef int DataType;typedef struct Stack
{DataType* array; // 用于存储栈中元素的数组指针int capacity; int size; // 栈中当前元素的数量
} Stack;// 初始化栈
void StackInit(Stack* ps)
{assert(ps); // 为栈的数组分配初始容量为3的空间ps->array = (DataType*)malloc(sizeof(DataType) * 3);if (NULL == ps->array) {assert(0); return;}ps->capacity = 3; ps->size = 0;
}void StackDestroy(Stack* ps)
{assert(ps); if (ps->array) {free(ps->array); // 释放栈数组所占用的内存ps->array = NULL;ps->capacity = 0;ps->size = 0;}
}// 检查并扩容
void CheckCapacity(Stack* ps)
{if (ps->size == ps->capacity) {int newcapacity = ps->capacity * 2; DataType* temp = (DataType*)realloc(ps->array, newcapacity * sizeof(DataType));if (temp == NULL) {perror("realloc申请空间失败!!!"); return;}ps->array = temp; ps->capacity = newcapacity; }
}// 入栈
void StackPush(Stack* ps, DataType data)
{assert(ps); CheckCapacity(ps); ps->array[ps->size] = data; // 将元素存入栈数组ps->size++;
}int StackEmpty(Stack* ps)
{assert(ps); return 0 == ps->size; // 如果栈中元素数量为0,返回1表示空,否则返回0
}// 出栈
void StackPop(Stack* ps)
{if (StackEmpty(ps)) return;ps->size--;
}// 获取栈顶元素
DataType StackTop(Stack* ps)
{assert(!StackEmpty(ps)); return ps->array[ps->size - 1];
}// 获取栈中元素的数量
int StackSize(Stack* ps)
{assert(ps); return ps->size;
}int main()
{Stack s;StackInit(&s); StackPush(&s, 1); StackPush(&s, 2); StackPush(&s, 3); StackPush(&s, 4); printf("%d\n", StackTop(&s)); printf("%d\n", StackSize(&s)); StackPop(&s); StackPop(&s); printf("%d\n", StackTop(&s)); printf("%d\n", StackSize(&s)); StackDestroy(&s); return 0;
}
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;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); cout << s.Top() << endl; cout << s.Size() << endl; s.Pop(); s.Pop(); cout << s.Top() << endl; cout << s.Size() << endl; s.Destroy(); return 0;
}
比较维度 | C 语言实现 | C++ 实现 |
---|---|---|
数据与操作的组织方式 | 结构体中只能定义存放数据的结构,数据和操作数据方式分离 | 通过类将数据与操作数据方式完美结合 |
函数参数 | 每个函数的第一个参数都是 Stack* | 不需显式传递 Stack * 参数,编译器自动维护 |
空指针检测 | 函数中必须要对第一个参数(Stack*)检测是否为 NULL | 无需手动对类似指针参数进行空指针检测,编译器自动维护 |
调用方式 | 调用时必须传递 Stack 结构体变量的地址 | 使用时如同使用自身成员 |
访问控制 | 通过访问权限(public、protected、private)控制哪些方法在类外可被调用,实现封装 | |
实现复杂度 | 涉及大量指针操作,容易出错,实现相对复杂 | 代码结构更清晰,一定程度上降低了因指针操作不当导致的错误风险 |