类一般定义在主函数外,包含private
和public
两个分区,每个分区内都可以有成员变量和成员函数;
class Date{ //类名
private: //私有分区,只有类体内能访问
int y, m, d;
public: //公有分区,内外都能访问
void Print(){
cout<<y<<m<<d;
}
}; //记得加分号!!
.
或->
直接访问,可以被继承因此在实际应用中,往往通过public分区内的成员函数,来访问private分区内的私有变量(如上面的Print函数)
Date d;
d.Print();//用Print访问y,m,d
ps.类还有protected分区
,表示“受保护成员”,除了可以被继承外,与private
相同
class A; //这儿直接写分号了
...
这样声明称为前向声明
,只是告诉系统,有一个新的类A,但具体是什么不知道,就像预告片一样。前向声明只能用作定义该类的指针/引用/以该类型为返回值的函数,而不能定义对象;只有当类体的定义完整后,才可以用于定义对象
class A; //前向声明
class B
{
public:
A &a;//对象的引用
}; //类体B的定义完整了
class A{
public:
B b;//对象
}; //类体A的定义完整了
如上,对类体A前向声明后,由于它不完整,在类体B内不能用它定义对象;而在具体描述类体A的过程中,类体B已经完整定义,故可以用B定义对象。
除指针外,其余全用.
(英文句号)表示调用;指针用->
表示调用
A t,&b=t,*p=&t;
//A是个类,t是普通对象,b是对象引用,p是对象指针
cout<<t.m<<b.m<<p->m;
在C++11标准之前,编译器不允许成员变量在定义的时候直接被赋值:
private:
int score=60; //Compile Error
因此,若想将对象内的成员变量初始化,需要用到构造函数。
构造函数是一种特殊的成员函数,一般用于初始化对象,函数名与类名相同,无函数类型,无返回值,在系统创建对象时自动调用;构造函数可以有多个(重载),但每个构造函数的参数必须不同(个数、类型不完全相同)。构造函数一般定义在public
内
没有参数,不能被重载,即每个类最多有一个无参构造函数。
class Date
{
private:
int b;
public:
Date(){ //无参数的构造函数
b=666; //对b进行初始化
}
};
Date d;
这种所谓“正常的声明方式”其实就是运用无参构造函数的声明方式。此时对象d中b=666
可以被重载,即可以有很多个参数类型或个数不同的有参构造函数
class Date
{
private:
int b, c;
public:
Date(int B){ //有一个参数的构造函数
b=B; //用参数赋值
c=666; //也可以随意赋值
}
Date(int B, int C){ //有两个参数的构造函数
b=B;
c=C;
}
};
Date m(10); //第一个有参构造函数
Date n(1,2); //第二个有参构造函数
对象m定义的时候,括号里只有一个参数,则自动匹配第一个有参构造函数;对象n定义的时候括号里有两个参数,自动匹配到第二个有参构造函数。
对象m中b=10,c=666
,对象n中b=1,c=2
如果用户没有定义构造函数,系统会自动创建一个无参构造函数,称为默认构造函数;但如果用户自己定义了构造函数(无论是有参还是无参),系统将不再自动创建无参构造函数。也就是说,若只定义有参构造函数,由于系统不再自动生成无参构造函数,会导致缺乏无参构造函数,即不能再用下面这种形式定义对象
Date d;
只有当用户自己补充无参构造函数后,才可以这样定义。
拷贝构造函数用于以现有对象为模版创建新的对象,可以实现部分复制。拷贝构造函数的函数名同类名,无返回值,参数仅有一个,为对象的引用。
class Date
{
private:
int b,c;
public:
Date(){ //打酱油的无参构造函数
b=999;c=666;
}
Date(Date &p){ //拷贝构造函数
b=p.b+1; //用已有对象p给新对象赋值
c=66;
}
};
Date m,n(m);
m是用无参构造函数定义的对象,m中b=999,c=666
;n是用拷贝构造函数定义的对象,以m为模版,n中b=999+1=1000,c=66
用户未声明拷贝构造函数时,系统自动创建一个默认的拷贝构造函数,将模版对象的数据全部复制到新创建的对象中。
函数名同类名,并在函数名前加~
,无函数类型,无返回值,无参数,不可重载,在对象寿命结束的时候被系统自动调用,用于释放所创建的对象。
波浪线是英文波浪线!
class Date{
private:
int b,c;
public:
~Date(){ //析构函数
cout<<"666"; //随便写
}
};
当用户没有自己定义析构函数时,系统会创建默认的析构函数。
建议把这段代码自己照着敲几遍,然后运行,看看结果。
#include
using namespace std;
class Date{
private:
int year, month, day;
public:
Date(){ //无参构造函数
year=1413, month=5, day=20;
cout<<"无参构造被调用"<<endl;
}
Date(int Y, int M){ //有参构造函数1
year=Y, month=M, day=4;
cout<<"有参构造1被调用"<<endl;
}
Date(int Y, int M, int D){ //有参构造函数2
year=Y, month=M, day=D;
cout<<"有参构造2被调用"<<endl;
}
Date(Date& d){ //拷贝构造函数
year=d.year-99;
month=d.month;
day=d.day+1;
cout<<"拷贝构造函数被调用"<<endl;
}
~Date(){ //析构函数
cout<<"析构函数被调用"<<endl;
}
void print(){ //用于输出的函数
cout<<year<<" "<<month<<" "<<day<<endl;
}
};
int main()
{
Date d0;//用无参创建对象
Date d1(2000,11);//用有参1创建对象
Date d2(2001,6,26);//用有参2创建对象
cout<<"d0: ";d0.print();
cout<<"d1: ";d1.print();
cout<<"d2: ";d2.print();
Date dd(d0);//用拷贝创建对象(模版是d0)
cout<<"dd: ";dd.print();
return 0;
}
运行结果:
无参构造被调用
有参构造1被调用
有参构造2被调用
d0: 1413 5 20
d1: 2000 11 4
d2: 2001 6 26
拷贝构造函数被调用
dd: 1413 5 21
析构函数被调用
析构函数被调用
析构函数被调用
析构函数被调用
类的数据成员可以是任意类型的变量、任意类型的函数,一般情况下,成员变量被定义在private分区,成员函数被定义在public分区。
class Date{
private:
int y, m, d;
public:
int getY(){ //内联函数
return y;
} //类体内实现
int getM(); //外联函数,声明的时候没有{},以;结尾
}; //类体结束
int Date::getM() {
return m;
} //类体外实现
在类体外实现外联函数的时候,按照类型 类名::函数
的格式,其中函数除了;
其他的必须原封不动抄下来,函数名、参数、参数的名字一个都不能错
构造函数也可以在类体内声明,在类体外实现,不过因为构造函数没有函数类型,所以在实现的时候不写函数类型。
class Date{
private:
int y, m, d;
public:
Date();
};
Date::Date() {
y=2020,m=5,d=15;
}
外联函数要想“融入集体”,变成内联函数,只要在实现过程中,最前面加上inline
即可
class Date{
private:
int y, m, d;
public:
Date();
};
inline Date::Date() {
y=2020,m=5,d=15;
}
可以在类体外定义指针,使其指向类体内的某个public
数据成员,并可以通过该指针修改对应数据成员的值。指针的格式为
成员类型
类名
::*指针名
=&类名
::数据成员
;
class Date{
public:
int t;//公有数据成员t
};
//对照着上面的格式看
int Date::*p=&Date::t;//指向t的指针p
int main()
{
Date a;
a.t=2;//直接调用t进行赋值
cout<<a.t<<endl;
a.*p=3;//通过指针进行赋值
cout<<a.t<<endl;
return 0;
}
输出:
2
3
可以在类体外定义指针,使其指向类体内的某个public
成员函数,并可以通过该指针调用对应的成员函数。指针的格式为
函数类型
(类名
::*指针名
)(参数类型
)=&类名
::成员函数名
对于参数类型
,如果没有参数就不填,但括号还得留着;如果有多个参数,就只写参数类型,中间用逗号隔开,不写参数名;
class Date
{
public:
Date(){a=10;}//打酱油的构造函数
int getA(){//无参数的成员函数1
return a;
}
int plus(int b){//一个参数的成员函数2
return a+b;
}
int mult(int j, int k){//两个参数的成员函数3
return a*(j+k);
}
private:
int a;
};
//对照着上面的格式看
int (Date::*pg)()=&Date::getA;//指向函数1
int (Date::*pp)(int)=&Date::plus;//指向函数2
int (Date::*pm)(int, int)=&Date::mult;//指向函数3
int main()
{
Date d;
cout<<(d.*pg)()<<endl;//没有参数也不能省略括号
cout<<(d.*pp)(6)<<endl;
cout<<(d.*pm)(2,3);//将参数填在第二个括号里
return 0;
}
输出:
10
16(10+6=16)
50(10*(2+3)=50)
上面程序中的主函数部分其实就相当于:
int main()
{
Date d;
cout<<d.getA()<<endl;
cout<<d.plus(6)<<endl;
cout<<d.mult(2,3);
return 0;
}
输出结果一样,只不过前者是用花里胡哨的指针操作的,后者是直接调用函数操作的。
静态成员在定义的时候,需要先加上一个static
。虽然在类体内定义,但它不属于某个对象,甚至可以脱离对象单独存在,属于整个类体,(就是上交给国家的感觉)。分为静态数据成员和静态成员函数两种。
静态数据成员在类体内声明,但在类体外初始化,且必须初始化。初始化的格式为:静态变量类型
类名
::赋值语句
class Date
{
public:
static int a;//声明a
};
int Date::a=6;//初始化a
构造函数可以改变静态成员的值,但不能用于初始化,也就是说,类体外的初始化语句是必须要有的。如果有多个静态成员变量,需要挨个初始化,而不能用逗号链接。
class Date
{
public:
static int a,b;
Date(){a=5;}
};
int Date::a=0;//如果任何一个静态变量没初始化,程序会报错
int Date::b=0;//a和b要各自初始化,不可连在一起
//int Date::a=0,b=0;这样写是错的
int main()
{
Date t;
cout<<t.a;
return 0;
}
输出:
5
静态成员不属于任何对象,可以不依赖对象,用类名::变量
直接输出。
class Date
{
public:
static int a;
};
int Date::a=0;
int main()
{
cout<<Date::a<<endl;
return 0;
}
输出:
0
由于它不受对象限制,单独存在,可以用于记录对象个数:
class Date{
private:
static int num;
public:
Date(){
num++;
}//每当用无参构造函数定义一个对象,num就会+1
};
int Date::num=0;//初始化num=0
int main()
{
Date d1,d2,d3;//num=3
return 0;
}
静态成员函数只能访问静态成员变量/其他静态成员函数,可以在类体内实现(内联函数),也可以在类体内声明,在类体外实现(外联函数)。静态成员函数像静态数据成员一样没有对象用类名::函数名
直接调用
class Date
{
public:
static int a;
static int plus(){
return a*a;//内联的静态成员函数
}
};
int Date::a=4;
int main()
{
cout<<Date::plus()<<endl;//不依赖对象的调用
Date d;
cout<<d.plus();//也可以用任意一个对象调用
return 0;
}
输出结果:
16
16
常成员分为常数据成员和常成员函数。“常”代表“只读”,也就是说一旦确立,一般情况下不会被改变。
常数据成员是只读变量,只能读取,不能修改。对于每个对象,常数据成员都是不可变的,但不同对象的常数据成员可以不同。在对其进行初始化的时候,要用到构造函数的初始化列表
,而不能像其他变量一样,直接在构造函数内初始化。初始化列表在构造函数的()和{}之间,格式为:变量名(值)
class Date{
private:
const int c;
int b;
public:
Date():c(5){//将c赋值为5
b=2;//普通变量的初始化
}
Date(int x):c(x){//用参数将c赋值为x
b=x;
}
};
int main()
{
Date d1,d2(6);
return 0;
}
主函数中,用无参构造函数定义对象d1,d1中b=2,c=5;用有参构造函数定义d2,d2中b=c=6;定义完成后c永远是这个数,不能再更改。
若有多个常数据成员,在初始化的时候用,
隔开:
class Date{
private:
const int a,b,c;
public:
Date():a(5),b(2),c(1){}
Date(int x):a(x),b(2),c(x){}
};
可以看出,如果没有需要初始化的普通数据成员,也不能省略{}
注意:若一个变量同为“常数据成员”和“静态数据成员”,那么要按照静态数据成员的方式进行初始化:
class Date{
private:
static const int a;//static在前
};
const int Date::a=0;
对于常成员指针、常成员引用,“初始化列表”中的()
可以替代=
的所有作用:
class Date{
private:
const int t,&m,*p;
public:
Date():t(2),m(t),p(&t){}
//如果是普通数据成员,相当于m=t,p=&t
void print(){
cout<<m<<" "<<*p;
}
};
int main( )
{
Date d;
d.print();
return 0;
}
输出:
2 2
一般情况下,常成员函数不能修改变量,只能读取变量。常成员函数只在声明/实现的时候添加const
符号,而调用的时候不再需要添加const符号。const符号添加在()和{}之间。
class Date{
private:
const int y,m;
public:
Date():y(2020),m(2){}//初始化
void print() const;//声明(外联函数)
};
void Date::print() const {//实现
cout<<y<<" "<<m;
}
int main( )
{
Date d;
d.print();//调用时没有添加const符号
return 0;
}
输出:
2020 2
ps.常成员函数也可以修改一种特殊类型的变量:mutable
型(与const型相反)
常对象不能调用普通成员函数,只能调用常成员函数。不过普通数据成员和常数据成员都可以被调用。
常对象定义时,类名
和const
位置随意。
class Date{
private:
const int y;
public:
Date():y(2020){}
void print() const{
cout<<y;
}//常成员函数
};
int main( )
{
const Date d1;
//const在前/在后都行
Date const d2;
d2.print();//常对象只能调用常成员函数
return 0;
}
“友元”是独立的,与类之间不存在包含关系。通过“友元”的声明,可以访问类中的任何成员。
友元函数不是这个类中的成员函数,只是一个普通的小可爱:在类体外声明、在类体外实现,跟普通的函数完全一样,不过需要在类体内“登记”一下,表示这个函数有权限访问类体内的所有成员。登记的格式是:
friend 函数(参数);
class Date{
private:
int y,m,d;
public:
Date(){
y=1314,m=5,d=21;
}
//友元函数登记:
friend void Print(Date a);
friend void changeY(int Y,Date &a);
friend int getM(Date a);
//注意这里登记的函数必须和下面的函数一模一样
//即参数个数、类型、名字都必须完全相同
};
void Print(Date a){
cout<<a.y<<" "<<a.m<<" "<<a.d<<endl;
}
void changeY(int Y,Date &a){
a.y=Y;
}
int getM(Date a){
return a.m;
}
int main( )
{
Date d0;
Print(d0);//像普通函数一样调用
changeY(1413,d0);
Print(d0);
cout<<getM(d0);
return 0;
}
输出:
1314 5 21
1413 5 21
5
友元函数的参数一般包括相应类的对象,比如上述三个友元函数里的Date a
,不然没对象光有权限有啥用哈哈哈哈哈
把友元函数当成普通函数用就可以,像普通函数一样定义、一样描述、一样调用,只是多了一个“登记”的步骤。
一个类作为另一个类的友元,则这个类中所有成员函数都是另一个类的友元函数。“登记”格式:friend class 类名;
class Date{
private:
int y,m,d;
public:
Date(){//打酱油的构造函数
y=1314,m=5,d=21;
}
friend class Print;//友元类登记
};
class Print{
public:
//Print类的成员函数都是Date类的友元函数
void printY(Date p){
cout<<p.y;//有访问私有成员y的权限
}
};
int main( )
{
Date p;
Print p0;
p0.printY(p);
return 0;
}
输出:
1314
即指向对象的指针,定义的时候与正常指针一样。
int *p;//int型的指针
Date *d;//Date型的指针
//(Date是一个类)
对象指针包含类的所有成员,但调用成员的时候需要用符号->
,而非正常对象用到的.
。比如在如下类体中:
class Date{
private:
int y,m,d;
public:
void print(){
cout<<y<<m<<d;
}
int t;
};
若有指向对象的指针Date *p;
,则相应的变量调用方式为:
p->t;//调用的时候不带*
p->print();
有两种赋值方式,均可以选择在定义的时候直接赋值,或先定义,再赋值:
Date d;
Date *p1=&d;//定义的时候直接赋值
Date *p2;//先定义后赋值
p2=&d;//赋值的时候不带*
new
赋值。系统会创建一个对象,把这个新创建的对象的地址赋给指针。创建对象时,要用到构造函数,所以new
之后的部分要匹配构造函数的格式。class Date{
private:
int y,m;
public:
Date(){
y=2020,m=5;
}
Date(int Y, int M){
y=Y,m=M;
}
void print(){
cout<<y<<" "<<m<<endl;
}
};
int main()
{
Date *p1=new Date;//无参构造函数
Date *p2;
p2=new Date(2019,9);//有参构造函数
p1->print();
p2->print();
return 0;
}
输出:
2020 5
2019 9
用对象指针作为函数的参数,可以实现传址调用,通过指针改变它所指向的对象的值,而且运行效率也比较高。
class Date{
public:
int y,m;
Date(){y=2020,m=5;}
};
void change1(Date a){
a.y=1314;
a.m=9;
}
void change2(Date *p){ //对象指针作为参数
p->y=1314;
p->m=9;
}
int main()
{
Date a,*p=&a;
change1(a);//a没有被改变
cout<<a.m<<" "<<a.y<<endl;
change2(p);//通过p改变a
cout<<a.m<<" "<<a.y;
return 0;
}
输出:
2020 5
1314 9
类名 *const 指针名
表示指向对象的常指针,特点是赋值后只能指向目前的变量,不可以让它指向别的变量。Date a,b;
Date *const p=&a;//锁死!
//p=&b;不行!
ps.这里的“锁死”指的是匹配关系的锁死,但可以通过指针改变该对象的值,大概就像:
往后余生 风雪是你 平淡是你 清贫也是你 荣华是你 心底温柔是你 目光所至 也是你
无论是什么样的你,只要是你就可以
const 类名 *指针名
表示常对象指针(又称常指针),特点是不能通过该指针改变相应对象的值,只能读取,就像“只读”模式一样。Date a, b;
const Date *p=&a;
**ps.**常指针不可以改变它所指向对象的值,但可以“换对象”,指向其他对象。就像渣男一样
const Date *p=&a;
p=&b;//换对象
p=&c;//换对象
**pps.**常对象只能用常对象指针指向(好绕)
const Date d;
const Date *p1=&d;//可以
//Date *p2=&d;//不行!
ppps.常对象指针应该调用常成员函数,不能调用普通函数
class Date{
public:
int y,m;
void print()const { //常成员函数
cout<<666;
}
};
int main()
{
Date a;
Date *const p=&a;
p->print();//调用常成员函数
return 0;
}
this指针由每个对象自动创建,指向自己,在实际编程中往往被省略,但需要的话也可以写出来。
class Date{
private:
int y;
public:
void plus(){
this->y++;
//相当于y++;
cout<<this->y;
//相当于cout<
}
Date(){y=0;}
};
int main()
{
Date a;
a.plus();
return 0;
}
输出:
1
只有类的一般成员函数可以使用this指针,类的友元函数、静态成员函数、类体外的一般函数等,都不能用this指针。
与对象指针类似,但在实际应用中,使用对象引用作为函数的参数,要比使用对象指针更加普遍。
class Date{
private:
int y;
public:
void change(Date &d){ //对象引用作为函数的形参
y=d.y;
cout<<y;
}
Date(){y=0;}
Date(int Y){
y=Y;
}//两个打酱油的构造函数
};
int main()
{
Date a, b(5);
a.change(b);//注意这里的()内只填b,没有&
return 0;
}
输出:
5
实际应用中,也可以把上面的change函数按照如下方式修改:
void change(const Date &d){ //常对象引用
y=d.y;
cout<<y;
}
此时函数的参数叫做常对象引用,类比常对象指针可知,常对象引用没有权限改变引用对象本身的值,只能借来用一下
跟正常数组一样声明、一样使用,对象数组里的每个元素都是对象,都可以调用自己的成员函数、成员变量,调用的时候先写[]
,再写.
.
Date d[5];
d[0].print();//假装有print这个成员函数
可以在声明的时候直接赋值,也可以声明后挨个赋值。赋值的时候需要用到同类对象:
class Date{
private:
int y;
public:
Date(int Y){
y=Y;
}//有参构造函数
};
int main()
{
Date d[2]={Date(1),Date(2),Date(3)};
d[0]=Date(1);//按有参构造函数定义对象
return 0;
}
上面的程序中,第一种赋值方式里,{}
内的“无名对象”即为数组成员,分别对应d[0],d[1],d[2]。第二种赋值方式里,Date(1)
相当于“临时对象”,存在的意义就是给数组成员d[0]赋值,在赋值语句结束后,临时对象消亡,大概就像卸磨杀驴的感觉。
需要注意的是,在第一种赋值方式中,无名对象个数恰好等于数组元素个数,这是因为缺乏无参构造函数,系统必须按照有参构造函数的格式定义对象。如果补充无参构造函数,就不必要求无名对象个数等于数组元素个数,甚至可以不写{},只定义数组:
class Date{
private:
int y;
public:
Date(int Y){
y=Y;
}//有参构造函数
Date(){y=0;}//无参构造函数
};
int main()
{
Date d[3];
Date dd[2]={Date(5)};
return 0;
}
指针数组,即每个元素都是指针的数组,赋值方法结合上述的对象数组赋值,以及前面提到的“对象指针”赋值。
class Date{
private:
int y;
public:
Date(int Y){
y=Y;
}
Date(){y=0;}
};
int main()
{
Date a,b,c;
Date *p[3]={&a,&b,&c};//用同类对象的地址赋值
for(int i=0;i<3;i++)
p[i]=new Date;//用new赋值
return 0;
}
对两种赋值方法的描述,可以参考《对象指针》那一篇。
用指针指向对象数组,实际上达到了用指针名代替数组名的效果,可以依此记忆。
Date *p1=new Date[5];//用new赋值
Date d[2]={Date(2),Date(1)};
Date *p2=d;//用同类对象的数组赋值
赋值后,p1和p2可以当作两个一维数组的数组名使用:
p1[0].print();//假装有print这个成员函数
p2[0].print();
上面的p2[0].print();
其实就相当于d[0].print();
即用一个类的对象,充当另一个类的数据成员:
class A{...};
class B{
private:
A a; //用类A的对象a,充当类B的数据成员
}
需要注意的是,由于类体的成员变量必须初始化,所以子对象也需要初始化。子对象的初始化方式很像常成员初始化时用到的“初始化列表”
class A{
private:
int x;
public:
A(){x=6;}//无参构造函数
A(int X){x=X;}//有参构造函数
};
class B{
private:
A a;
int t;//拥有子对象的同时,也可以拥有其他成员变量
public:
B(int T):a(2) { //借助有参构造函数+初始化列表
t=T;
}
B(int T):a() { //借助无参构造函数+初始化列表
t=T;
}
//使用无参构造函数时,必须要有a(),不可以省略
};
来一版带输出的程序,运行一下看看:
class A{
private:
int x;
public:
A(int X){ //有参构造函数
x=X;
}
void print(){cout<<x;} //成员函数
};
class B{
private:
A a;
public:
B(int T):a(2){}//花括号不可以省略
//利用有参构造函数+初始化列表
void output(){
a.print();
}
};
int main(){
B b(2);//定义对象的时候,不必考虑初始化列表
b.output();
return 0;
}
**ps.**初始化列表相关知识,可以类比“常成员”那一节的知识
主要牵扯new
和delete
的相关知识点。
运算符new
主要用于创建新的对象
:
new
创建一个普通类型的变量,如需初始化,直接在类型名后面加(初始值)
int *p1=new int(10);
int *p2=new int;//不需初始化则不用加()
cout<<*p1<<" "<<*p2;
输出:
10 0
new
创建一个普通类型的数组,一般不会在创建的时候直接初始化int *arr=new int[10];
如上创建后,arr
可以直接当数组名用:
arr[0]=1;
cout<<arr[0];
输出:
1
运算符delete
主要用于释放对象
,其作用类似于析构函数
delete
释放一个普通类型的变量int *p=new int;
delete p;
delete
释放一个普通类型的数组int *p=new int[10];
delete[] p;
如果能弄懂普通类型,对于“堆对象”,只需要把类名看作“普通类型名”即可
class A{
private:
int x;
public:
A(){x=0;}
A(int X){x=X;}
};
int main(){
A *p1=new A;//利用无参构造函数定义堆对象
A *p2=new A(2);//利用有参构造函数定义堆对象
A *arr=new A[6];//利用无参构造函数定义堆对象数组
delete p1;//释放p1
delete p2;//释放p2
delete [] arr;//释放arr
return 0;
}
在一个函数体内定义的类称为“局部类”
在一个类中定义另外一个类,新定义的类称为嵌套类,原来的类称为外围类
class A{
public:
class B{
public:
int a;
};
B b;//直接使用
};
A::B zbb;//用外围类限定
public
成员,不可访问外围类的private
成员class A{
public:
int a;
class B{
public:
int b;
void printB(){ //无法访问A的成员a
cout<<b;
}
};
void printA(){ //无法访问B的成员b
cout<<a;
}
};
**继承:**新的类从已有的类处得到已有特性的过程
**派生:**已有的类把自己的特性共享,以此为基础产生新的类的过程
格式:class 派生类名: 继承方式 基类名 {}
,其中{}
中写的是派生类自己的类体;继承方式有public
、private
和protected
三种。如果不写继承方式,默认继承方式为private
。
**ps.**若有多个基类,之间用逗号隔开
class dad1{...};
class dad2{...};
class son: public dad1
{
...
};
class dau: public dad1, public dad2
{
...
};
public
分区的变量可以被继承;protected
分区的变量可以被继承;private
分区的变量不可被继承;ps.protected
也是一种类型,与public和private同级,可以被继承,但不可以从类体外访问
class dad{//基类dad
private:
int wife;//不可以被继承
public:
int fame;//可以被继承
protected:
long long money;//可以被继承
}
public
继承:完全照搬
public
被继承,成为派生类的public
;protected
被继承,成为派生类的protected
protected
继承:全都protected
public
被继承,成为派生类的protected
;protected
被继承,成为派生类的protected
private
继承:全都private
public
被继承,成为派生类的private
;protected
被继承,成为派生类的private
**ps.因为不考填选,这块的内容了解即可,实际操作中无脑用public
**能解决大部分问题
在派生类中,继承过来的东西直接当自己的用,不用再花里胡哨的搞什么格式,也不需要对象
class dad{
public:
void output(){ //基类的公有成员函数
cout<<"666";
}
int fame;
};
class son: public dad
{
public:
void Print(){
output();//直接访问基类的公有成员函数
fame++;//直接访问基类的公有成员
}
};
需要注意,继承操作不能继承基类的构造函数。然而,派生类中包含了基类中那些被继承来的数据成员,这些成员也是需要初始化的。因此,牵扯到在派生类中,对基类的成员变量进行初始化的问题时,往往需要在派生类的构造函数中,采用类似初始化列表的方式,进行初始化。
class dad{ //基类
private:
int fame,money;
public:
dad(){ //无参构造函数
fame=0,money=0;
}
dad(int F,int M){ //有参构造函数
fame=F,money=M;
}
void output(){ //基类的公有成员函数
cout<<fame<<" "<<money<<" ";
}
};
class son: public dad //公有继承
{
private:
int gf;
public:
son():dad() //基类无参构造函数+初始化列表
{
gf=0;
}
son(int n):dad(n,10) //基类有参构造函数+初始化列表
{
gf=n;
}
void print(){
output();//直接访问基类公有成员函数
cout<<gf<<endl;
}
};
int main()
{
son a;
son b(6);
a.print();
b.print();
return 0;
}
输出:
0 0 0
9 10 6
在基类构造函数无法被继承的条件下,初始化列表实现了同时初始化基类变量和派生类新增变量的效果。
析构函数属于构造函数,不能被继承。为了明确析构顺序,基类的析构函数前需要加virtual
class dad{
public:
virtual ~dad(){cout<<"666";}
};
class son: public dad
{
···
};
子对象相关知识,可以参考“子对象与堆对象”那一节
二者在初始化的时候都会用到初始化列表,但继承使用类名(参数)
,而子对象使用对象名(参数)
class dad{
private:
int d;
public:
dad(int D){ //基类的有参构造函数
d=D;
}
};
class son: public dad
{
private:
dad dd;//子对象dd
int s;
public:
son(int d1,int d2):dad(d1),dd(d2)
{
s=0;
}
};
构造函数:基类->子对象->派生类;
析构函数:派生类->子对象->基类;
当一个类son中包含了另一个类dad中的所有行为,则称类son为类dad的子类型。例如,在公有继承下,派生类是基类的子类型。满足如下赋值规则:
class son: public dad{···} //公有继承
son s;
dad d1=s;
dad &d2=s;
dad *d3=&s;
多重继承即一个派生类脱胎于多个基类。使用该派生类创建对象时,先执行基类的构造函数,再执行派生类自己的构造函数。基类构造函数的执行顺序取决于定义派生类时规定的顺序。基类数据成员的初始化仍要借助初始化列表,但初始化列表中的顺序不会影响基类构造函数的调用顺序。例如:
class dad1{···};
class dad2{···};
class dad3{···};
class son:public dad1, public dad2, public dad3 //在此规定调用顺序
{
son():dad2(),dad1(),dad3(){···}//派生类构造函数
};
在上面的例子中,使用son定义对象时,按照dad1->dad2->dad3->son
的顺序,调用构造函数。
ps.析构函数的调用顺序与构造函数相反
注意,只有存在多重继承时,才会有所谓的“二义性”。
类名::函数名
表示调用某个基类的相应函数class dad1{ //基类1
public:
void print(){
cout<<666;
}
};
class dad2{ //基类2
public:
void print(){
cout<<"999";
}
};
class son: public dad1, public dad2
{
public:
void output1(){
dad1::print(); //调用基类1的print
}
};
类名::函数名
加以限制class grandpa{ //“基类的共同基类”
public:
void print(){
cout<<"666";
}
};
class dad1: public grandpa {};
class dad2: public grandpa {};
class son: public dad1, public dad2
{
public:
void output1(){
dad1::print();//通过dad1调用
}
};
多态性:发出同样的消息,被不同类型的对象接收时,可能导致完全不同的行为
多态的实现:函数重载、运算符重载、虚函数
函数名相同,但函数类型、参数类型及个数不完全相同。本质上还是不同的函数,只不过方便记忆。
int add(int a, int b)
{
return a+b;
}
double add(double a, double b)
{
return a+b;
}
int add(int a, int b, int c)
{
return a+b+c;
}
多种构造函数均以类名为函数名(无参构造函数、有参构造函数、拷贝构造函数),但它们的参数不同,所以本质上也是函数重载。
由于类的对象无法使用+``-``*``/``>``<
这一类的运算符,故通过运算符重载,在类中赋予已有的运算符以新的意义,来解决这一问题
不能被重载的运算符:
.
:如a.print();
.*
:如a.*p;
::
:如Date::print();
?:
:如return (a>b)? a:b;
sizeof()
不建议被重载的运算符:
->
:如p->print();
->*
:如p->*pa;
,
&
()
+
需要两个数相加,重载后也必须是操作两个数,而不能是一个数或多个数;+
的优先级还是低于*
的优先级;+
重载成-
;()``[]``->``=
的时候,运算符重载函数必须是类的成员,而重载其他运算符的时候,运算符重载函数可以是类的友元函数;int
、char
等已有类型的运算符;+
,但+=
不会被自动重载;格式:类型名 operator 运算符 (参数){···}
。
一般“参数”最多有一个,常为常对象引用。如果没有参数,也不能省略()
例如,如果有一个复数类(包含实部和虚部),重载+
实现复数相加减
class Complex{
private:
double r,i;
public:
Complex(double R, double I){
r=R,i=I;
}
Complex operator + (const Complex &c)
{
return Complex(r+c.r, i+c.i);
//返回无名对象
}
};
格式:friend 类型名 operator 运算符 (参数){···}
这里的参数个数等于运算符的操作数个数,+
就是两个参数,++
则是一个参数。这里的参数也多为常对象引用
class Complex{
private:
double r,i;
public:
Complex(double R, double I){
r=R,i=I;
}
void print(){
cout<<r<<" "<<i;
}
friend Complex operator + (const Complex &c1, const Complex &c2);
};
Complex operator + (const Complex &c1, const Complex &c2){
return Complex(c1.r+c2.r, c1.i+c2.i);
}
通常情况下,双目运算符采用友元函数的方法,而单目运算符采用成员函数的方法。
<<
和>>
只能使用友元函数法,<<
对应ostream
类型,>>
对应istream
类型,一般有固定的格式可以套用
class inter{
private:
int a, b;
public:
friend istream& operator >> (istream &stream, inter &i);
friend ostream& operator << (ostream &stream, inter &i);
};
istream& operator >> (istream &stream, inter &i){
stream>>i.a>>i.b;
return stream;
}
ostream& operator << (ostream &stream, inter &i){
stream<<i.a<<" 2 "<<i.b<<endl;
return stream;
}
int main()
{
inter a;
cin>>a;
cout<<a;
return 0;
}
在编译时进行,又称早期联编。编译时规定的地址不可以被后期改变,执行最开始的地址所在类的相应函数。
class dad{
public:
double Area(){
return 0.0;
}
};
class son: public dad{
public:
double Area(){
return 1.0;
}
};
void output(dad &d){ //编译时确定,函数output的参数地址来自dad类
cout<<d.Area()<<endl;
//此处的Area()永远是dad类的函数
}
int main()
{
son s;
output(s);
//尽管用子类型给基类地址赋值,也不能改变事先确定的规则,即Area()来自dad类
dad d;
cout<<s.Area()<<endl;//各自调用自己的Area();
cout<<d.Area()<<endl;
return 0;
}
输出:
0
1
0
将基类和派生类中所有的同名函数定义为虚函数,调用时用对象指针/对象引用进行调用,可以实现调用相应对象的函数,无视其他限制。
class dad{
public:
virtual double Area(){ //虚函数
return 0.0;
}
};
class son: public dad{
public:
double Area(){
return 1.0;
}
};
void output(dad &d){
cout<<d.Area()<<endl;
}
int main()
{
son s;
dad d;
output(s);//调用son的Area();
output(d);//调用dad的Area();
return 0;
}
输出:
1
0
ps.动态联编的必要条件:
格式:virtual 原函数
virtual
,则它的派生类中所有相同函数自动成为虚函数纯虚函数是一种特殊的虚函数,是一种没有具体实现的虚函数,即没有函数体,而是用=0
结尾。
virtual double Area()=0;
往往在基类中定义纯虚函数,而派生类中必须要“实现纯虚函数”,即提供有函数体的纯虚函数
class dad{
public:
virtual void print()=0;//纯虚函数
};
class son: public dad{
public:
void print(){ //纯虚函数在派生类中实现
cout<<"666";
}
};
void output(dad &d){
d.print();
}
int main()
{
son s;
output(s);
return 0;
}
输出:
666
纯虚函数本身不能被调用,即基类中的纯虚函数被打入冷宫,但凡提到它,调用的只能是派生类中的相应函数。强制实现了多态性。
含有至少一个纯虚函数的类称为抽象类。抽象类不能用做定义对象,但可以定义对象指针/对象引用,如上面程序中output()
函数的参数。