本文作为笔记,目前在持续更新中。
类(class)和对象(object)
类是具有相同的属性和操作的一组对象的集合,它为属于该类的全部对象提供了统一的抽象描述,其内部包括属性(数据变量)和操作(成员函数)两个主要部分。
简而言之,类可以理解成是一种用户自己构造的数据类型,用这种数据类型声明的变量被称为 “对象”。
类和结构体的区别
那么类(class)和结构体(structure)有什么区别呢?
- 结构体(structure)是一个包含了一个或者多个变量的集合,而且这些变量还可能是不同类型。结构体将其聚集,放在一个名称下面,这样方便使用。在其他一些语言中,例如 Pascal 中,结构体被称为“records”。相比结构体,类(class)还包含了操作变量的函数。虽然结构体也可以包含一些操作和使用函数,但是一般不这么用。简而言之,类通常用于数据抽象和进一步继承;而结构体通常用于数据分组;
- 结构体内的变量和函数一般是 public 的,可以在结构体作用域之外的地方使用;但是类的变量和函数一般是 private 的,只能被类作用域内的函数使用;
- 对于它们的继承者,默认情况下,结构体的成员类或者结构体是 public 的;而类的成员类或者结构体是 private 的。
类的声明
一般形式为:
class 类名{
private:
私有变量和函数
public:
公有变量和函数
protected:
保护变量和函数
};
- private 是默认情况,表示完全私有,只有本类内可以访问。派生类和外部都不可以访问;
- public 是内外部都可以访问;
- protected 是受保护的,只有本类内和派生类可以访问,外部不可以访问。
需要注意的是:
- 不能在类的声明中对数据变量进行初始化;
- 在类中声明的任何成员不能使用 extern、auto 和 register 存储类型关键字修饰;
- 类声明中可以给出成员函数的参数的默认值;
- 类中可以不含有任何成员变量和成员函数,这样的类被称为空类。
此外,在定义类的成员函数的时候,如果不在类的内部定义(在内部给出定义,默认为内联函数),那么需要使用以下格式:
返回值类型 类名::成员函数名(形参列表)
{
函数体
}
::
是作用域运算符,用于表示其后的成员函数属于前面这个类。并且类中定义的成员函数允许重载。
此外,还可以使用inline
将成员函数定义为内联函数。
下面是一个描述平面直角坐标系的点的类,以及如何使用:
#include <iostream>
using namespace std;
class Point
{
public:
int x, y;
int getX();
};
int Point::getX()
{
return x;
}
int main()
{
Point a;
a.x=10;
a.y=20;
cout<<"x = "<<a.getX();
}
如何使用类声明一个对象,以及如何调用成员变量和函数
上一节的例子里出现了最常用的声明方式和使用方式(用于非指针声明的对象):
//声明
类名 对象名;
//调用
对象名.成员变量名/成员函数名(参数表)
但还有一种方式来访问通过指定声明的对象的成员变量和成员函数:
指向对象的指针->成员函数、变量名
举个例子:
#include <iostream>
using namespace std;
class Point
{
public:
int x;
int y;
};
int main()
{
Point *b;
Point c;
c.x=10;
c.y=20;
b=&c;
cout<<b->x;
}
常量对象
使用const
声明的对象就是常量对象。
和其他常量一样,常量对象必须在声明的同时进行初始化,并且不能修改。
需要注意的是,常量对象只能调用常量成员函数,不能调用非常量函数。
常量成员
常量成员变量
常量成员变量和其他常量声明方式一样,如下:
const 类型 变量名称;
初始化的时候需要注意,常量成员变量必须在构造函数的参数列表中初始化,包括常引用变量。 下面举个例子:
class costClass
{
const int conMbr;
int Mbr;
public:
//常量成员变量必须在构造函数的参数部分初始化,不能放到大括号里
constClass():conMbr(0),Mbr(100){}}
constClass(int i):conMbr(i)
{
Mbr=200;
}
};
常量成员函数
这里需要注意的是,在常量成员函数中,也不能去修改成员变量的值,不论成员变量是不是常量。
类的静态成员
类的静态成员由两种,静态成员变量和静态成员函数。声明和定义的时候(静态成员变量只在声明的时候)在前面加上static
即可。不过这里说的不严谨,请看下面:
静态成员变量
静态成员变量就是一个共享给所有该类的对象的静态变量,是类的成员,并不独属于某一个对象。
需要注意的是,静态成员变量不能再类体内赋值初始化,必须在体外赋值初始化。
正确的格式如下:
//初始化
类型 类名::静态成员变量=初始值;
//调用方式有两种
类名::静态成员变量
对象.静态成员变量
//指针调用方式
对象->静态成员变量
这里不能加上static
是因为在类体内声明变量的时候用过了,这里不用重复。
下面举个例子:
class myDate{
public:
static int a;
};
//调用方式有两种,如下
int myDate::a=10;
cout<<myDate::a;
静态成员函数
在 C++ 中,静态成员函数主要作用是为了访问静态成员。静态成员函数只能访问静态成员。
需要注意的是:
- 静态函数与静态函数可以互相调用;
- 非静态函数与非静态函数可以互相调用;
- 非静态函数内可以调用静态函数;
- 但是,静态函数内不能调用非静态函数。
与一般成员函数相比,区别如下:
- 可以不通过对象来访问;
- 静态成员函数是类的成员,而不是对象的,被存储在一个公用内存中;
- 没有 this 指针,只能通过对象名或指向对象指针访问;
- 静态成员函数不能被说明为虚函数;
- 静态成员函数不能直接访问非静态函数。
构造函数
构造函数是类中的特殊成员函数,作用是完成对象的初始化。并且没有任何返回值。
在定义类的时候,开发者应该编写一个构造函数。如果没有编写,那么会自动添加一个不带任何参数的构造函数。
构造函数的声明和定义
构造函数的声明方式如下:
类名(参数);
构造函数的定义方式如下:
类名:构造函数名(声明参数)
{
为类的成员变量赋值
}
或者
类名:构造函数名(声明参数):类的成员变量(赋值),类的成员变量(赋值)...{}
再次提醒,构造函数不能使用返回值。
下面举几个例子:
例子 1:
myDate::myDate()
{
year=1970, month=1, day=1;
}
又可以写作:
myDate::myDate():year(1970), month(1), day(1){}
例子 2:
myDate::myDate(int y, int m, int d)
{
year=y, month=m, day=d;
}
又可以写作:
myDate::myDate(int y, int m, int d):year(y), month(m), day(d){}
构造函数的使用
假设有一个类myDate
,如下:
class myDate
{
public:
myDate();
myDate(int);
myDate(int, int);
myDate(int, int, int);
int returnDate();
private:
int year, month, day;
};
//默认情况
myDate::myDate()
{
year=1970;
month=1;
day=1;
}
//默认设置月份和年
myDate::myDate(int d):year(1970),month(1)
{
day=d;
}
//默认设置年
myDate::myDate(int m, int d):year(1970)
{
month=m;
day=d;
}
//无默认设置
myDate::myDate(int y, int m, int d)
{
year=y;
month=m;
day=d;
}
//输出日期
int myDate::returnDate()
{
std::cout<<year<<"/"<<month<<"/"<<day<<std::endl;
return 0;
}
在其他函数声明对象的时候,构造函数的使用方式如下:
myDate a(参数值, 参数值);
举个例子:
int main() {
myDate a(12,13);
a.returnDate();
return 0;
}
输出为:
1970/12/13
使用构造函数需要注意的地方
- 构造函数没有返回值。
- 如果没有声明的时候初始化,构造函数的参数值按照声明顺序从左往右赋值。如果有值被初始化,就跳过它。如果都初始化了,还是按照从左到右幅值。
- 构造函数中使用自增自减运算的时候,
i++
这种情况和在一般的函数中不一样。
在一般的函数中,假设i=1;
,那么i=i++;
之后,i
的值应该是2
。但是如果在析构函数中,i
还是等于1
. - 创建构造函数的时候还需要注意一点,就是如果手动定义了构造函数的重载,那么声明对象的时候一定要加括号,也就是调用构造函数。
比如下面这两端段代码:
#include <iostream>
using namespace std;
class myDate{
int year, month, day;
public:
myDate();
myDate(int, int, int);
void getDate();
};
myDate::myDate()
{
year=1970;
month=1;
day=1;
}
void myDate::getDate()
{
std::cout<<year<<"/"<<month<<"/"<<day;
}
int main()
{
//声明对象的时候后面可以不加括号
myDate date;
date.getDate();
}
#include <iostream>
using namespace std;
class myDate{
int year, month, day;
public:
myDate();
myDate(int, int, int);
void getDate();
};
myDate::myDate()
{
year=1970;
month=1;
day=1;
}
myDate::myDate(int y=2022, int m=1, int d=1)
{
year=y;
month=m;
day=d;
}
void myDate::getDate()
{
std::cout<<year<<"/"<<month<<"/"<<day;
}
int main()
{
//哪怕不会输入参数值,也需要加括号,不然会报错,会造成混淆
myDate date();
date.getDate();
}
构造函数创建对象指针
创建完一个指针之后,需要使用new
加上数据类型来让系统动态分配空间。那么这个空间中的初始值就可以使用构造函数来赋值。
下面假设有一个myDate
类定义了一个构造函数,如下:
myDate::myDate(int y=2022, int m=1, int d=1)
{
year=y;
month=m;
day=d;
}
然后创建指向对象的指针:
myDate *a = new myDate;
myDate *a = new myDate();
myDate *b = new myDate(2022);
myDate *c = new myDate(2022,8);
myDate *d = new myDate(2022,8,18);
这里需要注意一点,声明一个指向对象的指针并不会调用构造函数,使用new
来分配空间的时候才会调用构造函数。因为这个时候才算定义生成了一个对象
最后三种很简单,就是按顺序赋值给对应参数。重点是前面两种有何区别,如果输出一下会发现是一样的,如下:
myDate *a=new myDate;
a->getDate();
myDate *b=new myDate();
b->getDate();
输出:
2022/1/1
2022/1/1
那么二者的区别是什么呢?
- 首先,第一中方法只能有一个构造函数,不然会混淆报错。第二种则可以重载。
- 在没有手动定义构造函数的情况下,第一种只是划出一片空间出来,并且把首地址赋值给指针
a
,不会去还原内容;而第二种则会将这片空间的内容全部初始化为0
。
不过一般都会加上括号,根据上文也可以知晓原因。
复制构造函数——用于复制的构造函数
复制构造函数的作用就是:使用一个已经存在的对象去初始化另一个正在创建的对象。
如果没有定义一个复制构造函数,那么会自动生成一个默认复制构造函数。
下面举个例子。首先,声明一个myDate类型的数据,然后再声明一个myDate
的数组,如下:
myDate a;
myDate b[2]={a, myDate(1970,2,14)};
b[0].getDate();
b[1].getDate();
输出结果如下:
2022/1/1
1970/2/14
对象a
很明显是通过构造函数来初始化的,对象b[0]
是通过复制构造函数来初始化的,对象b[1]
是通过构造函数来初始化的。
在类中,声明复制构造函数的方式如下两种:
类名(类名 &对象名);
类名(const 类名 &对象名);
定义的方式如下:
类名::类名(类名 &对象名){
函数体
}
下面举个例子:
myDate(const myDate &d);
myDate::myDate(const myDate &d)
{
year=d.year;
month=d.month;
day=d.day;
}
析构函数
析构函数的作用是为了在对象消失的时候,释放由构造函数分配的内存。
声明的方式就是在构造函数前面加上一个~
,如下:
~类名();
定义的方式如下:
类名::类名(){
函数体
}
需要注意的是,类只能定义一个析构函数,并且不能设置参数。
和构造函数一样,如果没有手动定义析构函数,那么会自动生成一个默认的析构函数,函数体为空。
提到分配和删除空间,会想到new
和delete
。如果使用new
动态分配了空间,那么需要在析构函数中使用delete
释放掉这部分空间。
析构对象按照 FILO 的原则,但是当使用delete
调用析构函数的时候,则按照delete
的顺序进行析构。
析构函数在对象生命周期结束的时候,自动被编译系统调用,然后这个对象的内存被回收。
成员对象
如果一个类的一个成员变量是另外一个类的对象,那么这个成员变量被称为“成员对象”。这两个类为包含关系,包含成员对象的类被称为封闭类。
因为这个包含关系,那么在定义一个封闭类的构造函数的时候,就需要初始化参数列表,需要指明调用成员对象的哪个构造函数。格式如下:
封闭类名::构造函数名(参数表):成员变量1(参数),成员变量2(参数), ...
{
}
例如下面表示使用myDate
的不带任何参数的myDate()
构造函数:
Student::Student(string n):name(n), birthday(myDate()){}
执行封闭类的构造函数时,先执行成员对象的构造函数,然后再执行封闭类类的构造函数。
析构的时候还是先构造的后析构,所以会先执行封闭类的析构函数,然后再执行成员对象的析构函数。
多态
封装、继承和多态是面对程序设计语言的三种机制,运用这三种机制可以有效提高程序的可读性、可扩充性和可重用性。而多态可以精简代码(可重用性)和提高程序的可扩充性。
多态就是不同对象可以调用相同名称的函数,但可导致完全不同的行为。所以多态的目的是接口复用。
多态分为静态多态(编译时多态)和动态多态(运行时多态):
- 静态多态:在编译阶段就能绑定调用语句与调用函数入口地址,即函数和运算符的重载,也被称为早期联编(early binding)或静态联编(static binding)。(默认为静态联编)
- 动态多态:函数调用与入口地址的绑定需要在运行时刻才能确定,也被称为动态联编(dynamic binding)、动态绑定或晚期联编(late binding)。需要注意的是,动态多态通过基类指针或基类引用来调用虚函数。
编译器将源代码中的函数调用解释为特定的函数代码块被称为函数名联编(binding)。在 C++ 中,由于函数可以重载,所以不像 C 语言中那么简单,编译器必须要检查函数参数以及函数名才能确定使用哪个函数。C/C++ 编译器可以在编译过程中进行编译,但是虚函数让这个工作变得更困难,编译器不能确定使用哪一个函数,因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行的时候选择正确虚方法的代码。这也就是动态联编。
使用途径有以下几种:
- 可以通过基类的指针或引用调用虚函数来实现多态。
- 通过普通成员函数中调用其他虚成员函数,这样虚成员函数是多态的。
- 在构造函数或析构函数中调用虚函数,但这样调用的虚函数并不是多态的(因为不会被继承)。
虚函数
虚函数(virtual function)是使用关键字virtual
声明的成员函数。关键字virtual
只用在声明的时候写,不用在定义的时候写(派生类中继承自基类的虚函数,关键字virtual
省不省略都可以)。
格式大致如下:
class A
{
public:
virtual int func();
};
int A::func()
{
//函数体
}
关于虚函数需要注意几点:
- 虚函数一般不声明为内联函数(不会报错,但是内联函数是静态联编,而虚函数必须动态联编,所以没有声明的意义)。
- 构造函数不能声明为内联函数(虚函数是可以在基类和所有派生类中使用,而构造函数并不会被继承;而且二者概念冲突,对象没生成之前无法表现对象的多态)。
- 静态成员函数不能被声明为虚函数。
- 友元函数不能声明为虚函数。
- 全局函数不能声明为虚函数。
- 派生类重写基类的虚函数实现多态,要求函数名、参数、以及返回值类型要完全相同。
- 不要在构造函数和虚构函数中调用虚函数。因为此时对象不是完整的。
- 最好将析构函数声明为虚函数。
虚析构函数
虚析构函数就是在析构函数前面加上关键字virtual
。只要基类的析构函数被声明为虚函数,派生类的析构函数都自动成为虚函数。
使用虚析构函数的目的是为了在对象消亡时实现多态。具体来说,设置了虚析构函数,这样可以保证能够释放所有的内存空间,避免造成内存泄露。
纯虚函数
纯虚函数就是声明在基类中的虚函数,由派生类根据需要给出各自的定义。纯虚函数只有函数名而没有代码,所以不能调用基类中这个函数。
纯虚函数的一般格式如下:
virtual 函数类型 函数名(参数)=0
没有大括号,必须要有=0
。
抽象类
一个类可以说明多个纯虚函数,包含了纯虚函数的类被称为抽象类。抽象类的派生类中,如果没有给出全部纯虚函数的定义,那么派生类继续是抽象类。
一个抽象类只能作为基类,不能创建抽象类的对象。但是可以指定抽象的指针和引用。这样指针和引用可以指向并访问派生类的成员,这种访问具有多态性。也就是可以访问不同类的函数名相同的函数。