面向过程与面向对象
在学习类和对象之前,我们首先得认识一下什么是面向过程,什么又是面向对象。在我看来,面向过程和面向对象是两种编程思想。面向过程关注的是问题的求解步骤,C语言便是面向过程的。面向对象关注的是问题中不同对象间的关系和交互,Java、Python、C++都是面向对象的。
例如,在一个外卖系统中,主要的功能就是上架,点餐,派单,送餐。如果是面向过程的话,我们可以写出上架函数,点餐函数等,然后实现功能。而如果是面向过程,我们关注的便是参与到外卖系统的一系列对象:商家,骑手,用户。他们就是不同的类,接着便是商家上架,用户点餐等。在进行面向对象编程时,最终都会落到面向过程编程,但是面向对象能够更好的去描绘世界。
C++的结构体
因为C++兼容C语言,所以在C++中,可以完全像在C语言中那样使用结构体。不过,在C++中,结构体升级成了类。我们可以看下面的代码:
#include <iostream>
using namespace std;
struct Date
{
int day;
int month;
int year;
// 初始化
void init()
{
day = 1;
month = 1;
year = 1;
}
// 打印日期
void printDate(){
cout << day << "/" << month << "/" << year << endl;
}
// 检查日期是否有效
bool isValid(){
if (year < 0) {
return false;
}
if (month < 1 || month > 12) {
return false;
}
int daysInMonth[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
daysInMonth[2] = 29; // 闰年 2 月有 29 天
}
if (day < 1 || day > daysInMonth[month]) {
return false;
}
return true;
}
};
int main()
{
struct Date day_1;
day_1.printDate();
Date day_2;
cout << day_2.isValid() << endl;
return 0;
}
这个代码定义了一个结构体,其中包含了年、月、日这三个成员变量。同时,因为升级成了类,所以结构体中也可以定义函数。而在使用时也与结构体类似。类中的变量被称作类的成员变量或者类的属性,类中的函数被称作类的成员函数或者类的方法。
类的声明
因为struct升级成为了类,所以我们可以使用struct来声明类。同时,我们可以使用class来声明类,就像这样:
当我们使用class时,原本的程序就会报错,报错原因是无法访问private,这就要提一下C++中的访问限定符了
访问限定符
C++中有三种访问限定符,分别是public(公有)、protected(保护)、private(私有),他们标明了访问权限。这三个访问限定符可以分为两类:被public修饰的属性和方法可以在类外访问,而被protected或者private修饰的不能在类外访问。
访问限定符也存在作用范围。访问权限作用域从当前访问限定符开始,到下一个访问限定符结束。如果当前访问限定符后没有其他访问限定符,那么作用域就直到类结束。
class声明的类默认权限是private,struct声明的类默认权限是public。
因此,我们可以设置权限如下:
这样就解决编译器的报错了。一般来说,类中的成员变量权限是private,而方法则是public。
和定义函数类似,对于类中的成员函数,我们也可以让定义和声明分离,不过,需要注意的一点是,分离后定义函数时需要指明类域,请看下面的代码:
图中我将类中检验日期是否有效的函数声明与定义分离了。除了需要指明函数所属类域外,其余都没有任何变化。此外,类中定义的函数默认是内联函数,不过需要注意的是,内联仅仅是一个建议,最终决定权仍在编译器手里。因此,一般而言,我们会将比较小的代码直接在类中定义,而比较大的代码则让声明和定义分离。
类的声明也即将告一段落,在此之前,还有一个小小的建议。首先,请看下面的代码片段:
void setYear(int year)
{
year = year;
}
这个函数的作用非常明显,设置年份,虽然看起来比较奇怪,但它没有问题,等号右边的year是函数中的形参,而左边的则是类中的成员变量。虽然用起来没有问题,但是降低了代码的可读性,因此我们一般会对成员变量做些修饰,例如:
void setYear(int year)
{
_year = year;
//year_ = year;
//myear = year;
}
C++语法并没有这种要求,修饰也仅仅是提高一下代码的可读性,因此,怎么做取决于你,在这仅仅是一个建议。
面向对象三大特性
C++拥有很多的特性,但其中最出名的是封装、继承、多态。在这里我只会说一下封装,继承和多态之后再说。
首先,什么是封装?封装是将数据和操作数据的方法进行有机结合,形成类,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。类中想让外部能够访问的设置成public,不想让外部访问的设置成private。因此,当成员变量权限是private,方法是public时,如果想要访问对象的成员变量,仅仅只能通过类中的方法进行访问,相当于对数据的操作进行了规范。
封装的本质是管理,以求让程序员正确访问对象中的成员变量。C语言没有封装的特性,换句话说,C语言的数据和方法是分离的,我们可以跳过方法直接访问数据,直接访问的结果是什么就取决于程序员的水平。而对于C++而言,将数据和方法封装成一个类,程序员想要访问对象的数据时只能通过相应的方法,由此,如何操作数据由类的编写者决定,这会减少许多因为数据操作不规范导致的错误。
类的作用域
前文我提到了类域,它就是类定义的一种新的作用域。如果学习过C语言,对于域这一概念应该不会陌生,局部域,全局域和C++语言中的命名空间域。域的特点是,不同域中可以定义同名变量,同时域会限制对变量的访问。
一般而言,对于域的搜索顺序是先局部域后全局域。如果在类域中,类域是第二位,全局放在最后。对于命名空间域,想要访问它需要使用域访问限定符,或者命名空间展开。当命名空间域展开时,命名空间域和全局域类似。
只有局部域和全局域会影响生命周期。
类的实例化
前文讲了类的声明,在谈类的实例化之前,我们先看一下声明和定义的区别是什么。
对于函数而言,函数的定义是函数的具体实现。对于变量而言,定义便是分配内存空间。
在C语言中,我们不能直接访问结构体中的变量,如果要访问的话,我们需要先定义一个结构体变量,为结构体中的变量分配内存空间,然后才能进行访问。类也是一样的。类中所有的成员变量并没有相应的内存空间,无法通过类访问成员变量。类也不能直接访问成员函数。
如果想要访问,需要创建相应的对象,即Date day_2;这个过程叫做类实例化对象或者对象定义。这个过程中会分配相应的内存空间,从而能够访问对象的成员变量和成员方法。
类就相当于现实生活中的图纸,我们可以根据图纸建造一栋栋房子,房子可以住人但是图纸不能住人。类的实例化就相当于建房子,而访问对象中的成员变量相当于是在房子中住人。直接通过类访问成员变量就相当于是在图纸中住人,这是不可能的。
类的对象的大小
基本类型的变量都占有空间,例如整型变量需要4个字节,那么类的对象的大小是多少呢?
先说结论,类的对象的大小只与类中的成员变量有关,与成员函数无关。同时,类的大小和其对象的大小相同。我们可以运行下面的代码:
#include <iostream>
using namespace std;
class A
{
public :
void print()
{
cout << "A::print()" << endl;
}
private :
char a;
char b;
};
class B
{
public:
void print()
{
cout << "B::print()" << endl;
}
private:
char a;
};
class C
{
public:
void print()
{
cout << "C::print()" << endl;
}
private:
int a;
};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
}
运行后结果如下图:
由上面的例子我们可以看出,在计算类的对象的大小的时候,我们只需考虑成员变量即可,那么为什么不需要考虑成员函数呢?如果我们存储对象时都存储相应的函数,并且同一类的不同对象所使用的函数都是相同的,存储成员函数就会非常浪费空间。因此我们可以将成员函数放在一个公共代码区,然后存储对象时值存储其成员变量,调用函数的话直接前往公共代码区进行调用,这样既能调用函数,也能存储对象。
我们可以这么想,类相当于图纸,其对象就相当于一栋栋建好房子,房子中的卧室、厨房等就是成员变量,卧室、房子有不同的装饰,不同对象的成员变量也不相同。而成员函数就相当于是公共区域,像篮球场、游泳池,户主可以在篮球场一起打篮球,不同对象可以调用同一个函数。
当然,我们在计算类的对象的大小的时候也要像C语言结构体一样注意内存对齐,这一方面我不做详细描述。类的大小计算
那么,如果类中只有成员函数,或者类中什么都没有(空类),它的大小又是多少呢?我们可以试着运行下面的代码:
#include <iostream>
using namespace std;
class A
{
public :
void print()
{
cout << "A::print()" << endl;
}
private :
char a;
char b;
};
class B
{
public:
void print()
{
cout << "B::print()" << endl;
}
private :
char a;
};
class C
{
public:
void print()
{
cout << "C::print()" << endl;
}
};
class D
{};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
cout << sizeof(D) << endl;
}
其运行结果如下:
我们可以看到,当类中没有成员变量时,类的大小为1个Byte。不过,大小为1个Byte的类并非没有成员变量。那么,为什么没有成员变量时,其大小不为0呢?假设当类是空类时,如果其大小为0,我们这时如果创造两个对象(如下所示),我们又该如何识别哪个是d1,哪个又是d2呢?总结一下,没有成员变量的类对象,只占用一个字节,占了一个内存位,表示对象存在,但并不存储有效数据。
D d1, d2;
this指针
我们首先看下面的一段代码:
#include <iostream>
using namespace std;
class A
{
public :
void print()
{
cout << _a << endl;
}
void setA(int val = 1)
{
_a = val;
}
private :
int _a;
};
int main()
{
A a1;
A a2;
a1.setA(6);
a2.setA();
a1.print();
a2.print();
return 0;
}
代码运行结果如下:
上文讲过,类的对象调用成员函数时是在公共代码区调用的,那么为什么不同对象调用相同的成员函数时,结果却不同?这是因为C++中隐含的this指针。在通过对象调用成员函数时,编译器会自动传入一个指向该对象的指针,函数则通过this形参来接受这个指针,上面的print函数实际上是这样的:
cout << this->_a << endl;
由此,我们可以认为,在访问成员函数时,虽然我们使用的操作符是结构体访问操作符,但在这里它的功能并不是访问对象的内存地址,而是传递指向该对象的指针的功能,即我们可以将C++中的a1.print()看成是C语言中的print(&a1)。
需要注意的是,首先,this指针并不能显式地作为函数实参或者函数形参,但它可以在函数内部使用:
class A
{
public :
void print()
{
cout << this->_a << endl;
}
void setA(int val = 1)
{
_a = val;
}
private :
int _a;
};
同时,this指针的类型是类的类型* const,例如 A* const。
那么this指针存储在哪里呢?首先,在我们计算类的对象的大小时,并不考虑this指针,所以它并不会存储在类的对象中,即它并不会和类的成员变量存储在一起。前文说到,this指针是函数的形参,所以它会存储在栈中。
既然this是一个隐含的指针,那么this可以为空吗?在回答这个问题之前,我们先看下面两个问题:
程序B:
#include <iostream>
using namespace std;
class B
{
public:
void print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
B* p = nullptr;
p->print();
return 0;
}
程序A:
#include <iostream>
using namespace std;
class A
{
public:
void print()
{
cout << "print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->print();
return 0;
}
请问这两个程序的运行结果分别是什么呢?是编译错误、运行崩溃、还是正常运行?读者可以试着运行一下。
首先,这两个程序绝不可能编译错误,无论是对空指针或者野指针解引用,编译器都不会报错,这并不是编译错误。然后对象访问成员函数并不会访问对象内存,而是传递一个指向对象的指针,因此这两个程序在main函数中调用函数时并不会对空指针解应用,而是将空指针传递给函数(自动的)。A程序的print函数只是打印一行字符串,并未涉及任何对this指针的操作,因此正常运行;B程序的print函数需要打印对象中的成员变量_a,需要对this指针解引用,但是此时this指针是一个空指针,所以出现对空指针的解引用,程序运行崩溃。
总而言之,当使用的函数并不需要访问对象的成员变量时,this指针可以为空。其他情况下,this指针不能为空。当然,这仅仅是处于程序能否正常运行而得出的结论。在我看来,在日常使用的过程中,this指针最好不为空。
类和对象到此需要暂时告一段落,更多精彩请看下期博客。