一、前言
如果你想要搞清楚类在编程语言中的作用,可以看一看另一篇文章:结构体、类、接口
本文重在介绍C++中类的各种使用方法。
二、基本使用
C++中声明一个类的关键字是class
,其基本使用如下:
class Test {
public: //权限
int a; //属性
void fun() { //方法
}
};
//使用:
Test t; //根据类定义一个对象
t.a=10; //给对象的属性a赋值
t.fun(); //调用对象中的fun函数
对于一些简单开发来说,只要有上面的这点知识点,其实就已经可以正常写代码了。
因为它满足了我们对类的基本要求:自定义组合属性与方法
虽然在C++中结构体与类已经基本相同了,但结构体最开始的作用仅仅是组合属性。
但为了实现更多强大的功能,C++的类还提供了非常多的特性,满足类的三大基本要求:封装、继承、多态
三、封装
首先是封装,它对应的特性其实就是类中的权限控制。
比如下面这一段代码:
class Stu {
public:
string id; //存放学生id,比如为六位数字,比如:100000
//其它属性方法省略
};
int main() {
Stu s;
s.id = "1000"; //但如果直接赋值,不放6位也一样可以
}
相信大家应该也遇到过,一些卡号、手机号、身份证号都是有一定格式的,如果将其像结构体那样,直接暴露出来,就会出现问题。
因为我们无法控制赋值的格式,而只能被动的写注释去要求别人这样做,这显然是一种非常不安全的行为。
为了达到这样的目的,我们一般就会将其设置为私有的,并只能通过特定的函数对其进行赋值,然后我们就能在这个赋值函数中达到控制格式的目的:
class Stu {
private:
string id; //存放学生id,比如为六位数字,比如:100000
public:
bool set_id(const string& _id) {
if (id.size()==6) { //省略字符判断
id = _id;
return true;
}
else {
return false;
}
}
};
此时我们就用到了private
关键字,只要放在这个关键字后面的属性、方法,外界都是无法访问的,也就是私有的意思。
但我们可以通过在public
关键字后面放开专门去设置这个属性的方法,比如这里的set_id
函数,其作用就是设置它的属性id
。
但不同的地方来了,通过方法来设置对应的属性,我们就可以在这个方法中来控制这个过程了!
int main() {
Stu s;
//s.id = "1000"; //此时不允许直接赋值,编译器直接报错
s.set_id("110011"); //只能通过公有函数间接赋值,达到控制的目的
}
如果传入不符合格式的id
,就会直接赋值失败,从而达到了主动控制的目的。
这样的用法非常常见:属性本身私有化,然后专门为这个属性写两个公有函数,一个用来赋值、另一个用来获取。
也正因为太过于常见了,所以常常还会有getter
、setter
函数的叫法,如下:
class Stu {
private:
string id; //存放学生id,比如为六位数字,比如:100000
public:
//setter函数
bool set_id(const string& _id) {
id = _id;//这里省略其它控制赋值的条件语句
}
//getter函数
string get_id() {
return id;
}
};
很明显,此时它变得更加繁琐了,而这其实就是为了安全、稳定而做出的让步。
但在C++中其实也还好,只要公司不明确规定代码书写规范,怎么写都行。
而以万物皆对象为准则的Java中,这种写法就是标准的,甚至IDE
还会有自动为每个属性生成对应getter
、setter
函数的功能。
在C++中,大部分时候我们用到的可能都是public
、private
这两个权限。
而实际上,它还有一个权限叫做:protected
,即保护。
它的权限位于公开与私有之间:
- 公开:任何地方都能访问
- 私有:只有本类的内部可以访问
- 保护:本类以及子类的内部可以访问。
从这里就可以看出来,它主要是用于类继承这一特性的,继承后面马上会提,这里给个简单的示例:
class Stu {
protected:
string id; //存放学生id
};
class CollStu :public Stu { //公有继承
public:
void test() {
id = "1000"; //因为id为父类的保护属性,所以子类也可以访问
}
};
int main() {
Stu s;
//s.id //id为保护属性,外部不可访问
}
因此一般保护属性是用于设计类的,如果对于别人实例化类对象来说,那么保护属性与私有属性没什么区别,因为你都不可以访问。
它相当于弱化版本的私有属性,允许让子类去访问、修改,如果是私有属性,那么即使是继承,也依旧无法进行访问。
四、构造与析构
对于类来说,它实例化对象后第一步一般都是需要为对象上的字段初始化值,但由于封装的特性,一般我们都是使用函数对字段进行初始化操作,也就是上面所说的setter方法。
但这样初始化实在是太繁琐了,如果一个类有十个字段,难不成要调用十次方法去初始化吗?
为了解决这个问题,类本身自带了构造函数,可以让我们给字段设置默认值:
#include<iostream>
class Stu {
private:
int id;
public:
Stu() {
id = 100;
}
int get_id() { return id; }
};
int main() {
Stu s;
printf("%d",s.get_id());
}
效果如下:
方式很简单,就是写一个与类名同名的函数,这个函数没有返回值,这就叫做类的构造函数。
它的特点就是,无需我们主动调用,只要我们实例化一个对象,这个函数就会自动被调用,如果我们不写这个构造函数,其也会默认生成一个构造函数,这不过此时构造函数中什么都不会做。
但这中构造函数只能用于默认初始化,很多时候我们希望能够直接初始化一些字段,此时就可以写有参构造函数了:
class Stu {
private:
int id;
public:
Stu() {
id = 100;
}
Stu(int _id) {
id = _id;
}
};
可以看到,构造函数是可以有多个的,和函数重载的原理一样,只要各个函数的参数不同即可。
此时我们就可以分别调用两种方式的构造函数来实例化对象了:
int main() {
Stu s1;
Stu s2(200);
}
通过控制传入的参数值来决定我们使用的是哪一个构造函数进行初始化。
但如果仅仅只有构造函数还是会有问题的,比如下面这种情况:
class Stu {
private:
char *buf;
public:
Stu() {
buf = new char[100];
}
};
此时字段是一个指针类型,如果我们在构造函数中对其进行了初始化操作,外面使用的人怎么直到在不使用这个对象的时候要来释放这个指针呢?
为了解决这种问题,就出现了析构函数,与构造函数相反,析构函数是在对象销毁的时候自动被调用:
class Stu {
private:
char *buf;