前言
本期打C++学习的第一个大boss——类与对象!这一块内容第一次接触确实绕,一起看看吧
内容概览:
- 面对对象的概念
- 类和对象
- 和结构体的对比
- 成员
- 定义
- 访问权限
- 封装
- 类域
- 类对象模型
- this指针
- Stack再实现
1. 面向对象的概念
对比面向过程,面向过程是关注步骤,把问题分解,调用函数逐步解决;面向对象是关注对象,解决问题依靠对象之间的交互完成
好处:更好地抽象,更好地建模
2. 类和对象
2.1 类的介绍
和C中的结构体对比一下,因为类很像是结构体的“升级”,先看看C++对结构体增加了什么东西
C++中的结构体
:结构体内可以声明、定义函数
C++中的类
-
C++中,称上文的结构体为 类,类和结构体一样,都是声明一个类型
-
创建结构体变量叫创建,而“创建”一个对象,叫做实例化对象——用类(类型)来创建一个对象(变量),把类型变成实实在在的一个“例子”
-
类也定义了一个作用域,叫类域
类的所有成员都在此作用域内,类外用需要用 :: 指定类域
2.2 成员
结构体内可定义变量和函数,就称类,结构体有了“新名字”,其中的内容也有
- 原结构体的成员变量,称 属性
- 原结构体的成员函数,称 方法
所以:类包含属性和方法。
2.3 声明
简单讲了什么是类,来看看类怎么声明的
class People
{
void ShowInfo()
{
cout << name << '-' << age << '-' << sex << endl;
}
char* _name;
int _age;
char* _sex;
};
- class :关键字
- People:类名
- void ShowInfo( ): People类的方法
- _name,_age,_sex:People类的属性
诶?属性名前面加个下划线干嘛?
命名规范
:为了区分成员变量和形参
class Date
{
public:
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
int year;
int month;
int day;
};
int main()
{
Date d;
d.Init(2022, 9, 30);
return 0;
}
这样就晕了,简直没有可读性了,所以为了区分类的属性和形参,通常要做标识,可以是_name、mName、m_name之类
如果重名会如何?
编译器会把它识别为局部变量, 而不会识别为成员变量,若要区分,用this
声明定义分离
一般都要声明定义分离,为了可读性,但在类外给出方法的定义时,要指定这个定义是哪个类的方法定义(用域作用限定符)
void People::ShowInfo()
如果不分离,在对象内定义,可能会被视为内联函数
2.4 封装
本质是一种 严格管理
诶,那我自由管理,像C语言一样,不行吗?
孰优孰劣,也分情况,不过可以参照一下 中米疫情…
多数情况还是严格管理更优
2.5 访问权限
而为了进一步封装,又产生了一个关于类的新东西:访问修饰限定符.…
有
- public:类内类外都能访问(类外访问要用作用域限定符指定)
- private:类外不能访问
- protected:后续再讲,现在讲不透
class People
{
public://限定访问权限为公有
void ShowInfo()
{
cout << name << '-' << age << '-' << sex << endl;
}
private://限定访问权限为私有
char* _name;
int _age;
char* _sex;
};
把属性放到private,保护它们,做到封装(如果放到public,数据不安全,容易被动)
注意:
-
类中如果不限定,默认私有
-
结构体中如果不限定,默认公有(为了兼容C)
2.6 类对象的模型
我们已经学过,结构体中的变量经过内存对齐保存在结构体变量里,那类的对象中,方法、属性是如何存储的呢?
可以做出如下猜测
- 方法、属性都存在对象内
- 属性存在对象内,对于方法, 只存它的地址
- 只有属性存在对象内,方法放在代码段
分析:
- 显然不合理,若方法定义较长,再实例化10000个对象,就多了9999份冗余重复的代码
- 好像还行,但是这个地址是不是一定要存呢?其实没必要
- 此方法是C++对象存储采用的方案,方法定义放进代码段,编译后方法直接成为二进制指令,需要call,就jump到代码段对应位置执行指令(不会造成任何空间浪费)
知道了对象的存储模型,如何算对象大小?
对象大小的计算
方法是不用计算了,因为直接放在代码段,而属性的存储方式和结构体一模一样,要进行内存对齐
复习一下内存对齐:依照对齐数对齐
-
第一个成员在与结构体偏移量为0的地址处。
-
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8 -
结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
-
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
2.7 this指针
还有一个问题,我们方法全放在代码段了,不同对象调用同一块指令,执行的时候,方法本身怎么知道它是在给哪个对象干活呢?
:this指针就是解决此问题的
this指针:对象调用其方法的时候,会隐式地传递一个对象的指针作为形参,所有访问属性的操作都经过this指针
这不就是把C语言中老是要传传传的结构体指针给咱们自动传了嘛!美滋滋
现在就可以得出:
- 访问对象成员:通过this指针
- 调用对象方法:直接跳到代码段执行指令
看完这么妙的东西,再来两道题目考察一下:
【this指针存在哪里?】
:堆?代码段?都不是,而是栈!为什么?this是形参,存在栈帧内!
【this指针可以为空吗?】
:有时在纯语法上来说可以的——
- 访问属性时要解引用this,这时候this肯定不能为空
- 但调用方法时不经过this,这时候this在语法上可以为空
// 1.下面程序编译运行结果是?
//A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
答案是,C
虽然p作为this指针为空,但是程序中只调用方法,没有访问属性,不需要解引用this指针,也就不会报错
// 2.下面程序编译运行结果是?
// A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
答案是,B
p作为this指针为空,程序中访问了属性,需要解引用this指针,也就会运行崩溃
3. Stack再实现
//Stack.h
#include <iostream>
#include <assert.h>
using namespace std;
typedef int STDataType;
class Stack
{
public:
void Init(int capacity);
void Push(STDataType x);
void Pop();
STDataType Top();
bool Empty();
int Size();
void Destroy();
private:
void CheckCapacity();
STDataType* _a;
int _capacity;
int _top;
};
//Stack.cpp
#include "stack.h"
void Stack::Init(int capacity)
{
_a = (STDataType*)malloc(sizeof(STDataType) * capacity);
assert(_a);
_capacity = capacity;
_top = 0;
}
void Stack::CheckCapacity()
{
if (_capacity == _top)
{
int newCap = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * newCap);
assert(tmp);
_a = tmp;
_capacity = newCap;
}
}
void Stack::Push(STDataType x)
{
CheckCapacity();
_a[_top++] = x;
}
void Stack::Pop()
{
assert(!Empty());
_top--;
}
STDataType Stack::Top()
{
return _a[_top - 1];
}
void Stack::Destroy()
{
if (_a)
{
free(_a);
_a = NULL;
_capacity = _top = 0;
}
else
{
cout << "栈已空!\n" << endl;
}
}
bool Stack::Empty()
{
return _top == 0;
}
//test.cpp
int main()
{
Stack s;
s.Init(4);
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
s.Push(5);
s.Pop();
cout << s.Top() << endl;
s.Pop();
cout << s.Top() << endl;
s.Pop();
cout << s.Top() << endl;
s.Destroy();
return 0;
}
:4
3
2
可以看到,
- 函数命名简洁
- 访问类的属性,通过自动传递的this指针,不用像之前每次传结构体指针,方便
今天的分享告一段落了,如有错落不足的地方,望请斧正~
浅尝了类和对象的甜头,后面还有更带劲的,我们一起努力吧!
这里是培根的blog,期待与你共同进步!
下期见~