目录
一、面向过程与面向对象的初步认识
二、类的引入
三、类定义的两种方式
四、类的访问限定符及封装
1.封装
2.访问限定符
五、类的实例化
六、类对象大小及存储方式
七、this指针
1.隐藏的this指针
2.this指针的练习
C语言是 面向过程 的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是基于 面向对象 的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。比如我们要实现一个外卖点餐系统:面向过程:关注的是实现下单、接单、送餐这些过程。它体现到代码层面就是函数的实现;面向对象:关注的是实现类对象及对象之间的关系,用户、商家、骑手以及他们之间的关系。体现到代码层面就是类的设计及类之间的关系;
什么是类?
年龄作为类,可以包含青少年、中年人、老年人;学生作为类,可以包含姓名、学号、班级;日期作为类,可以包含年、月、日;
从这里可以看出和C语言中的结构体很类似,但是在C++中把结构体升级为了类;在C语言中我们创建了结构体后,想要定义一个结构体的变量,常常是这样定义的:struct Student s1;在C++中可以不用加struct,直接用Student s2;来进行定义变量;但是这里不能称之为变量,而是对象(即s2是Student的一个对象);这样的目的就是为了去兼容C而设计的;
struct Student
{
char name[10];
int age;
int id;
};
int main()
{
struct Student s1;//兼容C 去使用
Student s2; //升级到类,Student类名,也是类型;
strcpy(s1.name, "zhangsan");
s1.age = 18;
s1.id = 001;
strcpy(s2.name, "lisi");
s2.age = 20;
s2.id = 002;
return 0;
}
上图中的struct类,在C++中更喜欢用class来代替;
class className { // 类体:由成员函数和成员变量组成 }; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。 类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
1. 声明和定义全部放在类体中
struct Student
{
//成员变量
char _name[10];
int _age;
int _id;
//成员方法/成员函数
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void Print()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
}
};
int main()
{
struct Student s1;//兼容C 去使用
Student s2; //升级到类,Student类名,也是类型;
s1.Init("zhangsan", 18, 1);
s2.Init("lisi", 19, 2);
s1.Print();
s2.Print();
return 0;
}
2. 声明放在.h文件中,类的定义放在.cpp文件中
面向对象的三大特性: 封装、继承、多态 。在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们目的全封装起来,不让别人看。所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一下。不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问限定符 访问权限 public(公有) 类内与类外 private(保护) 类内 protected(私有) 类内 【访问限定符说明】1. public修饰的成员在类外可以直接被访问2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止4. class的默认访问权限为private,struct为public(因为struct要兼容C)注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
假如我们不想让外界访问我们的成员变量,仅仅里提供两个函数接口;
//定义了一个日期类
class Data
{
//公有部分包含了日期类的初始化和打印函数(成员函数)
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()
{
return 0;
}
当你在类中只有函数声明时,你想要在类的外部进行函数的定义;此时就需要用到作用域解析符 :: ;
//Stack.h
class Stack
{
//共有部分只有函数声明,没有定义;
public:
void Init();
void Push(int x);
private:
int* _a;
int _top;
int _capacity;
};
//Stack.c
#include "Stack.h"
void Stack::Init()//当我们在类外定义时,需要加上作用域解析符 ::,因为类的作用域仅仅只在类的内部
{
_a = nullptr;
_capacity = _top = 0;
}
void Stack::Push(int x)
{
}
用类类型创建对象的过程,称为类的实例化
1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它2. 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
//定义了一个日期类
class Data
{
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()
{
//将类Data实例化出多个对象
Data d1;
Data d2;
Data d3;
Data d4;
return 0;
}
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
//类中什么都没有---空类
class A3
{};
int main()
{
Stack s;
s.Init();
//对象中存了成员变量,是否存了成员函数呢?---没存
cout << sizeof(Stack) << endl;//12
cout << sizeof(s) << endl;//12
//空类会给1字节,这1byte不存储有效数据,只是为了占位,表示对象存在
A2 aa;
A2 bb;
cout << &aa << endl;
cout << &aa + 1 << endl;
cout << &bb << endl;
cout << &bb + 1 << endl;
return 0;
}
从上面的代码及运行结果来看,Stack类所占字节数为12;因为有3个int, 说明了它本质上和结构体中内存对齐是一样的,虽然类里面有成员函数,但是只占12字节,说明成员函数并没有存在类中,其实成员函数是存在公共的代码段;目的就是为了减少空间浪费;
对于A2、A3而言,都是空类(虽然A2有成员函数,但是不算做类里面成员所占空间),我们对其地址进行打印,发现是有地址的,打印出aa的地址是010FF703,并且它下一个地址为010FF704(bb也是如此),这就说明对于没有成员变量和空类而言,他们也是占用空间的,占1个字节;
总结:计算类或者类对象的大小,只看成员变量,考虑内存对齐,C++内存对齐规则跟C结构体一致
class Data
{
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()
{
//对于打印、如何确定它是调用的d1还是d2
Data d1;
d1.Init(2022, 1, 15);
d1.Print();
Data d2;
d2.Init(2022, 1, 18);
d2.Print();
return 0;
}
上述的代码,我们定义了一个日期类; 并且实例化出两个对象:d1和d2;他们都去调用同一个函数(以Print为例),编译器是如何调用d1和d2的呢?这里就其实隐藏了一个this指针。
d1和d2都是传的三个参数,其实本质上是传了4个,这里是被省略了;并且函数的形参也是4个参数,也被省略了;如下面的代码所示,为正在的调用过程
class Data
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
/*
上面代码处理如下(一般情况下,不会这样写)
void Init(Data* const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print(Data* const this)
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
*/
private:
int _year;
int _month;
int _day;
};
int main()
{
//对于打印、如何确定它是调用的d1还是d2
Data d1;
d1.Init(2022, 1, 15); //被处理成d1.Init(&d1, 2022, 1, 15);
d1.Print(); //被处理成d1.Print(&d1);
Data d2;
d2.Init(2022, 1, 18); //被处理成d2.Init(&d2, 2022, 1, 18);
d2.Print(); //被处理成d2.Print(&d2);
/*************************************************************/
//d2.Print(&d2);不可以自己把this指针加上
/*
1.调用成员函数时,不能显示传实参给this;
2.定义成员函数时,也不能显示声明形参this;
3.在成员函数内部,我们可以显示使用this;
*/
/*
重点:
this指针放在哪里?
1.一般情况下是在栈中的(形参)
2.有些编译器会放到寄存器中,如VS2019 ,放到了ecx中
*/
return 0;
}
判断下面两个程序的运行结果?
A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void show()
{
cout << "show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->show();
return 0;
}
/********************************************/
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
}
分析:
1.p虽然是空指针,但是p调用成员函数不会编译报错,因为空指针不是语法错误,编译器检查不出来;
2.p虽然是空指针,但是p调用成员函数也不会出现空指针访问。因为成员函数没有存在对象里面,是在代码段;
3.这里会把p作为实参传递给隐藏的this指针。
对于第一道题:它没有解引用this,第二道题堆空指针解引用了,就出现了运行崩溃