【C++】面向对象之类和对象(下篇)-004

第四章 类和对象


4.6 运算符重载


4.6.1 运算符重载基本概念

运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

运算符重载(operator overloading)只是一种”语法上的方便”,也就是它只是另一种函数调用的方式。

在c++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。

语法:
定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算符。函数的参数中参数个数取决于两个因素。

  • 运算符是一元(一个参数)的还是二元(两个参数);
  • 运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数-此时该类的对象用作左耳参数)

[两个极端]
有些人很容易滥用运算符重载。它确实是一个有趣的工具。但是应该注意,它仅仅是一种语法上的方便而已,是另外一种函数调用的方式。从这个角度来看,只有在能使涉及类的代码更易写,尤其是更易读时(请记住,读代码的机会比我们写代码多多了)才有理由重载运算符。如果不是这样,就改用其他更易用,更易读的方式。
对于运算符重载,另外一个常见的反应是恐慌:突然之间,C运算符的含义变得不同寻常了,一切都变了,所有C代码的功能都要改变!并非如此,对于内置的数据类型的表示总的所有运算符是不可能改变的。

4.6.2 运算符重载碰上友元函数

友元函数是一个全局函数,和我们上例写的全局函数类似,只是友元函数可以访问某个类私有数据。

案例: 重载左移操作符(<<),使得cout可以输出对象。

class Person{
    friend ostream& operator<<(ostream& os, Person& person);
public:
    Person(int id,int age){
        mID = id;
        mAge = age;
    }
private:
    int mID;
    int mAge;
};

ostream& operator<<(ostream& os, Person& person){
    os << "ID:" << person.mID << " Age:" << person.mAge;
    return os;
}

int main(){

    Person person(1001, 30);
    //cout << person; //cout.operator+(person)
    cout << person << " | " << endl;

    return EXIT_SUCCESS;
}

4.6.3 可重载的运算符

几乎C中所有的运算符都可以重载,但运算符重载的使用时相当受限制的。特别是不能使用C中当前没有意义的运算符(例如用求幂)不能改变运算符优先级,不能改变运算符的参数个数**。这样的限制有意义,否则,所有这些行为产生的运算符只会混淆而不是澄清寓语意。

在这里插入图片描述

4.6.4 自增自减(++/--)运算符重载

重载的++和--运算符有点让人不知所措,因为我们总是希望能根据它们出现在所作用对象的前面还是后面来调用不同的函数。解决办法很简单,例如当编译器看到++a(前置++),它就调用operator++(a),当编译器看到a++(后置++),它就会去调用operator++(a,int).

class Complex{
    friend ostream& operator<<(ostream& os,Complex& complex){
        os << "A:" << complex.mA << " B:" << complex.mB << endl;
        return os;
    }
public:
    Complex(){
        mA = 0;
        mB = 0;
    }
    //重载前置++
    Complex& operator++(){
        mA++;
        mB++;
        return *this;
    }
    //重载后置++
    Complex operator++(int){    
        Complex temp;
        temp.mA = this->mA;
        temp.mB = this->mB;
        mA++;
        mB++;
        return temp;
    }
    //前置--
    Complex& operator--(){
        mA--;
        mB--;
        return *this;
    }
    //后置--
    Complex operator--(int){
        Complex temp;
        temp.mA = mA;
        temp.mB = mB;
        mA--;
        mB--;
        return temp;
    }
    void ShowComplex(){
        cout << "A:" << mA << " B:" << mB << endl;
    }
private:
    int mA;
    int mB;
};

void test(){
    Complex complex;
    complex++;
    cout << complex;
    ++complex;
    cout << complex;

    Complex ret = complex++;
    cout << ret;
    cout << complex;

    cout << "------" << endl;
    ret--;
    --ret;
    cout << "ret:" << ret;
    complex--;
    --complex;
    cout << "complex:" << complex;
}

优先使用++和--的标准形式,优先调用前置++。
如果定义了++c,也要定义c++,递增操作符比较麻烦,因为他们都有前缀和后缀形式,而两种语义略有不同。重载operator++operator--时应该模仿他们对应的内置操作符。
对于++和--而言,后置形式是先返回,然后对象++或者--,返回的是对象的原值。前置形式,对象先++或--,返回当前对象,返回的是新对象。其标准形式为:

在这里插入图片描述

调用代码时候,要优先使用前缀形式,除非确实需要后缀形式返回的原值,前缀和后缀形式语义上是等价的,输入工作量也相当,只是效率经常会略高一些,由于前缀形式少创建了一个临时对象。

4.6.5 指针运算符(*、->)重载

class Person{
public:
    Person(int param){
        this->mParam = param;
    }
    void PrintPerson(){
        cout << "Param:" << mParam << endl;
    }
private:
    int mParam;
};

class SmartPointer{
public:
    SmartPointer(Person* person){
        this->pPerson = person;
    }
    //重载指针的->、*操作符
    Person* operator->(){
        return pPerson;
    }
    Person& operator*(){
        return *pPerson;
    }
    ~SmartPointer(){
        if (pPerson != NULL){
            delete pPerson;
        }
    }
public:
    Person* pPerson;
};

void test01(){
    
    //Person* person = new Person(100);
    //如果忘记释放,那么就会造成内存泄漏

    SmartPointer pointer(new Person(100));
    pointer->PrintPerson();
}

4.6.6 赋值(=)运算符重载

赋值符常常初学者的混淆。这是毫无疑问的,因为’=’在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。

class Person{
    friend ostream& operator<<(ostream& os,const Person& person){
        os << "ID:" << person.mID << " Age:" << person.mAge << endl;
        return os;
    }
public:
    Person(int id,int age){
        this->mID = id;
        this->mAge = age;
    }
    //重载赋值运算符
    Person& operator=(const Person& person){
        this->mID = person.mID;
        this->mAge = person.mAge;
        return *this;
    }
private:
    int mID;
    int mAge;
};

//1. =号混淆的地方
void test01(){
    Person person1(10, 20);
    Person person2 = person1; //调用拷贝构造
    //如果一个对象还没有被创建,则必须初始化,也就是调用构造函数
    //上述例子由于person2还没有初始化,所以会调用构造函数
    //由于person2是从已有的person1来创建的,所以只有一个选择
    //就是调用拷贝构造函数
    person2 = person1; //调用operator=函数
    //由于person2已经创建,不需要再调用构造函数,这时候调用的是重载的赋值运算符
}
//2. 赋值重载案例
void test02(){
    Person person1(20, 20);
    Person person2(30, 30);
    cout << "person1:" << person1;
    cout << "person2:" << person2;
    person2 = person1;
    cout << "person2:" << person2;
}
//常见错误,当准备给两个相同对象赋值时,应该首先检查一下这个对象是否对自身赋值了
//对于本例来讲,无论如何执行这些赋值运算都是无害的,但如果对类的实现进行修改,那么将会出现差异;
//3. 类中指针
class Person2{
    friend ostream& operator<<(ostream& os, const Person2& person){
        os << "Name:" << person.pName << " ID:" << person.mID << " Age:" << person.mAge << endl;
        return os;
    }
public:
    Person2(char* name,int id, int age){
        this->pName = new char[strlen(name) + 1];
        strcpy(this->pName, name);
        this->mID = id;
        this->mAge = age;
    }
#if 1
    //重载赋值运算符
    Person2& operator=(const Person2& person){

        //注意:由于当前对象已经创建完毕,那么就有可能pName指向堆内存
        //这个时候如果直接赋值,会导致内存没有及时释放
        if (this->pName != NULL){
            delete[] this->pName;
        }

        this->pName = new char[strlen(person.pName) + 1];
        strcpy(this->pName,person.pName);
        this->mID = person.mID;
        this->mAge = person.mAge;
        return *this;
    }
#endif
    //析构函数
    ~Person2(){
        if (this->pName != NULL){
            delete[] this->pName;
        }
    }
private:
    char* pName;
    int mID;
    int mAge;
};

void test03(){
    Person2 person1("John",20, 20);
    Person2 person2("Edward",30, 30);
    cout << "person1:" << person1;
    cout << "person2:" << person2;
    person2 = person1;
    cout << "person2:" << person2;
}

如果没有重载赋值运算符,编译器会自动创建默认的赋值运算符重载函数。行为类似默认拷贝构造,进行简单值拷贝。

4.6.7 等于和不等于(==、!=)运算符重载

class Complex{
public:
    Complex(char* name,int id,int age){
        this->pName = new char[strlen(name) + 1];
        strcpy(this->pName, name);
        this->mID = id;
        this->mAge = age;
    }
    //重载==号操作符
    bool operator==(const Complex& complex){
        if (strcmp(this->pName,complex.pName) == 0 && 
            this->mID == complex.mID && 
            this->mAge == complex.mAge){
            return true;
        }
        return false;
    }
    //重载!=操作符
    bool operator!=(const Complex& complex){
        if (strcmp(this->pName, complex.pName) != 0 || 
            this->mID != complex.mID || 
            this->mAge != complex.mAge){
            return true;
        }
        return false;
    }
    ~Complex(){
        if (this->pName != NULL){
            delete[] this->pName;
        }
    }
private:
    char* pName;
    int mID;
    int mAge;
};
void test(){
    Complex complex1("aaa", 10, 20);
    Complex complex2("bbb", 10, 20);
    if (complex1 == complex2){ cout << "相等!" << endl; }
    if (complex1 != complex2){ cout << "不相等!" << endl; }
}

4.6.8 函数调用符号()重载

class Complex{
public:
    int Add(int x,int y){
        return x + y;
    }
    int operator()(int x,int y){
        return x + y;
    }
};
void test01(){
    Complex complex;
    cout << complex.Add(10,20) << endl;
    //对象当做函数来调用
    cout << complex(10, 20) << endl;
}

4.6.9 不要重载&&、||

不能重载operator&&operator|| 的原因是,无法在这两种情况下实现内置操作符的完整语义。说得更具体一些,内置版本版本特殊之处在于:内置版本的&&和||首先计算左边的表达式,如果这完全能够决定结果,就无需计算右边的表达式了--而且能够保证不需要。我们都已经习惯这种方便的特性了。

我们说操作符重载其实是另一种形式的函数调用而已,对于函数调用总是在函数执行之前对所有参数进行求值。

class Complex{
public:
    Complex(int flag){
        this->flag = flag;
    }
    Complex& operator+=(Complex& complex){
        this->flag = this->flag + complex.flag;
        return *this;
    }
    bool operator&&(Complex& complex){
        return this->flag && complex.flag;
    }
public:
    int flag;
};
int main(){

    Complex complex1(0);  //flag 0 
    Complex complex2(1);  //flag 1

    //原来情况,应该从左往右运算,左边为假,则退出运算,结果为假
    //这边却是,先运算(complex1+complex2),导致,complex1的flag变为complex1+complex2的值, complex1.a = 1
    // 1 && 1
    //complex1.operator&&(complex1.operator+=(complex2))
    if (complex1 && (complex1 += complex2)){   
        cout << "真!" << endl;
    }
    else{
        cout << "假!" << endl;
    }

    return EXIT_SUCCESS;
}

根据内置&&的执行顺序,我们发现这个案例中执行顺序并不是从左向右,而是先右猴左,这就是不满足我们习惯的特性了。由于complex1 += complex2先执行,导致complex1 本身发生了变化,初始值是0,现在经过+=运算变成1,1 && 1输出了真。

4.6.10 符号重载总结

  • =, [], () 和 -> 操作符只能通过成员函数进行重载
  • << 和 >>只能通过全局函数配合友元函数进行重载
  • 不要重载 && 和 || 操作符,因为无法实现短路规则

常规建议

在这里插入图片描述

4.6.10 强化训练_字符串类封装

MyString.h

#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include <iostream>
using namespace std;

class MyString
{
    friend ostream& operator<< (ostream  & out, MyString& str);
    friend istream& operator>>(istream& in, MyString& str);

public:
    MyString(const char *);
    MyString(const MyString&);
    ~MyString();

    char& operator[](int index); //[]重载

    //=号重载
    MyString& operator=(const char * str);
    MyString& operator=(const MyString& str); 

    //字符串拼接 重载+号
    MyString operator+(const char * str );
    MyString operator+(const MyString& str);

    //字符串比较
    bool operator== (const char * str);
    bool operator== (const MyString& str);
private:
    char * pString; //指向堆区空间
    int m_Size; //字符串长度 不算'\0'
};

MyString.cpp

#include "MyString.h"

//左移运算符
ostream& operator<< (ostream & out, MyString& str)
{
    out << str.pString;
    return out;
}
//右移运算符
istream& operator>>(istream& in, MyString& str)
{
    //先将原有的数据释放
    if (str.pString != NULL)
    {
        delete[] str.pString;
        str.pString = NULL;
    }
    char buf[1024]; //开辟临时的字符数组,保存用户输入内容
    in >> buf;

    str.pString = new char[strlen(buf) + 1];
    strcpy(str.pString, buf);
    str.m_Size = strlen(buf);

    return in;
}

//构造函数
MyString::MyString(const char * str)
{
    this->pString = new char[strlen(str) + 1];
    strcpy(this->pString, str);
    this->m_Size = strlen(str);
}

//拷贝构造
MyString::MyString(const MyString& str)
{
    this->pString = new char[strlen(str.pString) + 1];
    strcpy(this->pString, str.pString);
    this->m_Size = str.m_Size;
}
//析构函数
MyString::~MyString()
{
    if (this->pString!=NULL)
    {
        delete[]this->pString;
        this->pString = NULL;
    }
}

char& MyString::operator[](int index)
{
    return this->pString[index];
}

MyString& MyString::operator=(const char * str)
{
    if (this->pString != NULL){
        delete[] this->pString;
        this->pString = NULL;
    }
    this->pString = new char[strlen(str) + 1];
    strcpy(this->pString, str);
    this->m_Size = strlen(str);
    return *this;
}

MyString& MyString::operator=(const MyString& str)
{
    if (this->pString != NULL){
        delete[] this->pString;
        this->pString = NULL;
    }
    this->pString = new char[strlen(str.pString) + 1];
    strcpy(this->pString, str.pString);
    this->m_Size = str.m_Size;
    return *this;
}


MyString MyString::operator+(const char * str)
{
    int newsize = this->m_Size + strlen(str) + 1;
    char *temp = new char[newsize];
    memset(temp, 0, newsize);
    strcat(temp, this->pString);
    strcat(temp, str);

    MyString newstring(temp);
    delete[] temp;

    return newstring;
}

MyString MyString::operator+(const MyString& str)
{
    int newsize = this->m_Size + str.m_Size + 1;
    char *temp = new char[newsize];
    memset(temp, 0, newsize);
    strcat(temp, this->pString);
    strcat(temp, str.pString);

    MyString newstring(temp);
    delete[] temp;
    return newstring;
}

bool MyString::operator==(const char * str)
{
    if (strcmp(this->pString, str) == 0 && strlen(str) == this->m_Size){
        return true;
    }

    return false;
}

bool MyString::operator==(const MyString& str)
{
    if (strcmp(this->pString, str.pString) == 0 && str.m_Size == this->m_Size){
        return true;
    }

    return false;
}

TestMyString.cpp

void test01()
{
    MyString str("hello World");

    cout << str << endl;

    //cout << "请输入MyString类型字符串:" << endl;
    //cin >> str;

    //cout << "字符串为: " << str << endl;

    //测试[]
    cout << "MyString的第一个字符为:" << str[0] << endl;

    //测试 =
    MyString str2 = "^_^";
    MyString str3 = "";
    str3 = "aaaa";
    str3 = str2;
    cout << "str2 = " << str2 << endl;
    cout << "str3 = " << str3 << endl;

    //测试 +
    MyString str4 = "我爱";
    MyString str5 = "北京";
    MyString str6 = str4 + str5;
    MyString str7 = str6 + "天安门";

    cout << str7 << endl;

    //测试 == 
    if (str6 == str7)
    {
        cout << "s6 与 s7相等" << endl;
    }
    else
    {
        cout << "s6 与 s7不相等" << endl;
    }
}

4.6.11 附录:运算符和结合性

优先级 运算符 名称或含义 使用形式 结合方向 说明
1 [] 数组下标 数组名[常量表达式] 左到右 --
1 () 圆括号 (表达式)/函数名(形参表) 左到右 --
1 . 成员选择(对象) 对象.成员名 左到右 --
1 -> 成员选择(指针) 对象指针->成员名 左到右 --
2 - 负号运算符 -表达式 右到左 单目运算符
2 ~ 按位取反运算符 ~表达式 右到左 单目运算符
2 ++ 自增运算符 ++变量名/变量名++ 右到左 单目运算符
2 -- 自减运算符 --变量名/变量名-- 右到左 单目运算符
2 * 取值运算符 *指针变量 右到左 单目运算符
2 & 取地址运算符 &变量名 右到左 单目运算符
2 ! 逻辑非运算符 !表达式 右到左 单目运算符
2 (类型) 强制类型转换 (数据类型)表达式 右到左 --
2 sizeof 长度运算符 sizeof(表达式) 右到左 --
3 / 表达式/表达式 左到右 双目运算符
3 * 表达式*表达式 左到右 双目运算符
3 % 余数(取模) 整型表达式%整型表达式 左到右 双目运算符
4 + 表达式+表达式 左到右 双目运算符
4 - 表达式-表达式 左到右 双目运算符
5 << 左移 变量<<表达式 左到右 双目运算符
5 >> 右移 变量>>表达式 左到右 双目运算符
6 > 大于 表达式>表达式 左到右 双目运算符
6 >= 大于等于 表达式>=表达式 左到右 双目运算符
6 < 小于 表达式<表达式 左到右 双目运算符
6 <= 小于等于 表达式<=表达式 左到右 双目运算符
7 == 等于 表达式==表达式 左到右 双目运算符
7 != 不等于 表达式!= 表达式 左到右 双目运算符
8 & 按位与 表达式&表达式 左到右 双目运算符
9 ^ 按位异或 表达式^表达式 左到右 双目运算符
10 I 按位或 表达式I表达式 左到右 双目运算符
11 && 逻辑与 表达式&&表达式 左到右 双目运算符
12 II 逻辑或 表达式II表达式 左到右 双目运算符
13 ? : 条件运算符 表达式1?表达式2: 表达式3 右到左 三目运算符
14 = 赋值运算符 变量=表达式 右到左 --
14 /= 除后赋值 变量/=表达式 右到左 --
14 *= 乘后赋值 变量*=表达式 右到左 --
14 %= 取模后赋值 变量%=表达式 右到左 --
14 += 加后赋值 变量+=表达式 右到左 --
14 -= 减后赋值 变量-=表达式 右到左 --
14 <<= 左移后赋值 变量<<=表达式 右到左 --
14 >>= 右移后赋值 变量>>=表达式 右到左 --
14 &= 按位与后赋值 变量&=表达式 右到左 --
14 ^= 按位异或后赋值 变量^=表达式 右到左 --
14 I= 按位或后赋值 变量I=表达式 右到左 --
15 逗号运算符 表达式,表达式,… 左到右 --

4.7 继承和派生


4.7.1 继承概述

4.7.1.1 为什么需要继承

在这里插入图片描述

在这里插入图片描述
网页类
class IndexPage{
public:
   //网页头部
   void Header(){
       cout << "网页头部!" << endl;
   }
   //网页左侧菜单
   void LeftNavigation(){
       cout << "左侧导航菜单!" << endl;
   }
   //网页主体部分
   void MainBody(){
       cout << "首页网页主题内容!" << endl;
   }
   //网页底部
   void Footer(){
       cout << "网页底部!" << endl;
   }
private:
   string mTitle; //网页标题
};

#if 0
//如果不使用继承,那么定义新闻页类,需要重新写一遍已经有的代码
class NewsPage{
public:
   //网页头部
   void Header(){
       cout << "网页头部!" << endl;
   }
   //网页左侧菜单
   void LeftNavigation(){
       cout << "左侧导航菜单!" << endl;
   }
   //网页主体部分
   void MainBody(){
       cout << "新闻网页主体内容!" << endl;
   }
   //网页底部
   void Footer(){
       cout << "网页底部!" << endl;
   }
private:
   string mTitle; //网页标题
};

void test(){
   NewsPage* newspage = new NewsPage;
   newspage->Header();
   newspage->MainBody();
   newspage->LeftNavigation();
   newspage->Footer();
}
#else
//使用继承,可以复用已有的代码,新闻业除了主体部分不一样,其他都是一样的
class NewsPage : public IndexPage{
public:
   //网页主体部分
   void MainBody(){
       cout << "新闻网页主主体内容!" << endl;
   }
};
void test(){
   NewsPage* newspage = new NewsPage;
   newspage->Header();
   newspage->MainBody();
   newspage->LeftNavigation();
   newspage->Footer();
}
#endif
int main(){

   test();

   return EXIT_SUCCESS;
}

4.7.1.2 继承基本概念

c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。

一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。

派生类中的成员,包含两大部分:

  • 一类是从基类继承过来的,一类是自己增加的成员。
  • 从基类继承过过来的表现其共性,而新增的成员体现了其个性。


    在这里插入图片描述

4.7.1.3 派生类定义

派生类定义格式:

   Class 派生类名 :  继承方式 基类名{
         //派生类新增的数据成员和成员函数
   }

三种继承方式:

  • public : 公有继承
  • private : 私有继承
  • protected : 保护继承

从继承源上分:

  • 单继承:指每个派生类只直接继承了一个基类的特征
  • 多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征

4.7.2 派生类访问控制

派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。

派生类的访问权限规则如下:


在这里插入图片描述

在这里插入图片描述
//基类
class A{
public:
    int mA;
protected:
    int mB;
private:
    int mC;
};

//1. 公有(public)继承
class B : public A{
public:
    void PrintB(){
        cout << mA << endl; //可访问基类public属性
        cout << mB << endl; //可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};
class SubB : public B{
    void PrintSubB(){
        cout << mA << endl; //可访问基类public属性
        cout << mB << endl; //可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};
void test01(){

    B b;
    cout << b.mA << endl; //可访问基类public属性
    //cout << b.mB << endl; //不可访问基类protected属性
    //cout << b.mC << endl; //不可访问基类private属性
}

//2. 私有(private)继承
class C : private A{
public:
    void PrintC(){
        cout << mA << endl; //可访问基类public属性
        cout << mB << endl; //可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};
class SubC : public C{
    void PrintSubC(){
        //cout << mA << endl; //不可访问基类public属性
        //cout << mB << endl; //不可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};
void test02(){
    C c;
    //cout << c.mA << endl; //不可访问基类public属性
    //cout << c.mB << endl; //不可访问基类protected属性
    //cout << c.mC << endl; //不可访问基类private属性
}
//3. 保护(protected)继承
class D : protected A{
public:
    void PrintD(){
        cout << mA << endl; //可访问基类public属性
        cout << mB << endl; //可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};
class SubD : public D{
    void PrintD(){
        cout << mA << endl; //可访问基类public属性
        cout << mB << endl; //可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};
void test03(){
    D d;
    //cout << d.mA << endl; //不可访问基类public属性
    //cout << d.mB << endl; //不可访问基类protected属性
    //cout << d.mC << endl; //不可访问基类private属性
}

4.7.3 继承中的构造和析构

4.7.3.1 继承中的对象模型

在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成:

class Aclass{
public:
    int mA;
    int mB;
};
class Bclass : public Aclass{
public:
    int mC;
};
class Cclass : public Bclass{
public:
    int mD;
};
void test(){
    cout << "A size:" << sizeof(Aclass) << endl;
    cout << "B size:" << sizeof(Bclass) << endl;
    cout << "C size:" << sizeof(Cclass) << endl;
}

4.7.3.2 对象构造和析构的调用原则

  • 继承中的构造和析构
  • 子类对象在创建时会首先调用父类的构造函数
  • 父类构造函数执行完毕后,才会调用子类的构造函数
  • 当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
  • 析构函数调用顺序和构造函数相反
class A{
public:
    A(){
        cout << "A类构造函数!" << endl;
    }
    ~A(){
        cout << "A类析构函数!" << endl;
    }
};

class B : public A{
public:
    B(){
        cout << "B类构造函数!" << endl;
    }
    ~B(){
        cout << "B类析构函数!" << endl;
    }
};

class C : public B{
public:
    C(){
        cout << "C类构造函数!" << endl;
    }
    ~C(){
        cout << "C类析构函数!" << endl;
    }
};

void test(){
    C c;
}
  • 继承与组合混搭的构造和析构


    在这里插入图片描述
class D{
public:
    D(){
        cout << "D类构造函数!" << endl;
    }
    ~D(){
        cout << "D类析构函数!" << endl;
    }
};
class A{
public:
    A(){
        cout << "A类构造函数!" << endl;
    }
    ~A(){
        cout << "A类析构函数!" << endl;
    }
};
class B : public A{
public:
    B(){
        cout << "B类构造函数!" << endl;
    }
    ~B(){
        cout << "B类析构函数!" << endl;
    }
};
class C : public B{
public:
    C(){
        cout << "C类构造函数!" << endl;
    }
    ~C(){
        cout << "C类析构函数!" << endl;
    }
public:
    D c;
};
void test(){
    C c;
}

4.7.3 继承中同名成员的处理方法

  • 当子类成员和父类成员同名时,子类依然从父类继承同名成员
  • 如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)
  • 在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)
class Base{
public:
  Base():mParam(0){}
  void Print(){ cout << mParam << endl; }
public:
  int mParam;
};

class Derived : public Base{
public:
  Derived():mParam(10){}
  void Print(){
      //在派生类中使用和基类的同名成员,显示使用类名限定符
      cout << Base::mParam << endl;
      cout << mParam << endl;
  }
  //返回基类重名成员
  int& getBaseParam(){ return  Base::mParam; }
public:
  int mParam;
};

int main(){

  Derived derived;
  //派生类和基类成员属性重名,子类访问成员默认是子类成员
  cout << derived.mParam << endl; //10
  derived.Print();
  //类外如何获得基类重名成员属性
  derived.getBaseParam() = 100;
  cout << "Base:mParam:" << derived.getBaseParam() << endl;

  return EXIT_SUCCESS;
}

注意: 如果重新定义了基类中的重载函数,将会发生什么?

class Base{
public:
    void func1(){
        cout << "Base::void func1()" << endl;
    };
    void func1(int param){
        cout << "Base::void func1(int param)" << endl;
    }
    void myfunc(){
        cout << "Base::void myfunc()" << endl;
    }
};
class Derived1 : public Base{
public:
    void myfunc(){
        cout << "Derived1::void myfunc()" << endl;
    }
};
class Derived2 : public Base{
public:
    //改变成员函数的参数列表
    void func1(int param1, int param2){
        cout << "Derived2::void func1(int param1,int param2)" << endl;
    };
};
class Derived3 : public Base{
public:
    //改变成员函数的返回值
    int func1(int param){
        cout << "Derived3::int func1(int param)" << endl;
        return 0;
    }
};
int main(){

    Derived1 derived1;
    derived1.func1();
    derived1.func1(20);
    derived1.myfunc();
    cout << "-------------" << endl;
    Derived2 derived2;
    //derived2.func1();  //func1被隐藏
    //derived2.func1(20); //func2被隐藏
    derived2.func1(10,20); //重载func1之后,基类的函数被隐藏
    derived2.myfunc();
    cout << "-------------" << endl;
    Derived3 derived3;
    //derived3.func1();  没有重新定义的重载版本被隐藏
    derived3.func1(20);
    derived3.myfunc();

    return EXIT_SUCCESS;
}
  • Derive1 重定义了Base类的myfunc函数,derive1可访问func1及其重载版本的函数。
  • Derive2通过改变函数参数列表的方式重新定义了基类的func1函数,则从基类中继承来的其他重载版本被隐藏,不可访问
  • Derive3通过改变函数返回类型的方式重新定义了基类的func1函数,则从基类继承来的没有重新定义的重载版本的函数将被隐藏。

Tips:
任何时候重新定义基类中的一个重载函数,在新类中所有的其他版本将被自动隐藏.

4.7.4 非自动继承的函数

不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。

另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。

在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。

4.7.5 继承中的静态成员特性

静态成员函数和非静态成员函数的共同点:

  1. 他们都可以被继承到派生类中。
  2. 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。
  3. 如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。

静态成员函数不能是虚函数(virtual function).

class Base{
public:
    static int getNum(){ return sNum; }
    static int getNum(int param){
        return sNum + param;
    }
public:
    static int sNum;
};
int Base::sNum = 10;

class Derived : public Base{
public:
    static int sNum; //基类静态成员属性将被隐藏
#if 0
    //重定义一个函数,基类中重载的函数被隐藏
    static int getNum(int param1, int param2){
        return sNum + param1 + param2;
    }
#else
    //改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数
    static void getNum(int param1, int param2){
        cout <<  sNum + param1 + param2 << endl;
    }
#endif
};
int Derived::sNum = 20;

4.7.6 多继承

4.7.6.1 多继承概念

我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。


在这里插入图片描述
class Base1{
public:
    void func1(){ cout << "Base1::func1" << endl; }
};
class Base2{
public:
    void func1(){ cout << "Base2::func1" << endl; }
    void func2(){ cout << "Base2::func2" << endl; }
};
//派生类继承Base1、Base2
class Derived : public Base1, public Base2{};
int main(){

    Derived derived;
    //func1是从Base1继承来的还是从Base2继承来的?
    //derived.func1(); 
    derived.func2();

    //解决歧义:显示指定调用那个基类的func1
    derived.Base1::func1(); 
    derived.Base2::func1();

    return EXIT_SUCCESS;
}

多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?

解决方法就是显示指定调用那个基类的版本。

4.7.6.2 菱形继承和虚继承

两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。


在这里插入图片描述

这种继承所带来的问题:

  1. 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
  2. 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
class BigBase{
public:
    BigBase(){ mParam = 0; }
    void func(){ cout << "BigBase::func" << endl; }
public:
    int mParam;
};

class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};

int main(){

    Derived derived;
    //1. 对“func”的访问不明确
    //derived.func();
    //cout << derived.mParam << endl;
    cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;
    cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;

    //2. 重复继承
    cout << "Derived size:" << sizeof(Derived) << endl; //8

    return EXIT_SUCCESS;
}

上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?

对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚基类。那么我们采用虚基类方式将代码修改如下:

class BigBase{
public:
    BigBase(){ mParam = 0; }
    void func(){ cout << "BigBase::func" << endl; }
public:
    int mParam;
};

class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
class Derived : public Base1, public Base2{};

int main(){

    Derived derived;
    //二义性问题解决
    derived.func();
    cout << derived.mParam << endl;
    //输出结果:12
    cout << "Derived size:" << sizeof(Derived) << endl;

    return EXIT_SUCCESS;
}

以上程序Base1Base2采用虚继承方式继承BigBase,那么BigBase被称为虚基类。
通过虚继承解决了菱形继承所带来的二义性问题。

但是虚基类是如何解决二义性的呢?并且derived大小为12字节,这是怎么回事?

4.7.6.3 虚继承实现原理

class BigBase{
public:
    BigBase(){ mParam = 0; }
    void func(){ cout << "BigBase::func" << endl; }
public: int mParam;
};
#if 0 //虚继承
class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
#else //普通继承
class Base1 :  public BigBase{};
class Base2 :  public BigBase{};
#endif
class Derived : public Base1, public Base2{};
普通继承 虚继承
BigBase:
在这里插入图片描述
在这里插入图片描述
Base1:
在这里插入图片描述
在这里插入图片描述
Base2:
在这里插入图片描述
在这里插入图片描述
Derived:
在这里插入图片描述
在这里插入图片描述

通过内存图,我们发现普通继承和虚继承的对象内存图是不一样的。我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚。

  • BigBase 菱形最顶层的类,内存布局图没有发生改变。
  • Base1Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。
  • Derived派生于Base1Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。

由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。现在模型就变成了Base1Base2 Derived三个类对象共享了一份BigBase数据。

当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的)。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。

class BigBase{
public:
    BigBase(int x){mParam = x;}
    void func(){cout << "BigBase::func" << endl;}
public:
    int mParam;
};
class Base1 : virtual public BigBase{
public:
    Base1() :BigBase(10){} //不调用BigBase构造
};
class Base2 : virtual public BigBase{
public:
    Base2() :BigBase(10){} //不调用BigBase构造
};

class Derived : public Base1, public Base2{
public:
    Derived() :BigBase(10){} //调用BigBase构造
};
//每一次继承子类中都必须书写初始化语句
int main(){
    Derived derived;
    return EXIT_SUCCESS;
}

注意:
虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的.

工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。

4.8 多态


4.8.1 多态基本概念

多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。

多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。

c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。

静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。

//计算器
class Caculator{
public:
    void setA(int a){
        this->mA = a;
    }
    void setB(int b){
        this->mB = b;
    }
    void setOperator(string oper){
        this->mOperator = oper;
    }
    int getResult(){
        
        if (this->mOperator == "+"){
            return mA + mB;
        }
        else if (this->mOperator == "-"){
            return mA - mB;
        }
        else if (this->mOperator == "*"){
            return mA * mB;
        }
        else if (this->mOperator == "/"){
            return mA / mB;
        }
    }
private:
    int mA;
    int mB;
    string mOperator;
};

//这种程序不利于扩展,维护困难,如果修改功能或者扩展功能需要在源代码基础上修改
//面向对象程序设计一个基本原则:开闭原则(对修改关闭,对扩展开放)

//抽象基类
class AbstractCaculator{
public:
    void setA(int a){
        this->mA = a;
    }
    virtual void setB(int b){
        this->mB = b;
    }
    virtual int getResult() = 0;
protected:
    int mA;
    int mB;
    string mOperator;
};

//加法计算器
class PlusCaculator : public AbstractCaculator{
public:
    virtual int getResult(){
        return mA + mB;
    }
};

//减法计算器
class MinusCaculator : public AbstractCaculator{
public:
    virtual int getResult(){
        return mA - mB;
    }
};

//乘法计算器
class MultipliesCaculator : public AbstractCaculator{
public:
    virtual int getResult(){
        return mA * mB;
    }
};

void DoBussiness(AbstractCaculator* caculator){
    int a = 10;
    int b = 20;
    caculator->setA(a);
    caculator->setB(b);
    cout << "计算结果:" << caculator->getResult() << endl;
    delete caculator;
}

4.8.2 向上类型转换及问题

4.8.2.1 问题抛出

对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。

也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。

class Animal{
public:
    void speak(){
        cout << "动物在唱歌..." << endl;
    }
};

class Dog : public Animal{
public:
    void speak(){
        cout << "小狗在唱歌..." << endl;
    }
};

void DoBussiness(Animal& animal){
    animal.speak();
}

void test(){
    Dog dog;
    DoBussiness(dog);
}
运行结果: 动物在唱歌
问题抛出: 我们给DoBussiness传入的对象是dog,而不是animal对象,输出的结果应该是Dog::speak。

4.8.2.1 问题解决思路

解决这个问题,我们需要了解下绑定(捆绑,binding)概念。

把函数体与函数调用相联系称为绑定(捆绑,binding)

当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding).C语言中只有一种函数调用方式,就是早绑定。

上面的问题就是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,编译器确定了应该调用的speakAnimal::speak的,而不是真正传入的对象Dog::speak

解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,late binding),意味着绑定要根据对象的实际类型,发生在运行。

C++语言要实现这种动态绑定,必须有某种机制来确定运行时对象的类型并调用合适的成员函数。对于一种编译语言,编译器并不知道实际的对象类型(编译器并不知道Animal类型的指针或引用指向的实际的对象类型)。

4.8.2.1 问题解决方案(虚函数,vitual function)

C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写。

对于特定的函数进行动态绑定,c++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用.

  • 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要.
  • 如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual的.
  • 在派生类中virtual函数的重定义称为重写(override).
  • Virtual关键字只能修饰成员函数.
  • 构造函数不能为虚函数

注意: 仅需要在基类中声明一个函数为virtual.调用所有匹配基类声明行为的派生类函数都将使用虚机制。虽然可以在派生类声明前使用关键字virtual(这也是无害的),但这个样会使得程序显得冗余和杂乱。(我建议写上)

class Animal{
public:
    virtual void speak(){
        cout << "动物在唱歌..." << endl;
    }
};
class Dog : public Animal{
public:
    virtual void speak(){
        cout << "小狗在唱歌..." << endl;
    }
};
void DoBussiness(Animal& animal){
    animal.speak();
}
void test(){
    Dog dog;
    DoBussiness(dog);
}

4.8.3 C++如何实现动态绑定

动态绑定什么时候发生?所有的工作都是由编译器在幕后完成。当我们告诉通过创建一个virtual函数来告诉编译器要进行动态绑定,那么编译器就会根据动态绑定机制来实现我们的要求, 不会再执行早绑定。

问题:C++的动态捆绑机制是怎么样的?

首先,我们看看编译器如何处理虚函数。当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr),这个指针是指向对象的虚函数表。在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。

验证对象中的虚指针:

class A{
public:
    virtual void func1(){}
    virtual void func2(){}
};

//B类为空,那么大小应该是1字节,实际情况是这样吗?
class B : public A{};

void test(){
    cout << "A size:" << sizeof(A) << endl;
    cout << "B size:" << sizeof(B) << endl;
}

在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?在对象构建的时候,也就是在对象初始化调用构造函数的时候。编译器首先默认会在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象的虚函数表。

起初,子类继承基类,子类继承了基类的vptr指针,这个vptr指针是指向基类虚函数表,当子类调用构造函数,使得子类的vptr指针指向了子类的虚函数表。

  • 当子类无重写基类虚函数时:


    在这里插入图片描述

    在这里插入图片描述

过程分析:

 Animal* animal = new Dog;
 animal->fun1();

当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,此时由于子类并没有重写也就是覆盖基类的func1函数,所以调用func1时,仍然调用的是基类的func1.

执行结果: 我是基类的func1

测试结论: 无重写基类的虚函数,无意义

  • 当子类重写基类虚函数时:


    在这里插入图片描述

    在这里插入图片描述

过程分析:

 Animal* animal = new Dog;
 animal->fun1();

当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,由于子类重写基类的func1函数,所以调用func1时,调用的是子类的func1.

执行结果: 我是子类的func1

测试结论: 无重写基类的虚函数,无意义

多态的成立条件:

  • 有继承
  • 子类重写父类虚函数函数
    a) 返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外)
    b) 子类中virtual关键字可写可不写,建议写
  • 类型兼容,父类指针,父类引用 指向 子类对象

4.8.4 抽象基类和纯虚函数(pure virtual function)

在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。

做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class).

  • 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。
  • 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
  • Virtual void fun() = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。

建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要事先(或者不需要完全实现)。可以创建一个公共类.

案例: 模板方法模式

在这里插入图片描述
//抽象制作饮品
class AbstractDrinking{
public:
    //烧水
    virtual void Boil() = 0;
    //冲泡
    virtual void Brew() = 0;
    //倒入杯中
    virtual void PourInCup() = 0;
    //加入辅料
    virtual void PutSomething() = 0;
    //规定流程
    void MakeDrink(){
        Boil();
        Brew();
        PourInCup();
        PutSomething();
    }
};

//制作咖啡
class Coffee : public AbstractDrinking{
public:
    //烧水
    virtual void Boil(){
        cout << "煮农夫山泉!" << endl;
    }
    //冲泡
    virtual void Brew(){
        cout << "冲泡咖啡!" << endl;
    }
    //倒入杯中
    virtual void PourInCup(){
        cout << "将咖啡倒入杯中!" << endl;
    }
    //加入辅料
    virtual void PutSomething(){
        cout << "加入牛奶!" << endl;
    }
};

//制作茶水
class Tea : public AbstractDrinking{
public:
    //烧水
    virtual void Boil(){
        cout << "煮自来水!" << endl;
    }
    //冲泡
    virtual void Brew(){
        cout << "冲泡茶叶!" << endl;
    }
    //倒入杯中
    virtual void PourInCup(){
        cout << "将茶水倒入杯中!" << endl;
    }
    //加入辅料
    virtual void PutSomething(){
        cout << "加入食盐!" << endl;
    }
};

//业务函数
void DoBussiness(AbstractDrinking* drink){
    drink->MakeDrink();
    delete drink;
}

void test(){
    DoBussiness(new Coffee);
    cout << "--------------" << endl;
    DoBussiness(new Tea);
}

4.8.5 纯虚函数和多继承
多继承带来了一些争议,但是接口继承可以说一种毫无争议的运用了。

绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的概念,c++中没有接口的概念,但是可以通过纯虚函数实现接口。

接口类中只有函数原型定义,没有任何数据定义。

多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。

注意:除了析构函数外,其他声明都是纯虚函数。

4.8.6 虚析构函数

4.8.6.1 虚析构函数作用

虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。

class People{
public:
    People(){
        cout << "构造函数 People!" << endl;
    }
    virtual void showName() = 0;
    virtual ~People(){
        cout << "析构函数 People!" << endl;
    }
};

class Worker : public People{
public:
    Worker(){
        cout << "构造函数 Worker!" << endl;
        pName = new char[10];
    }
    virtual void showName(){
        cout << "打印子类的名字!" << endl;
    }
    ~Worker(){
        cout << "析构函数 Worker!" << endl;
        if (pName != NULL){
            delete pName;
        }
    }
private:
    char* pName;
};

void test(){

    People* people = new Worker;
    people->~People();
}

4.8.6.2 纯虚析构函数

纯虚析构函数在c++中是合法的,但是在使用的时候有一个额外的限制:必须为纯虚析构函数提供一个函数体。

那么问题是:如果给虚析构函数提供函数体了,那怎么还能称作纯虚析构函数呢?

纯虚析构函数和非纯析构函数之间唯一的不同之处在于纯虚析构函数使得基类是抽象类,不能创建基类的对象。

//非纯虚析构函数
class A{
public:
    virtual ~A();
};

A::~A(){}

//纯析构函数
class B{
public:
    virtual ~B() = 0;
};

B::~B(){}

void test(){
    A a; //A类不是抽象类,可以实例化对象
    B b; //B类是抽象类,不可以实例化对象
}

如果类的目的不是为了实现多态,作为基类来使用,就不要声明虚析构函数,反之,则应该为类声明虚析构函数。

4.8.7 重写 重载 重定义

  • 重载,同一作用域的同名函数
  1. 同一个作用域
  2. 参数个数,参数顺序,参数类型不同
  3. 和函数返回值,没有关系
  4. const也可以作为重载条件 //do(const Teacher& t){} do(Teacher& t)
  • 重定义(隐藏)
  1. 有继承
  2. 子类(派生类)重新定义父类(基类)的同名成员(非virtual函数)
  • 重写(覆盖)
  1. 有继承
  2. 子类(派生类)重写父类(基类)的virtual函数
  3. 函数返回值,函数名字,函数参数,必须和基类中的虚函数一致
class A{
public:
    //同一作用域下,func1函数重载
    void func1(){}
    void func1(int a){}
    void func1(int a,int b){}
    void func2(){}
    virtual void func3(){}
};

class B : public A{
public:
    //重定义基类的func2,隐藏了基类的func2方法
    void func2(){}
    //重写基类的func3函数,也可以覆盖基类func3
    virtual void func3(){}
};

4.8.8 指向类成员的指针

4.8.8.1 指向成员变量的指针

  • 定义格式
 <数据类型> <类名>::*<指针名>
 
 例如: int A::*pPram;
  • 赋值/初始化
   <数据类型> <类名>::*<指针名> = &<类名>::<非静态数据成员>
   
   例如: int A::*pParam = &A::param;
  • 解引用
  <类对象名>.*<非静态数据成员指针>
  <类对象指针>->*<非静态数据成员指针>
  
  例如: A a; 
       a.*pParam; 
       a->*pParam;
class A{
public:
    A(int param){
        mParam = param;
    }
public:
    int mParam;
};

void test(){
    A a1(100);
    A* a2 = new A(200);
    int* p1 = &a1.mParam;
    int A::*p2 = &A::mParam;

    cout << "*p1:" << *p1 << endl;
    cout << "a1.*p2:" << a1.*p2 << endl;
    cout << "a2->*p2:" << a2->*p2 << endl;
}

4.8.8.2 指向成员函数的指针

  • 定义格式
    <返回类型> (<类名>::*<指针名>)(<参数列表>)
  
      例如: void (A::*pFunc)(int,int);
  • 赋值/初始化
   <返回类型>(<类名>::*<指针名>)(<参数列表>)  = &<类名>::<非静态数据函数>
   
   例如: void (A::pFunc)(int,int) = &A::func;
  • 解引用
   (<类对象名>.*<非静态成员函数>)(<参数列表>)
   (<类对象指针>->*<非静态成员函数>)(<参数列表>)
   
   例如: A a; 
        (a.*pFunc)(10,20); 
        (a->*pFunc)(10,20);
class A{
public:
    int func(int a,int b){
        return a + b;
    }
};

void test(){
    A a1;
    A* a2 = new A;

    //初始化成员函数指针
    int(A::*pFunc)(int, int) = &A::func;
    //指针解引用
    cout << "(a1.*pFunc)(10,20):" << (a1.*pFunc)(10, 20) << endl;
    cout << "(a2->*pFunc)(10,20):" << (a2->*pFunc)(10, 20) << endl;
}

4.8.8.3 指向静态成员的指针

  • 指向类静态数据成员的指针
    指向静态数据成员的指针的定义和使用与普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联。
  • 指向类静态成员函数的指针
    指向静态成员函数的指针和普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联·
class A{
public:
    static void dis(){
        cout << data << endl;
    }
    static int data;
};

int A::data = 100;

void test(){
    int *p = &A::data;
    cout << *p << endl;
    void(*pfunc)() = &A::dis;
    pfunc();
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • C++类和对象 C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是 C++ 的核心...
    863cda997e42阅读 640评论 0 4
  • 3. 类设计者工具 3.1 拷贝控制 五种函数拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符析构函数拷贝和移...
    王侦阅读 1,795评论 0 1
  • C++ 面向对象编程 博客园地址:http://www.cnblogs.com/xiongxuanwen/p/42...
    先之阅读 683评论 0 1
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,513评论 1 51
  • 社会高速运转,想要得到更好的发展,就要求人们的学习能力、挖掘有用信息的能力强,而这归根到底是思考力/思维能力的问题...
    阿拉斯加阅读 1,215评论 1 18