【C++篇】深度解析类与对象(上)
平凡程序猿~ 2024-10-22 10:35:02 阅读 72
目录
引言
一、类的定义
1.1类定义的基本格式
1.2 成员命名规范
1.3 class与struct的区别
1.4 访问限定符
1.5 类的作用域
二、实例化
2.1 类的实例化
2.2 对象的大小与内存对齐
三、this 指针
3.1 this指针的基本用法
3.2 为什么需要this指针?
3.3 this指针的限制
四、C++和C语言实现Stack的对比
4.1 C语言实现Stack
4.2 C++语言实现Stack
4.3 C++与C实现的对比总结
五、总结
引言
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
一、类的定义
在C++中,类通过将数据和行为封装在一起,模拟现实世界中的对象。类的定义通常包含成员变量(描述对象的状态)和成员函数(定义对象的行为)。类的定义使用<code>class关键字,并以分号结束。
1.1类定义的基本格式
类的定义格式如下所示:
#include <iostream>
using namespace std;
class Stack {
public:
// 初始化栈
void Init(int n = 4) {
array = new int[n];
capacity = n;
top = 0;
}
// 将元素推入栈
void Push(int x) {
array[top++] = x;
}
// 获取栈顶元素
int Top() {
if (top > 0) {
return array[top - 1];
}
return -1; // 栈为空时返回-1
}
// 销毁栈
void Destroy() {
delete[] array;
array = nullptr;
top = capacity = 0;
}
private:
int* array; // 栈数据数组
size_t capacity; // 栈容量
size_t top; // 栈顶指针
};
int main() {
Stack st;
st.Init();
st.Push(1);
st.Push(2);
cout << st.Top() << endl;
st.Destroy();
return 0;
}
在上述代码中:
class
关键字用于定义类,Stack
是类的名称。
public
修饰的成员函数可以在类的外部访问,例如Init
、Push
、Top
和Destroy
。
private
修饰的成员变量(如array
、capacity
和top
)只能在类的内部访问,无法在类外部直接使用。
1.2 成员命名规范
在C++中,通常会为类的成员变量使用特定的命名约定,以避免与函数参数或局部变量混淆。这些命名约定可以提高代码的可读性和维护性。例如:
使用下划线前缀:如_year
。
使用m_
前缀:如m_month
。
使用驼峰命名法:如dayOfMonth
。
1.3 class与struct的区别
C++中的class
和struct
的主要区别在于默认的访问权限:
在class
中,未标明的成员变量和成员函数默认是private
。
struct ExampleClass {
int a; // 默认 private
};
在struct
中,未标明的成员变量和成员函数默认是public
。
struct ExampleStruct {
int a; // 默认 public
};
1.4 访问限定符
访问限定符用于控制类的成员的可见性。C++支持三种访问限定符:
public
:公共成员可以在类的外部访问。
private
:私有成员只能在类的内部访问。
protected
:保护成员只能在类内部或派生类中访问(会在继承中详细讲解)。
访问限定符从出现的位置开始生效,直到遇到下一个访问限定符或类定义结束为止。例如:
<code>class Date {
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d;
d.Init(2024, 3, 31);
// 无法直接访问 _year, _month, _day,因为它们是私有成员
return 0;
}
在上述示例中,Init
函数是公共的,可以在类外部调用;而_year
、_month
和_day
是私有的,只能通过成员函数访问。
1.5 类的作用域
类的作用域决定了类成员的可访问性。当在类的外部定义成员函数时,需要使用作用域解析符::
来指明成员函数所属的类。
#include <iostream>
using namespace std;
class Stack {
public:
void Init(int n = 4);
private:
int* array;
size_t capacity;
size_t top;
};
// 在类外定义成员函数
void Stack::Init(int n) {
array = new int[n];
capacity = n;
top = 0;
}
int main() {
Stack st;
st.Init();
return 0;
}
通过使用Stack::Init
,编译器可以知道Init
函数属于Stack
类,并能在类的作用域中查找成员变量array
、capacity
和top
。
二、实例化
2.1 类的实例化
实例化是指在物理内存中创建对象的过程。类提供了对象的结构和行为,但本身不占用物理空间,只有实例化后才会在内存中分配空间。
⼀个类可以实例化出多个对象
,实例化出的对象 占⽤实际的物理空间,存储类成员变量。打个⽐⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。
<code>#include <iostream>
using namespace std;
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;
int _month;
int _day;
};
int main() {
Date d1;
d1.Init(2024, 3, 31);
d1.Print();
return 0;
}
在上述代码中,Date d1
实例化了一个Date
对象,并调用了Init
和Print
成员函数。
2.2 对象的大小与内存对齐
对象的大小由成员变量决定,成员函数不影响对象的大小。
类实例化出的每个对象,都有独⽴的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?⾸先函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段),那么对象中⾮要存储的话,只能是成员函数的指针。再分析⼀下,对
象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各⾃独⽴的成员变量_year/_month/_day存储各⾃的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象中就浪费了。如果⽤Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这⾥需要再额外哆嗦⼀下,
其实函数指针是不需要存储的,函数指针是⼀个地址,调⽤函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找
,只有动态多态是在运⾏时找,就需要存储函数地址,这个我们以后会讲解。
C++规定类的对象也需要符合内存对齐的规则,以提高访问效率。
<code>#include <iostream>
using namespace std;
class A {
private:
char _ch; // 1 字节
int _i; // 4 字节
};
int main() {
A a;
cout << sizeof(a) << endl; // 输出8字节,因内存对齐
return 0;
}
虽然_ch
和_i
占用5字节,但由于内存对齐,实际大小为8字节。这样可以优化内存访问的性能。
结构体对齐详细介绍可参考我的另一篇博客 。
三、this 指针
this
指针是C++中的一个隐含指针,指向调用成员函数的当前对象。它存在于每一个非静态成员函数中,用于区分成员变量和函数参数。当成员函数被调用时,this
指针会自动传递给函数,使其能够访问调用该函数的对象的成员。
3.1 this
指针的基本用法
在成员函数中,this
指针用于访问当前对象的成员变量。例如:
class Date {
public:
void Init(int year, int month, int day) {
this->_year = year; // 使用 this 指针
this->_month = month;
this->_day = day;
}
void Print() {
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
d1.Init(2024, 3, 31);
d1.Print();
return 0;
}
在上述代码中,this->_year = year
将参数year
的值赋给当前对象的_year
成员变量。this
指针指向调用Init
函数的对象(即d1
),使得函数能够正确地操作对象的数据。
3.2 为什么需要this
指针?
this
指针在以下情况下特别有用:
当成员变量和函数参数同名时,使用this
可以避免命名冲突。
在链式调用中,返回*this
可以实现对同一对象的连续操作。
class Person {
public:
Person& SetName(const string& name) {
this->name = name;
return *this;
}
Person& SetAge(int age) {
this->age = age;
return *this;
}
void Display() {
cout << "Name: "
<< name << ", Age: " << age << endl;
}
private:
string name;
int age;
};
int main() {
Person p;
p.SetName("Alice").SetAge(30).Display();
return 0;
}
在上述示例中,SetName
和SetAge
函数返回*this
,使得可以进行链式调用,即p.SetName("Alice").SetAge(30).Display()
。
3.3 this
指针的限制
this
指针是只读的,无法修改其指向。此外,在静态成员函数中无法使用this
指针,因为静态成员函数不与任何对象关联。
四、C++和C语言实现Stack
的对比
C++和C的区别不仅仅在于语法,而是在编程思想上的转变。C++是面向对象的编程语言,其三大特性为封装、继承和多态。在本节中,我们将通过对比C和C++两种语言的Stack
实现来初步了解封装特性的优势。
4.1 C语言实现Stack
在C语言中,Stack
的实现需要使用struct
来定义栈的数据结构,并通过一系列函数来操作栈。数据和函数是分开的,操作时需要手动传递结构体指针来访问数据。
C语言Stack
的代码示例
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int STDataType;
typedef struct Stack {
STDataType* a;
int top;
int capacity;
} ST;
// 初始化栈
void STInit(ST* ps) {
assert(ps);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
// 销毁栈
void STDestroy(ST* ps) {
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
// 入栈
void STPush(ST* ps, STDataType x) {
assert(ps);
// 栈满时扩容
if (ps->top == ps->capacity) {
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
// 检查栈是否为空
bool STEmpty(ST* ps) {
assert(ps);
return ps->top == 0;
}
// 出栈
void STPop(ST* ps) {
assert(ps);
assert(!STEmpty(ps));
ps->top--;
}
// 获取栈顶元素
STDataType STTop(ST* ps) {
assert(ps);
assert(!STEmpty(ps));
return ps->a[ps->top - 1];
}
// 获取栈的大小
int STSize(ST* ps) {
assert(ps);
return ps->top;
}
int main() {
ST s;
STInit(&s);
STPush(&s, 1);
STPush(&s, 2);
STPush(&s, 3);
STPush(&s, 4);
while (!STEmpty(&s)) {
printf("%d\n", STTop(&s));
STPop(&s);
}
STDestroy(&s);
return 0;
}
C语言实现的特点
1.数据与操作分离:数据和操作函数是分开的,需要通过传递结构体指针来操作数据。
2.手动内存管理:程序员需要显式地进行内存分配和释放(使用
malloc
、realloc
和free
)。
3.没有封装性:所有数据都是公开的,容易被随意修改,缺乏保护机制。
4.2 C++语言实现Stack
在C++中,可以利用类的封装特性将数据和操作结合在一起,使得栈的实现更为简洁和安全。C++通过构造函数和析构函数自动管理内存,无需手动初始化和销毁栈。
C++实现Stack
的代码示例
#include <iostream>
#include <cassert>
using namespace std;
typedef int STDataType;
class Stack {
public:
// 构造函数:初始化栈
Stack(int n = 4) {
_a = new STDataType[n];
_capacity = n;
_top = 0;
}
// 析构函数:释放内存
~Stack() {
delete[] _a;
_a = nullptr;
}
// 入栈
void Push(STDataType x) {
if (_top == _capacity) {
Expand(); // 栈满时扩容
}
_a[_top++] = x;
}
// 出栈
void Pop() {
assert(_top > 0); // 保证栈不为空
--_top;
}
// 获取栈顶元素
STDataType Top() const {
assert(_top > 0);
return _a[_top - 1];
}
// 检查栈是否为空
bool Empty() const {
return _top == 0;
}
// 获取栈的大小
size_t Size() const {
return _top;
}
private:
STDataType* _a; // 动态数组存储栈元素
size_t _capacity; // 栈的容量
size_t _top; // 栈顶指针
// 辅助函数:扩容栈
void Expand() {
size_t newCapacity = _capacity * 2;
STDataType* newArray = new STDataType[newCapacity];
for (size_t i = 0; i < _top; ++i) {
newArray[i] = _a[i];
}
delete[] _a;
_a = newArray;
_capacity = newCapacity;
}
};
int main() {
Stack s;
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
cout << "栈顶元素: " << s.Top() << endl; // 输出4
s.Pop();
cout << "栈顶元素: " << s.Top() << endl; // 输出3
cout << "栈的大小: " << s.Size() << endl; // 输出3
// 继续弹出栈中元素
while (!s.Empty()) {
cout << s.Top() << " ";
s.Pop();
}
cout << endl;
return 0;
}
C++实现的特点
1.数据与操作封装在一起:通过类的封装将数据和操作结合,使得操作更加安全和方便。
2.自动内存管理:利用构造函数和析构函数自动管理内存,无需手动调用初始化和销毁函数。
3.访问控制:可以使用
private
关键字将类的内部数据隐藏,防止外部直接访问,确保数据安全。
4.代码简洁:操作栈时不需要手动传递结构体指针,成员函数会自动使用
this
指针访问类的成员。
4.3 C++与C实现的对比总结
封装性:C++通过类的封装将数据和操作整合在一起,并且可以控制数据的访问权限(public
、private
、protected
),从而提高了代码的安全性和可维护性。而在C语言中,所有数据成员都可以被外部随意修改,缺乏数据保护机制。
内存管理:C++使用构造函数和析构函数来管理资源,防止内存泄漏和资源浪费。而C语言需要手动管理内存,容易出现忘记释放资源的情况。
操作简便:C++使用面向对象的编程方式,使得操作对象更加直观。成员函数自动使用this
指针,代码更加简洁。而在C语言中,操作数据时需要显式传递结构体指针。
代码扩展性:C++的类支持继承和多态,可以通过继承扩展类的功能,使得代码复用性和扩展性更强。而C语言没有这种机制,只能通过函数指针等手段来模拟多态。
五、总结
本文介绍了C++类与对象的基础知识,包括类的定义、访问限定符、类的作用域、实例化的概念、对象的大小、this
指针的使用等内容。通过这些内容,我们初步了解了C++面向对象编程中的封装特性。C++中的类通过封装将数据和操作整合在一起,能够更好地保护数据的安全性并简化操作流程。同时,this
指针的使用也为操作对象提供了便利。
尽管C++相较于C语言有诸多优点,但它的面向对象特性还包括继承和多态等内容,这些特性在构建复杂系统时显得尤为重要。后续的博客将深入探讨这些高级特性,帮助大家更好地掌握C++面向对象编程的精髓。希望这篇博客对你有所帮助,欢迎持续关注!
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。