C++面向对象编程 汇总

目录

  • 类与对象
    • 类的定义
      • 类声明中的“占位操作”
      • 直接调用成员的方式
    • 初始化——构造函数
      • 无参构造函数
        • 用无参构造函数定义对象
      • 有参构造函数
        • 用有参构造函数定义对象
        • 默认构造函数
      • 拷贝构造函数
        • 用拷贝构造函数定义对象
        • 默认的拷贝构造函数
      • 析构函数
      • 整体回顾
    • 成员函数与数据成员
      • 内联函数/外联函数
      • 指向数据成员的指针
      • 指向成员函数的指针
    • 静态成员
      • 静态数据成员
        • 静态数据成员的应用
      • 静态成员函数
    • 常成员
      • 常数据成员
      • 常成员函数
      • 常对象
    • 友元函数、友元类
      • 友元函数
      • 友元类
    • 对象指针
        • 对象指针的赋值
        • 用对象指针作函数参数
        • 常对象指针or指向对象的常指针
        • this指针
    • 对象引用
    • 对象数组
        • 对象数组的初始化
        • 对象的指针数组
        • 指向对象数组的指针
    • 子对象
    • 堆对象
    • 类的作用域和对象的生存期
        • 局部类
        • 嵌套类
  • 继承与派生
        • 三种继承方式&三种分区
        • 继承特性的基本用法
    • 继承过程中的构造函数
        • 有参/无参构造函数
        • 析构函数
        • 子对象与继承
        • 构造函数调用顺序问题
        • 子类型
        • 多重继承中的构造函数调用
        • 多重继承的二义性
  • 多态性和虚函数
      • 函数重载
      • 运算符重载
        • 注意事项
        • 运算符重载-成员函数法
        • 运算符重载-友元函数法
        • 重载`<<`和`>>`
        • 静态联编
        • 动态联编
        • 虚函数
        • 纯虚函数
        • 抽象类

类与对象

类的定义

类一般定义在主函数外,包含privatepublic两个分区,每个分区内都可以有成员变量和成员函数;

class Date{ //类名
private: //私有分区,只有类体内能访问
    int y, m, d;
public: //公有分区,内外都能访问
    void Print(){
        cout<<y<<m<<d;
    }
}; //记得加分号!!
  • private分区内的东西出了类体就不能直接访问了,不可被继承
  • public分区内的东西出了类体还可以用.->直接访问,可以被继承

因此在实际应用中,往往通过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

常对象指针or指向对象的常指针

  • 类名 *const 指针名表示指向对象的常指针,特点是赋值后只能指向目前的变量,不可以让它指向别的变量就像你粉的cp锁死了,不能再跟别人组成cp
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指针

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.**初始化列表相关知识,可以类比“常成员”那一节的知识


堆对象

主要牵扯newdelete的相关知识点。
运算符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 派生类名: 继承方式 基类名 {},其中{}中写的是派生类自己的类体;继承方式有publicprivateprotected三种。如果不写继承方式,默认继承方式为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的子类型。例如,在公有继承下,派生类是基类的子类型。满足如下赋值规则:

  • son类的对象可以给dad类的对象赋值
  • 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;
    • ,
    • &
    • ()

注意事项

  1. 运算符重载时,运算符的“目数”不变,比如+需要两个数相加,重载后也必须是操作两个数,而不能是一个数或多个数;
  2. 运算符重载时,优先级不变,比如重载后+的优先级还是低于*的优先级;
  3. 最好不要改变运算符的含义,比如强行把+重载成-
  4. 不能创建新的运算符;
  5. 运算符的操作对象中,至少有一个是对象/对象引用;
  6. 重载运算符()``[]``->``=的时候,运算符重载函数必须是类的成员,而重载其他运算符的时候,运算符重载函数可以是类的友元函数;
  7. 不可重载intchar等已有类型的运算符;
  8. 运算符重载是独立的,“推一步走一步”,不会自动产生“附加重载”,比如重载了+,但+=不会被自动重载;

运算符重载-成员函数法

格式:类型名 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.动态联编的必要条件:

  1. 基类中声明虚函数
  2. 派生类公有继承
  3. 使用基类对象指针/对象引用调用虚函数

虚函数

格式:virtual 原函数

  1. 静态成员函数、类体外的普通函数、构造函数不能被定义为虚函数,但析构函数可以被定义为虚函数
  2. 基类的析构函数必须是虚函数
  3. 具有继承性,只要在基类中声明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()函数的参数。

你可能感兴趣的:(类与对象,大一C++教程)