面向过程和面向对象初步认识
什么是面向过程?
c是面向过程,关注过程,分析解决问题的步骤,通过函数调用逐步解决问题.
什么是面向对象?
c++是基于面向对象,关注对象,将一件事情拆分成不同对象,靠对象之间的交互解决问题.
以外卖系统为例:
面向过程:
点餐、接单、取餐、送餐、评价…………(关注流程函数的实现)
面向对象:
商家 —— 上架、接单;
骑手 —— 取餐、送餐;
顾客 —— 点餐、评价.
这些对象之间的交互天然形成了过程.
注意:c++不是纯面向对象的语言,可以面向对象和面向过程混编.(C++兼容C)
类的引入
c++兼容c的struct语法,同时将struct升级成了类
1 类里可以定义成员变量,还可以定义成员函数
2 为了防止 成员函数的形参名 与 成员变量的名字 发生冲突,成员变量名一般会在前加上 '_'
struct Date
{
//成员函数
void init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void test()
{
cout << "test" << endl;
}
//成员变量
int _year;
int _month;
int _day;
};
3 类名同时也是类型,定义对象时可以直接省略struct
struct Date d1;//兼容C
Date d2;//没有typedef也直接省略struct
4 使用对应类的对象才能调用对应类的成员函数,调用方法与C访问结构体成员类似
例:使用日期类的对象才能调用日期类的成员函数
//日期类对象.日期类成员变量
//日期类对象.日期类成员函数
d1.init(2022,11,12);
d2.init(2022, 11, 13);
//日期类对象的指针-> 日期类成员变量
//日期类对象的指针-> 日期类成员函数
Date* d3 = &d1;
Date* d4 = &d2;
d3->test();
d4->test()
5 但c++更喜欢用class来代替struct.
class className
{
//类体:由成员函数和成员变量组成
};
实例化对象
什么是类的实例化?
用类去定义一个对象,叫类的实例化.
例:Date d1;//用日期类实例化一个对象d1
类和对象的关系
类就像是房子的设计图,我们可以使用建筑设计图建造出房子(即用类实例化对象)
但类只是个设计,不占用物理空间;实例化出的对象才能实际存储数据,占用物理空间
计算类/类对象的大小
计算类/类对象的大小要考虑什么?
不考虑成员函数,只需考虑成员变量
为什么只考虑成员变量?
根本原因:每个对象的成员变量是不同的,但对象能调用的成员函数都是相同的
//d1,d2,d3的成员变量空间各不相同,各自独立
//例:d1._year 和d2._year的地址/空间是不同的,各自独立
Date d1;
Date d2;
Date d3;
//但d1,d2,d3调用的成员函数init()是相同的,都用来初始化各自的成员变量
d1.init(2022, 11, 12);
d2.init(2022, 11, 13);
d3.init(2022, 11, 14);
cout << d1._year << " " << d1._month << " " << d1._day << endl;
cout << d2._year << " " << d2._month << " " << d2._day << endl;
cout << d3._year << " " << d3._month << " " << d3._day << endl;
类对象的存储的三种方案
方案1 成员函数、成员变量都存到对象中
当一个类实例化多个对象时,每个对象都保存一份函数代码,
相同的函数代码保存多份,浪费空间.
方案2 对象存成员变量,以及一张表的地址
成员函数代码只保存一份,成员函数地址单独用一张表存起来,对象中存这张表的地址.
方案3 只存成员变量
成员函数直接放在公共的代码区(和普通函数一样)
编译链接时就根据函数名去公共代码区,找到函数的地址
最终c++采用了方案三
如何计算类/类对象的大小?
计算类/类对象的大小只考虑成员变量,遵循内存对齐的规则
(1) 第一个成员在与结构体起始位置偏移量为0的地址处
(2) 其他成员变量要对齐到 对齐数 的整数倍地址处
(3) 成员变量的对齐数 = 编译器默认对齐数 与 该成员大小 的较小值
(4) 结构体总大小必须是最大对齐数的整数倍
(5) 如果嵌套了结构体,嵌套的结构体类型对齐到自己最大对齐数的整数倍地址处,
即嵌套结构体的对齐数就是自身内部的最大对齐数.
(6) 结构体的整体大小就是最大对齐数的整数倍.(要和嵌套结构体的对齐数做比较)
特殊:空类/仅有成员函数的类
大小为1byte,占位,标识对象存在.
类的作用域
类定义了一个新的作用域 —— 类域.
类的所有成员都在类域中.
在类外定义成员时,要使用::指明它属于哪一个类域.
class Date
{
//成员变量
void init(int year, int month, int day);
int _year;
int _month;
int _day;
};
void Date::init(int year, int month, int day)//指明这个函数属于Date这个类域,否则当成普通函数
{
//但普通函数内部没有定义_year、_month、_day,就会报错
_year = year;
_month = month;
_day = day;
}
拓展:
(1) 类域和命名空间域只影响访问【能不能找到某个函数或变量】,局部域会影响访问和生命周期
但主要影响生命周期的因素是变量存在什么区域,域主要影响访问
(2) 不同的域之间可以定义同名的函数(但不叫函数重载)
类的访问限定符
引出
c++用类将对象的属性和方法结合在一块.
属性就是类的成员变量,方法就是类的成员函数.
以栈为例(栈的具体实现可先忽略):
#include <iostream>
#include <stdlib.h>
#include <assert.h>
using namespace std;
struct Stack
{
//成员函数(方法)
void init(int capacity = 4);//初始化栈
void push(int num);//入栈
bool empty();//判断栈不为空
int top();//取栈顶元素
//成员变量(属性)
int* data;//指向动态开辟的一块空间
int _top;//栈顶
int _capacity;//当前容量
};
void Stack::init(int capacity)
{
data = (int*)malloc(sizeof(int) * capacity);
_capacity = capacity;
_top = 0;//栈顶初始化为0,说明_top实际指向栈顶元素的下一个位置
}
void Stack::push(int num)
{
if (_capacity == _top)//扩容
{
int* tmp = (int*)realloc(data, _capacity * 2);
assert(tmp);
data = tmp;
_capacity *= 2;
}
data[_top] = num;
_top++;
}
bool Stack::empty()
{
return _top == 0;
}
int Stack::top()
{
assert(!empty());
return data[_top - 1];
}
此时我们去使用栈
int main()
{
Stack st;
st.init();
st.push(1);
st.push(2);
st.push(3);
return 0;
}
问题:现在需要取栈顶元素,一般我们都是调用top().
但还有一种方式是通过直接访问成员变量获取栈顶元素.
cout << st.top() << endl;
cout << st.data[st._top] << endl;
//由于st._top实现为栈顶元素的下一个位置,所以st.data[st._top]取到的是随机值
总结
由于类的设计会有偏差:
例如栈的_top如果初始化为-1,那_top就是栈顶元素的下标;
_top如果初始化为0,那_top就是栈顶元素的下一个位置下标.
类里有些成员不应在类外面直接被访问,应该设置权限加以限制.
三种访问限定符
c++通过访问权限的设置,将接口(函数)提供给外部用户使用,隐藏类的实现细节(属性).
(1) public修饰的成员在类内外都可以直接被访问.
(2) protected/private修饰的成员在类外不能被访问,在类内可以访问.
(3) 访问权限作用域从该访问限定符出现位置开始,直到下一个访问限定符出现为止.
否则,直接修饰到结尾.
(4) 类的成员函数若在类外定义,这些成员函数仍然可以使用类的私有成员.
struct Stack
{
public: //在类内外都可以访问
//成员函数(方法)
void init(int capacity = 4);//初始化栈
void push(int num);//入栈
bool empty();//判断栈不为空
int top();//取栈顶元素
private://在类外不能访问
//成员变量(属性)
int* data;//指向动态开辟的一块空间
int _top;//栈顶
int _capacity;//当前容量
};
struct 和class的区别
c++为了兼容c,struct的内部成员默认访问权限是public,
而class的内部成员默认访问权限是private.
类的定义
方案1:成员函数的声明和定义全放在类体里,声明和定义不分离
如果成员函数在类中定义,编译器会默认该函数前加了inline,符合内联的条件就会视为内联函数.
方案2:成员函数的声明和定义分离
【成员函数声明】和【类的定义】一起放在.h文件中,成员函数定义放在.cpp文件.
(注意定义时要使用::指明这些函数属于哪个类域)
总结:
(1) 类的成员函数是小函数,成为内联函数直接定义在类中.
(2) 类的成员函数是大函数,声明和定义分离.
this指针
引出
用栈类实例化对象st后,我们可以用该对象调用类的成员函数,间接修改该对象的属性(成员变量).
那成员函数是怎么确定要修改哪个对象成员呢?我们传的参数并没有对象地址或引用?
Stack stA;
stA.push(1);
Stack stB;
stB.push(1);
编译器给每个【非静态】的成员函数增加了一个隐藏的指针参数,让该指针指向当前对象.
//注意:这是编译器处理后的结果,不能显式传对象的地址,不能显式写this形参
void Stack::push(Stack* const this,int num)
{
if (this->_capacity == this->_top)//扩容
{
int* tmp = (int*)realloc(this->data, this->_capacity * 2);
assert(tmp);
this->data = tmp;
this->_capacity *= 2;
}
this->data[this->_top] = num;
this->_top++;
}
stA.push(&stA,1);//编译器处理结果,不能显式传stA的地址
stB.push(&stB,1);
有了this指针,就能解释为什么成员函数能直接使用成员变量.
在成员函数内部使用成员变量时,会默认在前面加上this->.
特性
(1)实参和形参的位置不能显示传递或接收this指针
(2)成员函数内部可以显示使用this指针,但不能修改this指针.
void Stack::push(int num)
{
//成员变量前可以自行加this->,也可以不加
if (_capacity == this->_top)//扩容
{
int* tmp = (int*)realloc(this->data, this->_capacity * 2);
assert(tmp);
this->data = tmp;
this->_capacity *= 2;
}
this->data[this->_top] = num;
this->_top++;
}
(3)this指针的类型:类* const this.在成员函数里不能修改this指针.
例:Stack* const this.
(4)this指针本质上是成员函数的隐藏形参.
当对象调用成员函数时,自动将对象地址作为实参传递给this形参.
(5)this指针是形参,存在栈区中.但有些编译器会优化,放在寄存器中
因为this访问成员变量会很频繁,这样可以提高效率.