类和对象(上)

160 阅读4分钟

面向过程和面向对象初步认识

什么是面向过程?

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(20221112);
    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 成员函数、成员变量都存到对象中

当一个类实例化多个对象时,每个对象都保存一份函数代码,

相同的函数代码保存多份,浪费空间.

image.png

方案2 对象存成员变量,以及一张表的地址

成员函数代码只保存一份,成员函数地址单独用一张表存起来,对象中存这张表的地址.

image.png

方案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]取到的是随机值

image.png

总结

由于类的设计会有偏差

例如栈的_top如果初始化为-1,那_top就是栈顶元素的下标;

_top如果初始化为0,那_top就是栈顶元素的下一个位置下标.

类里有些成员不应在类外面直接被访问应该设置权限加以限制.

三种访问限定符

c++通过访问权限的设置,将接口(函数)提供给外部用户使用,隐藏类的实现细节(属性).

image.png

(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访问成员变量会很频繁,这样可以提高效率.