文章目录
- 前言
- 一.面向过程和面向对象
- 二.c++中的类
- 1.类的引入
- 2.类的定义
- 3.类的封装和访问限定符
- 4.类的作用域
- 5.类的实例化
- 6.类对象模型
- 三.`this`指针
- 1.`this`指针的引出
- 2.`this`指针的特性
- 3.C语言和c++实现栈Stack的对比
前言
在程序设计的广袤宇宙中,C++以其强大的功能和灵活性,成为众多开发者手中的利器。C++不仅继承了C语言的高效和直接操作硬件的能力,还引入了面向对象编程的概念,让代码的组织和管理变得更加清晰和高效。而在C++的面向对象体系中,类(class)无疑是最核心的概念之一。而本篇文章将初步学习类(class)。
一.面向过程和面向对象
C语言是面向过程的,关注的是解决过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
c++是面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间交互完成。
比如一个外卖系统,C语言关注的是点餐,下单,付款,送单等一系列的过程,而c++则关注的是顾客,商家,骑手等对象之间的交互。
但c++中的对象又是什么呢?
带着这个问题我们先来了解一下什么是类Class
。
二.c++中的类
1.类的引入
在C语言中我们知道结构体struct
中可以定义变量,而在c++中,struct
升级成了类,结构体中不仅可以定义变量,也可以定义函数,同时可以直接用类名不带struct
。以之前用C语言写的栈为例,用c++方式实现:
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
using namespace std;
typedef int STData;
struct Stack {
//成员函数
void StackInit(int capacity=4) {
_arr = (STData*)malloc(sizeof(STData) * capacity);
if (_arr == nullptr) {
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
void StackPush(STData x) {
if (_top == _capacity) {
STData* tmp = (STData*)realloc(_arr,sizeof(STData) * _capacity * 2);
if (tmp == nullptr) {
perror("malloc fail");
return;
}
_arr = tmp;
_capacity *= 2;
}
_arr[_top++] = x;
}
void StackPop() {
if (_top == 0) {
return;
}
_top--;
}
STData StackTop() {
if (_top == 0) {
return NULL;
}
return _arr[_top-1];
}
void StackDestroy() {
free(_arr);
_arr = nullptr;
_top = 0;
_capacity = 0;
}
//成员变量
STData* _arr;
int _top;
int _capacity;
};
int main() {
Stack st;
st.StackInit(10);
st.StackPush(1);
st.StackPush(2);
st.StackPush(3);
st.StackPush(4);
st.StackPush(5);
st.StackPush(6);
while (st._top != 0) {
cout << st.StackTop() << " ";
st.StackPop();
}
st.StackDestroy();
return 0;
}
上面用结构体的定义,在c++中更喜欢用class
来替代。
2.类的定义
类通过关键字class
来定义,后面跟着类名classname
和一对花括号{}
,花括号内的为类的主体也就是类的成员声明。注意花括号后的;
不要省略。
class classname{
//成员函数
void fun(){
....
}
....
//成员变量
int _a;
char _b;
....
};
类体中的内容为类的成员:
- 类中的变量称为类的属性或者成员变量,这些是类的数据部分,用于存储对象的状态信息。
- 类中的函数称为类的方法或者成员函数,这些是类的行为部分,用于描述对象可执行的操作。成员函数可以访问和修改成员变量的值。
类有两种定义方式:
-
声明和定义全部放在类体中:
class Data{ void dataprint(){ cout<<_year<<_month<<_day<<endl; } int _year; int _month; int _day; };
-
声明(
.h
)和定义(.cpp
)放在不同的文件中:在.cpp文件定义时,成员函数名前要加
类名::
//.h文件 class Data{ //声明 void Dataprint(); int _year; int _month; int _day; }; //.cpp文件 //定义 void Data::Dataprint(){ cout<<_year<<_month<<_day<<endl; }
成员变量命名规则建议:
如果成员变量名和成员函数参数名相同时就会容易混淆不易区分,比如:
class Data{
void Dataprint(int year){
year=year;
cout<<year<<month<<day<<endl;
}
int year;
int month;
int day;
};
为了解决这种情况,通常习惯加一些字符,比如一些公司要求成员变量名前加_
,有的会将_
加在成员变量名后,有的也会用其他字符区分。主要还是看公司要求,我们在日常练习时,可以根据自己喜好来设定。
class Data{
void Dataprint(int year){
_year=year;
cout<<_year<<_month<<_day<<endl;
}
int _year;
int _month;
int _day;
};
3.类的封装和访问限定符
前面我们了解到c++是面向对象的,而面向对象具有三大特性:
封装,继承,多态
在类和对象阶段,我们首先来学习类的封装特性。
封装是面向对象编程的一个基本原则,在c++语言实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部的实现细节,控制那些方法可以在类外部直接使用,提高代码的安全性和可维护性。
注意:通过访问权限限定的某些方法虽然不能在类外部直接使用,但是在类内部还是可以访问的。
而如何实现封装呢?
这就要借助c++的访问限定符,public
(公有),protected
(保护),private
(私有)。比如:
class Data{
//公有
public:
void Dataprint();
//私有
private:
int _year;
int _month;
int _day;
};
public
修饰的成员在类外可以直接被访问。protected
和private
修饰的成员在类外不能直接被访问,但是protected
成员在派生类(子类)中可以访问,private
成员只能在类内部访问。- 访问权限作用域从该访问符出现的位置开始直到下一个访问限定符出现时为止。如果后面没有访问限定符,作用域就到
}
也就是类结束为止。class
的默认访问权限为private
,struct
的为public
4.类的作用域
在c++入门的时候我们学过一个新的域,叫命名空间域,而现在我们了解到类之后要再认识一个新的域,也就是类域
类定义了一个新的作用域为类域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用::
域作用限定符来指明该成员属于哪个类域。
class Data{
public:
void Dataprint();
private:
int _year;
int _month;
int _day;
};
//这里需要指定Dataprint是属于Data这个类域中
void Data::Dataprint(){
cout<<_year<<_month<<_day<<endl;
}
5.类的实例化
类定义了对象的类型,但类本身不是对象,因此我们要创建对象。
而用类类型创建对象的过程,就叫类的实例化。
-
类是对对象进行描述的,是一个模型一样的东西,限定了类都有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。而类实例化对象,就是为该对象开辟空间来进行存储。
//以实例化一个栈为例 class Stack{ public: void Init(); void Destroy(); ... private: int*a; int top; int capacity; }; int main(){ //类实例化对象/对象定义 Stack st; }
-
一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量
capaciyt
是没用空间的,只有实例化的对象st1
才有具体的容量。//以实例化一个栈为例 class Stack{ public: void Init(); void Destroy(); ... //类的成员变量是声明不是定义 //声明和定义的区别是,声明不开空间而定义开空间 int*a; int top; int capacity; }; int main(){ //类实例化多个对象 Stack st1; Stack st2; //错误,声明没有开辟空间不能存储数据 Stack::capacity=4; //正确 st1.capacity=4; }
-
做个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就是设计图,只是设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
6.类对象模型
我们知道类中既有成员变量又可以有成员函数,那么一个类的对象中包含了什么?而一个类的大小又该怎样计算?
我们先假设类对象的存储方式为包含类的各个成员:
如果是上面这种情况,每个对象的成员变量不同,但是会调用相同的函数,按照这种方式存储,当一个类创建多个对象时,每个对象都有一份相同的函数,就会大大浪费空间,那么如何解决呢?
每个对象只存储成员变量,没有存储成员函数,成员函数存放在公共的代码段
上面这种存储方式大大节省了内存空间同时又提高了代码的可重用性。
明白了上面的之后我们在来看以下几种情况:
//类中既有成员变量,又有成员函数
class A1{
public:
void f1();
private:
int _a;
};
//类中只有成员函数
class A2{
public:
void f2();
};
//类中什么都没有,也就是空类
class A3{
};
上面这三种情况类的大小:
sizeof(A1)=4
;sizeof(A2)=1
;sizeof(A3)=1
;
结论:一个类的大小,实际就是该类中成员变量之和,注意内存对齐。而空类或者是没有成员变量的大小为1字节,是为了占位,表示对象存在,但不存储有效数据。
三.this
指针
1.this
指针的引出
我们先来看一下下面这个日期类Data
class Data {
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() {
Data d1;
Data d2;
d1.Init(2024, 10, 18);
d2.Init(2024, 10, 19);
d1.print();
d2.print();
return 0;
}
在上面这段代码中,我们定义了两个日期类对象d1
和d2
,Data
类中有两个成员函数Init
,print
,d1
和d2
都调用了这两个函数,那么又是如何对这两个不同的对象调用相同的函数进行区分呢?
c++中为了解决这种问题引入了this
指针,对于非静态成员函数来说,他们实际上都隐含了一个指向当前对象的指针,也就是this
指针,这个this指针在函数调用时自动传递,指向调用该成员函数的对象。这样,尽管成员函数的代码在公共段,他们依然能够通过this指针来访问和操作特定对象的成员变量。
2.this
指针的特性
-
this
指针的类型为:类类型*const,也就是成员函数中,不能给this
指针赋值。 -
this
指针本质上是成员函数的形参,当对象调用成员函数时,将对象地址作为实参传递给this
指针,所以对象中不存储this
指针。 -
this
指针允许在成员函数内部使用,但不能在形参和实参显示传递,一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递。 -
this
指针是形参,所以this指针和普通参数一样存在函数调用的栈帧里,调用结束时,栈帧销毁,this
指针也会销毁。 -
this
指针可以为空。但是不能在调用的成员函数中对其解引用。比如下面两段代码:运行正常:
class A1 { public: void print() { cout << "print()" << endl; } private: int _a; }; int main() { A1* p = nullptr; p->print(); return 0; }
运行崩溃:
class A1 { public: void print() { cout << _a << endl; } private: int _a; }; int main() { A1* p = nullptr; p->print(); return 0; }
为什么这两段代码的结果不同,虽然对象指针p
都为空,p
调用成员函数时作为实参传递该this
指针,但是第一种情况,在成员函数中,this
指针为空但没有发生解引用所以正常运行,而第二种情况this
指针为空并且发生了解引用,所以运行崩溃。
3.C语言和c++实现栈Stack的对比
-
C语言实现:
//stack.h头文件: #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int STData; typedef struct Stack1 { STData* array; int top; int capacity; }Stack1; void _InitStack(Stack1*st); bool _IsEmpty(Stack1*st); void _PushStack(Stack1*st,int x); void _PopStack(Stack1* st); STData _TopStack(Stack1* st); void _DestroyStack(Stack1* st); void _checkcapacity(Stack1* st); //stack.cpp定义实现文件: void _InitStack(Stack1* st) { assert(st); st->array = (STData*)malloc(sizeof(STData) * 4); if (st->array == nullptr) { perror("malloc fail"); return; } st->top = 0; st->capacity = 4; } void _checkcapacity(Stack1* st) { assert(st); if (st->top == st->capacity) { STData* tmp = (STData*)realloc(st->array, sizeof(STData) * (st->capacity) * 2); if (tmp == nullptr) { perror("malloc fail"); return; } st->array = tmp; st->capacity *= 2; } } bool _IsEmpty(Stack1* st) { assert(st); return st->top==0; } void _PushStack(Stack1* st, int x) { assert(st); _checkcapacity(st); st->array[st->top++] = x; } void _PopStack(Stack1* st) { assert(st); if (_IsEmpty(st)) { return; } st->top--; } STData _TopStack(Stack1* st) { assert(st); if (_IsEmpty(st)) { return 0; } return st->array[st->top - 1]; } void _DestroyStack(Stack1* st) { free(st->array); st->array = NULL; st->top = 0; st->capacity = 0; } //test.cpp测试文件: int main() { Stack1 st1; _InitStack(&st1); _PushStack(&st1, 1); _PushStack(&st1, 2); _PushStack(&st1, 3); _PushStack(&st1, 4); _PushStack(&st1, 5); _PushStack(&st1, 6); while (!_IsEmpty(&st1)) { printf("%d ", _TopStack(&st1)); _PopStack(&st1); } _DestroyStack(&st1); return 0; }
在用C语言实现时,Stack相关操作函数有以下共性:
- 每个函数的第一个参数都是Stack1*
- 函数中必须对第一个参数检查,判断是否为空
- 函数中都是通过Stack1*参数操作栈的
- 调用函数时必须传递Stack1结构体变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据 的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。
-
c++实现:
//stack.h头文件: #include<stdio.h> #include<stdlib.h> #include<iostream> using namespace std; typedef int STData; class Stack { public: void InitStack(); bool IsEmpty(); void PushStack(int x); void PopStack(); STData TopStack(); void DestroyStack(); private: void checkcapacity(); STData* _a; int _top; int _capacity; }; //stack.cpp定义实现文件: #include"stack.h" void Stack::InitStack() { _a = (STData*)malloc(sizeof(STData) * 4); if (_a == nullptr) { perror("malloc fail"); return; } _top = 0; _capacity = 4; } void Stack::checkcapacity() { if (_top == _capacity) { STData* tmp = (STData*)realloc(_a,sizeof(STData) * _capacity * 2); if (tmp == nullptr) { perror("malloc fail"); return; } _a = tmp; _capacity *= 2; } } void Stack::PushStack(int x) { checkcapacity(); _a[_top++] = x; } bool Stack::IsEmpty() { return _top == 0; } void Stack::PopStack() { if (IsEmpty()) { return; } _top--; } STData Stack::TopStack() { return _a[_top - 1]; } void Stack::DestroyStack() { free(_a); _a = nullptr; _top = 0; _capacity = 0; } //test.cpp测试文件: #include"stack.h" int main() { Stack st; st.InitStack(); st.PushStack(1); st.PushStack(2); st.PushStack(3); st.PushStack(4); st.PushStack(5); while (!st.IsEmpty()) { cout << st.TopStack() << " "; st.PopStack(); } st.DestroyStack(); return 0; }
c++中通过类可以将数据和操作数据的方法进行有机结合,通过访问权限可以控制那些方法在类外可以被调用,也就是封装。而且通过this指针,不需要传递Stack*的参数,编译器编译之后该参数会自动还原。使用起来会非常方便。
以上就是关于c++类初步的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!