C++类和对象

C++类和对象

封装

类的封装性

封装:

  • 把变量和函数合成一个整体

  • 对变量和函数进行访问控制

  • 类内部没有访问权限之分,所有成员可以相互访问

  • 在类的外部,访问权限才有意义。public protected private

  • 在累的外部值public修饰的成员才能被访问,在没有涉及继承与派生时,privateg和protected是同等级的,外部不允许访问。

类的初识

class 类名 // 私有,抽象概念, 系统不会给其分配空间
{
public:     // 公有  类的外部可访问
protected:  // 保护  类的外部不可访问
private:	// 私有  类的外部不可访问
};
#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
    void say()
    {
        money = 100;
        cout << "我是傻逼" << endl;
    }
protected:
    int age;
private:
    int money;
};
int main(int argc, char* argv[])
{
    Person p;
    // 虽然不可访问私有数据,但是可以使用公有方法访问私有变量
    p.say();
    return 0;
}
  • stuct和class的区别是struct的默认权限是公有的,而class默认权限是私有的。

将成员变量设为私有,可以赋予客户端访问数据的一致性。如果成员变量不是public,客户端唯一能够访问对象的方法就是通过成员函数。如果类中所有public权限的成员都是函数,客户在访问类成员的时候只会访问默认函数,不需要考虑访问成员需不需要添加()

可以细微划分访问控制,使用成员函数可以使得我们对变量的控制处理更加精细。如果我们让所有成员变量为public,每个人都可以读写它。如果我们设置为private,我们可以实现不准访问,只读访问,读写访问,设置可以写出只写访问。

构造和析构

构造函数和析构函数,这两个函数会被编译器自动调用,构造函数完成对象的初始化动作,析构函数在对象结束的时候完成清理工作。

如果你不提供构造函数和析构函数,编译器会给你增加默认的操作,但是默认操作不会做任何操作。

构造函数是创建对象时为成员属性赋值,构造函数由编译器自动调用,无需手动调用。析构函数主要用于对象销毁的时候自动调用,执行一些清理操作

构造和析构函数定义

构造函数名和类名相同,没有返回值,但是可以有参数 ClassName(){}

析构函数在类名前加~,没有返回值,不能重载,不能有参数 ~ClassName(){}

#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
    Person()
    {
        cout << "无参构造函数" << endl;
    }
    Person(int num)
    {
        this->num = num;
        cout << "有参数构造函数" << endl;
    }
    Person(const Person &other)
    {
        cout << "拷贝构造函数" << endl;
    }
    ~Person()
    {
        cout << "析构函数" << endl;
    }
private:
    int num;
};
int main(int argc, char* argv[])
{
    Person p;
    Person p1(1);
    return 0;
}
构造的分类以及调用
  • 构造函数的分类

按照参数类型:无参构造和有参构造

按照类型分类:普通构造和拷贝构造

  • 构造函数的调用

无参构造的调用形式:

Person p;             // 隐式调用
Person p1 = Person(); // 显示调用
Person();             // 匿名对象调用

有参构造的调用形式:

Person p(1);           // 隐式调用
Person p1 = Person(1); // 显示调用
Person p2 = 20;        // 隐式转换调用(只针对于有一个参数),尽量别用该方式
Person(1);             // 匿名对象调用

拷贝构造的调用形式:默认拷贝构造是浅拷贝,就对象初始化新对象的时候调用拷贝构造函数。

Person p;
Person p2 = Person(p);  // 显示调用
Person p3(p);           // 隐式调用
Person p4 = p;          // 使用等号隐式转换

注意: 构造函数和析构函数的顺序相反。

下方不会调用拷贝构造函数:

Person p(10);
Person p2;
p2 = p;

对于任何一个类,C++编译器至少会给我们写的类增加3个函数:

  • 默认构造
  • 默认析构
  • 默认拷贝构造

对类中的非静态成员属性简单的值拷贝,如果用户定义了拷贝构造,则C++不会再提供任何默认构造函数,如果用户提供了默认构造函数,C++不会提供默认无参构造函数,但是会提供拷贝构造函数。

因此我们在设计类的时候一般需要实现无参构造,有参构造,拷贝构造和析构函数。

深拷贝与浅拷贝

同一个对象之间可以赋值,使得2个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为浅拷贝。一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间的时候,析构函数做了动态内存释放的处理会导致内存重复释放的问题。

 浅拷贝,程序崩溃
#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
    Person()
    {
        name = nullptr;
        num = 0;
        cout << "无参构造" << endl;
    }
    Person(char *name, int num)
    {
        this->name = (char *) calloc(1, strlen(name) + 1);
        if(this->name == nullptr)
        {
            cout << "构造失败" << endl;
        }
        strcpy(this->name, name);
        this->num = num;
        cout << "有参构造" << endl;
    }
    ~Person()
    {
        if(name != nullptr)
        {
            cout << "空间被释放" << endl;
            free(name);
            name = nullptr;
        }
        cout << "析构函数" << endl;
    }
    void show()
    {
        cout << "num:" << num << " name:" << name << endl;
    }
private:
    char *name;
    int num;
};
int main(int argc, char* argv[])
{
    Person p("aasda", 10);
    p.show();
    Person p2 = p;
    return 0;
}
 深拷贝,正常释放
#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
    Person()
    {
        name = nullptr;
        num = 0;
        cout << "无参构造" << endl;
    }
    Person(char *name, int num)
    {
        this->name = (char *) calloc(1, strlen(name) + 1);
        if(this->name == nullptr)
        {
            cout << "构造失败" << endl;
        }
        strcpy(this->name, name);
        this->num = num;
        cout << "有参构造" << endl;
    }
    Person(const Person &other)
    {
        this->name = (char *) calloc(1, strlen(other.name) + 1);
        strcpy(this->name, other.name);
        this->num = other.num;
    }
    ~Person()
    {
        if(name != nullptr)
        {
            cout << "空间被释放" << endl;
            free(name);
            name = nullptr;
        }
        cout << "析构函数" << endl;
    }
    void show()
    {
        cout << "num:" << num << " name:" << name << endl;
    }
private:
    char *name;
    int num;
};
int main(int argc, char* argv[])
{
    Person p("aasda", 10);
    p.show();
    Person p2 = p;
    p2.show();
    return 0;
}
初始化列表与成员对象

构造函数和其他函数不同,除了有名字,参数列表函数体之外,还有初始化列表。

#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
    Person() : a(0), b(1), c(2)
    {
        cout << "无参构造" << endl;
    }
    Person(int a, int b, int c) : a(a), b(b), c(b)
    {
        cout << "无参构造" << endl;
    }
    void show()
    {
        cout << "a:" << a << " b:" << b << " c:" << c << endl;
    }
private:
    int a;
    int b;
    int c;
};
int main(int argc, char* argv[])
{
    Person p;
    p.show();
    Person p2(8, 9, 10);
    p2.show();
    return 0;
}

注意: 初始化列表只能在构造函数中使用

对象成员的初始化列表

在类中定义数据成员一般都是基本数据类型。但是当类中的成员也可以是对象,叫做对象成员。C++中对对象成员的初始化是非常重要的操作,当创建了一个对象的时候,C++编译器必须确保调用了所有子对象的构造函数。如果所有子对象有默认构造函数,编译器可以自动调用他们。但是如果子对象没有默认构造函数,或者想指定调用某个构造函数怎么办?初始化列表提供了对象成员的构造函数调用方式。

#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Aaa
{
public:
    Aaa(int a) : aaa(a) {
        cout << "有参构造" << endl;
    }
    Aaa() {
        cout << "无参构造" << endl;
    }
    int getA(){ return  aaa; }
private:
    int aaa;
};
class Person
{
public:
    Person() : a(Aaa(5)) {}
    Person(int a) : a(Aaa(a)) {}
    void show()
    {
        cout << "a:" << a.getA() << endl;
    }
private:
    Aaa a;
};
int main(int argc, char* argv[])
{
    Person p;
    p.show();
    Person p2(8);
    p2.show();
    return 0;
}
explicit 关键字

C++提供了关键字explicit,表示禁止通过构造函数进行隐式转换。声明为explicit的构造函数不能在隐式转换中使用。

explicit是针对只有一个参数的构造函数,或者是除了第一个参数其他的都是默认值的多参数的构造函数。

动态对象的创建

在我们创建数组的时候总是需要提前预定数组长度,然后编译器分配预定长度的数组空间,在使用数组时,会有这样的问题,数组也许空间太大,也许空间太小。所以对于数组而言,如果能动态的分配大小空间最好不过了,多以C提供了动态分配内存函数malloc和free,可以在运行时候从堆中分配存储单元。然而这些函数在C++中不能很好的运行,因为他们不能帮我们完成对象的创建。

对象的创建

当创建一个C++对象的时候,会发生两件事:

  • 为对象分配内存空间。
  • 调用构造函数初始化内存。

如果我们使用C函数的话会有以下问题:

  • 程序员必须确定对象长度
  • malloc返回的是一个void指针,C++不允许将void指针赋值给其他指针,必须强转
  • malloc可能申请失败,必须判断返回值来保证内存分配成功
  • 用户在使用对象之前必须对它初始化,构造函数不能显示调用初始化,用户有可能会忘记调用初始化函数

总之,C的动态内存分配函数太复杂,于是C++中出现了new和delete来创建和销毁一个对象。

new operator

C++中解决动态分配方案就是把创建一个对象所需要的操作都结合在一个称为new的运算符里面,当使用new创建一个对象时,它就在堆里面为对象分配内存并调用构造函数完成初始化。

给基本对象申请空间
int main(int argc, char* argv[])
{
    int *p = new int(100);
    cout << *p << endl;
    int *p2 = new int[5]{1,2,3,4,5};
    cout << p2[0] << " " << p2[1] << endl;
    delete p;
    delete [] p2;
    return 0;
}
给对象申请空间
#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
    Person() : a(5) {
        cout << "构造函数" << endl;
    }
    ~Person()
    {
        cout << "析构函数" << endl;
    }
    explicit Person(int a) : a(a) {
        cout << "有参构造函数" << endl;
    }
    void show()
    {
        cout << "a:" << a << endl;
    }
private:
    int a;
};
int main(int argc, char* argv[])
{
    // 申请空间,构造函数
    Person *p = new Person;
    // 析构函数,销毁空间
    delete p;
    Person *p2 = new Person[5];
    delete [] p2;
    Person *p3 = new Person[5]{Person(3),Person(2),Person(2),Person(2),Person(2)};
    delete [] p3;
    Person *p4 = new Person;
    void *p5 = p4;
    delete p5; // 不会析构
    return 0;
}

注意: 如果new没有加[] 释放的时候不用加[],如果new加了[]则delete也需要加[]。

delete 无法从void中寻找析构函数,释放的时候注意类型。

静态成员

在类定义中,成员包括成员变量和成员函数,这些成员可以使用static关键字声明为静态的,称为静态成员。不管这个类创建了多少个对象,静态成员只有一份拷贝,这个拷贝属于这个类的对象共享。

静态成员变量

在一个类中,如果将一个变量声明为static,这种成员称为静态成员变量。与一个的数据成员不同,无论建立多少个对象,该变量只有一份静态数据的拷贝。静态成员变量属于某个类所有的对象共享。静态变量是在编译期间就分配空间,对象还没创建的时候就已经分配了空间。

静态成员变量必须在类中声明,在类外定义。静态数据成员不属于某个对象,在为对象分配空间的时候不包括静态数据成员所占的控件。静态数据成员可以通过类名或者对象名来引用。

静态成员函数

针对于无法直接访问的private静态成员,可以使用静态成员函数访问。

#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
    Person() : a(5) {
        cout << "构造函数" << endl;
    }
    ~Person()
    {
        cout << "析构函数" << endl;
    }
    explicit Person(int a) : a(a) {
        cout << "有参构造函数" << endl;
    }
    void show()
    {
        cout << "a:" << a << endl;
    }
    static int getB()
    {
        return b;
    }
private:
    int a;
    static int b;
};
int Person::b = 20;
int main(int argc, char* argv[])
{
    cout << sizeof(Person) << endl;
    cout << Person::getB() << endl;
    return 0;
}
const 静态成员

对于一个类的成员,既要实现共享,又要实现不可变,那就用const修饰。定义静态const成员时,最好在类内部初始化。

this指针

C++封装性质:将数据和方法封装在一起

数据和方法是分开存储的。每个对象拥有独立的数据,但是对象方法是共享的。每个方法调用的时候都会隐式存在一个this指针。

C++规定:

  • this指针是隐含在成员函数内的一种指针,当一个对象被创建后,他的每一个成员函数都含有一个系统自动生成的隐含指针this,用来保存这个对象的地址。
  • 成员函数通过this指针即可知道操作的数据对象是谁
  • 隐藏在每一个非静态成员函数中,静态成员函数内部没有this指针,静态成员函数不能操作非静态成员变量
使用
  • 当形参和成员变量同名的时候可以使用this来区分。

  • 在类的普通成员函数中返回对象本身,可以使用 return *this;

const

const 修饰成员函数

使用const修饰成员函数时候,const修饰this指针指向的区域,成员函数体内不可以修改奔类中对的任何普通成员变量,放成员变量类型钱用mutable修饰时候例外。

#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
    int getA() const {
        a = 50;
        return a;
    }
private:
    mutable int a{0};
};
int main(int argc, char* argv[])
{
    Person p;
    cout << p.getA() << endl;
    return 0;
}

函数如果不会修改成员函数的数据,就给函数加const。表示在此函数内不会给成员函数赋值。

const 修饰对象

const修饰对象叫常对象,编译器认为普通函数都有修改成员变量的可能。

因此不能调用非const修饰的函数。

#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
    void setA(int a) const
    {
        // this->a = a;
    }
    int getA() const {
        a = 50;
        return a;
    }
private:
    mutable int a{0};
};
int main(int argc, char* argv[])
{
    const Person p;
    cout << p.getA() << endl;
    return 0;
}

友元

类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部访问。但是有时候需要在类的外部访问类的私有成员,怎么办?解决方法就是使用友元函数,友元函数是一种特权函数,C++允许这个特权函数访问私有成员。程序员可以把一个全局函数、某个类中的成员函数、甚至整个类声明为友元。

普通全局函数作为友元
#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
    friend int getA(const Person &p);
public:
private:
    int a{0};
};
int getA(const Person &p)
{
    return p.a;
}
int main(int argc, char* argv[])
{
    const Person p;
    cout << getA(p) << endl;
    return 0;
}
类的成员函数作为另一个类的友元
#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person;
class Dog
{
public:
    int getA(const Person &p);
};
class Person
{
    friend int Dog::getA(const Person &p);
public:
private:
    int a{0};
};
int Dog::getA(const Person &p)
{
    return p.a;
}
int main(int argc, char* argv[])
{
    const Person p;
    Dog d;
    cout << d.getA(p) << endl;
    return 0;
}
类作为另一个类的友元
#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person;
class Dog
{
public:s
    int getA(const Person &p);
};
class Person
{
    friend class Dog;
public:
private:
    int a{0};
};
int Dog::getA(const Person &p)
{
    return p.a;
}
int main(int argc, char* argv[])
{
    const Person p;
    Dog d;
    cout << d.getA(p) << endl;
    return 0;
}

运算符的重载

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

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

语法:函数名是由关键字operator紧跟着运算符

比如重载+ : operator+

注意:重载运算符不要更改运算符的本质。

可重载的运算符

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

+ - * / % ^ & | ~ ! = < > += -= *= /= %=
^= &= |= << >> >>= <<= == != <= >= && || ++ -- ->* ' -> [] () new delete new[] delete[]

不能重载的运算符有: . :: .* ?: sizeof

运算符与友元函数
#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
    friend ostream &operator<<(ostream &out, const Person &p);
    friend Person operator+(const Person p1, const Person &p2);
public:
private:
    int a{5};
};
ostream &operator<<(ostream &out, const Person &p)
{
    return out << p.a;
}
Person operator+(const Person p1, const Person &p2)
{
    Person p;
    p.a = p1.a + p2.a;
    return p;
}
int main(int argc, char* argv[])
{
    Person p;
    Person p2;
    cout << p << endl;
    cout << p2 + p << endl;
    return 0;
}
运算符与成员函数
#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
    friend ostream &operator<<(ostream &out, const Person &p);
public:
    Person operator+(const Person &p2)
    {
        Person p;
        p.a = this->a + p2.a;
        return p;
    }

private:
    int a{5};
};
ostream &operator<<(ostream &out, const Person &p)
{
    return out << p.a;
}
int main(int argc, char* argv[])
{
    Person p;
    Person p2;
    cout << p << endl;
    cout << p2 + p << endl;
    return 0;
}
重载++/–

重载++和–运算符 需要区分是前置++/–还是后置++/–;区分方式是按照参数个数区分。

#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
    friend ostream &operator<<(ostream &out, const Person &p);
public:
    Person operator+(const Person &p2)
    {
        Person p;
        p.a = this->a + p2.a;
        return p;
    }
    Person operator++(int)
    {
        Person p = *this;
        this->a++;
        return p;
    }
    Person &operator++()
    {
        ++this->a;
        return *this;
    }
    Person operator--(int)
    {
        Person p = *this;
        this->a--;
        return p;
    }
    Person &operator--()
    {
        --this->a;
        return *this;
    }
private:
    int a{5};
};
ostream &operator<<(ostream &out, const Person &p)
{
    return out << p.a;
}
int main(int argc, char* argv[])
{
    Person p;
    Person p2;
    cout << p << endl;
    cout << p2 + p << endl;
    cout << p++ << endl;
    cout << p << endl;
    cout << ++p2 << endl;
    cout << p-- << endl;
    cout << p << endl;
    cout << --p2 << endl;
    return 0;
}
重载->和*
#include <iostream>
#include <string>
#include <iostream>
using namespace std;
class Person
{
    friend ostream &operator<<(ostream &out, const Person &p);
public:
    Person()
    {
        cout << "Person" << endl;
    }
    ~Person()
    {
        cout << "~Person" << endl;
    }
    void show()
    {
        cout << "aaa" << endl;
    }
private:
    int a{5};
};
class SmartPoint
{
public:
    SmartPoint(Person *p)
    {
        this->p = p;
    }
    ~SmartPoint()
    {
        delete p;
    }
    Person* operator->()
    {
        return this->p;
    }
    Person& operator*()
    {
        return *this->p;
    }

private:
    Person *p;
};
ostream &operator<<(ostream &out, const Person &p)
{
    return out << p.a;
}
int main(int argc, char* argv[])
{
    SmartPoint point(new Person);
    point->show();
    (*point).show();
    return 0;
}

注意: 类中无指针成员的时候,无需重写=运算符,斗则必须重载=运算符

重载赋值运算符

指针作为类中的成员:

  • 拷贝构造函数必须自定义(默认拷贝构造是浅拷贝)
  • 必须重载=运算符(默认=运算符是浅拷贝)

如果不重写拷贝并且也不重载=运算符,编译器会给自动创建这些函数,默认的行为类似于浅拷贝。

#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
class Person
{
    friend ostream &operator<<(ostream &out, const Person &p);
public:
    Person()
    {
        cout << "Person" << endl;
    }
    ~Person()
    {
        cout << "~Person" << endl;
    }
private:
    int a{5};
};
class SmartPoint
{
public:
    SmartPoint()
    {

    }
    SmartPoint(Person *p)
    {
        this->p = p;
    }
    ~SmartPoint()
    {
        delete p;
    }
    explicit SmartPoint(const SmartPoint &other)
    {
        p = new Person(*other.p);
    }
    SmartPoint& operator=(const SmartPoint &other)
    {
        cout << "赋值操作" << endl;
        p = new Person(*other.p);
        return *this;
    }
    Person *p;
};
ostream &operator<<(ostream &out, const Person &p)
{
    return out << p.a;
}
int main(int argc, char* argv[])
{
    SmartPoint point(new Person);
    SmartPoint point2(point);
    SmartPoint point3;
    point3 = point;
    cout << *(point.p) << endl;
    cout << *(point2.p) << endl;
    cout << *(point3.p) << endl;
    return 0;
}
重载 != 、 == 、 () 等运算符号
#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
class Person
{
    friend ostream &operator<<(ostream &out, const Person &p);
public:
    Person(int a) : a(a) {}
    bool operator==(const Person &other)
    {
        return this->a == other.a;
    }
    bool operator!=(const Person &other)
    {
        return this->a != other.a;
    }
    Person & operator()(int a)
    {
        this->a += a;
        return *this;
    }
private:
    int a{5};
};
ostream &operator<<(ostream &out, const Person &p)
{
    return out << p.a;
}
int main(int argc, char* argv[])
{
    Person p(5);
    Person p2(6);
    Person p3(5);
    cout << (p==p2) << endl;
    cout << (p!=p3) << endl;
    Person p6(2);
    cout << p6(5) << endl;
    cout << Person(9)(5) << endl;
    return 0;
}

注意: 不要重载 && || ,因为用户无法实现 && 和 || 具有短路特性

总结: =,[],(),->操作符号只能通过成员函数进行重载,<< 和 >> 只能通过全局函数配合友元函数进行重载,不要重载&&和||操作符号

运算符使用建议
所有一元运算符成员
= () [] -> ->*必须是成员
+= -= /= *= ^= &= != %= >>= <<=成员
其他二元运算符非成员

继承和派生

C++最终要的特性是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类型不仅拥有旧类的成员,还拥有新定义的成员。一个B类继承自A类或者称为从A类派生出B类。这样的话A类称为基类(父类),B类称为派生类(子类),派生类中的成员包含两大部分:一类是从基类继承过来的,一类是自己增加的成员,从基类继承过来的表现其共性,而新增的成员体现了其个性。

子类继承于父类

父类派生出子类

继承的访问控制

代码示例:

class Derive : 继承方式 Base [, 继承方式 Base2 ... ]
{
// ...... 
}

继承方式分类:

访问权限:

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

父类个数:

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

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

代码示例

public继承:在类外可以访问父类的public变量,在类内可以访问public和protected数据。私有数据在子类内外都是不可见的。

#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
class Base
{
public:
    int a{1};
protected:
    int b{2};
private:
    int c{3};
};
class Derive : public Base
{
public:
    void show()
    {
        cout << a << b << endl;
    }
};
int main(int argc, char* argv[])
{
    Derive d;
    cout << d.a << endl;
    return 0;
}

protected继承:在类内可以访问public和protected数据。类外无法访问;private是不可见数据。

#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
class Base
{
public:
    int a{1};
protected:
    int b{2};
private:
    int c{3};
};
class Derive : protected Base
{
public:
    void show()
    {
        cout << a << b << endl;
    }
};
int main(int argc, char* argv[])
{
    Derive d;
    return 0;
}

private继承:在类内可以访问public和protected数据。类外无法访问;private是不可见数据。

#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
class Base
{
public:
    int a{1};
protected:
    int b{2};
private:
    int c{3};
};
class Derive : private Base
{
public:
    void show()
    {
        cout << a << b << endl;
    }
};
int main(int argc, char* argv[])
{
    Derive d;
    d.show();
    return 0;
}

继承中的构造和析构

子类是由父类成员叠加子类新成员而成。

构造顺序是 先父类再子类,析构顺序相反,如果有对象成员则是先父类再对象成员再子类,析构顺序相反。

#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
#include <fstream>
using namespace std;
class Other
{
public:
    Other()
    {
        cout << "Other" << endl;
    }
    ~Other()
    {
        cout << "~Other" << endl;
    }
};
class Base
{
public:
    Base()
    {
        cout << "Base" << endl;
    }
    ~Base()
    {
        cout << "~Base" << endl;
    }
};
class Derive : private Base
{
public:
    Derive()
    {
        cout << "Derive" << endl;
    }
    ~Derive()
    {
        cout << "~Derive" << endl;
    }
    Other o;
};
int main(int argc, char* argv[])
{
    Derive d;
    return 0;
}

继承中的同名变量和同名函数

同名变量

当父类和子类中成员变量同名时,默认会选择子类成员,如果想访问父类同名成员则必须加上父类的作用域

#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
#include <fstream>
using namespace std;
class Base
{
public:
    int num{10};
};
class Derive : public Base
{
public:
    int num{20};
};
int main(int argc, char* argv[])
{
    Derive d;
    cout << d.num << endl;
    cout << d.Base::num << endl;
    return 0;
}
同名函数

当子类实现父类同名成员函数时候,父类的所有同名函数将会被子类屏蔽。如果用户必须调用父类的同名函数则加上作用域即可。

#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
#include <fstream>
using namespace std;
class Base
{
public:
    int getNum() { return num; }
    int getNum(int a) { return num; }

private:
    int num{10};
};
class Derive : public Base
{
public:
    int getNum() { return num; }

private:
    int num{20};
};
int main(int argc, char* argv[])
{
    Derive d;
    cout << d.getNum() << endl;
    cout << d.Base::getNum() << endl;
    cout << d.Base::getNum(1) << endl;
    return 0;
}

C++中基类不是所有的函数都能被继承到派生类中,构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对他们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建,另外operator=也不能被继承,因为他完成类似于拷贝构造的行为,也就是说尽管我们知道基类如何由=右边的对象初始化左边的对象的所以后成员,但是这个并不意味着这对其派生类依然有效。在继承过程中,如果没有重写这些函数,则编译器会给这些函数生成默认函数。

静态同名成员和函数

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

  • 都可以被继承到派生类中
  • 如果重新定义一个静态成员函数,则所有基类的其他重载函数都会被屏蔽
  • 如果我们改变基类中的一个函数的特征,所以使用还函数名的基类模板都会被隐藏
#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
#include <fstream>
using namespace std;
class Base
{
public:
    static int getNum() { return num; }
    static int getNum(int a) { return num; }

private:
    static int num;
};
int Base::num{10};
class Derive : public Base
{
public:
    static int getNum() { return num; }

private:
    static int num;
};
int Derive::num{20};
int main(int argc, char* argv[])
{
    Derive d;
    cout << d.getNum() << endl;
    cout << d.Base::getNum() << endl;
    cout << d.Base::getNum(1) << endl;
    return 0;
}

多继承

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

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

#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
#include <fstream>
using namespace std;
class Base
{
public:
    int a{10};
};
class Base2
{
public:
    int a{20};
};
class Derive : public Base, public Base2
{
public:
};
int main(int argc, char* argv[])
{
    Derive d;
    cout << d.Base::a << endl;
    cout << d.Base2::a << endl;
    return 0;
}
菱形继承

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

因为继承的数据存在两份,因此调用数据的时候会存在二义性问题。这可以加作用域访问对应的父类的a,可以解决二义性,但是继承了两份数据。

对于菱形继承带来的问题,C++提供了一种方式,采用虚基类。

class Base
{
public:
    int a{10};
};
class Base2 : public Base
{
public:
};
class Base1 : public Base
{
public:
};
// Derive 中有两个a
class Derive : virtual public Base1, virtual public Base2
{
public:
};

虚继承

在继承方式前面加virtual修饰,叫做虚继承。使用虚继承,在菱形继承的时候不存在菱形继承的问题。

class Derive : virtual 继承方式 Base
{
// ......
};
#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
#include <fstream>
using namespace std;
class Base
{
public:
    int a{10};
};
class Base2 : virtual public Base
{
public:
};
class Base1 : virtual public Base
{
public:
};
class Derive : public Base1, public Base2
{
public:
};
int main(int argc, char* argv[])
{
    Derive d;
    cout << d.a << endl;
    return 0;
}

vbptr:虚基类指针,虚基类指针是指向虚基类表vbtable

vbtable:虚基类表中存放的是数据的偏移量。

总结: 使用虚继承产生vbptr 和 vbtable 的目的是为了保证不管多少个继承,虚基类的数据只有一份。

当使用虚继承的时候,虚基类是被共享的,也就是在继承体系中,无论被继承多少次,对象内存模型中之后出现一个虚基类的字对象(这和多继承是完全不同的)。即使共享虚基类,但是必须以后一个类来完成基类的初始化(因为所有的对象都必须完成初始化,那怕是默认的),同时还不能够进行重复的初始化。

那么虚基类由谁来初始化呢?C++标准中选择在每次继承子类中必须书写初始化语句(因为每一次继承子类都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。

多态

多态是面向对象程序设计中数据抽象和继承之外的第三个基本特征。多态性提供接口与具体实现之间的另一层隔离,从而将what和how分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以去扩展,而且当项目在需要新的功能时也能扩展。C++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数就是实现运行时多态。静态多态和动态多态的区别就是函数地址是早绑定(静态联编)和晚绑定(动态联编)。如果函数的调用在编译阶段就可以确定调用地址,并且产生函数代码,就是静态多态,就是说地址是早绑定的。而如果函数的调用地址不能在编译期间确定,而需要在运行时候才能决定,就是运行时多态, 属于晚绑定。

#include <iostream>
#include <string>
#include <iostream>
#include <algorithm>
#include <fstream>
using namespace std;
class Base
{
public:
    virtual void print()
    {
        cout << "Base" << endl;
    }
};
class Derive : public Base
{
public:
    void print() override
    {
        cout << "Derive" << endl;
    }
};
int main(int argc, char* argv[])
{
    Base *d = new Derive();
    d->print();
    return 0;
}

备注: 如果不适用virtual main函数中的print调用的会是父类的方法,而不是派生类的方法

C++动态多态主要就是通过虚函数来实现的,虚函数允许子类重新定义父类成员函数,而子类重新定义父类虚函数的做法称为覆盖或者称为重写(override)。对于特定的函数进行动态绑定,C++要求在基类中声明这个函数的时候使用virtual关键字修饰,动态绑定也就对virtual函数生效。为创建一个需要动态绑定的虚成员函数,可以简单的在这个函数声明前加上virtual关键字。如果一个函数在基类中被声明为virtual关键字,那么在所有派生类中它都是virtual的,在派生类中virtual函数的重定义称为重写(override)。

注意: virtual关键字只能修饰成员函数和析构函数,构造函数不能为虚函数。

虚函数

虚函数在类中会产生一个虚函数指针,虚函数指针指向虚函数表;

vfptr : 虚函数指针,指向虚函数表

vftable : 虚函数表,存放的是虚函数的入口地址

总结:

  • 如果类中不涉及到继承,则函数指针偏移量指向自身的函数。

  • 如果涉及到继承,派生类会继承基类的虚函数指针和虚函数表,编译器会将虚函数表中的函数入口地址更新为子类的函数地址。如果使用基类指针或者引用访问虚函数的时候会间接的调用子类的虚函数。

应用

一般使用基类指针或者引用作为函数参数:

#include <iostream>
using namespace std;
class Base
{
public:
    virtual void print()
    {
        cout << "Base" << endl;
    }
};
class Derive1 : public Base
{
public:
    void print() override
    {
        cout << "Derive1" << endl;
    }
};
class Derive2 : public Base
{
public:
    void print() override
    {
        cout << "Derive2" << endl;
    }
};
void func(Base &base)
{
    base.print();
}
int main(int argc, char* argv[])
{
    Derive1 d1;
    Derive2 d2;
    func(d2);
    func(d1);
    return 0;
}

虚析构

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

先看下面的例子:

#include <iostream>
using namespace std;
class Base
{
public:
    ~Base()
    {
        cout << "~Base" << endl;
    }
    virtual void print()
    {
        cout << "Base" << endl;
    }
};
class Derive : public Base
{
public:
    ~Derive()
    {
        cout << "~Derive" << endl;
    }
    void print() override
    {
        cout << "Derive" << endl;
    }
};
int main(int argc, char* argv[])
{
    Base *b = new Derive();
    delete b;
    return 0;
}

我们看到子类并没有被析构。

如果我们把析构函数改成虚函数,则发现都可以调用,代码如下:

#include <iostream>
using namespace std;
class Base
{
public:
    virtual ~Base()
    {
        cout << "~Base" << endl;
    }
    virtual void print()
    {
        cout << "Base" << endl;
    }
};
class Derive : public Base
{
public:
    ~Derive()
    {
        cout << "~Derive" << endl;
    }
    void print() override
    {
        cout << "Derive" << endl;
    }
};
int main(int argc, char* argv[])
{
    Base *b = new Derive();
    delete b;
    return 0;
}

建议: 如果类会被继承,把析构函数写成虚函数

纯虚函数和抽象类

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

纯虚函数格式是

virtual void function() = 0; 

如果一个类中有纯虚函数,那么这个类就是抽象类

抽象类不能实例化对象;virtual void function() = 0告诉编译器在vftable中为函数保留一个位置,但是这个特定的位置不放地址。

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

#include <iostream>
using namespace std;
class Base
{
public:
    virtual ~Base() = default;
    virtual void print1() = 0;
    virtual void print2() = 0;
    virtual void print3() = 0;
    virtual void print()
    {
        print1();
        print2();
        print3();
    }
};
class Derive1 : public Base
{
public:
    void print1() override
    {
        cout << "print1 ";
    }
    void print2() override
    {
        cout << "print2 ";
    }
    void print3() override
    {
        cout << "print2 Derive1" << endl;
    }
};
class Derive2 : public Base
{
public:
    void print1() override
    {
        cout << "print1 ";
    }
    void print2() override
    {
        cout << "print2 ";
    }
    void print3() override
    {
        cout << "print2 Derive2" << endl;
    }
};
int main(int argc, char* argv[])
{
    Base *b1 = new Derive1();
    Base *b2 = new Derive2();
    b1->print();
    b2->print();
    delete b1;
    delete b2;
    return 0;
}
纯虚函数和多继承

多继承带来了一些争议,但是接口继承可以说是一种毫无争议的运用。绝大多数面向对象的语言都不支持多继承,但是绝大数面向对象语言都支持接口的概念。C++中没有接口的概念,但是可以通过纯虚函数实现接口。

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

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

注意: 除了析构函数外,其他的都要声明成纯虚函数

纯虚析构函数

纯虚析构在C++中是合法的,但是在使用的时候有一个额外的限制,必须为纯虚析构提供一个函数体。那么问题是,如果给纯虚析构提供函数体的情况下,怎么还能称为纯虚析构函数呢?纯虚析构函数和非纯虚析构函数之间的唯一不同之处在于纯虚析构函数使得基类是抽象类,不能创建对象。

#include <iostream>
using namespace std;
class Base
{
public:
    virtual ~Base() = 0;
};
Base::~Base() {}
class Derive1 : public Base
{
public:
};
int main(int argc, char* argv[])
{
    // Base b;
    Base *b1 = new Derive1();
    return 0;
}

虚函数和纯虚函数、虚析构和纯虚析构

  • 虚函数:由virtual修饰的有函数体的函数。
  • 纯虚函数:virtual修饰,函数尾部是=0,没有函数体,所在类为抽象类。
  • 虚析构:修饰类中的析构函数。
  • 纯虚析构:析构函数后面加=0;必须在类外实现析构函数体。

重载、重写、重定义

重载:
  • 同一个作用域

  • 参数个数,参数顺序,参数类型不同

  • 与函数返回值没有关系

  • const也可以作为重载条件

int func(){}
int func(int a){}
int func(int a, int b){}
重定义 - 隐藏父类函数
  • 有继承

  • 子类重新定义父类的同名成员(非虚函数)

class Base
{
public:
    void func(){}
    void func(int, int){}
};
class Derive : public Base
{
public:
    void func(){}
}
重写 - 覆盖父类函数
  • 有继承
  • 子类重写父类中的虚函数
  • 函数的返回值,函数的名字,函数的参数,必须和基类中的虚函数一致
class Base
{
public:
    virtual void func(){}
    virtual void func(int, int){}
};
class Derive : public Base
{
public:
    void func() override{}
    void func(int, int) override{}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

turbolove

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值