感觉很多教程(例如b黑马程序员)都能把知识点讲清楚,却不能帮助学生构建知识体系,这个总结不仅会有知识点的梳理,还会尝试进行知识体系的构建来方便我自己记忆,理解,回顾这个内容的知识。当然因为我只是刚学习这个内容的学生也难免有错。
一、直击本质
跳过思考过程,我的结论是类与对象的内容都是为了“归纳大同,细化小异”也即分类这个目的,所有的知识点和功能都为这个目的服务。这样想很多知识就比较容易理解了。由于这个内容是与实践高度相关的,所以说明知识点的时候会举实例。
二、封装
1.封装的意义:创造概念,整合管理
要分类就得先有需要分类的概念即类。封装就是构建一个类的基础概念的方法,代码使用(类名以Student为例):
class Student
{public:
protected:
private:
};
关public:、protected:、privatee:后续会有提到
2.自然语言角度类的构建及语法:
(1)一个对象或者类必然有其的【属性】和【功能】,这在编程语言中体现为类中的【成员变量】和【成员函数】
就如生活中学生有姓名和学号,会写作业。
下例为类内成员函数和成员变量的写法(以学生类为例):
class Student
{public:
string m_name;
int m_id;
void dowork():
};
void Student::dowork()
{cout<<"学生在写作业"<<endl;
}
上例中为学生类中 成员变量 姓名、学号,成员函数 作业的构建。
值得注意的是:成员函数最好在类内声明,类外实现。
(2)就像研究物体的运动需要知道物体的状态和位置这些【初始值】
在我们 使用 类的时候也需要用一些初始值,这就需要【初始化】这个操作
一般变量的初始化我们可以通过类似int a=1;的形式实现,而在类内我们可以通过【构造函数】来实现
【构造函数】分为:默认构造函数、含参构造函数、拷贝构造函数 三种
有如下性质:
1.构造函数名与类名相同,无返回值,不用写void;
2.默认构造函数在类实例化的时候自动调用;
这里需要提到另一个知识点:类的实例化,就像学生类是泛指,而具体来说可以有一个小A学生,这就类似类的实例化
下例为类的实例化与默认构造函数的调用,和运行结果
#include <iostream>
#include<string>
using namespace std;
class Student
{
public:
int m_id;
string m_name;
Student()
{
cout << "Student的默认构造调用" << endl;
}
};
void test01()
{
Student a;//类的实例化
}
int main()
{
test01();
system("pause");
return 0;
}
3.含参构造函数与拷贝构造函数的定义
#include <iostream>
#include<string>
using namespace std;
class Student
{
public:
int m_id;
string m_name;
Student(int a,int b)
{
cout << "Student的含参构造调用" << endl;
}
Student(Student &a)
{
cout << "Student的拷贝构造调用" << endl;
}
};
void test01()
{
Student a(1,2);
Student b(a);
}
int main()
{
test01();
system("pause");
return 0;
}
了解了这些性质后我们就可以利用这些构造函数来初始化成员变量了
下列分别用默认构造函数、含参构造函数、拷贝构造函数,来初始化成员变量
#include <iostream>
#include<string>
using namespace std;
class Student
{
public:
int m_id;
string m_name;
Student()
{
m_id = 2;
m_name = "小A";
}
Student(int id,string name)
{
m_id = id;
m_name = name;
}
};
void test01()
{
Student a;
cout << "实例a的姓名:" << a.m_name << endl;
cout << "实例a的学号:"<<a.m_id << endl;
Student b(5, "小B");
cout << "实例b的姓名:" << b.m_name << endl;
cout << "实例b的学号:" << b.m_id << endl;
Student c(b);
cout << "实例c的姓名(应与b相同):" << c.m_name << endl;
cout << "实例c的学号(应与b相同):" << c.m_id << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
定义一个类概念,设定类内的【属性】和【能力】,即【成员变量】和【成员函数】,再把他们初始化。从自然语言的角度来说这已经完全建立好了一个类了
但是在编程语言的角度,我们更加关系这个类的使用,但只是这些性质,还不能满足我们使用的需求,封装还没有结束。
3.编程使用角度封装完整构建
(1)三种实例化方式
1.括号法:
Student a;
Student b(参数);
Student c(实例化类);
2.显示法
Student a;
Student b=Student (参数);
Student c=Student (实例化类);
3.隐式转换法
Student a;
Student b=参数;
Student c= 实例化类;
(2)访问权限:
成员变量,成员函数都可以设置访问权限,访问权限有三种
1.public:公共权限,类内类外都可以访问
2.protected:保护权限,类内可以访问,类外不能访问
3.private:私有权限,类内可以访问,类外不能访问
借助这个功能我们可以,通过公共函数+私有(保护)变量的方式来控制修改成员变量
(3)析构函数
在通过函数修改指针数据类型时,会在堆区申请内存空间,也需要手动释放,析构函数通常就用来完成这个任务。
析构函数形式:~类名()
性质:在类使用结束后调用
使用如下:
#include <iostream>
#include<string>
using namespace std;
class Student
{
public:
string* m_name;
Student()
{
m_name = new string;
}
Student(string name)
{
m_name = new string(name);
}
void print(string name);
~Student()
{
if (m_name != NULL)
{
delete m_name;
m_name = NULL;
}
}
};
void Student::print(string name)
{
*m_name = name;
cout << "姓名为:" << *m_name<<endl;
}
void test01()
{
Student a;
a.print("小A");
Student b("小B");
cout << "实例b的姓名为:" << *b.m_name << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
(4)静态成员
1.静态成员变量:
在对类的使用中,类可以是个概念同样也可以是对象(这里指自然语言中),如说学生有10个的时候,我们把学生这个类型当做对象来进行计算,这样计算的结果与类实例化的对象的具体属性无关,而与类本身有关。
而对于这种时候我们就可以用静态成员变量来表示这种量
静态变量有如下性质:
1.不属于某个实例中,也不属于保存在类中
2.需要再类内声明,类外初始化
3.可以直接使用 类名::变量名 形式访问
4.也有访问权限
其使用如下
class Student
{
public:
int m_id;
string m_name;
static int m_number;
Student()
{
m_number++;//用构造造数修改静态变量好像是无效的
}
Student(int id, string name)
{
m_id = id;
m_name = name;
}
void print();
~Student()
{
m_number++;
}
};
int Student::m_number = 0;
void Student::print()
{
m_number++;
}
void test01()
{
Student a(1, "xiao");
a.print();
Student::m_number++;
}
int main()
{
test01();
cout << "学生数:" << Student::m_number << endl;
system("pause");
return 0;
}
2.静态成员函数
与静态成员变量性质有些类似:
1.不属于某个实例中,也不属于保存在类中
2.需要再类内声明,类外定义
3.可以直接使用 类名::函数名 形式访问
4.也有访问权限
5.不允许访问非静态成员变量
感觉无现实意义对应
#include <iostream>
#include<string>
using namespace std;
class Student
{
public:
int m_id;
string m_name;
static int m_number;
static void print();
};
int Student::m_number = 0;
void Student::print()
{
cout << m_number << endl;
}
int main()
{
Student::print ();
system("pause");
return 0;
}
第二部分总结:
这个部分主要说明了封装的意义和实际实际使用,以构建一个类为主干,涉及知识点为:
用类内的【成员变量】、【成员函数】定义自然语言中的【属性】、【行为】,
为成员变量【初始化】
【实例化】对象
方便使用的性质
三种【访问权限】、【静态成员】
三、继承
不同类之间可能有异同关系,这种关系可以通过继承来体现
初学时我会认为父类被子类继承,子类包含父类的成员,所以应该大范围与小范围的关系
但后面发现这也包含了继承同父类的两个子类之间异同关系。
(1)继承操作与继承方式
类可以通过继承操作来继承其他类的成员
被继承的类称为父类(或基类),继承的类(派生类)
形式为 class 类名:继承方式 已定义类名
可以一个子类继承多个父类
class 类名:继承方式 父类名1,继承方式 父类名2...
以动物与小猫为例,使用如下:
#include<iostream>
#include<string>
using namespace std;
class animals
{
public:
int cell;
animals()
{
cell = 1000;
}
void eat()
{
cout << "动物在吃饭" << endl;
}
};
class cat:public animals
{
public:
int huxu;
};
void test01()
{
cat a;
cout << a.cell << endl;
a.eat();
}
int main()
{
test01();
system("pause");
return 0;
}
继承方式效果如下:
(2)继承构造与析构的先后关系:
我是这样理解构造与析构的先后关系的:
人是由躯干、四肢、内脏等组成的,可以说人类继承是自躯干类、四肢类、内脏类的子类,
所以是先有躯干类、四肢类、内脏类构造,才有人类的构造,先有父类的构造,才有子类的构造
而人类整体变得不完整,躯干类、四肢类、内脏类这些部分才跟着没,所以是先有子类的析构,才有父类的析构,即与构造顺序相反
(3)同名变量与静态同名变量在继承中的处理
子类中的同名变量可以直接访问,父类中需要加作用域
子类中静态同名变量可以直接访问,父类中需要加作用域
第三部分总结:
继承是体现类之间关系的操作,方便操作的同时,增加可读性
重点在于明白编程方面的性质即【继承方式】、【构造与析构的先后关系】、【同名变量的访问处理】
四、多态
成员函数代表的是自然语言中的行为,但是不同的种类也会有自然语言中相同的行为,如小猫的吃饭,和小狗的吃饭,
而这种情况我们大都希望用一个名字的函数来表示(因为方便),这就需要用到多态这种特性。
也就是说,多态体现的是同名函数的不同类中表现,是异同分类的重要实际标准之一。
(1)静态多态与动态多态
静态多态:函数地址早绑定:编译阶段确定函数地址(函数重载)
动态多态:函数地址晚绑定:运行阶段确定函数地址
(2)多态的使用例
1.虚函数 虚继承
virtual 返回值类型 函数();
性质:
写在父类函数中 class 类名: virtual 继承方式 父类名
可以达到虚继承的效果,解决菱形继承(一父继给两子,两子作两父继给一子,最后一子获得多个初始父的成员)的问题
解决访问同父类的子类之间、父类的同名函数问题,达到传a子访问a子,传b子访问b子,传父访问父
例
#include<iostream>
#include<string>
using namespace std;
class animals
{
public:
int cell;
animals()
{
cell = 1000;
}
virtual void eat()
{
cout << "动物在吃饭" << endl;
}
};
class cat:public animals
{
public:
int huxu;
void eat()
{
cout << "猫在吃饭" << endl;
}
};
class dog :public animals
{
public:
int huxu;
void eat()
{
cout << "狗在吃饭" << endl;
}
};
void doeat(animals &a)
{
a.eat();
}
void test01()
{
animals c;
dog b;
cat a;
doeat(a);
doeat(b);
doeat(c);
}
int main()
{
test01();
system("pause");
return 0;
}
2.运算符重载
重载运算符虽然是多态,却只体现方便操作的原则
运算符重载用几种
主要可以分为
*1二元计算运算符重载
+号例
成员函数作运算符重载
animals operator+(animals &a)
{ 重载操作
加号则成员相加赋值给临时类
return 临时类
}
全局函数作运算符重载
animals operator+(animals &a,animals &b)
{ 重载操作
加号则成员相加赋值给临时类
return 临时类
}
*2左移运算符重载
只能利用全局函数重载<<运算符
ostream& operator<<(lstream&cout,anmals&p)
{
cout<<....;
ruturn cout;
}
*3递增运算符
aninmals& operator++(animals&p)//前增
{ p.成员名++;
p.成员名++;
return &p;
}
aninmals operator++(animals&p)//后增
{animals q=p;
p.成员名++;
p.成员名++;
return q;
}
代码如下,this相关知识点在后面有展示
#include<iostream>
#include<string>
using namespace std;
class animals
{
public:
int cell;
animals()
{
cell = 0;
}
virtual void eat()
{
cout << "动物在吃饭" << endl;
}
animals& operator++()
{
this->cell++;
return *this;
}
animals operator++(int)
{
animals a = *this;
cell++;
return a;
}
};
ostream& operator<<(ostream& cout, animals& p)
{
cout << p.cell << endl;
return cout;
}
void test01()
{
animals a;
cout << a << endl;
cout << ++a << endl;
cout<<a++<<endl;
cout << a<< endl;
}
int main()
{
test01();
system("pause");
return 0;
}
第四点总结:
多态是用来解决同名函数,不同实现的方法,也是最开始分类目的的重要依据,除此之外是成员变量;
具体实现为【虚函数】,【运算符重载】
五、其他因需求和问题的产生的解决方法及思路
(1)this指针
这个指针无需要定义,在类内有效,指向类本身
可以用于返回自身,解决名称冲突
(2)空指针
将实例类的指针放置空
可以用来访问不包含类内变量的类内函数。
若打算使用时,要注意写函数时要用this指针判断是否为NULL
(3)深拷贝,浅拷贝
若类内使用new数据类型指向堆区,调用编译器默认提供的拷贝函数,会导致两个实例类指向同一个堆区地址,
在析构时重复释放导致报错,这时调整拷贝函数以解决问题
例:
#include<iostream>
#include<string>
using namespace std;
class animals
{
public:
int m_cell;
string* m_name;
animals()
{
}
animals(int cell ,string name)
{
m_name = new string(name);
}
animals(animals& a)
{
m_cell = a.m_cell;
m_name = new string(*a.m_name);
}
~animals()
{
if (m_name != NULL)
{
delete m_name;
m_name = NULL;
}
}
};
void test01()
{
animals a(10,"小猫");
animals b(a);
cout << "实例a:"<<a.m_cell << " " << *a.m_name << endl;
cout << "实例b:" << a.m_cell <<" " << *a.m_name << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
(4)友元
可用friend关键词,在类内设定这个类的友元,使得友元可以访问类中的保护权限成员
由于访问是操作,所以用函数表示,所以友元可以设定为带函数的对象:友元成员函数、友元类、友元全局函数
下以友元类为例
#include<iostream>
#include<string>
using namespace std;
class animals
{
public:
friend class ni;
int m_cell;
animals()
{
}
animals(int cell, string name)
{
m_cell = cell;
m_name = new string(name);
}
~animals()
{
if (m_name != NULL)
{
delete m_name;
m_name = NULL;
}
}
protected:
string* m_name;
};
class ni
{
public:
void print1(animals& a);
};
void ni::print1(animals& a)
{
cout << a.m_cell << endl;
cout << *a.m_name << endl;
}
void test01()
{
ni a;
animals b(10,"旺财");
a.print1(b);
}
int main()
{
test01();
system("pause");
return 0;
}
六、全文总结
可以看出类与对象的内容就是分类,对分类进行操作的内容,
特性【封装】用于创建用于分类的类概念并具备相应的功能,
特性【继承】用于体现类与类之间的关系,既能直接体现父类与子类的关系,也能间接体现子类之间的关系。
特性【多态】创造方便的使用形式,也是用于分类的本质的具体的特征之一
其他的知识就是为了解决从自然语言到编程语言转变的不便,和阻碍。
总得来看,我们还能发现这个学习过程是自然语言转向编程语言的过程,是概念走向使用的过程
有趣的是以知识点灌输形式的教程把象征两个端点的知识点都放在了开始,这也是我为什么认为大多教程都不能帮助学生构建知识体系