【C++初阶】类和对象(上)
- 1.面向对象与面向过程的初步认识
- 2.类的引入
- [3. 类的定义](#3. 类的定义)
- 4.类的访问限定符及封装
- 5.类的作用域
- 6.类的实例化
- 6.类的对象的大小计算
- 7.类的this指针
📃博客主页: 小镇敲码人
💞热门专栏:C++初阶
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞
1.面向对象与面向过程的初步认识
C++和C语言是不同的,C语言更关注过程,而C++关注更对象。
就好比我想把一头大象装进冰箱里面,C语言想要完成这件事需要先打开冰箱,然后把大象装进冰箱里面,这是具体的过程,C++并不关注具体的过程,C++会把整件事分为两个对象,冰箱和大象,靠对象之间的交互来完成这件事。
2.类的引入
在C语言中,我们学习了
struct
关键字所自定义的结构体类型,在C++中,我们在struct
里面不仅仅可以定义一些变量,而且可以定义函数,struct
升级变为了类。
我们在用C语言实现栈时,struct
里面只能定义变量,现在我们用C++实现栈,会发现它里面也可以定义函数:
cpp
typedef int DataTypeStack;
struct Stack
{
void init(size_t capacity)
{
_array = (DataTypeStack*)malloc(sizeof(DataTypeStack)*capacity);
if (nullptr == _array )
{
perror("malloc failed");
exit(-1);
}
_size = 0;
_capacity = capacity;
}
void push(DataTypeStack x)
{
//满了就扩容
if (_size == _capacity)
{
_capacity *= 2;
DataTypeStack* tmp = (DataTypeStack*)realloc(_array,sizeof(DataTypeStack) * _capacity);
if (nullptr == tmp)
{
perror("realloc failed");
exit(-1);
}
_array = tmp;
}
_array[_size] = x;
_size++;
}
DataTypeStack Top()
{
return _array[_size - 1];
}
void Destory()
{
free(_array);
_array = nullptr;
_size = 0;
_capacity = 0;
}
DataTypeStack* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack St;
St.init(4);
for (int i = 0; i < 10; i++)
{
St.push(i);
cout << St.Top() << endl;
}
St.Destory();
return 0;
}
运行结果:
这样用起来是不是感觉比C语言里面的方便了很多呢?
3. 类的定义
虽然用
struct
也可以定义类,但是C++有了一个新的关键字class
来定义类,它们有所区别,我们稍后再谈这个问题。
cpp
class classname
{
//类体:由成员变量和成员函数组成。
};
class
后面是类的名字,类里面的变量又叫做类的属性,类的成员变量,类里面的函数又叫做成员函数或者成员方法。
我们创建类通常有两种方式:
- 类的声明和定义都放在类里面,注意:声明和定义都放在类里面,编译器可能会将类里面的函数当成内联函数处理。
- 类的声明放在.h头文件里,类里面成员函数的定义放在.cpp文件里面,需要在成员函数前面加上
类名::
。
注意:我们在刷题的时候可以用第一种方式,但是以后工作了肯定是用第二中方式最佳。
关于类里面的成员变量的命名的建议:
cpp
class Date
{
void init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
int year;
int month;
int day;
};
这样看着就很抽象,你该如何区分我的成员变量和函数的形参呢?
所以外面应该这样写:
cpp
class Date
{
void init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
或者这样:
cpp
class Date
{
void init(int year, int month, int day)
{
year_ = year;
month_ = month;
day_ = day;
}
int year_;
int month_;
int day_;
};
具体看相应的要求,只要可以区分且易于知道成员变量的含义即可。
4.类的访问限定符及封装
4.1访问限定符
在类里面一般还有三个关键字,public
(公用)、private
(私有)、protected
(保护)。
class
里面不加访问限定符来修饰。里面的成员变量和成员函数默认是私有的,类外面访问是非法的。struct
里面不加访问限定符,默认里面的成员变量和成员函数是公有的,因为C++要兼容C语言。public
修饰表示公有在类外面可以访问,被private
和protected
修饰表示私有在类外面不能被访问,它们两个没有什么区别。- 访问限定符的作用域从一直从访问限定符出现的位置开始,到下一个访问限定符出现的位置结束。如果后面没有访问限定符了,那访问权限到
}
就结束了。
- 注意:访问限定符只在编译的时候有,当数据在内存里面时,没有这个概念。
C++struct
和class
的区别:
1.struct
里面默认的成员变量和函数是公有的,而class
里面默认的是私有的。
struct
可以定义结构体但是类不行,但是struct
可以用来定义类。
4.2封装
封装是类和对象的三大特性之一,其它的两大特性分别是:继承和多态。我们在类和对象阶段主要把封装搞清楚。
那什么是封装呢?
封装就是一种把数据和数据操作方法进行有机结合,把对象的属性和一些细节通过访问限定符的限制起来,只开放一些成员方法和外界进行交互。
就像我们使用电脑一样,电脑只对用户开放了USB接口,键盘、鼠标、显示器、开关机键,更重要的显卡、cpu则是封装起来了防止用户搞破坏,我们只需要使用它开放的一些工具就可以实现人机交互,C++的封装思想就类似于这个。
5.类的作用域
类定义了一个新的作用域,我们在类外面定义类里面的成员 函数时需要加上
类名::
。下面一段代码,帮助你知道如何使用:
cpp
typedef int DataTypeStack;
class Stack
{
public:
void init(size_t capacity);
private:
DataTypeStack* _array;
size_t _size;
size_t _capacity;
};
void Stack::init(size_t capacity)
{
_array = (DataTypeStack*)malloc(sizeof(DataTypeStack) * capacity);
if (nullptr == _array)
{
perror("malloc failed");
exit(-1);
}
_size = 0;
_capacity = 0;
}
这个类名::
是紧跟在成员函数名前面的,不能加空格,否则编译器就会报错,相当于告诉编译器,这个函数是给叫做stack
的类定义的。
6.类的实例化
类是对对象的描述,它就像建造一个工程所需要的图纸,是一个空格,是不占空间的,我们实例化对象的过程,就当于通过这个图纸建造房子了。
像如上代码,直接调用类里面的对象,没有实例化,是会报错的,这就像我们在C语言里面自定义了一个类型,但是没有用这个类型创建出一个变量那么我们就不能使用这个类型是一个道理。
我们想正确使用Stack
这个类,应该按如下代码:
cpp
typedef int DataTypeStack;
class Stack
{
public:
void init(size_t capacity);
private:
DataTypeStack* _array;
size_t _size;
size_t _capacity;
};
void Stack::init(size_t capacity)
{
_array = (DataTypeStack*)malloc(sizeof(DataTypeStack) * capacity);
if (nullptr == _array)
{
perror("malloc failed");
exit(-1);
}
_size = 0;
_capacity = 0;
}
int main()
{
Stack ST;
ST.init(4);
return 0;
}
6.类的对象的大小计算
那学会了实例化对象,我们就应该思考类对象的大小是如何计算的呢?我们想计算类对象的大小,就必须搞清楚一个问题,我们可以创建很多相同类的对象,那这些类对象是如何在内存中存储的呢?
我们假设了三种方式:
- 每一个对象都有自己的成员变量和成员函数,它们都会保存一份。
- 每一个对象只保存自己的成员变量,成员函数放在一个公共的代码段。
到底结果如何我们可以用下面的代码来验证一下:
cpp
#include<iostream>
#include<stdlib.h>
using namespace std;
//既有成员函数,又有成员变量
class c1
{
void f1();
int _a;
};
//只有成员函数
class c2
{
void f2();
};
//空类,什么都没有
class c3
{
};
int main()
{
c1 A;
c2 B;
c3 C;
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
return 0;
}
运行结果:
空类比较特殊,编译器给了一个字节来唯一标识这个类的对象,可以看到外面c2不是空类是有一个成员函数的,它也为1,说明类对象的存储方式应该是第二种,只保存成员变量,成员函数放在公共的代码段。
这里计算类对象大小的方法和计算结构体的大小是一样的,注意内存对齐规则即可。
7.类的this指针
7.1this指针的引入
我们先来定义一个Stack
的变量:
cpp
#include<iostream>
#include<stdlib.h>
using namespace std;
typedef int DataTypeStack;
class Stack
{
public:
void init(size_t capacity)
{
_array = (DataTypeStack*)malloc(sizeof(DataTypeStack)*capacity);
if (nullptr == _array )
{
perror("malloc failed");
exit(-1);
}
_size = 0;
_capacity = capacity;
}
void push(DataTypeStack x)
{
//满了就扩容
if (_size == _capacity)
{
_capacity *= 2;
DataTypeStack* tmp = (DataTypeStack*)realloc(_array,sizeof(DataTypeStack) * _capacity);
if (nullptr == tmp)
{
perror("realloc failed");
exit(-1);
}
_array = tmp;
}
_array[_size] = x;
_size++;
}
DataTypeStack Top()
{
return _array[_size - 1];
}
void Destory()
{
free(_array);
_array = nullptr;
_size = 0;
_capacity = 0;
}
private:
DataTypeStack* _array;
size_t _size;
size_t _capacity;
};
int mian()
{
Stack S1;
Stack S2;
S1.init(4);
S2.init(8);
return 0;
}
有这样一个问题,S1、S2都是调用公共代码段的函数init
,编译器该如何区分,是应该给S1的成员变量初始化呢,还是给S2的成员变量初始化呢?为了解决这个问题,C++引出了this指针的概念。
即:C++给每一个类的非静态成员函数,都隐式的添加了一个叫this
指针的参数,它保存该对象的地址,在函数里面访问对象里的成员变量,都是通过this
指针来访问的,只不过程序员不需要显式的写出来,这些都由编译器完成了。
7.2this指针的一些特性
- this指针的类型是
*const
,也就是说this保存的是对象的地址,这个地址是不能被改变的,否则,我们在成员函数里面就无法访问到对象的成员变量了。 - 只有在成员变量里面才能使用
this
指针。 this
本质是一个成员函数的一个形式参数,它的地址不保存在对象里面,当对象调用成员函数时将对象的地址传给this
指针,它是成员函数的第一个隐式的指针形参,不需要用户自己传,编译器通过ecx寄存器自动传了。
this指针可以为空吗?答案是可以的我们用下面的代码来验证:
cpp
#include<iostream>
#include<stdlib.h>
using namespace std;
class c1
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
c1* ptr1 = nullptr;
ptr1->Print();
return 0;
}
这段代码的结果是什么呢?是正常运行还是崩溃呢?
可以看到程序是正常运行了的,有人可能有疑惑,ptr1
不是空指针吗,对其进行->
操作不是会导致程序崩溃吗,是的,但是这里编译器其实没有真正的对其进行->
操作,
因为我们调用这个Print函数和类对象的地址是无关的,函数保存在公共的代码段,这里的ptr1->Print()
操作只是起到了一个告诉编译器,Print函数是属于ptr1这个类对象的仅此而已,就相当我们在类外面定义类的成员函数要加类名::
是相同的道理,如果你不信我们可以转到反汇编看一下:
再看下面一段代码,它的运行结果又是如何呢?
cpp
#include<iostream>
#include<stdlib.h>
using namespace std;
class c1
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
c1* ptr1 = nullptr;
ptr1->Print();
return 0;
}
运行结果:
这里程序crash了,我们应该知道,因为此时的this指针是一个空值,我们在Print函数里面访问_a,其实是this->_a
,只不过编译器帮助我们完成了这个工作了而已,当然只要形参和函数参数里面不显式的写出来就可以,在访问对象的成员变量时,是可以显式的写出this
指针的,这里对空指针进行->
操作显然是非法的,所以程序crash是必然的。
- 结论:
this
指针为空值,只要在调用的成员函数里面不使用this
指针访问成员变量,程序是不会crash的。