文章目录
- 1、面向对象和面向过程的初步理解
- 2、类的引入
- 3、类的定义
- 4、类的访问限定符及封装
- 1、访问限定符
- 2、封装
- 5.类的实例化
- 6、类对象模型
- 7、this
- 1、this指针
- 2、空指针问题
- 3、C语言和C++简单对比
1、面向对象和面向过程的初步理解
C语言是一个面向过程的语言,C++是一个面向对象的语言,但不是纯粹的面向对象。C++兼容C,所以过程或者对象都可以写。用洗衣服来举例子,面向过程的思路就是想象我要做的所有事,往洗衣机倒入洗衣液,倒水,把衣服放进去,选择哪个按钮,等待洗完,再甩干等等。而面向对象的思路就是总共有四个对象,人,洗衣液,洗衣机,衣服,人只要想好和每个对象是如何交互地即可,不去想应当选择哪个按钮等等。面向对象注重的是对象之间的交互。
2、类的引入
C语言的结构体里面只能放入数据,而C++的结构体不仅可以放入数据,还可以放入函数,一个叫成员变量,一个叫成员函数。C++把结构体升级成了类。
C语言中栈的实现是一个结构体,然后外面再跟很多函数的声明,另一个文件放定义。C++会是这样实现。
struct Stack
{
//数据和方法在一起
//成员函数
void Init(int n = 4)
{
a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == a)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
size = 0;
}
void Push(int x)
{
a[size] = x;
}
//成员变量
int* a;
int size;
int capacity;
};
在这个结构体里,同时拥有函数和变量,我们也不需要写StackInit作为函数名,因为这是在Stack这个类里的,系统知道这是栈的初始化函数。如果要创建一个栈,那么直接Stack st即可,因为C++把这个Stack就当作类型。类是一个整体,无论放到哪里,系统都会去全局搜索它。
使用的时候
int main()
{
Stack st;//st就是一个对象
st.Init();
st.Push(1);
st.Push(2);
st.Push(3);
return 0;
}
由于上面的Init是由缺省参数的,所以这里可以不用传参。
3、类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 注意后面要有分号
实际上C++中定义类是用class这个关键字的,ClassName就是类名,类体中的内容称为类的成员,类中的变量称为类的属性或成员变量,类中的函数称为类的方法或者成员函数。
上面是成员函数定义到类里面,那么如C语言一样,C++是否也可以声明和定义分离?
Stack.h
#pragma once
#include <stdlib.h>
#include <iostream>
using namespace std;
struct Stack
{
void Init(int n = 4);
void Push(int x);
int* a;
int size;
int capacity;
};
Stack.cpp
#include "Stack.h"
void Stack::Init(int n)
{
a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == a)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
size = 0;
}
void Stack::Push(int x)
{
a[size] = x;
}
这里面加上::是让编译器知道,这个函数栈类的成员函数,那么编译器就会去类里面找这个函数。::也是标明了类的作用域是在哪里。
test.cpp
#include "Stack.h"
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Push(2);
st.Push(3);
return 0;
}
4、类的访问限定符及封装
1、访问限定符
Stack.h文件里,可不可以把struct换成class?换了后就是这样:
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问限定符有private(私有),protected(保护), public(公有)三个,现阶段可以把保护和私有看成一个。看下面的规则。
【访问限定符说明】
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
不管是用struct还是class,我们都可以定义类的内容为公还是私。
这样程序就跑得开了。
struct因为适配C语言,所以它即可以把后面的Stack作为类型,也可以struct Stack作为类型。
实际写的时候,类里面函数参数和变量名字冲突时,会改一下变量名字,比如加上_,如果不改的话,编译无法通过, 这时候编译器遵循局部优先的规定,而函数形参更优先,所以在函数里,相当于形参给形参赋值,就会出问题。
2、封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质是为了一种更好的管理。
封装后面再提。
5.类的实例化
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;//声明
int _month;
int _day;
};
类中的成员变量,实际上都是在声明,即使里面有成型的函数,也只是在声明,如果在main函数中创建了一个这个classname类型的变量,那么此时类就实例化了,也就是开空间了,那么这些定义的变量和函数才在系统空间中存在。
形象化的例子就是建筑图纸,图纸上把要做的模样都已经写了出来,但是这个图纸不是实际的房子,实例化就是按照图纸建造房子。类像是一个计划,而实例化则是实际的行动。
Date._year = 1;
Date::_year = 0;
所以像这样是不行的,我们无法把数据存入数据里。
int main()
{
Date d1;
Date d2;
d1.Init(2023, 2, 2);
return 0;
}
这样即可。
6、类对象模型
上面的类中,成员函数是定义的,不是声明,那么实例化也是把这个函数实例化出来了吗?
int main()
{
Date d1;
Date d2;
d1.Init(2023, 2, 3);
cout << sizeof(d1) << endl;
return 0;
}
结果是12.所以可以看出算的只是三个int类型的变量,成员函数并没有计算。那为什成员函数不在对象里,而成员变量在对象里?
int main()
{
Date d1;
Date d2;
d1.Init(2023, 2, 3);
d1._year++;
d2.Init(2022, 2, 3);
d2._year++;
cout << sizeof(d1) << endl;
return 0;
}
把成员变量变成公有的。d1和d2的_year是不一样的,但成员函数是一样的,都是一样的代码实现。成员函数是不在对象里的,如果在的话空间浪费就多了,这就像宿舍里的浴室,每一层楼都有一个公共的,这也就是成员函数,而成员变量就是住的学生。当然每个屋独立卫生间这得加钱。
成员变量独立存储,成员函数放到代码段(共享公共空间)
类的计算也就是结构体的计算规则——内存对齐
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8 - 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
7、this
1、this指针
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
public:
int _year;//声明
int _month;
int _day;
};
在这段代码中,成员函数里day赋值给了_day,可是这个_day能接收它吗?在类里它只是一个声明,那么外面的实例化中,有个d1,调用了函数,给d1的成员变量的实例化赋值。我们似乎可以想到这个答案,但是这个共享函数,怎么知道是要给d1的成员变量赋值?为什么不会给d2?
C++是这样解决的,在处理这些代码的时候,会用一个关键字this,会在函数括号里加上对应类型的this指针,然后在函数内容里会变成this -> _day, 而在main函数里,用d1调用Init时,就会在括号里加上一个d1的地址,所以才保证了唯一的调用。
void Init(Date* this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
d1.Init(&d1, 2023, 2, 3);
d1._year++;
d2.Init(&d2, 2022, 2, 3);
d2._year++;
但操作者不可以自己加上this,会出错。不过我们在函数里面可以加上this。
void Init(int year, int month, int day)
{
cout << this << endl;
this->_year = year;
this->_month = month;
this->_day = day;
}
这就是两个this指针的地址。
this指针存在栈上。this是一个形参,隐含的形参,因为这是编译器自己做的指针。调用函数时,会一个个压参数进栈,并开辟栈帧。编译器在汇编时,就把this指针放到了里面。
2、空指针问题
加一个成员函数
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void func()
{
cout << "func()" << endl;
}
Date* ptr = nullptr;
ptr->func();
ptr->Init(2023, 2, 3);
这里就很正常地运行了,打印出了func(),但是调用Init却会让程序崩溃。
成员函数不在对象里,当调用函数,会产生一个call指令,去在代码段里找函数地址,然后执行函数,不过func函数里没有对指针做什么,只是打印一行代码,所以没什么,对指针并没有解引用;Init做了什么?this指针接收到了空指针,这没问题,也就是调用的时候不崩溃,但是Init对空指针解引用了,所以崩了。如果ptr->_day,也会崩,因为也解引用了。这里也不能直接func(),而不带ptr,因为在C++入门(1)里提到过,编译器会在全局域里找,但是func不在,所以也不行。那如果(*ptr).func()呢?正常运行,虽然这是解引用,但编译器明白,你是在调用func这个函数,而这个ptr的实际作用就是传给this指针。在汇编里,ptr->func() 和 (*ptr).func()的代码是一样的。
3、C语言和C++简单对比
对比栈代码
C++
class Stack
{
public:
void Init(int n)
{
a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == a)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
size = 0;
}
void Push(int x)
{
a[size] = x;
}
public:
int* a;
int size;
int capacity;
};
C
typedef int DataType;
typedef int STDatatype;
typedef struct Stack
{
STDatatype* a;
int capacity;
int top;
}ST;
void StackInit(ST* ps);
void StackDestroy(ST* ps);
void StackPush(ST* ps, STDatatype x);
void StackPop(ST* ps);
bool StackEmpty(ST* ps);
STDatatype StackTop(ST* ps);
STDatatype StackSize(ST* ps);
直接给出一些对比结果
CPP:
1、数据和方法都封装到类里面
2、控制访问方式。公有则可以访问,私有则不可以访问
C:
1、数据和方法是分离的
2、数据访问控制实自由的,不受控制(比如取栈顶,数据和方法分离那么就很自由)
结束。