类与对象(一)

80 阅读21分钟

类和对象(一)

本章内容:

一、面向过程和面向对象的初步认识

  • 面向过程

面向过程的编程语言包括:C、Fortran、Pascal、Basic等。

面向过程:C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

下面来看一个简单的例子:

咱们以把大象放进冰箱为例,面向过程的方式分为三步:

img

  • 面向对象

面向对象(Object Oriented)是软件开发方法,一种编程范式。面向对象是相对于面向过程来讲的,面向对象方法,把相关的数据和方法组织为一个整体来看待,从更高的层次来进行系统建模,更贴近事物的自然运行模式。

面向对象的语言包括:C++,Java,Python,C#以及JavaScript等。

面向对象:C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

img

下面来看这个例子:

img

二、类的引入

在C语言中,struct结构体中只能定义变量。

在C++中,在兼容C语言原本结构的使用的前提下,将struct结构体升级成了struct类,并且增添了class类。会发现类不仅可以满足在结构体中定义变量,还可以满足在结构体中定义函数。

  • 结构体对象:(C)
typedef struct ListNode_c
{
    struct ListNode_c* next;
    int val;
}LTNode;
  • 类对象:(C++)
struct ListNode_CPP
{
    ListNode_CPP* next;
    int val;
};

C++是兼容C的,C的结构体对象的定义方法在C++中仍适用。

来看看形象一点的 类对象 例子:

#include <iostream>
#include <string>
using namespace std;

struct killer
{

	//各种 类的 方法/函数
	
	//补充弹药
	void fill_up_ammunition()
	{
		cout << "补充弹药" << endl;
		ammunition = 60;
	}
	
	//开火
	void fire()
	{
		cout << "开火" << endl;
		ammunition -= 1;
	}

	//切换武器
	void swap_weapon(string newweapon)
	{
		cout << "切换武器" << endl;
		swap(newweapon, weapon);
	}

	//获取弹药信息
	int get_ammunition()
	{
		return ammunition;
	}

	//获取武器信息
	string get_weapon()
	{
		return weapon;
	}

	//获取性别信息
	string get_sex()
	{
		return sex;
	}

	//获取年龄信息
	int get_age()
	{
		return age;
	}
	
	private:
    //成员变量
		int age = 19;
		string sex = "男";
		string weapon = "gun";
		int ammunition = 60;
};
int main()
{
	//类对象的实例化(killer既是类名也是类型)
    //(若是struct killer lx,则lx为结构体对象。若是killer lx,则lx为类对象,也可以叫结构体对象)
	killer lx;

	//调用函数来获取我的个人信息
	cout<<lx.get_age()<<endl;
	cout << lx.get_sex() << endl;
	cout << lx.get_weapon() << endl;
	cout << lx.get_ammunition() << endl;

	cout << endl << endl;
	//调用方法/函数来 行动/操作
	lx.fire();
	cout << lx.get_ammunition() << endl;
	cout << endl;

	lx.fill_up_ammunition();
	cout << lx.get_ammunition() << endl;
	cout << endl;

	lx.swap_weapon(string("sword"));
	cout << lx.get_weapon() << endl;
	cout << endl;


	return 0;
}

img

上面结构体的定义,在C++中更喜欢用class来代替。

注:

  1. class:

class定义的结构体对象/类对象的成员变量和成员函数都默认是私有private的。在类内可以被访问,在类外无法被访问。

  1. struct:

struct定义的结构体对象/类对象的成员变量和成员函数都默认是公有public的。在类内可以被访问,在类外可以实例化对象后,通过对象进行访问**(如: lx.age; lx.sex; lx.fire();** lx.fill_up_ammunition())

三、类的定义

1. 主体框架:

class className
{
    //类体:由成员函数和成员变量组成
};//一定要注意最后结尾的分号

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。 类体中内容称为类的成员:类中的变量称为类的属性成员变量; 类中的函数称为类的方法或者成员函数。

2. 两种定义方式:

  1. 声明和定义全部放在类体中

需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理(代码短会被当成内联函数处理,代码长会当成普通函数处理) img

  1. 类声明放在.h文件中,成员函数定义放在.cpp文件中

注意:成员函数名前需要加类名::

img

编辑推荐使用第二种 ,声明和定义分离,可以让类的整体成员看起来更直观更清晰。

  • 成员变量命名规则建议:

错误的成员命名:

class Date
{
public:
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
//分不清。不会报错,但有歧义,这种方法是不建议的
year = year;
}
private:
int year;
};

正确的成员命名:(_year也可以改成Myear等,只要能使其与成员变量区分开来即可)

class Date
{
public:
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?//分不清,有歧义,这种方法是有问题的
_year = year;
}
private:
int _year;
};

四、类的访问限定符及封装

1.访问限定符

a.访问限定符的意义:

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

b.访问限定符的分类及其说明:

  • 访问限定符分类:

img

  • 访问限定符说明:
  1. public修饰的成员在类外可以直接被访问

  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)

  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止

  4. 如果后面没有访问限定符,作用域就到 } 即类结束

  5. class的默认访问权限为private,struct为public(因为struct要兼容C)

如:

class A
{
public: 
    void func1(int a)//属于public域,内外都可以直接访问,(外访问方式和访问结构体成员一样)
    {
      ;
    }

//遇到下一个访问限定符,这里的public终止
private:
    void func2(int b)//属于private域,只有内可以访问,外部没有权限,无法访问。
    {
      ;
    }
    
    int c;

//遇到下一个访问限定符,这里的private终止
protected://属于protected域,只有内可以访问,外部没有权限,无法方法。//除继承和多态外,                    
          //protected和private没有任何区别
    int d;
    int e;
    int f;
    int g;
//没有下一个访问限定符,到最后遇到},这里的protected终止
};


void test()
{
   A p;
   A.c=0;
   c.func2(1);
   
}

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

c.【面试题】(struct和class的区别):

问题:C++中struct和class的区别是什么?

解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用另外C++中struct还可以用来定义类。和class定义类是一样的,==区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。==注意:在继承和模板参数列表位置,struct和class也有区别。

结构体:

若在struct里定义了函数,则自动默认struct是类了。若只定义变量,则他可是结构体,也可以是类。

struct stu
{	

//int func1() 
//{
//;
//}
	const char *name;
	int age;
	int score;
};
void test()
{
	struct stu A;
	A.name = "zhangsan";
	A.age = 19;
	A.score = 100;
}

类:

struct stu
{	
public:
	const char* get_name()
	{
		return name;
	}
private:
	const char *name;
	int age;
	int score;
};

void test()
{
	stu A;
	//A.name是错误的,无法访问私有成员变量
	const char*B=A.get_name();
}

2.封装

(面向对象的三大特性:封装、继承、多态)

a.【面试题】(封装的定义和概念)

在类和对象阶段,主要是研究类的封装特性,那什么是封装呢? 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

**封装本质上是一种管理,让用户更方便使用类。**比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。

img

img

对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。

在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用

五、类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。

class Stu
{
public:
void PrintStuInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintStuInfo是属于Stu这个类域
void Stu::PrintStuInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}

六、类的实例化

1. 类的实例化定义:

用类类型创建对象的过程,称为类的实例化

注:类是没有分配空间的,类实例化出对象之后才分配空间

2.类实例化的理解:

**a.类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;**比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。只有实例化之后才会分配空间,即只有学生填写完学生信息表之后(注册完了),学校才会分配资源给学生。

b.一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。

c.做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。

img编辑

img

3.类的实例化

示例:

struct stu
{	
public:
	const char* get_name()
	{
		return _name;
	}
private:
	const char *_name="无名氏";
	int _age=20;
	int _score=90;
};

以Stu为例,下面进行实例化

  • 错误的实例化方式

    示例1:

int main()
{
Stu._age = 100; // 编译失败:error C2059: 语法错误:“.”
return 0;
}

​ 示例2:

int main()
{
//Stu ()._age=100;  //编译失败
//Stu A._age=100;   //编译失败
return 0;
}
  • 正确实例化方式

​ 示例:

int main()
{
	Stu stu1("小明",20,90);//"小明"初始化_name; 20初始化_age; 90初始化_score; //带参构造
    Stu stu2(stu1);//等价于Stu stu2=stu1;//拷贝构造
    Stu stu3();//无参构造,全缺省,使用缺省参数初始化。
    Stu stu4("张三",,);//带参构造,半缺省,使用缺省参数初始化。
}

注:

  • Stu类是没有空间的,只有Stu类实例化出的对象之后才有具体的年龄,不能在实例化之前直接使用类的年龄或者实例化的过程中使用正在实例化的未生成的对象的年龄。但是根据代码,我们可以运用结构体对齐规则将其算出来。

类里的成员变量是声明

class Stu
{
public:
void PrintStuInfo();
private:
char _name[20];//声明
char _gender[3];//声明
int _age;//还是声明,因为类未实例化是没有分配空间的
};

4. 头文件中定义变量或实例化对象容易出现的错误


小知识1:extern

  • 问题:在.h头文件中 要慎用全局变量,因为在.h头文件中。int a;就已经是定义了变量,如果在包含了该头文件的.cpp中再int a=1;定义变量。未来在编译阶段时候,会报错-头文件中的a和.cpp中的a出现了重定义。

  • 解决方法:

  1. 外部声明extern
extern int age//在.h头文件中的int age定义;换成extern int age声明,这样就从定义改成了声明了

那在如果是定义在了Person.cpp中变量,那在main.cpp中怎么获取该变量呢?

答:在头文件中声明了之后,在汇编阶段生成符号表。在链接阶段Person.cpp和main.cpp都会去符号表里面找对应的地址链接起来,使Person.cpp和main.cpp把在两个文件中的变量,合并成一个。

小知识2:static

  • static静态链接属性:
//改变了链接属性
int age;//链接属性所有文件可见
static int sex;//链接属性只在当前文件可见,.h头文件的不会放进符号表,.cpp的不会去符号表里找,作用是:使该变量只在该文件中可见,只在该文件中使用;其他文件不可见,其他文件不可使用该变量。

//test.h
void test();


//test.cpp
#include <iostream>
#include "test.h"
using namespace std;
static int a=10;
void test()
{
    cout << &a << endl;
}


//main.cpp
#include <iostream>
#include "test.h"
using namespace std;
int main()
{
    static int  a = 10;
    cout << &a << endl;
    test();

    return 0;
}
  • static改变生命周期:
#include <iostream>
using namespace std;
class A
{
public:
    static int a;
};
int A::a = 10;
int main()
{
    A* a1 = new A;
    A* a2 = new A;
    A* a3 = new A;
    A* a4 = new A;
 

    cout << a1->a<<endl;//stdout 10
    a1->a--;
    delete a1;
    cout << a2->a<<endl;//stdout 9
    a2->a--;
    delete a2;
    cout << a3->a<<endl;//stdout 8
    a3->a--;
    delete a3;
    cout << a4->a<<endl;//stdout 7
    return 0;
}

可见,理应在对象销毁之后,对象内部应该跟着销毁的变量。居然被所有对象所公有,且不随对象的销毁而销毁,生命周期发生了改变。static把变量的生命周期延续到了整个进程结束。

  • 用static修饰的类成员变量,如果要赋缺省值,则必须是常量。(其实staitc修饰的类成员变量只能在类外部定义,后面我们会讲)


七、类的对象模型

1.如何计算类的大小

思考:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
char _a;
};

问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

答:非空类只考虑变量的大小,函数大小不考虑,函数存在公共代码区。空类会给一个1字节的标志位来表示其是一个对象。

**2.**类对象的存储方式猜想

  • 猜测一、对象中包含类的各个成员

缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?

  • 猜想二、代码只保存一份,在对象中保存存放代码的地址

  • 猜想三、只保存成员变量,成员函数存放在公共的代码段(正确猜想)

究竟是那种,我们可以来测试一下。先预测一下三种猜想测试出来的结果是什么。这里我们采用求类的大小来检测。

#include <iostream>
using namespace std;
// 类中既有成员变量,又有成员函数
class A1
{
public:
    void f1() {}
private:
    int _a;
};

// 类中仅有成员函数
class A2
{
public:
    void f2() {}
};

// 类中什么都没有---空类
class A3
{};

int main()
{
    cout << sizeof(A1) << endl;//stdout 4
    //分析:int _a;占了4四字节。若是猜想1,2成立,则应该还存在一个f1的函数指针在内部,大小应该为int _a;和一个f1的函数指针的大小。猜想1,2.结果都应当为8。但这里结果为4.所以排除了猜想1,2的可能。
    
    cout << sizeof(A2) << endl;//stdout 1
    //分析:既然已知猜想1,2均不成立,那类里面不应该是空的吗,sizeof(A2)和sizeof(A3)的大小不应该都为0吗?其实空类比较特殊,编译器给了一个字节来唯一表示这个类的对象。这里注意:类里有函数,但是没有成员变量的类,也算是空类,也会用一个字节来唯一表示该类的对象。
    cout << sizeof(A3) << endl;//stdout 1
    //分析:同sizeof(A2);的分析。
    return 0;
}

结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐

==注:注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。非空类则没有该特殊一字节的标识符==

3. 结构体内存对齐规则

关于这个,已在之前的章节已做过深入理解。

详情请看 结构体 struct 的深入理解:(169条消息) 结构体 struct 的深入理解_struct.pack 打包八个bit位_Placideo的博客-CSDN博客

结构体内存对齐 总结:

  1. 第一个成员在与结构体偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。VS中默认的对齐数为8

  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

八、this指针

1.this指针的引出

我们先来定义一个日期类Date

#include <iostream>
using namespace std;
class Date
{
    public:
    void Init(int year,int month,int day)
    {
        _year=year;
        _month=month;
        _day=day;
	}
    
    void PrintDate()
    {
        cout<<_year<<'-'<<_month<<'-'<<_day<<endl;
	}
    
    private:
    int _year;//年
	int _month;//月
    int _day;//日
    int a;
}

int main()
{
    Date d1,d2;
    d1.Init(2023,2,23);
    d2.Init(2023,1,12);
    d1.Pirnt();
    d2.Print();
    return 0;
}

对于上述类,有这样的一个问题:

Date类中有 Init 与 PrintDate两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

  • 编译器自动处理--(以下是编译器自动处理的,我们不能这样处理)

编译器会自动给每个非static的类成员函数的第一个参数默认为this指针,该指针的地址就是该对象的地址。但这个this是隐藏的,我们可以使用this,但是不能显示在第一个位写传参给this。

用法:

  1. 类内:在同一个类中,非静态类成员函数可以显示传递this给别的函数,但是不能显示接收来自别的函数的this。如果接收来来自
//同一个类中,非静态成员函数,不能接收来自别的函数的this
class Date
{
public:
    //error
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
 		PrintDate(this);
    }

    
    void PrintDate(Date* this)//这样显示接收本身的this指针是错误的。正确的是直接使用成员函数的this指针
    {
        cout << _year << '-' << _month << '-' << _day << endl;
        cout << this->_year << ' ' << this->_month << ' ' << this->_day << endl;
    }
    
    ---------------------------------------------------------------------------------
        
    //正确
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    
    void PrintDate(/*Date* this*/)//这里的Date* this是隐藏的,我们直接调用即可
    {
        cout << _year << '-' << _month << '-' << _day << endl;
        cout << this->_year << ' ' << this->_month << ' ' << this->_day << endl;
    }
private:
    int _year;//年
    int _month;//月
    int _day;//日
    int a;

   
};

//非静态函数可以给别的函数传递this指针(函数包括类内成员函数-静态、非静态,和类外函数。但是由于非静态成员函数无法接收,所以非静态成员函数传this指针给非静态成员函数是没有意义的)
void test(Date* this)
{
    cout<<this->_year;
}
  1. 类外:在类外没有隐藏的this指针,但是非静态类成员函数可以通过函数把this指针传出去,外部的函数可以定义一个该对象的指针来接收this指针外部也可以把this指针传进来-在类外调用成员函数传进来,在类内用区别于隐藏的this指针的指针来接收。
void Init(Date * const this,int year, int month, int day)
{
    this = nullptr;//不能这样改,因为const指针修饰了,但是指向的内容可以改
	this->_year = year;
	this->_month = month;
	this->_day = day;
}
void PrintDate(Date* const this)
{
	cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}

int main()
{
	Date d1;
	d1.Init(&d1,2022, 9, 27);
	Date d2;
	d2.Init(&d2,2022, 9, 28);
	d1.PrintDate(&d1);
	d2.PrintDate(&d2);
    return 0;
}

综上总结:

  • 可以接收-A a对象-(别的对象)的this指针:非该类-A类-(别的类)的对象的对象的成员函数,外部的函数。
  • 可以接收本身对象的this指针:外部的函数。
  • 可以传递-A a对象-(别的对象)的this指针:该类-A类-(别的类)的非静态 成员函数。
  • 可以传递本身对象的this指针:该类的成员的非静态成员函数。

注:假设Person类是该类,则A类是区别于Person类的其他类。


如果上面对this指针的用法的解读有些许绕,那么记住这句话即可。

==类外可以随意传递该对象 或 别的对象指针;类内同一个类之间传递该对象的this指针是没有意义的,因为传递该对象的this指针语法上是正确,但是显示接收该对象的this指针是错误的。==

2.指针的特性

  1. this指针的类型:类类型* const。
  2. this指针一般情况下是在"成员函数"的内部使用,也有的时候会在内部使用函数获取到this指针给外部去使用(但是外部一般不会拥有访问内部成员变量的权限,所以给了this一般只能调用函数,而不能直接查看变量,但可以通过函数调用在内部间接查看或获取)。
  3. this指针本质上是一个成员函数的形参,是对象调用函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。而是在外面隐形的传进去。
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过exc寄存器自动传递,不需要用户传递。
  • 通过下面代码深入理解this指针
class E
{
public:
	void PrintE()
	{
		cout << _e << endl;
	}
	void Show()
	{
		cout << "Show()" << endl;
	}
private:
	int _e;
};

void test_this_character()
{
	E* p = nullptr;
	//p->Show();//第一句代码 run success
	//p->PrintE();//第二句代码 run error
}

  • 问:为什么第一句代码run success,第二句代码run error了?

p为空指针,第一和第二句代码都通过操作费"->",间接性的对p进行了解引用操作,一般来说是会认为程序是崩溃的。

但结果却是第一句代码run success了,而第二句代码run error了。

解释:

第一句代码:指针p是一个类的空指针,但当执行第一句代码时,程序并不会崩溃,因为第一句代码并没有对p进行解引用,因为Show等成员函数地址并没有存到对象里面的,里面代码段公共存在是地址的函数成员所以调用这些函数时并不需要对象指针址解引用

第二句代码:当程序执行第二句代码时,会因为内存的非法访问而崩溃,因为第二句代码,调用PrintE(此时并不会崩溃),然后打印了成员变量_e,成员变量 _e是存在对象里面的,要获取到成员变量是需要对对象指针取地址的,此时this指针为空,就会导致对空指针取地址而导致程序崩溃了