1.面向过程与面向对象
1.1面向过程
我们之前学习的C语言就是一种面向过程的语言,面向过程的语言强调的是具体实现的过程,一般用函数来具体实现。我们用面向过程的思想,就可以把炒菜分为以下几个步骤:
1.2面向对象
而对于面向对象的语言而言,它将问题分为了多个对象,问题的解决由对象之间的交互完成,而不再注重对象完成事情的具体过程,譬如上述炒菜的过程我们即可抽象为人、菜、锅的交互。
C++即是一门面向对象的语言,我们更强调对象之间的交互,而并不在意对象该如何解决问题。
1.3对比
面向过程的语言和面向对象的语言各有其优缺点:
面向过程的语言流程化分工明确,效率高,但可维护性差,拓展能力差。
面向对象的语言结构化更清晰,易维护,但开销大,性能低。
2.类的引入
在C++中,我们在结构体的基础上引入了类(class)的概念,并对struct在CPP的基础上重新进行了解释。
由于我们的面向对象不注重过程的实现,但是对象本身还是需要实现过程的,因此我们要给对象加上一定的过程,比如我们的人锅交互可以炒菜,我们就要在类中声明一个作用是炒菜的函数。
也就是说,CPP的类和结构体中可以定义成员函数!
2.1类的声明方式
在C++中,有一个关键字为class,我们可以使用这个关键字来声明一个类,类内可以声明成员函数和成员变量,其语法结构为:
cpp
class 类名
{
//类内的成员函数和成员变量
};
现在我们声明一个名为duanku的类:
cpp
class duanku
{
int a;//成员变量
int Add(int a, int b);//成员函数
};
2.2类的成员的两种定义方式
在定义类时,我们可以将类的定义与声明在同一文件书写,也可以在不同文件内书写。
2.2.1单文件定义
定义一个类,自然需要实现类体内的函数,我们可以将函数在类内定义,如下所示:
cpp
class duanku
{
int a;//成员变量
int Add(int a, int b)//成员函数
{
return a + b;
}
};
这里需要大家注意的是:成员函数若在类内定义,则可能会被编译器当作内联函数处理。
2.2.2多文件定义
一个类也可以分为两个文件来实现,我们可以将成员函数的定义放在cpp文件内部。
但这儿需要大家注意的一点是:你定义的函数是谁的函数呢?是只在这个文件内部用的,还是实现了哪个地方的声明呢?因此,我们需要加域作用限定符,让编译器知道我们实现的是哪个函数。
cpp
//.h文件
class duanku
{
int a;//成员变量
int Add(int a, int b);//成员函数
};
//.cpp文件
int duanku::Add(int a, int b)
{
return a + b;
}
PS:在日常使用时,我们可以将定义和声明都放在类中。但是在实际的工程中,更倾向于将声明和定义分离。
3.类的访问限定符与封装
3.1访问限定符
在C++类中有三种访问限定符:public、private、protect
它们的作用如下
- public修饰的成员可以在类外直接访问
- private和protect修饰的成员在类外不可直接被访问(后续文章会详细解释private和protect)
- 一个类中可以有多个访问限定符,一个限定访问符的作用域是该限定访问符出现的位置到下一个限定访问符出现的位置,如果后面没有限定访问符,则到类结束的位置为止。
- class的默认访问权限为private,struct的默认访问权限为public(下篇文章中会给出class和struct的详解)
3.2封装
封装:用类将对象的属性(数据)与操作数据的方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
通俗的说,封装其实是一种管理,是为了方便让用户更方便的使用类的一种方法。
比如说:对于一台电脑而言,我们提供给用户的是一系列的USB接口,我们可以通过它们和计算机进行一系列的交互,也可以自由更改每个USB接口的接什么,但是我们无法更改电脑的硬件元件。
这里,我们可以这样理解:
每个USB接口都是一个函数,我们可以自由更改它们,它们的访问限定为public。
而硬件元件则是类中的私有成员,不让外界访问,它们的访问限定为private。
4.类对象
4.1类对象的实例化
在类中定义的成员变量实际上是一种声明,而我们利用类名字定义的对象就是类对象的实例化。
即,用类的类型创造对象的过程,称为类的实例化。
1.类是对对象进行描述的 ,是一个模型 一样的东西,限定了类中有哪些成员,定义出一个类并不会分配实际的内存空间来存储它。
2.一个类可以实例化出多个对象,实例化出的对象才占用实际的内存空间,存储的是类的成员变量。
我们不可以直接利用类来进行定义,如下图所示
正确的定义方法如下:
cpp
Date a;
a._year = 3;
4.2类对象的存储
我们已经知道了类对象的创建方式,那么具体类中的成员变量与成员函数又是如何存储的呢?
4.2.1 存储方式1
每次创建对象时,都开辟一个空间存储类中的成员变量与成员函数。
但是这样存储就会出现一个问题,成员函数都是用的一个函数,我们每次都创建一个岂不是会有很大的开销吗?一个就能行的事儿为什么要用很多个呢?
于是,我们的计算机中,大多数都不是这么存储对象的。
4.2.2 存储方式2
我们首先将成员函数放在外面,之后我们每次创建对象时,都开辟一个空间存储类成员变量与成员函数的地址,需要成员函数的时候直接按照地址去找。
4.2.3存储方式3
每次创建对象时,都开辟一个空间存储类成员变量。而成员函数提前单独存储在另外一个区域(公共代码段)。
之后我们没去用到成员函数时,都去那个区域里面拿出来用。
一般的编译器采用的都是存储模式2或存储模式3,具体使用的是哪种存储模式,我们可以通过计算类大小来进行判断。
4.3类对象的大小
4.3.1普通类对象的大小
类型对象的大小也遵循结构体的内存对齐规则:
1.类的第一个成员对齐到结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 =编译器默认的一个对齐数与该成员变量大小的较小值。
--VS2022中的默认对齐数是8
--Liunx中gcc没有默认对齐数,对齐数就是成员自身的大小
3.类总大小为所有成员对齐数中的最大对齐数的整数倍
4.如果嵌套了结构体,嵌套的结构体成员对齐到自己的成员的最大对齐数处,结构体的整体大小就是所有成员的最大对齐数(含嵌套结构体成员)的整数倍。
当然,类大小的计算我们可以也借助sizeof计算。
cpp
#include<iostream>
using namespace std;
class Date
{
//可以被直接访问
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//不能被直接访问
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
cout << sizeof(d) << endl;
return 0;
}
由此可见,我们这里算出来的12是三个int类型的总和,而我们的成员函数则被放到了公共代码段中。 若是使用的存储模式2的话,则应该是12+成员函数地址的大小(32位系统是4,64位系统是8)。
4.3.2空类的计算
当类中只有成员函数,或者什么都没有时,类的大小是多少呢?
cpp
class A
{
public:
void func()
{
;
}
};
class B
{
};
int main()
{
A a;
B b;
cout << sizeof(a)<<endl << sizeof(b);
return 0;
}
这里我们发现空类的大小为1,现在我们可以思考这么一个问题,为什么空类的大小是1而不是0呢? 这是因为我们在进行空类的实例化时必须要有空间表示我们在这儿存储了一个对象。这时编译器就会默认给一个字节大小来标记这个对象。
5.this指针
5.1this指针的由来
cpp
#include<iostream>
using namespace std;
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, d2;
d1.Init(2022, 1, 11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
在这里我先给大家抛出这么一个问题:为什么d1调用Init函数时,该函数知道是给d1对象调用Init,而不是给d2对象调用Init呢?
在C++中通过引入this指针解决该问题,即:C++编译器给每个"非静态的成员函数"增加了一个隐藏的指针参数。通过不同的对象地址来分辨不同的对象,只不过所有的操作对用户是透明的的,即用户不需要来传递,编译器自动完成。实际代码如下:
cpp
void Init(Date* this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
d1.Init(&d1,2022, 1, 11);//实际传参
5.2this指针的特点
- this指针额类型为const,即我们无法给this指针赋值
- this指针的本质是"成员函数"的形参,当对象调用成员函数时,会将对象的地址当作实参传递给形参。
- this指针只能在成员函数中使用
- this指针是"成员函数"第一个隐藏起来的指针形参,一般由编译器放在通过eax寄存器自动传递,不需要用户传递。
5.3两个问题
5.3.1问题1
cpp
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class duanku
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
duanku* p = nullptr;
p->Print();
(*p).Print();
return 0;
}
这段代码是可以正常运行的,虽然p是一个空指针,但是由于我们的成员函数放在公共代码段中,因此我们还是可以找到这个函数的,不会因为对空指针解引用而找不到它。
5.3.2问题2
cpp
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class duanku
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
duanku* p = nullptr;
p->Print();
(*p).Print();
return 0;
}
这里就会让程序崩溃,因为函数中访问了_a ,nullptr->_a程序就崩溃了。
6.成员变量的命名
大家在使用类时,可能会出现以下这么一个问题:
这时为了解决问题这类问题,我们在定义成员变量时会对其进行特定地修饰。比如说_变量名
或者变量名_
。不同公司,不同的程序员可能都有一套自己的命名规则,这里我们采用第一种。
由此我们可以将上述代码改写为这样:
cpp
class Date
{
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};