前言
上一期我们介绍了一些C++入门的基础知识,本期我们来介绍面向对象。初步认识一下面向对象和面向过程、类、以及封装!
本期内容介绍
面向过程和面向对象
类的引入
类的定义
类的访问限定符和封装
类的作用域
类的实例化
类对象模型
this指针
一、面向过程和面向对象
面向过程(C语言)关注的是过程,即分析出求解问题的步骤,通过函数调用逐步解决问题。而面向对象(C++)关注的是对象,即解决的事情被分成不同的对象,各个对象配合完成。
以前我也不怎么理解面向对象和面向过程!后来看到了很多例子后逐渐有了理解!我来举一个我以前理解最好的例子 --- 洗衣服。
面向过程:用面向过程来洗衣服的步骤:找盆 ---> 接水 ---> 放洗衣液 ---> 放衣服 ---> 手搓 ---> 换水 ---> 再洗 ---> 换水 ---> 拧干 ---> 晾晒。你得关注每一步的过程
面想对象:用面向对象来洗衣服的步骤:人、衣服、洗衣机、洗衣液。整个过程就是人将衣服洗衣液,放到洗衣机中启动洗衣机,洗衣机洗好了会烘干!具体的洗衣服和烘干过程你根本不用关心,这是洗衣机的事情!
当然还有其他同类的例子,例如把大象装到冰箱里面!
二、类的引入
C语言的struct只能定义变量,在C++中不仅可以定义变量,而且可以定义函数。例如我们以前C语言中玩的栈,就是只能在结构中定义变量,而C++中可以定义函数!
C语言版本:
typedef int STDataType;
struct Stack
{
STDataType* a;
int top;
int capacity;
};
他的方法只能在外面!而C++就可以在里面!
C++版本:
#include <iostream>
typedef int STDataType;
struct Stack
{
STDataType* _a;
int _top;
int _capacity;
void Init(int capacity = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * capacity);
if (_a == nullptr)
{
perror("malloc failed");
exit(-1);
}
_capacity = capacity;
_top = 0;
}
//...
};
上述操作可以,但C++中更喜欢用class来替代struct
三、类的定义
class ClassName
{
//成员变量和成员方法(函数)
};//注意封号
class是C++中定义类的关键字,ClassName 是类的名字, {}中为是类的主体。和struct一样结束的 ; 不能省略
类体中(即{}中)的内容被称为类的成员,成员被分为变量和函数。成员变量又被称为成员的属性,用来描述该对象的特征!成员函数又被成为成员方法,是描述类的行为的!
类的两种定义方式
1、声明和定义全部放在类体中,需要注意的是:成员函数在类体中默认是内联的!
class Student
{
public:
//成员方法
void sleep()
{
cout << _name << "正在sleep()..." << endl;
}
private:
//成员属性
char* _name;
char* _sex;
int _age;
};
当然这里用struct也可以定义类!
2、声明和定义分开即声明放在.h,定义在.cpp中时,在定义的成员函数名前需要加类名和作用域限定符::,来指定是哪个域的!
小tips: 我们一般建议使用第二种
成员变量的命名规则
我一般习惯时在成员属性的成员的前面加一个下划线,这个的作用主要是区分形参和成员属性的。具体怎么加,看你的想法,以及需求,只要能分开都可以!但也要符合我们以前C语言介绍过的标识符命名规则!如下就是C语言的表示符命名规则:
- 标识符只能由字母、数字和下划线组成。
- 标识符必须以字母或下划线开头,不能以数字开头。
- 标识符区分大小写。
- 标识符的长度没有限制,但只有前31个字符有效。
- C语言的关键字(如if、for、while等)不能用作标识符。
- 标识符不能包含空格或其他特殊字符(如@、#、$等)。
- 一般情况下,标识符应具有描述性和易于理解的名称。
四、类的访问限定符和封装
访问限定符
C++中访问限定符有三个,分别是:public(公有的),protected(被保护的),private(私有的)
访问限定符的说明
public修饰的成员在类外可以直接访问
protec和private修饰的成员再类外面不能直接访问(目前认为他两一样,到后面介绍了继承、多态再解释他两的区别)
访问权限的作用域从该访问限定符开始到下一个访问限定符结束, 如果没有出现下一个访问限定符,则到}终止
class的默认访问权限是private的,struct为public的(因为struct要兼容C语言)
注意:访问限定符只要编译时有效,当数据映射到内存后,没有任何访问限定符上的区别
OK, 举个栗子看看:
public(在类外可以访问,以属性举例,方法同理):
class Stack
{
public:
void Init(int capacity = 3)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc failed");
exit(-1);
}
_size = 0;
_capacity = capacity;
}
void Push(int x)
{
//...
}
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack s;
s.Init();
cout << s._size << endl;
return 0;
}
这里把class直接换成struct也可以!
private/protectedd当前认为是一样的,在类外不可访问:
C++中struct和class的区别是什么?
C++需要兼容C语言,所以C++中的struct可以当成结构体使用。另外C++中的struct还可以和class一样用来定义类。区别是struct定义类的访问权限是public,class定义的类的访问权限是private。另外在继承和模板参数列表位置也是与区别的(后期介绍了会在拿出来介绍的)!
注意:在C++中如果用struct定义结构体的话,在结构体类在申明结构体变量时,不用在+struct了
struct SLNode
{
int* data;
SLNode* next;
};
原因是:C++对struct进行了升级变成了类,用它定义的就是一个类型,所以就可以直接用,C语言是struct _name{}才是一个类型!
封装
理论层面:C++实现封装的方式:用类将对象的属性和方法结合到一块,让对象更加完善,通过访问权限来隐藏内部具体的实现细节,选择性的把其部分接口提供给外部调用者(用户)。
代码层面:在C++类的内部,把不想让类外部直接访问的成员用private修饰,外部要访问只能使用提供公开的接口!
封装的本质是一种管理,为了让用户使用起来方便!
OK,来举一个栗子。你现在看这篇博客的手机或电脑,它的组成较为复杂,他只给用户提供开机键,输入输出设备即键盘显示器等让用户和计算机能进行交互的接口,但他真正工作的是CPU显卡等硬件!但你用关心这些吗?是不是不用啊!你只需要在人家给你的提供的接口上操作即可,管他的谁工作、如何工作。只要能完成你的操作即可~!
五、类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类外面定义时,需要使用::作用域限定符来指定成员属于哪个类!
class Date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print();
private:
int _year;
int _month;
int _day;
};
//这里注意要指定Print属于Date这个类
void Date::Print()
{
cout << "999" << endl;
}
六、类的实例化
用类类型创建对象的过程,称为类的实例化。
//类
class Date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;//实例化一个对象
d.Init(2023, 11, 25);
d.Print();
return 0;
}
1、类是描述对象的,是一个模型(模板),它限制了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储他。例如:开学时填写的学生信息表,这个表可以看成一个学生类,他来描述具体的学生(对象)!
2、一个类可以实例化多个对象,实例化出来的对象占用实际的物理空间,存储类成员变量
例如:刚刚上面说的把学生开学的信息表看做一个学生类的话,每登记一个学生就实例化了一个对象,而这个表可以被很多学生填写(实例化多个对象)!每个实例化出来的学生对象才有自己的实际空间(例如宿舍),这些学生有属于自己的年龄、姓名、性别等成员属性!
或者:你可以把盖楼房的图纸理解成一个类,按照这个图纸可以修很多的房子(实例化多个对象),图纸本身不占实际的空间,真正占空间的是按照图纸盖的房子,这些实例化的房子有客厅、卧室等属性!
七、类对象模型
上面我们已经清楚了类的定义以及其实例化对象。一个对象有成员属性也有成员的方法,而我们想知道的是一个对象包含了什么?
OK,关于这个问题我就直接先说结论,再解释了:
一个对象包含了成员属性和成员方法。成员属性是对象自身的属性(成员变量),也就是说成员属性属于对象。而成员方法是这个类都有的行为是属于类(所有类的对象都有成员)。
我用一个我家鸽鸽的例子来说明:我家鸽鸽属于人这个类,他的名字和性别等是他个人独属于的!他的成员方法(行为)有:唱、条、rap、打篮球。但只有我家鸽鸽会唱、条、rap、打篮球吗?你是不是也可以啊,所以类的成员方法属于类!
class People
{
public:
//方法属于整个类
void Sing()
{
cout << "鸡你太美~..." << endl;
}
private:
//属性属于对象本身
char _name[20];
char _sex[5];
int age;
};
如何计算一个类的大小?
可以用sizeof这个操作符来计算类的大小!一个类的大小是该类中的成员变量之和(不包含静态成员变量,虽然静态成员变量属于类但对类的大小无影响),当然要注意内存对齐!如果是没有成员变量的类,编译器会给一个字节来唯一标识这个类!
//既有成员变量又有成员方法的类
class A
{
public:
void Print()
{
//...
}
private:
int a;
};
//只有成员方法没有成员变量的类
class B
{
public:
void Print()
{
//...
}
};
//既没有成员变量有没有成员方法的类(空类)
class C
{
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
return 0;
}
目前我们可以认为类的大小和对象的大小是一样的!一个是在图纸上算空间,一个是实地测量!
结构体内存对齐规则(和C语言的一样):
1.结构的第一个成员变量永远放在相较于结构体起始位置偏移量为0处!
2.从第二个开始,后面的每个元素都要对齐到某个对齐数的整数倍处!对齐数:结构体成员自身大小和默认对齐数的较小值!
在VS上默认对齐数是8,而在gcc是没有默认对齐数的(对齐数就是成员的自身大小)!
3.结构体的总大小,必须是最大对齐数的整数倍。最大对齐数:所有成员的对齐数中的最大值!
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍(包含嵌套结构体的对齐数)!
如果这块还有问题,请点击后面的链接,这篇博文中我详细介绍过它的计算和栗子:
内存对齐
八、this指针
this指针的引出
我们先来看下面 一段代码的结果:
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(2023, 11, 25);
d1.Print();
Date d2;
d2.Init(2023, 11, 26);
d2.Print();
return 0;
}
Init和Print是类的成员方法,我们并没有指定是哪个对象的,为什么我们在调d1.Init的时候就是给d1对象赋值而不是给d2初始化呢?我们在d2.Print的时候是打印d2的属性的信息,为什么不打印d1会随机值呢????他到底是如何区别的呢?
C++为了解决上述的问题引入了this指针来解决!C++编译器给每一个非静态的成员函数都增加了一个隐藏的指针参数,让这个指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有的成员变量的操作,都是通过该指针去访问的。只不过所有的操作对用户是不可见的,也不需要用户手动传,编译器会自动完成!!
this指针的特性
1、this指针的类型:类类型* const ,即此时的const修饰this,只能在形参接受被实参拷贝的时候赋值一次,在成员函数中,不能给this赋值
2、this只能在成员函数(非静态成员函数,静态的成员函数没有this后面会再解释)内部使用,可以显示的写
3、this指针本质上是成员函数的形参(非静态成员函数),当对象调用成员函数,将对象的地址会传给this,所以对象不存储this指针
4、this指针是成员函数(非静态成员函数)第一个隐含的指针参数(形参),一般情况由寄存器ecx自动传递,调用者不需要手动传递!
this指针存在哪里?this指针可以为空吗?
这应该是这里很坑的两个问题了,对基础考察很细!OK,我还是先说结论,再用栗子来解释!
this指针是形参,存储在函数栈帧上!this可以为空,但为空时不能去访问成员属性否则就是则空指针异常操作了!
OK,两个栗子分别解释一下:
栗子1:下列程序的编译运行的结果是?A、编译报错 B、运行奔溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
OK,他是C、正常运行!我们来看一下:
原因是:这里虽然p是nullptr但他是个对象指针,他->Print是调用类的方法,当然没有问题!他调用Print传参的时候的确是nullpttr,this指针接收的是nullptr但他没有访问成员属性啊!所以不会报错!正常运行!
这里可能有点绕,我来举个栗子,这里的p假设是一个叫张三驴的学霸,它的分数是700,他可不可以报清华?当然可以。人家之人你的分数,不管你的人。这里也是一样,人家直看你是不是类的对象,不管你是不是nullptr 。咱们的张三驴同学虽然收到了很多学校的抢人电话并提前发了录取通知书,但他不去上,是不是就和他没有关系了。这里也是一样,我this的确是nullptr但我没用它访问成员变量呀!
栗子2:下列程序的编译运行的结果是?A、编译报错 B、运行奔溃 C、正常运行
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
OK,这里是B、运行崩溃!
原因是对空指针进行了操作!和上面的不一样的是下面的Print访问了成员员变量_a,这就出问题了,this是空,指向一块不存在的内存,你咋访问!就出问题了!还是上面的例子:我们的张三驴同学不小心收到了清京大学,一激动以为是那两所顶尖高校合作的一个学校。可高兴了,结果把学费一交都查不到学校地址~.....
C语言和C++实现栈的对比
C语言
#include <assert.h>
typedef int DataType;
typedef struct Stack
{
DataType* array;
int capacity;
int size;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
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;
}
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;
}
可以看到,在用 C 语言实现时, Stack 相关操作函数有以下共性:每个函数的第一个参数都是 Stack*函数中必须要对第一个参数检测,因为该参数可能会为 NULL函数中都是通过 Stack* 参数操作栈的调用时必须传递 Stack 结构体变量的地址结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即 数据和操作数据的方式是分离开的 ,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。
C++:
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;
}
C++ 中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在 类外可以被调用,即封装 ,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。而且每个方法不需要传递 Stack* 的参数了,编译器编译之后该参数会自动还原,即 C++ 中 Stack * 参数是编译器维护的, C 语言中需用用户自己维护 。