✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C++学习
贝蒂的主页:Betty's blog
1. 面向过程与面向对象
1.1. 面向过程
我们之前学习的C语言就是一种面向过程的语言,它强调事件的具体实现过程,一般以函数来具体实现。比如说我们用面向过程的思想炒菜就可以分为以下几个步骤:
1.2. 面向对象
C++就是一门典型的面向对象的语言,它将问题分成多个对象,更强调对象与对象之间的联系。我们仍以炒菜来举例,在这个问题中我们可以抽象出四个对象:人,菜,调料,锅。
在面向对象中,我们更强调对象之间的连续,并不太在意其内在是如何完成的。
1.3. 对比
无论是面向过程还是面向对象的语言,都有其优缺点:
面向过程 | 面向对象 | |
---|---|---|
优点 | 流程化分工明确,效率高 | 结构化更清晰,易维护 |
缺点 | 可维护性差,扩展能力差 | 开销大,性能低 |
2. 类的引入
在C++中,在原来C语言结构体的基础上引入了类的概念。与C语言最大的不同就是,C++可以在类中定义函数。
而由类声明定义的变量,我们称为对象。
2.1. 类的两种定义
2.1.1. 第一种
因为C++兼容C语言,所以可以利用C语言的结构体关键字struct
来定义类。但是由于struct在C++中升级为类,所以在定义对象时并不需要写struct关键字。
cpp
#include<iostream>
using namespace std;
struct Date
{
int a;
int b;
};
int main()
{
Date d1;//ok
struct Date d2;//ok
return 0;
}
2.1.2. 第二种
第二种是由C++的关键字class
构成,其语法结构如下:
class className
{
// 类体:由成员函数和成员变量组成
};
cpp
#include<iostream>
using namespace std;
class Date
{
int a;
int b;
};
2.2. 单文件与多文件书写
在定义类时,我们可以将类的定义与声明在同一文件书写,或者在不同文件书写。
- 声明和定全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
cpp
#include<iostream>
using namespace std;
class Date
{
//定义与声明一起
int Add(int x, int y)
{
return x + y;
}
int year;
int month;
int day;
};
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::。
cpp
#include<iostream>
using namespace std;
//test.h
class Date
{
//定义与声明一起
int Add(int x, int y);
int year;
int month;
int day;
};
//test.cpp
int Date::Add(int x,int y)
{
return x+y;
}
一般在我们平时练习时,更常用将定义与声明放在类中。但是在实际工程中,更倾向于奖定义与声明分类。
2.3. 成员变量的命名
虽然C++标准并没有规定成员变量的命名规则,但是大家约定俗成地在定义变量时会有一套特定的规则,目的就是解决可能存在的命名冲突的问题,比如说下面这段代码:
cpp
class Date
{
void Init(int year, int month, int day)
{
//命名冲突
year = year;
month = month;
day = day;
}
int year;
int month;
int day;
};
这时为了解决问题这类问题,我们在定义成员变量时会对其进行特定地修饰。比如说_变量名
或者是m_变量名
或者变量名_
。不同公司,不同的程序员可能都有一套自己的命名规则,但主流一般就是以上三种。
cpp
class Date
{
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
3. 类的访问限定符与封装
3.1. 访问限定符
在C++类中有三种访问限定符:**public,private,protected。**他们每一个都有自己独特的作用:
- public修饰的成员在类外可以直接被访问。
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)。
cpp
class Date
{
//可以被直接访问
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//不能被直接访问
private:
int _year;
int _month;
int _day;
};
3.2. 封装
封装:用类将对象的属性(数据)与操作数据的方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
**封装本质上是一种管理,让用户更方便使用类。**比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
4. 类对象模型
4.1. 类对象的实例化
在类中的成员变量实际是一种声明,相当于一个设计图纸。而我们利用类名定义的对象就是类对象的实例化,相当于通过设计图纸实际创建出来。单独的类是并不占据实际空间的大小。
cpp
Date::_year = 1;//error
4.2. 类对象的存储
我们知道了类对象的创建,那么具体类中的成员变量与成员函数又是如何存储的呢?
4.2.1. 存储一
每次创建对象时,都开辟一个空间存储类成员变量与成员函数。
4.2.2. 存储二
每次创建对象时,都开辟一个空间存储类成员变量与成员函数的地址。
4.2.3. 存储三
每次创建对象时,都开辟一个空间存储类成员变量。而成员函数提前单独存储一个区域
首先如果是存储一的话,每次对象的实例化都会开辟函数的空间。而每个函数的功能都是一样的,这就造成了空间的浪费。而如何判断时存储二还是存储三,我们可以通过计算类大小来判断。
4.3. 类对象的大小
4.3.1. 一般类的计算
类型对象的大小我们可以借助运算符sizeof计算,并且类的大小也遵循结构体内存对齐规则:
- 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
- 对⻬数=编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。(VS 中默认的值为 8 ,Linux中gcc没有默认对齐数,对⻬数就是成员⾃⾝的⼤⼩)
- 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
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,所以证明成员函数是存在于内存的其他位置。所以说存储三正确。一般这个存储成员函数的区域我们称之为公共代码段。
4.3.2. 空类的计算
当类中只有成员函数,或者什么都没有时。类的大小又为多少呢?
cpp
// 类中仅有成员函数
class A1
{
public:
void func2() {}
};
// 类中什么都没有---空类
class A2
{
};
int main()
{
A1 d1;
A2 d2;
cout << sizeof(d1) << endl;
cout << sizeof(d2) << endl;
return 0;
}
为什么空类的大小为1,而不是0呢?其实并不能难像,因为在我们进行空类的实例化时必须要有空间存储对象的大小。这是编译器就会默认给一个字节大小来标记这个类的对象,实际操作中实用性也很少。
5. this指针
5.1. this指针的引出
我们首先来看这一段代码:
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;
}
对于上述类,有这样的一个问题:
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
在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.2. this指针的特点
- this指针的类型:类型为*** const**,即成员函数中,不能给this指针赋值。
- 只能在"成员函数"的内部使用。
- this指针本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是"成员函数"第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
5.3. 两道问题
5.3.1. 问题一
cpp
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class Betty
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
Betty* p = nullptr;
p->Print();
(*p).Print();
return 0;
}
为什么程序会正常运行,对空指针解引用不是会发生运行崩溃吗?首先我们得明白成员函数并不存放在类对象中,而是存放在公共代码段。虽然我们表面看上去解引用,但实际上编译器不需要通过解引用去找对应函数,只需要去公共代码区执行对应函数即可。
5.3.2. 问题二
cpp
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class Betty
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
Betty* p = nullptr;
p->Print();
(*p).Print();
return 0;
}
这里就引起程序崩溃,因为我们知道访问对应的成员变量,会传递对应对象的地址。而这里的地址为nullptr
,通过nullptr->_a
引起程序崩溃。