C++ 类的全面理解

1. 类的访问属性:public,protect,private

C++中类的成员变量和函数都带有三种属性中的一种,假如没有特别声明,那么就默认是私有的(除了构造函数)。public表示是公开的,对象可以直接调用的变量或者函数;protect表示是保护性的,只有本类和子类函数能够访问(注意只是访问,本类对象和子类对象都不可以直接调用)而私有变量和函数是只有在本类中能够访问(有个例外就是友元函数)

class A
{
public:
    A(int b):m_public(b),m_protected(1), m_private(2){}
    int m_public;
protected:
    int m_protected;
private:
    int m_private;
};
int main( void )
{
    A a(10);
    cout<<a.m_public<<endl; //正确,可以直接调用
    cout<<a.m_protected<<endl; //错误,不可以直接调用
    cout<<a.m_private<<endl;//错误,不可能直接调用
}

而子类对父类的继承类型也有这三种属性,分别为公开继承,保护继承和私有继承。

class A
{
public:
    A(int b):m_public(b),m_protected(1), m_private(2){}
    int m_public;
protected:
    int m_protected;
private:
    int m_private;
};
class B: public A{} //公有继承
class C: protected A{} //保护继承
class D:private A{} //私有继承

父类成员的公开属性有三种,子类的继承属性也有同样的三种,那么一共就有九种搭配。我们只需要记住这两个里取严格的那一种。例如私有继承父类公开成员,那么在子类里父类的所有属性都变成子类私有的了。

  public protected private
public继承 public protected 不可用
protected继承  protected protected  不可用
private继承 private private 不可用

2. 类的四个默认函数:构造,拷贝构造,赋值,析构

  • 每当构建一个新的类,编译器都会给每个类生成以上四个默认的无参构造函数,并且这四个函数都是默认public的。

class A
{
public:
    double m_a;
};
int main( void )
{
    A a; //调用默认无参构造函数,此时m_a的值是不确定的,不能用

    //离开主函数前调用析构函数,释放a的内存
}

但是一旦自己定了带参数的构造函数,那么编译器就不会再生成默认的无参构造函数了,但是还是有默认的拷贝和赋值构造函数。因此假如只定义了有参数的构造函数,那么这个类就没有无参构造函数。

class A
{
public:
    A(int i):m_a(i){}
    int m_a;
};
int main( void )
{
    A a; //错误!没有无参构造函数
    A a1(5); // 调用了A中程序员定义的有参构造函数
    A a2(6); // 调用了A中程序员定义的有参构造函数
    A a3 = a1; //此处调用默认的拷贝构造函数
    a2 = a1; //此处调用默认的赋值函数
}

上面的程序中尤其需要注意的是 A a3 = a1;这一句,虽然有等号,但是仍然是拷贝构造函数。拷贝构造函数和赋值函数的区别在于等式左边的对象是否已经存在。a2 = a1;这一句执行的时候,a2已经存在,因此是赋值函数,而执行A a3 = a1;这一句的时候,a3还不存在,因此为拷贝构造函数。

默认的赋值和拷贝构造函数一般只是简单的拷贝类中成员的值,这一点当类中存在指针成员和静态成员变量的时候就非常危险。例如以下一种情况:

class A
{
public:
    A(int i, int* p):m_a(i), m_ptr(p){}int m_a;
    int *m_ptr;
};
int main( void )
{
    int m = 10, *p = &m;
    A a1(3, p);
    A a2 = a1; //a2 和 a1的m_ptr都指向了同一地址
    *p = 100;
    cout<<*(a2.m_ptr)<<endl; //输出为100
}

这也就是C++中由于指针带来的浅拷贝的问题,只赋值了地址,而没有新建对象。因此假如类中存在静态变量或者指针成员变量时一定要自己手动定义赋值、拷贝构造、析构函数。

子类会继承父类定义的构造函数吗?

  • 可以理解成不能。子类会继承父类所有的函数,包括构造函数,但是子类的构造函数会把父类的构造函数覆盖了,所以看起来就是没有继承。假如子类不定义任何构造函数,那么子类只会默认地调用父类的无参构造函数。当父类中只定义了有参构造函数,从而不存在无参构造函数的话,子类就无法创建对象。
class A
{
public:
    A(int b):m_public(b){}
    int m_public;
};
class B:public A
{
};

int main()
{
    B b; //出错,因为父类没有无参构造函数
}

因此在这种情况必须要显示定义子类的构造函数,并且在子类构造函数中显示调用父类的构造函数。

class A
{
public:
    A(int b):m_public(b){}
    int m_public;
};
class B:public A
{
public:
    B(int num):A(num){}
};

int main()
{
    B b1; //出错,由于父类没有无参构造函数,因此B也不存在无参构造
    B b2(5); //正确
}

构造函数的构造顺序:先构造基类,再构造子类中的成员,再构造子类。

有些时候,我们不希望一个类被过多地被实例化,比如有关全局的类、路由类等。这时候,我们就可以用这种方法为类设置构造函数并提供静态方法。

  • 假如把类的析构函数定义为私有,那么就无法在栈中生成对象,而必须要通过new来在堆中生成对象。
  • 另外在这里提及一点,对应的,如何让类只能在栈中生成,而不能new呢?就是将new 和delete重载为私有。

原因是C++是一个静态绑定的语言。在编译过程中,所有的非虚函数调用都必须分析完成。即使是虚函数,也需检查可访问性。因些,当在栈上生成对象时,对象会自动析构,也就说析构函数必须可以访问。而堆上生成对象,由于析构时机由程序员控制,所以不一定需要析构函数。

构造函数的初始化列表(就是构造函数冒号后面的东西,叫初始化列表,需要与{}中的函数内容区分开,初始化列表在分配内存的同时完成初始化,{}先分配内存再初始化)。有几种情况必须要使用初始化列表:常量成员、引用类型、没有默认构造函数的类类型。

class A
{
public:
    int m_a;
    A(int num):m_a(num){}
};

class B
{
public:
    const int m_const;
    int &m_ref;
    A a;

    B(int num, int b):m_ref(b), m_const(1), a(num) //初始化列表
    {
        cout<<"constructing B"<<endl;
    }
};

int main()
{
    int n = 5;
    B b(1, n);
}

还需要注意的一点是,初始化列表里的真正赋值的顺序其实是按照成员变量的声明顺序,而不是初始化列表中显示的顺序。例如这里是先初始化m_const,然后是m_ref,最后是a。

3. 成员函数的重载、隐藏与覆盖

成员函数的重载

  • 相同的范围(在同一个类中); 
  • 函数名字相同;
  • 参数不同 ,也可以仅仅是顺序不同;
  • virtual 关键字可有可无;
class A
{
public:
    int m_a;
    A(int num):m_a(num){}
    void show(int n){}                    // (1)
    virtual void show(int n){}            //  (2) 错误!!不是重载,重复定义了(1),因为virtual关键字不能重载函数
    void show(double d){}                 // (3)show函数的重载
    void show(int a, double b){}          // (4)show函数的重载
    void show(double b, int a){}          //  (5)show函数的重载
    void show(int a, double b) const {}   //  (6)show函数的重载,const关键可以作为重载的依据
    void show(const int a, double b){}    // (7)错误!!不是重载, 顶层const不可以作为重载的依据,重复定义了(6)
    void show(int *a){}                   //  (8)show函数的重载
    void show(const int *a){}             // (9)show函数的重载,只有底层const才可以作为重载的依据
    void show(int * const a){}            //  (10) 错误!!不是重载,重复定义了(8),因为这里也使用了顶层const

};

至于const能不能成为重载的依据取决于是顶层const还是底层const。顶层const是指对象本身是常量,而底层const是指指向或引用的对象才是常量。 底层const可以作为重载的依据,顶层const不可以

成员函数的隐藏,这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数。

  • 只要子类函数的名字与基类的相同,那么不管参数相同与否,都会直接屏蔽基类的同名函数。
class A
{
public:
    void show(int a) {cout<<"A::show()"<<endl;}//(1)
};
class B:public A
{
public:
    void show(){cout<<"B::show()"<<endl;} //(2)将(1)屏蔽了
};
  • 假如在子类中仍旧需要用到基类的同名函数,就要用using关键字显式声明。

class A
{
public:
    void show(int) {cout<<"A::show()"<<endl;}
};
class B:public A
{
public:
    using A::show;
    void show(){
        show(0); //一定要在前面显式声明using A中的show函数,否则此句会编译错误
        cout<<"B::show()"<<endl;
    }
};

int main()
{
    B b;
    b.show();
}

成员函数的覆盖

  • 不同的范围(分别位于派生类与基类);
  • 函数名字相同;
  • 参数相同 ;
  • 基类函数必须有virtual 关键字;

4. 友元函数、友元类

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。

class A
{
public:
    A(int n):m_a(n){}
    friend class B; //声明B为A的友元类
private:
    int m_a;
};

class B
{
public:
    B(A a){cout<<a.m_a<<endl;} //由于B是A的友元类,所以可以直接调用a的私有m_a成员
};

int main()
{
    A a(1);
    B b = B(a); //输出1
}

要注意尽管友元类很强大,但是友元类和类本身并没有任何继承关系和成员关系。友元类或友元函数都不是本类的成员类和成员函数。

就如名字定义的那样,只是朋友,不具有任何亲属关系,因此无法使用this指针进行调用

友元函数常用在重载运算符。因为通常重载运算符的时候都要用到私有变量,所以用友元函数来重载是非常合适的。

4. 运算符的重载

首先要明确,有6个运算符是不可以被重载的。

  • . (成员访问运算符);
  • .*, ->* (成员指针访问运算符);
  • :: (域运算符);
  • sizeof (长度运算符);
  • ?: (条件运算符);
  • =, [] ,() ,-> 四个符号只能通过成员函数来重载,不能通过友元函数来定义。因为当编译器发现当类中没有定义这4个运算符的重载成员函数时,就会自己加入默认的运算符重载成员函数。而如果这四个运算符写成友元函数时会报错,产生矛盾。

  • 不允许用户定义新的运算符作为重载运算符,不能修改原来运算符的优先级和结合性,不能改变操作对象等等限制。

重载原则如下:

  • 如果是一元操作,就用成员函数去实现;
  • 如果是二元操作,就尽量用友元函数去实现;
  • 如果是二元操作,但是对两个操作对象的处理不同,那么就尽可能用成员函数去实现;

运算符的重载:

class A
{
public:
    A(int n):m_a(n){}
    int m_a;
    friend A operator+(A const& a1, A const & a2);
};

A operator+(A const& a1, A const & a2)
{
    A res(0);
    res.m_a = 1 + a1.m_a + a2.m_a;
    return res;
}

int main()
{
    A a1(1), a2(2);
    A a3 = a1 + a2;
    cout<<a3.m_a; //输出4
}

5. 类中的const关键字

常量指针指向常对象, 常对象只能调用其常成员函数。因为非const成员函数默认是要改变常对象。

class A
{
public:
    A(int n):m_a(n){}
    int m_a;
    void show(){cout<<"A::show()"<<endl;}

};

int main()
{
    A a1(1);
    const A a2(2);
    a1.show(); //正确
    a2.show(); //错误
}

假如增加const函数后,就可以正常运行。

class A
{
public:
    A(int n):m_a(n){}
    int m_a;
    void show(){cout<<"A::show()"<<endl;}
    void show() const{cout<<"A::show() const"<<endl;}

};

int main()
{
    A a1(1);
    const A a2(2);
    a1.show(); //正确 输出A::show()
    a2.show(); //正确 输出A::show() const,自动调用const函数
}

A const* 和   const A* 等价,允许用A* 赋值 A const*,但是不允许用A const* 赋值A*。

class A
{
public:
    A(int n):m_a(n){}
    int m_a;
    void changeValue(int *p){cout<<"changeValue"<<endl;}
    void changeValue2(int const *p){cout<<"changeValue2"<<endl;}

};

int main()
{
    int n = 10;
    int const *p_n_const = &n;
    int *p_n = &n;
    A a(1);
    a.changeValue(p_n_const); //错误,无法把int const*类型,转成int *类型,但是反之可以
    a.changeValue2(p_n); //正确,输出changeValue2
}

这是因为形参const A表示指向的对象不能改变,所以如果传入A实参,只要不改变对象的值就不会有问题。

但是如果形参为A,则有可能改变A指向的对象,这是const A办不到的,所以编译器不允许传入const A作为实参传入。

posted @ 2024-03-21 16:45  小熊酱  阅读(280)  评论(0编辑  收藏  举报