C++萌新来看,一篇文让你让你彻底搞定类(超详细)!
Posted KookNut39
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++萌新来看,一篇文让你让你彻底搞定类(超详细)!相关的知识,希望对你有一定的参考价值。
前两天有位粉丝向我求助,说类好难懂,老师讲的他听懂了,但没完全懂。我顺便问他现在还单身吗?他说是单身,ok,那就好好学类,学校不分配对象,我们自己给自己new个对象出来就行了啊!!!
学习C++,类是我们永远绕不开的话题,有关类的知识点实在是太多了,所以废话不多说,直接进入正题吧。
此文适合C++初学者或者学了但没学会的读者朋友,码字不易,喜欢的话,一键三连,评论支持。您的鼓励是我最大的动力。
文章目录
一、什么是类?
1.类的概念
有关类的官方定义可以通过百度百科查看,但是我觉得官方的话总是比较抽象的,就像类本身一样。我用通俗的语言说一下自己对类的理解,类就是把数据和函数进行抽象,然后进行封装起来,对外部只提供一个接口去使用。外部的调用者看不到里面发生了什么。
举个例子:我们可以把狗抽象成类,狗有头,有爪子,有耳朵,这些相当于类的数据成员,然后狗会跑,跳,吠,吃东西,这些行为可以定义为类中的函数,这样我们就完成了类的抽象。
我们都知道狗是一个类,那每条狗都是一个实例,狗都有自己的名字,比如“二愣”,“黑豹”,“八戒”,“花猫”等名字,这些都是狗这种类产生的对象,他们都具有那些跑跳等行为,也都有爪子和耳朵。如果家里面狗狗听话,你让它叫他就会叫,但是至于它具体是怎么发出声音的,就不是主人考虑的问题了,主人只是知道它具有叫的功能,并且会使用它就行了。如果我们想改变狗的叫声,想让他和绵羊一样发出咩咩的声音,它是做不到的,也就是说我们调用者没有办法改变已经封装好的接口,我们只能使用,这就是类的封装性。
那狗又可以划分成各种各样的品种,比如罗威纳,阿拉斯加,藏獒,牧羊犬等等,就拿牧羊犬来说吧,它具有狗的那些所有基本特征的同时,又有自己的特殊能力,那就是这种狗可以牧羊,这就是类的继承性,在原有类的基础上,子类可以派生出自己特殊的能力,相当于更细化它的能力了,且不影响其他的子类。
看了半天看的烦了,接下来给大家讲一个故事:小丁有一个狗场,狗场中有许多的狗,那必然是有一个狗老大,替主人管理狗场的治安,而“黑豹”就是狗场的老大,它是一条霸气的罗威纳,每当哪个狗不听话,小丁就让“黑豹”去收拾它,但是狗场不是打打杀杀,狗场那是狗情世故,“黑豹”遇到关系好的狗,比如“八戒”,它就意思意思假装吠两声不会实质性攻击,但是遇到那种看着不爽的,比如“花猫”,那就直接干倒在地。“黑豹”对不同的狗狗,有不同的收拾方式,这就是类的多态性。
类的概念就说到这里,上面说到的抽象性、封装性、继承性、多态性是类的四大特性,这四种特性互相依赖,会在后面的内容中都涉及到。
2.在c++中声明一个类
2.1类的简单声明与解释
class是C++中的关键词,关键词可以理解为C++本身定义的变量,那用这个变量就可以定义类了:
class Class_Dog
{
public:
Class_Dog();//构造函数
~Class_Dog();//析构函数
};
Class_Dog::Class_Dog()
{
}
Class_Dog::~Class_Dog()
{
}
好好看一下这个类中包括了什么:
class关键字后面跟的Class_Dog就是类的名字,以后就用它来定义对象了。
接下来是publice关键词:public意思事公开的,那它在这里的意思自然就是下面包含的两个函数是公有的。有关public,接下来会细讲,先这样了解一下。
Class_Dog()是当前类的默认构造函数,构造函数可以干什么?可以在类生成一个对象时候,为这个对象分配内存空间。毕竟对象也是一个变量,需要存放在内存空间中,而构造函数就是干这个的。
~Class_Dog()是当前类的默认析构函数,析构函数可以在对象销毁的时候,释放内存空间,保证程序的安全性,防止造成内存泄露。
特别注意:类的名字和构造函数、析构函数的名字必须一致!!!
Class_Dog::Class_Dog()
{
}
Class_Dog::~Class_Dog()
{
}
这两段代码相信大家都能看懂,它什么也没干,就是写了个函数的定义,毕竟函数只是声明是不可以调用的,必须要有定义,但是这个函数定义好像和平时我们见到的函数定义并不相同?
拆解来看:首先在双冒号‘::’前面的Class_Dog是返回值类型,这个和我们平时见到的int、void都是一个概念,可以把它理解成数据类型,然后是双冒号“::”,这个东西叫做作用域,也就是说告诉编译器,后面这个函数,必须是我这个Class_Dog类生成的对象才能用,或者我内部自己用,别人不可以用!!!然后剩下的就是常规的函数定义了。
接下来,我们开始扩充我们的狗类!让我们的狗看起来威武一点,现在有点太虚了!
2.2 类的变量声明和初始化
这个代码可以直接运行测试,大家可以复制到自己的编译器里面进行测试。
#include<iostream>
using namespace std;
class Class_Dog
{
public:
Class_Dog();
~Class_Dog();
Class_Dog(string name,int age);//有参构造函数
private:
int legs;//狗腿
int head;//狗头
int mouth;//狗嘴
public:
string name;//狗狗名字
int age;//狗狗年龄
};
Class_Dog::Class_Dog()
{
//对于狗狗的外帽基本特征,在默认构造函数中进行赋值处理
legs = 4;
head = 1;
mouth = 1;
//对于这些不确定变量,可以在这里进行初始化为NULL
name = "";
age = 0;
}
Class_Dog::~Class_Dog()
{
}
Class_Dog::Class_Dog(string name, int age)
{
//对于狗狗的外帽基本特征,在有参函数中也可以进行赋值处理
legs = 4;
head = 1;
mouth = 1;
this->name = name;
this->age = age;
}
int main()
{
Class_Dog Dog_1("Mikey", 5);
Class_Dog Dog_2;
return 0;
}
相较于之前的Class,我们现在添加了什么呢?
一个函数,一个public关键词,一个private关键词,还有一些变量。
首先我来解释一下为什么又添加一个pubilc关键字,之前已经有了一个public关键字了,我们确实可以将下面代码中的两个变量直接写到最初的public下,我这么写是为了将函数和变量分开放,不至于混乱,纯属个人习惯。
public:
string name;//狗狗名字
int age;//狗狗年龄
我们定义了两个变量,一个是字符串类型,存放狗狗的名字,一个是整型,用来存放狗狗的年龄。public可以保证我们的这些函数和变量在类外可以被访问。
private:
int legs;//狗腿
int head;//狗头
int mouth;//狗嘴
private关键词,这是类的访问控制的又一关键词,只有类内成员函数才可以访问这些私有变量。在此处定义了一些基本的狗的特征,这些特征是不允许被外部成员访问的。
接下来定义了一个函数,这个函数我们把它叫做有参构造函数,和之前的无参构造函数相比,多了两个参数,是从外部传进来的:
Class_Dog(string name,int age);//有参构造函数
Class_Dog::Class_Dog(string name, int age)
{
//对于狗狗的外帽基本特征,在有参函数中也可以进行赋值处理
legs = 4;
head = 1;
mouth = 1;
this->name = name;
this->age = age;
}
从函数的定义来看,在有参构造函数中,我们对变量进行了初始化操作,注意:由于内部变量名和参数名是一致的,所以使用this指针进行区分,this指针是类特有的指针,可以用来指向类内的非静态成员和函数。
Class_Dog::Class_Dog()
{
//对于狗狗的外帽基本特征,在默认构造函数中进行赋值处理
legs = 4;
head = 1;
mouth = 1;
//对于这些不确定变量,可以在这里进行初始化为NULL
name = "";
age = 0;
}
这是无参构造函数,也是简单的对变量进行了初始化操作。
int main()
{
Class_Dog Dog_1("Mikey", 5);
Class_Dog Dog_2;
return 0;
}
我们来看一下最后的main函数中声明的两个变量Dog_1和Dog_2,首先说一下类的对象声明,一般来说和普通的类型声明是一样的,是 类名 对象名; 的形式,就像Dog_2一样,还有一种声明的形式就是Dog_1这种,类名加上对象名和参数。其中Dog_1应该是调用有参构造,Dog_2默认调用无参构造。其实这里涉及到了多态性的特性,因为Class_Dog(string name, int age)和Class_Dog()具有同样的函数名字,却可以根据是否有参数,编译器来决定去调用哪一个函数。
我们来看一下调试结果:
两个变量最大的不同在于共有成员的值不同,这也符合我们的预期。
2.3 类内成员函数的实现
现在这个狗稍微有点东西了,但是他还不会执行我们的命令,对于主人来说,肯定希望自己的狗狗有一些技能,我们现在来写一些供主人外部调用的接口,让狗狗完成进化。
#include<iostream>
using namespace std;
class Class_Dog
{
public:
Class_Dog();
~Class_Dog();
Class_Dog(string name,int age);//有参构造函数
void Dog_run();//跑
void Dog_bark();//吠
void Dog_eat();//吃
private:
int legs;//狗腿
int head;//狗头
int mouth;//狗嘴
public:
string name;
int age;
};
.........//此处省略的代码在上一个代码段中可以找到,时有关构造函数等函数的定义
//为了篇幅稍微短点,在此处只列出新增代码
void Class_Dog::Dog_run()
{
cout << "I am "<< name.c_str() <<"I am Running now" << endl;
}
void Class_Dog::Dog_bark()
{
cout << "I am " << name.c_str() << "I am Barking now" << endl;
}
void Class_Dog::Dog_eat()
{
cout << "I am " << name.c_str() << "I am Eating now" << endl;
}
int main()
{
Class_Dog Dog_1("Mikey", 5);
Dog_1.Dog_bark();
Dog_1.Dog_run();
return 0;
}
好的,此时我们从原本的基础之上又添加了吠、跑、叫三个成员函数,并且他们都是公有的,可以保证外部接口的调用。说一下几个重点拿Dog_run()函数举例:
void Class_Dog::Dog_run()
{
cout << "I am "<< name.c_str() <<"I am Running now" << endl;
}
这整个函数的实现其实很简单,里面输出了一句话,但是注意name这个变量,这是类的内部成员变量,在初始化时候被初始化为Mikey,而这是一个string类型的数据,想要输出它的字符串,就需要调用string类型的方法c_str()。且我们注意到调用方法使用的是点(’ . ‘),对于类的对象来说,调用类内的成员函数或者成员变量,都是用点(’ . ')的方式。看一下main函数的调用:
int main()
{
Class_Dog Dog_1("Mikey", 5);
Dog_1.Dog_bark();
Dog_1.Dog_run();
return 0;
}
使用有参构造函数声明的对象Dog_1,使用Dog_1.Dog_bark()调用了吠这个函数,其实这样的调用和结构体中的调用时特别像的。我们来看一下最终的输出结果:
总结:以上的简单代码已经让我们对类有了初步的了解,类具有抽象性和封装性,我们把狗这个动物的各种属性和动作抽象在一起,进行public或者private关键字封装之后,这个类生成的对象就可以有接口访问类中的共有成员。类的多态性,说的通俗一点就是多种形态的性能,可以分为静态多态和动态多态,其实静态多态在这里也有体现就是有参构造函数和无参构造函数。至于类的继承性,将在接下来进行探讨,虚函数也是动态多态实现的基本前提。
二、类的访问控制
1. 类内关键字
为了将类内关键字,我们重新写一个简单的类,能说明情况就行,避免类过于庞大,因为主要是讲一下概念。
class Demo
{
int defalut = 7;//这是个特殊的变量
public:
Demo();
~Demo();
int a = 1;
int set_number(int a,int b,int c);
private:
int b = 2;
protected:
int c = 3;
};
Demo::Demo()
{
}
Demo::~Demo()
{
}
int Demo::set_number(int a, int b, int c)
{
this->a = a;
this->b = b;
this->c = c;
return 0;
}
1.1 public关键字
上述代码中public关键字下面包含构造函数和析构函数,还有一个变量a和一个函数,用来修改a,b,c的值。现在我们在main函数中执行操作:
int main()
{
Demo demo;
demo.a = 5;//操作成功,允许访问,可以把a的值从1修改成5
demo.set_number(10, 11, 12);//允许调用,成功修改成员变量的值
return 0;
}
代码单步执行结果,从修改a之前,到修改a,到修改a,b,c三张图
总结:public关键字的访问域是公共的,也就是说,在public中定义的成员函数或者成员变量可以在外部被访问,也就是提供给外部的接口。
1.2 private关键字
对于private,首先要说一下类中默认的访问属性,类中默认的访问属性是private类型,所以我们在类的一开头定义的defalut变量,是一个private变量,它和变量b的属性是一样的,都是private的。
回到main中测试,试着做相同的事情:
int main()
{
Demo demo;
demo.b = 5;//我们尝试直接修改b的值,发现报错
return 0;
}
报错截图显示,Demo::b这个变量不可访问,也就是说类的私有成员变量不供外部接口使用,不能直接修改。那么defalut变量属于私有变量,自然也不可以访问。那我们可以修改私有成员吗?答案是可以,通过类的成员函数。
int main()
{
Demo demo;
demo.set_number(10, 11, 12);
return 0;
}
调用这个函数,我们可以成功把b的值进行修改,因为函数是public,所以可以被调用,而函数又属于类内部,又可以访问b,所以可以修改!
总结:类的私有成员不允许外部访问,这样的设计是为了保证类的安全性,我们把敏感的东西封装在private关键字的控制之下,就不用担心外部接口对它造成不可控制的操作。就算需要操作,也只能通过我们提供的外部接口来操作,而这些接口函数是我们编写,所以可以保证安全性。
1.3 protected关键字
protected关键字特别的有趣,它的性质和private特别的相似,在当前类中,可以说和private完全一样,也不能被外部访问。
这时候就有人有疑问了,那么为什么还要有这个关键字呢?它的意义又是什么?它的意义就在于继承时候,当前类如果发生继承,在子类中的可见性是不同的。关于这一块的内容,在稍后的继承性时候会细细讲!!先有个概念和映像就行了。
2. 类的静态成员
有关类的静态成员,是面试中高频知识点之一。上一篇博文我在说指针时候提到了类中的this指针,说到this指针时候说,this可以控制类内非静态成员函数以外的成员。那我们现在就要讲类的静态成员和静态函数了。
2.1 类的静态成员变量
我们说先说一下静态成员变量的一些特性,static是声明静态成员变量的关键字,静态成员变量在所有类的对象中都共享,比如我们定义Demo_1、Demo_2两个类对象,这两个类对象中的index变量是存放在同一个内存的值。这和我们的普通变量有很大的区别,如果是普通变量,每个对象中都会有新的内存空间,各个对象之间互不干扰。 并且静态数据成员必须在类外定义和初始化,因为需要以这样的方式来进行静态变量的初始化。正是因为这样,静态数据成员的生存周期也不随类的销毁而销毁下面我们结合代码来看一下:
class Demo
{
public:
Demo();
~Demo();
void get_index();
private:
static int Index;//只声明,不用管初始化的事情
};
Demo::Demo()
{
Index++;//每次有对象生成,就让索引值++
}
Demo::~Demo()
{
}
void Demo::get_index()
{
cout << Index << endl;
}
//在类内已经进行过声明定了了,但是必须在此处继续定义以分配内存空间
int Demo::Index = 0;
以上代码中有静态成员变量在类中的定义,在类外的定义和初始化,这些东西都是固定的,比如我们必须在类外定义Index的时候加上Demo类名的作用域的限制。
我们来看一下main函数中的调用:
int main()
{
Demo demo1;
demo1.get_index();//输出1
Demo demo2;
demo1.get_index();//输出2
demo2.get_index();//输出2
return 0;
}
我们知道每次声明Demo变量,都会执行到默认构造函数中,而默认构造函数中对静态变量Index进行++,所以最终在使用demo1调用get_index也会输出2。证明了共享一个静态变量。
必须要再次说明的一点,静态成员变量,不属于类的任何一个实例。就是说demo1和demo2的内存空间中并不存在Index变量,Index变量存在了另外一个地址空间中,和这两个对象所在的内存空间没关系。
2.2 类的静态成员函数
既然我们提到类的静态数据成员不在任何对象的内存空间中,那么我们肯定可以不通过对象直接访问它,具体要怎么做呢?让静态成员函数来解决这个问题吧!!静态成员函数可以直接访问类中的静态数据成员,而访问非静态的数据成员,需要通过对象名限定。来看下面的代码示例:
class Demo
{
public:
Demo();
~Demo();
static void get_index();//静态成员函数
private:
static int Index;
int a = 3;
};
Demo::Demo()
{
Index++;
}
Demo::~Demo()
{
}
void Demo::get_index()
{
cout << Index << endl;
//cout << a << endl;会报错
}
int Demo::Index = 0;
int main()
{
Demo::get_index();
Demo demo1;
demo1.get_index();//输出1
return 0;
}
我们把get_index定义为一个静态成员函数,当我们在静态成员函数中尝试访问非静态数据成员时,编译器会报错:
并且静态成员函数中不包含this指针:
静态成员函数一般用来处理静态数据成员,所以我们在main函数中直接用类名作用域限制之后就可以调用函数,当然也可以通过对象调用
int main()
{
Demo::get_index();//输出0
Demo demo1;
demo1.get_index();//输出1
return 0;
}
总结:静态成员函数一般用来对静态数据成员来进行处理,this指针在静态成员函数中不存在
3. 指针访问和对象访问
对于一个对象的声明,可以有两种方法,一种是静态分配内存,一种是用new来为对象动态申请内存,返回指针。我们来简单的看一下:
类还是之前的Class_Dog类,我们使用new来进行内存申请,返回一个Class_Dog类型的指针,然后我们执行其中的成员函数,或者取出其中的成员变量使用‘->’来取。如果是正常的分配内存,那就像Dog_1直接用’ . '来控制就行。
int main()
{
Class_Dog* Dog_Pointer = new Class_Dog("Mikey", 5);
Class_Dog Dog_1("Haney", 3);
Dog_Pointer->Dog_bark();
Dog_1.Dog_eat();
return 0;
}
说明:以上代码只是为了说明new出一个对象和普通内存分配之后,指针和普通对象的访问方式不同,至于指针的使用,还是有漏洞,使用new申请内存,需要判断是否申请成功。
三、类的特性详解
关于类的特性,我们已经提到过了,抽象和封装就不过多解释了,接下来我们细细说一下继承和多态。接下来讲的内容也是面试的重要知识点
1. 类的继承
1.1 三种继承方式
先说下继承是什么,继承就是字面意思,被继承的类叫做父类,继承的类叫做子类或者派生类,和皇位继承一样,朕该给你的就是你的,但朕私有的就不给你,并且还要看子类是如何继承父类的。类的继承总共有3种继承方法,也就是之前我们说的访问控制:public、private、protected。
1.1.1 public继承
公有继承public就是说:除了朕的私有,那其他的就都给你了,想要啥拿啥,那当然也包括了protected里面的东西,简单来说,只要不是父类private控制下的变量和函数,都可以被子类拥有。 并且继承过来的东西,属性不发生改变
来看个例子:
class Dog
{
public:
Dog();
~Dog();
void set_private_b(int b);
int a = 0;
private:
int b = 0;
protected:
int c = 0;
};
Dog::Dog()
{
cout << "Dog Create" << endl;
}
Dog::~Dog()
{
cout << "Dog Destory" << endl;
}
void Dog::set_private_b(int b)
{
this->b = b;
}
//子类 共有继承
class Rottweiler_Dog : public Dog
{
public:
Rottweiler_Dog();
C++萌新来看,一篇文让你让你彻底搞定类(超详细)!