C++类基础

本文详细介绍了C++中的类,包括类声明与定义、对象的存储空间、静态成员、构造函数、复制构造函数、运算符重载、虚函数、抽象类以及C++11中的`override`和`final`关键字。重点讲解了构造函数的初始化列表、虚函数表的原理、派生类构造函数的执行顺序,以及如何避免二义性问题和使用虚基类解决多继承的冲突。

1. 类

1.1 类声明与类定义

(1)类声明

class a;//声明一个类,而不是定义

  在声明之后,定义之前,类a是一个不完全类型,即已知类a是一个类型,但不知道其包含了哪些成员。
  不完整类型使用方式是有限的。不能定义不完整类型的对象,不完全类型只能定义指向该类型的指针或者引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数
  在创建类的对象之前,必须要完整的定义该类,而不只是声明。(定义是要分配存储空间的,定义即声明,但声明不是定义)同样,在使用引用和指针访问对象成员之前,必须已经定义了类。

(2)为类的成员使用类声明

  只有当类定义已经在前面出现过,数据成员才能被指定为该类类型。如果该类型是不完全类型,那么数据成员只能是指向该类类型的指针或引用。
  因为只有当类定义体完成后才能定义类,因此类不能具有自身类型的数据成员,然而,只要类名一出现,就可以认为该类已经声明。因此,类的数据成员可以是指向自身类型的指针或引用

class val{
    int time;
    int price;
};
class node{
    val v;//因为val类前面已经定义了,所以可以用val类来定义对象
    node *next;//因为在这个地方node类还没有完全定义,所以属于不完全类型,因此不能出现用node类定义对象,但只要类名出现,就可以认为声明了,所以可以用node类来定义指向该类型的指针或引用
};

1.2 对象的存储空间

  • 一般类定义的时候不进行存储空间的分配(有静态数据成员的,会在定义类的时候为其分配空间),而在用类定义对象的时候,将为这个类的对象进行存储空间的分配。
  • 对象的存储空间主要包含的是类的数据成员部分所占的空间
    • 类的大小(对象的大小),一般指非静态数据成员的大小,要考虑到数据对齐,不包含成员方法,因为那是代码,存放在代码区,且所有对象共有,公用的成员方法通过隐含的this指针判断对象调用的,然后该方法就可以去访问那个对象的数据空间了。
    • 如果类中包含虚函数,那么类对象在内存中还会被自动包含一个指向虚函数表的指针,也是占空间的, 一般这个指针在对象所在内存中起始地址处。
    • 如果类是没有定义任何数据成员,类的大小或对象的大小本应该是0,但因为一个对象往往是独一无二的,体现在内存中,是不同对象的地址是不同的,所以编译器会让这个空对像占一个字节,以便不同对象的起始地址不同。

1.3 static成员

(1)static成员函数

  • static成员函数没有this指针,因为static成员,包括数据成员和成员函数,都是类的组成部分,而不是任何对象的组成部分,因此static方法是没有this指针的。
  • 也是因为static成员不属于任何对象,因此static成员函数不能访问任何对象的数据成员,他只能访问类的static数据成员或其他的static成员方法。
  • static成员函数不能声明为虚函数。

(2)static数据成员

  • 普通static数据成员

    • 即非const的static数据成员,它必须在类定义体的外部定义,只定义一次。不像普通的数据成员,static数据成员不是通过构造函数进行初始化的。

      class A{
          private://public也行
              static int i;
          ....
      };
      static int A::i = 1;//error
      int A::i = 1;//ok
      int A::i;//ok,不显示初始化也正确,到时候会默认初始化为0
      

      【注意1】:这里static关键字只能用于类定义体内部,在外面定义时不能标识为static。
      【注意2】:且不管static数据成员是私有的还是共有的,都可以在类外定义初始化。
      【注意3】:sizeof类时候,计算的类大小是不包含static成员的,因为静态成员不占类和对象的空间。同理可以说明:静态成员的地址并不在对象的地址范围内
      【注意4!!!!!】:static变量必须在类外初始化,否则会报错。因为类的定义不占空间,类的定义只是给编译器说明这个类型有多大,好在定义对象的时候分配空间,因此不像变量的定义的那样,定义即分配空间,这里类的定义中,所有数据成员都只是声明,static类型也不例外,那为什么说static数据成员在类定义时就分配了空间了呢?因为static数据成员必须在类外定义,这个定义可以显示初始化,但一定要有这个类外的定义!!!!

  • 特殊的const static数据成员

    • 一般而言,类的static数据成员,和普通数据成员一样,不能在类的定义体中初始化。
    • 这个规则有个例外,如果是const static数据成员,则可以在类体中定义该数据成员时初始化。

      class A{
      private://public也行
          const static int i = 1;//ok
      };
      
  • static成员不是类对象的组成部分

    • static数据成员的类型可以是该成员所属的类类型,而非static数据成员被限定声明为指向其自身类类型的指针或引用。

      class A{
          private:
              static A mem1;//ok
              A mem2;//error
              A &mem3;//ok
              A *mem4;//ok
      };
      A::mem1(..)//static 类对象的构造函数
      

1.4 构造函数

(1)构造函数初始化列表

  构造函数的工作是保证每个对象的数据成员(除去static,因为static成员不属于对象)具有合适的初始值,一般建议使用构造函数初始化列表。
  构造函数初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟着一个放在圆括号中的初始化式

class A{
    private:
        int a;
        char c;
        string s;
    public:
        A():a(1),c('a'),s("hello");//erro
        A();
        或
        A():a(1),c('a'),s("hello")//ok
        {
        }
};
A::A():a(1),c('a'),s("hello");//ok

构造函数初始化列表不能在构造函数声明的时候出现,只在构造函数的定义时出现。
构造函数分两个阶段执行:

  1. 先执行初始化阶段(即执行初始化列表,属于显式初始化)
  2. 然后执行构造函数函数体阶段,该阶段不是初始化数据了,而是重新赋值。

对于在构造函数初始化列表中没有显式提及的每个成员

  • 内置类型(int、char、double、枚举等)或者复合类型(数组指针等)的成员的初始化值依赖于对象的作用域:
    • 对象在局部作用域中时,这些成员不被初始化
    • 对象在全局作用域中它们被初始化为0
  • 对于类类型数据成员,不管在哪个作用域,都运行该类型的默认构造函数来初始化,所以对于没有定义默认构造函数(无形参的构造函数)的类类型数据成员,一定要显式地在构造函数初始化列表中调用该类的带参数的构造函数。

总之:对于用类定义一个对象,会执行对象的构造函数,而执行构造函数,首先会执行构造函数初始化列表,如果有数据成员不在初始化列表中,则按上述原则进行初始化。然后再去执行构造函数函数体,但已经不是初始化了,而是重新赋值。

class A{
    public:
        int i;
    A():i(1024){}
};
class B{
    public:
        int j;
        A a;//因为在类的定义中,所以这是声明,不是定义对象a,定义是要分配空间的。
}
void fun(){
    B b;
    cout << b.j <<" "<<b.a.i<<endl;
}
B gb;
int main(){
    cout << gb.j<<" "<<gb.a.i<<endl;
    fun();
    return 0;
}

输出的值是

0 1024
不确定值 1024

如果是以下情况:

class A{
    public:
        int i;
    A(){}
};
class B{
    public:
        int j;
        A a;//因为在类的定义中,所以这是声明,不是定义对象a,定义是要分配空间的。
}
void fun(){
    B b;
    cout << b.j <<" "<<b.a.i<<endl;
}
B gb;
int main(){
    cout << gb.j<<" "<<gb.a.i<<endl;
    fun();
    return 0;
}

输出的值是

0 0
不确定值 不确定值

如果是以下情况:

class A{
    public:
        int i;
    A(){i= 123}
};
class B{
    public:
        int j;
        A a;//因为在类的定义中,所以这是声明,不是定义对象a,定义是要分配空间的。
}
void fun(){
    B b;
    cout << b.j <<" "<<b.a.i<<endl;
}
B gb;
int main(){
    cout << gb.j<<" "<<gb.a.i<<endl;
    fun();
    return 0;
}

输出的值是

0 123(i在初始化时候为0,然后执行构造函数体内的赋值,更新为123了)
不确定值 123

(2)什么时候需要构造函数初始化列表

  有些数据成员必须放在构造函数初始化列表中显式地初始化。对于这样的成员,在构造函数体中对它们赋值是不起作用的。

  1. 没有默认构造函数的类类型数据成员;
  2. const类型的数据成员;(const static除外)
  3. 引用类型的数据成员。

(3)构造函数初始化顺序

  • 数据成员(除去static,因为static成员不属于对象)被初始化的次序就是定义成员的次序。而不是初始化列表中显示的顺序。
  • 对于派生类的对象而言,如果构造函数初始化列表中包含基类的构造函数,那么,首先是调用基类的构造函数初始化基类,然后再按上面步骤初始化自己,如果没在初始化列表中包含基类的构造函数,那么会先自动调用基类的默认构造函数,然后再上述步骤初始化自己。

(4)合成的默认构造函数

  • 只要定义一个对象时没有提供初始化,就使用默认构造函数。
  • 一个类哪怕只定义了一个构造函数,编译器也不会再生成合成的默认构造函数。
  • 只有当一个类没有定义构造函数的时候,编译器才会自动生成一个默认的构造函数,称为合成的默认构造函数

合成的默认构造函数的函数体里面默认为空,那么合成的默认构造函数怎么来初始化数据成员的呢,同样的,也是上面说的使用与变量初始化相同的规则来初始化成员,具有类类型的成员通过运行各自的默认构造函数来进行初始化。内置和复合类型的成员,如指针和数组,只对定义在全局作用域中的对象才初始化,当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化。

class A{
    public:
        int i;
};
class B{
    public:
        int j;
        A a;//因为在类的定义中,所以这是声明,不是定义对象a,定义是要分配空间的。
}
void fun(){
    B b;
    cout << b.j <<" "<<b.a.i<<endl;
}
B gb;
int main(){
    cout << gb.j<<" "<<gb.a.i<<endl;
    fun();
    return 0;
}

输出的值是

0 0
不确定值 不确定值

(5)类成员的显式初始化

对于没有定义构造函数并且全体数据成员均为public的类,可以采用与初始化数组元素相同的方式初始化其成员。

class A{
    public:
        int a;
        int b;
};
A obja = {1,2};

要求类的全体数据成员都是public,且没有自定义构造函数,因此struct结构体正好也适合,因为默认情况下struct里面都是public成员。

1.5 隐式类类型转换

先看一个例子:

class A{
    private:
        string name;
        int age;
    public:
        A(string s):name(s),age(0){}
        bool is_same(A a){//注意在类的定义中,非static数据成员里不能有自身类定义的对象,但这里可以,因为是方法,不是数据成员
            if(a.s == this.s)
                return true;
            else return false;
        }
};
int main(){
    A man("hello");
    string name = "world";
    man.is_same(name);
    return ;
}

注意类的bool is_same(A a);方法,该方法定义的时候形参是A类的对象,而在main函数调用的时候,实参类型确是string对象!运行还能通过,为什么?
因为隐式转换,因为A类中的构造函数是以string为形参的,因此,当我们把一个string对象传递给A类的bool is_same(A a);时,编译器会先使用接受一个string的A类构造函数,生成一个临时的A类对象,然后把这个临时的对象传给is_same函数的形参(这时使用的是复制构造函数)。
可以抑制这种隐式转换,方法是使用explicit修饰构造函数。
注意explicit关键字只能用于类内部标明即可。

class A{
    ...
    explicit A(string a):name(s),a(0)//ok
    {
    }
};
或者
class A{ 
    ...
    explicit A(string a);
};
A::A(string s):name(s),a(0)//ok
{
}
explicit A::A(string s):name(s),a(0){}//error

1.6 复制构造函数

(1)定义

也称拷贝构造函数,只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为复制构造函数。主要用于:

  • 根据另一个同类型的对象显式或隐式的初始化一个对象。
  • 复制一个对象,将它作为实参传给一个函数,适合值传递的情况。
  • 从函数返回时复制一个对象,也是适合值传递的情况。
  • 初始化顺序容器中的元素。
    如果想禁止复制,那就自定义复制构造函数定义为私有的,这样编译器也不会生成一个默认的。

(2)合成的复制构造函数

我们没有定义的话,编译器会自动合成一个复制构造函数,复制构造函数的函数体里默认是执行逐个成员初始化,将新对象初始化为原对象的副本。

(3)自定义复制构造函数

可以自定义复制构造函数,形式如定义那样

1.7 赋值构造函数

即重载了赋值操作符

类类型& operator=(const 类类型 &){
    成员的赋值;
    return *this;//this是指向该类类型的const指针,*this==该类类型的对象,函数的返回值是该类类型的引用,对象赋给引用OK
}

1.8 运算符重载

(1)重载为类成员的运算符函数定义形式

左操作数必须为类的对象情况下,操作符才能重载为该类的成员函数。

函数类型  operator 运算符(形参)
{
       ......
}
参数个数=原操作数个数-1   (后置++、--除外)
  • C++ 几乎可以重载全部的运算符,而且只能够重载C++中已经有的。
  • 不能重载的运算符:“.”、“.*”、“::”、“?:”
    • 重载之后运算符的优先级和结合性都不会改变。
  • 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。例如:
    • 使复数类的对象可以用“+”运算符实现加法;
    • 时钟类对象可以用“++”运算符实现时间增加1秒。
  • 重载为类的非静态成员函数;
  • 重载为非成员函数。

(2)双目运算符的重载:

class Complex {
public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
    //运算符+重载成员函数
  Complex operator + (const Complex &c2) const;
    //运算符-重载成员函数
  Complex operator - (const Complex &c2) const;
private:
    double real;    //复数实部
    double imag;    //复数虚部
};
Complex Complex::operator+(const Complex &c2) const{
  //创建一个临时无名对象作为返回值 
  return Complex(real+c2.real, imag+c2.imag); 
}

Complex Complex::operator-(const Complex &c2) const{
 //创建一个临时无名对象作为返回值
    return Complex(real-c2.real, imag-c2.imag); 
}

(3)单目运算符的重载

  • 前置单目运算符重载规则
    • 如果要重载++(以单目运算符++为例)为类成员函数,使之能够实现表达式 ++oprd,其中 oprd 为A类对象,则 ++ 应被重载为 A 类的成员函数,无形参。
    • 经重载后,表达式 ++ oprd 相当于 oprd.operator ++()
  • 后置单目运算符 ++和–重载规则

    • 如果要重载 ++或–为类成员函数,使之能够实现表达式 oprd++ 或 oprd– ,其中 oprd 为A类对象,则 ++或– 应被重载为 A 类的成员函数,且具有一个 int 类型形参。
    • 经重载后,表达式 oprd++ 相当于 oprd.operator ++(0)
  • 前置单目运算符,重载函数没有形参

  • 后置++运算符,重载函数需要有一个int形参,主要使编译器能够区分同一操作符是前置还是后置的。

好好看例子!例如时钟类:

#include <iostream>
using namespace std;
class Clock {//时钟类定义
public: 
    Clock(int hour = 0, int minute = 0, int second = 0);
    void showTime() const;
  //前置单目运算符重载
    Clock& operator ++ ();
  //后置单目运算符重载
    Clock operator ++ (int);    
private:
    int hour, minute, second;
};

Clock::Clock(int hour, int minute, int second) {    
    if (0 <= hour && hour < 24 && 0 <= minute && minute < 60
        && 0 <= second && second < 60) {
        this->hour = hour;
        this->minute = minute;
        this->second = second;
    } else
        cout << "Time error!" << endl;
}
void Clock::showTime() const {  //显示时间
    cout << hour << ":" << minute << ":" << second << endl;
}

例8-2重载前置++和后置++为时钟类成员函数
Clock & Clock::operator ++ () { 
    second++;
    if (second >= 60) {
        second -= 60;  minute++;
        if (minute >= 60) {
          minute -= 60; hour = (hour + 1) % 24;
        }
    }
    return *this;
}

Clock Clock::operator ++ (int) {
    //注意形参表中的整型参数
    Clock old = *this;
    ++(*this);  //调用前置“++”运算符
    return old;
}
例8-2重载前置++和后置++为时钟类成员函数
int main() {
    Clock myClock(23, 59, 59);
    cout << "First time output: ";
    myClock.showTime();
    cout << "Show myClock++:    ";
    (myClock++).showTime();
    cout << "Show ++myClock:    ";
    (++myClock).showTime();
    return 0;
}

(4)运算符重载为非成员函数

有些运算符不能重载为成员函数,例如二元运算符的左操作数不是对象,或者是不能由我们重载运算符的对象,例如库中定义的类对象,我们无法修改源码添加重载运算符。

运算符重载为非成员函数的规则

  • 函数的形参代表依自左至右次序排列的各操作数。
  • 重载为非成员函数时
  • 参数个数=原操作数个数(后置++、–除外)
  • 至少应该有一个自定义类型的参数。
  • 后置单目运算符 ++和–的重载函数,形参列表中要增加一个int,但不必写形参名。
  • 如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元。

  • 双目运算符 B重载后,

    • 表达式oprd1 B oprd
    • 等同于operator B(oprd1,oprd2 )
  • 前置单目运算符 B重载后,

    • 表达式 B oprd
    • 等同于operator B(oprd )
  • 后置单目运算符 ++和–重载后,

    • 表达式 oprd B
    • 等同于operator B(oprd,0 )

重点例子:重载Complex的加减法和“<<”运算符为非成员函数

• 将+、-(双目)重载为非成员函数,并将其声明为复数类的友元,两个操作数都是复数类的常引用。
• 将<<(双目)重载为非成员函数,并将其声明为复数类的友元,它的左操作数是std::ostream引用,右操作数为复数类的常引用,返回std::ostream引用,用以支持下面形式的输出:

cout << a << b;

该输出调用的是:

operator << (operator << (cout, a), b);

源代码:

#include <iostream>
using namespace std;

class Complex {
public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }  
    friend Complex operator+(const Complex &c1, const Complex &c2);
    friend Complex operator-(const Complex &c1, const Complex &c2);
    friend ostream & operator<<(ostream &out, const Complex &c);
private:    
    double real;  //复数实部
    double imag;  //复数虚部
};

Complex operator+(const Complex &c1, const Complex &c2){
    return Complex(c1.real+c2.real, c1.imag+c2.imag); 
}
Complex operator-(const Complex &c1, const Complex &c2){
    return Complex(c1.real-c2.real, c1.imag-c2.imag); 
}

ostream & operator<<(ostream &out, const Complex &c){
    out << "(" << c.real << ", " << c.imag << ")";
    return out;
}

int main() {    
    Complex c1(5, 4), c2(2, 10), c3;    
    cout << "c1 = " << c1 << endl;
    cout << "c2 = " << c2 << endl;
    c3 = c1 - c2;   //使用重载运算符完成复数减法
    cout << "c3 = c1 - c2 = " << c3 << endl;
    c3 = c1 + c2;   //使用重载运算符完成复数加法
    cout << "c3 = c1 + c2 = " << c3 << endl;
    return 0;
}

1.9 虚函数

(1)初识虚函数

  • 用virtual关键字说明的函数
  • 虚函数是实现运行时多态性基础
  • C++中的虚函数是动态绑定的函数
  • 虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态。
  • 一般成员函数可以是虚函数
  • 构造函数不能是虚函数
  • 析构函数可以是虚函数

(2)一般虚函数成员

  • 虚函数的声明

    virtual 函数类型 函数名(形参表);
    
  • 虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
  • 在派生类中可以对基类中的成员函数进行覆盖
  • 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的。

(3)virtual 关键字

  • 派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:
    • 该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型;
    • 该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值;
  • 如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。
  • 派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式。
  • 一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性。

(4)虚函数的原理:

  • 每个包含虚函数的类的对象,其内存地址的第一个位置,放着一个指针,指向虚函数表的指针。
  • 虚函数表中的内容是函数指针。
    • 如果该类就是基类,那么虚表里的函数指针指向的就是自己的虚函数。
    • 如果该类是派生类,且重新实现了虚函数,那么这个函数指针指向的是该派生类中的重新定义的函数,如果没有重新实现基类的虚函数,那么该指针指向的就是基类的虚函数。

这里写图片描述

1.10 抽象类

(1)纯虚函数

  • 纯虚函数是一个在 基类 中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:

    virtual 函数类型 函数名(参数表) = 0;
    
  • 带有纯虚函数的类称为抽象类

(2)抽象类

  • 带有纯虚函数的类称为抽象类:

    class 类名 {
        virtual 类型 函数名(参数表)=0; 
        //其他成员…… 
    };
    
  • 抽象类作用

    • 抽象类为抽象和设计的目的而声明
      将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。
    • 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。
    • 如果派生类中没有实现纯虚函数,那么这个派生类也是抽象类,只能作为新的基类使用,而且不能定义对象。
  • 【注意】

    • 抽象类只能作为基类来使用。
    • 不能定义抽象类的对象。

1.11 C++11中override 与 final

(1)override

  • 多态行为的基础:基类声明虚函数,继承类声明一个函数覆盖该虚函数
  • 覆盖要求: 函数签名(signatture)完全一致
    函数签名包括:函数名 参数列表 const

下列程序就仅仅因为疏忽漏写了const,导致多态行为没有如期进行,因为编译器把这种情况当成了重载(同一作用域里的同名,不同形参为重载),不认为是多态这种形式的覆盖,因此编译器不会报错,正常通过,但运行时候得到的结果却不是想要的!

#include<iostream>
using namespace std;
class Base{
    public:
    virtual void display(int a) const{
        cout << "base"<<endl;
    }
};

class Drive:public Base{
    public:
    void display(int a){
        cout << "drive "<<endl;
    }
};

int main(){
    Base b,*bp;
    b.display(1);//base
    Drive d;
    d.display(1);//drive
    bp = &d;
    bp->display(1);//base
    return 0;
}

显式函数覆盖override

  • C++11 引入显式函数覆盖,在编译期而非运行期捕获此类错误。 - 在虚函数显式重载中运用,编译器会检查基类是否存在一虚拟函数,与派生类中带有声明override的虚拟函数,有相同的函数签名(signature);若不存在,则会回报错误。
  • 【注意】
    • override保留字修饰的函数(放在函数末尾):表示这个函数是一个要去重新实现的虚函数,在编译的时候会强制检查这个函数是否重写基类中对应的虚函数,以防出现上面的错误。
    • override只是C++保留字,不是关键字,这意味着只有在正确的使用位置,oerride才启“关键字”的作用,其他地方可以作为标志符
    • g++ -std=c++11;
    • override保留字只能用在类里面,不能出现在类外!而且只能用在派生类中!

例如:

#include<iostream>
using namespace std;
class Base{
    public:
    virtual void display(int a) const{
        cout << "base"<<endl;
    }
};

class Drive:public Base{
    public:
    void display(int a) const override;
};
void Drive::display(int a) const{
    cout << "drive "<<endl;
}

int main(){
    Base b,*bp;
    b.display(1);
    Drive d;
    d.display(1);
    bp = &d;
    bp->display(1);
    return 0;
}
g++ std=c++11 -o a xxx.c
输出
base
drive
drive
若Base类中void display(int a) override;声明中没有const,那么编译的时候会包错,所没有实现虚函数

(2)final

C++11提供的final,用来避免类被继承,或是基类的函数被改写 例:

struct Base1 final { };
struct Derived1 : Base1 { }; // 编译错误:Base1为final,不允许被继承
或
struct Base2 { 
    virtual void f() final; 
};
struct Derived2 : Base2 {
    void f();//编译错误:Base2::f为final,不允许被覆盖
};

1.12 友元

  允许特定的非成员函数访问一个类的私有成员,同时任然阻止一般的访问。例如重载输入输出操作符<<和>>,一般重载成非成员函数,见1.8.4。(当然可以让类提供访问私有数据的公有接口,则就可以不必直接访问私有成员了)
  友元机制允许一个类对其非公有成员的访问劝授予指定的函数或类,友元的声明以friend关键字开始。它只能出现在类定义的内部。

2.继承与派生

2.1继承

派生类的构成

  • 继承基类的成员(除构造和析构函数不继承,其余的都继承,c++11可用using语句继承基类的构造函数)
  • 改造基类的成员(不同层次的同名覆盖原理,vs 函数重载:同一层次下的同名函数,不同参数)
  • 添加新成员

分单继承与多继承

class 派生类类名 :继承方式1 基类名1 , 继承方式2 基类名2 ...
{
    成员声明;
};

2.2继承方式

(1)公有继承

  • 继承的访问控制
    • 基类的public和protected成员:访问属性在派生类中保持不变;
    • 基类的private成员:不可直接访问。(是指通过 对象名.成员名 的形式来访问对象中的成员的方式)
  • 访问权限
    • 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
    • 通过派生类的对象:只能访问public成员。

(2) 私有继承

  • 继承的访问控制
    • 基类的public和protected成员:都以private身份出现在派生类中;
    • 基类的private成员:不可直接访问。
  • 访问权限
    • 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
    • 通过派生类的对象:不能直接访问从基类继承的任何成员。

(3)保护继承

  • 继承的访问控制
    • 基类的public和protected成员:都以protected身份出现在派生类中;
    • 基类的private成员:不可直接访问。
  • 访问权限
    • 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
    • 通过派生类的对象:不能直接访问从基类继承的任何成员。
  • protected 成员的特点与作用
    • 对建立其所在类对象的模块来说,它与 private 成员的性质相同。
    • 对于其派生类来说,它与 public 成员的性质相同。
    • 既实现了数据隐藏,又方便继承,实现代码重用。
    • 如果派生类有多个基类,也就是多继承时,可以用不同的方式继承每个基类。

2.3类型转换

  • 公有派生类对象可以被当作基类的对象使用,反之则不可。
    • 派生类的对象可以隐含转换为基类对象;
    • 派生类的对象可以初始化基类的引用;
    • 派生类的指针可以隐含转换为基类的指针。
  • 通过基类对象名、指针只能使用从基类继承的成员

注意:不要重新定义继承而来的非虚函数

#include <iostream>
using namespace std;
class Base1 { //基类Base1定义
    public:
    void display() const {
        cout << "Base1::display()" << endl;
    }
};
class Base2: public Base1 { //公有派生类Base2定义
public:
    void display() const {
        cout << "Base2::display()" << endl;
    }
};
class Derived: public Base2 { //公有派生类Derived定义
public:
    void display() const {
        cout << "Derived::display()" << endl;
    }
};

void fun(Base1 *ptr) {  //参数为指向基类对象的指针
    ptr->display();     //"对象指针->成员名"
}

int main() {    //主函数
    Base1 base1;    //声明Base1类对象
    Base2 base2;    //声明Base2类对象
    Derived derived;    //声明Derived类对象

    fun(&base1);    //用Base1对象的指针调用fun函数
    fun(&base2);    //用Base2对象的指针调用fun函数
    fun(&derived); //用Derived对象的指针调用fun函数

    return 0;
}

结果均为Base1::display();

2.4派生类的构造与析构

2.4.1派生类的构造函数


  • 默认情况
    • 基类的构造函数不被继承;
    • 派生类需要定义自己的构造函数。
  • C++11规定
    • 可用using语句继承基类构造函数。
    • 但是只能初始化从基类继承的成员。
      • 派生类新增成员可以通过类内初始值进行初始化。
    • 语法形式:
      • using B::B;

(1)单继承构造函数的定义

派生类名::派生类名(基类所需的形参,本类成员所需的形参):
基类名(参数表), 本类成员初始化列表
{
    //其他初始化;
};

(2)多继承构造函数的定义

派生类名::派生类名(参数表) : 
基类名1(基类1初始化参数表), 
基类名2(基类2初始化参数表), 
...
基类名n(基类n初始化参数表), 
本类成员初始化列表
{
        //其他初始化;
};

(3)派生类与基类的构造函数

  • 当基类有默认构造函数时(默认构造函数就是没有参数的构造函数)
    • 派生类构造函数可以不向基类构造函数传递参数。
    • 构造派生类的对象时,基类的默认构造函数将被调用。
  • 如需执行基类中带参数的构造函数
    • 派生类构造函数应为基类构造函数提供参数。

(4)多继承且有对象成员时派生的构造函数定义
(如果对象成员没有提供默认构造函数,则需要在初始化列表中显示提供)

派生类名::派生类名(形参表):
基类名1(参数), 基类名2(参数), ..., 基类名n(参数), 
本类成员(含对象成员)初始化列表
{
        //其他初始化
};

(5)构造函数的执行顺序
(执行顺序与构造函数中声明的顺序无关,只与他们在类的定义时候的顺序有关)

  1. 调用基类构造函数。
    • 顺序按照它们被继承时声明的顺序(从左向右)。
  2. 对初始化列表中的成员进行初始化。
    • 顺序按照它们在类中定义的顺序。
    • 对象成员初始化时自动调用其所属类的构造函数。由初始化列表提供参数。
      (有默认构造函数的对象成员可以不用显示的在初始化列表中写出来,没有默认构造函数的对象成员则必须显示的在初始化列表中写出来)
  3. 执行派生类的构造函数体中的内容。

2.4.2派生类的复制构造函数


复制构造函数的形式:

类名 类名(const 类名 &c);

(1)派生类未定义复制构造函数的情况

  • 编译器会在需要时生成一个隐含的复制构造函数;(合成复制构造函数)
  • 先调用基类的复制构造函数;
  • 再为派生类新增的成员执行复制。

(2)派生类定义了复制构造函数的情况

  • 一般都要为基类的复制构造函数传递参数。
  • 复制构造函数只能接受一个参数,既用来初始化派生类定义的成员,也将被传递给基类的复制构造函数。
  • 基类的复制构造函数形参类型是基类对象的引用,实参可以是派生类对象的引用
    例如: C::C(const C &c1): B(c1) {…}

2.4.3派生类的析构函数


  • 析构函数不被继承,派生类如果需要,要自行声明析构函数。
  • 声明方法与无继承关系时类的析构函数相同。(析构函数没有参数,因此也不能被重载)
  • 不需要显式地调用基类的析构函数,系统会自动隐式调用。
  • 先执行派生类析构函数的函数体,再调用基类的析构函数。

2.5派生类成员的标识和访问


(1)当派生类与基类中有相同成员时:

  • 若未特别限定,则通过派生类对象使用的是派生类中的同名成员。
  • 如要通过派生类对象访问基类中被隐藏的同名成员,应使用基类名和作用域操作符(::)来限定。

(2)二义性问题

  • 如果从不同基类继承了同名成员,但是在派生类中没有定义同名成员,“派生类对象名或引用名.成员名”、“派生类指针->成员名”访问成员存在二义性问题
  • 解决方式:用类名限定

(3)虚基类

  • 需要解决的问题
    • 当派生类从多个基类派生,而这些基类又共同基类,则在访问此共同基类中的成员时,将产生冗余,并有可能因冗余带来不一致性
  • 虚基类声明
    • 以virtual说明基类继承方式
    • 例:class B1:virtual public B
  • 作用
    • 主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题
    • 为最远的派生类提供唯一的基类成员,而不重复产生多次复制
  • 注意:
    • 在第一级继承时就要将共同基类设计为虚基类。

(4)虚基类及其派生类构造函数

  • 建立对象时所指定的类称为最远派生类。
  • 虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
  • 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数。
  • 在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值